committee 1.15.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +5 -5
  2. data/bin/committee-stub +11 -38
  3. data/lib/committee/bin/committee_stub.rb +67 -0
  4. data/lib/committee/drivers/driver.rb +47 -0
  5. data/lib/committee/drivers/hyper_schema/driver.rb +105 -0
  6. data/lib/committee/drivers/hyper_schema/link.rb +68 -0
  7. data/lib/committee/drivers/hyper_schema/schema.rb +22 -0
  8. data/lib/committee/drivers/hyper_schema.rb +12 -0
  9. data/lib/committee/drivers/open_api_2/driver.rb +252 -0
  10. data/lib/committee/drivers/open_api_2/header_schema_builder.rb +33 -0
  11. data/lib/committee/drivers/open_api_2/link.rb +36 -0
  12. data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +83 -0
  13. data/lib/committee/drivers/open_api_2/schema.rb +26 -0
  14. data/lib/committee/drivers/open_api_2/schema_builder.rb +33 -0
  15. data/lib/committee/drivers/open_api_2.rb +13 -0
  16. data/lib/committee/drivers/open_api_3/driver.rb +51 -0
  17. data/lib/committee/drivers/open_api_3/schema.rb +41 -0
  18. data/lib/committee/drivers/open_api_3.rb +11 -0
  19. data/lib/committee/drivers/schema.rb +23 -0
  20. data/lib/committee/drivers.rb +84 -0
  21. data/lib/committee/errors.rb +17 -0
  22. data/lib/committee/middleware/base.rb +46 -29
  23. data/lib/committee/middleware/request_validation.rb +31 -49
  24. data/lib/committee/middleware/response_validation.rb +48 -25
  25. data/lib/committee/middleware/stub.rb +62 -37
  26. data/lib/committee/middleware.rb +11 -0
  27. data/lib/committee/request_unpacker.rb +58 -50
  28. data/lib/committee/schema_validator/hyper_schema/parameter_coercer.rb +79 -0
  29. data/lib/committee/schema_validator/hyper_schema/request_validator.rb +55 -0
  30. data/lib/committee/schema_validator/hyper_schema/response_generator.rb +102 -0
  31. data/lib/committee/schema_validator/hyper_schema/response_validator.rb +89 -0
  32. data/lib/committee/schema_validator/hyper_schema/router.rb +46 -0
  33. data/lib/committee/schema_validator/hyper_schema/string_params_coercer.rb +105 -0
  34. data/lib/committee/schema_validator/hyper_schema.rb +119 -0
  35. data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +139 -0
  36. data/lib/committee/schema_validator/open_api_3/request_validator.rb +52 -0
  37. data/lib/committee/schema_validator/open_api_3/response_validator.rb +29 -0
  38. data/lib/committee/schema_validator/open_api_3/router.rb +45 -0
  39. data/lib/committee/schema_validator/open_api_3.rb +120 -0
  40. data/lib/committee/schema_validator/option.rb +60 -0
  41. data/lib/committee/schema_validator.rb +23 -0
  42. data/lib/committee/test/methods.rb +68 -38
  43. data/lib/committee/test/schema_coverage.rb +101 -0
  44. data/lib/committee/utils.rb +28 -0
  45. data/lib/committee/validation_error.rb +5 -2
  46. data/lib/committee/version.rb +5 -0
  47. data/lib/committee.rb +31 -18
  48. data/test/bin/committee_stub_test.rb +57 -0
  49. data/test/bin_test.rb +25 -0
  50. data/test/committee_test.rb +77 -0
  51. data/test/drivers/hyper_schema/driver_test.rb +49 -0
  52. data/test/drivers/hyper_schema/link_test.rb +56 -0
  53. data/test/drivers/open_api_2/driver_test.rb +156 -0
  54. data/test/drivers/open_api_2/header_schema_builder_test.rb +26 -0
  55. data/test/drivers/open_api_2/link_test.rb +52 -0
  56. data/test/drivers/open_api_2/parameter_schema_builder_test.rb +195 -0
  57. data/test/drivers/open_api_3/driver_test.rb +84 -0
  58. data/test/drivers_test.rb +154 -0
  59. data/test/middleware/base_test.rb +96 -7
  60. data/test/middleware/request_validation_open_api_3_test.rb +626 -0
  61. data/test/middleware/request_validation_test.rb +423 -32
  62. data/test/middleware/response_validation_open_api_3_test.rb +291 -0
  63. data/test/middleware/response_validation_test.rb +125 -23
  64. data/test/middleware/stub_test.rb +81 -20
  65. data/test/request_unpacker_test.rb +126 -52
  66. data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +111 -0
  67. data/test/schema_validator/hyper_schema/request_validator_test.rb +151 -0
  68. data/test/schema_validator/hyper_schema/response_generator_test.rb +142 -0
  69. data/test/{response_validator_test.rb → schema_validator/hyper_schema/response_validator_test.rb} +43 -6
  70. data/test/schema_validator/hyper_schema/router_test.rb +88 -0
  71. data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +137 -0
  72. data/test/schema_validator/open_api_3/operation_wrapper_test.rb +218 -0
  73. data/test/schema_validator/open_api_3/request_validator_test.rb +110 -0
  74. data/test/schema_validator/open_api_3/response_validator_test.rb +92 -0
  75. data/test/test/methods_new_version_test.rb +97 -0
  76. data/test/test/methods_test.rb +334 -27
  77. data/test/test/schema_coverage_test.rb +216 -0
  78. data/test/test_helper.rb +108 -1
  79. data/test/validation_error_test.rb +3 -1
  80. metadata +190 -27
  81. data/lib/committee/query_params_coercer.rb +0 -45
  82. data/lib/committee/request_validator.rb +0 -44
  83. data/lib/committee/response_generator.rb +0 -35
  84. data/lib/committee/response_validator.rb +0 -59
  85. data/lib/committee/router.rb +0 -62
  86. data/test/query_params_coercer_test.rb +0 -70
  87. data/test/request_validator_test.rb +0 -103
  88. data/test/response_generator_test.rb +0 -61
  89. data/test/router_test.rb +0 -38
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: d5fcb82a192f894e1baec1dcab89f081782d222f
4
- data.tar.gz: 54d056b8be5b297b6a524166363f955db7a1bd15
2
+ SHA256:
3
+ metadata.gz: 284c8c1198255435e959dcee2b92cb25c3d80dd5fe6d4622b8077a69525712ed
4
+ data.tar.gz: b306ec0ad5e628d9cc7b6382781c4b710c99bedb127a7000edadf9d0e0b8fa84
5
5
  SHA512:
