verquest 0.6.4 → 0.7.0

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -1
  3. data/README.md +299 -45
  4. data/Rakefile +5 -0
  5. data/docs/rails.md +169 -0
  6. data/lib/verquest/base/condition_class_methods.rb +211 -0
  7. data/lib/verquest/base/private_class_methods.rb +31 -14
  8. data/lib/verquest/base/public_class_methods.rb +51 -1
  9. data/lib/verquest/base.rb +1 -0
  10. data/lib/verquest/conditions/branch.rb +111 -0
  11. data/lib/verquest/conditions/condition.rb +194 -0
  12. data/lib/verquest/conditions/conjunction.rb +39 -0
  13. data/lib/verquest/conditions/fragment_merge.rb +52 -0
  14. data/lib/verquest/conditions/property_merge.rb +47 -0
  15. data/lib/verquest/conditions/rule_set.rb +58 -0
  16. data/lib/verquest/conditions/rules/absent.rb +25 -0
  17. data/lib/verquest/conditions/rules/alternatives.rb +28 -0
  18. data/lib/verquest/conditions/rules/any.rb +27 -0
  19. data/lib/verquest/conditions/rules/equals.rb +22 -0
  20. data/lib/verquest/conditions/rules/names_rule.rb +23 -0
  21. data/lib/verquest/conditions/rules/none.rb +24 -0
  22. data/lib/verquest/conditions/rules/present.rb +22 -0
  23. data/lib/verquest/conditions/rules/property_rule.rb +48 -0
  24. data/lib/verquest/conditions/rules/satisfies.rb +22 -0
  25. data/lib/verquest/conditions/rules/value_in.rb +22 -0
  26. data/lib/verquest/conditions/rules.rb +17 -0
  27. data/lib/verquest/conditions.rb +36 -0
  28. data/lib/verquest/configuration.rb +19 -1
  29. data/lib/verquest/gem_version.rb +1 -1
  30. data/lib/verquest/helper_methods/conditional_properties.rb +126 -0
  31. data/lib/verquest/helper_methods/unnamed_one_of.rb +35 -0
  32. data/lib/verquest/properties/array.rb +15 -1
  33. data/lib/verquest/properties/base.rb +104 -10
  34. data/lib/verquest/properties/collection.rb +51 -18
  35. data/lib/verquest/properties/const.rb +2 -1
  36. data/lib/verquest/properties/object.rb +38 -4
  37. data/lib/verquest/properties/one_of/discriminated_schema.rb +84 -0
  38. data/lib/verquest/properties/one_of.rb +83 -44
  39. data/lib/verquest/properties/reference.rb +47 -16
  40. data/lib/verquest/query/coercer.rb +165 -0
  41. data/lib/verquest/query/definition_check.rb +91 -0
  42. data/lib/verquest/query/parameters.rb +79 -0
  43. data/lib/verquest/query.rb +25 -0
  44. data/lib/verquest/transformer/collection.rb +32 -0
  45. data/lib/verquest/transformer/const_as_enum.rb +47 -0
  46. data/lib/verquest/transformer/layout.rb +83 -0
  47. data/lib/verquest/transformer/mapping_layout.rb +166 -0
  48. data/lib/verquest/transformer/one_of.rb +111 -0
  49. data/lib/verquest/transformer/path.rb +188 -0
  50. data/lib/verquest/transformer/plan.rb +183 -0
  51. data/lib/verquest/transformer/scope.rb +64 -0
  52. data/lib/verquest/transformer.rb +43 -880
  53. data/lib/verquest/version.rb +152 -79
  54. data/lib/verquest.rb +6 -0
  55. metadata +42 -7
@@ -18,6 +18,9 @@ module Verquest
18
18
  # item: ProductRequest
19
19
  # )
20
20
  class Collection < Base
21
+ include HelperMethods::ConditionalProperties
22
+ include HelperMethods::UnnamedOneOf
23
+
21
24
  # Initialize a new Collection property
22
25
  #
23
26
  # @param name [String, Symbol] The name of the property
@@ -31,6 +34,7 @@ module Verquest
31
34
  raise ArgumentError, "You can not map collection to the root" if map == "/"
32
35
 
33
36
  @properties = {}
37
+ @conditions = []
34
38
 
35
39
  @name = name.to_s
36
40
  @item = item
@@ -51,9 +55,21 @@ module Verquest
51
55
  # @param property [Verquest::Properties::Base] The property to add to the collection items
