data_model 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -2
  3. data/.solargraph.yml +22 -0
  4. data/Gemfile.lock +71 -29
  5. data/Rakefile +0 -6
  6. data/Steepfile +27 -0
  7. data/data_model.gemspec +1 -2
  8. data/lib/data_model/boolean.rb +0 -2
  9. data/lib/data_model/builtin/array.rb +32 -25
  10. data/lib/data_model/builtin/big_decimal.rb +15 -14
  11. data/lib/data_model/builtin/boolean.rb +10 -7
  12. data/lib/data_model/builtin/date.rb +15 -12
  13. data/lib/data_model/builtin/float.rb +14 -13
  14. data/lib/data_model/builtin/hash.rb +109 -36
  15. data/lib/data_model/builtin/integer.rb +14 -13
  16. data/lib/data_model/builtin/numeric.rb +35 -0
  17. data/lib/data_model/builtin/object.rb +28 -0
  18. data/lib/data_model/builtin/or.rb +73 -0
  19. data/lib/data_model/builtin/string.rb +15 -16
  20. data/lib/data_model/builtin/symbol.rb +14 -13
  21. data/lib/data_model/builtin/time.rb +17 -14
  22. data/lib/data_model/builtin.rb +9 -9
  23. data/lib/data_model/error.rb +30 -18
  24. data/lib/data_model/errors.rb +79 -55
  25. data/lib/data_model/fixtures/array.rb +22 -9
  26. data/lib/data_model/fixtures/big_decimal.rb +9 -7
  27. data/lib/data_model/fixtures/boolean.rb +5 -5
  28. data/lib/data_model/fixtures/date.rb +13 -11
  29. data/lib/data_model/fixtures/example.rb +7 -7
  30. data/lib/data_model/fixtures/float.rb +9 -7
  31. data/lib/data_model/fixtures/hash.rb +22 -10
  32. data/lib/data_model/fixtures/integer.rb +9 -7
  33. data/lib/data_model/fixtures/numeric.rb +31 -0
  34. data/lib/data_model/fixtures/object.rb +27 -0
  35. data/lib/data_model/fixtures/or.rb +29 -0
  36. data/lib/data_model/fixtures/string.rb +15 -32
  37. data/lib/data_model/fixtures/symbol.rb +9 -7
  38. data/lib/data_model/fixtures/time.rb +13 -11
  39. data/lib/data_model/logging.rb +5 -8
  40. data/lib/data_model/model.rb +11 -8
  41. data/lib/data_model/registry.rb +37 -22
  42. data/lib/data_model/scanner.rb +23 -28
  43. data/lib/data_model/struct.rb +116 -0
  44. data/lib/data_model/testing/minitest.rb +33 -9
  45. data/lib/data_model/testing.rb +0 -2
  46. data/lib/data_model/type.rb +38 -22
  47. data/lib/data_model/version.rb +1 -3
  48. data/lib/data_model.rb +8 -17
  49. metadata +13 -26
  50. data/sorbet/config +0 -4
  51. data/sorbet/rbi/annotations/rainbow.rbi +0 -269
  52. data/sorbet/rbi/gems/minitest@5.18.0.rbi +0 -1491
  53. data/sorbet/rbi/gems/zeitwerk.rbi +0 -196
  54. data/sorbet/rbi/gems/zeitwerk@2.6.7.rbi +0 -966
  55. data/sorbet/rbi/todo.rbi +0 -5
  56. data/sorbet/tapioca/config.yml +0 -13
  57. data/sorbet/tapioca/require.rb +0 -4
@@ -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
@@ -1,61 +1,71 @@
1
- # typed: strict
2
-
3
1
  module DataModel
4
2
  # Registry allows for different type implementations to be used by the scanner.
5
3
  # It also acts as an error message registry, mostly for pragmatic reasons.
6
4
  class Registry
7
- extend T::Sig
8
-
9
5
  # Default types that will be used if alternative type map is not given
10
- sig { returns(TTypeMap) }
6
+ # @return [Hash] the default type map
11
7
  def self.default_types
12
8
  Builtin.types
13
9
  end
14
10
 
15
- sig { returns(Errors::TErrorMessages) }
11
+ # Default error messages that will be used if alternative error messages are not given
12
+ # @return [Hash] the default error messages
16
13
  def self.default_error_messages
17
14
  Errors.error_messages
18
15
  end
19
16
 
20
17
  # Singleton instance that will be used globally unless instances given
21
- sig { params(types: TTypeMap, errors: Errors::TErrorMessages).returns(Registry) }
18
+ # @param types [Hash] the type map to use
19
+ # @param errors [Hash] the error message map to use
20
+ # @return [Registry] the singleton instance
22
21
  def self.instance(types: default_types, errors: default_error_messages)
