her 0.0.1 → 0.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.
@@ -0,0 +1,2 @@
1
+ rvm: 1.9.2
2
+ script: "bundle exec rake spec"
data/README.md CHANGED
@@ -8,29 +8,55 @@ Her is an ORM (Object Relational Mapper) that maps REST resources to Ruby object
8
8
 
9
9
  In your Gemfile, add:
10
10
 
11
- gem "her"
11
+ ```ruby
12
+ gem "her"
13
+ ```
14
+
15
+ That’s it!
12
16
 
13
17
  ## Usage
14
18
 
15
- To add the ORM to a class, you just have to include `Her::Model` in it and define which API you want it to be bound to. For example, with Rails, you would define a `config/initializers/her.rb` file with this:
19
+ First, you have to define which API your models will be bound to. For example, with Rails, you would create a new `config/initializers/her.rb` file with this line:
16
20
 
17
21
  ```ruby
18
- $my_api = Her::API.new
19
- $my_api.setup :base_uri => "https://api.example.com"
22
+ Her::API.setup :base_uri => "https://api.example.com"
20
23
  ```
21
24
 
22
- And then, for each of your models:
25
+ And then to add the ORM behavior to a class, you just have to include `Her::Model` in it:
23
26
 
24
27
  ```ruby
25
28
  class User
26
29
  include Her::Model
27
- uses_api $my_api
28
30
  end
29
31
  ```
30
32
 
31
33
  After that, using Her is very similar to many ActiveModel-like ORMs:
32
34
 
33
35
  ```ruby
34
- User.all # => Fetches "https://api.example.com/users" and return an array of User objects
35
- User.find(1) # => Fetches "https://api.example.com/users/1" and return a User object
36
+ User.all # => Fetches "https://api.example.com/users" and return an array of User objects
37
+ User.find(1) # => Fetches "https://api.example.com/users/1" and return a User object
38
+ ```
39
+
40
+ ## Relationships
41
+
42
+ ```ruby
43
+ class User
44
+ include Her::Model
45
+ has_many :comments
46
+ end
47
+
48
+ class Comment
49
+ include Her::Model
50
+ end
51
+
52
+ user = User.find(1)
53
+ user.comments # => [#<Comment id=1>, #<Comment id=2>]
36
54
  ```
55
+
56
+ ## Custom requests
57
+
58
+ TBD.
59
+
60
+ ## Hooks
61
+
62
+ TBD.
@@ -9,6 +9,17 @@ module Her
9
9
  # @private
10
10
  attr_reader :base_uri, :parse_with
11
11
 
12
+ # Setup a default API connection
13
+ def self.setup(attrs={}) # {{{
14
+ @@default_api = new
15
+ @@default_api.setup(attrs)
16
+ end # }}}
17
+
18
+ # @private
19
+ def self.default_api(attrs={}) # {{{
20
+ defined?(@@default_api) ? @@default_api : nil
21
+ end # }}}
22
+
12
23
  # Setup the API connection
13
24
  def setup(attrs={}) # {{{
14
25
  @base_uri = attrs[:base_uri]
@@ -21,13 +32,13 @@ module Her
21
32
  }
22
33
  end
23
34
  @connection = Faraday.new(:url => @base_uri) do |builder|
24
- builder.request :url_encoded
25
- builder.adapter :net_http
35
+ builder.request :url_encoded
36
+ builder.adapter :net_http
26
37
  end
27
38
  end # }}}
28
39
 
29
40
  # Define a custom parsing procedure. The procedure is passed the response object and is
30
- # expected to return hash with three keys: a main resource Hash, an errors Array
41
+ # expected to return a hash with three keys: a main resource Hash, an errors Array
31
42
  # and a metadata Hash.
32
43
  #
33
44
  # @example
@@ -47,8 +58,18 @@ module Her
47
58
 
48
59
  # Make an HTTP request to the API
49
60
  def request(attrs={}) # {{{
