data_model 0.4.0 → 0.5.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 +36 -3
  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 +100 -35
  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 +112 -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 +12 -25
  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,112 @@
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
+ if !data.key?(req)
101
+ raise "Missing required field #{req}"
102
+ end
103
+ end
104
+ end
105
+
106
+ # convert struct to a hash
107
+ # @return [Hash] the struct as a hash
108
+ def to_hash
109
+ @values
110
+ end
111
+ end
112
+ 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.5.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.5.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-05-30 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
data/sorbet/config DELETED
@@ -1,4 +0,0 @@
1
- --dir
2
- .
3
- --ignore=/tmp/
4
- --ignore=/vendor/bundle