verquest 0.4.0 → 0.6.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.
@@ -20,31 +20,39 @@ module Verquest
20
20
  # @param name [String, Symbol] The name of the property
21
21
  # @param type [String, Symbol] The type of items in the array, can be a default type or a custom field type
22
22
  # @param map [String, nil] The mapping path for this property (nil for no explicit mapping)
23
- # @param required [Boolean] Whether this property is required
23
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names
24
+ # @param nullable [Boolean] Whether this property can be null
24
25
  # @param item_schema_options [Hash] Additional JSON schema options for the array items (merged with custom type options)
25
26
  # @param schema_options [Hash] Additional JSON schema options for the array property itself
26
27
  # @raise [ArgumentError] If type is not one of the allowed types (default or custom)
27
28
  # @raise [ArgumentError] If attempting to map an array to the root
28
- def initialize(name:, type:, map: nil, required: false, item_schema_options: {}, **schema_options)
29
+ def initialize(name:, type:, map: nil, required: false, nullable: false, item_schema_options: {}, **schema_options)
29
30
  raise ArgumentError, "Type must be one of #{allowed_types.join(", ")}" unless allowed_types.include?(type.to_s)
30
31
  raise ArgumentError, "You can not map array to the root" if map == "/"
31
32
 
32
33
  if (custom_type = Verquest.configuration.custom_field_types[type.to_sym])
33
- @type = custom_type[:type].to_s
34
+ @item_type = custom_type[:type].to_s
34
35
  @item_schema_options = if custom_type.key?(:schema_options)
35
36
  custom_type[:schema_options].merge(item_schema_options).transform_keys(&:to_s)
36
37
  else
37
38
  item_schema_options.transform_keys(&:to_s)
38
39
  end
39
40
  else
40
- @type = type.to_s
41
+ @item_type = type.to_s
41
42
  @item_schema_options = item_schema_options.transform_keys(&:to_s)
42
43
  end
43
44
 
44
45
  @name = name.to_s
45
46
  @map = map
46
47
  @required = required
48
+ @nullable = nullable
47
49
  @schema_options = schema_options&.transform_keys(&:to_s)
50
+
51
+ @type = if nullable
52
+ %w[array null]
53
+ else
54
+ "array"
55
+ end
48
56
  end
49
57
 
50
58
  # Generate JSON schema definition for this array property
@@ -53,8 +61,8 @@ module Verquest
53
61
  def to_schema
54
62
  {
55
63
  name => {
56
- "type" => "array",
57
- "items" => {"type" => type}.merge(item_schema_options)
64
+ "type" => type,
65
+ "items" => {"type" => item_type}.merge(item_schema_options)
58
66
  }.merge(schema_options)
59
67
  }
60
68
  end
@@ -67,12 +75,12 @@ module Verquest
67
75
  # @param version [String, nil] The version to create mapping for, defaults to configuration setting
68
76
  # @return [Hash] The updated mapping hash
69
77
  def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
70
- mapping[(key_prefix + [name]).join(".")] = mapping_value_key(value_prefix:)
78
+ mapping[(key_prefix + [name]).join("/")] = mapping_value_key(value_prefix:)
71
79
  end
72
80
 
73
81
  private
74
82
 
75
- attr_reader :type, :schema_options, :item_schema_options
83
+ attr_reader :type, :item_type, :schema_options, :item_schema_options
76
84
 
77
85
  # Gets the list of allowed item types, including both default and custom types
78
86
  #
@@ -10,6 +10,8 @@ module Verquest
10
10
  #
11
11
  # @abstract Subclass and override {#to_schema}, {#mapping} to implement
12
12
  class Base
13
+ include HelperMethods::RequiredProperties
14
+
13
15
  # @!attribute [rw] name
14
16
  # @return [String] The name of the property
15
17
  # @!attribute [rw] required
@@ -55,19 +57,23 @@ module Verquest
55
57
 
