iry 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/iry/macros.rb ADDED
@@ -0,0 +1,118 @@
1
+ module Iry
2
+ # Class-level methods available to classes executing `include Iry`
3
+ module Macros
4
+ # Constraints by name
5
+ # @return [{String => Constraint}]
6
+ def constraints
7
+ @constraints ||= {}
8
+ end
9
+
10
+ # Tracks check constraint for the given key and convert constraint errors into validation errors
11
+ # @param key [Symbol] key to apply validation errors to
12
+ # @param message [Symbol, String] the validation error message
13
+ # @param name [nil, String] constraint name. If omitted, it will be inferred using table name + key
14
+ # @raise [ArgumentError] raised if constraint name already in use by another constraint of any type
15
+ # @return [void]
16
+ def check_constraint(
17
+ key,
18
+ name: nil,
19
+ message: :invalid
20
+ )
21
+ name ||= Constraint::Check.infer_name(key, table_name)
22
+
23
+ if constraints.key?(name)
24
+ raise ArgumentError, "Constraint already exists"
25
+ end
26
+
27
+ constraints[name] = Constraint::Check.new(
28
+ key,
29
+ message: message,
30
+ name: name
31
+ )
32
+ end
33
+
34
+ # Tracks exclusion constraint for the given key and convert constraint errors into validation errors
35
+ # @param key [Symbol] key to apply validation errors to
36
+ # @param message [Symbol, String] the validation error message
37
+ # @param name [nil, String] constraint name. If omitted, it will be inferred using table name + key
38
+ # @raise [ArgumentError] raised if constraint name already in use by another constraint of any type
39
+ # @return [void]
40
+ def exclusion_constraint(
41
+ key,
42
+ name: nil,
43
+ message: :taken
44
+ )
45
+ name ||= Constraint::Exclusion.infer_name(key, table_name)
46
+
47
+ if constraints.key?(name)
48
+ raise ArgumentError, "Constraint already exists"
49
+ end
50
+
51
+ constraints[name] = Constraint::Exclusion.new(
52
+ key,
53
+ message: message,
54
+ name: name
55
+ )
56
+ end
57
+
58
+ # Tracks foreign key constraint for the given key (or keys) and convert constraint errors into validation errors
59
+ # @param key_or_keys [Symbol, <Symbol>] key or array of keys to track the foreign key constraint of
60
+ # @param message [Symbol, String] the validation error message
61
+ # @param name [nil, String] constraint name. If omitted, it will be inferred using table name + keys
62
+ # @param error_key [nil, Symbol] key to which the validation error will be applied to. If omitted, it will be
63
+ # applied to the first key
64
+ # @raise [ArgumentError] raised if constraint name already in use by another constraint of any type
65
+ # @return [void]
66
+ def foreign_key_constraint(
67
+ key_or_keys,
68
+ name: nil,
69
+ message: :required,
70
+ error_key: nil
71
+ )
72
+ keys = Array(key_or_keys)
73
+ name ||= Constraint::ForeignKey.infer_name(keys, table_name)
74
+ error_key ||= keys.first
75
+
76
+ if constraints.key?(name)
77
+ raise ArgumentError, "Constraint already exists"
78
+ end
79
+
80
+ constraints[name] = Constraint::ForeignKey.new(
81
+ keys,
82
+ message: message,
83
+ name: name,
84
+ error_key: error_key
85
+ )
86
+ end
87
+
88
+ # Tracks uniqueness constraint for the given key (or keys) and convert constraint errors into validation errors
89
+ # @param key_or_keys [Symbol, <Symbol>] key or array of keys to track the uniqueness constraint of
90
+ # @param message [Symbol, String] the validation error message
91
+ # @param name [nil, String] constraint name. If omitted, it will be inferred using table name + keys
92
+ # @param error_key [nil, Symbol] key to which the validation error will be applied to. If omitted, it will be
93
+ # applied to the first key
94
+ # @raise [ArgumentError] raised if constraint name already in use by another constraint of any type
95
+ # @return [void]
96
+ def unique_constraint(
97
+ key_or_keys,
98
+ name: nil,
99
+ message: :taken,
100
+ error_key: nil
101
+ )
102
+ keys = Array(key_or_keys)
103
+ name ||= Constraint::Unique.infer_name(keys, table_name)
104
+ error_key ||= keys.first
105
+
106
+ if constraints.key?(name)
107
+ raise ArgumentError, "Constraint already exists"
108
+ end
109
+
110
+ constraints[name] = Constraint::Unique.new(
111
+ keys,
112
+ message: message,
113
+ name: name,
114
+ error_key: error_key
115
+ )
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,4 @@
1
+ module Iry
2
+ # @return [String]
3
+ VERSION = File.read(File.expand_path("../../VERSION", __dir__)).strip.freeze
4
+ end
data/lib/iry.rb ADDED
@@ -0,0 +1,44 @@
1
+ require "active_record"
2
+
3
+ require_relative "iry/version"
4
+ require_relative "iry/handlers"
5
+ require_relative "iry/handlers/null"
6
+ require_relative "iry/handlers/pg"
7
+ require_relative "iry/callbacks"
8
+ require_relative "iry/macros"
9
+ require_relative "iry/constraint"
10
+ require_relative "iry/constraint/check"
11
+ require_relative "iry/constraint/exclusion"
12
+ require_relative "iry/constraint/foreign_key"
13
+ require_relative "iry/constraint/unique"
14
+
15
+ # Entrypoint of constraint validation, include in a class inheriting {ActiveRecord::Base} and the following class-level
16
+ # methods will be available:
17
+ # - {Macros#constraints}
18
+ # - {Macros#check_constraint}
19
+ # - {Macros#exclusion_constraint}
20
+ # - {Macros#foreign_key_constraint}
21
+ # - {Macros#unique_constraint}
22
+ #
23
+ # @example User unique constraint validation
24
+ # # The database schema has a unique constraint on email field
25
+ # class User < ActiveRecord::Base
26
+ # include Iry
27
+ #
28
+ # unique_constraint :email
29
+ # end
30
+ #
31
+ # user = User.create!(email: "user@example.com")
32
+ # fail_user = User.create(email: "user@example.com")
33
+ # fail_user.errors.details.fetch(:email) #=> [{error: :taken}]
34
+ module Iry
35
+ # @param klass [Module]
36
+ # @return [void]
37
+ # @private
38
+ def self.included(klass)
39
+ klass.class_eval do
40
+ extend(Iry::Macros)
41
+ around_save(Iry::Callbacks)
42
+ end
43
+ end
44
+ end
data/rbi/iry.rbi ADDED
@@ -0,0 +1,408 @@
1
+ # typed: strong
2
+ # Entrypoint of constraint validation, include in a class inheriting {ActiveRecord::Base} and the following class-level
3
+ # methods will be available:
4
+ # - {Macros#constraints}
5
+ # - {Macros#check_constraint}
6
+ # - {Macros#exclusion_constraint}
7
+ # - {Macros#foreign_key_constraint}
8
+ # - {Macros#unique_constraint}
9
+ #
10
+ # @example User unique constraint validation
11
+ # # The database schema has a unique constraint on email field
12
+ # class User < ActiveRecord::Base
13
+ # include Iry
14
+ #
15
+ # unique_constraint :email
16
+ # end
17
+ #
18
+ # user = User.create!(email: "user@example.com")
19
+ # fail_user = User.create(email: "user@example.com")
20
+ # fail_user.errors.details.fetch(:email) #=> [{error: :taken}]
21
+ module Iry
22
+ VERSION = T.let(File.read(File.expand_path("../../VERSION", __dir__)).strip.freeze, T.untyped)
23
+
24
+ # _@param_ `klass`
25
+ sig { params(klass: Module).void }
26
+ def self.included(klass); end
27
+
28
+ # Class-level methods available to classes executing `include Iry`
29
+ module Macros
30
+ # Constraints by name
31
+ sig { returns(T::Hash[String, Constraint]) }
32
+ def constraints; end
33
+
34
+ # Tracks check constraint for the given key and convert constraint errors into validation errors
35
+ #
36
+ # _@param_ `key` — key to apply validation errors to
37
+ #
38
+ # _@param_ `message` — the validation error message
39
+ #
40
+ # _@param_ `name` — constraint name. If omitted, it will be inferred using table name + key
41
+ sig { params(key: Symbol, name: T.nilable(String), message: T.any(Symbol, String)).void }
42
+ def check_constraint(key, name: nil, message: :invalid); end
43
+
44
+ # Tracks exclusion constraint for the given key and convert constraint errors into validation errors
45
+ #
46
+ # _@param_ `key` — key to apply validation errors to
47
+ #
48
+ # _@param_ `message` — the validation error message
49
+ #
50
+ # _@param_ `name` — constraint name. If omitted, it will be inferred using table name + key
51
+ sig { params(key: Symbol, name: T.nilable(String), message: T.any(Symbol, String)).void }
52
+ def exclusion_constraint(key, name: nil, message: :taken); end
53
+
54
+ # Tracks foreign key constraint for the given key (or keys) and convert constraint errors into validation errors
55
+ #
56
+ # _@param_ `key_or_keys` — key or array of keys to track the foreign key constraint of
57
+ #
58
+ # _@param_ `message` — the validation error message
59
+ #
60
+ # _@param_ `name` — constraint name. If omitted, it will be inferred using table name + keys
61
+ #
62
+ # _@param_ `error_key` — key to which the validation error will be applied to. If omitted, it will be applied to the first key
63
+ sig do
64
+ params(
65
+ key_or_keys: T.any(Symbol, T::Array[Symbol]),
66
+ name: T.nilable(String),
67
+ message: T.any(Symbol, String),
68
+ error_key: T.nilable(Symbol)
69
+ ).void
70
+ end
71
+ def foreign_key_constraint(key_or_keys, name: nil, message: :required, error_key: nil); end
72
+
73
+ # Tracks uniqueness constraint for the given key (or keys) and convert constraint errors into validation errors
74
+ #
75
+ # _@param_ `key_or_keys` — key or array of keys to track the uniqueness constraint of
76
+ #
77
+ # _@param_ `message` — the validation error message
78
+ #
79
+ # _@param_ `name` — constraint name. If omitted, it will be inferred using table name + keys
80
+ #
81
+ # _@param_ `error_key` — key to which the validation error will be applied to. If omitted, it will be applied to the first key
82
+ sig do
83
+ params(
84
+ key_or_keys: T.any(Symbol, T::Array[Symbol]),
85
+ name: T.nilable(String),
86
+ message: T.any(Symbol, String),
87
+ error_key: T.nilable(Symbol)
88
+ ).void
89
+ end
90
+ def unique_constraint(key_or_keys, name: nil, message: :taken, error_key: nil); end
91
+ end
92
+
93
+ module Handlers
94
+ # Interface for handlers of different database types
95
+ # @abstract
96
+ module Handler
97
+ # sord warn - ActiveRecord::StatementInvalid wasn't able to be resolved to a constant in this project
98
+ # _@param_ `err` — possible constraint error to handle
99
+ #
100
+ # _@return_ — true if this database handler is the correct one for this exception
101
+ sig { params(err: ActiveRecord::StatementInvalid).returns(T::Boolean) }
102
+ def handle?(err); end
103
+
104
+ # sord warn - ActiveRecord::StatementInvalid wasn't able to be resolved to a constant in this project
105
+ # _@param_ `err` — possible constraint error to handle
106
+ #
107
+ # _@param_ `model`
108
+ #
109
+ # _@return_ — true if this database handler handled the constraint error
110
+ sig { params(err: ActiveRecord::StatementInvalid, model: Model).returns(T::Boolean) }
111
+ def handle(err, model); end
112
+ end
113
+
114
+ # Interface of the model class. This class is usually inherits from {ActiveRecord::Base}
115
+ # @abstract
116
+ module ModelClass
117
+ sig { returns(String) }
118
+ def table_name; end
119
+
120
+ sig { returns(T::Hash[String, Constraint]) }
121
+ def constraints; end
122
+ end
123
+
124
+ # Interface of the model that should be used to handle constraints.
125
+ # This object is an instance of {ActiveRecord::Base}
126
+ # @abstract
127
+ module Model
128
+ # sord warn - ActiveModel::Errors wasn't able to be resolved to a constant in this project
129
+ sig { returns(ActiveModel::Errors) }
130
+ def errors; end
131
+
132
+ sig { returns(ModelClass) }
133
+ def class; end
134
+ end
135
+
136
+ # PostgreSQL handler through `pg` gem
137
+ # @private
138
+ module PG
139
+ extend Iry::Handlers::PG
140
+ REGEX = T.let(%r{
141
+ (?:
142
+ unique\sconstraint|
143
+ check\sconstraint|
144
+ exclusion\sconstraint|
145
+ foreign\skey\sconstraint
146
+ )
147
+ \s"(.+)"
148
+ }x, T.untyped)
149
+
150
+ # sord warn - ActiveRecord::StatementInvalid wasn't able to be resolved to a constant in this project
151
+ # When true, the handler is able to handle this exception, representing a constraint error in PostgreSQL.
152
+ # This method must ensure not to raise exception in case the postgresql adapter is missing and as such, the
153
+ # postgres constant is undefined
154
+ #
155
+ # _@param_ `err`
156
+ sig { params(err: ActiveRecord::StatementInvalid).returns(T::Boolean) }
157
+ def handle?(err); end
158
+
159
+ # sord warn - ActiveRecord::StatementInvalid wasn't able to be resolved to a constant in this project
160
+ # Appends constraint errors as model errors
161
+ #
162
+ # _@param_ `err`
163
+ #
164
+ # _@param_ `model` — should inherit {ActiveRecord::Base} and`include Iry` to match the interface
165
+ sig { params(err: ActiveRecord::StatementInvalid, model: Model).void }
166
+ def handle(err, model); end
167
+
168
+ # sord warn - ActiveRecord::StatementInvalid wasn't able to be resolved to a constant in this project
169
+ # When true, the handler is able to handle this exception, representing a constraint error in PostgreSQL.
170
+ # This method must ensure not to raise exception in case the postgresql adapter is missing and as such, the
171
+ # postgres constant is undefined
172
+ #
173
+ # _@param_ `err`
174
+ sig { params(err: ActiveRecord::StatementInvalid).returns(T::Boolean) }
175
+ def self.handle?(err); end
176
+
177
+ # sord warn - ActiveRecord::StatementInvalid wasn't able to be resolved to a constant in this project
178
+ # Appends constraint errors as model errors
179
+ #
180
+ # _@param_ `err`
181
+ #
182
+ # _@param_ `model` — should inherit {ActiveRecord::Base} and`include Iry` to match the interface
183
+ sig { params(err: ActiveRecord::StatementInvalid, model: Model).void }
184
+ def self.handle(err, model); end
185
+ end
186
+
187
+ # Catch-all handler for unrecognized database adapters
188
+ # @private
189
+ module Null
190
+ extend Iry::Handlers::Null
191
+
192
+ # sord warn - ActiveRecord::StatementInvalid wasn't able to be resolved to a constant in this project
193
+ # Returns always true, catching any unhandled database exception
194
+ #
195
+ # _@param_ `err`
196
+ sig { params(err: T.any(StandardError, ActiveRecord::StatementInvalid)).returns(T::Boolean) }
197
+ def handle?(err); end
198
+
199
+ # sord warn - ActiveRecord::StatementInvalid wasn't able to be resolved to a constant in this project
200
+ # Return always false, failing to handle any constraint
201
+ #
202
+ # _@param_ `err`
203
+ #
204
+ # _@param_ `model` — should inherit {ActiveRecord::Base} and`include Iry` to match the interface
205
+ sig { params(err: ActiveRecord::StatementInvalid, model: Model).returns(T::Boolean) }
206
+ def handle(err, model); end
207
+
208
+ # sord warn - ActiveRecord::StatementInvalid wasn't able to be resolved to a constant in this project
209
+ # Returns always true, catching any unhandled database exception
210
+ #
211
+ # _@param_ `err`
212
+ sig { params(err: T.any(StandardError, ActiveRecord::StatementInvalid)).returns(T::Boolean) }
213
+ def self.handle?(err); end
214
+
215
+ # sord warn - ActiveRecord::StatementInvalid wasn't able to be resolved to a constant in this project
216
+ # Return always false, failing to handle any constraint
217
+ #
218
+ # _@param_ `err`
219
+ #
220
+ # _@param_ `model` — should inherit {ActiveRecord::Base} and`include Iry` to match the interface
221
+ sig { params(err: ActiveRecord::StatementInvalid, model: Model).returns(T::Boolean) }
222
+ def self.handle(err, model); end
223
+ end
224
+ end
225
+
226
+ # Main function to kick-off **Iry** constraint-checking mechanism
227
+ # If interested in adding support for other databases beside Postgres, modify this file.
228
+ # @private
229
+ module Callbacks
230
+ extend Iry::Callbacks
231
+
232
+ # _@param_ `model`
233
+ sig { params(model: Handlers::Model).void }
234
+ def around_save(model); end
235
+
236
+ # _@param_ `model`
237
+ sig { params(model: Handlers::Model).void }
238
+ def self.around_save(model); end
239
+ end
240
+
241
+ # Interface representing a constraint.
242
+ # A constraint has a name and can apply errors to an object inheriting from {ActiveRecord::Base}
243
+ # @abstract
244
+ module Constraint
245
+ # Sets validation errors on the model
246
+ #
247
+ # _@param_ `model`
248
+ sig { params(model: Handlers::Model).void }
249
+ def apply(model); end
250
+
251
+ # Name of the constraint to be caught from the database
252
+ sig { returns(String) }
253
+ def name; end
254
+
255
+ # Message to be attached as validation error to the model
256
+ # (see Handlers::Model)
257
+ sig { returns(T.any(Symbol, String)) }
258
+ def message; end
259
+
260
+ class Check
261
+ # Infers the check constraint name based on key and table name
262
+ #
263
+ # _@param_ `key`
264
+ #
265
+ # _@param_ `table_name`
266
+ sig { params(key: Symbol, table_name: String).returns(String) }
267
+ def self.infer_name(key, table_name); end
268
+
269
+ # _@param_ `key` — key to apply error message for check constraint to
270
+ #
271
+ # _@param_ `message` — the validation error message
272
+ #
273
+ # _@param_ `name` — constraint name
274
+ sig { params(key: Symbol, name: String, message: T.any(Symbol, String)).void }
275
+ def initialize(key, name:, message: :invalid); end
276
+
277
+ # _@param_ `model`
278
+ sig { params(model: Handlers::Model).void }
279
+ def apply(model); end
280
+
281
+ sig { returns(Symbol) }
282
+ attr_accessor :key
283
+
284
+ sig { returns(T.any(Symbol, String)) }
285
+ attr_accessor :message
286
+
287
+ sig { returns(String) }
288
+ attr_accessor :name
289
+ end
290
+
291
+ class Unique
292
+ # Infers the unique constraint name based on keys and table name
293
+ #
294
+ # _@param_ `keys`
295
+ #
296
+ # _@param_ `table_name`
297
+ sig { params(keys: T::Array[Symbol], table_name: String).returns(String) }
298
+ def self.infer_name(keys, table_name); end
299
+
300
+ # _@param_ `keys` — array of keys to track the uniqueness constraint of
301
+ #
302
+ # _@param_ `message` — the validation error message
303
+ #
304
+ # _@param_ `name` — constraint name
305
+ #
306
+ # _@param_ `error_key` — key to which the validation error will be applied to
307
+ sig do
308
+ params(
309
+ keys: T::Array[Symbol],
310
+ name: String,
311
+ error_key: Symbol,
312
+ message: T.any(Symbol, String)
313
+ ).void
314
+ end
315
+ def initialize(keys, name:, error_key:, message: :taken); end
316
+
317
+ # _@param_ `model`
318
+ sig { params(model: Handlers::Model).void }
319
+ def apply(model); end
320
+
321
+ sig { returns(T::Array[Symbol]) }
322
+ attr_accessor :keys
323
+
324
+ sig { returns(T.any(Symbol, String)) }
325
+ attr_accessor :message
326
+
327
+ sig { returns(String) }
328
+ attr_accessor :name
329
+
330
+ sig { returns(Symbol) }
331
+ attr_accessor :error_key
332
+ end
333
+
334
+ class Exclusion
335
+ # Infers the exclusion constraint name based on key and table name
336
+ #
337
+ # _@param_ `key`
338
+ #
339
+ # _@param_ `table_name`
340
+ sig { params(key: Symbol, table_name: String).returns(String) }
341
+ def self.infer_name(key, table_name); end
342
+
343
+ # _@param_ `key` — key to apply error message for exclusion constraint to
344
+ #
345
+ # _@param_ `message` — the validation error message
346
+ #
347
+ # _@param_ `name` — constraint name
348
+ sig { params(key: Symbol, name: String, message: T.any(Symbol, String)).void }
349
+ def initialize(key, name:, message: :taken); end
350
+
351
+ # _@param_ `model`
352
+ sig { params(model: Handlers::Model).void }
353
+ def apply(model); end
354
+
355
+ sig { returns(Symbol) }
356
+ attr_accessor :key
357
+
358
+ sig { returns(T.any(Symbol, String)) }
359
+ attr_accessor :message
360
+
361
+ sig { returns(String) }
362
+ attr_accessor :name
363
+ end
364
+
365
+ class ForeignKey
366
+ # Infers the unique constraint name based on keys and table name
367
+ #
368
+ # _@param_ `keys`
369
+ #
370
+ # _@param_ `table_name`
371
+ sig { params(keys: T::Array[Symbol], table_name: String).returns(String) }
372
+ def self.infer_name(keys, table_name); end
373
+
374
+ # _@param_ `keys` — array of keys to track the uniqueness constraint of
375
+ #
376
+ # _@param_ `message` — the validation error message
377
+ #
378
+ # _@param_ `name` — constraint name
379
+ #
380
+ # _@param_ `error_key` — key to which the validation error will be applied to
381
+ sig do
382
+ params(
383
+ keys: T::Array[Symbol],
384
+ name: String,
385
+ error_key: Symbol,
386
+ message: T.any(Symbol, String)
387
+ ).void
388
+ end
389
+ def initialize(keys, name:, error_key:, message: :required); end
390
+
391
+ # _@param_ `model`
392
+ sig { params(model: Handlers::Model).void }
393
+ def apply(model); end
394
+
395
+ sig { returns(T::Array[Symbol]) }
396
+ attr_accessor :keys
397
+
398
+ sig { returns(T.any(Symbol, String)) }
399
+ attr_accessor :message
400
+
401
+ sig { returns(String) }
402
+ attr_accessor :name
403
+
404
+ sig { returns(Symbol) }
405
+ attr_accessor :error_key
406
+ end
407
+ end
408
+ end