typed_model 0.1.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b1bfb6a568683689e6333d52789ba54cecb6582f107c44398f86001a90168cd
4
- data.tar.gz: 220086f68dbb3033a6e9e91f562fe59c0709c7694d2ca79494b8452f722bd647
3
+ metadata.gz: e073e121645ca98973bfdccf4eec35d7b22280d57ffae982dd82adc5277200d6
4
+ data.tar.gz: 7dec738ef8f379ef9aef9640e1c7ffc40ae8eacbdc019d35fd53a861967abcc4
5
5
  SHA512:
6
- metadata.gz: 6461a2b3a7e02a68bffeb6e2d330b17b562be0e1598ce35fe514f1a4c3fde0e1980e61fa23cacda63eedac2f40d2c802b46954172be4d7e9b11a8e2c370f068b
7
- data.tar.gz: c939e41dd3c7e727f311e5ae9bede19ce4e1e9e1e1d9d2d790ddb055566982d1c735e573dcca198fb2ced894a0a79f08d9b1b169874113ebb328c7dbcdf106c1
6
+ metadata.gz: 07a337998fc5a7a9c4e7f3e48682ea3687dce65ee40c65d1c0b839ce10cb4755ca9ff40d0331ba8a21b5dd0cf1b0d980b9261cc44adfa2ba58d204875568293f
7
+ data.tar.gz: 6fd6b7833075e1860f923f1efd3bf4a610348ce6fe127a7eb8250034cfe350fff987e4a7e09b2937ccd844e29548d79ba8d77009377bcb3ef8b917eaf4c0f9a6
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.4
data/CHANGELOG.md ADDED
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ ### Bug Fixes
6
+
7
+ - `valid?` now resets errors between calls — previously errors accumulated across invocations
8
+ - `typecast` no longer swallows `false` return values (used `|| value` which treated `false` as nil)
9
+ - `instantiate_type` returns nil for nil input instead of raising NoMethodError
10
+ - `validate_recognized_types` uses `!value.nil?` instead of `value != nil` to avoid ActiveSupport DateTime comparison bug
11
+
12
+ ### Breaking Changes
13
+
14
+ - `assert_not_blank` is now a distinct validation from `assert_required` — checks for non-whitespace (`/\S/`) rather than just empty/nil
15
+ - Removed deprecated `Fixnum` from `PRIMITIVE_CLASSES`
16
+
17
+ ### Additions
18
+
19
+ - `Errors#to_h` added for accessing the underlying error hash
20
+ - Mapping key attribute assignment now works bidirectionally (set via name or mapping_key)
21
+
22
+ ### Code Quality
23
+
24
+ - Modern Ruby syntax: safe navigation, guard clauses, `rescue StandardError`, symbol method names
25
+ - Extracted `build_validators`, `build_seq_of`, `build_map_of` helpers in `TypeDef`
26
+ - Extracted `recognized_scalar_type?` and `merge_value_errors` in `TypeDef`
27
+ - `Errors#each_error` uses `yield` instead of `blk.call`
28
+ - `ModelValidations#each_error` uses anonymous block forwarding (`&`)
29
+ - Added `# frozen_string_literal: true` to `type_def.rb`
30
+
31
+ ### Tests
32
+
33
+ - Expanded `assert_not_blank` coverage (nil, empty, whitespace, non-blank, leading/trailing whitespace)
34
+ - `valid?` error reset between calls
35
+ - Boolean typecast with falsey values
36
+ - `instantiate_type` nil handling
37
+ - DateTime `<=>` nil edge case in validate
38
+ - Mapping key bidirectional assignment
39
+ - Nested association messages with mapping key
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # TypedModel
2
2
 
3
- A Ruby library for defining data schemas via classes with
4
- typed fields and built in hydration and serialization
5
-
6
- * data mapping (data -> object -> data)
7
- * declared types
8
- * declared validations
9
- * Nested models
10
-
3
+ A lightweight attribute declaration, validation, and type-coercion framework for Ruby. Include `TypedModel::ModelBase` in a class to get:
4
+
5
+ - **`attribute`** declarations with type and validation options
6
+ - **`initialize(values)`** construct from a hash
7
+ - **`attributes=`** — bulk-assign from a hash
8
+ - **`valid?`** / **`errors`** — run validations and inspect failures
9
+ - **`to_data`** — serialize to a plain hash
10
+
11
11
  ## Installation
