her 0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -39,6 +39,12 @@ User.find(1) # => Fetches "https://api.example.com/users/1" and return a User o
39
39
 
40
40
  ## Relationships
41
41
 
42
+ You can define `has_many` relationships in your models. The relationship data is handled in two different ways. When parsing a resource from JSON data, if there’s a relationship data included, it will be used to create new Ruby objects.
43
+
44
+ If no relationship data was included when parsing a resource, calling a method with the same name as the relationship will fetch the data (providing there’s an HTTP request available for it).
45
+
46
+ For example, with this setup:
47
+
42
48
  ```ruby
43
49
  class User
44
50
  include Her::Model
@@ -48,15 +54,43 @@ end
48
54
  class Comment
49
55
  include Her::Model
50
56
  end
57
+ ```
58
+
59
+ Including relationship data in the resource, no extra HTTP request is made when calling the `#comments` method:
60
+
61
+ ```ruby
62
+ @user = User.find(1) # { :data => { :id => 1, :name => "Rémi Prévost", :comments => [{ :id => 1, :text => "Foo" }, { :id => 2, :text => "Bar" }] }}
63
+ @user.comments # => [#<Comment id=1>, #<Comment id=2>] fetched directly from @user
64
+ ```
65
+
66
+ If there’s no relationship data in the resource, an extra HTTP request (to `GET /users/1/comments`) is made when calling the `#comments` method:
51
67
 
52
- user = User.find(1)
53
- user.comments # => [#<Comment id=1>, #<Comment id=2>]
68
+ ```ruby
69
+ @user = User.find(1) # { :data => { :id => 1, :name => "Rémi Prévost" }}
70
+ @user.comments # => [#<Comment id=1>, #<Comment id=2>] fetched from /users/1/comments
54
71
  ```
55
72
 
73
+ However, subsequent calls to `#comments` will not trigger the extra HTTP request.
74
+
56
75
  ## Custom requests
57
76
 
58
- TBD.
77
+ You can easily add custom methods for your models. You can either use `get_collection` (which maps the returned data to a collection of resources), `get_resource` (which maps the returned data to a single resource) or `get_raw` (which yields the parsed data return from the HTTP request).
59
78
 
60
- ## Hooks
79
+ ```ruby
80
+ class User
81
+ include Her::Model
61
82
 
62
- TBD.
83
+ def self.popular
84
+ get_collection("/users/popular")
85
+ end
86
+
87
+ def self.total
88
+ get_raw("/users/stats") do |parsed_data|
89
+ parsed_data[:data][:total_users]
90
+ end
91
+ end
92
+ end
93
+
94
+ User.popular # => [#<User id=1>, #<User id=2>]
95
+ User.total # => 42
96
+ ```
data/lib/her/api.rb CHANGED
@@ -26,7 +26,7 @@ module Her
26
26
  @parse_with = lambda do |response|
27
27
  json = JSON.parse(response.body, :symbolize_names => true)
28
28
  {
29
- :resource => json[:data],
29
+ :data => json[:data],
30
30
  :errors => json[:errors],
31
31
  :metadata => json[:metadata],
32
32
  }
@@ -38,7 +38,7 @@ module Her
38
38
  end # }}}
39
39
 
40
40
  # Define a custom parsing procedure. The procedure is passed the response object and is
41
- # expected to return a hash with three keys: a main resource Hash, an errors Array
41
+ # expected to return a hash with three keys: a main data Hash, an errors Array
42
42
  # and a metadata Hash.
43
43
  #
44
44
  # @example
@@ -12,7 +12,6 @@ module Her
12
12
  # @example
13
13
  # class User
14
14
  # include Her::Model
15
- # uses_api $example_api
16
15
  # collection_path "users"
17
16
  # end
18
17
  def collection_path(path=nil) # {{{
@@ -28,36 +27,99 @@ module Her
28
27
  end # }}}
29
28
 
30
29
  # Make a GET request and return the parsed JSON response (not mapped to objects)
31
- #
32
- # @example
33
- # User.get "/users/1"
34
- def get(path, attrs={}, &block) # {{{
30
+ def get_raw(path, attrs={}, &block) # {{{
35
31
  request(attrs.merge(:_method => :get, :_path => path), &block)
36
32
  end # }}}
37
33
 
