woahdae-consumer 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/History.txt +4 -0
  2. data/LICENSE +20 -0
  3. data/Manifest.txt +73 -0
  4. data/PostInstall.txt +8 -0
  5. data/README.rdoc +53 -0
  6. data/Rakefile +30 -0
  7. data/app_generators/consumer/USAGE +14 -0
  8. data/app_generators/consumer/consumer_generator.rb +89 -0
  9. data/app_generators/consumer/templates/LICENSE +20 -0
  10. data/app_generators/consumer/templates/README.rdoc +3 -0
  11. data/app_generators/consumer/templates/Rakefile +57 -0
  12. data/app_generators/consumer/templates/TODO +4 -0
  13. data/app_generators/consumer/templates/config/config.yml +2 -0
  14. data/app_generators/consumer/templates/config/config.yml.sample +1 -0
  15. data/app_generators/consumer/templates/lib/base.rb +6 -0
  16. data/app_generators/consumer/templates/rails/init.rb +1 -0
  17. data/app_generators/consumer/templates/script/destroy +14 -0
  18. data/app_generators/consumer/templates/script/generate +14 -0
  19. data/app_generators/consumer/templates/spec/spec_helper.rb +11 -0
  20. data/bin/consumer +17 -0
  21. data/config/website.yml.sample +2 -0
  22. data/consumer.gemspec +48 -0
  23. data/consumer_generators/request/USAGE +25 -0
  24. data/consumer_generators/request/request_generator.rb +94 -0
  25. data/consumer_generators/request/templates/lib/request.rb +55 -0
  26. data/consumer_generators/request/templates/lib/response.rb +12 -0
  27. data/consumer_generators/request/templates/spec/request_spec.rb +27 -0
  28. data/consumer_generators/request/templates/spec/response_spec.rb +10 -0
  29. data/consumer_generators/request/templates/spec/xml/response.xml +0 -0
  30. data/examples/active_record/README.txt +1 -0
  31. data/examples/active_record/ar_spec.rb +33 -0
  32. data/examples/active_record/database.sqlite +0 -0
  33. data/examples/active_record/environment.rb +15 -0
  34. data/examples/active_record/migration.rb +21 -0
  35. data/examples/active_record/models/book.rb +13 -0
  36. data/examples/active_record/models/contributor.rb +12 -0
  37. data/examples/active_record/xml/book.xml +6 -0
  38. data/examples/active_record/xml/book_with_contributors.xml +11 -0
  39. data/examples/active_record/xml/contributor.xml +3 -0
  40. data/examples/active_record/xml/contributor_with_books.xml +19 -0
  41. data/examples/shipping/environment.rb +3 -0
  42. data/examples/shipping/rate.rb +15 -0
  43. data/examples/shipping/shipping.yml.sample +8 -0
  44. data/examples/shipping/shipping_spec.rb +27 -0
  45. data/examples/shipping/ups_rate_request.rb +182 -0
  46. data/examples/shipping/ups_rate_response.xml +340 -0
  47. data/lib/consumer/helper.rb +111 -0
  48. data/lib/consumer/mapping.rb +184 -0
  49. data/lib/consumer/request.rb +280 -0
  50. data/lib/consumer.rb +28 -0
  51. data/script/console +10 -0
  52. data/script/destroy +14 -0
  53. data/script/generate +14 -0
  54. data/script/txt2html +71 -0
  55. data/spec/helper_spec.rb +136 -0
  56. data/spec/mapping_spec.rb +94 -0
  57. data/spec/request_spec.rb +75 -0
  58. data/spec/spec.opts +1 -0
  59. data/spec/spec_helper.rb +12 -0
  60. data/spec/xml/rate_response.xml +14 -0
  61. data/spec/xml/rate_response_error.xml +35 -0
  62. data/tasks/rspec.rake +21 -0
  63. data/test/test_consumer_generator.rb +68 -0
  64. data/test/test_generator_helper.rb +29 -0
  65. data/website/index.html +11 -0
  66. data/website/index.txt +81 -0
  67. data/website/javascripts/rounded_corners_lite.inc.js +285 -0
  68. data/website/stylesheets/screen.css +159 -0
  69. data/website/template.html.erb +50 -0
  70. 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)
@@ -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