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.
- checksums.yaml +4 -4
- data/.github/workflows/rake.yml +1 -1
- data/.github/workflows/release.yml +2 -2
- data/.gitignore +2 -0
- data/.rubocop.yml +8 -1
- data/.rubocop_todo.yml +207 -0
- data/Gemfile +24 -0
- data/README.adoc +74 -3
- data/lib/lutaml/model/attribute.rb +7 -1
- data/lib/lutaml/model/key_value_mapping.rb +7 -1
- data/lib/lutaml/model/mapping_hash.rb +42 -0
- data/lib/lutaml/model/mapping_rule.rb +30 -2
- data/lib/lutaml/model/schema/json_schema.rb +10 -20
- data/lib/lutaml/model/schema/relaxng_schema.rb +9 -19
- data/lib/lutaml/model/schema/xsd_schema.rb +11 -24
- data/lib/lutaml/model/schema/yaml_schema.rb +11 -21
- data/lib/lutaml/model/serialize.rb +204 -37
- data/lib/lutaml/model/toml_adapter/tomlib_adapter.rb +3 -2
- data/lib/lutaml/model/type.rb +144 -39
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +181 -56
- data/lib/lutaml/model/xml_adapter/oga_adapter.rb +5 -8
- data/lib/lutaml/model/xml_adapter/ox_adapter.rb +176 -32
- data/lib/lutaml/model/xml_adapter.rb +221 -8
- data/lib/lutaml/model/xml_mapping.rb +86 -8
- data/lib/lutaml/model/xml_mapping_rule.rb +27 -3
- data/lib/lutaml/model/xml_namespace.rb +47 -0
- data/lib/lutaml/model/yaml_adapter.rb +3 -1
- data/lutaml-model.gemspec +2 -15
- metadata +11 -149
- data/.github/workflows/main.yml +0 -27
data/lib/lutaml/model/type.rb
CHANGED
@@ -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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
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
|
-
|
60
|
-
|
128
|
+
return if value.nil?
|
129
|
+
|
130
|
+
if [String, Email].include?(type)
|
61
131
|
value.to_s
|
62
|
-
|
132
|
+
elsif [Integer, BigInteger].include?(type)
|
63
133
|
value.to_i
|
64
|
-
|
134
|
+
elsif type == Float
|
65
135
|
value.to_f
|
66
|
-
|
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
|
-
|
142
|
+
elsif type == DateTime
|
73
143
|
DateTime.cast(value)
|
74
|
-
|
144
|
+
elsif type == Time
|
75
145
|
::Time.parse(value.to_s)
|
76
|
-
|
146
|
+
elsif type == TimeWithoutDate
|
77
147
|
TimeWithoutDate.cast(value)
|
78
|
-
|
148
|
+
elsif type == Boolean
|
79
149
|
to_boolean(value)
|
80
|
-
|
150
|
+
elsif type == Decimal
|
81
151
|
BigDecimal(value.to_s)
|
82
|
-
|
83
|
-
Hash(value)
|
84
|
-
|
85
|
-
value
|
86
|
-
|
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
|
-
|
89
|
-
value.to_i
|
90
|
-
when Binary
|
158
|
+
elsif type == Binary
|
91
159
|
value.force_encoding("BINARY")
|
92
|
-
|
160
|
+
elsif type == URL
|
93
161
|
URI.parse(value.to_s)
|
94
|
-
|
95
|
-
value.to_s
|
96
|
-
when IPAddress
|
162
|
+
elsif type == IPAddress
|
97
163
|
IPAddr.new(value.to_s)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
data/lib/lutaml/model/version.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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 =
|
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
|
43
|
-
value = element
|
56
|
+
attribute_def = attribute_definition_for(element, element_rule)
|
57
|
+
value = attribute_value_for(element, element_rule)
|
44
58
|
|
45
|
-
if
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
56
|
-
xml_mapping.
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
124
|
+
end
|
63
125
|
end
|
64
126
|
end
|
65
127
|
|
66
|
-
def
|
67
|
-
|
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
|
-
|
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
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
-
|
86
|
-
result = element.children.each_with_object({}) do |child, hash|
|
87
|
-
next if child.text?
|
204
|
+
private
|
88
205
|
|
89
|
-
|
90
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
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
|
106
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
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
|