34
+ # Make a GET request and return a collection of resources
35
+ def get_collection(path, attrs={}) # {{{
36
+ get_raw(path, attrs) do |parsed_data|
37
+ new_collection(parsed_data)
38
+ end
39
+ end # }}}
40
+
41
+ # Make a GET request and return a collection of resources
42
+ def get_resource(path, attrs={}) # {{{
43
+ get_raw(path, attrs) do |parsed_data|
44
+ new(parsed_data[:data])
45
+ end
46
+ end # }}}
47
+
38
48
  # Make a POST request and return the parsed JSON response (not mapped to objects)
39
- #
40
- # @example
41
- # User.post "/users", :fullname => "G.O.B. Bluth"
42
- def post(path, attrs={}, &block) # {{{
49
+ def post_raw(path, attrs={}, &block) # {{{
43
50
  request(attrs.merge(:_method => :post, :_path => path), &block)
44
51
  end # }}}
45
52
 
53
+ # Make a POST request and return a collection of resources
54
+ def post_collection(path, attrs={}) # {{{
55
+ post_raw(path, attrs) do |parsed_data|
56
+ new_collection(parsed_data)
57
+ end
58
+ end # }}}
59
+
60
+ # Make a POST request and return a collection of resources
61
+ def post_resource(path, attrs={}) # {{{
62
+ post_raw(path, attrs) do |parsed_data|
63
+ new(parsed_data[:data])
64
+ end
65
+ end # }}}
66
+
46
67
  # Make a PUT request and return the parsed JSON response (not mapped to objects)
47
- #
48
- # @example
49
- # User.put "/users/1", :email => "gob@bluthcompany.com"
50
- def put(path, attrs={}, &block) # {{{
68
+ def put_raw(path, attrs={}, &block) # {{{
51
69
  request(attrs.merge(:_method => :put, :_path => path), &block)
52
70
  end # }}}
53
71
 
72
+ # Make a PUT request and return a collection of resources
73
+ def put_collection(path, attrs={}) # {{{
74
+ put_raw(path, attrs) do |parsed_data|
75
+ new_collection(parsed_data)
76
+ end
77
+ end # }}}
78
+
79
+ # Make a PUT request and return a collection of resources
80
+ def put_resource(path, attrs={}) # {{{
81
+ put_raw(path, attrs) do |parsed_data|
82
+ new(parsed_data[:data])
83
+ end
84
+ end # }}}
85
+
86
+ # Make a PATCH request and return the parsed JSON response (not mapped to objects)
87
+ def patch_raw(path, attrs={}, &block) # {{{
88
+ request(attrs.merge(:_method => :patch, :_path => path), &block)
89
+ end # }}}
90
+
91
+ # Make a PATCH request and return a collection of resources
92
+ def patch_collection(path, attrs={}) # {{{
93
+ patch_raw(path, attrs) do |parsed_data|
94
+ new_collection(parsed_data)
95
+ end
96
+ end # }}}
97
+
98
+ # Make a PATCH request and return a collection of resources
99
+ def patch_resource(path, attrs={}) # {{{
100
+ patch_raw(path, attrs) do |parsed_data|
101
+ new(parsed_data[:data])
102
+ end
103
+ end # }}}
104
+
54
105
  # Make a DELETE request and return the parsed JSON response (not mapped to objects)
55
- #
56
- # @example
57
- # User.delete "/users/1"
58
- def delete(path, attrs={}, &block) # {{{
106
+ def delete_raw(path, attrs={}, &block) # {{{
59
107
  request(attrs.merge(:_method => :delete, :_path => path), &block)
60
108
  end # }}}
109
+
110
+ # Make a DELETE request and return a collection of resources
111
+ def delete_collection(path, attrs={}) # {{{
112
+ delete_raw(path, attrs) do |parsed_data|
113
+ new_collection(parsed_data)
114
+ end
115
+ end # }}}
116
+
117
+ # Make a DELETE request and return a collection of resources
118
+ def delete_resource(path, attrs={}) # {{{
119
+ delete_raw(path, attrs) do |parsed_data|
120
+ new(parsed_data[:data])
121
+ end
122
+ end # }}}
61
123
  end
62
124
  end
63
125
  end
data/lib/her/model/orm.rb CHANGED
@@ -9,37 +9,67 @@ module Her
9
9
  @data = self.class.parse_relationships(@data)
10
10
  end # }}}
11
11
 
12
+ # Initialize a collection of resources with raw data from an HTTP request
13
+ #
14
+ # @example
15
+ # User.get("/users/popular") { |data| User.new_collection(data) }
16
+ def new_collection(parsed_data) # {{{
17
+ collection_data = parsed_data[:data]
18
+ Her::Model::ORM.initialize_collection(self.to_s.downcase.to_sym, collection_data)
19
+ end # }}}
20
+
12
21
  # Initialize a collection of resources
