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