From 44d106b2c196015e2e72e741286a6cacb174fce8 Mon Sep 17 00:00:00 2001 From: dblock Date: Fri, 26 Sep 2014 15:57:24 -0400 Subject: [PATCH] Added support for _curries. --- CHANGELOG.md | 1 + README.md | 22 ++++++----- lib/hyperclient/curie.rb | 49 ++++++++++++++++++++++++ lib/hyperclient/link_collection.rb | 42 +++++++++++++++----- lib/hyperclient/resource.rb | 2 +- test/fixtures/element.json | 13 ++++++- test/hyperclient/collection_test.rb | 2 +- test/hyperclient/curie_test.rb | 34 ++++++++++++++++ test/hyperclient/link_collection_test.rb | 23 ++++++++++- test/hyperclient/resource_test.rb | 2 +- 10 files changed, 166 insertions(+), 24 deletions(-) create mode 100644 lib/hyperclient/curie.rb create mode 100644 test/hyperclient/curie_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 13301a9..1618168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#58](https://github.com/codegram/hyperclient/issues/58): Automatically follow redirects (by [@dblock](https://github.com/dblock)). * [#63](https://github.com/codegram/hyperclient/pull/63): You can omit the navigational elements, `api.links.products` is now equivalent to `api.products` (by @dblock). * Implemented Rubocop, Ruby-style linter (by @dblock). + * [#64](https://github.com/codegram/hyperclient/issues/64): Added support for curies (by [@dblock](https://github.com/dblock)). * bug fixes * Nothing. diff --git a/README.md b/README.md index 7d33f40..23ac8a9 100644 --- a/README.md +++ b/README.md @@ -58,12 +58,22 @@ end Actually, you can call any [Enumerable][enumerable] method :D -If a Resource doesn't have friendly name you can always access it as a Hash: +If a resource doesn't have friendly name you can always access it as a hash: ```ruby api._links['http://myapi.org/rels/post_categories'] ``` +### Curies + +Curies are named tokens that you can define in the document and use to express curie relation URIs in a friendlier, more compact fashion. Hyperclient handles curies automatically and resolves them into full links. + +Access and expand curied links like any other link: + +```ruby +api._links['image:thumbnail'].expand(version: 'small') +``` + ### Embedded resources Accessing embedded resources is similar to accessing links: @@ -178,17 +188,9 @@ api.connection.use :http_cache There's also a PHP library named [HyperClient](https://github.com/FoxyCart/HyperClient), if that's what you were looking for :) -## TODO - -* Resource permissions: Using the `Allow` header Hyperclient should be able to - restrict the allowed method on a given `Resource`. -* Curie syntax support for links (see http://tools.ietf.org/html/draft-kelly-json-hal-03#section-8.2) -* Profile support for links - - ## Contributing -* [List of hyperclient contributors][contributors] +* [List of hyperclient contributors][contributors]. * Fork the project. * Make your feature addition or bug fix. diff --git a/lib/hyperclient/curie.rb b/lib/hyperclient/curie.rb new file mode 100644 index 0000000..bc0e4cb --- /dev/null +++ b/lib/hyperclient/curie.rb @@ -0,0 +1,49 @@ +require 'hyperclient/resource' + +module Hyperclient + # Internal: Curies are named tokens that you can define in the document and use + # to express curie relation URIs in a friendlier, more compact fashion. + # + class Curie + # Public: Initializes a new Curie. + # + # curie - The String with the URI of the curie. + # entry_point - The EntryPoint object to inject the cofnigutation. + def initialize(curie_hash, entry_point) + @curie_hash = curie_hash + @entry_point = entry_point + end + + # Public: Indicates if the curie is an URITemplate or a regular URI. + # + # Returns true if it is templated. + # Returns false if it not templated. + def templated? + !!@curie_hash['templated'] + end + + # Public: Returns the name property of the Curie + def name + @curie_hash['name'] + end + + # Public: Returns the href property of the Curie + def href + @curie_hash['href'] + end + + def inspect + "#<#{self.class.name} #{@curie_hash}>" + end + + # Public: Expands the Curie when is templated with the given variables. + # + # rel - The rel to expand. + # + # Returns a new expanded url. + def expand(rel) + return rel unless rel && templated? + href.gsub('{rel}', rel) if href + end + end +end diff --git a/lib/hyperclient/link_collection.rb b/lib/hyperclient/link_collection.rb index 2637973..d667742 100644 --- a/lib/hyperclient/link_collection.rb +++ b/lib/hyperclient/link_collection.rb @@ -1,5 +1,6 @@ require 'hyperclient/collection' require 'hyperclient/link' +require 'hyperclient/curie' module Hyperclient # Public: A wrapper class to easily acces the links in a Resource. @@ -12,13 +13,19 @@ module Hyperclient class LinkCollection < Collection # Public: Initializes a LinkCollection. # - # collection - The Hash with the links. + # collection - The Hash with the links. + # curies - Link curies. # entry_point - The EntryPoint object to inject the configuration. - def initialize(collection, entry_point) + def initialize(collection, curies, entry_point) fail "Invalid response for LinkCollection. The response was: #{collection.inspect}" if collection && !collection.respond_to?(:collect) + @curies = (curies || {}).reduce({}) do |hash, curie_hash| + curie = build_curie(curie_hash, entry_point) + hash.update(curie.name => curie) + end + @collection = (collection || {}).reduce({}) do |hash, (name, link)| - hash.update(name => build_link(name, link, entry_point)) + hash.update(name => build_link(name, link, @curies, entry_point)) end end @@ -27,16 +34,33 @@ def initialize(collection, entry_point) # Internal: Creates links from the response hash. # # link_or_links - A Hash or an Array of hashes with the links to build. - # entry_point - The EntryPoint object to inject the configuration. + # entry_point - The EntryPoint object to inject the configuration. + # curies - Optional curies for templated links. # # Returns a Link or an array of Links when given an Array. - def build_link(name, link_or_links, entry_point) + def build_link(name, link_or_links, curies, entry_point) return unless link_or_links - return Link.new(name, link_or_links, entry_point) unless link_or_links.respond_to?(:to_ary) - - link_or_links.map do |link| - build_link(name, link, entry_point) + if link_or_links.respond_to?(:to_ary) + link_or_links.map do |link| + build_link(name, link, curies, entry_point) + end + elsif (curie_parts = /(?[^:]+):(?.+)/.match(name)) + curie = curies[curie_parts[:ns]] + link_or_links['href'] = curie.expand(link_or_links['href']) if curie + Link.new(name, link_or_links, entry_point) + else + Link.new(name, link_or_links, entry_point) end end + + # Internal: Creates a curie from the response hash. + # + # curie_hash - A Hash with the curie. + # entry_point - The EntryPoint object to inject the configuration. + # + # Returns a Link or an array of Links when given an Array. + def build_curie(curie_hash, entry_point) + Curie.new(curie_hash, entry_point) + end end end diff --git a/lib/hyperclient/resource.rb b/lib/hyperclient/resource.rb index 05f86d9..c7ffc7b 100644 --- a/lib/hyperclient/resource.rb +++ b/lib/hyperclient/resource.rb @@ -33,7 +33,7 @@ class Resource # entry_point - The EntryPoint object to inject the configutation. def initialize(representation, entry_point, response = nil) representation = representation ? representation.dup : {} - @_links = LinkCollection.new(representation['_links'], entry_point) + @_links = LinkCollection.new(representation['_links'], representation['_curies'], entry_point) @_embedded = ResourceCollection.new(representation['_embedded'], entry_point) @_attributes = Attributes.new(representation) @_entry_point = entry_point diff --git a/test/fixtures/element.json b/test/fixtures/element.json index be608d2..bc06c7e 100644 --- a/test/fixtures/element.json +++ b/test/fixtures/element.json @@ -1,4 +1,11 @@ { + "_curies" : [ + { + "name": "image", + "href": "/images/{rel}", + "templated": true + } + ], "_links": { "self": { "href": "/productions/1" @@ -15,7 +22,11 @@ "href": "/gizmos/2" } ], - "null_link": null + "null_link": null, + "image:thumbnail": { + "href": "thumbnails/{version}.jpg", + "templated": true + } }, "title": "Real World ASP.NET MVC3", "description": "In this advanced, somewhat-opinionated production you'll get your very own startup off the ground using ASP.NET MVC 3...", diff --git a/test/hyperclient/collection_test.rb b/test/hyperclient/collection_test.rb index 4d66fb4..2546624 100644 --- a/test/hyperclient/collection_test.rb +++ b/test/hyperclient/collection_test.rb @@ -32,7 +32,7 @@ module Hyperclient name end - names.must_equal %w(_links title description permitted _hidden_attribute _embedded) + names.must_equal %w(_curies _links title description permitted _hidden_attribute _embedded) end describe '#to_hash' do diff --git a/test/hyperclient/curie_test.rb b/test/hyperclient/curie_test.rb new file mode 100644 index 0000000..c983990 --- /dev/null +++ b/test/hyperclient/curie_test.rb @@ -0,0 +1,34 @@ +require_relative '../test_helper' +require 'hyperclient/curie' +require 'hyperclient/entry_point' + +module Hyperclient + describe Curie do + let(:entry_point) do + EntryPoint.new('http://api.example.org/') + end + + describe 'templated?' do + it 'returns true if the curie is templated' do + curie = Curie.new({ 'name' => 'image', 'templated' => true }, entry_point) + + curie.templated?.must_equal true + end + + it 'returns false if the curie is not templated' do + curie = Curie.new({ 'name' => 'image' }, entry_point) + + curie.templated?.must_equal false + end + end + + let(:curie) do + Curie.new({ 'name' => 'image', 'href' => '/images/{?rel}', 'templated' => true }, entry_point) + end + describe '_name' do + it 'returns curie name' do + curie.name.must_equal 'image' + end + end + end +end diff --git a/test/hyperclient/link_collection_test.rb b/test/hyperclient/link_collection_test.rb index c24e79f..67b37e2 100644 --- a/test/hyperclient/link_collection_test.rb +++ b/test/hyperclient/link_collection_test.rb @@ -10,7 +10,7 @@ module Hyperclient end let(:links) do - LinkCollection.new(representation['_links'], entry_point) + LinkCollection.new(representation['_links'], representation['_curies'], entry_point) end it 'is a collection' do @@ -30,6 +30,27 @@ module Hyperclient links['gizmos'].must_be_kind_of Array end + describe 'plain link' do + let(:plain_link) { links.self } + it 'must be correct' do + plain_link._url.must_equal '/productions/1' + end + end + + describe 'templated link' do + let(:templated_link) { links.filter } + it 'must expand' do + templated_link._expand(filter: 'gizmos')._url.must_equal '/productions/1?categories=gizmos' + end + end + + describe 'curied link' do + let(:curied_link) { links['image:thumbnail'] } + it 'must expand' do + curied_link._expand(version: 'small')._url.must_equal '/images/thumbnails/small.jpg' + end + end + describe 'array of links' do let(:gizmos) { links.gizmos } diff --git a/test/hyperclient/resource_test.rb b/test/hyperclient/resource_test.rb index 471f186..ed939e2 100644 --- a/test/hyperclient/resource_test.rb +++ b/test/hyperclient/resource_test.rb @@ -7,7 +7,7 @@ module Hyperclient describe 'initialize' do it 'initializes its links' do - LinkCollection.expects(:new).with({ 'self' => { 'href' => '/orders/523' } }, entry_point) + LinkCollection.expects(:new).with({ 'self' => { 'href' => '/orders/523' } }, nil, entry_point) Resource.new({ '_links' => { 'self' => { 'href' => '/orders/523' } } }, entry_point) end