lutaml-model 0.6.4 → 0.6.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|