woahdae-consumer 0.8.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/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
|