hal-client 1.1.0 → 1.3.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: a22811cd4e0f71ae8fe9767f40314c6c070a603b
4
- data.tar.gz: de7b7a1b94bc47dd3a1da179d2241ce85e337eb0
3
+ metadata.gz: 2feba4ea1ae2adc4762239ce875c4d4db3dad541
4
+ data.tar.gz: d0854b133f4ca25cb6ee21d10e2e7ba8e87d1480
5
5
  SHA512:
6
- metadata.gz: 6d75d90c76548319814b03a18d7c8518609ef4911325d59c686a4ad63f5aba75933254fd70b850d05bb7b008504f5a8245a650841bc34046d99fb051aef048d8
7
- data.tar.gz: 9cf40c295ddc43e1083a45e67c74a75fb901ebe793bfb0a097acbcd9f93fc5d4bfc74b50037e5a3750b890b3254780df2141776a27ec449a94c1798004889f04
6
+ metadata.gz: 2ac5423a12091b050f6e27b93994b4a4914ab65a1702565b22ab724b45f73baa30a1896402ade0e62c024b1b2cbfacfde689143d41f9246d6900ddf6b723e4fa
7
+ data.tar.gz: be45454510e90beb073b2b9c8852e59718727ca91ff89cd1da6ef8ccb26d593b9d5a71d604fc02f7f2dad7453515340e8754dab217743bd79729763676ce52a9
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.2"
4
+ - "1.9.3"
5
+ - "2.0.0"
6
+ - ruby-head
7
+ - jruby-19mode # JRuby in 1.9 mode
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
+ [![Build Status](https://travis-ci.org/pezra/hal-client.png?branch=master)](https://travis-ci.org/pezra/hal-client)
2
+ [![Code Climate](https://codeclimate.com/github/pezra/hal-client.png)](https://codeclimate.com/github/pezra/hal-client)
3
+
1
4
  # HalClient
2
5
 
3
- An easy to use interface for REST APIs that use [HAL](http://stateless.co/hal_specification.html).
6
+ An easy to use client interface for REST APIs that use [HAL](http://stateless.co/hal_specification.html).
4
7
 
5
8
  ## Installation
6
9
 
@@ -16,21 +19,13 @@ Or install it yourself as:
16
19
 
17
20
  $ gem install hal-client
18
21
 
19
- ## Usage
20
-
21
- The first step to using HalClient is to create a `HalClient` instance.
22
-
23
- my_client = HalClient.new
24
-
25
- If the API uses one or more a custom mime types we can specify that they be included in the `Accept` header field of each request.
26
-
27
- my_client = HalClient.new(accept: "application/vnd.myapp+hal+json")
28
-
29
- ### `GET`ting an entry point
22
+ Usage
23
+ -----
30
24
 
31
- In normal usage you will rarely use the `HalClient` instance directly. Normally, you will traverse links on a representation which uses the `HalClient` indirectly. Getting API entry points is main use for the HalClient instance.
25
+ The first step in using a HAL based API is getting a representation of one of its entry point. The simplest way to do this is using the `get` class method of `HalClient`.
32
26
 
33
- blog = my_client.get("http://blog.me/")
27
+ blog = HalClient.get("http://blog.me/")
28
+ # => #<Representation: http://blog.me/>
34
29
 
35
30
  `HalClient::Representation`s expose a `#property` method to retrieve properties from the HAL document.
36
31
 
@@ -39,19 +34,36 @@ In normal usage you will rarely use the `HalClient` instance directly. Normally,
39
34
 
40
35
  ### Link navigation
41
36
 
42
- Once we have a representation we are going to need to navigate its links. This can be accomplished by using the `#related` method.
37
+ Once we have a representation we will want to navigate its links. This can be accomplished using the `#related` method.
43
38
 
44
39
  articles = blog.related("item")
45
40
  # => #<RepresentationSet:...>
46
41
 
47
- In the example above `item` is the link rel. The `#related` method looks up both embedded representations and links with the rel of `item`. Links are then dereferenced using the same `HalClient` instance used to retrieve the entry point. The dereferenced links and extracted embedded representations are converted into individual `HalClient::Representation`s and packaged into a `HalClient::RepresentationSet`. `HalClient` always returns `RepresentationSet`s when following links, even when there is only one result as doing so tends to result in simpler client code.
42
+ In the example above `item` is the link rel. The `#related` method extracts embedded representations and dereferences links with the specified rel. The resulting representations and packaged into a `HalClient::RepresentationSet`. `HalClient` always returns `RepresentationSet`s when following links, even when there is only one result as doing so tends to result in simpler client code.
48
43
 
49
- `RepresentationSet`s are `Enumerable` so they expose all your favorite methods like `#each`, `#map`, `#any?`, etc. Additionally, `RepresentationSet`s expose a `#related` method which calls `#related` on each member of the set and then merges the results into a new representation set.
44
+ `RepresentationSet`s are `Enumerable` so they expose all your favorite methods like `#each`, `#map`, `#any?`, etc. `RepresentationSet`s expose a `#related` method which calls `#related` on each member of the set and then merges the results into a new representation set.
50
45
 
51
46
  authors = blog.related("author").related("item")
52
47
  authors.first.property("name")
53
48
  # => "Bob Smith"
54
49
 
50
+ #### CURIEs
51
+
52
+ Links specified using a compact URI (or CURIE) as the rel are fully supported. They are accessed using the fully expanded version of the curie. For example, given a representations of an author:
53
+
54
+ { "name": "Bob Smith,
55
+ "_links": {
56
+ "so:homeLocation": { "href": "http://example.com/denver" },
57
+ "curies": [{ "name": "so", "href": "http://schema.org/{rel}", "templated": true }]
58
+ }}
59
+
60
+ Bob's home location can be retrieved with
61
+
62
+ author.related("http://schema.org/homeLocation")
63
+ # => #<Representation: http://example.com/denver>
64
+
65
+ Links are always accessed using the full link relation, rather than the CURIE, because the document producer can use any arbitrary string as the prefix. This means that clients must not make any assumptions regarding what prefix will be used because it might change over time or even between documents.
66
+
55
67
  ### Templated links
56
68
 
57
69
  The `#related` methods takes a `Hash` as its second argument which is used to expand any templated links that are involved in the navigation.
@@ -73,13 +85,21 @@ All `HalClient::Representation`s exposed an `#href` attribute which is its ident
73
85
 
74
86
  blog['title'] # => "Some Person's Blog"
75
87
  blog['item'] # => #<RepresentationSet:...>
76
-
88
+
89
+ ### Custom media types
90
+
91
+ If the API uses one or more a custom mime types we can specify that they be included in the `Accept` header field of each request.
92
+
93
+ my_client = HalClient.new(accept: "application/vnd.myapp+hal+json")
94
+ my_client.get("http://blog.me/")
95
+ # => #<Representation: http://blog.me/>
77
96
 
78
97
  ## Contributing
79
98
 
80
99
  1. Fork it ( http://github.com/pezra/hal-client/fork )
81
100
  2. Create your feature branch (`git checkout -b my-new-feature`)
82
- 3. Commit your changes (`git commit -am 'Add some feature'`)
83
- 3. Update `lib/hal_client/version.rb` following [semantic versioning rules](http://semver.org/)
84
- 4. Push to the branch (`git push origin my-new-feature`)
85
- 5. Create new Pull Request
101
+ 3. Implement your improvement
102
+ 4. Update `lib/hal_client/version.rb` following [semantic versioning rules](http://semver.org/)
103
+ 5. Commit your changes (`git commit -am 'Add some feature'`)
104
+ 6. Push to the branch (`git push origin my-new-feature`)
105
+ 7. Create new Pull Request
@@ -0,0 +1,52 @@
1
+ require 'addressable/template'
2
+
3
+ class HalClient
4
+
5
+ # Expands CURIEs to fully qualified URLs using a set curie
6
+ # definitions.
7
+ class CurieResolver
8
+
9
+ # Initialize new CurieResolver
10
+ #
11
+ # curie_defs - Array of curie definition links (per the HAL spec)
12
+ def initialize(curie_defs)
13
+ curie_defs = [curie_defs].flatten
14
+ @namespaces = interpret curie_defs
15
+ end
16
+
17
+ # Returns a an expanded version of `curie_or_uri` or the
18
+ # input. The input is returned when `curie_or_uri` is not a curie
19
+ # or is a curie whose namespace is not recognized.
20
+ #
21
+ # curie_or_uri - the (potential) curie to resolve
22
+ def resolve(curie_or_uri)
23
+ ns, short_name = split_curie curie_or_uri
24
+
25
+ if ns && (namespaces.has_key? ns)
26
+ namespaces[ns].expand(rel: short_name).to_s
27
+ else
28
+ curie_or_uri
29
+ end
30
+ end
31
+
32
+ protected
33
+ attr_reader :namespaces
34
+
35
+ def split_curie(a_curie)
36
+ curie_parts = /(?<ns>[^:]+):(?<short_name>.+)/.match(a_curie)
37
+
38
+ if curie_parts
39
+ [curie_parts[:ns], curie_parts[:short_name]]
40
+ else
41
+ [nil,nil]
42
+ end
43
+ end
44
+
45
+ def interpret(curie_defs)
46
+ Hash[curie_defs.map{|it|
47
+ [it["name"], Addressable::Template.new(it["href"])]
48
+ }]
49
+ end
50
+
51
+ end
52
+ end
@@ -131,17 +131,23 @@ class HalClient
131
131
  end
132
132
  end
133
133
 
134
+ # Returns a short human readable description of this
135
+ # representation.
136
+ def to_s
137
+ "#<" + self.class.name + ": " + href + ">"
138
+ end
139
+
134
140
  protected
135
141
  attr_reader :raw, :hal_client
136
142
 
137
143
  MISSING = Object.new
138
144
 
139
145
  def link_section
140
- @link_section ||= raw.fetch("_links", {})
146
+ @link_section ||= fully_qualified raw.fetch("_links", {})
141
147
  end
142
148
 
143
149
  def embedded_section
144
- @embedded_section ||= raw.fetch("_embedded", {})
150
+ @embedded_section ||= fully_qualified raw.fetch("_embedded", {})
145
151
  end
146
152
 
147
153
  def embedded(link_rel)
@@ -177,5 +183,17 @@ class HalClient
177
183
  end
178
184
  end
179
185
 
186
+ def fully_qualified(relations_section)
187
+ Hash[relations_section.map {|rel, link_info|
188
+ [(namespaces.resolve rel), link_info]
189
+ }]
190
+ end
191
+
192
+ def namespaces
193
+ @namespaces ||= CurieResolver.new raw.fetch("_links", {}).fetch("curies", [])
194
+ end
195
+
196
+
197
+
180
198
  end
181
199
  end
@@ -1,3 +1,3 @@
1
1
  class HalClient
2
- VERSION = "1.1.0"
2
+ VERSION = "1.3.1"
3
3
  end
data/lib/hal_client.rb CHANGED
@@ -5,6 +5,7 @@ require 'rest-client'
5
5
  class HalClient
6
6
  autoload :Representation, 'hal_client/representation'
7
7
  autoload :RepresentationSet, 'hal_client/representation_set'
8
+ autoload :CurieResolver, 'hal_client/curie_resolver'
8
9
 
9
10
  # Initializes a new client instance
10
11
  #
@@ -31,4 +32,22 @@ class HalClient
31
32
  def rest_client_options(overrides)
32
33
  {accept: default_accept}.merge overrides
33
34
  end
35
+
36
+ module EntryPointCovenienceMethods
37
+ # Returns a `Representation` of the resource identified by `url`.
38
+ #
39
+ # url - The URL of the resource of interest.
40
+ # options - set of options to pass to `RestClient#get`
41
+ def get(url, options={})
42
+ default_client.get(url, options)
43
+ end
44
+
45
+ protected
46
+
47
+ def default_client
48
+ @default_client ||= self.new
49
+ end
50
+ end
51
+ extend EntryPointCovenienceMethods
52
+
34
53
  end
@@ -0,0 +1,38 @@
1
+ require_relative "../spec_helper"
2
+
3
+ require 'hal_client/curie_resolver'
4
+
5
+ describe HalClient::CurieResolver do
6
+ describe "#new" do
7
+ it "takes an array of curie definitions" do
8
+ expect(described_class.new([f_ns, b_ns])).to be_kind_of described_class
9
+ end
10
+
11
+ it "takes a single curie definition" do
12
+ expect(described_class.new(f_ns)).to be_kind_of described_class
13
+ end
14
+ end
15
+
16
+ subject(:resolver) { described_class.new([f_ns, b_ns]) }
17
+
18
+ describe "#resolve" do
19
+ it "returns rel name given a standard rel name" do
20
+ expect(resolver.resolve("item")).to eq "item"
21
+ end
22
+
23
+ it "returns url given a fully qualified url" do
24
+ expect(resolver.resolve("http://example.com/foo")).to eq "http://example.com/foo"
25
+ end
26
+
27
+ it "returns expanded url given a curie in known namespace" do
28
+ expect(resolver.resolve("f:yer")).to eq "foo:yer"
29
+ end
30
+
31
+ it "returns unexpanded curie given a curie in unknown namespace" do
32
+ expect(resolver.resolve("ex:yer")).to eq "ex:yer"
33
+ end
34
+ end
35
+
36
+ let(:f_ns) { {"name" => "f", "href" => "foo:{rel}", "templated" => true} }
37
+ let(:b_ns) { {"name" => "b", "href" => "bar:{rel}", "templated" => true} }
38
+ end
@@ -31,6 +31,12 @@ describe HalClient::Representation do
31
31
  HAL
32
32
  subject(:repr) { described_class.new(a_client, MultiJson.load(raw_repr)) }
33
33
 
34
+ describe "#to_s" do
35
+ subject(:return_val) { repr.to_s }
36
+
37
+ it { should eq "#<HalClient::Representation: http://example.com/foo>" }
38
+ end
39
+
34
40
  describe "#property" do
35
41
  context "existent" do
36
42
  subject { repr.property "prop1" }
@@ -148,6 +154,61 @@ HAL
148
154
  end
149
155
 
150
156
 
157
+ context "curie links" do
158
+ let(:raw_repr) { <<-HAL }
159
+ { "_links": {
160
+ "self": { "href": "http://example.com/foo" }
161
+ ,"ex:bar": { "href": "http://example.com/bar" }
162
+ ,"curies": [{"name": "ex", "href": "http://example.com/rels/{rel}", "templated": true}]
163
+ }
164
+ }
165
+ HAL
166
+
167
+ describe "#related return value " do
168
+ subject(:return_val) { repr.related("http://example.com/rels/bar") }
169
+ it { should include_representation_of "http://example.com/bar" }
170
+ end
171
+
172
+ describe "#[] return value " do
173
+ subject(:return_val) { repr["http://example.com/rels/bar"] }
174
+ it { should include_representation_of "http://example.com/bar" }
175
+ end
176
+
177
+ describe "#related_hrefs return value " do
178
+ subject(:return_val) { repr.related_hrefs("http://example.com/rels/bar") }
179
+ it { should include "http://example.com/bar" }
180
+ end
181
+ end
182
+
183
+ context "curie embedded" do
184
+ let(:raw_repr) { <<-HAL }
185
+ { "_links": {
186
+ "self": { "href": "http://example.com/foo" }
187
+ ,"curies": {"name": "ex", "href": "http://example.com/rels/{rel}", "templated": true}
188
+ }
189
+ ,"_embedded": {
190
+ "ex:embed1": { "_links": { "self": { "href": "http://example.com/embed1" } } }
191
+ }
192
+ }
193
+ HAL
194
+
195
+ describe "#related return value " do
196
+ subject(:return_val) { repr.related("http://example.com/rels/embed1") }
197
+ it { should include_representation_of "http://example.com/embed1" }
198
+ end
199
+
200
+ describe "#[] return value " do
201
+ subject(:return_val) { repr["http://example.com/rels/embed1"] }
202
+ it { should include_representation_of "http://example.com/embed1" }
203
+ end
204
+
205
+ describe "#related_hrefs return value " do
206
+ subject(:return_val) { repr.related_hrefs("http://example.com/rels/embed1") }
207
+ it { should include "http://example.com/embed1" }
208
+ end
209
+ end
210
+
211
+
151
212
 
152
213
  let(:a_client) { HalClient.new }
153
214
  let!(:bar_request) { stub_identity_request("http://example.com/bar") }
@@ -39,6 +39,24 @@ describe HalClient do
39
39
  end
40
40
  end
41
41
 
42
+ describe ".get(<url>)" do
43
+ let!(:return_val) { HalClient.get "http://example.com/foo" }
44
+
45
+ it "returns a HalClient::Representation" do
46
+ expect(return_val).to be_kind_of HalClient::Representation
47
+ end
48
+
49
+ describe "request" do
50
+ subject { request }
51
+ it("should have been made") { should have_been_made }
52
+
53
+ it "sends accept header" do
54
+ expect(request.with(headers: {'Accept' => 'application/hal+json'})).
55
+ to have_been_made
56
+ end
57
+ end
58
+ end
59
+
42
60
  let!(:request) { stub_request(:get, "http://example.com/foo").
43
61
  to_return body: "{}" }
44
62
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hal-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-02-11 00:00:00.000000000 Z
11
+ date: 2014-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rest-client
@@ -152,6 +152,7 @@ extensions: []
152
152
  extra_rdoc_files: []
153
153
  files:
154
154
  - ".gitignore"
155
+ - ".travis.yml"
155
156
  - Gemfile
156
157
  - LICENSE.txt
157
158
  - README.md
@@ -159,9 +160,11 @@ files:
159
160
  - hal-client.gemspec
160
161
  - lib/hal-client.rb
161
162
  - lib/hal_client.rb
163
+ - lib/hal_client/curie_resolver.rb
162
164
  - lib/hal_client/representation.rb
163
165
  - lib/hal_client/representation_set.rb
164
166
  - lib/hal_client/version.rb
167
+ - spec/hal_client/curie_resolver_spec.rb
165
168
  - spec/hal_client/representation_set_spec.rb
166
169
  - spec/hal_client/representation_spec.rb
167
170
  - spec/hal_client_spec.rb
@@ -191,6 +194,7 @@ signing_key:
191
194
  specification_version: 4
192
195
  summary: Use HAL APIs easily
193
196
  test_files:
197
+ - spec/hal_client/curie_resolver_spec.rb
194
198
  - spec/hal_client/representation_set_spec.rb
195
199
  - spec/hal_client/representation_spec.rb
196
200
  - spec/hal_client_spec.rb