apicasso 0.4.5 → 0.4.6

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 (79) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +226 -223
  3. data/Rakefile +23 -23
  4. data/app/controllers/apicasso/apidocs_controller.rb +309 -299
  5. data/app/controllers/apicasso/application_controller.rb +170 -147
  6. data/app/controllers/apicasso/crud_controller.rb +246 -245
  7. data/app/controllers/concerns/orderable.rb +45 -45
  8. data/app/models/apicasso/ability.rb +38 -38
  9. data/app/models/apicasso/application_record.rb +6 -6
  10. data/app/models/apicasso/key.rb +25 -25
  11. data/app/models/apicasso/request.rb +8 -8
  12. data/config/routes.rb +14 -14
  13. data/lib/apicasso/active_record_extension.rb +0 -44
  14. data/lib/apicasso/engine.rb +13 -13
  15. data/lib/apicasso/version.rb +3 -3
  16. data/lib/apicasso.rb +15 -13
  17. data/lib/generators/apicasso/install/install_generator.rb +25 -25
  18. data/lib/generators/apicasso/install/templates/create_apicasso_tables.rb +20 -20
  19. data/spec/dummy/Gemfile +56 -0
  20. data/spec/dummy/Gemfile.lock +237 -0
  21. data/spec/dummy/app/controllers/application_controller.rb +1 -1
  22. data/spec/dummy/app/models/used_model.rb +42 -0
  23. data/spec/dummy/app/serializers/used_model_serializer.rb +3 -0
  24. data/spec/dummy/bin/rails +5 -0
  25. data/spec/dummy/bin/rake +5 -0
  26. data/spec/dummy/bin/setup +0 -3
  27. data/spec/dummy/bin/spring +17 -0
  28. data/spec/dummy/bin/update +0 -3
  29. data/spec/dummy/config/application.rb +14 -10
  30. data/spec/dummy/config/cable.yml +1 -1
  31. data/spec/dummy/config/credentials.yml.enc +1 -0
  32. data/spec/dummy/config/database.yml +5 -14
  33. data/spec/dummy/config/environments/development.rb +6 -10
  34. data/spec/dummy/config/environments/production.rb +1 -10
  35. data/spec/dummy/config/initializers/cors.rb +16 -0
  36. data/spec/dummy/config/locales/en.yml +7 -32
  37. data/spec/dummy/config/routes.rb +1 -1
  38. data/{db/migrate/20180826141433_create_apicasso_tables.rb → spec/dummy/db/migrate/20180918134607_create_apicasso_tables.rb} +1 -0
  39. data/spec/dummy/db/migrate/20180918141254_create_used_models.rb +44 -0
  40. data/spec/dummy/db/migrate/20180919130152_create_active_storage_tables.active_storage.rb +26 -0
  41. data/spec/dummy/db/migrate/20180920133933_change_used_model_to_validates.rb +7 -0
  42. data/spec/dummy/db/schema.rb +98 -0
  43. data/spec/dummy/db/seeds.rb +56 -0
  44. data/spec/factories/used_model.rb +28 -0
  45. data/spec/models/used_model_spec.rb +35 -0
  46. data/spec/rails_helper.rb +66 -0
  47. data/spec/requests/requests_spec.rb +227 -0
  48. data/spec/spec_helper.rb +7 -9
  49. data/spec/support/factory_bot.rb +3 -0
  50. metadata +83 -64
  51. data/spec/controllers/apicasso/aplication_controller_spec.rb +0 -18
  52. data/spec/controllers/apicasso/crud_controller_spec.rb +0 -107
  53. data/spec/dummy/app/assets/config/manifest.js +0 -3
  54. data/spec/dummy/app/assets/javascripts/application.js +0 -15
  55. data/spec/dummy/app/assets/javascripts/cable.js +0 -13
  56. data/spec/dummy/app/assets/stylesheets/application.css +0 -15
  57. data/spec/dummy/app/channels/application_cable/channel.rb +0 -4
  58. data/spec/dummy/app/channels/application_cable/connection.rb +0 -4
  59. data/spec/dummy/app/helpers/application_helper.rb +0 -2
  60. data/spec/dummy/app/jobs/application_job.rb +0 -2
  61. data/spec/dummy/app/mailers/application_mailer.rb +0 -4
  62. data/spec/dummy/app/views/layouts/application.html.erb +0 -15
  63. data/spec/dummy/app/views/layouts/mailer.html.erb +0 -13
  64. data/spec/dummy/app/views/layouts/mailer.text.erb +0 -1
  65. data/spec/dummy/bin/yarn +0 -11
  66. data/spec/dummy/config/initializers/assets.rb +0 -14
  67. data/spec/dummy/config/initializers/content_security_policy.rb +0 -25
  68. data/spec/dummy/config/initializers/cookies_serializer.rb +0 -5
  69. data/spec/dummy/log/development.log +0 -0
  70. data/spec/dummy/public/404.html +0 -67
  71. data/spec/dummy/public/422.html +0 -67
  72. data/spec/dummy/public/500.html +0 -66
  73. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  74. data/spec/dummy/public/apple-touch-icon.png +0 -0
  75. data/spec/dummy/public/favicon.ico +0 -0
  76. data/spec/factories/apicasso_key.rb +0 -9
  77. data/spec/factories/object.rb +0 -5
  78. data/spec/models/apicasso/key.rb +0 -5
  79. data/spec/routing/appointments_routing_spec.rb +0 -38
