relax 0.0.7 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/README +137 -114
- data/Rakefile +54 -0
- data/VERSION.yml +4 -0
- data/lib/relax/action.rb +39 -0
- data/lib/relax/context.rb +40 -0
- data/lib/relax/contextable.rb +15 -0
- data/lib/relax/endpoint.rb +21 -0
- data/lib/relax/instance.rb +23 -0
- data/lib/relax/parameter.rb +19 -0
- data/lib/relax/performer.rb +32 -0
- data/lib/relax/service.rb +19 -88
- data/lib/relax.rb +15 -10
- data/spec/relax/endpoint_spec.rb +69 -0
- data/spec/relax/integration_spec.rb +63 -0
- data/spec/relax/service_spec.rb +21 -0
- data/spec/services/flickr.rb +78 -0
- data/spec/spec_helper.rb +12 -0
- metadata +71 -30
- data/lib/relax/parsers/base.rb +0 -30
- data/lib/relax/parsers/factory.rb +0 -29
- data/lib/relax/parsers/hpricot.rb +0 -133
- data/lib/relax/parsers/rexml.rb +0 -147
- data/lib/relax/parsers.rb +0 -13
- data/lib/relax/query.rb +0 -46
- data/lib/relax/request.rb +0 -107
- data/lib/relax/response.rb +0 -82
- data/lib/relax/symbolic_hash.rb +0 -79
- data/spec/parsers/factory_spec.rb +0 -29
- data/spec/parsers/hpricot_spec.rb +0 -31
- data/spec/parsers/rexml_spec.rb +0 -36
- data/spec/query_spec.rb +0 -60
- data/spec/request_spec.rb +0 -114
- data/spec/response_spec.rb +0 -98
- data/spec/symbolic_hash_spec.rb +0 -67
data/LICENSE
CHANGED
data/README
CHANGED
@@ -1,171 +1,194 @@
|
|
1
1
|
= Relax
|
2
2
|
|
3
|
-
Relax is a
|
4
|
-
|
5
|
-
parameters, and parse
|
6
|
-
|
7
|
-
It provides a basic set of functionality common to most REST consumers:
|
8
|
-
|
9
|
-
- building HTTP queries (Relax::Request)
|
10
|
-
- issuing HTTP requests (Relax::Service)
|
11
|
-
- parsing XML responses (Relax::Response)
|
3
|
+
Relax is a library that provides a foundation for writing REST consumer APIs,
|
4
|
+
including the logic to handle the HTTP requests, build URLs with query
|
5
|
+
parameters, and parse responses.
|
12
6
|
|
13
7
|
|
14
8
|
== Tutorial
|
15
9
|
|
16
|
-
This short tutorial will walk you through the basic steps of creating a simple Flickr API that supports a single call to search for photos by tags.
|
10
|
+
This short tutorial will walk you through the basic steps of creating a simple Flickr API consumer that supports a single call to search for photos by tags.
|
11
|
+
|
17
12
|
|
18
|
-
===
|
13
|
+
=== First Things First
|
19
14
|
|
20
|
-
|
21
|
-
our Service class.
|
15
|
+
The first step we'll take is to load the Relax gem.
|
22
16
|
|
23
17
|
require 'rubygems'
|
18
|
+
|
19
|
+
gem 'relax', '~> 0.1.0'
|
24
20
|
require 'relax'
|
25
21
|
|
26
|
-
|
27
|
-
class Service < Relax::Service
|
28
|
-
ENDPOINT = 'http://api.flickr.com/services/rest/'
|
22
|
+
Then we'll define our service class.
|
29
23
|
|
30
|
-
|
31
|
-
super(ENDPOINT)
|
32
|
-
end
|
33
|
-
end
|
24
|
+
class Flickr < Relax::Service
|
34
25
|
end
|
35
26
|
|
36
27
|
|
37
|
-
===
|
28
|
+
=== Adding an Endpoint
|
38
29
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
parameter, and each response will have a "stat" attribute that will equal "ok"
|
43
|
-
when the response comes back without any errors.
|
30
|
+
An endpoint is the base URL for the service we'll be consuming. All of our
|
31
|
+
actions will be nested inside of an endpoint and build on top of it to
|
32
|
+
forumlate the final request URL.
|
44
33
|
|
45
|
-
|
46
|
-
|
47
|
-
parameter :method
|
34
|
+
class Flickr < Relax::Service
|
35
|
+
endpoint 'http://api.flickr.com/services/rest/' do
|
48
36
|
end
|
37
|
+
end
|
49
38
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
39
|
+
|
40
|
+
=== Adding Default Parameters
|
41
|
+
|
42
|
+
Since Flickr requires us to always pass an API key parameter let's make that a
|
43
|
+
required default parameter in our service class.
|
44
|
+
|
45
|
+
class Flickr < Relax::Service
|
46
|
+
defaults do
|
47
|
+
parameter :api_key, :required => true
|
54
48
|
end
|
55
|
-
end
|
56
49
|
|
57
|
-
|
58
|
-
our service to make sure that our Flickr API key gets passed along with each
|
59
|
-
request as well.
|
60
|
-
|
61
|
-
module Flickr
|
62
|
-
class Service < Relax::Service
|
63
|
-
ENDPOINT = 'http://api.flickr.com/services/rest/'
|
64
|
-
|
65
|
-
def initialize(api_key)
|
66
|
-
super(ENDPOINT)
|
67
|
-
Request[:api_key] = api_key
|
68
|
-
end
|
50
|
+
endpoint 'http://api.flickr.com/services/rest/' do
|
69
51
|
end
|
70
52
|
end
|
71
53
|
|
72
|
-
When we call our Request class as we have here, we're basically setting up a
|
73
|
-
value on our request that acts like a template. Each request we create now will
|
74
|
-
have the api_key property prepopulated for us.
|
75
54
|
|
55
|
+
=== Adding an Action
|
76
56
|
|
77
|
-
|
57
|
+
So we have our service now, but we need to define an action before we can
|
58
|
+
actually fetch any data from it.
|
78
59
|
|
79
|
-
|
80
|
-
|
60
|
+
class Flickr < Relax::Service
|
61
|
+
defaults do
|
62
|
+
parameter :api_key, :required => true
|
63
|
+
end
|
81
64
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
65
|
+
endpoint 'http://api.flickr.com/services/rest/' do
|
66
|
+
action :search do
|
67
|
+
parameter :method, :default => 'flickr.photos.search'
|
68
|
+
parameter :per_page, :default => 5
|
69
|
+
parameter :tags
|
70
|
+
end
|
86
71
|
end
|
87
72
|
end
|
88
73
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
to specify what type of data we're expecting the response to give us. The
|
94
|
-
default type is string.
|
74
|
+
We're defining an action here called <tt>:search</tt> that will create an
|
75
|
+
instance method on our service with the same name. There are three parameters,
|
76
|
+
<tt>:method</tt>, <tt>:per_page</tt>, and <tt>:tags</tt>, with <tt>:method</tt>
|
77
|
+
and <tt>:per_page</tt> receiving default values.
|
95
78
|
|
79
|
+
There are lots of other parameters for the <tt>flickr.photos.search</tt> method
|
80
|
+
that we could define, but we'll just stick with these for simplicities sake.
|
96
81
|
|
97
|
-
=== Step 4
|
98
82
|
|
99
|
-
|
100
|
-
things contained, a Relax best practice is to create a module for each call
|
101
|
-
on your service. The one we're creating here is the PhotoSearch module for the
|
102
|
-
"flickr.photos.search" call on the Flickr API.
|
83
|
+
=== A Small Refactoring
|
103
84
|
|
104
|
-
|
85
|
+
When adding more methods it will quickly become obvious that we have another
|
86
|
+
common parameter in <tt>:method</tt>. We can refactor our service class
|
87
|
+
slightly to make this a default parameter for all of the actions on our
|
88
|
+
endpoint.
|
105
89
|
|
106
|
-
|
107
|
-
|
108
|
-
|
90
|
+
class Flickr < Relax::Service
|
91
|
+
defaults do
|
92
|
+
parameter :api_key, :required => true
|
93
|
+
end
|
109
94
|
|
110
|
-
|
95
|
+
endpoint 'http://api.flickr.com/services/rest/' do
|
96
|
+
defaults do
|
97
|
+
parameter :method, :required => true
|
98
|
+
end
|
111
99
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
parameter :per_page
|
100
|
+
action :search do
|
101
|
+
set :method, 'flickr.photos.search'
|
102
|
+
parameter :per_page, :default => 5
|
116
103
|
parameter :tags
|
117
|
-
|
118
|
-
def initialize(options = {})
|
119
|
-
super
|
120
|
-
@method = 'flickr.photos.search'
|
121
|
-
end
|
122
104
|
end
|
105
|
+
end
|
106
|
+
end
|
123
107
|
|
124
|
-
|
125
|
-
|
126
|
-
end
|
108
|
+
The <tt>set</tt> method allows us to define a default value for a default
|
109
|
+
parameter so we don't have to redefine common parameters inside every action.
|
127
110
|
|
128
|
-
def search(options = {})
|
129
|
-
call(PhotoSearchRequest.new(options), PhotoSearchResponse)
|
130
|
-
end
|
131
111
|
|
132
|
-
|
133
|
-
|
134
|
-
|
112
|
+
=== Defining a Parser
|
113
|
+
|
114
|
+
Before we can actually perform a request we need to let the service know how to
|
115
|
+
handle the response. This is done by defining a parser.
|
116
|
+
|
117
|
+
class Flickr < Relax::Service
|
118
|
+
defaults do
|
119
|
+
parameter :api_key, :required => true
|
135
120
|
end
|
136
|
-
end
|
137
121
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
122
|
+
endpoint 'http://api.flickr.com/services/rest/' do
|
123
|
+
defaults do
|
124
|
+
parameter :method, :required => true
|
125
|
+
end
|
142
126
|
|
143
|
-
|
144
|
-
|
145
|
-
|
127
|
+
action :search do
|
128
|
+
set :method, 'flickr.photos.search'
|
129
|
+
parameter :per_page, :default => 5
|
130
|
+
parameter :tags
|
146
131
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
132
|
+
parser :rsp do
|
133
|
+
attribute :stat, :as => :status
|
134
|
+
|
135
|
+
element :photos do
|
136
|
+
attribute :page
|
137
|
+
attribute :pages
|
138
|
+
attribute :perpage, :as => :per_page
|
139
|
+
attribute :total
|
140
|
+
|
141
|
+
elements :photo do
|
142
|
+
attribute :id
|
143
|
+
attribute :owner
|
144
|
+
attribute :secret
|
145
|
+
attribute :server
|
146
|
+
attribute :farm
|
147
|
+
attribute :title
|
148
|
+
attribute :ispublic, :as => :is_public
|
149
|
+
attribute :isfriend, :as => :is_friend
|
150
|
+
attribute :isfamily, :as => :is_family
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
152
154
|
end
|
153
155
|
end
|
154
156
|
end
|
155
157
|
|
156
|
-
|
158
|
+
dhe parsing is performed by the Relief gem, so you can find out more about the
|
159
|
+
syntax in its own documentation.
|
157
160
|
|
158
|
-
flickr = Flickr::Service.new(ENV['FLICKR_API_KEY'])
|
159
|
-
relax = flickr.find_by_tag('relax', :per_page => 10)
|
160
161
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
162
|
+
=== Making a Call
|
163
|
+
|
164
|
+
Now, we're able to create a new instance of our service with the API key set.
|
165
|
+
|
166
|
+
flickr = Flickr.new(:api_key => FLICKR_API_KEY)
|
167
|
+
|
168
|
+
We can now use this object to search for photos by tag.
|
169
|
+
|
170
|
+
flickr.search(:tags => 'cucumbers,lemons')
|
171
|
+
|
172
|
+
This will return a Ruby response hash.
|
173
|
+
|
174
|
+
{
|
175
|
+
:status => "ok",
|
176
|
+
:photos => {
|
177
|
+
:page => "1",
|
178
|
+
:pages => "3947",
|
179
|
+
:per_page => "5",
|
180
|
+
:total => "19733",
|
181
|
+
:photo => [
|
182
|
+
{ :is_public => "1", :secret => "3c196485d2", :server => "3182", :is_friend => "0", :farm => "4", :title => "lemons", :is_family => "0", :id => "3509955709", :owner => "37013676@N06" },
|
183
|
+
{ :is_public => "1", :secret => "44f1306a63", :server => "3326", :is_friend => "0", :farm => "4", :title => "Peeto", :is_family => "0", :id => "3509461859", :owner => "13217824@N04" },
|
184
|
+
{ :is_public => "1", :secret => "dce53bce7f", :server => "3364", :is_friend => "0", :farm => "4", :title => "Peeto Above", :is_family => "0", :id => "3509459585", :owner => "13217824@N04" },
|
185
|
+
{ :is_public => "1", :secret => "12f9ba167c", :server => "3632", :is_friend => "0", :farm => "4", :title => "Lemonaid", :is_family => "0", :id => "3509415752", :owner => "35666391@N03" },
|
186
|
+
{ :is_public => "1", :secret => "8caac1ff46", :server => "3320", :is_friend => "0", :farm => "4", :title => "Gardening 365 (Day 8)", :is_family => "0", :id => "3509251322", :owner => "21778017@N06" }
|
187
|
+
]
|
188
|
+
}
|
189
|
+
}
|
166
190
|
|
167
|
-
This will output the IDs and titles for the first 10 photos on Flickr that have
|
168
|
-
the tag "relax."
|
169
191
|
|
192
|
+
== Copyright
|
170
193
|
|
171
|
-
Copyright (c) 2007-
|
194
|
+
Copyright (c) 2007-2009 Tyler Hunt. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
|
9
|
+
Jeweler::Tasks.new do |gem|
|
10
|
+
gem.name = "relax"
|
11
|
+
gem.summary = %Q{A flexible library for creating web service consumers.}
|
12
|
+
gem.email = "tyler@tylerhunt.com"
|
13
|
+
gem.homepage = "http://github.com/tylerhunt/relax"
|
14
|
+
gem.authors = ["Tyler Hunt"]
|
15
|
+
gem.rubyforge_project = 'relax'
|
16
|
+
|
17
|
+
gem.add_dependency('rest-client', '~> 0.9.2')
|
18
|
+
gem.add_dependency('nokogiri', '~> 1.2.3')
|
19
|
+
gem.add_dependency('relief', '~> 0.0.3')
|
20
|
+
|
21
|
+
gem.add_development_dependency('jeweler', '~> 0.11.0')
|
22
|
+
gem.add_development_dependency('rspec', '~> 1.2.2')
|
23
|
+
end
|
24
|
+
rescue LoadError
|
25
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
26
|
+
end
|
27
|
+
|
28
|
+
task :default => :spec
|
29
|
+
|
30
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
31
|
+
spec.libs << 'lib' << 'spec'
|
32
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
33
|
+
end
|
34
|
+
|
35
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
36
|
+
spec.libs << 'lib' << 'spec'
|
37
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
38
|
+
spec.rcov = true
|
39
|
+
end
|
40
|
+
|
41
|
+
Rake::RDocTask.new do |rdoc|
|
42
|
+
if File.exist?('VERSION.yml')
|
43
|
+
config = YAML.load(File.read('VERSION.yml'))
|
44
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
45
|
+
else
|
46
|
+
version = ""
|
47
|
+
end
|
48
|
+
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
50
|
+
rdoc.title = "relief #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('LICENSE*')
|
53
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
54
|
+
end
|
data/VERSION.yml
ADDED
data/lib/relax/action.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
module Relax
|
2
|
+
class Action
|
3
|
+
include Contextable
|
4
|
+
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(endpoint, name, options, &block)
|
8
|
+
@endpoint = endpoint
|
9
|
+
@name = name
|
10
|
+
@options = options
|
11
|
+
|
12
|
+
extend_context(endpoint)
|
13
|
+
context.evaluate(&block) if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute(values, credentials, *args)
|
17
|
+
args.unshift(values) if values
|
18
|
+
instance = Instance.new(*args)
|
19
|
+
response = performer(instance, credentials).perform
|
20
|
+
context.parse(response)
|
21
|
+
end
|
22
|
+
|
23
|
+
def method
|
24
|
+
@options[:method] || :get
|
25
|
+
end
|
26
|
+
private :method
|
27
|
+
|
28
|
+
def url
|
29
|
+
[@endpoint.url, @options[:url]].join
|
30
|
+
end
|
31
|
+
private :url
|
32
|
+
|
33
|
+
def performer(instance, credentials)
|
34
|
+
values = instance.values(context)
|
35
|
+
Performer.new(method, url, values, credentials)
|
36
|
+
end
|
37
|
+
private :performer
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Relax
|
2
|
+
class Context
|
3
|
+
attr_reader :parameters
|
4
|
+
|
5
|
+
def initialize(parameters=[]) # :nodoc:
|
6
|
+
@parameters = parameters
|
7
|
+
end
|
8
|
+
|
9
|
+
def evaluate(&block) # :nodoc:
|
10
|
+
instance_eval(&block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def parameter(name, options={})
|
14
|
+
unless @parameters.find { |parameter| parameter.name == name }
|
15
|
+
@parameters << Parameter.new(name, options)
|
16
|
+
else
|
17
|
+
raise ArgumentError.new("Duplicate parameter '#{name}'.")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(name, value)
|
22
|
+
if parameter = @parameters.find { |parameter| parameter.name == name }
|
23
|
+
parameter.value = value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def parser(root, options={}, &block) # :nodoc:
|
28
|
+
@parser ||= Relief::Parser.new(root, options, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse(response) # :nodoc:
|
32
|
+
@parser.parse(response)
|
33
|
+
end
|
34
|
+
|
35
|
+
def clone # :nodoc:
|
36
|
+
cloned_parameters = @parameters.collect { |parameter| parameter.clone }
|
37
|
+
self.class.new(cloned_parameters)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Relax
|
2
|
+
class Endpoint
|
3
|
+
include Contextable
|
4
|
+
|
5
|
+
attr_reader :url
|
6
|
+
|
7
|
+
def initialize(service, url, options, &block)
|
8
|
+
@service = service
|
9
|
+
@url = url
|
10
|
+
@options = options
|
11
|
+
|
12
|
+
extend_context(service)
|
13
|
+
instance_eval(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def action(name, options={}, &block)
|
17
|
+
action = Action.new(self, name, options, &block)
|
18
|
+
@service.register_action(action)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Relax
|
2
|
+
class Instance # :nodoc:
|
3
|
+
def initialize(*args)
|
4
|
+
@values = args.inject({}) do |values, arg|
|
5
|
+
arg.is_a?(Hash) ? values.merge(arg) : values
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def values(context)
|
10
|
+
context.parameters.inject({}) do |values, parameter|
|
11
|
+
name = parameter.name
|
12
|
+
|
13
|
+
if value = @values[parameter.name] || parameter.value
|
14
|
+
values[parameter.name] = value
|
15
|
+
elsif parameter.required?
|
16
|
+
raise ArgumentError.new("Missing value for '#{parameter.name}'.")
|
17
|
+
end
|
18
|
+
|
19
|
+
values
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Relax
|
2
|
+
class Parameter
|
3
|
+
attr_reader :name, :options
|
4
|
+
attr_writer :value
|
5
|
+
|
6
|
+
def initialize(name, options={})
|
7
|
+
@name = name
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def value
|
12
|
+
@value || @options[:default]
|
13
|
+
end
|
14
|
+
|
15
|
+
def required?
|
16
|
+
@options[:required]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Relax
|
2
|
+
class Performer
|
3
|
+
def initialize(method, url, values, credentials)
|
4
|
+
@method = method
|
5
|
+
@url = url
|
6
|
+
@values = values
|
7
|
+
@credentials = credentials
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform
|
11
|
+
case @method
|
12
|
+
when :delete, :get, :head then RestClient.send(@method, url)
|
13
|
+
when :post, :put then RestClient.send(@method, url, query)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def url
|
18
|
+
uri = URI.parse(@url)
|
19
|
+
uri.query = query unless query.nil? || query.empty?
|
20
|
+
uri.userinfo = @credentials.join(':') if @credentials
|
21
|
+
uri.to_s
|
22
|
+
end
|
23
|
+
private :url
|
24
|
+
|
25
|
+
def query
|
26
|
+
@values.collect do |name, value|
|
27
|
+
"#{name}=#{value}" if value
|
28
|
+
end.compact.join('&')
|
29
|
+
end
|
30
|
+
private :query
|
31
|
+
end
|
32
|
+
end
|