verquest 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15e8b4a4b772142287128e69acb19493af2b5976bc9067b7fe5fc00aae46132c
4
- data.tar.gz: 6f940f2835e31cabc89c5a8b6ab8d7c58bc13a4bf9a12bdf006b8c51125f5a2e
3
+ metadata.gz: 602d7b11dee9986d5005901641dc12ef407b61ee172f6c60972e16bff2bcae6b
4
+ data.tar.gz: 021af67a6befd09f2375683c2641696694690fceee4954eec9c71adce48bb78f
5
5
  SHA512:
6
- metadata.gz: 978761b91ed5208803b703a939d5f63b93317f121099b65c4ef7e4cce0bd7301ba23844037269448d6252ee1ab1ac4fc20d448360cea3a23c074f76786092676
7
- data.tar.gz: bde0a68b63950fc4aed639556a03f2cec9b011adea2ae39fef76c4e9f3ff1226176480ad146a6a0a64da5e9f64c98fc337b13368364321e402363d8658ca2b12
6
+ metadata.gz: d39fc339be03bac745e9e0e353136d25940de8cca6bfe8a28224bfa64fd05db984bc957473b404fcc68b40c1d1ec62c010238b374ec204eaad7fc1d434d5ca85
7
+ data.tar.gz: 24292a5eb4c594b1f58e724987c9acc2427347152f037027c234d0f0fbff0945afaaf2aa5369e322b022ab8e61ad92302a73397dccbbeb44f24a67eb8f1e28ca
data/.yardopts ADDED
@@ -0,0 +1,8 @@
1
+ --private
2
+ --title "Verquest Documentation"
3
+ --exclude lib/verquest/gem_version.rb
4
+ lib/**/*.rb
5
+ -
6
+ LICENSE.txt
7
+ CHANGELOG.md
8
+ CODE_OF_CONDUCT.md
data/CHANGELOG.md CHANGED
@@ -1,5 +1,7 @@
1
1
  ## [Unreleased]
2
2
 
3
+ - Initial support for versions, fields, collections, and references.
4
+
3
5
  ## [0.1.0] - 2025-06-14
4
6
 
5
7
  - Initial release
