lutaml-model 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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