52
56
  # @return [Verquest::Properties::Base] The added property
53
57
  def add(property)
58
+ check_unnamed_one_of(property)
54
59
  properties[property.name] = property
55
60
  end
56
61
 
62
+ # Add a condition to this collection's item definition
63
+ #
64
+ # @param condition [Verquest::Conditions::Condition] The condition to add
65
+ # @return [Array<Verquest::Conditions::Condition>] The conditions
66
+ # @raise [ArgumentError] If the collection items are defined by `item:`
67
+ def add_condition(condition)
68
+ raise ArgumentError, "condition can not be declared in a collection defined by item:" if has_item?
69
+
70
+ super
71
+ end
72
+
57
73
  # Check if this collection references an external item schema
58
74
  #
59
75
  # @return [Boolean] True if the collection uses an external reference
@@ -61,18 +77,21 @@ module Verquest
61
77
  !item.nil?
62
78
  end
63
79
 
64
- # Check if this collection contains a oneOf property as its item type
80
+ # Check if the items are a oneOf: an unnamed oneOf is the collection's only property
65
81
  #
66
- # @return [Boolean] True if the collection contains a oneOf property
82
+ # @return [Boolean] True if the item schema is the oneOf itself
67
83
  def has_one_of?
68
- properties.values.size == 1 && properties.values.first.is_a?(Verquest::Properties::OneOf)
84
+ # A named oneOf is a property of the item, like any other
85
+ properties.values.size == 1 && properties.values.first.is_a?(Verquest::Properties::OneOf) && properties.values.first.name.nil?
69
86
  end
70
87
 
71
88
  # Returns the oneOf property if this collection contains one
72
89
  #
73
90
  # @return [Verquest::Properties::OneOf, nil] The oneOf property or nil
91
+ # @raise [ArgumentError] If the collection also has conditions
74
92
  def one_of_property
75
93
  return nil unless has_one_of?
94
+ raise ArgumentError, "Conditions can not be declared next to a one_of in a collection" if conditions.any?
76
95
 
77
96
  properties.values.first
78
97
  end
@@ -104,10 +123,11 @@ module Verquest
104
123
  "items" => {
105
124
  "type" => "object",
106
125
  "required" => required_properties,
107
- "properties" => properties.transform_values { |property| property.to_schema[property.name] },
126
+ "properties" => all_properties.transform_values { |property| property.to_schema[property.name] },
108
127
  "additionalProperties" => Verquest.configuration.default_additional_properties
109
128
  }.tap do |schema|
110
129
  schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
130
+ add_conditions(schema)
111
131
  end
112
132
  }.merge(schema_options)
113
133
  }
@@ -141,10 +161,11 @@ module Verquest
141
161
  "items" => {
142
162
  "type" => "object",
143
163
  "required" => required_properties,
144
- "properties" => properties.transform_values { |property| property.to_validation_schema(version: version)[property.name] },
164
+ "properties" => all_properties.transform_values { |property| property.to_validation_schema(version: version)[property.name] },
145
165
  "additionalProperties" => Verquest.configuration.default_additional_properties
146
166
  }.tap do |schema|
147
167
  schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
168
+ add_conditions(schema)
148
169
  end
149
170
  }.merge(schema_options)
150
171
  }
@@ -174,24 +195,36 @@ module Verquest
174
195
  # @return [void]
175
196
  def mapping(key_prefix:, value_prefix:, mapping:, version:)
176
197
  if has_item?
177
- value_key_prefix = mapping_value_key(value_prefix: value_prefix, collection: true)
178
-
179
- reference_mapping = item.mapping(version:).dup
180
- reference_mapping.transform_keys! { |k| "#{(key_prefix + [name]).join("/")}[]/#{k}" }
181
- reference_mapping.transform_values! { |v| "#{value_key_prefix}/#{v}" }
182
-
183
- mapping.merge!(reference_mapping)
184
- elsif has_one_of?
185
- one_of_mapping = {}
186
- one_of_property.mapping(key_prefix: key_prefix + ["#{name}[]"], value_prefix: mapping_value_prefix(value_prefix: value_prefix, collection: true), mapping: one_of_mapping, version:)
187
- mapping.merge!(one_of_mapping)
198
+ add_request_mapping(item, key_prefix: key_prefix + ["#{name}[]"], value_prefix: mapping_value_prefix(value_prefix: value_prefix, collection: true), mapping:, version:)
188
199
  else
