cybertrain 0.1.1 → 0.2.1
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 +233 -67
- data/cybertrain/app_name.rb +36 -0
- data/cybertrain/cli/build.rb +93 -16
- data/cybertrain/cli/new_app.rb +22 -12
- data/cybertrain/cli/scaffold.rb +78 -11
- data/cybertrain/cli/templates.rb +16 -20
- data/cybertrain/cli/toolchain.rb +787 -0
- data/cybertrain/cli.rb +114 -25
- data/cybertrain/generator/inflector.rb +154 -88
- data/cybertrain/ident.rb +171 -0
- data/cybertrain/version.rb +5 -1
- metadata +8 -3
data/cybertrain/cli/build.rb
CHANGED
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
# `cybertrain build`: dist/ = the app binary with app/views embedded, plus
|
|
2
|
-
# public/. Also the command lists `cybertrain
|
|
2
|
+
# public/. Also the command lists `cybertrain db` and `server` run. Plain Ruby:
|
|
3
3
|
# runs under CRuby (the gem) and compiles under Spinel (spin install).
|
|
4
|
+
require "cybertrain/app_name"
|
|
5
|
+
|
|
4
6
|
module Cybertrain
|
|
5
7
|
module CLI
|
|
6
8
|
module Build
|
|
7
|
-
# The
|
|
9
|
+
# The entries assemble() keeps in dist/ besides the binary: the copied
|
|
10
|
+
# public/ and the storage/ and tmp/ directories. A package named like
|
|
11
|
+
# one of them would collide with it (the binary renamed to dist/public
|
|
12
|
+
# gets moved aside as dist/.public.old and deleted; a dist/storage
|
|
13
|
+
# directory cannot be replaced by the binary), so `cybertrain build`
|
|
14
|
+
# (require_dist_name!) and `cybertrain new` refuse these names;
|
|
15
|
+
# app_name, which every command reads, does not: `cybertrain db` and
|
|
16
|
+
# `server` work for an existing app named like one. assemble builds its
|
|
17
|
+
# paths from these constants so the two cannot drift.
|
|
18
|
+
DIST_PUBLIC = "public"
|
|
19
|
+
DIST_STORAGE = "storage"
|
|
20
|
+
DIST_TMP = "tmp"
|
|
21
|
+
DIST_ENTRIES = [DIST_PUBLIC, DIST_STORAGE, DIST_TMP]
|
|
22
|
+
|
|
23
|
+
# "" when name cannot collide with what assemble() puts in dist/, else
|
|
24
|
+
# the reason. A leading "." covers assemble's scratch entries
|
|
25
|
+
# (dist/.<name>.tmp, dist/.public.tmp, dist/.public.old) and "."/"..".
|
|
26
|
+
# The entry names match case-insensitively: on a case-insensitive
|
|
27
|
+
# filesystem (macOS's default APFS, one of the CI OSes; Windows) "Public"
|
|
28
|
+
# and "TMP" are the same directory entry as public and tmp. Only a
|
|
29
|
+
# different spelling of the whole name ("tmp2", "my_public") is another
|
|
30
|
+
# entry. downcase is ASCII-safe here: the entries are ASCII.
|
|
31
|
+
def self.dist_name_problem(name)
|
|
32
|
+
return "" unless DIST_ENTRIES.include?(name.downcase) || name.start_with?(".")
|
|
33
|
+
|
|
34
|
+
"collides with what `cybertrain build` keeps in dist/ (#{DIST_ENTRIES.join(", ")}) or its scratch entries: it cannot be one of those names (in any letter case) or start with '.'"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The [package] name in root/spin.toml, or "" when there is none. The
|
|
38
|
+
# name ends up in shell command strings and file paths, so it must be
|
|
39
|
+
# a name `cybertrain new` could have produced.
|
|
8
40
|
def self.app_name(root)
|
|
9
41
|
path = "#{root}/spin.toml"
|
|
10
42
|
return "" unless File.exist?(path)
|
|
@@ -22,22 +54,65 @@ module Cybertrain
|
|
|
22
54
|
break
|
|
23
55
|
end
|
|
24
56
|
end
|
|
57
|
+
# An empty name is run_in_app's "no spin.toml with a [package] name".
|
|
58
|
+
# Any other name must be usable as a `spin build` target and a
|
|
59
|
+
# build/bin/<name> path: the same predicate Application.new applies to
|
|
60
|
+
# the dev rebuilder's target. The name reaches the shell through
|
|
61
|
+
# quote_arg, so only what breaks that target or path is refused.
|
|
62
|
+
return "" if name.empty?
|
|
63
|
+
|
|
64
|
+
problem = AppName.problem(name)
|
|
65
|
+
raise InvalidArgument, "spin.toml [package] name '#{name}' #{problem}" unless problem.empty?
|
|
66
|
+
|
|
25
67
|
name
|
|
26
68
|
end
|
|
27
69
|
|
|
70
|
+
# The build path's check, made once in CLI.build_app before the
|
|
71
|
+
# toolchain is touched (Build.run does not repeat it): a name that
|
|
72
|
+
# collides with a dist/ entry only matters to assemble. Returns nil.
|
|
73
|
+
def self.require_dist_name!(name)
|
|
74
|
+
problem = dist_name_problem(name)
|
|
75
|
+
raise InvalidArgument, "spin.toml [package] name '#{name}' #{problem}" unless problem.empty?
|
|
76
|
+
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# name goes through quote_arg too: app_name validates it, but the
|
|
81
|
+
# command strings stay safe for a caller that skipped that, whatever the
|
|
82
|
+
# spelling. An empty name cannot reach these from the CLI (run_in_app
|
|
83
|
+
# refuses it first); a direct caller passing "" gets `spin build ''`.
|
|
28
84
|
def self.commands(name)
|
|
29
|
-
["spin run gen -- --embed-views", "spin build #{name}", "spin run gen"]
|
|
85
|
+
["spin run gen -- --embed-views", "spin build #{quote_arg(name)}", "spin run gen"]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# `cybertrain db ARGS`, through bin/db.rb (the app binary cannot compile
|
|
89
|
+
# before the first migration has produced gen/models). bin/db.rb only
|
|
90
|
+
# knows the migrations gen/migrations.rb lists, so gen runs first;
|
|
91
|
+
# migrate and rollback rewrite db/schema.rb, which gen/models is
|
|
92
|
+
# derived from, so gen runs again after them.
|
|
93
|
+
def self.db_commands(args)
|
|
94
|
+
words = args.map { |arg| quote_arg(arg) }.join(" ")
|
|
95
|
+
commands = ["spin run gen", "spin run db -- #{words}"]
|
|
96
|
+
commands << "spin run gen" if args[0] == "migrate" || args[0] == "rollback"
|
|
97
|
+
commands
|
|
30
98
|
end
|
|
31
99
|
|
|
32
|
-
#
|
|
33
|
-
# migration has produced gen/models.
|
|
100
|
+
# `cybertrain migration`: the same as `cybertrain db migrate`.
|
|
34
101
|
def self.migration_commands
|
|
35
|
-
["
|
|
102
|
+
db_commands(["migrate"])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# A word sh reads as itself stays bare; anything else is single-quoted.
|
|
106
|
+
def self.quote_arg(text)
|
|
107
|
+
return text if !text.empty? && text.match(/\A[A-Za-z0-9_:.\/-]+\z/)
|
|
108
|
+
|
|
109
|
+
shell_quote(text)
|
|
36
110
|
end
|
|
37
111
|
|
|
38
112
|
# port is "" (the app's default, 3000) or a port? string.
|
|
39
113
|
def self.server_commands(name, port)
|
|
40
|
-
|
|
114
|
+
target = quote_arg(name)
|
|
115
|
+
run = port == "" ? "spin run #{target}" : "spin run #{target} -- #{quote_arg(port)}"
|
|
41
116
|
["spin run gen", run]
|
|
42
117
|
end
|
|
43
118
|
|
|
@@ -66,6 +141,8 @@ module Cybertrain
|
|
|
66
141
|
end
|
|
67
142
|
|
|
68
143
|
# Runs the build in root and assembles dist/. Returns the exit code.
|
|
144
|
+
# name must already have passed require_dist_name!: CLI.build_app checks
|
|
145
|
+
# it once, before the toolchain is fetched.
|
|
69
146
|
def self.run(root, name, runner = Runner.new)
|
|
70
147
|
steps = commands(name)
|
|
71
148
|
ok = false
|
|
@@ -120,8 +197,8 @@ module Cybertrain
|
|
|
120
197
|
dist = "#{root}/dist"
|
|
121
198
|
Templates.mkdir_p(dist)
|
|
122
199
|
binary_tmp = "#{dist}/.#{name}.tmp"
|
|
123
|
-
public_tmp = "#{dist}
|
|
124
|
-
public_old = "#{dist}
|
|
200
|
+
public_tmp = "#{dist}/.#{DIST_PUBLIC}.tmp"
|
|
201
|
+
public_old = "#{dist}/.#{DIST_PUBLIC}.old"
|
|
125
202
|
# Leftovers of an interrupted earlier run.
|
|
126
203
|
rm_tree(binary_tmp)
|
|
127
204
|
rm_tree(public_tmp)
|
|
@@ -132,20 +209,20 @@ module Cybertrain
|
|
|
132
209
|
|
|
133
210
|
File.rename(binary_tmp, "#{dist}/#{name}")
|
|
134
211
|
|
|
135
|
-
if File.directory?("#{root}
|
|
136
|
-
public_command = "cp -R #{shell_quote("#{root}
|
|
137
|
-
raise InvalidArgument, "could not copy
|
|
212
|
+
if File.directory?("#{root}/#{DIST_PUBLIC}")
|
|
213
|
+
public_command = "cp -R #{shell_quote("#{root}/#{DIST_PUBLIC}")} #{shell_quote(public_tmp)}"
|
|
214
|
+
raise InvalidArgument, "could not copy #{DIST_PUBLIC}/ to dist/" unless system(public_command)
|
|
138
215
|
else
|
|
139
216
|
Dir.mkdir(public_tmp)
|
|
140
217
|
end
|
|
141
|
-
public_dir = "#{dist}
|
|
218
|
+
public_dir = "#{dist}/#{DIST_PUBLIC}"
|
|
142
219
|
File.rename(public_dir, public_old) if File.exist?(public_dir) || File.symlink?(public_dir)
|
|
143
220
|
File.rename(public_tmp, public_dir)
|
|
144
221
|
rm_tree(public_old)
|
|
145
222
|
|
|
146
|
-
Templates.mkdir_p("#{dist}
|
|
147
|
-
Templates.mkdir_p("#{dist}
|
|
148
|
-
["dist/#{name}", "dist/
|
|
223
|
+
Templates.mkdir_p("#{dist}/#{DIST_STORAGE}")
|
|
224
|
+
Templates.mkdir_p("#{dist}/#{DIST_TMP}")
|
|
225
|
+
["dist/#{name}", "dist/#{DIST_PUBLIC}/", "dist/#{DIST_STORAGE}/", "dist/#{DIST_TMP}/"]
|
|
149
226
|
end
|
|
150
227
|
|
|
151
228
|
# A symlink is removed itself, never followed: its target may be
|
data/cybertrain/cli/new_app.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# `cybertrain new blog`: writes the skeleton of an application package.
|
|
2
2
|
require "cybertrain/generator/inflector"
|
|
3
3
|
require "cybertrain/cli/templates"
|
|
4
|
+
require "cybertrain/cli/build"
|
|
5
|
+
require "cybertrain/cli/toolchain"
|
|
4
6
|
|
|
5
7
|
module Cybertrain
|
|
6
8
|
module CLI
|
|
7
9
|
module NewApp
|
|
8
|
-
KEEP_DIRS = ["db/migrate", "app/models", "app/helpers", "gen", "storage", "tmp", "test"]
|
|
9
|
-
|
|
10
10
|
# dir is the directory to create ("blog" or "path/to/blog"; its last
|
|
11
11
|
# component names the package). framework_dep is the TOML value that
|
|
12
12
|
# spin.toml's `cybertrain =` line gets: `{ path = "/abs/cybertrain" }` or
|
|
@@ -14,6 +14,11 @@ module Cybertrain
|
|
|
14
14
|
# relative to dir.
|
|
15
15
|
def self.create(dir, framework_dep)
|
|
16
16
|
package = File.basename(dir)
|
|
17
|
+
# Before anything is written: `public`, `storage` and `tmp` are valid
|
|
18
|
+
# identifiers but collide with dist/ entries (Build.dist_name_problem).
|
|
19
|
+
problem = Build.dist_name_problem(package)
|
|
20
|
+
raise InvalidArgument, "'#{package}' is not a valid app name: it #{problem}" unless problem.empty?
|
|
21
|
+
|
|
17
22
|
title = Inflector.camelize(package)
|
|
18
23
|
files = [
|
|
19
24
|
["spin.toml", Templates.spin_toml(package, framework_dep)],
|
|
@@ -47,28 +52,33 @@ module Cybertrain
|
|
|
47
52
|
|
|
48
53
|
# What `rails new` does with `bundle install`: resolve and lock the
|
|
49
54
|
# framework (fetching it into spin's cache, ~/.cache/spin), then write
|
|
50
|
-
# gen/ so `spin build` works straight away.
|
|
51
|
-
#
|
|
55
|
+
# gen/ so `spin build` works straight away. Installs Spinel first when
|
|
56
|
+
# this machine has none of the pinned release (Toolchain.ensure!).
|
|
57
|
+
# Returns false when a step failed.
|
|
52
58
|
def self.bootstrap(dir)
|
|
53
|
-
unless
|
|
54
|
-
puts "skip spin lock / spin run gen:
|
|
55
|
-
puts "
|
|
56
|
-
return
|
|
59
|
+
unless Toolchain.ensure!
|
|
60
|
+
puts "skip spin lock / spin run gen: Spinel #{Cybertrain::SPINEL_TAG} is not available"
|
|
61
|
+
puts " fix the problem above (`cybertrain doctor` lists the checks), then: cybertrain setup && #{recovery_command(dir)}"
|
|
62
|
+
return false
|
|
57
63
|
end
|
|
58
64
|
|
|
59
65
|
puts "run spin lock && spin run gen"
|
|
60
66
|
return true if system(bootstrap_command(dir))
|
|
61
67
|
|
|
62
|
-
puts "error: bootstrapping #{dir} failed; fix the cause, then run: #{
|
|
68
|
+
puts "error: bootstrapping #{dir} failed; fix the cause, then run: #{recovery_command(dir)}"
|
|
63
69
|
false
|
|
64
70
|
end
|
|
65
71
|
|
|
72
|
+
# Run by bootstrap once Toolchain.ensure! has put spin on this
|
|
73
|
+
# process's PATH.
|
|
66
74
|
def self.bootstrap_command(dir)
|
|
67
|
-
"cd #{shell_quote(dir)} && spin lock && spin run gen"
|
|
75
|
+
"cd #{Build.shell_quote(dir)} && spin lock && spin run gen"
|
|
68
76
|
end
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
# The same steps for the user to run by hand: through `cybertrain
|
|
79
|
+
# spin`, since the Spinel cybertrain installed is not on their PATH.
|
|
80
|
+
def self.recovery_command(dir)
|
|
81
|
+
"cd #{Build.shell_quote(dir)} && cybertrain spin lock && cybertrain spin run gen"
|
|
72
82
|
end
|
|
73
83
|
end
|
|
74
84
|
end
|
data/cybertrain/cli/scaffold.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# migration, model, controller and views of a resource and adds
|
|
3
3
|
# `resources :posts` to config/routes.rb, the way Rails' scaffold does.
|
|
4
4
|
require "cybertrain/generator/inflector"
|
|
5
|
+
require "cybertrain/ident"
|
|
5
6
|
require "cybertrain/cli/templates"
|
|
6
7
|
|
|
7
8
|
module Cybertrain
|
|
@@ -15,17 +16,12 @@ module Cybertrain
|
|
|
15
16
|
TYPES = %w[string text integer float boolean date datetime references]
|
|
16
17
|
|
|
17
18
|
# Columns every table already has (the primary key and t.timestamps).
|
|
19
|
+
# Everything else a column may not be called -- Ruby keywords, names
|
|
20
|
+
# that collide with the generated model -- comes from Cybertrain::Ident,
|
|
21
|
+
# the same rules the generator applies, so the scaffold refuses the
|
|
22
|
+
# name instead of writing files that `spin run gen` then rejects.
|
|
18
23
|
RESERVED_COLUMNS = %w[id created_at updated_at]
|
|
19
24
|
|
|
20
|
-
# Ruby keywords: a column (or resource) named after one would generate
|
|
21
|
-
# `def class` / `|end|` and break the generated code.
|
|
22
|
-
RUBY_KEYWORDS = %w[
|
|
23
|
-
__ENCODING__ __LINE__ __FILE__ BEGIN END alias and begin break case
|
|
24
|
-
class def defined do else elsif end ensure false for if in module
|
|
25
|
-
next nil not or redo rescue retry return self super then true undef
|
|
26
|
-
unless until when while yield
|
|
27
|
-
]
|
|
28
|
-
|
|
29
25
|
attr_reader :field_name, :field_type
|
|
30
26
|
|
|
31
27
|
def self.parse(arg)
|
|
@@ -38,11 +34,18 @@ module Cybertrain
|
|
|
38
34
|
type = parts.size > 1 ? parts[1].to_s : "string"
|
|
39
35
|
type = "references" if type == "belongs_to"
|
|
40
36
|
raise InvalidArgument, "bad field name '#{name}'" unless Templates.identifier?(name)
|
|
41
|
-
raise InvalidArgument, "'#{name}' is a Ruby keyword and cannot name a field" if RUBY_KEYWORDS.include?(name)
|
|
42
37
|
raise InvalidArgument, "unknown type '#{type}' for #{name} (use #{TYPES.join(", ")})" unless TYPES.include?(type)
|
|
43
38
|
|
|
44
39
|
field = Field.new(name, type)
|
|
45
40
|
raise InvalidArgument, "'#{field.column_name}' is a column every table already has" if RESERVED_COLUMNS.include?(field.column_name)
|
|
41
|
+
# Ident.unusable_reason is the one definition of a name the scaffold
|
|
42
|
+
# must not invent; Scaffold.refuse_unusable! turns its answer into the
|
|
43
|
+
# message. A name that only shadows an Object method is accepted, as the
|
|
44
|
+
# generator accepts it, and Scaffold.generate prints a note.
|
|
45
|
+
Scaffold.refuse_unusable!(field.column_name, "field")
|
|
46
|
+
# A references field also defines the reader `def <name>` (post:references
|
|
47
|
+
# -> def post), which answers to the same rules as a column.
|
|
48
|
+
Scaffold.refuse_unusable!(name, "reference") if field.reference?
|
|
46
49
|
|
|
47
50
|
field
|
|
48
51
|
end
|
|
@@ -92,6 +95,13 @@ module Cybertrain
|
|
|
92
95
|
@fields = fields
|
|
93
96
|
end
|
|
94
97
|
|
|
98
|
+
# The index route helper's name without _path, as Rails' scaffold
|
|
99
|
+
# names it: the plural, or "<plural>_index" for a word that is its own
|
|
100
|
+
# plural (sheep_index_path), matching the routes DSL.
|
|
101
|
+
def index_helper
|
|
102
|
+
@plural == @singular ? "#{@plural}_index" : @plural
|
|
103
|
+
end
|
|
104
|
+
|
|
95
105
|
# The column the model validates for presence ("" when there is none).
|
|
96
106
|
def first_string_field
|
|
97
107
|
@fields.each { |f| return f.field_name if f.field_type == "string" }
|
|
@@ -102,6 +112,36 @@ module Cybertrain
|
|
|
102
112
|
module Scaffold
|
|
103
113
|
DRAW_LINE = "Cybertrain::Routes.draw do"
|
|
104
114
|
|
|
115
|
+
# Policy: the scaffold refuses what the generator would RENAME (Ruby
|
|
116
|
+
# keywords, Ident::RESERVED_COLUMN_NAMES): its views and controller call
|
|
117
|
+
# the reader by the field's name, so a renamed reader would break them.
|
|
118
|
+
# It accepts what the generator only annotates (a name that shadows an
|
|
119
|
+
# Object method, `method`, `display`, `send`) and says so on stdout
|
|
120
|
+
# (shadow_notes), as the generator does in the model file.
|
|
121
|
+
#
|
|
122
|
+
# Raises InvalidArgument when `name` is a name the scaffold must not
|
|
123
|
+
# invent, with the message for the reason Ident.unusable_reason gives;
|
|
124
|
+
# `what` says what the name would name ("field", "reference",
|
|
125
|
+
# "resource"). The one place that maps a reason to a message, shared by the
|
|
126
|
+
# field column, the references reader and the resource singular: a
|
|
127
|
+
# reason Ident adds must be added here, and the `else` raises for one
|
|
128
|
+
# that is not (a `case` without it would return nil and let the name
|
|
129
|
+
# through silently). Returns nil, explicitly, for a usable name (NOTES
|
|
130
|
+
# rules 10/34: one return type).
|
|
131
|
+
def self.refuse_unusable!(name, what)
|
|
132
|
+
case Ident.unusable_reason(name)
|
|
133
|
+
when ""
|
|
134
|
+
nil
|
|
135
|
+
when "keyword"
|
|
136
|
+
raise InvalidArgument, "'#{name}' is a Ruby keyword and cannot name a #{what}"
|
|
137
|
+
when "reserved"
|
|
138
|
+
raise InvalidArgument, "'#{name}' would shadow a method of the generated model (Cybertrain::Model); pick another #{what}"
|
|
139
|
+
else
|
|
140
|
+
raise RuntimeError, "Ident.unusable_reason('#{name}') answered a reason Scaffold.refuse_unusable! has no message for"
|
|
141
|
+
end
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
105
145
|
# Returns the paths it created (files that already exist are reported
|
|
106
146
|
# as "identical" or "exist" and left alone). Raises InvalidArgument on a
|
|
107
147
|
# bad name, a bad or duplicate field, or a routes file without a
|
|
@@ -113,7 +153,18 @@ module Cybertrain
|
|
|
113
153
|
|
|
114
154
|
underscored = Inflector.underscore(name)
|
|
115
155
|
raise InvalidArgument, "bad resource name '#{name}'" unless Templates.identifier?(underscored)
|
|
116
|
-
|
|
156
|
+
singular = Inflector.singularize(underscored)
|
|
157
|
+
# Policy (refuse_unusable!): the scaffold refuses a singular the
|
|
158
|
+
# generator would RENAME. A reserved one (update, save) would give
|
|
159
|
+
# every referencing model a `<stem>_as_<col>` reader, and the singular
|
|
160
|
+
# is also the model class name. A shadowing one (display, tap) is
|
|
161
|
+
# accepted with a note (shadow_notes). The generator itself still
|
|
162
|
+
# generates a schema with a reserved table (a hand-written table named
|
|
163
|
+
# `updates` works), so this is the scaffold's choice, not the
|
|
164
|
+
# generator's refusal. The plural (the has_many side) is left to the
|
|
165
|
+
# generator, which renames such a reader (`errors_as_post`) -- and
|
|
166
|
+
# nothing may reference the table.
|
|
167
|
+
Scaffold.refuse_unusable!(singular, "resource")
|
|
117
168
|
|
|
118
169
|
parsed = Array.new(0) { Field.new("", "") }
|
|
119
170
|
columns = Array.new(0) { "" }
|
|
@@ -138,9 +189,25 @@ module Cybertrain
|
|
|
138
189
|
record(created, root, "#{views}/edit.html.erb", Templates.edit_view(res))
|
|
139
190
|
record(created, root, "#{views}/_form.html.erb", Templates.form_partial(res))
|
|
140
191
|
add_route(routes_path, res.plural)
|
|
192
|
+
shadow_notes(res).each { |note| puts note }
|
|
141
193
|
created
|
|
142
194
|
end
|
|
143
195
|
|
|
196
|
+
# One line per name the scaffold accepted that shadows an Object method
|
|
197
|
+
# (Ident::SHADOWING_COLUMN_NAMES), the generator's own note told on
|
|
198
|
+
# stdout: a field (or references reader) on the model, the resource
|
|
199
|
+
# singular on every model that references it.
|
|
200
|
+
def self.shadow_notes(res)
|
|
201
|
+
notes = Array.new(0) { "" }
|
|
202
|
+
res.fields.each do |f|
|
|
203
|
+
notes << "note: '#{f.field_name}' shadows Object##{f.field_name} on the generated model; call it as #{res.singular}.#{f.field_name}" if Ident.shadowing_column?(f.field_name)
|
|
204
|
+
end
|
|
205
|
+
if Ident.shadowing_column?(res.singular)
|
|
206
|
+
notes << "note: '#{res.singular}' shadows Object##{res.singular} on a model that references #{res.plural} (belongs_to reader); call it as record.#{res.singular}"
|
|
207
|
+
end
|
|
208
|
+
notes
|
|
209
|
+
end
|
|
210
|
+
|
|
144
211
|
def self.record(created, root, path, content)
|
|
145
212
|
created << path if Templates.write(root, path, content) == "create"
|
|
146
213
|
nil
|
data/cybertrain/cli/templates.rb
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
# The scaffold templates take a Cybertrain::CLI::Resource (cli/scaffold.rb).
|
|
6
6
|
# Generated views stay inside the template language of docs/design.md
|
|
7
7
|
# section 7: literals, @ivars, locals, helper calls, `if` and `each do`.
|
|
8
|
+
require "cybertrain/ident"
|
|
9
|
+
|
|
8
10
|
module Cybertrain
|
|
9
11
|
module CLI
|
|
10
12
|
module Templates
|
|
@@ -39,19 +41,12 @@ module Cybertrain
|
|
|
39
41
|
nil
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
# A lowercase Ruby-ish name: "post", "blog_post", "released_on".
|
|
44
|
+
# A lowercase Ruby-ish name: "post", "blog_post", "released_on". The
|
|
45
|
+
# snake_case subset of Ident.column? (what the generator accepts),
|
|
46
|
+
# letter-first since it names packages, binaries and files too; one
|
|
47
|
+
# definition of the characters, so the two cannot drift apart.
|
|
43
48
|
def self.identifier?(word)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
ok = true
|
|
47
|
-
first = true
|
|
48
|
-
word.each_char do |ch|
|
|
49
|
-
lower = ch >= "a" && ch <= "z"
|
|
50
|
-
digit = ch >= "0" && ch <= "9"
|
|
51
|
-
ok = false unless lower || (!first && (digit || ch == "_"))
|
|
52
|
-
first = false
|
|
53
|
-
end
|
|
54
|
-
ok
|
|
49
|
+
Ident.snake_case?(word, false)
|
|
55
50
|
end
|
|
56
51
|
|
|
57
52
|
# "blog_posts" -> "Blog posts"
|
|
@@ -92,17 +87,17 @@ module Cybertrain
|
|
|
92
87
|
|
|
93
88
|
```sh
|
|
94
89
|
cybertrain generate scaffold post title:string body:text
|
|
95
|
-
cybertrain
|
|
90
|
+
cybertrain db migrate # gen, apply db/migrate, gen again
|
|
96
91
|
cybertrain server # http://127.0.0.1:3000 (`cybertrain server 4000` for another port)
|
|
97
92
|
cybertrain build # dist/: the binary (views embedded) + public/
|
|
98
93
|
```
|
|
99
94
|
|
|
100
|
-
`spin run gen` (which `
|
|
95
|
+
`spin run gen` (which `db`, `server` and `build` run for you)
|
|
101
96
|
regenerates `gen/` from db/schema.rb, config/routes.rb and app/;
|
|
102
97
|
commit `gen/`. In development views under `app/views/` are read at
|
|
103
98
|
run time: edit them without rebuilding. `cybertrain db status` and
|
|
104
99
|
`cybertrain db rollback 1` reach the other database commands.
|
|
105
|
-
`cybertrain
|
|
100
|
+
`cybertrain db migrate` runs migrations through `bin/db.rb`; a
|
|
106
101
|
deployed `dist/#{package}` runs them with `./#{package} migrate`.
|
|
107
102
|
|
|
108
103
|
To deploy, copy `dist/` to a machine with the same OS and CPU, then
|
|
@@ -115,7 +110,8 @@ module Cybertrain
|
|
|
115
110
|
<<~RUBY
|
|
116
111
|
# Application settings; see Cybertrain::Config for every option.
|
|
117
112
|
# Environment variables (PORT, CYBERTRAIN_ENV, CYBERTRAIN_DATABASE,
|
|
118
|
-
# CYBERTRAIN_SECRET_KEY_BASE
|
|
113
|
+
# CYBERTRAIN_SECRET_KEY_BASE, CYBERTRAIN_HOST, CYBERTRAIN_SESSION_SAME_SITE,
|
|
114
|
+
# CYBERTRAIN_SESSION_PARTITIONED) are read before this block runs.
|
|
119
115
|
Cybertrain.configure do |c|
|
|
120
116
|
# c.port = 3000
|
|
121
117
|
# c.workers = 1
|
|
@@ -326,7 +322,7 @@ module Cybertrain
|
|
|
326
322
|
def destroy
|
|
327
323
|
@#{s}.destroy
|
|
328
324
|
flash[:notice] = "#{human} was successfully destroyed."
|
|
329
|
-
redirect_to #{
|
|
325
|
+
redirect_to #{res.index_helper}_path, status: :see_other
|
|
330
326
|
end
|
|
331
327
|
|
|
332
328
|
private
|
|
@@ -371,7 +367,7 @@ module Cybertrain
|
|
|
371
367
|
end
|
|
372
368
|
buf << "<p>\n"
|
|
373
369
|
buf << " <%= link_to \"Edit this #{words}\", edit_#{s}_path(@#{s}) %> |\n"
|
|
374
|
-
buf << " <%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.
|
|
370
|
+
buf << " <%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.index_helper}_path %>\n"
|
|
375
371
|
buf << "</p>\n\n"
|
|
376
372
|
buf << "<%= button_to \"Destroy this #{words}\", #{s}_path(@#{s}), method: :delete %>\n"
|
|
377
373
|
buf
|
|
@@ -382,7 +378,7 @@ module Cybertrain
|
|
|
382
378
|
buf = +""
|
|
383
379
|
buf << "<h1>New #{s.tr("_", " ")}</h1>\n\n"
|
|
384
380
|
buf << "<%= render \"form\", #{s}: @#{s} %>\n\n"
|
|
385
|
-
buf << "<p><%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.
|
|
381
|
+
buf << "<p><%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.index_helper}_path %></p>\n"
|
|
386
382
|
buf
|
|
387
383
|
end
|
|
388
384
|
|
|
@@ -393,7 +389,7 @@ module Cybertrain
|
|
|
393
389
|
buf << "<%= render \"form\", #{s}: @#{s} %>\n\n"
|
|
394
390
|
buf << "<p>\n"
|
|
395
391
|
buf << " <%= link_to \"Show this #{s.tr("_", " ")}\", #{s}_path(@#{s}) %> |\n"
|
|
396
|
-
buf << " <%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.
|
|
392
|
+
buf << " <%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.index_helper}_path %>\n"
|
|
397
393
|
buf << "</p>\n"
|
|
398
394
|
buf
|
|
399
395
|
end
|