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.
@@ -9,8 +9,6 @@ require "json"
9
9
  module Lutaml
10
10
  module Model
11
11
  module Type
12
- class Boolean; end
13
-
14
12
  %w(String
15
13
  Integer
16
14
  Float
@@ -28,20 +26,29 @@ module Lutaml
28
26
  IPAddress
29
27
  JSON
30
28
  Enum).each do |t|
31
- class_eval <<~HEREDOC
32
- class #{t}
33
- def self.cast(value)
34
- Type.cast(value, #{t})
35
- end
36
- end
37
-
38
- HEREDOC
29
+ class_eval <<~HEREDOC, __FILE__, __LINE__ + 1
30
+ class #{t} # class Integer
31
+ def self.cast(value) # def self.cast(value)
32
+ return if value.nil? # return if value.nil?
33
+
34
+ Type.cast(value, #{t}) # Type.cast(value, Integer)
35
+ end # end
36
+
37
+ def self.serialize(value) # def self.serialize(value)
38
+ return if value.nil? # return if value.nil?
39
+
40
+ Type.serialize(value, #{t}) # Type.serialize(value, Integer)
41
+ end # end
42
+ end # end
43
+ HEREDOC
39
44
  end
40
45
 
41
46
  class TimeWithoutDate
42
47
  def self.cast(value)
43
- parsed_time = ::Time.parse(value.to_s)
44
- parsed_time.strftime("%H:%M:%S")
48
+ return if value.nil?
49
+
50
+ ::Time.parse(value.to_s)
51
+ # .strftime("%H:%M:%S")
45
52
  end
46
53
 
47
54
  def self.serialize(value)
@@ -51,64 +58,162 @@ module Lutaml
51
58
 
52
59
  class DateTime
53
60
  def self.cast(value)
54
- ::DateTime.parse(value.to_s).new_offset(0).iso8601
61
+ return if value.nil?
62
+
63
+ ::DateTime.parse(value.to_s).new_offset(0)
64
+ end
65
+
66
+ def self.serialize(value)
67
+ value.iso8601
68
+ end
69
+ end
70
+
71
+ class Array
72
+ def initialize(array)
73
+ Array(array)
55
74
  end
56
75
  end
57
76
 
77
+ class TextWithTags
78
+ attr_reader :content
79
+
80
+ def initialize(ordered_text_with_tags)
81
+ @content = ordered_text_with_tags
82
+ end
83
+
84
+ def self.cast(value)
85
+ return value if value.is_a?(self)
86
+
87
+ new(value)
88
+ end
89
+
90
+ def self.serialize(value)
91
+ value.content.join
92
+ end
93
+ end
94
+
95
+ class JSON
96
+ attr_reader :value
97
+
98
+ def initialize(value)
99
+ @value = value
100
+ end
101
+
102
+ def to_json(*_args)
103
+ @value.to_json
104
+ end
105
+
106
+ def ==(other)
107
+ @value == if other.is_a?(::Hash)
108
+ other
109
+ else
110
+ other.value
111
+ end
112
+ end
113
+
114
+ def self.cast(value)
115
+ return value if value.is_a?(self) || value.nil?
116
+
117
+ new(::JSON.parse(value))
118
+ end
119
+
120
+ def self.serialize(value)
121
+ value.to_json
122
+ end
123
+ end
124
+
125
+ UUID_REGEX = /\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/
126
+
58
127
  def self.cast(value, type)
59
- case type
60
- when String
128
+ return if value.nil?
129
+
130
+ if [String, Email].include?(type)
61
131
  value.to_s
62
- when Integer
132
+ elsif [Integer, BigInteger].include?(type)
63
133
  value.to_i
64
- when Float
134
+ elsif type == Float
65
135
  value.to_f
66
- when Date
136
+ elsif type == Date
67
137
  begin
68
138
  ::Date.parse(value.to_s)
69
139
  rescue ArgumentError
70
140
  nil
71
141
  end
72
- when DateTime
142
+ elsif type == DateTime
73
143
  DateTime.cast(value)
74
- when Time
144
+ elsif type == Time
75
145
  ::Time.parse(value.to_s)
76
- when TimeWithoutDate
146
+ elsif type == TimeWithoutDate
77
147
  TimeWithoutDate.cast(value)
78
- when Boolean
148
+ elsif type == Boolean
79
149
  to_boolean(value)
80
- when Decimal
150
+ elsif type == Decimal
81
151
  BigDecimal(value.to_s)
82
- when Hash
83
- Hash(value)
84
- when UUID
85
- value =~ /\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/ ? value : SecureRandom.uuid
86
- when Symbol
152
+ elsif type == Hash
153
+ normalize_hash(Hash(value))
154
+ elsif type == UUID
155
+ UUID_REGEX.match?(value) ? value : SecureRandom.uuid
156
+ elsif type == Symbol
87
157
  value.to_sym
88
- when BigInteger
89
- value.to_i
90
- when Binary
158
+ elsif type == Binary
91
159
  value.force_encoding("BINARY")
92
- when URL
160
+ elsif type == URL
93
161
  URI.parse(value.to_s)
94
- when Email
95
- value.to_s
96
- when IPAddress
162
+ elsif type == IPAddress
97
163
  IPAddr.new(value.to_s)
98
- when JSON
99
- ::JSON.parse(value)
100
- when Enum
101
- value
164
+ elsif type == JSON
165
+ JSON.cast(value)
166
+ # elsif type == Enum
167
+ # value
102
168
  else
103
169
  value
104
170
  end
105
171
  end
106
172
 
173
+ def self.serialize(value, type)
174
+ return if value.nil?
175
+
176
+ if type == Date
177
+ value.iso8601
178
+ elsif type == DateTime
179
+ DateTime.serialize(value)
180
+ elsif type == Integer
181
+ value.to_i
182
+ elsif type == Float
183
+ value.to_f
184
+ elsif type == Boolean
185
+ to_boolean(value)
186
+ elsif type == Decimal
187
+ value.to_s("F")
188
+ elsif type == Hash
189
+ Hash(value)
190
+ elsif type == JSON
191
+ value.to_json
192
+ else
193
+ value.to_s
194
+ end
195
+ end
196
+
107
197
  def self.to_boolean(value)
108
198
  return true if value == true || value.to_s =~ (/^(true|t|yes|y|1)$/i)
109
199
  return false if value == false || value.nil? || value.to_s =~ (/^(false|f|no|n|0)$/i)
200
+
110
201
  raise ArgumentError.new("invalid value for Boolean: \"#{value}\"")
111
202
  end
203
+
204
+ def self.normalize_hash(hash)
205
+ return hash["text"] if hash.keys == ["text"]
206
+
207
+ hash.filter_map do |key, value|
208
+ next if key == "text"
209
+
210
+ if value.is_a?(::Hash)
211
+ [key, normalize_hash(value)]
212
+ else
213
+ [key, value]
214
+ end
215
+ end.to_h
216
+ end
112
217
  end
113
218
  end
114
219
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -12,104 +12,229 @@ 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 = Nokogiri::XML::Builder.new do |xml|
25
- build_element(xml, @root, options)
17
+ if root.is_a?(Lutaml::Model::XmlAdapter::NokogiriElement)
18
+ root.to_xml(xml)
19
+ else
20
+ options[:xml_attributes] = build_namespace_attributes(@root.class)
21
+ build_element(xml, @root, options)
22
+ end
26
23
  end
27
24
 
28
- xml_data = builder.to_xml(options[:pretty] ? { indent: 2 } : {})
25
+ xml_options = {}
26
+ xml_options[:indent] = 2 if options[:pretty]
27
+
28
+ xml_data = builder.doc.root.to_xml(xml_options)
29
29
  options[:declaration] ? declaration(options) + xml_data : xml_data
30
30
  end
31
31
 
32
32
  private
33
33
 
34
- def build_element(xml, element, options = {})
34
+ def build_unordered_element(xml, element, options = {})
35
35
  xml_mapping = element.class.mappings_for(:xml)
36
36
  return xml unless xml_mapping
37
37
 
38
- attributes = build_attributes(element, xml_mapping)
38
+ attributes = options[:xml_attributes] ||= {}
39
+ attributes = build_attributes(element,
40
+ xml_mapping).merge(attributes)&.compact
41
+
42
+ prefixed_xml = if options.key?(:namespace_prefix)
43
+ options[:namespace_prefix] ? xml[options[:namespace_prefix]] : xml
44
+ elsif xml_mapping.namespace_prefix
45
+ xml[xml_mapping.namespace_prefix]
46
+ else
47
+ xml
48
+ end
49
+
50
+ prefixed_xml.public_send(xml_mapping.root_element, attributes) do
51
+ if options.key?(:namespace_prefix) && !options[:namespace_prefix]
52
+ xml.parent.namespace = nil
53
+ end
39
54
 
40
- xml.send(xml_mapping.root_element, attributes) do
41
55
  xml_mapping.elements.each do |element_rule|
42
- attribute_def = element.class.attributes[element_rule.to]
43
- value = element.send(element_rule.to)
56
+ attribute_def = attribute_definition_for(element, element_rule)
57
+ value = attribute_value_for(element, element_rule)
44
58
 
45
- if attribute_def&.type <= Lutaml::Model::Serialize
46
- handle_nested_elements(xml, element_rule, value)
47
- else
48
- xml.send(element_rule.name) { xml.text value }
59
+ next if value.nil? && !element_rule.render_nil?
60
+
61
+ nsp_xml = element_rule.prefix ? xml[element_rule.prefix] : xml
62
+
63
+ if attribute_def.collection?
64
+ value.each do |v|
65
+ add_to_xml(nsp_xml, v, attribute_def, element_rule)
66
+ end
67
+ elsif !value.nil? || element_rule.render_nil?
68
+ add_to_xml(nsp_xml, value, attribute_def, element_rule)
49
69
  end
50
70
  end
51
- xml.text element.text unless xml_mapping.elements.any?
71
+
72
+ if xml_mapping.content_mapping
73
+ text = element.send(xml_mapping.content_mapping.to)
74
+ text = text.join if text.is_a?(Array)
75
+
76
+ prefixed_xml.text text
77
+ end
52
78
  end
53
79
  end
54
80
 
55
- def build_attributes(element, xml_mapping)
56
- xml_mapping.attributes.each_with_object(namespace_attributes(xml_mapping)) do |mapping_rule, hash|
57
- full_name = if mapping_rule.namespace
58
- "#{mapping_rule.prefix ? "#{mapping_rule.prefix}:" : ""}#{mapping_rule.name}"
59
- else
60
- mapping_rule.name
81
+ def build_ordered_element(xml, element, options = {})
82
+ xml_mapping = element.class.mappings_for(:xml)
83
+ return xml unless xml_mapping
84
+
85
+ attributes = build_attributes(element, xml_mapping)&.compact
86
+
87
+ prefixed_xml = if options.key?(:namespace_prefix)
88
+ options[:namespace_prefix] ? xml[options[:namespace_prefix]] : xml
89
+ elsif xml_mapping.namespace_prefix
90
+ xml[xml_mapping.namespace_prefix]
91
+ else
92
+ xml
93
+ end
94
+
95
+ prefixed_xml.public_send(xml_mapping.root_element, attributes) do
96
+ if options.key?(:namespace_prefix) && !options[:namespace_prefix]
97
+ xml.parent.namespace = nil
98
+ end
99
+
100
+ index_hash = {}
101
+
102
+ element.element_order.each do |name|
103
+ index_hash[name] ||= -1
104
+ curr_index = index_hash[name] += 1
105
+
106
+ element_rule = xml_mapping.find_by_name(name)
107
+ next if element_rule.nil?
108
+
109
+ attribute_def = attribute_definition_for(element, element_rule)
110
+ value = attribute_value_for(element, element_rule)
111
+ nsp_xml = element_rule.prefix ? xml[element_rule.prefix] : xml
112
+
113
+ if element_rule == xml_mapping.content_mapping
114
+ text = element.send(xml_mapping.content_mapping.to)
115
+ text = text[curr_index] if text.is_a?(Array)
116
+
117
+ prefixed_xml.text text
118
+ elsif attribute_def.collection?
119
+ add_to_xml(nsp_xml, value[curr_index], attribute_def,
120
+ element_rule)
121
+ elsif !value.nil? || element_rule.render_nil?
122
+ add_to_xml(nsp_xml, value, attribute_def, element_rule)
61
123
  end
62
- hash[full_name] = element.send(mapping_rule.to)
124
+ end
63
125
  end
64
126
  end
65
127
 
66
- def namespace_attributes(xml_mapping)
67
- return {} unless xml_mapping.namespace_uri
68
-
69
- if xml_mapping.namespace_prefix
70
- { "xmlns:#{xml_mapping.namespace_prefix}" => xml_mapping.namespace_uri }
128
+ def add_to_xml(xml, value, attribute, rule)
129
+ if value && (attribute&.type&.<= Lutaml::Model::Serialize)
130
+ handle_nested_elements(xml, value, rule)
71
131
  else
72
- { "xmlns" => xml_mapping.namespace_uri }
132
+ xml.public_send(rule.name) do
133
+ if !value.nil?
134
+ serialized_value = attribute.type.serialize(value)
135
+
136
+ if attribute.type == Lutaml::Model::Type::Hash
137
+ serialized_value.each do |key, val|
138
+ xml.public_send(key) { xml.text val }
139
+ end
140
+ else
141
+ xml.text(serialized_value)
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ class NokogiriElement < Element
150
+ def initialize(node, root_node: nil)
151
+ if root_node
152
+ node.namespaces.each do |prefix, name|
153
+ namespace = Lutaml::Model::XmlNamespace.new(name, prefix)
154
+
155
+ root_node.add_namespace(namespace)
156
+ end
157
+ end
158
+
159
+ attributes = {}
160
+ node.attributes.transform_values do |attr|
161
+ name = if attr.namespace
162
+ "#{attr.namespace.prefix}:#{attr.name}"
163
+ else
164
+ attr.name
165
+ end
166
+
167
+ attributes[name] = Attribute.new(
168
+ name,
169
+ attr.value,
170
+ namespace: attr.namespace&.href,
171
+ namespace_prefix: attr.namespace&.prefix,
172
+ )
73
173
  end
174
+
175
+ super(
176
+ node.name,
177
+ attributes,
178
+ parse_all_children(node, root_node: root_node || self),
179
+ node.text,
180
+ parent_document: root_node,
181
+ namespace_prefix: node.namespace&.prefix,
182
+ )
183
+ end
184
+
185
+ def text?
186
+ # false
187
+ children.empty? && text.length.positive?
74
188
  end
75
189
 
76
- def handle_nested_elements(xml, element_rule, value)
77
- case value
78
- when Array
79
- value.each { |val| build_element(xml, val) }
190
+ def to_xml(builder = nil)
191
+ builder ||= Nokogiri::XML::Builder.new
192
+
193
+ if name == "text"
194
+ builder.text(text)
80
195
  else
81
- build_element(xml, value)
196
+ builder.send(name, build_attributes(self)) do |xml|
197
+ children.each { |child| child.to_xml(xml) }
198
+ end
82
199
  end
200
+
201
+ builder
83
202
  end
84
203
 
85
- def parse_element(element)
86
- result = element.children.each_with_object({}) do |child, hash|
87
- next if child.text?
204
+ private
88
205
 
89
- hash[child.name] ||= []
90
- hash[child.name] << parse_element(child)
206
+ def parse_children(node, root_node: nil)
207
+ node.children.select(&:element?).map do |child|
208
+ NokogiriElement.new(child, root_node: root_node)
91
209
  end
92
- result["_text"] = element.text if element.text?
93
- result
94
210
  end
95
- end
96
211
 
97
- class NokogiriElement < Element
98
- def initialize(node)
99
- attributes = node.attributes.transform_values do |attr|
100
- Attribute.new(attr.name, attr.value, namespace: attr.namespace&.href, namespace_prefix: attr.namespace&.prefix)
212
+ def parse_all_children(node, root_node: nil)
213
+ node.children.map do |child|
214
+ NokogiriElement.new(child, root_node: root_node)
101
215
  end
102
- super(node.name, attributes, parse_children(node), node.text, namespace: node.namespace&.href, namespace_prefix: node.namespace&.prefix)
103
216
  end
104
217
 
105
- def text?
106
- false
218
+ def build_attributes(node)
219
+ attrs = node.attributes.transform_values(&:value)
220
+
221
+ attrs.merge(build_namespace_attributes(node))
107
222
  end
108
223
 
109
- private
224
+ def build_namespace_attributes(node)
225
+ namespace_attrs = {}
226
+
227
+ node.own_namespaces.each_value do |namespace|
228
+ namespace_attrs[namespace.attr_name] = namespace.uri
229
+ end
230
+
231
+ node.children.each do |child|
232
+ namespace_attrs = namespace_attrs.merge(
233
+ build_namespace_attributes(child),
234
+ )
235
+ end
110
236
 
111
- def parse_children(node)
112
- node.children.select(&:element?).map { |child| NokogiriElement.new(child) }
237
+ namespace_attrs
113
238
  end
114
239
  end
115
240
  end
@@ -12,10 +12,6 @@ module Lutaml
12
12
  new(root)
13
13
  end
14
14
 
15
- def initialize(root)
16
- @root = root
17
- end
18
-
19
15
  def to_h
20
16
  { @root.name => parse_element(@root) }
21
17
  end
@@ -40,15 +36,14 @@ module Lutaml
40
36
  end
41
37
 
42
38
  def build_attributes(attributes)
43
- attributes.each_with_object({}) do |(name, attr), hash|
44
- hash[name] = attr.value
45
- end
39
+ attributes.transform_values(&:value)
46
40
  end
47
41
 
48
42
  def parse_element(element)
49
43
  result = { "_text" => element.text }
50
44
  element.children.each do |child|
51
45
  next if child.is_a?(Oga::XML::Text)
46
+
52
47
  result[child.name] ||= []
53
48
  result[child.name] << parse_element(child)
54
49
  end
@@ -67,7 +62,9 @@ module Lutaml
67
62
  private
68
63
 
69
64
  def parse_children(node)
70
- node.children.select { |child| child.is_a?(Oga::XML::Element) }.map { |child| OgaElement.new(child) }
65
+ node.children.select do |child|
66
+ child.is_a?(Oga::XML::Element)
67
+ end.map { |child| OgaElement.new(child) }
71
68
  end
72
69
  end
73
70
  end