her 0.0.1 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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