her 0.0.1 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +2 -0
- data/README.md +34 -8
- data/lib/her/api.rb +26 -5
- data/lib/her/model.rb +4 -1
- data/lib/her/model/http.rb +27 -3
- data/lib/her/model/orm.rb +10 -3
- data/lib/her/version.rb +1 -1
- data/spec/api_spec.rb +9 -2
- data/spec/model_spec.rb +181 -5
- metadata +4 -3
data/.travis.yml
ADDED
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
|
-
|
11
|
+
```ruby
|
12
|
+
gem "her"
|
13
|
+
```
|
14
|
+
|
15
|
+
That’s it!
|
12
16
|
|
13
17
|
## Usage
|
14
18
|
|
15
|
-
|
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
|
-
|
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,
|
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
|
35
|
-
User.find(1)
|
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.
|
data/lib/her/api.rb
CHANGED
@@ -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
|
25
|
-
builder.adapter
|
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
|
-
|
51
|
-
|
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
|
data/lib/her/model.rb
CHANGED
@@ -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
|
data/lib/her/model/http.rb
CHANGED
@@ -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/
|
33
|
+
# User.get "/users/1"
|
34
34
|
def get(path, attrs={}, &block) # {{{
|
35
|
-
request(attrs.merge(:
|
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
|
data/lib/her/model/orm.rb
CHANGED
@@ -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(:
|
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(:
|
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
|
data/lib/her/version.rb
CHANGED
data/spec/api_spec.rb
CHANGED
@@ -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(:
|
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(:
|
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 }
|
data/spec/model_spec.rb
CHANGED
@@ -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
|
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'
|
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'
|
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
|
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'
|
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.
|
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:
|
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:
|
205
|
+
hash: -4283356893528248236
|
205
206
|
requirements: []
|
206
207
|
rubyforge_project:
|
207
208
|
rubygems_version: 1.8.18
|