cathode 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +346 -125
  3. data/Rakefile +1 -0
  4. data/app/controllers/cathode/base_controller.rb +28 -45
  5. data/app/models/cathode/token.rb +21 -0
  6. data/config/routes.rb +16 -0
  7. data/db/migrate/20140425164100_create_cathode_tokens.rb +11 -0
  8. data/lib/cathode.rb +0 -13
  9. data/lib/cathode/_version.rb +2 -1
  10. data/lib/cathode/action.rb +197 -39
  11. data/lib/cathode/action_dsl.rb +60 -0
  12. data/lib/cathode/base.rb +81 -12
  13. data/lib/cathode/create_request.rb +21 -0
  14. data/lib/cathode/custom_request.rb +5 -0
  15. data/lib/cathode/debug.rb +25 -0
  16. data/lib/cathode/destroy_request.rb +13 -0
  17. data/lib/cathode/engine.rb +9 -0
  18. data/lib/cathode/exceptions.rb +20 -0
  19. data/lib/cathode/index_request.rb +40 -0
  20. data/lib/cathode/object_collection.rb +49 -0
  21. data/lib/cathode/query.rb +24 -0
  22. data/lib/cathode/railtie.rb +21 -0
  23. data/lib/cathode/request.rb +139 -7
  24. data/lib/cathode/resource.rb +50 -19
  25. data/lib/cathode/resource_dsl.rb +46 -0
  26. data/lib/cathode/show_request.rb +13 -0
  27. data/lib/cathode/update_request.rb +26 -0
  28. data/lib/cathode/version.rb +112 -23
  29. data/lib/tasks/cathode_tasks.rake +5 -4
  30. data/spec/dummy/app/api/api.rb +0 -0
  31. data/spec/dummy/app/models/payment.rb +3 -0
  32. data/spec/dummy/app/models/product.rb +1 -0
  33. data/spec/dummy/app/models/sale.rb +5 -0
  34. data/spec/dummy/app/models/salesperson.rb +3 -0
  35. data/spec/dummy/db/development.sqlite3 +0 -0
  36. data/spec/dummy/db/migrate/20140409183635_create_sales.rb +11 -0
  37. data/spec/dummy/db/migrate/20140423172419_create_salespeople.rb +11 -0
  38. data/spec/dummy/db/migrate/20140424181343_create_payments.rb +10 -0
  39. data/spec/dummy/db/schema.rb +31 -1
  40. data/spec/dummy/db/test.sqlite3 +0 -0
  41. data/spec/dummy/log/development.log +1167 -0
  42. data/spec/dummy/log/test.log +180602 -0
  43. data/spec/dummy/spec/factories/payments.rb +8 -0
  44. data/spec/dummy/spec/factories/products.rb +1 -1
  45. data/spec/dummy/spec/factories/sales.rb +9 -0
  46. data/spec/dummy/spec/factories/salespeople.rb +7 -0
  47. data/spec/dummy/spec/requests/requests_spec.rb +434 -0
  48. data/spec/lib/cathode/action_spec.rb +136 -0
  49. data/spec/lib/cathode/base_spec.rb +34 -0
  50. data/spec/lib/cathode/create_request_spec.rb +40 -0
  51. data/spec/lib/cathode/custom_request_spec.rb +31 -0
  52. data/spec/lib/cathode/debug_spec.rb +25 -0
  53. data/spec/lib/cathode/destroy_request_spec.rb +28 -0
  54. data/spec/lib/cathode/index_request_spec.rb +62 -0
  55. data/spec/lib/cathode/object_collection_spec.rb +66 -0
  56. data/spec/lib/cathode/query_spec.rb +28 -0
  57. data/spec/lib/cathode/request_spec.rb +58 -0
  58. data/spec/lib/cathode/resource_spec.rb +482 -0
  59. data/spec/lib/cathode/show_request_spec.rb +23 -0
  60. data/spec/lib/cathode/update_request_spec.rb +41 -0
  61. data/spec/lib/cathode/version_spec.rb +416 -0
  62. data/spec/models/cathode/token_spec.rb +62 -0
  63. data/spec/spec_helper.rb +8 -2
  64. data/spec/support/factories/payments.rb +3 -0
  65. data/spec/support/factories/sale.rb +3 -0
  66. data/spec/support/factories/salespeople.rb +3 -0
  67. data/spec/support/factories/token.rb +3 -0
  68. data/spec/support/helpers.rb +13 -2
  69. metadata +192 -47
  70. data/app/helpers/cathode/application_helper.rb +0 -4
  71. data/spec/dummy/app/api/dummy_api.rb +0 -4
  72. data/spec/integration/api_spec.rb +0 -88
  73. data/spec/lib/action_spec.rb +0 -140
  74. data/spec/lib/base_spec.rb +0 -28
  75. data/spec/lib/request_spec.rb +0 -5
  76. data/spec/lib/resources_spec.rb +0 -78
  77. data/spec/lib/versioning_spec.rb +0 -104
