her 0.1 → 0.1.1

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/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