ECS 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,7 @@
1
+ 0.1.0
2
+ * Initial release
3
+ * Forces ECS API version 2007-01-17
4
+ * Requires libxml-ruby (tested with libxml-ruby-0.3.8.4)
5
+ * The XML returned from ECS, when parsed into an XML::Document or XML::Node object (libxml-ruby-0.3.8.4)
6
+ breaks the find() method on both classes. If I strip the namespace, it works.
7
+ ECS.strip_namespace? governs this behavior.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Zachary Holt
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,3 @@
1
+ Please see the MIT-LICENSE file for copyright and distribution information.
2
+
3
+ ECS is a Ruby interface to Amazon's E-Commerce Service.
data/lib/ecs.rb ADDED
@@ -0,0 +1,268 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ dir = File::dirname( __FILE__ )
4
+
5
+ require 'rubygems'
6
+ require 'xml/libxml'
7
+ require 'digest/md5'
8
+ require 'net/http'
9
+ require 'erb'
10
+
11
+ module ECS
12
+ Dir[File.join( File.dirname( __FILE__ ), "*.rb" )].each { |f| require f }
13
+
14
+ require File.join( File.dirname( __FILE__ ), 'ecs', 'operations' )
15
+ require File.join( File.dirname( __FILE__ ), 'ecs', 'response_groups' )
16
+ require File.join( File.dirname( __FILE__ ), 'ecs', 'help' )
17
+ require File.join( File.dirname( __FILE__ ), 'ecs', 'help_response_group' )
18
+ require File.join( File.dirname( __FILE__ ), 'ecs', 'time_management' )
19
+
20
+ # [ 'help', 'help_response_group', 'operations', 'response_groups', 'time_management' ].each { |f| require( File.join( File.dirname( __FILE__ ), 'ecs', f ) ) }
21
+ # Dir[File.join( File.join( File.dirname( __FILE__ ), 'ecs' ), "*.rb" )].each { |f| require f }
22
+ # Dir[File.join( File.join( File.dirname( __FILE__ ), 'ecs', 'operation' ), "*.rb" )].each { |f| require f }
23
+ # Dir[File.join( File.join( File.dirname( __FILE__ ), 'ecs', 'response_group' ), "*.rb" )].each { |f| require f }
24
+
25
+ @@associate_id = 'limn-20'
26
+ @@access_key_id = ''
27
+ @@default_default_locale = :us
28
+ @@default_locale = @@default_default_locale
29
+ @@cache_directory = ''
30
+ @@cache = true
31
+ @@cache_suffix = 'ecs_cache'
32
+
33
+ # libxml-ruby is not able to do XML::Document#find or XML::Node#find with xml returned from Amazon.
34
+ # If you remove the namespace from the xml, it works perfectly
35
+ @@strip_namespace = true
36
+
37
+ def self.method_missing( meth, *args )
38
+ meth_s = meth.to_s
39
+ begin
40
+ klass_name = meth_s.camel_case
41
+ instance = nil
42
+ if meth_s =~ /^(.*)_response_group$/
43
+ instance = self.resolve_response_group_klass( klass_name )
44
+ else
45
+ klass = self.resolve_operation_klass( klass_name )
46
+
47
+ # string_to_eval = "#{klass_name}.new"
48
+ # puts "Now going to #{string_to_eval}"
49
+ instance = klass.new
50
+ # instance = instance_eval( string_to_eval, __FILE__, __LINE__ )
51
+ instance.parameters = *args
52
+ instance.parameters = {} unless instance.parameters.is_a?( Hash )
53
+
54
+ # If no ResponsGroup is set, then allow the defaults
55
+ unless instance.parameters[:ResponseGroup].nil?
56
+ # Otherwise, be sure :Request is included
57
+ instance.parameters[:ResponseGroup] = [instance.parameters[:ResponseGroup]] unless instance.parameters[:ResponseGroup].is_a?( Array )
58
+ instance.parameters[:ResponseGroup] << :Request unless instance.parameters[:ResponseGroup].include?( :Request ) || instance.parameters[:ResponseGroup].include?( 'Request' )
59
+ end
60
+ end
61
+ instance
62
+ rescue NameError => e
63
+ raise NameError, "#{e.message}"#Are you sure that ECS::#{klass_name} is defined?"
64
+ end
65
+ end
66
+
67
+
68
+ def self.associate_id
69
+ @@associate_id
70
+ end
71
+ def self.associate_id=( k )
72
+ @@associate_id = k.to_s
73
+ end
74
+ def self.access_key_id
75
+ @@access_key_id
76
+ end
77
+ def self.access_key_id=( i )
78
+ @@access_key_id = i.to_s
79
+ end
80
+
81
+ def self.default_locale
82
+ @@default_locale
83
+ end
84
+ def self.default_locale=( loc )
85
+ @@default_locale = self.resolve_locale( loc )
86
+ end
87
+ def self.reset_default_locale
88
+ @@default_locale = @@default_default_locale
89
+ end
90
+
91
+ def self.cache_directory
92
+ @@cache_directory
93
+ end
94
+ def self.cache_directory=( d='' )
95
+ directory = File.expand_path( d )
96
+ Dir.mkdir( directory ) unless File.exist?( directory )
97
+ raise( EACCESS, "#{directory} is not a suitable cache directory" ) unless File.writable?( directory ) && File.directory?( directory )
98
+ @@cache_directory = directory
99
+ end
100
+
101
+
102
+ def self.available_locales
103
+ [ :ca, :de, :fr, :jp, :uk, :us ]
104
+ end
105
+
106
+ def self.base_urls
107
+ {
108
+ :ca => 'http://webservices.amazon.ca/onca/xml?Service=AWSECommerceService',
109
+ :de => 'http://webservices.amazon.de/onca/xml?Service=AWSECommerceService',
110
+ :fr => 'http://webservices.amazon.fr/onca/xml?Service=AWSECommerceService',
111
+ :jp => 'http://webservices.amazon.co.jp/onca/xml?Service=AWSECommerceService',
112
+ :uk => 'http://webservices.amazon.co.uk/onca/xml?Service=AWSECommerceService',
113
+ :us => 'http://webservices.amazon.com/onca/xml?Service=AWSECommerceService'
114
+ }
115
+ end
116
+
117
+
118
+ def self.api_version
119
+ '2007-02-22'
120
+ end
121
+
122
+ def self.strip_namespace?
123
+ @@strip_namespace != false
124
+ end
125
+ def self.strip_namespace=( s )
126
+ @@strip_namespace = s != false
127
+ end
128
+
129
+ def self.call_web_service( p={} )
130
+ parameters = p.is_a?( Hash ) ? p : {}
131
+
132
+ locale = self.resolve_locale( parameters[:locale] )
133
+ parameters.delete( :locale )
134
+
135
+ sleep( ECS::TimeManagement.sleep_duration )
136
+ url = "#{ self.base_urls[locale] }&AWSAccessKeyId=#{self.access_key_id}&Version=#{self.api_version}&AssociateTag=#{self.associate_id}&#{ parameters.keys.map{ |k| "#{ERB::Util.url_encode( k.to_s ) }=#{ERB::Util.url_encode( parameters[k].is_a?( Array ) ? parameters[k].join( ',' ) : parameters[k].to_s )}" }.join( '&' ) }"
137
+ #puts url
138
+ r = Net::HTTP.get_response( URI.parse( url ) )
139
+ r.value # this will raise an error if there was one.
140
+ @@strip_namespace && r.body =~ /(.*)xmlns\s*=\s*".*?"(.*)/m ? "#{$1}#{$2}" : r.body
141
+ end
142
+
143
+ def self.resolve_locale( l=nil )
144
+ !l.nil? && self.available_locales.include?( l.to_s.downcase.to_sym ) ? l.to_s.downcase.to_sym : self.default_locale
145
+ end
146
+
147
+ def self.resolve_response_group_klass( name='' )
148
+ klass_name = ECS::HelpResponseGroup.resolve_response_group_name( name )
149
+ klass = ECS::ResponseGroups[klass_name]
150
+ if klass.nil?
151
+ string_to_eval = "class #{klass_name} < HelpResponseGroup
152
+ @@response_group_name=nil
153
+ @@valid_operations=nil
154
+ @@elements=nil
155
+ @@help_xml=nil
156
+ ECS::ResponseGroups['#{klass_name}'] = self
157
+ end
158
+ #{klass_name}"
159
+ #puts string_to_eval
160
+ klass = instance_eval( string_to_eval, __FILE__, __LINE__ )
161
+ end
162
+ klass
163
+ end
164
+ def self.resolve_operation_klass( name='' )
165
+ klass_name = ECS::Help.resolve_operation_name( name )
166
+ klass = ECS::Operations[klass_name]
167
+ if klass.nil?
168
+ string_to_eval = "class #{klass_name} < Help
169
+ @@operation_name=nil
170
+ @@help_xml=nil
171
+ @@required_parameters=nil
172
+ @@available_parameters=nil
173
+ @@default_response_groups=nil
174
+ @@available_response_groups=nil
175
+ ECS::Operations['#{klass_name}'] = self
176
+ end
177
+ #{klass_name}"
178
+ # puts string_to_eval
179
+ klass = instance_eval( string_to_eval, __FILE__, __LINE__ )
180
+ end
181
+ klass
182
+ end
183
+
184
+ def self.xml_for_parameters( parameters={}, force=false )
185
+ # if you pass a block to this method, the XML::Document object
186
+ # will be yielded (if it's not cached) and you will be able to return
187
+ # true to cache it or false to not cache it. The object is yielded
188
+ # regardless of the value of ECS.cache?
189
+ # In other words, ECS.cache? is respected only when there is no block
190
+ tag = self.resolve_cache_tag( parameters )
191
+ xml = nil
192
+
193
+ xml = self.read_cache( tag ) unless force
194
+
195
+ if xml.nil?
196
+ xml = self.call_web_service( parameters )
197
+ parser = XML::Parser.new
198
+ parser.string = xml
199
+ xml = parser.parse
200
+ we_should_cache = block_given? ? yield( xml ) : self.cache?
201
+ self.write_cache( tag, xml.to_s ) if we_should_cache
202
+ end
203
+
204
+ xml
205
+ end
206
+
207
+ def self.read_cache( tag_or_parameters )
208
+ tag = tag_or_parameters.is_a?( String ) ? tag_or_parameters : self.resolve_cache_tag( tag_or_parameters )
209
+ XML::Document.file( tag ) if File.exist?( tag )
210
+ end
211
+ def self.write_cache( tag_or_parameters, content )
212
+ tag = tag_or_parameters.is_a?( String ) ? tag_or_parameters : self.resolve_cache_tag( tag_or_parameters )
213
+ File.open( tag, 'w' ) { |f| f << content }
214
+ end
215
+
216
+ def self.clear_cache( tag=nil )
217
+ if tag.nil?
218
+ Dir[File.join( self.cache_directory, "*.#{@@cache_suffix}" )].each { |f| File.unlink( f ) }
219
+ else
220
+ File.unlink( tag )
221
+ end
222
+ end
223
+
224
+ def self.cache?
225
+ @@cache != false
226
+ end
227
+ def self.cache=( true_or_false=true )
228
+ @@cache = true_or_false != false
229
+ end
230
+
231
+ def self.cached?( p={} )
232
+ File.exist?( p.is_a?( String ) ? p : self.resolve_cache_tag( p ) )
233
+ end
234
+
235
+
236
+ def self.query_string( p={} )
237
+ ( p.is_a?( Hash ) ? p : {} ).keys.map{ |k| k.to_s }.sort.map{ |k| "#{k}=#{p[k.to_sym]}" }.join( '&' )
238
+ end
239
+
240
+ def self.resolve_cache_tag( p={} )
241
+ "#{cache_directory}/#{Digest::MD5.hexdigest( p.is_a?( String ) ? p : query_string( p ) )}.#{@@cache_suffix}"
242
+ end
243
+ end
244
+
245
+ ##At this point I can do
246
+ ##
247
+ ##b = ECS.browse_node_lookup( :BrowseNodeId => 43242 )
248
+ ##
249
+ ##and b is an object of class ECS::BrowseNodeLookup
250
+ ##
251
+ ##
252
+ ##What I would like to be able to do is say
253
+ ##
254
+ ##1) b.results (or some such)
255
+ ##
256
+ ##and get either one ECS::BroseNodeInfo (which include ECS::ResponseGroup) object
257
+ ##or an array of ECS::BrowseNodeInfo objects
258
+ ##
259
+ ##
260
+ ##This should happen by
261
+ ##
262
+ ##2) Checking a @results instance variable
263
+ ##
264
+ ##3) If @results is null, parse and iterate over the xml, creating a ECS::BrowseNodeInfo object for each <BrowseNodeInfo> element
265
+ ##
266
+ ##4) If the xml is null, check the disk to see whether there is a cached version
267
+ ##
268
+ ##5) If there is no cached version, call the web service and get the xml, caching it when you are done with it
data/lib/ecs/help.rb ADDED
@@ -0,0 +1,282 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ module ECS
4
+ class Help
5
+ instance_variable_set( '@operation_name', nil )
6
+ instance_variable_set( '@help_xml', nil )
7
+ instance_variable_set( '@required_parameters', nil )
8
+ instance_variable_set( '@available_parameters', nil )
9
+ instance_variable_set( '@default_response_groups', nil )
10
+ instance_variable_set( '@available_response_groups', nil )
11
+
12
+
13
+ @@help_map = {
14
+ :required_parameters => { :call_path => 'HelpResponse.Information.OperationInformation.RequiredParameters.Parameter' },
15
+ :available_parameters => { :call_path => 'HelpResponse.Information.OperationInformation.AvailableParameters.Parameter' },
16
+ :default_response_groups => { :call_path => 'HelpResponse.Information.OperationInformation.DefaultResponseGroups.ResponseGroup' },
17
+ :available_response_groups => { :call_path => 'HelpResponse.Information.OperationInformation.AvailableResponseGroups.ResponseGroup' }
18
+ }
19
+
20
+ attr_accessor :parameters, :xml
21
+ ECS::Operations['Help'] = self
22
+
23
+ def self.operation_name
24
+ @operation_name ||= self.resolve_operation_name( self.name )
25
+ end
26
+ def self.help_xml( force=false )
27
+ if force || @help_xml.nil?
28
+ @help_xml = ECS.help( :HelpType => 'Operation', :About => self.operation_name ) { |x| ECS::Help.valid_aws_response( x ) }.xml
29
+ end
30
+ @help_xml
31
+ end
32
+
33
+
34
+ def self.valid_aws_response?( x )
35
+ xml = x
36
+ unless x.is_a?( XML::Document )
37
+ p = XML::Parser.new
38
+ p.string = x
39
+ xml = p.parse
40
+ end
41
+
42
+ if self.errors_from_aws( xml ).empty?
43
+ n = xml.find( '//Request/IsValid' ).first
44
+ n.nil? || n.content.downcase != 'false'
45
+ else
46
+ false
47
+ end
48
+ end
49
+
50
+ def self.errors_from_aws( x )
51
+ xml = x
52
+ unless x.is_a?( XML::Document )
53
+ p = XML::Parser.new
54
+ p.string = x
55
+ xml = p.parse
56
+ end
57
+
58
+ errors = []
59
+ xml.find( '//Errors/Error' ).each do |error_node|
60
+ begin
61
+ m = error_node.Message.content
62
+ c = error_node.Code.content.gsub( /[\.:\s,\-_]/, '' )
63
+ errors << RuntimeError.new( m )
64
+ rescue Exception => e
65
+ end
66
+ end
67
+ errors
68
+ end
69
+
70
+
71
+ def self.required_parameters( force=false )
72
+ if force || @required_parameters.nil?
73
+ @required_parameters = []
74
+ self.help_xml.HelpResponse.Information.OperationInformation.RequiredParameters.Parameter.each do |p|
75
+ @required_parameters << p.content.to_s.to_sym
76
+ end
77
+ end
78
+ @required_parameters
79
+ rescue
80
+ []
81
+ end
82
+ def self.available_parameters( force=false )
83
+ if force || @available_parameters.nil?
84
+ @available_parameters = []
85
+ self.help_xml.HelpResponse.Information.OperationInformation.AvailableParameters.Parameter.each do |p|
86
+ @available_parameters << p.content.to_s.to_sym
87
+ end
88
+ end
89
+ @available_parameters
90
+ end
91
+ def self.optional_parameters( force=false )
92
+ self.available_parameters( force )
93
+ end
94
+
95
+ def self.default_response_groups( force=false )
96
+ if force || @default_response_groups.nil?
97
+ @default_response_groups = []
98
+ self.help_xml.HelpResponse.Information.OperationInformation.DefaultResponseGroups.ResponseGroup.each do |p|
99
+ @default_response_groups << ECS.resolve_response_group_klass( "#{p.content.to_s}ResponseGroup" )
100
+ end
101
+ end
102
+ @default_response_groups
103
+ end
104
+ def self.available_response_groups( force=false )
105
+ if force || @available_response_groups.nil?
106
+ @available_response_groups = []
107
+ self.help_xml.HelpResponse.Information.OperationInformation.AvailableResponseGroups.ResponseGroup.each do |p|
108
+ @available_response_groups << ECS.resolve_response_group_klass( "#{p.content.to_s}ResponseGroup" )
109
+ end
110
+ end
111
+ @available_response_groups
112
+ end
113
+
114
+
115
+ def self.resolve_operation_name( t )
116
+ t =~ /::([^:]*)$/ ? $1 : t
117
+ end
118
+
119
+ def operation_name
120
+ self.class.operation_name
121
+ end
122
+
123
+ def help_xml( force=false )
124
+ self.class.help_xml( force )
125
+ end
126
+
127
+ def xml( force=false )
128
+ if force || @xml.nil?
129
+ self.class.required_parameters.each do |p|
130
+ raise(
131
+ ArgumentError,
132
+ "#{p} is a required parameter for the #{ self.operation_name } operation"
133
+ ) if !self.parameters.keys.include?( p.to_sym ) || self.parameters[p.to_sym].to_s.empty?
134
+ end
135
+
136
+ # Cache the xml only if it's a valid AWS Response
137
+ @xml = ECS.xml_for_parameters( self.parameters_for_xml ) { |x| ECS::Help.valid_aws_response?( x ) }
138
+ end
139
+ @xml
140
+ end
141
+
142
+ def parameters_for_xml
143
+ self.parameters.merge( :Operation => self.operation_name )
144
+ end
145
+
146
+ def cached?
147
+ ECS.cached?( self.parameters_for_xml )
148
+ end
149
+
150
+ def cached_content
151
+ ECS.read_cache( self.parameters_for_xml )
152
+ end
153
+
154
+ def headers( force=false )
155
+ if force || @headers.nil?
156
+ @headers = {}
157
+ self.xml.find( '//OperationRequest/HTTPHeaders/Header' ).each do |header|
158
+ name = ''
159
+ value = ''
160
+ header.each_attr do |attribute|
161
+ name = attribute.value.to_sym if attribute.name == 'Name'
162
+ value = attribute.value if attribute.name == 'Value'
163
+ end
164
+ @headers[name] = value
165
+ end
166
+ end
167
+ @headers
168
+ rescue
169
+ @headers = []
170
+ end
171
+
172
+ def request_id( force=false )
173
+ if force || @request_id.nil?
174
+ @request_id = self.xml.find( '//OperationRequest/RequestId' ).first.content
175
+ end
176
+ @request_id
177
+ rescue
178
+ @request_id = 'Could not determine request_id'
179
+ end
180
+
181
+ def arguments( force=false )
182
+ if force || @arguments.nil?
183
+ @arguments = {}
184
+ self.xml.find( '//OperationRequest/Arguments/Argument' ).each do |argument|
185
+ name = ''
186
+ value = ''
187
+ argument.each_attr do |attribute|
188
+ name = attribute.value.to_sym if attribute.name == 'Name'
189
+ value = attribute.value if attribute.name == 'Value'
190
+ end
191
+ @arguments[name] = value
192
+ end
193
+ end
194
+ @arguments
195
+ rescue
196
+ @arguments = {}
197
+ end
198
+
199
+ def request_processing_time( force=false )
200
+ if force || @request_processing_time.nil?
201
+ @request_processing_time = self.xml.find( '//OperationRequest/RequestProcessingTime' ).first.content.to_f
202
+ end
203
+ @request_processing_time
204
+ rescue
205
+ @request_processing_time = -1.0
206
+ end
207
+
208
+ def response_groups( force=false )
209
+ #these are the response groups that are included in this instance,
210
+ #not all those available to the class
211
+ if force || @response_groups.nil?
212
+ @response_groups = []
213
+
214
+ response_group_names = self.arguments[:ResponseGroup]
215
+
216
+ if response_group_names.nil?
217
+ @response_groups = self.class.default_response_groups
218
+ else
219
+ response_group_names.split( ',' ).each do |response_group_name|
220
+ @response_groups << ECS.resolve_response_group_klass( "#{response_group_name}ResponseGroup" )
221
+ end
222
+ end
223
+ end
224
+ @response_groups
225
+ end
226
+
227
+ def potential_elements( force=false )
228
+ if force || @potential_elements.nil?
229
+ @potential_elements = []
230
+ self.response_groups.each do |response_group|
231
+ @potential_elements << response_group.elements
232
+ end
233
+ @potential_elements.flatten!.uniq!
234
+ end
235
+ @potential_elements
236
+ end
237
+
238
+ def errors( force=false )
239
+ @errors = self.class.errors_from_aws( self.xml ) if force || @errors.nil?
240
+ @errors
241
+ end
242
+
243
+ def valid?( force=false )
244
+ @valid = self.class.valid_aws_response?( self.xml ) if force || @valid.nil?
245
+ @valid
246
+ end
247
+
248
+
249
+ def method_missing( meth, *args )
250
+ method_name = meth.to_s
251
+
252
+ # Note that if there is an element in the xml called each_*,
253
+ # in order to get to it, you can't pass a block
254
+ if method_name =~ /^each_(.*)$/ && block_given?
255
+ x = self.send( $1.to_sym, *args )
256
+ if [ Array, XML::Node::Set].include?( x.class )
257
+ x.each { |y| yield( y ) }
258
+ else
259
+ yield( x )
260
+ end
261
+ elsif method_name =~ /^(.*)_count$/
262
+ # Note that this will return the count for all descendants
263
+ # Not just children
264
+ x = self.send( $1.to_sym, *args )
265
+ x.respond_to?( :size ) ? x.size : 1
266
+ else
267
+ instance_variable_name = "@#{method_name}_var"
268
+ self.instance_eval( "
269
+ def #{method_name}( force=false )
270
+ if force || #{instance_variable_name}.nil?
271
+ #{instance_variable_name} = self.xml.find( '//#{method_name}' )
272
+ #{instance_variable_name} = #{instance_variable_name}.first if #{instance_variable_name}.size == 1
273
+ end
274
+ #{instance_variable_name}
275
+ end
276
+ " )
277
+
278
+ self.send( meth, *args )
279
+ end
280
+ end
281
+ end
282
+ end