data_model 0.0.1 → 0.1.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.
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 +67 -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 +278 -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 +77 -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,278 @@
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
+ ## Messages
80
+
81
+ # Generate a message for a type error
82
+ sig { params(cls: T.class_of(Object), value: Object).returns(String) }
83
+ def type_error_message(cls, value)
84
+ "#{value.inspect} is not a #{cls.name}, it is a #{value.class.name}"
85
+ end
86
+
87
+ # Generate a message for a coerce error
88
+ sig { params(cls: T.class_of(Object), value: Object).returns(String) }
89
+ def coerce_error_message(cls, value)
90
+ "cannot be coerced to #{cls.name}, it is a #{value.class.name}"
91
+ end
92
+
93
+ # Generate a message for a missing error
94
+ sig { params(cls: T.class_of(Object)).returns(String) }
95
+ def missing_error_message(cls)
96
+ "missing value, expected a #{cls.name}"
97
+ end
98
+
99
+ # Generate a message for an inclusion error
100
+ sig { params(set: T::Array[Symbol]).returns(String) }
101
+ def inclusion_error_message(set)
102
+ "must be one of #{set.join(', ')}"
103
+ end
104
+
105
+ # Generate a message for an exclusion error
106
+ sig { params(set: T::Array[Symbol]).returns(String) }
107
+ def exclusion_error_message(set)
108
+ "must not be one of #{set.join(', ')}"
109
+ end
110
+
111
+ # Generate a message for a blank error
112
+ sig { returns(String) }
113
+ def blank_error_message
114
+ "cannot be blank"
115
+ end
116
+
117
+ # Generate a message for an extra keys error
118
+ sig { params(keys: T::Array[Symbol]).returns(String) }
119
+ def extra_keys_error_message(keys)
120
+ "more elements found in closed hash then specified children: #{keys.join(', ')}"
121
+ end
122
+
123
+ # Generate a message for a min error
124
+ sig { params(min: Numeric, val: Numeric).returns(String) }
125
+ def min_error_message(min, val)
126
+ "value is less than the minimum of #{min}, it is #{val}"
127
+ end
128
+
129
+ # Generate a message for a min error
130
+ sig { params(max: Numeric, val: Numeric).returns(String) }
131
+ def max_error_message(max, val)
132
+ "value is more than the maximum of #{max}, it is #{val}"
133
+ end
134
+
135
+ # Generate a message for a value that occurs earlier then the specified earliest point
136
+ sig { params(earliest: TTemporal, val: TTemporal).returns(String) }
137
+ def early_error_message(earliest, val)
138
+ "value #{val} is before #{earliest}"
139
+ end
140
+
141
+ # Generate a message for a value that occurs later then the specified latest point
142
+ sig { params(latest: TTemporal, val: TTemporal).returns(String) }
143
+ def late_error_message(latest, val)
144
+ "value #{val} is after #{latest}"
145
+ end
146
+
147
+ ## API
148
+ # TODO: split this file
149
+
150
+ TErrorMessageBuilder = T.type_alias { T.proc.params(ctx: T.untyped).returns(String) }
151
+
152
+ # Register a custom error message for use with custom errors
153
+ sig { params(type: Symbol, block: TErrorMessageBuilder).void }
154
+ def register_error_message(type, &block)
155
+ error_message_builders[type] = block
156
+ end
157
+
158
+ TErrorMessages = T.type_alias { T::Hash[Symbol, TErrorMessageBuilder] }
159
+ TClassValueCtx = T.type_alias { [T.class_of(Object), Object] }
160
+ TClassCtx = T.type_alias { T.class_of(Object) }
161
+ TSetCtx = T.type_alias { T::Array[Symbol] }
162
+ TWithinCtx = T.type_alias { [Numeric, Numeric] }
163
+ TWithinTemporalCtx = T.type_alias { [TTemporal, TTemporal] }
164
+
165
+ # Get the error message builders
166
+ sig { returns(TErrorMessages) }
167
+ def error_message_builders
168
+ if @error_messages.nil?
169
+ @error_messages ||= T.let({}, T.nilable(TErrorMessages))
170
+
171
+ # wire up defaults
172
+
173
+ register_error_message(:type) do |ctx|
174
+ cls, val = T.let(ctx, TClassValueCtx)
175
+ type_error_message(cls, val)
176
+ end
177
+
178
+ register_error_message(:coerce) do |ctx|
179
+ cls, val = T.let(ctx, TClassValueCtx)
180
+ coerce_error_message(cls, val)
181
+ end
182
+
183
+ register_error_message(:missing) do |ctx|
184
+ cls = T.let(ctx, TClassCtx)
185
+ missing_error_message(cls)
186
+ end
187
+
188
+ register_error_message(:inclusion) do |ctx|
189
+ set = T.let(ctx, TSetCtx)
190
+ inclusion_error_message(set)
191
+ end
192
+
193
+ register_error_message(:exclusion) do |ctx|
194
+ set = T.let(ctx, TSetCtx)
195
+ exclusion_error_message(set)
196
+ end
197
+
198
+ register_error_message(:extra_keys) do |ctx|
199
+ set = T.let(ctx, TSetCtx)
200
+ extra_keys_error_message(set)
201
+ end
202
+
203
+ register_error_message(:min) do |ctx|
204
+ min, val = T.let(ctx, TWithinCtx)
205
+ min_error_message(min, val)
206
+ end
207
+
208
+ register_error_message(:max) do |ctx|
209
+ max, val = T.let(ctx, TWithinCtx)
210
+ max_error_message(max, val)
211
+ end
212
+
213
+ register_error_message(:earliest) do |ctx|
214
+ earliest, val = T.let(ctx, TWithinTemporalCtx)
215
+ early_error_message(earliest, val)
216
+ end
217
+
218
+ register_error_message(:latest) do |ctx|
219
+ latest, val = T.let(ctx, TWithinTemporalCtx)
220
+ late_error_message(latest, val)
221
+ end
222
+
223
+ register_error_message(:blank) do
224
+ blank_error_message
225
+ end
226
+ end
227
+
228
+ @error_messages
229
+ end
230
+
231
+ # Build the error message for a given error
232
+ sig { params(error: TError).returns(String) }
233
+ def error_message(error)
234
+ type = T.let(error[0], Symbol)
235
+ ctx = T.let(error[1], T.untyped)
236
+
237
+ builder = error_message_builders[type]
238
+
239
+ if builder.nil?
240
+ raise "no error message builder for #{type}"
241
+ end
242
+
243
+ builder.call(ctx)
244
+ end
245
+
246
+ # TODO: separate builders from other use cases for this mixin
247
+ # Build error messages from error object
248
+ sig { params(error: Error).returns(T::Hash[Symbol, T::Array[String]]) }
249
+ def error_messages(error)
250
+ error.to_h.transform_values do |error_list|
251
+ error_list.map { |e| error_message(e) }
252
+ end
253
+ end
254
+
255
+ sig { params(error: Error, from: T.class_of(Object), to: T.class_of(Object)).void }
256
+ def set_error_class(error, from, to)
257
+ error.transform_context do |ctx, type|
258
+ case type
259
+ when :type, :coerce
260
+ cls, val = T.cast(ctx, TClassValueCtx)
261
+ if cls == from
262
+ [to, val]
263
+ else
264
+ [cls, val]
265
+ end
266
+ when :missing
267
+ if ctx == from
268
+ [to, val]
269
+ else
270
+ [cls, val]
271
+ end
272
+ else
273
+ [cls, val]
274
+ end
275
+ end
276
+ end
277
+ end
278
+ 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