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.
@@ -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