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,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
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "validation/issue"
|
|
4
|
+
require_relative "validation/layer_result"
|
|
5
|
+
require_relative "validation/report"
|
|
6
|
+
require_relative "validation/context"
|
|
7
|
+
require_relative "validation/rule"
|
|
8
|
+
require_relative "validation/registry"
|
|
9
|
+
require_relative "validation/profile"
|
|
10
|
+
require_relative "validation/remediation"
|
|
11
|
+
require_relative "validation/remediation_result"
|
|
12
|
+
|
|
13
|
+
module Lutaml
|
|
14
|
+
module Model
|
|
15
|
+
# Document-level validation framework. Orthogonal to the existing
|
|
16
|
+
# attribute-level Validation module — this validates structural
|
|
17
|
+
# integrity, cross-references, and conformance against domain rules.
|
|
18
|
+
#
|
|
19
|
+
# @example Run all registered rules
|
|
20
|
+
# issues = Lutaml::Model::Validation.validate(context, registry)
|
|
21
|
+
# @example Run and raise on errors
|
|
22
|
+
# Lutaml::Model::Validation.validate!(context, registry)
|
|
23
|
+
module Validation
|
|
24
|
+
class << self
|
|
25
|
+
def new_registry
|
|
26
|
+
Registry.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validate(context, registry, profile: nil)
|
|
30
|
+
rules = if profile
|
|
31
|
+
profile.resolve(registry)
|
|
32
|
+
else
|
|
33
|
+
registry.all
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
all_issues = []
|
|
37
|
+
rules.each do |rule|
|
|
38
|
+
next unless rule.applicable?(context)
|
|
39
|
+
|
|
40
|
+
issues = rule.check(context)
|
|
41
|
+
if context.respond_to?(:add_error)
|
|
42
|
+
issues.each { |i| context.add_error(i) }
|
|
43
|
+
end
|
|
44
|
+
all_issues.concat(issues)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
all_issues
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def validate!(context, registry, profile: nil)
|
|
51
|
+
issues = validate(context, registry, profile: profile)
|
|
52
|
+
return if issues.empty?
|
|
53
|
+
|
|
54
|
+
errors = issues.select(&:error?)
|
|
55
|
+
unless errors.empty?
|
|
56
|
+
raise ValidationError.new(format_errors(errors), issues: errors)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def format_errors(errors)
|
|
63
|
+
errors.map { |e| "[#{e.code}] #{e.message}" }.join("\n")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class ValidationError < StandardError
|
|
68
|
+
attr_reader :issues
|
|
69
|
+
|
|
70
|
+
def initialize(message, issues: [])
|
|
71
|
+
super(message)
|
|
72
|
+
@issues = issues
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/model.rb
CHANGED
|
@@ -10,6 +10,8 @@ module Lutaml
|
|
|
10
10
|
autoload :Jsonl, "#{__dir__}/jsonl"
|
|
11
11
|
autoload :Yamls, "#{__dir__}/yamls"
|
|
12
12
|
autoload :Xml, "#{__dir__}/xml"
|
|
13
|
+
autoload :JsonLd, "#{__dir__}/jsonld"
|
|
14
|
+
autoload :Turtle, "#{__dir__}/turtle"
|
|
13
15
|
|
|
14
16
|
module Model
|
|
15
17
|
# Autoloads for lazy loading - set up BEFORE any requires
|
|
@@ -115,6 +117,8 @@ module Lutaml
|
|
|
115
117
|
"#{__dir__}/model/error/liquid_not_enabled_error"
|
|
116
118
|
autoload :LiquidClassNotFoundError,
|
|
117
119
|
"#{__dir__}/model/error/liquid_class_not_found_error"
|
|
120
|
+
autoload :LiquidDropAlreadyRegisteredError,
|
|
121
|
+
"#{__dir__}/model/error/liquid_drop_already_registered_error"
|
|
118
122
|
autoload :NoAttributesDefinedLiquidError,
|
|
119
123
|
"#{__dir__}/model/error/no_attributes_defined_liquid_error"
|
|
120
124
|
autoload :IncorrectMappingArgumentsError,
|
|
@@ -184,6 +188,8 @@ module Lutaml
|
|
|
184
188
|
"#{__dir__}/model/error/unresolvable_type_error"
|
|
185
189
|
autoload :MixedContentCollectionError,
|
|
186
190
|
"#{__dir__}/model/error/mixed_content_collection_error"
|
|
191
|
+
autoload :OrderedContentMappingError,
|
|
192
|
+
"#{__dir__}/model/error/ordered_content_mapping_error"
|
|
187
193
|
|
|
188
194
|
# Error for passing incorrect model type
|
|
189
195
|
#
|
|
@@ -237,6 +243,10 @@ require "#{__dir__}/jsonl"
|
|
|
237
243
|
require "#{__dir__}/yamls"
|
|
238
244
|
require "#{__dir__}/xml"
|
|
239
245
|
|
|
246
|
+
# Optional formats: require "lutaml/jsonld" or "lutaml/turtle" to enable.
|
|
247
|
+
# These are not eagerly loaded because they depend on optional gems
|
|
248
|
+
# (e.g., rdf-turtle) that most users don't need.
|
|
249
|
+
|
|
240
250
|
# Prepend builder interface into Serialize
|
|
241
251
|
# Builder must be prepended AFTER XML so its initialize runs first
|
|
242
252
|
# (Builder -> XML InstanceMethods -> Serialize)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Rdf
|
|
5
|
+
class Iri
|
|
6
|
+
include Comparable
|
|
7
|
+
|
|
8
|
+
attr_reader :value
|
|
9
|
+
|
|
10
|
+
def initialize(uri_string)
|
|
11
|
+
@value = uri_string.to_s.freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def expand(namespace_set)
|
|
15
|
+
namespace_set.resolve_compact_iri(value)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def compact(namespace_set)
|
|
19
|
+
namespace_set.compact(value)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def <=>(other)
|
|
23
|
+
other.is_a?(self.class) ? value <=> other.value : nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ==(other)
|
|
27
|
+
other.is_a?(self.class) && value == other.value
|
|
28
|
+
end
|
|
29
|
+
alias_method :eql?, :==
|
|
30
|
+
|
|
31
|
+
def hash
|
|
32
|
+
value.hash
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_s
|
|
36
|
+
value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def inspect
|
|
40
|
+
"#<#{self.class.name} #{value}>"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Rdf
|
|
5
|
+
class Literal
|
|
6
|
+
include LanguageTagged
|
|
7
|
+
|
|
8
|
+
attr_reader :value, :datatype, :language
|
|
9
|
+
|
|
10
|
+
def initialize(value, datatype: nil, language: nil)
|
|
11
|
+
@value = value
|
|
12
|
+
@datatype = datatype
|
|
13
|
+
@language = language
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_turtle
|
|
17
|
+
escaped = escape_turtle(value.to_s)
|
|
18
|
+
if language
|
|
19
|
+
"#{escaped}@#{language}"
|
|
20
|
+
elsif datatype
|
|
21
|
+
"#{escaped}^^<#{datatype}>"
|
|
22
|
+
else
|
|
23
|
+
escaped
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_jsonld_term
|
|
28
|
+
if language
|
|
29
|
+
{ "@value" => value, "@language" => language }
|
|
30
|
+
elsif datatype
|
|
31
|
+
{ "@value" => value, "@type" => datatype.to_s }
|
|
32
|
+
else
|
|
33
|
+
value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def ==(other)
|
|
38
|
+
other.is_a?(self.class) &&
|
|
39
|
+
value == other.value &&
|
|
40
|
+
datatype == other.datatype &&
|
|
41
|
+
language == other.language
|
|
42
|
+
end
|
|
43
|
+
alias_method :eql?, :==
|
|
44
|
+
|
|
45
|
+
def hash
|
|
46
|
+
[value, datatype, language].hash
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def escape_turtle(str)
|
|
52
|
+
escaped = str.gsub(/[\n\r\t"\\]/,
|
|
53
|
+
"\\" => "\\\\",
|
|
54
|
+
'"' => "\\\"",
|
|
55
|
+
"\n" => "\\n",
|
|
56
|
+
"\r" => "\\r",
|
|
57
|
+
"\t" => "\\t")
|
|
58
|
+
"\"#{escaped}\""
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|