56
58
  private
57
59
 
60
+ # @!attribute [r] nullable
61
+ # @return [Boolean] Whether this property can be null
62
+ attr_reader :nullable
63
+
58
64
  # Determines the mapping target key based on mapping configuration
59
65
  # @param value_prefix [Array<String>] Prefix for the target value
60
66
  # @param collection [Boolean] Whether this is a collection mapping
61
67
  # @return [String] The target mapping key
62
68
  def mapping_value_key(value_prefix:, collection: false)
63
69
  value_key = if map.nil?
64
- (value_prefix + [name]).join(".")
70
+ (value_prefix + [name]).join("/")
65
71
  elsif map == "/"
66
72
  ""
67
73
  elsif map.start_with?("/")
68
74
  map.gsub(%r{^/}, "")
69
75
  else
70
- (value_prefix + map.split(".")).join(".")
76
+ (value_prefix + map.split("/")).join("/")
71
77
  end
72
78
 
73
79
  if collection
@@ -87,9 +93,9 @@ module Verquest
87
93
  elsif map == "/"
88
94
  []
89
95
  elsif map.start_with?("/")
90
- map.gsub(%r{^/}, "").split(".")
96
+ map.gsub(%r{^/}, "").split("/")
91
97
  else
92
- value_prefix + map.split(".")
98
+ value_prefix + map.split("/")
93
99
  end
94
100
 
95
101
  if collection && value_prefix.any?
@@ -22,11 +22,12 @@ module Verquest
22
22
  #
23
23
  # @param name [String, Symbol] The name of the property
24
24
  # @param item [Verquest::Base, nil] Optional reference to an external schema class
25
- # @param required [Boolean] Whether this property is required
25
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names
26
+ # @param nullable [Boolean] Whether this property can be null
26
27
  # @param map [String, nil] The mapping path for this property
27
28
  # @param schema_options [Hash] Additional JSON schema options for this property
28
29
  # @raise [ArgumentError] If attempting to map a collection to the root
29
- def initialize(name:, item: nil, required: false, map: nil, **schema_options)
30
+ def initialize(name:, item: nil, required: false, nullable: false, map: nil, **schema_options)
30
31
  raise ArgumentError, "You can not map collection to the root" if map == "/"
31
32
 
32
33
  @properties = {}
@@ -34,8 +35,15 @@ module Verquest
34
35
  @name = name.to_s
35
36
  @item = item
36
37
  @required = required
38
+ @nullable = nullable
37
39
  @map = map
38
40
  @schema_options = schema_options&.transform_keys(&:to_s)
41
+
42
+ @type = if nullable
43
+ %w[array null]
44
+ else
45
+ "array"
46
+ end
39
47
  end
40
48
 
41
49
  # Add a child property to this collection's item definition
@@ -60,7 +68,7 @@ module Verquest
60
68
  if has_item?
61
69
  {
62
70
  name => {
63
- "type" => "array",
71
+ "type" => type,
64
72
  "items" => {
65
73
  "$ref" => item.to_ref
66
74
  }
@@ -69,12 +77,15 @@ module Verquest
69
77
  else
70
78
  {
71
79
  name => {
72
- "type" => "array",
80
+ "type" => type,
73
81
  "items" => {
74
82
  "type" => "object",
75
- "required" => properties.values.select(&:required).map(&:name),
76
- "properties" => properties.transform_values { |property| property.to_schema[property.name] }
77
- }
83
+ "required" => required_properties,
84
+ "properties" => properties.transform_values { |property| property.to_schema[property.name] },
85
+ "additionalProperties" => Verquest.configuration.default_additional_properties
86
+ }.tap do |schema|
87
+ schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
88
+ end
78
89
  }.merge(schema_options)
79
90
  }
80
91
  end
@@ -88,19 +99,22 @@ module Verquest
88
99
  if has_item?
89
100
  {
90
101
  name => {
91
- "type" => "array",
102
+ "type" => type,
92
103
  "items" => item.to_validation_schema(version: version)
93
104
  }.merge(schema_options)
94
105
  }
95
106
  else
96
107
  {
97
108
  name => {
98
- "type" => "array",
109
+ "type" => type,
99
110
  "items" => {
100
111
  "type" => "object",
101
- "required" => properties.values.select(&:required).map(&:name),
102
- "properties" => properties.transform_values { |property| property.to_validation_schema(version: version)[property.name] }
103
- }
112
+ "required" => required_properties,
113
+ "properties" => properties.transform_values { |property| property.to_validation_schema(version: version)[property.name] },
114
+ "additionalProperties" => Verquest.configuration.default_additional_properties
115
+ }.tap do |schema|
116
+ schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
117
+ end
104
118
  }.merge(schema_options)
105
119
  }
