Class: Parametric::Field

Inherits:
Object
  • Object
show all
Includes:
FieldDSL
Defined in:
lib/parametric/field.rb

Defined Under Namespace

Classes: PolicyWithKey, Result

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from FieldDSL

#declared, #nullable, #options, #present, #required

Constructor Details

#initialize(key, registry = Parametric.registry) ⇒ Field

Returns a new instance of Field.



19
20
21
22
23
24
25
26
# File 'lib/parametric/field.rb', line 19

def initialize(key, registry = Parametric.registry)
  @key = key
  @policies = []
  @registry = registry
  @default_block = nil
  @meta_data = {}
  @policies = []
end

Instance Attribute Details

#keyObject (readonly)

Returns the value of attribute key.



16
17
18
# File 'lib/parametric/field.rb', line 16

def key
  @key
end

#meta_dataObject (readonly)

Returns the value of attribute meta_data.



16
17
18
# File 'lib/parametric/field.rb', line 16

def 
  @meta_data
end

Instance Method Details

#==(other) ⇒ Object



28
29
30
# File 'lib/parametric/field.rb', line 28

def ==(other)
  other.is_a?(Field) && key == other.key && policies == other.policies &&  == other.
end

#default(value) ⇒ Object



37
38
39
40
41
# File 'lib/parametric/field.rb', line 37

def default(value)
  meta default: value
  @default_block = (value.respond_to?(:call) ? value : ->(key, payload, context) { value })
  self
end

#from(another_field) ⇒ Object



149
150
151
152
153
154
155
156
# File 'lib/parametric/field.rb', line 149

def from(another_field)
  meta another_field.
  another_field.policies.each do |plc|
    policies << plc
  end

  self
end

#has_policy?(key) ⇒ Boolean

Returns:

  • (Boolean)


158
159
160
# File 'lib/parametric/field.rb', line 158

def has_policy?(key)
  policies.any? { |pol| pol.key == key }
end

#meta(hash = nil) ⇒ Object



32
33
34
35
# File 'lib/parametric/field.rb', line 32

def meta(hash = nil)
  @meta_data = @meta_data.merge(hash) if hash.is_a?(Hash)
  self
end

#one_of(*schemas) ⇒ Field

Validate field value against multiple schemas, accepting the first valid match.

This method allows a field to accept one of several possible object structures. It validates the input against each provided schema in order and uses the output from the first schema that successfully validates the input.

The validation fails if:

  • No schemas match the input (invalid data)

  • Multiple schemas match the input (ambiguous structure)

Examples:

Define a field that can be either a user or admin object

user_schema = Schema.new { field(:name).type(:string).present }
admin_schema = Schema.new { field(:role).type(:string).options(['admin']) }

schema = Schema.new do |sc, _|
  sc.field(:person).type(:object).one_of(user_schema, admin_schema)
end

With different data structures

payment_schema = Schema.new do
  field(:amount).type(:number).present
  field(:currency).type(:string).present
end

credit_schema = Schema.new do
  field(:credits).type(:integer).present
end

schema = Schema.new do |sc, _|
  sc.field(:transaction).type(:object).one_of(payment_schema, credit_schema)
end

Parameters:

  • schemas (Array<Schema>)

    Variable number of Schema objects to validate against

Returns:

  • (Field)

    Returns self for method chaining



89
90
91
# File 'lib/parametric/field.rb', line 89

def one_of(*schemas)
  policy OneOf.new(schemas)
end

#policy(key, *args) ⇒ Object Also known as: type



43
44
45
46
47
48
# File 'lib/parametric/field.rb', line 43

def policy(key, *args)
  pol = lookup(key, args)
  meta pol.
  policies << pol
  self
end

#resolve(payload, context) ⇒ Object



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/parametric/field.rb', line 175

def resolve(payload, context)
  eligible = payload.key?(key)
  value = payload[key] # might be nil

  if !eligible && has_default?
    eligible = true
    value = default_block.call(key, payload, context)
    return Result.new(eligible, value)
  end

  policies.each do |policy|
    begin
      pol = policy.build(key, value, payload:, context:)
      if !pol.eligible?
        eligible = pol.include_non_eligible_in_ouput?
        if has_default?
          eligible = true
          value = default_block.call(key, payload, context)
        end
        break
      else
        value = pol.value
        if !pol.valid?
          eligible = true # eligible, but has errors
          context.add_error pol.message
          break # only one error at a time
        end
      end
    rescue StandardError => e
      context.add_error e.message
      break
    end
  end

  Result.new(eligible, value)
end

#schema(sc = nil, &block) ⇒ Object



143
144
145
146
147
# File 'lib/parametric/field.rb', line 143

def schema(sc = nil, &block)
  sc = (sc ? sc : Schema.new(&block))
  meta schema: sc
  policy sc.schema
end

#tagged_one_of(instance = nil, &block) ⇒ Object



51
52
53
# File 'lib/parametric/field.rb', line 51

def tagged_one_of(instance = nil, &block)
  policy(instance || Parametric::TaggedOneOf.new(&block))
end

#visit(meta_key = nil, &visitor) ⇒ Object



162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/parametric/field.rb', line 162

def visit(meta_key = nil, &visitor)
  if sc = [:schema]
    if sc.is_a?(Array)
      sc.map { |s| s.schema.visit(meta_key, &visitor) }
    else
      r = sc.schema.visit(meta_key, &visitor)
      ([:type] == :array) ? [r] : r
    end
  else
    meta_key ? [meta_key] : yield(self)
  end
end

#wrap(wrapper) ⇒ Field

Wraps a field with a custom type that handles both coercion and validation.

The wrapper object must implement two methods:

  • ‘coerce(value)`: Converts the input value to the desired type

  • ‘errors`: Returns a hash of validation errors (empty hash if valid)

This is useful for integrating domain objects, value objects, or custom types that have their own validation logic into Parametric schemas.

Examples:

Using with a Data class

UserType = Data.define(:name) do
  def self.coerce(value)
    return value if value.is_a?(self)
    new(value)
  end

  def errors
    return { name: ['cannot be blank'] } if name.nil? || name.strip.empty?
    {}
  end
end

schema = Parametric::Schema.new do
  field(:user).wrap(UserType).present
end

Using with a custom class

class EmailAddress
  def self.coerce(value)
    new(value.to_s)
  end

  def initialize(email)
    @email = email.strip.downcase
  end

  def errors
    return { email: ['invalid format'] } unless @email.match?(/\A[^@\s]+@[^@\s]+\z/)
    {}
  end
end

field(:email).wrap(EmailAddress)

Parameters:

  • wrapper (Object)

    An object that responds to ‘coerce(value)` and has an `errors` method

Returns:

  • (Field)

    Returns self for method chaining



139
140
141
# File 'lib/parametric/field.rb', line 139

def wrap(wrapper)
  policy Wrapper.new(wrapper)
end