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
@@ -1,39 +1,70 @@
1
1
  module Cathode
2
+ # A `Resource` is paired with a Rails model and describes the actions that the
3
+ # resource responds to.
2
4
  class Resource
3
- attr_reader :actions,
4
- :name
5
+ include ActionDsl
6
+ include ResourceDsl
5
7
 
6
- def initialize(resource_name, params = nil, &block)
8
+ attr_reader :controller_prefix,
9
+ :model,
10
+ :name,
11
+ :parent,
12
+ :singular,
13
+ :strong_params
14
+
15
+ # Creates a new resource.
16
+ # @param resource_name [Symbol] The resource's name
17
+ # @param params [Hash] An optional params hash, e.g. `{ actions: :all }`
18
+ # @param context [Resource] An optional parent resource
19
+ # @param block The resource's actions and properties, defined with the
20
+ # {ActionDsl} and {ResourceDsl}
21
+ def initialize(resource_name, params = nil, context = nil, &block)
7
22
  require_resource_constant! resource_name
8
23
 
9
- params ||= { actions: [] }
24
+ params ||= {}
25
+ params[:actions] ||= []
10
26
 
11
27
  @name = resource_name
12
28
 
13
- Cathode.const_set "#{resource_name.to_s.camelize}Controller", Class.new(Cathode::BaseController)
29
+ camelized_resource = resource_name.to_s.camelize
14
30
 
15
- @actions = {}
16
- actions_to_add = params[:actions] == [:all] ? [:index, :show, :create, :update, :destroy] : params[:actions]
17
- actions_to_add.each do |action_name|
18
- action action_name
31
+ @controller_prefix = if context.present? && context.is_a?(Resource)
32
+ @parent = context
33
+ "#{context.controller_prefix}#{camelized_resource}"
34
+ else
35
+ camelized_resource
19
36
  end
20
- self.instance_eval &block if block_given?
21
- actions = @actions
22
37
 
23
- Cathode::Engine.routes.draw do
24
- resources resource_name, only: actions.keys
38
+ controller_name = "#{@controller_prefix}Controller"
39
+ unless Cathode.const_defined? controller_name
40
+ Cathode.const_set controller_name, Class.new(Cathode::BaseController)
25
41
  end
26
- end
27
42
 
28
- private
43
+ @actions = ObjectCollection.new
44
+ actions_to_add = params[:actions] == :all ? DEFAULT_ACTIONS : params[:actions]
45
+ actions_to_add.each { |action_name| action action_name }
46
+
47
+ @singular = params[:singular]
48
+
49
+ instance_eval(&block) if block_given?
29
50
 
30
- def action(action, &block)
31
- @actions[action] = Action.create(action, @name, &block)
51
+ @actions.each do |action|
52
+ action.after_resource_initialized if action.respond_to? :after_resource_initialized
53
+ end
54
+
55
+ if @strong_params.present? && actions.find(:create).nil? && actions.find(:update).nil?
56
+ raise UnknownActionError,
57
+ 'An attributes block was specified without a :create or :update action'
58
+ end
32
59
  end
33
60
 
61
+ private
62
+
34
63
  def require_resource_constant!(resource_name)
35
- if resource_name.to_s.singularize.camelize.safe_constantize.nil?
36
- raise UnknownResourceError
64
+ constant = resource_name.to_s.singularize.camelize
65
+ @model = constant.safe_constantize
66
+ if @model.nil?
67
+ fail UnknownResourceError, "Could not find constant `#{constant}' for resource `#{resource_name}'"
37
68
  end
38
69
  end
39
70
  end
@@ -0,0 +1,46 @@
1
+ module Cathode
2
+ # Holds the domain-specific language (DSL) for describing resources.
3
+ module ResourceDsl
4
+ # Lists all the resources; initializes an empty `ObjectCollection` if there
5
+ # aren't any yet
6
+ # @return [Array] The resources
7
+ def _resources
8
+ @_resources ||= ObjectCollection.new
9
+ end
10
+
11
+ protected
12
+
13
+ def resources(resource_name, params = {}, &block)
14
+ add_resource resource_name, false, params, &block
15
+ end
16
+
17
+ def resource(resource_name, params = {}, &block)
18
+ add_resource resource_name, true, params, &block
19
+ end
20
+
21
+ def add_resource(resource_name, singular, params, &block)
22
+ params ||= {}
23
+ params = params.merge(singular: singular)
24
+ existing_resource = _resources.find resource_name
25
+ new_resource = Resource.new(resource_name, params, self, &block)
26
+
27
+ if existing_resource.present?
28
+ existing_resource.actions.add new_resource.actions.objects
29
+ else
30
+ @_resources << new_resource
31
+ end
32
+ end
33
+
34
+ def remove_resource(resources)
35
+ resources = [resources] unless resources.is_a?(Array)
36
+
37
+ resources.each do |resource|
38
+ if _resources.find(resource).nil?
39
+ fail UnknownResourceError, "Unknown resource `#{resource}'"
40
+ end
41
+
42
+ @_resources.delete(resource)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ module Cathode
2
+ # Defines the default behavior for a show request.
3
+ class ShowRequest < Request
4
+ # Determine the default action to use depending on the resource. If the
5
+ # resource is singular, set the body to the parent's associated record.
6
+ # Otherwise, lookup the record directly.
7
+ def default_action_block
8
+ proc do
9
+ body record
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ module Cathode
2
+ # Defines the default behavior for an update request.
3
+ class UpdateRequest < Request
4
+ # Sets the default action to update a resource. If the resource is
5
+ # singular, updates the parent's associated resource. Otherwise, updates the
6
+ # resource directly.
7
+ def default_action_block
8
+ proc do
9
+ begin
10
+ record = if resource.singular
11
+ parent_model = resource.parent.model.find(parent_resource_id)
12
+ parent_model.send resource.name
13
+ else
14
+ record = model.find(params[:id])
15
+ end
16
+
17
+ record.update(instance_eval(&@strong_params))
18
+ body record.reload
19
+ rescue ActionController::ParameterMissing => error
20
+ body error.message
21
+ status :bad_request
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,30 +1,41 @@
1
- require 'semantic'
2
-
3
1
  module Cathode