189
- properties.values.each do |property|
190
- property.mapping(key_prefix: key_prefix + ["#{name}[]"], value_prefix: mapping_value_prefix(value_prefix: value_prefix, collection: true), mapping:, version:)
200
+ children = has_one_of? ? [one_of_property] : all_properties.values
201
+ children.each do |property|
202
+ add_child_mapping(property, key_prefix: key_prefix + ["#{name}[]"], value_prefix: mapping_value_prefix(value_prefix: value_prefix, collection: true), mapping:, version:)
191
203
  end
192
204
  end
193
205
  end
194
206
 
207
+ # Adds this collection and its item properties to a transformer layout
208
+ #
209
+ # The collection's source and target paths are added too, so the plan
210
+ # knows where the items go when they hold a oneOf.
211
+ #
212
+ # @param key_prefix [Array<String>] Prefix for the source key
213
+ # @param value_prefix [Array<String>] Prefix for the target value
214
+ # @param layout [Verquest::Transformer::Layout] The layout to add to
215
+ # @param version [String, nil] The version to create the layout for
216
+ # @return [void]
217
+ def layout(key_prefix:, value_prefix:, layout:, version:)
218
+ item_key_prefix = key_prefix + ["#{name}[]"]
219
+ item_value_prefix = mapping_value_prefix(value_prefix:, collection: true)
220
+ layout.add_collection(item_key_prefix.join("/"), item_value_prefix.join("/"))
221
+ return add_request_layout(item, key_prefix: item_key_prefix, value_prefix: item_value_prefix, layout:, version:) if has_item?
222
+
223
+ (has_one_of? ? [one_of_property] : all_properties.values).each do |property|
224
+ property.layout(key_prefix: item_key_prefix, value_prefix: item_value_prefix, layout:, version:)
225
+ end
226
+ end
227
+
195
228
  private
196
229
 
197
230
  attr_reader :item, :schema_options, :properties, :type
@@ -30,7 +30,8 @@ module Verquest
30
30
  # @return [Hash] The schema definition for this constant
31
31
  def to_schema
32
32
  {
33
- name => nullable_schema({"const" => value}.merge(schema_options))
33
+ # Draft 4 and OpenAPI 3.0 have no const, but a one-value enum means the same
34
+ name => nullable_schema((Verquest.configuration.const_keyword? ? {"const" => value} : {"enum" => [value]}).merge(schema_options))
34
35
  }
35
36
  end
36
37
 
@@ -12,6 +12,9 @@ module Verquest
12
12
  # address.add(Verquest::Properties::Field.new(name: :street, type: :string))
13
13
  # address.add(Verquest::Properties::Field.new(name: :city, type: :string, required: true))
14
14
  class Object < Base
15
+ include HelperMethods::ConditionalProperties
16
+ include HelperMethods::UnnamedOneOf
17
+
15
18
  # Initialize a new Object property
16
19
  #
17
20
  # @param name [String, Symbol] The name of the property
@@ -21,6 +24,7 @@ module Verquest
21
24
  # @param schema_options [Hash] Additional JSON schema options for this property
22
25
  def initialize(name:, required: false, nullable: false, map: nil, **schema_options)
23
26
  @properties = {}
27
+ @conditions = []
24
28
 
25
29
  @name = name.to_s
26
30
  @required = required
@@ -44,6 +48,7 @@ module Verquest
44
48
  # @param property [Verquest::Properties::Base] The property to add to this object
45
49
  # @return [Verquest::Properties::Base] The added property
46
50
  def add(property)
51
+ check_unnamed_one_of(property)
47
52
  properties[property.name.to_s] = property
48
53
  end
49
54
 
@@ -55,9 +60,10 @@ module Verquest
55
60
  name => {
56
61
  "type" => type,
57
62
  "required" => required_properties,
58
- "properties" => properties.transform_values { |property| property.to_schema[property.name] }
63
+ "properties" => all_properties.transform_values { |property| property.to_schema[property.name] }
59
64
  }.merge(schema_options).tap do |schema|
60
65
  schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
66
+ add_conditions(schema)
61
67
  end
62
68
  }
63
69
  end
@@ -71,23 +77,51 @@ module Verquest
71
77
  name => {
72
78
  "type" => type,
73
79
  "required" => required_properties,
74
- "properties" => properties.transform_values { |property| property.to_validation_schema(version: version)[property.name] }
80
+ "properties" => all_properties.transform_values { |property| property.to_validation_schema(version: version)[property.name] }
75
81
  }.merge(schema_options).tap do |schema|
