PkgTemplates.jl/CONTRIBUTING.md
Chris de Graaf af744a21a2 Fix typo
2017-08-16 01:20:20 -05:00

6.3 KiB

Contributing to PkgTemplates

The best way to contribute to PkgTemplates is by adding new plugins.

Plugins are pretty simple. They're defined as subtypes to Plugin, in their own file inside src/plugins. Let's create one, called MyPlugin, in src/plugins/myplugin.jl:

@auto_hash_equals struct MyPlugin <: Plugin end

The @auto_hash_equals macro means we don't have to implement == or hash ourselves (ref).

All plugins need at least one attribute: gitignore_files. This is a Vector{AbstractString}, of which each entry will be inserted in the .gitignore of generated packages that use this plugin.

Maybe the service that MyPlugin is associated with creates a directory called secrets, containing top secret data. In that case, gitignore_files should contain that string:

@auto_hash_equals struct MyPlugin <: Plugin
    gitignore_files::Vector{AbstractString}

    function MyPlugin()
        new(["/secrets"])
    end
end

You can also add patterns like *.key, etc. to this array. Note that Windows Git also recognizes / as a path separator in .gitignore, so there's no need for joinpath.

Suppose that MyPlugin also has a configuration file at the root of the repo. We're going to put a default myplugin.yml in defaults, but we also want to let users supply their own, or choose to not use one at all:

@auto_hash_equals struct MyPlugin <: Plugin
    gitignore_files::Vector{AbstractString}
    config_file::Union{AbstractString, Void}

    function MyPlugin(; config_file::Union{AbstractString, Void}="")
        if config_file != nothing
            if isempty(config_file)
                config_file = joinpath(DEFAULTS_DIR, "myplugin.yml")
            end
            if !isfile(config_file)
                throw(ArgumentError("File $(abspath(config_file)) does not exist"))
            end
        end
        new(["/secrets"], config_file)
    end
end

Now to actually create this configuration file at package generation time, we need a gen_plugin method. This method looks like this:

function gen_plugin(plugin::MyPlugin, template::Template, pkg_name::AbstractString)
    if plugin.config_file == nothing
        return String[]
    end
    text = substitute(readstring(plugin.config_file), pkg_name, template)
    gen_file(joinpath(template.temp_dir, pkg_name, ".myplugin.yml"))
    return [".myplugin.yml"]
end

There are a few things to note here:

  • We use the substitute function on the config file's text.
  • We use the gen_file function to create the file.
    • It takes two arguments: the path to the file to be generated, and the text to be written.
  • We place our file in template.temp_dir.
    • template.temp_dir is where all file generation takes place, files are only moved to their final location at the end of package generation to avoid leftovers in the case of an error.
  • We return an array containing at most the name of our generated file.
    • This array should contain all root-level files or directories that were created. If we created myplugin/foo and myplugin/bar, we'd only need to return ["myplugin/"]. If nothing is created, then we return an empty array.

We've got the essentials now, but perhaps MyPlugin has a web interface that we want to access from the repo's homepage. We'll do this by adding a badge to the README:

function badges(_::MyPlugin, user::AbstractString, pkg_name::AbstractString)
    return [
        "[![MyPlugin](https://myplugin.com/badges/$user/$pkg_name.jl)](https://myplugin.com/$user/$pkg_name.jl)"
    ]
end

This method should return an array of Markdown-formatted strings that display badges and link to somewhere relevant. Note that a plugin can have any number of badges. The Markdown syntax is as follows:

[![Hover Text](https://badge-image.url)](https://link.url)

Badges for just about everything can be found at Shields.io.

We're not done yet though, we need to add the plugin type to the list of badge-enabled plugins. We want MyPlugin's badge to be displayed on the far right side, so we're going to add MyPlugin to the end of BADGE_ORDER in src/PkgTemplates.jl.

const BADGE_ORDER = [GitHubPages, TravisCI, AppVeyor, CodeCov, MyPlugin]

And we're done! We've just created a nifty new plugin.


Template Substitution

Since plugin configuration files are often specific to the package they belong to, we might want to replace some placeholder values in our plugin's config file. We can do this by following Mustache.jl's rules. Some replacements are defined by PkgTemplates:

  • {{PKGNAME}} is replaced by pkg_name.
  • {{VERSION}} is replaced by $major.$minor corresponding to template.julia_version.

Some conditional replacements are also defined:

  • {{DOCUMENTER}}Documenter{{/DOCUMENTER}}
    • "Documenter" only appears in the rendered text if the template contains a Documenter subtype.
  • {{CODECOV}}CodeCov{{/CODECOV}}
    • "CodeCov" only appears in the rendered text if the template contains the CodeCov plugin.
  • {{#AFTER}}After{{/AFTER}}
    • "After" only appears in the rendered text if something needs to happen after CI testing occurs. As of right now, this is true when either of the above two conditions are true.

We can also specify our own replacements by passing a dictionary to substitute:

view = Dict("KEY" => "VAL", "HEADS" => 2rand() > 1)
text = """
    {{KEY}}
    {{PKGNAME}}
    {{#HEADS}}Heads{{/HEADS}}
    """
substituted = substitute(text, "MyPkg", template; view=view)

This will return "VAL\nMyPkg\nHeads\n" if 2rand() > 1 was true, "VAL\nMyPkg\n\n" otherwise.

Note the double newline in the second outcome; Mustache has a bug with conditionals that inserts extra newlines (more detail here). We can get around this by writing ugly template files, like so:

{{KEY}}
{{PKGNAME}}{{#HEADS}}
Heads{{/HEADS}}

The resulting string will end with a single newline regardless of the value of view["HEADS"]

Also note that conditionals without a corresponding key in view won't error, but will simply be evaluated as false.