data/Rakefile CHANGED
@@ -16,6 +16,7 @@ end
16
16
 
17
17
  APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
18
  load 'rails/tasks/engine.rake'
19
+ load 'lib/tasks/cathode_tasks.rake'
19
20
 
20
21
  Bundler::GemHelper.install_tasks
21
22
 
@@ -1,47 +1,30 @@
1
- class Cathode::BaseController < ActionController::Base
2
- before_action :process_access_filter
3
-
4
- def index
5
- render json: resources.load
6
- end
7
-
8
- def create
9
- render json: model.create(resource_params)
10
- end
11
-
12
- def destroy
13
- resource.destroy
14
- head :ok
15
- end
16
-
17
- def show
18
- make_request(request)
19
- end
20
-
21
- private
22
-
23
- def make_request(http_request)
24
- request = Cathode::Request.new(http_request, params)
25
- render json: request.body, status: request.status
26
- end
27
-
28
- def resources
29
- model.all
30
- end
31
-
32
- def resource
33
- model.find params[:id]
34
- end
35
-
36
- def resource_params
37
- params[controller_name.singularize]
38
- end
39
-
40
- def model
41
- controller_name.classify.constantize
42
- end
43
-
44
- def process_access_filter
45
-
1
+ module Cathode
2
+ # Defines a basic controller for all Cathode controllers to inherit from.
3
+ # Intercepts all Rails requests and sends them off to {Request} with the
4
+ # context to to be processed.
5
+ class BaseController < ActionController::Base
6
+ %w(index show create update destroy custom).each do |method|
7
+ define_method method do
8
+ make_request
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def make_request
15
+ if Cathode::Base.tokens_required
16
+ authenticate_or_request_with_http_token do |token|
17
+ Token.find_by token: token
18
+ end
19
+ end
20
+
21
+ request = Cathode::Request.create self
22
+
23
+ render json: request._body, status: request._status unless performed?
24
+ end
25
+
26
+ def resource_params
27
+ params[controller_name.singularize]
28
+ end
46
29
  end
47
30
  end
@@ -0,0 +1,21 @@
1
+ module Cathode
2
+ # Defines a token model to hold the API tokens.
3
+ class Token < ActiveRecord::Base
4
+ after_initialize :generate_token
5
+
6
+ validates :token, uniqueness: true
7
+
8
+ # Expires the token by deactivating it and updating its `expired_at` field.
9
+ # @return [Token] self
10
+ def expire
11
+ update active: false, expired_at: Time.now
12
+ self
13
+ end
14
+
15
+ private
16
+
17
+ def generate_token
18
+ self.token = SecureRandom.hex
19
+ end
20
+ end
21
+ end
@@ -1,2 +1,18 @@
1
+ def attach_resources(resources)
2
+ resources.each do |resource|
3
+ method = resource.singular ? :resource : :resources
4
+ send method, resource.name, controller: resource.controller_prefix.underscore, only: resource.default_actions.map(&:name) do
5
+ resource.custom_actions.each do |action|
6
+ match action.name => action.name, action: 'custom', via: action.http_method
7
+ end
8
+ attach_resources(resource._resources)
9
+ end
10
+ end
11
+ end
12
+
1
13
  Cathode::Engine.routes.draw do
