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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +3 -1
  3. data/.rubocop.yml +18 -0
  4. data/.rubocop_todo.yml +16 -22
  5. data/Gemfile +2 -0
  6. data/README.adoc +327 -3
  7. data/docs/_guides/document-validation.adoc +303 -0
  8. data/docs/_guides/index.adoc +19 -0
  9. data/docs/_guides/jsonld-serialization.adoc +217 -0
  10. data/docs/_guides/rdf-serialization.adoc +344 -0
  11. data/docs/_guides/turtle-serialization.adoc +224 -0
  12. data/docs/_guides/xml-mapping.adoc +9 -1
  13. data/docs/_guides/xml_mappings/07_best_practices.adoc +36 -0
  14. data/docs/_guides/xml_mappings/08_troubleshooting.adoc +89 -0
  15. data/docs/_pages/serialization_adapters.adoc +31 -0
  16. data/docs/_references/index.adoc +1 -0
  17. data/docs/_references/rdf-namespaces.adoc +243 -0
  18. data/docs/_tutorials/lutaml-xml-architecture.adoc +6 -1
  19. data/docs/index.adoc +3 -2
  20. data/lib/lutaml/jsonld/adapter.rb +23 -0
  21. data/lib/lutaml/jsonld/context.rb +69 -0
  22. data/lib/lutaml/jsonld/term_definition.rb +39 -0
  23. data/lib/lutaml/jsonld/transform.rb +174 -0
  24. data/lib/lutaml/jsonld.rb +23 -0
  25. data/lib/lutaml/model/attribute.rb +19 -1
  26. data/lib/lutaml/model/error/liquid_drop_already_registered_error.rb +11 -0
  27. data/lib/lutaml/model/error/ordered_content_mapping_error.rb +17 -0
  28. data/lib/lutaml/model/format_registry.rb +10 -1
  29. data/lib/lutaml/model/global_context.rb +1 -0
  30. data/lib/lutaml/model/liquefiable.rb +12 -15
  31. data/lib/lutaml/model/mapping/mapping_rule.rb +10 -2
  32. data/lib/lutaml/model/mapping_hash.rb +1 -1
  33. data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
  34. data/lib/lutaml/model/services/transformer.rb +67 -32
  35. data/lib/lutaml/model/transform.rb +41 -4
  36. data/lib/lutaml/model/uninitialized_class.rb +11 -5
  37. data/lib/lutaml/model/validation/concerns/has_issues.rb +27 -0
  38. data/lib/lutaml/model/validation/context.rb +36 -0
  39. data/lib/lutaml/model/validation/issue.rb +62 -0
  40. data/lib/lutaml/model/validation/layer_result.rb +34 -0
  41. data/lib/lutaml/model/validation/profile.rb +66 -0
  42. data/lib/lutaml/model/validation/registry.rb +60 -0
  43. data/lib/lutaml/model/validation/remediation.rb +33 -0
  44. data/lib/lutaml/model/validation/remediation_result.rb +20 -0
  45. data/lib/lutaml/model/validation/report.rb +39 -0
  46. data/lib/lutaml/model/validation/rule.rb +59 -0
  47. data/lib/lutaml/model/validation.rb +2 -1
  48. data/lib/lutaml/model/validation_framework.rb +77 -0
  49. data/lib/lutaml/model/version.rb +1 -1
  50. data/lib/lutaml/model.rb +10 -0
  51. data/lib/lutaml/rdf/error.rb +7 -0
  52. data/lib/lutaml/rdf/iri.rb +44 -0
  53. data/lib/lutaml/rdf/language_tagged.rb +11 -0
  54. data/lib/lutaml/rdf/literal.rb +62 -0
  55. data/lib/lutaml/rdf/mapping.rb +71 -0
  56. data/lib/lutaml/rdf/mapping_rule.rb +35 -0
  57. data/lib/lutaml/rdf/member_rule.rb +13 -0
  58. data/lib/lutaml/rdf/namespace.rb +58 -0
  59. data/lib/lutaml/rdf/namespace_set.rb +69 -0
  60. data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
  61. data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
  62. data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
  63. data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
  64. data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
  65. data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
  66. data/lib/lutaml/rdf/namespaces.rb +14 -0
  67. data/lib/lutaml/rdf/transform.rb +36 -0
  68. data/lib/lutaml/rdf.rb +19 -0
  69. data/lib/lutaml/turtle/adapter.rb +35 -0
  70. data/lib/lutaml/turtle/mapping.rb +7 -0
  71. data/lib/lutaml/turtle/transform.rb +158 -0
  72. data/lib/lutaml/turtle.rb +22 -0
  73. data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +9 -2
  74. data/lib/lutaml/xml/adapter/oga_adapter.rb +11 -3
  75. data/lib/lutaml/xml/adapter/ox_adapter.rb +5 -2
  76. data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -3
  77. data/lib/lutaml/xml/adapter_element.rb +26 -2
  78. data/lib/lutaml/xml/data_model.rb +14 -0
  79. data/lib/lutaml/xml/document.rb +3 -0
  80. data/lib/lutaml/xml/element.rb +8 -2
  81. data/lib/lutaml/xml/mapping.rb +9 -0
  82. data/lib/lutaml/xml/model_transform.rb +42 -0
  83. data/lib/lutaml/xml/schema/xsd/base.rb +4 -1
  84. data/lib/lutaml/xml/serialization/instance_methods.rb +3 -1
  85. data/lib/lutaml/xml/transformation/ordered_applier.rb +46 -2
  86. data/lib/lutaml/xml/transformation.rb +40 -1
  87. data/lib/lutaml/xml/xml_element.rb +8 -7
  88. data/lutaml-model.gemspec +1 -1
  89. data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
  90. data/spec/lutaml/integration/multi_format_spec.rb +106 -0
  91. data/spec/lutaml/integration/round_trip_spec.rb +170 -0
  92. data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
  93. data/spec/lutaml/jsonld/context_spec.rb +114 -0
  94. data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
  95. data/spec/lutaml/jsonld/transform_spec.rb +211 -0
  96. data/spec/lutaml/model/attribute_default_cache_spec.rb +58 -0
  97. data/spec/lutaml/model/liquefiable_spec.rb +22 -6
  98. data/spec/lutaml/model/liquid_compatibility_spec.rb +442 -0
  99. data/spec/lutaml/model/ordered_content_spec.rb +5 -5
  100. data/spec/lutaml/model/services/transformer_spec.rb +43 -0
  101. data/spec/lutaml/model/transform_cache_spec.rb +62 -0
  102. data/spec/lutaml/model/transform_dynamic_attributes_spec.rb +41 -0
  103. data/spec/lutaml/model/uninitialized_class_deep_dup_spec.rb +39 -0
  104. data/spec/lutaml/model/uninitialized_class_spec.rb +14 -2
  105. data/spec/lutaml/model/validation/concerns/has_issues_spec.rb +76 -0
  106. data/spec/lutaml/model/validation/context_spec.rb +60 -0
  107. data/spec/lutaml/model/validation/issue_spec.rb +77 -0
  108. data/spec/lutaml/model/validation/layer_result_spec.rb +66 -0
  109. data/spec/lutaml/model/validation/profile_spec.rb +134 -0
  110. data/spec/lutaml/model/validation/registry_spec.rb +94 -0
  111. data/spec/lutaml/model/validation/remediation_result_spec.rb +23 -0
  112. data/spec/lutaml/model/validation/remediation_spec.rb +72 -0
  113. data/spec/lutaml/model/validation/report_spec.rb +58 -0
  114. data/spec/lutaml/model/validation/rule_spec.rb +134 -0
  115. data/spec/lutaml/model/validation/uninitialized_class_validate_spec.rb +29 -0
  116. data/spec/lutaml/model/validation/validation_error_spec.rb +29 -0
  117. data/spec/lutaml/model/validation/validation_framework_spec.rb +110 -0
  118. data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
  119. data/spec/lutaml/rdf/iri_spec.rb +73 -0
  120. data/spec/lutaml/rdf/literal_spec.rb +98 -0
  121. data/spec/lutaml/rdf/mapping_spec.rb +164 -0
  122. data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
  123. data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
  124. data/spec/lutaml/rdf/namespace_spec.rb +241 -0
  125. data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
  126. data/spec/lutaml/turtle/adapter_spec.rb +47 -0
  127. data/spec/lutaml/turtle/mapping_spec.rb +123 -0
  128. data/spec/lutaml/turtle/transform_spec.rb +273 -0
  129. data/spec/lutaml/xml/content_model_validation_spec.rb +157 -0
  130. data/spec/lutaml/xml/mapping_spec.rb +12 -7
  131. metadata +95 -7
  132. data/spec/fixtures/liquid_templates/_ceramics.liquid +0 -3
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ class Mapping < Lutaml::Model::Mapping
6
+ attr_reader :namespace_set, :rdf_subject, :rdf_type, :rdf_predicates,
7
+ :rdf_members
8
+
9
+ def initialize
10
+ super
11
+ @namespace_set = Lutaml::Rdf::NamespaceSet.new
12
+ @rdf_subject = nil
13
+ @rdf_type = nil
14
+ @rdf_predicates = []
15
+ @rdf_members = []
16
+ end
17
+
18
+ def namespace(*namespace_classes)
19
+ @namespace_set = Lutaml::Rdf::NamespaceSet.new(*namespace_classes)
20
+ end
21
+
22
+ def subject(&)
23
+ @rdf_subject = Proc.new(&) if block_given?
24
+ end
25
+
26
+ def type(value)
27
+ @rdf_type = value
28
+ end
29
+
30
+ def predicate(name, namespace:, to:, lang_tagged: false)
31
+ @rdf_predicates << Lutaml::Rdf::MappingRule.new(
32
+ name,
33
+ namespace: namespace,
34
+ to: to,
35
+ lang_tagged: lang_tagged,
36
+ )
37
+ end
38
+
39
+ def members(attr_name)
40
+ @rdf_members << Lutaml::Rdf::MemberRule.new(attr_name)
41
+ end
42
+
43
+ def mappings(_register_id = nil)
44
+ @rdf_predicates
45
+ end
46
+
47
+ def finalize(_mapper_class); end
48
+
49
+ def finalized?
50
+ true
51
+ end
52
+
53
+ def map_element(name, to:)
54
+ raise Lutaml::Model::IncorrectMappingArgumentsError,
55
+ "RDF mappings use `predicate` instead of `map_element`. " \
56
+ "Use `predicate :#{name}, namespace: MyNs, to: :#{to}` inside `rdf do`."
57
+ end
58
+
59
+ def deep_dup
60
+ self.class.new.tap do |new_mapping|
61
+ new_mapping.instance_variable_set(:@namespace_set, @namespace_set)
62
+ new_mapping.instance_variable_set(:@rdf_subject, @rdf_subject)
63
+ new_mapping.instance_variable_set(:@rdf_type, @rdf_type)
64
+ new_mapping.instance_variable_set(:@rdf_predicates,
65
+ @rdf_predicates.dup)
66
+ new_mapping.instance_variable_set(:@rdf_members, @rdf_members.dup)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ class MappingRule
6
+ attr_reader :predicate_name, :namespace, :to, :lang_tagged
7
+
8
+ def initialize(predicate_name, namespace:, to:, lang_tagged: false)
9
+ validate!(predicate_name, namespace, to)
10
+ @predicate_name = predicate_name.to_s.freeze
11
+ @namespace = namespace
12
+ @to = to
13
+ @lang_tagged = lang_tagged
14
+ end
15
+
16
+ def uri
17
+ @namespace[@predicate_name]
18
+ end
19
+
20
+ def prefixed_name
21
+ @namespace.prefixed(@predicate_name)
22
+ end
23
+
24
+ private
25
+
26
+ def validate!(name, namespace_class, target)
27
+ raise ArgumentError, "predicate_name is required" unless name
28
+ unless namespace_class.is_a?(Class) && namespace_class < Lutaml::Rdf::Namespace
29
+ raise ArgumentError, "namespace must be a Rdf::Namespace subclass"
30
+ end
31
+ raise ArgumentError, ":to is required" unless target
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ class MemberRule
6
+ attr_reader :attr_name
7
+
8
+ def initialize(attr_name)
9
+ @attr_name = attr_name.to_sym
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ class Namespace
6
+ class << self
7
+ def uri(value = nil)
8
+ return @uri if value.nil?
9
+ if @uri
10
+ raise FrozenError,
11
+ "#{name}.uri is already set to #{@uri.inspect}"
12
+ end
13
+
14
+ @uri = value.freeze
15
+ end
16
+
17
+ def prefix(value = nil)
18
+ return @prefix if value.nil?
19
+ if @prefix
20
+ raise FrozenError,
21
+ "#{name}.prefix is already set to #{@prefix.inspect}"
22
+ end
23
+
24
+ @prefix = value.to_s.freeze
25
+ end
26
+
27
+ def [](local_name)
28
+ "#{uri}#{local_name}"
29
+ end
30
+
31
+ def prefixed(local_name)
32
+ "#{prefix}:#{local_name}"
33
+ end
34
+
35
+ def resolve_compact_iri(compact_iri, namespaces)
36
+ return compact_iri unless compact_iri.include?(":")
37
+
38
+ pfx, local = compact_iri.split(":", 2)
39
+ ns = namespaces.find { |n| n.prefix == pfx }
40
+ ns ? ns[local] : compact_iri
41
+ end
42
+
43
+ def ==(other)
44
+ other.is_a?(Class) && other < Namespace &&
45
+ other.uri == uri && other.prefix == prefix
46
+ end
47
+
48
+ def hash
49
+ [uri, prefix].hash
50
+ end
51
+
52
+ def to_s
53
+ "#{name}(prefix: #{prefix.inspect}, uri: #{uri.inspect})"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ class NamespaceSet
6
+ include Enumerable
7
+
8
+ def initialize(*namespace_classes)
9
+ @by_prefix = {}
10
+ namespace_classes.each { |ns| add(ns) }
11
+ end
12
+
13
+ def add(namespace_class)
14
+ pfx = namespace_class.prefix
15
+ if @by_prefix.key?(pfx) && @by_prefix[pfx] != namespace_class
16
+ raise ArgumentError,
17
+ "Prefix '#{pfx}' conflicts: #{@by_prefix[pfx].name} vs #{namespace_class.name}"
18
+ end
19
+ @by_prefix[pfx] = namespace_class
20
+ self
21
+ end
22
+
23
+ def [](prefix)
24
+ @by_prefix[prefix]
25
+ end
26
+
27
+ def resolve_compact_iri(compact_iri)
28
+ Namespace.resolve_compact_iri(compact_iri, to_a)
29
+ end
30
+
31
+ def compact(full_uri)
32
+ each do |ns|
33
+ next unless full_uri.start_with?(ns.uri)
34
+
35
+ local = full_uri.delete_prefix(ns.uri)
36
+ return ns.prefixed(local)
37
+ end
38
+ nil
39
+ end
40
+
41
+ def each(&)
42
+ @by_prefix.each_value(&)
43
+ end
44
+
45
+ def size
46
+ @by_prefix.size
47
+ end
48
+
49
+ def empty?
50
+ @by_prefix.empty?
51
+ end
52
+
53
+ def to_a
54
+ @by_prefix.values
55
+ end
56
+
57
+ def to_hash
58
+ @by_prefix.transform_values(&:uri)
59
+ end
60
+
61
+ def merge(other_set)
62
+ return self if equal?(other_set)
63
+
64
+ other_set.each { |ns| add(ns) }
65
+ self
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ module Namespaces
6
+ class DctermsNamespace < Lutaml::Rdf::Namespace
7
+ uri "http://purl.org/dc/terms/"
8
+ prefix "dcterms"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ module Namespaces
6
+ class OwlNamespace < Lutaml::Rdf::Namespace
7
+ uri "http://www.w3.org/2002/07/owl#"
8
+ prefix "owl"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ module Namespaces
6
+ class RdfSyntaxNamespace < Lutaml::Rdf::Namespace
7
+ uri "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8
+ prefix "rdf"
9
+ end
10
+
11
+ RdfNamespace = RdfSyntaxNamespace
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ module Namespaces
6
+ class RdfsNamespace < Lutaml::Rdf::Namespace
7
+ uri "http://www.w3.org/2000/01/rdf-schema#"
8
+ prefix "rdfs"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ module Namespaces
6
+ class SkosNamespace < Lutaml::Rdf::Namespace
7
+ uri "http://www.w3.org/2004/02/skos/core#"
8
+ prefix "skos"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ module Namespaces
6
+ class XsdNamespace < Lutaml::Rdf::Namespace
7
+ uri "http://www.w3.org/2001/XMLSchema#"
8
+ prefix "xsd"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ module Namespaces
6
+ autoload :SkosNamespace, "#{__dir__}/namespaces/skos_namespace"
7
+ autoload :DctermsNamespace, "#{__dir__}/namespaces/dcterms_namespace"
8
+ autoload :RdfNamespace, "#{__dir__}/namespaces/rdf_namespace"
9
+ autoload :RdfsNamespace, "#{__dir__}/namespaces/rdfs_namespace"
10
+ autoload :OwlNamespace, "#{__dir__}/namespaces/owl_namespace"
11
+ autoload :XsdNamespace, "#{__dir__}/namespaces/xsd_namespace"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ class Transform < Lutaml::Model::Transform
6
+ protected
7
+
8
+ def resolve_subject_uri(mapping, instance)
9
+ mapping.rdf_subject&.call(instance)
10
+ end
11
+
12
+ def resolve_type_uri(mapping)
13
+ return unless mapping.rdf_type
14
+
15
+ mapping.namespace_set.resolve_compact_iri(mapping.rdf_type)
16
+ end
17
+
18
+ def resolve_type_compact(mapping)
19
+ mapping.rdf_type
20
+ end
21
+
22
+ def build_instance(attrs, options)
23
+ child_register = Lutaml::Model::Register.resolve_for_child(
24
+ model_class, lutaml_register
25
+ )
26
+ instance = model_class.new(attrs.merge(lutaml_register: child_register))
27
+ root_and_parent_assignment(instance, options)
28
+ instance
29
+ end
30
+
31
+ def extract_language(value)
32
+ value.language_tag if value.is_a?(Lutaml::Rdf::LanguageTagged)
33
+ end
34
+ end
35
+ end
36
+ end
data/lib/lutaml/rdf.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model"
4
+
5
+ module Lutaml
6
+ module Rdf
7
+ autoload :Error, "#{__dir__}/rdf/error"
8
+ autoload :Iri, "#{__dir__}/rdf/iri"
9
+ autoload :LanguageTagged, "#{__dir__}/rdf/language_tagged"
10
+ autoload :Literal, "#{__dir__}/rdf/literal"
11
+ autoload :Namespace, "#{__dir__}/rdf/namespace"
12
+ autoload :NamespaceSet, "#{__dir__}/rdf/namespace_set"
13
+ autoload :Mapping, "#{__dir__}/rdf/mapping"
14
+ autoload :MappingRule, "#{__dir__}/rdf/mapping_rule"
15
+ autoload :MemberRule, "#{__dir__}/rdf/member_rule"
16
+ autoload :Namespaces, "#{__dir__}/rdf/namespaces"
17
+ autoload :Transform, "#{__dir__}/rdf/transform"
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Turtle
5
+ class Adapter
6
+ attr_reader :data, :register
7
+
8
+ def initialize(data, register: nil)
9
+ @data = data
10
+ @register = register || Lutaml::Model::Config.default_register
11
+ end
12
+
13
+ def self.parse(turtle_string, _options = {})
14
+ require "rdf/turtle"
15
+ graph = RDF::Graph.new
16
+ RDF::Turtle::Reader.new(turtle_string).each_statement do |stmt|
17
+ graph << stmt
18
+ end
19
+ graph
20
+ end
21
+
22
+ def to_turtle(_options = {})
23
+ require "rdf/turtle"
24
+ case data
25
+ when String
26
+ data
27
+ when RDF::Enumerable
28
+ RDF::Turtle::Writer.buffer { |w| data.each_statement { |s| w << s } }
29
+ else
30
+ data.to_s
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Turtle
5
+ class Mapping < Lutaml::Rdf::Mapping; end
6
+ end
7
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Turtle
5
+ class MissingSubjectError < Lutaml::Rdf::Error; end
6
+
7
+ class Transform < Lutaml::Rdf::Transform
8
+ def model_to_data(instance, _format, options = {})
9
+ require "rdf/turtle"
10
+ mapping = extract_turtle_mapping(options)
11
+ return "" unless mapping
12
+
13
+ if !mapping.rdf_subject && mapping.rdf_predicates.any? && mapping.rdf_members.empty?
14
+ raise MissingSubjectError,
15
+ "Turtle mapping requires a subject block"
16
+ end
17
+
18
+ graph = build_graph(mapping, instance)
19
+ return "" if graph.empty?
20
+
21
+ prefixes = build_prefixes(mapping, instance)
22
+ RDF::Turtle::Writer.buffer(prefixes: prefixes) do |writer|
23
+ graph.each_statement { |stmt| writer << stmt }
24
+ end.strip
25
+ end
26
+
27
+ def data_to_model(data, _format, options = {})
28
+ require "rdf/turtle"
29
+ mapping = extract_turtle_mapping(options)
30
+ unless mapping&.rdf_subject
31
+ raise MissingSubjectError,
32
+ "Turtle mapping requires a subject block"
33
+ end
34
+
35
+ graph = data.is_a?(RDF::Graph) ? data : Lutaml::Turtle::Adapter.parse(data)
36
+ attrs = extract_attributes(graph, mapping)
37
+ build_instance(attrs, options)
38
+ end
39
+
40
+ private
41
+
42
+ def extract_turtle_mapping(options)
43
+ options[:mappings] || mappings_for(:turtle, lutaml_register)
44
+ end
45
+
46
+ def build_graph(mapping, instance)
47
+ graph = RDF::Graph.new
48
+
49
+ has_predicates_or_type = mapping.rdf_type || mapping.rdf_predicates.any?
50
+
51
+ if has_predicates_or_type
52
+ subject_uri = if mapping.rdf_subject
53
+ RDF::URI(resolve_subject_uri(mapping, instance))
54
+ else
55
+ RDF::Node.new
56
+ end
57
+
58
+ if mapping.rdf_type
59
+ type_uri = RDF::URI(resolve_type_uri(mapping))
60
+ graph << RDF::Statement.new(subject_uri, RDF.type, type_uri)
61
+ end
62
+
63
+ mapping.rdf_predicates.each do |rule|
64
+ value = instance.public_send(rule.to)
65
+ next if value.nil?
66
+
67
+ Array(value).each do |v|
68
+ object = build_rdf_object(v, rule)
69
+ graph << RDF::Statement.new(subject_uri, RDF::URI(rule.uri),
70
+ object)
71
+ end
72
+ end
73
+ end
74
+
75
+ mapping.rdf_members.each do |member_rule|
76
+ collection = Array(instance.public_send(member_rule.attr_name))
77
+ collection.each do |member|
78
+ member_mapping = member.class.mappings[:turtle]
79
+ next unless member_mapping
80
+
81
+ graph << build_graph(member_mapping, member)
82
+ end
83
+ end
84
+
85
+ graph
86
+ end
87
+
88
+ def build_rdf_object(value, rule)
89
+ if rule.lang_tagged
90
+ lang = extract_language(value)
91
+ RDF::Literal.new(value.to_s, language: lang)
92
+ else
93
+ case value
94
+ when Integer then RDF::Literal.new(value, datatype: RDF::XSD.integer)
95
+ when Float then RDF::Literal.new(value, datatype: RDF::XSD.double)
96
+ when TrueClass, FalseClass then RDF::Literal.new(value, datatype: RDF::XSD.boolean)
97
+ else RDF::Literal.new(value.to_s)
98
+ end
99
+ end
100
+ end
101
+
102
+ def build_prefixes(mapping, instance)
103
+ ns_set = mapping.namespace_set
104
+
105
+ mapping.rdf_members.each do |member_rule|
106
+ collection = Array(instance.public_send(member_rule.attr_name))
107
+ next if collection.empty?
108
+
109
+ member_mapping = collection.first.class.mappings[:turtle]
110
+ next unless member_mapping
111
+
112
+ ns_set = ns_set.merge(member_mapping.namespace_set)
113
+ end
114
+
115
+ ns_set.each.with_object({}) do |ns, h|
116
+ h[ns.prefix.to_sym] = ns.uri if ns.prefix && ns.uri
117
+ end
118
+ end
119
+
120
+ def extract_attributes(graph, mapping)
121
+ attrs = {}
122
+ type_uri = resolve_type_uri(mapping)
123
+
124
+ matching_subjects = find_subjects_by_type(graph, type_uri)
125
+
126
+ matching_subjects.each do |subject|
127
+ mapping.rdf_predicates.each do |rule|
128
+ stmts = graph.query([subject, RDF::URI(rule.uri), nil])
129
+ next if stmts.empty?
130
+
131
+ values = stmts.map { |s| literal_to_ruby(s.object) }
132
+ attrs[rule.to] = values.length == 1 ? values.first : values
133
+ end
134
+ end
135
+
136
+ attrs
137
+ end
138
+
139
+ def find_subjects_by_type(graph, type_uri)
140
+ graph.query([nil, RDF.type, RDF::URI(type_uri)]).map(&:subject).uniq
141
+ end
142
+
143
+ def literal_to_ruby(rdf_object)
144
+ case rdf_object
145
+ when RDF::Literal
146
+ case rdf_object.datatype
147
+ when RDF::XSD.integer then rdf_object.value.to_i
148
+ when RDF::XSD.double, RDF::XSD.decimal, RDF::XSD.float then rdf_object.value.to_f
149
+ when RDF::XSD.boolean then rdf_object.value == "true"
150
+ else rdf_object.value
151
+ end
152
+ else
153
+ rdf_object.to_s
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model"
4
+ require_relative "rdf"
5
+
6
+ module Lutaml
7
+ module Turtle
8
+ autoload :Mapping, "#{__dir__}/turtle/mapping"
9
+ autoload :Transform, "#{__dir__}/turtle/transform"
10
+ autoload :Adapter, "#{__dir__}/turtle/adapter"
11
+ end
12
+ end
13
+
14
+ Lutaml::Model::FormatRegistry.register(
15
+ :turtle,
16
+ mapping_class: Lutaml::Turtle::Mapping,
17
+ adapter_class: Lutaml::Turtle::Adapter,
18
+ transformer: Lutaml::Turtle::Transform,
19
+ key_value: false,
20
+ rdf: true,
21
+ error_types: ["RDF::ReaderError"],
22
+ )
@@ -191,9 +191,13 @@ module Lutaml
191
191
  def order
