the_schema_is 0.0.3 → 0.0.4

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 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