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