Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for query parameters named "some[thing]" #75

Merged
merged 4 commits into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 0.10.0
- Add support for query parameters named `"some[thing]"` ([issue](https://github.com/ahx/openapi_first/issues/40))

## 0.9.0
- Make request validation usable standalone

Expand Down
17 changes: 9 additions & 8 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
PATH
remote: .
specs:
openapi_first (0.9.0)
openapi_first (0.10.0)
deep_merge (>= 1.2.1)
hanami-router (~> 2.0.alpha2)
hanami-utils (~> 2.0.alpha1)
json_schemer (~> 0.2)
Expand All @@ -12,12 +13,12 @@ PATH
GEM
remote: https://rubygems.org/
specs:
activesupport (6.0.2.2)
activesupport (6.0.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2)
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
ast (2.4.0)
Expand All @@ -28,7 +29,7 @@ GEM
diff-lcs (1.3)
ecma-re-validator (0.2.1)
regexp_parser (~> 1.2)
hana (1.3.5)
hana (1.3.6)
hanami-router (2.0.0.alpha2)
mustermann (~> 1.0)
mustermann-contrib (~> 1.0)
Expand Down Expand Up @@ -66,7 +67,7 @@ GEM
mustermann-contrib (~> 1.1.1)
nokogiri
parallel (1.19.1)
parser (2.7.1.1)
parser (2.7.1.2)
ast (~> 2.4.0)
pry (0.13.1)
coderay (~> 1.1)
Expand All @@ -83,15 +84,15 @@ GEM
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-core (3.9.2)
rspec-support (~> 3.9.3)
rspec-expectations (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-support (3.9.2)
rspec-support (3.9.3)
rubocop (0.82.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
Expand Down
11 changes: 6 additions & 5 deletions benchmarks/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
PATH
remote: ..
specs:
openapi_first (0.9.0)
openapi_first (0.10.0)
deep_merge (>= 1.2.1)
hanami-router (~> 2.0.alpha2)
hanami-utils (~> 2.0.alpha1)
json_schemer (~> 0.2)
Expand All @@ -12,15 +13,15 @@ PATH
GEM
remote: https://rubygems.org/
specs:
activesupport (6.0.2.2)
activesupport (6.0.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2)
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
benchmark-ips (2.7.2)
benchmark-ips (2.8.2)
benchmark-memory (0.1.2)
memory_profiler (~> 0.9)
builder (3.2.4)
Expand Down Expand Up @@ -61,7 +62,7 @@ GEM
mustermann-grape (~> 1.0.0)
rack (>= 1.3.0)
rack-accept
hana (1.3.5)
hana (1.3.6)
hanami-router (2.0.0.alpha2)
mustermann (~> 1.0)
mustermann-contrib (~> 1.0)
Expand Down
28 changes: 23 additions & 5 deletions lib/openapi_first/operation.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'forwardable'
require_relative 'utils'

module OpenapiFirst
class Operation
Expand Down Expand Up @@ -39,14 +40,31 @@ def content_type_for(status)
def build_parameters_json_schema
return unless @operation.parameters&.any?

@operation.parameters.each_with_object(
@operation.parameters.each_with_object(new_node) do |parameter, schema|
params = Rack::Utils.parse_nested_query(parameter.name)
generate_schema(schema, params, parameter)
end
end

def generate_schema(schema, params, parameter)
params.each do |key, value|
schema['required'] << key if parameter.required
if value.is_a? Hash
property_schema = new_node
generate_schema(property_schema, value, parameter)
Utils.deep_merge!(schema['properties'], { key => property_schema })
else
schema['properties'][key] = parameter.schema
end
end
end

def new_node
{
'type' => 'object',
'required' => [],
'properties' => {}
) do |parameter, schema|
schema['required'] << parameter.name if parameter.required
schema['properties'][parameter.name] = parameter.schema
end
}
end
end
end
5 changes: 5 additions & 0 deletions lib/openapi_first/utils.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# frozen_string_literal: true

require 'hanami/utils/string'
require 'deep_merge/core'

module OpenapiFirst
module Utils
def self.deep_merge!(dest, source)
DeepMerge.deep_merge!(source, dest)
end

def self.underscore(string)
Hanami::Utils::String.underscore(string)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/openapi_first/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module OpenapiFirst
VERSION = '0.9.0'
VERSION = '0.10.0'
end
1 change: 1 addition & 0 deletions openapi_first.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Gem::Specification.new do |spec|
spec.bindir = 'exe'
spec.require_paths = ['lib']

spec.add_dependency 'deep_merge', '>= 1.2.1'
spec.add_dependency 'hanami-router', '~> 2.0.alpha2'
spec.add_dependency 'hanami-utils', '~> 2.0.alpha1'
spec.add_dependency 'json_schemer', '~> 0.2'
Expand Down
99 changes: 99 additions & 0 deletions spec/data/parameters-flat.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
openapi: "3.0.2"
info:
version: 1.0.0
title: Search example
contact:
name: Contact Name
email: contact@example.com
url: https://example.com/
servers:
- url: http://example.com
tags:
- name: search
description: Search
paths:
/search:
get:
summary: Search for pets
operationId: search
tags:
- search
parameters:
- name: term
in: query
description: The term you want to search for
required: true
schema:
type: string
- name: filter[tag]
in: query
required: true
schema:
type: string
- name: filter[other]
in: query
required: false
schema:
type: string
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
- name: birthdate
in: query
description: Search for a pet born on this date
required: false
schema:
type: string
format: date
- name: include
in: query
description: Relations you want to include
required: false
schema:
type: string
pattern: (parents|children)+(,(parents|children))*
responses:
"200":
description: A paged array of pets
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/info:
get:
summary: Get some info
operationId: info
tags:
- search
responses:
"200":
description: ok

components:
schemas:
Pets:
type: array
items:
$ref: "#/components/schemas/Pet"
Pet:
$ref: "./components/schemas/pet.yaml#/Pet"
Error:
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
49 changes: 48 additions & 1 deletion spec/operation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
let(:spec) { OpenapiFirst.load('./spec/data/parameters.yaml') }

describe '#parameters_json_schema' do
let(:schema) do
described_class.new(spec.operations.first).parameters_json_schema
end

let(:expected_schema) do
{
'type' => 'object',
Expand Down Expand Up @@ -46,9 +50,52 @@
end

it 'returns the JSON Schema for the request' do
schema = described_class.new(spec.operations.first).parameters_json_schema
expect(schema).to eq expected_schema
end

describe 'with flat named nested[params]' do
let(:spec) { OpenapiFirst.load('./spec/data/parameters-flat.yaml') }

let(:expected_schema) do
{
'type' => 'object',
'required' => %w[term filter],
'properties' => {
'birthdate' => {
'format' => 'date',
'type' => 'string'
},
'filter' => {
'type' => 'object',
'required' => ['tag'],
'properties' => {
'tag' => {
'type' => 'string'
},
'other' => {
'type' => 'string'
}
}
},
'include' => {
'type' => 'string',
'pattern' => '(parents|children)+(,(parents|children))*'
},
'limit' => {
'type' => 'integer',
'format' => 'int32'
},
'term' => {
'type' => 'string'
}
}
}
end

it 'converts it to a nested schema' do
expect(schema).to eq expected_schema
end
end
end

describe '#content_type_for' do
Expand Down
33 changes: 31 additions & 2 deletions spec/request_validation/parameter_validation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
'/search'
end

let(:spec) { OpenapiFirst.load('./spec/data/search.yaml') }

let(:app) do
oas = spec
Rack::Builder.app do
spec = OpenapiFirst.load('./spec/data/search.yaml')
use OpenapiFirst::Router, spec: spec
use OpenapiFirst::Router, spec: oas
use OpenapiFirst::RequestValidation
run lambda { |_env|
Rack::Response.new('hello', 200).finish
Expand Down Expand Up @@ -97,6 +99,33 @@
expect(last_request.env[OpenapiFirst::PARAMETERS]).to eq params
end

describe 'with nested[param]' do
let(:spec) { OpenapiFirst.load('./spec/data/parameters-flat.yaml') }

let(:params) do
{
'term' => 'Oscar',
'filter' => { 'tag' => 'dogs', 'other' => 'things' }
}
end

it 'returns 400 if nested[parameter] is missing' do
params['filter'].delete('tag')
get path, params

expect(last_response.status).to eq 400
error = response_body[:errors][0]
expect(error[:source][:parameter]).to eq 'filter'
expect(error[:title]).to eq 'is missing required properties: tag'
end

it 'passes if query parameters are valid' do
get path, params

expect(last_response.status).to eq 200
end
end

describe 'type conversion' do
def last_params
last_request.env[OpenapiFirst::PARAMETERS]
Expand Down