76
82
  schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
83
+ add_conditions(schema)
77
84
  end
78
85
  }
79
86
  end
80
87
 
81
88
  # Create mapping for this object property and all its children
82
89
  #
90
+ # Each nested oneOf gets its own mapping in the `_oneOfs` array, so its
91
+ # metadata can't clash with another oneOf.
92
+ #
83
93
  # @param key_prefix [Array<String>] Prefix for the source key
84
94
  # @param value_prefix [Array<String>] Prefix for the target value
85
95
  # @param mapping [Hash] The mapping hash to be updated
86
96
  # @param version [String, nil] The version to create mapping for
87
97
  # @return [void]
88
98
  def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
89
- properties.values.each do |property|
90
- property.mapping(key_prefix: key_prefix + [name], value_prefix: mapping_value_prefix(value_prefix:), mapping:, version:)
99
+ all_properties.values.each do |property|
100
+ add_child_mapping(property, key_prefix: key_prefix + [name], value_prefix: mapping_value_prefix(value_prefix:), mapping:, version:)
101
+ end
102
+ end
103
+
104
+ # Adds this object's properties to a transformer layout
105
+ #
106
+ # @param key_prefix [Array<String>] Prefix for the source key
107
+ # @param value_prefix [Array<String>] Prefix for the target value
108
+ # @param layout [Verquest::Transformer::Layout] The layout to add to
109
+ # @param version [String, nil] The version to create the layout for
110
+ # @return [void]
111
+ def layout(key_prefix:, value_prefix:, layout:, version:)
112
+ all_properties.each_value do |property|
113
+ property.layout(key_prefix: key_prefix + [name], value_prefix: mapping_value_prefix(value_prefix:), layout:, version:)
114
+ end
115
+ end
116
+
117
+ # Returns the query parameter options of this object and its properties
118
+ #
119
+ # @param prefix [Array<String>] Path of the parent property
120
+ # @param version [String] The version to resolve references with
121
+ # @return [Hash{Array<String> => Hash}] The options of each property path
122
+ def query_options(prefix:, version:)
123
+ all_properties.each_value.with_object(super) do |property, options|
124
+ options.merge!(property.query_options(prefix: prefix + [name], version:))
91
125
  end
92
126
  end
93
127
 
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ module Properties
5
+ class OneOf < Base
6
+ # Builds a self-contained oneOf validation schema that keeps the discriminator
7
+ #
8
+ # OpenAPI validators (json_schemer's OpenAPI 3.0 and 3.1 dialects) select the
9
+ # discriminated variant through each oneOf entry's `$ref`, so the variants move
10
+ # into local definitions. The content-based identifier makes those `$ref`s resolve
11
+ # against this schema wherever it is embedded, and identical content always gets
12
+ # the same identifier. Other dialects ignore the discriminator and validate every variant.
13
+ #
14
+ # The identifier is set as both `$id` (draft 6 and later, OpenAPI 3.1) and `id`
15
+ # (draft 4, OpenAPI 3.0), and the variants live under `definitions`, which every
16
+ # dialect can reference. The output is the same whatever `json_schema_version` is.
17
+ #
18
+ # @example
19
+ # DiscriminatedSchema.new(definitions: {"dog" => dog_schema, "cat" => cat_schema}, discriminator: "type").to_h
20
+ # # => {
21
+ # # "$id" => "https://verquest.local/one_of/3f1c9a0b2d4e5f67",
22
+ # # "id" => "https://verquest.local/one_of/3f1c9a0b2d4e5f67",
23
+ # # "definitions" => {"dog" => dog_schema, "cat" => cat_schema},
24
+ # # "oneOf" => [{"$ref" => "#/definitions/dog"}, {"$ref" => "#/definitions/cat"}],
25
+ # # "discriminator" => {"propertyName" => "type", "mapping" => {"dog" => "#/definitions/dog", "cat" => "#/definitions/cat"}}
26
+ # # }
27
+ class DiscriminatedSchema
28
+ # Base URI for the schema identifier
29
+ ID_PREFIX = "https://verquest.local/one_of/"
30
+
31
+ # Initialize a new DiscriminatedSchema
32
+ #
33
+ # @param definitions [Hash<String, Hash>] Variant name (discriminator value) => inline validation schema
34
+ # @param discriminator [String] The discriminator property name
35
+ def initialize(definitions:, discriminator:)
36
+ @definitions = definitions
37
+ @discriminator = discriminator
38
+ end
39
+
40
+ # Builds the validation schema
41
+ #
42
+ # @return [Hash] Schema with identifiers, definitions, oneOf refs, and discriminator
43
+ def to_h
44
+ {"$id" => id, "id" => id}.merge(body)
45
+ end
46
+
47
+ private
48
+
49
+ # @!attribute [r] definitions
50
+ # @return [Hash<String, Hash>] Variant name => inline validation schema
51
+ # @!attribute [r] discriminator
52
+ # @return [String] The discriminator property name
53
+ attr_reader :definitions, :discriminator
54
+
55
+ # The schema without the identifiers
56
+ #
57
+ # @return [Hash] Definitions, oneOf refs, and discriminator
58
+ def body
59
+ @body ||= {
60
+ "definitions" => definitions,
61
+ "oneOf" => refs.values.map { |ref| {"$ref" => ref} },
62
+ "discriminator" => {"propertyName" => discriminator, "mapping" => refs}
63
+ }
64
+ end
65
+
66
+ # Content-based identifier, so identical schemas share it
67
+ #
68
+ # `allow_nan` keeps schemas with infinite or NaN numbers (e.g. `maximum: Float::INFINITY`) hashable.
69
+ #
70
+ # @return [String] The identifier URI
71
+ def id
72
+ @id ||= "#{ID_PREFIX}#{Digest::SHA256.hexdigest(JSON.generate(body, allow_nan: true))[0, 16]}"
73
+ end
74
+
75
+ # Local `$ref` for each variant, keyed by variant name
76
+ #
77
+ # @return [Hash<String, String>] Variant name => `$ref` string
78
+ def refs
79
+ @refs ||= definitions.keys.to_h { |name| [name, "#/definitions/#{name}"] }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -30,6 +30,9 @@ module Verquest
30
30
  # @example oneOf without discriminator (pure JSON Schema validation)
