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 +4 -4
- data/.github/workflows/js-pr-check.yml +4 -0
- data/.github/workflows/js-sync.yml +4 -0
- data/.github/workflows/opal.yml +4 -0
- data/.github/workflows/release.yml +6 -0
- data/lib/lutaml/key_value/transformation.rb +14 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/spec/lutaml/key_value/transformation_spec.rb +68 -0
- data/spec/lutaml/model/collection_validation_spec.rb +265 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ad2065e687ebed599cff0524f7b165d48d7af977d4329650bcc4e4a5a0fd2acb
|
|
4
|
+
data.tar.gz: 829f9d6e8eb74705aa3e3ee522f4132805667091b155d1305e9ecde8e2eec817
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d87aa437e5db934316d42673eda31910bde0145da7a60019c08f60629575063176b3fc5bacb444e1a90a65e36735d5791e3230caed5449b4c76eb146ee4972e0
|
|
7
|
+
data.tar.gz: d77a73477b74c34626c6951e2de41b1eb8450872ab5164bec957b56407634a799ecce669050d531b1e54963c617a68000812ce6accb27cbb62bca8466c30169f
|
data/.github/workflows/opal.yml
CHANGED
|
@@ -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
|
-
|
|
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
|
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -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
|