lutaml-model 0.1.0 → 0.2.1

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