Class: Karafka::Core::Contractable::Contract

Inherits:
Object
  • Object
show all
Extended by:
Karafka::Core::Configurable
Defined in:
lib/karafka/core/contractable/contract.rb

Overview

Note:

This contract does NOT support rules inheritance as it was never needed in Karafka

Base contract for all the contracts that check data format

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Karafka::Core::Configurable

extended, included

Class Attribute Details

.rulesArray<Rule> (readonly)

Returns all the validation rules defined for a given contract.

Returns:

  • (Array<Rule>)

    all the validation rules defined for a given contract



30
31
32
# File 'lib/karafka/core/contractable/contract.rb', line 30

def rules
  @rules
end

Class Method Details

.nested(path) ⇒ Object

Allows for definition of a scope/namespace for nested validations

Examples:

nested(:key) do
  required(:inside) { |inside| inside.is_a?(String) }
end

Parameters:

  • path (Symbol)

    path in the hash for nesting



40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/karafka/core/contractable/contract.rb', line 40

def nested(path, &)
  init_accu
  @nested << path
  begin
    instance_eval(&)
  ensure
    # Always pop, even if the block raises. Otherwise a rescued exception during
    # contract definition would leave `path` on @nested and prefix it onto every rule
    # defined afterwards.
    @nested.pop
  end
end

.optional(*keys, &block) ⇒ Object

Parameters:

  • keys (Array<Symbol>)

    single or full path

  • block (Proc)

    validation rule



65
66
67
68
# File 'lib/karafka/core/contractable/contract.rb', line 65

def optional(*keys, &block)
  init_accu
  @rules << Rule.new(@nested + keys, :optional, block).freeze
end

.required(*keys, &block) ⇒ Object

Defines a rule for a required field (required means, that will automatically create an error if missing)

Parameters:

  • keys (Array<Symbol>)

    single or full path

  • block (Proc)

    validation rule



58
59
60
61
# File 'lib/karafka/core/contractable/contract.rb', line 58

def required(*keys, &block)
  init_accu
  @rules << Rule.new(@nested + keys, :required, block).freeze
end

.virtual(&block) ⇒ Object

Note:

The returned error Array is owned by the contract: #call prepends the current scope onto each pair in place and collects them, so a rule must return a freshly built Array on every call. Returning a memoized, shared or frozen Array is not supported -- in-place scoping would accumulate the prefix across validations (or raise FrozenError). Build the result in the block (e.g. [[%i[id], :invalid]]) rather than returning a constant.

Defines a virtual rule that validates the whole data rather than a single key. Unlike required/optional, the block receives the full data and returns its own errors.

Parameters:

  • block (Proc)

    validation rule, called with (data, errors, contract). It must return either a non-Array (true/nil/false) for "no errors", or an Array of [path, message] error pairs (where path is itself an Array of symbols).



83
84
85
86
# File 'lib/karafka/core/contractable/contract.rb', line 83

def virtual(&block)
  init_accu
  @rules << Rule.new([], :virtual, block).freeze
end

Instance Method Details

#call(data, scope: EMPTY_ARRAY) ⇒ Result

Runs the validation

The per-rule handling is inlined instead of dispatching to per-type methods because this runs per rule per validation, including the per-message validations in WaterDrop. Required and optional rules share the whole flow except the missing-key handling. DIG_MISS is compared via #equal? so we never dispatch #== to the validated (user-provided) values.

Parameters:

  • data (Hash)

    hash with data we want to validate

  • scope (Array<String>) (defaults to: EMPTY_ARRAY)

    scope of this contract (if any) or empty array if no parent scope is needed if contract starts from root

Returns:

  • (Result)

    validaton result



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/karafka/core/contractable/contract.rb', line 109

def call(data, scope: EMPTY_ARRAY)
  errors = []

  # A non-Hash root has no keys to dig; resolve it once here so #dig stays on its lean
  # fast path and is only invoked with a Hash. Non-Hash data makes every required rule
  # report missing, consistent with the non-Hash intermediate handling inside #dig.
  data_is_hash = data.is_a?(Hash)

  self.class.rules.each do |rule|
    if rule.type == :virtual
      result = rule.validator.call(data, errors, self)

      # A virtual rule signals "no errors" with any non-Array result (true, but also a
      # falsy `nil`/`false` returned by e.g. `condition && [[...], :err]`). Only an Array
      # of error pairs is iterated; previously a `false` return reached `false.each` and
      # raised NoMethodError (a `nil` return was already tolerated by the safe navigation).
      next unless result.is_a?(Array)

      # Apply the scope prefix in place on the rule's returned pairs and collect them
      # directly. Per the `virtual` contract the rule hands back a freshly built Array
      # each call (see `DSL#virtual`), so mutating it here is safe and avoids allocating a
      # new pair per error.
      result.each do |sub_result|
        sub_result[0] = scope + sub_result[0]
      end

      errors.push(*result)
    else
      for_checking = data_is_hash ? dig(data, rule.path) : DIG_MISS

      if DIG_MISS.equal?(for_checking)
        errors << [scope + rule.path, :missing] if rule.type == :required
      else
        result = rule.validator.call(for_checking, data, errors, self)

        next if result == true

        errors << [scope + rule.path, result || :format]
      end
    end
  end

  return Result.success if errors.empty?

  Result.new(errors, self)
end

#validate!(data, error_class, scope: EMPTY_ARRAY) ⇒ Boolean

Returns true.

Parameters:

  • data (Hash)

    data for validation

  • error_class (Class)

    error class that should be used when validation fails

  • scope (Array<String>) (defaults to: EMPTY_ARRAY)

    scope of this contract (if any) or empty array if no parent scope is needed if contract starts from root

Returns:

  • (Boolean)

    true

Raises:

  • (StandardError)

    any error provided in the error_class that inherits from the standard error



163
164
165
166
167
168
169
# File 'lib/karafka/core/contractable/contract.rb', line 163

def validate!(data, error_class, scope: EMPTY_ARRAY)
  result = call(data, scope: scope)

  return true if result.success?

  raise error_class, result.errors
end