lutaml-model 0.1.0 → 0.3.0

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.
@@ -12,62 +12,206 @@ module Lutaml
12
12
  new(root)
13
13
  end
14
14
 
15
- def initialize(root)
16
- @root = root
17
- end
18
-
19
- def to_h
20
- { @root.name => parse_element(@root) }
21
- end
22
-
23
15
  def to_xml(options = {})
24
16
  builder = Ox::Builder.new
25
- build_element(builder, @root, options)
26
- xml_data = Ox.dump(builder)
17
+ if @root.is_a?(Lutaml::Model::XmlAdapter::OxElement)
18
+ @root.to_xml(builder)
19
+ elsif @root.ordered?
20
+ build_ordered_element(builder, @root, options)
21
+ else
22
+ build_element(builder, @root, options)
23
+ end
24
+
25
+ # xml_data = Ox.dump(builder)
26
+ xml_data = builder.to_s
27
27
  options[:declaration] ? declaration(options) + xml_data : xml_data
28
28
  end
29
29
 
30
30
  private
31
31
 
32
- def build_element(builder, element, options = {})
33
- attributes = build_attributes(element.attributes)
34
- builder.element(element.name, attributes) do
35
- element.children.each do |child|
36
- build_element(builder, child, options)
32
+ def build_unordered_element(builder, element, options = {})
33
+ xml_mapping = element.class.mappings_for(:xml)
34
+ return xml unless xml_mapping
35
+
36
+ attributes = build_attributes(element, xml_mapping).compact
37
+
38
+ prefixed_name = if options.key?(:namespace_prefix)
39
+ [options[:namespace_prefix], xml_mapping.root_element].compact.join(":")
40
+ elsif xml_mapping.namespace_prefix
41
+ "#{xml_mapping.namespace_prefix}:#{xml_mapping.root_element}"
42
+ else
43
+ xml_mapping.root_element
44
+ end
45
+
46
+ builder.element(prefixed_name, attributes) do |el|
47
+ xml_mapping.elements.each do |element_rule|
48
+ attribute_def = attribute_definition_for(element, element_rule)
49
+ value = attribute_value_for(element, element_rule)
50
+
51
+ val = if attribute_def.collection?
52
+ value
53
+ elsif value || element_rule.render_nil?
54
+ [value]
55
+ else
56
+ []
57
+ end
58
+
59
+ val.each do |v|
60
+ if attribute_def&.type&.<= Lutaml::Model::Serialize
61
+ handle_nested_elements(el, v, element_rule)
62
+ else
63
+ builder.element(element_rule.prefixed_name) do |el|
64
+ el.text(attribute_def.type.serialize(v)) if v
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ if xml_mapping.content_mapping
71
+ text = element.send(xml_mapping.content_mapping.to)
72
+ text = text.join if text.is_a?(Array)
73
+
74
+ el.text text
37
75
  end
38
- builder.text(element.text) if element.text
39
76
  end
40
77
  end
41
78
 
42
- def build_attributes(attributes)
43
- attributes.each_with_object({}) do |(name, attr), hash|
44
- hash[name] = attr.value
79
+ def build_ordered_element(builder, element, _options = {})
80
+ xml_mapping = element.class.mappings_for(:xml)
81
+ return xml unless xml_mapping
82
+
83
+ attributes = build_attributes(element, xml_mapping).compact
84
+
85
+ builder.element(xml_mapping.root_element, attributes) do |el|
86
+ index_hash = {}
87
+
88
+ element.element_order.each do |name|
89
+ index_hash[name] ||= -1
90
+ curr_index = index_hash[name] += 1
91
+
92
+ element_rule = xml_mapping.find_by_name(name)
93
+
94
+ attribute_def = attribute_definition_for(element, element_rule)
95
+ value = attribute_value_for(element, element_rule)
96
+
97
+ if element_rule == xml_mapping.content_mapping
98
+ text = element.send(xml_mapping.content_mapping.to)
99
+ text = text[curr_index] if text.is_a?(Array)
100
+
101
+ el.text text
102
+ elsif attribute_def.collection?
103
+ add_to_xml(el, value[curr_index], attribute_def, element_rule)
104
+ elsif !value.nil? || element_rule.render_nil?
105
+ add_to_xml(el, value, attribute_def, element_rule)
106
+ end
107
+ end
45
108
  end
46
109
  end
47
110
 
