Update contributing guide

This commit is contained in:
Chris de Graaf 2017-08-17 03:08:51 -05:00
parent a265956339
commit dfa9f123de
2 changed files with 110 additions and 90 deletions

View File

@ -2,126 +2,135 @@
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
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.
## 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`:
```julia
@auto_hash_equals struct MyPlugin <: Plugin end
```
The `@auto_hash_equals` macro means we don't have to implement `==` or `hash`
ourselves ([ref](https://github.com/andrewcooke/AutoHashEquals.jl)).
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:
```julia
@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:
```julia
@auto_hash_equals struct MyPlugin <: Plugin
gitignore_files::Vector{AbstractString}
config_file::Union{AbstractString, Void}
@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")
end
if !isfile(config_file)
elseif !isfile(config_file)
throw(ArgumentError("File $(abspath(config_file)) does not exist"))
end
end
new(["/secrets"], config_file)
new([], config_file, ".myplugin.yml", [], Dict{String, Any}())
end
end
```
Now to actually create this configuration file at package generation time,
we need a `gen_plugin` method. This method looks like this:
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:
```julia
function gen_plugin(plugin::MyPlugin, template::Template, pkg_name::AbstractString)
if plugin.config_file == nothing
@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 ["[![You won!](https://i.imgur.com/poker-chip)](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
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:
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:
* We use the `substitute` function on the config file's text.
* More on that [later](#template-substitution).
* 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.
#### `gen_plugin`
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:
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)).
```julia
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
```
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 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:
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/"]`.
```
[![Hover Text](https://badge-image.url)](https://link.url)
```
#### `badges`
Badges for just about everything can be found at
[Shields.io](https://shields.io/).
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).
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`.
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.
```julia
const BADGE_ORDER = [GitHubPages, TravisCI, AppVeyor, CodeCov, MyPlugin]
```
And we're done! We've just created a nifty new plugin.
That's all there is to it! We've just created a nifty custom plugin.
***

View File

@ -93,6 +93,9 @@ Returns an array of generated file/directory names.
"""
function gen_readme(pkg_name::AbstractString, template::Template)
text = "# $pkg_name\n"
remaining = copy(collect(keys(template.plugins)))
# Generate the ordered badges first, then add any remaining ones to the right.
for plugin_type in BADGE_ORDER
if haskey(template.plugins, plugin_type)
text *= "\n"
@ -100,8 +103,16 @@ function gen_readme(pkg_name::AbstractString, template::Template)
badges(template.plugins[plugin_type], template.user, pkg_name),
"\n",
)
deleteat!(remaining, findin(remaining, plugin_type))
end
end
for plugin_type in remaining
text *= "\n"
text *= join(
badges(template.plugins[plugin_type], template.user, pkg_name),
"\n",
)
end
gen_file(joinpath(template.temp_dir, pkg_name, "README.md"), text)
return ["README.md"]