data_model 0.3.0 → 0.5.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -2
  3. data/.shadowenv.d/.gitignore +2 -0
  4. data/.shadowenv.d/550-ruby.lisp +37 -0
  5. data/.solargraph.yml +22 -0
  6. data/Gemfile.lock +38 -3
  7. data/Rakefile +0 -6
  8. data/Steepfile +27 -0
  9. data/data_model.gemspec +2 -2
  10. data/lib/data_model/boolean.rb +0 -2
  11. data/lib/data_model/builtin/array.rb +32 -25
  12. data/lib/data_model/builtin/big_decimal.rb +15 -14
  13. data/lib/data_model/builtin/boolean.rb +10 -7
  14. data/lib/data_model/builtin/date.rb +15 -12
  15. data/lib/data_model/builtin/float.rb +14 -13
  16. data/lib/data_model/builtin/hash.rb +100 -35
  17. data/lib/data_model/builtin/integer.rb +14 -13
  18. data/lib/data_model/builtin/numeric.rb +35 -0
  19. data/lib/data_model/builtin/object.rb +28 -0
  20. data/lib/data_model/builtin/or.rb +73 -0
  21. data/lib/data_model/builtin/string.rb +15 -16
  22. data/lib/data_model/builtin/symbol.rb +14 -13
  23. data/lib/data_model/builtin/time.rb +17 -14
  24. data/lib/data_model/builtin.rb +9 -9
  25. data/lib/data_model/error.rb +33 -33
  26. data/lib/data_model/errors.rb +107 -143
  27. data/lib/data_model/fixtures/array.rb +22 -9
  28. data/lib/data_model/fixtures/big_decimal.rb +9 -7
  29. data/lib/data_model/fixtures/boolean.rb +5 -5
  30. data/lib/data_model/fixtures/date.rb +13 -11
  31. data/lib/data_model/fixtures/example.rb +7 -7
  32. data/lib/data_model/fixtures/float.rb +9 -7
  33. data/lib/data_model/fixtures/hash.rb +22 -10
  34. data/lib/data_model/fixtures/integer.rb +9 -7
  35. data/lib/data_model/fixtures/numeric.rb +31 -0
  36. data/lib/data_model/fixtures/object.rb +27 -0
  37. data/lib/data_model/fixtures/or.rb +29 -0
  38. data/lib/data_model/fixtures/string.rb +15 -32
  39. data/lib/data_model/fixtures/symbol.rb +9 -7
  40. data/lib/data_model/fixtures/time.rb +13 -11
  41. data/lib/data_model/logging.rb +5 -8
  42. data/lib/data_model/model.rb +11 -8
  43. data/lib/data_model/registry.rb +129 -0
  44. data/lib/data_model/scanner.rb +24 -29
  45. data/lib/data_model/struct.rb +112 -0
  46. data/lib/data_model/testing/minitest.rb +33 -9
  47. data/lib/data_model/testing.rb +0 -2
  48. data/lib/data_model/type.rb +39 -23
  49. data/lib/data_model/version.rb +1 -3
  50. data/lib/data_model.rb +10 -19
  51. metadata +24 -21
  52. data/lib/data_model/type_registry.rb +0 -68
  53. data/sorbet/config +0 -4
  54. data/sorbet/rbi/annotations/rainbow.rbi +0 -269
  55. data/sorbet/rbi/gems/minitest@5.18.0.rbi +0 -1491
  56. data/sorbet/rbi/gems/zeitwerk.rbi +0 -196
  57. data/sorbet/rbi/gems/zeitwerk@2.6.7.rbi +0 -966
  58. data/sorbet/rbi/todo.rbi +0 -5
  59. data/sorbet/tapioca/config.yml +0 -13
  60. data/sorbet/tapioca/require.rb +0 -4
@@ -1,22 +1,20 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Provides fixtures for testing date types
4
3
  module Fixtures::Date
5
- extend T::Sig
6
- extend self
7
4
  include Fixtures
5
+ extend self
8
6
 
9
- sig { returns(::Date) }
7
+ # @return [Date] a date that is used by the #earliest example
10
8
  def earliest_date
11
9
  return ::Date.today - 1
12
10
  end
13
11
 
14
- sig { returns(::Date) }
12
+ # @return [Date] a date that is used by the #latest example
15
13
  def latest_date
16
14
  return ::Date.today + 1
17
15
  end
18
16
 