48
- def parse_element(element)
49
- result = { "_text" => element.text }
50
- element.nodes.each do |child|
51
- next if child.is_a?(Ox::Raw) || child.is_a?(Ox::Comment)
52
- result[child.name] ||= []
53
- result[child.name] << parse_element(child)
111
+ def add_to_xml(xml, value, attribute, rule)
112
+ if value && (attribute&.type&.<= Lutaml::Model::Serialize)
113
+ handle_nested_elements(xml, value, rule)
114
+ else
115
+ xml.element(rule.name) do |el|
116
+ if !value.nil?
117
+ serialized_value = attribute.type.serialize(value)
118
+
119
+ if attribute.type == Lutaml::Model::Type::Hash
120
+ serialized_value.each do |key, val|
121
+ el.element(key) { |child_el| child_el.text val }
122
+ end
123
+ else
124
+ el.text(serialized_value)
125
+ end
126
+ end
127
+ end
54
128
  end
55
- result
56
129
  end
57
130
  end
58
131
 
59
132
  class OxElement < Element
60
- def initialize(node)
61
- attributes = node.attributes.each_with_object({}) do |(name, value), hash|
62
- hash[name.to_s] = Attribute.new(name.to_s, value)
133
+ def initialize(node, root_node: nil)
134
+ if node.is_a?(String)
135
+ super("text", {}, [], node, parent_document: root_node)
136
+ else
137
+ namespace_attributes(node.attributes).each do |(name, value)|
138
+ if root_node
139
+ root_node.add_namespace(Lutaml::Model::XmlNamespace.new(value, name))
140
+ else
141
+ add_namespace(Lutaml::Model::XmlNamespace.new(value, name))
142
+ end
143
+ end
144
+
145
+ attributes = node.attributes.each_with_object({}) do |(name, value), hash|
146
+ next if attribute_is_namespace?(name)
147
+
148
+ namespace_prefix = name.to_s.split(":").first
149
+ if (n = name.to_s.split(":")).length > 1
150
+ namespace = (root_node || self).namespaces[namespace_prefix]&.uri
151
+ namespace ||= Lutaml::Model::XmlAdapter::XML_NAMESPACE_URI
152
+ prefix = n.first
153
+ end
154
+
155
+ hash[name.to_s] = Attribute.new(
156
+ name.to_s,
157
+ value,
158
+ namespace: namespace,
159
+ namespace_prefix: prefix,
160
+ )
161
+ end
162
+
163
+ super(
164
+ node.name.to_s,
165
+ attributes,
166
+ parse_children(node, root_node: root_node || self),
167
+ node.text,
168
+ parent_document: root_node
169
+ )
63
170
  end
64
- super(node.name.to_s, attributes, parse_children(node), node.text)
171
+ end
172
+
173
+ def to_xml(builder = nil)
174
+ builder ||= Ox::Builder.new
175
+ attrs = build_attributes(self)
176
+
177
+ if text?
178
+ builder.text(text)
179
+ else
180
+ builder.element(name, attrs) do |el|
181
+ children.each { |child| child.to_xml(el) }
182
+ end
183
+ end
184
+ end
185
+
186
+ def namespace_attributes(attributes)
187
+ attributes.select { |attr| attribute_is_namespace?(attr) }
188
+ end
189
+
190
+ def text?
191
+ # false
192
+ children.empty? && text&.length&.positive?
193
+ end
194
+
195
+ def build_attributes(node)
196
+ attrs = node.attributes.transform_values(&:value)
197
+
198
+ node.own_namespaces.each_value do |namespace|
199
+ attrs[namespace.attr_name] = namespace.uri
200
+ end
201
+
202
+ attrs
203
+ end
204
+
205
+ def nodes
206
+ children
65
207
  end
66
208
 
67
209
  private
68
210
 
69
- def parse_children(node)
70
- node.nodes.select { |child| child.is_a?(Ox::Element) }.map { |child| OxElement.new(child) }
211
+ def parse_children(node, root_node: nil)
212
+ node.nodes.map do |child|
213
+ OxElement.new(child, root_node: root_node)
214
+ end
71
215
  end
72
216
  end
73
217
  end
@@ -1,8 +1,13 @@
1
1
  # lib/lutaml/model/xml_adapter.rb
2
2
 
3
+ require_relative "xml_namespace"
4
+ require_relative "mapping_hash"
5
+
3
6
  module Lutaml
4
7
  module Model
5
8
  module XmlAdapter
9
+ XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace".freeze
10
+
6
11
  class Document
7
12
  attr_reader :root
8
13
 
@@ -19,30 +24,230 @@ module Lutaml
19
24
  end
20
25
 
21
26
  def declaration(options)