50
- # TODO Here, we would probably look for hooks that modify the request before calling the API
51
- @connection.send(attrs[:method], attrs[:path])
61
+ method = attrs.delete(:_method)
62
+ path = attrs.delete(:_path)
63
+ @connection.send method do |request|
64
+ if method == :get
65
+ # For GET requests, treat additional parameters as querystring data
66
+ request.url path, attrs
67
+ else
68
+ # For POST, PUT and DELETE requests, treat additional parameters as request body
69
+ request.url path
70
+ request.body = attrs
71
+ end
72
+ end
52
73
  end # }}}
53
74
 
54
75
  # Parse the HTTP response
@@ -22,11 +22,14 @@ module Her
22
22
 
23
23
  # Class methods
24
24
  included do
25
- @her_collection_path = "#{self.to_s.downcase.pluralize}"
26
25
  extend Her::Model::Base
27
26
  extend Her::Model::HTTP
28
27
  extend Her::Model::ORM
29
28
  extend Her::Model::Relationships
29
+
30
+ # Define default settings
31
+ collection_path "#{self.to_s.downcase.pluralize}"
32
+ uses_api Her::API.default_api
30
33
  end
31
34
  end
32
35
  end
@@ -27,12 +27,36 @@ module Her
27
27
  yield @her_api.parse(response)
28
28
  end # }}}
29
29
 
30
- # Make a GET request and return the parsed JSON response
30
+ # Make a GET request and return the parsed JSON response (not mapped to objects)
31
31
  #
32
32
  # @example
33
- # User.get "/users/foo"
33
+ # User.get "/users/1"
34
34
  def get(path, attrs={}, &block) # {{{
35
- request(attrs.merge(:method => :get, :path => path), &block)
35
+ request(attrs.merge(:_method => :get, :_path => path), &block)
36
+ end # }}}
37
+
38
+ # 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) # {{{
43
+ request(attrs.merge(:_method => :post, :_path => path), &block)
44
+ end # }}}
45
+
46
+ # 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) # {{{
51
+ request(attrs.merge(:_method => :put, :_path => path), &block)
52
+ end # }}}
53
+
54
+ # 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) # {{{
59
+ request(attrs.merge(:_method => :delete, :_path => path), &block)
36
60
  end # }}}
37
61
  end
38
62
  end
@@ -23,18 +23,25 @@ module Her
23
23
  end # }}}
24
24
 
25
25
  # Fetch a specific resource based on an ID
26
- def find(id) # {{{
27
- request(:method => :get, :path => "#{@her_collection_path}/#{id}") do |parsed_data|
26
+ def find(id, params={}) # {{{
27
+ request(params.merge(:_method => :get, :_path => "#{@her_collection_path}/#{id}")) do |parsed_data|
28
28
  new(parsed_data[:resource])
29
29
  end
30
30
  end # }}}
31
31
 
32
32
  # Fetch a collection of resources
33
33
  def all(params={}) # {{{
34
- request(:method => :get, :path => "#{@her_collection_path}") do |parsed_data|
34
+ request(params.merge(:_method => :get, :_path => "#{@her_collection_path}")) do |parsed_data|
35
35
  Her::Model::ORM.initialize_collection(to_s.downcase.pluralize, parsed_data[:resource])
36
36
  end
37
37
  end # }}}
38
+
39
+ # Create a resource
40
+ def create(params={}) # {{{
41
+ request(params.merge(:_method => :post, :_path => "#{@her_collection_path}")) do |parsed_data|
42
+ new(parsed_data[:resource])
43
+ end
44
+ end # }}}
38
45
  end
39
46
  end
40
47
  end
@@ -1,3 +1,3 @@
1
1
  module Her
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1"
3
3
  end
@@ -3,6 +3,13 @@ require File.join(File.dirname(__FILE__), "spec_helper.rb")
3
3
 
4
4
  describe Her::API do
5
5
  context "initialization" do
