lutaml-model 0.8.61 → 0.8.62

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: 60d029c4eab58efd3741553bd22cc4f689164143f6f1fed87d4c6956cf341cfd
4
- data.tar.gz: aefb886903788a07439e416b2a92ec7a5fe295f231aa6c137b606e6e9621b048
3
+ metadata.gz: 93221695e02a0814183bb7935d242d416e7b5fc98fdae729ae7b7a548276fa1a
4
+ data.tar.gz: a8c6631206d1ed3dc635012999f11a2deb9f29409a441a4b85e8d47dd7f2fa9c
5
5
  SHA512:
6
- metadata.gz: 82a30dcc2a930524ebb5d3d8cdcd9c9eb712f5425105b61101263501af2b66e93e5395e2455925e57d87e9a10689b8dbd1b816e9e0f302e4472ba68f084ed365
7
- data.tar.gz: 76625bdefe2dde62258b0ec3da4ca257679fa16a9b669ca0a4453108097e9d670109e9b4f77f5b67c0cf76f95e18164edd2b477a885bb3ed5cc73c928174d6a8
6
+ metadata.gz: db7ee4ad6d440102cbf7c2c8452b20844c1ddb251a44f86d44f0655e14f359eb1e1fa17debc2f0857485c34b09c9199c6921c31a55b1910aa103ac0fd5e11ff8
7
+ data.tar.gz: c8a43921457c96865466b5cb0111ccd4e6aba04585bb6e398bc7ad6f808a2009f2bbffa48e8dc1348ba10937776c96c0707249ab54d0bc018d900c4799eb98e3
@@ -44,6 +44,12 @@ jobs:
44
44
  runs-on: ubuntu-latest
45
45
  steps:
46
46
  - uses: actions/checkout@v6
47
+ # The release job commits the version bump to main while this job
48
+ # runs; a checkout of the dispatch-time HEAD reads the previous
49
+ # version and smoke-tests the wrong gem (0.8.60 was smoke-checked
50
+ # for the 0.8.61 release). Pull post-bump main before resolving.
51
+ with:
52
+ fetch-depth: 5
47
53
  - uses: ruby/setup-ruby@v1
48
54
  with:
49
55
  ruby-version: "3.4"
@@ -52,8 +58,13 @@ jobs:
52
58
  # From the version file, not git describe: the default checkout
53
59
  # is shallow with no tags, so git describe failed and the empty
54
60
  # version produced `gem install "lutaml-model:"`.
55
- run: echo "version=$(ruby -e 'print File.read(%q{lib/lutaml/model/version.rb})[/VERSION\s*=\s*"([^"]+)"/, 1]')" >> "$GITHUB_OUTPUT"
61
+ run: |
62
+ git pull --ff-only origin main
63
+ echo "version=$(ruby -e 'print File.read(%q{lib/lutaml/model/version.rb})[/VERSION\s*=\s*"([^"]+)"/, 1]')" >> "$GITHUB_OUTPUT"
56
64
  - name: Install and smoke the released gem
57
65
  run: |
58
66
  gem install "lutaml-model:${{ steps.version.outputs.version }}" --no-document
67
+ # The round-trip script exercises all five formats; the TOML
68
+ # legs require an adapter gem to be present.
69
+ gem install tomlib toml-rb --no-document
59
70
  ruby .github/scripts/test_installed_gem.rb
@@ -23,19 +23,22 @@ module Lutaml
23
23
  },
24
24
  }
25
25
 
26
- # Choice validation: each choice is its own group. Independent
27
- # choices combine conjunctively (allOf), so a document may
28
- # satisfy every group at once; a single choice renders its
29
- # group directly. Nested choices render as a nested group
30
- # schema, which a document satisfies through its own
31
- # oneOf/anyOf (#864, #865).
26
+ # Choice validation follows the serializer's contract: unset
27
+ # or empty members are omitted from output, and instances that
28
+ # leave a whole group empty are accepted (#869). Members are
29
+ # therefore never `required`; a max:1 group contributes an
30
+ # at-most-one constraint over its members, and independent
31
+ # groups combine conjunctively under allOf. Groups whose range
32
+ # admits several members impose no serialization constraint.
32
33
  if type.choice_attributes.any?
