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.
- data/History.txt +4 -0
- data/README.rdoc +29 -26
- data/Rakefile +19 -14
- data/lib/path-to.rb +1 -1
- data/lib/path-to/application.rb +1 -1
- data/lib/path-to/described_routes.rb +55 -0
- data/lib/path-to/http_client.rb +19 -0
- data/test/path-to/test_described_routes.rb +18 -0
- metadata +21 -12
data/History.txt
CHANGED
data/README.rdoc
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
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[
|
24
|
-
#=>
|
23
|
+
app.users['dojo'].articles.recent.get
|
24
|
+
#=> '<html>...</html>'
|
25
25
|
|
26
|
-
app.users[
|
26
|
+
app.users['dojo'].articles.recent['format' => 'json']
|
27
27
|
#=> http://example.com/users/dojo/articles/recent.json
|
28
|
-
app.users[
|
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
|
-
|
32
|
-
|
33
|
-
=== Local configuration
|
36
|
+
=== Local configuration - using path-to without described_routes
|
34
37
|
|
35
|
-
require
|
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 =>
|
42
|
-
:articles =>
|
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 =>
|
60
|
-
app.users[:user =>
|
61
|
-
app.articles(:user =>
|
62
|
-
app.users[:user =>
|
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[
|
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[
|
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.
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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.
|
20
|
+
['described_routes','>= 0.6.0'],
|
21
|
+
['link_header','>= 0.0.4']
|
17
22
|
]
|
18
|
-
|
23
|
+
self.extra_dev_deps = [
|
19
24
|
['newgem', ">= #{::Newgem::VERSION}"]
|
20
25
|
]
|
21
26
|
|
22
|
-
|
23
|
-
path = (
|
24
|
-
|
25
|
-
|
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
|
data/lib/path-to.rb
CHANGED
data/lib/path-to/application.rb
CHANGED
@@ -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
|
|
data/lib/path-to/http_client.rb
CHANGED
@@ -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.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
- Mike Burrows
|
7
|
+
- Mike Burrows
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
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.
|
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.
|
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:
|
73
|
+
version: 2.3.2
|
64
74
|
version:
|
65
|
-
description:
|
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://
|
111
|
+
homepage: http://github.com/asplake/path-to/tree
|
103
112
|
licenses: []
|
104
113
|
|
105
|
-
post_install_message:
|
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.
|
135
|
+
rubygems_version: 1.3.5
|
127
136
|
signing_key:
|
128
137
|
specification_version: 3
|
129
|
-
summary:
|
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
|