14
+ Cathode::Base.versions.each do |version|
15
+ attach_resources(version._resources)
16
+ end
17
+ match '*path' => 'base#custom', via: [:get, :post, :put, :delete]
2
18
  end
@@ -0,0 +1,11 @@
1
+ class CreateCathodeTokens < ActiveRecord::Migration
2
+ def change
3
+ create_table :cathode_tokens do |t|
4
+ t.boolean :active, default: true
5
+ t.datetime :expired_at
6
+ t.string :token
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -1,14 +1 @@
1
- require 'cathode/engine'
2
1
  require 'cathode/base'
3
- require 'cathode/exceptions'
4
-
5
- module Cathode
6
- class Engine < ::Rails::Engine
7
- config.generators do |g|
8
- g.test_framework :rspec, :fixture => false
9
- g.fixture_replacement :factory_girl, :dir => 'spec/factories'
10
- g.assets false
11
- g.helper false
12
- end
13
- end
14
- end
@@ -1,3 +1,4 @@
1
1
  module Cathode
2
- VERSION = "0.0.1"
2
+ # The current version of Cathode.
3
+ VERSION = '0.1.0'
3
4
  end
@@ -1,40 +1,78 @@
1
1
  module Cathode
2
+ # An `Action` can be added to a {Resource} or a {Version} and contains the
3
+ # default behavior of that action (if it is a default action), or the override
4
+ # behavior if it is a custom action or an overridden default action.
2
5
  class Action
6
+ include ActionDsl
7
+
8
+ attr_accessor :strong_params
3
9
  attr_reader :action_access_filter,
10
+ :action_block,
11
+ :http_method,
4
12
  :name,
13
+ :override_block,
5
14
  :resource
6
15
 
16
+ delegate :parent, to: :resource
7
17
 
8
- def self.create(action, resource, &block)
9
- klass = case action
10
- when :index
11
- IndexAction
12
- when :show
13
- ShowAction
14
- when :create
15
- CreateAction
16
- when :update
17
- UpdateAction
18
- when :destroy
19
- DestroyAction
20
- end
21
- klass.new(action, resource, &block)
18
+ class << self
19
+ # Creates an action by initializing the appropriate subclass
20
+ # @param action [Symbol] The action's name
21
+ # @param resource [Resource] The resource the action belongs to
22
+ # @param params [Hash] An optional params hash
23
+ # @param block The action's properties, defined with the {ActionDsl}
24
+ # @return [IndexAction, ShowAction, CreateAction, UpdateAction,
25
+ # DestroyAction, CustomAction] The subclassed action
26
+ def create(action, resource, params = nil, &block)
27
+ klass = case action
28
+ when :index
29
+ IndexAction
30
+ when :show
31
+ ShowAction
32
+ when :create
33
+ CreateAction
34
+ when :update
35
+ UpdateAction
36
+ when :destroy
37
+ DestroyAction
38
+ else
39
+ CustomAction
40
+ end
41
+ klass.new(action, resource, params, &block)
42
+ end
22
43
  end
23
44
 
24
- def initialize(action, resource, &block)
45
+ # Initializes the action
46
+ # @param action [Symbol] The action's name
47
+ # @param resource [Resource] The resource the action belongs to
48
+ # @param params [Hash] An optional params hash
49
+ # @param block The action's properties, defined with the {ActionDsl}
50
+ def initialize(action, resource, params = {}, &block)
25
51
  @name, @resource = action, resource
52
+ @allowed_subactions = []
26
53
 
27
- self.instance_eval &block if block_given?
28
- end
29
-
30
- def perform(params)
31
- if action_access_filter && !action_access_filter.call
32
- return { status: :unauthorized }
54
+ if block_given?
55
+ if params[:override]
56
+ override &block
57
+ else
58
+ if [:index, :show, :create, :update, :destroy].include? action
59
+ instance_eval &block
60
+ else
61
+ @action_block = block
62
+ end
63
+ end
33
64
  end
34
65
 
