generated_schema_validations 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +36 -4
- data/lib/generated_schema_validations/capistrano_tasks.rb +5 -5
- data/lib/generated_schema_validations/dump_checker.rb +4 -15
- data/lib/generated_schema_validations/dumper.rb +34 -10
- data/lib/generated_schema_validations/table.rb +108 -13
- data/lib/generated_schema_validations/tasks.rb +3 -0
- data/lib/generated_schema_validations/template.rb +6 -39
- data/lib/generated_schema_validations/validators/date_in_db_range_validator.rb +11 -0
- data/lib/generated_schema_validations/validators/date_time_in_db_range_validator.rb +11 -0
- data/lib/generated_schema_validations/validators/not_null_validator.rb +8 -0
- data/lib/generated_schema_validations/validators/time_in_db_range_validator.rb +17 -0
- metadata +51 -12
- data/.gitignore +0 -12
- data/.rspec +0 -3
- data/Gemfile +0 -9
- data/Gemfile.lock +0 -34
- data/Rakefile +0 -8
- data/generated_schema_validations.gemspec +0 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d49e52636e38031fc0717496547eebc4d6f92c04512830dcc5095b2c75f5215a
|
|
4
|
+
data.tar.gz: a2f01799b411e938aa8a79d6259353460e6d1180c3c5a86d42cb790cbc457620
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b0718456b5e41a09fefffef2818a57d3b247f59043804877cfe115681a53da70a2d7682a8a7b5808740c24728b85e51f4fac95fb2035c6dd2411525a34edaabb
|
|
7
|
+
data.tar.gz: b46f4a7c41e0e926ac4a20e9f494fe79dc231f97e87c52780996fb192cc015c585dfb7f14c67ce22eb080b7ee854fb3c9303db72e937a9e4b087c25b18e87217
|
data/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
This Gem helps to transfer the defaults from the database schema to the rails validations. To do this, it uses the information in schema.rb and transfers it to a concern file. This file should be included in the version control.
|
|
4
4
|
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
* Ruby 3.2 or newer
|
|
8
|
+
* Rails 7.2 to 8.1
|
|
9
|
+
* `db/schema.rb` as schema format (`structure.sql` is not supported)
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
Add this line to your application's Gemfile:
|
|
@@ -45,7 +51,7 @@ class User < ApplicationRecord
|
|
|
45
51
|
|
|
46
52
|
enum :actived, no: 0, yes: 1
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
validates :email_address, email_format: true
|
|
49
55
|
|
|
50
56
|
schema_validations # to use auto generated validations
|
|
51
57
|
|
|
@@ -90,21 +96,37 @@ It is hardcoded, that this will only run on development environment.
|
|
|
90
96
|
```ruby
|
|
91
97
|
t.text :stuff, null: false
|
|
92
98
|
# results in
|
|
93
|
-
|
|
99
|
+
validates :stuff, presence: true
|
|
94
100
|
|
|
95
101
|
t.string :stuff, limit: 10
|
|
96
102
|
# results in
|
|
97
|
-
|
|
103
|
+
validates :stuff, length: { maximum: 10, allow_nil: true }
|
|
98
104
|
|
|
99
105
|
t.integer :stuff
|
|
100
106
|
# results in
|
|
101
|
-
|
|
107
|
+
validates :stuff, numericality: { only_integer: true, greater_than_or_equal_to: -2147483648, less_than: 2147483648, allow_nil: true }
|
|
102
108
|
```
|
|
103
109
|
|
|
104
110
|
You can watch changes on `schema_validations.rb` to understand the generated validations.
|
|
105
111
|
|
|
106
112
|
## Changelog
|
|
107
113
|
|
|
114
|
+
### 0.7.0
|
|
115
|
+
|
|
116
|
+
* requires Ruby 3.2 and Rails 7.2 or newer
|
|
117
|
+
* supports all PostgreSQL column types of Rails (e.g. `citext`, `money`, `bit`, `enum`, `hstore`, ranges and geometric types)
|
|
118
|
+
* `enum` columns are validated to contain a value of the PostgreSQL enum type
|
|
119
|
+
* `serial` and `bigserial` columns are not validated for presence, because the database sets them
|
|
120
|
+
* unique constraints are validated like unique indexes
|
|
121
|
+
* not null `json`, `jsonb`, `hstore` and array columns accept empty values (`{}` or `[]`)
|
|
122
|
+
* the generated file contains only the validator classes used by the tables
|
|
123
|
+
* `bigint` columns and `integer` columns with `limit` get a range validation
|
|
124
|
+
* the generated file is the same on all Ruby versions (`Hash#inspect` changed with Ruby 3.4)
|
|
125
|
+
* abstract classes do not need `schema_validations` anymore
|
|
126
|
+
* ignore `virtual` columns, check and exclusion constraints
|
|
127
|
+
* skip generating if the schema format is not ruby
|
|
128
|
+
* adds tests
|
|
129
|
+
|
|
108
130
|
### 0.6.1
|
|
109
131
|
|
|
110
132
|
* adds support for Rails 8.1 (integer range validations no longer use the private `ActiveRecord::Type::Integer#range`, which was removed in Rails 8.1)
|
|
@@ -190,6 +212,16 @@ require 'generated_schema_validations/capistrano_tasks'
|
|
|
190
212
|
|
|
191
213
|
* Add column type `binary`
|
|
192
214
|
|
|
215
|
+
## Development
|
|
216
|
+
|
|
217
|
+
The specs run with SQLite. Set `DATABASE_URL` to run the PostgreSQL specs too:
|
|
218
|
+
|
|
219
|
+
$ DATABASE_URL=postgres://user:password@localhost/generated_schema_validations_test bundle exec rspec
|
|
220
|
+
|
|
221
|
+
Other Rails versions are tested with the gemfiles in `gemfiles/`:
|
|
222
|
+
|
|
223
|
+
$ BUNDLE_GEMFILE=gemfiles/rails_7.2.gemfile bundle exec rspec
|
|
224
|
+
|
|
193
225
|
## Contributing
|
|
194
226
|
|
|
195
227
|
Bug reports and pull requests are welcome on GitHub at https://github.com/Lichtbit/generated_schema_validations.
|
|
@@ -19,11 +19,13 @@ namespace :generated_schema_validations do
|
|
|
19
19
|
with(
|
|
20
20
|
rails_env: fetch(:rails_env),
|
|
21
21
|
rails_groups: fetch(:rails_assets_groups),
|
|
22
|
-
disable_database_environment_check: 1
|
|
22
|
+
disable_database_environment_check: 1,
|
|
23
23
|
) do
|
|
24
24
|
dump_output = capture(:rake, 'db:validation_dump_direct').strip
|
|
25
25
|
|
|
26
|
-
if dump_output
|
|
26
|
+
if dump_output == local_content
|
|
27
|
+
info '✔ Schema OK'
|
|
28
|
+
else
|
|
27
29
|
warn '❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌'
|
|
28
30
|
warn 'The online version of the automatically generated validations differs from the local version.'
|
|
29
31
|
warn 'Please check immediately.'
|
|
@@ -41,7 +43,7 @@ namespace :generated_schema_validations do
|
|
|
41
43
|
diffs.each do |piece|
|
|
42
44
|
hunk = Diff::LCS::Hunk.new(
|
|
43
45
|
old_lines, new_lines, piece,
|
|
44
|
-
3,
|
|
46
|
+
3, # context lines
|
|
45
47
|
file_length_difference
|
|
46
48
|
)
|
|
47
49
|
|
|
@@ -54,8 +56,6 @@ namespace :generated_schema_validations do
|
|
|
54
56
|
end
|
|
55
57
|
end
|
|
56
58
|
warn '❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌'
|
|
57
|
-
else
|
|
58
|
-
info '✔ Schema OK'
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
end
|
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'stringio'
|
|
4
|
+
|
|
3
5
|
class GeneratedSchemaValidations::DumpChecker < GeneratedSchemaValidations::Dumper
|
|
4
6
|
def self.read_schema_content
|
|
5
7
|
stream = StringIO.new
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
ActiveRecord::SchemaDumper.dump(
|
|
9
|
-
ActiveRecord::Base.connection_pool,
|
|
10
|
-
stream
|
|
11
|
-
)
|
|
12
|
-
else
|
|
13
|
-
ActiveRecord::SchemaDumper.dump(
|
|
14
|
-
ActiveRecord::Base.connection,
|
|
15
|
-
stream
|
|
16
|
-
)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
stream.rewind
|
|
20
|
-
stream.read
|
|
8
|
+
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection_pool, stream)
|
|
9
|
+
stream.string
|
|
21
10
|
end
|
|
22
11
|
|
|
23
12
|
def write_schema_validations(template_ruby)
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
|
|
3
5
|
class GeneratedSchemaValidations::Dumper
|
|
6
|
+
# Validators from validators/, added to the generated file only if a table uses them
|
|
7
|
+
VALIDATORS = %i[date_time_in_db_range date_in_db_range time_in_db_range not_null].freeze
|
|
8
|
+
|
|
4
9
|
def self.generate
|
|
5
10
|
file = Tempfile.new(['schema', '.rb'])
|
|
6
11
|
begin
|
|
7
|
-
schema_content = read_schema_content.gsub(/ActiveRecord::Schema(\[[^\]]+\])?/,
|
|
8
|
-
raise 'The
|
|
12
|
+
schema_content = read_schema_content.gsub(/ActiveRecord::Schema(\[[^\]]+\])?/, name)
|
|
13
|
+
raise 'The schema is not well-formed.' if schema_content.include?('ActiveRecord')
|
|
9
14
|
|
|
10
15
|
file.write(schema_content)
|
|
11
16
|
file.close
|
|
@@ -20,20 +25,21 @@ class GeneratedSchemaValidations::Dumper
|
|
|
20
25
|
File.read(Rails.root.join('db/schema.rb'))
|
|
21
26
|
end
|
|
22
27
|
|
|
23
|
-
def self.define(info = {}, &
|
|
24
|
-
new.define(info, &
|
|
28
|
+
def self.define(info = {}, &)
|
|
29
|
+
new.define(info, &)
|
|
25
30
|
end
|
|
26
31
|
|
|
27
|
-
def define(info, &
|
|
28
|
-
instance_eval(&
|
|
32
|
+
def define(info, &)
|
|
33
|
+
instance_eval(&)
|
|
29
34
|
|
|
30
35
|
template_ruby = File.read(File.expand_path('template.rb', File.dirname(__FILE__)))
|
|
31
36
|
template_ruby.gsub!('VERSION_INFO', info[:version].to_s)
|
|
32
37
|
indention_spaces = template_ruby.match(/( +)TABLE_VALIDATIONS/)[1]
|
|
33
|
-
table_validations_ruby = @table_validations_ruby.lines.map do |line|
|
|
38
|
+
table_validations_ruby = @table_validations_ruby.to_s.lines.map do |line|
|
|
34
39
|
line.strip.present? ? "#{indention_spaces}#{line}" : "\n"
|
|
35
|
-
end.join
|
|
40
|
+
end.join.delete_prefix("\n").chomp
|
|
36
41
|
template_ruby.gsub!("#{indention_spaces}TABLE_VALIDATIONS", table_validations_ruby)
|
|
42
|
+
template_ruby.sub!("\n\n VALIDATOR_CLASSES", validator_classes_ruby)
|
|
37
43
|
|
|
38
44
|
write_schema_validations(template_ruby)
|
|
39
45
|
end
|
|
@@ -44,12 +50,30 @@ class GeneratedSchemaValidations::Dumper
|
|
|
44
50
|
File.write(Rails.root.join('app/models/concerns/schema_validations.rb'), template_ruby)
|
|
45
51
|
end
|
|
46
52
|
|
|
47
|
-
def create_table(table_name, *, &
|
|
53
|
+
def create_table(table_name, *, &)
|
|
54
|
+
table = GeneratedSchemaValidations::Table.new(table_name, enum_types: @enum_types || {}, &)
|
|
55
|
+
@used_validators = (@used_validators || []) | table.validators
|
|
48
56
|
@table_validations_ruby ||= ''
|
|
49
|
-
@table_validations_ruby +=
|
|
57
|
+
@table_validations_ruby += table.to_s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def create_enum(name, values)
|
|
61
|
+
@enum_types ||= {}
|
|
62
|
+
@enum_types[name.to_s] = values
|
|
50
63
|
end
|
|
51
64
|
|
|
52
65
|
def do_nothing(*); end
|
|
53
66
|
alias enable_extension do_nothing
|
|
67
|
+
alias create_schema do_nothing
|
|
54
68
|
alias add_foreign_key do_nothing
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def validator_classes_ruby
|
|
73
|
+
(VALIDATORS & @used_validators.to_a).map do |validator|
|
|
74
|
+
ruby = File.read(File.expand_path("validators/#{validator}_validator.rb", __dir__))
|
|
75
|
+
ruby = ruby.delete_prefix("# frozen_string_literal: true\n\n")
|
|
76
|
+
"\n\n#{ruby.lines.map { |line| line.strip.present? ? " #{line}" : line }.join.chomp}"
|
|
77
|
+
end.join
|
|
78
|
+
end
|
|
55
79
|
end
|
|
@@ -1,29 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class GeneratedSchemaValidations::Table
|
|
4
|
+
# Hash#inspect changed with Ruby 3.4. Use its new style on all versions to get the same file everywhere.
|
|
5
|
+
def self.inspect_value(value)
|
|
6
|
+
return value.inspect unless value.is_a?(Hash)
|
|
7
|
+
|
|
8
|
+
"{#{value.map { |key, v| "#{key}: #{inspect_value(v)}" }.join(', ')}}"
|
|
9
|
+
end
|
|
10
|
+
|
|
4
11
|
Validation = Struct.new(:attribute, :validator, :options) do
|
|
5
12
|
def to_s
|
|
6
|
-
"validates_with_filter #{attribute.to_sym.inspect},
|
|
13
|
+
"validates_with_filter #{attribute.to_sym.inspect}, " \
|
|
14
|
+
"#{GeneratedSchemaValidations::Table.inspect_value(validator => options)}"
|
|
7
15
|
end
|
|
8
16
|
end
|
|
9
17
|
|
|
10
18
|
ValidationWithoutEnum = Struct.new(:attribute, :validator, :options) do
|
|
11
19
|
def to_s
|
|
12
|
-
"
|
|
20
|
+
"#{Validation.new(attribute, validator, options)} unless enums.include?(#{attribute.to_sym.inspect})"
|
|
13
21
|
end
|
|
14
22
|
end
|
|
15
23
|
|
|
24
|
+
# Valid values of these types can be blank, e.g. an empty hash or an empty bit string.
|
|
25
|
+
BLANK_VALUE_TYPES = %i[json jsonb hstore bit_varying ltree tsvector].freeze
|
|
26
|
+
|
|
16
27
|
attr_reader :table_name
|
|
17
28
|
|
|
18
|
-
def initialize(table_name, &
|
|
29
|
+
def initialize(table_name, enum_types: {}, &)
|
|
19
30
|
@table_name = table_name
|
|
31
|
+
@enum_types = enum_types
|
|
20
32
|
@column_names = []
|
|
21
33
|
@possible_belongs_to_not_null_columns = []
|
|
22
34
|
@bad_indexes = []
|
|
23
35
|
@unique_indexes = []
|
|
24
36
|
@validations = []
|
|
25
37
|
|
|
26
|
-
instance_eval(&
|
|
38
|
+
instance_eval(&)
|
|
27
39
|
end
|
|
28
40
|
|
|
29
41
|
def to_s
|
|
@@ -45,6 +57,10 @@ class GeneratedSchemaValidations::Table
|
|
|
45
57
|
"#{string}end\n"
|
|
46
58
|
end
|
|
47
59
|
|
|
60
|
+
def validators
|
|
61
|
+
@validations.map(&:validator).uniq
|
|
62
|
+
end
|
|
63
|
+
|
|
48
64
|
def validates(attribute, validator, options = {})
|
|
49
65
|
@validations.push(Validation.new(attribute, validator, options))
|
|
50
66
|
end
|
|
@@ -61,8 +77,8 @@ class GeneratedSchemaValidations::Table
|
|
|
61
77
|
@possible_belongs_to_not_null_columns.push(name.to_sym) if datatype.in?(%i[bigint integer uuid])
|
|
62
78
|
if datatype == :boolean
|
|
63
79
|
validates name, :inclusion, in: [true, false], message: :blank
|
|
64
|
-
elsif datatype.in?(
|
|
65
|
-
validates name, :
|
|
80
|
+
elsif datatype.in?(BLANK_VALUE_TYPES) || column_options[:array]
|
|
81
|
+
validates name, :not_null
|
|
66
82
|
else
|
|
67
83
|
validates name, :presence
|
|
68
84
|
end
|
|
@@ -74,27 +90,48 @@ class GeneratedSchemaValidations::Table
|
|
|
74
90
|
|
|
75
91
|
def bigint(name, column_options = {})
|
|
76
92
|
null_validation(:bigint, name, column_options)
|
|
77
|
-
|
|
78
|
-
validates_without_enum name, :numericality, allow_nil: true
|
|
93
|
+
integer_range_validation(name, column_options, default_limit: 8)
|
|
79
94
|
end
|
|
80
95
|
|
|
81
96
|
def integer(name, column_options = {})
|
|
82
97
|
null_validation(:integer, name, column_options)
|
|
98
|
+
integer_range_validation(name, column_options, default_limit: ::ActiveRecord::Type::Integer::DEFAULT_LIMIT)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def serial(name, column_options = {})
|
|
102
|
+
# The database sets the value on insert, so it is nil before.
|
|
103
|
+
null_validation(:serial, name, column_options.except(:null))
|
|
104
|
+
integer_range_validation(name, column_options, default_limit: 4)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def bigserial(name, column_options = {})
|
|
108
|
+
null_validation(:bigserial, name, column_options.except(:null))
|
|
109
|
+
integer_range_validation(name, column_options, default_limit: 8)
|
|
110
|
+
end
|
|
83
111
|
|
|
112
|
+
def oid(name, column_options = {})
|
|
113
|
+
null_validation(:oid, name, column_options)
|
|
84
114
|
return if column_options[:array]
|
|
85
115
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
options = { allow_nil: true, only_integer: true, greater_than_or_equal_to: -max_value, less_than: max_value }
|
|
116
|
+
validates name, :numericality, allow_nil: true, only_integer: true,
|
|
117
|
+
greater_than_or_equal_to: 0, less_than: 1 << 32
|
|
118
|
+
end
|
|
90
119
|
|
|
91
|
-
|
|
120
|
+
def money(name, column_options = {})
|
|
121
|
+
null_validation(:money, name, column_options)
|
|
122
|
+
return if column_options[:array]
|
|
123
|
+
|
|
124
|
+
# PostgreSQL stores money as 64 bit integer of the smallest currency unit
|
|
125
|
+
limit = (1 << 63) / (10**(column_options[:scale] || 2))
|
|
126
|
+
validates name, :numericality, allow_nil: true, greater_than_or_equal_to: -limit, less_than_or_equal_to: limit
|
|
92
127
|
end
|
|
93
128
|
|
|
94
129
|
def datetime(name, column_options = {})
|
|
95
130
|
null_validation(:datetime, name, column_options)
|
|
96
131
|
validates name, :date_time_in_db_range
|
|
97
132
|
end
|
|
133
|
+
alias timestamp datetime
|
|
134
|
+
alias timestamptz datetime
|
|
98
135
|
|
|
99
136
|
def date(name, column_options = {})
|
|
100
137
|
null_validation(:date, name, column_options)
|
|
@@ -153,6 +190,42 @@ class GeneratedSchemaValidations::Table
|
|
|
153
190
|
|
|
154
191
|
validates name, :length, allow_nil: true, maximum: column_options[:limit]
|
|
155
192
|
end
|
|
193
|
+
alias citext text
|
|
194
|
+
|
|
195
|
+
def bit(name, column_options = {})
|
|
196
|
+
null_validation(:bit, name, column_options)
|
|
197
|
+
return if column_options[:array]
|
|
198
|
+
|
|
199
|
+
validates name, :format, allow_nil: true, with: /\A[01]*\z/
|
|
200
|
+
validates name, :length, allow_nil: true, is: column_options[:limit] || 1
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def bit_varying(name, column_options = {})
|
|
204
|
+
null_validation(:bit_varying, name, column_options)
|
|
205
|
+
return if column_options[:array]
|
|
206
|
+
|
|
207
|
+
validates name, :format, allow_nil: true, with: /\A[01]*\z/
|
|
208
|
+
validates name, :length, allow_nil: true, maximum: column_options[:limit] if column_options[:limit]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def enum(name, column_options = {})
|
|
212
|
+
null_validation(:enum, name, column_options)
|
|
213
|
+
return if column_options[:array]
|
|
214
|
+
|
|
215
|
+
values = @enum_types[column_options[:enum_type].to_s]
|
|
216
|
+
validates_without_enum name, :inclusion, allow_nil: true, in: values if values
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Columns without own validations. The database checks the format, the not null constraint is validated.
|
|
220
|
+
%i[
|
|
221
|
+
daterange numrange tsrange tstzrange int4range int8range
|
|
222
|
+
hstore inet cidr macaddr ltree tsvector interval
|
|
223
|
+
point line lseg box path polygon circle
|
|
224
|
+
].each do |datatype|
|
|
225
|
+
define_method(datatype) do |name, column_options = {}|
|
|
226
|
+
null_validation(datatype, name, column_options)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
156
229
|
|
|
157
230
|
def index(names, index_options = {})
|
|
158
231
|
names = [names] unless names.is_a?(Array)
|
|
@@ -166,7 +239,29 @@ class GeneratedSchemaValidations::Table
|
|
|
166
239
|
end
|
|
167
240
|
end
|
|
168
241
|
|
|
242
|
+
def unique_constraint(names, _constraint_options = {})
|
|
243
|
+
index(names, unique: true)
|
|
244
|
+
end
|
|
245
|
+
|
|
169
246
|
def geography(*)
|
|
170
247
|
# do nothing
|
|
171
248
|
end
|
|
249
|
+
|
|
250
|
+
def do_nothing(*); end
|
|
251
|
+
alias virtual do_nothing # generated columns can not be written
|
|
252
|
+
alias check_constraint do_nothing
|
|
253
|
+
alias exclusion_constraint do_nothing
|
|
254
|
+
|
|
255
|
+
private
|
|
256
|
+
|
|
257
|
+
def integer_range_validation(name, column_options, default_limit:)
|
|
258
|
+
return if column_options[:array]
|
|
259
|
+
|
|
260
|
+
# Same bounds as ActiveModel::Type::Integer (min...max), computed here because
|
|
261
|
+
# Rails 8.1 removed the private #range method this used to call.
|
|
262
|
+
max_value = 1 << (((column_options[:limit] || default_limit) * 8) - 1)
|
|
263
|
+
options = { allow_nil: true, only_integer: true, greater_than_or_equal_to: -max_value, less_than: max_value }
|
|
264
|
+
|
|
265
|
+
validates_without_enum name, :numericality, options
|
|
266
|
+
end
|
|
172
267
|
end
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
Rake::Task['db:schema:dump'].enhance do
|
|
4
|
+
# The validations are generated from db/schema.rb, so there is nothing to do with structure.sql.
|
|
5
|
+
next unless (ENV['SCHEMA_FORMAT'] || ActiveRecord.schema_format).to_s == 'ruby'
|
|
6
|
+
|
|
4
7
|
require 'generated_schema_validations'
|
|
5
8
|
require_relative 'dumper'
|
|
6
9
|
require_relative 'table'
|
|
@@ -26,9 +26,10 @@ module SchemaValidations
|
|
|
26
26
|
if defined?(Rails::Railtie) && (Rails.env.development? || Rails.env.test?)
|
|
27
27
|
TracePoint.trace(:end) do |t|
|
|
28
28
|
if t.self.respond_to?(:schema_validations_called) &&
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
t.self < ApplicationRecord &&
|
|
30
|
+
!t.self.abstract_class? &&
|
|
31
|
+
!t.self.schema_validations_called &&
|
|
32
|
+
!SchemaValidations.relaxed?
|
|
32
33
|
raise "#{t.self}: schema_validations or skip_schema_validations missing"
|
|
33
34
|
end
|
|
34
35
|
end
|
|
@@ -73,7 +74,7 @@ module SchemaValidations
|
|
|
73
74
|
names.each do |name|
|
|
74
75
|
next if name.to_sym.in?(schema_validations_excluded_columns)
|
|
75
76
|
next unless defined?(Rails::Railtie)
|
|
76
|
-
next unless
|
|
77
|
+
next unless Rails.env.development? || Rails.env.test?
|
|
77
78
|
|
|
78
79
|
raise "Unique index with where clause is outside the scope of this gem.\n\n" \
|
|
79
80
|
"You can exclude this column: `schema_validations exclude: [:#{name}]`"
|
|
@@ -117,39 +118,5 @@ module SchemaValidations
|
|
|
117
118
|
end
|
|
118
119
|
end
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
def validate_each(record, attr_name, value)
|
|
122
|
-
return if value.nil?
|
|
123
|
-
return unless value.is_a?(DateTime) || value.is_a?(Time)
|
|
124
|
-
return if value.year.between?(-4711, 294_275) # see https://www.postgresql.org/docs/9.3/datatype-datetime.html
|
|
125
|
-
|
|
126
|
-
record.errors.add(attr_name, :invalid)
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
class DateInDbRangeValidator < ActiveModel::EachValidator
|
|
131
|
-
def validate_each(record, attr_name, value)
|
|
132
|
-
return if value.nil?
|
|
133
|
-
return unless value.is_a?(Date)
|
|
134
|
-
return if value.year.between?(-4711, 5_874_896) # see https://www.postgresql.org/docs/9.3/datatype-datetime.html
|
|
135
|
-
|
|
136
|
-
record.errors.add(attr_name, :invalid)
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
class TimeInDbRangeValidator < ActiveModel::EachValidator
|
|
141
|
-
def validate_each(record, attr_name, value)
|
|
142
|
-
return if value.nil?
|
|
143
|
-
|
|
144
|
-
unless value.is_a?(Time) || value.is_a?(ActiveSupport::TimeWithZone)
|
|
145
|
-
record.errors.add(attr_name, :invalid)
|
|
146
|
-
return
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# PostgreSQL allows only times in one day
|
|
150
|
-
return if value.to_date == Date.new(2000, 1, 1)
|
|
151
|
-
|
|
152
|
-
record.errors.add(attr_name, :invalid)
|
|
153
|
-
end
|
|
154
|
-
end
|
|
121
|
+
VALIDATOR_CLASSES
|
|
155
122
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class DateInDbRangeValidator < ActiveModel::EachValidator
|
|
4
|
+
def validate_each(record, attr_name, value)
|
|
5
|
+
return if value.nil?
|
|
6
|
+
return unless value.is_a?(Date)
|
|
7
|
+
return if value.year.between?(-4711, 5_874_896) # see https://www.postgresql.org/docs/9.3/datatype-datetime.html
|
|
8
|
+
|
|
9
|
+
record.errors.add(attr_name, :invalid)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class DateTimeInDbRangeValidator < ActiveModel::EachValidator
|
|
4
|
+
def validate_each(record, attr_name, value)
|
|
5
|
+
return if value.nil?
|
|
6
|
+
return unless value.is_a?(DateTime) || value.is_a?(Time)
|
|
7
|
+
return if value.year.between?(-4711, 294_275) # see https://www.postgresql.org/docs/9.3/datatype-datetime.html
|
|
8
|
+
|
|
9
|
+
record.errors.add(attr_name, :invalid)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Like presence, but accepts blank values like an empty hash or array
|
|
4
|
+
class NotNullValidator < ActiveModel::EachValidator
|
|
5
|
+
def validate_each(record, attr_name, value)
|
|
6
|
+
record.errors.add(attr_name, :blank) if value.nil?
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class TimeInDbRangeValidator < ActiveModel::EachValidator
|
|
4
|
+
def validate_each(record, attr_name, value)
|
|
5
|
+
return if value.nil?
|
|
6
|
+
|
|
7
|
+
unless value.is_a?(Time) || value.is_a?(ActiveSupport::TimeWithZone)
|
|
8
|
+
record.errors.add(attr_name, :invalid)
|
|
9
|
+
return
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# PostgreSQL allows only times in one day
|
|
13
|
+
return if value.to_date == Date.new(2000, 1, 1)
|
|
14
|
+
|
|
15
|
+
record.errors.add(attr_name, :invalid)
|
|
16
|
+
end
|
|
17
|
+
end
|
metadata
CHANGED
|
@@ -1,31 +1,65 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: generated_schema_validations
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Georg Limbach
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
12
|
-
dependencies:
|
|
11
|
+
date: 2026-10-29 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activerecord
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.2'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '9'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '7.2'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '9'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: railties
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.2'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '9'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '7.2'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '9'
|
|
13
53
|
description: After each migration it generates a file with some validations. Each
|
|
14
|
-
active record should include this file and can
|
|
54
|
+
active record should include this file and can use the generated validations.
|
|
15
55
|
email:
|
|
16
56
|
- georg.limbach@lichtbit.com
|
|
17
57
|
executables: []
|
|
18
58
|
extensions: []
|
|
19
59
|
extra_rdoc_files: []
|
|
20
60
|
files:
|
|
21
|
-
- ".gitignore"
|
|
22
|
-
- ".rspec"
|
|
23
|
-
- Gemfile
|
|
24
|
-
- Gemfile.lock
|
|
25
61
|
- LICENSE.txt
|
|
26
62
|
- README.md
|
|
27
|
-
- Rakefile
|
|
28
|
-
- generated_schema_validations.gemspec
|
|
29
63
|
- lib/generated_schema_validations.rb
|
|
30
64
|
- lib/generated_schema_validations/capistrano_tasks.rb
|
|
31
65
|
- lib/generated_schema_validations/dump_checker.rb
|
|
@@ -34,12 +68,17 @@ files:
|
|
|
34
68
|
- lib/generated_schema_validations/table.rb
|
|
35
69
|
- lib/generated_schema_validations/tasks.rb
|
|
36
70
|
- lib/generated_schema_validations/template.rb
|
|
71
|
+
- lib/generated_schema_validations/validators/date_in_db_range_validator.rb
|
|
72
|
+
- lib/generated_schema_validations/validators/date_time_in_db_range_validator.rb
|
|
73
|
+
- lib/generated_schema_validations/validators/not_null_validator.rb
|
|
74
|
+
- lib/generated_schema_validations/validators/time_in_db_range_validator.rb
|
|
37
75
|
homepage: https://github.com/Lichtbit/generated_schema_validations
|
|
38
76
|
licenses:
|
|
39
77
|
- MIT
|
|
40
78
|
metadata:
|
|
41
|
-
homepage_uri: https://github.com/Lichtbit/generated_schema_validations
|
|
42
79
|
source_code_uri: https://github.com/Lichtbit/generated_schema_validations
|
|
80
|
+
changelog_uri: https://github.com/Lichtbit/generated_schema_validations#changelog
|
|
81
|
+
rubygems_mfa_required: 'true'
|
|
43
82
|
post_install_message:
|
|
44
83
|
rdoc_options: []
|
|
45
84
|
require_paths:
|
|
@@ -48,7 +87,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
48
87
|
requirements:
|
|
49
88
|
- - ">="
|
|
50
89
|
- !ruby/object:Gem::Version
|
|
51
|
-
version:
|
|
90
|
+
version: '3.2'
|
|
52
91
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
53
92
|
requirements:
|
|
54
93
|
- - ">="
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/Gemfile
DELETED
data/Gemfile.lock
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
PATH
|
|
2
|
-
remote: .
|
|
3
|
-
specs:
|
|
4
|
-
generated_schema_validations (0.2.0)
|
|
5
|
-
|
|
6
|
-
GEM
|
|
7
|
-
remote: https://rubygems.org/
|
|
8
|
-
specs:
|
|
9
|
-
diff-lcs (1.4.4)
|
|
10
|
-
rake (12.3.3)
|
|
11
|
-
rspec (3.10.0)
|
|
12
|
-
rspec-core (~> 3.10.0)
|
|
13
|
-
rspec-expectations (~> 3.10.0)
|
|
14
|
-
rspec-mocks (~> 3.10.0)
|
|
15
|
-
rspec-core (3.10.1)
|
|
16
|
-
rspec-support (~> 3.10.0)
|
|
17
|
-
rspec-expectations (3.10.1)
|
|
18
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
|
19
|
-
rspec-support (~> 3.10.0)
|
|
20
|
-
rspec-mocks (3.10.2)
|
|
21
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
|
22
|
-
rspec-support (~> 3.10.0)
|
|
23
|
-
rspec-support (3.10.3)
|
|
24
|
-
|
|
25
|
-
PLATFORMS
|
|
26
|
-
ruby
|
|
27
|
-
|
|
28
|
-
DEPENDENCIES
|
|
29
|
-
generated_schema_validations!
|
|
30
|
-
rake (~> 12.0)
|
|
31
|
-
rspec (~> 3.0)
|
|
32
|
-
|
|
33
|
-
BUNDLED WITH
|
|
34
|
-
2.1.4
|
data/Rakefile
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Gem::Specification.new do |spec|
|
|
4
|
-
spec.name = 'generated_schema_validations'
|
|
5
|
-
spec.version = '0.6.1'
|
|
6
|
-
spec.authors = ['Georg Limbach']
|
|
7
|
-
spec.email = ['georg.limbach@lichtbit.com']
|
|
8
|
-
|
|
9
|
-
spec.summary = 'Generate rails validations from schema.rb file'
|
|
10
|
-
spec.description = 'After each migration it generates a file with some validations. Each active record should ' \
|
|
11
|
-
'include this file and can uns generated validations.'
|
|
12
|
-
spec.homepage = 'https://github.com/Lichtbit/generated_schema_validations'
|
|
13
|
-
spec.license = 'MIT'
|
|
14
|
-
spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
|
|
15
|
-
|
|
16
|
-
spec.metadata['homepage_uri'] = spec.homepage
|
|
17
|
-
spec.metadata['source_code_uri'] = 'https://github.com/Lichtbit/generated_schema_validations'
|
|
18
|
-
|
|
19
|
-
# Specify which files should be added to the gem when it is released.
|
|
20
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
21
|
-
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
22
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
23
|
-
end
|
|
24
|
-
spec.require_paths = ['lib']
|
|
25
|
-
end
|