106
120
  end
@@ -128,8 +142,8 @@ module Verquest
128
142
  value_key_prefix = mapping_value_key(value_prefix: value_prefix, collection: true)
129
143
 
130
144
  reference_mapping = item.mapping(version:).dup
131
- reference_mapping.transform_keys! { "#{(key_prefix + [name]).join(".")}[].#{_1}" }
132
- reference_mapping.transform_values! { "#{value_key_prefix}.#{_1}" }
145
+ reference_mapping.transform_keys! { "#{(key_prefix + [name]).join("/")}[]/#{_1}" }
146
+ reference_mapping.transform_values! { "#{value_key_prefix}/#{_1}" }
133
147
 
134
148
  mapping.merge!(reference_mapping)
135
149
  else
@@ -141,7 +155,7 @@ module Verquest
141
155
 
142
156
  private
143
157
 
144
- attr_reader :item, :schema_options, :properties
158
+ attr_reader :item, :schema_options, :properties, :type
145
159
  end
146
160
  end
147
161
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ module Properties
5
+ # The Const class represents a constant property with a fixed value in a JSON schema.
6
+ # It's used for properties that must have a specific, immutable value.
7
+ #
8
+ # @example
9
+ # const = Const.new(name: "type", value: "user")
10
+ class Const < Base
11
+ # Initialize a new constant property
12
+ #
13
+ # @param name [String, Symbol] The name of the constant property
14
+ # @param value [Object] The fixed value of the constant (can be any scalar value)
15
+ # @param map [Object, nil] Optional mapping information
16
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names (can be overridden by custom type)
17
+ # @param schema_options [Hash] Additional JSON schema options for this property
18
+ def initialize(name:, value:, map: nil, required: false, **schema_options)
19
+ @name = name.to_s
20
+ @value = value
21
+ @map = map
22
+ @required = required
23
+ @schema_options = schema_options&.transform_keys(&:to_s)
24
+ end
25
+
26
+ # Generate JSON schema definition for this constant
27
+ #
28
+ # @return [Hash] The schema definition for this constant
29
+ def to_schema
30
+ {
31
+ name => {
32
+ "const" => value
33
+ }.merge(schema_options)
34
+ }
35
+ end
36
+
37
+ # Create mapping for this const property
38
+ #
39
+ # @param key_prefix [Array<Symbol>] Prefix for the source key
40
+ # @param value_prefix [Array<String>] Prefix for the target value
41
+ # @param mapping [Hash] The mapping hash to be updated
42
+ # @param version [String, nil] The version to create mapping for
43
+ # @return [Hash] The updated mapping hash
44
+ def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
45
+ mapping[(key_prefix + [name]).join("/")] = mapping_value_key(value_prefix:)
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :value, :schema_options
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ module Properties
5
+ # The Enum class represents a enum property with a list of possible values in a JSON schema.
6
+ #
7
+ # @example
8
+ # enum = Enum.new(name: "type", values: ["member", "admin"])
9
+ class Enum < Base
10
+ # Initialize a new Enum property
11
+ #
12
+ # @param name [String, Symbol] The name of the property
13
+ # @param values [Array] The enum values for this property
14
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names
15
+ # @param nullable [Boolean] Whether this property can be null
16
+ # @param map [String, nil] The mapping path for this property
17
+ # @param schema_options [Hash] Additional JSON schema options for this property
18
+ # @raise [ArgumentError] If attempting to map an enum to root without a name
19
+ # @raise [ArgumentError] If values is empty
20
+ # @raise [ArgumentError] If values are not unique
21
+ # @raise [ArgumentError] If only one value is provided (should use const instead)
22
+ def initialize(name:, values:, required: false, nullable: false, map: nil, **schema_options)
23
+ raise ArgumentError, "You can not map enums to the root without a name" if map == "/"
24
+ raise ArgumentError, "Values must not be empty" if values.empty?
25
+ raise ArgumentError, "Values must be unique" if values.uniq.length != values.length
26
+ raise ArgumentError, "Use const for a single value" if values.length == 1
27
+
28
+ @name = name.to_s
29
+ @values = values
30
+ @required = required
31
+ @nullable = nullable
32
+ @map = map
33
+ @schema_options = schema_options&.transform_keys(&:to_s)
34
+
35
+ if nullable && !values.include?("null")
36
+ values << "null"
37
+ end
38
+ end
39
+
40
+ # Generate JSON schema definition for this enum
41
+ #
42
+ # @return [Hash] The schema definition for this enum
43
+ def to_schema
44
+ {
45
+ name => {"enum" => values}.merge(schema_options)
46
+ }
47
+ end
48
+
49
+ # Create mapping for this enum property
50
+ #
51
+ # @param key_prefix [Array<Symbol>] Prefix for the source key
52
+ # @param value_prefix [Array<String>] Prefix for the target value
53
+ # @param mapping [Hash] The mapping hash to be updated
54
+ # @param version [String, nil] The version to create mapping for
55
+ # @return [Hash] The updated mapping hash
56
+ def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
57
+ mapping[(key_prefix + [name]).join("/")] = mapping_value_key(value_prefix:)
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :values, :schema_options
63
+ end
64
+ end
65
+ end
@@ -24,12 +24,13 @@ module Verquest
24
24
  #
