data_model 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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