data_model 0.4.0 → 0.6.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 (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,43 +1,37 @@
1
- # typed: strict
2
-
3
- # Scan a schema into a struct that can be inspected to construct a model validator
4
- #
5
- # schema eg:
6
- # [:string, { min: 1, max: 10}]
7
- # [:tuple, { title: "coordinates" }, :double, :double]
8
- # [:hash, { open: false },
9
- # [:first_name, :string]
10
- # [:last_name, :string]]
11
- #
12
- # first param is type, which is a key lookup in the registry
13
- # second param is args, this is optional, but is a way to configure a type
14
- # rest are type params. these are used to configure a type at the point of instantiation. Think of them as generics.
15
- #
16
- # params are either
17
- # symbol, for example tuple types
18
- # array, for object types to configure child properties.
19
1
  module DataModel
2
+ # The scanner is responsible for scanning a schema into a data structure that is easier to work with.
3
+ #
4
+ # schema eg:
5
+ # [:string, { min: 1, max: 10}]
6
+ # [:tuple, { title: "coordinates" }, :double, :double]
7
+ # [:hash, { open: false },
8
+ # [:first_name, :string]
9
+ # [:last_name, :string]]
10
+ #
11
+ # first param is type, which is a key lookup in the registry
12
+ # second param is args, this is optional, but is a way to configure a type
13
+ # rest are type params. these are used to configure a type at the point of instantiation. Think of them as generics.
14
+ #
15
+ # params are either
16
+ # symbol, for example tuple types
17
+ # array, for object types to configure child properties.
20
18
  module Scanner
21
- include Kernel
22
19
  include Logging
23
-
24
- extend T::Sig
25
20
  extend self
26
21
 
27
- class Node < T::Struct
28
- prop :type, Symbol, default: :nothing
29
- prop :args, T::Hash[Symbol, Object], default: {}
30
- prop :params, T::Array[Object], default: []
31
- end
22
+ # cant use DataModel::Struct because it would be a circular dependency
23
+ Node = ::Struct.new(:type, :args, :params)
32
24
 
33
25
  # Scan a schema, which is defined as a data structure, into a struct that is easier to work with.
34
26
  # "Syntax" validations will be enforced at this level.
35
- sig { params(schema: TSchema, registry: DataModel::Registry).returns(Node) }
27
+ # @param schema [Array] the schema to scan
28
+ # @param registry [Registry] the registry to use
29
+ # @return [Node] the scanned node
36
30
  def scan(schema, registry = Registry.instance)
37
31
  # state:
38
32
  # nil (start) -> :type (we have a type) -> :args (we have arguments)
39
33
  scanned = Node.new
40
- state = T.let(nil, T.nilable(Symbol))
34
+ state = nil
41
35
 
42
36
  log.debug("scanning schema: #{schema.inspect}")
43
37
 
@@ -61,6 +55,7 @@ module DataModel
61
55
  raise "expected type params at (#{dbg}), which should be either a symbol or an array"
62
56
  end
63
57
 
58
+ scanned.params ||= []
64
59
  scanned.params << token
65
60
  log.debug("collecting params at (#{dbg})")
66
61
 
@@ -0,0 +1,116 @@
1
+ module DataModel
2
+ # A struct that has typed properties, which will raise when the wrong type is assigned
3
+ class Struct
4
+ class << self
5
+ # types configured by the prop macro
6
+ # @return [Hash] the types configured
7
+ attr_reader :types
8
+
9
+ # required types configured by the prop macro
10
+ # @return [Array<Symbol>] the required types configured
11
+ attr_reader :required_types
12
+ end
13
+
14
+ # Define a property on the struct.
15
+ # @param name [Symbol] the name of the property
16
+ # @param schema [Array] the schema to define
17
+ # @option opts [Boolean] :default the default value for the property
18
+ # @return [void]
19
+ def self.prop(name, schema, opts = {})
20
+ has_default = opts.key?(:default)
21
+ default = opts[:default]
22
+
23
+ # ensure values are initialized
24
+ @types ||= {}
25
+ @required_types ||= []
26
+
27
+ # if a prop does not have a default, it is required
28
+ if !has_default
29
+ @required_types << name
30
+ end
31
+
32
+ # store the data model for the prop
33
+ schema = Array(schema)
34
+ @types[name] = DataModel.define(schema)
35
+
36
+ # field getter
37
+ define_method(name) do
38
+ @values ||= {}
39
+ types = self.class.types
40
+
41
+ if !types.key?(name)
42
+ raise "No prop configured for #{name}"
43
+ end
44
+
45
+ if !@values.key?(name) && has_default
46
+ return default
47
+ end
48
+
49
+ return @values[name]
50
+ end
51
+
52
+ # field setter
53
+ define_method("#{name}=") do |val|
54
+ @values ||= {}
55
+ types = self.class.types
56
+
57
+ if !types.key?(name)
58
+ raise "No prop configured for #{name}"
59
+ end
60
+
61
+ model = types.fetch(name)
62
+
63
+ if @coerce
64
+ val, err = model.coerce(val)
65
+ else
66
+ err = model.validate(val)
67
+ end
68
+
69
+ if @strict && err.any?
70
+ errors.merge_child(prop, err)
71
+ raise "Invalid value for #{name}: #{err.to_messages.inspect}"
72
+ end
73
+
74
+ @values[name] = val
75
+ end
76
+ end
77
+
78
+ # @return [Error] the errors for this struct
79
+ attr_reader :errors
80
+
81
+ # @param data [Hash] the data to initialize the struct with
82
+ # @param coerce [Boolean] whether to coerce the data if it is not correctly typed
83
+ # @param strict [Boolean] whether to raise if a required field is missing
84
+ # @return [void]
85
+ def initialize(data = {}, coerce: false, strict: true)
86
+ @coerce = coerce
87
+ @strict = strict
88
+ @values = {}
89
+ @errors = Error.new
90
+
91
+ if data.nil?
92
+ return
93
+ end
94
+
95
+ for k, v in data
96
+ send("#{k}=", v)
97
+ end
98
+
99
+ for req in self.class.required_types
100
+ for key in data.keys
101
+ if key.to_s == req.to_s
102
+ next
103
+ end
104
+ end
105
+
106
+ raise "Missing required field #{req}"
107
+ end
108
+ end
109
+
110
+ # convert struct to a hash
111
+ # @return [Hash] the struct as a hash
112
+ def to_hash
113
+ @values
114
+ end
115
+ end
116
+ end
@@ -1,15 +1,18 @@
1
- # typed: strict
2
-
3
1
  require "minitest/assertions"
4
2
 
5
3
  module DataModel
4
+ # Provides assertions for minitest
6
5
  module Testing::Minitest
7
- extend T::Sig
8
6
  include Minitest::Assertions
9
- include Kernel
10
7
 
11
- sig { params(err: Error, type: Symbol, key: T.nilable(Symbol)).void }
8
+ # Assert that a child model error was found.
9
+ # @param err [Error] the error to check
10
+ # @param type [Symbol] the type of error to check for
11
+ # @param key [Array(Symbol)] limit checking to a specific child key
12
+ # @return [void]
12
13
  def assert_child_model_error(err, type, key = nil)
14
+ refute_nil(err)
15
+
13
16
  assert(err.children.any?, "validate was successful, but should not have been")
14
17
 
15
18
  for k in key ? [key] : err.children.keys
@@ -18,8 +21,13 @@ module DataModel
18
21
  end
19
22
  end
20
23
 
21
- sig { params(err: Error, type: Symbol).void }
24
+ # Assert that a model error was found.
25
+ # @param err [Error] the error to check
26
+ # @param type [Symbol] the type of error to check for
27
+ # @return [void]
22
28
  def assert_model_error(err, type)
29
+ refute_nil err
30
+
23
31
  assert(err.base.any?, "validate was successful, but should not have been")
24
32
 
25
33
  found = err.base.any? { |(t, _ctx)| t == type }
@@ -27,8 +35,14 @@ module DataModel
27
35
  assert(found, "validation was not successful, but #{type} error was not found #{err.inspect}")
28
36
  end
29
37
 
30
- sig { params(err: Error, type: T.nilable(Symbol), key: T.nilable(Symbol)).void }
38
+ # Assert that no child error is found
39
+ # @param err [Error] the error to check
40
+ # @param type [Symbol] the type of error to check for
41
+ # @param key [Symbol, Array<Symbol>] limit checking to a specific child key
42
+ # @return [void]
31
43
  def refute_child_model_error(err, type = nil, key = nil)
44
+ refute_nil(err)
45
+
32
46
  if !err.any?
33
47
  return
34
48
  end
@@ -44,8 +58,13 @@ module DataModel
44
58
  end
45
59
  end
46
60
 
47
- sig { params(err: Error, type: T.nilable(Symbol)).void }
61
+ # Assert that no base error is present
62
+ # @param err [Error] the error to check
63
+ # @param type [Symbol] the type of error to check for
64
+ # @return [void]
48
65
  def refute_model_error(err, type = nil)
66
+ refute_nil(err)
67
+
49
68
  if !err.any?
50
69
  return
51
70
  end
@@ -60,8 +79,13 @@ module DataModel
60
79
  refute(found, "#{type} error was found #{err.inspect}")
61
80
  end
62
81
 
63
- sig { params(err: Error, type: T.nilable(Symbol)).void }
82
+ # Assert that no errors are present
83
+ # @param err [Error] the error to check
84
+ # @param type [Symbol] the type of error to check for
85
+ # @return [void]
64
86
  def refute_all_errors(err, type = nil)
87
+ refute_nil(err)
88
+
65
89
  if !err.any?
66
90
  return
67
91
  end
@@ -1,5 +1,3 @@
1
- # typed: strict
2
-
3
1
  module DataModel
4
2
  module Testing
5
3
  end
@@ -1,32 +1,36 @@
1
- # typed: strict
2
-
3
- # Mixin included on every type. Type::Generic and Type::Parent are higher level specializations.
4
1
  module DataModel
2
+ # @abstract Base class for all types.
3
+ # Types have arguments, which configure the act of reading / validating / coercing data.
4
+ # They also have parameters, which configure the type itself, such as generic or child specification
5
+ # Arguments are passed to the type when it is invoked, and parameters are passed to the type when it is configured.
6
+ # Parameters are really only used in complex types, such as Array or Hash. If you don't need them, you can ignore them.
5
7
  class Type
6
- extend T::Sig
7
- extend T::Helpers
8
-
9
- abstract!
10
-
11
- TArguments = T.type_alias { T::Hash[Symbol, T.untyped] }
12
- TTypeParams = T.type_alias { T::Array[Object] }
13
- TTypeResult = T.type_alias { [Object, Error] }
14
-
15
- sig { params(args: TArguments, registry: Registry).void }
8
+ # @param args [Hash] type arguments, configures the reading process
9
+ # @param registry [Registry] the registry to use
10
+ # @return [void]
16
11
  def initialize(args, registry: Registry.instance)
17
12
  @type_args = args
18
13
  @type_registry = registry
19
14
  end
20
15
 
21
- sig { returns(TArguments) }
16
+ # @return [Hash] the type arguments
22
17
  attr_reader :type_args
23
18
 
24
- # configure must be overridden to use params
25
- sig { overridable.params(params: TTypeParams).void }
19
+ # @return [Registry] the type Registry
20
+ attr_reader :type_registry
21
+
22
+ # configure must be overridden to use params. If you don't need params, you can ignore this.
23
+ # @param params [Array] type parameters, configures the type itself
24
+ # @return [void]
26
25
  def configure(params); end
27
26
 
28
- # invoke another type by name
29
- sig { params(name: Symbol, val: Object, coerce: T::Boolean, args: Type::TArguments, params: T.nilable(TTypeParams)).returns(TTypeResult) }
27
+ # invoke another type by name. This is useful for specifying types like UUIDs which are specialized strings
28
+ # @param name [Symbol] the name of the type to invoke
29
+ # @param val [Object] the value to read
30
+ # @param coerce [Boolean] whether to coerce the value
31
+ # @param args [Hash] type arguments, configures the reading process
32
+ # @param params [Array] type parameters, configures the type itself
33
+ # @return [Array(Object, Error)] the result of reading the value
30
34
  def invoke(name, val, coerce: false, args: {}, params: nil)
31
35
  t = instantiate(name, args:, params:)
32
36
 
@@ -35,16 +39,28 @@ module DataModel
35
39
  return result
36
40
  end
37
41
 
38
- # instanciate another type
39
- sig { params(name: Symbol, args: Type::TArguments, params: T.nilable(TTypeParams)).returns(Type) }
42
+ # instanciate another type by name
43
+ # @param name [Symbol] the name of the type to instantiate
44
+ # @param args [Hash] type arguments, configures the reading Process
45
+ # @param params [Array] type parameters, configures the type itself
46
+ # @return [Type] the instantiated type
40
47
  def instantiate(name, args: {}, params: nil)
41
48
  t = @type_registry.type(name, args:, params:)
42
49
 
43
50
  return t
44
51
  end
45
52
 
46
- # default reader
47
- sig { abstract.params(data: Object, coerce: T::Boolean).returns(TTypeResult) }
53
+ # @abstract default reader, must be overridden for a type to be useful
54
+ # @param data [untyped] the data to read
55
+ # @param coerce [Boolean] whether to coerce the value
56
+ # @return [Array(Object, Error)] the result of reading the value
48
57
  def read(data, coerce: false); end
58
+
59
+ # name of the type without module prefix as a string
60
+ # useful for generating error messages
61
+ # @return [String] the type name
62
+ def type_name
63
+ @type_name ||= self.class.name.split("::").last
64
+ end
49
65
  end
50
66
  end
@@ -1,5 +1,3 @@
1
- # typed: strict
2
-
3
1
  module DataModel
4
- VERSION = "0.4.0"
2
+ VERSION = "0.6.0"
5
3
  end
data/lib/data_model.rb CHANGED
@@ -1,5 +1,3 @@
1
- # typed: strict
2
-
3
1
  require "logger"
4
2
  require "bigdecimal"
5
3
  require "date"
@@ -7,27 +5,17 @@ require "time"
7
5
 
8
6
  require "bundler/setup"
9
7
  require "zeitwerk"
10
- require "sorbet-runtime"
11
8
 
12
- loader = T.let(Zeitwerk::Loader.for_gem, Zeitwerk::Loader)
9
+ loader = Zeitwerk::Loader.for_gem
13
10
  loader.setup
14
11
 
15
12
  module DataModel
16
- extend T::Sig
17
13
  extend self
18
14
 
19
- TSchema = T.type_alias { T::Array[Object] }
20
- TData = T.type_alias { Object }
21
-
22
- # an error is a tuple of [error_type, error_context], where context
23
- # provides additional information about the error
24
- TError = T.type_alias { [Symbol, Object] }
25
-
26
- # a map of symbol => type, suitable for sending to a TypeRegistry
27
- TTypeMap = T.type_alias { T::Hash[Symbol, T.class_of(Type)] }
28
-
29
15
  # Scan a schema and create a data model, which is a configured type.
30
- sig { params(schema: TSchema, registry: Registry).returns(Model) }
16
+ # @param schema [Array] the schema to define
17
+ # @param registry [Registry] the registry to use
18
+ # @return [Model] the model built from the schema
31
19
  def define(schema, registry: Registry.instance)
32
20
  scanned = Scanner.scan(schema, registry)
33
21
 
@@ -42,7 +30,10 @@ module DataModel
42
30
  return model
43
31
  end
44
32
 
45
- sig { params(name: Symbol, type: T.class_of(Type)).void }
33
+ # Register a global type, which is available to all models.
34
+ # @param name [Symbol] the name of the Type
35
+ # @param type [Type] the type to register
36
+ # @return [void]
46
37
  def register_global_type(name, type)
47
38
  Registry.register(name, type)
48
39
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: data_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Briggs
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-18 00:00:00.000000000 Z
11
+ date: 2023-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: sorbet
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: zeitwerk
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -123,7 +109,7 @@ dependencies:
123
109
  - !ruby/object:Gem::Version
124
110
  version: '0'
125
111
  - !ruby/object:Gem::Dependency
126
- name: sorbet-runtime
112
+ name: steep
127
113
  requirement: !ruby/object:Gem::Requirement
128
114
  requirements:
129
115
  - - ">="
@@ -162,10 +148,12 @@ files:
162
148
  - ".ruby-version"
163
149
  - ".shadowenv.d/.gitignore"
164
150
  - ".shadowenv.d/550-ruby.lisp"
151
+ - ".solargraph.yml"
165
152
  - Gemfile
166
153
  - Gemfile.lock
167
154
  - Guardfile
168
155
  - Rakefile
156
+ - Steepfile
169
157
  - data_model.gemspec
170
158
  - lib/data_model.rb
171
159
  - lib/data_model/boolean.rb
@@ -177,6 +165,9 @@ files:
177
165
  - lib/data_model/builtin/float.rb
178
166
  - lib/data_model/builtin/hash.rb
179
167
  - lib/data_model/builtin/integer.rb
168
+ - lib/data_model/builtin/numeric.rb
169
+ - lib/data_model/builtin/object.rb
170
+ - lib/data_model/builtin/or.rb
180
171
  - lib/data_model/builtin/string.rb
181
172
  - lib/data_model/builtin/symbol.rb
182
173
  - lib/data_model/builtin/time.rb
@@ -190,6 +181,9 @@ files:
190
181
  - lib/data_model/fixtures/float.rb
191
182
  - lib/data_model/fixtures/hash.rb
192
183
  - lib/data_model/fixtures/integer.rb
184
+ - lib/data_model/fixtures/numeric.rb
185
+ - lib/data_model/fixtures/object.rb
186
+ - lib/data_model/fixtures/or.rb
193
187
  - lib/data_model/fixtures/string.rb
194
188
  - lib/data_model/fixtures/symbol.rb
195
189
  - lib/data_model/fixtures/time.rb
@@ -197,18 +191,11 @@ files:
197
191
  - lib/data_model/model.rb
198
192
  - lib/data_model/registry.rb
199
193
  - lib/data_model/scanner.rb
194
+ - lib/data_model/struct.rb
200
195
  - lib/data_model/testing.rb
201
196
  - lib/data_model/testing/minitest.rb
202
197
  - lib/data_model/type.rb
203
198
  - lib/data_model/version.rb
204
- - sorbet/config
205
- - sorbet/rbi/annotations/rainbow.rbi
206
- - sorbet/rbi/gems/minitest@5.18.0.rbi
207
- - sorbet/rbi/gems/zeitwerk.rbi
208
- - sorbet/rbi/gems/zeitwerk@2.6.7.rbi
209
- - sorbet/rbi/todo.rbi
210
- - sorbet/tapioca/config.yml
211
- - sorbet/tapioca/require.rb
212
199
  homepage: https://github.com/mbriggs/data_model
213
200
  licenses:
214
201
  - MIT
@@ -233,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
233
220
  - !ruby/object:Gem::Version
234
221
  version: '0'
235
222
  requirements: []
236
- rubygems_version: 3.4.10
223
+ rubygems_version: 3.4.18
237
224
  signing_key:
238
225
  specification_version: 4
239
226
  summary: Define a model for your data
data/sorbet/config DELETED
@@ -1,4 +0,0 @@
1
- --dir
2
- .
3
- --ignore=/tmp/
4
- --ignore=/vendor/bundle