lutaml-model 0.8.74 → 0.8.76

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45577608605b19115a3826164cf851363218b65e71efa0b48a0d25e3a0df913f
4
- data.tar.gz: '0181c1f69c5d731ad13dd65fa31a37c929714f3a0a7fd12d9dd59f7e0d093b1a'
3
+ metadata.gz: ad2065e687ebed599cff0524f7b165d48d7af977d4329650bcc4e4a5a0fd2acb
4
+ data.tar.gz: 829f9d6e8eb74705aa3e3ee522f4132805667091b155d1305e9ecde8e2eec817
5
5
  SHA512:
6
- metadata.gz: ea5765db2e067d6d9b5670eaaa3c4f1269bdea9486ed07179ffa048e4435c0a504542dbf9dbdc0f270f776d084511d7d91998425b024fd8516f3599b7d238616
7
- data.tar.gz: 88db0489938297bd0d74e24d169fbf6aecc0245998401159e0795122e24580e050e9dff58672c481f8b7fa14c64314305b23c3d7780962023282fa66a1d549c3
6
+ metadata.gz: d87aa437e5db934316d42673eda31910bde0145da7a60019c08f60629575063176b3fc5bacb444e1a90a65e36735d5791e3230caed5449b4c76eb146ee4972e0
7
+ data.tar.gz: d77a73477b74c34626c6951e2de41b1eb8450872ab5164bec957b56407634a799ecce669050d531b1e54963c617a68000812ce6accb27cbb62bca8466c30169f
@@ -31,6 +31,10 @@ permissions:
31
31
  contents: read
32
32
  pull-requests: write
33
33
 
34
+ concurrency:
35
+ group: js-pr-check-${{ github.ref }}
36
+ cancel-in-progress: true
37
+
34
38
  jobs:
35
39
  trigger:
36
40
  runs-on: ubuntu-latest
@@ -20,6 +20,10 @@ on:
20
20
  permissions:
21
21
  contents: read
22
22
 
23
+ concurrency:
24
+ group: js-sync-${{ github.ref }}
25
+ cancel-in-progress: true
26
+
23
27
  jobs:
24
28
  trigger:
25
29
  runs-on: ubuntu-latest
@@ -9,6 +9,10 @@ on:
9
9
  permissions:
10
10
  contents: read
11
11
 
12
+ concurrency:
13
+ group: opal-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
12
16
  jobs:
13
17
  test:
14
18
  runs-on: ubuntu-latest
@@ -20,6 +20,12 @@ permissions:
20
20
  contents: write
21
21
  id-token: write
22
22
 
23
+ # One release at a time: concurrent bumps race the version commit
24
+ # and half-published gems are worse than a queued release.
25
+ concurrency:
26
+ group: release
27
+ cancel-in-progress: false
28
+
23
29
  jobs:
24
30
  release:
25
31
  uses: metanorma/ci/.github/workflows/rubygems-release.yml@main
@@ -329,6 +329,11 @@ register = self.register)
329
329
  # (public contract, TODO.perf 19 keeps it): a scratch element
330
330
  # is merged into the output hash afterwards.
331
331
  doc = Lutaml::KeyValue::DataModel::Element.new("__custom__")
332
+ # Custom methods may write children, or doc.value[key]=, or
333
+ # nothing at all (pubid 2.0 regression: an empty scratch
334
+ # flattened to {key => nil} and crashed the merge). Seed the
335
+ # value hash and merge only hash-shaped results.
336
+ doc.value = {}
332
337
  # lutaml-model#550: custom methods may declare a third context
333
338
  # parameter to receive the options passed to `to_*`.
334
339
  if model_instance.method(to_method).parameters.size >= 3
@@ -337,7 +342,15 @@ register = self.register)
337
342
  else
338
343
  model_instance.public_send(to_method, model_instance, doc)
339
344
  end
340
- parent.update(doc.to_hash.fetch("__custom__", {}))
345
+ # The 0.8.68 contract: only the method's doc mutations count —
346
+ # its return value is ignored. (A mutation-style handler whose
347
+ # last statement is an assignment returns the assigned inner
348
+ # value; honoring it merged child content at the wrong level —
349
+ # lutaml-model#892.) Empty docs serialize as nothing, matching
350
+ # the pre-0.8.74 behavior.
351
+ merged = doc.to_hash
352
+ parent.update(merged["__custom__"]) if merged.is_a?(::Hash) &&
353
+ merged["__custom__"].is_a?(::Hash)
341
354
  return