25
25
  # @param name [String, Symbol] The name of the property
26
26
  # @param type [String, Symbol] The data type for this field, can be a default type or a custom field type
27
- # @param required [Boolean] Whether this property is required (overridden by custom type if it defines required)
27
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names (can be overridden by custom type)
28
+ # @param nullable [Boolean] Whether this property can be null
28
29
  # @param map [String, nil] The mapping path for this property
29
30
  # @param schema_options [Hash] Additional JSON schema options for this property (merged with custom type options)
30
31
  # @raise [ArgumentError] If type is not one of the allowed types (default or custom)
31
32
  # @raise [ArgumentError] If attempting to map a field to root without a name
32
- def initialize(name:, type:, required: false, map: nil, **schema_options)
33
+ def initialize(name:, type:, required: false, nullable: false, map: nil, **schema_options)
33
34
  raise ArgumentError, "Type must be one of #{allowed_types.join(", ")}" unless allowed_types.include?(type.to_s)
34
35
  raise ArgumentError, "You can not map fields to the root without a name" if map == "/"
35
36
 
@@ -48,14 +49,21 @@ module Verquest
48
49
  end
49
50
 
50
51
  @name = name.to_s
52
+ @nullable = nullable
51
53
  @map = map
54
+
55
+ if nullable
56
+ @type = [@type, "null"]
57
+ end
52
58
  end
53
59
 
54
60
  # Generate JSON schema definition for this field
55
61
  #
56
62
  # @return [Hash] The schema definition for this field
57
63
  def to_schema
58
- {name => {"type" => type}.merge(schema_options)}
64
+ {
65
+ name => {"type" => type}.merge(schema_options)
66
+ }
59
67
  end
