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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8d74d0cde5c073286613ad279b4221f967934c72
4
- data.tar.gz: a296e958733e8c9036709d63085dc0950b113ec8
3
+ metadata.gz: d7a9f3bb1b0fa79bccb708a0bf57e0b6a52f9e2b
4
+ data.tar.gz: 1e41b55a3b55791b8e5b66aacdffadddee401427
5
5
  SHA512:
6
- metadata.gz: 9a2b799f95dbd2f03e2e01e4cd8d2ee0f7332edadc5988b512e01e352f6819a0f6ebb05cda6fb214f8638894df84d98b16f2b951c4e4ad20f040a80fc5788187
7
- data.tar.gz: 2accbbe9c093abff560a73b6c596eb9890910b90cda5c1b2f84f75e7246cc3d9c4257b52d1cb9b047e5e1609ed5f2d321c5029ecaf48e631b94fe9c2f250ed18
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
- ### Model setup
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
- ### Credentials
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
- ```Braque::Model``` also supports multiple API authentication strategies. Defining `http_authorization_header` or `accept_header` with the model will pass those credentials to the provider API with each request.
29
+ ### Relations
30
30
 
31
- For example, `http_authorization_header` credentials defined here are added to the request's `Http-Authorization` headers.
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
- ```ruby
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
- Defining an `accept_header` credential will replace Hyperclient's default `Accept` header with the value you provide.
99
+ you may use the `belongs_to` helper to retrieve the associated resource more simply with
45
100
 
46
101
  ```ruby
47
- class Article
48
- include Braque::Model
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
- attribute :id
53
- attribute :title
54
- end
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
@@ -72,6 +72,7 @@ module Braque
72
72
 
73
73
  module ClassMethods
74
74
  include Braque::Collection
75
+ include Braque::Relations
75
76
 
76
77
  [:api_root_url, :accept_header, :authorization_header, :http_authorization_header].each do |config_option|
77
78
  define_method config_option do |value|
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Braque
2
- VERSION = '0.2.0'
2
+ VERSION = '0.2.1'
3
3
  end
data/lib/braque.rb CHANGED
@@ -3,6 +3,7 @@ require 'active_attr'
3
3
  require 'hyperclient'
4
4
  require 'braque/version'
5
5
  require 'braque/collection'
6
+ require 'braque/relations'
6
7
  require 'braque/model'
7
8
 
8
9
  module Braque
@@ -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
+ }
@@ -1,12 +1,16 @@
1
1
  {
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://example.org/breezes/1"
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
  }
@@ -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.0
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-10 00:00:00.000000000 Z
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