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.
- 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
|