22
- version = options[:declaration].is_a?(String) ? options[:declaration] : "1.0"
23
- encoding = options[:encoding].is_a?(String) ? options[:encoding] : (options[:encoding] ? "UTF-8" : nil)
27
+ version = "1.0"
28
+ version = options[:declaration] if options[:declaration].is_a?(String)
29
+
30
+ encoding = options[:encoding] ? "UTF-8" : nil
31
+ encoding = options[:encoding] if options[:encoding].is_a?(String)
32
+
24
33
  declaration = "<?xml version=\"#{version}\""
25
34
  declaration += " encoding=\"#{encoding}\"" if encoding
26
35
  declaration += "?>\n"
27
36
  declaration
28
37
  end
38
+
39
+ def to_h
40
+ parse_element(@root)
41
+ end
42
+
43
+ def order
44
+ @root.order
45
+ end
46
+
47
+ def handle_nested_elements(builder, value, rule = nil)
48
+ options = {}
49
+
50
+ if rule&.namespace_set?
51
+ options[:namespace_prefix] = rule.prefix
52
+ end
53
+
54
+ case value
55
+ when Array
56
+ value.each { |val| build_element(builder, val, options) }
57
+ else
58
+ build_element(builder, value, options)
59
+ end
60
+ end
61
+
62
+ def parse_element(element)
63
+ result = Lutaml::Model::MappingHash.new
64
+ result.item_order = element.order
65
+
66
+ element.children.each_with_object(result) do |child, hash|
67
+ value = child.text? ? child.text : parse_element(child)
68
+
69
+ if hash[child.unprefixed_name]
70
+ hash[child.unprefixed_name] =
71
+ [hash[child.unprefixed_name], value].flatten
72
+ else
73
+ hash[child.unprefixed_name] = value
74
+ end
75
+ end
76
+
77
+ element.attributes.each_value do |attr|
78
+ result[attr.unprefixed_name] = attr.value
79
+ end
80
+
81
+ result
82
+ end
83
+
84
+ def build_element(xml, element, _options = {})
85
+ if element.ordered?
86
+ build_ordered_element(xml, element, _options)
87
+ else
88
+ build_unordered_element(xml, element, _options)
89
+ end
90
+ end
91
+
92
+ def build_namespace_attributes(klass, processed = {})
93
+ xml_mappings = klass.mappings_for(:xml)
94
+ attributes = klass.attributes
95
+
96
+ attrs = {}
97
+
98
+ if xml_mappings.namespace_prefix
99
+ attrs["xmlns:#{xml_mappings.namespace_prefix}"] =
100
+ xml_mappings.namespace_uri
101
+ end
102
+
103
+ xml_mappings.mappings.each do |mapping_rule|
104
+ processed[klass] ||= {}
105
+
106
+ next if processed[klass][mapping_rule.name]
107
+
108
+ processed[klass][mapping_rule.name] = true
109
+
110
+ type = if mapping_rule.delegate
111
+ attributes[mapping_rule.delegate].type.attributes[mapping_rule.to].type
112
+ else
113
+ attributes[mapping_rule.to].type
114
+ end
115
+
116
+ if type <= Lutaml::Model::Serialize
117
+ attrs = attrs.merge(build_namespace_attributes(type, processed))
118
+ end
119
+
120
+ if mapping_rule.namespace
121
+ attrs["xmlns:#{mapping_rule.prefix}"] = mapping_rule.namespace
122
+ end
123
+ end
124
+
125
+ attrs
126
+ end
127
+
128
+ def build_attributes(element, xml_mapping)
129
+ attrs = namespace_attributes(xml_mapping)
130
+
131
+ xml_mapping.attributes.each_with_object(attrs) do |mapping_rule, hash|
132
+ if mapping_rule.namespace
133
+ hash["xmlns:#{mapping_rule.prefix}"] = mapping_rule.namespace
134
+ end
135
+
136
+ hash[mapping_rule.prefixed_name] = element.send(mapping_rule.to)
137
+ end
138
+
139
+ xml_mapping.elements.each_with_object(attrs) do |mapping_rule, hash|
140
+ if mapping_rule.namespace
141
+ hash["xmlns:#{mapping_rule.prefix}"] = mapping_rule.namespace
142
+ end
143
+ end
144
+ end
145
+
146
+ def attribute_definition_for(element, rule)
147
+ return element.class.attributes[rule.to] unless rule.delegate
148
+
149
+ element.send(rule.delegate).class.attributes[rule.to]
150
+ end
151
+
152
+ def attribute_value_for(element, rule)
153
+ return element.send(rule.to) unless rule.delegate
154
+
155
+ element.send(rule.delegate).send(rule.to)
156
+ end
157
+
158
+ def namespace_attributes(xml_mapping)
159
+ return {} unless xml_mapping.namespace_uri
160
+
161
+ key = ["xmlns", xml_mapping.namespace_prefix].compact.join(":")
162
+ { key => xml_mapping.namespace_uri }
163
+ end
29
164
  end
