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