35
- body = perform_action params
66
+ @http_method = params[:method] if params.present?
36
67
 
37
- return { body: body, status: :ok }
68
+ after_initialize if respond_to? :after_initialize
69
+ end
70
+
71
+ # Whether a subaction is permitted
72
+ # @param subaction [Symbol] The subaction's name
73
+ # @return [Boolean]
74
+ def allowed?(subaction)
75
+ @allowed_subactions.include? subaction
38
76
  end
39
77
 
40
78
  private
@@ -46,37 +84,157 @@ module Cathode
46
84
  def access_filter(&filter)
47
85
  @action_access_filter = filter
48
86
  end
87
+
88
+ def allows(*subactions)
89
+ @allowed_subactions = subactions
90
+ end
91
+
92
+ def replace(&block)
93
+ @action_block = block
94
+ end
95
+
96
+ def override(&block)
97
+ @override_block = block
98
+ end
99
+
100
+ def overridden?
101
+ override_block.present?
102
+ end
103
+
104
+ def after_resource_initialized
105
+ self
106
+ end
107
+
108
+ # Requires that the action has an attributes block defined or one is defined
109
+ # at the resource level
110
+ module RequiresStrongParams
111
+ # Raises an error if there is no attributes block defined
112
+ def after_resource_initialized
113
+ if strong_params.nil?
114
+ if resource.strong_params.present?
115
+ @strong_params = resource.strong_params
116
+ else
117
+ fail UnknownAttributesError, "An attributes block was not specified for `#{name}' action on resource `#{resource.name}'"
118
+ end
119
+ end
120
+
121
+ super
122
+ end
123
+ end
124
+
125
+ # Raises an error if the action was defined on a resource whose parent
126
+ # doesn't have an association to the resource
127
+ module RequiresAssociation
128
+ # Defines the possible associations as `:resources` (`has_many`) or
129
+ # `:resource` (`has_one`)
130
+ def association_keys
131
+ [
132
+ resource.name.to_s.singularize.to_sym,
133
+ resource.name.to_s.pluralize.to_sym
134
+ ]
135
+ end
136
+
137
+ # Determines whether an expected association is present.
138
+ def after_resource_initialized
139
+ if parent.present? && !overridden?
140
+ reflections = parent.model.reflections
141
+
142
+ if association_keys.map { |key| reflections.include?(key) }.none?
143
+ raise MissingAssociationError, error_message
144
+ end
145
+ end
146
+
147
+ super
148
+ end
149
+ end
150
+
151
+ # Raises an error if the action was defined on a resource whose parent
152
+ # doesn't have a `has_one` association to the resource
153
+ module RequiresHasOneAssociation
154
+ include RequiresAssociation
155
+
156
+ # Defines the possible associations as `:resource` (`has_one`)
157
+ def association_keys
158
+ [resource.name.to_s.singularize.to_sym]
159
+ end
160
+
161
+ # Provides an error message to display if the association is missing.
162
+ def error_message
163
+ "Can't use default :#{name} action on `#{parent.name}' without a has_one `#{resource.name.to_s.singularize}' association"
164
+ end
165
+ end
166
+
167
+ # Raises an error if the action was defined on a resource whose parent
168
+ # doesn't have a `has_many` association to the resource
169
+ module RequiresHasManyAssociation
170
+ include RequiresAssociation
171
+
172
+ # Defines the possible associations as `:resources` (`has_many`).
173
+ def association_keys
174
+ [resource.name.to_s.pluralize.to_sym]
175
+ end
176
+
177
+ # Provides an error message to display if the association is missing.
178
+ def error_message
179
+ "Can't use default :#{name} action on `#{parent.name}' without a has_many or has_and_belongs_to_many `#{resource.name.to_s.singularize}' association"
180
+ end
181
+ end
182
+
183
+ # Requires the resource to define custom action behavior if it is a singular
184
+ # resource.
185
+ module RequiresCustomActionForSingular
186
+ # Raises an error if the resource is singular and attempting to use the
187
+ # default action without a parent resource
188
+ def after_resource_initialized
189
+ if resource.singular && !overridden? && resource.parent.nil?
190
+ raise Cathode::ActionBehaviorMissingError,
191
+ "Can't use default :#{name} action on singular resource `#{resource.name}'"
192
+ end
193
+
194
+ super
195
+ end
196
+ end
49
197
  end
