the_schema_is 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91c0bfc087833138e49b3723c6c8c7682899473ac1569b7a98d73c4aa86f62e7
4
- data.tar.gz: c5e7980b3a600c1bfff051b719157c980cace74667db35517f225434e765e303
3
+ metadata.gz: 054c02711e49163e9600e925da6c9bab7007b276528472834e3383326284f5c9
4
+ data.tar.gz: 837d5d1f3910505a3ac0e2aa4ff7742ff960555ed0edd27dfa2a7f7b7f75d996
5
5
  SHA512:
6
- metadata.gz: 89cff82616f4fa927e93268347d44d2d335928073912c1c4dac3f8a309d0c3fe82922bf97c6975b54a978d6d6bd23704cb30f80d4cc919896185d0d6eb0d1f9d
7
- data.tar.gz: 0e6b141eb082561ea2939c131539e6d4ff58f47dd25d142f841b703ae6e3fe325ed10c4bc368a77b144c4cf817d4d7da8df361a6e877946d17a348f5253bac4f
6
+ metadata.gz: 2042bfe76d1813cc7ad043b4947fa885162e4630e27d4bdb30b5ff961de77ad9fd9e01ae36d348ddf25fa55d63d58157cb0256d1ebad80ec7fd9246368201d60
7
+ data.tar.gz: da567e5da9f013be952177ce79d2ba6f2ed1431f342723360f660b0dda909d02a120ad9bcfd6dfa5d9b039e74948f005655151280e10c1dd3509af27b6897d2e
data/Changelog.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # the-schema-is changes
2
2
 