19
- sig { returns(T::Hash[Symbol, Object]) }
17
+ # @return [Hash{Symbol => untyped}] the variants used by each example
20
18
  def variants
21
19
  today = ::Date.today
22
20
 
@@ -30,22 +28,26 @@ module DataModel
30
28
  }
31
29
  end
32
30
 
33
- sig { returns(Example) }
31
+ # A simple date schema
32
+ # @return [Example] the example
34
33
  def simple
35
34
  Example.new([:date], variants:)
36
35
  end
37
36
 
38
- sig { returns(Example) }
37
+ # A date schema that is optional
38
+ # @return [Example] the example
39
39
  def optional
40
40
  Example.new([:date, { optional: true }], variants:)
41
41
  end
42
42
 
43
- sig { returns(Example) }
43
+ # A date schema that has a restriction on the earliest date
44
+ # @return [Example] the example
44
45
  def earliest
45
46
  Example.new([:date, { earliest: earliest_date }], variants:)
46
47
  end
47
48
 
48
- sig { returns(Example) }
49
+ # A date schema that has a restriction on the latest date
50
+ # @return [Example] the example
49
51
  def latest
50
52
  Example.new([:date, { latest: latest_date }], variants:)
51
53
  end
@@ -1,21 +1,21 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # A simple abstraction to handle test data
4
3
  class Fixtures::Example
5
- extend T::Sig
6
-
7
- sig { params(schema: TSchema, variants: T::Hash[::Symbol, Object]).void }
4
+ # @param schema [Array] the schema
5
+ # @param variants [Hash{Symbol => untyped}] the variants used by each example
6
+ # @return [Schema] the schema
8
7
  def initialize(schema, variants:)
9
8
  @schema = schema
10
9
  @variants = variants
11
10
  end
12
11
 
13
- sig { returns(Model) }
12
+ # @return [Model] the model
14
13
  def model
15
14
  DataModel.define(@schema)
16
15
  end
17
16
 
18
- sig { params(type: Symbol).returns([Model, Object]) }
17
+ # @param type [Symbol] the variant to use
18
+ # @return [Array(Model, untyped)] a tuple of [model, variant]
19
19
  def [](type)
20
20
  if !@variants.key?(type)
21
21
  raise "#{type} is not a defined variant: #{@variants}"
@@ -1,12 +1,11 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Test data for float schemas
4
3
  module Fixtures::Float
5
4
  include Fixtures
6
- extend T::Sig
7
5
  extend self
8
6
 
9
- sig { returns(Example) }
7
+ # a simple float example
8
+ # @return [Example] the example
10
9
  def simple
11
10
  Example.new(
12
11
  [:float],
@@ -18,7 +17,8 @@ module DataModel
18
17
  )
19
18
  end
20
19
 
21
- sig { returns(Example) }
20
+ # a float example that is optional
21
+ # @return [Example] the example
22
22
  def optional
23
23
  Example.new(
24
24
  [:float, { optional: true }],
@@ -28,7 +28,8 @@ module DataModel
28
28
  )
29
29
  end
30
30
 
31
- sig { returns(Example) }
31
+ # a float example where the minimum value is 5
32
+ # @return [Example] the example
32
33
  def min
33
34
  Example.new(
34
35
  [:float, { min: 5 }],
@@ -39,7 +40,8 @@ module DataModel
39
40
  )
40
41
  end
41
42
 
42
- sig { returns(Example) }
43
+ # a float example where the maximum value is 5
44
+ # @return [Example] the example
43
45
  def max
