relax 0.0.7 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2007-2008 Tyler Hunt
1
+ Copyright (c) 2007-2009 Tyler Hunt
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README CHANGED
@@ -1,171 +1,194 @@
1
1
  = Relax
2
2
 
3
- Relax is a simple library that provides a foundation for writing REST consumer
4
- APIs, including the logic to handle the HTTP requests, build URLs with query
5
- parameters, and parse XML responses.
6
-
7
- It provides a basic set of functionality common to most REST consumers:
8
-
9
- - building HTTP queries (Relax::Request)
10
- - issuing HTTP requests (Relax::Service)
11
- - parsing XML responses (Relax::Response)
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.
12
6
 
13
7
 
14
8
  == Tutorial
15
9
 
16
- This short tutorial will walk you through the basic steps of creating a simple Flickr API that supports a single call to search for photos by tags.
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
+
17
12
 
18
- === Step 1
13
+ === First Things First
19
14
 
20
- In the first step we're going to simply include Relax, and define the basis for
21
- our Service class.
15
+ The first step we'll take is to load the Relax gem.
22
16
 
23
17
  require 'rubygems'
18
+
19
+ gem 'relax', '~> 0.1.0'
24
20
  require 'relax'
25
21
 
26
- module Flickr
27
- class Service < Relax::Service
28
- ENDPOINT = 'http://api.flickr.com/services/rest/'
22
+ Then we'll define our service class.
29
23
 
30
- def initialize
31
- super(ENDPOINT)
32
- end
33
- end
24
+ class Flickr < Relax::Service
34
25
  end
35
26
 
36
27
 
37
- === Step 2
28
+ === Adding an Endpoint
38
29
 
39
- Next we're going to define common Request and Response classes for use
40
- throughout our API. This gives us a single point to add any shared
41
- functionality. For Flickr, this means that each request will have a "method"
42
- parameter, and each response will have a "stat" attribute that will equal "ok"
43
- when the response comes back without any errors.
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.
44
33
 
45
- module Flickr
46
- class Request < Relax::Request
47
- parameter :method
34
+ class Flickr < Relax::Service
35
+ endpoint 'http://api.flickr.com/services/rest/' do
48
36
  end
37
+ end
49
38
 
50
- class Response < Relax::Response
51
- def successful?
52
- root[:stat] == 'ok'
53
- end
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
54
48
  end
55
- end
56
49
 
57
- While we're at it, we're also going to add a new line to the constructor from
58
- our service to make sure that our Flickr API key gets passed along with each
59
- request as well.
60
-
61
- module Flickr
62
- class Service < Relax::Service
63
- ENDPOINT = 'http://api.flickr.com/services/rest/'
64
-
65
- def initialize(api_key)
66
- super(ENDPOINT)
67
- Request[:api_key] = api_key
68
- end
50
+ endpoint 'http://api.flickr.com/services/rest/' do
69
51
  end
70
52
  end
71
53
 
72
- When we call our Request class as we have here, we're basically setting up a
73
- value on our request that acts like a template. Each request we create now will
74
- have the api_key property prepopulated for us.
75
54
 
55
+ === Adding an Action
76
56
 
77
- === Step 3
57
+ So we have our service now, but we need to define an action before we can
58
+ actually fetch any data from it.
78
59
 
79
- Next, we're going to need a basic Photo class to represent photos that Flickr
80
- returns to us.
60
+ class Flickr < Relax::Service
61
+ defaults do
62
+ parameter :api_key, :required => true
63
+ end
81
64
 
82
- module Flickr
83
- class Photo < Response
84
- parameter :id, :attribute => true, :type => :integer
85
- parameter :title, :attribute => true
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
86
71
  end
87
72
  end
88
73
 
89
- Here we're creating a Response class that extends our Flickr::Response, which
90
- has two parameters: "id" and "title." By setting the attribute option to true,
91
- we're telling Relax to look for an attribute by that name on the XML root
92
- instead of checking for an element by that name. The type options can be used
93
- to specify what type of data we're expecting the response to give us. The
94
- default type is string.
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.
95
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.
96
81
 
97
- === Step 4
98
82
 
99
- Now we arrive at the final piece of the puzzle: a service call module. To keep
100
- things contained, a Relax best practice is to create a module for each call
101
- on your service. The one we're creating here is the PhotoSearch module for the
102
- "flickr.photos.search" call on the Flickr API.
83
+ === A Small Refactoring
103
84
 
104
- There are three main pieces to every service call module:
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.
105
89
 
106
- 1. a Relax::Request object
107
- 2. a Relax::Response object
108
- 3. a call method that calls Relax::Service#call
90
+ class Flickr < Relax::Service
91
+ defaults do
92
+ parameter :api_key, :required => true
93
+ end
109
94
 
110
- Here's what the PhotoSearch module looks like:
95
+ endpoint 'http://api.flickr.com/services/rest/' do
96
+ defaults do
97
+ parameter :method, :required => true
98
+ end
111
99
 
