diff --git a/History.txt b/History.txt index edb0cea..98c9e01 100644 --- a/History.txt +++ b/History.txt @@ -1,3 +1,11 @@ +== 0.4.0 + +* Positional parameters for path-to/described_routes (only), e.g. delicious.posts('ruby'); app.users['dojo'] + +== 0.3.1 2009-05-08 + +* Add gem dependency on described_routes + == 0.3.0 2009-05-08 * Add http_options to Application, to be passed through to the HTTP client with requests diff --git a/README.rdoc b/README.rdoc index d5e8537..b49f12d 100644 --- a/README.rdoc +++ b/README.rdoc @@ -18,10 +18,17 @@ Create a client application configured from a server that supports described_rou app = PathTo::DescribedRoutes::Application.new(:json => Net::HTTP.get(URI.parse("http://example.com/described_routes.json"))) - app.users["user_id" => "dojo"].articles.recent #=> http://example.com/users/dojo/articles/recent - app.users["user_id" => "dojo"].articles.recent.get #=> "..." + app.users["dojo"].articles.recent + #=> http://example.com/users/dojo/articles/recent + app.users["dojo"].articles.recent.get + #=> "..." + + app.users["dojo"].articles.recent["format" => "json"] + #=> http://example.com/users/dojo/articles/recent.json + app.users["dojo"].articles.recent.get + #=> [...] -See examples/delicious.rb for an example based on a partial YAML-based description of the Delicious API held locally. +See examples/delicious.rb for an example based on a partial YAML-based description of the Delicious API. === Local configuration diff --git a/Rakefile b/Rakefile index deedabb..4fd1a6a 100644 --- a/Rakefile +++ b/Rakefile @@ -13,6 +13,7 @@ $hoe = Hoe.new('path-to', PathTo::VERSION) do |p| p.extra_deps = [ ['httparty','>= 0.4.2'], ['addressable','>= 2.0.2'], + ['described_routes','>= 0.3.6'] ] p.extra_dev_deps = [ ['newgem', ">= #{::Newgem::VERSION}"] diff --git a/examples/delicious.rb b/examples/delicious.rb index 7d7f79c..b557247 100644 --- a/examples/delicious.rb +++ b/examples/delicious.rb @@ -1,5 +1,4 @@ -# Adapted from jnunemaker/httparty/examples/delicious.rb to demonstrate path-to's metadata-driven REST client API capability -# For more information see http://positiveincline.com/?tag=path-to +# Adapted from jnunemaker/httparty/examples/delicious.rb to demonstrate path-to's metadata-driven client API capability require 'path-to/described_routes' require 'pp' @@ -38,6 +37,6 @@ :username => config['username'], :password => config['password']}}) -pp delicious.posts['tag' => 'ruby'].get -pp delicious.posts['tag' => 'ruby'].recent['count' => '5'].get +pp delicious.posts('ruby').get +pp delicious.posts('ruby').recent('count' => '5').get delicious.recent_posts.get['posts']['post'].each { |post| puts post['href'] } diff --git a/lib/path-to.rb b/lib/path-to.rb index 6af5c5f..c9ed834 100644 --- a/lib/path-to.rb +++ b/lib/path-to.rb @@ -1,5 +1,5 @@ module PathTo - VERSION = "0.3.0" + VERSION = "0.4.0" end $:.push File.dirname(__FILE__) diff --git a/lib/path-to/described_routes.rb b/lib/path-to/described_routes.rb index dcd3a93..8cf2181 100644 --- a/lib/path-to/described_routes.rb +++ b/lib/path-to/described_routes.rb @@ -2,10 +2,26 @@ require "described_routes/resource_template" module PathTo + # + # Application and Path implementations for DescribedRoutes, each resource described by a ResourceTemplate + # module DescribedRoutes + # + # Implements PathTo::Path, represents a resource described by a ResourceTemplate + # class TemplatedPath < PathTo::Path attr_reader :resource_template + # + # Initialize a TemplatedPath. Raises ArgumentError if params doesn't include all mandatory params expected by the resource + # template. + # + # Parameters: + # [parent] parent object path or application + # [service] unused - resource_template.name is passed to super() instead. TODO: refactor + # [params] hash of params; will be merged with the parent's params and passed when required to the resource template's URI template + # [resource_template] metadata describing the web resource + # def initialize(parent, service, params, resource_template) super(parent, resource_template.name, params) @resource_template = resource_template @@ -20,10 +36,16 @@ def initialize(parent, service, params, resource_template) end end + # + # Get and cache the uri template from the resource tamplte + # def uri_template @uri_template ||= resource_template.uri_template || (application.base + resource_template.path_template) end + # + # Create and cache the URI by filling in the URI template with params + # def uri @uri ||= begin Addressable::Template.new(uri_template).expand(params).to_s @@ -43,15 +65,38 @@ def child_class_for(instance, method, params, template) end # - # Creates a child instance with new params, potentially finding a nested resource template that takes the additional params + # Creates a child instance with new params, potentially finding a nested resource template that takes the additional params. + # May take a combination of positional and named parameters, e.g. + # + # users["dojo", {"format" => "json"}] # - def [](params = {}) - keys = self.params.merge(params).keys - child_resource_template = resource_template.resource_templates.detect{ |t| - t.rel.nil? && (t.params - keys).empty? - } || resource_template - child_class = child_class_for(self, nil, params, child_resource_template) - child(child_class, nil, params, child_resource_template) + # Positional parameters are unsupported however if a new child template is not identified. + # + def [](*args) + positional_params, params_hash = extract_params(args, params) + known_keys = params_hash.keys + + child_resource_template = resource_template.resource_templates.detect do |t| + if t.rel.nil? + (t.positional_params(resource_template)[positional_params.length..-1] - t.optional_params - known_keys).empty? + end + end + + if child_resource_template + # we have a new child resource template; apply any positional params to the hash + complete_params_hash!(params_hash, child_resource_template.positional_params(resource_template), positional_params) + else + # we're just adding optional params, no new template identified + unless positional_params.empty? + raise ArgumentError.new( + "No matching child template; only named parameters can be used here. " + + "positional_params=#{positional_params.inspect}, params_hash=#{params_hash.inspect}") + end + child_resource_template = resource_template + end + + child_class = child_class_for(self, nil, params_hash, child_resource_template) + child(child_class, nil, params_hash, child_resource_template) end # @@ -62,11 +107,16 @@ def [](params = {}) # # Otherwise we invoke super in the hope of avoiding any hard-to-debug behaviour! # + # May take a combination of positional and named parameters, e.g. + # + # users("dojo", "format" => "json") + # def method_missing(method, *args) child_resource_template = resource_template.resource_templates.detect{|t| t.rel == method.to_s} if child_resource_template && (child_class = child_class_for(self, method, params, child_resource_template)) - params = args.inject(Hash.new){|h, arg| h.merge(arg)} - child(child_class, method, params, child_resource_template) + positional_params, params_hash = extract_params(args, params) + complete_params_hash!(params_hash, child_resource_template.positional_params(resource_template), positional_params) + child(child_class, method, params_hash, child_resource_template) else super end @@ -74,6 +124,9 @@ def method_missing(method, *args) end + # + # DescribedRoutes implementation of PathTo::Application. + # class Application < WithParams # An Array of DescribedRoutes::Resource objects attr_reader :resource_templates @@ -122,7 +175,7 @@ def initialize(options) # # Creates a copy of self with additional params # - def [](params = {}) + def [](params) self.class.new(:parent => self, :params => params) end @@ -135,10 +188,11 @@ def [](params = {}) # Otherwise we invoke super in the hope of avoiding any hard-to-debug behaviour! # def method_missing(method, *args) - resource_template = resource_templates_by_name[method.to_s] - if resource_template && (child_class = child_class_for(self, method, params, resource_template)) - params = args.inject(Hash.new){|h, arg| h.merge(arg)} - child(child_class, method, params, resource_template) + child_resource_template = resource_templates_by_name[method.to_s] + if child_resource_template && (child_class = child_class_for(self, method, params, child_resource_template)) + positional_params, params_hash = extract_params(args, params) + complete_params_hash!(params_hash, child_resource_template.positional_params(nil), positional_params) + child(child_class, method, params_hash, child_resource_template) else super end diff --git a/lib/path-to/with_params.rb b/lib/path-to/with_params.rb index e8a6883..bd9111e 100644 --- a/lib/path-to/with_params.rb +++ b/lib/path-to/with_params.rb @@ -87,5 +87,32 @@ def method_missing(method, *args) super end end + + # + # Separates positional params from hash params + # TODO: this is initially just for the DescribedRoutes implementation but there will be some refactoring to do + # + def extract_params(args, params_hash={})#:nodoc: + positional_params = [] + params_hash = params_hash.clone + args.each do |arg| + if arg.kind_of?(Hash) + params_hash.merge!(arg) + else + positional_params << arg + end + end + [positional_params, params_hash] + end + + # + # Updates params_hash with positional parameters + # TODO: this is initially just for the DescribedRoutes implementation but there will be some refactoring to do + # + def complete_params_hash!(params_hash, names, values)#:nodoc: + names[0...values.length].each_with_index do |k, i| + params_hash[k] = values[i] + end + end end end diff --git a/test/path-to/test_described_routes.rb b/test/path-to/test_described_routes.rb index ee4e712..40e44ff 100644 --- a/test/path-to/test_described_routes.rb +++ b/test/path-to/test_described_routes.rb @@ -102,13 +102,22 @@ def test_uri_template_expansion end def test_path_optional_params - users_json = app.users["format" => "json"] - user_json = app.users["user_id" => "dojo"]["format" => "json"] + # more complicated than would be ideal, but the app has a different #method_missing d + user_articles = app.users["user_id" => "dojo"].articles("json") - assert_equal("users", users_json.service) - assert_equal("user", user_json.service) - assert_equal("http://localhost:3000/users.json", users_json.uri) - assert_equal("http://localhost:3000/users/dojo.json", user_json.uri) + assert_equal("user_articles", user_articles.service) + assert_equal({"user_id" => "dojo", "format" => "json"}, user_articles.params) + end + + def test_path_collection_positional_params + article_json = app.users["dojo"].articles["article-1"]["format" => "json"] + assert_equal("user_article", article_json.service) + assert_equal({"user_id" => "dojo", "article_id" => "article-1", "format" => "json"}, article_json.params) + assert_equal("http://localhost:3000/users/dojo/articles/article-1.json", article_json.uri) + + assert_raises(ArgumentError) do + article_json = app.users["dojo"]["json"] + end end def test_app_params