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