Skip to content

Commit

Permalink
Positional parameters for path-to/described_routes (only), e.g. delic…
Browse files Browse the repository at this point in the history
…ious.posts('ruby'); app.users['dojo']
  • Loading branch information
Michael Burrows authored and Michael Burrows committed May 12, 2009
1 parent 53ce2f6 commit 06a3de6
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 29 deletions.
8 changes: 8 additions & 0 deletions 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
Expand Down
13 changes: 10 additions & 3 deletions README.rdoc
Expand Up @@ -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 #=> "<html>...</html>"
app.users["dojo"].articles.recent
#=> http://example.com/users/dojo/articles/recent
app.users["dojo"].articles.recent.get
#=> "<html>...</html>"

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

Expand Down
1 change: 1 addition & 0 deletions Rakefile
Expand Up @@ -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}"]
Expand Down
7 changes: 3 additions & 4 deletions 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'
Expand Down Expand Up @@ -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'] }
2 changes: 1 addition & 1 deletion lib/path-to.rb
@@ -1,5 +1,5 @@
module PathTo
VERSION = "0.3.0"
VERSION = "0.4.0"
end

$:.push File.dirname(__FILE__)
Expand Down
84 changes: 69 additions & 15 deletions lib/path-to/described_routes.rb
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

#
Expand All @@ -62,18 +107,26 @@ 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
end

end

#
# DescribedRoutes implementation of PathTo::Application.
#
class Application < WithParams
# An Array of DescribedRoutes::Resource objects
attr_reader :resource_templates
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
27 changes: 27 additions & 0 deletions lib/path-to/with_params.rb
Expand Up @@ -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
21 changes: 15 additions & 6 deletions test/path-to/test_described_routes.rb
Expand Up @@ -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
Expand Down

0 comments on commit 06a3de6

Please sign in to comment.