cathode 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +346 -125
- data/Rakefile +1 -0
- data/app/controllers/cathode/base_controller.rb +28 -45
- data/app/models/cathode/token.rb +21 -0
- data/config/routes.rb +16 -0
- data/db/migrate/20140425164100_create_cathode_tokens.rb +11 -0
- data/lib/cathode.rb +0 -13
- data/lib/cathode/_version.rb +2 -1
- data/lib/cathode/action.rb +197 -39
- data/lib/cathode/action_dsl.rb +60 -0
- data/lib/cathode/base.rb +81 -12
- data/lib/cathode/create_request.rb +21 -0
- data/lib/cathode/custom_request.rb +5 -0
- data/lib/cathode/debug.rb +25 -0
- data/lib/cathode/destroy_request.rb +13 -0
- data/lib/cathode/engine.rb +9 -0
- data/lib/cathode/exceptions.rb +20 -0
- data/lib/cathode/index_request.rb +40 -0
- data/lib/cathode/object_collection.rb +49 -0
- data/lib/cathode/query.rb +24 -0
- data/lib/cathode/railtie.rb +21 -0
- data/lib/cathode/request.rb +139 -7
- data/lib/cathode/resource.rb +50 -19
- data/lib/cathode/resource_dsl.rb +46 -0
- data/lib/cathode/show_request.rb +13 -0
- data/lib/cathode/update_request.rb +26 -0
- data/lib/cathode/version.rb +112 -23
- data/lib/tasks/cathode_tasks.rake +5 -4
- data/spec/dummy/app/api/api.rb +0 -0
- data/spec/dummy/app/models/payment.rb +3 -0
- data/spec/dummy/app/models/product.rb +1 -0
- data/spec/dummy/app/models/sale.rb +5 -0
- data/spec/dummy/app/models/salesperson.rb +3 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20140409183635_create_sales.rb +11 -0
- data/spec/dummy/db/migrate/20140423172419_create_salespeople.rb +11 -0
- data/spec/dummy/db/migrate/20140424181343_create_payments.rb +10 -0
- data/spec/dummy/db/schema.rb +31 -1
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +1167 -0
- data/spec/dummy/log/test.log +180602 -0
- data/spec/dummy/spec/factories/payments.rb +8 -0
- data/spec/dummy/spec/factories/products.rb +1 -1
- data/spec/dummy/spec/factories/sales.rb +9 -0
- data/spec/dummy/spec/factories/salespeople.rb +7 -0
- data/spec/dummy/spec/requests/requests_spec.rb +434 -0
- data/spec/lib/cathode/action_spec.rb +136 -0
- data/spec/lib/cathode/base_spec.rb +34 -0
- data/spec/lib/cathode/create_request_spec.rb +40 -0
- data/spec/lib/cathode/custom_request_spec.rb +31 -0
- data/spec/lib/cathode/debug_spec.rb +25 -0
- data/spec/lib/cathode/destroy_request_spec.rb +28 -0
- data/spec/lib/cathode/index_request_spec.rb +62 -0
- data/spec/lib/cathode/object_collection_spec.rb +66 -0
- data/spec/lib/cathode/query_spec.rb +28 -0
- data/spec/lib/cathode/request_spec.rb +58 -0
- data/spec/lib/cathode/resource_spec.rb +482 -0
- data/spec/lib/cathode/show_request_spec.rb +23 -0
- data/spec/lib/cathode/update_request_spec.rb +41 -0
- data/spec/lib/cathode/version_spec.rb +416 -0
- data/spec/models/cathode/token_spec.rb +62 -0
- data/spec/spec_helper.rb +8 -2
- data/spec/support/factories/payments.rb +3 -0
- data/spec/support/factories/sale.rb +3 -0
- data/spec/support/factories/salespeople.rb +3 -0
- data/spec/support/factories/token.rb +3 -0
- data/spec/support/helpers.rb +13 -2
- metadata +192 -47
- data/app/helpers/cathode/application_helper.rb +0 -4
- data/spec/dummy/app/api/dummy_api.rb +0 -4
- data/spec/integration/api_spec.rb +0 -88
- data/spec/lib/action_spec.rb +0 -140
- data/spec/lib/base_spec.rb +0 -28
- data/spec/lib/request_spec.rb +0 -5
- data/spec/lib/resources_spec.rb +0 -78
- data/spec/lib/versioning_spec.rb +0 -104
data/lib/cathode/resource.rb
CHANGED
@@ -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
|
-
|
4
|
-
|
5
|
+
include ActionDsl
|
6
|
+
include ResourceDsl
|
5
7
|
|
6
|
-
|
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 ||= {
|
24
|
+
params ||= {}
|
25
|
+
params[:actions] ||= []
|
10
26
|
|
11
27
|
@name = resource_name
|
12
28
|
|
13
|
-
|
29
|
+
camelized_resource = resource_name.to_s.camelize
|
14
30
|
|
15
|
-
@
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
24
|
-
|
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
|
-
|
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
|
-
|
31
|
-
|
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
|
-
|
36
|
-
|
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
|
data/lib/cathode/version.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
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
|
-
|
17
|
-
|
8
|
+
attr_reader :ancestor,
|
9
|
+
:version
|
18
10
|
|
19
|
-
|
20
|
-
resources[resource].actions[params[:action].to_sym].perform params
|
21
|
-
end
|
11
|
+
@all = []
|
22
12
|
|
23
13
|
class << self
|
24
|
-
|
25
|
-
|
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
|
-
|
39
|
-
|
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
|
46
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
namespace :cathode do
|
2
|
+
task :info do
|
3
|
+
puts Cathode::Debug.info
|
4
|
+
end
|
5
|
+
end
|
File without changes
|
Binary file
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -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:
|
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
|