6
+ describe ".setup" do
7
+ it "creates a default connection" do # {{{
8
+ Her::API.setup :base_uri => "https://api.example.com"
9
+ Her::API.default_api.base_uri.should == "https://api.example.com"
10
+ end # }}}
11
+ end
12
+
6
13
  describe "#setup" do
7
14
  it "sets a base URI" do # {{{
8
15
  @api = Her::API.new
@@ -28,7 +35,7 @@ describe Her::API do
28
35
  end # }}}
29
36
 
30
37
  it "makes HTTP requests" do # {{{
31
- response = @api.request(:method => :get, :path => "/foo")
38
+ response = @api.request(:_method => :get, :_path => "/foo")
32
39
  response.body.should == "Foo, it is."
33
40
  end # }}}
34
41
  end
@@ -41,7 +48,7 @@ describe Her::API do
41
48
  end # }}}
42
49
 
43
50
  it "parses a request" do # {{{
44
- @api.parse @api.request(:method => :get, :path => "users/1") do |parsed_data|
51
+ @api.parse @api.request(:_method => :get, :_path => "users/1") do |parsed_data|
45
52
  parsed_data[:resource].should == { :id => 1, :name => "George Michael Bluth" }
46
53
  parsed_data[:errors].should == ["This is a single error"]
47
54
  parsed_data[:metadata].should == { :page => 1, :per_page => 10 }
@@ -2,6 +2,133 @@
2
2
  require File.join(File.dirname(__FILE__), "spec_helper.rb")
3
3
 
4
4
  describe Her::Model do
