braque 0.2.0 → 0.2.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.
- 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
|
[](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
|