path-to 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,7 @@
1
+ == 0.6.0 2009-05-25
2
+
3
+ * Application discovery via link headers
4
+
1
5
  == 0.5.1 2009-05-25
2
6
 
3
7
  * More ResourceTemplate refactoring
@@ -1,45 +1,48 @@
1
1
  = path-to README
2
2
 
3
- Model web apps easily and access them via nice app-specific Ruby APIs.
4
-
5
- Note to reader: You are invited to comment on the roadmap at http://positiveincline.com/?p=213
6
-
7
- == Description
8
-
9
- path-to allows web applications to be modelled via URI templates and then accessed through an application-specific Ruby API. It is designed to be extended easily to support discovery mechanisms; included is an implementation based on the resource templates of described_routes.
3
+ Model web apps easily and access them via nice app-specific Ruby APIs on the client side. For suitably-configured servers (e.g. those using described_routes), path-to applications can bootstrap themselves from the address of any resource in the target application.
10
4
 
11
5
  == Synopsis
12
6
 
13
7
  === Automatic configuration with described_routes
14
8
 
15
- Create a client application configured from a server that supports described_routes:
9
+ Create a client application for a web app that supports the described_routes discovery protocol:
16
10
 
17
11
  require 'path-to/described_routes'
12
+ app = PathTo::DescribedRoutes::Application.discover('http://example.com')
18
13
 
19
- app = PathTo::DescribedRoutes::Application.new(:json => Net::HTTP.get(URI.parse("http://example.com/described_routes.json")))
14
+ Or create one from a <code>ResourceTemplate</code> description in JSON or YAML found at some known place, preferable on the server. This could be generated and served by the Rails controller provided by <code>described_routes</code> or perhaps hand-crafted and configured statically:
15
+
16
+ require 'path-to/described_routes'
17
+ app = PathTo::DescribedRoutes::Application.new(:json => Net::HTTP.get(URI.parse('http://example.com/described_routes.json')))
20
18
 
21
- app.users["dojo"].articles.recent
19
+ The <code>app</code> object then automatically supports an API that reflects the structure of the web application, for example:
20
+
21
+ app.users['dojo'].articles.recent
22
22
  #=> http://example.com/users/dojo/articles/recent
23
- app.users["dojo"].articles.recent.get
24
- #=> "<html>...</html>"
23
+ app.users['dojo'].articles.recent.get
24
+ #=> '<html>...</html>'
25
25
 
26
- app.users["dojo"].articles.recent["format" => "json"]
26
+ app.users['dojo'].articles.recent['format' => 'json']
27
27
  #=> http://example.com/users/dojo/articles/recent.json
28
- app.users["dojo"].articles.recent.get
28
+ app.users['dojo'].articles.recent.get
29
29
  #=> [...]
30
+
31
+ The API objects include the <code>#get</code>, <code>#put</code>, <code>#post</code> and <code>#delete</code> of HTTParty (or other HTTP client provided at initialization). Alternatively you can use their <code>#uri</code> or <code>#path</code> methods with an un-integrated HTTP client:
32
+
33
+ your_favourite_http_client.get app.users['dojo'].articles.recent.uri
34
+ #=> '<html>...</html>'
30
35
 
31
- See examples/delicious.rb for an example based on a partial YAML-based description of the Delicious API.
32
-
33
- === Local configuration
36
+ === Local configuration - using path-to without described_routes
34
37
 
35
- require "path-to"
38
+ require 'path-to'
36
39
 
37
40
  class Users < PathTo::Path ; end
38
41
  class Articles < PathTo::Path ; end
39
42
 
40
43
  app = Application.new(
41
- :users => "http://example.com/users/{user}",
42
- :articles => "http://example.com/users/{user}/articles/{slug}") do |app|
44
+ :users => 'http://example.com/users/{user}',
45
+ :articles => 'http://example.com/users/{user}/articles/{slug}') do |app|
43
46
  def app.child_class_for(instance, method, params)
