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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4c6ddb5aba085ae7a2c3d0adf46164ecaf9bf3d3e7d6a0ec6891d48a0af0b77
4
- data.tar.gz: 3612f9a678802ed4353d964734932951fb5a09fdceaf52eca76090907210a225
3
+ metadata.gz: ef2f5f7c8f444e02045209688a8fecdc8157a4e7ac6c6e5ca64614bef263f568
4
+ data.tar.gz: bd843a7998284ab0caf9c989fa0dedd17a2830d3f7a6fe5ad1f306d5417137a0
5
5
  SHA512:
6
- metadata.gz: 7181f7d9cd318fb362ab77a6bc9da5b9bf6bc41373b6de6d8e3e41ddb92d23b9a2a46c073d88570692edf644ab748c9f7fdb70e565372e710ece850978ed3cf0
7
- data.tar.gz: 7998b13d22814989ebcd0cda3fab50b05a522dc853157dde131a0e0bb4b53f06aabb25c84c120136f2204ba289cc274a05793beb039edc466c172835bf3ceca5
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
- ## Built-in policies
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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parametric
4
- VERSION = '0.2.23'
4
+ VERSION = '0.2.24'
5
5
  end
@@ -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.23
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-09-22 00:00:00.000000000 Z
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