woahdae-consumer 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/LICENSE +20 -0
- data/Manifest.txt +73 -0
- data/PostInstall.txt +8 -0
- data/README.rdoc +53 -0
- data/Rakefile +30 -0
- data/app_generators/consumer/USAGE +14 -0
- data/app_generators/consumer/consumer_generator.rb +89 -0
- data/app_generators/consumer/templates/LICENSE +20 -0
- data/app_generators/consumer/templates/README.rdoc +3 -0
- data/app_generators/consumer/templates/Rakefile +57 -0
- data/app_generators/consumer/templates/TODO +4 -0
- data/app_generators/consumer/templates/config/config.yml +2 -0
- data/app_generators/consumer/templates/config/config.yml.sample +1 -0
- data/app_generators/consumer/templates/lib/base.rb +6 -0
- data/app_generators/consumer/templates/rails/init.rb +1 -0
- data/app_generators/consumer/templates/script/destroy +14 -0
- data/app_generators/consumer/templates/script/generate +14 -0
- data/app_generators/consumer/templates/spec/spec_helper.rb +11 -0
- data/bin/consumer +17 -0
- data/config/website.yml.sample +2 -0
- data/consumer.gemspec +48 -0
- data/consumer_generators/request/USAGE +25 -0
- data/consumer_generators/request/request_generator.rb +94 -0
- data/consumer_generators/request/templates/lib/request.rb +55 -0
- data/consumer_generators/request/templates/lib/response.rb +12 -0
- data/consumer_generators/request/templates/spec/request_spec.rb +27 -0
- data/consumer_generators/request/templates/spec/response_spec.rb +10 -0
- data/consumer_generators/request/templates/spec/xml/response.xml +0 -0
- data/examples/active_record/README.txt +1 -0
- data/examples/active_record/ar_spec.rb +33 -0
- data/examples/active_record/database.sqlite +0 -0
- data/examples/active_record/environment.rb +15 -0
- data/examples/active_record/migration.rb +21 -0
- data/examples/active_record/models/book.rb +13 -0
- data/examples/active_record/models/contributor.rb +12 -0
- data/examples/active_record/xml/book.xml +6 -0
- data/examples/active_record/xml/book_with_contributors.xml +11 -0
- data/examples/active_record/xml/contributor.xml +3 -0
- data/examples/active_record/xml/contributor_with_books.xml +19 -0
- data/examples/shipping/environment.rb +3 -0
- data/examples/shipping/rate.rb +15 -0
- data/examples/shipping/shipping.yml.sample +8 -0
- data/examples/shipping/shipping_spec.rb +27 -0
- data/examples/shipping/ups_rate_request.rb +182 -0
- data/examples/shipping/ups_rate_response.xml +340 -0
- data/lib/consumer/helper.rb +111 -0
- data/lib/consumer/mapping.rb +184 -0
- data/lib/consumer/request.rb +280 -0
- data/lib/consumer.rb +28 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/script/txt2html +71 -0
- data/spec/helper_spec.rb +136 -0
- data/spec/mapping_spec.rb +94 -0
- data/spec/request_spec.rb +75 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/xml/rate_response.xml +14 -0
- data/spec/xml/rate_response_error.xml +35 -0
- data/tasks/rspec.rake +21 -0
- data/test/test_consumer_generator.rb +68 -0
- data/test/test_generator_helper.rb +29 -0
- data/website/index.html +11 -0
- data/website/index.txt +81 -0
- data/website/javascripts/rounded_corners_lite.inc.js +285 -0
- data/website/stylesheets/screen.css +159 -0
- data/website/template.html.erb +50 -0
- metadata +180 -0
@@ -0,0 +1,280 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
##
|
6
|
+
# === Class Attrubutes
|
7
|
+
# [+required+] Defines attributes that must be present in any instance
|
8
|
+
# before calling +do+. Anything defined here but not set in
|
9
|
+
# the instance will raise a RequiredFieldError on calling +do+
|
10
|
+
#
|
11
|
+
# Defaluts to []
|
12
|
+
# [+response_class+] String for setting the class the request will use to parse
|
13
|
+
# the response.
|
14
|
+
#
|
15
|
+
# Defaults to [Something]Request, ex. for a
|
16
|
+
# RateRequest, this would default to "Rate".
|
17
|
+
#
|
18
|
+
# Note that the instance method with the same name returns a
|
19
|
+
# constant rather than the string set here.
|
20
|
+
# [+yaml_defaults+] Consists of two parameters. In order:
|
21
|
+
# * The location (as a string) for a yaml file containing
|
22
|
+
# attribute defaults.
|
23
|
+
# * A namespace to grab the defaults out of.
|
24
|
+
#
|
25
|
+
# If your yaml looked like this:
|
26
|
+
#
|
27
|
+
# <pre>
|
28
|
+
# ups:
|
29
|
+
# user_id: Woody
|
30
|
+
# usps:
|
31
|
+
# user_id: John
|
32
|
+
# </pre>
|
33
|
+
#
|
34
|
+
# UPSRequest would want to use "ups" as the namespace value,
|
35
|
+
# where USPSRequest would want to use "usps"
|
36
|
+
#
|
37
|
+
# This is optional and has no default
|
38
|
+
# [+error_paths+] If you define this in your subclasses to return a hash for
|
39
|
+
# error options (see "Options" below) they will raise
|
40
|
+
# informative RequestError errors with xml error info in the
|
41
|
+
# message.
|
42
|
+
#
|
43
|
+
# If this is left undefined and the remote server returns
|
44
|
+
# error xml rather than what you were expecting, you'll get
|
45
|
+
# generic xml parsing errors instead of something informative.
|
46
|
+
#
|
47
|
+
# Note: currently only handles one error.
|
48
|
+
#
|
49
|
+
# === Options
|
50
|
+
#
|
51
|
+
# All options are xpaths. All options except +:root+ are relative
|
52
|
+
# to the root (unless prefixed with "//")
|
53
|
+
# * +:root+ - Root element of the error(s)
|
54
|
+
# * +:code+ - Remote API's error code for this error
|
55
|
+
# * +:message+ - Informative part of the error
|
56
|
+
#
|
57
|
+
# === Example
|
58
|
+
#
|
59
|
+
# <pre>
|
60
|
+
# {
|
61
|
+
# :root => "//Error",
|
62
|
+
# :code => "ErrorCode",
|
63
|
+
# :message => "LongDescription"
|
64
|
+
# }
|
65
|
+
# </pre>
|
66
|
+
#
|
67
|
+
# Anything passed in to initialize will override these, though.
|
68
|
+
class Consumer::Request
|
69
|
+
include Consumer
|
70
|
+
|
71
|
+
class << self
|
72
|
+
def url(url = nil)
|
73
|
+
@url = url if url
|
74
|
+
@url
|
75
|
+
end
|
76
|
+
|
77
|
+
def required(*args)
|
78
|
+
@required = args if !args.empty?
|
79
|
+
@required || []
|
80
|
+
end
|
81
|
+
|
82
|
+
def response_class(klass = nil)
|
83
|
+
@response_class = klass if klass
|
84
|
+
self.to_s =~ /(.+?)Request/
|
85
|
+
@response_class || $1
|
86
|
+
end
|
87
|
+
|
88
|
+
def yaml_defaults(*args)
|
89
|
+
@yaml_defaults = args if !args.empty?
|
90
|
+
@yaml_defaults
|
91
|
+
end
|
92
|
+
|
93
|
+
def defaults(defaults = nil)
|
94
|
+
@defaults = defaults if defaults
|
95
|
+
@defaults || {}
|
96
|
+
end
|
97
|
+
|
98
|
+
def error_paths(options = nil)
|
99
|
+
@error_paths = options if options
|
100
|
+
@error_paths
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class RequestError < StandardError;end
|
105
|
+
class RequiredFieldError < StandardError;end
|
106
|
+
|
107
|
+
attr_reader :response_xml, :request_xml
|
108
|
+
|
109
|
+
# First gets defaults from self.defaults, merges them with defaults from
|
110
|
+
# +yaml_defaults+, merges all that with passed in attrs, and initializes all
|
111
|
+
# those into instance variables for use in to_xml.
|
112
|
+
def initialize(attrs = {})
|
113
|
+
# it's really handy to have all the other attrs init'd when we call
|
114
|
+
# self.defaults 'cuz we can use them to help define conditional defaults.
|
115
|
+
root = self.config_root
|
116
|
+
yaml = Helper.hash_from_yaml(root, *yaml_defaults)
|
117
|
+
yaml, attrs = symbolize_keys(yaml, attrs)
|
118
|
+
|
119
|
+
initialize_attrs(yaml.merge(attrs)) # load yaml, but attrs will overwrite dups
|
120
|
+
|
121
|
+
# now self.defaults has access to above stuff
|
122
|
+
class_defaults = self.defaults
|
123
|
+
class_defaults = symbolize_keys(class_defaults).first
|
124
|
+
|
125
|
+
# but we wanted defaults loaded first.
|
126
|
+
all_defaults = class_defaults.merge(yaml)
|
127
|
+
final_attrs = all_defaults.merge(attrs)
|
128
|
+
initialize_attrs(final_attrs)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Sends self.to_xml to self.url and returns new object(s) created via
|
132
|
+
# [response_class].from_xml
|
133
|
+
# === Prerequisites
|
134
|
+
# * All attributes in self.required must exist; raises RequiredFieldError
|
135
|
+
# otherwise
|
136
|
+
# * self.to_xml must be defined; RuntimeError otherwise
|
137
|
+
# * self.url must be set; RuntimeError otherwise
|
138
|
+
# * response_class.from_xml must be defined; RuntimeError otherwise
|
139
|
+
# === Returns
|
140
|
+
# Whatever response_class.from_xml(@response_xml) returns, which should be
|
141
|
+
# an object or array of objects (an array of objects if response_class is
|
142
|
+
# using Consumer::Mapping)
|
143
|
+
def do
|
144
|
+
return if defined?(self.abort?) && self.abort?
|
145
|
+
raise "to_xml not defined for #{self.class}" if not defined?(self.to_xml)
|
146
|
+
raise "url not defined for #{self.class}" if not self.url
|
147
|
+
raise "from_xml not defined for #{response_class}" if not defined?(response_class.from_xml)
|
148
|
+
|
149
|
+
@request_xml = self.to_xml_etc
|
150
|
+
|
151
|
+
http, uri = Helper.http_from_url(self.url)
|
152
|
+
head = defined?(self.headers) ? self.headers : {}
|
153
|
+
|
154
|
+
puts "\n##### Request to #{url}:\n\n#{@request_xml}\n" if $DEBUG
|
155
|
+
debugger if $POST_DEBUGGER
|
156
|
+
resp = http.post(uri.request_uri, @request_xml, head)
|
157
|
+
|
158
|
+
if resp.response.code == "302" # moved
|
159
|
+
puts "\n##### Redirected to #{resp['Location']}\n" if $DEBUG
|
160
|
+
http, uri = Helper.http_from_url(resp['Location'])
|
161
|
+
resp = http.post(uri.request_uri, @request_xml, head)
|
162
|
+
end
|
163
|
+
|
164
|
+
@response_xml = resp.body
|
165
|
+
puts "\n##### Response:\n\n#{Helper.tidy(@response_xml)}\n" if $DEBUG
|
166
|
+
|
167
|
+
check_request_error(@response_xml)
|
168
|
+
|
169
|
+
return response_class.from_xml(@response_xml)
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.do(args = {})
|
173
|
+
self.new(args).do
|
174
|
+
end
|
175
|
+
|
176
|
+
# Gets called during do instead of just to_xml, and does a bit more than
|
177
|
+
# just return xml.
|
178
|
+
#
|
179
|
+
# First, it calls before_to_xml if it has been defined.
|
180
|
+
# Then it calls check_required, then returns the results of to_xml sans
|
181
|
+
# empty nodes (see Helper.compact_xml).
|
182
|
+
#
|
183
|
+
# You can set a COMPACT_XML constant to false to avoid the latter behavior,
|
184
|
+
# but most APIs complain when you send them empty nodes (even if the nodes
|
185
|
+
# were optional to begin with).
|
186
|
+
def to_xml_etc
|
187
|
+
self.before_to_xml if defined?(before_to_xml)
|
188
|
+
self.check_required
|
189
|
+
xml = self.to_xml
|
190
|
+
return (defined?(COMPACT_XML) && !COMPACT_XML) ? xml : Helper.compact_xml(xml)
|
191
|
+
end
|
192
|
+
|
193
|
+
# returns self.class.response_class as a constant (not a string)
|
194
|
+
#
|
195
|
+
# Raises a runtime error if self.class.response_class is nil
|
196
|
+
def response_class
|
197
|
+
ret = self.class.response_class
|
198
|
+
raise "Invalid response_class; see docs for naming conventions etc" if !ret
|
199
|
+
return Object.const_get(ret)
|
200
|
+
end
|
201
|
+
|
202
|
+
def error_paths
|
203
|
+
self.class.error_paths
|
204
|
+
end
|
205
|
+
|
206
|
+
def required
|
207
|
+
self.class.required
|
208
|
+
end
|
209
|
+
|
210
|
+
def yaml_defaults
|
211
|
+
self.class.yaml_defaults
|
212
|
+
end
|
213
|
+
|
214
|
+
def url
|
215
|
+
self.class.url
|
216
|
+
end
|
217
|
+
|
218
|
+
def defaults
|
219
|
+
self.class.defaults
|
220
|
+
end
|
221
|
+
|
222
|
+
def config_root
|
223
|
+
if defined?(RAILS_ROOT)
|
224
|
+
RAILS_ROOT + "/config"
|
225
|
+
else
|
226
|
+
"config"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
protected
|
231
|
+
|
232
|
+
# Will raise a RequiredFieldError if an attribute in self.required is nil
|
233
|
+
def check_required
|
234
|
+
return if self.required.nil?
|
235
|
+
|
236
|
+
self.required.each do |attribute|
|
237
|
+
if eval("@#{attribute}").nil?
|
238
|
+
raise RequiredFieldError, "#{attribute} needs to be set"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
private
|
244
|
+
|
245
|
+
# If the xml contains an error notification, this'll raise a
|
246
|
+
# RequestError with the xml error code and message as defined in
|
247
|
+
# the options in error_paths. Returns nil otherwise.
|
248
|
+
def check_request_error(xml)
|
249
|
+
return if !error_paths
|
250
|
+
return if !xml || !xml.include?('<?xml')
|
251
|
+
|
252
|
+
response_doc = LibXML::XML::Parser.string(xml).parse
|
253
|
+
error = response_doc.find_first(error_paths[:root])
|
254
|
+
return if error.nil? || error.empty?
|
255
|
+
|
256
|
+
code = error.find_first(error_paths[:code]).first.content
|
257
|
+
message = error.find_first(error_paths[:message]).first.content
|
258
|
+
|
259
|
+
raise RequestError, "Code #{code}: #{message}"
|
260
|
+
end
|
261
|
+
|
262
|
+
def builder # :nodoc:
|
263
|
+
@builder ||= Builder::XmlMarkup.new(:target => @xml, :indent => 2)
|
264
|
+
end
|
265
|
+
alias :b :builder
|
266
|
+
|
267
|
+
|
268
|
+
# set instance variables based on a hash, i.e. @key = value
|
269
|
+
def initialize_attrs(attrs)
|
270
|
+
attrs.each do |attr, value|
|
271
|
+
self.instance_variable_set("@#{attr}", value)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def symbolize_keys(*hashes)
|
276
|
+
hashes.each do |hash|
|
277
|
+
hash.each {|k,v| hash[k.to_sym] = v;hash.delete(k.to_s)}
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
data/lib/consumer.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'libxml'
|
5
|
+
require 'builder'
|
6
|
+
|
7
|
+
# I just want singularize and constantize from ActiveSupport,
|
8
|
+
# but I don't need the rest of AS
|
9
|
+
module ActiveSupport
|
10
|
+
module CoreExtensions
|
11
|
+
module String
|
12
|
+
module Inflections
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
require 'active_support/core_ext/string/inflections'
|
18
|
+
class String
|
19
|
+
include ActiveSupport::CoreExtensions::String::Inflections
|
20
|
+
end
|
21
|
+
|
22
|
+
module Consumer
|
23
|
+
VERSION = '0.8.1'
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'consumer/mapping'
|
27
|
+
require 'consumer/request'
|
28
|
+
require 'consumer/helper'
|
data/script/console
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# File: script/console
|
3
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
4
|
+
|
5
|
+
libs = " -r irb/completion"
|
6
|
+
# Perhaps use a console_lib to store any extra methods I may want available in the cosole
|
7
|
+
# libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
|
8
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/consumer.rb'}"
|
9
|
+
puts "Loading consumer gem"
|
10
|
+
exec "#{irb} #{libs} --simple-prompt"
|
data/script/destroy
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/destroy'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Destroy.new.run(ARGV)
|
data/script/generate
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/generate'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Generate.new.run(ARGV)
|
data/script/txt2html
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
load File.dirname(__FILE__) + "/../Rakefile"
|
4
|
+
require 'rubyforge'
|
5
|
+
require 'redcloth'
|
6
|
+
require 'syntax/convertors/html'
|
7
|
+
require 'erb'
|
8
|
+
|
9
|
+
download = "http://rubyforge.org/projects/#{$hoe.rubyforge_name}"
|
10
|
+
version = $hoe.version
|
11
|
+
|
12
|
+
def rubyforge_project_id
|
13
|
+
RubyForge.new.configure.autoconfig["group_ids"][$hoe.rubyforge_name]
|
14
|
+
end
|
15
|
+
|
16
|
+
class Fixnum
|
17
|
+
def ordinal
|
18
|
+
# teens
|
19
|
+
return 'th' if (10..19).include?(self % 100)
|
20
|
+
# others
|
21
|
+
case self % 10
|
22
|
+
when 1: return 'st'
|
23
|
+
when 2: return 'nd'
|
24
|
+
when 3: return 'rd'
|
25
|
+
else return 'th'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Time
|
31
|
+
def pretty
|
32
|
+
return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def convert_syntax(syntax, source)
|
37
|
+
return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^<pre>|</pre>$!,'')
|
38
|
+
end
|
39
|
+
|
40
|
+
if ARGV.length >= 1
|
41
|
+
src, template = ARGV
|
42
|
+
template ||= File.join(File.dirname(__FILE__), '/../website/template.html.erb')
|
43
|
+
else
|
44
|
+
puts("Usage: #{File.split($0).last} source.txt [template.html.erb] > output.html")
|
45
|
+
exit!
|
46
|
+
end
|
47
|
+
|
48
|
+
template = ERB.new(File.open(template).read)
|
49
|
+
|
50
|
+
title = nil
|
51
|
+
body = nil
|
52
|
+
File.open(src) do |fsrc|
|
53
|
+
title_text = fsrc.readline
|
54
|
+
body_text_template = fsrc.read
|
55
|
+
body_text = ERB.new(body_text_template).result(binding)
|
56
|
+
syntax_items = []
|
57
|
+
body_text.gsub!(%r!<(pre|code)[^>]*?syntax=['"]([^'"]+)[^>]*>(.*?)</\1>!m){
|
58
|
+
ident = syntax_items.length
|
59
|
+
element, syntax, source = $1, $2, $3
|
60
|
+
syntax_items << "<#{element} class='syntax'>#{convert_syntax(syntax, source)}</#{element}>"
|
61
|
+
"syntax-temp-#{ident}"
|
62
|
+
}
|
63
|
+
title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip
|
64
|
+
body = RedCloth.new(body_text).to_html
|
65
|
+
body.gsub!(%r!(?:<pre><code>)?syntax-temp-(\d+)(?:</code></pre>)?!){ syntax_items[$1.to_i] }
|
66
|
+
end
|
67
|
+
stat = File.stat(src)
|
68
|
+
created = stat.ctime
|
69
|
+
modified = stat.mtime
|
70
|
+
|
71
|
+
$stdout << template.result(binding)
|
data/spec/helper_spec.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/spec_helper"
|
2
|
+
|
3
|
+
describe Consumer::Helper do
|
4
|
+
describe "tidy" do
|
5
|
+
it "formats a basic newline-less glob of xml into something pretty" do
|
6
|
+
@dirty = "<hello><woot>hoo yeah nelly</woot></hello>"
|
7
|
+
@clean = <<-EOS
|
8
|
+
<hello>
|
9
|
+
<woot>hoo yeah nelly</woot>
|
10
|
+
</hello>
|
11
|
+
EOS
|
12
|
+
end
|
13
|
+
|
14
|
+
it "reformats nil nodes into <element/>" do
|
15
|
+
@dirty = "<hello><nil></nil><notnil>text</notnil></hello>"
|
16
|
+
@clean = <<-EOS
|
17
|
+
<hello>
|
18
|
+
<nil/>
|
19
|
+
<notnil>text</notnil>
|
20
|
+
</hello>
|
21
|
+
EOS
|
22
|
+
end
|
23
|
+
|
24
|
+
it "reformats more complex globs" do
|
25
|
+
@dirty = "<?xml version=\"1.0\"?><RatingServiceSelectionResponse><Response><TransactionReference><CustomerContext>RatingandService</CustomerContext><XpciVersion>1.0001</XpciVersion></TransactionReference><ResponseStatusCode>1</ResponseStatusCode><ResponseStatusDescription>Success</ResponseStatusDescription></Response><RatedShipment><Service><Code>03</Code></Service></RatedShipment></RatingServiceSelectionResponse>"
|
26
|
+
@clean = <<-EOS
|
27
|
+
<?xml version="1.0"?>
|
28
|
+
<RatingServiceSelectionResponse>
|
29
|
+
<Response>
|
30
|
+
<TransactionReference>
|
31
|
+
<CustomerContext>RatingandService</CustomerContext>
|
32
|
+
<XpciVersion>1.0001</XpciVersion>
|
33
|
+
</TransactionReference>
|
34
|
+
<ResponseStatusCode>1</ResponseStatusCode>
|
35
|
+
<ResponseStatusDescription>Success</ResponseStatusDescription>
|
36
|
+
</Response>
|
37
|
+
<RatedShipment>
|
38
|
+
<Service>
|
39
|
+
<Code>03</Code>
|
40
|
+
</Service>
|
41
|
+
</RatedShipment>
|
42
|
+
</RatingServiceSelectionResponse>
|
43
|
+
EOS
|
44
|
+
end
|
45
|
+
|
46
|
+
it "reformats xml with newlines" do
|
47
|
+
@dirty = <<-EOS
|
48
|
+
<?xml version="1.0"?>
|
49
|
+
<Error><Number>-2147219490</Number><Source>Rate_Respond;SOLServerRatesTest.RateV2_Respond</Source><Description>Invalid value for origin ZIP Code.</Description><HelpFile></HelpFile><HelpContext>1000440</HelpContext></Error>
|
50
|
+
EOS
|
51
|
+
@clean = <<-EOS
|
52
|
+
<?xml version="1.0"?>
|
53
|
+
<Error>
|
54
|
+
<Number>-2147219490</Number>
|
55
|
+
<Source>Rate_Respond;SOLServerRatesTest.RateV2_Respond</Source>
|
56
|
+
<Description>Invalid value for origin ZIP Code.</Description>
|
57
|
+
<HelpFile/>
|
58
|
+
<HelpContext>1000440</HelpContext>
|
59
|
+
</Error>
|
60
|
+
|
61
|
+
EOS
|
62
|
+
end
|
63
|
+
|
64
|
+
after(:each) do
|
65
|
+
res = Consumer::Helper.tidy(@dirty)
|
66
|
+
puts res if res != @clean
|
67
|
+
res.should == @clean
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "compact_xml" do
|
72
|
+
it "removes empty xml nodes" do
|
73
|
+
@dirty = "<hello><woot></woot><moogle>blah</moogle></hello>"
|
74
|
+
@clean = "<hello><moogle>blah</moogle></hello>"
|
75
|
+
end
|
76
|
+
|
77
|
+
it "removes nodes with only empty nodes inside" do
|
78
|
+
@dirty = "<hello><woot></woot><moogle><still_empty></still_empty></moogle></hello>"
|
79
|
+
@clean = ""
|
80
|
+
end
|
81
|
+
|
82
|
+
it "remove empty nodes containing whitespace characters" do
|
83
|
+
@dirty = "<hello> \r \t\n</hello>"
|
84
|
+
@clean = ""
|
85
|
+
end
|
86
|
+
|
87
|
+
after(:each) do
|
88
|
+
Consumer::Helper.compact_xml(@dirty).should == @clean
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "hash_from_yaml" do
|
93
|
+
it "should load a yaml file and return a hash" do
|
94
|
+
@yaml = <<-EOS
|
95
|
+
hello: world
|
96
|
+
EOS
|
97
|
+
perform
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should return a subsection of the yaml if given a namespace" do
|
101
|
+
@yaml = <<-EOS
|
102
|
+
greetings:
|
103
|
+
hello: world
|
104
|
+
other:
|
105
|
+
not: relevant
|
106
|
+
EOS
|
107
|
+
@namespace = "greetings"
|
108
|
+
perform
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should return {} if no hash" do
|
112
|
+
@yaml = ""
|
113
|
+
@namespace = "greetings"
|
114
|
+
perform({})
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should have 'all' as a global namespace" do
|
118
|
+
@yaml = <<-EOS
|
119
|
+
all:
|
120
|
+
answer: 42
|
121
|
+
greetings:
|
122
|
+
hello: world
|
123
|
+
other:
|
124
|
+
not: relevant
|
125
|
+
EOS
|
126
|
+
@namespace = "greetings"
|
127
|
+
perform({"hello" => "world", "answer" => 42})
|
128
|
+
end
|
129
|
+
|
130
|
+
def perform(result = {"hello" => "world"})
|
131
|
+
File.should_receive(:read).with("config/file.yml").and_return(@yaml)
|
132
|
+
Consumer::Helper.hash_from_yaml("config", "file.yml", @namespace).should == result
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/spec_helper"
|
2
|
+
|
3
|
+
class MockObject
|
4
|
+
include Consumer::Mapping
|
5
|
+
attr_accessor :price, :integers
|
6
|
+
end
|
7
|
+
|
8
|
+
describe Consumer::Mapping do
|
9
|
+
before(:each) do
|
10
|
+
# since maps are defined on the class and we want to test variations
|
11
|
+
# on the same map, let's clear it before each spec
|
12
|
+
MockObject.instance_variable_set("@maps", [])
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "find_nodes_and_map" do
|
16
|
+
it "finds the correct map for the xml" do
|
17
|
+
xml = File.read("spec/xml/rate_response.xml")
|
18
|
+
map = MockObject.map(:first, "//CrazyCarrierRateResponse/ShipService",{})
|
19
|
+
not_the_map = MockObject.map(:first, "//NotTheMapping/ShipService", {})
|
20
|
+
MockObject.send(:find_nodes_and_map, xml)[1].should == map
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it "creates an attribute hash given a node and a map registry" do
|
25
|
+
xml = File.read("spec/xml/rate_response.xml")
|
26
|
+
node = LibXML::XML::Parser.string(xml).parse.find("//ShipService").first
|
27
|
+
registry = {:price => "Cost"}
|
28
|
+
attrs = MockObject.send(:attrs_from_node_and_registry, node, registry)
|
29
|
+
attrs.should == {:price => "5.00"}
|
30
|
+
end
|
31
|
+
|
32
|
+
it "creates a new instance from an attribute hash" do
|
33
|
+
attrs = {:price => "5.00"}
|
34
|
+
object = MockObject.from_hash(attrs)
|
35
|
+
object.price.should == "5.00"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "creates instances from xml via the appropriate map" do
|
39
|
+
xml = <<-EOF
|
40
|
+
<CarrierResponse>
|
41
|
+
<ShipService>
|
42
|
+
<Cost>5.00</Cost>
|
43
|
+
</ShipService>
|
44
|
+
</CarrierResponse>
|
45
|
+
EOF
|
46
|
+
MockObject.map(:first, "//CarrierResponse/ShipService",{:price => "Cost"})
|
47
|
+
object = MockObject.from_xml_via_map(xml)
|
48
|
+
object.price.should == "5.00"
|
49
|
+
end
|
50
|
+
|
51
|
+
it "raises an error if you try to define the same map root twice" do
|
52
|
+
MockObject.map(:first, "//CarrierResponse",{:something => "woot"})
|
53
|
+
|
54
|
+
lambda {
|
55
|
+
MockObject.map(:first, "//CarrierResponse",{:another => "AAA"})
|
56
|
+
}.should raise_error
|
57
|
+
end
|
58
|
+
|
59
|
+
it "calls map blocks" do
|
60
|
+
MockObject.map(:first, "//empty", {}) {|instance| instance.price = "5"}
|
61
|
+
object = MockObject.from_xml_via_map("<empty></empty>")
|
62
|
+
object.price.should == "5"
|
63
|
+
end
|
64
|
+
|
65
|
+
it "calls map blocks with node also" do
|
66
|
+
MockObject.map(:first, "//empty", {}) {|instance, node| instance.price = node.find_first("//empty").content}
|
67
|
+
object = MockObject.from_xml_via_map("<empty>5</empty>")
|
68
|
+
object.price.should == "5"
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "association_from_xml" do
|
72
|
+
it "creates an association from xml" do
|
73
|
+
object = MockObject.new
|
74
|
+
Integer.should_receive(:from_xml).with("some xml").and_return([1,2])
|
75
|
+
object.should_receive("integers=").with([1,2])
|
76
|
+
object.association_from_xml("some xml", :integers)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "registry" do
|
81
|
+
it "accepts attributs as values" do
|
82
|
+
xml = <<-EOF
|
83
|
+
<CarrierResponse>
|
84
|
+
<ShipService cost="5.00">
|
85
|
+
Irrelevant value in content
|
86
|
+
</ShipService>
|
87
|
+
</CarrierResponse>
|
88
|
+
EOF
|
89
|
+
MockObject.map(:first, "//CarrierResponse/ShipService",{:price => "attribute::cost"})
|
90
|
+
object = MockObject.from_xml_via_map(xml)
|
91
|
+
object.price.should == "5.00"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|