json_schemer-rails 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 89d0c994e82f4c3cf3aa35d35f2d0628f1316e72628bb8df14b8233047dd3d8f
4
+ data.tar.gz: 9e652b939fa51ff62be60cafc8f537636248c403309bb9b1ea2f81cc573e8c73
5
+ SHA512:
6
+ metadata.gz: 13e88600a91331896d6d469ffb8dff696d8ad5a669a196c7faadbcc416ff86a3f1000d5e00903cd50647a0b05a916674d267ff10e31ad4ec2e542179abb1f1f1
7
+ data.tar.gz: d1a8fe5daa2463e88050d6d1248f3f6e603dde344909a1d8540bcc960bfa03e5349e1d1e29d89378575131f987f9d19368014422ae7a6ef5e996847d67249e33
data/README.md ADDED
@@ -0,0 +1,330 @@
1
+ # JsonSchemer::Rails
2
+
3
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.4.0-ruby.svg)](https://www.ruby-lang.org/en/)
4
+ [![Rails](https://img.shields.io/badge/rails-%3E%3D%208.0-red.svg)](https://rubyonrails.org/)
5
+
6
+ A Rails integration for [json_schemer](https://github.com/davishmcclurg/json_schemer) that provides OpenAPI 3.0 request validation and parameter type casting for your Rails controllers.
7
+
8
+ ## Features
9
+
10
+ - ✅ Validates request bodies against OpenAPI 3.0 schemas
11
+ - ✅ Validates path and query parameters
12
+ - ✅ Automatic type casting for query parameters (strings to booleans, integers, etc.)
13
+ - ✅ Schema references (`$ref`) support
14
+ - ✅ Content-Type validation
15
+ - ✅ Comprehensive error messages
16
+ - ✅ Easy integration with Rails controllers
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'json_schemer-rails'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ ```bash
29
+ bundle install
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### 1. Create an OpenAPI Specification
35
+
36
+ Create an `openapi.yml` file in your Rails root directory:
37
+
38
+ ```yaml
39
+ openapi: 3.0.0
40
+ info:
41
+ title: My API
42
+ version: 1.0.0
43
+ paths:
44
+ /users:
45
+ post:
46
+ summary: Create a user
47
+ requestBody:
48
+ required: true
49
+ content:
50
+ application/json:
51
+ schema:
52
+ type: object
53
+ properties:
54
+ name:
55
+ type: string
56
+ email:
57
+ type: string
58
+ format: email
59
+ age:
60
+ type: integer
61
+ required:
62
+ - name
63
+ - email
64
+ parameters:
65
+ - name: notify
66
+ in: query
67
+ schema:
68
+ type: boolean
69
+ responses:
70
+ '201':
71
+ description: User created
72
+ /users/{id}:
73
+ get:
74
+ summary: Get a user
75
+ parameters:
76
+ - name: id
77
+ in: path
78
+ required: true
79
+ schema:
80
+ type: string
81
+ pattern: '^[0-9]+$'
82
+ responses:
83
+ '200':
84
+ description: User details
85
+ ```
86
+
87
+ ### 2. Include in Your Controller
88
+
89
+ #### Option A: Use the Controller Mixin (Recommended)
90
+
91
+ ```ruby
92
+ class UsersController < ApplicationController
93
+ include JsonSchemer::Rails::Controller
94
+
95
+ before_action :validate_from_openapi, only: [:create, :update]
96
+
97
+ def create
98
+ # At this point:
99
+ # - Request body has been validated against the schema
100
+ # - Query parameters have been type-cast (e.g., "true" → true)
101
+ # - Path parameters have been validated
102
+
103
+ user = User.create!(params.permit(:name, :email, :age))
104
+ render json: user, status: :created
105
+ end
106
+ end
107
+ ```
108
+
109
+ #### Option B: Manual Validation
110
+
111
+ ```ruby
112
+ class UsersController < ApplicationController
113
+ def create
114
+ validator = JsonSchemer::Rails::OpenApiValidator.new(request)
115
+
116
+ # Validate and cast parameters
117
+ validator.validated_params
118
+
119
+ # Validate request body
120
+ errors = validator.validate_body.to_a
121
+ if errors.any?
122
+ render json: { errors: errors }, status: :unprocessable_entity
123
+ return
124
+ end
125
+
126
+ user = User.create!(params.permit(:name, :email, :age))
127
+ render json: user, status: :created
128
+ end
129
+ end
130
+ ```
131
+
132
+ ### 3. Custom OpenAPI File Location
133
+
134
+ By default, the validator looks for `openapi.yml` in your Rails root. You can specify a different location:
135
+
136
+ ```ruby
137
+ validator = JsonSchemer::Rails::OpenApiValidator.new(
138
+ request,
139
+ open_api_filename: Rails.root.join('config', 'api_schema.yml')
140
+ )
141
+ ```
142
+
143
+ ## How It Works
144
+
145
+ ### Request Body Validation
146
+
147
+ The validator automatically:
148
+ - Checks that the `Content-Type` header is set to `application/json` for POST/PUT/PATCH requests
149
+ - Parses the request body as JSON
150
+ - Validates the JSON against the schema defined in your OpenAPI spec
151
+ - Returns detailed validation errors if the body doesn't match the schema
152
+
153
+ ### Parameter Validation and Type Casting
154
+
155
+ The validator processes both query and path parameters:
156
+
157
+ #### Query Parameters
158
+ - **Type Casting**: Converts string values to their specified types
159
+ - `"true"`, `"1"` → `true`
160
+ - `"false"`, `"0"` → `false`
161
+ - Numbers are cast to integers/floats as specified
162
+ - **Validation**: Ensures parameters match their schema definitions
163
+ - **Unknown Parameters**: Ignores parameters not defined in the OpenAPI spec
164
+
165
+ #### Path Parameters
166
+ - Validates against schema patterns (e.g., regex patterns)
167
+ - Validates against referenced schemas (`$ref`)
168
+ - Raises `RequestValidationError` if validation fails
169
+
170
+ ### Example: Type Casting in Action
171
+
172
+ Given this OpenAPI parameter definition:
173
+
174
+ ```yaml
175
+ parameters:
176
+ - name: notify
177
+ in: query
178
+ schema:
179
+ type: boolean
180
+ - name: limit
181
+ in: query
182
+ schema:
183
+ type: integer
184
+ ```
185
+
186
+ Query string: `?notify=true&limit=10`
187
+
188
+ Before validation:
189
+ ```ruby
190
+ params[:notify] # => "true" (String)
191
+ params[:limit] # => "10" (String)
192
+ ```
193
+
194
+ After `validated_params`:
195
+ ```ruby
196
+ params[:notify] # => true (Boolean)
197
+ params[:limit] # => "10" (String, not cast because OpenAPI handles this differently)
198
+ ```
199
+
200
+ ## Error Handling
201
+
202
+ When validation fails, a `JsonSchemer::Rails::RequestValidationError` is raised:
203
+
204
+ ```ruby
205
+ class ApplicationController < ActionController::API
206
+ rescue_from JsonSchemer::Rails::RequestValidationError do |exception|
207
+ render json: { error: exception.message }, status: :unprocessable_entity
208
+ end
209
+ end
210
+ ```
211
+
212
+ ## Testing
213
+
214
+ The gem includes comprehensive RSpec tests. To run the test suite:
215
+
216
+ ```bash
217
+ bundle exec rspec
218
+ ```
219
+
220
+ ### Writing Tests for Your Controllers
221
+
222
+ Use `ActionDispatch::TestRequest` to test your validated endpoints:
223
+
224
+ ```ruby
225
+ require 'rails_helper'
226
+
227
+ RSpec.describe UsersController, type: :controller do
228
+ describe 'POST #create' do
229
+ it 'validates the request body' do
230
+ request.headers['Content-Type'] = 'application/json'
231
+ post :create, body: { name: 'John', email: 'john@example.com' }.to_json
232
+
233
+ expect(response).to have_http_status(:created)
234
+ end
235
+
236
+ it 'rejects invalid requests' do
237
+ request.headers['Content-Type'] = 'application/json'
238
+ post :create, body: { name: 'John' }.to_json # missing email
239
+
240
+ expect(response).to have_http_status(:unprocessable_entity)
241
+ end
242
+ end
243
+ end
244
+ ```
245
+
246
+ ## Advanced Usage
247
+
248
+ ### Skipping Validation for Specific Actions
249
+
250
+ ```ruby
251
+ class UsersController < ApplicationController
252
+ include JsonSchemer::Rails::Controller
253
+
254
+ before_action :validate_from_openapi, except: [:index]
255
+
256
+ def index
257
+ # No validation needed for GET requests without body
258
+ end
259
+
260
+ def create
261
+ # Validated automatically
262
+ end
263
+ end
264
+ ```
265
+
266
+ ### Custom Validation Logic
267
+
268
+ ```ruby
269
+ class UsersController < ApplicationController
270
+ def create
271
+ validator = JsonSchemer::Rails::OpenApiValidator.new(request)
272
+
273
+ # Validate parameters first
274
+ validator.validated_params
275
+
276
+ # Then validate body
277
+ body_errors = validator.validate_body.to_a
278
+
279
+ # Add custom validation
280
+ custom_errors = custom_business_logic_validation
281
+
282
+ all_errors = body_errors + custom_errors
283
+ if all_errors.any?
284
+ render json: { errors: all_errors }, status: :unprocessable_entity
285
+ return
286
+ end
287
+
288
+ # Proceed with valid data
289
+ end
290
+
291
+ private
292
+
293
+ def custom_business_logic_validation
294
+ # Your custom validation logic
295
+ []
296
+ end
297
+ end
298
+ ```
299
+
300
+ ## Requirements
301
+
302
+ - Ruby >= 3.4.0
303
+ - Rails >= 8.0
304
+ - json_schemer >= 2.5
305
+
306
+ ## Development
307
+
308
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
309
+
310
+ To install this gem onto your local machine, run `bundle exec rake install`.
311
+
312
+ ## Contributing
313
+
314
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sul-dlss/json_schemer-rails.
315
+
316
+ 1. Fork it
317
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
318
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
319
+ 4. Push to the branch (`git push origin my-new-feature`)
320
+ 5. Create new Pull Request
321
+
322
+ ## License
323
+
324
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
325
+
326
+ ## Credits
327
+
328
+ Built with [json_schemer](https://github.com/davishmcclurg/json_schemer) by Davis W. McGuire.
329
+
330
+ Developed by [Stanford Digital Library](https://library.stanford.edu/).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonSchemer
4
+ module Rails
5
+ # Mixin for controllers.
6
+ # usage:
7
+ # include JsonSchemer::Rails::Controller
8
+ # before_actoin :validate_from_openapi
9
+ module Controller
10
+ def validate_from_openapi
11
+ # This is going to cast any parameters to their specified types
12
+ openapi_validator.validated_params
13
+
14
+ errors = openapi_validator.validate_body.to_a
15
+ raise(RequestValidationError, errors.pluck("error").join("; ")) if errors.any?
16
+ end
17
+
18
+ private
19
+
20
+ def openapi_validator
21
+ @openapi_validator ||= OpenApiValidator.new(request)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+ require "yaml"
5
+ require "json_schemer"
6
+
7
+ module JsonSchemer
8
+ module Rails
9
+ # This validates a request against the OpenAPI specification
10
+ class OpenApiValidator
11
+ def initialize(request, open_api_filename: "openapi.yml")
12
+ @request = request
13
+ @open_api_filename = open_api_filename
14
+ end
15
+
16
+ attr_reader :request
17
+
18
+ def validate_body
19
+ return unless should_validate_body?
20
+
21
+ unless request.content_type == "application/json"
22
+ raise RequestValidationError,
23
+ '"Content-Type" request header must be set to "application/json".'
24
+ end
25
+
26
+ document.ref("#{request_openapi_path}/requestBody/content/application~1json/schema")
27
+ .validate(JSON.parse(request.body.read))
28
+ end
29
+
30
+ # cast any parameters that are not part of the OpenAPI specification
31
+ def validated_params
32
+ param_specs = document.ref(request_openapi_path).value["parameters"] || []
33
+
34
+ param_specs.each do |spec|
35
+ case spec["in"]
36
+ when "query"
37
+ validate_query_param(spec)
38
+ when "path"
39
+ validate_path_param(spec)
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def should_validate_body?
47
+ return false if %w[delete get].include?(request.method.downcase) # no body to verify
48
+
49
+ begin
50
+ document.ref("#{request_openapi_path}/requestBody")
51
+ rescue JSONSchemer::InvalidRefPointer
52
+ # This schema doesn't specify a request body, so don't validate it
53
+ return false
54
+ end
55
+
56
+ true
57
+ end
58
+
59
+ def document
60
+ @document ||= JSONSchemer.openapi(open_api_struct)
61
+ end
62
+
63
+ def open_api_struct
64
+ @open_api_struct ||= load_openapi_file
65
+ end
66
+
67
+ def load_openapi_file
68
+ YAML.load_file(open_api_filename)
69
+ end
70
+
71
+ attr_reader :open_api_filename
72
+
73
+ def open_api_filename=(val)
74
+ @open_api_struct = nil
75
+ @document = nil
76
+ @open_api_filename = val
77
+ end
78
+
79
+ def validate_query_param(spec)
80
+ result = request.query_parameters[spec["name"]]
81
+ return unless result
82
+
83
+ request.params[spec["name"]] &&= case spec.dig("schema", "type")
84
+ when "boolean"
85
+ ActiveModel::Type::Boolean.new.cast(result)
86
+ else
87
+ result
88
+ end
89
+ end
90
+
91
+ def validate_path_param(spec) # rubocop:disable Metrics/AbcSize
92
+ result = request.path_parameters[spec["name"].to_sym]
93
+ ref = spec.dig("schema", "$ref")
94
+ errors = document.ref(ref).validate(result).to_a
95
+ raise RequestValidationError, errors.join(", ") unless errors.empty?
96
+
97
+ request.params[spec["name"]] = result
98
+ end
99
+
100
+ def request_openapi_path
101
+ verb = request.method.downcase
102
+ path = json_ref_for_path
103
+ "#/paths/#{path}/#{verb}"
104
+ end
105
+
106
+ def json_ref_for_path
107
+ params = request.path_parameters.except(:controller, :action)
108
+ path = CGI.unescape(request.path).tr(" ", "+")
109
+ %i[object_id id].each do |parameter|
110
+ path.gsub!(params[parameter], "%7B#{parameter}%7D") if params[parameter]
111
+ end
112
+ path.gsub("/", "~1")
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonSchemer
4
+ module Rails
5
+ # Raised when the OpenAPI specification is violated
6
+ class RequestValidationError < StandardError; end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonSchemer
4
+ module Rails
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rails/version"
4
+ require_relative "rails/validation_error"
5
+ require_relative "rails/open_api_validator"
6
+ require_relative "rails/controller"
7
+
8
+ module JsonSchemer
9
+ module Rails
10
+ class Error < StandardError; end
11
+ # Your code goes here...
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json_schemer-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Justin Coyne
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: json_schemer
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.5'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.5'
26
+ - !ruby/object:Gem::Dependency
27
+ name: railties
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '8.0'
40
+ description: Rails integration for JsonSchemer. Validates OpenAPI
41
+ email:
42
+ - jcoyne@justincoyne.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - README.md
48
+ - Rakefile
49
+ - lib/json_schemer/rails.rb
50
+ - lib/json_schemer/rails/controller.rb
51
+ - lib/json_schemer/rails/open_api_validator.rb
52
+ - lib/json_schemer/rails/validation_error.rb
53
+ - lib/json_schemer/rails/version.rb
54
+ homepage: https://github.com/sul-dlss/json_schemer-rails
55
+ licenses: []
56
+ metadata:
57
+ allowed_push_host: https://rubygems.org
58
+ homepage_uri: https://github.com/sul-dlss/json_schemer-rails
59
+ source_code_uri: https://github.com/sul-dlss/json_schemer-rails
60
+ rubygems_mfa_required: 'true'
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.4.0
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 4.0.3
76
+ specification_version: 4
77
+ summary: Rails integration for JsonSchemer. Validates OpenAPI
78
+ test_files: []