44
46
  Example.new(
45
47
  [:float, { max: 5.0 }],
@@ -1,14 +1,11 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Test data for hash schemas
4
3
  module Fixtures::Hash
5
4
  include Fixtures
6
- extend T::Sig
7
5
  extend self
8
6
 
9
- TContact = T.type_alias { T::Hash[Symbol, T.untyped] }
10
-
11
- sig { returns(TContact) }
7
+ # hash data conforming to the contact schema
8
+ # @return [Hash{Symbol => String}] the hash
12
9
  def example_contact
13
10
  {
14
11
  first_name: "foo",
@@ -17,7 +14,20 @@ module DataModel
17
14
  }
18
15
  end
19
16
 
20
- sig { returns(Example) }
17
+ # alternate hash syntax for when you want to type keys and values
18
+ # @return [Example] the example
19
+ def dictionary
20
+ Example.new(
21
+ [:hash, [symbol: :string]],
22
+ variants: {
23
+ valid: { foo: "bar" },
24
+ invalid: { foo: 123 }
25
+ },
26
+ )
27
+ end
28
+
29
+ # hash contact example
30
+ # @return [Example] the example
21
31
  def contact
22
32
  Example.new(
23
33
  [:hash,
@@ -28,14 +38,15 @@ module DataModel
28
38
  valid: example_contact,
29
39
  missing: nil,
30
40
  coercible: example_contact.to_a,
31
- missing_email: example_contact.tap { |h| T.cast(h, TContact).delete(:email) },
41
+ missing_email: example_contact.tap { |h| h.delete(:email) },
32
42
  invalid_field: example_contact.merge(email: 123),
33
43
  other_type: []
34
44
  },
35
45
  )
36
46
  end
37
47
 
38
- sig { returns(Example) }
48
+ # hash contact example that is optional
49
+ # @return [Example] the example
39
50
  def optional_contact
40
51
  Example.new(
41
52
  [:hash, { optional: true },
@@ -49,7 +60,8 @@ module DataModel
49
60
  )
50
61
  end
51
62
 
52
- sig { returns(Example) }
63
+ # hash contact example that is closed to extra keys
64
+ # @return [Example] the example
53
65
  def closed_contact
54
66
  Example.new(
55
67
  [:hash, { open: false },
@@ -1,12 +1,11 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Test data for integer schemas
4
3
  module Fixtures::Integer
5
4
  include Fixtures
6
- extend T::Sig
7
5
  extend self
8
6
 
9
- sig { returns(Example) }
7
+ # simple integer example
8
+ # @return [Hash{Symbol => untyped}] the variants used by each example
10
9
  def simple
11
10
  Example.new(
12
11
  [:integer],
@@ -18,7 +17,8 @@ module DataModel
18
17
  )
19
18
  end
20
19
 
21
- sig { returns(Example) }
20
+ # integer example that is optional
21
+ # @return [Example] the example
22
22
  def optional
23
23
  Example.new(
24
24
  [:integer, { optional: true }],
@@ -28,7 +28,8 @@ module DataModel
28
28
  )
29
29
  end
30
30
 
31
- sig { returns(Example) }
31
+ # integer example that has a minimum value
32
+ # @return [Example] the example
32
33
  def min
33
34
  Example.new(
34
35
  [:integer, { min: 5 }],
@@ -39,7 +40,8 @@ module DataModel
39
40
  )
40
41
  end
41
42
 
42
- sig { returns(Example) }
43
+ # integer example that has a maximum value
44
+ # @return [Example] the example
43
45
  def max
44
46
  Example.new(
45
47
  [:integer, { max: 5 }],
@@ -0,0 +1,31 @@
1
+ require "bigdecimal/util"
2
+
3
+ module DataModel
4
+ # test fixtures for object type
5
+ module Fixtures::Numeric
6
+ extend self
7
+ include Fixtures
8
+
9
+ def variants
10
+ {
11
+ missing: nil,
12
+ integer: 1,
13
+ float: 1.0,
14
+ decimal: 1.0.to_d,
15
+ string: ["1", 1]
16
+ }
17
+ end
18
+
19
+ # a simple numeric example
20
+ # @return [Example] the example
21
+ def simple
22
+ Example.new([:numeric], variants:)
23
+ end
24
+
25
+ # a numeric example that is optional
26
+ # @return [Example] the example
27
+ def optional
28
+ Example.new([:numeric, { optional: true }], variants:)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ module DataModel
2
+ # test fixtures for object type
3
+ module Fixtures::Object
4
+ extend self
5
+ include Fixtures
6
+
7
+ def variants
8
+ {
9
+ missing: nil,
10
+ integer: 1,
11
+ string: "string"
12
+ }
13
+ end
14
+
15
+ # a simple object example
16
+ # @return [Example] the example
17
+ def simple
18
+ Example.new([:object], variants:)
19
+ end
20
+
21
+ # a object example that is optional
22
+ # @return [Example] the example
23
+ def optional
24
+ Example.new([:object, { optional: true }], variants:)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ module DataModel
2
+ # test fixtures for object type
3
+ module Fixtures::Or
4
+ extend self
5
+ include Fixtures
6
+
7
+ def variants
8
+ {
9
+ missing: nil,
10
+ integer: 1,
11
+ int_array: [1],
12
+ string: ["1", 1],
13
+ float: 1.0
14
+ }
15
+ end
16
+
17
+ # a simple numeric example, integer or integer array
18
+ # @return [Example] the example
19
+ def simple
20
+ Example.new([:or, :integer, [:array, :integer]], variants:)
21
+ end
22
+
23
+ # a numeric example that is optional
24
+ # @return [Example] the example
25
+ def optional
26
+ Example.new([:or, { optional: true }, :integer, [:array, :integer]], variants:)
27
+ end
28
+ end
29
+ end
@@ -1,12 +1,11 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Test data for string schemas
4
3
  module Fixtures::String
5
4
  extend self
6
- extend T::Sig
7
5
  include Fixtures
8
6
 
9
- sig { returns(Example) }
7
+ # a simple string example
8
+ # @return [Example] the example
10
9
  def simple
11
10
  Example.new(
12
11
  [:string],
@@ -18,7 +17,8 @@ module DataModel
18
17
  )
19
18
  end
20
19
 
21
- sig { returns(Example) }
20
+ # an email string example
21
+ # @return [Example] the example
22
22
  def email
23
23
  Example.new(
24
24
  [:string, { format: "@" }],
@@ -29,29 +29,8 @@ module DataModel
29
29
  )
30
30
  end
31
31
 
32
- sig { returns(Example) }
33
- def email_regexp
34
- Example.new(
35
- [:string, { format: /@/ }],
36
- variants: {
37
- valid: "foo@bar.com",
38
- invalid: "invalid"
39
- },
40
- )
41
- end
42
-
43
- sig { returns(Example) }
44
- def email_proc
45
- Example.new(
46
- [:string, { format: ->(val) { val.match?(/@/) } }],
47
- variants: {
48
- valid: "foo@bar.com",
49
- invalid: "invalid"
50
- },
51
- )
52
- end
53
-
54
- sig { returns(Example) }
32
+ # a string example that is optional
33
+ # @return [Example] the example
55
34
  def optional
56
35
  Example.new(
57
36
  [:string, { optional: true }],
@@ -63,7 +42,8 @@ module DataModel
63
42
  )
64
43
  end
65
44
 
66
- sig { returns(Example) }
45
+ # a string example where "valid" is the only allowed String
46
+ # @return [Example] the example
67
47
  def inclusion
68
48
  Example.new(
69
49
  [:string, { included: ["valid"] }],
@@ -74,7 +54,8 @@ module DataModel
74
54
  )
75
55
  end
76
56
 
77
- sig { returns(Example) }
57
+ # a string example where "invalid" is the only disallowed String
58
+ # @return [Example] the example
78
59
  def exclusion
79
60
  Example.new(
80
61
  [:string, { excluded: ["invalid"] }],
@@ -85,7 +66,8 @@ module DataModel
85
66
  )
86
67
  end
87
68
 
88
- sig { returns(Example) }
69
+ # a string example where blank strings are allowed
70
+ # @return [Example] the example
89
71
  def allow_blank
90
72
  Example.new(
91
73
  [:string, { allow_blank: true }],
@@ -97,7 +79,8 @@ module DataModel
97
79
  )
98
80
  end
99
81
 
100
- sig { returns(Example) }
82
+ # a string example where blank strings are not allowed
83
+ # @return [Example] the example
101
84
  def dont_allow_blank
102
85
  Example.new(
103
86
  [:string, { allow_blank: false }],
@@ -1,12 +1,11 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Test data around symbol schemas
4
3
  module Fixtures::Symbol
5
4
  extend self
6
5
  include Fixtures
7
- extend T::Sig
8
6
 
9
- sig { returns(Example) }
7
+ # a simple symbol example
8
+ # @return [Example] the example
10
9
  def simple
11
10
  Example.new(
12
11
  [:symbol],
@@ -19,7 +18,8 @@ module DataModel
19
18
  )
20
19
  end
21
20
 
22
- sig { returns(Example) }
21
+ # a symbol example that is optional
22
+ # @return [Example] the example
23
23
  def optional
24
24
  Example.new(
25
25
  [:symbol, { optional: true }],
@@ -31,7 +31,8 @@ module DataModel
31
31
  )
32
32
  end
33
33
 
34
- sig { returns(Example) }
34
+ # a symbol example where :valid is the only allowed Symbol
35
+ # @return [Example] the example
35
36
  def inclusion
36
37
  Example.new(
37
38
  [:symbol, { included: [:valid] }],
@@ -42,7 +43,8 @@ module DataModel
42
43
  )
43
44
  end
44
45
 
45
- sig { returns(Example) }
46
+ # a symbol example where :invalid is the only disallowed Symbol
47
+ # @return [Example] the example
46
48
  def exclusion
47
49
  Example.new(
48
50
  [:symbol, { excluded: [:invalid] }],
@@ -1,22 +1,20 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Test data around time schemas
4
3
  module Fixtures::Time
5
- extend T::Sig
6
- extend self
7
4
  include Fixtures
5
+ extend self
8
6
 
9
- sig { returns(::Time) }
7
+ # @return [Time] a time that is used by the #earliest example
10
8
  def earliest_time
11
9
  return ::Time.now - 1
12
10
  end
13
11
 
14
- sig { returns(::Time) }
12
+ # @return [Time] a time that is used by the #latest example
15
13
  def latest_time
16
14
  return ::Time.now + 1
17
15
  end
18
16
 
19
- sig { returns(T::Hash[Symbol, Object]) }
17
+ # @return [Hash{Symbol => untyped}] the variants used by each example
20
18
  def variants
21
19
  now = ::Time.now
22
20
 
@@ -30,22 +28,26 @@ module DataModel
30
28
  }
31
29
  end
32
30
 
33
- sig { returns(Example) }
31
+ # A simple time schema
32
+ # @return [Example] the example
34
33
  def simple
35
34
  Example.new([:time], variants:)
36
35
  end
37
36
 
38
- sig { returns(Example) }
37
+ # A time schema that is optional
38
+ # @return [Example] the example
39
39
  def optional
40
40
  Example.new([:time, { optional: true }], variants:)
41
41
  end
42
42
 
43
- sig { returns(Example) }
43
+ # A time schema that has a restriction on the earliest time
44
+ # @return [Example] the example
44
45
  def earliest
45
46
  Example.new([:time, { earliest: earliest_time }], variants:)
46
47
  end
47
48
 
48
- sig { returns(Example) }
49
+ # A time schema that has a restriction on the latest time
50
+ # @return [Example] the example
49
51
  def latest
50
52
  Example.new([:time, { latest: latest_time }], variants:)
51
53
  end
@@ -1,15 +1,12 @@
1
- # typed: strict
2
-
3
1
  require "logger"
4
2
 
5
3
  module DataModel
4
+ # Provides a logger for classes that include it
6
5
  module Logging
7
- extend T::Sig
8
- include Kernel
9
-
10
- sig { returns(Logger) }
6
+ # Get a logger
7
+ # @return [Logger] the logger for this class
11
8
  def log
12
- target = T.let(respond_to?(:name) ? self : self.class, T.any(Class, Module))
9
+ target = respond_to?(:name) ? self : self.class
13
10
 
14
11
  logger = Logger.new(
15
12
  STDERR,
@@ -17,7 +14,7 @@ module DataModel
17
14
  progname: target.name,
18
15
  )
19
16
 
20
- return @log ||= T.let(logger, T.nilable(Logger))
17
+ return @log ||= logger
21
18
  end
22
19
  end
23
20
  end
@@ -1,28 +1,31 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # A model is a schema and a type. It is the primary interface for interacting
3
+ # with the data_model gem.
4
4
  class Model
5
- extend T::Sig
6
-
7
- sig { params(schema: TSchema, type: Type).void }
5
+ # Create a new model.
6
+ # @param schema [Array] the schema to define
7
+ # @param type [Type] the type to use
8
+ # @return [void]
8
9
  def initialize(schema, type)
9
10
  @schema = schema
10
11
  @type = type
11
12
  end
12
13
 
13
- sig { returns(TSchema) }
14
+ # @return [Array] the schema configured
14
15
  attr_reader :schema
15
16
 
16
17
  # Validate data against the model. This will return true if the data is valid,
17
18
  # or false if it is not. If it is not valid, it will raise an exception.
18
- sig { params(data: TData).returns(Error) }
19
+ # @param data [Hash] the data to validate
20
+ # @return [Boolean] true if the data is valid, false if it is not
19
21
  def validate(data)
20
22
  _, err = @type.read(data)
21
23
  return err
22
24
  end
23
25
 
24
26
  # Read data with the model. This will return a tuple of [data, error]
25
- sig { params(data: TData).returns([TData, Error]) }
27
+ # @param data [Hash] the data to read
28
+ # @return [Array] a tuple of [data, error]
26
29
  def coerce(data)
27
30
  result = @type.read(data, coerce: true)
28
31
  return result