lutaml-model 0.1.0 → 0.2.1

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