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.
@@ -0,0 +1,171 @@
1
+ # Cybertrain::Ident -- the one place that says what a column (or scaffold
2
+ # field) may be called. Used by the generator (ModelsEmitter), the ORM
3
+ # (Relation#order) and the CLI (`cybertrain generate scaffold`), so a name
4
+ # the scaffold accepts is a name the generator and the models accept, and
5
+ # the failure comes from the command that would write the files.
6
+ #
7
+ # Plain Ruby with no requires: the CLI gem ships this file and runs it under
8
+ # CRuby, the framework compiles it under Spinel (no Regexp, NOTES rule 1).
9
+ module Cybertrain
10
+ module Ident
11
+ # Column names whose plain reader the generated class cannot host: it
12
+ # would shadow a method the generated class or the framework calls on the
13
+ # record. The generator renames them, so an existing schema keeps
14
+ # generating: the column reads as `<column>_column`
15
+ # (ModelsEmitter.reader_name). The scaffold refuses them: its views and
16
+ # controller call the reader by the field's name, which a renamed reader
17
+ # would break. `id` is deliberately
18
+ # absent: the primary key is Model#id. Names that end in `?` or `!`
19
+ # cannot be columns at all (column? rejects them), so `valid?`,
20
+ # `persisted?`, `is_a?` need no entry; keywords (`class`) live in
21
+ # RUBY_KEYWORDS only, and the generator renames those the same way
22
+ # (`class_column`). Class methods (`table_name`, `column_names`,
23
+ # `from_row`) need no entry either: the framework calls them on the
24
+ # class (`self.class.table_name`, `Post.from_row`), which a column
25
+ # reader on the instance does not shadow, so a schema with a
26
+ # `table_name` column keeps generating. script/check-reserved-names
27
+ # keeps the Model and Object groups here and in SHADOWING_COLUMN_NAMES
28
+ # in step with the code (it skips `def self.` methods).
29
+ RESERVED_COLUMN_NAMES = [
30
+ # Cybertrain::Model: its methods and the ivars behind them (`errors`,
31
+ # `persisted` -> @errors, @persisted)
32
+ "errors", "persisted", "attributes", "save", "update", "destroy",
33
+ "reload", "model_name", "to_json", "as_json", "to_param", "to_row",
34
+ "load_row", "set_id", "run_callbacks", "insert_row", "update_row",
35
+ "read_attribute", "write_attribute", "assign_attributes",
36
+ "read_association", "call_view_method", "initialize",
37
+ # Object methods Ruby or the framework calls on a record without being
38
+ # asked: string interpolation and templates (to_s), error messages
39
+ # (inspect), Hash keys (hash)
40
+ "hash", "inspect", "to_s",
41
+ # Implicit-conversion and dispatch hooks Ruby calls on its own (none is
42
+ # defined on Object, so no reflection finds them): an arity-0 reader
43
+ # returning a String breaks `puts post` / `[post].flatten` / a splat
44
+ # (to_ary), string coercion (to_str), keyword splats (to_hash), `&post`
45
+ # (to_proc), Integer coercion (to_int), and turns every NoMethodError
46
+ # into an ArgumentError (method_missing). `respond_to_missing?` ends in
47
+ # `?` and needs no entry.
48
+ "to_ary", "to_str", "to_hash", "to_proc", "to_int", "to_a", "to_h",
49
+ "to_sym", "to_io", "to_path", "to_regexp", "coerce", "method_missing",
50
+ # Hooks Ruby itself calls WITH an argument, which it finds whether they
51
+ # are public or private (so Object.instance_methods never lists the
52
+ # private ones): `record.dup` / `clone` call initialize_copy(orig)
53
+ # (initialize_dup / initialize_clone on the way), and defining, removing
54
+ # or undefining a singleton method (`def record.x`) calls
55
+ # singleton_method_added / _removed / _undefined. A zero-arity column
56
+ # reader in their place raises ArgumentError at that call. These are
57
+ # reserved, not merely shadowing: the problem is Ruby's call, not a
58
+ # method the caller could have wanted. script/check-reserved-names
59
+ # fails when one of Ruby's private hooks is missing here.
60
+ "initialize_copy", "initialize_dup", "initialize_clone",
61
+ "singleton_method_added", "singleton_method_removed",
62
+ "singleton_method_undefined",
63
+ # The one Kernel method the model calls on implicit self (Model#save!
64
+ # and #reload would dispatch `raise RecordInvalid, ...` to the
65
+ # zero-arity reader), and its alias. No other private Kernel method is
66
+ # reserved: `open`, `format`, `load`, `print`, `select`, `test` are
67
+ # ordinary column names and nothing in the generated class calls them.
68
+ "raise", "fail"
69
+ ]
70
+
71
+ # The rest of Object's public instance methods: a column so named shadows
72
+ # them on its model (`record.display`, `record.tap` are the column), but
73
+ # neither the generated class nor the framework calls any of them on a
74
+ # record, so the generator accepts the column and writes a note into the
75
+ # generated file instead of refusing an existing schema. The scaffold
76
+ # accepts them the same way, with a note on stdout (Scaffold.shadow_notes):
77
+ # `payment.method` is the field, which its views and controller call.
78
+ SHADOWING_COLUMN_NAMES = [
79
+ "object_id", "__id__", "send", "__send__", "public_send", "freeze",
80
+ "display", "method", "methods", "public_method", "public_methods",
81
+ "private_methods", "protected_methods", "singleton_class",
82
+ "singleton_method", "singleton_methods", "define_singleton_method",
83
+ "remove_instance_variable", "instance_variable_get",
84
+ "instance_variable_set", "instance_variables", "instance_eval",
85
+ "instance_exec", "dup", "clone", "tap", "yield_self", "itself",
86
+ "extend", "enum_for", "to_enum"
87
+ ]
88
+
89
+ # Ruby keywords a column could be spelled like: `@end` / `def class` are
90
+ # not shapes worth supporting. The generator reads such a column as
91
+ # `<column>_column` (ModelsEmitter.reader_name), like a reserved name;
92
+ # the scaffold refuses them (its views call the reader by name). The
93
+ # uppercase ones are reachable since column? accepts capitals. `defined`
94
+ # is not here: the keyword is `defined?`, which column? rejects, and
95
+ # `defined` is an ordinary method name (`attr_accessor :defined` works).
96
+ RUBY_KEYWORDS = [
97
+ "alias", "and", "begin", "break", "case", "class", "def", "do",
98
+ "else", "elsif", "end", "ensure", "false", "for", "if", "in",
99
+ "module", "next", "nil", "not", "or", "redo", "rescue", "retry",
100
+ "return", "self", "super", "then", "true", "undef", "unless", "until",
101
+ "when", "while", "yield",
102
+ "BEGIN", "END", "__ENCODING__", "__FILE__", "__LINE__"
103
+ ]
104
+
105
+ # /\A[A-Za-z_][A-Za-z0-9_]*\z/ spelled out: what Ruby needs of a method
106
+ # name, so a schema with camelCase or mixed-case columns (`createdAt`,
107
+ # `userId`, a legacy SQLite table) generates under its own name, not a
108
+ # renamed one; `attr_accessor :createdAt` and `attr_accessor :Title` both
109
+ # compile and dispatch under Spinel (probed on 2026.09.12). The
110
+ # scaffold, which chooses new names, keeps the stricter snake_case rule
111
+ # (CLI::Templates.identifier?). A flag rather than a `return` inside
112
+ # the block, like Templates.identifier?: a `return` from an each_char
113
+ # block does leave the method under Spinel (probed), but the flag needs
114
+ # no such guarantee.
115
+ def self.column?(name)
116
+ return false if name.empty?
117
+
118
+ ok = true
119
+ first = true
120
+ name.each_char do |ch|
121
+ letter = (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z")
122
+ digit = ch >= "0" && ch <= "9"
123
+ ok = false unless letter || ch == "_" || (digit && !first)
124
+ first = false
125
+ end
126
+ ok
127
+ end
128
+
129
+ # The snake_case subset of column?: lowercase only, and a leading `_`
130
+ # only when allowed. CLI::Templates.identifier? (app, resource and field
131
+ # names: letter-first) and Template::Lexer.identifier? (template locals
132
+ # and block parameters: `_x` allowed) are both this predicate, so the two
133
+ # cannot drift. A positional default rather than a keyword argument: this
134
+ # file compiles under Spinel and keeps to the shapes it already uses.
135
+ def self.snake_case?(word, leading_underscore = true)
136
+ column?(word) && word == word.downcase && (leading_underscore || !word.start_with?("_"))
137
+ end
138
+
139
+ def self.keyword?(name)
140
+ RUBY_KEYWORDS.include?(name)
141
+ end
142
+
143
+ def self.reserved_column?(name)
144
+ RESERVED_COLUMN_NAMES.include?(name)
145
+ end
146
+
147
+ def self.shadowing_column?(name)
148
+ SHADOWING_COLUMN_NAMES.include?(name)
149
+ end
150
+
151
+ # The one definition of "a name the scaffold must not invent": "" when
152
+ # `name` may be chosen, otherwise why not -- "keyword" (RUBY_KEYWORDS) or
153
+ # "reserved" (RESERVED_COLUMN_NAMES): the two kinds the generator RENAMES,
154
+ # which the scaffold cannot accept because its views and controller call
155
+ # the reader by the field's name. A SHADOWING_COLUMN_NAMES entry is not
156
+ # refused: the generator keeps its name with a note, and so does the
157
+ # scaffold (shadowing_column?). The scaffold asks here at every place it
158
+ # takes a name from the user (field column, references reader, resource
159
+ # singular) and raises the message for the reason, so a new list of names
160
+ # is added to this method once, not to each call site. A String, not nil
161
+ # or a Symbol, so the return type is the same on every path (NOTES rules
162
+ # 10/34); a keyword that is also listed in a column array reports
163
+ # "keyword" first.
164
+ def self.unusable_reason(name)
165
+ return "keyword" if keyword?(name)
166
+ return "reserved" if reserved_column?(name)
167
+
168
+ ""
169
+ end
170
+ end
171
+ end
@@ -1,8 +1,12 @@
1
1
  module Cybertrain
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.1"
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
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.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Saeki Mototsune
@@ -12,20 +12,25 @@ 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`, `cybertrain generate scaffold`);
16
- building and running an application needs Spinel's `spin` on PATH.
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: []
20
22
  extra_rdoc_files: []
21
23
  files:
22
24
  - README.md
25
+ - cybertrain/app_name.rb
23
26
  - cybertrain/cli.rb
24
27
  - cybertrain/cli/build.rb
25
28
  - cybertrain/cli/new_app.rb
26
29
  - cybertrain/cli/scaffold.rb
27
30
  - cybertrain/cli/templates.rb
31
+ - cybertrain/cli/toolchain.rb
28
32
  - cybertrain/generator/inflector.rb
33
+ - cybertrain/ident.rb
29
34
  - cybertrain/version.rb
30
35
  - exe/cybertrain
31
36
  homepage: https://github.com/saeki-mototsune/cybertrain