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 +4 -4
- data/Changelog.md +6 -0
- data/README.md +22 -16
- data/config/defaults.yml +4 -0
- data/lib/the_schema_is/cops.rb +85 -74
- data/lib/the_schema_is/cops/node_util.rb +47 -4
- data/lib/the_schema_is/cops/parser.rb +26 -22
- metadata +6 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 054c02711e49163e9600e925da6c9bab7007b276528472834e3383326284f5c9
|
4
|
+
data.tar.gz: 837d5d1f3910505a3ac0e2aa4ff7742ff960555ed0edd27dfa2a7f7b7f75d996
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
38
|
-
* if on different developer's machines column order or defaults is different
|
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
|
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-
|
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-
|
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
|
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
|
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?**
|
162
|
-
*
|
163
|
-
* **Q:
|
164
|
-
*
|
165
|
-
* **
|
166
|
-
*
|
167
|
-
|
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
|
data/lib/the_schema_is/cops.rb
CHANGED
@@ -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
|
-
|
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
|
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.
|
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
|
-
|
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::
|
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
|
-
|
86
|
-
return unless schema
|
84
|
+
private
|
87
85
|
|
88
|
-
|
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
|
-
|
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 #{
|
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
|
106
|
-
schema.nil?
|
107
|
-
|
108
|
-
|
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::
|
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
|
139
|
+
def register_offense(_node)
|
144
140
|
return if model.schema.nil? || schema.nil?
|
145
141
|
|
146
|
-
missing_columns.each do |
|
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::
|
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
|
-
|
162
|
-
|
163
|
-
|
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::
|
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
|
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
|
-
|
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
|
8
|
-
|
50
|
+
def ast_search(expr)
|
51
|
+
Patterns.search(expr, self).to_a
|
9
52
|
end
|
10
53
|
|
11
|
-
def
|
12
|
-
|
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,
|
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 =
|
32
|
-
|
33
|
-
|
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.
|
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.
|
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
|
-
|
55
|
-
|
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.
|
59
|
+
class_name = name_node.loc.expression.source
|
58
60
|
|
59
|
-
|
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.
|
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:
|
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.
|
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 =
|
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.
|
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:
|
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:
|
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:
|
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.
|
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.
|
147
|
+
rubygems_version: 3.1.6
|
162
148
|
signing_key:
|
163
149
|
specification_version: 4
|
164
150
|
summary: ActiveRecord model annotations done right
|