2
+ # A `Version` encapsulates a specific SemVer-compliant version of the API with
3
+ # a set of resources and actions.
4
4
  class Version
5
- attr_reader :resources,
6
- :version
7
-
8
- @@all = {}
9
-
10
- def initialize(version_number, &block)
11
- @version = Semantic::Version.new Version.standardize(version_number)
12
- @resources = {}
13
-
14
- self.instance_eval &block if block_given?
5
+ include ActionDsl
6
+ include ResourceDsl
15
7
 
16
- Version.all[@version.to_s] = self
17
- end
8
+ attr_reader :ancestor,
9
+ :version
18
10
 
19
- def perform_request(resource, params)
20
- resources[resource].actions[params[:action].to_sym].perform params
21
- end
11
+ @all = []
22
12
 
23
13
  class << self
24
- def all
25
- @@all
14
+ attr_reader :all
15
+
16
+ # Defines a new version.
17
+ # @param version_number [String, Fixnum, Float] A number or string
18
+ # representing a SemVer-compliant version number. If a `Fixnum` or
19
+ # `Float` is passed, it will be converted to a string before being
20
+ # evaluated for SemVer compliance, so passing `1.5` is equivalent to
21
+ # passing `'1.5.0'`.
22
+ # @param block A block defining the version's resources and actions, and
23
+ # has access to the methods in the {Cathode::ActionDsl} and {Cathode::ResourceDsl}
24
+ # @return [Version]
25
+ def define(version_number, &block)
26
+ version = Version.find(version_number)
27
+ if version.present?
28
+ version.instance_eval(&block)
29
+ else
30
+ version = self.new(version_number, &block)
31
+ end
32
+ version
26
33
  end
27
34
 
35
+ # Polyfills a version number snippet to be SemVer-compliant.
36
+ # @param rough_version [String] A version number snippet such as '1' or
37
+ # '2.5'
38
+ # @return [String] The SemVer-compliant version number
28
39
  def standardize(rough_version)
29
40
  version_parts = rough_version.to_s.split '.'
30
41
  if version_parts.count < 2
@@ -35,15 +46,93 @@ module Cathode
35
46
  version_parts.join '.'
36
47
  end
37
48
 
38
- def perform_request_with_version(version, resource, params)
39
- Version.all[standardize(version)].perform_request resource, params
49
+ # Looks up a version by version number.
50
+ # @param version_number [String] The version to find
51
+ # @return [Version, nil] The version if found, `nil` if there is no such
52
+ # version
53
+ def find(version_number)
54
+ Version.all.detect { |v| v.version == standardize(version_number) }
55
+ rescue ArgumentError
56
+ nil
57
+ end
58
+
59
+ # Whether a given version exists
60
+ # @param version_number [String] The version to check
61
+ # @return [Boolean]
62
+ def exists?(version_number)
63
+ find(version_number).present?
40
64
  end
41
65
  end
42
66
 
67
+ # Initializes a new version.
68
+ # @param version_number [String] A SemVer-compliant version number.
69
+ # @param block A block defining the version's resources and actions, and
70
+ # has access to the methods in the {Cathode::ActionDsl} and {Cathode::ResourceDsl}
71
+ def initialize(version_number, &block)
72
+ @version = Semantic::Version.new Version.standardize(version_number)
73
+
74
+ if Version.all.present?
75
+ @ancestor = Version.all.last
76
+ @_resources = DeepClone.clone @ancestor._resources
77
+ actions.add ancestor.actions.objects
78
+ end
79
+
80
+ instance_eval(&block) if block_given?
81
+
82
+ Version.all << self
83
+ end
84
+
85
+ # Whether a resource is defined on the version.
86
+ # @param resource [Symbol] The resource's name
87
+ # @return [Boolean]
88
+ def resource?(resource)
89
+ _resources.names.include? resource.to_sym
90
+ end
91
+
92
+ # Whether an action is defined on a resource on the version.
93
+ # @param resource [Symbol] The resource's name
94
+ # @param action [Symbol] The action's name
95
+ # @return [Boolean]
96
+ def action?(resource, action)
97
+ resource = resource.to_sym
98
+ action = action.to_sym
99
+
100
+ return false unless resource?(resource)
101
+
102
+ _resources.find(resource).actions.names.include? action
103
+ end
104
+
43
105
  private
