lutaml-model 0.8.2 → 0.8.4
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/.rubocop_todo.yml +23 -23
- data/README.adoc +213 -1
- data/docs/_guides/document-validation.adoc +303 -0
- data/docs/_guides/index.adoc +1 -0
- data/docs/_guides/xml-mapping.adoc +9 -1
- data/docs/_guides/xml_mappings/07_best_practices.adoc +36 -0
- data/docs/_guides/xml_mappings/08_troubleshooting.adoc +89 -0
- data/docs/_tutorials/lutaml-xml-architecture.adoc +6 -1
- data/lib/lutaml/model/attribute.rb +19 -1
- data/lib/lutaml/model/error/liquid_drop_already_registered_error.rb +11 -0
- data/lib/lutaml/model/error/ordered_content_mapping_error.rb +17 -0
- data/lib/lutaml/model/global_context.rb +1 -0
- data/lib/lutaml/model/liquefiable.rb +12 -15
- data/lib/lutaml/model/mapping/mapping_rule.rb +10 -2
- data/lib/lutaml/model/mapping_hash.rb +1 -1
- data/lib/lutaml/model/services/transformer.rb +67 -32
- data/lib/lutaml/model/transform.rb +41 -4
- data/lib/lutaml/model/uninitialized_class.rb +11 -5
- data/lib/lutaml/model/validation/concerns/has_issues.rb +27 -0
- data/lib/lutaml/model/validation/context.rb +36 -0
- data/lib/lutaml/model/validation/issue.rb +62 -0
- data/lib/lutaml/model/validation/layer_result.rb +34 -0
- data/lib/lutaml/model/validation/profile.rb +66 -0
- data/lib/lutaml/model/validation/registry.rb +60 -0
- data/lib/lutaml/model/validation/remediation.rb +33 -0
- data/lib/lutaml/model/validation/remediation_result.rb +20 -0
- data/lib/lutaml/model/validation/report.rb +39 -0
- data/lib/lutaml/model/validation/rule.rb +59 -0
- data/lib/lutaml/model/validation.rb +2 -1
- data/lib/lutaml/model/validation_framework.rb +77 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +4 -0
- data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +9 -2
- data/lib/lutaml/xml/adapter/oga_adapter.rb +11 -3
- data/lib/lutaml/xml/adapter/ox_adapter.rb +5 -2
- data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -3
- data/lib/lutaml/xml/adapter_element.rb +26 -2
- data/lib/lutaml/xml/data_model.rb +14 -0
- data/lib/lutaml/xml/document.rb +3 -0
- data/lib/lutaml/xml/element.rb +8 -2
- data/lib/lutaml/xml/mapping.rb +9 -0
- data/lib/lutaml/xml/model_transform.rb +42 -0
- data/lib/lutaml/xml/schema/xsd/base.rb +4 -1
- data/lib/lutaml/xml/schema/xsd/schema_path.rb +6 -0
- data/lib/lutaml/xml/serialization/instance_methods.rb +3 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +46 -2
- data/lib/lutaml/xml/transformation.rb +40 -1
- data/lib/lutaml/xml/xml_element.rb +8 -7
- data/lutaml-model.gemspec +1 -2
- data/spec/lutaml/model/attribute_default_cache_spec.rb +58 -0
- data/spec/lutaml/model/liquefiable_spec.rb +22 -6
- data/spec/lutaml/model/liquid_compatibility_spec.rb +442 -0
- data/spec/lutaml/model/ordered_content_spec.rb +5 -5
- data/spec/lutaml/model/services/transformer_spec.rb +43 -0
- data/spec/lutaml/model/transform_cache_spec.rb +62 -0
- data/spec/lutaml/model/transform_dynamic_attributes_spec.rb +41 -0
- data/spec/lutaml/model/uninitialized_class_deep_dup_spec.rb +39 -0
- data/spec/lutaml/model/uninitialized_class_spec.rb +14 -2
- data/spec/lutaml/model/validation/concerns/has_issues_spec.rb +76 -0
- data/spec/lutaml/model/validation/context_spec.rb +60 -0
- data/spec/lutaml/model/validation/issue_spec.rb +77 -0
- data/spec/lutaml/model/validation/layer_result_spec.rb +66 -0
- data/spec/lutaml/model/validation/profile_spec.rb +134 -0
- data/spec/lutaml/model/validation/registry_spec.rb +94 -0
- data/spec/lutaml/model/validation/remediation_result_spec.rb +23 -0
- data/spec/lutaml/model/validation/remediation_spec.rb +72 -0
- data/spec/lutaml/model/validation/report_spec.rb +58 -0
- data/spec/lutaml/model/validation/rule_spec.rb +134 -0
- data/spec/lutaml/model/validation/uninitialized_class_validate_spec.rb +29 -0
- data/spec/lutaml/model/validation/validation_error_spec.rb +29 -0
- data/spec/lutaml/model/validation/validation_framework_spec.rb +110 -0
- data/spec/lutaml/xml/content_model_validation_spec.rb +157 -0
- data/spec/lutaml/xml/mapping_spec.rb +12 -7
- data/spec/lutaml/xml/schema/xsd/glob_spec.rb +12 -0
- metadata +46 -21
- data/spec/fixtures/liquid_templates/_ceramics.liquid +0 -3
|
@@ -5,6 +5,37 @@ module Lutaml
|
|
|
5
5
|
def call(value, rule, attribute, format: nil)
|
|
6
6
|
new(rule, attribute, format).call(value)
|
|
7
7
|
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def get_transform_static(obj, direction)
|
|
12
|
+
transform = obj&.transform
|
|
13
|
+
return nil if transform.nil? || transform.is_a?(Class)
|
|
14
|
+
|
|
15
|
+
transform.is_a?(::Hash) ? transform[direction] : transform
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def apply_static(value, sources, direction, format)
|
|
19
|
+
methods = sources.filter_map do |obj|
|
|
20
|
+
get_transform_static(obj, direction)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class_transformers = sources.filter_map do |obj|
|
|
24
|
+
next unless obj&.transform.is_a?(Class) &&
|
|
25
|
+
obj.transform < Lutaml::Model::ValueTransformer
|
|
26
|
+
|
|
27
|
+
obj.transform
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return value if methods.empty? && class_transformers.empty?
|
|
31
|
+
|
|
32
|
+
apply_direction = direction == :import ? :from : :to
|
|
33
|
+
result = class_transformers.reduce(value) do |v, tc|
|
|
34
|
+
tc.public_send(apply_direction, v, format)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
methods.reduce(result) { |tv, m| m.call(tv) }
|
|
38
|
+
end
|
|
8
39
|
end
|
|
9
40
|
|
|
10
41
|
attr_reader :rule, :attribute, :format
|
|
@@ -16,46 +47,38 @@ module Lutaml
|
|
|
16
47
|
end
|
|
17
48
|
|
|
18
49
|
def call(value)
|
|
19
|
-
# Collect all class-based and hash/proc-based transformers
|
|
20
|
-
# Apply them in the correct order based on transformation_methods
|
|
21
|
-
|
|
22
|
-
# Get ordered transformation methods (already in correct precedence)
|
|
23
50
|
methods = transformation_methods
|
|
24
51
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class_transformers << rule.transform if rule&.transform.is_a?(Class) && rule.transform < Lutaml::Model::ValueTransformer
|
|
31
|
-
else
|
|
32
|
-
# Import order: rule first, then attribute
|
|
33
|
-
class_transformers << rule.transform if rule&.transform.is_a?(Class) && rule.transform < Lutaml::Model::ValueTransformer
|
|
34
|
-
class_transformers << attribute.transform if attribute&.transform.is_a?(Class) && attribute.transform < Lutaml::Model::ValueTransformer
|
|
52
|
+
class_transformers = ordered_sources.filter_map do |obj|
|
|
53
|
+
next unless obj&.transform.is_a?(Class) &&
|
|
54
|
+
obj.transform < Lutaml::Model::ValueTransformer
|
|
55
|
+
|
|
56
|
+
obj.transform
|
|
35
57
|
end
|
|
36
58
|
|
|
37
|
-
# Apply class transformers first, then hash/proc transformers
|
|
38
59
|
result = class_transformers.reduce(value) do |v, transformer_class|
|
|
39
60
|
apply_class_transformer(v, transformer_class, format)
|
|
40
61
|
end
|
|
41
62
|
|
|
42
|
-
# Then apply hash/proc transformers
|
|
43
63
|
methods.reduce(result) do |transformed_value, method|
|
|
44
64
|
method.call(transformed_value)
|
|
45
65
|
end
|
|
46
66
|
end
|
|
47
67
|
|
|
48
68
|
def apply_class_transformer(value, transformer_class, format)
|
|
49
|
-
if
|
|
69
|
+
if export_direction?
|
|
50
70
|
transformer_class.to(value, format)
|
|
51
71
|
else
|
|
52
72
|
transformer_class.from(value, format)
|
|
53
73
|
end
|
|
54
74
|
end
|
|
55
75
|
|
|
76
|
+
def export_direction?
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
|
|
56
80
|
def get_transform(obj, direction)
|
|
57
81
|
transform = obj&.transform
|
|
58
|
-
|
|
59
82
|
return nil if transform.is_a?(Class)
|
|
60
83
|
|
|
61
84
|
transform.is_a?(::Hash) ? transform[direction] : transform
|
|
@@ -63,26 +86,38 @@ module Lutaml
|
|
|
63
86
|
end
|
|
64
87
|
|
|
65
88
|
class ImportTransformer < Transformer
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
class << self
|
|
90
|
+
def call(value, rule, attribute, format: nil)
|
|
91
|
+
apply_static(value, [rule, attribute], :import, format)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def ordered_sources
|
|
96
|
+
[rule, attribute]
|
|
97
|
+
end
|
|
98
|
+
|
|
69
99
|
def transformation_methods
|
|
70
|
-
|
|
71
|
-
get_transform(rule, :import),
|
|
72
|
-
get_transform(attribute, :import),
|
|
73
|
-
].compact
|
|
100
|
+
ordered_sources.filter_map { |obj| get_transform(obj, :import) }
|
|
74
101
|
end
|
|
75
102
|
end
|
|
76
103
|
|
|
77
104
|
class ExportTransformer < Transformer
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
105
|
+
class << self
|
|
106
|
+
def call(value, rule, attribute, format: nil)
|
|
107
|
+
apply_static(value, [attribute, rule], :export, format)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def ordered_sources
|
|
112
|
+
[attribute, rule]
|
|
113
|
+
end
|
|
114
|
+
|
|
81
115
|
def transformation_methods
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
ordered_sources.filter_map { |obj| get_transform(obj, :export) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def export_direction?
|
|
120
|
+
true
|
|
86
121
|
end
|
|
87
122
|
end
|
|
88
123
|
end
|
|
@@ -3,21 +3,58 @@
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Model
|
|
5
5
|
class Transform
|
|
6
|
+
@transform_cache = {}
|
|
7
|
+
|
|
8
|
+
# Maximum number of cached Transform instances before eviction.
|
|
9
|
+
MAX_CACHE_SIZE = 256
|
|
10
|
+
|
|
6
11
|
def self.data_to_model(context, data, format, options = {})
|
|
7
|
-
|
|
12
|
+
register = options[:register] || Lutaml::Model::Config.default_register
|
|
13
|
+
transform = cached_transform(context, register)
|
|
14
|
+
transform.data_to_model(data, format, options)
|
|
8
15
|
end
|
|
9
16
|
|
|
10
17
|
def self.model_to_data(context, model, format, options = {})
|
|
11
18
|
register = model.lutaml_register if model.respond_to?(:lutaml_register)
|
|
12
|
-
|
|
19
|
+
register ||= Lutaml::Model::Config.default_register
|
|
20
|
+
transform = cached_transform(context, register)
|
|
21
|
+
transform.model_to_data(model, format, options)
|
|
13
22
|
end
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
def self.cached_transform(context, register)
|
|
25
|
+
@transform_cache ||= {}
|
|
26
|
+
cache_key = [context.object_id, register]
|
|
27
|
+
entry = @transform_cache[cache_key]
|
|
28
|
+
return entry if entry
|
|
29
|
+
|
|
30
|
+
evict_if_needed if @transform_cache.size >= MAX_CACHE_SIZE
|
|
31
|
+
@transform_cache[cache_key] = new(context, register)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.clear_cache!
|
|
35
|
+
@transform_cache&.clear
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.cache_size
|
|
39
|
+
@transform_cache&.size || 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.evict_if_needed
|
|
43
|
+
# Evict oldest half of entries when cache is full
|
|
44
|
+
keys_to_remove = @transform_cache.keys.first(@transform_cache.size / 2)
|
|
45
|
+
keys_to_remove.each { |k| @transform_cache.delete(k) }
|
|
46
|
+
end
|
|
47
|
+
private_class_method :evict_if_needed
|
|
48
|
+
|
|
49
|
+
attr_reader :context, :lutaml_register
|
|
16
50
|
|
|
17
51
|
def initialize(context, register = nil)
|
|
18
52
|
@context = context
|
|
19
53
|
@lutaml_register = register || Lutaml::Model::Config.default_register
|
|
20
|
-
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def attributes
|
|
57
|
+
context.attributes(lutaml_register)
|
|
21
58
|
end
|
|
22
59
|
|
|
23
60
|
def model_class
|
|
@@ -49,16 +49,22 @@ module Lutaml
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def method_missing(method, *_args, &)
|
|
52
|
-
if method.end_with?("?")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
super
|
|
56
|
-
end
|
|
52
|
+
return false if method.end_with?("?")
|
|
53
|
+
|
|
54
|
+
nil
|
|
57
55
|
end
|
|
58
56
|
|
|
59
57
|
def respond_to_missing?(method_name, _include_private = false)
|
|
60
58
|
method_name.end_with?("?")
|
|
61
59
|
end
|
|
60
|
+
|
|
61
|
+
def dup
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def clone
|
|
66
|
+
self
|
|
67
|
+
end
|
|
62
68
|
end
|
|
63
69
|
end
|
|
64
70
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Validation
|
|
6
|
+
# Shared severity filtering for objects that expose an `issues`
|
|
7
|
+
# collection. Included by LayerResult and Report.
|
|
8
|
+
module HasIssues
|
|
9
|
+
def errors
|
|
10
|
+
issues.select(&:error?)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def warnings
|
|
14
|
+
issues.select(&:warning?)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def infos
|
|
18
|
+
issues.select(&:info?)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def notices
|
|
22
|
+
issues.select(&:notice?)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Validation
|
|
6
|
+
# Mutable validation context that accumulates errors and provides
|
|
7
|
+
# per-rule state. Used when validation needs imperative error
|
|
8
|
+
# accumulation (e.g., streaming SAX validation).
|
|
9
|
+
class Context
|
|
10
|
+
attr_reader :errors
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@errors = []
|
|
14
|
+
@per_rule_state = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_error(issue)
|
|
18
|
+
@errors << issue
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add_errors(issues)
|
|
22
|
+
@errors.concat(issues)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def rule_state(rule_code)
|
|
26
|
+
@per_rule_state[rule_code] ||= {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset!
|
|
30
|
+
@errors.clear
|
|
31
|
+
@per_rule_state.clear
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Validation
|
|
6
|
+
# Serializable validation issue with severity, code, location, and
|
|
7
|
+
# suggestion. Used by Rule#check to report problems and aggregated
|
|
8
|
+
# by LayerResult and Report.
|
|
9
|
+
class Issue < Lutaml::Model::Serializable
|
|
10
|
+
# Allowed severity levels for validation issues.
|
|
11
|
+
SEVERITIES = %w[error warning info notice].freeze
|
|
12
|
+
|
|
13
|
+
attribute :severity, :string
|
|
14
|
+
attribute :code, :string
|
|
15
|
+
attribute :message, :string
|
|
16
|
+
attribute :location, :string
|
|
17
|
+
attribute :line, :integer
|
|
18
|
+
attribute :suggestion, :string
|
|
19
|
+
|
|
20
|
+
json do
|
|
21
|
+
map "severity", to: :severity
|
|
22
|
+
map "code", to: :code
|
|
23
|
+
map "message", to: :message
|
|
24
|
+
map "location", to: :location
|
|
25
|
+
map "line", to: :line
|
|
26
|
+
map "suggestion", to: :suggestion
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(attributes = {})
|
|
30
|
+
super
|
|
31
|
+
validate_severity!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def error?
|
|
35
|
+
severity == "error"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def warning?
|
|
39
|
+
severity == "warning"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def info?
|
|
43
|
+
severity == "info"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def notice?
|
|
47
|
+
severity == "notice"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def validate_severity!
|
|
53
|
+
return if severity.nil? || SEVERITIES.include?(severity)
|
|
54
|
+
|
|
55
|
+
raise ArgumentError,
|
|
56
|
+
"Invalid severity: #{severity}. " \
|
|
57
|
+
"Must be one of: #{SEVERITIES.join(', ')}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "issue"
|
|
4
|
+
require_relative "concerns/has_issues"
|
|
5
|
+
|
|
6
|
+
module Lutaml
|
|
7
|
+
module Model
|
|
8
|
+
module Validation
|
|
9
|
+
class LayerResult < Lutaml::Model::Serializable
|
|
10
|
+
include HasIssues
|
|
11
|
+
|
|
12
|
+
attribute :name, :string
|
|
13
|
+
attribute :status, :string
|
|
14
|
+
attribute :duration_ms, :integer
|
|
15
|
+
attribute :issues, Issue, collection: true
|
|
16
|
+
|
|
17
|
+
json do
|
|
18
|
+
map "name", to: :name
|
|
19
|
+
map "status", to: :status
|
|
20
|
+
map "duration_ms", to: :duration_ms
|
|
21
|
+
map "issues", to: :issues
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pass?
|
|
25
|
+
status == "pass"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def fail?
|
|
29
|
+
status == "fail"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Model
|
|
7
|
+
module Validation
|
|
8
|
+
# Composable validation profile loaded from YAML. Selects which rules
|
|
9
|
+
# run during validation. Profiles can import other profiles for
|
|
10
|
+
# rule reuse across validation levels (e.g., basic → strict).
|
|
11
|
+
class Profile
|
|
12
|
+
attr_reader :name, :description, :rule_names, :imports
|
|
13
|
+
|
|
14
|
+
def initialize(name:, description: nil, rule_names: [], imports: [])
|
|
15
|
+
@name = name
|
|
16
|
+
@description = description
|
|
17
|
+
@rule_names = rule_names
|
|
18
|
+
@imports = imports
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.load(path)
|
|
22
|
+
data = YAML.safe_load_file(path, symbolize_names: false)
|
|
23
|
+
unless data.is_a?(::Hash) && data["name"].is_a?(String)
|
|
24
|
+
raise ArgumentError,
|
|
25
|
+
"Profile YAML must contain a 'name' string key: #{path}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
new(
|
|
29
|
+
name: data["name"],
|
|
30
|
+
description: data["description"],
|
|
31
|
+
rule_names: data["rules"] || [],
|
|
32
|
+
imports: data["import"] || [],
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resolve(registry, profiles_by_name = {}, visited: Set.new)
|
|
37
|
+
if visited.include?(name)
|
|
38
|
+
raise ArgumentError,
|
|
39
|
+
"Circular profile import detected: #{name}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
resolved = resolve_imports(registry, profiles_by_name,
|
|
43
|
+
visited: visited + [name])
|
|
44
|
+
resolved | resolve_names(registry)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def resolve_names(registry)
|
|
50
|
+
rule_names.filter_map do |name|
|
|
51
|
+
registry.all.find { |r| r.class.name == name }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def resolve_imports(registry, profiles_by_name, visited: Set.new)
|
|
56
|
+
imports.flat_map do |import_name|
|
|
57
|
+
imported = profiles_by_name[import_name]
|
|
58
|
+
next [] unless imported
|
|
59
|
+
|
|
60
|
+
imported.resolve(registry, profiles_by_name, visited: visited)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Validation
|
|
6
|
+
# Instance-based rule registry. Register rule classes, look up by
|
|
7
|
+
# code or category, and instantiate all registered rules for
|
|
8
|
+
# validation runs.
|
|
9
|
+
class Registry
|
|
10
|
+
def initialize
|
|
11
|
+
@rules = []
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@all = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register(rule_class)
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
return if @rules.include?(rule_class)
|
|
19
|
+
|
|
20
|
+
@rules << rule_class
|
|
21
|
+
@all = nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def auto_discover(dir, pattern: "**/*_rule.rb")
|
|
26
|
+
Dir.glob(File.join(dir, pattern)).each { |path| require path }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def all
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
@all ||= @rules.map(&:new)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def for_category(category)
|
|
36
|
+
all.select { |r| r.category == category }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def find(code)
|
|
40
|
+
all.find { |r| r.code == code }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset!
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
@rules.clear
|
|
46
|
+
@all = nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def rule_classes
|
|
51
|
+
@rules.dup
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def size
|
|
55
|
+
@rules.size
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Validation
|
|
6
|
+
# Abstract base class for validation remediation. Subclass and
|
|
7
|
+
# override {#id}, {#targets}, {#applicable?}, {#fix}, and
|
|
8
|
+
# {#preview} to implement auto-fix logic for specific issue codes.
|
|
9
|
+
class Remediation
|
|
10
|
+
def id
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def targets
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def applicable?(_context, _report)
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fix(_context, _report)
|
|
23
|
+
raise NotImplementedError,
|
|
24
|
+
"#{self.class}#fix must be implemented by subclass"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def preview(_context, _report)
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Validation
|
|
6
|
+
# Result of a remediation fix attempt. Serializable to JSON.
|
|
7
|
+
class RemediationResult < Lutaml::Model::Serializable
|
|
8
|
+
attribute :success, :boolean
|
|
9
|
+
attribute :message, :string
|
|
10
|
+
attribute :fixed_codes, :string, collection: true
|
|
11
|
+
|
|
12
|
+
json do
|
|
13
|
+
map "success", to: :success
|
|
14
|
+
map "message", to: :message
|
|
15
|
+
map "fixed_codes", to: :fixed_codes
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "concerns/has_issues"
|
|
5
|
+
require_relative "issue"
|
|
6
|
+
require_relative "layer_result"
|
|
7
|
+
|
|
8
|
+
module Lutaml
|
|
9
|
+
module Model
|
|
10
|
+
module Validation
|
|
11
|
+
class Report < Lutaml::Model::Serializable
|
|
12
|
+
include HasIssues
|
|
13
|
+
|
|
14
|
+
attribute :source, :string
|
|
15
|
+
attribute :timestamp, :string
|
|
16
|
+
attribute :valid, :boolean
|
|
17
|
+
attribute :duration_ms, :integer
|
|
18
|
+
attribute :layers, LayerResult, collection: true
|
|
19
|
+
|
|
20
|
+
json do
|
|
21
|
+
map "source", to: :source
|
|
22
|
+
map "timestamp", to: :timestamp
|
|
23
|
+
map "valid", to: :valid
|
|
24
|
+
map "duration_ms", to: :duration_ms
|
|
25
|
+
map "layers", to: :layers
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(attributes = {})
|
|
29
|
+
super
|
|
30
|
+
self.timestamp ||= Time.now.utc.iso8601
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def issues
|
|
34
|
+
layers ? layers.flat_map(&:issues) : []
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Validation
|
|
6
|
+
# Abstract base class for validation rules. Subclass and override
|
|
7
|
+
# {#code}, {#category}, {#severity}, {#applicable?}, and {#check}
|
|
8
|
+
# to implement domain-specific validation logic.
|
|
9
|
+
#
|
|
10
|
+
# Use the private {#issue} helper inside {#check} to create issues
|
|
11
|
+
# that inherit the rule's severity and code by default.
|
|
12
|
+
class Rule
|
|
13
|
+
def code
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def category
|
|
18
|
+
:general
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def severity
|
|
22
|
+
"error"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def applicable?(_context)
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def check(_context)
|
|
30
|
+
[]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def needs_deferred?
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def collect(_element, _context); end
|
|
38
|
+
|
|
39
|
+
def complete(_context)
|
|
40
|
+
[]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def issue(message, location: nil, line: nil, suggestion: nil,
|
|
46
|
+
severity: nil, code: nil)
|
|
47
|
+
Issue.new(
|
|
48
|
+
severity: severity || self.severity,
|
|
49
|
+
code: code || self.code,
|
|
50
|
+
message: message,
|
|
51
|
+
location: location,
|
|
52
|
+
line: line,
|
|
53
|
+
suggestion: suggestion,
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -11,7 +11,8 @@ module Lutaml
|
|
|
11
11
|
|
|
12
12
|
begin
|
|
13
13
|
if value.respond_to?(:validate)
|
|
14
|
-
|
|
14
|
+
sub_errors = value.validate
|
|
15
|
+
errors.concat(sub_errors) if sub_errors.is_a?(Array)
|
|
15
16
|
else
|
|
16
17
|
resolver = Lutaml::Model::Services::DefaultValueResolver.new(
|
|
17
18
|
attr, register, self
|