112
- module Flickr
113
- module PhotoSearch
114
- class PhotoSearchRequest < Flickr::Request
115
- parameter :per_page
100
+ action :search do
101
+ set :method, 'flickr.photos.search'
102
+ parameter :per_page, :default => 5
116
103
  parameter :tags
117
-
118
- def initialize(options = {})
119
- super
120
- @method = 'flickr.photos.search'
121
- end
122
104
  end
105
+ end
106
+ end
123
107
 
124
- class PhotoSearchResponse < Flickr::Response
125
- parameter :photos, :element => 'photos/photo', :collection => Photo
126
- end
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.
127
110
 
128
- def search(options = {})
129
- call(PhotoSearchRequest.new(options), PhotoSearchResponse)
130
- end
131
111
 
132
- def find_by_tag(tags, options = {})
133
- search(options.merge(:tags => tags))
134
- end
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
135
120
  end
136
- end
137
121
 
138
- As you can see, we have our request (PhotoSearchRequest), response
139
- (PhotoSearchResponse), and call method (actually, two in this case: search and
140
- find_by_tag). This now needs to be included into our Flickr::Service class,
141
- and then we'll be able to use it by calling either of the call methods.
122
+ endpoint 'http://api.flickr.com/services/rest/' do
123
+ defaults do
124
+ parameter :method, :required => true
125
+ end
142
126
 
143
- module Flickr
144
- class Service < Relax::Service
145
- include Flickr::PhotoSearch
127
+ action :search do
128
+ set :method, 'flickr.photos.search'
129
+ parameter :per_page, :default => 5
130
+ parameter :tags
146
131
 
147
- ENDPOINT = 'http://api.flickr.com/services/rest/'
148
-
149
- def initialize(api_key)
150
- super(ENDPOINT)
151
- Request[:api_key] = api_key
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
152
154
  end
153
155
  end
154
156
  end
155
157
 
156
- Now we're ready to make a call against the API:
158
+ dhe parsing is performed by the Relief gem, so you can find out more about the
159
+ syntax in its own documentation.
157
160
 
158
- flickr = Flickr::Service.new(ENV['FLICKR_API_KEY'])
159
- relax = flickr.find_by_tag('relax', :per_page => 10)
160
161
 
161
- if relax.successful?
162
- relax.photos.each do |photo|
163
- puts "[#{photo.id}] #{photo.title}"
164
- end
165
- end
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
+ }
166
190
 
167
- This will output the IDs and titles for the first 10 photos on Flickr that have
168
- the tag "relax."
169
191
 
192
+ == Copyright
170
193
 
171
- Copyright (c) 2007-2008 Tyler Hunt, released under the MIT license
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
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
@@ -0,0 +1,39 @@
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
+ context.evaluate(&block) if block_given?
14
+ end
15
+
16
+ def execute(values, credentials, *args)
17
+ args.unshift(values) if values
18
+ instance = Instance.new(*args)
19
+ response = performer(instance, credentials).perform
20
+ context.parse(response)
21
+ end
22
+
23
+ def method
24
+ @options[:method] || :get
25
+ end
26
+ private :method
27
+
28
+ def url
29
+ [@endpoint.url, @options[:url]].join
30
+ end
31
+ private :url
32
+
33
+ def performer(instance, credentials)
34
+ values = instance.values(context)
35
+ Performer.new(method, url, values, credentials)
36
+ end
37
+ private :performer
38
+ end
39
+ end
@@ -0,0 +1,40 @@
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 ||= Relief::Parser.new(root, options, &block)
29
+ end
30
+
31
+ def parse(response) # :nodoc:
32
+ @parser.parse(response)
33
+ end
34
+
35
+ def clone # :nodoc:
36
+ cloned_parameters = @parameters.collect { |parameter| parameter.clone }
37
+ self.class.new(cloned_parameters)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ module Relax
2
+ module Contextable # :nodoc:
3
+ def context
4
+ @context ||= Context.new
5
+ end
6
+
7
+ def extend_context(base)
8
+ @context = base.context.clone
9
+ end
10
+
11
+ def defaults(&block)
12
+ context.evaluate(&block)
13
+ end
14
+ end
15
+ 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
@@ -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,32 @@
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
+ end
9
+
10
+ def perform
11
+ case @method
12
+ when :delete, :get, :head then RestClient.send(@method, url)
13
+ when :post, :put then RestClient.send(@method, url, query)
14
+ end
15
+ end
16
+
17
+ def url
18
+ uri = URI.parse(@url)
19
+ uri.query = query unless query.nil? || query.empty?
20
+ uri.userinfo = @credentials.join(':') if @credentials
21
+ uri.to_s
22
+ end
23
+ private :url
24
+
25
+ def query
26
+ @values.collect do |name, value|
27
+ "#{name}=#{value}" if value
28
+ end.compact.join('&')
29
+ end
30
+ private :query
31
+ end
32
+ end