data_model 0.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +6 -2
  3. data/.rubocop.yml +11 -2
  4. data/.ruby-version +2 -0
  5. data/Gemfile.lock +91 -54
  6. data/Guardfile +20 -0
  7. data/Rakefile +32 -0
  8. data/data_model.gemspec +52 -0
  9. data/lib/data_model/boolean.rb +7 -0
  10. data/lib/data_model/builtin/array.rb +73 -0
  11. data/lib/data_model/builtin/big_decimal.rb +64 -0
  12. data/lib/data_model/builtin/boolean.rb +37 -0
  13. data/lib/data_model/builtin/date.rb +60 -0
  14. data/lib/data_model/builtin/float.rb +64 -0
  15. data/lib/data_model/builtin/hash.rb +119 -0
  16. data/lib/data_model/builtin/integer.rb +64 -0
  17. data/lib/data_model/builtin/string.rb +88 -0
  18. data/lib/data_model/builtin/symbol.rb +64 -0
  19. data/lib/data_model/builtin/time.rb +60 -0
  20. data/lib/data_model/builtin.rb +23 -0
  21. data/lib/data_model/error.rb +107 -0
  22. data/lib/data_model/errors.rb +296 -0
  23. data/lib/data_model/fixtures/array.rb +61 -0
  24. data/lib/data_model/fixtures/big_decimal.rb +55 -0
  25. data/lib/data_model/fixtures/boolean.rb +35 -0
  26. data/lib/data_model/fixtures/date.rb +53 -0
  27. data/lib/data_model/fixtures/example.rb +29 -0
  28. data/lib/data_model/fixtures/float.rb +53 -0
  29. data/lib/data_model/fixtures/hash.rb +66 -0
  30. data/lib/data_model/fixtures/integer.rb +53 -0
  31. data/lib/data_model/fixtures/string.rb +110 -0
  32. data/lib/data_model/fixtures/symbol.rb +56 -0
  33. data/lib/data_model/fixtures/time.rb +53 -0
  34. data/lib/data_model/logging.rb +23 -0
  35. data/lib/data_model/model.rb +21 -44
  36. data/lib/data_model/scanner.rb +92 -56
  37. data/lib/data_model/testing/minitest.rb +79 -0
  38. data/lib/data_model/testing.rb +6 -0
  39. data/lib/data_model/type.rb +41 -39
  40. data/lib/data_model/type_registry.rb +68 -0
  41. data/lib/data_model/version.rb +3 -1
  42. data/lib/data_model.rb +32 -16
  43. data/sorbet/config +4 -0
  44. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  45. data/sorbet/rbi/gems/minitest@5.18.0.rbi +1491 -0
  46. data/sorbet/rbi/gems/zeitwerk.rbi +196 -0
  47. data/sorbet/rbi/gems/zeitwerk@2.6.7.rbi +966 -0
  48. data/sorbet/rbi/todo.rbi +5 -0
  49. data/sorbet/tapioca/config.yml +13 -0
  50. data/sorbet/tapioca/require.rb +4 -0
  51. metadata +139 -17
  52. data/config/sus.rb +0 -2
  53. data/fixtures/schema.rb +0 -14
  54. data/lib/data_model/registry.rb +0 -44