31
31
  # one_of = Verquest::Properties::OneOf.new(name: :value)
32
32
  # # Validates that exactly one schema matches
33
+ #
34
+ # With a discriminator, the validation schema keeps the discriminator (see DiscriminatedSchema),
35
+ # so OpenAPI 3.0 and 3.1 validators validate only the variant the discriminator selects.
33
36
  class OneOf < Base
34
37
  # @return [String, nil] The discriminator property name for schema selection
35
38
  attr_reader :discriminator
@@ -113,8 +116,33 @@ module Verquest
113
116
 
114
117
  build_variant_mappings(mapping, source_prefix, target_prefix, version)
115
118
  store_discriminator_path(mapping, source_prefix)
116
- store_variant_schemas(mapping, version) unless discriminator
117
- store_nullable_metadata(mapping, target_prefix:) if nullable
119
+ store_variant_schemas(mapping, source_prefix, version) unless discriminator
120
+ store_nullable_metadata(mapping, source_prefix:, target_prefix:) if nullable
121
+ end
122
+
123
+ # Adds this oneOf to a transformer layout
124
+ #
125
+ # Each variant gets its own layout, so it can hold oneOfs and collections.
126
+ # Like in the mapping, variant properties sit directly under the oneOf
127
+ # path, not under the variant name.
128
+ #
129
+ # @param key_prefix [Array<String>] Prefix for the source key
130
+ # @param value_prefix [Array<String>] Prefix for the target value
131
+ # @param layout [Verquest::Transformer::Layout] The layout to add to
132
+ # @param version [String, nil] The version to create the layout for
133
+ # @return [void]
134
+ def layout(key_prefix:, value_prefix:, layout:, version:)
135
+ freeze_schemas
136
+ source_prefix = compute_source_prefix(key_prefix)
137
+ target_prefix = compute_target_prefix(value_prefix)
138
+
139
+ layout.add_one_of(Transformer::Layout::OneOfLayout.new(
140
+ path: source_prefix.join("/"),
141
+ discriminator: discriminator && join_path(source_prefix, discriminator),
142
+ variants: schemas.each_value.to_h { |schema| [schema.name, variant_layout(schema, source_prefix, target_prefix, layout, version)] },
143
+ variant_schemas: (variant_schemas(version:) unless discriminator),
144
+ null_target: (target_prefix.join("/") if nullable && !source_prefix.empty?)
145
+ ))
118
146
  end
119
147
 
120
148
  # Returns validation schemas for all variants
