7.2 KiB
Contributing to PkgTemplates
The best way to contribute to PkgTemplates
is by adding new plugins.
There are two main types of plugins:
GenericPlugin
s
and
CustomPlugin
s.
Writing a Generic Plugin
As the name suggests, generic plugins are simpler than custom ones, and as
such are extremely easy to implement. They have the ability to add patterns
the the generated .gitignore
, as well as create a single configuration file.
We're going to define a new generic plugin MyPlugin
in
src/plugins/myplugin.jl
:
@auto_hash_equals struct MyPlugin <: GenericPlugin
gitignore::Vector{AbstractString}
src::Nullable{AbstractString}
dest::AbstractString
badges::Vector{AbstractString}
view::Dict{String, Any}
function MyPlugin(; config_file::Union{AbstractString, Void}="")
if config_file != nothing
if isempty(config_file)
config_file = joinpath(DEFAULTS_DIR, "myplugin.yml")
elseif !isfile(config_file)
throw(ArgumentError("File $(abspath(config_file)) does not exist"))
end
end
new([], config_file, ".myplugin.yml", [], Dict{String, Any}())
end
end
That's all there is to it! Let's take a better look at what we've done:
- The plugin has five attributes, these must be exactly as they are.
gitignore
is the array of patterns to add the the generated package's.gitignore
, we chose not to add any with this plugin.src
is the location of the config file we're going to copy into the generated package repository. If this isnothing
, no config file will be generated. This came from theconfig_file
keyword argument, which defaulted to an empty string. That's because we've placed a default config file atdefaults/myplugin.yml
.dest
is the path to our generated config file, relative to the root of the package repository. In this example, the file will go in.myplugin.yml
at the root of the repository.badges
is an array of Markdown-formatted badge strings to be displayed on the package's README. We chose not to include any here. TODO talk aboutsubstitute
.view
is a dictionary of additional replacements tosubstitute
.
Plenty of services like
TravisCI
and
CodeCov
follow this format, so generic plugins should be able to get you pretty far.
Writing a Custom Plugin
When a service doesn't follow the pattern demonstrated above, it's time to write a custom
plugin. These are still pretty simple, needing at most two additional methods. Let's create
a custom plugin called Gamble
in src/plugins/gamble.jl
that only generates a file if
you get lucky enough:
@auto_hash_equals struct Gamble <: CustomPlugin
gitignore:Vector{AbstractString}
src::AbstractString
success::Bool
function Gamble(config_file::AbstractString)
if !isfile(config_file)
throw(ArgumentError("File $(abspath(config_file)) does not exist"))
end
success = rand() > 0.8
println(success ? "Congratulations!" : "Maybe next time.")
new([], config_file, success)
end
end
function badges(plugin: Gamble, user::AbstractString, pkg_name::AbstractString)
if plugin.success
return ["[](https://pokerstars.net)"]
else
return String[]
end
end
function gen_plugin(plugin::Gamble, template::Template, pkg_name::AbstractString)
if plugin.success
text = substitute(readstring(plugin.src), template, pkg_name)
gen_file(joinpath(t.temp_dir, ".gambler.yml"), text)
return [".gambler.yml"]
else
return String[]
end
end
With that, we've got everything we need. Note that this plugin still has a gitignore
attribute; it's required for all plugins. Let's look at the extra methods we implemented:
gen_plugin
We read the text from the plugin's source file, and then we run it through the substitute
function (more on that later).
Next, we use gen_file
to write the text, with substitutions applied, to the destination
file in t.temp_dir
. Generating our repository in a temp directory means we're not stuck
with leftovers in the case of an error.
This function returns an array of all the root-level files or directories
that were created. If both foo/bar
and foo/baz
were created, we only need
to return ["foo/"]
.
badges
This function returns an array of Markdown-formatted badges to be displayed on the package README. You can find badges and Markdown strings for just about everything on Shields.io.
This will do the trick, but if we want our badge to appear at a specific
position in the README, we need to edit BADGE_ORDER
in
[src/PkgTemplates.jl
(https://github.com/invenia/PkgTemplates.jl/blob/master/src/PkgTemplates.jl).
Say we want our badge to appear before all others, we'll add Gamble
to the
beginning of the array.
That's all there is to it! We've just created a nifty custom 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 bypkg_name
.{{VERSION}}
is replaced by$major.$minor
corresponding totemplate.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.
- "Documenter" only appears in the rendered text if the template contains
a
{{CODECOV}}CodeCov{{/CODECOV}}
- "CodeCov" only appears in the rendered text if the template contains
the
CodeCov
plugin.
- "CodeCov" only appears in the rendered text if the template contains
the
{{#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.