File: auto_validations.rb

package info (click to toggle)
ruby-sequel 5.63.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,408 kB
  • sloc: ruby: 113,747; makefile: 3
file content (302 lines) | stat: -rw-r--r-- 13,257 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# frozen-string-literal: true

module Sequel
  module Plugins
    # The auto_validations plugin automatically sets up the following types of validations
    # for your model columns:
    #
    # 1. type validations for all columns
    # 2. not_null validations on NOT NULL columns (optionally, presence validations)
    # 3. unique validations on columns or sets of columns with unique indexes
    # 4. max length validations on string columns
    # 5. no null byte validations on string columns
    # 6. minimum and maximum values on columns
    #
    # To determine the columns to use for the type/not_null/max_length/no_null_byte/max_value/min_value validations,
    # the plugin looks at the database schema for the model's table.  To determine
    # the unique validations, Sequel looks at the indexes on the table.  In order
    # for this plugin to be fully functional, the underlying database adapter needs
    # to support both schema and index parsing.  Additionally, unique validations are
    # only added for models that select from a simple table, they are not added for models
    # that select from a subquery.
    #
    # This plugin uses the validation_helpers plugin underneath to implement the
    # validations.  It does not allow for any per-column validation message
    # customization, but you can alter the messages for the given type of validation
    # on a per-model basis (see the validation_helpers documentation).
    #
    # You can skip certain types of validations from being automatically added via:
    #
    #   Model.skip_auto_validations(:not_null)
    #
    # If you want to skip all auto validations (only useful if loading the plugin
    # in a superclass):
    #
    #   Model.skip_auto_validations(:all)
    #
    # It is possible to skip auto validations on a per-model-instance basis via:
    #
    #   instance.skip_auto_validations(:unique, :not_null) do
    #     puts instance.valid?
    #   end
    #
    # By default, the plugin uses a not_null validation for NOT NULL columns, but that
    # can be changed to a presence validation using an option:
    #
    #   Model.plugin :auto_validations, not_null: :presence
    #
    # This is useful if you want to enforce that NOT NULL string columns do not
    # allow empty values.
    #
    # You can also supply hashes to pass options through to the underlying validators:
    #
    #   Model.plugin :auto_validations, unique_opts: {only_if_modified: true}
    #
    # This works for unique_opts, max_length_opts, schema_types_opts, max_value_opts, min_value_opts, no_null_byte_opts,
    # explicit_not_null_opts, and not_null_opts.
    #
    # If you only want auto_validations to add validations to columns that do not already
    # have an error associated with them, you can use the skip_invalid option:
    #
    #   Model.plugin :auto_validations, skip_invalid: true
    #
    # Usage:
    #
    #   # Make all model subclass use auto validations (called before loading subclasses)
    #   Sequel::Model.plugin :auto_validations
    #
    #   # Make the Album class use auto validations
    #   Album.plugin :auto_validations
    module AutoValidations
      NOT_NULL_OPTIONS = {:from=>:values}.freeze
      EXPLICIT_NOT_NULL_OPTIONS = {:from=>:values, :allow_missing=>true}.freeze
      MAX_LENGTH_OPTIONS = {:from=>:values, :allow_nil=>true}.freeze
      SCHEMA_TYPES_OPTIONS = NOT_NULL_OPTIONS
      UNIQUE_OPTIONS = NOT_NULL_OPTIONS
      NO_NULL_BYTE_OPTIONS = MAX_LENGTH_OPTIONS
      MAX_VALUE_OPTIONS = {:from=>:values, :allow_nil=>true, :skip_invalid=>true}.freeze
      MIN_VALUE_OPTIONS = MAX_VALUE_OPTIONS
      AUTO_VALIDATE_OPTIONS = {
        :no_null_byte=>NO_NULL_BYTE_OPTIONS,
        :not_null=>NOT_NULL_OPTIONS,
        :explicit_not_null=>EXPLICIT_NOT_NULL_OPTIONS,
        :max_length=>MAX_LENGTH_OPTIONS,
        :max_value=>MAX_VALUE_OPTIONS,
        :min_value=>MIN_VALUE_OPTIONS,
        :schema_types=>SCHEMA_TYPES_OPTIONS,
        :unique=>UNIQUE_OPTIONS
      }.freeze

      EMPTY_ARRAY = [].freeze

      def self.apply(model, opts=OPTS)
        model.instance_exec do
          plugin :validation_helpers
          @auto_validate_presence = false
          @auto_validate_no_null_byte_columns = []
          @auto_validate_not_null_columns = []
          @auto_validate_explicit_not_null_columns = []
          @auto_validate_max_length_columns = []
          @auto_validate_max_value_columns = []
          @auto_validate_min_value_columns = []
          @auto_validate_unique_columns = []
          @auto_validate_types = true
          @auto_validate_options = AUTO_VALIDATE_OPTIONS
        end
      end

      # Setup auto validations for the model if it has a dataset.
      def self.configure(model, opts=OPTS)
        model.instance_exec do
          setup_auto_validations if @dataset
          if opts[:not_null] == :presence
            @auto_validate_presence = true
          end

          h = @auto_validate_options.dup
          [:not_null, :explicit_not_null, :max_length, :max_value, :min_value, :no_null_byte, :schema_types, :unique].each do |type|
            if type_opts = opts[:"#{type}_opts"]
              h[type] = h[type].merge(type_opts).freeze
            end
          end

          if opts[:skip_invalid]
            [:not_null, :explicit_not_null, :no_null_byte, :max_length, :schema_types].each do |type|
              h[type] = h[type].merge(:skip_invalid=>true).freeze
            end
          end

          @auto_validate_options = h.freeze
        end
      end

      module ClassMethods
        # The columns with automatic no_null_byte validations
        attr_reader :auto_validate_no_null_byte_columns

        # The columns with automatic not_null validations
        attr_reader :auto_validate_not_null_columns

        # The columns with automatic not_null validations for columns present in the values.
        attr_reader :auto_validate_explicit_not_null_columns

        # The columns or sets of columns with automatic max_length validations, as an array of
        # pairs, with the first entry being the column name and second entry being the maximum length.
        attr_reader :auto_validate_max_length_columns

        # The columns with automatch max value validations, as an array of
        # pairs, with the first entry being the column name and second entry being the maximum value.
        attr_reader :auto_validate_max_value_columns

        # The columns with automatch min value validations, as an array of
        # pairs, with the first entry being the column name and second entry being the minimum value.
        attr_reader :auto_validate_min_value_columns

        # The columns or sets of columns with automatic unique validations
        attr_reader :auto_validate_unique_columns

        # Inherited options
        attr_reader :auto_validate_options

        Plugins.inherited_instance_variables(self,
          :@auto_validate_presence=>nil,
          :@auto_validate_types=>nil,
          :@auto_validate_no_null_byte_columns=>:dup,
          :@auto_validate_not_null_columns=>:dup,
          :@auto_validate_explicit_not_null_columns=>:dup,
          :@auto_validate_max_length_columns=>:dup,
          :@auto_validate_max_value_columns=>:dup,
          :@auto_validate_min_value_columns=>:dup,
          :@auto_validate_unique_columns=>:dup,
          :@auto_validate_options => :dup)
        Plugins.after_set_dataset(self, :setup_auto_validations)

        # Whether to use a presence validation for not null columns
        def auto_validate_presence?
          @auto_validate_presence
        end

        # Whether to automatically validate schema types for all columns
        def auto_validate_types?
          @auto_validate_types
        end

        # Freeze auto_validation settings when freezing model class.
        def freeze
          @auto_validate_no_null_byte_columns.freeze
          @auto_validate_not_null_columns.freeze
          @auto_validate_explicit_not_null_columns.freeze
          @auto_validate_max_length_columns.freeze
          @auto_validate_max_value_columns.freeze
          @auto_validate_min_value_columns.freeze
          @auto_validate_unique_columns.freeze

          super
        end

        # Skip automatic validations for the given validation type
        # (:not_null, :no_null_byte, :types, :unique, :max_length, :max_value, :min_value).
        # If :all is given as the type, skip all auto validations.
        #
        # Skipping types validation automatically skips max_value and min_value validations,
        # since those validations require valid types.
        def skip_auto_validations(type)
          case type
          when :all
            [:not_null, :no_null_byte, :types, :unique, :max_length, :max_value, :min_value].each{|v| skip_auto_validations(v)}
          when :not_null
            auto_validate_not_null_columns.clear
            auto_validate_explicit_not_null_columns.clear
          when :types
            @auto_validate_types = false
          else
            public_send("auto_validate_#{type}_columns").clear
          end
        end

        private

        # Parse the database schema and indexes and record the columns to automatically validate.
        def setup_auto_validations
          not_null_cols, explicit_not_null_cols = db_schema.select{|col, sch| sch[:allow_null] == false}.partition{|col, sch| sch[:default].nil?}.map{|cs| cs.map{|col, sch| col}}
          @auto_validate_not_null_columns = not_null_cols - Array(primary_key)
          explicit_not_null_cols += Array(primary_key)
          @auto_validate_explicit_not_null_columns = explicit_not_null_cols.uniq
          @auto_validate_max_length_columns = db_schema.select{|col, sch| sch[:type] == :string && sch[:max_length].is_a?(Integer)}.map{|col, sch| [col, sch[:max_length]]}
          @auto_validate_max_value_columns = db_schema.select{|col, sch| sch[:max_value]}.map{|col, sch| [col, sch[:max_value]]}
          @auto_validate_min_value_columns = db_schema.select{|col, sch| sch[:min_value]}.map{|col, sch| [col, sch[:min_value]]}
          @auto_validate_no_null_byte_columns = db_schema.select{|_, sch| sch[:type] == :string}.map{|col, _| col}
          table = dataset.first_source_table
          @auto_validate_unique_columns = if db.supports_index_parsing? && [Symbol, SQL::QualifiedIdentifier, SQL::Identifier, String].any?{|c| table.is_a?(c)}
            db.indexes(table).select{|name, idx| idx[:unique] == true}.map{|name, idx| idx[:columns].length == 1 ? idx[:columns].first : idx[:columns]}
          else
            []
          end
        end
      end

      module InstanceMethods
        # Skip the given types of auto validations on this instance inside the block.
        def skip_auto_validations(*types)
          types << :all if types.empty?
          @_skip_auto_validations = types
          yield
        ensure
          @_skip_auto_validations = nil
        end

        # Validate the model's auto validations columns
        def validate
          super
          skip = @_skip_auto_validations || EMPTY_ARRAY
          return if skip.include?(:all)
          opts = model.auto_validate_options

          unless skip.include?(:no_null_byte) || (no_null_byte_columns = model.auto_validate_no_null_byte_columns).empty?
            validates_no_null_byte(no_null_byte_columns, opts[:no_null_byte])
          end

          unless skip.include?(:not_null)
            not_null_method = model.auto_validate_presence? ? :validates_presence : :validates_not_null
            unless (not_null_columns = model.auto_validate_not_null_columns).empty?
              public_send(not_null_method, not_null_columns, opts[:not_null])
            end
            unless (not_null_columns = model.auto_validate_explicit_not_null_columns).empty?
              public_send(not_null_method, not_null_columns, opts[:explicit_not_null])
            end
          end

          unless skip.include?(:max_length) || (max_length_columns = model.auto_validate_max_length_columns).empty?
            max_length_columns.each do |col, len|
              validates_max_length(len, col, opts[:max_length])
            end
          end

          unless skip.include?(:types) || !model.auto_validate_types?
            validates_schema_types(keys, opts[:schema_types])

            unless skip.include?(:max_value) || ((max_value_columns = model.auto_validate_max_value_columns).empty?)
              max_value_columns.each do |col, max|
                validates_max_value(max, col, opts[:max_value])
              end
            end

            unless skip.include?(:min_value) || ((min_value_columns = model.auto_validate_min_value_columns).empty?)
              min_value_columns.each do |col, min|
                validates_min_value(min, col, opts[:min_value])
              end
            end
          end

          unless skip.include?(:unique)
            unique_opts = Hash[opts[:unique]]
            if model.respond_to?(:sti_dataset)
              unique_opts[:dataset] = model.sti_dataset
            end
            model.auto_validate_unique_columns.each{|cols| validates_unique(cols, unique_opts)}
          end
        end
      end
    end
  end
end