5
+ describe Her::Model::HTTP do
6
+ context "binding a model with an API" do # {{{
7
+ it "binds a model to an instance of Her::API" do # {{{
8
+ @api = Her::API.new
9
+ @api.setup :base_uri => "https://api.example.com"
10
+
11
+ Object.instance_eval { remove_const :User } if Object.const_defined?(:User)
12
+ class User
13
+ include Her::Model
14
+ end
15
+ User.uses_api @api
16
+
17
+ User.class_eval do
18
+ @her_api.should_not == nil
19
+ @her_api.base_uri.should == "https://api.example.com"
20
+ end
21
+ end # }}}
22
+
23
+ it "binds a model directly to Her::API" do # {{{
24
+ Her::API.setup :base_uri => "https://api.example.com"
25
+
26
+ Object.instance_eval { remove_const :User } if Object.const_defined?(:User)
27
+ class User
28
+ include Her::Model
29
+ end
30
+
31
+ User.class_eval do
32
+ @her_api.should_not == nil
33
+ @her_api.base_uri.should == "https://api.example.com"
34
+ end
35
+ end # }}}
36
+
37
+ it "binds two models to two different instances of Her::API" do # {{{
38
+ @api1 = Her::API.new
39
+ @api1.setup :base_uri => "https://api1.example.com"
40
+
41
+ Object.instance_eval { remove_const :User } if Object.const_defined?(:User)
42
+ class User; include Her::Model; end
43
+ User.uses_api @api1
44
+
45
+ User.class_eval do
46
+ @her_api.base_uri.should == "https://api1.example.com"
47
+ end
48
+
49
+ @api2 = Her::API.new
50
+ @api2.setup :base_uri => "https://api2.example.com"
51
+
52
+ Object.instance_eval { remove_const :Comment } if Object.const_defined?(:Comment)
53
+ class Comment; include Her::Model; end
54
+ Comment.uses_api @api2
55
+
56
+ Comment.class_eval do
57
+ @her_api.base_uri.should == "https://api2.example.com"
58
+ end
59
+ end # }}}
60
+
61
+ it "binds one model to Her::API and another one to an instance of Her::API" do # {{{
62
+ Her::API.setup :base_uri => "https://api1.example.com"
63
+ Object.instance_eval { remove_const :User } if Object.const_defined?(:User)
64
+ class User; include Her::Model; end
65
+
66
+ User.class_eval do
67
+ @her_api.base_uri.should == "https://api1.example.com"
68
+ end
69
+
70
+ @api = Her::API.new
71
+ @api.setup :base_uri => "https://api2.example.com"
72
+
73
+ Object.instance_eval { remove_const :Comment } if Object.const_defined?(:Comment)
74
+ class Comment; include Her::Model; end
75
+ Comment.uses_api @api
76
+
77
+ Comment.class_eval do
78
+ @her_api.base_uri.should == "https://api2.example.com"
79
+ end
80
+ end # }}}
81
+ end # }}}
82
+
83
+ context "making HTTP requests" do
84
+ before do # {{{
85
+ @api = Her::API.new
86
+ @api.setup :base_uri => "https://api.example.com"
87
+ FakeWeb.register_uri(:get, "https://api.example.com/users", :body => { :data => [{ :id => 1 }] }.to_json)
88
+ FakeWeb.register_uri(:get, "https://api.example.com/users?page=2", :body => { :data => [{ :id => 2 }] }.to_json)
89
+ FakeWeb.register_uri(:post, "https://api.example.com/users", :body => { :data => [{ :id => 3 }] }.to_json)
90
+ FakeWeb.register_uri(:put, "https://api.example.com/users/4", :body => { :data => [{ :id => 4 }] }.to_json)
91
+ FakeWeb.register_uri(:delete, "https://api.example.com/users/5", :body => { :data => [{ :id => 5 }] }.to_json)
92
+
93
+ Object.instance_eval { remove_const :User } if Object.const_defined?(:User)
94
+ class User
95
+ include Her::Model
96
+ end
97
+ User.uses_api @api
98
+ end # }}}
99
+
100
+ it "handle GET" do # {{{
101
+ User.get("/users") do |parsed_data|
102
+ parsed_data[:resource].should == [{ :id => 1 }]
103
+ end
104
+ end # }}}
105
+
106
+ it "handle POST" do # {{{
107
+ User.post("/users") do |parsed_data|
108
+ parsed_data[:resource].should == [{ :id => 3 }]
109
+ end
110
+ end # }}}
111
+
112
+ it "handle PUT" do # {{{
113
+ User.put("/users/4") do |parsed_data|
114
+ parsed_data[:resource].should == [{ :id => 4 }]
115
+ end
116
+ end # }}}
117
+
118
+ it "handle DELETE" do # {{{
119
+ User.delete("/users/5") do |parsed_data|
120
+ parsed_data[:resource].should == [{ :id => 5 }]
121
+ end
122
+ end # }}}
123
+
124
+ it "handle querystring parameters" do # {{{
125
+ User.get("/users", :page => 2) do |parsed_data|
126
+ parsed_data[:resource].should == [{ :id => 2 }]
127
+ end
128
+ end # }}}
129
+ end
130
+ end
131
+
5
132
  describe Her::Model::ORM do
6
133
  context "mapping data to Ruby objects" do # {{{
7
134
  before do # {{{
@@ -29,10 +156,24 @@ describe Her::Model do
29
156
  @users.first.name.should == "Tobias Fünke"
30
157
  end # }}}
31
158
  end # }}}
159
+
160
+ context "creating resources" do
161
+ before do # {{{
162
+ @api = Her::API.new
163
+ @api.setup :base_uri => "https://api.example.com"
164
+ FakeWeb.register_uri(:post, "https://api.example.com/users", :body => { :data => { :id => 1, :fullname => "Tobias Fünke" } }.to_json)
165
+ end # }}}
166
+
167
+ it "handle one-line resource creation" do # {{{
168
+ @user = User.create(:fullname => "Tobias Fünke")
169
+ @user.id.should == 1
170
+ @user.fullname.should == "Tobias Fünke"
171
+ end # }}}
172
+ end
32
173
  end
33
174
 
34
175
  describe Her::Model::Relationships do
35
- context "setting associations" do # {{{
176
+ context "setting relationships" do # {{{
36
177
  before do # {{{
37
178
  Object.instance_eval { remove_const :User } if Object.const_defined?(:User)
