diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9c132f0..40535d5 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,5 +1,5 @@ # This configuration was generated by `rubocop --auto-gen-config` -# on 2014-09-17 21:23:50 -0400 using RuboCop version 0.26.0. +# on 2014-09-18 12:21:47 -0400 using RuboCop version 0.26.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -8,28 +8,36 @@ # Offense count: 1 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 106 + Max: 118 -# Offense count: 36 +# Offense count: 2 +Metrics/CyclomaticComplexity: + Max: 8 + +# Offense count: 39 # Configuration parameters: AllowURI. Metrics/LineLength: - Max: 140 + Max: 145 # Offense count: 1 # Configuration parameters: CountComments. Metrics/MethodLength: Max: 14 +# Offense count: 2 +Metrics/PerceivedComplexity: + Max: 9 + # Offense count: 1 Style/AsciiComments: Enabled: false -# Offense count: 3 +# Offense count: 2 # Configuration parameters: EnforcedStyle, SupportedStyles. Style/ClassAndModuleChildren: Enabled: false -# Offense count: 15 +# Offense count: 14 Style/Documentation: Enabled: false @@ -41,7 +49,7 @@ Style/DoubleNegation: Style/Lambda: Enabled: false -# Offense count: 6 +# Offense count: 5 # Configuration parameters: MaxSlashes. Style/RegexpLiteral: Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ca5f50..13301a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,21 @@ == Next + +* backwards incompatible changes + * All navigational structure methods, including `links`, `get` or `post`, have been renamed to `_link`, `_get`, or `_post` respectively (by @dblock). + * enhancements * [#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). + +* bug fixes + * Nothing. + +* deprecations + * Nothing. == 0.3.1 + * backwards incompatible changes * Nothing. diff --git a/Readme.md b/README.md similarity index 69% rename from Readme.md rename to README.md index f4bf4e7..7d33f40 100644 --- a/Readme.md +++ b/README.md @@ -16,12 +16,11 @@ Example API client: ```ruby api = Hyperclient.new('http://myapp.com/api').tap do |api| api.digest_auth('user', 'password') - api.headers.update({'accept-encoding' => 'deflate, gzip'}) + api.headers.update('accept-encoding' => 'deflate, gzip') end ``` -By default, Hyperclient adds `application/json` as `Content-Type` and `Accept` -headers. It will also sent requests as JSON and parse JSON responses. +By default, Hyperclient adds `application/json` as `Content-Type` and `Accept` headers. It will also sent requests as JSON and parse JSON responses. [More examples][examples] @@ -38,15 +37,22 @@ Hyperclient will try to fetch and discover the resources from your API. Accessing the links for a given resource is quite straightforward: ```ruby -api.links.posts_categories +api._links.posts_categories +# => # +``` + +Or omit `_links`, which will look for a link called "posts_categories" by default: + +```ruby +api.posts_categories # => # ``` You can also iterate between all the links: ```ruby -api.links.each do |name, link| - puts name, link.url +api._links.each do |name, link| + puts name, link._url end ``` @@ -55,7 +61,7 @@ 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: ```ruby -api.links['http://myapi.org/rels/post_categories'] +api._links['http://myapi.org/rels/post_categories'] ``` ### Embedded resources @@ -63,7 +69,13 @@ api.links['http://myapi.org/rels/post_categories'] Accessing embedded resources is similar to accessing links: ```ruby -api.embedded.posts +api._embedded.posts +``` + +Or omit the `_embedded` keyword. By default Hyperclient will look for a "posts" link, then for an embedded "posts" collection: + +```ruby +api.posts ``` And you can also iterate between them: @@ -80,10 +92,25 @@ You can even chain different calls (this also applies for links): api.embedded.posts.first.links.author ``` +Or omit the navigational structures: + +```ruby +api.posts.first.author +``` + +If you have a named link that retrieves an embedded collection of the same name, you can collapse the nested reference. The following statements produce identical results: + +```ruby +api.links.posts.embedded.posts.first +``` + +```ruby +api.posts.first +``` + ### Attributes -Not only you might have links and embedded resources in a Resource, but also -its attributes: +Not only you might have links and embedded resources in a Resource, but also its attributes: ```ruby api.embedded.posts.first.attributes @@ -95,6 +122,9 @@ api.embedded.posts.first.attributes You can access the attribute values via attribute methods, or as a hash: ```ruby +api.posts.first.title +# => 'Linting the hell out of your Ruby classes with Pelusa' + api.embedded.posts.first.attributes.title # => 'Linting the hell out of your Ruby classes with Pelusa' @@ -107,35 +137,38 @@ api.embedded.posts.first.attributes.fetch('title') ### HTTP -OK, navigating an API is really cool, but you may want to actually do something -with it, right? +OK, navigating an API is really cool, but you may want to actually do something with it, right? -Hyperclient uses [Faraday][faraday] under the hood to perform HTTP calls. You can -call any valid HTTP method on any Resource: +Hyperclient uses [Faraday][faraday] under the hood to perform HTTP calls. You can call any valid HTTP method on any Resource: ```ruby -post = api.embedded.posts.first -post.get -post.head -post.put({title: 'New title'}) -post.patch({title: 'New title'}) -post.delete -post.options - -posts = api.links.posts -posts.post({title: "I'm a blogger!", body: 'Wohoo!!'}) +post = api._embedded.posts.first +post._get +post._head +post._put(title: 'New title') +post._patch(title: 'New title') +post._delete +post._options + +posts = api._links.posts +posts._post(title: "I'm a blogger!", body: 'Wohoo!!') ``` If you have a templated link you can expand it like so: ```ruby -api.links.post.expand(:id => 3).first +api._links.post._expand(id: 3).first +# => # +``` + +You can omit the "_expand" keyword. + +```ruby +api.post(id: 3).first # => # ``` -You can access the Faraday connection (to add middlewares or do whatever -you want) by calling `connection` on the entry point. As an example, you could use the [faraday-http-cache-middleware](https://github.com/plataformatec/faraday-http-cache) -: +You can access the Faraday connection (to add middlewares or do whatever you want) by calling `connection` on the entry point. As an example, you could use the [faraday-http-cache-middleware](https://github.com/plataformatec/faraday-http-cache): ```ruby api.connection.use :http_cache @@ -168,7 +201,7 @@ There's also a PHP library named [HyperClient](https://github.com/FoxyCart/Hyper ## License -MIT License. Copyright 2012 [Codegram Technologies][codegram] +MIT License. Copyright 2012-2014 [Codegram Technologies][codegram] [hal]: http://stateless.co/hal_specification.html [contributors]: https://github.com/codegram/hyperclient/contributors diff --git a/examples/cyberscore.rb b/examples/cyberscore.rb index 52dcf0e..32dd7f0 100644 --- a/examples/cyberscore.rb +++ b/examples/cyberscore.rb @@ -44,7 +44,7 @@ def print_links(links) if link.is_a?(Array) print_links(link) else - puts %(Found "#{name}" at "#{link.url}" ) + puts %(Found "#{name}" at "#{link._url}" ) end end end diff --git a/examples/hal_shop.rb b/examples/hal_shop.rb index 1a5e04a..03006ec 100644 --- a/examples/hal_shop.rb +++ b/examples/hal_shop.rb @@ -4,7 +4,7 @@ def print_resources(resources) resources.each do |name, resource| begin - puts %(Found #{name} at #{resource.url}) + puts %(Found #{name} at #{resource._url}) rescue puts %(Found #{name}) end diff --git a/features/steps/api_navigation.rb b/features/steps/api_navigation.rb index 08217ba..a77bd4b 100644 --- a/features/steps/api_navigation.rb +++ b/features/steps/api_navigation.rb @@ -2,15 +2,15 @@ class Spinach::Features::ApiNavigation < Spinach::FeatureSteps include API step 'I should be able to navigate to posts and authors' do - api.links.posts.resource - api.links['api:authors'].resource + api._links.posts._resource + api._links['api:authors']._resource assert_requested :get, 'http://api.example.org/posts' assert_requested :get, 'http://api.example.org/authors' end step 'I search for a post with a templated link' do - api.links.search.expand(q: 'something').resource + api._links.search._expand(q: 'something')._resource end step 'the API should receive the request with all the params' do @@ -18,16 +18,16 @@ class Spinach::Features::ApiNavigation < Spinach::FeatureSteps end step 'I load a single post' do - @post = api.links.posts.links.last_post + @post = api._links.posts._links.last_post end step 'I should be able to access it\'s title and body' do - @post.attributes.title.wont_equal nil - @post.attributes.body.wont_equal nil + @post._attributes.title.wont_equal nil + @post._attributes.body.wont_equal nil end step 'I should also be able to access it\'s embedded comments' do - comment = @post.embedded.comments.first - comment.attributes.title.wont_equal nil + comment = @post._embedded.comments.first + comment._attributes.title.wont_equal nil end end diff --git a/features/steps/default_config.rb b/features/steps/default_config.rb index 8bb441c..ad0c888 100644 --- a/features/steps/default_config.rb +++ b/features/steps/default_config.rb @@ -11,7 +11,7 @@ class Spinach::Features::DefaultConfig < Spinach::FeatureSteps step 'I send some data to the API' do stub_request(:post, 'http://api.example.org/posts') - assert_equal 200, api.links.posts.post(title: 'My first blog post').status + assert_equal 200, api._links.posts._post(title: 'My first blog post').status end step 'it should have been encoded as JSON' do @@ -19,11 +19,11 @@ class Spinach::Features::DefaultConfig < Spinach::FeatureSteps end step 'I get some data from the API' do - @posts = api.links.posts + @posts = api._links.posts end step 'it should have been parsed as JSON' do - @posts.attributes.total_posts.to_i.must_equal 9 - @posts.attributes['total_posts'].to_i.must_equal 9 + @posts._attributes.total_posts.to_i.must_equal 9 + @posts._attributes['total_posts'].to_i.must_equal 9 end end diff --git a/features/support/api.rb b/features/support/api.rb index 77e41a1..525a60e 100644 --- a/features/support/api.rb +++ b/features/support/api.rb @@ -15,7 +15,7 @@ def api end step 'I connect to the API' do - api.links + api._links end after do diff --git a/lib/hyperclient/entry_point.rb b/lib/hyperclient/entry_point.rb index ef25191..f1ff4bc 100644 --- a/lib/hyperclient/entry_point.rb +++ b/lib/hyperclient/entry_point.rb @@ -27,7 +27,7 @@ def initialize(url) # # Returns a Faraday::Connection. def connection - @connection ||= Faraday.new(url, { headers: default_headers }, &default_faraday_block) + @connection ||= Faraday.new(_url, { headers: default_headers }, &default_faraday_block) end private diff --git a/lib/hyperclient/link.rb b/lib/hyperclient/link.rb index 097dbad..1b4d885 100644 --- a/lib/hyperclient/link.rb +++ b/lib/hyperclient/link.rb @@ -3,16 +3,18 @@ require 'futuroscope' module Hyperclient - # Internal: The Link is used to let a Resource interact with the API. + # Internal: The Link is used to let a Resource interact with the API. # class Link # Public: Initializes a new Link. # + # key - The key or name of the link. # link - The String with the URI of the link. # entry_point - The EntryPoint object to inject the cofnigutation. # uri_variables - The optional Hash with the variables to expand the link # if it is templated. - def initialize(link, entry_point, uri_variables = nil) + def initialize(key, link, entry_point, uri_variables = nil) + @key = key @link = link @entry_point = entry_point @uri_variables = uri_variables @@ -22,7 +24,7 @@ def initialize(link, entry_point, uri_variables = nil) # # Returns true if it is templated. # Returns false if it not templated. - def templated? + def _templated? !!@link['templated'] end @@ -31,62 +33,62 @@ def templated? # uri_variables - The Hash with the variables to expand the URITemplate. # # Returns a new Link with the expanded variables. - def expand(uri_variables) - self.class.new(@link, @entry_point, uri_variables) + def _expand(uri_variables) + self.class.new(@key, @link, @entry_point, uri_variables) end # Public: Returns the url of the Link. # # Raises MissingURITemplateVariables if the Link is templated but there are # no uri variables to expand it. - def url - return @link['href'] unless templated? + def _url + return @link['href'] unless _templated? fail MissingURITemplateVariablesException if @uri_variables.nil? - @url ||= uri_template.expand(@uri_variables) + @url ||= _uri_template.expand(@uri_variables) end # Public: Returns an array of variables from the URITemplate. # # Returns an empty array for regular URIs. - def variables - uri_template.variables + def _variables + _uri_template.variables end # Public: Returns the type property of the Link - def type + def _type @link['type'] end # Public: Returns the name property of the Link - def name + def _name @link['name'] end # Public: Returns the deprecation property of the Link - def deprecation + def _deprecation @link['deprecation'] end # Public: Returns the profile property of the Link - def profile + def _profile @link['profile'] end # Public: Returns the title property of the Link - def title + def _title @link['title'] end # Public: Returns the hreflang property of the Link - def hreflang + def _hreflang @link['hreflang'] end # Public: Returns the Resource which the Link is pointing to. - def resource + def _resource @resource ||= begin - response = get + response = _get if response.success? Resource.new(response.body, @entry_point, response) @@ -96,65 +98,67 @@ def resource end end - def connection + def _connection @entry_point.connection end - def get + def _get Futuroscope::Future.new do - connection.get(url) + _connection.get(_url) end end - def options + def _options Futuroscope::Future.new do - connection.run_request(:options, url, nil, nil) + _connection.run_request(:options, _url, nil, nil) end end - def head + def _head Futuroscope::Future.new do - connection.head(url) + _connection.head(_url) end end - def delete + def _delete Futuroscope::Future.new do - connection.delete(url) + _connection.delete(_url) end end - def post(params = {}) + def _post(params = {}) Futuroscope::Future.new do - connection.post(url, params) + _connection.post(_url, params) end end - def put(params = {}) + def _put(params = {}) Futuroscope::Future.new do - connection.put(url, params) + _connection.put(_url, params) end end - def patch(params = {}) + def _patch(params = {}) Futuroscope::Future.new do - connection.patch(url, params) + _connection.patch(_url, params) end end def inspect - "#<#{self.class.name} #{@link}>" + "#<#{self.class.name}(#{@key}) #{@link}>" end private # Internal: Delegate the method to the API if it exists. # - # This allows `api.links.posts.embedded` instead of - # `api.links.posts.resource.embedded` + # This allows `api.posts` instead of `api.links.posts.embedded` def method_missing(method, *args, &block) - if resource.respond_to?(method) - resource.send(method, *args, &block) + if @key && _resource.respond_to?(@key) && (delegate = _resource.send(@key)) && delegate.respond_to?(method.to_s) + # named.named becomes named + delegate.send(method, *args, &block) + elsif _resource.respond_to?(method.to_s) + _resource.send(method, *args, &block) else super end @@ -163,7 +167,11 @@ def method_missing(method, *args, &block) # Internal: Accessory method to allow the link respond to the # methods that will hit method_missing. def respond_to_missing?(method, _include_private = false) - resource.respond_to?(method.to_s) + if @key && _resource.respond_to?(@key) && (delegate = _resource.send(@key)) && delegate.respond_to?(method.to_s) + true + else + _resource.respond_to?(method.to_s) + end end # Internal: avoid delegating to resource @@ -175,7 +183,7 @@ def to_ary end # Internal: Memoization for a URITemplate instance - def uri_template + def _uri_template @uri_template ||= URITemplate.new(@link['href']) end end diff --git a/lib/hyperclient/link_collection.rb b/lib/hyperclient/link_collection.rb index 768f474..2637973 100644 --- a/lib/hyperclient/link_collection.rb +++ b/lib/hyperclient/link_collection.rb @@ -18,7 +18,7 @@ def initialize(collection, entry_point) fail "Invalid response for LinkCollection. The response was: #{collection.inspect}" if collection && !collection.respond_to?(:collect) @collection = (collection || {}).reduce({}) do |hash, (name, link)| - hash.update(name => build_link(link, entry_point)) + hash.update(name => build_link(name, link, entry_point)) end end @@ -30,12 +30,12 @@ def initialize(collection, entry_point) # entry_point - The EntryPoint object to inject the configuration. # # Returns a Link or an array of Links when given an Array. - def build_link(link_or_links, entry_point) + def build_link(name, link_or_links, entry_point) return unless link_or_links - return Link.new(link_or_links, entry_point) unless link_or_links.respond_to?(:to_ary) + 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(link, entry_point) + build_link(name, link, entry_point) end end end diff --git a/lib/hyperclient/resource.rb b/lib/hyperclient/resource.rb index f07fbb1..05f86d9 100644 --- a/lib/hyperclient/resource.rb +++ b/lib/hyperclient/resource.rb @@ -10,22 +10,22 @@ class Resource extend Forwardable # Public: Returns the attributes of the Resource as Attributes. - attr_reader :attributes + attr_reader :_attributes # Public: Returns the links of the Resource as a LinkCollection. - attr_reader :links + attr_reader :_links # Public: Returns the embedded resource of the Resource as a # ResourceCollection. - attr_reader :embedded + attr_reader :_embedded # Public: Returns the response object for the HTTP request that created this # resource, if one exists. - attr_reader :response + attr_reader :_response # Public: Delegate all HTTP methods (get, post, put, delete, options and # head) to its self link. - def_delegators :self_link, :get, :post, :put, :delete, :options, :head + def_delegators :_self_link, :_get, :_post, :_put, :_delete, :_options, :_head # Public: Initializes a Resource. # @@ -33,31 +33,58 @@ 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) - @embedded = ResourceCollection.new(representation['_embedded'], entry_point) - @attributes = Attributes.new(representation) - @entry_point = entry_point - @response = response + @_links = LinkCollection.new(representation['_links'], entry_point) + @_embedded = ResourceCollection.new(representation['_embedded'], entry_point) + @_attributes = Attributes.new(representation) + @_entry_point = entry_point + @_response = response end def inspect - "#<#{self.class.name} self_link:#{self_link.inspect} attributes:#{@attributes.inspect}>" + "#<#{self.class.name} self_link:#{_self_link.inspect} attributes:#{@_attributes.inspect}>" end - def success? - response && response.success? + def _success? + _response && _response.success? end - def status - response && response.status + def _status + _response && _response.status end private # Internal: Returns the self Link of the Resource. Used to handle the HTTP # methods. - def self_link - @links['self'] + def _self_link + @_links['self'] + end + + private + + # Internal: Delegate the method to various elements of the resource. + # + # This allows `api.posts` instead of `api.links.posts.resource` + # as well as api.posts(id: 1) assuming posts is a link. + def method_missing(method, *args, &block) + if args.any? && args.first.is_a?(Hash) + _links.send(method, [], &block)._expand(*args) + else + [:_attributes, :_embedded, :_links].each do |target| + target = send(target) + return target.send(method, *args, &block) if target.respond_to?(method.to_s) + end + super + end + end + + # Internal: Accessory method to allow the resource respond to + # methods that will hit method_missing. + def respond_to_missing?(method, include_private = false) + [:_attributes, :_embedded, :_links].each do |target| + return true if send(target).respond_to?(method, include_private) + end + false end end end diff --git a/test/hyperclient/entry_point_test.rb b/test/hyperclient/entry_point_test.rb index 670a2e5..8d29f11 100644 --- a/test/hyperclient/entry_point_test.rb +++ b/test/hyperclient/entry_point_test.rb @@ -28,7 +28,7 @@ module Hyperclient describe 'initialize' do it 'sets a Link with the entry point url' do - entry_point.url.must_equal 'http://my.api.org' + entry_point._url.must_equal 'http://my.api.org' end end end diff --git a/test/hyperclient/link_test.rb b/test/hyperclient/link_test.rb index 1ef2c9c..f4be44e 100644 --- a/test/hyperclient/link_test.rb +++ b/test/hyperclient/link_test.rb @@ -11,88 +11,88 @@ module Hyperclient %w(type deprecation name profile title hreflang).each do |prop| describe prop do it 'returns the property value' do - link = Link.new({ prop => 'value' }, entry_point) - link.send(prop).must_equal 'value' + link = Link.new('key', { prop => 'value' }, entry_point) + link.send("_#{prop}").must_equal 'value' end it 'returns nil if the property is not present' do - link = Link.new({}, entry_point) - link.send(prop).must_equal nil + link = Link.new('key', {}, entry_point) + link.send("_#{prop}").must_equal nil end end end - describe 'templated?' do + describe '_templated?' do it 'returns true if the link is templated' do - link = Link.new({ 'templated' => true }, entry_point) + link = Link.new('key', { 'templated' => true }, entry_point) - link.templated?.must_equal true + link._templated?.must_equal true end it 'returns false if the link is not templated' do - link = Link.new({}, entry_point) + link = Link.new('key', {}, entry_point) - link.templated?.must_equal false + link._templated?.must_equal false end end - describe 'variables' do + describe '_variables' do it 'returns a list of required variables' do - link = Link.new({ 'href' => '/orders{?id,owner}', 'templated' => true }, entry_point) + link = Link.new('key', { 'href' => '/orders{?id,owner}', 'templated' => true }, entry_point) - link.variables.must_equal %w(id owner) + link._variables.must_equal %w(id owner) end it 'returns an empty array for untemplated links' do - link = Link.new({ 'href' => '/orders' }, entry_point) + link = Link.new('key', { 'href' => '/orders' }, entry_point) - link.variables.must_equal [] + link._variables.must_equal [] end end - describe 'expand' do + describe '_expand' do it 'buils a Link with the templated URI representation' do - link = Link.new({ 'href' => '/orders{?id}', 'templated' => true }, entry_point) + link = Link.new('key', { 'href' => '/orders{?id}', 'templated' => true }, entry_point) - Link.expects(:new).with(anything, entry_point, id: '1') - link.expand(id: '1') + Link.expects(:new).with('key', anything, entry_point, id: '1') + link._expand(id: '1') end it 'raises if no uri variables are given' do - link = Link.new({ 'href' => '/orders{?id}', 'templated' => true }, entry_point) - lambda { link.expand }.must_raise ArgumentError + link = Link.new('key', { 'href' => '/orders{?id}', 'templated' => true }, entry_point) + lambda { link._expand }.must_raise ArgumentError end end - describe 'url' do + describe '_url' do it 'raises when missing required uri_variables' do - link = Link.new({ 'href' => '/orders{?id}', 'templated' => true }, entry_point) + link = Link.new('key', { 'href' => '/orders{?id}', 'templated' => true }, entry_point) - lambda { link.url }.must_raise MissingURITemplateVariablesException + lambda { link._url }.must_raise MissingURITemplateVariablesException end it 'expands an uri template with variables' do - link = Link.new({ 'href' => '/orders{?id}', 'templated' => true }, entry_point, id: 1) + link = Link.new('key', { 'href' => '/orders{?id}', 'templated' => true }, entry_point, id: 1) - link.url.must_equal '/orders?id=1' + link._url.must_equal '/orders?id=1' end it 'returns the link when no uri template' do - link = Link.new({ 'href' => '/orders' }, entry_point) - link.url.must_equal '/orders' + link = Link.new('key', { 'href' => '/orders' }, entry_point) + link._url.must_equal '/orders' end end - describe 'resource' do + describe '_resource' do it 'builds a resource with the link href representation' do mock_response = mock(body: {}, success?: true) Resource.expects(:new).with({}, entry_point, mock_response) - link = Link.new({ 'href' => '/' }, entry_point) - link.expects(:get).returns(mock_response) + link = Link.new('key', { 'href' => '/' }, entry_point) + link.expects(:_get).returns(mock_response) - link.resource + link._resource end it 'has an empty body when the response fails' do @@ -100,100 +100,100 @@ module Hyperclient Resource.expects(:new).with(nil, entry_point, mock_response) - link = Link.new({ 'href' => '/' }, entry_point) - link.expects(:get).returns(mock_response) + link = Link.new('key', { 'href' => '/' }, entry_point) + link.expects(:_get).returns(mock_response) - link.resource + link._resource end end - describe 'connection' do + describe '_connection' do it 'returns the entry point connection' do - Link.new({}, entry_point).connection.must_equal entry_point.connection + Link.new('key', {}, entry_point)._connection.must_equal entry_point.connection end end describe 'get' do it 'sends a GET request with the link url' do - link = Link.new({ 'href' => '/productions/1' }, entry_point) + link = Link.new('key', { 'href' => '/productions/1' }, entry_point) entry_point.connection.expects(:get).with('/productions/1') - link.get.inspect + link._get.inspect end end - describe 'options' do + describe '_options' do it 'sends a OPTIONS request with the link url' do - link = Link.new({ 'href' => '/productions/1' }, entry_point) + link = Link.new('key', { 'href' => '/productions/1' }, entry_point) entry_point.connection.expects(:run_request).with(:options, '/productions/1', nil, nil) - link.options.inspect + link._options.inspect end end - describe 'head' do + describe '_head' do it 'sends a HEAD request with the link url' do - link = Link.new({ 'href' => '/productions/1' }, entry_point) + link = Link.new('key', { 'href' => '/productions/1' }, entry_point) entry_point.connection.expects(:head).with('/productions/1') - link.head.inspect + link._head.inspect end end - describe 'delete' do + describe '_delete' do it 'sends a DELETE request with the link url' do - link = Link.new({ 'href' => '/productions/1' }, entry_point) + link = Link.new('key', { 'href' => '/productions/1' }, entry_point) entry_point.connection.expects(:delete).with('/productions/1') - link.delete.inspect + link._delete.inspect end end - describe 'post' do - let(:link) { Link.new({ 'href' => '/productions/1' }, entry_point) } + describe '_post' do + let(:link) { Link.new('key', { 'href' => '/productions/1' }, entry_point) } it 'sends a POST request with the link url and params' do entry_point.connection.expects(:post).with('/productions/1', 'foo' => 'bar') - link.post('foo' => 'bar').inspect + link._post('foo' => 'bar').inspect end it 'defaults params to an empty hash' do entry_point.connection.expects(:post).with('/productions/1', {}) - link.post.inspect + link._post.inspect end end - describe 'put' do - let(:link) { Link.new({ 'href' => '/productions/1' }, entry_point) } + describe '_put' do + let(:link) { Link.new('key', { 'href' => '/productions/1' }, entry_point) } it 'sends a PUT request with the link url and params' do entry_point.connection.expects(:put).with('/productions/1', 'foo' => 'bar') - link.put('foo' => 'bar').inspect + link._put('foo' => 'bar').inspect end it 'defaults params to an empty hash' do entry_point.connection.expects(:put).with('/productions/1', {}) - link.put.inspect + link._put.inspect end end - describe 'patch' do - let(:link) { Link.new({ 'href' => '/productions/1' }, entry_point) } + describe '_patch' do + let(:link) { Link.new('key', { 'href' => '/productions/1' }, entry_point) } it 'sends a PATCH request with the link url and params' do entry_point.connection.expects(:patch).with('/productions/1', 'foo' => 'bar') - link.patch('foo' => 'bar').inspect + link._patch('foo' => 'bar').inspect end it 'defaults params to an empty hash' do entry_point.connection.expects(:patch).with('/productions/1', {}) - link.patch.inspect + link._patch.inspect end end describe 'inspect' do it 'outputs a custom-friendly output' do - link = Link.new({ 'href' => '/productions/1' }, 'foo') + link = Link.new('key', { 'href' => '/productions/1' }, 'foo') link.inspect.must_include 'Link' link.inspect.must_include '"href"=>"/productions/1"' @@ -201,34 +201,53 @@ module Hyperclient end describe 'method_missing' do - before do - stub_request(:get, 'http://myapi.org/orders') - .to_return(body: '{"resource": "This is the resource"}') - Resource.stubs(:new).returns(resource) + describe 'delegation' do + it 'delegates when link key matches' do + resource = Resource.new({ '_links' => { 'orders' => { 'href' => '/orders' } } }, entry_point) + stub_request(:get, 'http://api.example.org/orders').to_return(body: { '_embedded' => { 'orders' => [{ 'id' => 1 }] } }) + resource.orders._embedded.orders.first.id.must_equal 1 + resource.orders.first.id.must_equal 1 + end + + it "doesn't delegate when link key doesn't match" do + resource = Resource.new({ '_links' => { 'foos' => { 'href' => '/orders' } } }, entry_point) + stub_request(:get, 'http://api.example.org/orders').to_return(body: { '_embedded' => { 'orders' => [{ 'id' => 1 }] } }) + resource.foos._embedded.orders.first.id.must_equal 1 + resource.foos.first.must_equal nil + end end - let(:link) { Link.new({ 'href' => 'http://myapi.org/orders' }, entry_point) } - let(:resource) { mock('Resource') } + describe 'resource' do + before do + stub_request(:get, 'http://myapi.org/orders') + .to_return(body: '{"resource": "This is the resource"}') + Resource.stubs(:new).returns(resource) + end - it 'delegates unkown methods to the resource' do - Resource.expects(:new).returns(resource).at_least_once - resource.expects(:embedded) + let(:resource) { mock('Resource') } + let(:link) { Link.new('orders', { 'href' => 'http://myapi.org/orders' }, entry_point) } - link.embedded - end + it 'delegates unkown methods to the resource' do + Resource.expects(:new).returns(resource).at_least_once + resource.expects(:embedded) - it 'raises an error when the method does not exist in the resource' do - lambda { link.this_method_does_not_exist }.must_raise(NoMethodError) - end + link.embedded + end - it 'responds to missing methods' do - resource.expects(:respond_to?).with('embedded').returns(true) - link.respond_to?(:embedded).must_equal true - end + it 'raises an error when the method does not exist in the resource' do + lambda { link.this_method_does_not_exist }.must_raise NoMethodError + end - it 'does not delegate to_ary to resource' do - resource.expects(:to_ary).never - [[link, link]].flatten.must_equal [link, link] + it 'responds to missing methods' do + resource.expects(:respond_to?).with('orders').returns(false) + resource.expects(:respond_to?).with('embedded').returns(true) + link.respond_to?(:embedded).must_equal true + end + + it 'does not delegate to_ary to resource' do + resource.expects(:to_ary).never + [[link, link]].flatten.must_equal [link, link] + end end end end diff --git a/test/hyperclient/resource_test.rb b/test/hyperclient/resource_test.rb index f83e2d5..471f186 100644 --- a/test/hyperclient/resource_test.rb +++ b/test/hyperclient/resource_test.rb @@ -29,7 +29,7 @@ module Hyperclient resource = Resource.new(mock_response.body, entry_point, mock_response) - resource.response.must_equal mock_response + resource._response.must_equal mock_response end it 'does not mutate the response.body' do @@ -38,9 +38,17 @@ module Hyperclient resource = Resource.new(mock_response.body, entry_point, mock_response) - resource.response.body.must_equal body + resource._response.body.must_equal body end + end + describe '_links' do + it '_expand' do + resource = Resource.new({ '_links' => { 'orders' => { 'href' => '/orders/{id}', 'templated' => true } } }, entry_point) + resource._links.orders._expand(id: 1)._url.must_equal '/orders/1' + resource.orders._expand(id: 1)._url.must_equal '/orders/1' + resource.orders(id: 1)._url.must_equal '/orders/1' + end end describe 'accessors' do @@ -50,34 +58,58 @@ module Hyperclient describe 'links' do it 'returns a LinkCollection' do - resource.links.must_be_kind_of LinkCollection + resource._links.must_be_kind_of LinkCollection end end describe 'attributes' do it 'returns a Attributes' do - resource.attributes.must_be_kind_of Attributes + resource._attributes.must_be_kind_of Attributes end end describe 'embedded' do it 'returns a ResourceCollection' do - resource.embedded.must_be_kind_of ResourceCollection + resource._embedded.must_be_kind_of ResourceCollection + end + end + + describe 'method_missing' do + it 'delegates to attributes' do + resource._attributes.expects(:foo).returns('bar') + resource.foo.must_equal 'bar' + end + + it 'delegates to links' do + resource._links.expects(:foo).returns('bar') + resource.foo.must_equal 'bar' + end + + it 'delegates to embedded' do + resource._embedded.expects(:foo).returns('bar') + resource.foo.must_equal 'bar' + end + + it 'delegates to attributes, links, embedded' do + resource._attributes.expects('respond_to?').with('foo').returns(false) + resource._links.expects('respond_to?').with('foo').returns(false) + resource._embedded.expects('respond_to?').with('foo').returns(false) + lambda { resource.foo }.must_raise NoMethodError end end end it 'uses its self Link to handle HTTP connections' do self_link = mock('Self Link') - self_link.expects(:get) + self_link.expects(:_get) LinkCollection.expects(:new).returns('self' => self_link) resource = Resource.new({}, entry_point) - resource.get + resource._get end - describe '.success?' do + describe '._success?' do describe 'with a response object' do let(:resource) do Resource.new({}, entry_point, mock_response) @@ -88,7 +120,7 @@ module Hyperclient end it 'proxies to the response object' do - resource.success?.must_equal true + resource._success?.must_equal true end end @@ -98,12 +130,12 @@ module Hyperclient end it 'returns nil' do - resource.success?.must_be_nil + resource._success?.must_be_nil end end end - describe '.status' do + describe '._status' do describe 'with a response object' do let(:resource) do Resource.new({}, entry_point, mock_response) @@ -114,7 +146,7 @@ module Hyperclient end it 'proxies to the response object' do - resource.status.must_equal 200 + resource._status.must_equal 200 end end @@ -124,7 +156,7 @@ module Hyperclient end it 'returns nil' do - resource.status.must_be_nil + resource._status.must_be_nil end end end