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.
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