13
22
  # @private
14
23
  def self.initialize_collection(name, collection_data) # {{{
15
24
  collection_data.map { |item_data| Object.const_get(name.to_s.classify).new(item_data) }
16
25
  end # }}}
17
26
 
18
- # Handles missing methods
27
+ # Handles missing methods by routing them through @data
19
28
  # @private
20
- def method_missing(method) # {{{
21
- method = method.to_s.gsub(/(\?|\!)$/, "").to_sym
22
- @data.include?(method) ? @data[method] : super
29
+ def method_missing(method, attrs=nil) # {{{
30
+ assignment_method = method.to_s =~ /\=$/
31
+ method = method.to_s.gsub(/(\?|\!|\=)$/, "").to_sym
32
+ if @data.include?(method)
33
+ if attrs and assignment_method
34
+ @data[method.to_s.gsub(/\=$/, "").to_sym] = attrs
35
+ else
36
+ @data[method]
37
+ end
38
+ else
39
+ super
40
+ end
23
41
  end # }}}
24
42
 
25
43
  # Fetch a specific resource based on an ID
26
44
  def find(id, params={}) # {{{
27
45
  request(params.merge(:_method => :get, :_path => "#{@her_collection_path}/#{id}")) do |parsed_data|
28
- new(parsed_data[:resource])
46
+ new(parsed_data[:data])
29
47
  end
30
48
  end # }}}
31
49
 
32
50
  # Fetch a collection of resources
33
51
  def all(params={}) # {{{
34
52
  request(params.merge(:_method => :get, :_path => "#{@her_collection_path}")) do |parsed_data|
35
- Her::Model::ORM.initialize_collection(to_s.downcase.pluralize, parsed_data[:resource])
53
+ Her::Model::ORM.initialize_collection(to_s.downcase.pluralize, parsed_data[:data])
36
54
  end
37
55
  end # }}}
38
56
 
39
57
  # Create a resource
40
58
  def create(params={}) # {{{
41
59
  request(params.merge(:_method => :post, :_path => "#{@her_collection_path}")) do |parsed_data|
42
- new(parsed_data[:resource])
60
+ new(parsed_data[:data])
61
+ end
62
+ end # }}}
63
+
64
+ # Save a resource
65
+ def save # {{{
66
+ params = @data.dup
67
+ if @data[:id]
68
+ self.class.request(params.merge(:_method => :put, :_path => "#{self.class.collection_path}/#{id}")) do |parsed_data|
69
+ @data = parsed_data[:data]
70
+ end
71
+ else
72
+ self.class.create(params)
43
73
  end
44
74
  end # }}}
45
75
  end
@@ -34,9 +34,7 @@ module Her
34
34
 
35
35
  define_method(name) do
36
36
  return @data[name] if @data.include?(name) # Do not fetch from API again if we have it in @data
37
- self.class.get("#{collection_path}/#{id}/#{Object.const_get(name.to_s.classify).collection_path}") do |parsed_data|
38
- @data[name] = Her::Model::ORM.initialize_collection(name, parsed_data[:resource])
39
- end
37
+ self.class.get_collection("#{collection_path}/#{id}/#{Object.const_get(name.to_s.classify).collection_path}")
40
38
  end
41
39
  end # }}}
42
40
 
@@ -50,6 +48,8 @@ module Her
50
48
  def belongs_to(name, attrs={}) # {{{
51
49
  @her_relationships ||= {}
52
50
  (@her_relationships[:belongs_to] ||= []) << attrs.merge(:name => name)
51
+
52
+ # TODO Write some code here
53
53
  end # }}}
54
54
  end
55
55
  end
data/lib/her/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Her
2
- VERSION = "0.1"
2
+ VERSION = "0.1.1"
3
3
  end
data/spec/model_spec.rb CHANGED
@@ -3,7 +3,7 @@ require File.join(File.dirname(__FILE__), "spec_helper.rb")
3
3
 
4
4
  describe Her::Model do
5
5
  describe Her::Model::HTTP do
6
- context "binding a model with an API" do # {{{
6
+ context "binding a model with an API" do
7
7
  it "binds a model to an instance of Her::API" do # {{{
8
8
  @api = Her::API.new
9
9
  @api.setup :base_uri => "https://api.example.com"
@@ -78,7 +78,7 @@ describe Her::Model do
78
78
  @her_api.base_uri.should == "https://api2.example.com"
79
79
  end
80
80
  end # }}}
81
- end # }}}
81
+ end
82
82
 
83
83
  context "making HTTP requests" do
84
84
  before do # {{{
