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