relax 0.0.7 → 0.1.0

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