30
165
 
31
166
  class Element
32
- attr_reader :name, :attributes, :children, :text, :namespace, :namespace_prefix
167
+ attr_reader :attributes,
168
+ :children,
169
+ :text,
170
+ :namespace_prefix,
171
+ :parent_document
33
172
 
34
- def initialize(name, attributes = {}, children = [], text = nil, namespace: nil, namespace_prefix: nil)
35
- @name = name
36
- @attributes = attributes.map { |k, v| Attribute.new(k, v) }
173
+ def initialize(
174
+ name,
175
+ attributes = {},
176
+ children = [],
177
+ text = nil,
178
+ parent_document: nil,
179
+ namespace_prefix: nil
180
+ )
181
+ @name = extract_name(name)
182
+ @namespace_prefix = namespace_prefix || extract_namespace_prefix(name)
183
+ @attributes = attributes # .map { |k, v| Attribute.new(k, v) }
37
184
  @children = children
38
185
  @text = text
39
- @namespace = namespace
40
- @namespace_prefix = namespace_prefix
186
+ @parent_document = parent_document
187
+ end
188
+
189
+ def name
190
+ if namespace_prefix
191
+ "#{namespace_prefix}:#{@name}"
192
+ else
193
+ @name
194
+ end
195
+ end
196
+
197
+ def unprefixed_name
198
+ @name
41
199
  end
42
200
 
43
201
  def document
44
202
  Document.new(self)
45
203
  end
204
+
205
+ def namespaces
206
+ @namespaces || @parent_document&.namespaces || {}
207
+ end
208
+
209
+ def own_namespaces
210
+ @namespaces || {}
211
+ end
212
+
213
+ def namespace
214
+ return default_namespace unless namespace_prefix
215
+
216
+ namespaces[namespace_prefix]
217
+ end
218
+
219
+ def attribute_is_namespace?(name)
220
+ name.to_s.start_with?("xmlns")
221
+ end
222
+
223
+ def add_namespace(namespace)
224
+ @namespaces ||= {}
225
+ @namespaces[namespace.prefix] = namespace
226
+ end
227
+
228
+ def default_namespace
229
+ namespaces[nil] || @parent_document&.namespaces&.dig(nil)
230
+ end
231
+
232
+ def extract_name(name)
233
+ n = name.to_s.split(":")
234
+ return name if n.length <= 1
235
+
236
+ n[1..].join(":")
237
+ end
238
+
239
+ def extract_namespace_prefix(name)
240
+ n = name.to_s.split(":")
241
+ return if n.length <= 1
242
+
243
+ n.first
244
+ end
245
+
246
+ def order
247
+ children.each_with_object([]) do |child, arr|
248
+ arr << child.unprefixed_name
249
+ end
250
+ end
46
251
  end
47
252
 
48
253
  class Attribute
@@ -54,6 +259,14 @@ module Lutaml
54
259
  @namespace = namespace
55
260
  @namespace_prefix = namespace_prefix
56
261
  end
262
+
263
+ def unprefixed_name
264
+ if namespace_prefix
265
+ name.split(":").last
266
+ else
267
+ name
268
+ end
269
+ end
57
270
  end
58
271
  end
59
272
  end
@@ -4,16 +4,31 @@ require_relative "xml_mapping_rule"
4
4
  module Lutaml
5
5
  module Model
6
6
  class XmlMapping
7
- attr_reader :root_element, :namespace_uri, :namespace_prefix
7
+ attr_reader :root_element,
8
+ :namespace_uri,
9
+ :namespace_prefix,
10
+ :mixed_content
8
11
 
9
12
  def initialize
10
13
  @elements = []
11
14
  @attributes = []
12
15
  @content_mapping = nil
16
+ @mixed_content = false
13
17
  end
14
18
 
15
- def root(name)
19
+ alias mixed_content? mixed_content
20
+
21
+ def root(name, mixed: false)
16
22
  @root_element = name
23
+ @mixed_content = mixed
24
+ end
25
+
26
+ def prefixed_root
27
+ if namespace_uri && namespace_prefix
28
+ "#{namespace_prefix}:#{root_element}"
29
+ else
30
+ root_element
31
+ end
17
32
  end