@@ -86,8 +86,11 @@ describe Her::Model do
86
86
  @api.setup :base_uri => "https://api.example.com"
87
87
  FakeWeb.register_uri(:get, "https://api.example.com/users", :body => { :data => [{ :id => 1 }] }.to_json)
88
88
  FakeWeb.register_uri(:get, "https://api.example.com/users?page=2", :body => { :data => [{ :id => 2 }] }.to_json)
89
+ FakeWeb.register_uri(:get, "https://api.example.com/users/popular", :body => { :data => [{ :id => 1 }, { :id => 2 }] }.to_json)
90
+ FakeWeb.register_uri(:get, "https://api.example.com/users/1", :body => { :data => { :id => 1 } }.to_json)
89
91
  FakeWeb.register_uri(:post, "https://api.example.com/users", :body => { :data => [{ :id => 3 }] }.to_json)
90
92
  FakeWeb.register_uri(:put, "https://api.example.com/users/4", :body => { :data => [{ :id => 4 }] }.to_json)
93
+ FakeWeb.register_uri(:patch, "https://api.example.com/users/6", :body => { :data => [{ :id => 6 }] }.to_json)
91
94
  FakeWeb.register_uri(:delete, "https://api.example.com/users/5", :body => { :data => [{ :id => 5 }] }.to_json)
92
95
 
93
96
  Object.instance_eval { remove_const :User } if Object.const_defined?(:User)
@@ -97,40 +100,57 @@ describe Her::Model do
97
100
  User.uses_api @api
98
101
  end # }}}
99
102
 
100
- it "handle GET" do # {{{
101
- User.get("/users") do |parsed_data|
102
- parsed_data[:resource].should == [{ :id => 1 }]
103
+ it "handle raw GET" do # {{{
104
+ User.get_raw("/users") do |parsed_data|
105
+ parsed_data[:data].should == [{ :id => 1 }]
106
+ end
107
+ end # }}}
108
+
109
+ it "handle raw POST" do # {{{
110
+ User.post_raw("/users") do |parsed_data|
111
+ parsed_data[:data].should == [{ :id => 3 }]
103
112
  end
104
113
  end # }}}
105
114
 
106
- it "handle POST" do # {{{
107
- User.post("/users") do |parsed_data|
108
- parsed_data[:resource].should == [{ :id => 3 }]
115
+ it "handle raw PUT" do # {{{
116
+ User.put_raw("/users/4") do |parsed_data|
117
+ parsed_data[:data].should == [{ :id => 4 }]
109
118
  end
110
119
  end # }}}
111
120
 
112
- it "handle PUT" do # {{{
113
- User.put("/users/4") do |parsed_data|
114
- parsed_data[:resource].should == [{ :id => 4 }]
121
+ it "handle raw PATCH" do # {{{
122
+ User.patch_raw("/users/6") do |parsed_data|
123
+ parsed_data[:data].should == [{ :id => 6 }]
115
124
  end
116
125
  end # }}}
117
126
 
118
- it "handle DELETE" do # {{{
119
- User.delete("/users/5") do |parsed_data|
120
- parsed_data[:resource].should == [{ :id => 5 }]
127
+ it "handle raw DELETE" do # {{{
128
+ User.delete_raw("/users/5") do |parsed_data|
129
+ parsed_data[:data].should == [{ :id => 5 }]
121
130
  end
122
131
  end # }}}
123
132
 
124
133
  it "handle querystring parameters" do # {{{
125
- User.get("/users", :page => 2) do |parsed_data|
126
- parsed_data[:resource].should == [{ :id => 2 }]
134
+ User.get_raw("/users", :page => 2) do |parsed_data|
135
+ parsed_data[:data].should == [{ :id => 2 }]
127
136
  end
128
137
  end # }}}
138
+
139
+ it "handle GET collection" do # {{{
140
+ @users = User.get_collection("/users/popular")
141
+ @users.length.should == 2
142
+ @users.first.id.should == 1
143
+ end # }}}
144
+
145
+ it "handle GET resource" do # {{{
146
+ @user = User.get_resource("/users/1")
147
+ @user.id.should == 1
148
+ end # }}}
129
149
  end
130
150
  end
131
151
 
132
152
  describe Her::Model::ORM do
133
- context "mapping data to Ruby objects" do # {{{
153
+ context "mapping data to Ruby objects" do
134
154
  before do # {{{
135
155
  @api = Her::API.new
136
156
  @api.setup :base_uri => "https://api.example.com"
@@ -155,13 +175,17 @@ describe Her::Model do
155
175
  @users.length.should == 2
