relax 0.0.7 → 0.1.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/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
|