12
12
 
13
13
  Add this line to your application's Gemfile:
@@ -24,76 +24,217 @@ Or install it yourself as:
24
24
 
25
25
  $ gem install typed_model
26
26
 
27
- ## Usage
28
-
29
- Employee
30
- include ModelBase
31
-
32
- attribute :a_boolean, type: :boolean
33
- attribute :an_integer, type: :integer
34
- attribute :a_timestamp, type: :timestamp
35
- attribute :widget1, type: :map
36
- attribute :address, type: a_c
37
- attribute :addresses, seq_of: a_c
38
- attribute :colors, type: :seq
39
- end
40
-
41
- ### Sequences
42
-
43
- # non-empty/nil sequence
44
- attribute :a_seq, type: :seq, :validations [:required]
45
-
46
- attribute :a_seq, seq_of: :string
47
-
48
- attribute :a_seq, seq_of: Address
49
-
50
- # sequence of string -> integer maps
51
- attribute :a_Seq, seq_of: [:string, :integer]
52
-
53
- attribute :a_seq, seq_of: { type: Adress, :validations [:some_validation] }
54
-
55
- ### Maps
56
-
57
- # string -> string
58
- attribute :a_map, :map_of [:string, :integer]
59
-
60
- attribute :a_map, type: :map, validations: [:required]
61
-
62
- # map of maps
63
- attribute :map_of_maps, :map_of [:string, [:string, :integer]]
64
-
65
- # map of maps
66
- attribute :a_map, :map_of [{type: :string, validators: [:some_check]},
67
- {type: Address, validators: [:required]},
68
- :validators [:required]
69
-
70
- ### Validations
71
-
72
- Primitive types (e.g. :string, :integer, :timestamp, :boolean, etc) are
73
- automatically validated.
74
-
75
- ##### Custom validations
76
-
77
- Via :keyword or `Validator instance`
78
-
79
- class Employee
80
- ...
81
- attribute :name, validations: [:my_validation]
82
-
83
- def assert_my_validation(attr)
84
- add_error(attr, 'some error')
85
- end
86
- end
87
-
88
-
89
- validator = Validator.new(:foo) do
90
- ['some error']
91
- end
92
-
93
- class Employee
94
- ...
95
- attribute :name, validations: [validator]
27
+ ## Built-in Types
28
+
29
+ `:string`, `:integer`, `:boolean`, `:timestamp`, `:map`, `:seq`
30
+
31
+ ## Basic Example
32
+
33
+ ```ruby
34
+ class Employee
35
+ include TypedModel::ModelBase
36
+
37
+ attribute :name
38
+ attribute :a_boolean, type: :boolean
39
+ attribute :an_integer, type: :integer
40
+ attribute :a_timestamp, type: :timestamp
41
+ attribute :widget1, type: :map
42
+ attribute :address, type: Address
43
+ attribute :addresses, seq_of: Address
44
+ attribute :colors, type: :seq
45
+ end
46
+ ```
47
+
48
+ ## Sequences
49
+
50
+ ```ruby
51
+ # non-empty/nil sequence
52
+ attribute :a_seq, type: :seq, validations: [:required]
53
+
54
+ # typed sequence of strings
55
+ attribute :a_seq, seq_of: :string
56
+
57
+ # sequence of Address models
58
+ attribute :a_seq, seq_of: Address
59
+
60
+ # sequence of [string, integer] pairs
61
+ attribute :a_seq, seq_of: [:string, :integer]
62
+
63
+ # sequence with nested type and validations
64
+ attribute :a_seq, seq_of: { type: Address, validations: [:some_validation] }
65
+ ```
66
+
67
+ ## Maps
68
+
69
+ ```ruby
70
+ # string -> integer
71
+ attribute :a_map, map_of: [:string, :integer]
72
+
73
+ # required map
74
+ attribute :a_map, type: :map, validations: [:required]
75
+
76
+ # map of maps
77
+ attribute :map_of_maps, map_of: [:string, [:string, :integer]]
78
+ ```
79
+
80
+ ## Validations
81
+
82
+ Primitive types (`:string`, `:integer`, `:timestamp`, `:boolean`, etc.) are automatically validated. Supply built-in or custom validators via the `validations:` option on `attribute`:
83
+
84
+ ```ruby
85
+ attribute :name, type: :string, validations: [:required]
86
+ attribute :email, type: :string, validations: [:not_blank]
87
+ attribute :started_at, type: :timestamp, validations: [:required]
88
+ ```
89
+
90
+ ### Built-in Validators
91
+
92
+ | Keyword | Fails when | Error key |
93
+ | ------------- | ----------------------------------------------- | ---------- |
94
+ | `:required` | value is `nil` or empty | `:required` |
95
+ | `:not_blank` | value has no non-whitespace characters | `:required` |
96
+ | `:not_nil` | value is `nil` (empty is OK) | `:required_not_nil` |
97
+ | `:timestamp` | value cannot be parsed as a `Time` | `:invalid` |
98
+ | `:integer` | value cannot be coerced to `Integer` | `:invalid` |
99
+ | `:string` | value cannot be coerced to `String` | `:invalid` |
100
+
101
+ ### Custom Validations
102
+
103
+ Via `:keyword` — define an `assert_<name>` method:
104
+
105
+ ```ruby
106
+ class Employee
107
+ include TypedModel::ModelBase
108
+
109
+ attribute :name, validations: [:required, :my_validation]
110
+
111
+ def assert_my_validation(attr)
112
+ add_error(attr, 'some error')
113
+ end
114
+ end
115
+ ```
116
+
117
+ Via `Validator` instance:
118
+
119
+ ```ruby
120
+ validator = TypedModel::Validator.new(:foo) do
121
+ ['some error']
122
+ end
123
+
124
+ class Employee
125
+ include TypedModel::ModelBase
126
+
127
+ attribute :name, validations: [validator]
128
+ end
129
+ ```
130
+
131
+ ### Before-Validation Callbacks
132
+
133
+ Register methods that run before the main `validate` pass. Callbacks are inherited and run in ancestor order (parent -> child):
96
134
 
135
+ ```ruby
136
+ class Employee
137
+ include TypedModel::ModelBase
138
+
139
+ before_validation :normalize_name
140
+
141
+ attribute :name, type: :string
142
+
143
+ def normalize_name
144
+ self.name = name&.strip
145
+ add_error(:name, :required) if name.to_s.empty?
146
+ end
147
+ end
148
+ ```
149
+
150
+ ## Validating Payloads and Reporting Errors
151
+
152
+ Data models are commonly used to validate API request and response payloads. The standard pattern is:
153
+
154
+ 1. Instantiate the model from the payload hash
155
+ 2. Call `valid?` — returns `true`/`false` and populates `errors`
156
+ 3. If invalid, report the problems using `errors.inspect`
157
+
158
+ ### The `errors` Object
159
+
160
+ `errors` is an instance of `TypedModel::Errors` — a hash of attribute names to arrays of error symbols/messages:
161
+
162
+ ```ruby
163
+ model.errors[:name] # => [:required]
164
+ model.errors['address/street'] # => [:required] (nested path)
165
+ model.errors['addresses/1/type'] # => [:required] (array index in path)
166
+ model.errors.empty? # => true/false
167
+ ```
168
+
169
+ Iterate all errors:
170
+
171
+ ```ruby
172
+ model.each_error { |key, msg| puts "#{key}: #{msg}" }
173
+ ```
174
+
175
+ ### Validating Inbound Requests
176
+
177
+ Validate input before processing. Return or raise with `errors.inspect` to convey what's wrong:
178
+
179
+ ```ruby
180
+ def process_request(params)
181
+ request = MyRequest.new(params)
182
+ raise "Invalid request: #{request.errors.inspect}" unless request.valid?
183
+
184
+ handle(request)
185
+ end
186
+ ```
187
+
188
+ In a Rails controller, render the errors directly as JSON:
189
+
190
+ ```ruby
191
+ def create
192
+ response = MyResponse.new(response_params)
193
+
194
+ unless response.valid?
195
+ render json: response.errors.to_h, status: :bad_request
196
+ return
197
+ end
198
+
199
+ process(response)
200
+ render json: response, status: :created
201
+ end
202
+ ```
203
+
204
+ ### Validating Outbound Responses
205
+
206
+ Validate data received from external APIs to catch unexpected payloads early:
207
+
208
+ ```ruby
209
+ def build_response(klass, data)
210
+ response = klass.new(data)
211
+
212
+ unless response.valid?
213
+ raise InvalidResponse, "Invalid #{klass}: #{response.errors.inspect}"
214
+ end
215
+
216
+ response
217
+ end
218
+ ```
219
+
220
+ Avoid logging raw payload data — it may contain sensitive information. When logging `errors.inspect`, ensure your error messages do not embed raw field values or other sensitive data, and prefer structured or otherwise sanitized error formats where possible.
221
+
222
+ ### Soft vs Hard Failure
223
+
224
+ Choose the error-handling strategy that fits the context:
225
+
226
+ ```ruby
227
+ # Soft — log and return a fallback (caller can continue)
228
+ unless request.valid?
229
+ logger.info("Invalid request: #{request.errors.inspect}")
230
+ return empty_response(errors: request.errors, invalid_request: true)
231
+ end
232
+
233
+ # Hard — raise so the caller knows something is fundamentally wrong
234
+ unless request.valid?
235
+ raise "Invalid request: #{request.errors.inspect}"
236
+ end
237
+ ```
97
238
 
98
239
  ## Development
99
240
 
@@ -103,7 +244,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
103
244
 
104
245
  ## Contributing
105
246
 
106
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/typed_model.
247
+ Bug reports and pull requests are welcome on GitHub at https://github.com/lenny/typed_model.
107
248
 
108
249
  ## License
109
250
 
@@ -7,7 +7,8 @@ module TypedModel
7
7
  attr_reader :name, :spec, :model_validations, :mapping_key
8
8
 
9
9
  def initialize(name:, type: nil, seq_of: nil, map_of: nil, validations: [], mapping_key: nil)
10
- @name, @mapping_key = name, mapping_key
10
+ @name = name
11
+ @mapping_key = mapping_key
11
12
  @model_validations = []
12
13
  spec_validations = []
13
14
  validations.each do |v|
@@ -32,12 +33,9 @@ module TypedModel
32
33
  value = attr_value(model)
33
34
  spec.validate(value, model.errors, name)
34
35
  model_validations.each do |v|
35
- method_name = "assert_#{v}"
36
- if model.respond_to?(method_name)
37
- model.send(method_name, name)
38
- else
39
- raise "Unrecognized validation '#{v}'"
40
- end
36
+ method_name = :"assert_#{v}"
37
+ raise "Unrecognized validation '#{v}'" unless model.respond_to?(method_name)
38
+ model.send(method_name, name)
41
39
  end
42
40
  end
43
41
 
@@ -16,12 +16,16 @@ module TypedModel
16
16
  end
17
17
  end
18
18
 
19
- def each_error(&blk)
19
+ def each_error
20
20
  @errors.each do |(k, msgs)|
21
- msgs.each { |m| blk.call(k, m) }
21
+ msgs.each { |m| yield(k, m) }
22
22
  end
23
23
  end
24
24
 
25
+ def to_h
26
+ @errors.to_h
27
+ end
28
+
25
29
  def [](k)
26
30
  @errors[k]
27
31
  end
@@ -8,10 +8,8 @@ module TypedModel
8
8
  end
9
9
 
10
10
  def typecast_value(values)
11
- unless values.nil?
12
- values.each_with_object({}) do |(k, v), h|
13
- h[key_spec.typecast_value(k)] = value_spec.typecast_value(v)
14
- end
11
+ values&.each_with_object({}) do |(k, v), h|
12
+ h[key_spec.typecast_value(k)] = value_spec.typecast_value(v)
15
13
  end
16
14
  end
17
15
 
@@ -22,11 +20,9 @@ module TypedModel
22
20
  end
23
21
 
24
22
  def validate(value, errors, key_prefix)
25
- unless value.nil?
26
- value.each_pair do |k, v|
27
- key_spec.validate(k, errors, "#{key_prefix}/keys/#{k}")
28
- value_spec.validate(v, errors, "#{key_prefix}/#{k}")
29
- end
23
+ value&.each_pair do |k, v|
24
+ key_spec.validate(k, errors, "#{key_prefix}/keys/#{k}")
25
+ value_spec.validate(v, errors, "#{key_prefix}/#{k}")
30
26
  end
31
27
  end
32
28
  end
@@ -32,12 +32,11 @@ module TypedModel
32
32
  class << self
33
33
  def attribute(name, opts = {})
34
34
  @declared_attributes ||= {}
35
- attribute_def = AttributeDefinition.new(opts.merge(name: name))
35
+ attribute_def = AttributeDefinition.new(**opts, name: name)
36
36
  @declared_attributes[name.to_sym] = attribute_def
37
- @declared_attributes[attribute_def.mapping_key.to_sym] = attribute_def
38
37
  attr_reader name
39
- define_method "#{name}=" do |v|
40
- instance_variable_set("@#{name}", attribute_def.typecast_value(v))
38
+ define_method :"#{name}=" do |v|
39
+ instance_variable_set(:"@#{name}", attribute_def.typecast_value(v))
41
40
  end
42
41
  end
43
42
 
@@ -46,6 +45,13 @@ module TypedModel
46
45
  attributes.merge!(c.instance_variable_get(:@declared_attributes) || {})
47
46
  end
48
47
  end
48
+
49
+ def declared_attributes_with_alternates
50
+ declared_attributes.each_with_object({}) do |(name, attr_def), h|
51
+ h[name] = attr_def
52
+ h[attr_def.mapping_key.to_sym] = attr_def
53
+ end
54
+ end
49
55
  end
50
56
  end
51
57
  end
@@ -57,9 +63,10 @@ module TypedModel
57
63
  def attributes=(values = {})
58
64
  return if values.nil?
59
65
 
66
+ attrs_with_alternates = self.class.declared_attributes_with_alternates
60
67
  values.each_pair do |k, v|
61
- if (attr_def = self.class.declared_attributes[k.to_sym])
62
- setter = "#{attr_def.name}="
68
+ if (attr_def = attrs_with_alternates[k.to_sym])
69
+ setter = :"#{attr_def.name}="
63
70
  send(setter, v) if respond_to?(setter)
64
71
  end
65
72
  end
@@ -38,22 +38,21 @@ module TypedModel
38
38
 
39
39
  def assert_not_blank(field)
40
40
  v = send(field)
41
- add_error(field, :required) unless v.to_s.match(/\S+/)
41
+ add_error(field, :required) unless v.to_s.match?(/\S/)
42
42
  end
43
43
 
44
44
  def assert_timestamp(field)
45
45
  v = send(field)
46
- if !v.is_a?(Time)
46
+ unless v.is_a?(Time)
47
47
  s = v.to_s
48
- if s.match(/\S/)
49
- send("#{field}=", Time.parse(s))
50
- end
48
+ send(:"#{field}=", Time.parse(s)) if s.match?(/\S/)
51
49
  end
52
- rescue
50
+ rescue StandardError
53
51
  add_error(field, :invalid)
54
52
  end
55
53
 
56
54
  def valid?
55
+ @errors = Errors.new
57
56
  self.class.before_validation_callbacks.each do |fname|
58
57
  send(fname)
59
58
  end
@@ -15,10 +15,8 @@ module TypedModel
15
15
  end
16
16
 
17
17
  def validate(value, errors, key_prefix)
18
- unless value.nil?
19
- value.each_with_index do |v, index|
20
- spec.validate(v, errors, "#{key_prefix}/#{index}")
21
- end
18
+ value&.each_with_index do |v, index|
19
+ spec.validate(v, errors, "#{key_prefix}/#{index}")
22
20
  end
23
21
  end
24
22
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'typed_model/seq_of'
2
4
  require 'typed_model/map_of'
3
5
  require 'typed_model/types'
@@ -11,26 +13,37 @@ module TypedModel
11
13
 
12
14
  t, seq_of, map_of, validations = spec_map.values_at(:type, :seq_of, :map_of, :validations)
13
15
 
14
- validators = (validations || []).map do |v|
15
- Validator.build(v)
16
- end
17
-
16
+ validators = build_validators(validations)
18
17
  spec_opts = { type: t, validators: validators }
19
18
 
20
19
  if seq_of
21
- seq_spec = build(seq_of)
22
- new(spec_opts.merge(type: SeqOf.new(seq_spec)))
20
+ build_seq_of(spec_opts, seq_of)
23
21
  elsif map_of
24
- key_spec_opts, val_spec_opts = map_of
25
- if val_spec_opts.is_a?(Array)
26
- val_spec_opts = { type: :map, map_of: val_spec_opts }
27
- end
28
- map_of = MapOf.new(build(key_spec_opts), build(val_spec_opts))
29
- new(spec_opts.merge(type: map_of))
22
+ build_map_of(spec_opts, map_of)
30
23
  else
31
24
  new(spec_opts)
32
25
  end
33
26
  end
27
+
28
+ private
29
+
30
+ def build_validators(validations)
31
+ (validations || []).map { |v| Validator.build(v) }
32
+ end
33
+
34
+ def build_seq_of(spec_opts, seq_of)
35
+ seq_spec = build(seq_of)
36
+ new(spec_opts.merge(type: SeqOf.new(seq_spec)))
37
+ end
38
+
39
+ def build_map_of(spec_opts, map_of)
40
+ key_spec_opts, val_spec_opts = map_of
41
+ if val_spec_opts.is_a?(Array)
42
+ val_spec_opts = { type: :map, map_of: val_spec_opts }
43
+ end
44
+ map_of = MapOf.new(build(key_spec_opts), build(val_spec_opts))
45
+ new(spec_opts.merge(type: map_of))
46
+ end
34
47
  end
35
48
 
36
49
  attr_accessor :value_type, :validators
@@ -41,18 +54,16 @@ module TypedModel
41
54
  end
42
55
 
43
56
  def typecast_value(v)
44
- if value_type
45
- if value_type.respond_to?(:typecast_value)
46
- value_type.typecast_value(v)
47
- elsif value_type.is_a?(Symbol) && Types.recognized?(value_type)
48
- Types.typecast(value_type, v)
49
- elsif value_type.is_a?(Class)
50
- instantiate_type(v)
51
- else
52
- raise "unrecognized type '#{value_type.inspect}'"
53
- end
57
+ return v unless value_type
58
+
59
+ if value_type.respond_to?(:typecast_value)
60
+ value_type.typecast_value(v)
61
+ elsif recognized_scalar_type?
62
+ Types.typecast(value_type, v)
63
+ elsif value_type.is_a?(Class)
64
+ instantiate_type(v)
54
65
  else
55
- v
66
+ raise "unrecognized type '#{value_type.inspect}'"
56
67
  end
57
68
  end
58
69
 
@@ -65,9 +76,7 @@ module TypedModel
65
76
  value_type.validate(value, errors, key_prefix)
66
77
  end
67
78
 
68
- if value.respond_to?(:valid?) && value.respond_to?(:errors) && !value.valid?
69
- errors.merge!(value.errors, key_prefix)
70
- end
79
+ merge_value_errors(value, errors, key_prefix)
71
80
  end
72
81
 
73
82
  def to_data(value)
@@ -82,8 +91,12 @@ module TypedModel
82
91
 
83
92
  private
84
93
 
94
+ def recognized_scalar_type?
95
+ value_type.is_a?(Symbol) && Types.recognized?(value_type)
96
+ end
97
+
85
98
  def execute_validators(errors, key_prefix, value)
86
- validators.each_with_object(errors) do |v, errors|
99
+ validators.each do |v|
87
100
  (v.validate(value) || []).each do |s|
88
101
  errors.add(key_prefix, s)
89
102
  end
@@ -91,14 +104,22 @@ module TypedModel
91
104
  end
92
105
 
93
106
  def validate_recognized_types(errors, key_prefix, value)
94
- if value_type.is_a?(Symbol) && Types.recognized?(value_type)
95
- if value != nil && Types.send(value_type, value).nil?
107
+ if recognized_scalar_type?
108
+ if !value.nil? && Types.send(value_type, value).nil?
96
109
  errors.add(key_prefix, :invalid)
97
110
  end
98
111
  end
99
112
  end
100
113
 
114
+ def merge_value_errors(value, errors, key_prefix)
115
+ if value.respond_to?(:valid?) && value.respond_to?(:errors) && !value.valid?
116
+ errors.merge!(value.errors, key_prefix)
117
+ end
118
+ end
119
+
101
120
  def instantiate_type(v)
121
+ return nil if v.nil?
122
+
102
123
  if v.is_a?(value_type)
103
124
  v
104
125
  else
@@ -108,5 +129,3 @@ module TypedModel
108
129
  end
109
130
  end
110
131
  end
111
-
112
-
@@ -1,3 +1,5 @@
1
+ require 'set'
2
+
1
3
  module TypedModel
2
4
 
3
5
  # Supported Types/Type coercion
@@ -11,7 +13,7 @@ module TypedModel
11
13
  #
12
14
  #
13
15
  class Types
14
- PRIMITIVE_CLASSES = Set.new([String, TrueClass, FalseClass, Integer, Float, Fixnum])
16
+ PRIMITIVE_CLASSES = Set.new([String, TrueClass, FalseClass, Integer, Float])
15
17
 
16
18
  class << self
17
19
  def recognized?(t)
@@ -19,7 +21,8 @@ module TypedModel
19
21
  end
20
22
 
21
23
  def typecast(sym, value)
22
- send(sym, value) || value
24
+ type_casted = send(sym, value)
25
+ type_casted.nil? ? value : type_casted
23
26
  end
24
27
 
25
28
  def timestamp(v)
@@ -28,24 +31,22 @@ module TypedModel
28
31
  else
29
32
  Time.parse(v)
30
33
  end
31
- rescue
34
+ rescue StandardError
32
35
  nil
33
36
  end
34
37
 
35
38
  def boolean(v)
36
39
  case v
37
- when "true", true then
40
+ when 'true', true
38
41
  true
39
- when "false", false then
42
+ when 'false', false
40
43
  false
41
- else
42
- nil
43
44
  end
44
45
  end
45
46
 
46
47
  def integer(v)
47
48
  Integer(v)
48
- rescue
49
+ rescue StandardError
49
50
  nil
50
51
  end
51
52
 
@@ -4,24 +4,16 @@ module TypedModel
4
4
  class Validator
5
5
  class << self
6
6
  def build(arg)
7
- if arg.respond_to?(:validate) && arg.respond_to?(:name)
8
- return arg
9
- end
10
- if arg.is_a?(Symbol)
11
- if Validators.recognized?(arg)
12
- new_builtin_validator(arg)
13
- else
14
- raise "Unrecognized validation '#{arg}'"
15
- end
16
- else
17
- raise "failed to create validator from '#{arg}'"
18
- end
7
+ return arg if arg.respond_to?(:validate) && arg.respond_to?(:name)
8
+ raise "failed to create validator from '#{arg}'" unless arg.is_a?(Symbol)
9
+ raise "Unrecognized validation '#{arg}'" unless Validators.recognized?(arg)
10
+ new_builtin_validator(arg)
19
11
  end
20
12
 
21
13
  def from_primitive(t)
22
14
  new(t) do |v|
23
15
  errors = []
24
- if v != nil && (msg = send("assert_#{t}", v))
16
+ if !v.nil? && (msg = send(:"assert_#{t}", v))
25
17
  errors << msg
26
18
  end
27
19
  errors
@@ -5,47 +5,39 @@ module TypedModel
5
5
  module_function
6
6
 
7
7
  def recognized?(sym)
8
- respond_to?("assert_#{sym}")
8
+ respond_to?(:"assert_#{sym}")
9
9
  end
10
10
 
11
11
  def validate(sym, value)
12
- method_name = "assert_#{sym}"
13
- unless respond_to?(method_name)
14
- raise "Unrecognized validation '#{sym}'"
15
- end
12
+ method_name = :"assert_#{sym}"
13
+ raise "Unrecognized validation '#{sym}'" unless respond_to?(method_name)
16
14
  send(method_name, value)
17
15
  end
18
16
 
19
17
  def assert_timestamp(v)
20
- if Types.timestamp(v).nil?
21
- :invalid
22
- end
18
+ :invalid if Types.timestamp(v).nil?
23
19
  end
24
20
 
25
21
  def assert_required(v)
26
- if v.nil? || (v.respond_to?(:empty?) && v.empty?)
27
- :required
28
- end
22
+ :required if v.nil? || (v.respond_to?(:empty?) && v.empty?)
29
23
  end
30
24
 
31
- alias assert_not_blank assert_required
25
+ # Checks that value contains at least one non-whitespace character.
26
+ # Semantically different from assert_required which only checks empty/nil.
27
+ def assert_not_blank(v)
28
+ :required unless /\S/.match?(v.to_s)
29
+ end
32
30
 
33
31
  def assert_not_nil(v)
34
- if v.nil?
35
- :required_not_nil
36
- end
32
+ :required_not_nil if v.nil?
37
33
  end
38
34
 
39
35
  def assert_string(v)
40
- if Types.string(v).nil?
41
- :invalid
42
- end
36
+ :invalid if Types.string(v).nil?
43
37
  end
44
38
 
45
39
  def assert_integer(v)
46
- if Types.integer(v).nil?
47
- :invalid
48
- end
40
+ :invalid if Types.integer(v).nil?
49
41
  end
50
42
  end
51
43
  end
@@ -1,3 +1,3 @@
1
1
  module TypedModel
2
- VERSION = "0.1.0"
2
+ VERSION = "1.0.0"
3
3
  end
data/typed_model.gemspec CHANGED
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
37
  spec.require_paths = ["lib"]
38
38
 
39
- spec.add_development_dependency "bundler", "~> 1.16"
39
+ spec.add_development_dependency "bundler"
40
40
  spec.add_development_dependency "rake", "~> 10.0"
41
41
  spec.add_development_dependency "rspec", "~> 3.0"
42
42
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typed_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lenny Marks
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-08-10 00:00:00.000000000 Z
11
+ date: 2026-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.16'
19
+ version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.16'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -62,7 +62,9 @@ extra_rdoc_files: []
62
62
  files:
63
63
  - ".gitignore"
64
64
  - ".rspec"
65
+ - ".ruby-version"
65
66
  - ".travis.yml"
67
+ - CHANGELOG.md
66
68
  - Gemfile
67
69
  - LICENSE
68
70
  - LICENSE.txt
@@ -91,7 +93,7 @@ metadata:
91
93
  homepage_uri: https://github.com/lenny/typed_model
92
94
  source_code_uri: https://github.com/lenny/typed_model
93
95
  changelog_uri: https://github.com/lenny/typed_model/blob/master/CHANGELOG.md
94
- post_install_message:
96
+ post_install_message:
95
97
  rdoc_options: []
96
98
  require_paths:
97
99
  - lib
@@ -106,9 +108,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
108
  - !ruby/object:Gem::Version
107
109
  version: '0'
108
110
  requirements: []
109
- rubyforge_project:
110
- rubygems_version: 2.7.8
111
- signing_key:
111
+ rubygems_version: 3.3.26
112
+ signing_key:
112
113
  specification_version: 4
113
114
  summary: A Ruby library for defining data schemas via classes with typed fields and
114
115
  built in hydration and serialization