@@ -191,7 +219,7 @@ module Verquest
191
219
  # Builds variant mappings for each schema option
192
220
  #
193
221
  # Handles both Reference and Object schemas:
194
- # - Reference: delegates to the referenced schema's mapping
222
+ # - Reference: maps the referenced request's properties under the oneOf path
195
223
  # - Object: builds mapping from child properties directly
196
224
  #
197
225
  # @param mapping [Hash] The mapping hash to populate
@@ -211,6 +239,9 @@ module Verquest
211
239
 
212
240
  # Builds mapping for a Reference variant
213
241
  #
242
+ # Maps the referenced request's properties under the oneOf path, so its
243
+ # oneOfs land in the variant's `_oneOfs` with full paths.
244
+ #
214
245
  # @param mapping [Hash] The mapping hash to populate
215
246
  # @param schema [Verquest::Properties::Reference] The reference schema
216
247
  # @param source_prefix [Array<String>] Source path prefix
@@ -218,15 +249,10 @@ module Verquest
218
249
  # @param version [String, nil] The version for schema resolution
219
250
  # @return [void]
220
251
  def build_reference_variant_mapping(mapping, schema, source_prefix, target_prefix, version)
221
- reference_mapping = schema.send(:from).mapping(version: version)
222
- reference_map = schema.send(:map)
223
- variant_target_prefix = compute_reference_target_prefix(reference_map, target_prefix)
252
+ variant_target_prefix = compute_reference_target_prefix(schema.send(:map), target_prefix)
253
+ variant_mapping = mapping[schema.name] = {}
224
254
 
225
- mapping[schema.name] = build_prefixed_mapping(
226
- reference_mapping,
227
- source_prefix,
228
- variant_target_prefix
229
- )
255
+ add_request_mapping(schema.send(:from), key_prefix: source_prefix, value_prefix: variant_target_prefix, mapping: variant_mapping, version:)
230
256
  end
231
257
 
232
258
  # Builds mapping for an inline Object variant
@@ -242,34 +268,34 @@ module Verquest
242
268
  # @param version [String, nil] The version for schema resolution
243
269
  # @return [void]
244
270
  def build_object_variant_mapping(mapping, schema, source_prefix, target_prefix, version)
245
- object_mapping = {}
246
- object_map = schema.send(:map)
247
- variant_target_prefix = compute_reference_target_prefix(object_map, target_prefix)
271
+ variant_target_prefix = compute_reference_target_prefix(schema.send(:map), target_prefix)
272
+ variant_mapping = mapping[schema.name] = {}
248
273
 
249
- # Build mapping from object's child properties
250
274
  # Properties are mapped directly under source_prefix (not source_prefix + object_name)
251
- # to match how Reference variants work
252
- schema.send(:properties).each_value do |property|
253
- property.mapping(
254
- key_prefix: source_prefix,
255
- value_prefix: variant_target_prefix,
256
- mapping: object_mapping,
257
- version: version
258
- )
275
+ # to match how Reference variants work; a nested oneOf lands in the variant's _oneOfs
276
+ schema.all_properties.each_value do |property|
277
+ add_child_mapping(property, key_prefix: source_prefix, value_prefix: variant_target_prefix, mapping: variant_mapping, version:)
259
278
  end
260
-
261
- mapping[schema.name] = object_mapping
262
279
  end
263
280
 
264
- # Builds a mapping hash with prefixes applied to all keys and values
281
+ # Adds a variant's properties to its layout, directly under the oneOf path
265
282
  #
266
- # @param base_mapping [Hash] The source mapping from the referenced schema
267
- # @param source_prefix [Array<String>] Prefix for source keys
268
- # @param target_prefix [Array<String>] Prefix for target values
269
- # @return [Hash] The mapping with prefixes applied
270
- def build_prefixed_mapping(base_mapping, source_prefix, target_prefix)
271
- base_mapping.each_with_object({}) do |(source_key, target_value), result|
272
- result[join_path(source_prefix, source_key)] = join_path(target_prefix, target_value)
283
+ # @param schema [Verquest::Properties::Reference, Verquest::Properties::Object] The variant
284
+ # @param source_prefix [Array<String>] The oneOf source path
285
+ # @param target_prefix [Array<String>] The oneOf target path
286
+ # @param layout [Verquest::Transformer::Layout] The layout holding the oneOf
287
+ # @param version [String, nil] The version for schema resolution
288
+ # @return [Verquest::Transformer::Layout] The variant layout
289
+ def variant_layout(schema, source_prefix, target_prefix, layout, version)
290
+ variant_target_prefix = compute_reference_target_prefix(schema.send(:map), target_prefix)
291
+ layout.variant.tap do |variant|
292
+ if schema.is_a?(Verquest::Properties::Reference)
293
+ add_request_layout(schema.send(:from), key_prefix: source_prefix, value_prefix: variant_target_prefix, layout: variant, version:)
294
+ else
295
+ schema.all_properties.each_value do |property|
296
+ property.layout(key_prefix: source_prefix, value_prefix: variant_target_prefix, layout: variant, version:)
297
+ end
298
+ end
273
299
  end
