verquest 0.1.0 → 0.2.1

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.
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ # Private class methods for Verquest::Base
5
+ #
6
+ # This module contains internal class methods used by the versioning system
7
+ # that are not intended for direct use by client code.
8
+ #
9
+ # @api private
10
+ module Base::PrivateClassMethods
11
+ # Resolves the version to use, either from the provided version,
12
+ # configuration's current_version, or raises an error if none is available
13
+ #
14
+ # @param version [String, nil] The specific version to resolve
15
+ # @return [Verquest::Version] The resolved version object
16
+ # @raise [ArgumentError] If no version is provided and no current_version is configured
17
+ def resolve(version)
18
+ if version.nil? && Verquest.configuration.current_version
19
+ version = instance_exec(&Verquest.configuration.current_version)
20
+ elsif version.nil?
21
+ raise ArgumentError, "Version must be provided or set by Verquest.configuration.current_version"
22
+ end
23
+
24
+ versions.resolve(version)
25
+ end
26
+
27
+ private
28
+
29
+ # @return [Verquest::Version, Verquest::Properties::Object] The current scope being defined
30
+ attr_reader :current_scope
31
+
32
+ # @return [Hash] Default options for property definitions
33
+ # Default options used when using teh with_options method.
34
+ attr_reader :default_options
35
+
36
+ # Returns the versions container, initializing it if needed
37
+ #
38
+ # @return [Verquest::Versions] The versions container
39
+ def versions
40
+ @versions ||= Versions.new
41
+ end
42
+
43
+ # Defines a new version with optional inheritance from another version
44
+ #
45
+ # @param name [String] The name/identifier of the version
46
+ # @param inherit [Boolean, String] Whether to inherit from current scope or specific version name
47
+ # @param exclude_properties [Array<Symbol>] Properties to exclude when inheriting
48
+ # @yield Block defining the version's structure and properties
49
+ # @return [void]
50
+ def version(name, inherit: true, exclude_properties: [], &block)
51
+ version = Version.new(name:)
52
+ versions.add(version)
53
+
54
+ if inherit && @current_scope
55
+ version.copy_from(@current_scope, exclude_properties:)
56
+ elsif inherit.is_a?(String)
57
+ inherit_version = versions.resolve(inherit)
58
+ version.copy_from(inherit_version, exclude_properties:)
59
+ end
60
+
61
+ @default_options = {}
62
+ @current_scope = version
63
+
64
+ instance_exec(&block)
65
+ ensure
66
+ version.description ||= versions.description
67
+ version.schema_options = versions.schema_options.merge(version.schema_options)
68
+ version.prepare
69
+ end
70
+
71
+ # Sets the description for the current version scope or globally
72
+ #
73
+ # @param text [String] The description text
74
+ # @return [void]
75
+ # @raise [RuntimeError] If called outside of a version scope
76
+ def description(text)
77
+ if current_scope.nil?
78
+ versions.description = text
79
+ elsif current_scope.is_a?(Version)
80
+ current_scope.description = text
81
+ else
82
+ raise "Description can only be set within a version scope or globally"
83
+ end
84
+ end
85
+
86
+ # Sets additional schema options for the current version scope or globally
87
+ #
88
+ # @param schema_options [Hash] The schema options to set
89
+ # @return [void]
90
+ # @raise [RuntimeError] If called outside of a version scope
91
+ def schema_options(**schema_options)
92
+ camelize(schema_options)
93
+
94
+ if current_scope.nil?
95
+ versions.schema_options = schema_options
96
+ elsif current_scope.is_a?(Version)
97
+ current_scope.schema_options = schema_options
98
+ else
99
+ raise "Additional properties can only be set within a version scope or globally"
100
+ end
101
+ end
102
+
103
+ # Executes the given block with the specified options, temporarily overriding
104
+ # the default options for the duration of the block
105
+ #
106
+ # @param options [Hash] The options to set during the block execution
107
+ # @yield Block to be executed with the temporary options
108
+ # @return [void]
109
+ def with_options(**options, &block)
110
+ camelize(options)
111
+
112
+ original_options = default_options
113
+ @default_options = options.except(:map)
114
+
115
+ instance_exec(&block)
116
+ ensure
117
+ @default_options = original_options
118
+ end
119
+
120
+ # Defines a new field for the current version scope
121
+ #
122
+ # @param name [Symbol] The name of the field
123
+ # @param type [Symbol] The data type of the field
124
+ # @param map [String, nil] An optional mapping to another field
125
+ # @param required [Boolean] Whether the field is required
126
+ # @param schema_options [Hash] Additional schema options for the field
127
+ # @return [void]
128
+ def field(name, type: nil, map: nil, required: false, **schema_options)
129
+ camelize(schema_options)
130
+
131
+ type = default_options.fetch(:type, type)
132
+ required = default_options.fetch(:required, required)
133
+ schema_options = default_options.except(:type, :required).merge(schema_options)
134
+
135
+ field = Properties::Field.new(name:, type:, map:, required:, **schema_options)
136
+ current_scope.add(field)
137
+ end
138
+
139
+ # Defines a new object for the current version scope
140
+ #
141
+ # @param name [Symbol] The name of the object
142
+ # @param map [String, nil] An optional mapping to another object
143
+ # @param required [Boolean] Whether the object is required
144
+ # @param schema_options [Hash] Additional schema options for the object
145
+ # @yield Block executed in the context of the new object definition
146
+ # @return [void]
147
+ def object(name, map: nil, required: false, **schema_options, &block)
148
+ unless block_given?
149
+ raise ArgumentError, "a block must be given to define the object"
150
+ end
151
+
152
+ camelize(schema_options)
153
+ required = default_options.fetch(:required, required)
154
+ schema_options = default_options.except(:type, :required).merge(schema_options)
155
+
156
+ object = Properties::Object.new(name:, map:, required:, **schema_options)
157
+ current_scope.add(object)
158
+
159
+ if block_given?
160
+ previous_scope = current_scope
161
+ @current_scope = object
162
+
163
+ instance_exec(&block)
164
+ end
165
+ ensure
166
+ @current_scope = previous_scope if block_given?
167
+ end
168
+
169
+ # Defines a new collection for the current version scope
170
+ #
171
+ # @param name [Symbol] The name of the collection
172
+ # @param item [Class, nil] The item type in the collection
173
+ # @param required [Boolean] Whether the collection is required
174
+ # @param map [String, nil] An optional mapping to another collection
175
+ # @param schema_options [Hash] Additional schema options for the collection
176
+ # @yield Block executed in the context of the new collection definition
177
+ # @return [void]
178
+ def collection(name, item: nil, required: false, map: nil, **schema_options, &block)
179
+ if item.nil? && !block_given?
180
+ raise ArgumentError, "item must be provided or a block must be given to define the collection"
181
+ elsif !item.nil? && !block_given? && !(item <= Verquest::Base)
182
+ raise ArgumentError, "item must be a child of Verquest::Base class or nil" unless type.is_a?(Verquest::Properties::Base)
183
+ end
184
+
185
+ camelize(schema_options)
186
+ required = default_options.fetch(:required, required)
187
+ schema_options = default_options.except(:required).merge(schema_options)
188
+
189
+ collection = Properties::Collection.new(name:, item:, required:, map:, **schema_options)
190
+ current_scope.add(collection)
191
+
192
+ if block_given?
193
+ previous_scope = current_scope
194
+ @current_scope = collection
195
+
196
+ instance_exec(&block)
197
+ end
198
+ ensure
199
+ @current_scope = previous_scope if block_given?
200
+ end
201
+
202
+ # Defines a new reference for the current version scope
203
+ #
204
+ # @param name [Symbol] The name of the reference
205
+ # @param from [Verquest::Base] The source of the reference
206
+ # @param property [Symbol, nil] An optional specific property to reference
207
+ # @param map [String, nil] An optional mapping to another reference
208
+ # @param required [Boolean] Whether the reference is required
209
+ # @return [void]
210
+ def reference(name, from:, property: nil, map: nil, required: false)
211
+ required = default_options.fetch(:required, required)
212
+
213
+ reference = Properties::Reference.new(name:, from:, property:, map:, required:)
214
+ current_scope.add(reference)
215
+ end
216
+
217
+ # Defines a new array property for the current version scope
218
+ #
219
+ # @param name [Symbol] The name of the array property
220
+ # @param type [String] The data type of the array elements
221
+ # @param required [Boolean] Whether the array property is required
222
+ # @param map [String, nil] An optional mapping to another array property
223
+ # @param schema_options [Hash] Additional schema options for the array property
224
+ # @return [void]
225
+ def array(name, type:, required: false, map: nil, **schema_options)
226
+ camelize(schema_options)
227
+
228
+ type = default_options.fetch(:type, type)
229
+ required = default_options.fetch(:required, required)
230
+ schema_options = default_options.except(:type, :required).merge(schema_options)
231
+
232
+ array = Properties::Array.new(name:, type:, required:, map:, **schema_options)
233
+ current_scope.add(array)
234
+ end
235
+
236
+ # Excludes specified properties from the current scope by removing them
237
+ # from the version's property set
238
+ #
239
+ # @param names [Array<Symbol>] The names of the properties to exclude
240
+ # @return [void]
241
+ def exclude_properties(*names)
242
+ names.each do |name|
243
+ current_scope.remove(name)
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ # Public class methods to be included in Verquest::Base
5
+ #
6
+ # This module contains class methods that handle parameter mapping, schema generation,
7
+ # and validation functionality for Verquest API request objects.
8
+ module Base::PublicClassMethods
9
+ # Maps incoming parameters to the appropriate structure based on version mapping
10
+ #
11
+ # @param params [Hash] The parameters to be mapped
12
+ # @param version [String, nil] Specific version to use, defaults to configuration setting
13
+ # @param validate [Boolean, nil] Whether to validate the params, defaults to configuration setting
14
+ # @param remove_extra_root_keys [Boolean, nil] Whether to remove extra keys at the root level, defaults to configuration setting
15
+ # @return [Verquest::Result] Success result with mapped params or failure result with validation errors
16
+ def process(params, version: nil, validate: nil, remove_extra_root_keys: nil)
17
+ validate = Verquest.configuration.validate_params if validate.nil?
18
+ remove_extra_root_keys = Verquest.configuration.remove_extra_root_keys if remove_extra_root_keys.nil?
19
+
20
+ version_class = resolve(version)
21
+
22
+ params = params.dup
23
+ params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h)
24
+ params = params.slice(*version_class.properties.keys) if remove_extra_root_keys
25
+
26
+ if validate && (validation_result = version_class.validate_params(params: params, component_reference: to_ref, remove_extra_root_keys: remove_extra_root_keys)) && validation_result.any?
27
+ case Verquest.configuration.validation_error_handling
28
+ when :raise
29
+ raise InvalidParamsError.new("Validation failed", errors: validation_result)
30
+ when :result
31
+ Result.failure(validation_result)
32
+ end
33
+ else
34
+ mapped_params = version_class.map_params(params)
35
+
36
+ case Verquest.configuration.validation_error_handling
37
+ when :raise
38
+ mapped_params
39
+ when :result
40
+ Result.success(mapped_params)
41
+ end
42
+ end
43
+ end
44
+
45
+ # Returns the JSON schema for the request
46
+ #
47
+ # @param version [String, nil] Specific version to use, defaults to configuration setting
48
+ # @return [Hash] The JSON schema for the request
49
+ def to_schema(version: nil)
50
+ resolve(version).schema
51
+ end
52
+
53
+ # Returns the validation JSON schema for the request or a specific property. It contains all schemas from references.
54
+ #
55
+ # @param version [String, nil] Specific version to use, defaults to configuration setting
56
+ # @param property [Symbol, nil] Specific property to retrieve schema for
57
+ # @return [Hash] The validation schema or property schema
58
+ def to_validation_schema(version: nil, property: nil)
59
+ version = resolve(version)
60
+
61
+ if property
62
+ version.validation_schema[:properties][property]
63
+ else
64
+ version.validation_schema
65
+ end
66
+ end
67
+
68
+ # Validates the generated JSON schema structure
69
+ #
70
+ # @param version [String, nil] Specific version to use, defaults to configuration setting
71
+ # @return [Boolean] True if schema is valid
72
+ def validate_schema(version: nil)
73
+ resolve(version).validate_schema
74
+ end
75
+
76
+ # Returns the mapping for a specific version or property
77
+ #
78
+ # @param version [String, nil] Specific version to use, defaults to configuration setting
79
+ # @param property [Symbol, nil] Specific property to retrieve mapping for
80
+ # @return [Hash] The mapping configuration
81
+ def mapping(version: nil, property: nil)
82
+ version = resolve(version)
83
+
84
+ if property
85
+ version.mapping_for(property)
86
+ else
87
+ version.mapping
88
+ end
89
+ end
90
+
91
+ # Returns the JSON reference for the request or a specific property
92
+ #
93
+ # @param property [Symbol, nil] Specific property to retrieve reference for
94
+ # @return [String] The JSON reference for the request or property
95
+ def to_ref(property: nil)
96
+ base = "#/components/schemas/#{component_name}"
97
+
98
+ property ? "#{base}/properties/#{property}" : base
99
+ end
100
+
101
+ # Returns the component name derived from the class name. It is used in JSON schema references.
102
+ #
103
+ # @return [String] The component name
104
+ def component_name
105
+ name.to_s.split("::", 2).last.tr("::", "")
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ # Base class for API request definition and mapping
5
+ #
6
+ # This class is the foundation of the Verquest versioning system. Classes that inherit from Base
7
+ # can define their request structure using the versioning DSL, including fields, objects,
8
+ # collections, and references. The Base class handles parameter mapping, schema generation,
9
+ # and validation based on version specifications.
10
+ #
11
+ # @example Define a versioned API request class
12
+ # class UserCreateRequest < Verquest::Base
13
+ # description "User Create Request"
14
+ # schema_options additional_properties: false
15
+ #
16
+ # version "2025-06" do
17
+ # field :email, type: :string, required: true, format: "email"
18
+ # field :name, type: :string
19
+ #
20
+ # object :address do
21
+ # field :street, type: :string, map: "/address_street"
22
+ # field :city, type: :string, required: true, map: "/address_city"
23
+ # field :zip_code, type: :string, map: "/address_zip_code"
24
+ # end
25
+ # end
26
+ #
27
+ # version "2025-08", exclude_properties: %i[name] do
28
+ # field :name, type: :string, required: true
29
+ # end
30
+ # end
31
+ #
32
+ # @see Verquest::Base::PublicClassMethods for available class methods
33
+ class Base
34
+ extend Base::HelperClassMethods
35
+ extend Base::PrivateClassMethods
36
+ extend Base::PublicClassMethods
37
+ end
38
+ end
@@ -0,0 +1,73 @@
1
+ module Verquest
2
+ # Configuration for the Verquest gem
3
+ #
4
+ # This class manages configuration settings for the Verquest gem, including
5
+ # validation behavior, JSON Schema version, and version resolution strategy.
6
+ # It's used to customize the behavior of versioned API requests.
7
+ #
8
+ # @example Basic configuration
9
+ # Verquest.configure do |config|
10
+ # config.validate_params = true
11
+ # config.current_version = -> { Current.api_version }
12
+ # end
13
+ class Configuration
14
+ # @!attribute [rw] validate_params
15
+ # Controls whether parameters are automatically validated against the schema
16
+ # @return [Boolean] true if validation is enabled, false otherwise
17
+ #
18
+ # @!attribute [rw] json_schema_version
19
+ # The JSON Schema draft version to use for validation and schema generation (see the json-schema gem)
20
+ # @return [Symbol] The JSON Schema version (e.g., :draft4, :draft5)
21
+ #
22
+ # @!attribute [rw] validation_error_handling
23
+ # Controls how errors during parameter processing are handled
24
+ # @return [Symbol] :raise to raise errors (default) or :result to return errors in the Result object
25
+ #
26
+ # @!attribute [rw] remove_extra_root_keys
27
+ # Controls if extra root keys not defined in the schema should be removed from the parameters
28
+ # @return [Boolean] true if extra keys should be removed, false otherwise
29
+ attr_accessor :validate_params, :json_schema_version, :validation_error_handling, :remove_extra_root_keys
30
+
31
+ # @!attribute [r] current_version
32
+ # A callable object that returns the current API version to use when not explicitly specified
33
+ # @return [#call] An object responding to call that determines the current version
34
+ #
35
+ # @!attribute [r] version_resolver
36
+ # The resolver used to map version strings/identifiers to version objects
37
+ # @return [#call] An object that responds to `call` for resolving versions
38
+ attr_reader :current_version, :version_resolver
39
+
40
+ # Initialize a new Configuration with default values
41
+ #
42
+ # @return [Configuration] A new configuration instance with default settings
43
+ def initialize
44
+ @validate_params = true
45
+ @json_schema_version = :draft6
46
+ @validation_error_handling = :raise # or :result
47
+ @remove_extra_root_keys = true
48
+ @version_resolver = VersionResolver
49
+ end
50
+
51
+ # Sets the current version strategy using a callable object
52
+ #
53
+ # @param current_version [#call] An object that returns the current version when called
54
+ # @raise [ArgumentError] If the provided value doesn't respond to call
55
+ # @return [#call] The callable object that was set
56
+ def current_version=(current_version)
57
+ raise ArgumentError, "The current_version must respond to a call method" unless current_version.respond_to?(:call)
58
+
59
+ @current_version = current_version
60
+ end
61
+
62
+ # Sets the version resolver
63
+ #
64
+ # @param version_resolver [#call] An object with a call method for resolving versions
65
+ # @raise [ArgumentError] If the provided resolver doesn't respond to call
66
+ # @return [#call] The resolver that was set
67
+ def version_resolver=(version_resolver)
68
+ raise ArgumentError, "The version_resolver must respond to a call method" unless version_resolver.respond_to?(:call)
69
+
70
+ @version_resolver = version_resolver
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ GEM_VERSION = "0.2.1"
5
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ module Properties
5
+ # Array property type for schema generation and mapping
6
+ #
7
+ # Represents an array data structure in the schema with specified item type.
8
+ # Used to define arrays of scalar types (string, number, integer, boolean).
9
+ #
10
+ # @example Define an array of strings
11
+ # array = Verquest::Properties::Array.new(
12
+ # name: :tags,
13
+ # type: :string,
14
+ # required: true
15
+ # )
16
+ class Array < Base
17
+ # Initialize a new Array property
18
+ #
19
+ # @param name [Symbol] The name of the property
20
+ # @param type [Symbol] The type of items in the array
21
+ # @param map [String, nil] The mapping path for this property (nil for no explicit mapping)
22
+ # @param required [Boolean] Whether this property is required
23
+ # @param schema_options [Hash] Additional JSON schema options for this property
24
+ # @raise [ArgumentError] If attempting to map an array to the root
25
+ def initialize(name:, type:, map: nil, required: false, **schema_options)
26
+ raise ArgumentError, "You can not map array to the root" if map == "/"
27
+
28
+ @name = name
29
+ @type = type
30
+ @map = map
31
+ @required = required
32
+ @schema_options = schema_options
33
+ end
34
+
35
+ # Generate JSON schema definition for this array property
36
+ #
37
+ # @return [Hash] The schema definition for this array property
38
+ def to_schema
39
+ {
40
+ name => {
41
+ type: :array,
42
+ items: {type: type}
43
+ }.merge(schema_options)
44
+ }
45
+ end
46
+
47
+ # Create mapping for this array property
48
+ #
49
+ # @param key_prefix [Array<Symbol>] Prefix for the source key
50
+ # @param value_prefix [Array<Symbol>] Prefix for the target value
51
+ # @param mapping [Hash] The mapping hash to be updated
52
+ # @param version [String, nil] The version to create mapping for, defaults to configuration setting
53
+ # @return [Hash] The updated mapping hash
54
+ def mapping(key_prefix:, value_prefix:, mapping:, version: nil)
55
+ mapping[(key_prefix + [name]).join(".")] = mapping_value_key(value_prefix:)
56
+ end
57
+
58
+ private
59
+
60
+ attr_reader :type, :schema_options
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ module Properties
5
+ # Base class for all property types
6
+ #
7
+ # This abstract class defines the interface for all property types
8
+ # in the Verquest schema system. All property classes should inherit
9
+ # from this base class and implement its required methods.
10
+ #
11
+ # @abstract Subclass and override {#to_schema}, {#mapping} to implement
12
+ class Base
13
+ # @!attribute [rw] name
14
+ # @return [Symbol] The name of the property
15
+ # @!attribute [rw] required
16
+ # @return [Boolean] Whether this property is required
17
+ # @!attribute [rw] map
18
+ # @return [String, nil] The mapping path for this property
19
+ attr_accessor :name, :required, :map
20
+
21
+ # Adds a child property to this property
22
+ # @abstract
23
+ # @param property [Verquest::Properties::Base] The property to add
24
+ # @raise [NoMethodError] This is an abstract method that must be overridden
25
+ def add(property)
26
+ raise NoMethodError
27
+ end
28
+
29
+ # Generates JSON schema for this property
30
+ # @abstract
31
+ # @return [Hash] The schema definition for this property
32
+ # @raise [NoMethodError] This is an abstract method that must be overridden
33
+ def to_schema
34
+ raise NoMethodError
35
+ end
36
+
37
+ # Generates validation schema for this property, defaults to the same as `to_schema`
38
+ # @param version [String, nil] The version to generate validation schema for
39
+ # @return [Hash] The validation schema for this property
40
+ def to_validation_schema(version: nil)
41
+ to_schema
42
+ end
43
+
44
+ # Creates mapping for this property
45
+ # @abstract
46
+ # @param key_prefix [Array<Symbol>] Prefix for the source key
47
+ # @param value_prefix [Array<String>] Prefix for the target value
48
+ # @param mapping [Hash] The mapping hash to be updated
49
+ # @param version [String, nil] The version to create mapping for
50
+ # @return [Hash] The updated mapping hash
51
+ # @raise [NoMethodError] This is an abstract method that must be overridden
52
+ def mapping(key_prefix:, value_prefix:, mapping:, version:)
53
+ raise NoMethodError
54
+ end
55
+
56
+ private
57
+
58
+ # Determines the mapping target key based on mapping configuration
59
+ # @param value_prefix [Array<String>] Prefix for the target value
60
+ # @param collection [Boolean] Whether this is a collection mapping
61
+ # @return [String] The target mapping key
62
+ def mapping_value_key(value_prefix:, collection: false)
63
+ value_key = if map.nil?
64
+ (value_prefix + [name]).join(".")
65
+ elsif map == "/"
66
+ ""
67
+ elsif map.start_with?("/")
68
+ map.gsub(%r{^/}, "")
69
+ else
70
+ (value_prefix + map.split(".")).join(".")
71
+ end
72
+
73
+ if collection
74
+ value_key + "[]"
75
+ else
76
+ value_key
77
+ end
78
+ end
79
+
80
+ # Determines the mapping target value prefix based on mapping configuration
81
+ # @param value_prefix [Array<String>] Prefix for the target value
82
+ # @param collection [Boolean] Whether this is a collection mapping
83
+ # @return [Array<String>] The target mapping value prefix
84
+ def mapping_value_prefix(value_prefix:, collection: false)
85
+ value_prefix = if map.nil?
86
+ value_prefix + [name]
87
+ elsif map == "/"
88
+ []
89
+ elsif map.start_with?("/")
90
+ map.gsub(%r{^/}, "").split(".")
91
+ else
92
+ value_prefix + map.split(".")
93
+ end
94
+
95
+ if collection && value_prefix.any?
96
+ last = value_prefix.pop
97
+ value_prefix.push((last.to_s + "[]").to_sym)
98
+ end
99
+
100
+ value_prefix
101
+ end
102
+ end
103
+ end
104
+ end