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