tylerhunt-relax 0.0.5 → 0.1.1
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.rdoc +194 -0
- data/Rakefile +54 -0
- data/VERSION.yml +4 -0
- data/lib/relax.rb +15 -10
- data/lib/relax/action.rb +49 -0
- data/lib/relax/context.rb +41 -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 +47 -0
- data/lib/relax/service.rb +20 -87
- data/spec/relax/context_spec.rb +10 -0
- data/spec/relax/endpoint_spec.rb +99 -0
- data/spec/relax/integration_spec.rb +63 -0
- data/spec/relax/service_spec.rb +32 -0
- data/spec/services/flickr.rb +78 -0
- data/spec/services/service_with_custom_parser.rb +28 -0
- data/spec/spec_helper.rb +13 -0
- metadata +77 -38
- data/README +0 -171
- data/lib/relax/parsers.rb +0 -13
- data/lib/relax/parsers/base.rb +0 -34
- data/lib/relax/parsers/factory.rb +0 -43
- data/lib/relax/parsers/hpricot.rb +0 -145
- data/lib/relax/parsers/rexml.rb +0 -158
- data/lib/relax/query.rb +0 -46
- data/lib/relax/request.rb +0 -95
- data/lib/relax/response.rb +0 -78
- data/lib/relax/symbolic_hash.rb +0 -79
- data/spec/parsers/factory_spec.rb +0 -29
- data/spec/parsers/hpricot_spec.rb +0 -35
- data/spec/parsers/rexml_spec.rb +0 -40
- data/spec/query_spec.rb +0 -60
- data/spec/request_spec.rb +0 -108
- data/spec/response_spec.rb +0 -98
- data/spec/symbolic_hash_spec.rb +0 -67
data/LICENSE
CHANGED
data/README.rdoc
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
= Relax
|
2
|
+
|
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.
|
6
|
+
|
7
|
+
|
8
|
+
== Tutorial
|
9
|
+
|
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
|
+
|
12
|
+
|
13
|
+
=== First Things First
|
14
|
+
|
15
|
+
The first step we'll take is to load the Relax gem.
|
16
|
+
|
17
|
+
require 'rubygems'
|
18
|
+
|
19
|
+
gem 'relax', '~> 0.1.0'
|
20
|
+
require 'relax'
|
21
|
+
|
22
|
+
Then we'll define our service class.
|
23
|
+
|
24
|
+
class Flickr < Relax::Service
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
=== Adding an Endpoint
|
29
|
+
|
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.
|
33
|
+
|
34
|
+
class Flickr < Relax::Service
|
35
|
+
endpoint 'http://api.flickr.com/services/rest/' do
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
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
|
48
|
+
end
|
49
|
+
|
50
|
+
endpoint 'http://api.flickr.com/services/rest/' do
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
=== Adding an Action
|
56
|
+
|
57
|
+
So we have our service now, but we need to define an action before we can
|
58
|
+
actually fetch any data from it.
|
59
|
+
|
60
|
+
class Flickr < Relax::Service
|
61
|
+
defaults do
|
62
|
+
parameter :api_key, :required => true
|
63
|
+
end
|
64
|
+
|
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
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
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.
|
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.
|
81
|
+
|
82
|
+
|
83
|
+
=== A Small Refactoring
|
84
|
+
|
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.
|
89
|
+
|
90
|
+
class Flickr < Relax::Service
|
91
|
+
defaults do
|
92
|
+
parameter :api_key, :required => true
|
93
|
+
end
|
94
|
+
|
95
|
+
endpoint 'http://api.flickr.com/services/rest/' do
|
96
|
+
defaults do
|
97
|
+
parameter :method, :required => true
|
98
|
+
end
|
99
|
+
|
100
|
+
action :search do
|
101
|
+
set :method, 'flickr.photos.search'
|
102
|
+
parameter :per_page, :default => 5
|
103
|
+
parameter :tags
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
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.
|
110
|
+
|
111
|
+
|
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
|
120
|
+
end
|
121
|
+
|
122
|
+
endpoint 'http://api.flickr.com/services/rest/' do
|
123
|
+
defaults do
|
124
|
+
parameter :method, :required => true
|
125
|
+
end
|
126
|
+
|
127
|
+
action :search do
|
128
|
+
set :method, 'flickr.photos.search'
|
129
|
+
parameter :per_page, :default => 5
|
130
|
+
parameter :tags
|
131
|
+
|
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
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
dhe parsing is performed by the Relief gem, so you can find out more about the
|
159
|
+
syntax in its own documentation.
|
160
|
+
|
161
|
+
|
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
|
+
}
|
190
|
+
|
191
|
+
|
192
|
+
== Copyright
|
193
|
+
|
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.rb
CHANGED
@@ -1,13 +1,18 @@
|
|
1
|
-
|
1
|
+
require 'rubygems'
|
2
2
|
|
3
|
-
|
4
|
-
require '
|
5
|
-
require 'relax/request'
|
6
|
-
require 'relax/response'
|
7
|
-
require 'relax/service'
|
8
|
-
require 'relax/symbolic_hash'
|
3
|
+
gem 'relief', '~> 0.0.2'
|
4
|
+
require 'relief'
|
9
5
|
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
gem 'rest-client', '~> 0.9.2'
|
7
|
+
require 'restclient'
|
8
|
+
|
9
|
+
module Relax # :nodoc:
|
10
|
+
autoload :Action, 'relax/action'
|
11
|
+
autoload :Context, 'relax/context'
|
12
|
+
autoload :Contextable, 'relax/contextable'
|
13
|
+
autoload :Endpoint, 'relax/endpoint'
|
14
|
+
autoload :Instance, 'relax/instance'
|
15
|
+
autoload :Parameter, 'relax/parameter'
|
16
|
+
autoload :Performer, 'relax/performer'
|
17
|
+
autoload :Service, 'relax/service'
|
13
18
|
end
|
data/lib/relax/action.rb
ADDED
@@ -0,0 +1,49 @@
|
|
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
|
+
parse_url_parameters
|
14
|
+
context.evaluate(&block) if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute(values, credentials, *args)
|
18
|
+
args.unshift(values) if values
|
19
|
+
instance = Instance.new(*args)
|
20
|
+
response = performer(instance, credentials).perform
|
21
|
+
context.parse(response)
|
22
|
+
end
|
23
|
+
|
24
|
+
def method
|
25
|
+
@options[:method] || :get
|
26
|
+
end
|
27
|
+
private :method
|
28
|
+
|
29
|
+
def url
|
30
|
+
[@endpoint.url, @options[:url]].join
|
31
|
+
end
|
32
|
+
private :url
|
33
|
+
|
34
|
+
def performer(instance, credentials)
|
35
|
+
values = instance.values(context)
|
36
|
+
Performer.new(method, url, values, credentials)
|
37
|
+
end
|
38
|
+
private :performer
|
39
|
+
|
40
|
+
def parse_url_parameters
|
41
|
+
url.scan(/(?:\:)([a-z_]+)/).flatten.each do |name|
|
42
|
+
defaults do
|
43
|
+
parameter name.to_sym, :required => true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
private :parse_url_parameters
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,41 @@
|
|
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 ||= root.kind_of?(Class) ? root.new(options, &block) :
|
29
|
+
Relief::Parser.new(root, options, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse(response) # :nodoc:
|
33
|
+
@parser.parse(response)
|
34
|
+
end
|
35
|
+
|
36
|
+
def clone # :nodoc:
|
37
|
+
cloned_parameters = @parameters.collect { |parameter| parameter.clone }
|
38
|
+
self.class.new(cloned_parameters)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
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
|