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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16b8569b5f2b450dfa8218c1c55a2175f154790220a54536bc9acbf6bc718312
4
- data.tar.gz: d9d62e793690a72856e2663a67eb4eeb9cd8b909a861a91b093d8b849674a983
3
+ metadata.gz: 07d4f2ac62702a8ac27070d4d3246e59c367937cf50b649979ecb3c525f9b60e
4
+ data.tar.gz: 31adf8c870b4196ea69a065559ebbb5c2f35719aba3256597d9e24474bcbfa57
5
5
  SHA512:
6
- metadata.gz: 5e491d76d913445b9929b4e1939becc31c4b1ccb656aeaaa3e83caa20bf3c5e53142c3cc45f1a82b0a85d4b7111ab6994d5a5c03588c4f42dd8f76d101c95db0
7
- data.tar.gz: e18a44f0047929b93c2bc66c82f8942d3cf48818c6597722bba0b78107c2630aa59f84b3b4034a105155d889e64fa7e8f11881fbc9eca076b3103c21f8f189cf
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-15 02:54:02 UTC using RuboCop version 1.71.2.
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: 467
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: 48
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: 87
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: 37
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
- @raw = !!options[:raw]
23
+ validate_presence!(type, options[:method_name])
24
+ process_type!(type) if type
25
+ process_options!
26
+ end
28
27
 
29
- if collection?
30
- validate_collection_range
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
- if value.is_a?(Array)
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}`, `pattern` is only allowed for :string type"
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.6.4"
5
+ VERSION = "0.6.5"
6
6
  end
7
7
  end
@@ -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 "#default?" do
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
- subject(:mapper) { described_class.new }
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
- it "adds the attribute and getter setter for that attribute" do
118
- expect { described_class.attribute("foo", Lutaml::Model::Type::String) }
119
- .to change { described_class.attributes.keys }.from([]).to(["foo"])
120
- .and change { mapper.respond_to?(:foo) }.from(false).to(true)
121
- .and change { mapper.respond_to?(:foo=) }.from(false).to(true)
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
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