38
179
  class User
@@ -40,27 +181,62 @@ describe Her::Model do
40
181
  end
41
182
  end # }}}
42
183
 
43
- it "handles a single 'has_many' association" do # {{{
184
+ it "handles a single 'has_many' relationship" do # {{{
44
185
  User.has_many :comments
45
186
  User.relationships[:has_many].should == [{ :name => :comments }]
46
187
  end # }}}
47
188
 
48
- it "handles multiples 'has_many' associations" do # {{{
189
+ it "handles multiples 'has_many' relationship" do # {{{
49
190
  User.has_many :comments
50
191
  User.has_many :posts
51
192
  User.relationships[:has_many].should == [{ :name => :comments }, { :name => :posts }]
52
193
  end # }}}
53
194
 
54
- it "handles a single belongs_to association" do # {{{
195
+ it "handles a single belongs_to relationship" do # {{{
55
196
  User.belongs_to :organization
56
197
  User.relationships[:belongs_to].should == [{ :name => :organization }]
57
198
  end # }}}
58
199
 
59
- it "handles multiples 'belongs_to' associations" do # {{{
200
+ it "handles multiples 'belongs_to' relationship" do # {{{
60
201
  User.belongs_to :organization
61
202
  User.belongs_to :family
62
203
  User.relationships[:belongs_to].should == [{ :name => :organization }, { :name => :family }]
63
204
  end # }}}
64
205
  end # }}}
206
+
207
+ context "handling relationships" do # {{{
208
+ before do # {{{
209
+ Her::API.setup :base_uri => "https://api.example.com"
210
+ 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)
211
+ FakeWeb.register_uri(:get, "https://api.example.com/users/2", :body => { :data => { :id => 2, :name => "Lindsay Fünke" } }.to_json)
212
+ FakeWeb.register_uri(:get, "https://api.example.com/users/2/comments", :body => { :data => [{ :id => 4, :body => "They're having a FIRESALE?" }, { :id => 5, :body => "Is this the tiny town from Footloose?" }] }.to_json)
213
+
214
+ Object.instance_eval { remove_const :User } if Object.const_defined?(:User)
215
+ class User
216
+ include Her::Model
217
+ has_many :comments
218
+ end
219
+
220
+ Object.instance_eval { remove_const :Comment } if Object.const_defined?(:Comment)
221
+ class Comment
222
+ include Her::Model
223
+ end
224
+ end # }}}
225
+
226
+ it "maps an array of included data" do # {{{
227
+ @user = User.find(1)
228
+ @user.comments.length.should == 2
229
+ @user.comments.first.id.should == 2
230
+ @user.comments.first.body.should == "Tobias, you blow hard!"
231
+ end # }}}
232
+
233
+ it "fetches data that was not included" do # {{{
234
+ @user = User.find(2)
235
+ @user.comments.length.should == 2
236
+ @user.comments.first.id.should == 4
237
+ @user.comments.first.body.should == "They're having a FIRESALE?"
238
+ end # }}}
239
+
240
+ end # }}}
65
241
  end
66
242
  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.0.1
4
+ version: '0.1'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -163,6 +163,7 @@ extensions: []
163
163
  extra_rdoc_files: []
164
164
  files:
165
165
  - .gitignore
166
+ - .travis.yml
166
167
  - Gemfile
167
168
  - README.md
168
169
  - Rakefile
@@ -192,7 +193,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
192
193
  version: '0'
193
194
  segments:
194
195
  - 0
195
- hash: 1076475942848490039
196
+ hash: -4283356893528248236
196
197
  required_rubygems_version: !ruby/object:Gem::Requirement
197
198
  none: false
198
199
  requirements:
@@ -201,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
201
202
  version: '0'
202
203
  segments:
203
204
  - 0
204
- hash: 1076475942848490039
205
+ hash: -4283356893528248236
205
206
  requirements: []
206
207
  rubyforge_project:
207
208
  rubygems_version: 1.8.18