veragem 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +230 -34
- data/Gemfile.lock +254 -0
- data/README.md +143 -48
- data/lib/veragem/citation.rb +116 -0
- data/lib/veragem/generator.rb +105 -56
- data/lib/veragem/license.rb +27 -0
- data/lib/veragem/name.rb +110 -48
- data/lib/veragem/options.rb +116 -30
- data/lib/veragem/project.rb +200 -39
- data/lib/veragem/questions.rb +163 -0
- data/lib/veragem/summary.rb +62 -0
- data/lib/veragem/version.rb +2 -1
- data/lib/veragem.rb +12 -3
- data/plugins.rb +4 -0
- data/sig/veragem.rbs +151 -23
- data/template/.gitattributes +3 -0
- data/template/.githooks/pre-commit +2 -3
- data/template/.github/PULL_REQUEST_TEMPLATE.md +2 -1
- data/template/.github/dependabot.yml +4 -8
- data/template/.github/image-registries.sh +4 -3
- data/template/.github/workflows/dependabot-automerge.yml +29 -34
- data/template/.github/workflows/dependency-review.yml +19 -15
- data/template/.github/workflows/image.yml +65 -14
- data/template/.github/workflows/links.yml +35 -0
- data/template/.github/workflows/nix.yml +3 -3
- data/template/.github/workflows/release.yml +135 -22
- data/template/.github/workflows/ruby-ci.yml +31 -9
- data/template/.github/workflows/security.yml +15 -9
- data/template/.github/workflows/style-check-cocogitto.yml +3 -4
- data/template/.github/workflows/style-check-dprint.yml +10 -10
- data/template/.gitignore +5 -0
- data/template/.mutineer.yml +28 -0
- data/template/.rubocop.yml +49 -10
- data/template/.simplecov.erb +10 -0
- data/template/.yard-lint.yml +386 -0
- data/template/.yardopts +2 -0
- data/template/AGENTS.md.erb +76 -38
- data/template/CITATION.cff.erb +50 -0
- data/template/CODE_OF_CONDUCT.md.erb +1 -1
- data/template/CONTRIBUTING.md.erb +107 -48
- data/template/Gemfile +10 -7
- data/template/README.md.erb +12 -12
- data/template/Rakefile +167 -31
- data/template/Steepfile.erb +1 -1
- data/template/bin/setup +2 -0
- data/template/cliff.toml +40 -23
- data/template/dprint.json +1 -6
- data/template/flake.lock +3 -3
- data/template/flake.nix +25 -10
- data/template/lib/%path%/cli.rb.erb +19 -15
- data/template/lib/%path%/version.rb.erb +1 -1
- data/template/lib/%path%.rb.erb +3 -1
- data/template/sig/%path%.rbs.erb +1 -1
- data/template/test/test_%underscored%.rb.erb +4 -3
- data/template/test/test_cli.rb.erb +37 -12
- data/template/test/test_helper.rb.erb +15 -7
- metadata +16 -4
- data/template/.github/workflows/rubocop-autocorrect.yml +0 -114
data/README.md
CHANGED
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
# veragem
|
|
2
2
|
|
|
3
|
-
A Bundler plugin that generates a Ruby gem with its toolchain wired up:
|
|
4
|
-
|
|
3
|
+
A Bundler plugin that generates a Ruby 4.0+ gem with its toolchain wired up: Steep types,
|
|
4
|
+
RuboCop, Minitest at 100% coverage with mutation testing, YARD, CI on Linux, macOS and Windows, a
|
|
5
|
+
Nix flake and releases by Trusted Publishing. It is the Ruby counterpart of the
|
|
6
|
+
[templates](https://github.com/veralvx/templates) repository's Python and Rust templates.
|
|
5
7
|
`bundle gem` gives a gemspec, a Rakefile, Minitest and RuboCop; `bundle veragem NAME` gives that
|
|
6
8
|
and the rest:
|
|
7
9
|
|
|
8
|
-
- **Ruby 4.0** or later, `required_ruby_version` and CI alike
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
- **Ruby 4.0** or later, `required_ruby_version` and CI alike, on Linux, macOS and Windows:
|
|
11
|
+
Gemfile.lock holds Windows' platform (`bin/setup`), `.gitattributes` keeps every text file LF,
|
|
12
|
+
and the executables' mode is in git's index, which a commit keeps where the file system has none.
|
|
13
|
+
- **Types**: Steep checks `lib/` against the signatures in `sig/`, its `strict` diagnostics on, and
|
|
14
|
+
that each call is typed (`rake steep:typed`: none on an untyped receiver); TypeProf infers
|
|
15
|
+
signatures to start them from, and its editor support is configured.
|
|
11
16
|
- **Lint and format**: RuboCop with every new cop, rubocop-minitest, rubocop-packaging,
|
|
12
17
|
rubocop-performance and rubocop-rake; the project is indexed first (rubydex), so the cops that see
|
|
13
18
|
across files do. In CI, offences are annotated on the pull request and a failed run's summary
|
|
14
|
-
holds the safe corrections as a patch
|
|
15
|
-
|
|
19
|
+
holds the safe corrections as a patch, to `git apply`: a Dependabot bump the new cops fail stays
|
|
20
|
+
unmerged until you apply it, no workflow pushing code the bumped gems wrote.
|
|
21
|
+
- **Documentation**: YARD, every public object of `lib/` documented, held by yard-lint at 100%
|
|
22
|
+
coverage (`rake yard` writes it to `doc/`).
|
|
16
23
|
- **Tests**: Minitest, a property test with [pbt](https://github.com/ohbarye/pbt), and SimpleCov's
|
|
17
|
-
`strict` profile: every line, branch and method of `lib/` covered
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
`strict` profile: every line, branch and method of `lib/` covered; a warning from `lib/` or the
|
|
25
|
+
tests fails them. [mutineer](https://github.com/davidteren/mutineer) mutates `lib/`: one mutant
|
|
26
|
+
no test catches, or an ignore that names none, fails it. `rake checks` mutates the lines changed
|
|
27
|
+
since `origin/main`, CI every line (`rake mutation`).
|
|
28
|
+
- **Links**: lychee checks the links of the files git tracks but the tests' (`rake links`), in
|
|
29
|
+
`rake checks` and monthly (`links.yml`).
|
|
30
|
+
- **Security**: bundler-audit over `Gemfile.lock`, daily and on every pull request; a pull request's
|
|
31
|
+
new gems reviewed for their licences and advisories, its new actions for their licences
|
|
32
|
+
(`dependency-review.yml`, not a required check, as in the Python and Rust templates); zizmor over
|
|
33
|
+
the workflows and Dependabot's configuration; Dependabot for the gems, the actions and the flake,
|
|
34
|
+
each update proposed once its release is a week old.
|
|
22
35
|
- **Commits and releases**: Conventional Commits (cocogitto, in CI and opt-in git hooks), a
|
|
23
36
|
changelog written by git-cliff at each release, `rake "bump[minor]"` then Bundler's own
|
|
24
|
-
`rake release`; the tag publishes to RubyGems by Trusted Publishing, no API key stored,
|
|
25
|
-
the
|
|
37
|
+
`rake release`; the tag publishes to RubyGems by Trusted Publishing, no API key stored, the gem
|
|
38
|
+
built apart from the tools' gems and signed (its Sigstore attestation, RubyGems' provenance), and
|
|
39
|
+
makes the GitHub release.
|
|
26
40
|
- **Nix**: a flake with the development shell (Ruby and every tool the checks run), the gem as a
|
|
27
41
|
package (`nix build`), `nix fmt`, and checks for the Nix, the shell scripts, the workflows and
|
|
28
42
|
the package. With `--exe`, `nix run` runs the command, and its OCI image (`nix build .#image`)
|
|
@@ -31,11 +45,11 @@ and the rest:
|
|
|
31
45
|
`AGENTS.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`), issue and pull request templates.
|
|
32
46
|
|
|
33
47
|
In the generated gem, `bundle exec rake` runs the Ruby checks, as CI does;
|
|
34
|
-
`bundle exec rake checks` runs
|
|
48
|
+
`bundle exec rake checks` runs CI's checks, the links too, but mutates the changed lines alone.
|
|
35
49
|
|
|
36
50
|
## Install
|
|
37
51
|
|
|
38
|
-
Ruby 4.0 or later, then:
|
|
52
|
+
Ruby 4.0 or later and git, then:
|
|
39
53
|
|
|
40
54
|
```sh
|
|
41
55
|
bundle plugin install veragem
|
|
@@ -43,69 +57,146 @@ bundle plugin install veragem
|
|
|
43
57
|
|
|
44
58
|
Run outside a project, it installs for your user, and `bundle veragem` then works anywhere. From
|
|
45
59
|
the repository instead: `bundle plugin install veragem --git https://github.com/veralvx/veragem`
|
|
46
|
-
(or `--path` a local checkout).
|
|
60
|
+
(or `--path` a local checkout). This README follows `main`, which can be ahead of the release:
|
|
61
|
+
[CHANGELOG.md](CHANGELOG.md) lists what each release holds, and `--git` installs `main`.
|
|
47
62
|
|
|
48
63
|
## Usage
|
|
49
64
|
|
|
50
65
|
```sh
|
|
51
|
-
bundle veragem my_gem #
|
|
66
|
+
bundle veragem my_gem # with a terminal for input, it asks for what no option gives
|
|
52
67
|
cd my_gem
|
|
53
68
|
bin/setup # the gems, and Gemfile.lock written
|
|
54
69
|
git add .
|
|
55
70
|
git commit -m "chore: initial commit from veragem"
|
|
56
|
-
bundle exec rake checks #
|
|
71
|
+
bundle exec rake checks # the tools the generated CONTRIBUTING.md lists, or `nix develop`'s
|
|
57
72
|
```
|
|
58
73
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
74
|
+
As `bundle gem` does, veragem makes the git repository and adds the files, and runs no
|
|
75
|
+
`bundle install`: `bin/setup` writes the lock, to commit. The generated `CONTRIBUTING.md` lists
|
|
76
|
+
the first steps: the repository's settings, RubyGems' pending trusted publisher.
|
|
77
|
+
|
|
78
|
+
### Options
|
|
79
|
+
|
|
80
|
+
| Option | Default | Goes into |
|
|
81
|
+
| -------------------------- | ------------------------------------------- | ------------------------------------------------------ |
|
|
82
|
+
| `NAME` | -- | the gem, its module, its files: as `bundle gem` |
|
|
83
|
+
| `--[no-]exe` | `--no-exe`: a library alone | a command line: `exe/NAME`, `lib/PATH/cli.rb` |
|
|
84
|
+
| `--license=LICENSE` | `dual` (MIT OR Apache-2.0) | `mit`, `apache` or `dual`: the gemspec, LICENSE |
|
|
85
|
+
| `--author=NAME` | `git config user.name` (required) | the gemspec, the licence's copyright line |
|
|
86
|
+
| `--[no-]email=EMAIL` | `git config user.email`; `--no-email`: none | the gemspec, the code of conduct |
|
|
87
|
+
| `--github=USER` | `git config github.user` (required) | the homepage, the repository's links |
|
|
88
|
+
| `--summary=TEXT` | `A Ruby gem.` | the gemspec, the README, the module's comment |
|
|
89
|
+
| `--[no-]citation` | `--no-citation` | `CITATION.cff`: GitHub's "Cite this repository" |
|
|
90
|
+
| `--family-names=NAMES` | (required with `--citation`) | with `--citation`: the author's, `CITATION.cff` |
|
|
91
|
+
| `--[no-]given-names=NAMES` | optional; `--no-given-names`: none | with `--citation`: the author's, `CITATION.cff` |
|
|
92
|
+
| `--[no-]alias=ALIAS` | optional; `--no-alias`: none | with `--citation`: the author's handle, `CITATION.cff` |
|
|
93
|
+
| `--orcid=ID` | optional | with `--citation`: the author's ORCID iD |
|
|
94
|
+
| `--keywords=LIST` | optional | with `--citation`: keywords, comma-separated |
|
|
95
|
+
|
|
96
|
+
A licence, an option's or an answer's, is taken in any case, abbreviated (`a`) or as its SPDX
|
|
97
|
+
expression (`Apache-2.0`, `MIT OR Apache-2.0` in either order). Without a terminal, when veragem
|
|
98
|
+
refuses a value from git's config, its error names the key and the option or options that replace
|
|
99
|
+
it.
|
|
100
|
+
|
|
101
|
+
### Questions
|
|
102
|
+
|
|
103
|
+
With a terminal for input, `bundle veragem` asks for each value no option gives, the name too, once
|
|
104
|
+
the options are checked. A question offers its default unless veragem refuses it, saying why and
|
|
105
|
+
where it is from (`git config user.name`); Enter takes it, an optional one takes `-` for none, and a
|
|
106
|
+
refused answer, or none where there is no default, is asked again, saying why. `-` is no option's
|
|
107
|
+
value, as no answer can give it: `--no-email`, `--no-given-names` and `--no-alias` give none, and an
|
|
108
|
+
`--orcid` or `--keywords` left out does. With other input (CI, a pipe, `< /dev/null` to skip the
|
|
109
|
+
questions) nothing is asked: the defaults apply, and a missing name, author, GitHub user or, with
|
|
110
|
+
`--citation`, family names is refused.
|
|
111
|
+
|
|
112
|
+
### CITATION.cff
|
|
113
|
+
|
|
114
|
+
`CITATION.cff` (Citation File Format 1.2.0) is for research software meant to be cited: GitHub
|
|
115
|
+
offers its "Cite this repository", and Zenodo, once the repository is enabled there (the generated
|
|
116
|
+
CONTRIBUTING.md's first steps), archives each release with its values and gives it a DOI. Its
|
|
117
|
+
author is a person, family names required, given names, alias, ORCID iD and keywords optional.
|
|
118
|
+
Asked at a terminal, the family names offer the author's last word, the given names the words before
|
|
119
|
+
the family names, where the author ends with them, and the alias, asked last, the GitHub user. That
|
|
120
|
+
user may be an organization, so it is only offered: without a terminal, there is no alias unless
|
|
121
|
+
`--alias` gives one. The citation's options need `--citation`: given without it, they are refused,
|
|
122
|
+
and at a terminal they make its question take y alone.
|
|
123
|
+
|
|
124
|
+
It holds what GitHub and Zenodo read, written as Zenodo's import takes it (narrower than CFF), but
|
|
125
|
+
the licences as CFF writes them:
|
|
126
|
+
|
|
127
|
+
- one licence as its SPDX identifier, and `dual`'s two as a list, CFF's OR: Zenodo's import refuses
|
|
128
|
+
a list, so it archives no release of a dual-licensed gem until
|
|
129
|
+
[zenodo/zenodo-rdm#1509](https://github.com/zenodo/zenodo-rdm/issues/1509) is fixed (first
|
|
130
|
+
reported as [zenodo/zenodo#2515](https://github.com/zenodo/zenodo/issues/2515));
|
|
131
|
+
- an ORCID iD Zenodo's check takes, its check digit and its block ORCID's, written as its URL;
|
|
132
|
+
- names without a comma: Zenodo reads one as the end of the family names (`Smith, Jr.`); an alias
|
|
133
|
+
may hold one, as Zenodo reads no alias;
|
|
134
|
+
- a name of 3 characters or more, Zenodo's least title;
|
|
135
|
+
- the repository, RubyGems' page as its artifact, and its API documentation on rubydoc.info as
|
|
136
|
+
its URL (there from the first release); no email (CFF's pattern is stricter than veragem's);
|
|
137
|
+
- its DOIs commented out, the concept DOI and a version's, to uncomment once Zenodo gives them
|
|
138
|
+
after a release.
|
|
139
|
+
|
|
140
|
+
`rake bump` sets its version and date at each release, and `nix flake check` validates it against
|
|
141
|
+
CFF's schema (cffconvert): a check of the schema alone, which passes files Zenodo's import refuses.
|
|
142
|
+
|
|
143
|
+
### The name
|
|
68
144
|
|
|
69
145
|
The name follows `bundle gem`'s rules, lower-case: `my_gem` is `MyGem` in `lib/my_gem.rb`, `my-gem`
|
|
70
146
|
is `My::Gem` in `lib/my/gem.rb`. Refused before anything is written, a name:
|
|
71
147
|
|
|
72
|
-
- whose module is already defined, by Ruby,
|
|
73
|
-
|
|
74
|
-
|
|
148
|
+
- whose module is already defined, by Ruby, by what its tools load with its code (Bundler, the
|
|
149
|
+
Rakefile and its tasks, the console, the tests: `c_s_v`, `j_s_o_n`, `weak_ref`), where installed,
|
|
150
|
+
or by its top-level library where this Ruby has one (`set`, `option_parser`, `date-holidays`: Date
|
|
151
|
+
is a class);
|
|
152
|
+
- whose file would shadow a library this Ruby has, in any case, as macOS and Windows ignore it
|
|
153
|
+
(`ripper`: lib/ripper.rb; `english`: English.rb);
|
|
75
154
|
- whose module its tests or `gem build` would take for Minitest's or RubyGems' own
|
|
76
155
|
(`lifecycle_hooks`, `date_like`);
|
|
77
156
|
- that `bundle gem` would refuse or mangle (`1gem`, `my--gem`);
|
|
78
|
-
-
|
|
157
|
+
- a gem the Gemfile's tools load, theirs included (`rubocop-rake`, `parser`): the tools' bundle
|
|
158
|
+
would resolve it to the new gem;
|
|
79
159
|
- `test_helper`, whose lib/test_helper.rb would shadow the tests', or one that would write a
|
|
80
160
|
template file twice (`helper`: test/test_helper.rb);
|
|
161
|
+
- one of Windows' device names, a word between its dashes (`con`, `my-nul`), no file's there;
|
|
81
162
|
- longer than 40 characters or deeper than 4 modules (the generated code's lines fit RuboCop's);
|
|
82
163
|
- with `--exe`, whose module is CLI or VERSION (`my-c_l_i`: its command line would find another
|
|
83
164
|
first).
|
|
84
165
|
|
|
85
|
-
The summary
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
166
|
+
### The summary
|
|
167
|
+
|
|
168
|
+
The summary is plain text, as RubyGems shows it: the README shows it as written, its Markdown
|
|
169
|
+
characters escaped (`_`, `` ` ``, `<`...), a URL's `://` and `www.` too, which GitHub would link
|
|
170
|
+
with its escapes shown, and its spaces single, as dprint writes them; GitHub's emoji (`:gem:`)
|
|
171
|
+
alone it cannot keep. It is the module's comment too, so one the gem's yard-lint or RuboCop would
|
|
172
|
+
not take as its documentation is refused: a tag first, YARD's (`@param`, `(see`) or written
|
|
173
|
+
informally (`Note:`, `See also:`), a RuboCop annotation (`HACK:`, `review the code`), a magic
|
|
174
|
+
comment (`encoding:`, `warn_indent:`), Markdown yard-lint refuses (an em or en dash outside code,
|
|
175
|
+
a `•` list marker or a code fence first), a backtick or `**` left open.
|
|
89
176
|
|
|
90
177
|
## What you get
|
|
91
178
|
|
|
92
179
|
```
|
|
93
|
-
NAME.gemspec the gem: its version from lib/
|
|
180
|
+
NAME.gemspec the gem: its version from lib/PATH/version.rb (`my-gem`'s lib/my/gem/), its
|
|
181
|
+
files lib/, sig/, exe/, the README, the changelog and the licences
|
|
94
182
|
Gemfile the gemspec, then the tools; Gemfile.lock pins them (written by bin/setup)
|
|
95
|
-
Rakefile `rake`: RuboCop, Steep, the tests; `rake checks`: what CI runs;
|
|
183
|
+
Rakefile `rake`: RuboCop, Steep, the tests, yard-lint; `rake checks`: what CI runs;
|
|
96
184
|
`rake "bump[level]"`, `rake release`; `rake -T` lists them all
|
|
97
185
|
Steepfile, sig/ the types; typeprof.conf.jsonc, TypeProf's editor support
|
|
98
|
-
.rubocop.yml RuboCop and its extensions
|
|
99
|
-
|
|
186
|
+
.rubocop.yml RuboCop and its extensions; .yard-lint.yml, the documentation's lint
|
|
187
|
+
.simplecov, .mutineer.yml the coverage gate; the mutation test's operators and its ignores
|
|
188
|
+
lib/, test/, exe/ starter code (exe/NAME with --exe): typed, documented, every line and
|
|
189
|
+
branch tested
|
|
100
190
|
bin/setup, bin/console
|
|
101
191
|
flake.nix, flake.lock the dev shell, the package (and the command's image), `nix fmt`, the checks
|
|
102
192
|
.githooks/ opt-in (`rake hooks`): the quick checks before a commit, Conventional Commits
|
|
103
193
|
cliff.toml, cog.toml git-cliff's changelog, cocogitto's commit checks
|
|
104
194
|
dprint.json, .editorconfig
|
|
105
195
|
.github/ CI, the flake, bundler-audit, the dependency review, the release (and the
|
|
106
|
-
image), the style checks,
|
|
107
|
-
|
|
196
|
+
image), the style checks, the links, Dependabot and its auto-merge; issue
|
|
197
|
+
and pull request templates
|
|
108
198
|
README, CONTRIBUTING, AGENTS, SECURITY, CODE_OF_CONDUCT, LICENSE-MIT / LICENSE-APACHE
|
|
199
|
+
CITATION.cff with --citation: the citation, its version and date `rake bump`'s
|
|
109
200
|
```
|
|
110
201
|
|
|
111
202
|
The Gemfile leaves the development tools unconstrained: the lock pins them at the versions current
|
|
@@ -120,11 +211,15 @@ packages few of them, and older).
|
|
|
120
211
|
property), the options, every file of each kind and licence rendered, parsed and kept executable,
|
|
121
212
|
the gemspec read back, the values written as given whatever the locale.
|
|
122
213
|
- The harness here is the template's: this repository's Gemfile, Rakefile, workflows, hooks, flake
|
|
123
|
-
and tools' configuration are the template's own files, byte for byte (`test/test_sync.rb
|
|
124
|
-
|
|
125
|
-
|
|
214
|
+
and tools' configuration are the template's own plain files, byte for byte (`test/test_sync.rb`;
|
|
215
|
+
but `dependabot.yml`, which moves both, `.mutineer.yml`, whose ignored mutants are the plugin's,
|
|
216
|
+
and the smoke test's workflow, the plugin's alone), so the template's are checked in place by
|
|
217
|
+
this repository's checks, and a command line's own (`image.yml`) by the smoke test's command
|
|
218
|
+
gem.
|
|
126
219
|
- The smoke test (`dev/smoke.sh`, `smoke.yml`, weekly too): the plugin installed from this checkout
|
|
127
|
-
generates a gem of each kind and licence, and each
|
|
220
|
+
generates a gem of each kind and licence, and each, its git hooks on, passes its own
|
|
221
|
+
`rake checks`, but the links (`links.yml`'s), and builds; the citation's gem is released three
|
|
222
|
+
times (`rake bump`, then `rake release` to a local remote): its first release, a patch, a minor.
|
|
128
223
|
|
|
129
224
|
## Contributing
|
|
130
225
|
|
|
@@ -137,8 +232,8 @@ packages few of them, and older).
|
|
|
137
232
|
Licensed under either of
|
|
138
233
|
|
|
139
234
|
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
|
|
140
|
-
<
|
|
141
|
-
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <
|
|
235
|
+
<https://www.apache.org/licenses/LICENSE-2.0>)
|
|
236
|
+
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <https://opensource.org/license/MIT>)
|
|
142
237
|
|
|
143
238
|
at your option.
|
|
144
239
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Veragem
|
|
4
|
+
# CITATION.cff's rules beyond its values' text, as Zenodo's import takes them: who may give them,
|
|
5
|
+
# the gem's name, the author's names (no comma), alias (which Zenodo reads not) and ORCID iD, the
|
|
6
|
+
# keywords; and what its questions offer.
|
|
7
|
+
module Citation
|
|
8
|
+
# The values CITATION.cff alone writes.
|
|
9
|
+
KEYS = %i[given_names family_names alias orcid keywords].freeze
|
|
10
|
+
# An ORCID iD, bare or as its URL, spaces around it (Unicode's too): its 16th character, the
|
|
11
|
+
# check digit. Matched ASCII-downcased: /i would widen its letters to Unicode's (ſ).
|
|
12
|
+
ORCID = %r{\A[[:space:]]*(?:https?://orcid\.org/)?(\d{4}-\d{4}-\d{4}-\d{3}[\dx])[[:space:]]*\z}
|
|
13
|
+
# The ORCID iDs ORCID issues, without their check digit: those Zenodo's import takes (idutils).
|
|
14
|
+
ORCID_BLOCKS = [15_000_000..35_000_000, 900_000_000_000..900_100_000_000].freeze
|
|
15
|
+
private_constant :ORCID, :ORCID_BLOCKS
|
|
16
|
+
|
|
17
|
+
# Whether a citation's values are given: --no-given-names' false is none.
|
|
18
|
+
# @param values [Hash{Symbol => String, Boolean, nil}]
|
|
19
|
+
# @return [Boolean]
|
|
20
|
+
def self.cited?(values) = values.values_at(*KEYS).any?
|
|
21
|
+
|
|
22
|
+
# A citation's values, given only with a citation.
|
|
23
|
+
# @param values [Hash{Symbol => String, Boolean, nil}] a citation's among them
|
|
24
|
+
# @param citation [Boolean] whether the gem has CITATION.cff
|
|
25
|
+
# @return [void]
|
|
26
|
+
# @raise [Veragem::Error] one is given without a citation
|
|
27
|
+
def self.check_values(values, citation)
|
|
28
|
+
return if citation || !cited?(values)
|
|
29
|
+
|
|
30
|
+
raise Error, "--given-names, --family-names, --alias, --orcid and --keywords are CITATION.cff's: " \
|
|
31
|
+
"they need a citation"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The value a citation's +key+ takes, as Project.check's: an ORCID iD its URL, keywords joined by
|
|
35
|
+
# ", ".
|
|
36
|
+
# @param key [Symbol] one of KEYS
|
|
37
|
+
# @param value [String] valid UTF-8, as Project.check passes it
|
|
38
|
+
# @yieldparam field [String] a value's name, as a refusal names it
|
|
39
|
+
# @yieldparam text [String] the value, or a keyword
|
|
40
|
+
# @yieldreturn [String] it checked, one line
|
|
41
|
+
# @return [String]
|
|
42
|
+
# @raise [Veragem::Error] +key+ cannot take it
|
|
43
|
+
def self.check(key, value)
|
|
44
|
+
field = key.name.tr("_", " ")
|
|
45
|
+
case key
|
|
46
|
+
when :orcid then orcid(value)
|
|
47
|
+
when :keywords then keywords(value) { yield("keyword", it) }.join(", ")
|
|
48
|
+
# Zenodo's import reads no alias: a comma divides nothing in it
|
|
49
|
+
when :alias then yield(field, value)
|
|
50
|
+
else names(field, yield(field, value))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The name, CITATION.cff's title: Zenodo's import refuses one under 3 characters.
|
|
55
|
+
# @param name [String] the gem's
|
|
56
|
+
# @param citation [Boolean] whether the gem has CITATION.cff
|
|
57
|
+
# @return [void]
|
|
58
|
+
# @raise [Veragem::Error] the citation cannot have the name
|
|
59
|
+
def self.check_name(name, citation)
|
|
60
|
+
return unless citation && name.length < 3
|
|
61
|
+
|
|
62
|
+
raise Error, "invalid gem name #{name} for a citation: Zenodo takes a title of 3 characters or more"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# What a citation's question offers. The citation, y where its values are given, which need one.
|
|
66
|
+
# The names, guessed from the author's words, Unicode's spaces between them: the last the family
|
|
67
|
+
# names, those before the family names the given names -- none where the author does not end
|
|
68
|
+
# with them. The alias, the GitHub user: only offered, as it may name an organization.
|
|
69
|
+
# @param key [Symbol] the question's
|
|
70
|
+
# @param values [Hash{Symbol => String, Boolean, nil}] the answers so far, checked
|
|
71
|
+
# @return [String, nil] nil for none, and for any other question
|
|
72
|
+
def self.default(key, values)
|
|
73
|
+
author = values[:author].to_s
|
|
74
|
+
case key
|
|
75
|
+
when :citation then cited?(values) ? "y" : "n"
|
|
76
|
+
when :family_names then author.split(/[[:space:]]+/).last
|
|
77
|
+
when :given_names then author[/\A(.+?)[[:space:]]+#{Regexp.escape(values[:family_names].to_s)}\z/, 1]
|
|
78
|
+
when :alias then values[:github].to_s
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# An author's names, as one line: no comma, which Zenodo reads as the family names' end
|
|
83
|
+
# ("family, given"), so "Smith, Jr." would lose its given names.
|
|
84
|
+
private_class_method def self.names(field, names)
|
|
85
|
+
return names unless names.include?(",")
|
|
86
|
+
|
|
87
|
+
raise Error, "invalid #{field} #{names.inspect}: no comma, Zenodo's divide of family and given names"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Keywords, comma-separated, as CFF's list takes them: blanks and repeats dropped, each checked
|
|
91
|
+
# by the block.
|
|
92
|
+
private_class_method def self.keywords(value, &)
|
|
93
|
+
words = value.split(",").grep_v(/\A[[:space:]]*\z/).map(&).uniq
|
|
94
|
+
return words unless words.empty?
|
|
95
|
+
|
|
96
|
+
raise Error, "invalid keywords #{value.inspect}: one or more, comma-separated"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# An ORCID iD as CFF writes it, its URL, checked as Zenodo's import checks it (idutils' is_orcid):
|
|
100
|
+
# its check digit ISO 7064 MOD 11-2's, its number in a block ORCID issues. Zenodo refuses the
|
|
101
|
+
# whole citation for a bad one.
|
|
102
|
+
private_class_method def self.orcid(value)
|
|
103
|
+
id = value.downcase(:ascii)[ORCID, 1]&.upcase
|
|
104
|
+
return "https://orcid.org/#{id}" if id && valid?(id.delete("-"))
|
|
105
|
+
|
|
106
|
+
raise Error, "invalid ORCID iD #{value.inspect}: 0000-0002-1825-0097, or its https://orcid.org/ URL"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private_class_method def self.valid?(digits)
|
|
110
|
+
*number, last = digits.chars
|
|
111
|
+
sum = number.reduce(0) { |total, digit| (total + digit.to_i) * 2 }
|
|
112
|
+
check = (12 - (sum % 11)) % 11
|
|
113
|
+
last == (check == 10 ? "X" : check.to_s) && ORCID_BLOCKS.any? { it.cover?(number.join.to_i) }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/veragem/generator.rb
CHANGED
|
@@ -4,14 +4,19 @@ require "erb"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
|
|
6
6
|
module Veragem
|
|
7
|
-
# Writes a Project's gem from template/: each `.erb` file rendered, every other copied,
|
|
8
|
-
#
|
|
7
|
+
# Writes a Project's gem from template/: each `.erb` file rendered, every other copied, the
|
|
8
|
+
# executables executable; then a git repository, its files added, as `bundle gem` leaves one.
|
|
9
9
|
module Generator
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
# template/: a generated gem's files, ERB and plain. (`|| raise`: __dir__ is nil only in code
|
|
11
|
+
# evaluated from a string, never in a required file.)
|
|
12
|
+
TEMPLATE = File.expand_path("../../template", __dir__ || raise)
|
|
13
|
+
# The files a command line alone needs: its code, its test, its OCI image's workflow and its script.
|
|
13
14
|
EXE = %w[exe/%name%.erb lib/%path%/cli.rb.erb test/test_cli.rb.erb .github/workflows/image.yml
|
|
14
15
|
.github/image-registries.sh].freeze
|
|
16
|
+
# The files a generated gem runs as commands: executable there, and so in git's index, which a
|
|
17
|
+
# commit keeps, where the file system has no mode (Windows: core.fileMode false).
|
|
18
|
+
EXECUTABLES = %w[.githooks/commit-msg .githooks/pre-commit .githooks/pre-push .github/image-registries.sh
|
|
19
|
+
.github/release-notes.sh bin/console.erb bin/setup exe/%name%.erb].freeze
|
|
15
20
|
# git's variables that point it at another repository (`git rev-parse --local-env-vars`, but the
|
|
16
21
|
# configuration given with `git -c`), which a git hook sets: unset, the repository is the gem's.
|
|
17
22
|
GIT_LOCAL = %w[GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_CONFIG GIT_OBJECT_DIRECTORY GIT_DIR GIT_WORK_TREE
|
|
@@ -19,66 +24,96 @@ module Veragem
|
|
|
19
24
|
GIT_PREFIX GIT_SHALLOW_FILE GIT_COMMON_DIR].to_h { [it, nil] }.freeze
|
|
20
25
|
# Each LICENSE-* file's SPDX identifier.
|
|
21
26
|
LICENSE_FILES = { "LICENSE-MIT.erb" => "MIT", "LICENSE-APACHE.erb" => "Apache-2.0" }.freeze
|
|
27
|
+
# The citation's file, a citation's alone (--citation).
|
|
28
|
+
CITATION = "CITATION.cff.erb"
|
|
29
|
+
# The tools' resolution: this repository's Gemfile is the template's (test_sync.rb), so its lock
|
|
30
|
+
# names every gem a generated gem's tools load, theirs included.
|
|
31
|
+
TOOLS_LOCK = File.expand_path("../../Gemfile.lock", __dir__ || raise)
|
|
32
|
+
private_constant :GIT_LOCAL, :LICENSE_FILES, :CITATION, :TOOLS_LOCK
|
|
22
33
|
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
files
|
|
34
|
+
# Into +target+, a new directory: a failure, an interrupt included, leaves none behind.
|
|
35
|
+
# @param project [Veragem::Project] the gem
|
|
36
|
+
# @param target [String] its directory, not yet there
|
|
37
|
+
# @return [void]
|
|
38
|
+
# @raise [Veragem::Error] +target+ exists, its name cannot be generated, or a write or git fails
|
|
39
|
+
def self.generate(project, target)
|
|
40
|
+
check(project.name, exe: project.exe, citation: project.citation, target:)
|
|
41
|
+
written = files(project)
|
|
42
|
+
Dir.mkdir(target)
|
|
43
|
+
removed_unless_done(target) do
|
|
44
|
+
written.each { |file, path| write(file, File.join(target, path), project) }
|
|
45
|
+
stage(target, written.filter_map { |file, path| path if EXECUTABLES.include?(file) })
|
|
46
|
+
end
|
|
47
|
+
rescue SystemCallError => e
|
|
48
|
+
raise Error, e.message
|
|
31
49
|
end
|
|
32
50
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
# Every rule a gem's name meets before anything is written, its kinds' and its directory's: the
|
|
52
|
+
# questions' and the generation's one check.
|
|
53
|
+
# @param name [String] as typed
|
|
54
|
+
# @param exe [Boolean] whether the gem has a command line
|
|
55
|
+
# @param citation [Boolean] whether the gem has CITATION.cff
|
|
56
|
+
# @param target [String] its directory, not yet there
|
|
57
|
+
# @return [String] +name+
|
|
58
|
+
# @raise [Veragem::Error] the gem cannot have the name, its modules could not be checked, or
|
|
59
|
+
# +target+ exists
|
|
60
|
+
def self.check(name, exe:, citation:, target:)
|
|
61
|
+
Name.check(name)
|
|
62
|
+
check_target(target)
|
|
63
|
+
check_name(name, exe)
|
|
64
|
+
Citation.check_name(name, citation)
|
|
65
|
+
Name.check_modules(name)
|
|
66
|
+
name
|
|
40
67
|
end
|
|
41
68
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
69
|
+
# What a name and its kind decide. A gem the Gemfile's tools load (its lock's), the bundle would
|
|
70
|
+
# resolve to the new gem itself and break them. A command line names the gem's module from
|
|
71
|
+
# within it, <Gem>::CLI, whose constant lookup finds CLI and VERSION first: neither can be its
|
|
72
|
+
# module. Two files at one path: `helper`'s test would be test/test_helper.rb.
|
|
73
|
+
# @param name [String] one Name.check takes
|
|
74
|
+
# @param exe [Boolean] whether the gem has a command line
|
|
75
|
+
# @return [void]
|
|
76
|
+
# @raise [Veragem::Error] the kind cannot have the name
|
|
77
|
+
def self.check_name(name, exe)
|
|
78
|
+
raise Error, "invalid gem name #{name}: a gem the Gemfile's tools load" if tools.include?(name)
|
|
79
|
+
|
|
80
|
+
innermost = Name.modules(name).last
|
|
81
|
+
if exe && %w[CLI VERSION].include?(innermost)
|
|
82
|
+
raise Error, "invalid gem name #{name}: its command line would find another #{innermost} before its module"
|
|
83
|
+
end
|
|
47
84
|
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
clash = templates(exe).map { path_of(it, name) }.tally.find { |_, count| count > 1 }
|
|
86
|
+
raise Error, "invalid gem name #{name}: it would write #{clash[0]} twice" if clash
|
|
50
87
|
end
|
|
51
88
|
|
|
52
|
-
#
|
|
53
|
-
def self.
|
|
89
|
+
# +target+ is free: neither a file nor a link, dangling ones too
|
|
90
|
+
private_class_method def self.check_target(target)
|
|
91
|
+
raise Error, "#{target} already exists" if File.exist?(target) || File.symlink?(target)
|
|
92
|
+
end
|
|
54
93
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
project.exe
|
|
94
|
+
private_class_method def self.files(project)
|
|
95
|
+
skipped = LICENSE_FILES.reject { |_, spdx| project.licenses.include?(spdx) }.keys
|
|
96
|
+
skipped << CITATION unless project.citation
|
|
97
|
+
(templates(project.exe) - skipped).to_h { [it, path_of(it, project.name)] }
|
|
59
98
|
end
|
|
60
99
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
100
|
+
# The template's files a kind takes (+exe+, a command line's too), each licence's included.
|
|
101
|
+
private_class_method def self.templates(exe)
|
|
102
|
+
files = Dir.glob("**/*", File::FNM_DOTMATCH, base: TEMPLATE)
|
|
103
|
+
files.reject { File.directory?(File.join(TEMPLATE, it)) || (!exe && EXE.include?(it)) }
|
|
64
104
|
end
|
|
65
105
|
|
|
66
|
-
#
|
|
67
|
-
def self.
|
|
68
|
-
|
|
106
|
+
# The lock's gems, its GEM section's specs (a dependency indented deeper), its lines LF's or CRLF's
|
|
107
|
+
private_class_method def self.tools(lock = File.read(TOOLS_LOCK))
|
|
108
|
+
lock[/^GEM\r?\n.*?^\r?$/m].to_s.scan(/^ {4}(\S+) \(/).flatten
|
|
109
|
+
end
|
|
69
110
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
files.each { |file, path| write(File.join(TEMPLATE, file), File.join(target, path), project) }
|
|
74
|
-
git(target, "init", "--quiet", "--initial-branch=main")
|
|
75
|
-
git(target, "add", "--all")
|
|
76
|
-
end
|
|
77
|
-
rescue SystemCallError => e
|
|
78
|
-
raise Error, e.message
|
|
111
|
+
private_class_method def self.path_of(file, name)
|
|
112
|
+
file.delete_suffix(".erb").gsub("%name%", name).gsub("%path%", Name.path(name))
|
|
113
|
+
.gsub("%underscored%", Name.underscored(name))
|
|
79
114
|
end
|
|
80
115
|
|
|
81
|
-
def self.removed_unless_done(target)
|
|
116
|
+
private_class_method def self.removed_unless_done(target)
|
|
82
117
|
done = false
|
|
83
118
|
begin
|
|
84
119
|
yield
|
|
@@ -88,22 +123,36 @@ module Veragem
|
|
|
88
123
|
end
|
|
89
124
|
end
|
|
90
125
|
|
|
91
|
-
def self.write(
|
|
126
|
+
private_class_method def self.write(file, destination, project)
|
|
127
|
+
source = File.join(TEMPLATE, file)
|
|
92
128
|
FileUtils.mkdir_p(File.dirname(destination))
|
|
93
|
-
File.binwrite(destination,
|
|
94
|
-
return unless
|
|
129
|
+
File.binwrite(destination, file.end_with?(".erb") ? render(source, project) : File.binread(source))
|
|
130
|
+
return unless EXECUTABLES.include?(file)
|
|
95
131
|
|
|
96
|
-
# executable where readable, as
|
|
132
|
+
# executable where readable: the umask decides, as it does the read bits
|
|
97
133
|
mode = File.stat(destination).mode
|
|
98
134
|
File.chmod(mode | ((mode & 0o444) >> 2), destination)
|
|
99
135
|
end
|
|
100
136
|
|
|
101
|
-
|
|
137
|
+
# A repository of its own, every file added, the executables' mode in the index too
|
|
138
|
+
private_class_method def self.stage(target, executables)
|
|
139
|
+
git(target, "init", "--quiet", "--initial-branch=main")
|
|
140
|
+
# --force: the user's own excludes (core.excludesFile) keep none of the gem's files out
|
|
141
|
+
git(target, "add", "--all", "--force")
|
|
142
|
+
git(target, "update-index", "--chmod=+x", "--", *executables)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The ERB's values are +project+'s: its methods, called bare.
|
|
146
|
+
private_class_method def self.render(source, project)
|
|
102
147
|
ERB.new(File.read(source, encoding: Encoding::UTF_8), trim_mode: "-").result(project.instance_eval { binding })
|
|
103
148
|
end
|
|
104
149
|
|
|
105
|
-
|
|
106
|
-
|
|
150
|
+
# system's nil: git could not run; false: it failed
|
|
151
|
+
private_class_method def self.git(dir, *args)
|
|
152
|
+
case system(GIT_LOCAL, "git", *args, chdir: dir)
|
|
153
|
+
when nil then raise Error, "cannot run git in #{dir}"
|
|
154
|
+
when false then raise Error, "git #{args.join(" ")} failed in #{dir}"
|
|
155
|
+
end
|
|
107
156
|
end
|
|
108
157
|
end
|
|
109
158
|
end
|