apes 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rubocop.yml +82 -0
- data/.travis-gemfile +15 -0
- data/.travis.yml +15 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +22 -0
- data/README.md +177 -0
- data/Rakefile +44 -0
- data/apes.gemspec +34 -0
- data/doc/Apes.html +130 -0
- data/doc/Apes/Concerns.html +127 -0
- data/doc/Apes/Concerns/Errors.html +1089 -0
- data/doc/Apes/Concerns/Pagination.html +636 -0
- data/doc/Apes/Concerns/Request.html +766 -0
- data/doc/Apes/Concerns/Response.html +940 -0
- data/doc/Apes/Controller.html +1100 -0
- data/doc/Apes/Errors.html +125 -0
- data/doc/Apes/Errors/AuthenticationError.html +133 -0
- data/doc/Apes/Errors/BadRequestError.html +157 -0
- data/doc/Apes/Errors/BaseError.html +320 -0
- data/doc/Apes/Errors/InvalidDataError.html +157 -0
- data/doc/Apes/Errors/MissingDataError.html +157 -0
- data/doc/Apes/Model.html +378 -0
- data/doc/Apes/PaginationCursor.html +2138 -0
- data/doc/Apes/RuntimeConfiguration.html +909 -0
- data/doc/Apes/Serializers.html +125 -0
- data/doc/Apes/Serializers/JSON.html +389 -0
- data/doc/Apes/Serializers/JWT.html +452 -0
- data/doc/Apes/Serializers/List.html +347 -0
- data/doc/Apes/UrlsParser.html +1432 -0
- data/doc/Apes/Validators.html +125 -0
- data/doc/Apes/Validators/BaseValidator.html +278 -0
- data/doc/Apes/Validators/BooleanValidator.html +494 -0
- data/doc/Apes/Validators/EmailValidator.html +350 -0
- data/doc/Apes/Validators/PhoneValidator.html +375 -0
- data/doc/Apes/Validators/ReferenceValidator.html +372 -0
- data/doc/Apes/Validators/TimestampValidator.html +640 -0
- data/doc/Apes/Validators/UuidValidator.html +372 -0
- data/doc/Apes/Validators/ZipCodeValidator.html +372 -0
- data/doc/Apes/Version.html +189 -0
- data/doc/ApplicationController.html +547 -0
- data/doc/Concerns.html +128 -0
- data/doc/Concerns/ErrorHandling.html +826 -0
- data/doc/Concerns/PaginationHandling.html +463 -0
- data/doc/Concerns/RequestHandling.html +512 -0
- data/doc/Concerns/ResponseHandling.html +579 -0
- data/doc/Errors.html +126 -0
- data/doc/Errors/AuthenticationError.html +123 -0
- data/doc/Errors/BadRequestError.html +147 -0
- data/doc/Errors/BaseError.html +289 -0
- data/doc/Errors/InvalidDataError.html +147 -0
- data/doc/Errors/MissingDataError.html +147 -0
- data/doc/Model.html +315 -0
- data/doc/PaginationCursor.html +764 -0
- data/doc/Serializers.html +126 -0
- data/doc/Serializers/JSON.html +253 -0
- data/doc/Serializers/JWT.html +253 -0
- data/doc/Serializers/List.html +245 -0
- data/doc/Validators.html +126 -0
- data/doc/Validators/BaseValidator.html +209 -0
- data/doc/Validators/BooleanValidator.html +391 -0
- data/doc/Validators/EmailValidator.html +298 -0
- data/doc/Validators/PhoneValidator.html +313 -0
- data/doc/Validators/ReferenceValidator.html +284 -0
- data/doc/Validators/TimestampValidator.html +476 -0
- data/doc/Validators/UuidValidator.html +310 -0
- data/doc/Validators/ZipCodeValidator.html +310 -0
- data/doc/_index.html +435 -0
- data/doc/class_list.html +58 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +57 -0
- data/doc/css/style.css +339 -0
- data/doc/file.README.html +252 -0
- data/doc/file_list.html +60 -0
- data/doc/frames.html +26 -0
- data/doc/index.html +252 -0
- data/doc/js/app.js +219 -0
- data/doc/js/full_list.js +181 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +615 -0
- data/doc/top-level-namespace.html +112 -0
- data/lib/apes.rb +40 -0
- data/lib/apes/concerns/errors.rb +111 -0
- data/lib/apes/concerns/pagination.rb +81 -0
- data/lib/apes/concerns/request.rb +237 -0
- data/lib/apes/concerns/response.rb +74 -0
- data/lib/apes/controller.rb +77 -0
- data/lib/apes/errors.rb +38 -0
- data/lib/apes/model.rb +94 -0
- data/lib/apes/pagination_cursor.rb +152 -0
- data/lib/apes/runtime_configuration.rb +80 -0
- data/lib/apes/serializers.rb +88 -0
- data/lib/apes/urls_parser.rb +233 -0
- data/lib/apes/validators.rb +234 -0
- data/lib/apes/version.rb +24 -0
- data/spec/apes/concerns/errors_spec.rb +141 -0
- data/spec/apes/concerns/pagination_spec.rb +114 -0
- data/spec/apes/concerns/request_spec.rb +244 -0
- data/spec/apes/concerns/response_spec.rb +79 -0
- data/spec/apes/controller_spec.rb +54 -0
- data/spec/apes/errors_spec.rb +14 -0
- data/spec/apes/models_spec.rb +148 -0
- data/spec/apes/pagination_cursor_spec.rb +113 -0
- data/spec/apes/runtime_configuration_spec.rb +100 -0
- data/spec/apes/serializers_spec.rb +70 -0
- data/spec/apes/urls_parser_spec.rb +150 -0
- data/spec/apes/validators_spec.rb +237 -0
- data/spec/spec_helper.rb +30 -0
- data/views/_included.json.jbuilder +9 -0
- data/views/_pagination.json.jbuilder +9 -0
- data/views/collection.json.jbuilder +4 -0
- data/views/errors/400.json.jbuilder +9 -0
- data/views/errors/403.json.jbuilder +7 -0
- data/views/errors/404.json.jbuilder +6 -0
- data/views/errors/422.json.jbuilder +19 -0
- data/views/errors/500.json.jbuilder +12 -0
- data/views/errors/501.json.jbuilder +7 -0
- data/views/layouts/general.json.jbuilder +36 -0
- data/views/object.json.jbuilder +4 -0
- metadata +262 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
|
2
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
6
|
+
<title>
|
|
7
|
+
Top Level Namespace
|
|
8
|
+
|
|
9
|
+
— Documentation by YARD 0.8.7.6
|
|
10
|
+
|
|
11
|
+
</title>
|
|
12
|
+
|
|
13
|
+
<link rel="stylesheet" href="css/style.css" type="text/css" charset="utf-8" />
|
|
14
|
+
|
|
15
|
+
<link rel="stylesheet" href="css/common.css" type="text/css" charset="utf-8" />
|
|
16
|
+
|
|
17
|
+
<script type="text/javascript" charset="utf-8">
|
|
18
|
+
hasFrames = window.top.frames.main ? true : false;
|
|
19
|
+
relpath = '';
|
|
20
|
+
framesUrl = "frames.html#!top-level-namespace.html";
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
<script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
|
|
25
|
+
|
|
26
|
+
<script type="text/javascript" charset="utf-8" src="js/app.js"></script>
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<div id="header">
|
|
32
|
+
<div id="menu">
|
|
33
|
+
|
|
34
|
+
<a href="_index.html">Index</a> »
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
<span class="title">Top Level Namespace</span>
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
<div class="noframes"><span class="title">(</span><a href="." target="_top">no frames</a><span class="title">)</span></div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div id="search">
|
|
44
|
+
|
|
45
|
+
<a class="full_list_link" id="class_list_link"
|
|
46
|
+
href="class_list.html">
|
|
47
|
+
Class List
|
|
48
|
+
</a>
|
|
49
|
+
|
|
50
|
+
<a class="full_list_link" id="method_list_link"
|
|
51
|
+
href="method_list.html">
|
|
52
|
+
Method List
|
|
53
|
+
</a>
|
|
54
|
+
|
|
55
|
+
<a class="full_list_link" id="file_list_link"
|
|
56
|
+
href="file_list.html">
|
|
57
|
+
File List
|
|
58
|
+
</a>
|
|
59
|
+
|
|
60
|
+
</div>
|
|
61
|
+
<div class="clear"></div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<iframe id="search_frame"></iframe>
|
|
65
|
+
|
|
66
|
+
<div id="content"><h1>Top Level Namespace
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
</h1>
|
|
71
|
+
|
|
72
|
+
<dl class="box">
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
</dl>
|
|
82
|
+
<div class="clear"></div>
|
|
83
|
+
|
|
84
|
+
<h2>Defined Under Namespace</h2>
|
|
85
|
+
<p class="children">
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
<strong class="modules">Modules:</strong> <span class='object_link'><a href="Apes.html" title="Apes (module)">Apes</a></span>
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div id="footer">
|
|
106
|
+
Generated on Sat Jun 4 12:41:53 2016 by
|
|
107
|
+
<a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
108
|
+
0.8.7.6 (ruby-2.3.0).
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
</body>
|
|
112
|
+
</html>
|
data/lib/apes.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the apes gem. Copyright (C) 2016 and above Shogun <shogun@cowtech.it>.
|
|
3
|
+
# Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
require "lazier"
|
|
7
|
+
require "mustache"
|
|
8
|
+
require "jwt"
|
|
9
|
+
require "jbuilder"
|
|
10
|
+
require "active_model"
|
|
11
|
+
require "rails"
|
|
12
|
+
require "rails-api/action_controller/api"
|
|
13
|
+
|
|
14
|
+
require "apes/version" unless defined?(Apes::Version)
|
|
15
|
+
require "apes/runtime_configuration"
|
|
16
|
+
|
|
17
|
+
require "apes/errors"
|
|
18
|
+
require "apes/urls_parser"
|
|
19
|
+
require "apes/pagination_cursor"
|
|
20
|
+
|
|
21
|
+
require "apes/serializers"
|
|
22
|
+
require "apes/validators"
|
|
23
|
+
require "apes/model"
|
|
24
|
+
|
|
25
|
+
require "apes/concerns/errors"
|
|
26
|
+
require "apes/concerns/pagination"
|
|
27
|
+
require "apes/concerns/request"
|
|
28
|
+
require "apes/concerns/response"
|
|
29
|
+
require "apes/controller"
|
|
30
|
+
|
|
31
|
+
Lazier.load!(:object, :string)
|
|
32
|
+
|
|
33
|
+
ActiveSupport.on_load(:action_controller) do
|
|
34
|
+
prepend_view_path(Apes::RuntimeConfiguration.root + "/views") if respond_to?(:prepend_view_path)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
ActiveSupport.on_load(:action_view) do
|
|
38
|
+
include Apes::Concerns::Response
|
|
39
|
+
include Apes::Concerns::Pagination
|
|
40
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the apes gem. Copyright (C) 2016 and above Shogun <shogun@cowtech.it>.
|
|
3
|
+
# Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
module Apes
|
|
7
|
+
# A set of module to handle JSON API in a Rails controller.
|
|
8
|
+
module Concerns
|
|
9
|
+
# Errors handling module.
|
|
10
|
+
module Errors
|
|
11
|
+
# Default map of error handlers
|
|
12
|
+
ERROR_HANDLERS = {
|
|
13
|
+
"ActiveRecord::RecordNotFound" => :error_handle_not_found,
|
|
14
|
+
"Apes::Errors::AuthenticationError" => :error_handle_fordidden,
|
|
15
|
+
"Apes::Errors::InvalidModelError" => :error_handle_invalid_source,
|
|
16
|
+
"Apes::Errors::BadRequestError" => :error_handle_bad_request,
|
|
17
|
+
"Apes::Errors::MissingDataError" => :error_handle_missing_data,
|
|
18
|
+
"Apes::Errors::InvalidDataError" => :error_handle_invalid_data,
|
|
19
|
+
"JSON::ParserError" => :error_handle_invalid_data,
|
|
20
|
+
"ActiveRecord::RecordInvalid" => :error_handle_validation,
|
|
21
|
+
"ActiveRecord::UnknownAttributeError" => :error_handle_unknown_attribute,
|
|
22
|
+
"ActionController::UnpermittedParameters" => :error_handle_unknown_attribute,
|
|
23
|
+
"Apes::Errors::BaseError" => :error_handle_general,
|
|
24
|
+
"Lazier::Exceptions::Debug" => :error_handle_debug
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# Handles a failed request.
|
|
28
|
+
#
|
|
29
|
+
# @param status [Symbol|Fixnum] The HTTP error code.
|
|
30
|
+
# @param error [Object] The occurred error.
|
|
31
|
+
def fail_request!(status, error)
|
|
32
|
+
raise(::Apes::Errors::BaseError, {status: status, error: error})
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Default unexpected exception handler.
|
|
36
|
+
#
|
|
37
|
+
# @param exception [Exception] The exception to handle.
|
|
38
|
+
def error_handle_exception(exception)
|
|
39
|
+
handler = ERROR_HANDLERS.fetch(exception.class.to_s, :error_handle_others)
|
|
40
|
+
send(handler, exception)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Handles base exceptions.
|
|
44
|
+
#
|
|
45
|
+
# @param exception [Exception] The exception to handle.
|
|
46
|
+
def error_handle_general(exception)
|
|
47
|
+
render_error(exception.details[:status], exception.details[:error])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Handles other exceptions.
|
|
51
|
+
#
|
|
52
|
+
# @param exception [Exception] The exception to handle.
|
|
53
|
+
def error_handle_others(exception)
|
|
54
|
+
@exception = exception
|
|
55
|
+
@backtrace = exception.backtrace
|
|
56
|
+
.slice(0, 50).map { |line| line.gsub(Apes::RuntimeConfiguration.rails_root, "$RAILS").gsub(Apes::RuntimeConfiguration.gems_root, "$GEMS") }
|
|
57
|
+
render("errors/500", status: :internal_server_error)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Handles debug exceptions.
|
|
61
|
+
#
|
|
62
|
+
# @param exception [Exception] The exception to handle.
|
|
63
|
+
def error_handle_debug(exception)
|
|
64
|
+
render("errors/400", status: 418, locals: {debug: YAML.load(exception.message)})
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Handles unauthorized requests.
|
|
68
|
+
#
|
|
69
|
+
# @param exception [Exception] The exception to handle.
|
|
70
|
+
def error_handle_fordidden(exception)
|
|
71
|
+
@authentication_error = {error: exception.message.present? ? exception.message : "You don't have access to this resource."}
|
|
72
|
+
render("errors/403", status: :forbidden)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Handles requests of missing data.
|
|
76
|
+
def error_handle_not_found(_ = nil)
|
|
77
|
+
render("errors/404", status: :not_found)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Handles requests containing invalid data.
|
|
81
|
+
def error_handle_bad_request(_ = nil)
|
|
82
|
+
@reason = "Invalid Content-Type specified. Please use \"#{request_valid_content_type}\" when performing write operations."
|
|
83
|
+
render("errors/400", status: :bad_request)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Handles requests that miss data.
|
|
87
|
+
def error_handle_missing_data(_ = nil)
|
|
88
|
+
@reason = "Missing data."
|
|
89
|
+
render("errors/400", status: :bad_request)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Handles requests that send invalid data.
|
|
93
|
+
def error_handle_invalid_data(_ = nil)
|
|
94
|
+
@reason = "Invalid data provided."
|
|
95
|
+
render("errors/400", status: :bad_request)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Handles requests that send data with unexpected attributes.
|
|
99
|
+
def error_handle_unknown_attribute(exception)
|
|
100
|
+
@errors = exception.is_a?(ActionController::UnpermittedParameters) ? exception.params : exception.attribute
|
|
101
|
+
render("errors/422", status: :unprocessable_entity)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Handles requests that send data with invalid attributes.
|
|
105
|
+
def error_handle_validation(exception)
|
|
106
|
+
@errors = exception.record.errors.to_hash
|
|
107
|
+
render("errors/422", status: :unprocessable_entity)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the apes gem. Copyright (C) 2016 and above Shogun <shogun@cowtech.it>.
|
|
3
|
+
# Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
module Apes
|
|
7
|
+
module Concerns
|
|
8
|
+
# Pagination handling module.
|
|
9
|
+
module Pagination
|
|
10
|
+
# Paginates a collection according to the current cursor.
|
|
11
|
+
#
|
|
12
|
+
# @param collection [ActiveRecord::Relation] The collection to paginate.
|
|
13
|
+
# @param sort_field [Symbol] The field to use for pagination.
|
|
14
|
+
# @param sort_order [Symbol] The order to use for pagination.
|
|
15
|
+
# @return [ActiveRecord::Relation] The paginated collection.
|
|
16
|
+
def paginate(collection, sort_field: :id, sort_order: :desc)
|
|
17
|
+
direction = @cursor.direction
|
|
18
|
+
value = @cursor.value
|
|
19
|
+
|
|
20
|
+
# Apply the query
|
|
21
|
+
collection = apply_value(collection, value, sort_field, sort_order)
|
|
22
|
+
collection = collection.limit(@cursor.size).order(sprintf("%s %s", sort_field, sort_order.upcase))
|
|
23
|
+
|
|
24
|
+
# If we're fetching previous we reverse the order to make sure we fetch the results adiacents to the previous request,
|
|
25
|
+
# then we reverse results to ensure the order requested
|
|
26
|
+
if direction != "next"
|
|
27
|
+
collection = collection.reverse_order
|
|
28
|
+
collection = collection.reverse
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
collection
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The field to use for pagination.
|
|
35
|
+
#
|
|
36
|
+
# @return [Symbol] The field to use for pagination.
|
|
37
|
+
def pagination_field
|
|
38
|
+
@pagination_field ||= :handle
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Whether to skip pagination. This is used by template generation.
|
|
42
|
+
#
|
|
43
|
+
# @return [Boolean] `true` if pagination must be skipped in template, `false` otherwise.
|
|
44
|
+
def pagination_skip?
|
|
45
|
+
@skip_pagination
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Checks if current collection supports pagination. This is used by template generation.
|
|
49
|
+
#
|
|
50
|
+
# @return [Boolean] `true` if pagination is supported, `false` otherwise.
|
|
51
|
+
def pagination_supported?
|
|
52
|
+
@objects.respond_to?(:first) && @objects.respond_to?(:last)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns the URL a specific page of the current collection.
|
|
56
|
+
#
|
|
57
|
+
# @param page [Symbol] The page to return.
|
|
58
|
+
# @return [String] The URL for a page of the current collection.
|
|
59
|
+
def pagination_url(page = nil)
|
|
60
|
+
exist = @cursor.might_exist?(page, @objects)
|
|
61
|
+
exist ? url_for(request.params.merge(page: @cursor.save(@objects, page, field: pagination_field)).merge(only_path: false)) : nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# :nodoc:
|
|
67
|
+
def apply_value(collection, value, sort_field, sort_order)
|
|
68
|
+
if value
|
|
69
|
+
if cursor.use_offset
|
|
70
|
+
collection = collection.offset(value)
|
|
71
|
+
else
|
|
72
|
+
value = DateTime.parse(value, PaginationCursor::TIMESTAMP_FORMAT) if collection.columns_hash[sort_field.to_s].type == :datetime
|
|
73
|
+
collection = collection.where(sprintf("%s %s ?", sort_field, @cursor.operator(sort_order)), value)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
collection
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the apes gem. Copyright (C) 2016 and above Shogun <shogun@cowtech.it>.
|
|
3
|
+
# Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
module Apes
|
|
7
|
+
module Concerns
|
|
8
|
+
# JSON API request handling module.
|
|
9
|
+
module Request
|
|
10
|
+
# Valid JSON API content type
|
|
11
|
+
CONTENT_TYPE = "application/vnd.api+json".freeze
|
|
12
|
+
|
|
13
|
+
# Sets headers for CORS handling.
|
|
14
|
+
def request_handle_cors
|
|
15
|
+
cors_source = Apes::RuntimeConfiguration.development? ? "http://#{request_source_host}:4200" : Apes::RuntimeConfiguration.cors_source
|
|
16
|
+
|
|
17
|
+
headers["Access-Control-Allow-Origin"] = cors_source
|
|
18
|
+
headers["Access-Control-Allow-Methods"] = "POST, GET, PUT, DELETE, OPTIONS"
|
|
19
|
+
headers["Access-Control-Allow-Headers"] = "Content-Type, X-User-Email, X-User-Token"
|
|
20
|
+
headers["Access-Control-Max-Age"] = 1.year.to_i.to_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Validates a request according to JSON API.
|
|
24
|
+
def request_validate
|
|
25
|
+
content_type = request_valid_content_type
|
|
26
|
+
request.format = :json
|
|
27
|
+
response.content_type = content_type unless Apes::RuntimeConfiguration.development? && params["json"]
|
|
28
|
+
|
|
29
|
+
@cursor = PaginationCursor.new(params, :page)
|
|
30
|
+
|
|
31
|
+
params[:data] ||= HashWithIndifferentAccess.new
|
|
32
|
+
|
|
33
|
+
validate_data(content_type)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the hostname of the client.
|
|
37
|
+
#
|
|
38
|
+
# @return The hostname of the client.
|
|
39
|
+
def request_source_host
|
|
40
|
+
@api_source ||= URI.parse(request.url).host
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns the valid content type for a non GET JSON API request.
|
|
44
|
+
#
|
|
45
|
+
# @return [String] valid content type for a JSON API request.
|
|
46
|
+
def request_valid_content_type
|
|
47
|
+
Apes::Concerns::Request::CONTENT_TYPE
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Extract all attributes from input data making they are all valid and present.
|
|
51
|
+
#
|
|
52
|
+
# @param target [Object] The target model. This is use to obtain validations.
|
|
53
|
+
# @param type_field [Symbol] The attribute which contains input type.
|
|
54
|
+
# @param attributes_field [Symbol] The attribute which contains input attributes.
|
|
55
|
+
# @param relationships_field [Symbol] The attribute which contains relationships specifications.
|
|
56
|
+
# @return [HashWithIndifferentAccess] The attributes to create or update a target model.
|
|
57
|
+
def request_extract_model(target, type_field: :type, attributes_field: :attributes, relationships_field: :relationships)
|
|
58
|
+
data = params[:data]
|
|
59
|
+
|
|
60
|
+
request_validate_model_type(target, data, type_field)
|
|
61
|
+
|
|
62
|
+
data = data[attributes_field]
|
|
63
|
+
fail_request!(:bad_request, "Missing attributes in the \"attributes\" field.") if data.blank?
|
|
64
|
+
|
|
65
|
+
# Extract attributes using strong parameters
|
|
66
|
+
data = unembed_relationships(validate_attributes(data, target), target, relationships_field)
|
|
67
|
+
|
|
68
|
+
# Extract relationships
|
|
69
|
+
data.merge!(validate_relationships(params[:data], target, relationships_field))
|
|
70
|
+
|
|
71
|
+
data
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Converts attributes for a target model in the desired types.
|
|
75
|
+
#
|
|
76
|
+
# @param target [Object] The target model. This is use to obtain types.
|
|
77
|
+
# @param attributes [HashWithIndifferentAccess] The attributes to convert.
|
|
78
|
+
# @return [HashWithIndifferentAccess] The converted attributes.
|
|
79
|
+
def request_cast_attributes(target, attributes)
|
|
80
|
+
types = target.class.column_types
|
|
81
|
+
|
|
82
|
+
attributes.each do |k, v|
|
|
83
|
+
request_cast_attribute(target, attributes, types, k, v)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
attributes
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# :nodoc:
|
|
92
|
+
def validate_data(content_type)
|
|
93
|
+
if request.post? || request.patch?
|
|
94
|
+
raise(Apes::Errors::BadRequestError) unless request.content_type == content_type
|
|
95
|
+
|
|
96
|
+
request_load_data
|
|
97
|
+
raise(Apes::Errors::MissingDataError) unless params[:data].present?
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# :nodoc:
|
|
102
|
+
def request_load_data
|
|
103
|
+
data_source =
|
|
104
|
+
begin
|
|
105
|
+
request.body.read
|
|
106
|
+
rescue
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
return if data_source.blank?
|
|
111
|
+
|
|
112
|
+
data = ActiveSupport::JSON.decode(data_source)
|
|
113
|
+
params[:data] = data.fetch("data", {}).with_indifferent_access
|
|
114
|
+
rescue JSON::ParserError
|
|
115
|
+
raise(Apes::Errors::InvalidDataError)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# :nodoc:
|
|
119
|
+
def request_validate_model_type(target, data, type_field)
|
|
120
|
+
provided_type = data[type_field]
|
|
121
|
+
expected_type = sanitize_model_name(target.class.name)
|
|
122
|
+
|
|
123
|
+
return if sanitize_model_name(data[type_field]) == expected_type
|
|
124
|
+
|
|
125
|
+
fail_request!(
|
|
126
|
+
:bad_request, "#{provided_type.present? ? "Invalid type \"#{provided_type}\"" : "No type"} provided when type \"#{expected_type}\" was expected."
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# :nodoc:
|
|
131
|
+
def request_cast_attribute(target, attributes, types, key, value)
|
|
132
|
+
case types[key].type
|
|
133
|
+
when :boolean then
|
|
134
|
+
Validators::BooleanValidator.parse(value, raise_errors: true)
|
|
135
|
+
attributes[key] = value.to_boolean
|
|
136
|
+
when :datetime
|
|
137
|
+
value = Validators::TimestampValidator.parse(value, raise_errors: true)
|
|
138
|
+
attributes[key] = value
|
|
139
|
+
end
|
|
140
|
+
rescue => e
|
|
141
|
+
target.additional_errors.add(key, e.message)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# :nodoc:
|
|
145
|
+
def sanitize_model_name(name)
|
|
146
|
+
name.ensure_string.underscore.singularize
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# :nodoc:
|
|
150
|
+
def validate_attributes(data, target)
|
|
151
|
+
# Before performing the validation, copy all embedded data to a temporary hash and replace with boolean in order to pass validation
|
|
152
|
+
copied = {}
|
|
153
|
+
|
|
154
|
+
data.each do |k, v|
|
|
155
|
+
if v.is_a?(Hash)
|
|
156
|
+
copied[k] = v
|
|
157
|
+
data[k] = true
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
ActionController::Parameters.new(data).permit(target.class::ATTRIBUTES).merge(copied) # Now return by restoring copied attributes
|
|
162
|
+
rescue ActionController::UnpermittedParameters => e
|
|
163
|
+
e.params.map! { |s| sprintf("attributes.%s", s) }
|
|
164
|
+
raise e
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# :nodoc:
|
|
168
|
+
def unembed_relationships(data, target, field)
|
|
169
|
+
return data unless defined?(target.class::RELATIONSHIPS)
|
|
170
|
+
relationships = target.class::RELATIONSHIPS
|
|
171
|
+
|
|
172
|
+
data.each do |k, v|
|
|
173
|
+
k = k.to_sym
|
|
174
|
+
next unless relationships.include?(k)
|
|
175
|
+
|
|
176
|
+
params[:data][field] ||= {}
|
|
177
|
+
params[:data][field][k] = {data: {type: sanitize_model_name(relationships[k] || k), id: v}}
|
|
178
|
+
data.delete(k)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
data
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# :nodoc:
|
|
185
|
+
def validate_relationships(data, target, field)
|
|
186
|
+
return {} unless defined?(target.class::RELATIONSHIPS)
|
|
187
|
+
relationships = target.class::RELATIONSHIPS
|
|
188
|
+
|
|
189
|
+
allowed = relationships.keys.reduce({}) do |accu, k|
|
|
190
|
+
accu[k] = {data: [:type, :id]}
|
|
191
|
+
accu
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
resolve_references(target, relationships, ActionController::Parameters.new(data[field]).permit(allowed))
|
|
195
|
+
rescue ActionController::UnpermittedParameters => e
|
|
196
|
+
e.params.map! { |s| sprintf("%s.%s", field, s) }
|
|
197
|
+
raise e
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# :nodoc:
|
|
201
|
+
def resolve_references(target, relationships, references)
|
|
202
|
+
references.reduce({}) do |accu, (field, data)|
|
|
203
|
+
begin
|
|
204
|
+
expected, id, sanitized, type = prepare_resolution(data, field, relationships)
|
|
205
|
+
accu[field] = validate_reference(expected, id, sanitized, type)
|
|
206
|
+
rescue => e
|
|
207
|
+
raise e if e.is_a?(Lazier::Exceptions::Debug)
|
|
208
|
+
target.additional_errors.add(field, e.message)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
accu
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# :nodoc:
|
|
216
|
+
def validate_reference(expected, id, sanitized, type)
|
|
217
|
+
raise("Relationship does not contain the \"data.type\" attribute") if type.blank?
|
|
218
|
+
raise("Relationship does not contain the \"data.id\" attribute") if id.blank?
|
|
219
|
+
raise("Invalid relationship type \"#{type}\" provided for when type \"#{expected}\" was expected.") unless sanitized == sanitize_model_name(expected)
|
|
220
|
+
|
|
221
|
+
reference = expected.classify.constantize.find_with_any(id)
|
|
222
|
+
raise("Refers to a non existing \"#{sanitized}\" resource.") unless reference
|
|
223
|
+
reference
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# :nodoc:
|
|
227
|
+
def prepare_resolution(data, field, relationships)
|
|
228
|
+
type = data.dig(:data, :type)
|
|
229
|
+
id = data.dig(:data, :id)
|
|
230
|
+
expected = sanitize_model_name(relationships[field.to_sym] || field.classify)
|
|
231
|
+
sanitized = sanitize_model_name(type)
|
|
232
|
+
|
|
233
|
+
[expected, id, sanitized, type]
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|