braque 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +133 -19
- data/lib/braque/model.rb +1 -0
- data/lib/braque/relations.rb +27 -0
- data/lib/braque/version.rb +1 -1
- data/lib/braque.rb +1 -0
- data/spec/braque/relations_spec.rb +89 -0
- data/spec/fixtures/associated_collection.json +32 -0
- data/spec/fixtures/associated_resource.json +14 -0
- data/spec/fixtures/resource.json +14 -10
- data/spec/fixtures/root.json +8 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d7a9f3bb1b0fa79bccb708a0bf57e0b6a52f9e2b
|
4
|
+
data.tar.gz: 1e41b55a3b55791b8e5b66aacdffadddee401427
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 69f0e4507806a662441bbde237a173b17bd5d4067ef31d11208aa40f77efb437e0578e6f48d7c22af29849dac032760da27bafc45b50d5b97747268a067e9336
|
7
|
+
data.tar.gz: b0c7e6a4e5a7dd55ce7d39f48227edf1efd1280ff7e0579ea47230cabd099a4288f3b06adbe9e356428ba70b49ef9feb8fd5b8efa20e1a851449a985555f388b
|
data/README.md
CHANGED
@@ -2,13 +2,13 @@
|
|
2
2
|
|
3
3
|
Braque aims to provide a simple and familiar interface for setting up clients to interact with [Hypermedia (hal+json)](http://stateless.co/hal_specification.html) API services. It is a lightweight wrapper around [Hyperclient](https://github.com/codegram/hyperclient) and [ActiveAttr](https://github.com/cgriego/active_attr).
|
4
4
|
|
5
|
-
Braque is an early-stage and exploratory project. That said, at [Artsy](https://www.artsy.net), we've used Braque to consume [Gris](https://github.com/artsy/gris) APIs with great benefit.
|
5
|
+
Braque is an early-stage and exploratory project. That said, at [Artsy](https://www.artsy.net), we've used Braque to quickly consume [Gris](https://github.com/artsy/gris) Hypermedia APIs with great benefit.
|
6
6
|
|
7
7
|
[![Build Status](https://semaphoreci.com/api/v1/projects/c557a59e-1c1a-4719-a41a-6462a424ddfa/381676/badge.png)](https://semaphoreci.com/dylanfareed/braque)
|
8
8
|
|
9
|
-
###
|
9
|
+
### Basic model setup
|
10
10
|
|
11
|
-
```Braque::Model``` is ActiveSupport concern. You can use Braque::Model to map a remote resource to a class in your application. Do so by including Braque::Model in the class, defining the API service's root url, and listing attributes which we expect to receive from the API.
|
11
|
+
```Braque::Model``` is an ActiveSupport concern. You can use Braque::Model to map a remote resource to a class in your application. Do so by including Braque::Model in the class, defining the API service's root url (required), and listing attributes which we expect to receive from the API.
|
12
12
|
|
13
13
|
```ruby
|
14
14
|
class Article
|
@@ -24,34 +24,94 @@ class Article
|
|
24
24
|
end
|
25
25
|
```
|
26
26
|
|
27
|
-
|
27
|
+
Braque::Model adds familiar "Active Record"-like `create`, `find`, and `list` class methods to the embedding class (`Article` in the example above) as well as `save` and `destroy` instance methods. These methods wrap Hyperclient to make calls to a remote hypermedia API.
|
28
28
|
|
29
|
-
|
29
|
+
### Relations
|
30
30
|
|
31
|
-
|
31
|
+
If your remote API includes associated resources in the `_links` node for a given resource, you can use Braque's relations helpers to make navigating to those associated resources somewhat simpler.
|
32
32
|
|
33
|
-
|
34
|
-
class Article
|
35
|
-
include Braque::Model
|
36
|
-
api_root_url Rails.application.config_for(:articles_service)['url']
|
37
|
-
http_authorization_header Rails.application.config_for(:articles_service)['token']
|
33
|
+
For example if the remote API returns a response for the Book resource like this:
|
38
34
|
|
35
|
+
```
|
36
|
+
{
|
37
|
+
"id":1,
|
38
|
+
"title":"My Magazine",
|
39
|
+
"_links":{
|
40
|
+
"self":{
|
41
|
+
"href":"http://localhost:9292/magazines/1"
|
42
|
+
},
|
43
|
+
"articles":{
|
44
|
+
"href":"http://localhost:9292/articles?magazine_id=1{&page,size}",
|
45
|
+
"templated":true
|
46
|
+
}
|
47
|
+
}
|
48
|
+
}
|
49
|
+
```
|
50
|
+
|
51
|
+
And the remote API returns something like the following for a give Article resource:
|
52
|
+
|
53
|
+
```
|
54
|
+
{
|
55
|
+
"id":1,
|
56
|
+
"title":"My Article",
|
57
|
+
"magazine_id": 1,
|
58
|
+
"content":"Lorem ipsum...",
|
59
|
+
"_links":{
|
60
|
+
"self":{
|
61
|
+
"href":"http://localhost:9292/articles/1"
|
62
|
+
},
|
63
|
+
"magazine":{
|
64
|
+
"href":"http://localhost:9292/magazines/1"
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
```
|
69
|
+
|
70
|
+
In this situation you could choose to setup your Magazine and Article models to include `has_many` and `belongs_to` association helpers.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class Magazine
|
74
|
+
include ::Braque::Model
|
75
|
+
api_root_url 'http://localhost:9292'
|
76
|
+
has_many :articles
|
39
77
|
attribute :id
|
40
78
|
attribute :title
|
41
79
|
end
|
80
|
+
|
81
|
+
class Article
|
82
|
+
include ::Braque::Model
|
83
|
+
api_root_url 'http://localhost:9292'
|
84
|
+
belongs_to :magazine
|
85
|
+
attribute :id
|
86
|
+
attribute :content
|
87
|
+
attribute :magazine_id
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
This will allow you to retrieve associated resources without manually constructing a new link.
|
92
|
+
|
93
|
+
So instead of something like
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
magazine = Magazine.find id: article.magazine_id
|
42
97
|
```
|
43
98
|
|
44
|
-
|
99
|
+
you may use the `belongs_to` helper to retrieve the associated resource more simply with
|
45
100
|
|
46
101
|
```ruby
|
47
|
-
|
48
|
-
|
49
|
-
api_root_url Rails.application.config_for(:articles_service)['url']
|
50
|
-
accept_header Rails.application.config_for(:articles_service)['accept_header']
|
102
|
+
magazine = article.magazine
|
103
|
+
```
|
51
104
|
|
52
|
-
|
53
|
-
|
54
|
-
|
105
|
+
Similarly, the `has_many` helper provides retrieval methods for accessing associated resources.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
articles = magazine.articles
|
109
|
+
```
|
110
|
+
|
111
|
+
This method supports passing params to the remote API as well.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
articles = magazine.articles(page: 2, size: 20)
|
55
115
|
```
|
56
116
|
|
57
117
|
### Controllers
|
@@ -98,3 +158,57 @@ class ArticlesController < ApplicationController
|
|
98
158
|
end
|
99
159
|
|
100
160
|
```
|
161
|
+
|
162
|
+
### Subclassing
|
163
|
+
|
164
|
+
Braque supports inheritance for shared setup across multiple models in your app that make calls to the same remote API.
|
165
|
+
|
166
|
+
In the following example, `Article` and `Author` classes will inherit the `api_root_url` and `id`, `created_at`, and `updated_at` attributes from `RemoteModel`.
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
class RemoteModel
|
170
|
+
include Braque::Model
|
171
|
+
api_root_url Rails.application.config_for(:remote_service)['url']
|
172
|
+
|
173
|
+
attribute :id
|
174
|
+
attribute :created_at
|
175
|
+
attribute :updated_at
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
class Article < RemoteModel
|
181
|
+
attribute :title
|
182
|
+
attribute :body
|
183
|
+
attribute :summary
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
class Author < RemoteModel
|
189
|
+
attribute :first_name
|
190
|
+
attribute :last_name
|
191
|
+
end
|
192
|
+
```
|
193
|
+
|
194
|
+
### Custom Headers
|
195
|
+
|
196
|
+
Braque also supports passing additional headers along with API requests to your remote provider API service.
|
197
|
+
|
198
|
+
* Defining an `accept_header` will replace Hyperclient's default `Accept` header with the value you provide.
|
199
|
+
* Defining an `authorization_header` with your model will result in this value being sent over in an `Authorization` header with your requests.
|
200
|
+
* Defining an `http_authorization_header` with your model will result in this value being sent over in an `Http-Authorization` header with your requests.
|
201
|
+
|
202
|
+
To wit:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
class Article
|
206
|
+
include Braque::Model
|
207
|
+
api_root_url Rails.application.config_for(:articles_service)['url']
|
208
|
+
http_authorization_header Rails.application.config_for(:articles_service)['token']
|
209
|
+
accept_header Rails.application.config_for(:articles_service)['accept_header']
|
210
|
+
|
211
|
+
attribute :id
|
212
|
+
attribute :title
|
213
|
+
end
|
214
|
+
```
|
data/lib/braque/model.rb
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Braque
|
2
|
+
module Relations
|
3
|
+
def belongs_to(relation)
|
4
|
+
define_method relation do
|
5
|
+
relation.to_s.classify.constantize.new(
|
6
|
+
self.class.client.method(self.class.instance_method_name)
|
7
|
+
.call(resource_find_options)
|
8
|
+
.method(relation).call
|
9
|
+
)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# rubocop:disable Style/PredicateName
|
14
|
+
def has_many(relation)
|
15
|
+
define_method relation do |params = {}|
|
16
|
+
response = self.class.client.method(self.class.instance_method_name)
|
17
|
+
.call(resource_find_options)
|
18
|
+
.method(relation).call(params)
|
19
|
+
Braque::Collection::LinkedArray.new(
|
20
|
+
response,
|
21
|
+
relation.to_s.classify.singularize.constantize
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
# rubocop:enable Style/PredicateName
|
26
|
+
end
|
27
|
+
end
|
data/lib/braque/version.rb
CHANGED
data/lib/braque.rb
CHANGED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Braque::Relations, type: :model do
|
4
|
+
context 'with multiple defined Braque::Model classes' do
|
5
|
+
before do
|
6
|
+
class Breeze
|
7
|
+
include ::Braque::Model
|
8
|
+
api_root_url 'http://localhost:9292'
|
9
|
+
has_many :tides
|
10
|
+
attribute :id
|
11
|
+
attribute :title
|
12
|
+
end
|
13
|
+
|
14
|
+
class Tide
|
15
|
+
include ::Braque::Model
|
16
|
+
api_root_url 'http://localhost:9292'
|
17
|
+
belongs_to :breeze
|
18
|
+
attribute :id
|
19
|
+
attribute :title
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:root_response) { JSON.parse(File.read 'spec/fixtures/root.json') }
|
24
|
+
let(:breeze_response) { JSON.parse(File.read 'spec/fixtures/resource.json') }
|
25
|
+
let(:tide_response) { JSON.parse(File.read 'spec/fixtures/associated_resource.json') }
|
26
|
+
let(:tides_response) { JSON.parse(File.read 'spec/fixtures/associated_collection.json') }
|
27
|
+
|
28
|
+
let(:root_request) do
|
29
|
+
WebMock.stub_request(:get, "#{Breeze.config[:api_root_url]}/")
|
30
|
+
.to_return(status: 200, body: root_response)
|
31
|
+
end
|
32
|
+
|
33
|
+
let(:breeze_request) do
|
34
|
+
WebMock.stub_request(:get, "#{Breeze.config[:api_root_url]}/breezes/1")
|
35
|
+
.to_return(status: 200, body: breeze_response)
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:tide_request) do
|
39
|
+
WebMock.stub_request(:get, "#{Tide.config[:api_root_url]}/tides/1")
|
40
|
+
.to_return(status: 200, body: tide_response)
|
41
|
+
end
|
42
|
+
|
43
|
+
let(:tides_request) do
|
44
|
+
WebMock.stub_request(:get, "#{Tide.config[:api_root_url]}/tides?account_id=1")
|
45
|
+
.to_return(status: 200, body: tides_response)
|
46
|
+
end
|
47
|
+
|
48
|
+
let(:breeze) { Breeze.new id: 1 }
|
49
|
+
let(:tide) { Tide.new id: 1 }
|
50
|
+
|
51
|
+
before do
|
52
|
+
root_request
|
53
|
+
breeze_request
|
54
|
+
tide_request
|
55
|
+
tides_request
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'has_many' do
|
59
|
+
it 'defines a dynamic instance method for association' do
|
60
|
+
expect(breeze.methods).to include :tides
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'returns an array of associated objects' do
|
64
|
+
associated = breeze.tides
|
65
|
+
expect(associated.total_count).to eq 2
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'returns an array of associated objects' do
|
69
|
+
associated = breeze.tides.first
|
70
|
+
expect(associated.class).to eq Tide
|
71
|
+
expect(associated.title).to eq(
|
72
|
+
tides_response['_embedded']['tides'].first['title']
|
73
|
+
)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'belongs_to' do
|
78
|
+
it 'defines a dynamic instance method for association' do
|
79
|
+
expect(tide.methods).to include :breeze
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'returns the correct associated object' do
|
83
|
+
associated = tide.breeze
|
84
|
+
expect(associated.class).to eq Breeze
|
85
|
+
expect(associated.title).to eq breeze_response['title']
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
{
|
2
|
+
"total_count":2,
|
3
|
+
"total_pages":1,
|
4
|
+
"current_page":1,
|
5
|
+
"_links":{
|
6
|
+
"self":{
|
7
|
+
"href":"http://localhost:9292/tides"
|
8
|
+
}
|
9
|
+
},
|
10
|
+
"_embedded":{
|
11
|
+
"tides":[
|
12
|
+
{
|
13
|
+
"id":0,
|
14
|
+
"title":"Southerly Tides",
|
15
|
+
"_links":{
|
16
|
+
"self":{
|
17
|
+
"href":"http://localhost:9292/tides/0"
|
18
|
+
}
|
19
|
+
}
|
20
|
+
},
|
21
|
+
{
|
22
|
+
"id":1,
|
23
|
+
"title":"Westerly Tides",
|
24
|
+
"_links":{
|
25
|
+
"self":{
|
26
|
+
"href":"http://localhost:9292/tides/1"
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
]
|
31
|
+
}
|
32
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
{
|
2
|
+
"id":1,
|
3
|
+
"title":"Some tide",
|
4
|
+
"created_at":"2015-02-11T18:59:01.669Z",
|
5
|
+
"updated_at":"2015-02-12T00:26:22.255Z",
|
6
|
+
"_links":{
|
7
|
+
"self":{
|
8
|
+
"href":"http://localhost:9292/tides/1"
|
9
|
+
},
|
10
|
+
"breeze":{
|
11
|
+
"href":"http://localhost:9292/breezes/1"
|
12
|
+
}
|
13
|
+
}
|
14
|
+
}
|
data/spec/fixtures/resource.json
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
{
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
2
|
+
"id":1,
|
3
|
+
"token":123,
|
4
|
+
"title":"Southerly Breeze",
|
5
|
+
"created_at":"2015-02-11T18:59:01.669Z",
|
6
|
+
"updated_at":"2015-02-12T00:26:22.255Z",
|
7
|
+
"_links":{
|
8
|
+
"self":{
|
9
|
+
"href":"http://localhost:9292/breezes/1"
|
10
|
+
},
|
11
|
+
"tides":{
|
12
|
+
"href":"http://localhost:9292/tides?account_id=1{&page,size}",
|
13
|
+
"templated":true
|
14
|
+
}
|
15
|
+
}
|
12
16
|
}
|
data/spec/fixtures/root.json
CHANGED
@@ -13,6 +13,14 @@
|
|
13
13
|
"breeze":{
|
14
14
|
"href":"http://localhost:9292/breezes/{id}",
|
15
15
|
"templated":true
|
16
|
+
},
|
17
|
+
"tides":{
|
18
|
+
"href":"http://localhost:9292/tides{?page,size}",
|
19
|
+
"templated":true
|
20
|
+
},
|
21
|
+
"tide":{
|
22
|
+
"href":"http://localhost:9292/tides/{id}",
|
23
|
+
"templated":true
|
16
24
|
}
|
17
25
|
}
|
18
26
|
}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: braque
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dylan Fareed
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-09-
|
11
|
+
date: 2015-09-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hyperclient
|
@@ -65,6 +65,7 @@ files:
|
|
65
65
|
- lib/braque.rb
|
66
66
|
- lib/braque/collection.rb
|
67
67
|
- lib/braque/model.rb
|
68
|
+
- lib/braque/relations.rb
|
68
69
|
- lib/braque/version.rb
|
69
70
|
- spec/braque/api_root_spec.rb
|
70
71
|
- spec/braque/client_headers_spec.rb
|
@@ -72,7 +73,10 @@ files:
|
|
72
73
|
- spec/braque/model_attributes_spec.rb
|
73
74
|
- spec/braque/model_spec.rb
|
74
75
|
- spec/braque/multiple_model_spec.rb
|
76
|
+
- spec/braque/relations_spec.rb
|
75
77
|
- spec/braque/subclassed_model_spec.rb
|
78
|
+
- spec/fixtures/associated_collection.json
|
79
|
+
- spec/fixtures/associated_resource.json
|
76
80
|
- spec/fixtures/collection.json
|
77
81
|
- spec/fixtures/resource.json
|
78
82
|
- spec/fixtures/root.json
|