33
- if type.choice_attributes.one?
34
- @schema[name].merge!(choice_schema(type.choice_attributes.first))
35
- else
36
- @schema[name]["allOf"] = type.choice_attributes.map do |choice|
37
- choice_schema(choice)
38
- end
34
+ groups = type.choice_attributes
35
+ .select { |choice| choice.max == 1 }
36
+ .map { |choice| { "oneOf" => exclusive_branches(choice) } }
37
+
38
+ if groups.one?
39
+ @schema[name].merge!(groups.first)
40
+ elsif groups.any?
41
+ @schema[name]["allOf"] = groups
39
42
  end
40
43
  end
41
44
 
@@ -44,30 +47,47 @@ module Lutaml
44
47
 
45
48
  private
46
49
 
47
- # min == max == 1 makes the members mutually exclusive (oneOf);
48
- # any other range admits several members together (anyOf with
49
- # per-member requirement).
50
- def choice_schema(choice)
51
- key = choice.min == 1 && choice.max == 1 ? "oneOf" : "anyOf"
52
- { key => choice_branches(choice) }
53
- end
54
-
55
- def choice_branches(choice)
56
- choice.attributes.map do |member|
50
+ # Leaves of the group (nested choices collapse into it), as
51
+ # [name, attribute] pairs.
52
+ def choice_leaves(choice)
53
+ choice.attributes.flat_map do |member|
57
54
  if member.is_a?(Lutaml::Model::Choice)
58
- choice_schema(member)
55
+ choice_leaves(member)
59
56
  else
60
- {
61
- "type" => "object",
62
- "properties" => PropertiesCollection.from_attributes(
63
- [member], extract_register_from(type)
64
- ).to_schema,
65
- "required" => [member.name.to_s],
66
- }
57
+ [[member.name.to_s, member]]
67
58
  end
68
59
  end
69
60
  end
70
61
 
62
+ # Exactly-one-present construction over the leaves: no leaf, or
63
+ # exactly one leaf present without the others. Accepts the
64
+ # all-empty document the serializer emits; rejects documents
65
+ # with two members of the group at once (max:1).
66
+ def exclusive_branches(choice)
67
+ leaves = choice_leaves(choice)
68
+ names = leaves.map(&:first)
69
+ none = {
70
+ "not" => {
71
+ "anyOf" => names.map { |n| { "required" => [n] } },
72
+ },
73
+ }
74
+ singles = leaves.map do |name, attribute|
75
+ others = names - [name]
76
+ {
77
+ "allOf" => (
78
+ [{
79
+ "type" => "object",
80
+ "properties" => PropertiesCollection.from_attributes(
81
+ [attribute], extract_register_from(type)
82
+ ).to_schema,
83
+ "required" => [name],
84
+ }] + others.map { |o| { "not" => { "required" => [o] } } }
85
+ ),
86
+ }
87
+ end
88
+ [none] + singles
89
+ end
90
+
71
91
  def properties_to_schema(type)
72
92
  PropertiesCollection.from_class(type).to_schema
73
93
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.61"
5
+ VERSION = "0.8.62"
6
6
  end
7
7
  end
@@ -70,25 +70,60 @@ RSpec.describe "JSON/YAML schema generation regressions (#863, #864, #865, #866)
70
70
  end
71
71
 
72
72
  describe "#864: nested choice blocks" do
73
- it "renders the nested group as its own oneOf" do
73
+ it "collapses the nested group into the outer at-most-one set" do
74
74
  schema = JSON.parse(Lutaml::Model::Schema.to_json(SchemaRepro::Outer))
75
75
  one_of = schema["$defs"]["SchemaRepro_Outer"]["oneOf"]