60
68
 
61
69
  # Create mapping for this field property
@@ -66,7 +74,7 @@ module Verquest
66
74
  # @param version [String, nil] The version to create mapping for
67
75
  # @return [Hash] The updated mapping hash
68
76
  def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
69
- mapping[(key_prefix + [name]).join(".")] = mapping_value_key(value_prefix:)
77
+ mapping[(key_prefix + [name]).join("/")] = mapping_value_key(value_prefix:)
70
78
  end
71
79
 
72
80
  private
@@ -15,16 +15,28 @@ module Verquest
15
15
  # Initialize a new Object property
16
16
  #
17
17
  # @param name [String, Symbol] The name of the property
18
- # @param required [Boolean] Whether this property is required
18
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names
19
+ # @param nullable [Boolean] Whether this property can be null
19
20
  # @param map [String, nil] The mapping path for this property
20
21
  # @param schema_options [Hash] Additional JSON schema options for this property
21
- def initialize(name:, required: false, map: nil, **schema_options)
22
+ def initialize(name:, required: false, nullable: false, map: nil, **schema_options)
22
23
  @properties = {}
23
24
 
24
25
  @name = name.to_s
25
26
  @required = required
27
+ @nullable = nullable
26
28
  @map = map
27
- @schema_options = schema_options&.transform_keys(&:to_s)
29
+ @schema_options = {
30
+ additionalProperties: Verquest.configuration.default_additional_properties
31
+ }.merge(schema_options)
32
+ .delete_if { |_, v| v.nil? }
33
+ .transform_keys(&:to_s)
34
+
35
+ @type = if nullable
36
+ %w[object null]
37
+ else
38
+ "object"
39
+ end
28
40
  end
29
41
 
30
42
  # Add a child property to this object
@@ -41,10 +53,12 @@ module Verquest
41
53
  def to_schema
42
54
  {
43
55
  name => {
44
- "type" => "object",
45
- "required" => properties.values.select(&:required).map(&:name),
56
+ "type" => type,
57
+ "required" => required_properties,
46
58
  "properties" => properties.transform_values { |property| property.to_schema[property.name] }
47
- }.merge(schema_options)
59
+ }.merge(schema_options).tap do |schema|
60
+ schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
61
+ end
48
62
  }
49
63
  end
50
64
 
@@ -55,10 +69,12 @@ module Verquest
55
69
  def to_validation_schema(version: nil)
56
70
  {
57
71
  name => {
58
- "type" => "object",
59
- "required" => properties.values.select(&:required).map(&:name),
72
+ "type" => type,
73
+ "required" => required_properties,
60
74
  "properties" => properties.transform_values { |property| property.to_validation_schema(version: version)[property.name] }
61
- }.merge(schema_options)
75
+ }.merge(schema_options).tap do |schema|
76
+ schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
77
+ end
62
78
  }
63
79
  end
64
80
 
@@ -26,12 +26,14 @@ module Verquest
26
26
  # @param name [String, Symbol] The name of the property
27
27
  # @param from [Class] The schema class to reference
28
28
  # @param property [Symbol, nil] Optional specific property to reference
29
+ # @param nullable [Boolean] Whether this property can be null
29
30
  # @param map [String, nil] The mapping path for this property
30
- # @param required [Boolean] Whether this property is required
31
- def initialize(name:, from:, property: nil, map: nil, required: false)
31
+ # @param required [Boolean, Array<Symbol>] Whether this property is required, or array of dependency names
32
+ def initialize(name:, from:, property: nil, nullable: false, map: nil, required: false)
32
33
  @name = name.to_s
33
34
  @from = from
34
35
  @property = property
36
+ @nullable = nullable
35
37
  @map = map
36
38
  @required = required
37
39
  end
@@ -40,9 +42,20 @@ module Verquest
40
42
  #