6
- metadata.gz: f22924efd2696d884eb499978cdb2a9f6e81ead9d25dacbe3ea331b9fb4d771886a4ccea1926fe2d09256886433a451fdbe25ef47cbe8d4c80a07a4157e0b9bc
7
- data.tar.gz: db8d1aa50e4f4fa854bda1d5484df09e7262801bd845195c1340a718fc6d0792666360698ea2fe8722f1c57a287fcfb4b1f0258706032f47a2ef7cceaffd7df9
6
+ metadata.gz: 687d9a47d2a17786313bde939339d464a8d775795458b09ad7d7784bfa337f848a9ffabe240e9f39173c4466589c026f64a8eede768e7abfff4b01b2672339f5
7
+ data.tar.gz: 4ee7781341de9ebafae28d01d98c0f72d54067c425f3ee10b221c4eb88da85e79b36853bbda4818b20348177f8d975103cc5be694ff7e8252a6634a856853b7c
data/bin/committee-stub CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'optparse'
4
5
  require 'yaml'
@@ -6,45 +7,17 @@ require 'yaml'
6
7
  require_relative "../lib/committee"
7
8
 
8
9
  args = ARGV.dup
9
- options = { port: 9292, tolerant: false }
10
- opt_parser = OptionParser.new do |opts|
11
- opts.banner = "Usage: rackup [options] [JSON Schema file]"
10
+ bin = Committee::Bin::CommitteeStub.new
11
+ options, parser = bin.get_options_parser
12
12
 
13
- opts.separator ""
14
- opts.separator "Options:"
13
+ parser.parse!(args)
15
14
 
16
- opts.on_tail("-h", "-?", "--help", "Show this message") {
17
- puts opts
18
- exit
19
- }
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])))
20
21
 
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
22
+ Rack::Server.start(app: bin.get_app(schema, options), Port: options[:port])
34
23
  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
- end
43
-
44
- use Committee::Middleware::Stub, schema: schema
45
- run lambda { |_|
46
- [404, {}, ["Not found"]]
47
- }
48
- }
49
-
50
- Rack::Server.start(app: app, Port: options[:port])
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module Drivers
5
+ module HyperSchema
6
+ end
7
+ end
8
+ end
9
+
10
+ require_relative 'hyper_schema/driver'
11
+ require_relative 'hyper_schema/link'
12
+ require_relative 'hyper_schema/schema'
@@ -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