23
- @instance ||= T.let(new(types:, errors:), T.nilable(Registry))
22
+ @instance ||= new(types:, errors:)
24
23
  end
25
24
 
26
25
  # Register a type on the global instance
27
- sig { params(name: Symbol, type: T.class_of(Type)).void }
26
+ # @param name [Symbol] the name of the type
27
+ # @param type [Type] the type to register
28
+ # @return [void]
28
29
  def self.register(name, type)
29
30
  instance.register(name, type)
30
31
  end
31
32
 
32
33
  # Instanciate a new type registry. Default errors will always be used, but additional
33
34
  # errors can be registered.
34
- sig { params(types: TTypeMap, errors: Errors::TErrorMessages).void }
35
+ # @param types [Hash] the type map to use
36
+ # @param errors [Hash] the error message map to use
37
+ # @return [Registry] the new instance
35
38
  def initialize(types: self.class.default_types, errors: self.class.default_error_messages)
36
- @error_messages = T.let(nil, T.nilable(Errors::TErrorMessages))
39
+ @error_messages = nil
40
+
37
41
  if errors
38
42
  errors.each { |type, builder| register_error_message(type, &builder) }
39
43
  end
40
44
 
41
- @types = T.let({}, TTypeMap)
45
+ @types = {}
42
46
  types.each { |(name, type)| register(name, type) }
43
47
  end
44
48
 
45
49
  # Register a type on this instance
46
- sig { params(name: Symbol, type: T.class_of(Type)).void }
50
+ # @param name [Symbol] the name of the Type
51
+ # @param type [Type] the type to register
52
+ # @return [void]
47
53
  def register(name, type)
48
54
  @types[name] = type
49
55
  end
50
56
 
51
57
  # Check if a type is registered
52
- sig { params(name: Symbol).returns(T::Boolean) }
58
+ # @param name [Symbol] the name of the type
59
+ # @return [Boolean] whether the type is registered
53
60
  def type?(name)
54
61
  @types.key?(name)
55
62
  end
56
63
 
57
64
  # Access and configure registered type
58
- sig { params(name: Symbol, args: Type::TArguments, params: T.nilable(T::Array[Object])).returns(Type) }
65
+ # @param name [Symbol] the name of the Type
66
+ # @param args [Hash] the arguments to pass to the Type
67
+ # @param params [Array] the parameters to configure the Type with
68
+ # @return [Type] the configured type
59
69
  def type(name, args: {}, params: nil)
60
70
  if !type?(name)
61
71
  raise "#{name} is not registered as a type"
@@ -73,26 +83,30 @@ module DataModel
73
83
  ## API
74
84
 
75
85
  # Register a custom error message for use with custom errors
76
- sig { params(type: Symbol, block: Errors::TErrorMessageBuilder).void }
86
+ # @param type [Symbol] the type of error to register
87
+ # @param block [Proc] the block to use to build the error message, shoudl take the error context and return a string
88
+ # @return [void]
77
89
  def register_error_message(type, &block)
78
90
  error_message_builders[type] = block
79
91
  end
80
92
 
81
93
  # Get the error message builders
82
- sig { returns(Errors::TErrorMessages) }
94
+ # @return [Hash] the error message builders
83
95
  def error_message_builders
84
96
  if @error_messages.nil?
85
- @error_messages ||= T.let({}, T.nilable(Errors::TErrorMessages))
97
+ @error_messages ||= {}
86
98
  end
87
99
 
88
100
  @error_messages
89
101
  end
90
102
 
91
103
  # Build the error message for a given error
92
- sig { params(error: TError).returns(String) }
104
+ # @param error [Error] the error to build the message for
105
+ # @return [String] the error message
106
+ # @raise [RuntimeError] if no error message builder is registered for the error type
93
107
  def error_message(error)
94
- type = T.let(error[0], Symbol)
95
- ctx = T.let(error[1], T.untyped)
108
+ type = error[0]
109
+ ctx = error[1]
96
110
 
97
111
  builder = error_message_builders[type]
98
112
 
@@ -104,7 +118,8 @@ module DataModel
104
118
  end
105
119
 
106
120
  # Build error messages from error object
107
- sig { params(error: Error).returns(T::Hash[Symbol, T::Array[String]]) }
121
+ # @param error [Error] the error to build the messages for
122
+ # @return [Hash] the error messages
108
123
  def error_messages(error)
109
124
  error.to_h.transform_values do |error_list|
110
125
  error_list.map { |e| error_message(e) }