41
43
  # @return [Hash] The schema definition with a $ref pointer
42
44
  def to_schema
43
- {
44
- name => {"$ref" => from.to_ref(property: property)}
45
- }
45
+ if nullable
46
+ {
47
+ name => {
48
+ "oneOf" => [
49
+ {"$ref" => from.to_ref(property: property)},
50
+ {"type" => "null"}
51
+ ]
52
+ }
53
+ }
54
+ else
55
+ {
56
+ name => {"$ref" => from.to_ref(property: property)}
57
+ }
58
+ end
46
59
  end
47
60
 
48
61
  # Generate validation schema for this reference property
@@ -50,8 +63,14 @@ module Verquest
50
63
  # @param version [String, nil] The version to generate validation schema for
51
64
  # @return [Hash] The validation schema for this reference
52
65
  def to_validation_schema(version: nil)
66
+ schema = from.to_validation_schema(version:, property: property).dup
67
+
68
+ if nullable
69
+ schema["type"] = [schema["type"], "null"] unless schema["type"].include?("null")
70
+ end
71
+
53
72
  {
54
- name => from.to_validation_schema(version:, property: property)
73
+ name => schema
55
74
  }
56
75
  end
57
76
 
@@ -68,16 +87,16 @@ module Verquest
68
87
  value_key_prefix = mapping_value_key(value_prefix:)
69
88
 
70
89
  # Single field mapping
71
- if property && reference_mapping.size == 1 && !reference_mapping.keys.first.include?(".")
90
+ if property && reference_mapping.size == 1 && !reference_mapping.keys.first.include?("/")
72
91
  reference_mapping = {
73
- (key_prefix + [name]).join(".") => value_key_prefix
92
+ (key_prefix + [name]).join("/") => value_key_prefix
74
93
  }
75
94
  else
76
- if value_key_prefix != "" && !value_key_prefix.end_with?(".")
77
- value_key_prefix = "#{value_key_prefix}."
95
+ if value_key_prefix != "" && !value_key_prefix.end_with?("/")
96
+ value_key_prefix = "#{value_key_prefix}/"
78
97
  end
79
98
 
80
- reference_mapping.transform_keys! { "#{(key_prefix + [name]).join(".")}.#{_1}" }
99
+ reference_mapping.transform_keys! { "#{(key_prefix + [name]).join("/")}/#{_1}" }
81
100
  reference_mapping.transform_values! { "#{value_key_prefix}#{_1}" }
82
101
  end
83
102
 
@@ -3,13 +3,13 @@ module Verquest
3
3
  #
4
4
  # The Transformer class handles the conversion of parameter structures based on
5
5
  # a mapping of source paths to target paths. It supports deep nested structures,
6
- # array notations, and complex path expressions using dot notation.
6
+ # array notations, and complex path expressions using slash notation.
7
7
  #
8
8
  # @example Basic transformation
9
9
  # mapping = {
10
- # "user.firstName" => "user.first_name",
11
- # "user.lastName" => "user.last_name",
12
- # "addresses[].zip" => "addresses[].postal_code"
10
+ # "user/firstName" => "user/first_name",
11
+ # "user/lastName" => "user/last_name",
12
+ # "addresses[]/zip" => "addresses[]/postal_code"
13
13
  # }
14
14
  #
15
15
  # transformer = Verquest::Transformer.new(mapping: mapping)
@@ -84,13 +84,13 @@ module Verquest
84
84
  end
85
85
  end
86
86
 
87
- # Parses a dot-notation path into structured path parts
87
+ # Parses a slash-notation path into structured path parts
88
88
  # Uses memoization for performance optimization
89
89
  #
90
- # @param path [String] The dot-notation path (e.g., "user.address.street")
90
+ # @param path [String] The slash-notation path (e.g., "user/address/street")
91
91
  # @return [Array<Hash>] Array of path parts with :key and :array attributes
92
92
  def parse_path(path)
