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.
@@ -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
- attr_reader :endpoint
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
- protected
63
-
64
- # Calls the service using a query built from the Request object passed in
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
- private
77
-
78
- def default_query
79
- Query.new
80
- end
12
+ class << self
13
+ include Contextable
81
14
 
82
- def query(request)
83
- Query.new(default_query.merge(request.to_query))
84
- end
15
+ def endpoint(url, options={}, &block)
16
+ Endpoint.new(self, url, options, &block)
17
+ end
85
18
 
86
- def request(uri)
87
- puts "Request:\n#{uri.to_s}\n\n" if $DEBUG
88
- http = Net::HTTP.new(uri.host, uri.port)
19
+ def register_action(action) # :nodoc:
20
+ @actions ||= {}
89
21
 
90
- if uri.scheme == 'https'
91
- http.use_ssl = true
92
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
93
- end
22
+ unless @actions[action.name]
23
+ @actions[action.name] = action.name
94
24
 
95
- http.start do |http|
96
- request = Net::HTTP::Get.new("#{uri.path}?#{uri.query}")
97
- http.request(request)
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,10 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe Relax::Context do
4
+
5
+ it "utilizes a custom parser for Class parsers" do
6
+ service = ServiceWithCustomParser.new
7
+ service.test.should == 'parsed'
8
+ end
9
+
10
+ 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