committee 1.15.0 → 2.0.0.pre
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 +4 -4
- data/bin/committee-stub +10 -36
- data/lib/committee/bin/committee_stub.rb +61 -0
- data/lib/committee/drivers/hyper_schema.rb +151 -0
- data/lib/committee/drivers/open_api_2.rb +297 -0
- data/lib/committee/drivers.rb +57 -0
- data/lib/committee/middleware/base.rb +52 -13
- data/lib/committee/middleware/request_validation.rb +33 -10
- data/lib/committee/middleware/response_validation.rb +2 -1
- data/lib/committee/middleware/stub.rb +24 -8
- data/lib/committee/request_validator.rb +1 -1
- data/lib/committee/response_generator.rb +58 -13
- data/lib/committee/response_validator.rb +32 -8
- data/lib/committee/router.rb +5 -33
- data/lib/committee/{query_params_coercer.rb → string_params_coercer.rb} +11 -6
- data/lib/committee/test/methods.rb +49 -12
- data/lib/committee.rb +15 -1
- data/test/bin/committee_stub_test.rb +45 -0
- data/test/bin_test.rb +20 -0
- data/test/committee_test.rb +49 -0
- data/test/drivers/hyper_schema_test.rb +95 -0
- data/test/drivers/open_api_2_test.rb +255 -0
- data/test/drivers_test.rb +60 -0
- data/test/middleware/base_test.rb +49 -5
- data/test/middleware/request_validation_test.rb +39 -25
- data/test/middleware/response_validation_test.rb +32 -20
- data/test/middleware/stub_test.rb +50 -19
- data/test/request_unpacker_test.rb +10 -0
- data/test/request_validator_test.rb +4 -3
- data/test/response_generator_test.rb +50 -6
- data/test/response_validator_test.rb +29 -4
- data/test/router_test.rb +40 -13
- data/test/{query_params_coercer_test.rb → string_params_coercer_test.rb} +3 -4
- data/test/test/methods_test.rb +44 -5
- data/test/test_helper.rb +59 -1
- metadata +62 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5911eb64b8a07bb45eaf264399bebeb95ea2395b
|
4
|
+
data.tar.gz: 35d800d2dd5cd0c3d402b26813d7b156f0809006
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86c361cc0bd1f4d0e340335402ab452b99a0c0d0eae814aa3296794ed0a076177f9dc3ebc772ae95a66afbe45efcbeccc77e2851ef3a0e385171fc549e31092c
|
7
|
+
data.tar.gz: 306a67c72463d3879dd7fb4e9aff88a17d34e54d83c209a439cabc47b912027178c5d23fc9b5270cc6b0f9d65d4957f152ec630c7e3484931bba1bda46ed4bd0
|
data/bin/committee-stub
CHANGED
@@ -6,45 +6,19 @@ require 'yaml'
|
|
6
6
|
require_relative "../lib/committee"
|
7
7
|
|
8
8
|
args = ARGV.dup
|
9
|
-
|
10
|
-
|
11
|
-
opts.banner = "Usage: rackup [options] [JSON Schema file]"
|
9
|
+
bin = Committee::Bin::CommitteeStub.new
|
10
|
+
options, parser = bin.get_options_parser
|
12
11
|
|
13
|
-
|
14
|
-
|
12
|
+
if $0 == __FILE__
|
13
|
+
parser.parse!(args)
|
15
14
|
|
16
|
-
|
17
|
-
puts
|
15
|
+
unless args.count == 1 || options[:help]
|
16
|
+
puts parser.to_s
|
18
17
|
exit
|
19
|
-
}
|
20
|
-
|
21
|
-
opts.on("-t", "--tolerant", "don't perform request/response validations") {
|
22
|
-
options[:tolerant] = true
|
23
|
-
}
|
24
|
-
|
25
|
-
opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port|
|
26
|
-
options[:port] = port
|
27
|
-
}
|
28
|
-
end
|
29
|
-
opt_parser.parse!(args)
|
30
|
-
|
31
|
-
unless args.count == 1
|
32
|
-
puts opt_parser.to_s
|
33
|
-
exit
|
34
|
-
end
|
35
|
-
|
36
|
-
schema = ::YAML.load(File.read(args[0]))
|
37
|
-
|
38
|
-
app = Rack::Builder.new {
|
39
|
-
unless options[:tolerant]
|
40
|
-
use Committee::Middleware::RequestValidation, schema: schema
|
41
|
-
use Committee::Middleware::ResponseValidation, schema: schema
|
42
18
|
end
|
43
19
|
|
44
|
-
|
45
|
-
|
46
|
-
[404, {}, ["Not found"]]
|
47
|
-
}
|
48
|
-
}
|
20
|
+
driver = Committee::Drivers.driver_from_name(options[:driver])
|
21
|
+
schema = driver.parse(::YAML.load(File.read(args[0])))
|
49
22
|
|
50
|
-
Rack::Server.start(app:
|
23
|
+
Rack::Server.start(app: bin.get_app(schema, options), Port: options[:port])
|
24
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Committee
|
2
|
+
module Bin
|
3
|
+
# CommitteeStub internalizes the functionality of bin/committee-stub so
|
4
|
+
# that we can test code that would otherwise be difficult to get at in an
|
5
|
+
# executable.
|
6
|
+
class CommitteeStub
|
7
|
+
# Gets a Rack app suitable for use as a stub.
|
8
|
+
def get_app(schema, options)
|
9
|
+
Rack::Builder.new {
|
10
|
+
unless options[:tolerant]
|
11
|
+
use Committee::Middleware::RequestValidation, schema: schema
|
12
|
+
use Committee::Middleware::ResponseValidation, schema: schema
|
13
|
+
end
|
14
|
+
|
15
|
+
use Committee::Middleware::Stub, schema: schema
|
16
|
+
|
17
|
+
run lambda { |_|
|
18
|
+
[404, {}, ["Not found"]]
|
19
|
+
}
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
# Gets an option parser for command line arguments.
|
24
|
+
def get_options_parser
|
25
|
+
options = {
|
26
|
+
:driver => :hyper_schema,
|
27
|
+
:help => false,
|
28
|
+
:port => 9292,
|
29
|
+
:tolerant => false,
|
30
|
+
}
|
31
|
+
|
32
|
+
parser = OptionParser.new do |opts|
|
33
|
+
opts.banner = "Usage: rackup [options] [JSON Schema file]"
|
34
|
+
|
35
|
+
opts.separator ""
|
36
|
+
opts.separator "Options:"
|
37
|
+
|
38
|
+
opts.on_tail("-h", "-?", "--help", "Show this message") {
|
39
|
+
options[:help] = true
|
40
|
+
}
|
41
|
+
|
42
|
+
opts.on("-d", "--driver NAME", "name of driver [open_api_2|hyper_schema]") { |name|
|
43
|
+
options[:driver] = name.to_sym
|
44
|
+
}
|
45
|
+
|
46
|
+
opts.on("-t", "--tolerant", "don't perform request/response validations") {
|
47
|
+
options[:tolerant] = true
|
48
|
+
}
|
49
|
+
|
50
|
+
opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port|
|
51
|
+
options[:port] = port
|
52
|
+
}
|
53
|
+
end
|
54
|
+
[options, parser]
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_schema(driver_name, data)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module Committee::Drivers
|
2
|
+
class HyperSchema < Committee::Drivers::Driver
|
3
|
+
# Whether parameters in a request's path will be considered and coerced by
|
4
|
+
# default.
|
5
|
+
def default_path_params
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
# Whether parameters in a request's query string will be considered and
|
10
|
+
# coerced by default.
|
11
|
+
def default_query_params
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
:hyper_schema
|
17
|
+
end
|
18
|
+
|
19
|
+
# Parses an API schema and builds a set of route definitions for use with
|
20
|
+
# Committee.
|
21
|
+
#
|
22
|
+
# The expected input format is a data hash with keys as strings (as opposed
|
23
|
+
# to symbols) like the kind produced by JSON.parse or YAML.load.
|
24
|
+
def parse(schema)
|
25
|
+
# Really we'd like to only have data hashes passed into drivers these
|
26
|
+
# days, but here we handle a JsonSchema::Schema for now to maintain
|
27
|
+
# backward compatibility (this library used to be hyper-schema only).
|
28
|
+
if schema.is_a?(JsonSchema::Schema)
|
29
|
+
hyper_schema = schema
|
30
|
+
else
|
31
|
+
hyper_schema = JsonSchema.parse!(schema)
|
32
|
+
hyper_schema.expand_references!
|
33
|
+
end
|
34
|
+
|
35
|
+
schema = Schema.new
|
36
|
+
schema.driver = self
|
37
|
+
schema.routes = build_routes(hyper_schema)
|
38
|
+
schema
|
39
|
+
end
|
40
|
+
|
41
|
+
def schema_class
|
42
|
+
Committee::Drivers::HyperSchema::Schema
|
43
|
+
end
|
44
|
+
|
45
|
+
# Link abstracts an API link specifically for JSON hyper-schema.
|
46
|
+
#
|
47
|
+
# For most operations, it's a simple pass through to a
|
48
|
+
# JsonSchema::Schema::Link, but implements some exotic behavior in a few
|
49
|
+
# places.
|
50
|
+
class Link
|
51
|
+
def initialize(hyper_schema_link)
|
52
|
+
self.hyper_schema_link = hyper_schema_link
|
53
|
+
end
|
54
|
+
|
55
|
+
# The link's input media type. i.e. How requests should be encoded.
|
56
|
+
def enc_type
|
57
|
+
hyper_schema_link.enc_type
|
58
|
+
end
|
59
|
+
|
60
|
+
def href
|
61
|
+
hyper_schema_link.href
|
62
|
+
end
|
63
|
+
|
64
|
+
# The link's output media type. i.e. How responses should be encoded.
|
65
|
+
def media_type
|
66
|
+
hyper_schema_link.media_type
|
67
|
+
end
|
68
|
+
|
69
|
+
def method
|
70
|
+
hyper_schema_link.method
|
71
|
+
end
|
72
|
+
|
73
|
+
# Passes through a link's parent resource. Note that this is *not* part
|
74
|
+
# of the Link interface and is here to support a legacy Heroku-ism
|
75
|
+
# behavior that allowed a link tagged with rel=instances to imply that a
|
76
|
+
# list will be returned.
|
77
|
+
def parent
|
78
|
+
hyper_schema_link.parent
|
79
|
+
end
|
80
|
+
|
81
|
+
def rel
|
82
|
+
hyper_schema_link.rel
|
83
|
+
end
|
84
|
+
|
85
|
+
# The link's input schema. i.e. How we validate an endpoint's incoming
|
86
|
+
# parameters.
|
87
|
+
def schema
|
88
|
+
hyper_schema_link.schema
|
89
|
+
end
|
90
|
+
|
91
|
+
def status_success
|
92
|
+
hyper_schema_link.rel == "create" ? 201 : 200
|
93
|
+
end
|
94
|
+
|
95
|
+
# The link's output schema. i.e. How we validate an endpoint's response
|
96
|
+
# data.
|
97
|
+
def target_schema
|
98
|
+
hyper_schema_link.target_schema
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
attr_accessor :hyper_schema_link
|
104
|
+
end
|
105
|
+
|
106
|
+
class Schema < Committee::Drivers::Schema
|
107
|
+
# A link back to the derivative instace of Committee::Drivers::Driver
|
108
|
+
# that create this schema.
|
109
|
+
attr_accessor :driver
|
110
|
+
|
111
|
+
attr_accessor :routes
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def build_routes(hyper_schema)
|
117
|
+
routes = {}
|
118
|
+
|
119
|
+
hyper_schema.links.each do |link|
|
120
|
+
method, href = parse_link(link)
|
121
|
+
next unless method
|
122
|
+
|
123
|
+
rx = %r{^#{href}$}
|
124
|
+
Committee.log_debug "Created route: #{method} #{href} (regex #{rx})"
|
125
|
+
|
126
|
+
routes[method] ||= []
|
127
|
+
routes[method] << [rx, Link.new(link)]
|
128
|
+
end
|
129
|
+
|
130
|
+
# recursively iterate through all `properties` subschemas to build a
|
131
|
+
# complete routing table
|
132
|
+
hyper_schema.properties.each do |_, subschema|
|
133
|
+
routes.merge!(build_routes(subschema)) { |_, r1, r2| r1 + r2 }
|
134
|
+
end
|
135
|
+
|
136
|
+
routes
|
137
|
+
end
|
138
|
+
|
139
|
+
def href_to_regex(href)
|
140
|
+
href.gsub(/\{(.*?)\}/, "[^/]+")
|
141
|
+
end
|
142
|
+
|
143
|
+
def parse_link(link)
|
144
|
+
return nil, nil if !link.method || !link.href
|
145
|
+
method = link.method.to_s.upcase
|
146
|
+
# /apps/{id} --> /apps/([^/]+)
|
147
|
+
href = href_to_regex(link.href)
|
148
|
+
[method, href]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
module Committee::Drivers
|
2
|
+
class OpenAPI2 < Committee::Drivers::Driver
|
3
|
+
# Whether parameters in a request's path will be considered and coerced by
|
4
|
+
# default.
|
5
|
+
def default_path_params
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
# Whether parameters in a request's query string will be considered and
|
10
|
+
# coerced by default.
|
11
|
+
def default_query_params
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
:open_api_2
|
17
|
+
end
|
18
|
+
|
19
|
+
# Parses an API schema and builds a set of route definitions for use with
|
20
|
+
# Committee.
|
21
|
+
#
|
22
|
+
# The expected input format is a data hash with keys as strings (as opposed
|
23
|
+
# to symbols) like the kind produced by JSON.parse or YAML.load.
|
24
|
+
def parse(data)
|
25
|
+
REQUIRED_FIELDS.each do |field|
|
26
|
+
if !data[field]
|
27
|
+
raise ArgumentError, "Committee: no #{field} section in spec data."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if data['swagger'] != '2.0'
|
32
|
+
raise ArgumentError, "Committee: driver requires OpenAPI 2.0."
|
33
|
+
end
|
34
|
+
|
35
|
+
schema = Schema.new
|
36
|
+
schema.driver = self
|
37
|
+
|
38
|
+
schema.base_path = data['basePath'] || ''
|
39
|
+
|
40
|
+
# Arbitrarily choose the first media type found in these arrays. This
|
41
|
+
# appraoch could probably stand to be improved, but at least users will
|
42
|
+
# for now have the option of turning media type validation off if they so
|
43
|
+
# choose.
|
44
|
+
schema.consumes = data['consumes'].first
|
45
|
+
schema.produces = data['produces'].first
|
46
|
+
|
47
|
+
schema.definitions, store = parse_definitions!(data)
|
48
|
+
schema.routes = parse_routes!(data, schema, store)
|
49
|
+
|
50
|
+
schema
|
51
|
+
end
|
52
|
+
|
53
|
+
def schema_class
|
54
|
+
Committee::Drivers::OpenAPI2::Schema
|
55
|
+
end
|
56
|
+
|
57
|
+
# Link abstracts an API link specifically for OpenAPI 2.
|
58
|
+
class Link
|
59
|
+
# The link's input media type. i.e. How requests should be encoded.
|
60
|
+
attr_accessor :enc_type
|
61
|
+
|
62
|
+
attr_accessor :href
|
63
|
+
|
64
|
+
# The link's output media type. i.e. How responses should be encoded.
|
65
|
+
attr_accessor :media_type
|
66
|
+
|
67
|
+
attr_accessor :method
|
68
|
+
|
69
|
+
# The link's input schema. i.e. How we validate an endpoint's incoming
|
70
|
+
# parameters.
|
71
|
+
attr_accessor :schema
|
72
|
+
|
73
|
+
attr_accessor :status_success
|
74
|
+
|
75
|
+
# The link's output schema. i.e. How we validate an endpoint's response
|
76
|
+
# data.
|
77
|
+
attr_accessor :target_schema
|
78
|
+
|
79
|
+
def rel
|
80
|
+
raise "Committee: rel not implemented for OpenAPI"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# ParameterSchemaBuilder converts OpenAPI 2 link parameters, which are not
|
85
|
+
# quite JSON schemas (but will be in OpenAPI 3) into synthetic schemas that
|
86
|
+
# we can use to do some basic request validation.
|
87
|
+
class ParameterSchemaBuilder
|
88
|
+
def initialize(link_data)
|
89
|
+
self.link_data = link_data
|
90
|
+
end
|
91
|
+
|
92
|
+
def call
|
93
|
+
link_schema = JsonSchema::Schema.new
|
94
|
+
link_schema.properties = {}
|
95
|
+
link_schema.required = []
|
96
|
+
|
97
|
+
if link_data["parameters"]
|
98
|
+
link_data["parameters"].each do |param_data|
|
99
|
+
LINK_REQUIRED_FIELDS.each do |field|
|
100
|
+
if !param_data[field]
|
101
|
+
raise ArgumentError,
|
102
|
+
"Committee: no #{field} section in link data."
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
param_schema = JsonSchema::Schema.new
|
107
|
+
|
108
|
+
# We could probably use more validation here, but the formats of
|
109
|
+
# OpenAPI 2 are based off of what's available in JSON schema, and
|
110
|
+
# therefore this should map over quite well.
|
111
|
+
param_schema.type = [param_data["type"]]
|
112
|
+
|
113
|
+
# And same idea: despite parameters not being schemas, the items
|
114
|
+
# key (if preset) is actually a schema that defines each item of an
|
115
|
+
# array type, so we can just reflect that directly onto our
|
116
|
+
# artifical schema.
|
117
|
+
if param_data["type"] == "array" && param_data["items"]
|
118
|
+
param_schema.items = param_data["items"]
|
119
|
+
end
|
120
|
+
|
121
|
+
link_schema.properties[param_data["name"]] = param_schema
|
122
|
+
if param_data["required"] == true
|
123
|
+
link_schema.required << param_data["name"]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
link_schema
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
LINK_REQUIRED_FIELDS = [
|
134
|
+
:name
|
135
|
+
].map(&:to_s).freeze
|
136
|
+
|
137
|
+
attr_accessor :link_data
|
138
|
+
end
|
139
|
+
|
140
|
+
class Schema < Committee::Drivers::Schema
|
141
|
+
attr_accessor :base_path
|
142
|
+
attr_accessor :consumes
|
143
|
+
|
144
|
+
# A link back to the derivative instace of Committee::Drivers::Driver
|
145
|
+
# that create this schema.
|
146
|
+
attr_accessor :driver
|
147
|
+
|
148
|
+
attr_accessor :definitions
|
149
|
+
attr_accessor :produces
|
150
|
+
attr_accessor :routes
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
DEFINITIONS_PSEUDO_URI = "http://json-schema.org/committee-definitions"
|
156
|
+
|
157
|
+
# These are fields that the OpenAPI 2 spec considers mandatory to be
|
158
|
+
# included in the document's top level.
|
159
|
+
REQUIRED_FIELDS = [
|
160
|
+
:consumes,
|
161
|
+
:definitions,
|
162
|
+
:paths,
|
163
|
+
:produces,
|
164
|
+
:swagger,
|
165
|
+
].map(&:to_s).freeze
|
166
|
+
|
167
|
+
def find_best_fit_response(link_data)
|
168
|
+
if response_data = link_data["responses"]["200"]
|
169
|
+
[200, response_data]
|
170
|
+
elsif response_data = link_data["responses"]["201"]
|
171
|
+
[201, response_data]
|
172
|
+
else
|
173
|
+
# Sort responses so that we can try to prefer any 3-digit status code.
|
174
|
+
# If there are none, we'll just take anything from the list.
|
175
|
+
ordered_responses = link_data["responses"].
|
176
|
+
select { |k, v| k =~ /[0-9]{3}/ }
|
177
|
+
if first = ordered_responses.first
|
178
|
+
[first[0].to_i, first[1]]
|
179
|
+
else
|
180
|
+
[nil, nil]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def href_to_regex(href)
|
186
|
+
href.gsub(/\{(.*?)\}/, '(?<\1>[^/]+)')
|
187
|
+
end
|
188
|
+
|
189
|
+
def parse_definitions!(data)
|
190
|
+
# The "definitions" section of an OpenAPI 2 spec is a valid JSON schema.
|
191
|
+
# We extract it from the spec and parse it as a schema in isolation so
|
192
|
+
# that all references to it will still have correct paths (i.e. we can
|
193
|
+
# still find a resource at '#/definitions/resource' instead of
|
194
|
+
# '#/resource').
|
195
|
+
schema = JsonSchema.parse!({
|
196
|
+
"definitions" => data['definitions'],
|
197
|
+
})
|
198
|
+
schema.expand_references!
|
199
|
+
schema.uri = DEFINITIONS_PSEUDO_URI
|
200
|
+
|
201
|
+
# So this is a little weird: an OpenAPI specification is _not_ a valid
|
202
|
+
# JSON schema and yet it self-references like it is a valid JSON schema.
|
203
|
+
# To work around this what we do is parse its "definitions" section as a
|
204
|
+
# JSON schema and then build a document store here containing that. When
|
205
|
+
# trying to resolve a reference from elsewhere in the spec, we build a
|
206
|
+
# synthetic schema with a JSON reference to the document created from
|
207
|
+
# "definitions" and then expand references against this store.
|
208
|
+
store = JsonSchema::DocumentStore.new
|
209
|
+
store.add_schema(schema)
|
210
|
+
|
211
|
+
[schema, store]
|
212
|
+
end
|
213
|
+
|
214
|
+
def parse_routes!(data, schema, store)
|
215
|
+
routes = {}
|
216
|
+
|
217
|
+
# This is a performance optimization: instead of going through each link
|
218
|
+
# and parsing out its JSON schema separately, instead we just aggregate
|
219
|
+
# all schemas into one big hash and then parse it all at the end. After
|
220
|
+
# we parse it, go through each link and assign a proper schema object. In
|
221
|
+
# practice this comes out to somewhere on the order of 50x faster.
|
222
|
+
target_schemas_data = { "properties" => {} }
|
223
|
+
|
224
|
+
data['paths'].each do |path, methods|
|
225
|
+
href = schema.base_path + path
|
226
|
+
target_schemas_data["properties"][href] = { "properties" => {} }
|
227
|
+
|
228
|
+
methods.each do |method, link_data|
|
229
|
+
method = method.upcase
|
230
|
+
|
231
|
+
link = Link.new
|
232
|
+
link.enc_type = schema.consumes
|
233
|
+
link.href = href
|
234
|
+
link.media_type = schema.produces
|
235
|
+
link.method = method
|
236
|
+
|
237
|
+
# Convert the spec's parameter pseudo-schemas into JSON schemas that
|
238
|
+
# we can use for some basic request validation.
|
239
|
+
link.schema = ParameterSchemaBuilder.new(link_data).call
|
240
|
+
|
241
|
+
# Arbitrarily pick one response for the time being. Prefers in order:
|
242
|
+
# a 200, 201, any 3-digit numerical response, then anything at all.
|
243
|
+
status, response_data = find_best_fit_response(link_data)
|
244
|
+
if status
|
245
|
+
link.status_success = status
|
246
|
+
|
247
|
+
# A link need not necessarily specify a target schema.
|
248
|
+
if response_data["schema"]
|
249
|
+
target_schemas_data["properties"][href]["properties"][method] =
|
250
|
+
response_data["schema"]
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
rx = %r{^#{href_to_regex(link.href)}$}
|
255
|
+
Committee.log_debug "Created route: #{link.method} #{link.href} (regex #{rx})"
|
256
|
+
|
257
|
+
routes[method] ||= []
|
258
|
+
routes[method] << [rx, link]
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# See the note on our DocumentStore's initialization in
|
263
|
+
# #parse_definitions!, but what we're doing here is prefixing references
|
264
|
+
# with a specialized internal URI so that they can reference definitions
|
265
|
+
# from another document in the store.
|
266
|
+
target_schemas = rewrite_references(target_schemas_data)
|
267
|
+
|
268
|
+
target_schemas = JsonSchema.parse!(target_schemas)
|
269
|
+
target_schemas.expand_references!(store: store)
|
270
|
+
|
271
|
+
# As noted above, now that we've parsed our aggregate response schema, go
|
272
|
+
# back through each link and them their response schema.
|
273
|
+
routes.each do |method, method_routes|
|
274
|
+
method_routes.each do |(_, link)|
|
275
|
+
link.target_schema =
|
276
|
+
target_schemas.properties[link.href].properties[method]
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
routes
|
281
|
+
end
|
282
|
+
|
283
|
+
def rewrite_references(schema)
|
284
|
+
if schema.is_a?(Hash)
|
285
|
+
ref = schema["$ref"]
|
286
|
+
if ref && ref.is_a?(String) && ref[0] == "#"
|
287
|
+
schema["$ref"] = DEFINITIONS_PSEUDO_URI + ref
|
288
|
+
else
|
289
|
+
schema.each do |_, v|
|
290
|
+
rewrite_references(v)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
schema
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Committee
|
2
|
+
module Drivers
|
3
|
+
# Gets a driver instance from the specified name. Raises ArgumentError for
|
4
|
+
# an unknown driver name.
|
5
|
+
def self.driver_from_name(name)
|
6
|
+
case name
|
7
|
+
when :hyper_schema
|
8
|
+
Committee::Drivers::HyperSchema.new
|
9
|
+
when :open_api_2
|
10
|
+
Committee::Drivers::OpenAPI2.new
|
11
|
+
else
|
12
|
+
raise ArgumentError, %{Committee: unknown driver "#{name}".}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Driver is a base class for driver implementations.
|
17
|
+
class Driver
|
18
|
+
# Whether parameters in a request's path will be considered and coerced
|
19
|
+
# by default.
|
20
|
+
def default_path_params
|
21
|
+
raise "needs implementation"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Whether parameters in a request's query string will be considered and
|
25
|
+
# coerced by default.
|
26
|
+
def default_query_params
|
27
|
+
raise "needs implementation"
|
28
|
+
end
|
29
|
+
|
30
|
+
def name
|
31
|
+
raise "needs implementation"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Parses an API schema and builds a set of route definitions for use with
|
35
|
+
# Committee.
|
36
|
+
#
|
37
|
+
# The expected input format is a data hash with keys as strings (as
|
38
|
+
# opposed to symbols) like the kind produced by JSON.parse or YAML.load.
|
39
|
+
def parse(data)
|
40
|
+
raise "needs implementation"
|
41
|
+
end
|
42
|
+
|
43
|
+
def schema_class
|
44
|
+
raise "needs implementation"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Schema is a base class for driver schema implementations.
|
49
|
+
class Schema
|
50
|
+
# A link back to the derivative instace of Committee::Drivers::Driver
|
51
|
+
# that create this schema.
|
52
|
+
def driver
|
53
|
+
raise "needs implementation"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|