PkgTemplates.jl/CONTRIBUTING.md

194 lines
7.2 KiB
Markdown
Raw Normal View History

2017-08-16 05:34:37 +00:00
# Contributing to PkgTemplates
The best way to contribute to `PkgTemplates` is by adding new plugins.
2017-08-17 08:08:51 +00:00
There are two main types of plugins:
[`GenericPlugin`](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugins.html#GenericPlugin-1)s
and
[`CustomPlugin`](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugins.html#CustomPlugin-1)s.
2017-08-16 05:34:37 +00:00
2017-08-17 08:08:51 +00:00
## Writing a Generic Plugin
2017-08-16 05:34:37 +00:00
2017-08-17 08:08:51 +00:00
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`:
2017-08-16 05:34:37 +00:00
```julia
2017-08-17 08:08:51 +00:00
@auto_hash_equals struct MyPlugin <: GenericPlugin
gitignore::Vector{AbstractString}
src::Nullable{AbstractString}
dest::AbstractString
badges::Vector{AbstractString}
view::Dict{String, Any}
2017-08-16 05:34:37 +00:00
function MyPlugin(; config_file::Union{AbstractString, Void}="")
if config_file != nothing
if isempty(config_file)
config_file = joinpath(DEFAULTS_DIR, "myplugin.yml")
2017-08-17 08:08:51 +00:00
elseif !isfile(config_file)
2017-08-16 05:34:37 +00:00
throw(ArgumentError("File $(abspath(config_file)) does not exist"))
end
end
2017-08-17 08:08:51 +00:00
new([], config_file, ".myplugin.yml", [], Dict{String, Any}())
2017-08-16 05:34:37 +00:00
end
end
```
2017-08-17 08:08:51 +00:00
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 is `nothing`, no config file will be
generated. This came from the `config_file` keyword argument, which
defaulted to an empty string. That's because we've placed a default
config file at `defaults/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 about
`substitute`.
* `view` is a dictionary of additional replacements to `substitute`.
Plenty of services like
[`TravisCI`](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugins.html#TravisCI-1)
and
[`CodeCov`](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugins.html#CodeCov-1)
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:
2017-08-16 05:34:37 +00:00
```julia
2017-08-17 08:08:51 +00:00
@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)
2017-08-16 05:34:37 +00:00
end
end
2017-08-17 08:08:51 +00:00
function badges(plugin: Gamble, user::AbstractString, pkg_name::AbstractString)
if plugin.success
return ["[![You won!](https://i.imgur.com/poker-chip)](https://pokerstars.net)"]
else
return String[]
end
end
2017-08-16 05:34:37 +00:00
2017-08-17 08:08:51 +00:00
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
2017-08-16 05:34:37 +00:00
end
```
2017-08-17 08:08:51 +00:00
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:
2017-08-16 05:34:37 +00:00
2017-08-17 08:08:51 +00:00
#### `gen_plugin`
2017-08-16 05:34:37 +00:00
2017-08-17 08:08:51 +00:00
We read the text from the plugin's source file, and then we run it through the `substitute`
function (more on that [later](#template-substitution)).
2017-08-16 05:34:37 +00:00
2017-08-17 08:08:51 +00:00
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.
2017-08-16 05:34:37 +00:00
2017-08-17 08:08:51 +00:00
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](https://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.
2017-08-16 05:34:37 +00:00
2017-08-17 08:08:51 +00:00
That's all there is to it! We've just created a nifty custom plugin.
2017-08-16 05:34:37 +00:00
***
### 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](https://github.com/jverzani/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`](src/plugins/documenter.jl) subtype.
* `{{CODECOV}}CodeCov{{/CODECOV}}`
* "CodeCov" only appears in the rendered text if the template contains
the [`CodeCov`](src/plugins/codecov.jl) 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`:
```julia
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](https://github.com/jverzani/Mustache.jl/issues/47)). 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.