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.
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.rdoc ADDED
@@ -0,0 +1,194 @@
1
+ = Relax
2
+
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.
6
+
7
+
8
+ == Tutorial
9
+
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
+
12
+
13
+ === First Things First
14
+
15
+ The first step we'll take is to load the Relax gem.
16
+
17
+ require 'rubygems'
18
+
19
+ gem 'relax', '~> 0.1.0'
20
+ require 'relax'
21
+
22
+ Then we'll define our service class.
23
+
24
+ class Flickr < Relax::Service
25
+ end
26
+
27
+
28
+ === Adding an Endpoint
29
+
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.
33
+
34
+ class Flickr < Relax::Service
35
+ endpoint 'http://api.flickr.com/services/rest/' do
36
+ end
37
+ end
38
+
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
48
+ end
49
+
50
+ endpoint 'http://api.flickr.com/services/rest/' do
51
+ end
52
+ end
53
+
54
+
55
+ === Adding an Action
56
+
57
+ So we have our service now, but we need to define an action before we can
58
+ actually fetch any data from it.
59
+
60
+ class Flickr < Relax::Service
61
+ defaults do
62
+ parameter :api_key, :required => true
63
+ end
64
+
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
71
+ end
72
+ end
73
+
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.
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.
81
+
82
+
83
+ === A Small Refactoring
84
+
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.
89
+
90
+ class Flickr < Relax::Service
91
+ defaults do
92
+ parameter :api_key, :required => true
93
+ end
94
+
95
+ endpoint 'http://api.flickr.com/services/rest/' do
96
+ defaults do
97
+ parameter :method, :required => true
98
+ end
99
+
100
+ action :search do
101
+ set :method, 'flickr.photos.search'
102
+ parameter :per_page, :default => 5
103
+ parameter :tags
104
+ end
105
+ end
106
+ end
107
+
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.
110
+
111
+
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
120
+ end
121
+
122
+ endpoint 'http://api.flickr.com/services/rest/' do
123
+ defaults do
124
+ parameter :method, :required => true
125
+ end
126
+
127
+ action :search do
128
+ set :method, 'flickr.photos.search'
129
+ parameter :per_page, :default => 5
130
+ parameter :tags
131
+
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
154
+ end
155
+ end
156
+ end
157
+
158
+ dhe parsing is performed by the Relief gem, so you can find out more about the
159
+ syntax in its own documentation.
160
+
161
+
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
+ }
190
+
191
+
192
+ == Copyright
193
+
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: 1
data/lib/relax.rb CHANGED
@@ -1,13 +1,18 @@
1
- $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
1
+ require 'rubygems'
2
2
 
3
- require 'relax/query'
4
- require 'relax/parsers'
5
- require 'relax/request'
6
- require 'relax/response'
7
- require 'relax/service'
8
- require 'relax/symbolic_hash'
3
+ gem 'relief', '~> 0.0.2'
4
+ require 'relief'
9
5
 
10
- module Relax
11
- class MissingParameter < ArgumentError ; end
12
- class UnrecognizedParser < ArgumentError ; end
6
+ gem 'rest-client', '~> 0.9.2'
7
+ require 'restclient'
8
+
9
+ module Relax # :nodoc:
10
+ autoload :Action, 'relax/action'
11
+ autoload :Context, 'relax/context'
12
+ autoload :Contextable, 'relax/contextable'
13
+ autoload :Endpoint, 'relax/endpoint'
14
+ autoload :Instance, 'relax/instance'
15
+ autoload :Parameter, 'relax/parameter'
16
+ autoload :Performer, 'relax/performer'
17
+ autoload :Service, 'relax/service'
13
18
  end
@@ -0,0 +1,49 @@
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
+ parse_url_parameters
14
+ context.evaluate(&block) if block_given?
15
+ end
16
+
17
+ def execute(values, credentials, *args)
18
+ args.unshift(values) if values
19
+ instance = Instance.new(*args)
20
+ response = performer(instance, credentials).perform
21
+ context.parse(response)
22
+ end
23
+
24
+ def method
25
+ @options[:method] || :get
26
+ end
27
+ private :method
28
+
29
+ def url
30
+ [@endpoint.url, @options[:url]].join
31
+ end
32
+ private :url
33
+
34
+ def performer(instance, credentials)
35
+ values = instance.values(context)
36
+ Performer.new(method, url, values, credentials)
37
+ end
38
+ private :performer
39
+
40
+ def parse_url_parameters
41
+ url.scan(/(?:\:)([a-z_]+)/).flatten.each do |name|
42
+ defaults do
43
+ parameter name.to_sym, :required => true
44
+ end
45
+ end
46
+ end
47
+ private :parse_url_parameters
48
+ end
49
+ end
@@ -0,0 +1,41 @@
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 ||= root.kind_of?(Class) ? root.new(options, &block) :
29
+ Relief::Parser.new(root, options, &block)
30
+ end
31
+
32
+ def parse(response) # :nodoc:
33
+ @parser.parse(response)
34
+ end
35
+
36
+ def clone # :nodoc:
37
+ cloned_parameters = @parameters.collect { |parameter| parameter.clone }
38
+ self.class.new(cloned_parameters)
39
+ end
40
+ end
41
+ 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