herr 0.7.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.travis.yml +15 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +10 -0
- data/LICENSE +7 -0
- data/README.md +990 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +81 -0
- data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
- data/gemfiles/Gemfile.activemodel-4.0 +7 -0
- data/gemfiles/Gemfile.activemodel-4.1 +7 -0
- data/gemfiles/Gemfile.activemodel-4.2 +7 -0
- data/her.gemspec +30 -0
- data/lib/her.rb +16 -0
- data/lib/her/api.rb +115 -0
- data/lib/her/collection.rb +12 -0
- data/lib/her/errors.rb +27 -0
- data/lib/her/middleware.rb +10 -0
- data/lib/her/middleware/accept_json.rb +17 -0
- data/lib/her/middleware/first_level_parse_json.rb +36 -0
- data/lib/her/middleware/parse_json.rb +21 -0
- data/lib/her/middleware/second_level_parse_json.rb +36 -0
- data/lib/her/model.rb +72 -0
- data/lib/her/model/associations.rb +141 -0
- data/lib/her/model/associations/association.rb +103 -0
- data/lib/her/model/associations/association_proxy.rb +46 -0
- data/lib/her/model/associations/belongs_to_association.rb +96 -0
- data/lib/her/model/associations/has_many_association.rb +100 -0
- data/lib/her/model/associations/has_one_association.rb +79 -0
- data/lib/her/model/attributes.rb +266 -0
- data/lib/her/model/base.rb +33 -0
- data/lib/her/model/deprecated_methods.rb +61 -0
- data/lib/her/model/http.rb +114 -0
- data/lib/her/model/introspection.rb +65 -0
- data/lib/her/model/nested_attributes.rb +45 -0
- data/lib/her/model/orm.rb +205 -0
- data/lib/her/model/parse.rb +227 -0
- data/lib/her/model/paths.rb +121 -0
- data/lib/her/model/relation.rb +164 -0
- data/lib/her/version.rb +3 -0
- data/spec/api_spec.rb +131 -0
- data/spec/collection_spec.rb +26 -0
- data/spec/middleware/accept_json_spec.rb +10 -0
- data/spec/middleware/first_level_parse_json_spec.rb +62 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations_spec.rb +416 -0
- data/spec/model/attributes_spec.rb +268 -0
- data/spec/model/callbacks_spec.rb +145 -0
- data/spec/model/dirty_spec.rb +86 -0
- data/spec/model/http_spec.rb +194 -0
- data/spec/model/introspection_spec.rb +76 -0
- data/spec/model/nested_attributes_spec.rb +134 -0
- data/spec/model/orm_spec.rb +479 -0
- data/spec/model/parse_spec.rb +373 -0
- data/spec/model/paths_spec.rb +341 -0
- data/spec/model/relation_spec.rb +226 -0
- data/spec/model/validations_spec.rb +42 -0
- data/spec/model_spec.rb +31 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/extensions/array.rb +5 -0
- data/spec/support/extensions/hash.rb +5 -0
- data/spec/support/macros/her_macros.rb +17 -0
- data/spec/support/macros/model_macros.rb +29 -0
- data/spec/support/macros/request_macros.rb +27 -0
- metadata +280 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 895649b69ac63c97d89e642b1e0ff7f4db53af6e
|
4
|
+
data.tar.gz: c62273356117a58566e4276e85f9f30a696dc650
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ca95a2041c09b94d2aec3809de44e13ded4134cb6c89e045fca79c793828576386454044781a6a20d4ebd19403f94b5d164b294ca7eb8cb6369ff7b732d54f13
|
7
|
+
data.tar.gz: 9a2260095bda92e11ad1c3866d75f46a7b50df62d28662248f4cf11ac7798f67079e9ad1543d3e04b6e8d279b171e950426ec6392eb16cc126ca7ce9f9161593
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour --format=Fivemat
|
data/.travis.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
language: ruby
|
2
|
+
|
3
|
+
sudo: false
|
4
|
+
|
5
|
+
rvm:
|
6
|
+
- 2.0.0
|
7
|
+
- 1.9.3
|
8
|
+
|
9
|
+
gemfile:
|
10
|
+
- gemfiles/Gemfile.activemodel-4.2
|
11
|
+
- gemfiles/Gemfile.activemodel-4.1
|
12
|
+
- gemfiles/Gemfile.activemodel-4.0
|
13
|
+
- gemfiles/Gemfile.activemodel-3.2.x
|
14
|
+
|
15
|
+
script: "echo 'COME ON!' && bundle exec rake spec"
|
data/.yardopts
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# How to contribute
|
2
|
+
|
3
|
+
_(This file is heavily based on [factory\_girl\_rails](https://github.com/thoughtbot/factory_girl_rails/blob/master/CONTRIBUTING.md)’s Contribution Guide)_
|
4
|
+
|
5
|
+
We love pull requests. Here’s a quick guide:
|
6
|
+
|
7
|
+
* Fork the repository.
|
8
|
+
* Run `rake spec` (to make sure you start with a clean slate).
|
9
|
+
* Implement your feature or fix.
|
10
|
+
* Add examples that describe it (in the `spec` directory). Only refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, we need examples!
|
11
|
+
* Make sure `rake spec` passes after your modifications.
|
12
|
+
* Commit (bonus points for doing it in a `feature-*` branch).
|
13
|
+
* Push to your fork and send your pull request!
|
14
|
+
|
15
|
+
If we have not replied to your pull request in three or four days, do not hesitate to post another comment in it — yes, we can be lazy sometimes.
|
16
|
+
|
17
|
+
## Syntax Guide
|
18
|
+
|
19
|
+
Do not hesitate to submit patches that fix syntax issues. Some may have slipped under our nose.
|
20
|
+
|
21
|
+
* Two spaces, no tabs (but you already knew that, right?).
|
22
|
+
* No trailing whitespace. Blank lines should not have any space. There are few things we **hate** more than trailing whitespace. Seriously.
|
23
|
+
* `MyClass.my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`.
|
24
|
+
* `[:foo, :bar]` and not `[ :foo, :bar ]`, `{ :foo => :bar }` and not `{:foo => :bar}`
|
25
|
+
* `a = b` and not `a=b`.
|
26
|
+
* Follow the conventions you see used in the source already.
|
data/Gemfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
gemspec
|
3
|
+
|
4
|
+
if RbConfig::CONFIG['RUBY_PROGRAM_VERSION'] && RbConfig::CONFIG['RUBY_PROGRAM_VERSION'] >= '1.9.3'
|
5
|
+
gem 'activemodel', '>= 3.2.0'
|
6
|
+
gem 'activesupport', '>= 3.2.0'
|
7
|
+
else
|
8
|
+
gem 'activemodel', '~> 3.2.0'
|
9
|
+
gem 'activesupport', '~> 3.2.0'
|
10
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2012-2013 Rémi Prévost
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,990 @@
|
|
1
|
+
<p align="center">
|
2
|
+
Herr is an ORM (Object Relational Mapper) that maps REST resources to Ruby objects.<br /> It is designed to build applications that are powered by a RESTful API instead of a database.
|
3
|
+
<br /><br />
|
4
|
+
<a href="https://rubygems.org/gems/herr"><img src="http://img.shields.io/gem/v/her.svg" /></a>
|
5
|
+
<a href="https://codeclimate.com/github/hderms/herr"><img src="http://img.shields.io/codeclimate/github/hderms/herr.svg" /></a>
|
6
|
+
<a href="https://travis-ci.org/hderms/herr"><img src="http://img.shields.io/travis/hderms/herr/master.svg" /></a>
|
7
|
+
</p>
|
8
|
+
|
9
|
+
---
|
10
|
+
## IMPORTANT
|
11
|
+
This is forked from the gem `her`, which was extraordinarily useful, but had not undergone maintenance at a rate sufficient for my needs. Please check [https://github.com/remiprev/her] and see if maintenance has resumed before using this gem.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
In your Gemfile, add:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem "herr"
|
19
|
+
```
|
20
|
+
|
21
|
+
That’s it!
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
_For a complete reference of all the methods you can use, check out [the documentation](http://rdoc.info/github/remiprev/her)._
|
26
|
+
|
27
|
+
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 these lines:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
# config/initializers/her.rb
|
31
|
+
Her::API.setup url: "https://api.example.com" do |c|
|
32
|
+
# Request
|
33
|
+
c.use Faraday::Request::UrlEncoded
|
34
|
+
|
35
|
+
# Response
|
36
|
+
c.use Her::Middleware::DefaultParseJSON
|
37
|
+
|
38
|
+
# Adapter
|
39
|
+
c.use Faraday::Adapter::NetHttp
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
And then to add the ORM behavior to a class, you just have to include `Her::Model` in it:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
class User
|
47
|
+
include Her::Model
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
After that, using Her is very similar to many ActiveRecord-like ORMs:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
User.all
|
55
|
+
# GET "https://api.example.com/users" and return an array of User objects
|
56
|
+
|
57
|
+
User.find(1)
|
58
|
+
# GET "https://api.example.com/users/1" and return a User object
|
59
|
+
|
60
|
+
@user = User.create(fullname: "Tobias Fünke")
|
61
|
+
# POST "https://api.example.com/users" with `fullname=Tobias+Fünke` and return the saved User object
|
62
|
+
|
63
|
+
@user = User.new(fullname: "Tobias Fünke")
|
64
|
+
@user.occupation = "actor"
|
65
|
+
@user.save
|
66
|
+
# POST "https://api.example.com/users" with `fullname=Tobias+Fünke&occupation=actor` and return the saved User object
|
67
|
+
|
68
|
+
@user = User.find(1)
|
69
|
+
@user.fullname = "Lindsay Fünke"
|
70
|
+
@user.save
|
71
|
+
# PUT "https://api.example.com/users/1" with `fullname=Lindsay+Fünke` and return the updated User object
|
72
|
+
```
|
73
|
+
|
74
|
+
### ActiveRecord-like methods
|
75
|
+
|
76
|
+
These are the basic ActiveRecord-like methods you can use with your models:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
class User
|
80
|
+
include Her::Model
|
81
|
+
end
|
82
|
+
|
83
|
+
# Update a fetched resource
|
84
|
+
user = User.find(1)
|
85
|
+
user.fullname = "Lindsay Fünke" # OR user.assign_attributes(fullname: "Lindsay Fünke")
|
86
|
+
user.save # returns false if it fails, errors in user.response_errors array
|
87
|
+
# PUT "/users/1" with `fullname=Lindsay+Fünke`
|
88
|
+
|
89
|
+
# Update a resource without fetching it
|
90
|
+
User.save_existing(1, fullname: "Lindsay Fünke")
|
91
|
+
# PUT "/users/1" with `fullname=Lindsay+Fünke`
|
92
|
+
|
93
|
+
# Destroy a fetched resource
|
94
|
+
user = User.find(1)
|
95
|
+
user.destroy
|
96
|
+
# DELETE "/users/1"
|
97
|
+
|
98
|
+
# Destroy a resource without fetching it
|
99
|
+
User.destroy_existing(1)
|
100
|
+
# DELETE "/users/1"
|
101
|
+
|
102
|
+
# Fetching a collection of resources
|
103
|
+
User.all
|
104
|
+
# GET "/users"
|
105
|
+
User.where(moderator: 1).all
|
106
|
+
# GET "/users?moderator=1"
|
107
|
+
|
108
|
+
# Create a new resource
|
109
|
+
User.create(fullname: "Maeby Fünke")
|
110
|
+
# POST "/users" with `fullname=Maeby+Fünke`
|
111
|
+
|
112
|
+
# Save a new resource
|
113
|
+
user = User.new(fullname: "Maeby Fünke")
|
114
|
+
user.save! # raises Her::Errors::ResourceInvalid if it fails
|
115
|
+
# POST "/users" with `fullname=Maeby+Fünke`
|
116
|
+
```
|
117
|
+
|
118
|
+
You can look into the [`her-example`](https://github.com/remiprev/her-example) repository for a sample application using Her.
|
119
|
+
|
120
|
+
## Middleware
|
121
|
+
|
122
|
+
Since Her relies on [Faraday](https://github.com/lostisland/faraday) to send HTTP requests, you can choose the middleware used to handle requests and responses. Using the block in the `setup` call, you have access to Faraday’s `connection` object and are able to customize the middleware stack used on each request and response.
|
123
|
+
|
124
|
+
### Authentication
|
125
|
+
|
126
|
+
Her doesn’t support authentication by default. However, it’s easy to implement one with request middleware. Using the `setup` block, we can add it to the middleware stack.
|
127
|
+
|
128
|
+
For example, to add a token header to your API requests in a Rails application, you could use the excellent [`request_store`](https://rubygems.org/gems/request_store) gem like this:
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
# app/controllers/application_controller.rb
|
132
|
+
class ApplicationController < ActionController::Base
|
133
|
+
before_filter :set_user_api_token
|
134
|
+
|
135
|
+
protected
|
136
|
+
def set_user_api_token
|
137
|
+
RequestStore.store[:my_api_token] = current_user.api_token # or something similar based on `session`
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# lib/my_token_authentication.rb
|
142
|
+
class MyTokenAuthentication < Faraday::Middleware
|
143
|
+
def call(env)
|
144
|
+
env[:request_headers]["X-API-Token"] = RequestStore.store[:my_api_token]
|
145
|
+
@app.call(env)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# config/initializers/her.rb
|
150
|
+
require "lib/my_token_authentication"
|
151
|
+
|
152
|
+
Her::API.setup url: "https://api.example.com" do |c|
|
153
|
+
# Request
|
154
|
+
c.use MyTokenAuthentication
|
155
|
+
c.use Faraday::Request::UrlEncoded
|
156
|
+
|
157
|
+
# Response
|
158
|
+
c.use Her::Middleware::DefaultParseJSON
|
159
|
+
|
160
|
+
# Adapter
|
161
|
+
c.use Faraday::Adapter::NetHttp
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
Now, each HTTP request made by Her will have the `X-API-Token` header.
|
166
|
+
|
167
|
+
### OAuth
|
168
|
+
|
169
|
+
Using the `faraday_middleware` and `simple_oauth` gems, it’s fairly easy to use OAuth authentication with Her.
|
170
|
+
|
171
|
+
In your Gemfile:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
gem "her"
|
175
|
+
gem "faraday_middleware"
|
176
|
+
gem "simple_oauth"
|
177
|
+
```
|
178
|
+
|
179
|
+
In your Ruby code:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
# Create an application on `https://dev.twitter.com/apps` to set these values
|
183
|
+
TWITTER_CREDENTIALS = {
|
184
|
+
consumer_key: "",
|
185
|
+
consumer_secret: "",
|
186
|
+
token: "",
|
187
|
+
token_secret: ""
|
188
|
+
}
|
189
|
+
|
190
|
+
Her::API.setup url: "https://api.twitter.com/1/" do |c|
|
191
|
+
# Request
|
192
|
+
c.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
|
193
|
+
|
194
|
+
# Response
|
195
|
+
c.use Her::Middleware::DefaultParseJSON
|
196
|
+
|
197
|
+
# Adapter
|
198
|
+
c.use Faraday::Adapter::NetHttp
|
199
|
+
end
|
200
|
+
|
201
|
+
class Tweet
|
202
|
+
include Her::Model
|
203
|
+
end
|
204
|
+
|
205
|
+
@tweets = Tweet.get("/statuses/home_timeline.json")
|
206
|
+
```
|
207
|
+
|
208
|
+
See the *Authentication* middleware section for an example of how to pass different credentials based on the current user.
|
209
|
+
|
210
|
+
### Parsing JSON data
|
211
|
+
|
212
|
+
By default, Her handles JSON data. It expects the resource/collection data to be returned at the first level.
|
213
|
+
|
214
|
+
```javascript
|
215
|
+
// The response of GET /users/1
|
216
|
+
{ "id" : 1, "name" : "Tobias Fünke" }
|
217
|
+
|
218
|
+
// The response of GET /users
|
219
|
+
[{ "id" : 1, "name" : "Tobias Fünke" }]
|
220
|
+
```
|
221
|
+
|
222
|
+
However, if you want Her to be able to parse the data from a single root element (usually based on the model name), you’ll have to use the `parse_root_in_json` method (See the **JSON attributes-wrapping** section).
|
223
|
+
|
224
|
+
Also, you can define your own parsing method using a response middleware. The middleware should set `env[:body]` to a hash with three symbol keys: `:data`, `:errors` and `:metadata`. The following code uses a custom middleware to parse the JSON data:
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
# Expects responses like:
|
228
|
+
#
|
229
|
+
# {
|
230
|
+
# "result": { "id": 1, "name": "Tobias Fünke" },
|
231
|
+
# "errors": []
|
232
|
+
# }
|
233
|
+
#
|
234
|
+
class MyCustomParser < Faraday::Response::Middleware
|
235
|
+
def on_complete(env)
|
236
|
+
json = MultiJson.load(env[:body], symbolize_keys: true)
|
237
|
+
env[:body] = {
|
238
|
+
data: json[:result],
|
239
|
+
errors: json[:errors],
|
240
|
+
metadata: json[:metadata]
|
241
|
+
}
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
Her::API.setup url: "https://api.example.com" do |c|
|
246
|
+
# Response
|
247
|
+
c.use MyCustomParser
|
248
|
+
|
249
|
+
# Adapter
|
250
|
+
c.use Faraday::Adapter::NetHttp
|
251
|
+
end
|
252
|
+
```
|
253
|
+
|
254
|
+
### Caching
|
255
|
+
|
256
|
+
Again, using the `faraday_middleware` and `memcached` gems makes it very easy to cache requests and responses.
|
257
|
+
|
258
|
+
In your Gemfile:
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
gem "her"
|
262
|
+
gem "faraday_middleware"
|
263
|
+
gem "memcached"
|
264
|
+
```
|
265
|
+
|
266
|
+
In your Ruby code:
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
Her::API.setup url: "https://api.example.com" do |c|
|
270
|
+
# Request
|
271
|
+
c.use FaradayMiddleware::Caching, Memcached::Rails.new('127.0.0.1:11211')
|
272
|
+
|
273
|
+
# Response
|
274
|
+
c.use Her::Middleware::DefaultParseJSON
|
275
|
+
|
276
|
+
# Adapter
|
277
|
+
c.use Faraday::Adapter::NetHttp
|
278
|
+
end
|
279
|
+
|
280
|
+
class User
|
281
|
+
include Her::Model
|
282
|
+
end
|
283
|
+
|
284
|
+
@user = User.find(1)
|
285
|
+
# GET "/users/1"
|
286
|
+
|
287
|
+
@user = User.find(1)
|
288
|
+
# This request will be fetched from memcached
|
289
|
+
```
|
290
|
+
|
291
|
+
## Advanced Features
|
292
|
+
|
293
|
+
Here’s a list of several useful features available in Her.
|
294
|
+
|
295
|
+
### Associations
|
296
|
+
|
297
|
+
Examples use this code:
|
298
|
+
|
299
|
+
```ruby
|
300
|
+
class User
|
301
|
+
include Her::Model
|
302
|
+
has_many :comments
|
303
|
+
has_one :role
|
304
|
+
belongs_to :organization
|
305
|
+
end
|
306
|
+
|
307
|
+
class Comment
|
308
|
+
include Her::Model
|
309
|
+
end
|
310
|
+
|
311
|
+
class Role
|
312
|
+
include Her::Model
|
313
|
+
end
|
314
|
+
|
315
|
+
class Organization
|
316
|
+
include Her::Model
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
320
|
+
#### Fetching data
|
321
|
+
|
322
|
+
You can define `has_many`, `has_one` and `belongs_to` associations in your models. The association data is handled in two different ways.
|
323
|
+
|
324
|
+
1. If Her finds association data when parsing a resource, that data will be used to create the associated model objects on the resource.
|
325
|
+
2. If no association data was included when parsing a resource, calling a method with the same name as the association will fetch the data (providing there’s an HTTP request available for it in the API).
|
326
|
+
|
327
|
+
For example, if there’s association data in the resource, no extra HTTP request is made when calling the `#comments` method and an array of resources is returned:
|
328
|
+
|
329
|
+
```ruby
|
330
|
+
@user = User.find(1)
|
331
|
+
# GET "/users/1", response is:
|
332
|
+
# {
|
333
|
+
# "id": 1,
|
334
|
+
# "name": "George Michael Bluth",
|
335
|
+
# "comments": [
|
336
|
+
# { "id": 1, "text": "Foo" },
|
337
|
+
# { "id": 2, "text": "Bar" }
|
338
|
+
# ],
|
339
|
+
# "role": { "id": 1, "name": "Admin" },
|
340
|
+
# "organization": { "id": 2, "name": "Bluth Company" }
|
341
|
+
# }
|
342
|
+
|
343
|
+
@user.comments
|
344
|
+
# => [#<Comment id=1 text="Foo">, #<Comment id=2 text="Bar">]
|
345
|
+
|
346
|
+
@user.role
|
347
|
+
# => #<Role id=1 name="Admin">
|
348
|
+
|
349
|
+
@user.organization
|
350
|
+
# => #<Organization id=2 name="Bluth Company">
|
351
|
+
```
|
352
|
+
|
353
|
+
If there’s no association data in the resource, Her makes a HTTP request to retrieve the data.
|
354
|
+
|
355
|
+
```ruby
|
356
|
+
@user = User.find(1)
|
357
|
+
# GET "/users/1", response is { "id": 1, "name": "George Michael Bluth", "organization_id": 2 }
|
358
|
+
|
359
|
+
# has_many association:
|
360
|
+
@user.comments
|
361
|
+
# GET "/users/1/comments"
|
362
|
+
# => [#<Comment id=1>, #<Comment id=2>]
|
363
|
+
|
364
|
+
@user.comments.where(approved: 1)
|
365
|
+
# GET "/users/1/comments?approved=1"
|
366
|
+
# => [#<Comment id=1>]
|
367
|
+
|
368
|
+
# has_one association:
|
369
|
+
@user.role
|
370
|
+
# GET "/users/1/role"
|
371
|
+
# => #<Role id=1>
|
372
|
+
|
373
|
+
# belongs_to association:
|
374
|
+
@user.organization
|
375
|
+
# (the organization id comes from :organization_id, by default)
|
376
|
+
# GET "/organizations/2"
|
377
|
+
# => #<Organization id=2>
|
378
|
+
```
|
379
|
+
|
380
|
+
Subsequent calls to `#comments`, `#role` and `#organization` will not trigger extra HTTP requests and will return the cached objects.
|
381
|
+
|
382
|
+
#### Creating data
|
383
|
+
|
384
|
+
You can use the association methods to build new objects and save them.
|
385
|
+
|
386
|
+
```ruby
|
387
|
+
@user = User.find(1)
|
388
|
+
@user.comments.build(body: "Just a draft")
|
389
|
+
# => [#<Comment body="Just a draft" user_id=1>]
|
390
|
+
|
391
|
+
@user.comments.create(body: "Hello world.")
|
392
|
+
# POST "/users/1/comments" with `body=Hello+world.`
|
393
|
+
# => [#<Comment id=3 body="Hello world." user_id=1>]
|
394
|
+
```
|
395
|
+
|
396
|
+
You can also explicitly request a new object via the API when using ``build``. This is useful if you're dealing with default attributes.
|
397
|
+
|
398
|
+
```ruby
|
399
|
+
class Comment
|
400
|
+
include Her::Model
|
401
|
+
request_new_object_on_build true
|
402
|
+
end
|
403
|
+
|
404
|
+
@user = User.find(1)
|
405
|
+
@user.comments.build(body: "Just a draft")
|
406
|
+
# GET "/users/1/comments/new" with `body=Just+a+draft.`
|
407
|
+
# => [#<Comment id=nil body="Just a draft" archived=false user_id=1>]
|
408
|
+
```
|
409
|
+
|
410
|
+
#### Notes about paths
|
411
|
+
|
412
|
+
Resources must always have all the required attributes to build their complete path. For example, if you have these models:
|
413
|
+
|
414
|
+
```ruby
|
415
|
+
class User
|
416
|
+
include Her::Model
|
417
|
+
collection_path "organizations/:organization_id/users"
|
418
|
+
end
|
419
|
+
|
420
|
+
class Organization
|
421
|
+
include Her::Model
|
422
|
+
has_many :users
|
423
|
+
end
|
424
|
+
```
|
425
|
+
|
426
|
+
Her expects all `User` resources to have an `:organization_id` (or `:_organization_id`) attribute. Otherwise, calling mostly all methods, like `User.all`, will thrown an exception like this one:
|
427
|
+
|
428
|
+
```ruby
|
429
|
+
Her::Errors::PathError: Missing :_organization_id parameter to build the request path. Path is `organizations/:organization_id/users`. Parameters are `{ … }`.
|
430
|
+
```
|
431
|
+
|
432
|
+
### Validations
|
433
|
+
|
434
|
+
Her includes `ActiveModel::Validations` so you can declare validations the same way you do in Rails.
|
435
|
+
|
436
|
+
However, validations must be triggered manually — they are not run, for example, when calling `#save` on an object, or `#create` on a model class.
|
437
|
+
|
438
|
+
```ruby
|
439
|
+
class User
|
440
|
+
include Her::Model
|
441
|
+
|
442
|
+
attributes :fullname, :email
|
443
|
+
validates :fullname, presence: true
|
444
|
+
validates :email, presence: true
|
445
|
+
end
|
446
|
+
|
447
|
+
@user = User.new(fullname: "Tobias Fünke")
|
448
|
+
@user.valid? # => false
|
449
|
+
|
450
|
+
@user.save
|
451
|
+
# POST "/users" with `fullname=Tobias+Fünke` will still be called, even if the user is not valid
|
452
|
+
```
|
453
|
+
|
454
|
+
### Dirty attributes
|
455
|
+
|
456
|
+
Her includes `ActiveModel::Dirty` so you can keep track of the attributes that have changed in an object.
|
457
|
+
|
458
|
+
```ruby
|
459
|
+
class User
|
460
|
+
include Her::Model
|
461
|
+
|
462
|
+
attributes :fullname, :email
|
463
|
+
end
|
464
|
+
|
465
|
+
@user = User.new(fullname: "Tobias Fünke")
|
466
|
+
@user.fullname_changed? # => true
|
467
|
+
@user.changes # => { :fullname => [nil, "Tobias Fünke"] }
|
468
|
+
|
469
|
+
@user.save
|
470
|
+
# POST "/users" with `fullname=Tobias+Fünke`
|
471
|
+
|
472
|
+
@user.fullname_changed? # => false
|
473
|
+
@user.changes # => {}
|
474
|
+
```
|
475
|
+
|
476
|
+
To update only the modified attributes specify `:send_only_modified_attributes => true` in the setup.
|
477
|
+
|
478
|
+
### Callbacks
|
479
|
+
|
480
|
+
You can add *before* and *after* callbacks to your models that are triggered on specific actions. You can use symbols or blocks.
|
481
|
+
|
482
|
+
```ruby
|
483
|
+
class User
|
484
|
+
include Her::Model
|
485
|
+
before_save :set_internal_id
|
486
|
+
after_find { |u| u.fullname.upcase! }
|
487
|
+
|
488
|
+
def set_internal_id
|
489
|
+
self.internal_id = 42 # Will be passed in the HTTP request
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
@user = User.create(fullname: "Tobias Fünke")
|
494
|
+
# POST "/users" with `fullname=Tobias+Fünke&internal_id=42`
|
495
|
+
|
496
|
+
@user = User.find(1)
|
497
|
+
@user.fullname # => "TOBIAS FUNKE"
|
498
|
+
```
|
499
|
+
|
500
|
+
The available callbacks are:
|
501
|
+
|
502
|
+
* `before_save`
|
503
|
+
* `before_create`
|
504
|
+
* `before_update`
|
505
|
+
* `before_destroy`
|
506
|
+
* `after_save`
|
507
|
+
* `after_create`
|
508
|
+
* `after_update`
|
509
|
+
* `after_destroy`
|
510
|
+
* `after_find`
|
511
|
+
* `after_initialize`
|
512
|
+
|
513
|
+
### JSON attributes-wrapping
|
514
|
+
|
515
|
+
Her supports *sending* and *parsing* JSON data wrapped in a root element (to be compatible with Rails’ `include_root_in_json` setting), like so:
|
516
|
+
|
517
|
+
#### Sending
|
518
|
+
|
519
|
+
If you want to send all data to your API wrapped in a *root* element based on the model name.
|
520
|
+
|
521
|
+
```ruby
|
522
|
+
class User
|
523
|
+
include Her::Model
|
524
|
+
include_root_in_json true
|
525
|
+
end
|
526
|
+
|
527
|
+
class Article
|
528
|
+
include Her::Model
|
529
|
+
include_root_in_json :post
|
530
|
+
end
|
531
|
+
|
532
|
+
User.create(fullname: "Tobias Fünke")
|
533
|
+
# POST "/users" with `user[fullname]=Tobias+Fünke`
|
534
|
+
|
535
|
+
Article.create(title: "Hello world.")
|
536
|
+
# POST "/articles" with `post[title]=Hello+world`
|
537
|
+
```
|
538
|
+
|
539
|
+
#### Parsing
|
540
|
+
|
541
|
+
If the API returns data wrapped in a *root* element based on the model name.
|
542
|
+
|
543
|
+
```ruby
|
544
|
+
class User
|
545
|
+
include Her::Model
|
546
|
+
parse_root_in_json true
|
547
|
+
end
|
548
|
+
|
549
|
+
class Article
|
550
|
+
include Her::Model
|
551
|
+
parse_root_in_json :post
|
552
|
+
end
|
553
|
+
|
554
|
+
user = User.create(fullname: "Tobias Fünke")
|
555
|
+
# POST "/users" with `fullname=Tobias+Fünke`, response is { "user": { "fullname": "Tobias Fünke" } }
|
556
|
+
user.fullname # => "Tobias Fünke"
|
557
|
+
|
558
|
+
article = Article.create(title: "Hello world.")
|
559
|
+
# POST "/articles" with `title=Hello+world.`, response is { "post": { "title": "Hello world." } }
|
560
|
+
article.title # => "Hello world."
|
561
|
+
```
|
562
|
+
|
563
|
+
Of course, you can use both `include_root_in_json` and `parse_root_in_json` at the same time.
|
564
|
+
|
565
|
+
#### ActiveModel::Serializers support
|
566
|
+
|
567
|
+
If the API returns data in the default format used by the
|
568
|
+
[ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers)
|
569
|
+
project you need to configure Her as follows:
|
570
|
+
|
571
|
+
```ruby
|
572
|
+
class User
|
573
|
+
include Her::Model
|
574
|
+
parse_root_in_json true, format: :active_model_serializers
|
575
|
+
end
|
576
|
+
|
577
|
+
user = Users.find(1)
|
578
|
+
# GET "/users/1", response is { "user": { "id": 1, "fullname": "Lindsay Fünke" } }
|
579
|
+
|
580
|
+
users = Users.all
|
581
|
+
# GET "/users", response is { "users": [{ "id": 1, "fullname": "Lindsay Fünke" }, { "id": 1, "fullname": "Tobias Fünke" }] }
|
582
|
+
```
|
583
|
+
|
584
|
+
#### JSON API support
|
585
|
+
|
586
|
+
If the API returns data in the [JSON API format](http://jsonapi.org/) you need
|
587
|
+
to configure Her as follows:
|
588
|
+
|
589
|
+
```ruby
|
590
|
+
class User
|
591
|
+
include Her::Model
|
592
|
+
parse_root_in_json true, format: :json_api
|
593
|
+
end
|
594
|
+
|
595
|
+
user = Users.find(1)
|
596
|
+
# GET "/users/1", response is { "users": [{ "id": 1, "fullname": "Lindsay Fünke" }] }
|
597
|
+
|
598
|
+
users = Users.all
|
599
|
+
# GET "/users", response is { "users": [{ "id": 1, "fullname": "Lindsay Fünke" }, { "id": 2, "fullname": "Tobias Fünke" }] }
|
600
|
+
```
|
601
|
+
|
602
|
+
### Custom requests
|
603
|
+
|
604
|
+
You can easily define custom requests for your models using `custom_get`, `custom_post`, etc.
|
605
|
+
|
606
|
+
```ruby
|
607
|
+
class User
|
608
|
+
include Her::Model
|
609
|
+
|
610
|
+
custom_get :popular, :unpopular
|
611
|
+
custom_post :from_default
|
612
|
+
end
|
613
|
+
|
614
|
+
User.popular
|
615
|
+
# GET "/users/popular"
|
616
|
+
# => [#<User id=1>, #<User id=2>]
|
617
|
+
|
618
|
+
User.unpopular
|
619
|
+
# GET "/users/unpopular"
|
620
|
+
# => [#<User id=3>, #<User id=4>]
|
621
|
+
|
622
|
+
User.from_default(name: "Maeby Fünke")
|
623
|
+
# POST "/users/from_default" with `name=Maeby+Fünke`
|
624
|
+
# => #<User id=5 name="Maeby Fünke">
|
625
|
+
```
|
626
|
+
|
627
|
+
You can also use `get`, `post`, `put` or `delete` (which maps the returned data to either a collection or a resource).
|
628
|
+
|
629
|
+
```ruby
|
630
|
+
class User
|
631
|
+
include Her::Model
|
632
|
+
end
|
633
|
+
|
634
|
+
User.get(:popular)
|
635
|
+
# GET "/users/popular"
|
636
|
+
# => [#<User id=1>, #<User id=2>]
|
637
|
+
|
638
|
+
User.get(:single_best)
|
639
|
+
# GET "/users/single_best"
|
640
|
+
# => #<User id=1>
|
641
|
+
```
|
642
|
+
|
643
|
+
You can also use `get_raw` which yields the parsed data and the raw response from the HTTP request. Other HTTP methods are supported (`post_raw`, `put_raw`, etc.).
|
644
|
+
|
645
|
+
```ruby
|
646
|
+
class User
|
647
|
+
include Her::Model
|
648
|
+
|
649
|
+
def self.total
|
650
|
+
get_raw(:stats) do |parsed_data, response|
|
651
|
+
parsed_data[:data][:total_users]
|
652
|
+
end
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
User.total
|
657
|
+
# GET "/users/stats"
|
658
|
+
# => 42
|
659
|
+
```
|
660
|
+
|
661
|
+
You can also use full request paths (with strings instead of symbols).
|
662
|
+
|
663
|
+
```ruby
|
664
|
+
class User
|
665
|
+
include Her::Model
|
666
|
+
end
|
667
|
+
|
668
|
+
User.get("/users/popular")
|
669
|
+
# GET "/users/popular"
|
670
|
+
# => [#<User id=1>, #<User id=2>]
|
671
|
+
```
|
672
|
+
|
673
|
+
### Custom paths
|
674
|
+
|
675
|
+
You can define custom HTTP paths for your models:
|
676
|
+
|
677
|
+
```ruby
|
678
|
+
class User
|
679
|
+
include Her::Model
|
680
|
+
collection_path "/hello_users/:id"
|
681
|
+
end
|
682
|
+
|
683
|
+
@user = User.find(1)
|
684
|
+
# GET "/hello_users/1"
|
685
|
+
```
|
686
|
+
|
687
|
+
You can also include custom variables in your paths:
|
688
|
+
|
689
|
+
```ruby
|
690
|
+
class User
|
691
|
+
include Her::Model
|
692
|
+
collection_path "/organizations/:organization_id/users"
|
693
|
+
end
|
694
|
+
|
695
|
+
@user = User.find(1, _organization_id: 2)
|
696
|
+
# GET "/organizations/2/users/1"
|
697
|
+
|
698
|
+
@user = User.all(_organization_id: 2)
|
699
|
+
# GET "/organizations/2/users"
|
700
|
+
|
701
|
+
@user = User.new(fullname: "Tobias Fünke", organization_id: 2)
|
702
|
+
@user.save
|
703
|
+
# POST "/organizations/2/users" with `fullname=Tobias+Fünke`
|
704
|
+
```
|
705
|
+
|
706
|
+
### Custom primary keys
|
707
|
+
|
708
|
+
If your record uses an attribute other than `:id` to identify itself, specify it using the `primary_key` method:
|
709
|
+
|
710
|
+
```ruby
|
711
|
+
class User
|
712
|
+
include Her::Model
|
713
|
+
primary_key :_id
|
714
|
+
end
|
715
|
+
|
716
|
+
user = User.find("4fd89a42ff204b03a905c535")
|
717
|
+
# GET "/users/4fd89a42ff204b03a905c535", response is { "_id": "4fd89a42ff204b03a905c535", "name": "Tobias" }
|
718
|
+
|
719
|
+
user.destroy
|
720
|
+
# DELETE "/users/4fd89a42ff204b03a905c535"
|
721
|
+
```
|
722
|
+
|
723
|
+
### Inheritance
|
724
|
+
|
725
|
+
If all your models share the same settings, you might want to make them children of a class and only include `Her::Model` in that class. However, there are a few settings that don’t get passed to the children classes:
|
726
|
+
|
727
|
+
* `root_element`
|
728
|
+
* `collection_path` and `resource_path`
|
729
|
+
|
730
|
+
Those settings are based on the class name, so you don’t have to redefine them each time you create a new children class (but you still can). Every other setting is inherited from the parent (associations, scopes, JSON settings, etc.).
|
731
|
+
|
732
|
+
```ruby
|
733
|
+
module MyAPI
|
734
|
+
class Model
|
735
|
+
include Her::Model
|
736
|
+
|
737
|
+
parse_root_in_json true
|
738
|
+
include_root_in_json true
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
class User < MyAPI::Model
|
743
|
+
end
|
744
|
+
|
745
|
+
User.find(1)
|
746
|
+
# GET "/users/1"
|
747
|
+
```
|
748
|
+
|
749
|
+
### Scopes
|
750
|
+
|
751
|
+
Just like with ActiveRecord, you can define named scopes for your models. Scopes are chainable and can be used within other scopes.
|
752
|
+
|
753
|
+
```ruby
|
754
|
+
class User
|
755
|
+
include Her::Model
|
756
|
+
|
757
|
+
scope :by_role, -> { |role| where(role: role) }
|
758
|
+
scope :admins, -> { by_role('admin') }
|
759
|
+
scope :active, -> { where(active: 1) }
|
760
|
+
end
|
761
|
+
|
762
|
+
@admins = User.admins
|
763
|
+
# GET "/users?role=admin"
|
764
|
+
|
765
|
+
@moderators = User.by_role('moderator')
|
766
|
+
# GET "/users?role=moderator"
|
767
|
+
|
768
|
+
@active_admins = User.active.admins # @admins.active would have worked here too
|
769
|
+
# GET "/users?role=admin&active=1"
|
770
|
+
```
|
771
|
+
|
772
|
+
A neat trick you can do with scopes is interact with complex paths.
|
773
|
+
|
774
|
+
```ruby
|
775
|
+
class User
|
776
|
+
include Her::Model
|
777
|
+
|
778
|
+
collection_path "organizations/:organization_id/users"
|
779
|
+
scope :for_organization, -> { |id| where(organization_id: id) }
|
780
|
+
end
|
781
|
+
|
782
|
+
@user = User.for_organization(3).find(2)
|
783
|
+
# GET "/organizations/3/users/2"
|
784
|
+
|
785
|
+
@user = User.for_organization(3).create(fullname: "Tobias Fünke")
|
786
|
+
# POST "/organizations/3" with `fullname=Tobias+Fünke`
|
787
|
+
```
|
788
|
+
|
789
|
+
### Multiple APIs
|
790
|
+
|
791
|
+
It is possible to use different APIs for different models. Instead of calling `Her::API.setup`, you can create instances of `Her::API`:
|
792
|
+
|
793
|
+
```ruby
|
794
|
+
# config/initializers/her.rb
|
795
|
+
MY_API = Her::API.new
|
796
|
+
MY_API.setup url: "https://my-api.example.com" do |c|
|
797
|
+
# Response
|
798
|
+
c.use Her::Middleware::DefaultParseJSON
|
799
|
+
|
800
|
+
# Adapter
|
801
|
+
c.use Faraday::Adapter::NetHttp
|
802
|
+
end
|
803
|
+
|
804
|
+
OTHER_API = Her::API.new
|
805
|
+
OTHER_API.setup url: "https://other-api.example.com" do |c|
|
806
|
+
# Response
|
807
|
+
c.use Her::Middleware::DefaultParseJSON
|
808
|
+
|
809
|
+
# Adapter
|
810
|
+
c.use Faraday::Adapter::NetHttp
|
811
|
+
end
|
812
|
+
```
|
813
|
+
|
814
|
+
You can then define which API a model will use:
|
815
|
+
|
816
|
+
```ruby
|
817
|
+
class User
|
818
|
+
include Her::Model
|
819
|
+
use_api MY_API
|
820
|
+
end
|
821
|
+
|
822
|
+
class Category
|
823
|
+
include Her::Model
|
824
|
+
use_api OTHER_API
|
825
|
+
end
|
826
|
+
|
827
|
+
User.all
|
828
|
+
# GET "https://my-api.example.com/users"
|
829
|
+
|
830
|
+
Category.all
|
831
|
+
# GET "https://other-api.example.com/categories"
|
832
|
+
```
|
833
|
+
|
834
|
+
### SSL
|
835
|
+
|
836
|
+
When initializing `Her::API`, you can pass any parameter supported by `Faraday.new`. So [to use HTTPS](https://github.com/lostisland/faraday/wiki/Setting-up-SSL-certificates), you can use Faraday’s `:ssl` option.
|
837
|
+
|
838
|
+
```ruby
|
839
|
+
ssl_options = { ca_path: "/usr/lib/ssl/certs" }
|
840
|
+
Her::API.setup url: "https://api.example.com", ssl: ssl_options do |c|
|
841
|
+
# Response
|
842
|
+
c.use Her::Middleware::DefaultParseJSON
|
843
|
+
|
844
|
+
# Adapter
|
845
|
+
c.use Faraday::Adapter::NetHttp
|
846
|
+
end
|
847
|
+
```
|
848
|
+
|
849
|
+
## Testing
|
850
|
+
|
851
|
+
Suppose we have these two models bound to your API:
|
852
|
+
|
853
|
+
```ruby
|
854
|
+
# app/models/user.rb
|
855
|
+
class User
|
856
|
+
include Her::Model
|
857
|
+
custom_get :popular
|
858
|
+
end
|
859
|
+
|
860
|
+
# app/models/post.rb
|
861
|
+
class Post
|
862
|
+
include Her::Model
|
863
|
+
custom_get :recent, :archived
|
864
|
+
end
|
865
|
+
```
|
866
|
+
|
867
|
+
In order to test them, we’ll have to stub the remote API requests. With [RSpec](https://github.com/rspec/rspec-core), we can do this like so:
|
868
|
+
|
869
|
+
```ruby
|
870
|
+
# spec/spec_helper.rb
|
871
|
+
RSpec.configure do |config|
|
872
|
+
config.include(Module.new do
|
873
|
+
def stub_api_for(klass)
|
874
|
+
klass.use_api (api = Her::API.new)
|
875
|
+
|
876
|
+
# Here, you would customize this for your own API (URL, middleware, etc)
|
877
|
+
# like you have done in your application’s initializer
|
878
|
+
api.setup url: "http://api.example.com" do |c|
|
879
|
+
c.use Her::Middleware::FirstLevelParseJSON
|
880
|
+
c.adapter(:test) { |s| yield(s) }
|
881
|
+
end
|
882
|
+
end
|
883
|
+
end)
|
884
|
+
end
|
885
|
+
```
|
886
|
+
|
887
|
+
Then, in your tests, we can specify what (fake) HTTP requests will return:
|
888
|
+
|
889
|
+
```ruby
|
890
|
+
# spec/models/user.rb
|
891
|
+
describe User do
|
892
|
+
before do
|
893
|
+
stub_api_for(User) do |stub|
|
894
|
+
stub.get("/users/popular") { |env| [200, {}, [{ id: 1, name: "Tobias Fünke" }, { id: 2, name: "Lindsay Fünke" }].to_json] }
|
895
|
+
end
|
896
|
+
end
|
897
|
+
|
898
|
+
describe :popular do
|
899
|
+
subject { User.popular }
|
900
|
+
its(:length) { should == 2 }
|
901
|
+
its(:errors) { should be_empty }
|
902
|
+
end
|
903
|
+
end
|
904
|
+
```
|
905
|
+
|
906
|
+
We can redefine the API for a model as many times as we want, like for more complex tests:
|
907
|
+
|
908
|
+
```ruby
|
909
|
+
# spec/models/user.rb
|
910
|
+
describe Post do
|
911
|
+
describe :recent do
|
912
|
+
before do
|
913
|
+
stub_api_for(Post) do |stub|
|
914
|
+
stub.get("/posts/recent") { |env| [200, {}, [{ id: 1 }, { id: 2 }].to_json] }
|
915
|
+
end
|
916
|
+
end
|
917
|
+
|
918
|
+
subject { Post.recent }
|
919
|
+
its(:length) { should == 2 }
|
920
|
+
its(:errors) { should be_empty }
|
921
|
+
end
|
922
|
+
|
923
|
+
describe :archived do
|
924
|
+
before do
|
925
|
+
stub_api_for(Post) do |stub|
|
926
|
+
stub.get("/posts/archived") { |env| [200, {}, [{ id: 1 }, { id: 2 }].to_json] }
|
927
|
+
end
|
928
|
+
end
|
929
|
+
|
930
|
+
subject { Post.archived }
|
931
|
+
its(:length) { should == 2 }
|
932
|
+
its(:errors) { should be_empty }
|
933
|
+
end
|
934
|
+
end
|
935
|
+
```
|
936
|
+
|
937
|
+
## Upgrade
|
938
|
+
|
939
|
+
See the [UPGRADE.md](https://github.com/remiprev/her/blob/master/UPGRADE.md) for backward compatibility issues.
|
940
|
+
|
941
|
+
## Her IRL
|
942
|
+
|
943
|
+
Most projects I know that use Her are internal or private projects but here’s a list of public ones:
|
944
|
+
|
945
|
+
* [tumbz](https://github.com/remiprev/tumbz)
|
946
|
+
* [crowdher](https://github.com/simonprev/crowdher)
|
947
|
+
* [vodka](https://github.com/magnolia-fan/vodka)
|
948
|
+
* [webistrano_cli](https://github.com/chytreg/webistrano_cli)
|
949
|
+
* [ASMALLWORLD](https://www.asmallworld.com)
|
950
|
+
|
951
|
+
## History
|
952
|
+
|
953
|
+
I told myself a few months ago that it would be great to build a gem to replace Rails’ [ActiveResource](http://api.rubyonrails.org/classes/ActiveResource/Base.html) since it was barely maintained (and now removed from Rails 4.0), lacking features and hard to extend/customize. I had built a few of these REST-powered ORMs for client projects before but I decided I wanted to write one for myself that I could release as an open-source project.
|
954
|
+
|
955
|
+
Most of Her’s core concepts were written on a Saturday morning of April 2012 ([first commit](https://github.com/remiprev/her/commit/689d8e88916dc2ad258e69a2a91a283f061cbef2) at 7am!).
|
956
|
+
|
957
|
+
## Contribute
|
958
|
+
|
959
|
+
Yes please! Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues). There’s no such thing as a bad pull request — even if it’s for a typo, a small improvement to the code or the documentation!
|
960
|
+
|
961
|
+
See [CONTRIBUTING.md](https://github.com/remiprev/her/blob/master/CONTRIBUTING.md) for best practices.
|
962
|
+
|
963
|
+
### Contributors
|
964
|
+
|
965
|
+
These [fine folks](https://github.com/remiprev/her/contributors) helped with Her:
|
966
|
+
|
967
|
+
* [@jfcixmedia](https://github.com/jfcixmedia)
|
968
|
+
* [@EtienneLem](https://github.com/EtienneLem)
|
969
|
+
* [@rafaelss](https://github.com/rafaelss)
|
970
|
+
* [@tysontate](https://github.com/tysontate)
|
971
|
+
* [@nfo](https://github.com/nfo)
|
972
|
+
* [@simonprevost](https://github.com/simonprevost)
|
973
|
+
* [@jmlacroix](https://github.com/jmlacroix)
|
974
|
+
* [@thomsbg](https://github.com/thomsbg)
|
975
|
+
* [@calmyournerves](https://github.com/calmyournerves)
|
976
|
+
* [@luflux](https://github.com/luxflux)
|
977
|
+
* [@simonc](https://github.com/simonc)
|
978
|
+
* [@pencil](https://github.com/pencil)
|
979
|
+
* [@joanniclaborde](https://github.com/joanniclaborde)
|
980
|
+
* [@seanreads](https://github.com/seanreads)
|
981
|
+
* [@jonkarna](https://github.com/jonkarna)
|
982
|
+
* [@aclevy](https://github.com/aclevy)
|
983
|
+
* [@stevschmid](https://github.com/stevschmid)
|
984
|
+
* [@prognostikos](https://github.com/prognostikos)
|
985
|
+
* [@dturnerTS](https://github.com/dturnerTS)
|
986
|
+
* [@kritik](https://github.com/kritik)
|
987
|
+
|
988
|
+
## License
|
989
|
+
|
990
|
+
Her is © 2012-2013 [Rémi Prévost](http://exomel.com) and may be freely distributed under the [MIT license](https://github.com/remiprev/her/blob/master/LICENSE). See the `LICENSE` file.
|