lutaml-model 0.6.4 → 0.6.5
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/.rubocop_todo.yml +5 -5
- data/README.adoc +38 -0
- data/lib/lutaml/model/attribute.rb +52 -24
- data/lib/lutaml/model/serialize.rb +17 -2
- data/lib/lutaml/model/version.rb +1 -1
- data/spec/lutaml/model/attribute_spec.rb +30 -1
- data/spec/lutaml/model/serializable_spec.rb +35 -6
- data/spec/lutaml/model/xml/derived_attributes_spec.rb +78 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 07d4f2ac62702a8ac27070d4d3246e59c367937cf50b649979ecb3c525f9b60e
|
4
|
+
data.tar.gz: 31adf8c870b4196ea69a065559ebbb5c2f35719aba3256597d9e24474bcbfa57
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ef7e89a66c74f214202d44a5b8a55d7c2aedde5129e2723f36128e8651a019cd578df6d24c375b2f760f81cd9dcd2d85ed6ae5f8972345467c13466da71c527
|
7
|
+
data.tar.gz: 74aff7dee7711d4c9ec88730332b91f15ba530b92946b50b1cb218157856c1c31e3c6d9bdf292b8d9ab2832586c2ee61ae29d49a8254fecbd44cc92510f6667e
|
data/.rubocop_todo.yml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on 2025-02-
|
3
|
+
# on 2025-02-21 10:20:24 UTC using RuboCop version 1.71.2.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
@@ -27,7 +27,7 @@ Layout/IndentationWidth:
|
|
27
27
|
Exclude:
|
28
28
|
- 'lib/lutaml/model/schema/xml_compiler.rb'
|
29
29
|
|
30
|
-
# Offense count:
|
30
|
+
# Offense count: 466
|
31
31
|
# This cop supports safe autocorrection (--autocorrect).
|
32
32
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
|
33
33
|
# URISchemes: http, https
|
@@ -68,7 +68,7 @@ Metrics/AbcSize:
|
|
68
68
|
Metrics/BlockLength:
|
69
69
|
Max: 46
|
70
70
|
|
71
|
-
# Offense count:
|
71
|
+
# Offense count: 47
|
72
72
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
73
73
|
Metrics/CyclomaticComplexity:
|
74
74
|
Exclude:
|
@@ -85,7 +85,7 @@ Metrics/CyclomaticComplexity:
|
|
85
85
|
- 'lib/lutaml/model/xml_adapter/ox_adapter.rb'
|
86
86
|
- 'lib/lutaml/model/xml_adapter/xml_document.rb'
|
87
87
|
|
88
|
-
# Offense count:
|
88
|
+
# Offense count: 86
|
89
89
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
90
90
|
Metrics/MethodLength:
|
91
91
|
Max: 45
|
@@ -95,7 +95,7 @@ Metrics/MethodLength:
|
|
95
95
|
Metrics/ParameterLists:
|
96
96
|
Max: 15
|
97
97
|
|
98
|
-
# Offense count:
|
98
|
+
# Offense count: 36
|
99
99
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
100
100
|
Metrics/PerceivedComplexity:
|
101
101
|
Exclude:
|
data/README.adoc
CHANGED
@@ -347,6 +347,44 @@ same class and all their attributes are equal.
|
|
347
347
|
|
348
348
|
== Defining attributes
|
349
349
|
|
350
|
+
=== Derived Attributes
|
351
|
+
|
352
|
+
A derived attribute is computed dynamically based on an instance method instead of storing a static value. It is defined using the `method:` option.
|
353
|
+
|
354
|
+
Syntax:
|
355
|
+
|
356
|
+
[source,ruby]
|
357
|
+
----
|
358
|
+
attribute :name_of_attribute, method: :instance_method_name
|
359
|
+
----
|
360
|
+
|
361
|
+
.Defining methods as attributes
|
362
|
+
[example]
|
363
|
+
====
|
364
|
+
[source,ruby]
|
365
|
+
----
|
366
|
+
class Invoice < Lutaml::Model::Serializable
|
367
|
+
attribute :subtotal, :float
|
368
|
+
attribute :tax, :float
|
369
|
+
attribute :total, method: :total_value
|
370
|
+
|
371
|
+
def total_value
|
372
|
+
subtotal + tax
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
i = Invoice.new(subtotal: 100.0, tax: 12.0)
|
377
|
+
i.total
|
378
|
+
#=> 112.0
|
379
|
+
|
380
|
+
puts i.to_yaml
|
381
|
+
#=> ---
|
382
|
+
#=> subtotal: 100.0
|
383
|
+
#=> tax: 12.0
|
384
|
+
#=> total: 112.0
|
385
|
+
----
|
386
|
+
====
|
387
|
+
|
350
388
|
=== Supported attribute value types
|
351
389
|
|
352
390
|
==== General types
|
@@ -13,23 +13,20 @@ module Lutaml
|
|
13
13
|
transform
|
14
14
|
choice
|
15
15
|
sequence
|
16
|
+
method_name
|
16
17
|
].freeze
|
17
18
|
|
18
19
|
def initialize(name, type, options = {})
|
19
20
|
@name = name
|
20
|
-
|
21
|
-
validate_type!(type)
|
22
|
-
@type = cast_type!(type)
|
23
|
-
|
24
|
-
validate_options!(options)
|
25
21
|
@options = options
|
26
22
|
|
27
|
-
|
23
|
+
validate_presence!(type, options[:method_name])
|
24
|
+
process_type!(type) if type
|
25
|
+
process_options!
|
26
|
+
end
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
@options[:default] = -> { [] } unless options[:default]
|
32
|
-
end
|
28
|
+
def derived?
|
29
|
+
type.nil?
|
33
30
|
end
|
34
31
|
|
35
32
|
def delegate
|
@@ -40,6 +37,10 @@ module Lutaml
|
|
40
37
|
@options[:transform] || {}
|
41
38
|
end
|
42
39
|
|
40
|
+
def method_name
|
41
|
+
@options[:method_name]
|
42
|
+
end
|
43
|
+
|
43
44
|
def cast_type!(type)
|
44
45
|
case type
|
45
46
|
when Symbol
|
@@ -231,20 +232,11 @@ module Lutaml
|
|
231
232
|
|
232
233
|
def serialize(value, format, options = {})
|
233
234
|
return if value.nil?
|
235
|
+
return value if derived?
|
236
|
+
return serialize_array(value, format, options) if value.is_a?(Array)
|
237
|
+
return serialize_model(value, format, options) if type <= Serialize
|
234
238
|
|
235
|
-
|
236
|
-
value.map do |v|
|
237
|
-
serialize(v, format, options)
|
238
|
-
end
|
239
|
-
elsif type <= Serialize
|
240
|
-
if Utils.present?(value)
|
241
|
-
type.public_send(:"as_#{format}", value, options)
|
242
|
-
end
|
243
|
-
else
|
244
|
-
# Convert to Value instance if not already
|
245
|
-
value = type.new(value) unless value.is_a?(Type::Value)
|
246
|
-
value.send(:"to_#{format}")
|
247
|
-
end
|
239
|
+
serialize_value(value, format)
|
248
240
|
end
|
249
241
|
|
250
242
|
def cast(value, format, options = {})
|
@@ -269,6 +261,41 @@ module Lutaml
|
|
269
261
|
(format == :xml && value.is_a?(Lutaml::Model::XmlAdapter::XmlElement))
|
270
262
|
end
|
271
263
|
|
264
|
+
def serialize_array(value, format, options)
|
265
|
+
value.map { |v| serialize(v, format, options) }
|
266
|
+
end
|
267
|
+
|
268
|
+
def serialize_model(value, format, options)
|
269
|
+
type.as(format, value, options) if Utils.present?(value)
|
270
|
+
end
|
271
|
+
|
272
|
+
def serialize_value(value, format)
|
273
|
+
value = type.new(value) unless value.is_a?(Type::Value)
|
274
|
+
value.send(:"to_#{format}")
|
275
|
+
end
|
276
|
+
|
277
|
+
def validate_presence!(type, method_name)
|
278
|
+
return if type || method_name
|
279
|
+
|
280
|
+
raise ArgumentError, "method or type must be set for an attribute"
|
281
|
+
end
|
282
|
+
|
283
|
+
def process_type!(type)
|
284
|
+
validate_type!(type)
|
285
|
+
@type = cast_type!(type)
|
286
|
+
end
|
287
|
+
|
288
|
+
def process_options!
|
289
|
+
validate_options!(@options)
|
290
|
+
@raw = !!@options[:raw]
|
291
|
+
set_default_for_collection if collection?
|
292
|
+
end
|
293
|
+
|
294
|
+
def set_default_for_collection
|
295
|
+
validate_collection_range
|
296
|
+
@options[:default] ||= -> { [] }
|
297
|
+
end
|
298
|
+
|
272
299
|
def validate_options!(options)
|
273
300
|
if (invalid_opts = options.keys - ALLOWED_OPTIONS).any?
|
274
301
|
raise StandardError,
|
@@ -277,7 +304,8 @@ module Lutaml
|
|
277
304
|
|
278
305
|
if options.key?(:pattern) && type != Lutaml::Model::Type::String
|
279
306
|
raise StandardError,
|
280
|
-
"Invalid option `pattern` given for `#{name}`,
|
307
|
+
"Invalid option `pattern` given for `#{name}`, " \
|
308
|
+
"`pattern` is only allowed for :string type"
|
281
309
|
end
|
282
310
|
|
283
311
|
true
|
@@ -96,6 +96,11 @@ module Lutaml
|
|
96
96
|
|
97
97
|
# Define an attribute for the model
|
98
98
|
def attribute(name, type, options = {})
|
99
|
+
if type.is_a?(Hash)
|
100
|
+
options[:method_name] = type[:method]
|
101
|
+
type = nil
|
102
|
+
end
|
103
|
+
|
99
104
|
attr = Attribute.new(name, type, options)
|
100
105
|
attributes[name] = attr
|
101
106
|
|
@@ -106,6 +111,10 @@ module Lutaml
|
|
106
111
|
options[:values],
|
107
112
|
collection: options[:collection],
|
108
113
|
)
|
114
|
+
elsif attr.derived? && name != attr.method_name
|
115
|
+
define_method(name) do
|
116
|
+
public_send(attr.method_name)
|
117
|
+
end
|
109
118
|
else
|
110
119
|
define_method(name) do
|
111
120
|
instance_variable_get(:"@#{name}")
|
@@ -283,6 +292,10 @@ module Lutaml
|
|
283
292
|
end
|
284
293
|
end
|
285
294
|
|
295
|
+
def as(format, instance, options = {})
|
296
|
+
public_send(:"as_#{format}", instance, options)
|
297
|
+
end
|
298
|
+
|
286
299
|
def key_value(&block)
|
287
300
|
Lutaml::Model::Config::KEY_VALUE_FORMATS.each do |format|
|
288
301
|
mappings[format] ||= KeyValueMapping.new
|
@@ -484,8 +497,7 @@ module Lutaml
|
|
484
497
|
return instance unless doc
|
485
498
|
|
486
499
|
if options[:default_namespace].nil?
|
487
|
-
options[:default_namespace] =
|
488
|
-
mappings_for(:xml)&.namespace_uri
|
500
|
+
options[:default_namespace] = mappings_for(:xml)&.namespace_uri
|
489
501
|
end
|
490
502
|
mappings = options[:mappings] || mappings_for(:xml).mappings
|
491
503
|
|
@@ -517,6 +529,7 @@ module Lutaml
|
|
517
529
|
raise "Attribute '#{rule.to}' not found in #{self}" unless valid_rule?(rule)
|
518
530
|
|
519
531
|
attr = attribute_for_rule(rule)
|
532
|
+
next if attr&.derived?
|
520
533
|
|
521
534
|
value = if rule.raw_mapping?
|
522
535
|
doc.root.inner_xml
|
@@ -703,6 +716,8 @@ module Lutaml
|
|
703
716
|
end
|
704
717
|
|
705
718
|
self.class.attributes.each do |name, attr|
|
719
|
+
next if attr.derived?
|
720
|
+
|
706
721
|
value = if attrs.key?(name) || attrs.key?(name.to_s)
|
707
722
|
attr_value(attrs, name, attr)
|
708
723
|
else
|
data/lib/lutaml/model/version.rb
CHANGED
@@ -7,6 +7,10 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
7
7
|
described_class.new("name", :string)
|
8
8
|
end
|
9
9
|
|
10
|
+
let(:method_attr) do
|
11
|
+
described_class.new("name", nil, method_name: nil)
|
12
|
+
end
|
13
|
+
|
10
14
|
let(:test_record_class) do
|
11
15
|
Class.new(Lutaml::Model::Serializable) do
|
12
16
|
attribute :age, :integer
|
@@ -33,6 +37,13 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
33
37
|
.to("avatar.png")
|
34
38
|
end
|
35
39
|
|
40
|
+
it "raises error if both type and method_name are not given" do
|
41
|
+
expect { method_attr }.to raise_error(
|
42
|
+
ArgumentError,
|
43
|
+
"method or type must be set for an attribute",
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
36
47
|
describe "#validate_options!" do
|
37
48
|
let(:validate_options) { name_attr.method(:validate_options!) }
|
38
49
|
|
@@ -103,7 +114,25 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
103
114
|
end
|
104
115
|
end
|
105
116
|
|
106
|
-
describe "#
|
117
|
+
describe "#derived?" do
|
118
|
+
context "when type is set" do
|
119
|
+
let(:attribute) { described_class.new("name", :string) }
|
120
|
+
|
121
|
+
it "returns false" do
|
122
|
+
expect(attribute.derived?).to be(false)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context "when type is nil and method_name is set" do
|
127
|
+
let(:attribute) { described_class.new("name", nil, method_name: :tmp) }
|
128
|
+
|
129
|
+
it "returns true" do
|
130
|
+
expect(attribute.derived?).to be(true)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "#default" do
|
107
136
|
context "when default is not set" do
|
108
137
|
let(:attribute) { described_class.new("name", :string) }
|
109
138
|
|
@@ -112,13 +112,42 @@ RSpec.describe Lutaml::Model::Serializable do
|
|
112
112
|
end
|
113
113
|
|
114
114
|
describe ".attribute" do
|
115
|
-
|
115
|
+
before do
|
116
|
+
stub_const("TestClass", Class.new(described_class))
|
117
|
+
end
|
118
|
+
|
119
|
+
context "when method_name is given" do
|
120
|
+
let(:attribute) do
|
121
|
+
TestClass.attribute("test", method: :foobar)
|
122
|
+
end
|
116
123
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
124
|
+
it "adds derived attribute" do
|
125
|
+
expect { attribute }
|
126
|
+
.to change { TestClass.attributes["test"] }
|
127
|
+
.from(nil)
|
128
|
+
.to(Lutaml::Model::Attribute)
|
129
|
+
end
|
130
|
+
|
131
|
+
it "returns true for derived?" do
|
132
|
+
expect(attribute.derived?).to be(true)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
context "when type is given" do
|
137
|
+
let(:attribute) do
|
138
|
+
TestClass.attribute("foo", Lutaml::Model::Type::String)
|
139
|
+
end
|
140
|
+
|
141
|
+
it "adds the attribute and getter setter for that attribute" do
|
142
|
+
expect { attribute }
|
143
|
+
.to change { TestClass.attributes.keys }.from([]).to(["foo"])
|
144
|
+
.and change { TestClass.new.respond_to?(:foo) }.from(false).to(true)
|
145
|
+
.and change { TestClass.new.respond_to?(:foo=) }.from(false).to(true)
|
146
|
+
end
|
147
|
+
|
148
|
+
it "returns false for derived?" do
|
149
|
+
expect(attribute.derived?).to be(false)
|
150
|
+
end
|
122
151
|
end
|
123
152
|
end
|
124
153
|
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
module DerivedAttributesSpecs
|
6
|
+
class Ceramic < Lutaml::Model::Serializable
|
7
|
+
attribute :name, :string
|
8
|
+
attribute :value, :float
|
9
|
+
end
|
10
|
+
|
11
|
+
class CeramicCollection < Lutaml::Model::Serializable
|
12
|
+
attribute :items, Ceramic, collection: true
|
13
|
+
attribute :total_value, method: :total_value
|
14
|
+
|
15
|
+
# Derived property
|
16
|
+
def total_value
|
17
|
+
items.sum(&:value)
|
18
|
+
end
|
19
|
+
|
20
|
+
xml do
|
21
|
+
root "ceramic-collection"
|
22
|
+
map_element "total-value", to: :total_value
|
23
|
+
map_element "item", to: :items
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
RSpec.describe "XML::DerivedAttributes" do
|
29
|
+
let(:xml) do
|
30
|
+
<<~XML.strip
|
31
|
+
<ceramic-collection>
|
32
|
+
<total-value>2500.0</total-value>
|
33
|
+
<item>
|
34
|
+
<name>Ancient Vase</name>
|
35
|
+
<value>1500.0</value>
|
36
|
+
</item>
|
37
|
+
<item>
|
38
|
+
<name>Historic Bowl</name>
|
39
|
+
<value>1000.0</value>
|
40
|
+
</item>
|
41
|
+
</ceramic-collection>
|
42
|
+
XML
|
43
|
+
end
|
44
|
+
|
45
|
+
let(:ancient_vase) do
|
46
|
+
DerivedAttributesSpecs::Ceramic.new(name: "Ancient Vase", value: 1500.0)
|
47
|
+
end
|
48
|
+
|
49
|
+
let(:historic_bowl) do
|
50
|
+
DerivedAttributesSpecs::Ceramic.new(name: "Historic Bowl", value: 1000.0)
|
51
|
+
end
|
52
|
+
|
53
|
+
let(:ceramic_collection) do
|
54
|
+
DerivedAttributesSpecs::CeramicCollection.new(
|
55
|
+
items: [ancient_vase, historic_bowl],
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
describe ".from_xml" do
|
60
|
+
let(:parsed) do
|
61
|
+
DerivedAttributesSpecs::CeramicCollection.from_xml(xml)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "correctly parses items" do
|
65
|
+
expect(parsed).to eq(ceramic_collection)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "correctly calculates total-value" do
|
69
|
+
expect(parsed.total_value).to eq(2500)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe ".to_xml" do
|
74
|
+
it "convert to correct xml" do
|
75
|
+
expect(ceramic_collection.to_xml).to eq(xml)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lutaml-model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ribose Inc.
|
@@ -257,6 +257,7 @@ files:
|
|
257
257
|
- spec/lutaml/model/utils_spec.rb
|
258
258
|
- spec/lutaml/model/validation_spec.rb
|
259
259
|
- spec/lutaml/model/with_child_mapping_spec.rb
|
260
|
+
- spec/lutaml/model/xml/derived_attributes_spec.rb
|
260
261
|
- spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb
|
261
262
|
- spec/lutaml/model/xml_adapter/oga_adapter_spec.rb
|
262
263
|
- spec/lutaml/model/xml_adapter/ox_adapter_spec.rb
|