76
- at_branch = one_of.find { |b| b["required"] == %w[at] }
77
- inner = one_of.find { |b| b["oneOf"] }
78
- expect(at_branch).to be_truthy
79
- expect(inner["oneOf"].map { |b| b["required"] })
80
- .to contain_exactly(%w[from], %w[to])
76
+ requireds = one_of.map { |b| b["allOf"].to_a.filter_map { |s| s["required"]&.first } }
77
+ expect(requireds).to contain_exactly([], %w[from], %w[to], %w[at])
81
78
  end
82
79
  end
83
80
 
84
81
  describe "#865: independent choices combine conjunctively" do
85
- it "renders an allOf of oneOf groups with discriminating required keys" do
82
+ it "renders an allOf of oneOf at-most-one groups" do
86
83
  schema = JSON.parse(Lutaml::Model::Schema.to_json(SchemaRepro::Relation))
87
84
  all_of = schema["$defs"]["SchemaRepro_Relation"]["allOf"]
88
85
  expect(all_of.length).to eq(2)
89
86
  all_of.each do |group|
90
87
  expect(group).to have_key("oneOf")
91
- group["oneOf"].each { |branch| expect(branch["required"]).to be_truthy }
88
+ expect(group["oneOf"].length).to eq(3)
89
+ end
90
+ end
91
+ end
92
+
93
+ describe "#869: the serializer's own output validates" do
94
+ it "emits no required keys for initialize_empty choice members" do
95
+ schema = JSON.parse(Lutaml::Model::Schema.to_json(SchemaRepro::Relation))
96
+ definition = schema["$defs"]["SchemaRepro_Relation"]
97
+ groups = definition["allOf"] || [definition]
98
+ groups.each do |group|
99
+ group["oneOf"].each do |branch|
100
+ Array(branch["allOf"]).each do |sub|
101
+ expect(sub).not_to have_key("required") if sub.key?("not")
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ it "accepts the document the default instance serializes to" do
108
+ inst = SchemaRepro::Relation.new(type: "includes")
109
+ doc = inst.to_hash
110
+ expect(doc).to eq("type" => "includes")
111
+ schema = JSON.parse(Lutaml::Model::Schema.to_json(SchemaRepro::Relation))
112
+ definition = schema["$defs"]["SchemaRepro_Relation"]
113
+ groups = definition["allOf"] || [definition]
114
+ groups.each do |group|
115
+ satisfied = group["oneOf"].count do |branch|
116
+ Array(branch["allOf"]).all? do |sub|
117
+ if sub.key?("not")
118
+ Array(sub["not"]["anyOf"]).none? do |req|
119
+ doc.key?(req["required"].first)
120
+ end
121
+ else
122
+ doc.key?(sub["required"].first)
123
+ end
124
+ end
125
+ end
126
+ expect(satisfied).to eq(1)
92
127
  end
93
128
  end
94
129
  end
@@ -161,26 +161,7 @@ RSpec.describe Lutaml::Model::Schema::JsonSchema do
161
161
  "type" => ["string", "null"],
162
162
  },
163
163
  },
164
- "anyOf" => [
165
- {
166
- "type" => "object",
167
- "properties" => {
168
- "email" => {
169
- "type" => ["string", "null"],
170
- },
171
- },
172
- "required" => ["email"],
173
- },
174
- {
175
- "type" => "object",
176
- "properties" => {
177
- "phone" => {
178
- "type" => ["string", "null"],
179
- },
180
- },
181
- "required" => ["phone"],
182
- },
183
- ],
164
+
184
165
  },
185
166
  },
186
167
  }
@@ -136,23 +136,6 @@ RSpec.describe Lutaml::Model::Schema::YamlSchema do
136
136
  type:
137
137
  - string
138
138
  - 'null'
139
- anyOf:
140
- - type: object
141
- properties:
142
- email:
143
- type:
144
- - string
145
- - 'null'
146
- required:
147
- - email
148
- - type: object
149
- properties:
150
- phone:
151
- type:
152
- - string
153
- - 'null'
154
- required:
155
- - phone
156
139
  YAML
157
140
  end
158
141
 
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.61
4
+ version: 0.8.62
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.