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
@@ -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