apicasso 0.4.5 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
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