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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/validation_framework"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Validation::RemediationResult do
|
|
7
|
+
it "serializes to JSON" do
|
|
8
|
+
result = described_class.new(
|
|
9
|
+
success: true,
|
|
10
|
+
message: "Fixed",
|
|
11
|
+
fixed_codes: ["DOC-020", "DOC-030"],
|
|
12
|
+
)
|
|
13
|
+
parsed = JSON.parse(result.to_json)
|
|
14
|
+
expect(parsed["success"]).to be true
|
|
15
|
+
expect(parsed["message"]).to eq("Fixed")
|
|
16
|
+
expect(parsed["fixed_codes"]).to eq(["DOC-020", "DOC-030"])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "defaults fixed_codes to nil when not provided" do
|
|
20
|
+
result = described_class.new(success: false, message: "nope")
|
|
21
|
+
expect(result.fixed_codes).to be_nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/validation_framework"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Validation::Remediation do
|
|
7
|
+
subject(:remediation) { described_class.new }
|
|
8
|
+
|
|
9
|
+
it "returns nil id by default" do
|
|
10
|
+
expect(remediation.id).to be_nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "returns nil targets by default" do
|
|
14
|
+
expect(remediation.targets).to be_nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "is always applicable" do
|
|
18
|
+
expect(remediation.applicable?(nil, nil)).to be true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "raises NotImplementedError from base fix" do
|
|
22
|
+
expect { remediation.fix(nil, nil) }
|
|
23
|
+
.to raise_error(NotImplementedError, /must be implemented/)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "returns nil preview by default" do
|
|
27
|
+
expect(remediation.preview(nil, nil)).to be_nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe "custom subclass" do
|
|
31
|
+
let(:custom_remediation) do
|
|
32
|
+
Class.new(described_class) do
|
|
33
|
+
def id = "REM-001"
|
|
34
|
+
def targets = ["DOC-020"]
|
|
35
|
+
|
|
36
|
+
def applicable?(_context, report)
|
|
37
|
+
report.any? { |i| i.code == "DOC-020" }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def fix(_context, _report)
|
|
41
|
+
Lutaml::Model::Validation::RemediationResult.new(
|
|
42
|
+
success: true,
|
|
43
|
+
message: "Fixed DOC-020",
|
|
44
|
+
fixed_codes: ["DOC-020"],
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "overrides id and targets" do
|
|
51
|
+
rem = custom_remediation.new
|
|
52
|
+
expect(rem.id).to eq("REM-001")
|
|
53
|
+
expect(rem.targets).to eq(["DOC-020"])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "checks applicability" do
|
|
57
|
+
rem = custom_remediation.new
|
|
58
|
+
issue = Lutaml::Model::Validation::Issue.new(
|
|
59
|
+
severity: "error", code: "DOC-020", message: "bad",
|
|
60
|
+
)
|
|
61
|
+
expect(rem.applicable?(nil, [issue])).to be true
|
|
62
|
+
expect(rem.applicable?(nil, [])).to be false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "returns successful fix" do
|
|
66
|
+
rem = custom_remediation.new
|
|
67
|
+
result = rem.fix(nil, nil)
|
|
68
|
+
expect(result.success).to be true
|
|
69
|
+
expect(result.fixed_codes).to eq(["DOC-020"])
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/validation_framework"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Validation::Report do
|
|
7
|
+
subject(:report) do
|
|
8
|
+
described_class.new(
|
|
9
|
+
source: "test.xml",
|
|
10
|
+
valid: false,
|
|
11
|
+
duration_ms: 50,
|
|
12
|
+
layers: [layer],
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let(:issue) do
|
|
17
|
+
Lutaml::Model::Validation::Issue.new(
|
|
18
|
+
severity: "error", code: "TEST-003", message: "Broken",
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:layer) do
|
|
23
|
+
Lutaml::Model::Validation::LayerResult.new(
|
|
24
|
+
name: "Check", status: "fail", issues: [issue],
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "serializes to JSON" do
|
|
29
|
+
parsed = JSON.parse(report.to_json)
|
|
30
|
+
expect(parsed["source"]).to eq("test.xml")
|
|
31
|
+
expect(parsed["valid"]).to be(false)
|
|
32
|
+
expect(parsed["timestamp"]).not_to be_nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "auto-sets timestamp on initialization" do
|
|
36
|
+
expect(report.timestamp).to match(/\d{4}-\d{2}-\d{2}T/)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe "issue aggregation via HasIssues" do
|
|
40
|
+
it "aggregates issues from all layers" do
|
|
41
|
+
expect(report.issues.length).to eq(1)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "filters errors" do
|
|
45
|
+
expect(report.errors.length).to eq(1)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "returns empty warnings when none present" do
|
|
49
|
+
expect(report.warnings).to be_empty
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "handles empty layers" do
|
|
54
|
+
empty_report = described_class.new(source: "empty.xml", valid: true)
|
|
55
|
+
expect(empty_report.issues).to be_empty
|
|
56
|
+
expect(empty_report.errors).to be_empty
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/validation_framework"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Validation::Rule do
|
|
7
|
+
subject(:rule) { described_class.new }
|
|
8
|
+
|
|
9
|
+
describe "defaults" do
|
|
10
|
+
it "returns nil code" do
|
|
11
|
+
expect(rule.code).to be_nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "returns :general category" do
|
|
15
|
+
expect(rule.category).to eq(:general)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "returns error severity" do
|
|
19
|
+
expect(rule.severity).to eq("error")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "is always applicable" do
|
|
23
|
+
expect(rule.applicable?(nil)).to be true
|
|
24
|
+
expect(rule.applicable?({})).to be true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "returns empty issues from check" do
|
|
28
|
+
expect(rule.check(nil)).to eq([])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "is not deferred" do
|
|
32
|
+
expect(rule.needs_deferred?).to be false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "returns nil from collect" do
|
|
36
|
+
expect(rule.collect(:element, nil)).to be_nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "returns empty array from complete" do
|
|
40
|
+
expect(rule.complete(nil)).to eq([])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe "custom subclass" do
|
|
45
|
+
let(:custom_rule_class) do
|
|
46
|
+
Class.new(described_class) do
|
|
47
|
+
def code = "CUSTOM-001"
|
|
48
|
+
def category = :custom
|
|
49
|
+
def severity = "warning"
|
|
50
|
+
|
|
51
|
+
def applicable?(context)
|
|
52
|
+
context&.dig(:enabled) != false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def check(_context)
|
|
56
|
+
[issue("Found a problem")]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "overrides defaults" do
|
|
62
|
+
rule = custom_rule_class.new
|
|
63
|
+
expect(rule.code).to eq("CUSTOM-001")
|
|
64
|
+
expect(rule.category).to eq(:custom)
|
|
65
|
+
expect(rule.severity).to eq("warning")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "produces issues via helper" do
|
|
69
|
+
rule = custom_rule_class.new
|
|
70
|
+
issues = rule.check(nil)
|
|
71
|
+
expect(issues.length).to eq(1)
|
|
72
|
+
expect(issues.first.code).to eq("CUSTOM-001")
|
|
73
|
+
expect(issues.first.severity).to eq("warning")
|
|
74
|
+
expect(issues.first.message).to eq("Found a problem")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "respects applicable?" do
|
|
78
|
+
rule = custom_rule_class.new
|
|
79
|
+
expect(rule.applicable?({ enabled: true })).to be true
|
|
80
|
+
expect(rule.applicable?({ enabled: false })).to be false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "allows issue helper to override severity and code" do
|
|
84
|
+
klass = Class.new(described_class) do
|
|
85
|
+
def code = "OVERRIDE"
|
|
86
|
+
def severity = "error"
|
|
87
|
+
|
|
88
|
+
def check(_context)
|
|
89
|
+
[issue("msg", severity: "info", code: "OTHER")]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
issues = klass.new.check(nil)
|
|
93
|
+
expect(issues.first.severity).to eq("info")
|
|
94
|
+
expect(issues.first.code).to eq("OTHER")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe "streaming subclass" do
|
|
99
|
+
let(:streaming_rule_class) do
|
|
100
|
+
Class.new(described_class) do
|
|
101
|
+
def code = "STREAM-001"
|
|
102
|
+
def needs_deferred? = true
|
|
103
|
+
|
|
104
|
+
def collect(element, context)
|
|
105
|
+
state = context.rule_state(code)
|
|
106
|
+
state[:items] ||= []
|
|
107
|
+
state[:items] << element
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def complete(context)
|
|
111
|
+
state = context.rule_state(code)
|
|
112
|
+
return [] unless state[:items]&.any?
|
|
113
|
+
|
|
114
|
+
[issue("Collected #{state[:items].length} items")]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "signals deferred collection" do
|
|
120
|
+
rule = streaming_rule_class.new
|
|
121
|
+
expect(rule.needs_deferred?).to be true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "collects elements and reports in complete" do
|
|
125
|
+
ctx = Lutaml::Model::Validation::Context.new
|
|
126
|
+
rule = streaming_rule_class.new
|
|
127
|
+
rule.collect("item_a", ctx)
|
|
128
|
+
rule.collect("item_b", ctx)
|
|
129
|
+
issues = rule.complete(ctx)
|
|
130
|
+
expect(issues.length).to eq(1)
|
|
131
|
+
expect(issues.first.message).to eq("Collected 2 items")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Lutaml::Model::Validation with UninitializedClass" do
|
|
6
|
+
before do
|
|
7
|
+
stub_const("ValidateWithUninitializedModel", Class.new(Lutaml::Model::Serializable) do
|
|
8
|
+
attribute :name, :string
|
|
9
|
+
attribute :role, :string, values: %w[admin guest], default: -> { "guest" }
|
|
10
|
+
end)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "does not crash when validating with uninitialized attributes" do
|
|
14
|
+
model = ValidateWithUninitializedModel.new
|
|
15
|
+
# name is uninitialized, but validate should not crash
|
|
16
|
+
expect { model.validate }.not_to raise_error
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "still catches value constraint violations" do
|
|
20
|
+
model = ValidateWithUninitializedModel.new(role: "hacker")
|
|
21
|
+
errors = model.validate
|
|
22
|
+
expect(errors).not_to be_empty
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "returns empty errors for valid instance" do
|
|
26
|
+
model = ValidateWithUninitializedModel.new(name: "ok", role: "admin")
|
|
27
|
+
expect(model.validate).to be_empty
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/validation_framework"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Validation::ValidationError do
|
|
7
|
+
it "is a StandardError" do
|
|
8
|
+
expect(described_class).to be < StandardError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "stores message" do
|
|
12
|
+
error = described_class.new("Something broke")
|
|
13
|
+
expect(error.message).to eq("Something broke")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "stores issues" do
|
|
17
|
+
issue = Lutaml::Model::Validation::Issue.new(
|
|
18
|
+
severity: "error", code: "E-001", message: "bad",
|
|
19
|
+
)
|
|
20
|
+
error = described_class.new("Failed", issues: [issue])
|
|
21
|
+
expect(error.issues.length).to eq(1)
|
|
22
|
+
expect(error.issues.first.code).to eq("E-001")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "defaults issues to empty array" do
|
|
26
|
+
error = described_class.new("Failed")
|
|
27
|
+
expect(error.issues).to eq([])
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/validation_framework"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Validation, ".validate / .validate!" do
|
|
7
|
+
let(:registry) { described_class.new_registry }
|
|
8
|
+
|
|
9
|
+
let(:rule_a_class) do
|
|
10
|
+
Class.new(Lutaml::Model::Validation::Rule) do
|
|
11
|
+
def code = "E2E-001"
|
|
12
|
+
def severity = "error"
|
|
13
|
+
|
|
14
|
+
def check(context)
|
|
15
|
+
context[:items].empty? ? [issue("No items found")] : []
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
let(:rule_b_class) do
|
|
21
|
+
Class.new(Lutaml::Model::Validation::Rule) do
|
|
22
|
+
def code = "E2E-002"
|
|
23
|
+
def severity = "warning"
|
|
24
|
+
|
|
25
|
+
def check(context)
|
|
26
|
+
context[:items].length > 100 ? [issue("Too many items")] : []
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
before do
|
|
32
|
+
registry.register(rule_a_class)
|
|
33
|
+
registry.register(rule_b_class)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe ".validate" do
|
|
37
|
+
it "finds no issues with valid data" do
|
|
38
|
+
issues = described_class.validate({ items: (1..50).to_a }, registry)
|
|
39
|
+
expect(issues).to be_empty
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "finds errors for empty items" do
|
|
43
|
+
issues = described_class.validate({ items: [] }, registry)
|
|
44
|
+
expect(issues.length).to eq(1)
|
|
45
|
+
expect(issues.first.code).to eq("E2E-001")
|
|
46
|
+
expect(issues.first).to be_error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "finds warnings for too many items" do
|
|
50
|
+
issues = described_class.validate({ items: (1..101).to_a }, registry)
|
|
51
|
+
expect(issues.length).to eq(1)
|
|
52
|
+
expect(issues.first.code).to eq("E2E-002")
|
|
53
|
+
expect(issues.first).to be_warning
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "skips inapplicable rules" do
|
|
57
|
+
skip_rule = Class.new(Lutaml::Model::Validation::Rule) do
|
|
58
|
+
def code = "SKIP"
|
|
59
|
+
def applicable?(_ctx) = false
|
|
60
|
+
def check(_ctx) = [issue("should not appear")]
|
|
61
|
+
end
|
|
62
|
+
registry.register(skip_rule)
|
|
63
|
+
issues = described_class.validate({ items: [1] }, registry)
|
|
64
|
+
expect(issues).to be_empty
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "does not crash with contexts that support add_error" do
|
|
68
|
+
Lutaml::Model::Validation::Context.new
|
|
69
|
+
# Use plain hash for data — rules expect [:items]
|
|
70
|
+
issues = described_class.validate({ items: [] }, registry)
|
|
71
|
+
expect(issues.length).to eq(1)
|
|
72
|
+
# Plain hash doesn't accumulate, but validate still works
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe ".validate!" do
|
|
77
|
+
it "raises on errors" do
|
|
78
|
+
expect do
|
|
79
|
+
described_class.validate!({ items: [] }, registry)
|
|
80
|
+
end.to raise_error(Lutaml::Model::Validation::ValidationError, /E2E-001/)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "exposes issues on ValidationError" do
|
|
84
|
+
described_class.validate!({ items: [] }, registry)
|
|
85
|
+
rescue Lutaml::Model::Validation::ValidationError => e
|
|
86
|
+
expect(e.issues.length).to eq(1)
|
|
87
|
+
expect(e.issues.first.code).to eq("E2E-001")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "does not raise when only warnings" do
|
|
91
|
+
expect do
|
|
92
|
+
described_class.validate!({ items: (1..101).to_a }, registry)
|
|
93
|
+
end.not_to raise_error
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "does not raise when no issues" do
|
|
97
|
+
expect do
|
|
98
|
+
described_class.validate!({ items: (1..50).to_a }, registry)
|
|
99
|
+
end.not_to raise_error
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
describe ".new_registry" do
|
|
104
|
+
it "returns a fresh Registry instance" do
|
|
105
|
+
reg = described_class.new_registry
|
|
106
|
+
expect(reg).to be_a(Lutaml::Model::Validation::Registry)
|
|
107
|
+
expect(reg.size).to eq(0)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# spec/lutaml/xml/content_model_validation_spec.rb
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require_relative "../../../lib/lutaml/model"
|
|
5
|
+
|
|
6
|
+
RSpec.describe "Content model validation" do
|
|
7
|
+
describe "OrderedContentMappingError" do
|
|
8
|
+
it "raises when ordered + map_content" do
|
|
9
|
+
expect do
|
|
10
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
11
|
+
attribute :content, :string
|
|
12
|
+
|
|
13
|
+
xml do
|
|
14
|
+
element "test"
|
|
15
|
+
ordered
|
|
16
|
+
map_content to: :content
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.name
|
|
20
|
+
"OrderedWithContentTest"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end.to raise_error(
|
|
24
|
+
Lutaml::Model::OrderedContentMappingError,
|
|
25
|
+
/Element-only content model.*does not support `map_content`/,
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "raises when root with ordered: true and map_content" do
|
|
30
|
+
expect do
|
|
31
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
32
|
+
attribute :content, :string
|
|
33
|
+
|
|
34
|
+
xml do
|
|
35
|
+
root "test", ordered: true
|
|
36
|
+
map_content to: :content
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.name
|
|
40
|
+
"RootOrderedWithContentTest"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end.to raise_error(Lutaml::Model::OrderedContentMappingError)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "does not raise when ordered without map_content" do
|
|
47
|
+
expect do
|
|
48
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
49
|
+
attribute :child, :string
|
|
50
|
+
|
|
51
|
+
xml do
|
|
52
|
+
element "test"
|
|
53
|
+
ordered
|
|
54
|
+
map_element "child", to: :child
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.name
|
|
58
|
+
"OrderedWithoutContentTest"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end.not_to raise_error
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "does not raise when mixed_content with map_content (collection)" do
|
|
65
|
+
expect do
|
|
66
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
67
|
+
attribute :content, :string, collection: true
|
|
68
|
+
|
|
69
|
+
xml do
|
|
70
|
+
element "test"
|
|
71
|
+
mixed_content
|
|
72
|
+
map_content to: :content
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.name
|
|
76
|
+
"MixedWithContentTest"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end.not_to raise_error
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "does not raise when root with mixed: true and map_content (collection)" do
|
|
83
|
+
expect do
|
|
84
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
85
|
+
attribute :content, :string, collection: true
|
|
86
|
+
|
|
87
|
+
xml do
|
|
88
|
+
root "test", mixed: true
|
|
89
|
+
map_content to: :content
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.name
|
|
93
|
+
"RootMixedWithContentTest"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end.not_to raise_error
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "does not raise when default (no ordered/mixed) with map_content" do
|
|
100
|
+
expect do
|
|
101
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
102
|
+
attribute :content, :string
|
|
103
|
+
|
|
104
|
+
xml do
|
|
105
|
+
element "test"
|
|
106
|
+
map_content to: :content
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.name
|
|
110
|
+
"DefaultWithContentTest"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end.not_to raise_error
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
describe "MixedContentCollectionError" do
|
|
118
|
+
it "raises when mixed_content + map_content to non-collection attribute" do
|
|
119
|
+
expect do
|
|
120
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
121
|
+
attribute :content, :string
|
|
122
|
+
|
|
123
|
+
xml do
|
|
124
|
+
element "test"
|
|
125
|
+
mixed_content
|
|
126
|
+
map_content to: :content
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.name
|
|
130
|
+
"MixedContentNonCollectionTest"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end.to raise_error(
|
|
134
|
+
Lutaml::Model::MixedContentCollectionError,
|
|
135
|
+
/Mixed content requires.*to be a string collection/,
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "does not raise when mixed_content + map_content to collection attribute" do
|
|
140
|
+
expect do
|
|
141
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
142
|
+
attribute :content, :string, collection: true
|
|
143
|
+
|
|
144
|
+
xml do
|
|
145
|
+
element "test"
|
|
146
|
+
mixed_content
|
|
147
|
+
map_content to: :content
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.name
|
|
151
|
+
"MixedContentCollectionTest"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end.not_to raise_error
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -755,15 +755,20 @@ RSpec.describe Lutaml::Xml::Mapping do
|
|
|
755
755
|
end
|
|
756
756
|
|
|
757
757
|
it "element_order should be correct" do
|
|
758
|
-
|
|
758
|
+
# Filter out whitespace-only text nodes for comparison with expected_order
|
|
759
|
+
non_ws_order = parsed.element_order.reject do |e|
|
|
760
|
+
e.type == "Text" && e.text_content&.strip&.empty?
|
|
761
|
+
end
|
|
762
|
+
expect(non_ws_order).to eq(expected_order)
|
|
759
763
|
end
|
|
760
764
|
|
|
761
|
-
it "element_order
|
|
762
|
-
#
|
|
763
|
-
#
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
765
|
+
it "element_order includes whitespace-only text nodes" do
|
|
766
|
+
# Whitespace-only text nodes between elements are preserved in element_order
|
|
767
|
+
# for mixed-content round-trip fidelity.
|
|
768
|
+
text_entries = parsed.element_order.select do |e|
|
|
769
|
+
e.type == "Text" && e.text_content&.strip&.empty?
|
|
770
|
+
end
|
|
771
|
+
expect(text_entries).not_to be_empty
|
|
767
772
|
end
|
|
768
773
|
|
|
769
774
|
it "to_xml should be correct" do
|
|
@@ -194,6 +194,18 @@ RSpec.describe Lutaml::Xml::Schema::Xsd::Glob do
|
|
|
194
194
|
end
|
|
195
195
|
end
|
|
196
196
|
|
|
197
|
+
context "with whitespace in schemaLocation" do
|
|
198
|
+
it "strips leading/trailing whitespace from schema location" do
|
|
199
|
+
clean = described_class.include_schema("metaschema.xsd")
|
|
200
|
+
padded = described_class.include_schema(" metaschema.xsd ")
|
|
201
|
+
expect(padded).to eq(clean)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "handles whitespace-only schema location gracefully" do
|
|
205
|
+
expect(described_class.include_schema(" ")).to be_nil
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
197
209
|
context "with regex pattern mapping" do
|
|
198
210
|
before do
|
|
199
211
|
# Use forward slashes for cross-platform compatibility in regex patterns
|