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.
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