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.
- checksums.yaml +4 -4
- data/.github/workflows/rake.yml +1 -1
- data/.github/workflows/release.yml +2 -2
- data/.rubocop.yml +1 -1
- data/Gemfile +20 -0
- data/lib/lutaml/model/attribute.rb +3 -1
- data/lib/lutaml/model/key_value_mapping.rb +7 -1
- data/lib/lutaml/model/mapping_rule.rb +9 -0
- 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 +181 -25
- data/lib/lutaml/model/toml_adapter/tomlib_adapter.rb +3 -2
- data/lib/lutaml/model/type.rb +138 -39
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +177 -47
- data/lib/lutaml/model/xml_adapter/oga_adapter.rb +5 -8
- data/lib/lutaml/model/xml_adapter/ox_adapter.rb +132 -20
- data/lib/lutaml/model/xml_adapter.rb +109 -8
- data/lib/lutaml/model/xml_mapping.rb +53 -5
- 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 +9 -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,156 @@ 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)
|
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
|
-
|
60
|
-
|
117
|
+
return if value.nil?
|
118
|
+
|
119
|
+
if [String, Email].include?(type)
|
61
120
|
value.to_s
|
62
|
-
|
121
|
+
elsif [Integer, BigInteger].include?(type)
|
63
122
|
value.to_i
|
64
|
-
|
123
|
+
elsif type == Float
|
65
124
|
value.to_f
|
66
|
-
|
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
|
-
|
131
|
+
elsif type == DateTime
|
73
132
|
DateTime.cast(value)
|
74
|
-
|
133
|
+
elsif type == Time
|
75
134
|
::Time.parse(value.to_s)
|
76
|
-
|
135
|
+
elsif type == TimeWithoutDate
|
77
136
|
TimeWithoutDate.cast(value)
|
78
|
-
|
137
|
+
elsif type == Boolean
|
79
138
|
to_boolean(value)
|
80
|
-
|
139
|
+
elsif type == Decimal
|
81
140
|
BigDecimal(value.to_s)
|
82
|
-
|
83
|
-
Hash(value)
|
84
|
-
|
85
|
-
value
|
86
|
-
|
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
|
-
|
89
|
-
value.to_i
|
90
|
-
when Binary
|
147
|
+
elsif type == Binary
|
91
148
|
value.force_encoding("BINARY")
|
92
|
-
|
149
|
+
elsif type == URL
|
93
150
|
URI.parse(value.to_s)
|
94
|
-
|
95
|
-
value.to_s
|
96
|
-
when IPAddress
|
151
|
+
elsif type == IPAddress
|
97
152
|
IPAddr.new(value.to_s)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
data/lib/lutaml/model/version.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
49
|
+
prefixed_xml = if xml_mapping.namespace_prefix
|
50
|
+
xml[xml_mapping.namespace_prefix]
|
51
|
+
else
|
52
|
+
xml
|
53
|
+
end
|
44
54
|
|
45
|
-
|
46
|
-
|
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
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
63
|
-
|
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,
|
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
|
-
|
125
|
+
value = child.text? ? child.text : parse_element(child)
|
88
126
|
|
89
|
-
hash[child.
|
90
|
-
|
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
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
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
|
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.
|
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
|