274
300
  end
275
301
 
@@ -295,11 +321,13 @@ module Verquest
295
321
  # schemas to determine which variant matches the input data.
296
322
  #
297
323
  # @param mapping [Hash] The mapping hash to update
324
+ # @param source_prefix [Array<String>] The source path prefix, ending with the oneOf name
298
325
  # @param version [String, nil] The version for schema resolution
299
326
  # @return [void]
300
- def store_variant_schemas(mapping, version)
327
+ def store_variant_schemas(mapping, source_prefix, version)
301
328
  mapping["_variant_schemas"] = variant_schemas(version: version)
302
- mapping["_variant_path"] = name unless root_level?
329
+ # An unnamed oneOf is only at the root when nothing references it
330
+ mapping["_variant_path"] = source_prefix.join("/") unless source_prefix.empty?
303
331
  end
304
332
 
305
333
  # Stores nullable metadata in the mapping
@@ -308,13 +336,14 @@ module Verquest
308
336
  # without attempting variant resolution.
309
337
  #
310
338
  # @param mapping [Hash] The mapping hash to update
339
+ # @param source_prefix [Array<String>] The source path prefix, ending with the oneOf name
311
340
  # @param target_prefix [Array<String>] The target path prefix for the oneOf property
312
341
  # @return [void]
313
- def store_nullable_metadata(mapping, target_prefix:)
342
+ def store_nullable_metadata(mapping, source_prefix:, target_prefix:)
314
343
  mapping["_nullable"] = true
315
- return if root_level?
344
+ return if source_prefix.empty?
316
345
 
317
- mapping["_nullable_path"] = name
346
+ mapping["_nullable_path"] = source_prefix.join("/")
318
347
  mapping["_nullable_target_path"] = target_prefix.join("/")
319
348
  end
320
349
 
@@ -369,14 +398,16 @@ module Verquest
369
398
 
370
399
  # Builds the validation schema structure with inline definitions
371
400
  #
372
- # Unlike the documentation schema, the validation schema omits the discriminator
373
- # since it's an OpenAPI extension that JSON Schema validators ignore.
374
- # Validators validate against the oneOf array directly.
401
+ # Without a discriminator, the variants are inlined in the oneOf array.
402
+ # With a discriminator, DiscriminatedSchema builds a self-contained schema
403
+ # that keeps the discriminator.
375
404
  #
376
405
  # @param version [String, nil] The version to generate validation schema for
377
- # @return [Hash] Validation schema with oneOf array (no discriminator)
406
+ # @return [Hash] Validation schema with oneOf array
378
407
  def build_validation_schema(version:)
379
- {schema_keyword => collect_inline_schemas(version)}
408
+ return {schema_keyword => collect_inline_schemas(version)} unless discriminator
409
+
410
+ DiscriminatedSchema.new(definitions: collect_inline_definitions(version), discriminator: discriminator).to_h
380
411
  end
381
412
 
382
413
  # Collects $ref schema references for all variants
@@ -391,7 +422,15 @@ module Verquest
391
422
  # @param version [String, nil] The version for schema resolution
392
423
  # @return [Array<Hash>] Array of inline schema definitions
393
424
  def collect_inline_schemas(version)
394
- schemas.values.map { |schema| schema.to_validation_schema(version: version)[schema.name] }
425
+ collect_inline_definitions(version).values
426
+ end
427
+
428
+ # Collects inline schema definitions keyed by variant name
429
+ #
430
+ # @param version [String, nil] The version for schema resolution
431
+ # @return [Hash<String, Hash>] Variant name => inline schema definition
432
+ def collect_inline_definitions(version)
433
+ schemas.transform_values { |schema| schema.to_validation_schema(version: version)[schema.name] }
395
434
  end
396
435
 
397
436
  # Adds discriminator information to the schema if present
