cathode 0.0.1 → 0.1.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 (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