respect-rails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/FAQ.md +98 -0
- data/MIT-LICENSE +20 -0
- data/README.md +291 -0
- data/RELATED_WORK.md +47 -0
- data/RELEASE_NOTES.md +20 -0
- data/Rakefile +32 -0
- data/app/assets/javascripts/respect/rails/schemas.js +32 -0
- data/app/assets/stylesheets/respect/rails/schemas.css +160 -0
- data/app/controllers/respect/rails/schemas_controller.rb +36 -0
- data/app/helpers/respect/rails/schemas_helper.rb +78 -0
- data/app/views/layouts/respect/rails/schemas.html.erb +14 -0
- data/app/views/respect/rails/request_validation_exception.html.erb +38 -0
- data/app/views/respect/rails/schemas/doc.html.erb +158 -0
- data/app/views/respect/rails/schemas/index.html.erb +4 -0
- data/config/routes.rb +4 -0
- data/lib/respect/rails/action_def.rb +37 -0
- data/lib/respect/rails/action_schema.rb +41 -0
- data/lib/respect/rails/application_info.rb +26 -0
- data/lib/respect/rails/controller_helper.rb +107 -0
- data/lib/respect/rails/engine.rb +63 -0
- data/lib/respect/rails/engine_info.rb +27 -0
- data/lib/respect/rails/headers_helper.rb +11 -0
- data/lib/respect/rails/headers_simplifier.rb +31 -0
- data/lib/respect/rails/info.rb +72 -0
- data/lib/respect/rails/request_def.rb +38 -0
- data/lib/respect/rails/request_helper.rb +101 -0
- data/lib/respect/rails/request_schema.rb +102 -0
- data/lib/respect/rails/response_def.rb +43 -0
- data/lib/respect/rails/response_helper.rb +67 -0
- data/lib/respect/rails/response_schema.rb +84 -0
- data/lib/respect/rails/response_schema_set.rb +60 -0
- data/lib/respect/rails/route_info.rb +101 -0
- data/lib/respect/rails/version.rb +5 -0
- data/lib/respect/rails.rb +74 -0
- data/lib/tasks/respect_tasks.rake +4 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/controllers/automatic_validation_controller.rb +300 -0
- data/test/dummy/app/controllers/caught_exception_controller.rb +58 -0
- data/test/dummy/app/controllers/disabled_controller.rb +37 -0
- data/test/dummy/app/controllers/manual_validation_controller.rb +63 -0
- data/test/dummy/app/controllers/no_schema_controller.rb +17 -0
- data/test/dummy/app/controllers/skipped_automatic_validation_controller.rb +35 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/helpers/respect/application_macros.rb +10 -0
- data/test/dummy/app/helpers/respect/circle_schema.rb +16 -0
- data/test/dummy/app/helpers/respect/point_schema.rb +19 -0
- data/test/dummy/app/helpers/respect/rgba_schema.rb +18 -0
- data/test/dummy/app/views/automatic_validation/request_format.html.erb +1 -0
- data/test/dummy/app/views/automatic_validation/request_format.pdf.erb +1 -0
- data/test/dummy/app/views/caught_exception/response_validator.html.erb +1 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config/application.rb +58 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/respect.rb +9 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +38 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/schema.rb +16 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/lib/exts/Rgba.rb +11 -0
- data/test/dummy/lib/exts/circle.rb +11 -0
- data/test/dummy/lib/exts/point.rb +11 -0
- data/test/dummy/log/development.log +6 -0
- data/test/dummy/log/test.log +851 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/functional/automatic_validation_controller_test.rb +131 -0
- data/test/functional/caught_exception_controller_test.rb +50 -0
- data/test/functional/disabled_controller_test.rb +16 -0
- data/test/functional/manual_validation_controller_test.rb +24 -0
- data/test/functional/no_schema_controller_test.rb +12 -0
- data/test/functional/respect/rails/schemas_controller_test.rb +18 -0
- data/test/functional/skipped_automatic_validation_controller_test.rb +12 -0
- data/test/headers_can_dumped_in_json.sh +33 -0
- data/test/integration/navigation_test.rb +38 -0
- data/test/request_headers_validation_in_dev_mode.sh +33 -0
- data/test/test_helper.rb +17 -0
- data/test/unit/action_schema_test.rb +21 -0
- data/test/unit/application_info_test.rb +11 -0
- data/test/unit/controller_helper_test.rb +4 -0
- data/test/unit/engine_info_test.rb +11 -0
- data/test/unit/engine_test.rb +7 -0
- data/test/unit/info_test.rb +42 -0
- data/test/unit/request_def_test.rb +22 -0
- data/test/unit/request_helper_test.rb +67 -0
- data/test/unit/request_schema_test.rb +164 -0
- data/test/unit/response_def_test.rb +9 -0
- data/test/unit/response_helper_test.rb +73 -0
- data/test/unit/response_schema_set_test.rb +18 -0
- data/test/unit/response_schema_test.rb +147 -0
- metadata +334 -0
@@ -0,0 +1,72 @@
|
|
1
|
+
module Respect
|
2
|
+
module Rails
|
3
|
+
# The implementation is strongly inspired from
|
4
|
+
# ActionDispatch::Routing::RoutesInspector.
|
5
|
+
class Info
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@engines = {}
|
9
|
+
@app = ApplicationInfo.new
|
10
|
+
@app.routes = collect_routes(::Rails.application.routes.routes)
|
11
|
+
@engines[@app.name] = @app
|
12
|
+
@toc = build_toc(self.routes)
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :engines
|
16
|
+
attr_reader :app
|
17
|
+
attr_reader :toc
|
18
|
+
|
19
|
+
def routes
|
20
|
+
@app.routes
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def collect_routes(routes, mounted_point = nil)
|
26
|
+
result = []
|
27
|
+
routes.each do |route|
|
28
|
+
route = RouteInfo.new(route, mounted_point)
|
29
|
+
next if route.internal?
|
30
|
+
if route.engine?
|
31
|
+
result += collect_engine_routes(route)
|
32
|
+
else
|
33
|
+
if route.has_schema?
|
34
|
+
result << route
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
def collect_engine_routes(route)
|
42
|
+
return unless route.engine?
|
43
|
+
engine_info = EngineInfo.new(route.endpoint)
|
44
|
+
return if @engines[engine_info.name]
|
45
|
+
|
46
|
+
routes = route.rack_app.routes
|
47
|
+
if routes.is_a?(ActionDispatch::Routing::RouteSet)
|
48
|
+
engine_info.routes = collect_routes(routes.routes, route)
|
49
|
+
@engines[engine_info.name] = engine_info
|
50
|
+
engine_info.routes
|
51
|
+
else
|
52
|
+
[]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_toc(routes)
|
57
|
+
toc = {}
|
58
|
+
routes.each do |route|
|
59
|
+
next if route.engine?
|
60
|
+
if route.has_schema?
|
61
|
+
controller = route.controller_name
|
62
|
+
action = route.action_name
|
63
|
+
toc[controller] ||= {}
|
64
|
+
toc[controller][action] = route
|
65
|
+
end
|
66
|
+
end
|
67
|
+
toc
|
68
|
+
end
|
69
|
+
|
70
|
+
end # class Info
|
71
|
+
end # module Rails
|
72
|
+
end # module Respect
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Respect
|
2
|
+
module Rails
|
3
|
+
class RequestDef
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def eval(*args, &block)
|
7
|
+
new(*args).eval(&block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
@request_schema = RequestSchema.new(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def eval(&block)
|
16
|
+
block.call(self)
|
17
|
+
@request_schema
|
18
|
+
end
|
19
|
+
|
20
|
+
def path_parameters(&block)
|
21
|
+
@request_schema.path_parameters = HashSchema.define(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def body_parameters(&block)
|
25
|
+
@request_schema.body_parameters = HashSchema.define(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def query_parameters(&block)
|
29
|
+
@request_schema.query_parameters = HashSchema.define(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def headers(&block)
|
33
|
+
@request_schema.headers = HashSchema.define(&block)
|
34
|
+
end
|
35
|
+
|
36
|
+
end # class RequestDef
|
37
|
+
end
|
38
|
+
end # module Respect
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Respect
|
2
|
+
module Rails
|
3
|
+
module RequestHelper
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Return whether this request validates the schema.
|
7
|
+
# You can get the validation error via {#last_validation_error}.
|
8
|
+
def validate_schema?
|
9
|
+
begin
|
10
|
+
validate_schema
|
11
|
+
rescue Respect::Rails::RequestValidationError
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Raise a {Respect::Rails::RequestValidationError} exception if this
|
17
|
+
# request does not validate the schema.
|
18
|
+
def validate_schema
|
19
|
+
log_msg = " Request validation: "
|
20
|
+
@validated = nil
|
21
|
+
measure = Benchmark.realtime do
|
22
|
+
@validated = request_schema.validate?(self) unless request_schema.nil?
|
23
|
+
end
|
24
|
+
if @validated.nil?
|
25
|
+
log_msg += "none"
|
26
|
+
else
|
27
|
+
if @validated == true
|
28
|
+
log_msg += "success"
|
29
|
+
else
|
30
|
+
log_msg += "failure"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
log_msg += " (%.1fms)" % [ measure * 1000 ]
|
34
|
+
::Rails.logger.info log_msg
|
35
|
+
if @validated == false
|
36
|
+
last_validation_error.context.each do |msg|
|
37
|
+
::Rails.logger.info " #{msg}"
|
38
|
+
end
|
39
|
+
raise last_validation_error
|
40
|
+
end
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
def response_schema(http_status)
|
45
|
+
unless action_schema.nil? || action_schema.responses.nil?
|
46
|
+
action_schema.responses[http_status]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def request_schema
|
51
|
+
action_schema.request if action_schema
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :action_schema
|
55
|
+
attr_writer :action_schema
|
56
|
+
private :action_schema=
|
57
|
+
|
58
|
+
def has_schema?
|
59
|
+
!!action_schema
|
60
|
+
end
|
61
|
+
|
62
|
+
def last_validation_error
|
63
|
+
request_schema.last_error
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the sanitized parameters if the schema validation has succeed.
|
67
|
+
def sane_params
|
68
|
+
request_schema.sanitized_params if request_schema
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns +nil+ if never validated, +true+ if validated successfully and
|
72
|
+
# +false+ if validation failed.
|
73
|
+
def validated
|
74
|
+
@validated
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns +true+ if the validation has been done and succeed and +false+
|
78
|
+
# otherwise.
|
79
|
+
def validated?
|
80
|
+
!!validated
|
81
|
+
end
|
82
|
+
|
83
|
+
# Sanitize all the request's parameters (path, query and body) *in-place*.
|
84
|
+
# if the schema validation has succeed. Validate it first if it has not
|
85
|
+
# been yet.
|
86
|
+
def sanitize_params!
|
87
|
+
if request_schema
|
88
|
+
if validated.nil?
|
89
|
+
validate_schema
|
90
|
+
end
|
91
|
+
if validated?
|
92
|
+
request_schema.sanitize!(self)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end # module RequestHelper
|
98
|
+
end # module Rails
|
99
|
+
end # module Respect
|
100
|
+
|
101
|
+
ActionDispatch::Request.send :include, Respect::Rails::RequestHelper
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Respect
|
2
|
+
module Rails
|
3
|
+
class RequestSchema
|
4
|
+
include HeadersSimplifier
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def define(*args, &block)
|
8
|
+
RequestDef.eval(*args, &block)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(controller, action)
|
13
|
+
@controller = controller
|
14
|
+
@action = action
|
15
|
+
@default_path_parameters = HashSchema.define do |s|
|
16
|
+
s.string "controller", equal_to: @controller.to_s, doc: false
|
17
|
+
s.string "action", equal_to: @action.to_s, doc: false
|
18
|
+
end
|
19
|
+
@path_parameters = @default_path_parameters.dup
|
20
|
+
@body_parameters = HashSchema.new
|
21
|
+
@query_parameters = HashSchema.new
|
22
|
+
@headers = HashSchema.new
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :controller, :action
|
26
|
+
|
27
|
+
attr_accessor :headers
|
28
|
+
|
29
|
+
attr_accessor :body_parameters, :query_parameters
|
30
|
+
|
31
|
+
attr_reader :path_parameters, :default_path_parameters
|
32
|
+
|
33
|
+
# Merge +path_parameters+ with {#default_path_parameters} and store it.
|
34
|
+
def path_parameters=(path_parameters)
|
35
|
+
@path_parameters = @default_path_parameters.merge(path_parameters)
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :sanitized_params
|
39
|
+
|
40
|
+
# Validate the given +request+.
|
41
|
+
# Raise a {RequestValidationError} if an error occur.
|
42
|
+
# Returns +true+ on success.
|
43
|
+
def validate(request)
|
44
|
+
# Validate requests.
|
45
|
+
unless Respect::Rails::Engine.disable_request_headers_validation
|
46
|
+
begin
|
47
|
+
headers.validate(request.headers)
|
48
|
+
rescue Respect::ValidationError => e
|
49
|
+
raise RequestValidationError.new(e, :headers, simplify_headers(request.headers))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
[ :path, :query, :body ].each do |name|
|
53
|
+
begin
|
54
|
+
send("#{name}_parameters").validate(request.params)
|
55
|
+
rescue Respect::ValidationError => e
|
56
|
+
raise RequestValidationError.new(e, name, request.params)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
# Build sane parameters.
|
60
|
+
@sanitized_params = {}
|
61
|
+
[ :path, :query, :body ].each do |name|
|
62
|
+
@sanitized_params.merge!(send("#{name}_parameters").sanitized_object)
|
63
|
+
end
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
def validate?(request)
|
68
|
+
begin
|
69
|
+
validate(request)
|
70
|
+
true
|
71
|
+
rescue RequestValidationError => e
|
72
|
+
@last_error = e
|
73
|
+
false
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Return the last validation error that happens during the
|
78
|
+
# validation process. (set by {#validate?})
|
79
|
+
# Reset each time {#validate?} is called.
|
80
|
+
attr_reader :last_error
|
81
|
+
|
82
|
+
# Validate the request and sanitize its parameters if the validation succeed.
|
83
|
+
# You can disable the sanitization with
|
84
|
+
# {Respect::Rails::Engine.sanitize_request_parameters}.
|
85
|
+
def validate!(request)
|
86
|
+
valid = validate?(request)
|
87
|
+
if valid
|
88
|
+
sanitize!(request)
|
89
|
+
end
|
90
|
+
valid
|
91
|
+
end
|
92
|
+
|
93
|
+
# Sanitize all the request's parameters (path, query and body) *in-place*.
|
94
|
+
def sanitize!(request)
|
95
|
+
[ :path, :query, :body ].each do |name|
|
96
|
+
send("#{name}_parameters").sanitize_object!(request.params)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Respect
|
2
|
+
module Rails
|
3
|
+
class ResponseDef
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def eval(*args, &block)
|
7
|
+
new(*args).eval(&block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
@response_schema = ResponseSchema.new(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def eval(&block)
|
16
|
+
block.call(self)
|
17
|
+
@response_schema
|
18
|
+
end
|
19
|
+
|
20
|
+
# Define the schema of the response body.
|
21
|
+
# @option options [Boolean] hash (true) whether the body is hash schema.
|
22
|
+
def body(options = {}, &block)
|
23
|
+
@response_schema.body = (
|
24
|
+
if options.fetch(:hash, true)
|
25
|
+
HashSchema.define(&block)
|
26
|
+
else
|
27
|
+
Schema.define(&block)
|
28
|
+
end
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def headers(options = {}, &block)
|
33
|
+
@response_schema.headers = HashSchema.define(options, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Set the documentation to +text+.
|
37
|
+
def documentation(text)
|
38
|
+
@response_schema.documentation = text
|
39
|
+
end
|
40
|
+
|
41
|
+
end # class ResponseDef
|
42
|
+
end # module Rails
|
43
|
+
end # module Respect
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Respect
|
2
|
+
module Rails
|
3
|
+
module ResponseHelper
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
attr_reader :schema
|
7
|
+
attr_writer :schema
|
8
|
+
private :schema=
|
9
|
+
|
10
|
+
def has_schema?
|
11
|
+
!!@schema
|
12
|
+
end
|
13
|
+
|
14
|
+
# Return whether this response validates the schema.
|
15
|
+
# You can get the validation error via {#last_validation_error}.
|
16
|
+
def validate_schema?
|
17
|
+
begin
|
18
|
+
validate_schema
|
19
|
+
rescue Respect::Rails::ResponseValidationError => e
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Raise a {Respect::Rails::ResponseValidationError} exception if this
|
25
|
+
# response does not validate the schema.
|
26
|
+
def validate_schema
|
27
|
+
log_msg = " Response validation: "
|
28
|
+
valid = nil
|
29
|
+
measure = Benchmark.realtime do
|
30
|
+
if schema && content_type == Mime::JSON
|
31
|
+
valid = schema.validate?(self)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
if valid.nil?
|
35
|
+
log_msg += "none"
|
36
|
+
else
|
37
|
+
if valid
|
38
|
+
log_msg += "success"
|
39
|
+
else
|
40
|
+
log_msg += "failure"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
log_msg += " (%.1fms)" % [ measure * 1000 ]
|
44
|
+
::Rails.logger.info log_msg
|
45
|
+
if valid == false
|
46
|
+
last_validation_error.context.each do |msg|
|
47
|
+
::Rails.logger.info " #{msg}"
|
48
|
+
end
|
49
|
+
if Respect::Rails::Engine.catch_response_validation_error
|
50
|
+
self.body = last_validation_error.to_json
|
51
|
+
self.status = :internal_server_error
|
52
|
+
else
|
53
|
+
raise last_validation_error
|
54
|
+
end
|
55
|
+
end
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
def last_validation_error
|
60
|
+
schema.last_error
|
61
|
+
end
|
62
|
+
|
63
|
+
end # module ResponseHelper
|
64
|
+
end # module Rails
|
65
|
+
end # module Respect
|
66
|
+
|
67
|
+
ActionDispatch::Response.send :include, Respect::Rails::ResponseHelper
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Respect
|
2
|
+
module Rails
|
3
|
+
class ResponseSchema
|
4
|
+
include HeadersSimplifier
|
5
|
+
include Respect::DocHelper
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def http_status(status)
|
10
|
+
Rack::Utils.status_code(status)
|
11
|
+
end
|
12
|
+
|
13
|
+
# FIXME(Nicolas Despres): Move me to another module/class.
|
14
|
+
def symbolize_http_status(http_status)
|
15
|
+
h = Rack::Utils::HTTP_STATUS_CODES
|
16
|
+
(h[http_status] || h[500]).downcase.gsub(/\s|-/, '_').to_sym
|
17
|
+
end
|
18
|
+
|
19
|
+
def define(*args, &block)
|
20
|
+
ResponseDef.eval(*args, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Read the response schema definition from the given +filename+ and
|
24
|
+
# evaluate it as a block passed to {ResponseSchema.define}
|
25
|
+
def from_file(status, filename)
|
26
|
+
define(status) do |r|
|
27
|
+
r.instance_eval(File.read(filename), filename)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(status = :ok)
|
33
|
+
@status = status
|
34
|
+
@headers = HashSchema.new
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :status
|
38
|
+
|
39
|
+
def http_status
|
40
|
+
self.class.http_status(@status)
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_accessor :body
|
44
|
+
|
45
|
+
def ==(other)
|
46
|
+
@status == other.status && @body == other.body
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate(response)
|
50
|
+
if headers
|
51
|
+
begin
|
52
|
+
headers.validate(response.headers)
|
53
|
+
rescue Respect::ValidationError => e
|
54
|
+
raise Respect::Rails::ResponseValidationError.new(e, :headers, simplify_headers(response.headers))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
if body
|
58
|
+
decoded_body = ActiveSupport::JSON.decode(response.body)
|
59
|
+
begin
|
60
|
+
body.validate(decoded_body)
|
61
|
+
rescue Respect::ValidationError => e
|
62
|
+
raise Respect::Rails::ResponseValidationError.new(e, :body, decoded_body)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
def validate?(response)
|
69
|
+
begin
|
70
|
+
validate(response)
|
71
|
+
true
|
72
|
+
rescue Respect::Rails::ResponseValidationError => e
|
73
|
+
@last_error = e
|
74
|
+
false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
attr_reader :last_error
|
79
|
+
|
80
|
+
attr_accessor :headers
|
81
|
+
attr_accessor :documentation
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Respect
|
2
|
+
module Rails
|
3
|
+
class ResponseSchemaSet
|
4
|
+
|
5
|
+
class << self
|
6
|
+
# FIXME(Nicolas Despres): Move me to another module/class.
|
7
|
+
def symbolize_status(status)
|
8
|
+
case status
|
9
|
+
when Symbol
|
10
|
+
status
|
11
|
+
when String
|
12
|
+
if status =~ /^\d+$/
|
13
|
+
symbolize_status(status.to_i)
|
14
|
+
else
|
15
|
+
status.to_sym
|
16
|
+
end
|
17
|
+
when Numeric
|
18
|
+
ResponseSchema.symbolize_http_status(status.to_i)
|
19
|
+
else
|
20
|
+
raise ArgumentError, "cannot normalize status '#{status}:#{status.class}'"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Initialize a new ResponseSchemaSet object for the given controller's
|
26
|
+
# action and collect response' schema from their respective file.
|
27
|
+
def initialize(controller_name, action_name)
|
28
|
+
@controller_name = controller_name
|
29
|
+
@action_name = action_name
|
30
|
+
@set = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :controller_name, :action_name
|
34
|
+
|
35
|
+
def method_missing(method_name, *arguments, &block)
|
36
|
+
define_response(method_name, *arguments, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def [](http_status)
|
40
|
+
@set[http_status]
|
41
|
+
end
|
42
|
+
|
43
|
+
delegate :each, :empty?, to: :@set
|
44
|
+
|
45
|
+
def <<(response_schema)
|
46
|
+
@set[response_schema.http_status] = response_schema
|
47
|
+
end
|
48
|
+
|
49
|
+
def is(status, *arguments, &block)
|
50
|
+
define_response(status, *arguments, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def define_response(status, *arguments, &block)
|
54
|
+
status = self.class.symbolize_status(status)
|
55
|
+
self << ResponseSchema.define(status, *arguments, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end # module Rails
|
60
|
+
end # module Respect
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Respect
|
2
|
+
module Rails
|
3
|
+
# A wrapper around ActionDispatch::Routing::Route. It provides only
|
4
|
+
# access to the information we need when generating doc.
|
5
|
+
# It is very strongly inspired from ActionDispatch::Routing::RouteWrapper class
|
6
|
+
# which is internal and designed to be used with
|
7
|
+
# ActionDispoatch::Routing::RouteInspector. These classes are not meant
|
8
|
+
# to be used by external user as far as I can read in rails code base.
|
9
|
+
# So, we made up our own so that we can easily adjust it to our need.
|
10
|
+
class RouteInfo
|
11
|
+
include Comparable
|
12
|
+
|
13
|
+
def initialize(route, mount_point)
|
14
|
+
@route = route
|
15
|
+
if !mount_point.is_a?(self.class) && !mount_point.nil? && !mount_point.engine?
|
16
|
+
raise "'#{mount_point.inspect}' must be a route to an engine if set"
|
17
|
+
end
|
18
|
+
@mount_point = mount_point
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :route, :mount_point
|
22
|
+
|
23
|
+
def path
|
24
|
+
path = @route.path.spec.to_s
|
25
|
+
if @mount_point
|
26
|
+
path = @mount_point.path + path
|
27
|
+
end
|
28
|
+
if path.length > 1 && path =~ %r{/$}
|
29
|
+
path.chop!
|
30
|
+
end
|
31
|
+
path
|
32
|
+
end
|
33
|
+
|
34
|
+
def internal?
|
35
|
+
path =~ %r{\A#{::Rails.application.config.assets.prefix}} \
|
36
|
+
|| controller_name =~ %r{\Arails/(info|welcome)}
|
37
|
+
end
|
38
|
+
|
39
|
+
def <=>(other)
|
40
|
+
self.path <=> other.path
|
41
|
+
end
|
42
|
+
|
43
|
+
def verb
|
44
|
+
@route.verb.source.gsub(/[$^]/, '')
|
45
|
+
end
|
46
|
+
|
47
|
+
def controller_name
|
48
|
+
@route.requirements[:controller] || ':controller'
|
49
|
+
end
|
50
|
+
|
51
|
+
def action_name
|
52
|
+
@route.requirements[:action] || ':action'
|
53
|
+
end
|
54
|
+
|
55
|
+
def schema
|
56
|
+
@schema = ActionSchema.from_controller(controller_name, action_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def has_schema?
|
60
|
+
schema && schema.has_schema?
|
61
|
+
end
|
62
|
+
|
63
|
+
def spec
|
64
|
+
"#{verb} #{path}".strip
|
65
|
+
end
|
66
|
+
|
67
|
+
def url
|
68
|
+
if schema
|
69
|
+
options = schema_set.default_url_options
|
70
|
+
else
|
71
|
+
options = @route.defaults.merge({ format: 'json' })
|
72
|
+
end
|
73
|
+
::Rails.application.routes.url_for(options)
|
74
|
+
end
|
75
|
+
|
76
|
+
def endpoint
|
77
|
+
rack_app ? rack_app : nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def rack_app(app = @route.app)
|
81
|
+
@rack_app ||= begin
|
82
|
+
class_name = app.class.name.to_s
|
83
|
+
if class_name == "ActionDispatch::Routing::Mapper::Constraints"
|
84
|
+
rack_app(app.app)
|
85
|
+
elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/
|
86
|
+
app
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def engine?
|
92
|
+
rack_app && rack_app.respond_to?(:routes)
|
93
|
+
end
|
94
|
+
|
95
|
+
def anchor
|
96
|
+
"#{controller_name}_#{action_name}"
|
97
|
+
end
|
98
|
+
|
99
|
+
end # class RouteInfo
|
100
|
+
end # module Rails
|
101
|
+
end # module Respect
|