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,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module JsonLd
|
|
7
|
+
class Transform < Lutaml::Rdf::Transform
|
|
8
|
+
def model_to_data(instance, _format, options = {})
|
|
9
|
+
mapping = extract_mapping(options)
|
|
10
|
+
return {} unless mapping
|
|
11
|
+
|
|
12
|
+
if mapping.rdf_members.any?
|
|
13
|
+
build_graph_document(mapping, instance)
|
|
14
|
+
else
|
|
15
|
+
build_resource_object(mapping, instance)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def data_to_model(data, _format, options = {})
|
|
20
|
+
mapping = extract_mapping(options)
|
|
21
|
+
return model_class.new unless mapping
|
|
22
|
+
|
|
23
|
+
hash = data.is_a?(String) ? JSON.parse(data) : data
|
|
24
|
+
|
|
25
|
+
if hash.key?("@graph") && hash["@graph"].is_a?(Array) && !hash["@graph"].empty?
|
|
26
|
+
graph_data = hash["@graph"]
|
|
27
|
+
first = graph_data.first
|
|
28
|
+
hash = first.is_a?(Hash) ? first : {}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
hash = strip_jsonld_keywords(hash)
|
|
32
|
+
|
|
33
|
+
attrs = {}
|
|
34
|
+
mapping.rdf_predicates.each do |rule|
|
|
35
|
+
value = hash[rule.predicate_name]
|
|
36
|
+
next if value.nil?
|
|
37
|
+
|
|
38
|
+
attrs[rule.to] = if rule.lang_tagged && value.is_a?(Hash)
|
|
39
|
+
flatten_language_map(value)
|
|
40
|
+
else
|
|
41
|
+
value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
build_instance(attrs, options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def extract_mapping(options)
|
|
51
|
+
options[:mappings] || mappings_for(:jsonld, lutaml_register)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_graph_document(mapping, instance)
|
|
55
|
+
context = build_merged_context(mapping, instance)
|
|
56
|
+
graph = []
|
|
57
|
+
|
|
58
|
+
if mapping.rdf_subject
|
|
59
|
+
resource = build_resource_data(mapping, instance)
|
|
60
|
+
graph << resource unless resource.empty?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
mapping.rdf_members.each do |member_rule|
|
|
64
|
+
collection = Array(instance.public_send(member_rule.attr_name))
|
|
65
|
+
collection.each do |member|
|
|
66
|
+
member_mapping = member.class.mappings[:jsonld]
|
|
67
|
+
next unless member_mapping
|
|
68
|
+
|
|
69
|
+
resource = build_resource_data(member_mapping, member)
|
|
70
|
+
graph << resource unless resource.empty?
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
{ "@context" => context, "@graph" => graph }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_merged_context(mapping, instance)
|
|
78
|
+
context_hash = build_context_from_mapping(mapping).to_hash
|
|
79
|
+
|
|
80
|
+
mapping.rdf_members.each do |member_rule|
|
|
81
|
+
collection = Array(instance.public_send(member_rule.attr_name))
|
|
82
|
+
next if collection.empty?
|
|
83
|
+
|
|
84
|
+
member_mapping = collection.first.class.mappings[:jsonld]
|
|
85
|
+
next unless member_mapping
|
|
86
|
+
|
|
87
|
+
context_hash.merge!(build_context_from_mapping(member_mapping).to_hash)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
context_hash
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_context_from_mapping(mapping)
|
|
94
|
+
context = Context.new
|
|
95
|
+
mapping.namespace_set.each { |ns| context.prefix(ns) }
|
|
96
|
+
mapping.rdf_predicates.each do |pred|
|
|
97
|
+
if pred.lang_tagged
|
|
98
|
+
context.term(pred.predicate_name,
|
|
99
|
+
id: pred.uri,
|
|
100
|
+
container: :language)
|
|
101
|
+
else
|
|
102
|
+
context.term(pred.predicate_name, id: pred.uri)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
context
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_resource_object(mapping, instance)
|
|
109
|
+
context = build_context_from_mapping(mapping).to_hash
|
|
110
|
+
data = build_resource_data(mapping, instance)
|
|
111
|
+
{ "@context" => context }.merge(data)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_resource_data(mapping, instance)
|
|
115
|
+
result = {}
|
|
116
|
+
|
|
117
|
+
if mapping.rdf_type
|
|
118
|
+
result["@type"] = resolve_type_compact(mapping)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if mapping.rdf_subject
|
|
122
|
+
result["@id"] = resolve_subject_uri(mapping, instance)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
mapping.rdf_predicates.each do |rule|
|
|
126
|
+
value = instance.public_send(rule.to)
|
|
127
|
+
next if value.nil?
|
|
128
|
+
next if value.is_a?(String) && value.empty?
|
|
129
|
+
|
|
130
|
+
result[rule.predicate_name] = if rule.lang_tagged
|
|
131
|
+
build_language_map(value)
|
|
132
|
+
else
|
|
133
|
+
serialize_rdf_value(value)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
result
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def build_language_map(values)
|
|
141
|
+
case values
|
|
142
|
+
when Array
|
|
143
|
+
map = {}
|
|
144
|
+
values.each do |v|
|
|
145
|
+
lang = extract_language(v)
|
|
146
|
+
map[lang] = v.to_s if lang
|
|
147
|
+
end
|
|
148
|
+
map.empty? ? nil : map
|
|
149
|
+
else
|
|
150
|
+
lang = extract_language(values)
|
|
151
|
+
lang ? { lang => values.to_s } : values.to_s
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def flatten_language_map(lang_map)
|
|
156
|
+
lang_map.values
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def serialize_rdf_value(value)
|
|
160
|
+
case value
|
|
161
|
+
when Array then value.map { |v| serialize_rdf_value(v) }
|
|
162
|
+
when Integer, Float, TrueClass, FalseClass then value
|
|
163
|
+
else value.to_s
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def strip_jsonld_keywords(data)
|
|
168
|
+
return data unless data.is_a?(Hash)
|
|
169
|
+
|
|
170
|
+
data.reject { |key, _| key.start_with?("@") }
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "model"
|
|
4
|
+
require_relative "rdf"
|
|
5
|
+
|
|
6
|
+
module Lutaml
|
|
7
|
+
module JsonLd
|
|
8
|
+
autoload :Context, "#{__dir__}/jsonld/context"
|
|
9
|
+
autoload :TermDefinition, "#{__dir__}/jsonld/term_definition"
|
|
10
|
+
autoload :Transform, "#{__dir__}/jsonld/transform"
|
|
11
|
+
autoload :Adapter, "#{__dir__}/jsonld/adapter"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Lutaml::Model::FormatRegistry.register(
|
|
16
|
+
:jsonld,
|
|
17
|
+
mapping_class: Lutaml::Rdf::Mapping,
|
|
18
|
+
adapter_class: Lutaml::JsonLd::Adapter,
|
|
19
|
+
transformer: Lutaml::JsonLd::Transform,
|
|
20
|
+
key_value: false,
|
|
21
|
+
rdf: true,
|
|
22
|
+
error_types: ["JSON::ParserError"],
|
|
23
|
+
)
|
|
@@ -330,7 +330,25 @@ module Lutaml
|
|
|
330
330
|
|
|
331
331
|
def default(register = Lutaml::Model::Config.default_register,
|
|
332
332
|
instance_object = nil)
|
|
333
|
-
|
|
333
|
+
if instance_object.nil?
|
|
334
|
+
@default_cache ||= {}
|
|
335
|
+
cached = @default_cache[register]
|
|
336
|
+
return cached if cached
|
|
337
|
+
|
|
338
|
+
result = cast_value(default_value(register, nil), register)
|
|
339
|
+
if immutable_value?(result)
|
|
340
|
+
@default_cache[register] = result
|
|
341
|
+
end
|
|
342
|
+
result
|
|
343
|
+
else
|
|
344
|
+
cast_value(default_value(register, instance_object), register)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def immutable_value?(value)
|
|
349
|
+
value.nil? || value.is_a?(Numeric) || value.is_a?(String) ||
|
|
350
|
+
value.is_a?(Symbol) || value == true || value == false ||
|
|
351
|
+
value.frozen?
|
|
334
352
|
end
|
|
335
353
|
|
|
336
354
|
def default_value(register, instance_object = nil)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
class OrderedContentMappingError < Error
|
|
6
|
+
def initialize(model_class)
|
|
7
|
+
@model_class = model_class
|
|
8
|
+
super()
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_s
|
|
12
|
+
"Element-only content model (`ordered`) does not support `map_content` in #{@model_class}. " \
|
|
13
|
+
"Use `mixed_content` instead of `ordered` when you need to capture text content between elements."
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -23,7 +23,7 @@ module Lutaml
|
|
|
23
23
|
# @param adapter_options [Hash, nil] { available: [...], default: :name }
|
|
24
24
|
def register(format, mapping_class:, adapter_class:, transformer:,
|
|
25
25
|
adapter_loader: nil, castable_type: nil, key_value: nil,
|
|
26
|
-
error_types: nil, adapter_options: nil)
|
|
26
|
+
rdf: nil, error_types: nil, adapter_options: nil)
|
|
27
27
|
validate_registration!(format, mapping_class, transformer)
|
|
28
28
|
|
|
29
29
|
registered_formats[format] = {
|
|
@@ -33,6 +33,7 @@ module Lutaml
|
|
|
33
33
|
adapter_loader: adapter_loader,
|
|
34
34
|
castable_type: castable_type,
|
|
35
35
|
key_value: key_value,
|
|
36
|
+
rdf: rdf,
|
|
36
37
|
error_types: error_types,
|
|
37
38
|
adapter_options: adapter_options,
|
|
38
39
|
registered_at: Time.now,
|
|
@@ -109,6 +110,14 @@ module Lutaml
|
|
|
109
110
|
registered_formats.dig(format, :key_value) == true
|
|
110
111
|
end
|
|
111
112
|
|
|
113
|
+
def rdf_formats
|
|
114
|
+
registered_formats.select { |_, info| info[:rdf] }.keys
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def rdf?(format)
|
|
118
|
+
registered_formats.dig(format, :rdf) == true
|
|
119
|
+
end
|
|
120
|
+
|
|
112
121
|
def error_types_for(format)
|
|
113
122
|
registered_formats.dig(format, :error_types)
|
|
114
123
|
end
|
|
@@ -36,7 +36,8 @@ module Lutaml
|
|
|
36
36
|
def register_liquid_drop_class
|
|
37
37
|
validate_liquid!
|
|
38
38
|
if base_drop_class
|
|
39
|
-
raise
|
|
39
|
+
raise Lutaml::Model::LiquidDropAlreadyRegisteredError,
|
|
40
|
+
drop_class_name
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
const_set(drop_class_name,
|
|
@@ -45,6 +46,14 @@ module Lutaml
|
|
|
45
46
|
super()
|
|
46
47
|
@object = object
|
|
47
48
|
end
|
|
49
|
+
|
|
50
|
+
def liquefy_value(value)
|
|
51
|
+
if value.is_a?(Array)
|
|
52
|
+
value.map(&:to_liquid)
|
|
53
|
+
else
|
|
54
|
+
value.to_liquid
|
|
55
|
+
end
|
|
56
|
+
end
|
|
48
57
|
end)
|
|
49
58
|
end
|
|
50
59
|
|
|
@@ -99,13 +108,7 @@ module Lutaml
|
|
|
99
108
|
return if base_drop_class.method_defined?(method_name)
|
|
100
109
|
|
|
101
110
|
base_drop_class.define_method(method_name) do
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if value.is_a?(Array)
|
|
105
|
-
value.map(&:to_liquid)
|
|
106
|
-
else
|
|
107
|
-
value.to_liquid
|
|
108
|
-
end
|
|
111
|
+
liquefy_value(@object.public_send(method_name))
|
|
109
112
|
end
|
|
110
113
|
end
|
|
111
114
|
|
|
@@ -114,13 +117,7 @@ module Lutaml
|
|
|
114
117
|
|
|
115
118
|
liquid_mappings.mappings.each do |key, method_name|
|
|
116
119
|
base_drop_class.define_method(key) do
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if value.is_a?(Array)
|
|
120
|
-
value.map(&:to_liquid)
|
|
121
|
-
else
|
|
122
|
-
value.to_liquid
|
|
123
|
-
end
|
|
120
|
+
liquefy_value(@object.public_send(method_name))
|
|
124
121
|
end
|
|
125
122
|
end
|
|
126
123
|
end
|
|
@@ -81,6 +81,10 @@ module Lutaml
|
|
|
81
81
|
@polymorphic_map = polymorphic_map
|
|
82
82
|
@transform = transform
|
|
83
83
|
|
|
84
|
+
# Cache whether this rule needs the full deserialize chain.
|
|
85
|
+
# Over 95% of rules are "simple" (no custom method, no delegate).
|
|
86
|
+
@needs_full_deserialize = has_custom_method_for_deserialization? || !!delegate
|
|
87
|
+
|
|
84
88
|
# Only calculate default_value_map if value_map is not fully provided
|
|
85
89
|
if value_map.empty? || !value_map[:from] || !value_map[:to]
|
|
86
90
|
# Build value_map by starting with defaults from render_nil/render_empty,
|
|
@@ -280,9 +284,13 @@ module Lutaml
|
|
|
280
284
|
end
|
|
281
285
|
|
|
282
286
|
def deserialize(model, value, attributes, mapper_class = nil)
|
|
283
|
-
|
|
284
|
-
|
|
287
|
+
if @needs_full_deserialize
|
|
288
|
+
handle_custom_method(model, value, mapper_class) ||
|
|
289
|
+
handle_delegate(model, value, attributes) ||
|
|
290
|
+
handle_transform_method(model, value, attributes)
|
|
291
|
+
else
|
|
285
292
|
handle_transform_method(model, value, attributes)
|
|
293
|
+
end
|
|
286
294
|
end
|
|
287
295
|
|
|
288
296
|
def has_custom_method_for_serialization?
|
|
@@ -15,7 +15,12 @@ module Lutaml
|
|
|
15
15
|
# @param block [Proc] The DSL block to evaluate
|
|
16
16
|
def process_mapping(format, *_args, &)
|
|
17
17
|
klass = ::Lutaml::Model::Config.mappings_class_for(format)
|
|
18
|
-
mappings[format]
|
|
18
|
+
existing = mappings[format]
|
|
19
|
+
mappings[format] = if existing.nil? || !existing.is_a?(klass)
|
|
20
|
+
klass.new
|
|
21
|
+
else
|
|
22
|
+
existing
|
|
23
|
+
end
|
|
19
24
|
mappings[format].instance_eval(&)
|
|
20
25
|
|
|
21
26
|
if mappings[format].respond_to?(:finalize)
|
|
@@ -273,6 +278,17 @@ module Lutaml
|
|
|
273
278
|
end
|
|
274
279
|
end
|
|
275
280
|
|
|
281
|
+
# Define RDF mappings for multiple formats (Turtle, JSON-LD, etc.).
|
|
282
|
+
# Discovers RDF formats dynamically from FormatRegistry.
|
|
283
|
+
#
|
|
284
|
+
# @param block [Proc] The DSL block
|
|
285
|
+
def rdf(&block)
|
|
286
|
+
Lutaml::Model::FormatRegistry.rdf_formats.each do |format|
|
|
287
|
+
mappings[format] = Lutaml::Rdf::Mapping.new
|
|
288
|
+
mappings[format].instance_eval(&block)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
276
292
|
# Get resolved mapping for a format
|
|
277
293
|
#
|
|
278
294
|
# Delegates to TransformationRegistry for centralized caching
|
|
@@ -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
|