data/README.md CHANGED
@@ -1,38 +1,441 @@
1
1
  # Verquest
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ [![Gem Version](https://badge.fury.io/rb/verquest.svg)](https://badge.fury.io/rb/verquest)
4
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
4
5
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/verquest`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+ Verquest is a Ruby gem that offers an elegant solution for versioning API requests. It simplifies the process of defining and evolving your API schema over time, with robust support for:
7
+
8
+ - Defining versioned request structures
9
+ - Gracefully handling API versioning
10
+ - Mapping between external and internal parameter structures
11
+ - Validating parameters against [JSON Schema](https://json-schema.org/learn)
12
+ - Generating components for OpenAPI documentation
13
+ - Mapping error keys back to the external API structure (planned feature)
14
+
15
+ > The gem is still in development. Until version 1.0, the API may change. There are some features like `oneOf`, `anyOf`, `allOf` that are not implemented yet.
6
16
 
7
17
  ## Installation
8
18
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
19
+ Add this line to your application's Gemfile:
10
20
 
11
- Install the gem and add to the application's Gemfile by executing:
21
+ ```ruby
22
+ gem "verquest", "~> 0.2"
23
+ ```
24
+
25
+ And then execute:
12
26
 
13
27
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
28
+ bundle install
15
29
  ```
16
30
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
31
+ ## Quick Start
18
32
 
19
- ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
33
+ ### Define a versioned API requests
34
+
35
+ Address Create Request
36
+ ```ruby
37
+ class AddressCreateRequest < Verquest::Base
38
+ description "Address Create Request"
39
+ schema_options additional_properties: false
40
+
41
+ version "2025-06" do # or v1 or anything you need (use a custom version_resolver if needed)
42
+ with_options type: :string, required: true do
43
+ field :street, description: "Street address"
44
+ field :city, description: "City of residence"
45
+ field :postal_code, description: "Postal code"
46
+ field :country, description: "Country of residence"
47
+ end
48
+ end
49
+ end
50
+ ````
51
+
52
+ User Create Request that uses the `AddressCreateRequest`
53
+ ```ruby
54
+ class UserCreateRequest < Verquest::Base
55
+ description "User Create Request"
56
+ schema_options additional_properties: false
57
+
58
+ version "2025-06" do # or v1 or anything you need (use a custom version_resolver if needed)
59
+ with_options type: :string, required: true do
60
+ field :first_name, description: "The first name of the user", max_length: 50
61
+ field :last_name, description: "The last name of the user", max_length: 50
62
+ field :email, format: "email", description: "The email address of the user"
63
+ end
64
+
65
+ field :birth_date, type: :string, format: "date", description: "The birth date of the user"
66
+
67
+ reference :address, from: AddressCreateRequest, required: true
68
+
69
+ collection :permissions, description: "Permissions associated with the user" do
70
+ field :name, type: :string, required: true, description: "Name of the permission"
71
+
72
+ with_options type: :boolean do
73
+ field :read, description: "Permission to read"
74
+ field :write, description: "Permission to write"
75
+ end
76
+ end
77
+
78
+ field :role, type: :string, description: "Role of the user", enum: %w[member manager], default: "member"
79
+
80
+ object :profile_details do
81
+ field :bio, type: :string, description: "Short biography of the user"
82
+
83
+ array :hobbies, type: :string, description: "Tags associated with the user"
84
+
85
+ object :social_links, description: "Some social networks" do
86
+ with_options type: :string, format: "uri" do
87
+ field :github, description: "GitHub profile URL"
88
+ field :mastodon, description: "Mastodon profile URL"
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### Example usage in Rails Controller
97
+
98
+ ```ruby
99
+
100
+ class UsersController < ApplicationController
101
+ rescue_from Verquest::InvalidParamsError, with: :handle_invalid_params
102
+
103
+ def create
104
+ result = Users::Create.call(params: user_params) # service object to handle the creation logic
105
+
106
+ if result.success?
107
+ # render success response
108
+ else
109
+ # render error response
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def user_params
116
+ UserCreateRequest.process(params, version: params[:api_version])
117
+ end
118
+ end
119
+ ```
120
+
121
+ ### JSON schema for OpenAPI
122
+ You can generate JSON Schema for your versioned requests, which can be used for API documentation:
123
+
124
+ ```ruby
125
+ UserCreateRequest.to_schema(version: "2025-06")
126
+ ```
127
+
128
+ Output:
129
+ ```ruby
130
+ {
131
+ type: :object,
132
+ description: "User Create Request",
133
+ required: [:first_name, :last_name, :email, :address],
134
+ properties: {
135
+ first_name: {type: :string, description: "The first name of the user", maxLength: 50},
136
+ last_name: {type: :string, description: "The last name of the user", maxLength: 50},
137
+ email: {type: :string, format: "email", description: "The email address of the user"},
138
+ birth_date: {type: :string, format: "date", description: "The birth date of the user"},
139
+ address: {"$ref": "#/components/schemas/AddressCreateRequest"},
140
+ permissions: {
141
+ type: :array,
142
+ items: {
143
+ type: :object,
144
+ required: [:name],
145
+ properties: {
146
+ name: {type: :string, description: "Name of the permission"},
147
+ read: {type: :boolean, description: "Permission to read"},
148
+ write: {type: :boolean, description: "Permission to write"}
149
+ }
150
+ },
151
+ description: "Permissions associated with the user"
152
+ },
153
+ role: {type: :string, description: "Role of the user", enum: ["member", "manager"], default: "member"},
154
+ profile_details: {
155
+ type: :object,
156
+ required: [],
157
+ properties: {
158
+ bio: {type: :string, description: "Short biography of the user"},
159
+ hobbies: {type: :array, items: {type: :string}, description: "Tags associated with the user"},
160
+ social_links: {
161
+ type: :object,
162
+ required: [],
163
+ properties: {
164
+ github: {type: :string, format: "uri", description: "GitHub profile URL"},
165
+ mastodon: {type: :string, format: "uri", description: "Mastodon profile URL"}
166
+ },
167
+ description: "Some social networks"
168
+ }
169
+ }
170
+ }
171
+ },
172
+ additionalProperties: false
173
+ }
174
+ ```
175
+
176
+ ### JSON schema for validation
177
+
178
+ You can check the validation JSON schema for a specific version of your request:
179
+
180
+ ```ruby
181
+ UserCreateRequest.to_validation_schema(version: "2025-06")
182
+ ```
183
+
184
+ Output:
185
+ ```ruby
186
+ {
187
+ type: :object,
188
+ description: "User Create Request",
189
+ required: [:first_name, :last_name, :email, :address],
190
+ properties: {
191
+ first_name: {type: :string, description: "The first name of the user", maxLength: 50},
192
+ last_name: {type: :string, description: "The last name of the user", maxLength: 50},
193
+ email: {type: :string, format: "email", description: "The email address of the user"},
194
+ birth_date: {type: :string, format: "date", description: "The birth date of the user"},
195
+ address: { # from the AddressCreateRequest
196
+ type: :object,
197
+ description: "Address Create Request",
198
+ required: [:street, :city, :postal_code, :country],
199
+ properties: {
200
+ street: {type: :string, description: "Street address"},
201
+ city: {type: :string, description: "City of residence"},
202
+ postal_code: {type: :string, description: "Postal code"},
203
+ country: {type: :string, description: "Country of residence"}
204
+ },
205
+ additionalProperties: false
206
+ },
207
+ permissions: {
208
+ type: :array,
209
+ items: {
210
+ type: :object,
211
+ required: [:name],
212
+ properties: {
213
+ name: {type: :string, description: "Name of the permission"},
214
+ read: {type: :boolean, description: "Permission to read"},
215
+ write: {type: :boolean, description: "Permission to write"}
216
+ }
217
+ },
218
+ description: "Permissions associated with the user"
219
+ },
220
+ role: {type: :string, description: "Role of the user", enum: ["member", "manager"], default: "member"},
221
+ profile_details: {
222
+ type: :object,
223
+ required: [],
224
+ properties: {
225
+ bio: {type: :string, description: "Short biography of the user"},
226
+ hobbies: {type: :array, items: {type: :string}, description: "Tags associated with the user"},
227
+ social_links: {
228
+ type: :object,
229
+ required: [],
230
+ properties: {
231
+ github: {type: :string, format: "uri", description: "GitHub profile URL"},
232
+ mastodon: {type: :string, format: "uri", description: "Mastodon profile URL"}
233
+ },
234
+ description: "Some social networks"
235
+ }
236
+ }
237
+ }
238
+ },
239
+ additionalProperties: false
240
+ }
241
+ ```
242
+
243
+ You can also validate it to ensure it meets the JSON Schema standards:
244
+
245
+ ```ruby
246
+ UserCreateRequest.validate_schema(version: "2025-06") # => true/false
247
+ ```
248
+
249
+ ## Core Features
250
+
251
+ ### Schema Definition and Validation
252
+
253
+ See the example above for how to define a request schema. Verquest provides a DSL to define your API requests with various component types and helper methods based on JSON Schema, which is also used in [OpenAPI specification](https://swagger.io/specification/#schema-object-examples) for components.
254
+
255
+ The JSON schema can be used for both validation of incoming parameters and for generating OpenAPI documentation components.
256
+
257
+ #### Component types
258
+
259
+ - `field`: Represents a scalar value (string, integer, boolean, etc.).
260
+ - `object`: Represents a JSON object with properties.
261
+ - `array`: Represents a JSON array with scalar items.
262
+ - `collection`: Represents a array of objects defined manually or by a reference to another request.
263
+ - `reference`: Represents a reference to another request, allowing you to reuse existing request structures.
264
+
265
+ #### Helper methods
266
+
267
+ - `description`: Adds a description to the request or per version.
268
+ - `schema_options`: Allows you to set additional options for the JSON Schema, such as `additional_properties` for request or per version. All fields (except `reference`) can be defined with options like `required`, `format`, `min_lenght`, `max_length`, etc. all in snake case.
269
+ - `with_options`: Allows you to define multiple fields with the same options, reducing repetition.
270
+
271
+ ### Versioning
272
+
273
+ Verquest allows you to define multiple versions of your API requests, making it easy to evolve your API over time:
274
+
275
+ ```ruby
276
+ class UserCreateRequest < Verquest::Base
277
+ version "2025-04" do
278
+ field :name, type: :string, required: true
279
+ field :email, type: :string, format: "email", required: true
280
+
281
+ field :street, type: :string
282
+ field :city, type: :string
283
+ end
284
+
285
+ # Implicit inheritance from the previous version
286
+ version "2025-06", exclude_properties: %i[street city] do
287
+ field :phone, type: :string
288
+
289
+ # Replace street and city with a structured address object with zip
290
+ object :address do
291
+ field :street, type: :string
292
+ field :city, type: :string
293
+ field :zip, type: :string
294
+ end
295
+ end
296
+
297
+ # Disabled inheritance, `inherit` can also be set to a specific version
298
+ version "2025-08", inherit: false do
299
+ field :name, type: :string, required: true
300
+ field :email, type: :string, format: "email", required: true
301
+ field :phone, type: :string
302
+
303
+ # Replace address with a more structured version
304
+ object :address do
305
+ field :street_line1, type: :string, required: true
306
+ field :street_line2, type: :string
307
+ field :city, type: :string, required: true
308
+ field :state, type: :string
309
+ field :postal_code, type: :string, required: true
310
+ field :country, type: :string, required: true
311
+ end
312
+ end
313
+ end
314
+ ```
315
+
316
+ Internal `Verquest::VersionResolver` is then used to resolve the right version for the one specified in the call. It implements a "downgrading" strategy - when an exact version match isn't found, it returns the closest earlier version.
317
+
318
+ Example:
319
+ ```ruby
320
+ UserCreateRequest.process(params, version: "2025-05") # => use the defined version "2025-04"
321
+ UserCreateRequest.process(params, version: "2025-06") # => use the defined version "2025-06"
322
+ UserCreateRequest.process(params, version: "2025-07") # => use the closest earlier version "2025-06"
323
+ UserCreateRequest.process(params, version: "2025-08") # => use the defined version "2025-08"
324
+ UserCreateRequest.process(params, version: "2025-10") # => use the closest earlier version "2025-08"
325
+ ```
326
+
327
+ This is used across all referenced requests, so if you have a `UserRequest` that references an `AddressCreateRequest`, it will also resolve the correct version of the `AddressCreateRequest` based on the initial requested version (as the `AddressCreateRequest` can have different versions defined).
328
+
329
+ The goal here is to avoid redefining the same request structure in multiple versions when there are no changes, and to facilitate the easy evolution of API requests over time. When a new API version is created and there are no changes to the requests, you don't need to update anything.
330
+
331
+ ### Mapping request structure
332
+
333
+ Verquest's mapping system allows transforming external API request structures into your internal application data structures.
334
+
335
+ Here’s a short example: we store the address in the same table as the user internally, but the API request structure is different.
336
+
337
+ ```ruby
338
+ class UserCreateRequest < Verquest::Base
339
+ version "2025-06", exclude_properties: %i[street city] do
340
+ field :full_name, type: :string, map: "name"
341
+ field :email, type: :string, format: "email", required: true
342
+ field :phone, type: :string
343
+
344
+ object :address do
345
+ field :street, type: :string, map: "/address_street"
346
+ field :city, type: :string, map: "/address_city"
347
+ field :postal_code, type: :string, map: "/address_zip"
348
+ end
349
+ end
350
+ end
351
+ ```
352
+
353
+ When called with `UserCreateRequest.process(params)`, the `address` object will be mapped to the internal structure with keys `address_street`, `address_city`, and `address_zip`.
354
+
355
+ Example request params
356
+ ```ruby
357
+ {
358
+ "full_name": "John Doe",
359
+ "email": "john@doe.com",
360
+ "phone": "1234567890",
361
+ "address": {
362
+ "street": "123 Main St",
363
+ "city": "Springfield",
364
+ "postal_code": "12345"
365
+ }
366
+ }
367
+ ```
368
+
369
+ Will be transformed to:
370
+ ```ruby
371
+ {
372
+ "name": "John Doe",
373
+ "email": "john@doe.com",
374
+ "phone": "1234567890",
375
+ "address_street": "123 Main St",
376
+ "address_city": "Springfield",
377
+ "address_zip": "12345"
378
+ }
379
+ ````
380
+
381
+ What you can use:
382
+ - `/` to reference the root of the request structure
383
+ - `nested.structure` use dot notation to reference nested structures
384
+ - if the `map` is not set, the field name will be used as the key in the internal structure
385
+
386
+ There are some limitations and the implementation can be improved, but it should works for most common use cases.
387
+
388
+ See the mapping test (in `test/verquest/base_test.rb`) for more examples of mapping.
389
+
390
+ ### Component Generation for OpenAPI
391
+
392
+ Generate JSON Schema, component name and reference for OpenAPI documentation:
393
+
394
+ ```ruby
395
+ UserCreateRequest.component_name # => "UserCreateRequest"
396
+ UserCreateRequest.to_ref # => "#/components/schemas/UserCreateRequest"
397
+ component_schema = UserCreateRequest.to_schema(version: "2025-06")
398
+ ```
399
+
400
+ ## Configuration
401
+
402
+ Configure Verquest globally:
403
+
404
+ ```ruby
405
+ Verquest.configure do |config|
406
+ # Enable validation by default
407
+ config.validate_params = true # default
408
+
409
+ # Set the default version to use
410
+ config.current_version = -> { Current.api_version }
411
+
412
+ # Set the JSON Schema version
413
+ config.json_schema_version = :draft6 # default
414
+
415
+ # Set the error handling strategy for processing params
416
+ config.validation_error_handling = :raise # default, can be set also to :result
417
+
418
+ # Remove extra root keys from provided params
419
+ config.remove_extra_root_keys = true # default
420
+
421
+ # Set custom version resolver
422
+ config.version_resolver = CustomeVersionResolver # default is `Verquest::VersionResolver`
423
+ end
21
424
  ```
22
425
 
23
- ## Usage
426
+ ## Documentation
24
427
 
25
- TODO: Write usage instructions here
428
+ For detailed documentation, please visit the [YARD documentation](https://www.rubydoc.info/gems/verquest).
26
429
 
27
430
  ## Development
28
431
 
29
432
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
433
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
434
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `gem_version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
435
 
33
436
  ## Contributing
34
437
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/verquest. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/verquest/blob/main/CODE_OF_CONDUCT.md).
438
+ Bug reports and pull requests are welcome on GitHub at https://github.com/CiTroNaK/verquest. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/CiTroNaK/verquest/blob/main/CODE_OF_CONDUCT.md).
36
439
 
37
440
  ## License
38
441
 
@@ -40,4 +443,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
40
443
 
41
444
  ## Code of Conduct
42
445
 
43
- Everyone interacting in the Verquest project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/verquest/blob/main/CODE_OF_CONDUCT.md).
446
+ Everyone interacting in the Verquest project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/CiTroNaK/verquest/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verquest
4
+ # Helper methods for Verquest::Base class methods
5
+ #
6
+ # This module contains utility methods for working with string and hash
7
+ # transformations. It provides methods for converting between different
8
+ # naming conventions (snake_case to camelCase) which is particularly useful
9
+ # when working with JSON Schema properties.
10
+ #
11
+ # @api private
12
+ module Base::HelperClassMethods
13
+ # Converts hash keys from snake_case to camelCase format
14
+ #
15
+ # Transforms all keys in the given hash from snake_case (e.g., "max_length")
16
+ # to camelCase (e.g., "maxLength") format, which is commonly used in JSON Schema.
17
+ # The transformation happens in place, modifying the original hash.
18
+ #
19
+ # @param hash [Hash] The hash containing snake_case keys
20
+ # @return [Hash] The same hash with keys transformed to camelCase
21
+ def camelize(hash)
22
+ hash.transform_keys! { |key| snake_to_camel(key.to_s).to_sym }
23
+ end
24
+
25
+ # Converts a snake_case string to camelCase
26
+ #
27
+ # Takes a string in snake_case format (e.g., "max_length") and converts it
28
+ # to camelCase format (e.g., "maxLength") by capitalizing each word after
29
+ # the first one and removing underscores.
30
+ #
31
+ # @param str [String] The snake_case string to convert
32
+ # @return [String] The converted camelCase string
33
+ def snake_to_camel(str)
34
+ str.split("_").inject { |memo, word| memo + word.capitalize }
35
+ end
36
+ end
37
+ end