tylerhunt-relax 0.0.5 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -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,47 @@
|
|
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
|
+
|
9
|
+
parse_url_tokens
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform
|
13
|
+
case @method
|
14
|
+
when :delete, :get, :head then RestClient.send(@method, url)
|
15
|
+
when :post, :put then RestClient.send(@method, url, query)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def url
|
20
|
+
url = @url.gsub(/\:[a-z_]+/) do |name|
|
21
|
+
@url_values[name[1..-1].to_sym]
|
22
|
+
end
|
23
|
+
|
24
|
+
uri = URI.parse(url)
|
25
|
+
uri.query = query unless query.nil? || query.empty?
|
26
|
+
uri.userinfo = @credentials.join(':') if @credentials
|
27
|
+
uri.to_s
|
28
|
+
end
|
29
|
+
private :url
|
30
|
+
|
31
|
+
def query
|
32
|
+
@values.collect do |name, value|
|
33
|
+
"#{name}=#{value}" if value
|
34
|
+
end.compact.join('&')
|
35
|
+
end
|
36
|
+
private :query
|
37
|
+
|
38
|
+
def parse_url_tokens
|
39
|
+
@url_values = @url.scan(/(?:\:)([a-z_]+)/).flatten.inject({}) do |values, name|
|
40
|
+
name = name.to_sym
|
41
|
+
values[name] = @values.delete(name) if @values.key?(name)
|
42
|
+
values
|
43
|
+
end
|
44
|
+
end
|
45
|
+
private :parse_url_tokens
|
46
|
+
end
|
47
|
+
end
|
data/lib/relax/service.rb
CHANGED
@@ -1,100 +1,33 @@
|
|
1
|
-
require 'openssl'
|
2
|
-
require 'net/https'
|
3
|
-
require 'uri'
|
4
|
-
require 'date'
|
5
|
-
require 'base64'
|
6
|
-
require 'erb'
|
7
|
-
|
8
1
|
module Relax
|
9
|
-
# Service is the starting point for any REST consumer written with Relax. It
|
10
|
-
# is responsible for setting up the endpoint for the service, and issuing the
|
11
|
-
# HTTP requests for each call made.
|
12
|
-
#
|
13
|
-
# == Extending Service
|
14
|
-
#
|
15
|
-
# When writing consumers, you should start by extending Service by inheriting
|
16
|
-
# from it and calling its constructor with the endpoint for the service.
|
17
|
-
#
|
18
|
-
# === Example
|
19
|
-
#
|
20
|
-
# class Service < Relax::Service
|
21
|
-
# ENDPOINT = 'http://example.com/services/rest/'
|
22
|
-
#
|
23
|
-
# def initialize
|
24
|
-
# super(ENDPOINT)
|
25
|
-
# end
|
26
|
-
# end
|
27
|
-
#
|
28
|
-
# == Calling a Service
|
29
|
-
#
|
30
|
-
# Each call made to the service goes through the #call method of Service,
|
31
|
-
# which takes in a Request object and a Response class. The Request object is
|
32
|
-
# used to generate the query string that will be passed to the endpoint. The
|
33
|
-
# Reponse class is instantiated with the body of the response from the HTTP
|
34
|
-
# request.
|
35
|
-
#
|
36
|
-
# === Example
|
37
|
-
#
|
38
|
-
# This example show how to create a barebones call. This module can be then
|
39
|
-
# included into your Service class.
|
40
|
-
#
|
41
|
-
# module Search
|
42
|
-
# class SearchRequest < Relax::Request
|
43
|
-
# end
|
44
|
-
#
|
45
|
-
# class SearchResponse < Relax::Response
|
46
|
-
# end
|
47
|
-
#
|
48
|
-
# def search(options = {})
|
49
|
-
# call(SearchRequest.new(options), SearchResponse)
|
50
|
-
# end
|
51
|
-
# end
|
52
|
-
#
|
53
2
|
class Service
|
54
|
-
|
55
|
-
|
56
|
-
# This constructor should be called from your Service with the endpoint URL
|
57
|
-
# for the REST service.
|
58
|
-
def initialize(endpoint)
|
59
|
-
@endpoint = URI::parse(endpoint)
|
3
|
+
def initialize(values={})
|
4
|
+
@values = values
|
60
5
|
end
|
61
6
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
# as its first parameter. Once the response comes back from the service,
|
66
|
-
# the body of the response is used to instantiate the response class, and
|
67
|
-
# this response object is returned.
|
68
|
-
def call(request, response_class)
|
69
|
-
uri = @endpoint.clone
|
70
|
-
uri.query = query(request).to_s
|
71
|
-
response = request(uri)
|
72
|
-
puts "Response:\n#{response.body}\n\n" if $DEBUG
|
73
|
-
response_class.new(response.body)
|
7
|
+
def authenticate(*args)
|
8
|
+
@credentials = args
|
9
|
+
self
|
74
10
|
end
|
75
11
|
|
76
|
-
|
77
|
-
|
78
|
-
def default_query
|
79
|
-
Query.new
|
80
|
-
end
|
12
|
+
class << self
|
13
|
+
include Contextable
|
81
14
|
|
82
|
-
|
83
|
-
|
84
|
-
|
15
|
+
def endpoint(url, options={}, &block)
|
16
|
+
Endpoint.new(self, url, options, &block)
|
17
|
+
end
|
85
18
|
|
86
|
-
|
87
|
-
|
88
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
19
|
+
def register_action(action) # :nodoc:
|
20
|
+
@actions ||= {}
|
89
21
|
|
90
|
-
|
91
|
-
|
92
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
93
|
-
end
|
22
|
+
unless @actions[action.name]
|
23
|
+
@actions[action.name] = action.name
|
94
24
|
|
95
|
-
|
96
|
-
|
97
|
-
|
25
|
+
define_method(action.name) do |*args|
|
26
|
+
action.execute(@values, @credentials, *args)
|
27
|
+
end
|
28
|
+
else
|
29
|
+
raise ArgumentError.new("Duplicate action '#{action.name}'.")
|
30
|
+
end
|
98
31
|
end
|
99
32
|
end
|
100
33
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe Relax::Endpoint do
|
4
|
+
it "provides access to the URL" do
|
5
|
+
service = Class.new(Relax::Service)
|
6
|
+
endpoint = service.endpoint("http://api.example.com/") { }
|
7
|
+
endpoint.url.should == "http://api.example.com/"
|
8
|
+
end
|
9
|
+
|
10
|
+
it "allows contextual defaults to be set" do
|
11
|
+
service = Class.new(Relax::Service)
|
12
|
+
endpoint = service.endpoint("http://api.example.com/") { }
|
13
|
+
endpoint.should respond_to(:defaults)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "actions" do
|
17
|
+
it "should check for required values for service defaults" do
|
18
|
+
service = Class.new(Relax::Service) do
|
19
|
+
defaults { parameter :api_key, :required => true }
|
20
|
+
endpoint("http://api.example.com/") { action(:fetch) { } }
|
21
|
+
end
|
22
|
+
|
23
|
+
proc {
|
24
|
+
service.new.fetch
|
25
|
+
}.should raise_error(ArgumentError, /missing.*api_key/i)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should check for required values for endpoint defaults" do
|
29
|
+
service = Class.new(Relax::Service) do
|
30
|
+
endpoint("http://api.example.com/") do
|
31
|
+
defaults { parameter :operation, :required => true }
|
32
|
+
action(:fetch) { }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
proc {
|
37
|
+
service.new.fetch
|
38
|
+
}.should raise_error(ArgumentError, /missing.*operation/i)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should check for required values for action parameters" do
|
42
|
+
service = Class.new(Relax::Service) do
|
43
|
+
endpoint("http://api.example.com/") do
|
44
|
+
action(:fetch) { parameter :id, :required => true }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
proc {
|
49
|
+
service.new.fetch
|
50
|
+
}.should raise_error(ArgumentError, /missing.*id/i)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should create required parameters from tokens in the endpoint URL" do
|
54
|
+
service = Class.new(Relax::Service) do
|
55
|
+
endpoint("http://api.example.com/:version/") do
|
56
|
+
action(:fetch) { parameter :id }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
proc {
|
61
|
+
service.new.fetch
|
62
|
+
}.should raise_error(ArgumentError, /missing.*version/i)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should replace parameter tokens in the endpoint URL" do
|
66
|
+
service = Class.new(Relax::Service) do
|
67
|
+
endpoint("http://api.example.com/:version/") do
|
68
|
+
action(:fetch) do
|
69
|
+
parameter :id
|
70
|
+
parser(:xml) { attribute :status }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
FakeWeb.register_uri(:get, 'http://api.example.com/v1/', :string => <<-RESPONSE)
|
76
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
77
|
+
<response status="ok" />
|
78
|
+
RESPONSE
|
79
|
+
|
80
|
+
service.new(:version => 'v1').fetch.should == { :status => 'ok' }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe ".action" do
|
85
|
+
it "is callable from within an Endpoint" do
|
86
|
+
service = Class.new(Relax::Service)
|
87
|
+
endpoint = service.endpoint("http://api.example.com/") { }
|
88
|
+
endpoint.should respond_to(:action)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "defines an instance method by the same name on the Service" do
|
92
|
+
service = Class.new(Relax::Service)
|
93
|
+
service.new.should_not respond_to(:fetch)
|
94
|
+
|
95
|
+
service.endpoint("http://api.example.com/") { action :fetch }
|
96
|
+
service.new.should respond_to(:fetch)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe "an example service's" do
|
4
|
+
describe "get_photos action" do
|
5
|
+
it "includes :get_photos" do
|
6
|
+
Flickr.new.should respond_to(:get_photos)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "requires an API key" do
|
10
|
+
proc {
|
11
|
+
Flickr.new.get_photos
|
12
|
+
}.should raise_error(ArgumentError, /missing.*api_key/i)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "requires a user ID" do
|
16
|
+
proc {
|
17
|
+
Flickr.new(:api_key => 'secret').get_photos
|
18
|
+
}.should raise_error(ArgumentError, /missing.*user_id/i)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "parses the response" do
|
22
|
+
flickr = Flickr.new(:api_key => 'secret')
|
23
|
+
flickr.get_photos(:user_id => '59532755@N00', :per_page => 3).should == {
|
24
|
+
:status => 'ok',
|
25
|
+
:photos => {
|
26
|
+
:total => '7830',
|
27
|
+
:photo => [
|
28
|
+
{ :ispublic => '1', :isfriend => '0', :owner => '59532755@N00', :isfamily => '0', :secret => '2ebe0307e3', :server => '3562', :farm => '4', :id => '3508500178', :title => 'Rich Kilmer'},
|
29
|
+
{:ispublic => '1', :isfriend => '0', :owner => '59532755@N00', :isfamily => '0', :secret => '10b217377b', :server => '3593', :farm => '4', :id => '3508500140', :title => 'Women In Rails'},
|
30
|
+
{:ispublic => '1', :isfriend => '0', :owner => '59532755@N00', :isfamily => '0', :secret => '83bc8fbf71', :server => '3620', :farm => '4', :id => '3507688713', :title => 'Obie Fernandez'}
|
31
|
+
],
|
32
|
+
:per_page => '3',
|
33
|
+
:pages => '2610',
|
34
|
+
:page => '1'
|
35
|
+
}
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "get_user_by_username action" do
|
41
|
+
it "includes :get_user_by_username" do
|
42
|
+
Flickr.new.should respond_to(:get_user_by_username)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "requires an API key" do
|
46
|
+
proc {
|
47
|
+
Flickr.new.get_user_by_username
|
48
|
+
}.should raise_error(ArgumentError, /missing.*api_key/i)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "parses the response" do
|
52
|
+
flickr = Flickr.new(:api_key => 'secret')
|
53
|
+
flickr.get_user_by_username(:username => 'duncandavidson').should == {
|
54
|
+
:user => {
|
55
|
+
:username => 'duncandavidson',
|
56
|
+
:nsid => '59532755@N00',
|
57
|
+
:id => '59532755@N00'
|
58
|
+
},
|
59
|
+
:status => 'ok'
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe Relax::Service do
|
4
|
+
it "allows contextual defaults to be set" do
|
5
|
+
Relax::Service.should respond_to(:defaults)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#authenticate" do
|
9
|
+
it "is callable from within a Service" do
|
10
|
+
Relax::Service.new.should respond_to(:authenticate)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns the service" do
|
14
|
+
service = Relax::Service.new
|
15
|
+
service.authenticate('username', 'password').should == service
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe ".endpoint" do
|
20
|
+
it "is callable from within a Service" do
|
21
|
+
Relax::Service.should respond_to(:endpoint)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "creates a new Endpoint" do
|
25
|
+
Relax::Endpoint.should_receive(:new)
|
26
|
+
|
27
|
+
class Service < Relax::Service
|
28
|
+
endpoint "http://api.example.com/"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class Flickr < Relax::Service
|
2
|
+
defaults do
|
3
|
+
parameter :api_key, :required => true
|
4
|
+
end
|
5
|
+
|
6
|
+
endpoint "http://api.flickr.com/services/rest" do
|
7
|
+
defaults do
|
8
|
+
parameter :method, :required => true
|
9
|
+
end
|
10
|
+
|
11
|
+
action :get_photos do
|
12
|
+
set :method, "flickr.people.getPublicPhotos"
|
13
|
+
parameter :user_id, :required => true
|
14
|
+
parameter :safe_search
|
15
|
+
parameter :extras
|
16
|
+
parameter :per_page
|
17
|
+
parameter :page
|
18
|
+
|
19
|
+
parser :rsp do
|
20
|
+
element :status, :attribute => :stat
|
21
|
+
|
22
|
+
element :photos do
|
23
|
+
element :page, :attribute => true
|
24
|
+
element :pages, :attribute => true
|
25
|
+
element :per_page, :attribute => :perpage
|
26
|
+
element :total, :attribute => true
|
27
|
+
|
28
|
+
elements :photo do
|
29
|
+
element :id, :attribute => true
|
30
|
+
element :owner, :attribute => true
|
31
|
+
element :secret, :attribute => true
|
32
|
+
element :server, :attribute => true
|
33
|
+
element :farm, :attribute => true
|
34
|
+
element :title, :attribute => true
|
35
|
+
element :ispublic, :attribute => true
|
36
|
+
element :isfriend, :attribute => true
|
37
|
+
element :isfamily, :attribute => true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
action :get_user_by_username do
|
44
|
+
set :method, "flickr.people.findByUsername"
|
45
|
+
parameter :username, :required => true
|
46
|
+
|
47
|
+
parser :rsp do
|
48
|
+
element :status, :attribute => :stat
|
49
|
+
|
50
|
+
element :user do
|
51
|
+
element :id, :attribute => true
|
52
|
+
element :nsid, :attribute => true
|
53
|
+
element :username
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
FakeWeb.register_uri(:get, 'http://api.flickr.com/services/rest?api_key=secret&method=flickr.people.findByUsername&username=duncandavidson', :string => <<-RESPONSE)
|
61
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
62
|
+
<rsp stat="ok">
|
63
|
+
<user id="59532755@N00" nsid="59532755@N00">
|
64
|
+
<username>duncandavidson</username>
|
65
|
+
</user>
|
66
|
+
</rsp>
|
67
|
+
RESPONSE
|
68
|
+
|
69
|
+
FakeWeb.register_uri(:get, 'http://api.flickr.com/services/rest?user_id=59532755@N00&per_page=3&method=flickr.people.getPublicPhotos&api_key=secret', :string => <<-RESPONSE)
|
70
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
71
|
+
<rsp stat="ok">
|
72
|
+
<photos page="1" pages="2610" perpage="3" total="7830">
|
73
|
+
<photo id="3508500178" owner="59532755@N00" secret="2ebe0307e3" server="3562" farm="4" title="Rich Kilmer" ispublic="1" isfriend="0" isfamily="0" />
|
74
|
+
<photo id="3508500140" owner="59532755@N00" secret="10b217377b" server="3593" farm="4" title="Women In Rails" ispublic="1" isfriend="0" isfamily="0" />
|
75
|
+
<photo id="3507688713" owner="59532755@N00" secret="83bc8fbf71" server="3620" farm="4" title="Obie Fernandez" ispublic="1" isfriend="0" isfamily="0" />
|
76
|
+
</photos>
|
77
|
+
</rsp>
|
78
|
+
RESPONSE
|