342
355
  end
343
356
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.74"
5
+ VERSION = "0.8.76"
6
6
  end
7
7
  end
@@ -290,4 +290,72 @@ RSpec.describe Lutaml::KeyValue::Transformation do
290
290
  end.not_to raise_error
291
291
  end
292
292
  end
293
+
294
+ describe "custom to: methods under hash-direct emission (pubid regression)" do
295
+ before do
296
+ stub_const("PubidRegress", Module.new)
297
+ stub_const("PubidRegress::EmptyCustom", empty_custom_class)
298
+ stub_const("PubidRegress::ValueWriter", value_writer_class)
299
+ stub_const("PubidRegress::ReturnOnly", return_only_class)
300
+ end
301
+
302
+ let(:empty_custom_class) do
303
+ Class.new(Lutaml::Model::Serializable) do
304
+ attribute :code, :string
305
+ key_value do
306
+ map "code", to: :code, with: { to: :to_code }
307
+ end
308
+
309
+ # pubid 2.0's shape: inspects the doc and may return without
310
+ # writing anything — must not crash the emission.
311
+ def to_code(_model, doc); end
312
+ end
313
+ end
314
+
315
+ let(:value_writer_class) do
316
+ Class.new(Lutaml::Model::Serializable) do
317
+ attribute :code, :string
318
+ key_value do
319
+ map "code", to: :code, with: { to: :to_code }
320
+ end
321
+
322
+ def to_code(model, doc)
323
+ doc.value["code"] = model.code
324
+ end
325
+ end
326
+ end
327
+
328
+ let(:return_only_class) do
329
+ Class.new(Lutaml::Model::Serializable) do
330
+ attribute :code, :string
331
+ key_value do
332
+ map "code", to: :code, with: { to: :to_code }
333
+ end
334
+
335
+ # Returns its value without touching the doc — a shape the
336
+ # mutation-only contract must serialize as nothing (#892).
337
+ def to_code(_model, _doc)
338
+ { "code" => "RETURNED" }
339
+ end
340
+ end
341
+ end
342
+
343
+ it "tolerates a custom method that writes nothing to the doc" do
344
+ model = PubidRegress::EmptyCustom.new(code: "X")
345
+ expect(model.to_hash).to eq({})
346
+ end
347
+
348
+ it "merges a custom method writing through doc.value" do
349
+ model = PubidRegress::ValueWriter.new(code: "X")
350
+ expect(model.to_hash).to eq("code" => "X")
351
+ end
352
+
353
+ it "ignores a custom method's return value (mutation-only contract)" do
354
+ # A method returning a hash without touching the doc serializes
355
+ # as nothing — honoring returns merged child content at the
356
+ # wrong level (lutaml-model#892).
357
+ model = PubidRegress::ReturnOnly.new(code: "X")
358
+ expect(model.to_hash).to eq({})
359
+ end
360
+ end
293
361
  end
@@ -732,5 +732,270 @@ RSpec.describe Lutaml::Model::Collection do
732
732
  end
733
733
  end
734
734
  end