3
+ ## 2021-09-15 - 0.0.4
4
+
5
+ * Get rid of [Fast](https://jonatas.github.io/fast/) dependency. It is cool, but we switched to use Rubocop's own `NodePattern` to lessen the dependency burden (Fast was depending on [astrolabe](https://github.com/yujinakayama/astrolabe) which wasn't updated in 6 years, locking parser dependency to old version and making Fast incompatible with newer Rubocop);
6
+ * Introduce mandatory table name in the `the_schema_is` DSL (and the `WrongTableName` cop to check it);
7
+ * Internally, change cop classes to comply to newer (> 1.0) Rubocop API.
8
+
3
9
  ## 2020-05-07 - 0.0.3
4
10
 
5
11
  First really working release.
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/the_schema_is.svg)](http://badge.fury.io/rb/the_schema_is)
4
4
  ![Build Status](https://github.com/zverok/the_schema_is/workflows/CI/badge.svg?branch=master)
5
5
 
6
- `the_schema_is` is a model schema annotation DSL in ActiveSupport.
6
+ `the_schema_is` is a model schema annotation DSL for ActiveSupport models, enforced by Rubocop. [Jump to detailed description →](#so-how-your-approach-is-different).
7
7
 
8
8
  ### Why annotate?
9
9
 
@@ -34,10 +34,10 @@ Annotate gem provides a very powerful and configurable CLI/rake task which allow
34
34
 
35
35
  It kinda achieves the goal, but in our experience, it also brings some problems:
36
36
 
37
- * annotation regeneration is disruptive, just replacing the whole block with a new one, which produces a lot of "false changes" (e.g. one field with a bit longer name was added → spacing of all fields were changed);
38
- * if on different developer's machines column order or defaults is different on dev. DB, annotate also decides to rewrite all the annotations, sometimes adding tens files "changed" to PR;
39
- * regeneration makes it hard to use schema annotation for commenting/explaining some fields: because regeneration will lose them, and because comments-between-comments will be hard to distinguish;
40
- * the syntax of annotations is kinda ad-hoc, which makes it harder to add them by hand, so regeneration becomes the _only_ way to add them.
37
+ * annotation **regeneration is disruptive**, just replacing the whole block with a new one, which produces a lot of "false changes" (e.g. one field with a bit longer name was added → spacing of all fields were changed);
38
+ * if on different developer's machines **column order or defaults is different** in dev. DB, annotate also decides to rewrite all the annotations, sometimes adding tens files "changed" to PR;
39
+ * regeneration makes it **hard to use schema annotation for commenting/explaining** some fields: because regeneration will lose them, and because comments-between-comments will be hard to distinguish;
40
+ * the **syntax of annotations is kinda ad-hoc**, which makes it harder to add them by hand, so regeneration becomes the _only_ way to add them.
41
41
 
42
42
  ### So, how your approach is different?..
43
43
 
@@ -88,7 +88,7 @@ Using existing Rubocop's infrastructure brings several great benefits:
88
88
 
89
89
  * you can include checking "if all annotations are actual" in your CI/pre-commit hooks easily;
90
90
  * you can preview problems found, and then fix them automatically (with `rubocop -a`) or manually however you see suitable;
91
- * the changes made with auto-fix is very local (just add/remove/change line related to relevant column), so your custom structuring, like separating groups of related columns with empty lines and comments, will be preserved;
91
+ * the changes made with auto-correct is very local (just add/remove/change line related to relevant column), so your custom structuring, like separating groups of related columns with empty lines and comments, will be preserved;
92
92
  * rubocop is easy to run on some sub-folder or one file, or files corresponding to some pattern; or exclude permanently for some file or folder.
93
93
 
94
94
  ### But what the block itself does?
@@ -110,11 +110,12 @@ The block isn't even evaluated at all (so potentially can contain any code, and
110
110
  - the_schema_is/cops
111
111
  ```
112
112
  3. Run `rubocop` and see what it now says about your models.
113
- 4. Now you can add schema definitions manually, or allow `rubocop --auto-fix` (or `-a`) to do its job! NB: you can always use `rubocop --auto-fix --only TheSchemaIs` to auto-fix ONLY this schema thing
113
+ 4. Now you can add schema definitions manually, or allow `rubocop --auto-correct` (or `-a`) to do its job! NB: you can always use `rubocop --auto-correct --only TheSchemaIs` to auto-correct ONLY this schema thing
114
114
 
115
115
  To make reporting cleaner, all cops are split into:
116
116
 
117
117
  * `Presence`
118
+ * `WrongTableName`
118
119
  * `MissingColumn`
119
120
  * `UnknownColumn`
120
121
  * `WrongColumnDefinition`
@@ -143,12 +144,12 @@ TheSchemaIs:
143
144
  BaseClass: OurOwnBase
144
145
  ```
145
146
 
146
- Note that Rubocop allows per-folder settings out of the box, which allows TheSchemaIs even at the tender version of 0.0.1, support complicated configurations with multiple databases and engines.
147
+ Note that Rubocop allows per-folder settings out of the box, which allows TheSchemaIs to support complicated configurations with multiple databases and engines.
147
148
 
148
149
  For example, consider your models are split into `app/models/users/` and `app/models/products` which are stored in the different databases, then you probably have different schemas and base classes for them. So, to configure it properly, you might want to do in `app/models/users/.rubocop.yml`:
149
150
 
150
151
  ```yaml
151
- # Don't forget this for all other cops to not be ignored
152
+ # Don't forget this for all other cop settings to not be ignored
152
153
  inherit_from: ../../../.rubocop.yml
153
154
 
154
155
  TheSchemaIs:
@@ -158,13 +159,18 @@ TheSchemaIs:
158
159
 
159
160
  ## Some Q&A
160
161
 
161
- * **Q: It doesn't check the actual DB?** A: No, it does not! At the current moment, our belief is that in a healthy Rails codebase `schema.rb` is always corresponding to DB state, so checking against it is enough. This approach makes the tooling much easier (with existing Rubocop's ecosystem of parsers/offenses/configurations).
162
- * **Q: What if I don't use Rubocop?** A: You may want to try, at least? Do you know that you may disable or configure most of its checks to your liking? And auto-correct any code to your preferences?.. Or automatically create "TODO" config-file (which disables all the cops currently raising offenses, and allows to review them and later setup one-by-one)?.. It is much more than "linter making your code to complain about some rigid style guide".
163
- * **Q: Cool, but I still don't want to.** ...OK, then you can disable all cops _except_ for `TheSchemaIs` namespace :)
164
- * **How do I annotate my fabrics, model specs, routes, controllers, ... (which `annotate` allows)?** You don't. The same way you don't copy-paste the whole definition of the class into spec file which tests this class: Definition is in one place, tests and other code using this definition is another. DRY!
165
- * **Rubocop is unhappy with the code `TheSchemaIs` generated**. There are two known things in generated `the_schema_is` blocks that Rubocop may complain about:
166
- * Usage of double quotes for strings, if your config insists on single quotes: that's because we just copy code objects from `schema.rb`. Rubocop's auto-fix will fix it :) (Even in one run: "fixing TheSchemaIs, then fixing quotes");
167
- * Too long blocks (if you have tables with dozens of columns, God forbid... as we do). It can be fixed by adding this to `.rubocop.yml`:
162
+ * **Q: It doesn't check the actual DB?**
163
+ * A: No, it does not! At the current moment, our belief is that in a healthy Rails codebase `schema.rb` is always corresponding to DB state, so checking against it is enough. This approach makes the tooling much easier (with existing Rubocop's ecosystem of parsers/offenses/configurations).
164
+ * **Q: What if I don't use Rubocop?**
165
+ * A: You may want to try, at least? Do you know that you may disable or configure most of its checks to your liking? And auto-correct any code to your preferences?.. Or automatically create "TODO" config-file (which disables all the cops currently raising offenses, and allows to review them and later setup one-by-one)?.. It is much more than "linter making your code to complain about some rigid style guide".
166
+ * **Q: Cool, but I still don't want to.**
167
+ * A: ...OK, then you can disable all cops _except_ for `TheSchemaIs` namespace :)
168
+ * **How do I annotate my fabrics, model specs, routes, controllers, ... (which `annotate` allows)?**
169
+ * A: You don't. The same way you don't copy-paste the whole definition of the class into spec file which tests this class: Definition is in one place, tests and other code using this definition is another. DRY!
170
+ * **Rubocop is unhappy with the code `TheSchemaIs` generated**.
171
+ * A: There are two known things in generated `the_schema_is` blocks that Rubocop may complain about:
172
+ * Usage of double quotes for strings, if your config insists on single quotes: that's because we just copy code objects from `schema.rb`. Rubocop's auto-correct will fix it :) (Even in one run: "fixing TheSchemaIs, then fixing quotes");
173
+ * Too long blocks (if you have tables with dozens of columns, God forbid... as we do). It can be fixed by adding this to `.rubocop.yml`:
168
174
  ```yaml
169
175
  Metrics/BlockLength:
170
176
  ExcludedMethods:
data/config/defaults.yml CHANGED
@@ -11,6 +11,10 @@ TheSchemaIs/Presence:
11
11
  Description: "Check presence of the_schema_is statement in ActiveRecord models"
12
12
  Enabled: true
13
13
 
14
+ TheSchemaIs/WrongTableName:
15
+ Description: "Check table name define by the_schema_is statement against one in schema.rb"
16
+ Enabled: true
17
+
14
18
  TheSchemaIs/MissingColumn:
15
19
  Description: "Check columns missing in the_schema_is definition (but present in schema.rb)"
16
20
  Enabled: true
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'memoist'
4
4
  require 'rubocop'
5
- require 'fast'
6
5
  require 'backports/latest'
7
6
 
8
7
  require_relative 'cops/node_util'
@@ -25,9 +24,10 @@ module TheSchemaIs
25
24
  extend Memoist
26
25
 
27
26
  def self.included(cls)
28
- cls.define_singleton_method(:badge) {
27
+ cls.define_singleton_method(:badge) do
29
28
  RuboCop::Cop::Badge.for("TheSchemaIs::#{name.split('::').last}")
30
- }
29
+ end
30
+ super
31
31
  end
32
32
 
33
33
  def on_class(node)
@@ -35,7 +35,7 @@ module TheSchemaIs
35
35
  base_classes: cop_config.fetch('BaseClass'),
36
36
  table_prefix: cop_config['TablePrefix']) or return
37
37
 
38
- validate
38
+ register_offense(node)
39
39
  end
40
40
 
41
41
  # We need this method to tell Rubocop that EVEN if app/models/user.rb haven't changed, and
@@ -50,7 +50,7 @@ module TheSchemaIs
50
50
 
51
51
  attr_reader :model
52
52
 
53
- def validate
53
+ def register_offense(node)
54
54
  fail NotImplementedError
55
55
  end
56
56
 
@@ -63,34 +63,34 @@ module TheSchemaIs
63
63
  end
64
64
 
65
65
  memoize def model_columns
66
- statements = model.schema.ffast('(block (send nil :the_schema_is) (args) $...)').last.last
66
+ statements = model.schema.ast_search('(block (send nil? :the_schema_is _?) _ $...)')
67
+ .last.last
67
68
 
68
69
  Cops::Parser.columns(statements).to_h { |col| [col.name, col] }
69
70
  end
70
71
 
71
72
  memoize def schema_columns
72
- # FIXME: should be already done in Parser.schema, probably!
73
- statements = schema.ffast('(block (send nil :create_table) (args) $...)').last.last
74
-
75
- Cops::Parser.columns(statements).to_h { |col| [col.name, col] }
73
+ Cops::Parser.columns(schema).to_h { |col| [col.name, col] }
76
74
  end
77
75
  end
78
76
 
79
- class Presence < RuboCop::Cop::Cop
77
+ class Presence < RuboCop::Cop::Base
80
78
  include Common
79
+ extend RuboCop::Cop::AutoCorrector
81
80
 
82
81
  MSG_NO_MODEL_SCHEMA = 'The schema is not specified in the model (use the_schema_is statement)'
83
82
  MSG_NO_DB_SCHEMA = 'Table "%s" is not defined in %s'
84
83
 
85
- def autocorrect(node)
86
- return unless schema
84
+ private
87
85
 
88
- m = model
86
+ def register_offense(node)
87
+ schema.nil? and
88
+ add_offense(model.source, message: MSG_NO_DB_SCHEMA % [model.table_name, schema_path])
89
89
 
90
- lambda do |corrector|
90
+ model.schema.nil? and add_offense(model.source, message: MSG_NO_MODEL_SCHEMA) do |corrector|
91
91
  indent = node.loc.expression.column + 2
92
92
  code = [
93
- "the_schema_is #{m.table_name.to_s.inspect} do |t|",
93
+ "the_schema_is #{model.table_name.to_s.inspect} do |t|",
94
94
  *schema_columns.map { |_, col| " #{col.source.loc.expression.source}" },
95
95
  'end'
96
96
  ].map { |s| ' ' * indent + s }.join("\n").then { |s| "\n#{s}\n" }
@@ -99,52 +99,71 @@ module TheSchemaIs
99
99
  corrector.insert_after(node.children[1].loc.expression, code)
100
100
  end
101
101
  end
102
+ end
103
+
104
+ class WrongTableName < RuboCop::Cop::Base
105
+ include Common
106
+ extend RuboCop::Cop::AutoCorrector
107
+
108
+ MSG_WRONG_TABLE_NAME = 'The real table name should be %p'
109
+ MSG_NO_TABLE_NAME = 'Table name is not specified'
102
110
 
103
111
  private
104
112
 
105
- def validate
106
- schema.nil? and
107
- add_offense(model.source, message: MSG_NO_DB_SCHEMA % [model.table_name, schema_path])
108
- model.schema.nil? and add_offense(model.source, message: MSG_NO_MODEL_SCHEMA)
113
+ def register_offense(_node)
114
+ return if model.schema.nil? || schema.nil?
115
+
116
+ pp
117
+
118
+ if model.table_name_node.nil?
119
+ add_offense(model.schema, message: MSG_NO_TABLE_NAME) do |corrector|
120
+ corrector.insert_after(model.schema.children[0].loc.expression, " #{model.table_name.to_s.inspect}")
121
+ end
122
+ elsif model.table_name_node.children[0] != model.table_name
123
+ add_offense(model.table_name_node,
124
+ message: MSG_WRONG_TABLE_NAME % model.table_name) do |corrector|
125
+ corrector.replace(model.table_name_node.loc.expression, model.table_name.to_s.inspect)
126
+ end
127
+ end
109
128
  end
110
129
  end
111
130
 
112
- class MissingColumn < RuboCop::Cop::Cop
131
+ class MissingColumn < RuboCop::Cop::Base
113
132
  include Common
133
+ extend RuboCop::Cop::AutoCorrector
114
134
 
115
135
  MSG = 'Column "%s" definition is missing'
116
136
 
117
- def autocorrect(_node)
118
- lambda do |corrector|
119
- missing_columns.each { |name, col|
120
- prev_statement = model_columns
121
- .slice(*schema_columns.keys[0...schema_columns.keys.index(name)])
122
- .values.last&.source
123
- if prev_statement
124
- indent = prev_statement.loc.expression.column
125
- corrector.insert_after(
126
- prev_statement.loc.expression,
127
- "\n#{' ' * indent}#{col.source.loc.expression.source}"
128
- )
129
- else
130
- indent = model.schema.loc.expression.column + 2
131
- corrector.insert_after(
132
- # of "the_schema_is do |t|" -- children[1] is "|t|""
133
- model.schema.children[1].loc.expression,
134
- "\n#{' ' * indent}#{col.source.loc.expression.source}"
135
- )
136
- end
137
- }
138
- end
139
- end
140
-
141
137
  private
142
138
 
143
- def validate
139
+ def register_offense(_node)
144
140
  return if model.schema.nil? || schema.nil?
145
141
 
146
- missing_columns.each do |_, col|
147
- add_offense(model.schema, message: MSG % col.name)
142
+ missing_columns.each do |name, col|
143
+ add_offense(model.schema, message: MSG % col.name) do |corrector|
144
+ insert_column(corrector, name, col)
145
+ end
146
+ end
147
+ end
148
+
149
+ def insert_column(corrector, name, col)
150
+ prev_statement = model_columns
151
+ .slice(*schema_columns.keys[0...schema_columns.keys.index(name)])
152
+ .values.last&.source
153
+
154
+ if prev_statement
155
+ indent = prev_statement.loc.expression.column
156
+ corrector.insert_after(
157
+ prev_statement.loc.expression,
158
+ "\n#{' ' * indent}#{col.source.loc.expression.source}"
159
+ )
160
+ else
161
+ indent = model.schema.loc.expression.column + 2
162
+ corrector.insert_after(
163
+ # of "the_schema_is do |t|" -- children[1] is "|t|""
164
+ model.schema.children[1].loc.expression,
165
+ "\n#{' ' * indent}#{col.source.loc.expression.source}"
166
+ )
148
167
  end
149
168
  end
150
169
 
@@ -153,14 +172,19 @@ module TheSchemaIs
153
172
  end
154
173
  end
155
174
 
156
- class UnknownColumn < RuboCop::Cop::Cop
175
+ class UnknownColumn < RuboCop::Cop::Base
157
176
  include Common
177
+ extend RuboCop::Cop::AutoCorrector
158
178
 
159
179
  MSG = 'Uknown column "%s"'
160
180
 
161
- def autocorrect(_node)
162
- lambda do |corrector|
163
- extra_columns.each do |_, col|
181
+ private
182
+
183
+ def register_offense(_node)
184
+ return if model.schema.nil? || schema.nil?
185
+
186
+ extra_columns.each do |_, col|
187
+ add_offense(col.source, message: MSG % col.name) do |corrector|
164
188
  src_range = col.source.loc.expression
165
189
  end_pos = col.source.next_sibling.then { |n|
166
190
  n ? n.loc.expression.begin_pos - 2 : col.source.find_parent(:block).loc.end.begin_pos
@@ -172,42 +196,27 @@ module TheSchemaIs
172
196
  end
173
197
  end
174
198
 
175
- private
176
-
177
- def validate
178
- return if model.schema.nil? || schema.nil?
179
-
180
- extra_columns.each do |_, col|
181
- add_offense(col.source, message: MSG % col.name)
182
- end
183
- end
184
-
185
199
  def extra_columns
186
200
  model_columns.reject { |name,| schema_columns.keys.include?(name) }
187
201
  end
188
202
  end
189
203
 
190
- class WrongColumnDefinition < RuboCop::Cop::Cop
204
+ class WrongColumnDefinition < RuboCop::Cop::Base
191
205
  include Common
206
+ extend RuboCop::Cop::AutoCorrector
192
207
 
193
208
  MSG = 'Wrong column definition: expected `%s`'
194
209
 
195
- def autocorrect(_node)
196
- lambda do |corrector|
197
- wrong_columns.each do |mcol, scol|
198
- corrector.replace(mcol.source.loc.expression, scol.source.loc.expression.source)
199
- end
200
- end
201
- end
202
-
203
210
  private
204
211
 
205
- def validate
212
+ def register_offense(_node)
206
213
  return if model.schema.nil? || schema.nil?
207
214
 
208
215
  wrong_columns
209
216
  .each do |mcol, scol|
210
- add_offense(mcol.source, message: MSG % scol.source.loc.expression.source)
217
+ add_offense(mcol.source, message: MSG % scol.source.loc.expression.source) do |corrector|
218
+ corrector.replace(mcol.source.loc.expression, scol.source.loc.expression.source)
219
+ end
211
220
  end
212
221
  end
213
222
 
@@ -215,7 +224,9 @@ module TheSchemaIs
215
224
  model_columns
216
225
  .map { |name, col| [col, schema_columns[name]] }
217
226
  .reject { |mcol, scol|
218
- mcol.type == scol.type && mcol.definition_source == scol.definition_source
227
+ # When column is not in schema, we shouldn't try to check it: UnknownColumn cop will
228
+ # handle.
229
+ !scol || mcol.type == scol.type && mcol.definition_source == scol.definition_source
219
230
  }
220
231
  end
221
232
  end
@@ -1,15 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'singleton'
4
+
3
5
  module TheSchemaIs
4
6
  module Cops
7
+ # This module mitigates usage of RuboCop's NodePattern in a more flexible manner. NodePattern
8
+ # (targeting RuboCop's goals) was only available as a metaprogramming macro, requiring to
9
+ # `def_node_search :some_method, pattern` before the pattern can be used, while we wanted to
10
+ # just do `ast.ast_search(some_pattern)`; so this method defines a method for each used pattern
11
+ # on the fly and hides this discrepancy. Used by NodeRefinements (mixed into parser's node) to
12
+ # provide `Node#ast_search` and `Node#ast_match`.
13
+ class Patterns
14
+ include Singleton
15
+ extend RuboCop::AST::NodePattern::Macros
16
+
17
+ class << self
18
+ extend Memoist
19
+
20
+ def search(pattern, node)
21
+ search_methods[pattern].then { |m| instance.send(m, node) }
22
+ end
23
+
24
+ def match(pattern, node)
25
+ match_methods[pattern].then { |m| instance.send(m, node) }
26
+ end
27
+
28
+ private
29
+
30
+ memoize def search_methods
31
+ Hash.new { |h, pattern|
32
+ method_name = "search_#{h.size}"
33
+ def_node_search method_name, pattern
34
+ h[pattern] = method_name
35
+ }
36
+ end
37
+
38
+ memoize def match_methods
39
+ Hash.new { |h, pattern|
40
+ method_name = "match_#{h.size}"
41
+ def_node_search method_name, pattern
42
+ h[pattern] = method_name
43
+ }
44
+ end
45
+ end
46
+ end
47
+
5
48
  module NodeRefinements
6
49
  refine ::Parser::AST::Node do
7
- def ffast(expr)
8
- Fast.search(expr, self)
50
+ def ast_search(expr)
51
+ Patterns.search(expr, self).to_a
9
52
  end
10
53
 
11
- def ffast_match?(expr)
12
- Fast.match?(expr, self)
54
+ def ast_match(expr)
55
+ Patterns.match(expr, self).to_a.first
13
56
  end
14
57
 
15
58
  def arraify
@@ -17,64 +17,68 @@ module TheSchemaIs
17
17
 
18
18
  COLUMN_DEFS = (STANDARD_COLUMN_TYPES + POSTGRES_COLUMN_TYPES + %i[column]).freeze
19
19
 
20
- Model = Struct.new(:class_name, :table_name, :source, :schema, keyword_init: true)
20
+ Model = Struct.new(:class_name, :table_name, :source, :schema, :table_name_node,
21
+ keyword_init: true)
21
22
 
22
23
  Column = Struct.new(:name, :type, :definition, :source, keyword_init: true) do
23
24
  def definition_source
24
25
  return unless definition
25
26
 
26
- eval('{' + definition.loc.expression.source + '}') # rubocop:disable Security/Eval
27
+ eval('{' + definition.loc.expression.source + '}') # rubocop:disable Security/Eval,Style/StringConcatenation
27
28
  end
28
29
  end
29
30
 
31
+ def self.parse(code)
32
+ # TODO: Some kind of "current version" (ask Rubocop!)
33
+ RuboCop::AST::ProcessedSource.new(code, 2.7).ast
34
+ end
35
+
30
36
  def self.schema(path)
31
- ast = Fast.ast(File.read(path))
32
-
33
- content = Fast.search(
34
- '(block (send (const (const nil :ActiveRecord) :Schema) :define) _ $_)',
35
- ast
36
- ).last.first
37
- Fast.search('(block (send nil :create_table (str $_)) _ _)', content)
38
- .each_slice(2).to_h { |t, name| [Array(name).first, t] } # FIXME: Why it sometimes makes arrays, and sometimes not?..
37
+ ast = parse(File.read(path))
38
+
39
+ ast.ast_search('(block (send nil? :create_table (str $_) _) _ $_)').to_h
39
40
  end
40
41
 
41
42
  def self.model(ast, base_classes: %w[ActiveRecord::Base ApplicationRecord], table_prefix: nil)
42
43
  base = base_classes_query(base_classes)
43
- ast.ffast("(class $_ #{base})").each_slice(2)
44
+ ast.ast_search("$(class $_ #{base} _)")
44
45
  .map { |node, name| node2model(name, node, table_prefix.to_s) }
45
46
  .compact
46
47
  .first
47
48
  end
48
49
 
49
- def self.node2model(name_node, definition_node, table_prefix)
50
- return if definition_node.ffast('(send self abstract_class= true)').any?
50
+ def self.node2model(name_node, definition_node, table_prefix) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
51
+ return if definition_node.ast_search('(send self :abstract_class= true)')&.any?
51
52
 
52
53
  # If all children are classes/modules -- model is here only as a namespace, shouldn't be
53
54
  # parsed/have the_schema_is
54
- return if definition_node
55
- .children[2]&.arraify&.all? { |n| %i[class module].include?(n.type) }
55
+ if definition_node.children&.dig(2)&.arraify&.all? { |n| %i[class module].include?(n.type) }
56
+ return
57
+ end
56
58
 
57
- class_name = name_node.first.loc.expression.source
59
+ class_name = name_node.loc.expression.source
58
60
 
59
- schema = definition_node.ffast('$(block (send nil :the_schema_is) _ ...')&.last
61
+ schema_node, name_node =
62
+ definition_node.ast_search('$(block (send nil? :the_schema_is $_?) _ ...)')&.last
60
63
 
61
64
  # TODO: https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-table_name
62
65
  # * consider table_prefix/table_suffix settings
63
66
  # * also, consider engines!
64
- table_name = definition_node.ffast('(send self table_name= (str $_)')&.last
67
+ table_name = definition_node.ast_search('(send self :table_name= (str $_))')&.last
65
68
 
66
69
  Model.new(
67
70
  class_name: class_name,
68
71
  table_name: table_name ||
69
72
  table_prefix.+(ActiveSupport::Inflector.tableize(class_name)),
70
73
  source: definition_node,
71
- schema: schema
74
+ schema: schema_node,
75
+ table_name_node: name_node&.first
72
76
  )
73
77
  end
74
78
 
75
79
  def self.base_classes_query(classes)
76
80
  classes
77
- .map { |cls| cls.split('::').inject('nil') { |res, str| "(const #{res} :#{str})" } }
81
+ .map { |cls| cls.split('::').inject('nil?') { |res, str| "(const #{res} :#{str})" } }
78
82
  .join(' ')
79
83
  .then { |str| "{#{str}}" }
80
84
  end
@@ -83,10 +87,10 @@ module TheSchemaIs
83
87
  ast.arraify.map { |node|
84
88
  # FIXME: Of course it should be easier to say "optional additional params"
85
89
  if (type, name, defs =
86
- node.ffast_match?('(send {(send nil t) (lvar t)} $_ (str $_) $...'))
90
+ node.ast_match('(send {(send nil? :t) (lvar :t)} $_ (str $_) $_)'))
87
91
  Column.new(name: name, type: type, definition: defs, source: node) \
88
92
  if COLUMN_DEFS.include?(type)
89
- elsif (type, name = Fast.match?('(send {(send nil t) (lvar t)} $_ (str $_)', node))
93
+ elsif (type, name = node.ast_match('(send {(send nil? :t) (lvar :t)} $_ (str $_))'))
90
94
  Column.new(name: name, type: type, source: node) if COLUMN_DEFS.include?(type)
91
95
  end
92
96
  }.compact
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: the_schema_is
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Victor Shepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-12 00:00:00.000000000 Z
11
+ date: 2021-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: backports
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: 1.0.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: 1.0.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: activesupport
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: ffast
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: 0.1.8
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: 0.1.8
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: memoist
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -151,14 +137,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
151
137
  requirements:
152
138
  - - ">="
153
139
  - !ruby/object:Gem::Version
154
- version: 2.6.0
140
+ version: 2.5.0
155
141
  required_rubygems_version: !ruby/object:Gem::Requirement
156
142
  requirements:
157
143
  - - ">="
158
144
  - !ruby/object:Gem::Version
159
145
  version: '0'
160
146
  requirements: []
161
- rubygems_version: 3.0.3
147
+ rubygems_version: 3.1.6
162
148
  signing_key:
163
149
  specification_version: 4
164
150
  summary: ActiveRecord model annotations done right