156
176
  @users.first.name.should == "Tobias Fünke"
157
177
  end # }}}
158
- end # }}}
178
+ end
159
179
 
160
180
  context "creating resources" do
161
181
  before do # {{{
162
- @api = Her::API.new
163
- @api.setup :base_uri => "https://api.example.com"
182
+ Her::API.setup :base_uri => "https://api.example.com"
164
183
  FakeWeb.register_uri(:post, "https://api.example.com/users", :body => { :data => { :id => 1, :fullname => "Tobias Fünke" } }.to_json)
184
+
185
+ Object.instance_eval { remove_const :User } if Object.const_defined?(:User)
186
+ class User
187
+ include Her::Model
188
+ end
165
189
  end # }}}
166
190
 
167
191
  it "handle one-line resource creation" do # {{{
@@ -169,11 +193,45 @@ describe Her::Model do
169
193
  @user.id.should == 1
170
194
  @user.fullname.should == "Tobias Fünke"
171
195
  end # }}}
196
+
197
+ it "handle resource creation through Model.new + #save" do # {{{
198
+ @user = User.new(:fullname => "Tobias Fünke")
199
+ @user.save
200
+ @user.fullname.should == "Tobias Fünke"
201
+ end # }}}
202
+ end
203
+
204
+ context "updating resources" do
205
+ before do # {{{
206
+ @api = Her::API.new
207
+ @api.setup :base_uri => "https://api.example.com"
208
+ FakeWeb.register_uri(:get, "https://api.example.com/users/1", :body => { :data => { :id => 1, :fullname => "Tobias Fünke" } }.to_json)
209
+ FakeWeb.register_uri(:put, "https://api.example.com/users/1", :body => { :data => { :id => 1, :fullname => "Lindsay Fünke" } }.to_json)
210
+
211
+ Object.instance_eval { remove_const :User } if Object.const_defined?(:User)
212
+ class User
213
+ include Her::Model
214
+ end
215
+ end # }}}
216
+
217
+ it "handle resource data update without saving it" do
218
+ @user = User.find(1)
219
+ @user.fullname.should == "Tobias Fünke"
220
+ @user.fullname = "Kittie Sanchez"
221
+ @user.fullname.should == "Kittie Sanchez"
222
+ end
223
+
224
+ it "handle resource update through #save on an existing resource" do # {{{
225
+ @user = User.find(1)
226
+ @user.fullname = "Lindsay Fünke"
227
+ @user.save
228
+ @user.fullname.should == "Lindsay Fünke"
229
+ end # }}}
172
230
  end
173
231
  end
174
232
 
175
233
  describe Her::Model::Relationships do
176
- context "setting relationships" do # {{{
234
+ context "setting relationships" do
177
235
  before do # {{{
178
236
  Object.instance_eval { remove_const :User } if Object.const_defined?(:User)
179
237
  class User
@@ -202,9 +260,9 @@ describe Her::Model do
202
260
  User.belongs_to :family
203
261
  User.relationships[:belongs_to].should == [{ :name => :organization }, { :name => :family }]
204
262
  end # }}}
205
- end # }}}
263
+ end
206
264
 
207
- context "handling relationships" do # {{{
265
+ context "handling relationships" do
208
266
  before do # {{{
209
267
  Her::API.setup :base_uri => "https://api.example.com"
210
268
  FakeWeb.register_uri(:get, "https://api.example.com/users/1", :body => { :data => { :id => 1, :name => "Tobias Fünke", :comments => [{ :id => 2, :body => "Tobias, you blow hard!" }, { :id => 3, :body => "I wouldn't mind kissing that man between the cheeks, so to speak" }] } }.to_json)
@@ -236,7 +294,6 @@ describe Her::Model do
236
294
  @user.comments.first.id.should == 4
237
295
  @user.comments.first.body.should == "They're having a FIRESALE?"
238
296
  end # }}}
239
-
240
- end # }}}
297
+ end
241
298
  end
242
299
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: her
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: 0.1.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -193,7 +193,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
193
193
  version: '0'
194
194
  segments:
195
195
  - 0
196
- hash: -4283356893528248236
196
+ hash: -3007322587400233058
197
197
  required_rubygems_version: !ruby/object:Gem::Requirement
198
198
  none: false
199
199
  requirements:
@@ -202,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
202
202
  version: '0'
203
203
  segments:
204
204
  - 0
205
- hash: -4283356893528248236
205
+ hash: -3007322587400233058
206
206
  requirements: []
207
207
  rubyforge_project:
208
208
  rubygems_version: 1.8.18