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/lib/veragem/project.rb
CHANGED
|
@@ -1,77 +1,238 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module Veragem
|
|
4
6
|
# The gem to generate: the values given, checked, and what follows from them.
|
|
5
7
|
class Project
|
|
6
|
-
# GitHub's rule for a user or organization name
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# GitHub's rule for a user or organization name: ASCII's letters, which /i would widen to Unicode's
|
|
9
|
+
# (ſ, the Kelvin sign).
|
|
10
|
+
GITHUB = /\A[a-zA-Z\d](?:[a-zA-Z\d]|-(?=[a-zA-Z\d])){0,38}\z/
|
|
11
|
+
# Each value a refusal names otherwise than its key.
|
|
12
|
+
FIELDS = { github: "GitHub user", license: "license (#{License::CHOICES})", orcid: "ORCID iD" }.freeze
|
|
13
|
+
# What Markdown would read as markup in a text: a character to escape, but an `_` within a word;
|
|
14
|
+
# and a URL's `://` or `www.`, which GFM would link, its escapes shown.
|
|
15
|
+
MARKUP = %r{[\\`*\[\]<~$] | (?<![[:alnum:]])_ | _(?![[:alnum:]]) | &(?=\#?[[:alnum:]]+;) | :(?=//) | (?<=www)\.}x
|
|
16
|
+
# The values that may be none, and how an option gives none: a question takes "-".
|
|
17
|
+
OPTIONAL = { email: "--no-email", given_names: "--no-given-names", alias: "--no-alias",
|
|
18
|
+
orcid: "leave --orcid out", keywords: "leave --keywords out" }.freeze
|
|
19
|
+
private_constant :GITHUB, :FIELDS, :MARKUP
|
|
20
|
+
|
|
21
|
+
# The gem's name.
|
|
22
|
+
# @return [String]
|
|
23
|
+
attr_reader :name
|
|
24
|
+
# Its author.
|
|
25
|
+
# @return [String]
|
|
26
|
+
attr_reader :author
|
|
27
|
+
# The author's email, none nil.
|
|
28
|
+
# @return [String, nil]
|
|
29
|
+
attr_reader :email
|
|
30
|
+
# The GitHub user or organization.
|
|
31
|
+
# @return [String]
|
|
32
|
+
attr_reader :github
|
|
33
|
+
# Its summary.
|
|
34
|
+
# @return [String]
|
|
35
|
+
attr_reader :summary
|
|
36
|
+
# Its licence's choice: mit, apache or dual.
|
|
37
|
+
# @return [String]
|
|
38
|
+
attr_reader :license
|
|
39
|
+
# Whether it has a command line.
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
attr_reader :exe
|
|
42
|
+
# Whether it has CITATION.cff.
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
attr_reader :citation
|
|
45
|
+
# CITATION.cff's given names, none nil.
|
|
46
|
+
# @return [String, nil]
|
|
47
|
+
attr_reader :given_names
|
|
48
|
+
# CITATION.cff's family names, nil without a citation.
|
|
49
|
+
# @return [String, nil]
|
|
50
|
+
attr_reader :family_names
|
|
51
|
+
# CITATION.cff's alias, none nil.
|
|
52
|
+
# @return [String, nil]
|
|
53
|
+
attr_reader :alias_name
|
|
54
|
+
# CITATION.cff's ORCID iD, its URL; none nil.
|
|
55
|
+
# @return [String, nil]
|
|
56
|
+
attr_reader :orcid
|
|
57
|
+
# CITATION.cff's keywords, none nil.
|
|
58
|
+
# @return [Array<String>, nil]
|
|
59
|
+
attr_reader :keywords
|
|
60
|
+
|
|
61
|
+
# The value +key+ names, as a refusal names it: `GitHub user` :github, `given names` :given_names.
|
|
62
|
+
# @param key [Symbol] a value's
|
|
63
|
+
# @return [String]
|
|
64
|
+
def self.field(key) = FIELDS.fetch(key) { key.name.tr("_", " ") }
|
|
65
|
+
|
|
66
|
+
# +text+ without the spaces around it, Unicode's too (String#strip's are ASCII's alone).
|
|
67
|
+
# @param text [String]
|
|
68
|
+
# @return [String]
|
|
69
|
+
def self.strip(text) = text.gsub(/\A[[:space:]]+|[[:space:]]+\z/, "")
|
|
10
70
|
|
|
11
|
-
|
|
71
|
+
# +value+ as a Ruby string literal, as written whatever the locale (String#inspect escapes
|
|
72
|
+
# what the locale cannot show): a quote, a backslash and an interpolation escaped.
|
|
73
|
+
# @param value [String] a value of the gem's, checked
|
|
74
|
+
# @return [String]
|
|
75
|
+
def self.quote(value) = %("#{value.gsub(/["\\]|#(?=[{$@])/) { "\\#{it}" }}")
|
|
76
|
+
|
|
77
|
+
# The value +key+ takes, an option's or an answer's: checked, or an Error saying why not.
|
|
78
|
+
# @param key [Symbol] :author, :email, :github, :summary, :license, :given_names, :family_names,
|
|
79
|
+
# :alias, :orcid or :keywords
|
|
80
|
+
# @param value [String, nil] as given; nil, none, is refused as "" is
|
|
81
|
+
# @return [String] the value kept: a licence its choice (`MIT` mit, `Apache-2.0` apache), an ORCID
|
|
82
|
+
# iD its URL, keywords joined by ", "
|
|
83
|
+
# @raise [Veragem::Error] +key+ cannot take it
|
|
84
|
+
def self.check(key, value)
|
|
85
|
+
value = utf8(key, value)
|
|
86
|
+
return Citation.check(key, value) { |name, text| line(name, text) } if Citation::KEYS.include?(key)
|
|
87
|
+
|
|
88
|
+
case key
|
|
89
|
+
when :github then github(strip(value))
|
|
90
|
+
when :license then License.check(strip(value))
|
|
91
|
+
when :summary then Summary.check(text(field(key), value))
|
|
92
|
+
else text(field(key), value)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
12
95
|
|
|
13
|
-
# +name+, and +values+: the author, email, github, summary and
|
|
14
|
-
#
|
|
96
|
+
# +name+, and +values+: the author, email, github, summary, license and a citation's given and
|
|
97
|
+
# family names, alias, ORCID iD and keywords (strings, but an email, given names or alias false,
|
|
98
|
+
# --no-email's, --no-given-names', --no-alias', or nil for none), exe and citation (true for a
|
|
99
|
+
# command line, for CITATION.cff), as Options reads them.
|
|
100
|
+
# @param name [String] the gem's name, as given
|
|
101
|
+
# @param values [Hash{Symbol => String, Boolean, nil}] the rest, as given
|
|
102
|
+
# @return [void]
|
|
103
|
+
# @raise [Veragem::Error] a value is refused
|
|
15
104
|
def initialize(name, values)
|
|
16
105
|
@name = Name.check(name)
|
|
17
|
-
@author =
|
|
18
|
-
@email =
|
|
19
|
-
@github =
|
|
20
|
-
@summary =
|
|
21
|
-
@license =
|
|
22
|
-
|
|
106
|
+
@author = Project.check(:author, values[:author])
|
|
107
|
+
@email = optional(:email, values[:email])
|
|
108
|
+
@github = Project.check(:github, values[:github])
|
|
109
|
+
@summary = Project.check(:summary, values[:summary])
|
|
110
|
+
@license = Project.check(:license, values[:license])
|
|
111
|
+
extras(values)
|
|
23
112
|
end
|
|
24
113
|
|
|
25
|
-
# The file the gem requires: `my-gem`'s
|
|
26
|
-
|
|
114
|
+
# The file the gem requires, lib/'s: `my-gem`'s my/gem.
|
|
115
|
+
# @return [String]
|
|
116
|
+
def path = Name.path(name)
|
|
27
117
|
|
|
28
|
-
|
|
118
|
+
# Its name, its words joined by `_` alone: its test's file name.
|
|
119
|
+
# @return [String]
|
|
120
|
+
def underscored = Name.underscored(name)
|
|
29
121
|
|
|
30
122
|
# Its module, as `bundle gem` names it.
|
|
123
|
+
# @return [String]
|
|
31
124
|
def constant = modules.join("::")
|
|
32
125
|
|
|
126
|
+
# Its modules, outermost first.
|
|
127
|
+
# @return [Array<String>]
|
|
33
128
|
def modules = Name.modules(name)
|
|
34
129
|
|
|
35
|
-
|
|
130
|
+
# The SPDX identifiers of its licence.
|
|
131
|
+
# @return [Array<String>]
|
|
132
|
+
def licenses = License::SPDX.fetch(license)
|
|
36
133
|
|
|
134
|
+
# The copyright's year: this one.
|
|
135
|
+
# @return [Integer]
|
|
37
136
|
def year = Time.now.year
|
|
38
137
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
|
|
138
|
+
# Today in UTC, as CFF writes a date: CITATION.cff's date-released until `rake bump` sets a
|
|
139
|
+
# release's, UTC's too.
|
|
140
|
+
# @return [String]
|
|
141
|
+
def today = Time.now.utc.strftime("%F")
|
|
142
|
+
|
|
143
|
+
# +value+ as a YAML double-quoted string: JSON's, which YAML reads alike.
|
|
144
|
+
# @param value [String] a value of the gem's, checked
|
|
145
|
+
# @return [String]
|
|
146
|
+
def yaml(value) = JSON.generate(value)
|
|
147
|
+
|
|
148
|
+
# +value+ as Markdown's text, shown as written, as RubyGems shows a summary: its markup escaped,
|
|
149
|
+
# one space between words, as dprint writes them.
|
|
150
|
+
# @param value [String] a value of the gem's, checked
|
|
151
|
+
# @return [String]
|
|
152
|
+
def markdown(value)
|
|
153
|
+
escaped = value.squeeze(" ").gsub(MARKUP) { "\\#{it}" }
|
|
154
|
+
escaped.sub(/\A(?=[#>+-])|\A\d+\K(?=[.)])/) { "\\" }
|
|
155
|
+
end
|
|
42
156
|
|
|
43
|
-
#
|
|
157
|
+
# Project.quote, for the templates.
|
|
158
|
+
# @param value [String] a value of the gem's, checked
|
|
159
|
+
# @return [String]
|
|
160
|
+
def quote(value) = Project.quote(value)
|
|
161
|
+
|
|
162
|
+
# +body+ inside the gem's modules, each nested and indented, +doc+ a comment on the innermost and
|
|
163
|
+
# the outer ones each a namespace's: one file documents them, as YARD would have it.
|
|
164
|
+
# @param body [String] Ruby code
|
|
165
|
+
# @param doc [String, nil] the comment
|
|
166
|
+
# @return [String]
|
|
44
167
|
def nest(body, doc: nil)
|
|
45
168
|
*outer, inner = modules
|
|
46
169
|
code = "#{"# #{doc}\n" if doc}module #{inner}\n#{indent(body)}end\n"
|
|
47
|
-
outer.reverse.reduce(code)
|
|
170
|
+
outer.reverse.reduce(code) do |nested, mod|
|
|
171
|
+
"#{"# The namespace of #{constant}.\n" if doc}module #{mod}\n#{indent(nested)}end\n"
|
|
172
|
+
end
|
|
48
173
|
end
|
|
49
174
|
|
|
50
|
-
|
|
175
|
+
# +value+'s bytes as UTF-8, whatever their encoding (an argument's are binary where the locale's
|
|
176
|
+
# encoding cannot read them), refused unless valid: every check after it takes valid UTF-8.
|
|
177
|
+
# None (nil) is "", which each check refuses.
|
|
178
|
+
private_class_method def self.utf8(key, value)
|
|
179
|
+
value = value.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
180
|
+
raise Error, "invalid #{field(key)} #{value.inspect}: not UTF-8" unless value.valid_encoding?
|
|
51
181
|
|
|
52
|
-
|
|
182
|
+
value
|
|
183
|
+
end
|
|
53
184
|
|
|
54
|
-
def
|
|
55
|
-
|
|
185
|
+
private_class_method def self.github(value)
|
|
186
|
+
return value if GITHUB.match?(value)
|
|
56
187
|
|
|
57
|
-
value
|
|
188
|
+
raise Error, "invalid #{field(:github)} #{value.inspect}: 1 to 39 ASCII letters, digits and inner single hyphens"
|
|
58
189
|
end
|
|
59
190
|
|
|
60
|
-
# A value
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
value = value.dup.force_encoding(Encoding::UTF_8)
|
|
65
|
-
one_line = value.valid_encoding? && !value.match?(/[[:cntrl:]\u2028\u2029]/)
|
|
66
|
-
line = one_line ? value.gsub(/\A[[:space:]]+|[[:space:]]+\z/, "") : ""
|
|
67
|
-
raise Error, "invalid #{field} #{value.inspect}: one line of text" if line.empty?
|
|
191
|
+
# A value the gemspec quotes: short enough for its line (RuboCop's 120 columns), and not one
|
|
192
|
+
# RubyGems refuses as unfinished.
|
|
193
|
+
private_class_method def self.text(field, value)
|
|
194
|
+
line = line(field, value)
|
|
68
195
|
raise Error, "invalid #{field}: too long, its Ruby string at most 100 characters" if quote(line).length > 100
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
196
|
+
return line unless line.match?(/\A(TODO|FIXME)/)
|
|
197
|
+
|
|
198
|
+
raise Error, "invalid #{field} #{line.inspect}: RubyGems refuses one starting TODO or FIXME"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# One line, its surrounding spaces stripped (Unicode's too). U+FFFE and U+FFFF are outside YAML's
|
|
202
|
+
# printable characters: CITATION.cff's.
|
|
203
|
+
private_class_method def self.line(field, value)
|
|
204
|
+
line = value.match?(/[[:cntrl:]\u2028\u2029\uFFFE\uFFFF]/) ? "" : strip(value)
|
|
205
|
+
raise Error, "invalid #{field} #{value.inspect}: one line of text" if line.empty?
|
|
73
206
|
|
|
74
207
|
line
|
|
75
208
|
end
|
|
209
|
+
|
|
210
|
+
private
|
|
211
|
+
|
|
212
|
+
# What the gem holds beyond the library: a command line, a citation.
|
|
213
|
+
def extras(values)
|
|
214
|
+
@exe = values[:exe] == true
|
|
215
|
+
@citation = values[:citation] == true
|
|
216
|
+
Citation.check_values(values, citation)
|
|
217
|
+
cite(values) if citation
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# CITATION.cff's author is a person: the family names it needs, the rest it may have.
|
|
221
|
+
def cite(values)
|
|
222
|
+
family = values[:family_names]
|
|
223
|
+
raise Error, "no family names: --family-names, CITATION.cff's author" unless family
|
|
224
|
+
|
|
225
|
+
@given_names = optional(:given_names, values[:given_names])
|
|
226
|
+
@family_names = Project.check(:family_names, family)
|
|
227
|
+
@alias_name = optional(:alias, values[:alias])
|
|
228
|
+
@orcid = optional(:orcid, values[:orcid])
|
|
229
|
+
@keywords = optional(:keywords, values[:keywords])&.split(", ")
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# A value OPTIONAL names: none, false or nil, is nil.
|
|
233
|
+
def optional(key, value) = (Project.check(key, value) if value)
|
|
234
|
+
|
|
235
|
+
# a blank line left blank, no trailing spaces
|
|
236
|
+
def indent(code) = code.gsub(/^(?=.)/, " ")
|
|
76
237
|
end
|
|
77
238
|
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Veragem
|
|
4
|
+
# A terminal's questions for what no option gives: each offers its default, which an empty answer
|
|
5
|
+
# takes, and is asked again until the answer passes the check the project's value would.
|
|
6
|
+
class Questions
|
|
7
|
+
# Each value's question, in the order asked; the name's comes first.
|
|
8
|
+
QUESTIONS = {
|
|
9
|
+
exe: "A command line, exe/%<name>s (y/n)",
|
|
10
|
+
citation: "A citation file, CITATION.cff (y/n)",
|
|
11
|
+
license: "License (#{License::CHOICES})",
|
|
12
|
+
summary: "Summary",
|
|
13
|
+
author: "Author",
|
|
14
|
+
family_names: "Family names, CITATION.cff's",
|
|
15
|
+
given_names: "Given names, CITATION.cff's",
|
|
16
|
+
orcid: "ORCID iD, CITATION.cff's",
|
|
17
|
+
keywords: "Keywords, comma-separated, CITATION.cff's",
|
|
18
|
+
email: "Email",
|
|
19
|
+
github: "GitHub user",
|
|
20
|
+
alias: "Alias, CITATION.cff's"
|
|
21
|
+
}.freeze
|
|
22
|
+
private_constant :QUESTIONS
|
|
23
|
+
|
|
24
|
+
# Whether +text+ is "-", an optional question's none, its spaces around it stripped as a value's
|
|
25
|
+
# are (Project.strip).
|
|
26
|
+
# @param text [String] an answer or an option's value, its bytes as given
|
|
27
|
+
# @return [Boolean]
|
|
28
|
+
def self.none?(text)
|
|
29
|
+
text = text.dup.force_encoding(Encoding::UTF_8)
|
|
30
|
+
text.valid_encoding? && Project.strip(text) == "-"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Questions on +output+, answered on +input+, for a gem generated into +dir+.
|
|
34
|
+
# @param input [#gets, #tty?] where the answers are typed
|
|
35
|
+
# @param output [#print, #puts] where the questions are shown
|
|
36
|
+
# @param dir [String] where the gem is to be generated
|
|
37
|
+
# @param git [Hash{Symbol => String}] each value's git config key, which a refusal of the default
|
|
38
|
+
# git gives names
|
|
39
|
+
# @return [void]
|
|
40
|
+
def initialize(input, output, dir, git: {})
|
|
41
|
+
@input = input
|
|
42
|
+
@output = output
|
|
43
|
+
@dir = dir
|
|
44
|
+
@git = git
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The name, +given+ or else asked for: checked as far as it, +exe+ and +citation+ decide, before
|
|
48
|
+
# the rest.
|
|
49
|
+
# @param given [String, nil] the name the arguments give
|
|
50
|
+
# @param exe [Boolean] whether --exe is given
|
|
51
|
+
# @param citation [Boolean] whether --citation, or a citation's value, is given
|
|
52
|
+
# @return [String]
|
|
53
|
+
# @raise [Veragem::Error] +given+ is refused, nothing is answered, or the terminal cannot be asked
|
|
54
|
+
def name(given, exe:, citation:)
|
|
55
|
+
return check_name(given, exe, citation) if given
|
|
56
|
+
|
|
57
|
+
ask("Gem name", check: -> { check_name(it, exe, citation) }) #: String
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# +given+ (the options) and the answers to the rest, +defaults+ offered; a citation's values asked
|
|
61
|
+
# only for a citation, the author's words offered for its names, the GitHub user for its alias.
|
|
62
|
+
# @param name [String] the gem's, checked
|
|
63
|
+
# @param given [Hash{Symbol => String, Boolean}] the options' values
|
|
64
|
+
# @param defaults [Hash{Symbol => String}] git's and Options' own
|
|
65
|
+
# @return [Hash{Symbol => String, Boolean, nil}] a value for each key asked, nil for none
|
|
66
|
+
# @raise [Veragem::Error] nothing is answered, or the terminal cannot be asked
|
|
67
|
+
def values(name, given, defaults)
|
|
68
|
+
values = given.dup #: Hash[Symbol, String | bool | nil]
|
|
69
|
+
QUESTIONS.except(*given.keys).each do |key, question|
|
|
70
|
+
next if Citation::KEYS.include?(key) && values[:citation] != true
|
|
71
|
+
|
|
72
|
+
check = -> { answer(key, it, name, values) }
|
|
73
|
+
offer = default(key, defaults, values)
|
|
74
|
+
values[key] = ask(format(question, name:), offer,
|
|
75
|
+
from: source(key), check:, optional: Project::OPTIONAL.key?(key))
|
|
76
|
+
end
|
|
77
|
+
values #: Project::values
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def check_name(name, exe, citation) = Generator.check(name, exe:, citation:, target: File.join(@dir, name))
|
|
83
|
+
|
|
84
|
+
def answer(key, text, name, values)
|
|
85
|
+
case key
|
|
86
|
+
when :exe then yes?(text).tap { Generator.check_name(name, it) }
|
|
87
|
+
when :citation
|
|
88
|
+
yes?(text).tap do |cited|
|
|
89
|
+
Citation.check_values(values, cited)
|
|
90
|
+
Citation.check_name(name, cited)
|
|
91
|
+
end
|
|
92
|
+
else Project.check(key, text)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The answer, or the default, through +check+; asked again, saying why, when there is neither, or
|
|
97
|
+
# the check fails, but an +optional+ one is nil -- and "-" its none, which a default would take
|
|
98
|
+
# otherwise.
|
|
99
|
+
def ask(question, default = nil, check:, from: "the default", optional: false)
|
|
100
|
+
default = offered(question, default, from, check)
|
|
101
|
+
loop do
|
|
102
|
+
reply = read(question, default, optional) || default
|
|
103
|
+
return if optional && (reply.nil? || Questions.none?(reply))
|
|
104
|
+
|
|
105
|
+
value = reply.nil? ? unanswered(question) : checked(question, reply, check)
|
|
106
|
+
return value unless value.nil?
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# +default+ if it passes +check+: one refused, said why and where it is from, is not offered.
|
|
111
|
+
def offered(question, default, from, check)
|
|
112
|
+
default unless default.nil? || checked(question, default, check, " (#{from}, not offered)").nil?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A line's text, nil when empty: its bytes as UTF-8 (whatever the locale's encoding), or as bytes
|
|
116
|
+
# where they are no UTF-8, as an argument's; stripped after, as Project strips a value.
|
|
117
|
+
def read(question, default, optional)
|
|
118
|
+
line = terminal(question) do
|
|
119
|
+
@output.print "#{question}#{" (- for none)" if optional}#{" [#{default}]" if default}: "
|
|
120
|
+
@input.gets.tap { @output.puts unless it }
|
|
121
|
+
end
|
|
122
|
+
raise Error, "no answer to #{question}" unless line
|
|
123
|
+
|
|
124
|
+
text = line.dup.force_encoding(Encoding::UTF_8)
|
|
125
|
+
answer = Project.strip(text.valid_encoding? ? text : text.b)
|
|
126
|
+
answer unless answer.empty?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# neither an answer nor a default: said so, and nil, as a refusal is
|
|
130
|
+
def unanswered(question)
|
|
131
|
+
terminal(question) { @output.puts "an answer is required" }
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def checked(question, answer, check, note = "")
|
|
136
|
+
check.call(answer)
|
|
137
|
+
rescue Error => e
|
|
138
|
+
terminal(question) { @output.puts "#{e}#{note}" }
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The block's reading or writing of the terminal: one that fails cannot ask +question+, unlike a
|
|
143
|
+
# check's own failure.
|
|
144
|
+
def terminal(question)
|
|
145
|
+
yield
|
|
146
|
+
rescue SystemCallError, IOError, EncodingError => e
|
|
147
|
+
raise Error, "cannot ask #{question}: #{e}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def yes?(answer)
|
|
151
|
+
case answer.downcase
|
|
152
|
+
in "y" | "yes" then true
|
|
153
|
+
in "n" | "no" then false
|
|
154
|
+
else raise Error, "y or n"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def default(key, defaults, values) = Citation.default(key, values) || { exe: "n", **defaults }[key]
|
|
159
|
+
|
|
160
|
+
# where a default is from, as a refusal of it says
|
|
161
|
+
def source(key) = @git[key] ? "git config #{@git[key]}" : "the default"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Veragem
|
|
4
|
+
# The gem's summary, which is its module's comment too: one the gem's own yard-lint and RuboCop take
|
|
5
|
+
# as its documentation.
|
|
6
|
+
module Summary
|
|
7
|
+
# What YARD reads as a tag first in a comment: its own (`@param`, `(see`), and those yard-lint
|
|
8
|
+
# (.yard-lint.yml) takes for one written informally.
|
|
9
|
+
TAG = /\A(?:@|\(see\b|(?:note|important|todo|fixme|see(?:\ also)?|warning|deprecated|author|version|since
|
|
10
|
+
|returns|raises|example):)/xi
|
|
11
|
+
# A RuboCop annotation, no documentation, as its AnnotationComment reads one: a keyword first, then
|
|
12
|
+
# a colon, spaces before it or not, or a space -- but a sentence's capitalized first word, a word
|
|
13
|
+
# after it (a summary, stripped, has one after a space).
|
|
14
|
+
ANNOTATION = /\A(todo|fixme|optimize|hack|review|note)\b(\s*:)?(\s+)?/i
|
|
15
|
+
# A magic comment -- an encoding's anywhere, lower-case (Ruby's rule); Ruby's others first (past
|
|
16
|
+
# the ' " : ; it skips) or within -*-, in any case, `-` for `_` (a frozen string literal's after
|
|
17
|
+
# code, a warning: one the tests raise) -- or a RuboCop directive, which would turn its cops off,
|
|
18
|
+
# or fail them.
|
|
19
|
+
MAGIC = /\b(?:en)?coding\s*[:=]|(?:\A['":;\s]*|-\*-.*)(?i:(?:frozen[_-]string[_-]literal|warn[_-]indent|
|
|
20
|
+
shareable[_-]constant[_-]value)\s*:)|(?i:\brubocop\s*:)/x
|
|
21
|
+
# What yard-lint's Markdown and text checks refuse: a list marker (• ·) or a code block's fence
|
|
22
|
+
# first, an em or en dash outside code (.yard-lint.yml's TextSubstitution).
|
|
23
|
+
MARKDOWN = /\A(?:[•·]|```)/
|
|
24
|
+
DASH = /[—–]/
|
|
25
|
+
private_constant :TAG, :ANNOTATION, :MAGIC, :MARKDOWN, :DASH
|
|
26
|
+
|
|
27
|
+
# +summary+, refused where it holds a tag, an annotation, a magic comment or Markdown yard-lint
|
|
28
|
+
# refuses, or leaves a backtick or bold open -- a `**` outside code with a non-space beside it, as
|
|
29
|
+
# yard-lint counts (`x ** 2` is no bold).
|
|
30
|
+
# @param summary [String] one line of text, as Project.check passes it
|
|
31
|
+
# @return [String] +summary+
|
|
32
|
+
# @raise [Veragem::Error] the gem's yard-lint or RuboCop would not take it
|
|
33
|
+
def self.check(summary)
|
|
34
|
+
why = refusal(summary)
|
|
35
|
+
raise Error, "invalid summary #{summary.inspect}: #{why}" if why
|
|
36
|
+
|
|
37
|
+
summary
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private_class_method def self.refusal(summary)
|
|
41
|
+
return "YARD would read a tag" if summary.match?(TAG)
|
|
42
|
+
return "RuboCop or Ruby would read an annotation or a directive" if annotation?(summary) || summary.match?(MAGIC)
|
|
43
|
+
return "yard-lint would refuse its Markdown" if summary.match?(MARKDOWN) || prose(summary).match?(DASH)
|
|
44
|
+
|
|
45
|
+
"a backtick or ** left open" if left_open?(summary)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private_class_method def self.left_open?(summary)
|
|
49
|
+
summary.count("`").odd? || prose(summary).scan(/(?<=\S)\*\*|\*\*(?=\S)/).size.odd?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Its code's spans aside, as yard-lint reads the prose.
|
|
53
|
+
private_class_method def self.prose(summary) = summary.gsub(/`[^`]*`/, "")
|
|
54
|
+
|
|
55
|
+
private_class_method def self.annotation?(summary)
|
|
56
|
+
keyword, colon, space = summary.match(ANNOTATION)&.captures
|
|
57
|
+
return false unless keyword && (colon || space)
|
|
58
|
+
|
|
59
|
+
!(keyword == keyword.capitalize && !colon)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/veragem/version.rb
CHANGED
data/lib/veragem.rb
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
require "bundler"
|
|
4
4
|
require_relative "veragem/version"
|
|
5
5
|
require_relative "veragem/name"
|
|
6
|
+
require_relative "veragem/citation"
|
|
7
|
+
require_relative "veragem/license"
|
|
8
|
+
require_relative "veragem/summary"
|
|
6
9
|
require_relative "veragem/project"
|
|
10
|
+
require_relative "veragem/questions"
|
|
7
11
|
require_relative "veragem/options"
|
|
8
12
|
require_relative "veragem/generator"
|
|
9
13
|
|
|
@@ -12,9 +16,14 @@ module Veragem
|
|
|
12
16
|
# A refusal: Bundler prints its message alone, and exits with its status.
|
|
13
17
|
class Error < Bundler::PluginError; end
|
|
14
18
|
|
|
15
|
-
# Generates the gem +argv+ names into +dir
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
# Generates the gem +argv+ names into +dir+, what no option gives asked of +input+ when a terminal.
|
|
20
|
+
# @param argv [Array<String>] the command's arguments
|
|
21
|
+
# @param dir [String] where the gem is to be generated
|
|
22
|
+
# @param input [#gets, #tty?] where the answers are typed
|
|
23
|
+
# @return [String] the gem's directory
|
|
24
|
+
# @raise [Veragem::Error] veragem refuses
|
|
25
|
+
def self.run(argv, dir: Dir.pwd, input: $stdin)
|
|
26
|
+
project = Options.parse(argv, dir:, input:)
|
|
18
27
|
target = File.join(dir, project.name)
|
|
19
28
|
Generator.generate(project, target)
|
|
20
29
|
puts "Created #{project.name}. Next: cd #{project.name} && bin/setup (CONTRIBUTING.md's first steps)"
|
data/plugins.rb
CHANGED
|
@@ -8,6 +8,10 @@ module Veragem
|
|
|
8
8
|
class Command < Bundler::Plugin::API
|
|
9
9
|
command "veragem"
|
|
10
10
|
|
|
11
|
+
# Runs `bundle veragem ARGS`.
|
|
12
|
+
# @param _command [String] "veragem"
|
|
13
|
+
# @param args [Array<String>] the command's arguments
|
|
14
|
+
# @return [String] the gem's directory
|
|
11
15
|
def exec(_command, args)
|
|
12
16
|
Veragem.run(args)
|
|
13
17
|
end
|