amfetamine 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ module Amfetamine
2
+ VERSION = "0.1.5"
3
+ 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