apes 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rubocop.yml +82 -0
  4. data/.travis-gemfile +15 -0
  5. data/.travis.yml +15 -0
  6. data/.yardopts +1 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +22 -0
  9. data/README.md +177 -0
  10. data/Rakefile +44 -0
  11. data/apes.gemspec +34 -0
  12. data/doc/Apes.html +130 -0
  13. data/doc/Apes/Concerns.html +127 -0
  14. data/doc/Apes/Concerns/Errors.html +1089 -0
  15. data/doc/Apes/Concerns/Pagination.html +636 -0
  16. data/doc/Apes/Concerns/Request.html +766 -0
  17. data/doc/Apes/Concerns/Response.html +940 -0
  18. data/doc/Apes/Controller.html +1100 -0
  19. data/doc/Apes/Errors.html +125 -0
  20. data/doc/Apes/Errors/AuthenticationError.html +133 -0
  21. data/doc/Apes/Errors/BadRequestError.html +157 -0
  22. data/doc/Apes/Errors/BaseError.html +320 -0
  23. data/doc/Apes/Errors/InvalidDataError.html +157 -0
  24. data/doc/Apes/Errors/MissingDataError.html +157 -0
  25. data/doc/Apes/Model.html +378 -0
  26. data/doc/Apes/PaginationCursor.html +2138 -0
  27. data/doc/Apes/RuntimeConfiguration.html +909 -0
  28. data/doc/Apes/Serializers.html +125 -0
  29. data/doc/Apes/Serializers/JSON.html +389 -0
  30. data/doc/Apes/Serializers/JWT.html +452 -0
  31. data/doc/Apes/Serializers/List.html +347 -0
  32. data/doc/Apes/UrlsParser.html +1432 -0
  33. data/doc/Apes/Validators.html +125 -0
  34. data/doc/Apes/Validators/BaseValidator.html +278 -0
  35. data/doc/Apes/Validators/BooleanValidator.html +494 -0
  36. data/doc/Apes/Validators/EmailValidator.html +350 -0
  37. data/doc/Apes/Validators/PhoneValidator.html +375 -0
  38. data/doc/Apes/Validators/ReferenceValidator.html +372 -0
  39. data/doc/Apes/Validators/TimestampValidator.html +640 -0
  40. data/doc/Apes/Validators/UuidValidator.html +372 -0
  41. data/doc/Apes/Validators/ZipCodeValidator.html +372 -0
  42. data/doc/Apes/Version.html +189 -0
  43. data/doc/ApplicationController.html +547 -0
  44. data/doc/Concerns.html +128 -0
  45. data/doc/Concerns/ErrorHandling.html +826 -0
  46. data/doc/Concerns/PaginationHandling.html +463 -0
  47. data/doc/Concerns/RequestHandling.html +512 -0
  48. data/doc/Concerns/ResponseHandling.html +579 -0
  49. data/doc/Errors.html +126 -0
  50. data/doc/Errors/AuthenticationError.html +123 -0
  51. data/doc/Errors/BadRequestError.html +147 -0
  52. data/doc/Errors/BaseError.html +289 -0
  53. data/doc/Errors/InvalidDataError.html +147 -0
  54. data/doc/Errors/MissingDataError.html +147 -0
  55. data/doc/Model.html +315 -0
  56. data/doc/PaginationCursor.html +764 -0
  57. data/doc/Serializers.html +126 -0
  58. data/doc/Serializers/JSON.html +253 -0
  59. data/doc/Serializers/JWT.html +253 -0
  60. data/doc/Serializers/List.html +245 -0
  61. data/doc/Validators.html +126 -0
  62. data/doc/Validators/BaseValidator.html +209 -0
  63. data/doc/Validators/BooleanValidator.html +391 -0
  64. data/doc/Validators/EmailValidator.html +298 -0
  65. data/doc/Validators/PhoneValidator.html +313 -0
  66. data/doc/Validators/ReferenceValidator.html +284 -0
  67. data/doc/Validators/TimestampValidator.html +476 -0
  68. data/doc/Validators/UuidValidator.html +310 -0
  69. data/doc/Validators/ZipCodeValidator.html +310 -0
  70. data/doc/_index.html +435 -0
  71. data/doc/class_list.html +58 -0
  72. data/doc/css/common.css +1 -0
  73. data/doc/css/full_list.css +57 -0
  74. data/doc/css/style.css +339 -0
  75. data/doc/file.README.html +252 -0
  76. data/doc/file_list.html +60 -0
  77. data/doc/frames.html +26 -0
  78. data/doc/index.html +252 -0
  79. data/doc/js/app.js +219 -0
  80. data/doc/js/full_list.js +181 -0
  81. data/doc/js/jquery.js +4 -0
  82. data/doc/method_list.html +615 -0
  83. data/doc/top-level-namespace.html +112 -0
  84. data/lib/apes.rb +40 -0
  85. data/lib/apes/concerns/errors.rb +111 -0
  86. data/lib/apes/concerns/pagination.rb +81 -0
  87. data/lib/apes/concerns/request.rb +237 -0
  88. data/lib/apes/concerns/response.rb +74 -0
  89. data/lib/apes/controller.rb +77 -0
  90. data/lib/apes/errors.rb +38 -0
  91. data/lib/apes/model.rb +94 -0
  92. data/lib/apes/pagination_cursor.rb +152 -0
  93. data/lib/apes/runtime_configuration.rb +80 -0
  94. data/lib/apes/serializers.rb +88 -0
  95. data/lib/apes/urls_parser.rb +233 -0
  96. data/lib/apes/validators.rb +234 -0
  97. data/lib/apes/version.rb +24 -0
  98. data/spec/apes/concerns/errors_spec.rb +141 -0
  99. data/spec/apes/concerns/pagination_spec.rb +114 -0
  100. data/spec/apes/concerns/request_spec.rb +244 -0
  101. data/spec/apes/concerns/response_spec.rb +79 -0
  102. data/spec/apes/controller_spec.rb +54 -0
  103. data/spec/apes/errors_spec.rb +14 -0
  104. data/spec/apes/models_spec.rb +148 -0
  105. data/spec/apes/pagination_cursor_spec.rb +113 -0
  106. data/spec/apes/runtime_configuration_spec.rb +100 -0
  107. data/spec/apes/serializers_spec.rb +70 -0
  108. data/spec/apes/urls_parser_spec.rb +150 -0
  109. data/spec/apes/validators_spec.rb +237 -0
  110. data/spec/spec_helper.rb +30 -0
  111. data/views/_included.json.jbuilder +9 -0
  112. data/views/_pagination.json.jbuilder +9 -0
  113. data/views/collection.json.jbuilder +4 -0
  114. data/views/errors/400.json.jbuilder +9 -0
  115. data/views/errors/403.json.jbuilder +7 -0
  116. data/views/errors/404.json.jbuilder +6 -0
  117. data/views/errors/422.json.jbuilder +19 -0
  118. data/views/errors/500.json.jbuilder +12 -0
  119. data/views/errors/501.json.jbuilder +7 -0
  120. data/views/layouts/general.json.jbuilder +36 -0
  121. data/views/object.json.jbuilder +4 -0
  122. 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
+ &mdash; 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> &raquo;
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>
@@ -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