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