cybertrain 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/README.md +240 -73
- data/cybertrain/cli/build.rb +192 -0
- data/cybertrain/cli/new_app.rb +19 -14
- data/cybertrain/cli/scaffold.rb +7 -0
- data/cybertrain/cli/templates.rb +29 -20
- data/cybertrain/cli/toolchain.rb +787 -0
- data/cybertrain/cli.rb +151 -11
- data/cybertrain/generator/inflector.rb +151 -86
- data/cybertrain/version.rb +5 -1
- data/exe/cybertrain +9 -1
- metadata +7 -3
data/cybertrain/cli.rb
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
# The `cybertrain` command: exe/cybertrain under CRuby (the gem), or
|
|
2
2
|
# bin/cybertrain.rb built by spin (`spin install` from a checkout).
|
|
3
3
|
#
|
|
4
|
-
# cybertrain new NAME [--path DIR | --version V] [--skip-spin]
|
|
4
|
+
# cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]] [--skip-spin]
|
|
5
5
|
# cybertrain generate scaffold NAME field:type ... [parent:references]
|
|
6
|
-
# cybertrain
|
|
6
|
+
# cybertrain db migrate | db COMMAND... | server [PORT] | build | spin ARGS...
|
|
7
|
+
# cybertrain setup [--force] | doctor | version | help
|
|
8
|
+
#
|
|
9
|
+
# Every command that runs spin first puts a Spinel of the pinned release on
|
|
10
|
+
# PATH (Toolchain.ensure!), installing it under ~/.cybertrain the first time.
|
|
7
11
|
require "cybertrain/version"
|
|
8
12
|
require "cybertrain/cli/templates"
|
|
9
13
|
require "cybertrain/cli/new_app"
|
|
10
14
|
require "cybertrain/cli/scaffold"
|
|
15
|
+
require "cybertrain/cli/build"
|
|
16
|
+
require "cybertrain/cli/toolchain"
|
|
11
17
|
|
|
12
18
|
module Cybertrain
|
|
13
19
|
module CLI
|
|
14
20
|
USAGE = <<~TEXT
|
|
15
21
|
Usage:
|
|
16
|
-
cybertrain new NAME [--path DIR | --version V] [--skip-spin]
|
|
22
|
+
cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]] [--skip-spin]
|
|
17
23
|
Create an application in NAME, then run `spin lock` and
|
|
18
24
|
`spin run gen` in it (skipped with --skip-spin). Its spin.toml
|
|
19
25
|
depends on this cybertrain's release by default:
|
|
@@ -21,9 +27,33 @@ module Cybertrain
|
|
|
21
27
|
--path DIR: the framework checkout at DIR (relative to the
|
|
22
28
|
current directory; written as an absolute path).
|
|
23
29
|
--version V: the index version constraint V.
|
|
30
|
+
--git URL [--ref R]: the framework at URL, at branch or tag R.
|
|
24
31
|
cybertrain generate scaffold NAME field:type ... [parent:references]
|
|
25
32
|
Add a resource to the application in the current directory.
|
|
26
33
|
Types: #{Field::TYPES.join(", ")} (default string).
|
|
34
|
+
cybertrain db migrate
|
|
35
|
+
Generate, apply pending migrations, generate again
|
|
36
|
+
(spin run gen; spin run db -- migrate; spin run gen).
|
|
37
|
+
cybertrain db COMMAND...
|
|
38
|
+
Any other database command, after `spin run gen`: status,
|
|
39
|
+
rollback [N] (generates again afterwards), schema:dump, create.
|
|
40
|
+
`cybertrain migration` is the same as `cybertrain db migrate`.
|
|
41
|
+
cybertrain server [PORT]
|
|
42
|
+
Generate, then start the development server
|
|
43
|
+
(spin run gen; spin run NAME [-- PORT]; default port 3000).
|
|
44
|
+
cybertrain build
|
|
45
|
+
Build NAME with app/views embedded and assemble dist/
|
|
46
|
+
(the binary, public/, storage/, tmp/).
|
|
47
|
+
cybertrain spin ARGS...
|
|
48
|
+
Run spin with the pinned Spinel, from any directory
|
|
49
|
+
(cybertrain spin test; cybertrain spin run gen -- --check).
|
|
50
|
+
cybertrain setup [--force]
|
|
51
|
+
Install Spinel #{Cybertrain::SPINEL_TAG} into ~/.cybertrain (CYBERTRAIN_HOME)
|
|
52
|
+
unless a spinel of that release is already on PATH, and print
|
|
53
|
+
the PATH line for ~/.cybertrain/bin. The commands above do the
|
|
54
|
+
install by themselves the first time they need spin.
|
|
55
|
+
cybertrain doctor
|
|
56
|
+
Check the C toolchain, the SQLite headers and the Spinel install.
|
|
27
57
|
cybertrain version
|
|
28
58
|
cybertrain help
|
|
29
59
|
TEXT
|
|
@@ -34,6 +64,13 @@ module Cybertrain
|
|
|
34
64
|
case command
|
|
35
65
|
when "new" then run_new(argv)
|
|
36
66
|
when "generate", "g" then run_generate(argv)
|
|
67
|
+
when "migration" then run_in_app { |_name| run_spin(Build.db_commands(["migrate"])) }
|
|
68
|
+
when "db" then run_in_app { |_name| run_db(argv) }
|
|
69
|
+
when "server" then run_in_app { |name| run_server(name, argv) }
|
|
70
|
+
when "build" then run_in_app { |name| Toolchain.ensure! ? Build.run(".", name) : 1 }
|
|
71
|
+
when "spin" then run_spin_passthrough(argv)
|
|
72
|
+
when "setup" then Toolchain.setup(argv[1, argv.size - 1])
|
|
73
|
+
when "doctor" then run_doctor(argv)
|
|
37
74
|
when "version", "--version", "-v"
|
|
38
75
|
puts "cybertrain #{Cybertrain::VERSION}"
|
|
39
76
|
0
|
|
@@ -51,7 +88,7 @@ module Cybertrain
|
|
|
51
88
|
end
|
|
52
89
|
|
|
53
90
|
def self.run_new(argv)
|
|
54
|
-
raise InvalidArgument, "usage: cybertrain new NAME [--path DIR | --version V] [--skip-spin]" if argv.size < 2
|
|
91
|
+
raise InvalidArgument, "usage: cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]] [--skip-spin]" if argv.size < 2
|
|
55
92
|
|
|
56
93
|
dir = argv[1]
|
|
57
94
|
raise InvalidArgument, "'#{File.basename(dir)}' is not a valid app name (lowercase letters, digits and _)" unless Templates.identifier?(File.basename(dir))
|
|
@@ -59,8 +96,15 @@ module Cybertrain
|
|
|
59
96
|
|
|
60
97
|
NewApp.create(dir, framework_dep(argv))
|
|
61
98
|
return 0 if argv.include?("--skip-spin")
|
|
99
|
+
return 1 unless NewApp.bootstrap(dir)
|
|
62
100
|
|
|
63
|
-
|
|
101
|
+
puts ""
|
|
102
|
+
puts "next:"
|
|
103
|
+
puts " cd #{Build.quote_arg(dir)}"
|
|
104
|
+
puts " cybertrain generate scaffold article title:string body:text"
|
|
105
|
+
puts " cybertrain db migrate"
|
|
106
|
+
puts " cybertrain server"
|
|
107
|
+
0
|
|
64
108
|
end
|
|
65
109
|
|
|
66
110
|
# The spin.toml value of the `cybertrain =` dependency. By default the
|
|
@@ -72,20 +116,116 @@ module Cybertrain
|
|
|
72
116
|
def self.framework_dep(argv)
|
|
73
117
|
path = option(argv, "--path")
|
|
74
118
|
version = option(argv, "--version")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
119
|
+
git = option(argv, "--git")
|
|
120
|
+
ref = option(argv, "--ref")
|
|
121
|
+
given = 0
|
|
122
|
+
given += 1 unless path == ""
|
|
123
|
+
given += 1 unless version == ""
|
|
124
|
+
given += 1 unless git == ""
|
|
125
|
+
raise InvalidArgument, "pass only one of --path, --version and --git" if given > 1
|
|
126
|
+
raise InvalidArgument, "--ref needs --git" if ref != "" && git == ""
|
|
127
|
+
return toml_string(version) unless version == ""
|
|
128
|
+
return "{ path = #{toml_string(File.expand_path(path, Dir.pwd))} }" unless path == ""
|
|
129
|
+
return "{ git = #{toml_string(git)} }" if git != "" && ref == ""
|
|
130
|
+
return "{ git = #{toml_string(git)}, ref = #{toml_string(ref)} }" unless git == ""
|
|
78
131
|
|
|
79
132
|
"{ git = \"#{Cybertrain::REPOSITORY}\", ref = \"v#{Cybertrain::VERSION}\" }"
|
|
80
133
|
end
|
|
81
134
|
|
|
82
|
-
#
|
|
135
|
+
# `value` as a TOML basic string: `\` and `"` escaped. Control
|
|
136
|
+
# characters are refused rather than encoded: no path, URL or git ref
|
|
137
|
+
# needs one, and spin.toml stays readable.
|
|
138
|
+
def self.toml_string(value)
|
|
139
|
+
raise InvalidArgument, "control characters are not allowed in --path, --version, --git or --ref" unless value.bytes.all? { |b| b >= 32 && b != 127 }
|
|
140
|
+
|
|
141
|
+
"\"#{value.gsub("\\", "\\\\\\\\").gsub("\"", "\\\"")}\""
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The value after `--flag`, or "" when the flag is absent. An empty
|
|
145
|
+
# value, or another option in the value's place, means it is missing.
|
|
83
146
|
def self.option(argv, flag)
|
|
84
147
|
i = argv.index(flag)
|
|
85
148
|
return "" if i.nil?
|
|
86
|
-
raise InvalidArgument, "#{flag} needs a value" if i + 1 >= argv.size
|
|
87
149
|
|
|
88
|
-
argv[i + 1]
|
|
150
|
+
value = i + 1 < argv.size ? argv[i + 1] : ""
|
|
151
|
+
raise InvalidArgument, "#{flag} needs a value" if value == "" || value.start_with?("--")
|
|
152
|
+
|
|
153
|
+
value
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Commands that need the app: its name comes from ./spin.toml.
|
|
157
|
+
def self.run_in_app
|
|
158
|
+
name = Build.app_name(".")
|
|
159
|
+
if name == ""
|
|
160
|
+
puts "error: no spin.toml with a [package] name here; run this inside a cybertrain application"
|
|
161
|
+
return 1
|
|
162
|
+
end
|
|
163
|
+
yield name
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Runs spin commands in turn once a Spinel of the pinned release is on
|
|
167
|
+
# PATH; stops at the first failure.
|
|
168
|
+
def self.run_spin(commands)
|
|
169
|
+
return 1 unless Toolchain.ensure!
|
|
170
|
+
|
|
171
|
+
run_all(commands)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def self.run_all(commands)
|
|
175
|
+
status = 0
|
|
176
|
+
commands.each do |command|
|
|
177
|
+
puts "run #{command}"
|
|
178
|
+
unless system(command)
|
|
179
|
+
status = 1
|
|
180
|
+
break
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
status
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# `cybertrain spin ARGS...`: spin from the pinned toolchain, wherever
|
|
187
|
+
# the command runs (an application directory or not).
|
|
188
|
+
def self.run_spin_passthrough(argv)
|
|
189
|
+
args = argv[1, argv.size - 1]
|
|
190
|
+
if args.empty?
|
|
191
|
+
puts "usage: cybertrain spin ARGS... (for example: cybertrain spin test)"
|
|
192
|
+
return 1
|
|
193
|
+
end
|
|
194
|
+
return 1 unless Toolchain.ensure!
|
|
195
|
+
|
|
196
|
+
system((["spin"] + args.map { |arg| Build.quote_arg(arg) }).join(" ")) ? 0 : 1
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def self.run_doctor(argv)
|
|
200
|
+
if argv.size > 1
|
|
201
|
+
puts "usage: cybertrain doctor"
|
|
202
|
+
return 1
|
|
203
|
+
end
|
|
204
|
+
Toolchain.doctor
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# `cybertrain db COMMAND...`.
|
|
208
|
+
def self.run_db(argv)
|
|
209
|
+
args = argv[1, argv.size - 1]
|
|
210
|
+
if args.empty?
|
|
211
|
+
puts "usage: cybertrain db (migrate|rollback [N]|status|schema:dump|create)"
|
|
212
|
+
return 1
|
|
213
|
+
end
|
|
214
|
+
run_spin(Build.db_commands(args))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# `cybertrain server [PORT]`.
|
|
218
|
+
def self.run_server(name, argv)
|
|
219
|
+
port = argv.size > 1 ? argv[1] : ""
|
|
220
|
+
unless port == "" || Build.port?(port)
|
|
221
|
+
puts "error: PORT must be a number"
|
|
222
|
+
return 1
|
|
223
|
+
end
|
|
224
|
+
if argv.size > 2
|
|
225
|
+
puts "error: unexpected argument '#{argv[2]}'"
|
|
226
|
+
return 1
|
|
227
|
+
end
|
|
228
|
+
run_spin(Build.server_commands(name, port))
|
|
89
229
|
end
|
|
90
230
|
|
|
91
231
|
def self.run_generate(argv)
|
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
module Cybertrain
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# ActiveSupport::Inflector's camelize, underscore, pluralize and
|
|
3
|
+
# singularize with Rails' default English inflections
|
|
4
|
+
# (active_support/inflections.rb), checked rule for rule against
|
|
5
|
+
# ActiveSupport 8.1.4.
|
|
6
|
+
#
|
|
7
|
+
# Rails keeps its rules in tables that inflect.plural/singular/irregular/
|
|
8
|
+
# uncountable prepend to and tries them newest first, the first match
|
|
9
|
+
# winning. Spinel cannot keep Regexp objects in a collection, so each table
|
|
10
|
+
# is written out below as literal regexes in that application order: the
|
|
11
|
+
# expansion of every inflect.irregular (newest first), then the plain rules
|
|
12
|
+
# (newest first). An uncountable word -- or a compound ending in one after a
|
|
13
|
+
# word boundary, so "my money" but not "blog_fish" -- and "" come back
|
|
14
|
+
# unchanged.
|
|
15
|
+
#
|
|
16
|
+
# Application-defined inflections (config/initializers/inflections.rb:
|
|
17
|
+
# inflect.irregular, inflect.uncountable, inflect.acronym, ...) and locales
|
|
18
|
+
# other than English are not supported; camelize and underscore behave as
|
|
19
|
+
# Rails' do with no acronyms defined.
|
|
5
20
|
module Inflector
|
|
6
|
-
IRREGULARS = {
|
|
7
|
-
"person" => "people", "man" => "men", "woman" => "women",
|
|
8
|
-
"child" => "children", "mouse" => "mice", "ox" => "oxen"
|
|
9
|
-
}
|
|
10
|
-
UNCOUNTABLE = %w[equipment information rice money species series fish sheep news jeans]
|
|
11
|
-
# Singulars ending in "s" whose plural adds "es" (Rails' alias/status/bus rules).
|
|
12
|
-
ES_WORDS = %w[status alias bus]
|
|
13
|
-
|
|
14
21
|
# "posts_controller" -> "PostsController"; "admin/posts" -> "Admin::Posts".
|
|
15
|
-
|
|
22
|
+
# As Rails: the leading lowercase/digit run is capitalized, and each "_"
|
|
23
|
+
# or "/" is dropped and the alphanumeric run after it capitalized (the
|
|
24
|
+
# rest of that run downcased: "foo_BAR" -> "FooBar"); "/" becomes "::".
|
|
25
|
+
def self.camelize(term)
|
|
16
26
|
out = +""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
n = term.size
|
|
28
|
+
i = 0
|
|
29
|
+
while i < n && lower_or_digit?(term[i])
|
|
30
|
+
out << (i == 0 ? term[i].upcase : term[i])
|
|
31
|
+
i += 1
|
|
32
|
+
end
|
|
33
|
+
while i < n
|
|
34
|
+
ch = term[i]
|
|
35
|
+
i += 1
|
|
36
|
+
if ch == "_" || ch == "/"
|
|
37
|
+
out << "::" if ch == "/"
|
|
38
|
+
start = i
|
|
39
|
+
while i < n && alnum?(term[i])
|
|
40
|
+
out << (i == start ? term[i].upcase : term[i].downcase)
|
|
41
|
+
i += 1
|
|
42
|
+
end
|
|
27
43
|
else
|
|
28
44
|
out << ch
|
|
29
45
|
end
|
|
@@ -31,96 +47,145 @@ module Cybertrain
|
|
|
31
47
|
out
|
|
32
48
|
end
|
|
33
49
|
|
|
34
|
-
# "PostsController" -> "posts_controller"; "
|
|
35
|
-
|
|
50
|
+
# "PostsController" -> "posts_controller"; "HTMLTidyGenerator" ->
|
|
51
|
+
# "html_tidy_generator"; "Admin::Posts" -> "admin/posts". As Rails: "_"
|
|
52
|
+
# goes before an uppercase letter that follows a lowercase letter or digit,
|
|
53
|
+
# or that follows an uppercase letter and precedes a lowercase one; "-"
|
|
54
|
+
# becomes "_" and the result is downcased.
|
|
55
|
+
def self.underscore(camel_cased_word)
|
|
56
|
+
return camel_cased_word.dup unless camel_cased_word.match?(/[A-Z-]|::/)
|
|
57
|
+
|
|
36
58
|
# Two steps: in a threaded program a collection inside `chars` freed the
|
|
37
59
|
# unnamed gsub result it was splitting ("Post" came back as "").
|
|
38
|
-
path =
|
|
60
|
+
path = camel_cased_word.gsub("::", "/")
|
|
39
61
|
chars = path.chars
|
|
40
62
|
out = +""
|
|
41
63
|
chars.each_with_index do |ch, i|
|
|
42
|
-
if upper?(ch)
|
|
43
|
-
prev =
|
|
64
|
+
if i > 0 && upper?(ch)
|
|
65
|
+
prev = chars[i - 1]
|
|
44
66
|
nxt = i + 1 < chars.size ? chars[i + 1] : ""
|
|
45
|
-
|
|
46
|
-
out << "_"
|
|
47
|
-
end
|
|
48
|
-
out << ch.downcase
|
|
49
|
-
else
|
|
50
|
-
out << ch
|
|
67
|
+
out << "_" if lower_or_digit?(prev) || (upper?(prev) && lower?(nxt))
|
|
51
68
|
end
|
|
69
|
+
out << (ch == "-" ? "_" : ch)
|
|
52
70
|
end
|
|
53
|
-
out
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# "posts" -> "post"; only the last "_" word is inflected ("blog_posts").
|
|
57
|
-
def self.singularize(str)
|
|
58
|
-
parts = split_last_word(str)
|
|
59
|
-
parts[0] + singular_word(parts[1])
|
|
71
|
+
out.downcase
|
|
60
72
|
end
|
|
61
73
|
|
|
62
|
-
# "post" -> "posts"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def self.singular_word(word)
|
|
69
|
-
return word if UNCOUNTABLE.include?(word)
|
|
74
|
+
# "post" -> "posts", "person" -> "people", "sales_person" ->
|
|
75
|
+
# "sales_people", "Category" -> "Categories", "sheep" -> "sheep".
|
|
76
|
+
def self.pluralize(word)
|
|
77
|
+
return word.dup if word.empty? || uncountable?(word)
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
return word if
|
|
79
|
+
# inflect.irregular, newest first: zombie, move, sex, child, man, person.
|
|
80
|
+
return word.sub(/(z)ombies$/i, '\1ombies') if word.match?(/(z)ombies$/i)
|
|
81
|
+
return word.sub(/(z)ombie$/i, '\1ombies') if word.match?(/(z)ombie$/i)
|
|
82
|
+
return word.sub(/(m)oves$/i, '\1oves') if word.match?(/(m)oves$/i)
|
|
83
|
+
return word.sub(/(m)ove$/i, '\1oves') if word.match?(/(m)ove$/i)
|
|
84
|
+
return word.sub(/(s)exes$/i, '\1exes') if word.match?(/(s)exes$/i)
|
|
85
|
+
return word.sub(/(s)ex$/i, '\1exes') if word.match?(/(s)ex$/i)
|
|
86
|
+
return word.sub(/(c)hildren$/i, '\1hildren') if word.match?(/(c)hildren$/i)
|
|
87
|
+
return word.sub(/(c)hild$/i, '\1hildren') if word.match?(/(c)hild$/i)
|
|
88
|
+
return word.sub(/(m)en$/i, '\1en') if word.match?(/(m)en$/i)
|
|
89
|
+
return word.sub(/(m)an$/i, '\1en') if word.match?(/(m)an$/i)
|
|
90
|
+
return word.sub(/(p)eople$/i, '\1eople') if word.match?(/(p)eople$/i)
|
|
91
|
+
return word.sub(/(p)erson$/i, '\1eople') if word.match?(/(p)erson$/i)
|
|
92
|
+
# inflect.plural, newest first.
|
|
93
|
+
return word.sub(/(quiz)$/i, '\1zes') if word.match?(/(quiz)$/i)
|
|
94
|
+
return word.sub(/^(oxen)$/i, '\1') if word.match?(/^(oxen)$/i)
|
|
95
|
+
return word.sub(/^(ox)$/i, '\1en') if word.match?(/^(ox)$/i)
|
|
96
|
+
return word.sub(/^(m|l)ice$/i, '\1ice') if word.match?(/^(m|l)ice$/i)
|
|
97
|
+
return word.sub(/^(m|l)ouse$/i, '\1ice') if word.match?(/^(m|l)ouse$/i)
|
|
98
|
+
return word.sub(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices') if word.match?(/(matr|vert|ind)(?:ix|ex)$/i)
|
|
99
|
+
return word.sub(/(x|ch|ss|sh)$/i, '\1es') if word.match?(/(x|ch|ss|sh)$/i)
|
|
100
|
+
return word.sub(/([^aeiouy]|qu)y$/i, '\1ies') if word.match?(/([^aeiouy]|qu)y$/i)
|
|
101
|
+
return word.sub(/(hive)$/i, '\1s') if word.match?(/(hive)$/i)
|
|
102
|
+
return word.sub(/(?:([^f])fe|([lr])f)$/i, '\1\2ves') if word.match?(/(?:([^f])fe|([lr])f)$/i)
|
|
103
|
+
return word.sub(/sis$/i, "ses") if word.match?(/sis$/i)
|
|
104
|
+
return word.sub(/([ti])a$/i, '\1a') if word.match?(/([ti])a$/i)
|
|
105
|
+
return word.sub(/([ti])um$/i, '\1a') if word.match?(/([ti])um$/i)
|
|
106
|
+
return word.sub(/(buffal|tomat)o$/i, '\1oes') if word.match?(/(buffal|tomat)o$/i)
|
|
107
|
+
return word.sub(/(bu)s$/i, '\1ses') if word.match?(/(bu)s$/i)
|
|
108
|
+
return word.sub(/(alias|status)$/i, '\1es') if word.match?(/(alias|status)$/i)
|
|
109
|
+
return word.sub(/(octop|vir)i$/i, '\1i') if word.match?(/(octop|vir)i$/i)
|
|
110
|
+
return word.sub(/(octop|vir)us$/i, '\1i') if word.match?(/(octop|vir)us$/i)
|
|
111
|
+
return word.sub(/^(ax|test)is$/i, '\1es') if word.match?(/^(ax|test)is$/i)
|
|
112
|
+
return word.sub(/s$/i, "s") if word.match?(/s$/i)
|
|
73
113
|
|
|
74
|
-
|
|
75
|
-
word[0, word.size - 3] + "y"
|
|
76
|
-
elsif ES_WORDS.include?(word[0, word.size - 2]) || word.end_with?("sses") ||
|
|
77
|
-
word.end_with?("xes") || word.end_with?("ches") || word.end_with?("shes")
|
|
78
|
-
word[0, word.size - 2]
|
|
79
|
-
elsif word.end_with?("ss") || ES_WORDS.include?(word)
|
|
80
|
-
word
|
|
81
|
-
elsif word.end_with?("s")
|
|
82
|
-
word[0, word.size - 1]
|
|
83
|
-
else
|
|
84
|
-
word
|
|
85
|
-
end
|
|
114
|
+
word.sub(/$/, "s")
|
|
86
115
|
end
|
|
87
116
|
|
|
88
|
-
|
|
89
|
-
|
|
117
|
+
# "posts" -> "post", "people" -> "person", "sales_people" ->
|
|
118
|
+
# "sales_person", "Categories" -> "Category", "news" -> "news".
|
|
119
|
+
def self.singularize(word)
|
|
120
|
+
return word.dup if word.empty? || uncountable?(word)
|
|
90
121
|
|
|
91
|
-
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
122
|
+
# inflect.irregular, newest first: zombie, move, sex, child, man, person.
|
|
123
|
+
return word.sub(/(z)ombies$/i, '\1ombie') if word.match?(/(z)ombies$/i)
|
|
124
|
+
return word.sub(/(z)ombie$/i, '\1ombie') if word.match?(/(z)ombie$/i)
|
|
125
|
+
return word.sub(/(m)oves$/i, '\1ove') if word.match?(/(m)oves$/i)
|
|
126
|
+
return word.sub(/(m)ove$/i, '\1ove') if word.match?(/(m)ove$/i)
|
|
127
|
+
return word.sub(/(s)exes$/i, '\1ex') if word.match?(/(s)exes$/i)
|
|
128
|
+
return word.sub(/(s)ex$/i, '\1ex') if word.match?(/(s)ex$/i)
|
|
129
|
+
return word.sub(/(c)hildren$/i, '\1hild') if word.match?(/(c)hildren$/i)
|
|
130
|
+
return word.sub(/(c)hild$/i, '\1hild') if word.match?(/(c)hild$/i)
|
|
131
|
+
return word.sub(/(m)en$/i, '\1an') if word.match?(/(m)en$/i)
|
|
132
|
+
return word.sub(/(m)an$/i, '\1an') if word.match?(/(m)an$/i)
|
|
133
|
+
return word.sub(/(p)eople$/i, '\1erson') if word.match?(/(p)eople$/i)
|
|
134
|
+
return word.sub(/(p)erson$/i, '\1erson') if word.match?(/(p)erson$/i)
|
|
135
|
+
# inflect.singular, newest first.
|
|
136
|
+
return word.sub(/(database)s$/i, '\1') if word.match?(/(database)s$/i)
|
|
137
|
+
return word.sub(/(quiz)zes$/i, '\1') if word.match?(/(quiz)zes$/i)
|
|
138
|
+
return word.sub(/(matr)ices$/i, '\1ix') if word.match?(/(matr)ices$/i)
|
|
139
|
+
return word.sub(/(vert|ind)ices$/i, '\1ex') if word.match?(/(vert|ind)ices$/i)
|
|
140
|
+
return word.sub(/^(ox)en/i, '\1') if word.match?(/^(ox)en/i)
|
|
141
|
+
return word.sub(/(alias|status)(es)?$/i, '\1') if word.match?(/(alias|status)(es)?$/i)
|
|
142
|
+
return word.sub(/(octop|vir)(us|i)$/i, '\1us') if word.match?(/(octop|vir)(us|i)$/i)
|
|
143
|
+
return word.sub(/^(a)x[ie]s$/i, '\1xis') if word.match?(/^(a)x[ie]s$/i)
|
|
144
|
+
return word.sub(/(cris|test)(is|es)$/i, '\1is') if word.match?(/(cris|test)(is|es)$/i)
|
|
145
|
+
return word.sub(/(shoe)s$/i, '\1') if word.match?(/(shoe)s$/i)
|
|
146
|
+
return word.sub(/(o)es$/i, '\1') if word.match?(/(o)es$/i)
|
|
147
|
+
return word.sub(/(bus)(es)?$/i, '\1') if word.match?(/(bus)(es)?$/i)
|
|
148
|
+
return word.sub(/^(m|l)ice$/i, '\1ouse') if word.match?(/^(m|l)ice$/i)
|
|
149
|
+
return word.sub(/(x|ch|ss|sh)es$/i, '\1') if word.match?(/(x|ch|ss|sh)es$/i)
|
|
150
|
+
return word.sub(/(m)ovies$/i, '\1ovie') if word.match?(/(m)ovies$/i)
|
|
151
|
+
return word.sub(/(s)eries$/i, '\1eries') if word.match?(/(s)eries$/i)
|
|
152
|
+
return word.sub(/([^aeiouy]|qu)ies$/i, '\1y') if word.match?(/([^aeiouy]|qu)ies$/i)
|
|
153
|
+
return word.sub(/([lr])ves$/i, '\1f') if word.match?(/([lr])ves$/i)
|
|
154
|
+
return word.sub(/(tive)s$/i, '\1') if word.match?(/(tive)s$/i)
|
|
155
|
+
return word.sub(/(hive)s$/i, '\1') if word.match?(/(hive)s$/i)
|
|
156
|
+
return word.sub(/([^f])ves$/i, '\1fe') if word.match?(/([^f])ves$/i)
|
|
157
|
+
return word.sub(/(^analy)(sis|ses)$/i, '\1sis') if word.match?(/(^analy)(sis|ses)$/i)
|
|
158
|
+
if word.match?(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i)
|
|
159
|
+
return word.sub(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1sis')
|
|
103
160
|
end
|
|
104
|
-
|
|
161
|
+
return word.sub(/([ti])a$/i, '\1um') if word.match?(/([ti])a$/i)
|
|
162
|
+
return word.sub(/(n)ews$/i, '\1ews') if word.match?(/(n)ews$/i)
|
|
163
|
+
return word.sub(/(ss)$/i, '\1') if word.match?(/(ss)$/i)
|
|
164
|
+
return word.sub(/s$/i, "") if word.match?(/s$/i)
|
|
105
165
|
|
|
106
|
-
|
|
107
|
-
def self.split_last_word(str)
|
|
108
|
-
i = str.rindex("_")
|
|
109
|
-
return ["", str] if i.nil?
|
|
110
|
-
|
|
111
|
-
[str[0, i + 1], str[i + 1, str.size - i - 1]]
|
|
166
|
+
word.dup
|
|
112
167
|
end
|
|
113
168
|
|
|
114
|
-
|
|
115
|
-
|
|
169
|
+
# inflect.uncountable: Rails matches /\b<word>\Z/i, so "my money" and
|
|
170
|
+
# "Sheep" are uncountable but "blog_fish" ("_" is a word character) is not.
|
|
171
|
+
def self.uncountable?(word)
|
|
172
|
+
word.match?(/\b(?:equipment|information|rice|money|species|series|fish|sheep|jeans|police)\Z/i)
|
|
116
173
|
end
|
|
117
174
|
|
|
118
175
|
def self.upper?(ch)
|
|
119
176
|
ch >= "A" && ch <= "Z"
|
|
120
177
|
end
|
|
121
178
|
|
|
179
|
+
def self.lower?(ch)
|
|
180
|
+
ch >= "a" && ch <= "z"
|
|
181
|
+
end
|
|
182
|
+
|
|
122
183
|
def self.lower_or_digit?(ch)
|
|
123
|
-
(ch
|
|
184
|
+
lower?(ch) || (ch >= "0" && ch <= "9")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def self.alnum?(ch)
|
|
188
|
+
upper?(ch) || lower_or_digit?(ch)
|
|
124
189
|
end
|
|
125
190
|
end
|
|
126
191
|
end
|
data/cybertrain/version.rb
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
module Cybertrain
|
|
2
|
-
VERSION = "0.
|
|
2
|
+
VERSION = "0.2.0"
|
|
3
3
|
|
|
4
4
|
# Where `cybertrain new` points an app's spin.toml by default: the git tag
|
|
5
5
|
# "v#{VERSION}" of this repository. Kept here because the gem (CRuby) and
|
|
6
6
|
# the spin-built CLI read the same file.
|
|
7
7
|
REPOSITORY = "https://github.com/saeki-mototsune/cybertrain"
|
|
8
|
+
|
|
9
|
+
# The Spinel release cybertrain is written and tested against: the tag
|
|
10
|
+
# `cybertrain setup` builds and CI's SPINEL_TAG.
|
|
11
|
+
SPINEL_TAG = "2026.09.12"
|
|
8
12
|
end
|
data/exe/cybertrain
CHANGED
|
@@ -4,4 +4,12 @@
|
|
|
4
4
|
# same entry point, compiled by spin).
|
|
5
5
|
require "cybertrain/cli"
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# Ctrl-C reaches the whole process group: the child (`spin run NAME`, a
|
|
8
|
+
# build) stops itself, and CRuby raises Interrupt out of `system` here.
|
|
9
|
+
# Build.run's ensure has restored gen/views.rb by the time it arrives.
|
|
10
|
+
begin
|
|
11
|
+
status = Cybertrain::CLI.run(ARGV)
|
|
12
|
+
rescue Interrupt
|
|
13
|
+
status = 130
|
|
14
|
+
end
|
|
15
|
+
exit(status)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cybertrain
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Saeki Mototsune
|
|
@@ -12,8 +12,10 @@ dependencies: []
|
|
|
12
12
|
description: |
|
|
13
13
|
cybertrain is a Rails-shaped web framework written for Spinel, which
|
|
14
14
|
compiles an application into a native binary. This gem installs the
|
|
15
|
-
`cybertrain` command (`cybertrain new`, `
|
|
16
|
-
|
|
15
|
+
`cybertrain` command (`cybertrain new`, `generate scaffold`,
|
|
16
|
+
`db migrate`, `server`, `build`). The first command that needs Spinel
|
|
17
|
+
builds the pinned release into ~/.cybertrain (a C compiler, make, git,
|
|
18
|
+
curl and the SQLite headers are required); `cybertrain doctor` checks.
|
|
17
19
|
executables:
|
|
18
20
|
- cybertrain
|
|
19
21
|
extensions: []
|
|
@@ -21,9 +23,11 @@ extra_rdoc_files: []
|
|
|
21
23
|
files:
|
|
22
24
|
- README.md
|
|
23
25
|
- cybertrain/cli.rb
|
|
26
|
+
- cybertrain/cli/build.rb
|
|
24
27
|
- cybertrain/cli/new_app.rb
|
|
25
28
|
- cybertrain/cli/scaffold.rb
|
|
26
29
|
- cybertrain/cli/templates.rb
|
|
30
|
+
- cybertrain/cli/toolchain.rb
|
|
27
31
|
- cybertrain/generator/inflector.rb
|
|
28
32
|
- cybertrain/version.rb
|
|
29
33
|
- exe/cybertrain
|