lutaml-model 0.8.3 → 0.8.5
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/.github/workflows/dependent-tests.yml +3 -1
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +16 -22
- data/Gemfile +2 -0
- data/README.adoc +327 -3
- data/docs/_guides/document-validation.adoc +303 -0
- data/docs/_guides/index.adoc +19 -0
- data/docs/_guides/jsonld-serialization.adoc +217 -0
- data/docs/_guides/rdf-serialization.adoc +344 -0
- data/docs/_guides/turtle-serialization.adoc +224 -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/_pages/serialization_adapters.adoc +31 -0
- data/docs/_references/index.adoc +1 -0
- data/docs/_references/rdf-namespaces.adoc +243 -0
- data/docs/_tutorials/lutaml-xml-architecture.adoc +6 -1
- data/docs/index.adoc +3 -2
- data/lib/lutaml/jsonld/adapter.rb +23 -0
- data/lib/lutaml/jsonld/context.rb +69 -0
- data/lib/lutaml/jsonld/term_definition.rb +39 -0
- data/lib/lutaml/jsonld/transform.rb +174 -0
- data/lib/lutaml/jsonld.rb +23 -0
- 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/format_registry.rb +10 -1
- 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/serialize/format_conversion.rb +17 -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 +10 -0
- data/lib/lutaml/rdf/error.rb +7 -0
- data/lib/lutaml/rdf/iri.rb +44 -0
- data/lib/lutaml/rdf/language_tagged.rb +11 -0
- data/lib/lutaml/rdf/literal.rb +62 -0
- data/lib/lutaml/rdf/mapping.rb +71 -0
- data/lib/lutaml/rdf/mapping_rule.rb +35 -0
- data/lib/lutaml/rdf/member_rule.rb +13 -0
- data/lib/lutaml/rdf/namespace.rb +58 -0
- data/lib/lutaml/rdf/namespace_set.rb +69 -0
- data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
- data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces.rb +14 -0
- data/lib/lutaml/rdf/transform.rb +36 -0
- data/lib/lutaml/rdf.rb +19 -0
- data/lib/lutaml/turtle/adapter.rb +35 -0
- data/lib/lutaml/turtle/mapping.rb +7 -0
- data/lib/lutaml/turtle/transform.rb +158 -0
- data/lib/lutaml/turtle.rb +22 -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/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 -1
- data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
- data/spec/lutaml/integration/multi_format_spec.rb +106 -0
- data/spec/lutaml/integration/round_trip_spec.rb +170 -0
- data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
- data/spec/lutaml/jsonld/context_spec.rb +114 -0
- data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
- data/spec/lutaml/jsonld/transform_spec.rb +211 -0
- 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/rdf/graph_serialization_spec.rb +137 -0
- data/spec/lutaml/rdf/iri_spec.rb +73 -0
- data/spec/lutaml/rdf/literal_spec.rb +98 -0
- data/spec/lutaml/rdf/mapping_spec.rb +164 -0
- data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
- data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
- data/spec/lutaml/rdf/namespace_spec.rb +241 -0
- data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
- data/spec/lutaml/turtle/adapter_spec.rb +47 -0
- data/spec/lutaml/turtle/mapping_spec.rb +123 -0
- data/spec/lutaml/turtle/transform_spec.rb +273 -0
- data/spec/lutaml/xml/content_model_validation_spec.rb +157 -0
- data/spec/lutaml/xml/mapping_spec.rb +12 -7
- metadata +95 -7
- data/spec/fixtures/liquid_templates/_ceramics.liquid +0 -3
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/validation_framework"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Validation::HasIssues do
|
|
7
|
+
let(:error_issue) do
|
|
8
|
+
Lutaml::Model::Validation::Issue.new(
|
|
9
|
+
severity: "error", code: "E-001", message: "bad",
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
let(:warning_issue) do
|
|
14
|
+
Lutaml::Model::Validation::Issue.new(
|
|
15
|
+
severity: "warning", code: "W-001", message: "meh",
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
let(:info_issue) do
|
|
20
|
+
Lutaml::Model::Validation::Issue.new(
|
|
21
|
+
severity: "info", code: "I-001", message: "fyi",
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
let(:notice_issue) do
|
|
26
|
+
Lutaml::Model::Validation::Issue.new(
|
|
27
|
+
severity: "notice", code: "N-001", message: "note",
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
let(:container) do
|
|
32
|
+
all_issues = [error_issue, warning_issue, info_issue, notice_issue]
|
|
33
|
+
klass = Class.new do
|
|
34
|
+
include Lutaml::Model::Validation::HasIssues
|
|
35
|
+
|
|
36
|
+
attr_reader :issues
|
|
37
|
+
|
|
38
|
+
def initialize(issues)
|
|
39
|
+
@issues = issues
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
klass.new(all_issues)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "filters errors" do
|
|
46
|
+
expect(container.errors).to eq([error_issue])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "filters warnings" do
|
|
50
|
+
expect(container.warnings).to eq([warning_issue])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "filters infos" do
|
|
54
|
+
expect(container.infos).to eq([info_issue])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "filters notices" do
|
|
58
|
+
expect(container.notices).to eq([notice_issue])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "returns empty arrays when no matching severity" do
|
|
62
|
+
only_errors = Class.new do
|
|
63
|
+
include Lutaml::Model::Validation::HasIssues
|
|
64
|
+
|
|
65
|
+
def issues
|
|
66
|
+
[Lutaml::Model::Validation::Issue.new(
|
|
67
|
+
severity: "error", code: "X", message: "y",
|
|
68
|
+
)]
|
|
69
|
+
end
|
|
70
|
+
end.new
|
|
71
|
+
|
|
72
|
+
expect(only_errors.warnings).to be_empty
|
|
73
|
+
expect(only_errors.infos).to be_empty
|
|
74
|
+
expect(only_errors.notices).to be_empty
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/validation_framework"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Validation::Context do
|
|
7
|
+
subject(:context) { described_class.new }
|
|
8
|
+
|
|
9
|
+
it "starts with empty errors" do
|
|
10
|
+
expect(context.errors).to be_empty
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe "#add_error" do
|
|
14
|
+
it "accumulates errors" do
|
|
15
|
+
issue = Lutaml::Model::Validation::Issue.new(
|
|
16
|
+
severity: "error", code: "T-001", message: "bad",
|
|
17
|
+
)
|
|
18
|
+
context.add_error(issue)
|
|
19
|
+
expect(context.errors.length).to eq(1)
|
|
20
|
+
expect(context.errors.first).to eq(issue)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "#add_errors" do
|
|
25
|
+
it "concatenates multiple errors" do
|
|
26
|
+
issues = Array.new(2) do |i|
|
|
27
|
+
Lutaml::Model::Validation::Issue.new(
|
|
28
|
+
severity: "error", code: "T-#{i}", message: "bad #{i}",
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
context.add_errors(issues)
|
|
32
|
+
expect(context.errors.length).to eq(2)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe "#rule_state" do
|
|
37
|
+
it "provides per-rule state hash" do
|
|
38
|
+
state = context.rule_state("R-001")
|
|
39
|
+
state[:count] = 5
|
|
40
|
+
expect(context.rule_state("R-001")[:count]).to eq(5)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "isolates state between rules" do
|
|
44
|
+
context.rule_state("R-001")[:val] = "a"
|
|
45
|
+
context.rule_state("R-002")[:val] = "b"
|
|
46
|
+
expect(context.rule_state("R-001")[:val]).to eq("a")
|
|
47
|
+
expect(context.rule_state("R-002")[:val]).to eq("b")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe "#reset!" do
|
|
52
|
+
it "clears errors and state" do
|
|
53
|
+
context.add_error(double("issue"))
|
|
54
|
+
context.rule_state("R")[:x] = 1
|
|
55
|
+
context.reset!
|
|
56
|
+
expect(context.errors).to be_empty
|
|
57
|
+
expect(context.rule_state("R")).to be_empty
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/validation_framework"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Validation::Issue do
|
|
7
|
+
subject(:issue) do
|
|
8
|
+
described_class.new(
|
|
9
|
+
severity: "error",
|
|
10
|
+
code: "TEST-001",
|
|
11
|
+
message: "Something is wrong",
|
|
12
|
+
location: "file.xml",
|
|
13
|
+
line: 42,
|
|
14
|
+
suggestion: "Fix it",
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe "SEVERITIES constant" do
|
|
19
|
+
it "defines allowed severity levels" do
|
|
20
|
+
expect(described_class::SEVERITIES).to eq(%w[error warning info notice])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "severity validation" do
|
|
25
|
+
it "accepts valid severities" do
|
|
26
|
+
described_class::SEVERITIES.each do |sev|
|
|
27
|
+
expect { described_class.new(severity: sev, code: "T", message: "m") }
|
|
28
|
+
.not_to raise_error
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "rejects invalid severity" do
|
|
33
|
+
expect do
|
|
34
|
+
described_class.new(severity: "critical", code: "T", message: "m")
|
|
35
|
+
end.to raise_error(ArgumentError, /Invalid severity: critical/)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "allows nil severity" do
|
|
39
|
+
expect { described_class.new(severity: nil, code: "T", message: "m") }
|
|
40
|
+
.not_to raise_error
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe "serialization" do
|
|
45
|
+
it "serializes to JSON" do
|
|
46
|
+
parsed = JSON.parse(issue.to_json)
|
|
47
|
+
expect(parsed["severity"]).to eq("error")
|
|
48
|
+
expect(parsed["code"]).to eq("TEST-001")
|
|
49
|
+
expect(parsed["message"]).to eq("Something is wrong")
|
|
50
|
+
expect(parsed["location"]).to eq("file.xml")
|
|
51
|
+
expect(parsed["line"]).to eq(42)
|
|
52
|
+
expect(parsed["suggestion"]).to eq("Fix it")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "round-trips through JSON" do
|
|
56
|
+
restored = described_class.from_json(issue.to_json)
|
|
57
|
+
expect(restored.code).to eq("TEST-001")
|
|
58
|
+
expect(restored.message).to eq("Something is wrong")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe "severity predicates" do
|
|
63
|
+
it "returns correct predicate for error" do
|
|
64
|
+
expect(issue).to be_error
|
|
65
|
+
expect(issue).not_to be_warning
|
|
66
|
+
expect(issue).not_to be_info
|
|
67
|
+
expect(issue).not_to be_notice
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "returns correct predicate for warning" do
|
|
71
|
+
warning = described_class.new(severity: "warning", code: "W",
|
|
72
|
+
message: "m")
|
|
73
|
+
expect(warning).to be_warning
|
|
74
|
+
expect(warning).not_to be_error
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/validation_framework"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Validation::LayerResult do
|
|
7
|
+
subject(:layer) do
|
|
8
|
+
described_class.new(
|
|
9
|
+
name: "Structure",
|
|
10
|
+
status: "fail",
|
|
11
|
+
duration_ms: 15,
|
|
12
|
+
issues: [error_issue, warning_issue],
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let(:error_issue) do
|
|
17
|
+
Lutaml::Model::Validation::Issue.new(
|
|
18
|
+
severity: "error", code: "E-001", message: "bad",
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:warning_issue) do
|
|
23
|
+
Lutaml::Model::Validation::Issue.new(
|
|
24
|
+
severity: "warning", code: "W-001", message: "meh",
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "serializes to JSON" do
|
|
29
|
+
parsed = JSON.parse(layer.to_json)
|
|
30
|
+
expect(parsed["name"]).to eq("Structure")
|
|
31
|
+
expect(parsed["status"]).to eq("fail")
|
|
32
|
+
expect(parsed["issues"].length).to eq(2)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe "#pass? / #fail?" do
|
|
36
|
+
it "checks status" do
|
|
37
|
+
expect(layer).not_to be_pass
|
|
38
|
+
expect(layer).to be_fail
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "returns true for pass status" do
|
|
42
|
+
passing = described_class.new(name: "x", status: "pass")
|
|
43
|
+
expect(passing).to be_pass
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe "severity filtering via HasIssues" do
|
|
48
|
+
it "filters errors" do
|
|
49
|
+
expect(layer.errors.length).to eq(1)
|
|
50
|
+
expect(layer.errors.first.code).to eq("E-001")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "filters warnings" do
|
|
54
|
+
expect(layer.warnings.length).to eq(1)
|
|
55
|
+
expect(layer.warnings.first.code).to eq("W-001")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "returns empty for infos" do
|
|
59
|
+
expect(layer.infos).to be_empty
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "returns empty for notices" do
|
|
63
|
+
expect(layer.notices).to be_empty
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/validation_framework"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require "yaml"
|
|
7
|
+
|
|
8
|
+
RSpec.describe Lutaml::Model::Validation::Profile do
|
|
9
|
+
let(:registry) { Lutaml::Model::Validation.new_registry }
|
|
10
|
+
|
|
11
|
+
let(:profile) do
|
|
12
|
+
described_class.new(
|
|
13
|
+
name: "basic",
|
|
14
|
+
description: "Basic checks",
|
|
15
|
+
rule_names: ["TestRule"],
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "stores profile attributes" do
|
|
20
|
+
expect(profile.name).to eq("basic")
|
|
21
|
+
expect(profile.description).to eq("Basic checks")
|
|
22
|
+
expect(profile.rule_names).to eq(["TestRule"])
|
|
23
|
+
expect(profile.imports).to eq([])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe ".load" do
|
|
27
|
+
it "loads profile from YAML file" do
|
|
28
|
+
Dir.mktmpdir do |dir|
|
|
29
|
+
yaml_path = File.join(dir, "basic.yml")
|
|
30
|
+
File.write(yaml_path, YAML.dump({
|
|
31
|
+
"name" => "loaded",
|
|
32
|
+
"description" => "Loaded profile",
|
|
33
|
+
"rules" => ["RuleA", "RuleB"],
|
|
34
|
+
"import" => ["base"],
|
|
35
|
+
}))
|
|
36
|
+
loaded = described_class.load(yaml_path)
|
|
37
|
+
expect(loaded.name).to eq("loaded")
|
|
38
|
+
expect(loaded.rule_names).to eq(["RuleA", "RuleB"])
|
|
39
|
+
expect(loaded.imports).to eq(["base"])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "rejects YAML without a name key" do
|
|
44
|
+
Dir.mktmpdir do |dir|
|
|
45
|
+
yaml_path = File.join(dir, "bad.yml")
|
|
46
|
+
File.write(yaml_path, YAML.dump({ "description" => "no name" }))
|
|
47
|
+
expect { described_class.load(yaml_path) }
|
|
48
|
+
.to raise_error(ArgumentError, /must contain a 'name' string key/)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "rejects non-hash YAML content" do
|
|
53
|
+
Dir.mktmpdir do |dir|
|
|
54
|
+
yaml_path = File.join(dir, "array.yml")
|
|
55
|
+
File.write(yaml_path, YAML.dump(["just", "an", "array"]))
|
|
56
|
+
expect { described_class.load(yaml_path) }
|
|
57
|
+
.to raise_error(ArgumentError, /must contain a 'name' string key/)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe "#resolve" do
|
|
63
|
+
context "with imports" do
|
|
64
|
+
let(:base_rule_class) do
|
|
65
|
+
Class.new(Lutaml::Model::Validation::Rule) do
|
|
66
|
+
def self.name = "BaseRule"
|
|
67
|
+
def code = "BASE-001"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
let(:extra_rule_class) do
|
|
72
|
+
Class.new(Lutaml::Model::Validation::Rule) do
|
|
73
|
+
def self.name = "ExtraRule"
|
|
74
|
+
def code = "EXTRA-001"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
let(:base_profile) do
|
|
79
|
+
described_class.new(name: "base", rule_names: ["BaseRule"])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
let(:extended_profile) do
|
|
83
|
+
described_class.new(
|
|
84
|
+
name: "extended",
|
|
85
|
+
rule_names: ["ExtraRule"],
|
|
86
|
+
imports: ["base"],
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "resolves imports" do
|
|
91
|
+
registry.register(base_rule_class)
|
|
92
|
+
registry.register(extra_rule_class)
|
|
93
|
+
profiles = { "base" => base_profile, "extended" => extended_profile }
|
|
94
|
+
|
|
95
|
+
rules = extended_profile.resolve(registry, profiles)
|
|
96
|
+
codes = rules.map(&:code)
|
|
97
|
+
expect(codes).to include("BASE-001", "EXTRA-001")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "ignores missing imports" do
|
|
101
|
+
registry.register(extra_rule_class)
|
|
102
|
+
orphan = described_class.new(
|
|
103
|
+
name: "orphan",
|
|
104
|
+
rule_names: ["ExtraRule"],
|
|
105
|
+
imports: ["nonexistent"],
|
|
106
|
+
)
|
|
107
|
+
rules = orphan.resolve(registry, {})
|
|
108
|
+
expect(rules.map(&:code)).to eq(["EXTRA-001"])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "detects circular imports" do
|
|
112
|
+
a = described_class.new(name: "a", imports: ["b"])
|
|
113
|
+
b = described_class.new(name: "b", imports: ["a"])
|
|
114
|
+
profiles = { "a" => a, "b" => b }
|
|
115
|
+
expect { a.resolve(registry, profiles) }
|
|
116
|
+
.to raise_error(ArgumentError, /Circular profile import detected: a/)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "detects deep circular imports" do
|
|
120
|
+
a = described_class.new(name: "a", imports: ["b"])
|
|
121
|
+
b = described_class.new(name: "b", imports: ["c"])
|
|
122
|
+
c = described_class.new(name: "c", imports: ["a"])
|
|
123
|
+
profiles = { "a" => a, "b" => b, "c" => c }
|
|
124
|
+
expect { a.resolve(registry, profiles) }
|
|
125
|
+
.to raise_error(ArgumentError, /Circular profile import detected/)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "skips rules with unknown class names" do
|
|
130
|
+
result = profile.resolve(registry)
|
|
131
|
+
expect(result).to be_empty
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/validation_framework"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Validation::Registry do
|
|
7
|
+
subject(:registry) { described_class.new }
|
|
8
|
+
|
|
9
|
+
let(:rule_class) do
|
|
10
|
+
Class.new(Lutaml::Model::Validation::Rule) do
|
|
11
|
+
def code = "REG-001"
|
|
12
|
+
def category = :test
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
before { registry.register(rule_class) }
|
|
17
|
+
|
|
18
|
+
it "registers and returns rule instances" do
|
|
19
|
+
rules = registry.all
|
|
20
|
+
expect(rules.length).to eq(1)
|
|
21
|
+
expect(rules.first).to be_a(rule_class)
|
|
22
|
+
expect(rules.first.code).to eq("REG-001")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "caches rule instances" do
|
|
26
|
+
first_call = registry.all
|
|
27
|
+
second_call = registry.all
|
|
28
|
+
expect(first_call).to equal(second_call)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "invalidates cache on new registration" do
|
|
32
|
+
first = registry.all
|
|
33
|
+
new_rule = Class.new(Lutaml::Model::Validation::Rule) do
|
|
34
|
+
def code = "REG-002"
|
|
35
|
+
end
|
|
36
|
+
registry.register(new_rule)
|
|
37
|
+
expect(registry.all).not_to equal(first)
|
|
38
|
+
expect(registry.all.length).to eq(2)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "prevents duplicate registration" do
|
|
42
|
+
registry.register(rule_class)
|
|
43
|
+
expect(registry.size).to eq(1)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "filters by category" do
|
|
47
|
+
rules = registry.for_category(:test)
|
|
48
|
+
expect(rules.length).to eq(1)
|
|
49
|
+
expect(registry.for_category(:other)).to be_empty
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "finds by code" do
|
|
53
|
+
rule = registry.find("REG-001")
|
|
54
|
+
expect(rule).not_to be_nil
|
|
55
|
+
expect(rule.code).to eq("REG-001")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "returns nil for unknown code" do
|
|
59
|
+
expect(registry.find("UNKNOWN")).to be_nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "resets" do
|
|
63
|
+
registry.reset!
|
|
64
|
+
expect(registry.size).to eq(0)
|
|
65
|
+
expect(registry.all).to be_empty
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "returns rule classes" do
|
|
69
|
+
expect(registry.rule_classes).to eq([rule_class])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "returns a defensive copy of rule_classes" do
|
|
73
|
+
classes = registry.rule_classes
|
|
74
|
+
classes << String
|
|
75
|
+
expect(registry.rule_classes.length).to eq(1)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe "#auto_discover" do
|
|
79
|
+
it "requires rule files from a directory" do
|
|
80
|
+
Dir.mktmpdir do |dir|
|
|
81
|
+
rule_file = File.join(dir, "discovered_rule.rb")
|
|
82
|
+
File.write(rule_file, <<~RUBY)
|
|
83
|
+
class DiscoveredTestRule < Lutaml::Model::Validation::Rule
|
|
84
|
+
def code = "DISC-001"
|
|
85
|
+
end
|
|
86
|
+
RUBY
|
|
87
|
+
|
|
88
|
+
registry.auto_discover(dir, pattern: "*_rule.rb")
|
|
89
|
+
registry.register(DiscoveredTestRule)
|
|
90
|
+
expect(registry.find("DISC-001")).not_to be_nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -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
|