93
- path_cache[path] ||= path.split(".").map do |part|
93
+ path_cache[path] ||= path.split("/").map do |part|
94
94
  if part.end_with?("[]")
95
95
  {key: part[0...-2], array: true}
96
96
  else
@@ -19,6 +19,8 @@ module Verquest
19
19
  # # Get mapping
20
20
  # mapping = version.mapping
21
21
  class Version
22
+ include HelperMethods::RequiredProperties
23
+
22
24
  # @!attribute [r] name
23
25
  # @return [String] The name/identifier of the version (e.g., "2023-01")
24
26
  #
@@ -36,7 +38,10 @@ module Verquest
36
38
  #
37
39
  # @!attribute [r] transformer
38
40
  # @return [Verquest::Transformer] The transformer that applies the mapping
39
- attr_reader :name, :properties, :schema, :validation_schema, :mapping, :transformer
41
+ #
42
+ # @!attribute [r] external_mapping
43
+ # @return [Hash] The mapping from internal attribute paths back to external paths
44
+ attr_reader :name, :properties, :schema, :validation_schema, :mapping, :transformer, :external_mapping
40
45
 
41
46
  # @!attribute [rw] schema_options
42
47
  # @return [Hash] Additional JSON schema options for this version
@@ -102,9 +107,16 @@ module Verquest
102
107
  def prepare
103
108
  return if frozen?
104
109
 
110
+ unless schema_options.key?("additionalProperties")
111
+ schema_options["additionalProperties"] = Verquest.configuration.default_additional_properties
112
+ end
113
+
114
+ schema_options.delete_if { |_, v| v.nil? }
115
+
105
116
  prepare_schema
106
117
  prepare_validation_schema
107
118
  prepare_mapping
119
+ prepare_external_mapping
108
120
  @transformer = Transformer.new(mapping: mapping)
109
121
 
110
122
  freeze
@@ -197,9 +209,11 @@ module Verquest
197
209
  @schema = {
198
210
  "type" => "object",
199
211
  "description" => description,
200
- "required" => properties.values.select(&:required).map(&:name),
212
+ "required" => required_properties,
201
213
  "properties" => properties.transform_values { |property| property.to_schema[property.name] }
202
- }.merge(schema_options).freeze
214
+ }.merge(schema_options).tap do |schema|
215
+ schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
216
+ end.freeze
203
217
  end
204
218
 
205
219
  # Generates the validation schema for this version
@@ -212,9 +226,11 @@ module Verquest
212
226
  @validation_schema = {
213
227
  "type" => "object",
214
228
  "description" => description,
215
- "required" => properties.values.select(&:required).map(&:name),
229
+ "required" => required_properties,
216
230
  "properties" => properties.transform_values { |property| property.to_validation_schema(version: name)[property.name] }
217
- }.merge(schema_options).freeze
231
+ }.merge(schema_options).tap do |schema|
232
+ schema["dependentRequired"] = dependent_required_properties if dependent_required_properties.any?
233
+ end.freeze
218
234
  end
219
235
 
220
236
  # Prepares the parameter mapping for this version
@@ -233,5 +249,18 @@ module Verquest
233
249
  raise MappingError.new("Mapping must be unique. Found duplicates in version '#{name}': #{duplicates.join(", ")}")
234
250
  end
235
251
  end
252
+
253
+ # Prepares the inverted parameter mapping for this version
254
+ #
255
+ # Inverts the standard mapping to create a reverse lookup from internal
256
+ # attribute names back to external parameter names. This is useful when
257
+ # transforming internal data back to the external API representation.
258
+ #
259
+ # @return [Hash] The frozen inverted mapping where keys are internal attribute
260
+ # paths and values are the corresponding external schema paths
261
+ # @see #prepare_mapping
262
+ def prepare_external_mapping
263
+ @external_mapping = mapping.invert.freeze
264
+ end
236
265
  end
237
266
  end