Module: Lutaml::Model::Validation

Included in:
Serialize
Defined in:
lib/lutaml/model/validation_framework.rb,
lib/lutaml/model/validation.rb,
lib/lutaml/model/validation/rule.rb,
lib/lutaml/model/validation/issue.rb,
lib/lutaml/model/validation/report.rb,
lib/lutaml/model/validation/context.rb,
lib/lutaml/model/validation/profile.rb,
lib/lutaml/model/validation/registry.rb,
lib/lutaml/model/validation/remediation.rb,
lib/lutaml/model/validation/layer_result.rb,
lib/lutaml/model/validation/remediation_result.rb,
lib/lutaml/model/validation/concerns/has_issues.rb

Overview

Document-level validation framework. Orthogonal to the existing attribute-level Validation module — this validates structural integrity, cross-references, and conformance against domain rules.

Examples:

Run all registered rules

issues = Lutaml::Model::Validation.validate(context, registry)

Run and raise on errors

Lutaml::Model::Validation.validate!(context, registry)

Defined Under Namespace

Modules: HasIssues Classes: Context, Issue, LayerResult, Profile, Registry, Remediation, RemediationResult, Report, Rule, ValidationError

Constant Summary collapse

VALIDATING_KEY =

Models being validated on this thread's current stack.

:lutaml_model_validating

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.new_registryObject



25
26
27
# File 'lib/lutaml/model/validation_framework.rb', line 25

def new_registry
  Registry.new
end

.validate(context, registry, profile: nil) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/lutaml/model/validation_framework.rb', line 29

def validate(context, registry, profile: nil)
  rules = if profile
            profile.resolve(registry)
          else
            registry.all
          end

  all_issues = []
  rules.each do |rule|
    next unless rule.applicable?(context)

    issues = rule.check(context)
    if context.is_a?(Validation::Context)
      issues.each { |i| context.add_error(i) }
    end
    all_issues.concat(issues)
  end

  all_issues
end

.validate!(context, registry, profile: nil) ⇒ Object



50
51
52
53
54
55
56
57
58
# File 'lib/lutaml/model/validation_framework.rb', line 50

def validate!(context, registry, profile: nil)
  issues = validate(context, registry, profile: profile)
  return if issues.empty?

  errors = issues.select(&:error?)
  unless errors.empty?
    raise ValidationError.new(format_errors(errors), issues: errors)
  end
end

.visiting?(model) ⇒ Boolean

Whether a model is already being validated further up this stack.



10
11
12
13
# File 'lib/lutaml/model/validation.rb', line 10

def self.visiting?(model)
  stack = Thread.current[VALIDATING_KEY]
  !stack.nil? && stack.key?(model)
end

Instance Method Details

#collect_validation_errors(register) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/lutaml/model/validation.rb', line 37

def collect_validation_errors(register)
  errors = []

  self.class.attributes(register).each do |name, attr|
    value = public_send(:"#{name}")

    begin
      # Recurse into nested models — a single child or every element of
      # an array — so their own validation errors surface here. A
      # Collection is itself a model, so it is validated as one: that
      # runs its collection-level rules as well as its elements.
      # #validate takes no arguments here: it is a public override point
      # that downstream models legitimately define with zero arity.
      (value.is_a?(::Array) ? value : [value]).each do |item|
        next unless item.is_a?(Lutaml::Model::Serialize)
        # Skip a model already on the stack rather than calling into it.
        # A downstream #validate override sits above the guard, so
        # re-entering it would run the override's own body a second time.
        next if Validation.visiting?(item)

        sub_errors = item.validate
        errors.concat(sub_errors) if sub_errors.is_a?(Array)
      end

      # Always run attribute-level validation (cardinality, required,
      # enum, pattern, polymorphic, custom) regardless of value type.
      attr.validate_value!(value, register, instance_object: self)
    rescue Lutaml::Model::CollectionCountOutOfRangeError => e
      errors << e unless attr.choice
    rescue Lutaml::Model::InvalidValueError,
           Lutaml::Model::CollectionTrueMissingError,
           Lutaml::Model::PolymorphicError,
           Lutaml::Model::ValidationFailedError,
           Lutaml::Model::RequiredAttributeMissingError,
           Lutaml::Model::PatternNotMatchedError => e
      errors << e
    end
  end

  validate_helper(errors, register)
end

#element_orderObject

Default: no element order. XML overrides via InstanceMethods prepend with attr_accessor :element_order.



121
122
123
# File 'lib/lutaml/model/validation.rb', line 121

def element_order
  nil
end

#format_element_sequences(_register) ⇒ Array?

Hook for getting format-specific element sequences for validation. XML overrides via InstanceMethods prepend.



115
116
117
# File 'lib/lutaml/model/validation.rb', line 115

def format_element_sequences(_register)
  nil
end

#order_namesObject



125
126
127
128
129
130
131
132
133
# File 'lib/lutaml/model/validation.rb', line 125

def order_names
  return [] unless element_order

  element_order.each_with_object([]) do |element, arr|
    next if element.text?

    arr << element.name
  end
end

#validate(register: Lutaml::Model::Config.default_register) ⇒ Object

The single guarded entry point. A model can appear inside its own subtree, and re-entering it would recurse forever, so a re-entry returns early — the outer call is already collecting its errors.

Override #collect_validation_errors, not this method, to add validation that must run inside the cycle guard. Overriding #validate directly puts the override outside the guard.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/lutaml/model/validation.rb', line 22

def validate(register: Lutaml::Model::Config.default_register)
  visiting = (Thread.current[VALIDATING_KEY] ||= {}.compare_by_identity)
  return [] if visiting.key?(self)

  begin
    # Marked inside the begin, so an exception delivered between the
    # check and the mark cannot strand an entry on the stack.
    visiting[self] = true
    collect_validation_errors(register)
  ensure
    visiting.delete(self)
    Thread.current[VALIDATING_KEY] = nil if visiting.empty?
  end
end

#validate!(register: Lutaml::Model::Config.default_register) ⇒ Object



79
80
81
82
# File 'lib/lutaml/model/validation.rb', line 79

def validate!(register: Lutaml::Model::Config.default_register)
  errors = validate(register: register)
  raise Lutaml::Model::ValidationError.new(errors) if errors.any?
end

#validate_helper(errors, register) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
# File 'lib/lutaml/model/validation.rb', line 84

def validate_helper(errors, register)
  self.class.choice_attributes.each do |attribute|
    attribute.validate_content!(self, register)
  end

  validate_sequence!(errors, order_names, register)
  errors
rescue Lutaml::Model::ChoiceUpperBoundError,
       Lutaml::Model::ChoiceLowerBoundError => e
  errors << e
end

#validate_sequence!(errors, names, register) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/lutaml/model/validation.rb', line 96

def validate_sequence!(errors, names, register)
  sequences = format_element_sequences(register)
  return errors if names.empty? || sequences.nil?

  sequences.each do |sequence|
    sequence.validate_content!(names, self, register)
  end
  errors
rescue Lutaml::Model::IncorrectSequenceError,
       Lutaml::Model::ChoiceUpperBoundError,
       Lutaml::Model::ChoiceLowerBoundError => e
  errors << e
end