192
192
  children.filter_map do |child|
193
193
  if child.text?
194
- next if child.text.nil? || child.text.strip.empty?
194
+ next if child.text.nil?
195
195
 
196
196
  Element.new("Text", "text", text_content: child.text)
197
+ elsif child.comment?
198
+ Element.new("Comment", "comment",
199
+ text_content: child.content,
200
+ node_type: :comment)
197
201
  else
198
202
  Element.new("Element", child.unprefixed_name)
199
203
  end
@@ -967,7 +971,7 @@ module Lutaml
967
971
  # Pass THIS element as parent so children can inherit namespaces
968
972
  child_element_index = 0
969
973
  previous_sibling_had_xmlns_blank = false
970
- xml_element.children.each do |xml_child|
974
+ xml_element.children.each do |xml_child| # rubocop:disable Metrics/BlockLength
971
975
  # Entity reference nodes are preserved via the marker
972
976
  # preprocessing approach in NokogiriAdapter.parse.
973
977
  if xml_child.is_a?(Lutaml::Xml::NokogiriElement) &&
@@ -993,6 +997,9 @@ module Lutaml
993
997
  elsif xml_child.is_a?(String)
994
998
  add_content_node(element, xml_child, doc,
995
999
  cdata: xml_element.cdata && !xml_child.strip.empty?)
1000
+ elsif xml_child.is_a?(::Lutaml::Xml::DataModel::XmlComment)
1001
+ comment_node = doc.create_comment(xml_child.content)
1002
+ element.add_child(comment_node)
996
1003
  end
997
1004
  end
998
1005