@@ -1,147 +1,170 @@
1
- # frozen_string_literal: true
2
-
3
- module Apicasso
4
- # Controller to extract common API features, such as authentication and
5
- # authorization. Used to be inherited by non-CRUD controllers when your
6
- # application needs to create custom actions.
7
- class ApplicationController < ActionController::API
8
- include ActionController::HttpAuthentication::Token::ControllerMethods
9
- prepend_before_action :restrict_access, unless: -> { preflight? }
10
- before_action :set_access_control_headers
11
- after_action :register_api_request
12
-
13
- # Sets the authorization scope for the current API key, it's a getter
14
- # to make scoping easier
15
- def current_ability
16
- @current_ability ||= Apicasso::Ability.new(@api_key)
17
- end
18
-
19
- private
20
-
21
- # Identifies API key used in the request, avoiding unauthenticated access.
22
- # Responds with status 401 when token is not present or not found.
23
- # Access restriction happens on the `Authentication` HTTP header.
24
- # Example:
25
- # curl -X GET http://example.com/objects -H 'authorization: Token token=f1e048a0b0ef4071a9a64ceecd48c64b'
26
- def restrict_access
27
- authenticate_or_request_with_http_token do |token, _options|
28
- @api_key = Apicasso::Key.find_by(token: token)
29
- end
30
- end
31
-
32
- # Creates a request object in databse, registering the API key and
33
- # a hash of the request and the response. It's an auditing proccess,
34
- # all relevant information about the requests and it's reponses get
35
- # recorded within the `Apicasso::Request`. This method assumes that
36
- # your project is using some kind of ActiveRecord extension with a
37
- # `.delay` method, which when not present makes your API very slow.
38
- def register_api_request
39
- Apicasso::Request.delay.create(api_key_id: @api_key.try(:id),
40
- object: { request: request_metadata,
41
- response: response_metadata })
42
- rescue NoMethodError
43
- Apicasso::Request.create(api_key_id: @api_key.try(:id),
44
- object: { request: request_metadata,
45
- response: response_metadata })
46
- end
47
-
48
- # Information that gets inserted on `register_api_request` as auditing data
49
- # about the request. Returns a Hash with UUID, URL, HTTP Headers and IP
50
- def request_metadata
51
- {
52
- uuid: request.uuid,
53
- url: request.original_url,
54
- headers: request.env.select { |key, _v| key =~ /^HTTP_/ },
55
- ip: request.remote_ip
56
- }
57
- end
58
-
59
- # Information that gets inserted on `register_api_request` as auditing data
60
- # about the response sent back to the client. Returns HTTP Status and request body
61
- def response_metadata
62
- {
63
- status: response.status,
64
- body: (response.body.present? ? JSON.parse(response.body) : '')
65
- }
66
- end
67
-
68
- # Used to avoid errors parsing the search query, which can be passed as
69
- # a JSON or as a key-value param. JSON is preferred because it generates
70
- # shorter URLs on GET parameters.
71
- def parsed_query
72
- JSON.parse(params[:q])
73
- rescue JSON::ParserError, TypeError
74
- params[:q]
75
- end
76
-
77
- # Used to avoid errors in included associations parsing and to enable a
78
- # insertion point for a change on splitting method.
79
- def parsed_include
80
- params[:include].split(',')
81
- rescue NoMethodError
82
- []
83
- end
84
-
85
- # Used to avoid errors in fieldset selection parsing and to enable a
86
- # insertion point for a change on splitting method.
87
- def parsed_select
88
- params[:select].split(',')
89
- rescue NoMethodError
90
- []
91
- end
92
-
93
- # Receives a `.paginate`d collection and returns the pagination
94
- # metadata to be merged into response
95
- def pagination_metadata_for(records)
96
- { total: records.total_entries,
97
- total_pages: records.total_pages,
98
- last_page: records.next_page.blank?,
99
- previous_page: previous_link_for(records),
100
- next_page: next_link_for(records),
101
- out_of_bounds: records.out_of_bounds?,
102
- offset: records.offset }
103
- end
104
-
105
- # Generates a contextualized URL of the next page for the request
106
- def next_link_for(records)
107
- uri = URI.parse(request.original_url)
108
- query = Rack::Utils.parse_query(uri.query)
109
- query['page'] = records.next_page
110
- uri.query = Rack::Utils.build_query(query)
111
- uri.to_s
112
- end
113
-
114
- # Generates a contextualized URL of the previous page for the request
115
- def previous_link_for(records)
116
- uri = URI.parse(request.original_url)
117
- query = Rack::Utils.parse_query(uri.query)
118
- query['page'] = records.previous_page
119
- uri.query = Rack::Utils.build_query(query)
120
- uri.to_s
121
- end
122
-
123
- # Receives a `:action, :resource, :object` hash to validate authorization
124
- # Example:
125
- # > authorize_for action: :read, resource: :object_class, object: :object
126
- def authorize_for(opts = {})
127
- authorize! opts[:action], opts[:resource] if opts[:resource].present?
128
- authorize! opts[:action], opts[:object] if opts[:object].present?
129
- end
130
-
131
- # @TODO
132
- # Remove this in favor of a more controllable aproach of CORS
133
- def set_access_control_headers
134
- response.headers['Access-Control-Allow-Origin'] = request.protocol + request.host
135
- response.headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
136
- response.headers['Access-Control-Allow-Credentials'] = 'true'
137
- response.headers['Access-Control-Allow-Headers'] = 'Origin, Content-Type, Accept, Authorization, Token, Auth-Token, Email, X-User-Token, X-User-Email'
138
- response.headers['Access-Control-Max-Age'] = '1728000'
139
- end
140
-
141
- # Checks if current request is a CORS preflight check
142
- def preflight?
143
- request.request_method == 'OPTIONS' &&
144
- !request.headers['Authorization'].present?
145
- end
146
- end
147
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Apicasso
4
+ # Controller to extract common API features, such as authentication and
5
+ # authorization. Used to be inherited by non-CRUD controllers when your
6
+ # application needs to create custom actions.
7
+ class ApplicationController < ActionController::API
8
+ include ActionController::HttpAuthentication::Token::ControllerMethods
9
+ prepend_before_action :restrict_access, unless: -> { preflight? }
10
+ before_action :set_access_control_headers
11
+ after_action :register_api_request
12
+
13
+ # Sets the authorization scope for the current API key, it's a getter
14
+ # to make scoping easier
15
+ def current_ability
16
+ @current_ability ||= Apicasso::Ability.new(@api_key)
17
+ end
18
+
19
+ private
20
+
21
+ # Identifies API key used in the request, avoiding unauthenticated access.
22
+ # Responds with status 401 when token is not present or not found.
23
+ # Access restriction happens on the `Authentication` HTTP header.
24
+ # Example:
25
+ # curl -X GET http://example.com/objects -H 'authorization: Token token=f1e048a0b0ef4071a9a64ceecd48c64b'
26
+ def restrict_access
27
+ authenticate_or_request_with_http_token do |token, _options|
28
+ @api_key = Apicasso::Key.find_by(token: token)
29
+ end
30
+ end
31
+
32
+ # Creates a request object in databse, registering the API key and
33
+ # a hash of the request and the response. It's an auditing proccess,
34
+ # all relevant information about the requests and it's reponses get
35
+ # recorded within the `Apicasso::Request`. This method assumes that
36
+ # your project is using some kind of ActiveRecord extension with a
37
+ # `.delay` method, which when not present makes your API very slow.
38
+ def register_api_request
39
+ Apicasso::Request.delay.create(api_key_id: @api_key.try(:id),
40
+ object: { request: request_metadata,
41
+ response: response_metadata })
42
+ rescue NoMethodError
43
+ Apicasso::Request.create(api_key_id: @api_key.try(:id),
44
+ object: { request: request_metadata,
45
+ response: response_metadata })
46
+ end
47
+
48
+ # Information that gets inserted on `register_api_request` as auditing data
49
+ # about the request. Returns a Hash with UUID, URL, HTTP Headers and IP
50
+ def request_metadata
51
+ {
52
+ uuid: request.uuid,
53
+ url: request.original_url,
54
+ headers: request.env.select { |key, _v| key =~ /^HTTP_/ },
55
+ ip: request.remote_ip
56
+ }
57
+ end
58
+
59
+ # Information that gets inserted on `register_api_request` as auditing data
60
+ # about the response sent back to the client. Returns HTTP Status and request body
61
+ def response_metadata
62
+ {
63
+ status: response.status,
64
+ body: (response.body.present? ? JSON.parse(response.body) : '')
65
+ }
66
+ end
67
+
68
+ # A method to extract all assosciations available
69
+ def associations_array
70
+ resource.reflect_on_all_associations.map { |association| association.name.to_s }
71
+ end
72
+
73
+ # Used to avoid errors parsing the search query, which can be passed as
74
+ # a JSON or as a key-value param. JSON is preferred because it generates
75
+ # shorter URLs on GET parameters.
76
+ def parsed_query
77
+ JSON.parse(params[:q])
78
+ rescue JSON::ParserError, TypeError
79
+ params[:q]
80
+ end
81
+
82
+ # Used to avoid errors in included associations parsing and to enable a
83
+ # insertion point for a change on splitting method.
84
+ def parsed_associations
85
+ params[:include].split(',').map do |param|
86
+ if @object.respond_to?(param)
87
+ param if associations_array.include?(param)
88
+ end
89
+ end.compact
90
+ rescue NoMethodError
91
+ []
92
+ end
93
+
94
+ # Used to avoid errors in included associations parsing and to enable a
95
+ # insertion point for a change on splitting method.
96
+ def parsed_methods
97
+ params[:include].split(',').map do |param|
98
+ if @object.respond_to?(param)
99
+ param unless associations_array.include?(param)
100
+ end
101
+ end.compact
102
+ rescue NoMethodError
103
+ []
104
+ end
105
+
106
+ # Used to avoid errors in fieldset selection parsing and to enable a
107
+ # insertion point for a change on splitting method.
108
+ def parsed_select
109
+ params[:select].split(',').map do |field|
110
+ field if @records.column_names.include?(field)
111
+ end
112
+ rescue NoMethodError
113
+ []
114
+ end
115
+
116
+ # Receives a `.paginate`d collection and returns the pagination
117
+ # metadata to be merged into response
118
+ def pagination_metadata_for(records)
119
+ { total: records.total_entries,
120
+ total_pages: records.total_pages,
121
+ last_page: records.next_page.blank?,
122
+ previous_page: previous_link_for(records),
123
+ next_page: next_link_for(records),
124
+ out_of_bounds: records.out_of_bounds?,
125
+ offset: records.offset }
126
+ end
127
+
128
+ # Generates a contextualized URL of the next page for the request
129
+ def next_link_for(records)
130
+ uri = URI.parse(request.original_url)
131
+ query = Rack::Utils.parse_query(uri.query)
132
+ query['page'] = records.next_page
133
+ uri.query = Rack::Utils.build_query(query)
134
+ uri.to_s
135
+ end
136
+
137
+ # Generates a contextualized URL of the previous page for the request
138
+ def previous_link_for(records)
139
+ uri = URI.parse(request.original_url)
140
+ query = Rack::Utils.parse_query(uri.query)
141
+ query['page'] = records.previous_page
142
+ uri.query = Rack::Utils.build_query(query)
143
+ uri.to_s
144
+ end
145
+
146
+ # Receives a `:action, :resource, :object` hash to validate authorization
147
+ # Example:
148
+ # > authorize_for action: :read, resource: :object_class, object: :object
149
+ def authorize_for(opts = {})
150
+ authorize! opts[:action], opts[:resource] if opts[:resource].present?
151
+ authorize! opts[:action], opts[:object] if opts[:object].present?
152
+ end
153
+
154
+ # @TODO
155
+ # Remove this in favor of a more controllable aproach of CORS
156
+ def set_access_control_headers
157
+ response.headers['Access-Control-Allow-Origin'] = request.headers["Origin"]
158
+ response.headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
159
+ response.headers['Access-Control-Allow-Credentials'] = 'true'
160
+ response.headers['Access-Control-Allow-Headers'] = 'Origin, Content-Type, Accept, Authorization, Token, Auth-Token, Email, X-User-Token, X-User-Email'
161
+ response.headers['Access-Control-Max-Age'] = '1728000'
162
+ end
163
+
164
+ # Checks if current request is a CORS preflight check
165
+ def preflight?
166
+ request.request_method == 'OPTIONS' &&
167
+ !request.headers['Authorization'].present?
168
+ end
169
+ end
170
+ end