@@ -0,0 +1,296 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ # Provide Error building functionality as a mixin
5
+ module Errors
6
+ include Kernel
7
+ extend T::Sig
8
+
9
+ TTemporal = T.type_alias { T.any(::Date, ::Time, ::DateTime) }
10
+
11
+ ## Constructors
12
+
13
+ # Type error applies when a value is not of the expected type
14
+ sig { params(cls: T.class_of(Object), value: Object).returns(TError) }
15
+ def type_error(cls, value)
16
+ [:type, [cls, value]]
17
+ end
18
+
19
+ # Coerce error applies when a value cannot be coerced to the expected type
20
+ sig { params(cls: T.class_of(Object), value: Object).returns(TError) }
21
+ def coerce_error(cls, value)
22
+ [:coerce, [cls, value]]
23
+ end
24
+
25
+ # Missing error applies when a value is missing
26
+ sig { params(cls: T.class_of(Object)).returns(TError) }
27
+ def missing_error(cls)
28
+ [:missing, cls]
29
+ end
30
+
31
+ # Inclusion error applies when a value is not in a set of allowed values
32
+ sig { params(set: T::Array[T.any(Symbol, String)]).returns(TError) }
33
+ def inclusion_error(set)
34
+ [:inclusion, set]
35
+ end
36
+
37
+ # Exclusive error applies when a value is in a set of disallowed values
38
+ sig { params(set: T::Array[T.any(Symbol, String)]).returns(TError) }
39
+ def exclusion_error(set)
40
+ [:exclusion, set]
41
+ end
42
+
43
+ # Blank error applies when a value is blank
44
+ sig { returns(TError) }
45
+ def blank_error
46
+ [:blank, nil]
47
+ end
48
+
49
+ # Extra keys error applies when a hash has extra keys
50
+ sig { params(keys: T::Array[Symbol]).returns(TError) }
51
+ def extra_keys_error(keys)
52
+ [:extra_keys, keys]
53
+ end
54
+
55
+ # Min applies when value is less then the minimum
56
+ sig { params(min: Numeric, val: Numeric).returns(TError) }
57
+ def min_error(min, val)
58
+ [:min, [min, val]]
59
+ end
60
+
61
+ # Max applies when value is less then the minimum
62
+ sig { params(min: Numeric, val: Numeric).returns(TError) }
63
+ def max_error(min, val)
64
+ [:max, [min, val]]
65
+ end
66
+
67
+ # Earliest applies when value is earlier then earliest
68
+ sig { params(earliest: TTemporal, val: TTemporal).returns(TError) }
69
+ def earliest_error(earliest, val)
70
+ [:earliest, [earliest, val]]
71
+ end
72
+
73
+ # Latest applies when value is earlier then earliest
74
+ sig { params(latest: TTemporal, val: TTemporal).returns(TError) }
75
+ def latest_error(latest, val)
76
+ [:latest, [latest, val]]
77
+ end
78
+
79
+ # Format applies when value does not match a format
80
+ sig { params(format: Object, val: String).returns(TError) }
81
+ def format_error(format, val)
82
+ [:format, [format, val]]
83
+ end
84
+
85
+ ## Messages
86
+
87
+ # Generate a message for a type error
88
+ sig { params(cls: T.class_of(Object), value: Object).returns(String) }
89
+ def type_error_message(cls, value)
90
+ "#{value.inspect} is not a #{cls.name}, it is a #{value.class.name}"
91
+ end
92
+
93
+ # Generate a message for a coerce error
94
+ sig { params(cls: T.class_of(Object), value: Object).returns(String) }
95
+ def coerce_error_message(cls, value)
96
+ "cannot be coerced to #{cls.name}, it is a #{value.class.name}"
97
+ end
98
+
99
+ # Generate a message for a missing error
100
+ sig { params(cls: T.class_of(Object)).returns(String) }
101
+ def missing_error_message(cls)
102
+ "missing value, expected a #{cls.name}"
103
+ end
104
+
105
+ # Generate a message for an inclusion error
106
+ sig { params(set: T::Array[Symbol]).returns(String) }
107
+ def inclusion_error_message(set)
108
+ "must be one of #{set.join(', ')}"
109
+ end
110
+
111
+ # Generate a message for an exclusion error
112
+ sig { params(set: T::Array[Symbol]).returns(String) }
113
+ def exclusion_error_message(set)
114
+ "must not be one of #{set.join(', ')}"
115
+ end
116
+
117
+ # Generate a message for a blank error
118
+ sig { returns(String) }
119
+ def blank_error_message
120
+ "cannot be blank"
121
+ end
122
+
123
+ # Generate a message for an extra keys error
124
+ sig { params(keys: T::Array[Symbol]).returns(String) }
125
+ def extra_keys_error_message(keys)
126
+ "more elements found in closed hash then specified children: #{keys.join(', ')}"
127
+ end
128
+
129
+ # Generate a message for a min error
130
+ sig { params(min: Numeric, val: Numeric).returns(String) }
131
+ def min_error_message(min, val)
132
+ "value is less than the minimum of #{min}, it is #{val}"
133
+ end
134
+
135
+ # Generate a message for a min error
136
+ sig { params(max: Numeric, val: Numeric).returns(String) }
137
+ def max_error_message(max, val)
138
+ "value is more than the maximum of #{max}, it is #{val}"
139
+ end
140
+
141
+ # Generate a message for a value that occurs earlier then the specified earliest point
142
+ sig { params(earliest: TTemporal, val: TTemporal).returns(String) }
143
+ def early_error_message(earliest, val)
144
+ "value #{val} is before #{earliest}"
145
+ end
146
+
147
+ # Generate a message for a value that occurs later then the specified latest point
148
+ sig { params(latest: TTemporal, val: TTemporal).returns(String) }
149
+ def late_error_message(latest, val)
150
+ "value #{val} is after #{latest}"
151
+ end
152
+
153
+ # Generate a message for a value that does not match the format
154
+ sig { params(format: Object, val: String).returns(String) }
155
+ def format_error_message(format, val)
156
+ "value #{val} does not match format #{format}"
157
+ end
158
+
159
+ ## API
160
+ # TODO: split this file
161
+
162
+ TErrorMessageBuilder = T.type_alias { T.proc.params(ctx: T.untyped).returns(String) }
163
+
164
+ # Register a custom error message for use with custom errors
165
+ sig { params(type: Symbol, block: TErrorMessageBuilder).void }
166
+ def register_error_message(type, &block)
167
+ error_message_builders[type] = block
168
+ end
169
+
170
+ TErrorMessages = T.type_alias { T::Hash[Symbol, TErrorMessageBuilder] }
171
+ TClassValueCtx = T.type_alias { [T.class_of(Object), Object] }
172
+ TClassCtx = T.type_alias { T.class_of(Object) }
173
+ TSetCtx = T.type_alias { T::Array[Symbol] }
174
+ TWithinCtx = T.type_alias { [Numeric, Numeric] }
175
+ TWithinTemporalCtx = T.type_alias { [TTemporal, TTemporal] }
176
+ TFormatCtx = T.type_alias { [Object, String] }
177
+
178
+ # Get the error message builders
179
+ sig { returns(TErrorMessages) }
180
+ def error_message_builders
181
+ if @error_messages.nil?
182
+ @error_messages ||= T.let({}, T.nilable(TErrorMessages))
183
+
184
+ # wire up defaults
185
+
186
+ register_error_message(:type) do |ctx|
187
+ cls, val = T.let(ctx, TClassValueCtx)
188
+ type_error_message(cls, val)
189
+ end
190
+
191
+ register_error_message(:coerce) do |ctx|
192
+ cls, val = T.let(ctx, TClassValueCtx)
193
+ coerce_error_message(cls, val)
194
+ end
195
+
196
+ register_error_message(:missing) do |ctx|
197
+ cls = T.let(ctx, TClassCtx)
198
+ missing_error_message(cls)
199
+ end
200
+
201
+ register_error_message(:inclusion) do |ctx|
202
+ set = T.let(ctx, TSetCtx)
203
+ inclusion_error_message(set)
204
+ end
205
+
206
+ register_error_message(:exclusion) do |ctx|
207
+ set = T.let(ctx, TSetCtx)
208
+ exclusion_error_message(set)
209
+ end
210
+
211
+ register_error_message(:extra_keys) do |ctx|
212
+ set = T.let(ctx, TSetCtx)
213
+ extra_keys_error_message(set)
214
+ end
215
+
216
+ register_error_message(:min) do |ctx|
217
+ min, val = T.let(ctx, TWithinCtx)
218
+ min_error_message(min, val)
219
+ end
220
+
221
+ register_error_message(:max) do |ctx|
222
+ max, val = T.let(ctx, TWithinCtx)
223
+ max_error_message(max, val)
224
+ end
225
+
226
+ register_error_message(:earliest) do |ctx|
227
+ earliest, val = T.let(ctx, TWithinTemporalCtx)
228
+ early_error_message(earliest, val)
229
+ end
230
+
231
+ register_error_message(:latest) do |ctx|
232
+ latest, val = T.let(ctx, TWithinTemporalCtx)
233
+ late_error_message(latest, val)
234
+ end
235
+
236
+ register_error_message(:blank) do
237
+ blank_error_message
238
+ end
239
+
240
+ register_error_message(:format) do |ctx|
241
+ format, val = T.let(ctx, TFormatCtx)
242
+ format_error_message(format, val)
243
+ end
244
+ end
245
+
246
+ @error_messages
247
+ end
248
+
249
+ # Build the error message for a given error
250
+ sig { params(error: TError).returns(String) }
251
+ def error_message(error)
252
+ type = T.let(error[0], Symbol)
253
+ ctx = T.let(error[1], T.untyped)
254
+
255
+ builder = error_message_builders[type]
256
+
257
+ if builder.nil?
258
+ raise "no error message builder for #{type}"
259
+ end
260
+
261
+ builder.call(ctx)
262
+ end
263
+
264
+ # TODO: separate builders from other use cases for this mixin
265
+ # Build error messages from error object
266
+ sig { params(error: Error).returns(T::Hash[Symbol, T::Array[String]]) }
267
+ def error_messages(error)
268
+ error.to_h.transform_values do |error_list|
269
+ error_list.map { |e| error_message(e) }
270
+ end
271
+ end
272
+
273
+ sig { params(error: Error, from: T.class_of(Object), to: T.class_of(Object)).void }
274
+ def set_error_class(error, from, to)
275
+ error.transform_context do |ctx, type|
276
+ case type
277
+ when :type, :coerce
278
+ cls, val = T.cast(ctx, TClassValueCtx)
279
+ if cls == from
280
+ [to, val]
281
+ else
282
+ [cls, val]
283
+ end
284
+ when :missing
285
+ if ctx == from
286
+ [to, val]
287
+ else
288
+ [cls, val]
289
+ end
290
+ else
291
+ [cls, val]
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,61 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ module Fixtures::Array
5
+ extend self
6
+ extend T::Sig
7
+ include Fixtures
8
+
9
+ sig { returns(Example) }
10
+ def string_array
11
+ Example.new(
12
+ [:array, :string],
13
+ variants: {
14
+ strings: ["a", "b", "c"],
15
+ string: ["a", ["a"]],
16
+ number: [1, ["1"]],
17
+ missing: nil,
18
+ numbers: [[1, 2, 3], ["1", "2", "3"]],
19
+ other_type: Object.new
20
+ },
21
+ )
22
+ end
23
+
24
+ sig { returns(Example) }
25
+ def wrapping_string_array
26
+ Example.new(
27
+ [:array, { wrap_single_value: true }, :string],
28
+ variants: {
29
+ strings: ["a", "b", "c"],
30
+ string: ["a", ["a"]],
31
+ number: [1, ["1"]],
32
+ missing: nil,
33
+ numbers: [[1, 2, 3], ["1", "2", "3"]],
34
+ other_type: Object.new
35
+ },
36
+ )
37
+ end
38
+
39
+ sig { returns(Example) }
40
+ def optional_string_array
41
+ Example.new(
42
+ [:array, { optional: true }, :string],
43
+ variants: {
44
+ strings: ["a", "b", "c"],
45
+ missing: nil
46
+ },
47
+ )
48
+ end
49
+
50
+ sig { returns(Example) }
51
+ def array_optional_string
52
+ Example.new(
53
+ [:array, [:string, { optional: true }]],
54
+ variants: {
55
+ optional_strings: ["a", nil, "c"],
56
+ numbers: [1, nil, 3]
57
+ },
58
+ )
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,55 @@
1
+ # typed: strict
2
+
3
+ require "bigdecimal/util"
4
+
5
+ module DataModel
6
+ module Fixtures::BigDecimal
7
+ include Fixtures
8
+ extend T::Sig
9
+ extend self
10
+
11
+ sig { returns(Example) }
12
+ def simple
13
+ Example.new(
14
+ [:decimal],
15
+ variants: {
16
+ valid: 5.to_d,
17
+ missing: nil,
18
+ string: ["5", 5.to_d]
19
+ },
20
+ )
21
+ end
22
+
23
+ sig { returns(Example) }
24
+ def optional
25
+ Example.new(
26
+ [:decimal, { optional: true }],
27
+ variants: {
28
+ missing: nil
29
+ },
30
+ )
31
+ end
32
+
33
+ sig { returns(Example) }
34
+ def min
35
+ Example.new(
36
+ [:decimal, { min: 5 }],
37
+ variants: {
38
+ bigger: 6.to_d,
39
+ smaller: 4.to_d
40
+ },
41
+ )
42
+ end
43
+
44
+ sig { returns(Example) }
45
+ def max
46
+ Example.new(
47
+ [:decimal, { max: 5 }],
48
+ variants: {
49
+ bigger: 6.to_d,
50
+ smaller: 4.to_d
51
+ },
52
+ )
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,35 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ module Fixtures::Boolean
5
+ extend T::Sig
6
+ extend self
7
+ include Fixtures
8
+
9
+ sig { returns(Example) }
10
+ def simple
11
+ Example.new(
12
+ [:boolean],
13
+ variants: {
14
+ true: true,
15
+ false: false,
16
+ string: ["true", true],
17
+ missing: nil
18
+ },
19
+ )
20
+ end
21
+
22
+ sig { returns(Example) }
23
+ def optional
24
+ Example.new(
25
+ [:boolean, { optional: true }],
26
+ variants: {
27
+ true: true,
28
+ false: false,
29
+ string: "true",
30
+ missing: nil
31
+ },
32
+ )
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,53 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ module Fixtures::Date
5
+ extend T::Sig
6
+ extend self
7
+ include Fixtures
8
+
9
+ sig { returns(::Date) }
10
+ def earliest_date
11
+ return ::Date.today - 1
12
+ end
13
+
14
+ sig { returns(::Date) }
15
+ def latest_date
16
+ return ::Date.today + 1
17
+ end
18
+
19
+ sig { returns(T::Hash[Symbol, Object]) }
20
+ def variants
21
+ today = ::Date.today
22
+
23
+ {
24
+ date: today,
25
+ string: [today.to_s, today],
26
+ invalid: "invalid",
27
+ early: earliest_date - 1,
28
+ late: latest_date + 1,
29
+ missing: nil
30
+ }
31
+ end
32
+
33
+ sig { returns(Example) }
34
+ def simple
35
+ Example.new([:date], variants:)
36
+ end
37
+
38
+ sig { returns(Example) }
39
+ def optional
40
+ Example.new([:date, { optional: true }], variants:)
41
+ end
42
+
43
+ sig { returns(Example) }
44
+ def earliest
45
+ Example.new([:date, { earliest: earliest_date }], variants:)
46
+ end
47
+
48
+ sig { returns(Example) }
49
+ def latest
50
+ Example.new([:date, { latest: latest_date }], variants:)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ class Fixtures::Example
5
+ extend T::Sig
6
+
7
+ sig { params(schema: TSchema, variants: T::Hash[::Symbol, Object]).void }
8
+ def initialize(schema, variants:)
9
+ @schema = schema
10
+ @variants = variants
11
+ end
12
+
13
+ sig { returns(Model) }
14
+ def model
15
+ DataModel.define(@schema)
16
+ end
17
+
18
+ sig { params(type: Symbol).returns([Model, Object]) }
19
+ def [](type)
20
+ if !@variants.key?(type)
21
+ raise "#{type} is not a defined variant: #{@variants}"
22
+ end
23
+
24
+ result = @variants.fetch(type)
25
+
26
+ return [model, result]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,53 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ module Fixtures::Float
5
+ include Fixtures
6
+ extend T::Sig
7
+ extend self
8
+
9
+ sig { returns(Example) }
10
+ def simple
11
+ Example.new(
12
+ [:float],
13
+ variants: {
14
+ valid: 5.0,
15
+ missing: nil,
16
+ string: ["5", 5.0]
17
+ },
18
+ )
19
+ end
20
+
21
+ sig { returns(Example) }
22
+ def optional
23
+ Example.new(
24
+ [:float, { optional: true }],
25
+ variants: {
26
+ missing: nil
27
+ },
28
+ )
29
+ end
30
+
31
+ sig { returns(Example) }
32
+ def min
33
+ Example.new(
34
+ [:float, { min: 5 }],
35
+ variants: {
36
+ bigger: 6.0,
37
+ smaller: 4.0
38
+ },
39
+ )
40
+ end
41
+
42
+ sig { returns(Example) }
43
+ def max
44
+ Example.new(
45
+ [:float, { max: 5.0 }],
46
+ variants: {
47
+ bigger: 6.0,
48
+ smaller: 4.0
49
+ },
50
+ )
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,66 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ module Fixtures::Hash
5
+ include Fixtures
6
+ extend T::Sig
7
+ extend self
8
+
9
+ TContact = T.type_alias { T::Hash[Symbol, T.untyped] }
10
+
11
+ sig { returns(TContact) }
12
+ def example_contact
13
+ {
14
+ first_name: "foo",
15
+ last_name: "bar",
16
+ email: "foo@bar.com"
17
+ }
18
+ end
19
+
20
+ sig { returns(Example) }
21
+ def contact
22
+ Example.new(
23
+ [:hash,
24
+ [:first_name, :string],
25
+ [:last_name, :string, { optional: true }],
26
+ [:email, :string]],
27
+ variants: {
28
+ valid: example_contact,
29
+ missing: nil,
30
+ coercible: example_contact.to_a,
31
+ missing_email: example_contact.tap { |h| T.cast(h, TContact).delete(:email) },
32
+ invalid_field: example_contact.merge(email: 123),
33
+ other_type: []
34
+ },
35
+ )
36
+ end
37
+
38
+ sig { returns(Example) }
39
+ def optional_contact
40
+ Example.new(
41
+ [:hash, { optional: true },
42
+ [:first_name, :string],
43
+ [:last_name, :string, { optional: true }],
44
+ [:email, :string]],
45
+ variants: {
46
+ valid: example_contact,
47
+ missing: nil
48
+ },
49
+ )
50
+ end
51
+
52
+ sig { returns(Example) }
53
+ def closed_contact
54
+ Example.new(
55
+ [:hash, { open: false },
56
+ [:first_name, :string],
57
+ [:last_name, :string, { optional: true }],
58
+ [:email, :string]],
59
+ variants: {
60
+ valid: example_contact,
61
+ extra_keys: example_contact.merge(extra: "keys")
62
+ },
63
+ )
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,53 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ module Fixtures::Integer
5
+ include Fixtures
6
+ extend T::Sig
7
+ extend self
8
+
9
+ sig { returns(Example) }
10
+ def simple
11
+ Example.new(
12
+ [:integer],
13
+ variants: {
14
+ valid: 5,
15
+ missing: nil,
16
+ string: ["5", 5]
17
+ },
18
+ )
19
+ end
20
+
21
+ sig { returns(Example) }
22
+ def optional
23
+ Example.new(
24
+ [:integer, { optional: true }],
25
+ variants: {
26
+ missing: nil
27
+ },
28
+ )
29
+ end
30
+
31
+ sig { returns(Example) }
32
+ def min
33
+ Example.new(
34
+ [:integer, { min: 5 }],
35
+ variants: {
36
+ bigger: 6,
37
+ smaller: 4
38
+ },
39
+ )
40
+ end
41
+
42
+ sig { returns(Example) }
43
+ def max
44
+ Example.new(
45
+ [:integer, { max: 5 }],
46
+ variants: {
47
+ bigger: 6,
48
+ smaller: 4
49
+ },
50
+ )
51
+ end
52
+ end
53
+ end