committee 0.1
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.
- 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: []
|