44
47
  {
45
48
  :users => Users,
@@ -56,19 +59,19 @@ app.articles cause objects of the appropriate class to be generated. These in t
56
59
  params, like this:
57
60
 
58
61
  app.users #=> http://example.com/users/ <Users>
59
- app.users(:user => "dojo") #=> http://example.com/users/dojo <Users>
60
- app.users[:user => "dojo"] #=> http://example.com/users/dojo <Users>
61
- app.articles(:user => "dojo", :slug => "my-article") #=> http://example.com/users/dojo/articles/my-article <Articles>
62
- app.users[:user => "dojo"].articles[:slug => "my-article"] #=> http://example.com/users/dojo/articles/my-article <Articles>
62
+ app.users(:user => 'dojo') #=> http://example.com/users/dojo <Users>
63
+ app.users[:user => 'dojo'] #=> http://example.com/users/dojo <Users>
64
+ app.articles(:user => 'dojo', :slug => 'my-article') #=> http://example.com/users/dojo/articles/my-article <Articles>
65
+ app.users[:user => 'dojo'].articles[:slug => 'my-article'] #=> http://example.com/users/dojo/articles/my-article <Articles>
63
66
 
64
67
  With a little more work (overriding Users#[] and Articles#[] - as described in the documentation for the Path class), the last example
65
68
  becomes simply:
66
69
 
67
- app.users["dojo"].articles["my-article"] #=> http://example.com/users/dojo/articles/my-article <Articles>
70
+ app.users['dojo'].articles['my-article'] #=> http://example.com/users/dojo/articles/my-article <Articles>
68
71
 
69
72
  HTTP support comes courtesy of HTTParty (the Path class includes it). To GET an article in the above example, just invoke the get method on the path object:
70
73
 
71
- app.users["dojo"].articles["my-article"].get #=> "<html>...</html>"
74
+ app.users['dojo'].articles['my-article'].get #=> '<html>...</html>'
72
75
 
73
76
  == Installation
74
77
 
data/Rakefile CHANGED
@@ -1,28 +1,33 @@
1
- %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
1
+ %w[rubygems rake rake/clean fileutils newgem rubigen hoe].each { |f| require f }
2
2
  $:.push File.dirname(__FILE__) + '/lib'
3
3
  require 'path-to'
4
4
 
5
5
  # Generate all the Rake tasks
6
6
  # Run 'rake -T' to see list of generated tasks (from gem root directory)
7
- $hoe = Hoe.new('path-to', PathTo::VERSION) do |p|
8
- p.developer('Mike Burrows (asplake)', 'mjb@asplake.co.uk')
9
- p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
10
- p.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
11
- p.rubyforge_name = p.name
12
- p.url = 'http://positiveincline.com/?p=213'
13
- p.extra_deps = [
7
+ $hoe = Hoe.spec 'path-to' do
8
+ developer('Mike Burrows', 'mjb@asplake.co.uk')
9
+ self.version = PathTo::VERSION
10
+ self.readme_file = "README.rdoc"
11
+ self.summary = self.description = paragraphs_of(self.readme_file, 1..1).join("\n\n")
12
+ self.changes = paragraphs_of("History.txt", 0..1).join("\n\n")
13
+ self.rubyforge_name = 'path-to'
14
+ self.url = 'http://github.com/asplake/path-to/tree'
15
+ self.extra_deps = [
16
+ ]
17
+ self.extra_deps = [
14
18
  ['httparty','>= 0.4.2'],
15
19
  ['addressable','>= 2.1.0'],
16
- ['described_routes','>= 0.5.0']
20
+ ['described_routes','>= 0.6.0'],
21
+ ['link_header','>= 0.0.4']
17
22
  ]
18
- p.extra_dev_deps = [
23
+ self.extra_dev_deps = [
19
24
  ['newgem', ">= #{::Newgem::VERSION}"]
20
25
  ]
21
26
 
22
- p.clean_globs |= %w[**/.DS_Store tmp *.log]
23
- path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
24
- p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
25
- p.rsync_args = '-av --delete --ignore-errors'
27
+ self.clean_globs |= %w[**/.DS_Store tmp *.log]
28
+ path = (rubyforge_name == name) ? rubyforge_name : "\#{rubyforge_name}/\#{name}"
29
+ self.remote_rdoc_dir = File.join(path.gsub(/^#{rubyforge_name}\/?/,''), 'rdoc')
30
+ self.rsync_args = '-av --delete --ignore-errors'
26
31
  end
27
32
 
28
33
  task :info do
@@ -1,5 +1,5 @@
1
1
  module PathTo
2
- VERSION = "0.5.1"
2
+ VERSION = "0.6.0"
3
3
  end
4
4
 
5
5
  $:.push File.dirname(__FILE__)
@@ -1,6 +1,7 @@
1
1
  require "path-to/path"
2
2
  require "path-to/http_client"
3
3
  require "addressable/template"
4
+ require "net/http"
4
5
 
5
6
  module PathTo
6
7
  #
@@ -115,7 +116,6 @@ module PathTo
115
116
  # TODO Consider taking an instance as the first parameter, as #child_class_for does
116
117
  #
117
118
  def uri_for(method, params = {})
118
- # TODO it's a 1-line fix to Addressable to permit symbols (etc) as keys
119
119
  if (t = uri_template_for(method, params))
120
120
  string_keyed_params = params.keys.inject({}){|hash, key| hash[key.to_s] = params[key]; hash}
121
121
  Addressable::Template.new(t).expand(string_keyed_params).to_s
@@ -1,11 +1,19 @@
1
1
  require "path-to"
2
2
  require "described_routes"
3
+ require "link_header"
3
4
 
4
5
  module PathTo
5
6
  #
6
7
  # Application and Path implementations for DescribedRoutes, each resource described by a ResourceTemplate
7
8
  #
8
9
  module DescribedRoutes
10
+ #
11
+ # Raised in the event of discovery protocol errors, e.g. responses other than 200 OK or missing headers.
12
+ # Low-level exceptions are NOT swallowed.
13
+ #
14
+ class ProtocolError < Exception
15
+ end
16
+
9
17
  #
10
18
  # Implements PathTo::Path, represents a resource described by a ResourceTemplate
11
19
  #
@@ -132,6 +140,53 @@ module PathTo
132
140
  # Hash of options to be included in HTTP method calls
133
141
  attr_reader :http_options
134
142
 
143
+ def self.discover(url, options={})
144
+ http_options = options[:http_options] || nil
145
+ default_type = options[:default_type] || Path
146
+ http_client = options[:http_client] || HTTPClient
147
+
148
+ metadata_link = self.discover_metadata_link(url, http_client)
149
+ unless metadata_link
150
+ raise ProtocolError.new("no metadata link found")
151
+ end
152
+
153
+ json = http_client.get(metadata_link.href, :format => :text, :headers => {"Accept" => "application/json"})
154
+ unless json
155
+ raise ProtocolError.new("no json found")
156
+ end
157
+
158
+ self.new(options.merge(:json => json))
159
+ end
160
+
161
+ def self.discover_metadata_link(url, http_client)
162
+ response = http_client.head(url, {"Accept" => "application/json"})
163
+ unless response.kind_of?(Net::HTTPOK)
164
+ raise ProtocolError.new("got response #{response.inspect} from #{url}")
165
+ end
166
+ link_header = LinkHeader.parse(response["Link"])
167
+
168
+ app_templates_link = link_header.find_link(["rel", "describedby"], ["meta", "ResourceTemplates"])
169
+ unless app_templates_link
170
+ resource_template_link = link_header.find_link(["rel", "describedby"], ["meta", "ResourceTemplate"])
171
+ if resource_template_link
172
+ response = http_client.head(resource_template_link.href, {"Accept" => "application/json"})
173
+ unless response.kind_of?(Net::HTTPOK)
174
+ raise ProtocolError.new("got response #{response.inspect} from #{url}")
175
+ end
176
+ link_header = LinkHeader.parse(response["Link"])
177
+ app_templates_link = link_header.find_link(["rel", "index"], ["meta", "ResourceTemplates"])
178
+ unless app_templates_link
179
+ raise ProtocolError.new("(2) couldn't find link with rel=\"index\" and meta=\"ResourceTemplates\" at #{resource_template_link.href}")
180
+ end
181
+ else
182
+ unless app_templates_link
183
+ raise ProtocolError.new("(1) couldn't find link with rel=\"described_by\" and meta=\"ResourceTemplates\" or meta=\"ResourceTemplate\" at #{url}")
184
+ end
185
+ end
186
+ end
187
+ app_templates_link
188
+ end
189
+
135
190
  def initialize(options)
136
191
  super(options[:parent], options[:service], options[:params])
137
192
 
@@ -1,4 +1,5 @@
1
1
  require "httparty"
2
+ require "uri"
2
3
 
3
4
  module PathTo
4
5
  #
@@ -10,5 +11,23 @@ module PathTo
10
11
  #
11
12
  class HTTPClient
12
13
  include HTTParty
14
+
15
+ Request::SupportedHTTPMethods.push(Net::HTTP::Head)
16
+
17
+
18
+ #
19
+ # HEAD request, returns some sort of NET::HTTPResponse
20
+ #
21
+ # A bit ugly and out of place this, but HTTParty doesn's support HEAD yet.
22
+ # (@jnunemaker@asplake there is a patch for head requests I need to pull in)
23
+ #
24
+ def self.head(uri_string, headers={})
25
+ uri = URI.parse(uri_string)
26
+ raise URI::InvalidURIError.new("#{uri_string.inspect} is not a valid http URI") unless uri.kind_of?(URI::HTTP) && uri.host
27
+
28
+ Net::HTTP.new(uri.host, uri.port).start do |http|
29
+ return http.request(Net::HTTP::Head.new(uri.request_uri, headers))
30
+ end
31
+ end
13
32
  end
14
33
  end
@@ -127,4 +127,22 @@ class TestDescribedRoutes < Test::Unit::TestCase
127
127
  assert_equal(app, app_json.parent)
128
128
  assert_equal("http://localhost:3000/users.json", users_json.uri)
129
129
  end
130
+
131
+ def test_discover_with_empty_uri
132
+ assert_raise(URI::InvalidURIError) do
133
+ PathTo::DescribedRoutes::Application.discover("")
134
+ end
135
+ end
136
+
137
+ def test_discover_with_invalid_uri
138
+ assert_raise(URI::InvalidURIError) do
139
+ PathTo::DescribedRoutes::Application.discover("foo bar")
140
+ end
141
+ end
142
+
143
+ def test_discover_with_nonexistent_uri
144
+ assert_raise(PathTo::DescribedRoutes::ProtocolError) do
145
+ PathTo::DescribedRoutes::Application.discover("http://localhost/NONEXISTENT")
146
+ end
147
+ end
130
148
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: path-to
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
- - Mike Burrows (asplake)
7
+ - Mike Burrows
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-05-25 00:00:00 +01:00
12
+ date: 2009-07-27 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -40,7 +40,17 @@ dependencies:
40
40
  requirements:
41
41
  - - ">="
42
42
  - !ruby/object:Gem::Version
43
- version: 0.5.0
43
+ version: 0.6.0
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: link_header
47
+ type: :runtime
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 0.0.4
44
54
  version:
45
55
  - !ruby/object:Gem::Dependency
46
56
  name: newgem
@@ -50,7 +60,7 @@ dependencies:
50
60
  requirements:
51
61
  - - ">="
52
62
  - !ruby/object:Gem::Version
53
- version: 1.4.1
63
+ version: 1.5.1
54
64
  version:
55
65
  - !ruby/object:Gem::Dependency
56
66
  name: hoe
@@ -60,9 +70,9 @@ dependencies:
60
70
  requirements:
61
71
  - - ">="
62
72
  - !ruby/object:Gem::Version
63
- version: 1.8.0
73
+ version: 2.3.2
64
74
  version:
65
- description: path-to allows web applications to be modelled via URI templates and then accessed through an application-specific Ruby API. It is designed to be extended easily to support discovery mechanisms; included is an implementation based on the resource templates of described_routes.
75
+ description: Model web apps easily and access them via nice app-specific Ruby APIs on the client side. For suitably-configured servers (e.g. those using described_routes), path-to applications can bootstrap themselves from the address of any resource in the target application.
66
76
  email:
67
77
  - mjb@asplake.co.uk
68
78
  executables: []
@@ -73,7 +83,6 @@ extra_rdoc_files:
73
83
  - History.txt
74
84
  - Manifest.txt
75
85
  - PostInstall.txt
76
- - README.rdoc
77
86
  files:
78
87
  - History.txt
79
88
  - LICENSE
@@ -99,10 +108,10 @@ files:
99
108
  - test/path-to/test_with_params.rb
100
109
  - test/test_helper.rb
101
110
  has_rdoc: true
102
- homepage: http://positiveincline.com/?p=213
111
+ homepage: http://github.com/asplake/path-to/tree
103
112
  licenses: []
104
113
 
105
- post_install_message: PostInstall.txt
114
+ post_install_message:
106
115
  rdoc_options:
107
116
  - --main
108
117
  - README.rdoc
@@ -123,10 +132,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
132
  requirements: []
124
133
 
125
134
  rubyforge_project: path-to
126
- rubygems_version: 1.3.3
135
+ rubygems_version: 1.3.5
127
136
  signing_key:
128
137
  specification_version: 3
129
- summary: path-to allows web applications to be modelled via URI templates and then accessed through an application-specific Ruby API
138
+ summary: Model web apps easily and access them via nice app-specific Ruby APIs on the client side. For suitably-configured servers (e.g. those using described_routes), path-to applications can bootstrap themselves from the address of any resource in the target application.
130
139
  test_files:
131
140
  - test/path-to/test_application.rb
132
141
  - test/path-to/test_described_routes.rb