18
33
 
19
34
  def namespace(uri, prefix = nil)
@@ -21,16 +36,69 @@ module Lutaml
21
36
  @namespace_prefix = prefix
22
37
  end
23
38
 
24
- def map_element(name, to:, render_nil: false, with: {}, delegate: nil, namespace: nil, prefix: nil)
25
- @elements << XmlMappingRule.new(name, to: to, render_nil: render_nil, with: with, delegate: delegate, namespace: namespace, prefix: prefix)
39
+ # rubocop:disable Metrics/ParameterLists
40
+ def map_element(
41
+ name,
42
+ to:,
43
+ render_nil: false,
44
+ with: {},
45
+ delegate: nil,
46
+ namespace: (namespace_set = false
47
+ nil),
48
+ prefix: nil,
49
+ mixed: false
50
+ )
51
+ @elements << XmlMappingRule.new(
52
+ name,
53
+ to: to,
54
+ render_nil: render_nil,
55
+ with: with,
56
+ delegate: delegate,
57
+ namespace: namespace,
58
+ prefix: prefix,
59
+ mixed_content: mixed,
60
+ namespace_set: namespace_set != false,
61
+ )
26
62
  end
27
63
 
28
- def map_attribute(name, to:, render_nil: false, with: {}, delegate: nil, namespace: nil, prefix: nil)
29
- @attributes << XmlMappingRule.new(name, to: to, render_nil: render_nil, with: with, delegate: delegate, namespace: namespace, prefix: prefix)
64
+ def map_attribute(
65
+ name,
66
+ to:,
67
+ render_nil: false,
68
+ with: {},
69
+ delegate: nil,
70
+ namespace: (namespace_set = false
71
+ nil),
72
+ prefix: nil
73
+ )
74
+ @attributes << XmlMappingRule.new(
75
+ name,
76
+ to: to,
77
+ render_nil: render_nil,
78
+ with: with,
79
+ delegate: delegate,
80
+ namespace: namespace,
81
+ prefix: prefix,
82
+ namespace_set: namespace_set != false,
83
+ )
30
84
  end
85
+ # rubocop:enable Metrics/ParameterLists
31
86
 
32
- def map_content(to:, render_nil: false, with: {}, delegate: nil)
33
- @content_mapping = XmlMappingRule.new(nil, to: to, render_nil: render_nil, with: with, delegate: delegate)
87
+ def map_content(
88
+ to:,
89
+ render_nil: false,
90
+ with: {},
91
+ delegate: nil,
92
+ mixed: false
93
+ )
94
+ @content_mapping = XmlMappingRule.new(
95
+ nil,
96
+ to: to,
97
+ render_nil: render_nil,
98
+ with: with,
99
+ delegate: delegate,
100
+ mixed_content: mixed,
101
+ )
34
102
  end
35
103
 
36
104
  def elements
@@ -60,6 +128,16 @@ module Lutaml
60
128
  name == rule.to
61
129
  end
62
130
  end
131
+
132
+ def find_by_name(name)
133
+ if name.to_s == "text"
134
+ content_mapping
135
+ else
136
+ mappings.detect do |rule|
137
+ rule.name == name.to_s || rule.name == name.to_sym
138
+ end
139
+ end
140
+ end
63
141
  end
64
142
  end
65
143
  end
@@ -6,9 +6,33 @@ module Lutaml
6
6
  class XmlMappingRule < MappingRule
7
7
  attr_reader :namespace, :prefix
8
8
 
9
- def initialize(name, to:, render_nil: false, with: {}, delegate: nil, namespace: nil, prefix: nil)
10
- super(name, to: to, render_nil: render_nil, with: with, delegate: delegate)
11
- @namespace = namespace
9
+ def initialize(
10
+ name,
11
+ to:,
12
+ render_nil: false,
13
+ with: {},
14
+ delegate: nil,
15
+ namespace: nil,
16
+ prefix: nil,
17
+ mixed_content: false,
18
+ namespace_set: false
19
+ )
20
+ super(
21
+ name,
22
+ to: to,
23
+ render_nil: render_nil,
24
+ with: with,
25
+ delegate: delegate,
26
+ mixed_content: mixed_content,
27
+ namespace_set: namespace_set
28
+ )
29
+
30
+ @namespace = if namespace.to_s == "inherit"
31
+ # we are using inherit_namespace in xml builder by
32
+ # default so no need to do anything here.
33
+ else
34
+ namespace
35
+ end
12
36
  @prefix = prefix
13
37
  end
14
38
  end