735
+
736
+ describe "Conditional validation chaining with context" do
737
+ context "with context sharing between validations" do
738
+ it "allows validations to read results from previous validations" do
739
+ chain_executed = []
740
+
741
+ chained_collection = Class.new(Lutaml::Model::Collection) do
742
+ instances :publications, CollectionValidationTests::Publication
743
+
744
+ validates_uniqueness_of :id
745
+
746
+ validate_collection do |_collection, _errors, ctx|
747
+ chain_executed << :first
748
+ ctx[:duplicates_found] = !ctx[:duplicates_of_id].nil? && ctx[:duplicates_of_id].any?
749
+ end
750
+
751
+ validate_collection do |_collection, errors, ctx|
752
+ chain_executed << :second
753
+ if ctx[:duplicates_found]
754
+ errors.add(:collection, "Cannot proceed with duplicates")
755
+ end
756
+ end
757
+ end
758
+
759
+ # Test with duplicates
760
+ collection = chained_collection.new([science_publication, duplicate_id_publication])
761
+ expect { collection.validate! }.to raise_error(Lutaml::Model::ValidationError) do |error|
762
+ expect(error.message).to include("Cannot proceed with duplicates")
763
+ end
764
+ expect(chain_executed).to eq(%i[first second])
765
+
766
+ # Test without duplicates
767
+ chain_executed.clear
768
+ collection = chained_collection.new([science_publication, fiction_publication])
769
+ expect { collection.validate! }.not_to raise_error
770
+ expect(chain_executed).to eq(%i[first second])
771
+ end
772
+
773
+ it "stores duplicate values in context for downstream validations" do
774
+ chained_collection = Class.new(Lutaml::Model::Collection) do
775
+ instances :publications, CollectionValidationTests::Publication
776
+
777
+ validates_uniqueness_of :id
778
+
779
+ validate_collection do |_collection, errors, ctx|
780
+ if ctx[:duplicates_of_id]&.include?("1")
781
+ errors.add(:collection, "Found duplicate ID: 1")
782
+ end
783
+ end
784
+ end
785
+
786
+ collection = chained_collection.new([science_publication, duplicate_id_publication])
787
+ expect { collection.validate! }.to raise_error(Lutaml::Model::ValidationError) do |error|
788
+ expect(error.message).to include("Found duplicate ID: 1")
789
+ end
790
+ end
791
+
792
+ it "stores missing count in context for validates_all_present" do
793
+ chained_collection = Class.new(Lutaml::Model::Collection) do
794
+ instances :publications, CollectionValidationTests::Publication
795
+
796
+ validates_all_present :author
797
+
798
+ validate_collection do |_collection, errors, ctx|
799
+ if ctx[:missing_author_count].to_i > 0
800
+ errors.add(:collection, "#{ctx[:missing_author_count]} items missing author")
801
+ end
802
+ end
803
+ end
804
+
805
+ collection = chained_collection.new([science_publication, publication_without_author])
806
+ expect { collection.validate! }.to raise_error(Lutaml::Model::ValidationError) do |error|
807
+ expect(error.message).to include("1 items missing author")
808
+ end
809
+ end
810
+ end
811
+
812
+ context "with conditional validation using :if_cond option" do
813
+ it "only runs validation when condition is met" do
814
+ chained_collection = Class.new(Lutaml::Model::Collection) do
815
+ instances :publications, CollectionValidationTests::Publication
816
+
817
+ validates_uniqueness_of :id
818
+
819
+ validate_collection(if_cond: ->(ctx) { ctx[:duplicates_of_id]&.any? }) do |_collection, errors, _ctx|
820
+ errors.add(:collection, "Validation ran because duplicates exist")
821
+ end
822
+ end
823
+
824
+ # With duplicates - conditional validation should run
825
+ collection = chained_collection.new([science_publication, duplicate_id_publication])
826
+ expect { collection.validate! }.to raise_error(Lutaml::Model::ValidationError) do |error|
827
+ expect(error.message).to include("Validation ran because duplicates exist")
828
+ end
829
+
830
+ # Without duplicates - conditional validation should not run
831
+ collection = chained_collection.new([science_publication, fiction_publication])
832
+ expect { collection.validate! }.not_to raise_error
833
+ end
834
+
835
+ it "can skip validation based on context state" do
836
+ expensive_validation_ran = false
837
+
838
+ chained_collection = Class.new(Lutaml::Model::Collection) do
839
+ instances :publications, CollectionValidationTests::Publication
840
+
841
+ validates_uniqueness_of :id
842
+
843
+ validate_collection(if_cond: ->(ctx) { !ctx[:duplicates_of_id]&.any? }) do |_collection, _errors, _ctx|
844
+ expensive_validation_ran = true
845
+ end
846
+ end
847
+
848
+ # With duplicates - the uniqueness error raises (landed
849
+ # semantics) AND the expensive validation still skipped
850
+ collection = chained_collection.new([science_publication, duplicate_id_publication])
851
+ expect { collection.validate! }.to raise_error(Lutaml::Model::ValidationError,
852
+ /unique across collection/)
853
+ expect(expensive_validation_ran).to be false
854
+
855
+ # Without duplicates - expensive validation should run
856
+ expensive_validation_ran = false
857
+ collection = chained_collection.new([science_publication, fiction_publication])
858
+ collection.validate!
859
+ expect(expensive_validation_ran).to be true
860
+ end
861
+ end
862
+
863
+ context "with conditional validation using :unless_cond option" do
864
+ it "skips validation when condition is met" do
865
+ ran_count = 0
866
+
867
+ chained_collection = Class.new(Lutaml::Model::Collection) do
868
+ instances :publications, CollectionValidationTests::Publication
869
+
870
+ validates_uniqueness_of :id
871
+
872
+ validate_collection(unless_cond: ->(ctx) { ctx[:duplicates_of_id]&.any? }) do |_collection, _errors, _ctx|
873
+ ran_count += 1
874
+ end
875
+ end
876
+
877
+ # With duplicates - the uniqueness error raises (landed
878
+ # semantics) AND the conditional validation still skipped
879
+ collection = chained_collection.new([science_publication, duplicate_id_publication])
880
+ expect { collection.validate! }.to raise_error(Lutaml::Model::ValidationError,
881
+ /unique across collection/)
882
+ expect(ran_count).to be 0
883
+
884
+ # Without duplicates - should run
885
+ collection = chained_collection.new([science_publication, fiction_publication])
886
+ collection.validate!
887
+ expect(ran_count).to be 1
888
+ end
889
+ end
890
+
891
+ context "with ctx.stop!" do
892
+ it "stops validation chain when ctx.stop! is called" do
893
+ chain_order = []
894
+
895
+ chained_collection = Class.new(Lutaml::Model::Collection) do
896
+ instances :publications, CollectionValidationTests::Publication
897
+
898
+ validate_collection do |_collection, _errors, ctx|
899
+ chain_order << :first
900
+ ctx.stop!
901
+ end
902
+
903
+ validate_collection do |_collection, _errors, ctx|
904
+ chain_order << :second
905
+ ctx.stop!
906
+ end
907
+
908
+ validate_collection do |_collection, _errors, _ctx|
909
+ chain_order << :third
910
+ end
911
+ end
912
+
913
+ collection = chained_collection.new([science_publication])
914
+ collection.validate!
915
+ expect(chain_order).to eq([:first])
916
+ end
917
+ end
918
+
919
+ context "with backwards compatible block signatures" do
920
+ it "works with 1-argument blocks" do
921
+ ran = false
922
+
923
+ collection_class = Class.new(Lutaml::Model::Collection) do
924
+ instances :publications, CollectionValidationTests::Publication
925
+
926
+ validate_collection do |collection|
927
+ ran = true if !collection.empty?
928
+ end
929
+ end
930
+
931
+ collection = collection_class.new([science_publication])
932
+ collection.validate!
933
+ expect(ran).to be true
934
+ end
935
+
936
+ it "works with 2-argument blocks (original signature)" do
937
+ ran_args = []
938
+
939
+ collection_class = Class.new(Lutaml::Model::Collection) do
940
+ instances :publications, CollectionValidationTests::Publication
941
+
942
+ validate_collection do |collection, errors|
943
+ ran_args << [collection.size, errors.class.name]
944
+ errors.add(:collection, "Test error") if !collection.empty?
945
+ end
946
+ end
947
+
948
+ collection = collection_class.new([science_publication])
949
+ errors = collection.validate
950
+ expect(ran_args.first.first).to eq 1
951
+ expect(ran_args.first.last).to include("Errors")
952
+ expect(errors.map(&:message).first).to include("Test error")
953
+ end
954
+
955
+ it "works with 3-argument blocks (new signature with context)" do
956
+ received_context = nil
957
+
958
+ collection_class = Class.new(Lutaml::Model::Collection) do
959
+ instances :publications, CollectionValidationTests::Publication
960
+
961
+ validate_collection do |_collection, _errors, ctx|
962
+ received_context = ctx
963
+ ctx[:test_value] = "from_validation"
964
+ end
965
+ end
966
+
967
+ collection = collection_class.new([science_publication])
968
+ collection.validate
969
+ expect(received_context).to be_a(Lutaml::Model::ValidationContext)
970
+ expect(received_context[:test_value]).to eq("from_validation")
971
+ end
972
+ end
973
+
974
+ context "with ctx.failed? helper" do
975
+ it "returns true when errors have been added" do
976
+ result = nil
977
+
978
+ chained_collection = Class.new(Lutaml::Model::Collection) do
979
+ instances :publications, CollectionValidationTests::Publication
980
+
981
+ validates_uniqueness_of :id
982
+
983
+ validate_collection do |_collection, _errors, ctx|
984
+ result = ctx.failed?
985
+ end
986
+ end
987
+
988
+ # Without errors
989
+ collection = chained_collection.new([science_publication, fiction_publication])
990
+ collection.validate
991
+ expect(result).to be false
992
+
993
+ # With errors
994
+ collection = chained_collection.new([science_publication, duplicate_id_publication])
995
+ collection.validate
996
+ expect(result).to be true
997
+ end
998
+ end
999
+ end
735
1000
  end
736
1001
  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.8.74
4
+ version: 0.8.76
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.