path-to 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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