parametric 0.2.23 → 0.2.24
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 +4 -4
- data/README.md +24 -1
- data/lib/parametric/field.rb +51 -0
- data/lib/parametric/version.rb +1 -1
- data/lib/parametric/wrapper.rb +102 -0
- data/spec/schema_spec.rb +42 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef2f5f7c8f444e02045209688a8fecdc8157a4e7ac6c6e5ca64614bef263f568
|
4
|
+
data.tar.gz: bd843a7998284ab0caf9c989fa0dedd17a2830d3f7a6fe5ad1f306d5417137a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ff1524559a0caa82acf4ad8ed896387d151096a3cc0e2caf9035a61ebf4151d02f0249d7cb2506e3f8be30f983c836861cdc1024869f8e98b1b75f228728ef5
|
7
|
+
data.tar.gz: 3f0e20759b4a52412ad8b620098065dd6ff6dc6ceb1680a0e2e718b99216ac85c5b7ed9a65d4a4f0003916542cf63ae59a78a2c5afbed6a002764b241486b8b1
|
data/README.md
CHANGED
@@ -243,7 +243,30 @@ instance.data # => #<MyStruct::Data2:...> (User struct)
|
|
243
243
|
instance.data.name # => 'Joe'
|
244
244
|
```
|
245
245
|
|
246
|
-
##
|
246
|
+
## `wrap`
|
247
|
+
|
248
|
+
This helper turns a custom object into a policy. The object must respond to `.coerce(value)` and return something that responds to `#errors() Hash`.
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
UserType = Data.define(:name) do
|
252
|
+
def self.coerce(value)
|
253
|
+
return value if value.is_a?(self)
|
254
|
+
new(value)
|
255
|
+
end
|
256
|
+
|
257
|
+
def errors
|
258
|
+
return { name: ['cannot be blank'] } if name.nil? || name.strip.empty?
|
259
|
+
{}
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
schema = Parametric::Schema.new do
|
264
|
+
field(:user).wrap(UserType).present
|
265
|
+
end
|
266
|
+
|
267
|
+
```
|
268
|
+
|
269
|
+
## Built-in policies
|
247
270
|
|
248
271
|
Type coercions (the `type` method) and validations (the `validate` method) are all _policies_.
|
249
272
|
|
data/lib/parametric/field.rb
CHANGED
@@ -5,6 +5,7 @@ require 'parametric/field_dsl'
|
|
5
5
|
require 'parametric/policy_adapter'
|
6
6
|
require 'parametric/one_of'
|
7
7
|
require 'parametric/tagged_one_of'
|
8
|
+
require 'parametric/wrapper'
|
8
9
|
|
9
10
|
module Parametric
|
10
11
|
class ConfigurationError < StandardError; end
|
@@ -89,6 +90,56 @@ module Parametric
|
|
89
90
|
policy OneOf.new(schemas)
|
90
91
|
end
|
91
92
|
|
93
|
+
# Wraps a field with a custom type that handles both coercion and validation.
|
94
|
+
#
|
95
|
+
# The wrapper object must implement two methods:
|
96
|
+
# - `coerce(value)`: Converts the input value to the desired type
|
97
|
+
# - `errors`: Returns a hash of validation errors (empty hash if valid)
|
98
|
+
#
|
99
|
+
# This is useful for integrating domain objects, value objects, or custom types
|
100
|
+
# that have their own validation logic into Parametric schemas.
|
101
|
+
#
|
102
|
+
# @param wrapper [Object] An object that responds to `coerce(value)` and has an `errors` method
|
103
|
+
# @return [Field] Returns self for method chaining
|
104
|
+
#
|
105
|
+
# @example Using with a Data class
|
106
|
+
# UserType = Data.define(:name) do
|
107
|
+
# def self.coerce(value)
|
108
|
+
# return value if value.is_a?(self)
|
109
|
+
# new(value)
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# def errors
|
113
|
+
# return { name: ['cannot be blank'] } if name.nil? || name.strip.empty?
|
114
|
+
# {}
|
115
|
+
# end
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# schema = Parametric::Schema.new do
|
119
|
+
# field(:user).wrap(UserType).present
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# @example Using with a custom class
|
123
|
+
# class EmailAddress
|
124
|
+
# def self.coerce(value)
|
125
|
+
# new(value.to_s)
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# def initialize(email)
|
129
|
+
# @email = email.strip.downcase
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# def errors
|
133
|
+
# return { email: ['invalid format'] } unless @email.match?(/\A[^@\s]+@[^@\s]+\z/)
|
134
|
+
# {}
|
135
|
+
# end
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# field(:email).wrap(EmailAddress)
|
139
|
+
def wrap(wrapper)
|
140
|
+
policy Wrapper.new(wrapper)
|
141
|
+
end
|
142
|
+
|
92
143
|
def schema(sc = nil, &block)
|
93
144
|
sc = (sc ? sc : Schema.new(&block))
|
94
145
|
meta schema: sc
|
data/lib/parametric/version.rb
CHANGED
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parametric
|
4
|
+
# A policy wrapper that delegates type coercion and validation to external objects.
|
5
|
+
#
|
6
|
+
# This allows integration of custom types, domain objects, or value objects that
|
7
|
+
# implement their own coercion and validation logic into Parametric schemas.
|
8
|
+
#
|
9
|
+
# The wrapped object must implement:
|
10
|
+
# - `coerce(value)`: Method to convert input values to the desired type
|
11
|
+
# - The returned object must have an `errors` method that returns validation errors
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# Money = Data.define(:amount, :currency) do
|
15
|
+
# def self.coerce(value)
|
16
|
+
# case value
|
17
|
+
# when Hash
|
18
|
+
# new(value[:amount], value[:currency])
|
19
|
+
# when String
|
20
|
+
# parts = value.split(' ')
|
21
|
+
# new(parts[0].to_f, parts[1])
|
22
|
+
# else
|
23
|
+
# new(value, 'USD')
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# def errors
|
28
|
+
# errors = {}
|
29
|
+
# errors[:amount] = ['must be positive'] if amount <= 0
|
30
|
+
# errors[:currency] = ['invalid'] unless %w[USD EUR GBP].include?(currency)
|
31
|
+
# errors
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# field(:price).wrap(Money)
|
36
|
+
#
|
37
|
+
# @see Field#wrap
|
38
|
+
class Wrapper
|
39
|
+
# Initialize the wrapper with a caster object.
|
40
|
+
#
|
41
|
+
# @param caster [Object] Object that responds to `coerce(value)` method
|
42
|
+
def initialize(caster)
|
43
|
+
@caster = caster
|
44
|
+
end
|
45
|
+
|
46
|
+
# Build a policy runner for this wrapper.
|
47
|
+
#
|
48
|
+
# @param key [Symbol] The field key being processed
|
49
|
+
# @param value [Object] The input value to be coerced and validated
|
50
|
+
# @param payload [Hash] The complete input payload (unused)
|
51
|
+
# @param context [Context] The validation context (unused)
|
52
|
+
# @return [Runner] A runner instance that handles the coercion and validation
|
53
|
+
def build(key, value, payload:, context:)
|
54
|
+
Runner.new(@caster, key, value)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return metadata about this policy.
|
58
|
+
#
|
59
|
+
# @return [Hash] Metadata hash containing the wrapper type
|
60
|
+
def meta_data
|
61
|
+
{ type: @caster }
|
62
|
+
end
|
63
|
+
|
64
|
+
# Policy runner that executes the wrapper's coercion and validation logic.
|
65
|
+
#
|
66
|
+
# This class implements the policy runner interface required by Parametric's
|
67
|
+
# policy system. It delegates coercion to the wrapper object and collects
|
68
|
+
# validation errors from the coerced value.
|
69
|
+
class Runner
|
70
|
+
attr_reader :key, :value
|
71
|
+
|
72
|
+
# Initialize the runner with coercion logic.
|
73
|
+
#
|
74
|
+
# @param caster [Object] Object that responds to `coerce(value)`
|
75
|
+
# @param key [Symbol] The field key being processed
|
76
|
+
# @param value [Object] The input value to be coerced and validated
|
77
|
+
def initialize(caster, key, value)
|
78
|
+
@caster = caster
|
79
|
+
@key = key
|
80
|
+
@value = caster.coerce(value)
|
81
|
+
@errors = @value.errors
|
82
|
+
end
|
83
|
+
|
84
|
+
# Check if this policy should run.
|
85
|
+
#
|
86
|
+
# @return [Boolean] Always returns true for wrapper policies
|
87
|
+
def eligible?
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
# Check if the coerced value is valid.
|
92
|
+
#
|
93
|
+
# @return [Boolean] True if no validation errors, false otherwise
|
94
|
+
def valid? = @errors.empty?
|
95
|
+
|
96
|
+
# Generate a human-readable error message from validation errors.
|
97
|
+
#
|
98
|
+
# @return [String] Formatted error message combining all validation errors
|
99
|
+
def message = @errors.map { |k, v| "#{k} #{v.join(', ')}" }.join('. ')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/spec/schema_spec.rb
CHANGED
@@ -274,6 +274,48 @@ describe Parametric::Schema do
|
|
274
274
|
end
|
275
275
|
end
|
276
276
|
|
277
|
+
describe '#wrap' do
|
278
|
+
let(:schema) do
|
279
|
+
described_class.new do |sc, _|
|
280
|
+
sc.field(:user).wrap(user_type).present
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
let(:user_type) do
|
285
|
+
Data.define(:name) do
|
286
|
+
def self.coerce(value)
|
287
|
+
return value if value.is_a?(self)
|
288
|
+
|
289
|
+
new(value)
|
290
|
+
end
|
291
|
+
|
292
|
+
def errors
|
293
|
+
return { name: ['cannot be blank'] } if name.nil? || name.strip.empty?
|
294
|
+
|
295
|
+
{}
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
it 'delegates to .coerce, #errors interface' do
|
301
|
+
result = schema.resolve(user: 'Joe')
|
302
|
+
expect(result.valid?).to be true
|
303
|
+
expect(result.output[:user]).to eq(user_type.new('Joe'))
|
304
|
+
|
305
|
+
result = schema.resolve(user: '')
|
306
|
+
expect(result.valid?).to be false
|
307
|
+
expect(result.errors['$.user']).to eq ['name cannot be blank']
|
308
|
+
|
309
|
+
result = schema.resolve(user: user_type.new('Joe'))
|
310
|
+
expect(result.valid?).to be true
|
311
|
+
expect(result.output[:user]).to eq(user_type.new('Joe'))
|
312
|
+
end
|
313
|
+
|
314
|
+
it 'does not break #walk' do
|
315
|
+
expect(schema.walk(:default).output).to be_a(Hash)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
277
319
|
describe '#one_of' do
|
278
320
|
let(:kwh_schema) do
|
279
321
|
described_class.new do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: parametric
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.24
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ismael Celis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -86,6 +86,7 @@ files:
|
|
86
86
|
- lib/parametric/struct.rb
|
87
87
|
- lib/parametric/tagged_one_of.rb
|
88
88
|
- lib/parametric/version.rb
|
89
|
+
- lib/parametric/wrapper.rb
|
89
90
|
- parametric.gemspec
|
90
91
|
- spec/custom_block_validator_spec.rb
|
91
92
|
- spec/dsl_spec.rb
|