committee 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/committee-stub +43 -0
- data/lib/committee/errors.rb +13 -0
- data/lib/committee/middleware/base.rb +19 -0
- data/lib/committee/middleware/request_validation.rb +19 -0
- data/lib/committee/middleware/response_validation.rb +33 -0
- data/lib/committee/middleware/stub.rb +33 -0
- data/lib/committee/param_validator.rb +132 -0
- data/lib/committee/request_unpacker.rb +59 -0
- data/lib/committee/response_generator.rb +30 -0
- data/lib/committee/response_validator.rb +127 -0
- data/lib/committee/router.rb +38 -0
- data/lib/committee/schema.rb +55 -0
- data/lib/committee.rb +15 -0
- data/test/middleware/request_validation_test.rb +59 -0
- data/test/middleware/response_validation_test.rb +64 -0
- data/test/middleware/stub_test.rb +24 -0
- data/test/param_validator_test.rb +74 -0
- data/test/request_unpacker_test.rb +66 -0
- data/test/response_generator_test.rb +32 -0
- data/test/response_validator_test.rb +75 -0
- data/test/router_test.rb +17 -0
- data/test/schema_test.rb +36 -0
- data/test/test_helper.rb +36 -0
- metadata +149 -0
data/bin/committee-stub
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
require_relative "../lib/committee"
|
6
|
+
|
7
|
+
args = ARGV.dup
|
8
|
+
options = { Port: 9292 }
|
9
|
+
opt_parser = OptionParser.new do |opts|
|
10
|
+
opts.banner = "Usage: rackup [options] [JSON Schema file]"
|
11
|
+
|
12
|
+
opts.separator ""
|
13
|
+
opts.separator "Options:"
|
14
|
+
|
15
|
+
opts.on_tail("-h", "-?", "--help", "Show this message") {
|
16
|
+
puts opts
|
17
|
+
exit
|
18
|
+
}
|
19
|
+
|
20
|
+
opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port|
|
21
|
+
options[:Port] = port
|
22
|
+
}
|
23
|
+
end
|
24
|
+
opt_parser.parse!(args)
|
25
|
+
|
26
|
+
unless args.count == 1
|
27
|
+
puts opt_parser.to_s
|
28
|
+
exit
|
29
|
+
end
|
30
|
+
|
31
|
+
schema = File.read(args[0])
|
32
|
+
|
33
|
+
app = Rack::Builder.new {
|
34
|
+
use Committee::Middleware::RequestValidation, schema: schema
|
35
|
+
use Committee::Middleware::Stub, schema: schema
|
36
|
+
use Committee::Middleware::ResponseValidation, schema: schema
|
37
|
+
run lambda { |_|
|
38
|
+
[404, {}, ["Not found"]]
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
options[:app] = app
|
43
|
+
Rack::Server.start(options)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Committee::Middleware
|
2
|
+
class Base
|
3
|
+
def initialize(app, options={})
|
4
|
+
@app = app
|
5
|
+
|
6
|
+
@params_key = options[:params_key] || "committee.params"
|
7
|
+
data = options[:schema] || raise("need option `schema`")
|
8
|
+
@schema = Committee::Schema.new(data)
|
9
|
+
@router = Committee::Router.new(@schema)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def render_error(status, id, message)
|
15
|
+
[status, { "Content-Type" => "application/json" },
|
16
|
+
[MultiJson.encode(id: id, error: message)]]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Committee::Middleware
|
2
|
+
class RequestValidation < Base
|
3
|
+
def call(env)
|
4
|
+
request = Rack::Request.new(env)
|
5
|
+
env[@params_key] = Committee::RequestUnpacker.new(request).call
|
6
|
+
link, _ = @router.routes_request?(request)
|
7
|
+
if link
|
8
|
+
Committee::ParamValidator.new(env[@params_key], @schema, link).call
|
9
|
+
end
|
10
|
+
@app.call(env)
|
11
|
+
rescue Committee::BadRequest
|
12
|
+
render_error(400, :bad_request, $!.message)
|
13
|
+
rescue Committee::InvalidParams
|
14
|
+
render_error(422, :invalid_params, $!.message)
|
15
|
+
rescue MultiJson::LoadError
|
16
|
+
render_error(400, :invalid_params, "Request body wasn't valid JSON.")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Committee::Middleware
|
2
|
+
class ResponseValidation < Base
|
3
|
+
def call(env)
|
4
|
+
status, headers, response = @app.call(env)
|
5
|
+
request = Rack::Request.new(env)
|
6
|
+
link_schema, type_schema = @router.routes_request?(request)
|
7
|
+
if type_schema
|
8
|
+
check_content_type!(headers)
|
9
|
+
str = response.reduce("") { |str, s| str << s }
|
10
|
+
Committee::ResponseValidator.new(
|
11
|
+
MultiJson.decode(str),
|
12
|
+
@schema,
|
13
|
+
link_schema,
|
14
|
+
type_schema
|
15
|
+
).call
|
16
|
+
end
|
17
|
+
[status, headers, response]
|
18
|
+
rescue Committee::InvalidResponse
|
19
|
+
render_error(500, :invalid_response, $!.message)
|
20
|
+
rescue MultiJson::LoadError
|
21
|
+
render_error(500, :invalid_response, "Response wasn't valid JSON.")
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def check_content_type!(headers)
|
27
|
+
unless headers["Content-Type"] =~ %r{application/json}
|
28
|
+
raise Committee::InvalidResponse,
|
29
|
+
%{"Content-Type" response header must be set to "application/json".}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Committee::Middleware
|
2
|
+
class Stub < Base
|
3
|
+
def initialize(app, options={})
|
4
|
+
super
|
5
|
+
@cache = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
request = Rack::Request.new(env)
|
10
|
+
link_schema, type_schema = @router.routes_request?(request)
|
11
|
+
if type_schema
|
12
|
+
str = cache(link_schema["method"], link_schema["href"]) do
|
13
|
+
data = Committee::ResponseGenerator.new(@schema, type_schema, link_schema).call
|
14
|
+
MultiJson.encode(data, pretty: true)
|
15
|
+
end
|
16
|
+
[200, { "Content-Type" => "application/json" }, [str]]
|
17
|
+
else
|
18
|
+
@app.call(env)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def cache(method, href)
|
25
|
+
key = "#{method}+#{href}"
|
26
|
+
if @cache[key]
|
27
|
+
@cache[key]
|
28
|
+
else
|
29
|
+
@cache[key] = yield
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Committee
|
2
|
+
class ParamValidator
|
3
|
+
def initialize(params, schema, link_schema)
|
4
|
+
@params = params
|
5
|
+
@schema = schema
|
6
|
+
@link_schema = link_schema
|
7
|
+
end
|
8
|
+
|
9
|
+
def call
|
10
|
+
detect_missing!
|
11
|
+
detect_extra!
|
12
|
+
check_data!
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def all_keys
|
18
|
+
properties = @link_schema["schema"] && @link_schema["schema"]["properties"]
|
19
|
+
properties && properties.keys || []
|
20
|
+
end
|
21
|
+
|
22
|
+
def check_data!
|
23
|
+
return if !@link_schema["schema"] || !@link_schema["schema"]["properties"]
|
24
|
+
|
25
|
+
@link_schema["schema"]["properties"].each do |key, value|
|
26
|
+
# don't try to check this unless it was actually specificed
|
27
|
+
next unless @params.key?(key)
|
28
|
+
|
29
|
+
match = false
|
30
|
+
definitions = find_definitions(value["$ref"])
|
31
|
+
|
32
|
+
# try to match data against any possible definition
|
33
|
+
definitions.each do |definition|
|
34
|
+
if check_type(definition["type"], @params[key], key) &&
|
35
|
+
check_format(definition["format"], @params[key], key) &&
|
36
|
+
check_pattern(definition["pattern"], @params[key], key)
|
37
|
+
match = true
|
38
|
+
next
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# if nothing was matched, throw error according to first definition
|
43
|
+
if !match && definition = definitions.first
|
44
|
+
check_type!(definition["type"], @params[key], key)
|
45
|
+
check_format!(definition["format"], @params[key], key)
|
46
|
+
check_pattern!(definition["pattern"], @params[key], key)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def check_format(format, value, key)
|
52
|
+
case format
|
53
|
+
when "date-time"
|
54
|
+
value =~ /^(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(Z|[+-](\d{2})\:(\d{2}))$/
|
55
|
+
when "email"
|
56
|
+
value =~ /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/i
|
57
|
+
when "uuid"
|
58
|
+
value =~ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
|
59
|
+
else
|
60
|
+
true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def check_format!(format, value, key)
|
65
|
+
unless check_format(format, value, key)
|
66
|
+
raise InvalidParams,
|
67
|
+
%{Invalid format for key "#{key}": expected "#{value}" to be "#{format}".}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def check_pattern(pattern, value, key)
|
72
|
+
!pattern || value =~ pattern
|
73
|
+
end
|
74
|
+
|
75
|
+
def check_pattern!(pattern, value, key)
|
76
|
+
unless check_pattern(pattern, value, key)
|
77
|
+
raise InvalidParams,
|
78
|
+
%{Invalid pattern for key "#{key}": expected #{value} to match "#{pattern}".}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def check_type(types, value, key)
|
83
|
+
type = case value
|
84
|
+
when NilClass
|
85
|
+
"null"
|
86
|
+
when TrueClass, FalseClass
|
87
|
+
"boolean"
|
88
|
+
when Fixnum
|
89
|
+
"integer"
|
90
|
+
when String
|
91
|
+
"string"
|
92
|
+
else
|
93
|
+
"unknown"
|
94
|
+
end
|
95
|
+
types.include?(type)
|
96
|
+
end
|
97
|
+
|
98
|
+
def check_type!(types, value, key)
|
99
|
+
unless check_type(types, value, key)
|
100
|
+
raise InvalidParams,
|
101
|
+
%{Invalid type for key "#{key}": expected #{value} to be #{types}.}
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def detect_extra!
|
106
|
+
extra = @params.keys - all_keys
|
107
|
+
if extra.count > 0
|
108
|
+
raise InvalidParams.new("Unknown params: #{extra.join(', ')}.")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def detect_missing!
|
113
|
+
missing = required_keys - @params.keys
|
114
|
+
if missing.count > 0
|
115
|
+
raise InvalidParams.new("Require params: #{missing.join(', ')}.")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def find_definitions(ref)
|
120
|
+
definition = @schema.find(ref)
|
121
|
+
if definition["anyOf"]
|
122
|
+
definition["anyOf"].map { |r| @schema.find(r["$ref"]) }
|
123
|
+
else
|
124
|
+
[definition]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def required_keys
|
129
|
+
(@link_schema["schema"] && @link_schema["schema"]["required"]) || []
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Committee
|
2
|
+
class RequestUnpacker
|
3
|
+
def initialize(request)
|
4
|
+
@request = request
|
5
|
+
end
|
6
|
+
|
7
|
+
def call
|
8
|
+
if !@request.content_type || @request.content_type =~ %r{application/json}
|
9
|
+
# if Content-Type is empty or JSON, and there was a request body, try
|
10
|
+
# to interpret it as JSON
|
11
|
+
if (body = @request.body.read).length != 0
|
12
|
+
@request.body.rewind
|
13
|
+
hash = MultiJson.decode(body)
|
14
|
+
# We want a hash specifically. '42', 42, and [42] will all be
|
15
|
+
# decoded properly, but we can't use them here.
|
16
|
+
if !hash.is_a?(Hash)
|
17
|
+
raise BadRequest,
|
18
|
+
"Invalid JSON input. Require object with parameters as keys."
|
19
|
+
end
|
20
|
+
indifferent_params(hash)
|
21
|
+
# if request body is empty, we just have empty params
|
22
|
+
else
|
23
|
+
{}
|
24
|
+
end
|
25
|
+
elsif @request.content_type == "application/x-www-form-urlencoded"
|
26
|
+
# Actually, POST means anything in the request body, could be from
|
27
|
+
# PUT or PATCH too. Silly Rack.
|
28
|
+
indifferent_params(@request.POST)
|
29
|
+
else
|
30
|
+
raise BadRequest, "Unsupported Content-Type: #{@request.content_type}."
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Creates a Hash with indifferent access.
|
37
|
+
#
|
38
|
+
# (Copied from Sinatra)
|
39
|
+
def indifferent_hash
|
40
|
+
Hash.new { |hash,key| hash[key.to_s] if Symbol === key }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Enable string or symbol key access to the nested params hash.
|
44
|
+
#
|
45
|
+
# (Copied from Sinatra)
|
46
|
+
def indifferent_params(object)
|
47
|
+
case object
|
48
|
+
when Hash
|
49
|
+
new_hash = indifferent_hash
|
50
|
+
object.each { |key, value| new_hash[key] = indifferent_params(value) }
|
51
|
+
new_hash
|
52
|
+
when Array
|
53
|
+
object.map { |item| indifferent_params(item) }
|
54
|
+
else
|
55
|
+
object
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Committee
|
2
|
+
class ResponseGenerator
|
3
|
+
def initialize(schema, type_schema, link_schema)
|
4
|
+
@schema = schema
|
5
|
+
@type_schema = type_schema
|
6
|
+
@link_schema = link_schema
|
7
|
+
end
|
8
|
+
|
9
|
+
def call
|
10
|
+
generate_properties(@type_schema)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def generate_properties(schema)
|
16
|
+
data = {}
|
17
|
+
schema["properties"].each do |name, value|
|
18
|
+
data[name] = if value["properties"]
|
19
|
+
generate_properties(value)
|
20
|
+
else
|
21
|
+
definition = @schema.find(value["$ref"])
|
22
|
+
definition["example"]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
# list is a special case; wrap data in an array
|
26
|
+
data = [data] if @link_schema["title"] == "List"
|
27
|
+
data
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Committee
|
2
|
+
class ResponseValidator
|
3
|
+
def initialize(data, schema, link_schema, type_schema)
|
4
|
+
@data = data
|
5
|
+
@schema = schema
|
6
|
+
@link_schema = link_schema
|
7
|
+
@type_schema = type_schema
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
data = if @link_schema["title"] == "List"
|
12
|
+
if !@data.is_a?(Array)
|
13
|
+
raise InvalidResponse, "List endpoints must return an array of objects."
|
14
|
+
end
|
15
|
+
# only consider the first object during the validation from here on
|
16
|
+
@data[0]
|
17
|
+
else
|
18
|
+
@data
|
19
|
+
end
|
20
|
+
|
21
|
+
data_keys = build_data_keys(data)
|
22
|
+
schema_keys = build_schema_keys
|
23
|
+
|
24
|
+
extra = data_keys - schema_keys
|
25
|
+
missing = schema_keys - data_keys
|
26
|
+
|
27
|
+
errors = []
|
28
|
+
|
29
|
+
if extra.count > 0
|
30
|
+
errors << "Extra keys in response: #{extra.join(', ')}."
|
31
|
+
end
|
32
|
+
|
33
|
+
if missing.count > 0
|
34
|
+
errors << "Missing keys in response: #{missing.join(', ')}."
|
35
|
+
end
|
36
|
+
|
37
|
+
unless errors.empty?
|
38
|
+
raise InvalidResponse, ["`#{@link_schema['method']} #{@link_schema['href']}` deviates from schema.", *errors].join(' ')
|
39
|
+
end
|
40
|
+
|
41
|
+
check_data!(@type_schema, data, [])
|
42
|
+
end
|
43
|
+
|
44
|
+
def check_data!(schema, data, path)
|
45
|
+
schema["properties"].each do |key, value|
|
46
|
+
if value["properties"]
|
47
|
+
check_data!(value, data[key], path + [key])
|
48
|
+
else
|
49
|
+
definition = @schema.find(value["$ref"])
|
50
|
+
check_type!(definition["type"], data[key], path + [key])
|
51
|
+
check_format!(definition["format"], data[key], path + [key])
|
52
|
+
check_pattern!(definition["pattern"], data[key], path + [key])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def check_format!(format, value, path)
|
58
|
+
return if !format
|
59
|
+
valid = case format
|
60
|
+
when "date-time"
|
61
|
+
value =~ /^(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(Z|[+-](\d{2})\:(\d{2}))$/
|
62
|
+
when "email"
|
63
|
+
value =~ /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/i
|
64
|
+
when "uuid"
|
65
|
+
value =~ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
|
66
|
+
else
|
67
|
+
true
|
68
|
+
end
|
69
|
+
unless valid
|
70
|
+
raise InvalidResponse,
|
71
|
+
%{Invalid format at "#{path.join(":")}": expected "#{value}" to be "#{format}".}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def check_pattern!(pattern, value, path)
|
76
|
+
if pattern && !(value =~ pattern)
|
77
|
+
raise InvalidResponse,
|
78
|
+
%{Invalid pattern at "#{path.join(":")}": expected #{value} to match "#{pattern}".}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def check_type!(types, value, path)
|
83
|
+
type = case value
|
84
|
+
when NilClass
|
85
|
+
"null"
|
86
|
+
when TrueClass, FalseClass
|
87
|
+
"boolean"
|
88
|
+
when Fixnum
|
89
|
+
"integer"
|
90
|
+
when String
|
91
|
+
"string"
|
92
|
+
else
|
93
|
+
"unknown"
|
94
|
+
end
|
95
|
+
unless types.include?(type)
|
96
|
+
raise InvalidResponse,
|
97
|
+
%{Invalid type at "#{path.join(":")}": expected #{value} to be #{types} (was: #{type}).}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def build_data_keys(data)
|
104
|
+
keys = []
|
105
|
+
data.each do |key, value|
|
106
|
+
if value.is_a?(Hash)
|
107
|
+
keys += value.keys.map { |k| "#{key}:#{k}" }
|
108
|
+
else
|
109
|
+
keys << key
|
110
|
+
end
|
111
|
+
end
|
112
|
+
keys
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_schema_keys
|
116
|
+
keys = []
|
117
|
+
@type_schema["properties"].each do |key, info|
|
118
|
+
if info["properties"]
|
119
|
+
keys += info["properties"].keys.map { |k| "#{key}:#{k}" }
|
120
|
+
else
|
121
|
+
keys << key
|
122
|
+
end
|
123
|
+
end
|
124
|
+
keys
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Committee
|
2
|
+
class Router
|
3
|
+
def initialize(schema)
|
4
|
+
@routes = build_routes(schema)
|
5
|
+
end
|
6
|
+
|
7
|
+
def routes?(method, path)
|
8
|
+
if method_routes = @routes[method]
|
9
|
+
method_routes.each do |pattern, link, schema|
|
10
|
+
if path =~ pattern
|
11
|
+
return link, schema
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
[nil, nil]
|
16
|
+
end
|
17
|
+
|
18
|
+
def routes_request?(request)
|
19
|
+
routes?(request.request_method, request.path_info)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def build_routes(schema)
|
25
|
+
routes = {}
|
26
|
+
schema.each do |_, type_schema|
|
27
|
+
type_schema["links"].each do |link|
|
28
|
+
routes[link["method"]] ||= []
|
29
|
+
# /apps/{id} --> /apps/([^/]+)
|
30
|
+
pattern = link["href"].gsub(/\{(.*?)\}/, "[^/]+")
|
31
|
+
routes[link["method"]] <<
|
32
|
+
[Regexp.new(pattern), link, type_schema]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
routes
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Committee
|
2
|
+
class Schema
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize(data)
|
6
|
+
@cache = {}
|
7
|
+
@schema = MultiJson.decode(data)
|
8
|
+
manifest_regex
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](type)
|
12
|
+
@schema["definitions"][type]
|
13
|
+
end
|
14
|
+
|
15
|
+
def each
|
16
|
+
@schema["definitions"].each do |type, type_schema|
|
17
|
+
yield(type, type_schema)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def find(ref)
|
22
|
+
cache(ref) do
|
23
|
+
parts = ref.split("/")
|
24
|
+
parts.shift if parts.first == "#"
|
25
|
+
pointer = @schema
|
26
|
+
parts.each do |p|
|
27
|
+
next unless pointer
|
28
|
+
pointer = pointer[p]
|
29
|
+
end
|
30
|
+
raise ReferenceNotFound, "Reference not found: #{ref}." if !pointer
|
31
|
+
pointer
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def cache(key)
|
38
|
+
if @cache[key]
|
39
|
+
@cache[key]
|
40
|
+
else
|
41
|
+
@cache[key] = yield
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def manifest_regex
|
46
|
+
@schema["definitions"].each do |_, type_schema|
|
47
|
+
type_schema["definitions"].each do |_, property_schema|
|
48
|
+
if pattern = property_schema["pattern"]
|
49
|
+
property_schema["pattern"] = Regexp.new(pattern)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/committee.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "multi_json"
|
2
|
+
require "rack"
|
3
|
+
|
4
|
+
require_relative "committee/errors"
|
5
|
+
require_relative "committee/param_validator"
|
6
|
+
require_relative "committee/request_unpacker"
|
7
|
+
require_relative "committee/response_generator"
|
8
|
+
require_relative "committee/response_validator"
|
9
|
+
require_relative "committee/router"
|
10
|
+
require_relative "committee/schema"
|
11
|
+
|
12
|
+
require_relative "committee/middleware/base"
|
13
|
+
require_relative "committee/middleware/request_validation"
|
14
|
+
require_relative "committee/middleware/response_validation"
|
15
|
+
require_relative "committee/middleware/stub"
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
|
3
|
+
describe Committee::Middleware::RequestValidation do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
StubApp = Rack::Builder.new {
|
7
|
+
use Committee::Middleware::RequestValidation,
|
8
|
+
schema: File.read("./test/data/schema.json")
|
9
|
+
run lambda { |_|
|
10
|
+
[200, {}, []]
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
def app
|
15
|
+
StubApp
|
16
|
+
end
|
17
|
+
|
18
|
+
before do
|
19
|
+
header "Content-Type", "application/json"
|
20
|
+
end
|
21
|
+
|
22
|
+
it "detects an invalid Content-Type" do
|
23
|
+
header "Content-Type", "application/whats-this"
|
24
|
+
post "/account/app-transfers", "{}"
|
25
|
+
assert_equal 400, last_response.status
|
26
|
+
end
|
27
|
+
|
28
|
+
it "passes through a valid request" do
|
29
|
+
params = {
|
30
|
+
"app" => "heroku-api",
|
31
|
+
"recipient" => "owner@heroku.com",
|
32
|
+
}
|
33
|
+
post "/account/app-transfers", MultiJson.encode(params)
|
34
|
+
assert_equal 200, last_response.status
|
35
|
+
end
|
36
|
+
|
37
|
+
it "detects a missing parameter" do
|
38
|
+
post "/account/app-transfers", "{}"
|
39
|
+
assert_equal 422, last_response.status
|
40
|
+
assert_match /require params/i, last_response.body
|
41
|
+
end
|
42
|
+
|
43
|
+
it "detects an extra parameter" do
|
44
|
+
params = {
|
45
|
+
"app" => "heroku-api",
|
46
|
+
"cloud" => "production",
|
47
|
+
"recipient" => "owner@heroku.com",
|
48
|
+
}
|
49
|
+
post "/account/app-transfers", MultiJson.encode(params)
|
50
|
+
assert_equal 422, last_response.status
|
51
|
+
assert_match /unknown params/i, last_response.body
|
52
|
+
end
|
53
|
+
|
54
|
+
it "rescues JSON errors" do
|
55
|
+
post "/account/app-transfers", "{x:y}"
|
56
|
+
assert_equal 400, last_response.status
|
57
|
+
assert_match /valid json/i, last_response.body
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
|
3
|
+
describe Committee::Middleware::ResponseValidation do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
def app
|
7
|
+
@app
|
8
|
+
end
|
9
|
+
|
10
|
+
it "passes through a valid response" do
|
11
|
+
@app = new_rack_app(MultiJson.encode([ValidApp]))
|
12
|
+
get "/apps"
|
13
|
+
assert_equal 200, last_response.status
|
14
|
+
end
|
15
|
+
|
16
|
+
it "detects an invalid response Content-Type" do
|
17
|
+
@app = new_rack_app(MultiJson.encode([ValidApp]), {})
|
18
|
+
get "/apps"
|
19
|
+
assert_equal 500, last_response.status
|
20
|
+
assert_match /response header must be set to/i, last_response.body
|
21
|
+
end
|
22
|
+
|
23
|
+
it "detects an invalid response" do
|
24
|
+
@app = new_rack_app("")
|
25
|
+
get "/apps"
|
26
|
+
assert_equal 500, last_response.status
|
27
|
+
assert_match /valid JSON/i, last_response.body
|
28
|
+
end
|
29
|
+
|
30
|
+
it "detects missing keys in response" do
|
31
|
+
data = ValidApp.dup
|
32
|
+
data.delete("name")
|
33
|
+
@app = new_rack_app(MultiJson.encode([data]))
|
34
|
+
get "/apps"
|
35
|
+
assert_equal 500, last_response.status
|
36
|
+
assert_match /missing keys/i, last_response.body
|
37
|
+
end
|
38
|
+
|
39
|
+
it "detects extra keys in response" do
|
40
|
+
data = ValidApp.dup
|
41
|
+
data.merge!("tier" => "important")
|
42
|
+
@app = new_rack_app(MultiJson.encode([data]))
|
43
|
+
get "/apps"
|
44
|
+
assert_equal 500, last_response.status
|
45
|
+
assert_match /extra keys/i, last_response.body
|
46
|
+
end
|
47
|
+
|
48
|
+
it "rescues JSON errors" do
|
49
|
+
@app = new_rack_app("[{x:y}]")
|
50
|
+
get "/apps"
|
51
|
+
assert_equal 500, last_response.status
|
52
|
+
assert_match /valid json/i, last_response.body
|
53
|
+
end
|
54
|
+
|
55
|
+
def new_rack_app(response, headers={ "Content-Type" => "application/json" })
|
56
|
+
Rack::Builder.new {
|
57
|
+
use Committee::Middleware::ResponseValidation,
|
58
|
+
schema: File.read("./test/data/schema.json")
|
59
|
+
run lambda { |_|
|
60
|
+
[200, headers, [response]]
|
61
|
+
}
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
|
3
|
+
describe Committee::Middleware::Stub do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
ParamValidationApp = Rack::Builder.new {
|
7
|
+
use Committee::Middleware::Stub,
|
8
|
+
schema: File.read("./test/data/schema.json")
|
9
|
+
run lambda { |_|
|
10
|
+
[200, {}, []]
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
def app
|
15
|
+
ParamValidationApp
|
16
|
+
end
|
17
|
+
|
18
|
+
it "responds with a stubbed response" do
|
19
|
+
get "/apps/heroku-api"
|
20
|
+
assert_equal 200, last_response.status
|
21
|
+
data = MultiJson.decode(last_response.body)
|
22
|
+
assert_equal ValidApp.keys.sort, data.keys.sort
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
describe Committee::ParamValidator do
|
4
|
+
before do
|
5
|
+
@schema = Committee::Schema.new(File.read("./test/data/schema.json"))
|
6
|
+
# POST /account/app-transfers
|
7
|
+
@link_schema = @schema["app-transfer"]["links"][0]
|
8
|
+
end
|
9
|
+
|
10
|
+
it "passes through a valid request" do
|
11
|
+
params = {
|
12
|
+
"app" => "heroku-api",
|
13
|
+
"recipient" => "owner@heroku.com",
|
14
|
+
}
|
15
|
+
Committee::ParamValidator.new(params, @schema, @link_schema).call
|
16
|
+
end
|
17
|
+
|
18
|
+
it "detects a missing parameter" do
|
19
|
+
e = assert_raises(Committee::InvalidParams) do
|
20
|
+
Committee::ParamValidator.new({}, @schema, @link_schema).call
|
21
|
+
end
|
22
|
+
message = "Require params: app, recipient."
|
23
|
+
assert_equal message, e.message
|
24
|
+
end
|
25
|
+
|
26
|
+
it "detects an extraneous parameter" do
|
27
|
+
params = {
|
28
|
+
"app" => "heroku-api",
|
29
|
+
"cloud" => "production",
|
30
|
+
"recipient" => "owner@heroku.com",
|
31
|
+
}
|
32
|
+
e = assert_raises(Committee::InvalidParams) do
|
33
|
+
Committee::ParamValidator.new(params, @schema, @link_schema).call
|
34
|
+
end
|
35
|
+
message = "Unknown params: cloud."
|
36
|
+
assert_equal message, e.message
|
37
|
+
end
|
38
|
+
|
39
|
+
it "detects a parameter of the wrong type" do
|
40
|
+
params = {
|
41
|
+
"app" => "heroku-api",
|
42
|
+
"recipient" => 123,
|
43
|
+
}
|
44
|
+
e = assert_raises(Committee::InvalidParams) do
|
45
|
+
Committee::ParamValidator.new(params, @schema, @link_schema).call
|
46
|
+
end
|
47
|
+
message = %{Invalid type for key "recipient": expected 123 to be ["string"].}
|
48
|
+
assert_equal message, e.message
|
49
|
+
end
|
50
|
+
|
51
|
+
it "detects a parameter of the wrong format" do
|
52
|
+
params = {
|
53
|
+
"app" => "heroku-api",
|
54
|
+
"recipient" => "not-email",
|
55
|
+
}
|
56
|
+
e = assert_raises(Committee::InvalidParams) do
|
57
|
+
Committee::ParamValidator.new(params, @schema, @link_schema).call
|
58
|
+
end
|
59
|
+
message = %{Invalid format for key "recipient": expected "not-email" to be "email".}
|
60
|
+
assert_equal message, e.message
|
61
|
+
end
|
62
|
+
|
63
|
+
it "detects a parameter of the wrong pattern" do
|
64
|
+
params = {
|
65
|
+
"name" => "%@!"
|
66
|
+
}
|
67
|
+
link_schema = @schema["app"]["links"][0]
|
68
|
+
e = assert_raises(Committee::InvalidParams) do
|
69
|
+
Committee::ParamValidator.new(params, @schema, link_schema).call
|
70
|
+
end
|
71
|
+
message = %{Invalid pattern for key "name": expected %@! to match "(?-mix:^[a-z][a-z0-9-]{3,30}$)".}
|
72
|
+
assert_equal message, e.message
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
require "stringio"
|
4
|
+
|
5
|
+
describe Committee::RequestUnpacker do
|
6
|
+
it "unpacks JSON on Content-Type: application/json" do
|
7
|
+
env = {
|
8
|
+
"CONTENT_TYPE" => "application/json",
|
9
|
+
"rack.input" => StringIO.new('{"x":"y"}'),
|
10
|
+
}
|
11
|
+
request = Rack::Request.new(env)
|
12
|
+
params = Committee::RequestUnpacker.new(request).call
|
13
|
+
assert_equal({ "x" => "y" }, params)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "unpacks JSON on no Content-Type" do
|
17
|
+
env = {
|
18
|
+
"rack.input" => StringIO.new('{"x":"y"}'),
|
19
|
+
}
|
20
|
+
request = Rack::Request.new(env)
|
21
|
+
params = Committee::RequestUnpacker.new(request).call
|
22
|
+
assert_equal({ "x" => "y" }, params)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "unpacks an empty hash on an empty request body" do
|
26
|
+
env = {
|
27
|
+
"CONTENT_TYPE" => "application/json",
|
28
|
+
"rack.input" => StringIO.new(""),
|
29
|
+
}
|
30
|
+
request = Rack::Request.new(env)
|
31
|
+
params = Committee::RequestUnpacker.new(request).call
|
32
|
+
assert_equal({}, params)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "unpacks params on Content-Type: application/x-www-form-urlencoded" do
|
36
|
+
env = {
|
37
|
+
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
|
38
|
+
"rack.input" => StringIO.new("x=y"),
|
39
|
+
}
|
40
|
+
request = Rack::Request.new(env)
|
41
|
+
params = Committee::RequestUnpacker.new(request).call
|
42
|
+
assert_equal({ "x" => "y" }, params)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "errors if JSON is not an object" do
|
46
|
+
env = {
|
47
|
+
"CONTENT_TYPE" => "application/json",
|
48
|
+
"rack.input" => StringIO.new('[2]'),
|
49
|
+
}
|
50
|
+
request = Rack::Request.new(env)
|
51
|
+
assert_raises(Committee::BadRequest) do
|
52
|
+
Committee::RequestUnpacker.new(request).call
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it "errors on an unknown Content-Type" do
|
57
|
+
env = {
|
58
|
+
"CONTENT_TYPE" => "application/whats-this",
|
59
|
+
"rack.input" => StringIO.new('{"x":"y"}'),
|
60
|
+
}
|
61
|
+
request = Rack::Request.new(env)
|
62
|
+
assert_raises(Committee::BadRequest) do
|
63
|
+
Committee::RequestUnpacker.new(request).call
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
describe Committee::ResponseGenerator do
|
4
|
+
before do
|
5
|
+
@schema = Committee::Schema.new(File.read("./test/data/schema.json"))
|
6
|
+
@generator = Committee::ResponseGenerator.new(
|
7
|
+
@schema,
|
8
|
+
@schema["app"],
|
9
|
+
@schema["app"]["links"][0]
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "generates string properties" do
|
14
|
+
data = @generator.call
|
15
|
+
assert data["name"].is_a?(String)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "generates non-string properties" do
|
19
|
+
data = @generator.call
|
20
|
+
assert [FalseClass, TrueClass].include?(data["maintenance"].class)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "wraps list data in an array" do
|
24
|
+
@generator = Committee::ResponseGenerator.new(
|
25
|
+
@schema,
|
26
|
+
@schema["app"],
|
27
|
+
@schema["app"]["links"][3]
|
28
|
+
)
|
29
|
+
data = @generator.call
|
30
|
+
assert data.is_a?(Array)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
describe Committee::ResponseValidator do
|
4
|
+
before do
|
5
|
+
@data = ValidApp.dup
|
6
|
+
@schema = Committee::Schema.new(File.read("./test/data/schema.json"))
|
7
|
+
# GET /apps/:id
|
8
|
+
@link_schema = @schema["app"]["links"][2]
|
9
|
+
end
|
10
|
+
|
11
|
+
it "passes through a valid response" do
|
12
|
+
call
|
13
|
+
end
|
14
|
+
|
15
|
+
it "passes through a valid list response" do
|
16
|
+
@data = [@data]
|
17
|
+
# GET /apps
|
18
|
+
@link_schema = @schema["app"]["links"][3]
|
19
|
+
call
|
20
|
+
end
|
21
|
+
|
22
|
+
it "detects an improperly formatted list response" do
|
23
|
+
# GET /apps
|
24
|
+
@link_schema = @schema["app"]["links"][3]
|
25
|
+
e = assert_raises(Committee::InvalidResponse) { call }
|
26
|
+
message = "List endpoints must return an array of objects."
|
27
|
+
assert_equal message, e.message
|
28
|
+
end
|
29
|
+
|
30
|
+
it "detects missing keys in response" do
|
31
|
+
@data.delete("name")
|
32
|
+
e = assert_raises(Committee::InvalidResponse) { call }
|
33
|
+
message = %r{Missing keys in response: name.}
|
34
|
+
assert_match message, e.message
|
35
|
+
end
|
36
|
+
|
37
|
+
it "detects extra keys in response" do
|
38
|
+
@data.merge!("tier" => "important")
|
39
|
+
e = assert_raises(Committee::InvalidResponse) { call }
|
40
|
+
message = %r{Extra keys in response: tier.}
|
41
|
+
assert_match message, e.message
|
42
|
+
end
|
43
|
+
|
44
|
+
it "detects mismatched types" do
|
45
|
+
@data.merge!("maintenance" => "not-bool")
|
46
|
+
e = assert_raises(Committee::InvalidResponse) { call }
|
47
|
+
message = %{Invalid type at "maintenance": expected not-bool to be ["boolean"] (was: string).}
|
48
|
+
assert_equal message, e.message
|
49
|
+
end
|
50
|
+
|
51
|
+
it "detects bad formats" do
|
52
|
+
@data.merge!("id" => "123")
|
53
|
+
e = assert_raises(Committee::InvalidResponse) { call }
|
54
|
+
message = %{Invalid format at "id": expected "123" to be "uuid".}
|
55
|
+
assert_equal message, e.message
|
56
|
+
end
|
57
|
+
|
58
|
+
it "detects bad patterns" do
|
59
|
+
@data.merge!("name" => "%@!")
|
60
|
+
e = assert_raises(Committee::InvalidResponse) { call }
|
61
|
+
message = %{Invalid pattern at "name": expected %@! to match "(?-mix:^[a-z][a-z0-9-]{3,30}$)".}
|
62
|
+
assert_equal message, e.message
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def call
|
68
|
+
Committee::ResponseValidator.new(
|
69
|
+
@data,
|
70
|
+
@schema,
|
71
|
+
@link_schema,
|
72
|
+
@schema["app"]
|
73
|
+
).call
|
74
|
+
end
|
75
|
+
end
|
data/test/router_test.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
describe Committee::Router do
|
4
|
+
before do
|
5
|
+
data = File.read("./test/data/schema.json")
|
6
|
+
schema = Committee::Schema.new(data)
|
7
|
+
@router = Committee::Router.new(schema)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "builds routes without parameters" do
|
11
|
+
assert @router.routes?("GET", "/apps")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "builds routes with parameters" do
|
15
|
+
assert @router.routes?("GET", "/apps/123")
|
16
|
+
end
|
17
|
+
end
|
data/test/schema_test.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
describe Committee::Schema do
|
4
|
+
before do
|
5
|
+
data = File.read("./test/data/schema.json")
|
6
|
+
@schema = Committee::Schema.new(data)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "is enumerable" do
|
10
|
+
arr = @schema.to_a
|
11
|
+
assert_equal "account", arr[0][0]
|
12
|
+
assert arr[0][1].is_a?(Hash)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "can lookup definitions" do
|
16
|
+
expected = {
|
17
|
+
"default" => nil,
|
18
|
+
"description" => "slug size in bytes of app",
|
19
|
+
"example" => 0,
|
20
|
+
"readOnly" => true,
|
21
|
+
"type" => ["integer", "null"]
|
22
|
+
}
|
23
|
+
assert_equal expected, @schema.find("#/definitions/app/definitions/slug_size")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "raises error on a non-existent definition" do
|
27
|
+
assert_raises(Committee::ReferenceNotFound) do
|
28
|
+
@schema.find("/schema/app#/definitions/bad_field")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it "materializes regexes" do
|
33
|
+
definition = @schema.find("#/definitions/app/definitions/git_url")
|
34
|
+
assert definition["pattern"].is_a?(Regexp)
|
35
|
+
end
|
36
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require "minitest"
|
2
|
+
require "minitest/spec"
|
3
|
+
require 'minitest/pride'
|
4
|
+
require "minitest/autorun"
|
5
|
+
|
6
|
+
require "bundler/setup"
|
7
|
+
Bundler.require(:development)
|
8
|
+
|
9
|
+
require_relative "../lib/committee"
|
10
|
+
|
11
|
+
ValidApp = {
|
12
|
+
"archived_at" => "2012-01-01T12:00:00Z",
|
13
|
+
"buildpack_provided_description" => "Ruby/Rack",
|
14
|
+
"created_at" => "2012-01-01T12:00:00Z",
|
15
|
+
"git_url" => "git@heroku.com/example.git",
|
16
|
+
"id" => "01234567-89ab-cdef-0123-456789abcdef",
|
17
|
+
"maintenance" => false,
|
18
|
+
"name" => "example",
|
19
|
+
"owner" => {
|
20
|
+
"email" => "username@example.com",
|
21
|
+
"id" => "01234567-89ab-cdef-0123-456789abcdef"
|
22
|
+
},
|
23
|
+
"region" => {
|
24
|
+
"id" => "01234567-89ab-cdef-0123-456789abcdef",
|
25
|
+
"name" => "us"
|
26
|
+
},
|
27
|
+
"released_at" => "2012-01-01T12:00:00Z",
|
28
|
+
"repo_size" => 0,
|
29
|
+
"slug_size" => 0,
|
30
|
+
"stack" => {
|
31
|
+
"id" => "01234567-89ab-cdef-0123-456789abcdef",
|
32
|
+
"name" => "cedar"
|
33
|
+
},
|
34
|
+
"updated_at" => "2012-01-01T12:00:00Z",
|
35
|
+
"web_url" => "http://example.herokuapp.com"
|
36
|
+
}.freeze
|
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: committee
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Brandur
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-12-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: multi_json
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>'
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>'
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rack
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>'
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0.0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>'
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0.0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: minitest
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rack-test
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rake
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description:
|
95
|
+
email: brandur@mutelight.org
|
96
|
+
executables:
|
97
|
+
- committee-stub
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- lib/committee/errors.rb
|
102
|
+
- lib/committee/middleware/base.rb
|
103
|
+
- lib/committee/middleware/request_validation.rb
|
104
|
+
- lib/committee/middleware/response_validation.rb
|
105
|
+
- lib/committee/middleware/stub.rb
|
106
|
+
- lib/committee/param_validator.rb
|
107
|
+
- lib/committee/request_unpacker.rb
|
108
|
+
- lib/committee/response_generator.rb
|
109
|
+
- lib/committee/response_validator.rb
|
110
|
+
- lib/committee/router.rb
|
111
|
+
- lib/committee/schema.rb
|
112
|
+
- lib/committee.rb
|
113
|
+
- test/middleware/request_validation_test.rb
|
114
|
+
- test/middleware/response_validation_test.rb
|
115
|
+
- test/middleware/stub_test.rb
|
116
|
+
- test/param_validator_test.rb
|
117
|
+
- test/request_unpacker_test.rb
|
118
|
+
- test/response_generator_test.rb
|
119
|
+
- test/response_validator_test.rb
|
120
|
+
- test/router_test.rb
|
121
|
+
- test/schema_test.rb
|
122
|
+
- test/test_helper.rb
|
123
|
+
- bin/committee-stub
|
124
|
+
homepage: https://github.com/brandur/rack-committee
|
125
|
+
licenses:
|
126
|
+
- MIT
|
127
|
+
post_install_message:
|
128
|
+
rdoc_options: []
|
129
|
+
require_paths:
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
none: false
|
133
|
+
requirements:
|
134
|
+
- - ! '>='
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
requirements: []
|
144
|
+
rubyforge_project:
|
145
|
+
rubygems_version: 1.8.24
|
146
|
+
signing_key:
|
147
|
+
specification_version: 3
|
148
|
+
summary: A collection of middleware to support JSON Schema.
|
149
|
+
test_files: []
|