44
106
 
45
- def resource(resource, params = nil, &block)
46
- @resources[resource] = Resource.new(resource, params, &block)
107
+ def remove_action(*args)
108
+ if args.last.is_a?(Hash)
109
+ resource_name = args.last[:from]
110
+ resource = @_resources.find(resource_name)
111
+ actions_to_remove = args.take args.size - 1
112
+
113
+ if resource.nil?
114
+ fail UnknownResourceError, "Unknown resource `#{resource_name}' on ancestor version #{ancestor.version}"
115
+ end
116
+
117
+ actions_to_remove.each do |action|
118
+ if resource.actions.find(action).nil?
119
+ fail UnknownActionError, "Unknown action `#{action}' on resource `#{resource_name}'"
120
+ end
121
+
122
+ resource.actions.delete action
123
+ end
124
+ else
125
+ args.each do |action|
126
+ if actions.find(action).nil?
127
+ fail UnknownActionError, "Unknown action `#{action}' on ancestor version #{ancestor.version}"
128
+ end
129
+
130
+ actions.delete action
131
+ end
132
+ end
47
133
  end
134
+
135
+ alias_method :remove_resources, :remove_resource
136
+ alias_method :remove_actions, :remove_action
48
137
  end
49
138
  end
@@ -1,4 +1,5 @@
1
- # desc "Explaining what the task does"
2
- # task :cathode do
3
- # # Task goes here
4
- # end
1
+ namespace :cathode do
2
+ task :info do
3
+ puts Cathode::Debug.info
4
+ end
5
+ end
File without changes
@@ -0,0 +1,3 @@
1
+ class Payment < ActiveRecord::Base
2
+ belongs_to :sale
3
+ end
@@ -1,2 +1,3 @@
1
1
  class Product < ActiveRecord::Base
2
+ has_many :sales
2
3
  end
@@ -0,0 +1,5 @@
1
+ class Sale < ActiveRecord::Base
2
+ belongs_to :salesperson
3
+ belongs_to :product
4
+ has_one :payment
5
+ end
@@ -0,0 +1,3 @@
1
+ class Salesperson < ActiveRecord::Base
2
+ has_many :sales
3
+ end
@@ -0,0 +1,11 @@
1
+ class CreateSales < ActiveRecord::Migration
2
+ def change
3
+ create_table :sales do |t|
4
+ t.integer :product_id
5
+ t.integer :subtotal
6
+ t.integer :taxes
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ class CreateSalespeople < ActiveRecord::Migration
2
+ def change
3
+ create_table :salespeople do |t|
4
+ t.string :name
5
+
6
+ t.timestamps
7
+ end
8
+
9
+ add_column :sales, :salesperson_id, :integer
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ class CreatePayments < ActiveRecord::Migration
2
+ def change
3
+ create_table :payments do |t|
4
+ t.integer :amount
5
+ t.integer :sale_id
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -11,7 +11,22 @@
11
11
  #
12
12
  # It's strongly recommended that you check this file into your version control system.
13
13
 
14
- ActiveRecord::Schema.define(version: 20140404222551) do
14
+ ActiveRecord::Schema.define(version: 20140425164100) do
15
+
16
+ create_table "cathode_tokens", force: true do |t|
17
+ t.boolean "active", default: true
18
+ t.datetime "expired_at"
19
+ t.string "token"
20
+ t.datetime "created_at"
21
+ t.datetime "updated_at"
22
+ end
23
+
24
+ create_table "payments", force: true do |t|
25
+ t.integer "amount"
26
+ t.integer "sale_id"
27
+ t.datetime "created_at"
28
+ t.datetime "updated_at"
29
+ end
15
30
 
16
31
  create_table "products", force: true do |t|
17
32
  t.string "title"
@@ -20,4 +35,19 @@ ActiveRecord::Schema.define(version: 20140404222551) do
20
35
  t.datetime "updated_at"
21
36
  end
22
37
 
38
+ create_table "sales", force: true do |t|
39
+ t.integer "product_id"
40
+ t.integer "subtotal"
41
+ t.integer "taxes"
42
+ t.datetime "created_at"
43
+ t.datetime "updated_at"
44
+ t.integer "salesperson_id"
45
+ end
46
+
47
+ create_table "salespeople", force: true do |t|
48
+ t.string "name"
49
+ t.datetime "created_at"
50
+ t.datetime "updated_at"
51
+ end
52
+
23
53
  end