amfetamine 0.1.5
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.
- data/.gitignore +8 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.md +196 -0
- data/Rakefile +11 -0
- data/amfetamine.gemspec +40 -0
- data/lib/amfetamine.rb +19 -0
- data/lib/amfetamine/base.rb +189 -0
- data/lib/amfetamine/cache.rb +9 -0
- data/lib/amfetamine/caching_adapter.rb +66 -0
- data/lib/amfetamine/config.rb +34 -0
- data/lib/amfetamine/exceptions.rb +9 -0
- data/lib/amfetamine/helpers/rspec_matchers.rb +5 -0
- data/lib/amfetamine/helpers/test_helpers.rb +113 -0
- data/lib/amfetamine/logger.rb +18 -0
- data/lib/amfetamine/query_methods.rb +187 -0
- data/lib/amfetamine/relationship.rb +108 -0
- data/lib/amfetamine/relationships.rb +77 -0
- data/lib/amfetamine/rest_helpers.rb +122 -0
- data/lib/amfetamine/version.rb +3 -0
- data/spec/amfetamine/base_spec.rb +207 -0
- data/spec/amfetamine/caching_spec.rb +37 -0
- data/spec/amfetamine/callbacks_spec.rb +36 -0
- data/spec/amfetamine/conditions_spec.rb +110 -0
- data/spec/amfetamine/dummy_spec.rb +27 -0
- data/spec/amfetamine/relationships_spec.rb +103 -0
- data/spec/amfetamine/rest_helpers_spec.rb +25 -0
- data/spec/amfetamine/rspec_matchers_spec.rb +7 -0
- data/spec/amfetamine/testing_helpers_spec.rb +101 -0
- data/spec/dummy/child.rb +25 -0
- data/spec/dummy/configure.rb +4 -0
- data/spec/dummy/dummy.rb +48 -0
- data/spec/dummy/dummy_rest_client.rb +6 -0
- data/spec/helpers/active_model_lint.rb +21 -0
- data/spec/helpers/fakeweb_responses.rb +120 -0
- data/spec/spec_helper.rb +33 -0
- metadata +246 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
module Amfetamine
|
2
|
+
class Relationship
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :on, :type, :from
|
6
|
+
|
7
|
+
def initialize(opts)
|
8
|
+
@type = opts[:type]
|
9
|
+
@on = opts[:on] # Target class
|
10
|
+
@from = opts[:from] # receiving object
|
11
|
+
end
|
12
|
+
|
13
|
+
def << (other)
|
14
|
+
other.send("#{from_singular_name}_id=", @from.id)
|
15
|
+
other.instance_variable_set("@#{from_singular_name}", Amfetamine::Relationship.new(:on => @from, :from => other, :type => :belongs_to))
|
16
|
+
@children ||= [] # No need to do a request here, but it needs to be an array if it isn't yet.
|
17
|
+
@children << other
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_class
|
21
|
+
if @on.is_a?(Symbol)
|
22
|
+
Amfetamine.parent.const_get(@on.to_s.gsub('/', '::').singularize.gsub('_','').capitalize)
|
23
|
+
else
|
24
|
+
@on.class
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Id of object this relationship references
|
29
|
+
def parent_id
|
30
|
+
if @on.is_a?(Symbol)
|
31
|
+
@from.send(@on.to_s.downcase + "_id") if @type == :belongs_to
|
32
|
+
else
|
33
|
+
@on.id
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Id of the receiving object
|
38
|
+
def from_id
|
39
|
+
@from.id
|
40
|
+
end
|
41
|
+
|
42
|
+
def from_plural_name
|
43
|
+
@from.class.name.to_s.downcase.pluralize
|
44
|
+
end
|
45
|
+
|
46
|
+
def from_singular_name
|
47
|
+
@from.class.name.to_s.downcase
|
48
|
+
end
|
49
|
+
|
50
|
+
def on_plural_name
|
51
|
+
if @on.is_a?(Symbol)
|
52
|
+
@on.to_s.pluralize
|
53
|
+
else
|
54
|
+
@on.class.name.to_s.pluralize.downcase
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def rest_path
|
59
|
+
on_class.rest_path(:relationship => self)
|
60
|
+
end
|
61
|
+
|
62
|
+
def find_path(id)
|
63
|
+
on_class.find_path(id, :relationship => self)
|
64
|
+
end
|
65
|
+
|
66
|
+
def singular_path
|
67
|
+
find_path(@from.id)
|
68
|
+
end
|
69
|
+
|
70
|
+
def full_path
|
71
|
+
if @type == :has_many
|
72
|
+
raise InvalidPath if from_id == nil
|
73
|
+
"#{from_plural_name}/#{from_id}/#{on_plural_name}"
|
74
|
+
elsif @type == :belongs_to
|
75
|
+
raise InvalidPath if parent_id == nil
|
76
|
+
"#{on_plural_name}/#{parent_id}/#{from_plural_name}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def each
|
81
|
+
all.each { |c| yield c }
|
82
|
+
end
|
83
|
+
|
84
|
+
# Delegates the all method to child class with a nested path set
|
85
|
+
def all(opts={})
|
86
|
+
force = opts.delete(:force)
|
87
|
+
request = lambda { on_class.all({ :nested_path => rest_path }.merge(opts)) }
|
88
|
+
|
89
|
+
@children = if force
|
90
|
+
request.call
|
91
|
+
else
|
92
|
+
@children || request.call
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Delegates the find method to child class with a nested path set
|
97
|
+
def find(id, opts={})
|
98
|
+
on_class.find(id, {:nested_path => find_path(id)}.merge(opts))
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
def include?(other)
|
103
|
+
self.all
|
104
|
+
@children.include?(other)
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Amfetamine
|
2
|
+
module Relationships
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(args={})
|
8
|
+
#super(args)
|
9
|
+
if self.class._relationship_children
|
10
|
+
self.class._relationship_children.each do |klass|
|
11
|
+
instance_variable_set("@#{klass}", Amfetamine::Relationship.new(:on => klass, :from => self, :type => :has_many))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
if self.class._relationship_parents
|
16
|
+
self.class._relationship_parents.each do |klass|
|
17
|
+
instance_variable_set("@#{klass}", Amfetamine::Relationship.new(:on => klass, :from => self, :type => :belongs_to))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def belongs_to_relationship?
|
23
|
+
self.class._relationship_parents && self.class._relationship_parents.any?
|
24
|
+
end
|
25
|
+
|
26
|
+
def belongs_to_relationships
|
27
|
+
if self.class._relationship_parents
|
28
|
+
self.class._relationship_parents.collect { |e| self.send(e) }
|
29
|
+
else
|
30
|
+
[]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
def has_many_resources(*klasses)
|
36
|
+
self.class_eval do
|
37
|
+
@_relationship_children = []
|
38
|
+
klasses.each do |klass|
|
39
|
+
attr_reader klass
|
40
|
+
@_relationship_children << klass
|
41
|
+
|
42
|
+
parent_id_field = self.name.to_s.downcase.singularize + "_id"
|
43
|
+
|
44
|
+
define_method("build_#{klass.to_s.singularize}") do |*args|
|
45
|
+
args = args.shift || {}
|
46
|
+
Amfetamine.parent.const_get(klass.to_s.gsub('/', '::').singularize.gsub('_','').capitalize).new(args.merge(parent_id_field => self.id))
|
47
|
+
end
|
48
|
+
|
49
|
+
define_method("create_#{klass.to_s.singularize}") do |*args|
|
50
|
+
args = args.shift || {}
|
51
|
+
Amfetamine.parent.const_get(klass.to_s.gsub('/', '::').singularize.gsub('_','').capitalize).create(args.merge(parent_id_field => self.id))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def _relationship_children
|
58
|
+
@_relationship_children
|
59
|
+
end
|
60
|
+
|
61
|
+
def belongs_to_resource(*klasses)
|
62
|
+
self.class_eval do
|
63
|
+
@_relationship_parents = []
|
64
|
+
klasses.each do |klass|
|
65
|
+
attr_reader klass
|
66
|
+
|
67
|
+
@_relationship_parents << klass
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def _relationship_parents
|
73
|
+
@_relationship_parents
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Amfetamine
|
5
|
+
module RestHelpers
|
6
|
+
|
7
|
+
RESPONSE_STATUSES = { 422 => :errors, 404 => :notfound, 200 => :success, 201 => :created, 500 => :server_error, 406 => :not_acceptable }
|
8
|
+
|
9
|
+
def rest_path(args={})
|
10
|
+
self.class.rest_path(args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.included(base)
|
14
|
+
base.extend ClassMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
def singular_path(args={})
|
18
|
+
self.class.find_path(self.id, args)
|
19
|
+
end
|
20
|
+
|
21
|
+
# This method handles the save response
|
22
|
+
# TODO: Needs refactoring, now just want to make the test pass =)
|
23
|
+
# Making assumption here that when response is nil, it should have possitive result. Needs refactor when slept more
|
24
|
+
def handle_response(response)
|
25
|
+
if response[:status] == :success || response[:status] == :created
|
26
|
+
self.instance_variable_set('@notsaved', false)
|
27
|
+
true
|
28
|
+
elsif response[:status] == :errors
|
29
|
+
Amfetamine.logger.warn "Errors from response"
|
30
|
+
response[:body].each do |attr, mesg|
|
31
|
+
errors.add(attr.to_sym, mesg )
|
32
|
+
end
|
33
|
+
false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
def rest_path(params={})
|
40
|
+
result = if params[:relationship]
|
41
|
+
relationship = params[:relationship]
|
42
|
+
"/#{relationship.full_path}"
|
43
|
+
else
|
44
|
+
"/#{self.name.downcase.pluralize}"
|
45
|
+
end
|
46
|
+
|
47
|
+
result = base_uri + result unless params[:no_base_uri]
|
48
|
+
result = result + resource_suffix unless params[:no_resource_suffix]
|
49
|
+
return result
|
50
|
+
end
|
51
|
+
|
52
|
+
def find_path(id, params={})
|
53
|
+
params_for_rest_path = params.merge({:no_base_uri => true, :no_resource_suffix => true})
|
54
|
+
result = "#{self.rest_path(params_for_rest_path)}/#{id.to_s}"
|
55
|
+
|
56
|
+
result = base_uri + result unless params[:no_base_uri]
|
57
|
+
result = result + resource_suffix unless params[:no_resource_suffix]
|
58
|
+
return result
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def base_uri
|
63
|
+
@base_uri || Amfetamine::Config.base_uri
|
64
|
+
end
|
65
|
+
|
66
|
+
# wraps rest requests to the corresponding service
|
67
|
+
# *emerging*
|
68
|
+
def handle_request(method, path, opts={})
|
69
|
+
Amfetamine.logger.warn "Making request to #{path} with #{method} and #{opts.inspect}"
|
70
|
+
case method
|
71
|
+
when :get
|
72
|
+
response = rest_client.get(path, opts)
|
73
|
+
when :post
|
74
|
+
response = rest_client.post(path, opts)
|
75
|
+
when :put
|
76
|
+
response = rest_client.put(path, opts)
|
77
|
+
when :delete
|
78
|
+
response = rest_client.delete(path, opts)
|
79
|
+
else
|
80
|
+
raise UnknownRESTMethod, "handle_request only responds to get, put, post and delete"
|
81
|
+
end
|
82
|
+
parse_response(response)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns a hash with human readable status and parsed body
|
86
|
+
def parse_response(response)
|
87
|
+
status = RESPONSE_STATUSES.fetch(response.code) { raise "Response not known" }
|
88
|
+
raise Amfetamine::RecordNotFound if status == :notfound
|
89
|
+
body = if response.body && !(response.body.blank?)
|
90
|
+
response.parsed_response
|
91
|
+
else
|
92
|
+
self.to_json
|
93
|
+
end
|
94
|
+
{ :status => status, :body => body }
|
95
|
+
end
|
96
|
+
|
97
|
+
def rest_client
|
98
|
+
@rest_client || Amfetamine::Config.rest_client
|
99
|
+
end
|
100
|
+
|
101
|
+
def resource_suffix
|
102
|
+
@resource_suffix || Amfetamine::Config.resource_suffix || ""
|
103
|
+
end
|
104
|
+
|
105
|
+
# Allows setting a different rest client per class
|
106
|
+
def rest_client=(value)
|
107
|
+
raise Amfetamine::ConfigurationInvalid, 'Invalid value for rest_client' if ![:get,:put,:delete,:post].all? { |m| value.respond_to?(m) }
|
108
|
+
@rest_client = value
|
109
|
+
end
|
110
|
+
|
111
|
+
def resource_suffix=(value)
|
112
|
+
raise Amfetamine::ConfigurationInvalid, 'Invalid value for resource suffix' if !value.is_a?(String)
|
113
|
+
@resource_suffix = value
|
114
|
+
end
|
115
|
+
|
116
|
+
def base_uri=(value)
|
117
|
+
raise Amfetamine::ConfigurationInvalid, 'Invalid value for base uri' if !value.is_a?(String)
|
118
|
+
@base_uri = value
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# Integration tests :)
|
4
|
+
describe Amfetamine::Base do
|
5
|
+
describe "Dummy, our ever faitful test subject" do
|
6
|
+
# Some hight level tests, due to the complexity this makes it a lot easier to refactor
|
7
|
+
let(:dummy) { build(:dummy) }
|
8
|
+
subject { dummy }
|
9
|
+
|
10
|
+
it { should be_valid }
|
11
|
+
its(:title) { should ==('Dummy')}
|
12
|
+
its(:description) { should ==('Crash me!')}
|
13
|
+
its(:to_json) { should match(/dummy/) }
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "Class dummy, setup with amfetamine::base" do
|
18
|
+
let(:dummy) { build(:dummy) }
|
19
|
+
let(:dummy2) { build(:dummy) }
|
20
|
+
subject { Dummy}
|
21
|
+
|
22
|
+
it { should be_cacheable }
|
23
|
+
|
24
|
+
context "#attributes" do
|
25
|
+
it "should update attribute correctly if I edit it" do
|
26
|
+
dummy.title = "Oh a new title!"
|
27
|
+
dummy.attributes['title'].should == "Oh a new title!"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should include attributes in json" do
|
31
|
+
dummy.title = "Something new"
|
32
|
+
dummy.to_json.should match(/Something new/)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "#find" do
|
37
|
+
it "should find dummy" do
|
38
|
+
dummy.instance_variable_set('@notsaved', false)
|
39
|
+
stub_single_response(dummy) do
|
40
|
+
Dummy.find(dummy.id).should == dummy
|
41
|
+
end
|
42
|
+
dummy.should be_cached
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should return nil if object not found" do
|
46
|
+
lambda {
|
47
|
+
stub_nil_response do
|
48
|
+
Dummy.find(dummy.id * 2).should be_nil
|
49
|
+
end
|
50
|
+
}.should raise_exception(Amfetamine::RecordNotFound)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "#all" do
|
55
|
+
it "should find all if objects are present" do
|
56
|
+
dummies = []
|
57
|
+
dummy.instance_variable_set('@notsaved', false)
|
58
|
+
dummy2.instance_variable_set('@notsaved', false)
|
59
|
+
|
60
|
+
stub_all_response(dummy, dummy2) do
|
61
|
+
dummies = Dummy.all
|
62
|
+
end
|
63
|
+
|
64
|
+
dummies.should include(dummy)
|
65
|
+
dummies.should include(dummy2)
|
66
|
+
dummies.length.should eq(2)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should return empty array if objects are not present" do
|
70
|
+
dummies = []
|
71
|
+
stub_all_nil_response do
|
72
|
+
dummies = Dummy.all
|
73
|
+
end
|
74
|
+
|
75
|
+
dummies.should eq([])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "#create" do
|
80
|
+
it "should create an object if data is correct" do
|
81
|
+
new_dummy = nil
|
82
|
+
stub_post_response do
|
83
|
+
new_dummy = Dummy.create({:title => 'test', :description => 'blabla'})
|
84
|
+
end
|
85
|
+
new_dummy.should be_a(Dummy)
|
86
|
+
new_dummy.should_not be_new
|
87
|
+
new_dummy.should be_cached
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should return errors if data is incorrect" do
|
91
|
+
new_dummy = nil
|
92
|
+
stub_post_errornous_response do
|
93
|
+
new_dummy = Dummy.create({:title => 'test'})
|
94
|
+
end
|
95
|
+
new_dummy.should be_new
|
96
|
+
new_dummy.errors.messages.should eq({:description => ['can\'t be blank']})
|
97
|
+
new_dummy.should_not be_cached
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context "#update" do
|
102
|
+
before(:each) do
|
103
|
+
dummy.send(:notsaved=, false)
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should update if response is succesful" do
|
107
|
+
stub_update_response do
|
108
|
+
dummy.update_attributes({:title => 'zomg'})
|
109
|
+
end
|
110
|
+
dummy.should_not be_new
|
111
|
+
dummy.title.should eq('zomg')
|
112
|
+
dummy.should be_cached
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should show errors if response is not succesful" do
|
116
|
+
stub_update_errornous_response do
|
117
|
+
dummy.update_attributes({:title => ''})
|
118
|
+
end
|
119
|
+
dummy.should_not be_new
|
120
|
+
dummy.errors.messages.should eq({:title => ['can\'t be blank']})
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should not do a request if the data doesn't change" do
|
124
|
+
# Assumes that dummy.update would raise if not within stubbed request.
|
125
|
+
dummy.update_attributes({:title => dummy.title})
|
126
|
+
dummy.errors.should be_empty
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context "#save" do
|
131
|
+
before(:each) do
|
132
|
+
dummy.send(:notsaved=, true)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should update the id if data is received from post" do
|
136
|
+
old_id = dummy.id
|
137
|
+
stub_post_response(dummy) do
|
138
|
+
dummy.send(:id=, nil)
|
139
|
+
dummy.save
|
140
|
+
end
|
141
|
+
dummy.id.should == old_id
|
142
|
+
dummy.attributes[:id].should == old_id
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should update attributes if data is received from update" do
|
146
|
+
dummy.send(:notsaved=, false)
|
147
|
+
old_id = dummy.id
|
148
|
+
dummy.title = "BLABLABLA"
|
149
|
+
stub_update_response(dummy) do
|
150
|
+
dummy.title = "BLABLABLA"
|
151
|
+
dummy.save
|
152
|
+
end
|
153
|
+
dummy.id.should == old_id
|
154
|
+
dummy.title.should == "BLABLABLA"
|
155
|
+
dummy.attributes[:title] = "BLABLABLA"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
context "#delete" do
|
160
|
+
before(:each) do
|
161
|
+
dummy.send(:notsaved=, false)
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should delete the object if response is succesful" do
|
165
|
+
stub_delete_response do
|
166
|
+
dummy.destroy
|
167
|
+
end
|
168
|
+
dummy.should be_new
|
169
|
+
dummy.id.should be_nil
|
170
|
+
dummy.should_not be_cached
|
171
|
+
end
|
172
|
+
|
173
|
+
it "should return false if delete failed" do
|
174
|
+
stub_delete_errornous_response do
|
175
|
+
dummy.destroy
|
176
|
+
end
|
177
|
+
dummy.should_not be_new
|
178
|
+
dummy.should_not be_cached
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe "Features and bugs" do
|
184
|
+
it "should raise an exception if cached args are nil" do
|
185
|
+
lambda { Dummy.build_object(nil) }.should raise_exception(Amfetamine::InvalidCacheData)
|
186
|
+
end
|
187
|
+
|
188
|
+
it "should raise an exception if cached args do not contain an ID" do
|
189
|
+
lambda { Dummy.build_object(:no_id => 'present') }.should raise_exception(Amfetamine::InvalidCacheData)
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should raise correct exception is data is not expected format" do
|
193
|
+
lambda { Dummy.build_object([]) }.should raise_exception(Amfetamine::InvalidCacheData)
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should receive data when doing a post" do
|
197
|
+
Dummy.prevent_external_connections! do
|
198
|
+
dummy = build(:dummy)
|
199
|
+
Dummy.rest_client.should_receive(:post).with("/dummies", :body => dummy.to_json).
|
200
|
+
and_return(Amfetamine::FakeResponse.new('post', 201, lambda { dummy }))
|
201
|
+
dummy.save
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|