@@ -56,37 +56,68 @@ module Verquest
56
56
  end
57
57
 
58
58
  # Create mapping for this reference property
59
- # This delegates to the referenced schema's mapping with appropriate key prefixing
59
+ #
60
+ # The referenced request's properties map themselves under this
61
+ # reference's prefixes. A reference to a single property maps that
62
+ # property as if it were declared here, with this reference's name and map:.
60
63
  #
61
64
  # @param key_prefix [Array<String>] Prefix for the source key
62
65
  # @param value_prefix [Array<String>] Prefix for the target value
63
66
  # @param mapping [Hash] The mapping hash to be updated
64
67
  # @param version [String] The version to create mapping for
65
68
  # @return [void]
69
+ # @raise [Verquest::PropertyNotFoundError] If the referenced property doesn't exist
66
70
  def mapping(key_prefix:, value_prefix:, mapping:, version:)
67
- reference_mapping = from.mapping(version:, property:).dup
68
- value_key_prefix = mapping_value_key(value_prefix:)
71
+ return add_request_mapping(from, key_prefix: key_prefix + [name], value_prefix: mapping_value_prefix(value_prefix:), mapping:, version:) unless property
69
72
 
70
- # Single field mapping
71
- if property && reference_mapping.size == 1 && !reference_mapping.keys.first.include?("/")
72
- reference_mapping = {
73
- (key_prefix + [name]).join("/") => value_key_prefix
74
- }
75
- else
76
- if value_key_prefix != "" && !value_key_prefix.end_with?("/")
77
- value_key_prefix = "#{value_key_prefix}/"
78
- end
73
+ referenced = from.resolve(version)
74
+ add_child_mapping(referenced.fetch_property(property).renamed(name:, map:), key_prefix:, value_prefix:, mapping:, version: referenced.name)
75
+ end
79
76
 
80
- reference_mapping.transform_keys! { "#{(key_prefix + [name]).join("/")}/#{_1}" }
81
- reference_mapping.transform_values! { "#{value_key_prefix}#{_1}" }
82
- end
77
+ # Adds the referenced request's properties, or the referenced property, to a transformer layout
78
+ #
79
+ # @param key_prefix [Array<String>] Prefix for the source key
80
+ # @param value_prefix [Array<String>] Prefix for the target value
81
+ # @param layout [Verquest::Transformer::Layout] The layout to add to
82
+ # @param version [String, nil] The version to create the layout for
83
+ # @return [void]
84
+ # @raise [Verquest::PropertyNotFoundError] If the referenced property doesn't exist
85
+ def layout(key_prefix:, value_prefix:, layout:, version:)
86
+ return add_request_layout(from, key_prefix: key_prefix + [name], value_prefix: mapping_value_prefix(value_prefix:), layout:, version:) unless property
87
+
88
+ referenced = from.resolve(version)
89
+ referenced.fetch_property(property).renamed(name:, map:).layout(key_prefix:, value_prefix:, layout:, version: referenced.name)
90
+ end
91
+
92
+ # Returns the query parameter options of the referenced request, or of
93
+ # the referenced property renamed to this reference
94
+ #
95
+ # This reference's own parameter examples replace the referenced ones.
96
+ #
97
+ # @param prefix [Array<String>] Path of the parent property
98
+ # @param version [String] The version to resolve references with
99
+ # @return [Hash{Array<String> => Hash}] The options of each property path
100
+ # @raise [Verquest::PropertyNotFoundError] If the referenced property doesn't exist
101
+ def query_options(prefix:, version:)
102
+ referenced = from.resolve(version)
103
+ return referenced_property_options(referenced, prefix:) if property
83
104
 
84
- mapping.merge!(reference_mapping)
105
+ referenced.all_properties.each_value.with_object(super) do |child, options|
106
+ options.merge!(child.query_options(prefix: prefix + [name], version: referenced.name))
107
+ end
85
108
  end
86
109
 
87
110
  private
88
111
 
89
112
  attr_reader :from, :property
113
+
114
+ # @return [Hash{Array<String> => Hash}] The options of the referenced property under this reference's name
115
+ def referenced_property_options(referenced, prefix:)
116
+ copy = referenced.fetch_property(property).renamed(name:, map:)
117
+ copy.parameter_examples = parameter_examples if parameter_examples
118
+
119
+ copy.query_options(prefix:, version: referenced.name)
120
+ end
90
121
  end
91
122
  end
92
123
  end