50
198
 
199
+ # Provides additional behavior for index actions.
51
200
  class IndexAction < Action
52
- def perform_action(params)
53
- model.all
54
- end
201
+ include RequiresCustomActionForSingular
202
+ include RequiresHasManyAssociation
55
203
  end
56
204
 
205
+ # Provides additional behavior for show actions.
57
206
  class ShowAction < Action
58
- def perform_action(params)
59
- model.find params[:id]
60
- end
207
+ include RequiresCustomActionForSingular
208
+ include RequiresHasOneAssociation
61
209
  end
62
210
 
211
+ # Provides additional behavior for create actions.
63
212
  class CreateAction < Action
64
- def perform_action(params)
65
- model.create params
66
- end
213
+ include RequiresCustomActionForSingular
214
+ include RequiresStrongParams
215
+ include RequiresAssociation
67
216
  end
68
217
 
218
+ # Provides additional behavior for update actions.
69
219
  class UpdateAction < Action
70
- def perform_action(params)
71
- record = model.find(params[:id])
72
- record.update params
73
- record.reload
74
- end
220
+ include RequiresCustomActionForSingular
221
+ include RequiresStrongParams
222
+ include RequiresHasOneAssociation
75
223
  end
76
224
 
225
+ # Provides additional behavior for destroy actions.
77
226
  class DestroyAction < Action
78
- def perform_action(params)
79
- model.find(params[:id]).destroy
227
+ include RequiresCustomActionForSingular
228
+ include RequiresHasOneAssociation
229
+ end
230
+
231
+ # Provides additional behavior for non-default actions.
232
+ class CustomAction < Action
233
+ # Raises an error if the action was defined without an HTTP method
234
+ def after_initialize
235
+ if http_method.nil?
236
+ raise RequestMethodMissingError, "You must specify an HTTP method (get, put, post, delete) for action `#{@name}'"
237
+ end
80
238
  end
81
239
  end
82
240
  end
@@ -0,0 +1,60 @@
1
+ module Cathode
2
+ # Holds the domain-specific language (DSL) for describing actions.
3
+ module ActionDsl
4
+ # Lists the actions that are default (i.e., `index`, `show`, `create`,
5
+ # `update`, and `destroy`)
6
+ # @return [Array] The default actions
7
+ def default_actions
8
+ actions.select { |action| DEFAULT_ACTIONS.include? action.name }
9
+ end
10
+
11
+ # Lists the actions that are not default
12
+ # @return [Array] The custom actions
13
+ def custom_actions
14
+ actions - default_actions
15
+ end
16
+
17
+ # Lists all the actions; initializes an empty `ObjectCollection` if there
18
+ # aren't any yet
19
+ # @return [Array] The actions
20
+ def actions
21
+ @actions ||= ObjectCollection.new
22
+ end
23
+
24
+ protected
25
+
26
+ def action(action, params = {}, &block)
27
+ actions << Action.create(action, self, params, &block)
28
+ end
29
+
30
+ def get(action_name, &block)
31
+ action action_name, method: :get, &block
32
+ end
33
+
34
+ def post(action_name, &block)
35
+ action action_name, method: :post, &block
36
+ end
37
+
38
+ def put(action_name, &block)
39
+ action action_name, method: :put, &block
40
+ end
41
+
42
+ def delete(action_name, &block)
43
+ action action_name, method: :delete, &block
44
+ end
45
+
46
+ def replace_action(action_name, &block)
47
+ action action_name do
48
+ replace(&block)
49
+ end
50
+ end
51
+
52
+ def override_action(action_name, params = {}, &block)
53
+ action action_name, params.merge(override: true), &block
54
+ end
55
+
56
+ def attributes(&block)
57
+ @strong_params = block
58
+ end
59
+ end
60
+ end