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