praxis 0.14.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -1
  3. data/Gemfile +5 -0
  4. data/bin/praxis +7 -4
  5. data/lib/api_browser/package.json +1 -1
  6. data/lib/praxis/action_definition.rb +10 -5
  7. data/lib/praxis/application.rb +30 -0
  8. data/lib/praxis/bootloader_stages/subgroup_loader.rb +2 -2
  9. data/lib/praxis/bootloader_stages/warn_unloaded_files.rb +7 -2
  10. data/lib/praxis/file_group.rb +1 -1
  11. data/lib/praxis/handlers/json.rb +32 -0
  12. data/lib/praxis/handlers/www_form.rb +20 -0
  13. data/lib/praxis/handlers/xml.rb +78 -0
  14. data/lib/praxis/links.rb +4 -1
  15. data/lib/praxis/media_type.rb +55 -0
  16. data/lib/praxis/media_type_identifier.rb +198 -0
  17. data/lib/praxis/multipart/part.rb +16 -0
  18. data/lib/praxis/request.rb +34 -25
  19. data/lib/praxis/response.rb +22 -3
  20. data/lib/praxis/response_definition.rb +11 -36
  21. data/lib/praxis/restful_doc_generator.rb +1 -1
  22. data/lib/praxis/simple_media_type.rb +6 -1
  23. data/lib/praxis/types/media_type_common.rb +8 -3
  24. data/lib/praxis/types/multipart.rb +6 -6
  25. data/lib/praxis/version.rb +1 -1
  26. data/lib/praxis.rb +7 -1
  27. data/praxis.gemspec +1 -1
  28. data/spec/functional_spec.rb +3 -1
  29. data/spec/praxis/application_spec.rb +58 -1
  30. data/spec/praxis/handlers/json_spec.rb +37 -0
  31. data/spec/praxis/handlers/xml_spec.rb +155 -0
  32. data/spec/praxis/media_type_identifier_spec.rb +209 -0
  33. data/spec/praxis/media_type_spec.rb +50 -3
  34. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +2 -2
  35. data/spec/praxis/request_spec.rb +39 -1
  36. data/spec/praxis/response_definition_spec.rb +12 -9
  37. data/spec/praxis/response_spec.rb +37 -6
  38. data/spec/praxis/types/collection_spec.rb +2 -2
  39. data/spec/praxis/types/multipart_spec.rb +17 -0
  40. data/spec/spec_app/app/controllers/instances.rb +1 -1
  41. data/spec/support/spec_media_types.rb +19 -0
  42. metadata +11 -6
  43. data/lib/praxis/content_type_parser.rb +0 -62
  44. data/spec/praxis/content_type_parser_spec.rb +0 -91
@@ -12,8 +12,8 @@ module Praxis
12
12
  API_VERSION_HEADER_NAME = "HTTP_X_API_VERSION".freeze
13
13
  API_VERSION_PARAM_NAME = 'api_version'.freeze
14
14
  API_NO_VERSION_NAME = 'n/a'.freeze
15
- VERSION_USING_DEFAULTS = [:header,:params].freeze
16
-
15
+ VERSION_USING_DEFAULTS = [:header, :params].freeze
16
+
17
17
  def initialize(env)
18
18
  @env = env
19
19
  @query = Rack::Utils.parse_nested_query(env[QUERY_STRING_NAME])
@@ -21,18 +21,25 @@ module Praxis
21
21
  @path_version_matcher = path_version_matcher
22
22
  end
23
23
 
24
+ # Determine the content type of this request as indicated by the Content-Type header.
25
+ #
26
+ # @return [nil,MediaTypeIdentifier] nil if the header is missing, else a media-type identifier
24
27
  def content_type
25
- @env[CONTENT_TYPE_NAME]
28
+ header = @env[CONTENT_TYPE_NAME]
29
+ @content_type ||= (header && MediaTypeIdentifier.load(header)).freeze
26
30
  end
27
31
 
28
- # The media type (type/subtype) portion of the CONTENT_TYPE header
29
- # without any media type parameters. e.g., when CONTENT_TYPE is
30
- # "text/plain;charset=utf-8", the media-type is "text/plain".
32
+ # The media type (type/subtype+suffix) portion of the Content-Type
33
+ # header without any media type parameters. e.g., when Content-Type
34
+ # is "text/plain;charset=utf-8", the media-type is "text/plain".
31
35
  #
32
36
  # For more information on the use of media types in HTTP, see:
33
37
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
38
+ #
39
+ # @return [String]
40
+ # @see MediaTypeIdentifier#without_parameters
34
41
  def media_type
35
- content_type && content_type.split(/\s*[;,]\s*/, 2).first.downcase
42
+ content_type.without_parameters.to_s
36
43
  end
37
44
 
38
45
  def path
@@ -71,27 +78,27 @@ module Praxis
71
78
  self.raw_params
72
79
  self.raw_payload
73
80
  end
74
-
81
+
75
82
  def self.path_version_prefix
76
83
  PATH_VERSION_PREFIX
77
84
  end
78
85
 
79
86
  PATH_VERSION_MATCHER = %r{^#{self.path_version_prefix}(?<version>[^\/]+)\/}.freeze
80
-
87
+
81
88
  def path_version_matcher
82
89
  PATH_VERSION_MATCHER
83
90
  end
84
-
85
- def version(using: VERSION_USING_DEFAULTS )
91
+
92
+ def version(using: VERSION_USING_DEFAULTS)
86
93
  result = nil
87
94
  Array(using).find do |mode|
88
95
  case mode
89
- when :header ;
96
+ when :header;
90
97
  result = env[API_VERSION_HEADER_NAME]
91
- when :params ;
98
+ when :params;
92
99
  result = @query[API_VERSION_PARAM_NAME]
93
- when :path ;
94
- m = self.path.match(@path_version_matcher)
100
+ when :path;
101
+ m = self.path.match(@path_version_matcher)
95
102
  result = m[:version] unless m.nil?
96
103
  else
97
104
  raise "Unknown method for retrieving the API version: #{mode}"
@@ -102,7 +109,7 @@ module Praxis
102
109
 
103
110
  def load_headers(context)
104
111
  return unless action.headers
105
- defined_headers = action.precomputed_header_keys_for_rack.each_with_object(Hash.new) do |(upper,original), hash|
112
+ defined_headers = action.precomputed_header_keys_for_rack.each_with_object(Hash.new) do |(upper, original), hash|
106
113
  hash[original] = self.env[upper] if self.env.has_key? upper
107
114
  end
108
115
  self.headers = action.headers.load(defined_headers, context)
@@ -115,16 +122,18 @@ module Praxis
115
122
 
116
123
  def load_payload(context)
117
124
  return unless action.payload
118
- raw = case content_type
119
- when %r|^application/x-www-form-urlencoded|i
120
- Rack::Utils.parse_nested_query(self.raw_payload)
121
- when nil
122
- {}
123
- else
124
- self.raw_payload
125
- end
125
+ return if content_type.nil?
126
+
127
+ handler = Praxis::Application.instance.handlers[content_type.handler_name]
128
+
129
+ if handler
130
+ raw = handler.parse(self.raw_payload)
131
+ else
132
+ # TODO is this a good default?
133
+ raw = self.raw_payload
134
+ end
126
135
 
127
- self.payload = action.payload.load(raw, context, content_type: content_type)
136
+ self.payload = action.payload.load(raw, context, content_type: content_type.to_s)
128
137
  end
129
138
 
130
139
  def validate_headers(context)
@@ -1,7 +1,5 @@
1
1
  module Praxis
2
2
  class Response
3
-
4
-
5
3
  attr_reader :name
6
4
  attr_reader :parts
7
5
 
@@ -29,6 +27,22 @@ module Praxis
29
27
  @parts = Hash.new
30
28
  end
31
29
 
30
+ # Determine the content type of this response.
31
+ #
32
+ # @return [MediaTypeIdentifier]
33
+ def content_type
34
+ MediaTypeIdentifier.load(headers['Content-Type']).freeze
35
+ end
36
+
37
+ # Set the content type for this response.
38
+ # @todo DRY this out (also used in Multipart::Part)
39
+ #
40
+ # @return [String]
41
+ # @param [String,MediaTypeIdentifier] identifier
42
+ def content_type=(identifier)
43
+ headers['Content-Type'] = MediaTypeIdentifier.load(identifier).to_s
44
+ end
45
+
32
46
  def handle
33
47
  end
34
48
 
@@ -54,7 +68,12 @@ module Praxis
54
68
  def encode!
55
69
  case @body
56
70
  when Hash, Array
57
- @body = JSON.pretty_generate(@body)
71
+ # response payload is structured data; transform it into an entity using the handler
72
+ # implied by the response's media type. If no handler is registered for this
73
+ # name, assume JSON as a default handler.
74
+ handlers = Praxis::Application.instance.handlers
75
+ handler = (content_type && handlers[content_type.handler_name]) || handlers['json']
76
+ @body = handler.generate(@body)
58
77
  end
59
78
  end
60
79
 
@@ -210,19 +210,15 @@ module Praxis
210
210
  case value
211
211
  when String
212
212
  if response.headers[name] != value
213
- unless valid
214
- raise Exceptions::Validation.new(
215
- "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name]}."
216
- )
217
- end
213
+ raise Exceptions::Validation.new(
214
+ "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name]}."
215
+ )
218
216
  end
219
217
  when Regexp
220
218
  if response.headers[name] !~ value
221
- unless valid
222
- raise Exceptions::Validation.new(
223
- "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name].inspect}."
224
- )
225
- end
219
+ raise Exceptions::Validation.new(
220
+ "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name].inspect}."
221
+ )
226
222
  end
227
223
  end
228
224
  end
@@ -238,36 +234,15 @@ module Praxis
238
234
  def validate_content_type!(response)
239
235
  return unless media_type
240
236
 
241
- response_content_type = {}
242
- if response.headers['Content-Type']
243
- response_content_type = Praxis::ContentTypeParser.parse(response.headers['Content-Type'])
244
- end
245
-
246
- expected_content_type = Praxis::ContentTypeParser.parse(media_type.identifier)
237
+ response_content_type = response.content_type
238
+ expected_content_type = Praxis::MediaTypeIdentifier.load(media_type.identifier)
247
239
 
248
- unless response_content_type[:type] == expected_content_type[:type]
240
+ unless expected_content_type.match(response_content_type)
249
241
  raise Exceptions::Validation.new(
250
- "Bad Content-Type header. #{response.headers['Content-Type']}" +
251
- " does not match type #{expected_content_type[:type]} as described in response: #{self.name}"
242
+ "Bad Content-Type header. #{response_content_type}" +
243
+ " is incompatible with #{expected_content_type} as described in response: #{self.name}"
252
244
  )
253
245
  end
254
-
255
- if (expected_params = expected_content_type[:params])
256
- expected_params.each do |param_name,expected_param|
257
- response_param = response_content_type[:params].fetch(param_name) do
258
- raise Exceptions::Validation.new(
259
- "Bad Content-Type header: #{response.headers['Content-Type']}" +
260
- " does not contain expected param '#{param_name}' as described in response: #{self.name}"
261
- )
262
- end
263
- unless response_param == expected_param
264
- raise Exceptions::Validation.new(
265
- "Bad Content-Type header: #{response.headers['Content-Type']}" +
266
- " param: #{param_name} does not match expected value #{expected_param} as described in response: #{self.name}"
267
- )
268
- end
269
- end
270
- end
271
246
  end
272
247
 
273
248
  def validate_parts!(response)
@@ -226,7 +226,7 @@ module Praxis
226
226
  type_output[:example] = if example_data.respond_to? :render
227
227
  example_data.render(:master)
228
228
  else
229
- example_data.dump
229
+ type.dump(example_data)
230
230
  end
231
231
  end
232
232
 
@@ -1,5 +1,10 @@
1
1
  module Praxis
2
-
2
+
3
+ # Stripped-down representation of an Internet Media Type where the structure and content of the
4
+ # type are unknown, or are defined externally to the Praxis application.
5
+ #
6
+ # @see Praxis::MediaType
7
+ # @see Praxis::Types::MediaTypeCommon
3
8
  SimpleMediaType = Struct.new(:identifier) do
4
9
  def name
5
10
  self.class.name
@@ -1,6 +1,7 @@
1
1
  module Praxis
2
2
  module Types
3
3
 
4
+ # Traits that are shared by MediaType and SimpleMediaType.
4
5
  module MediaTypeCommon
5
6
  extend ::ActiveSupport::Concern
6
7
 
@@ -18,10 +19,14 @@ module Praxis
18
19
  @description
19
20
  end
20
21
 
22
+ # Get or set the identifier of this media type.
23
+ #
24
+ # @deprecated this method is not deprecated, but its return type will change to MediaTypeIdentifier in Praxis 1.0
25
+ #
26
+ # @return [String] the string-representation of this type's identifier
21
27
  def identifier(identifier=nil)
22
- return @identifier unless identifier
23
- # TODO: parse the string and extract things like collection , and format type?...
24
- @identifier = identifier
28
+ return @identifier.to_s unless identifier
29
+ (@identifier = MediaTypeIdentifier.load(identifier)).to_s
25
30
  end
26
31
  end
27
32
 
@@ -4,11 +4,14 @@ module Praxis
4
4
 
5
5
  @key_type = Attributor::String
6
6
 
7
- def self.load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, content_type:)
8
- #return super if content_type.nil?
7
+ def self.load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, content_type:nil)
8
+ return value if value.kind_of?(self) || value.nil?
9
9
 
10
+ unless (value.kind_of?(::String) && ! content_type.nil?)
11
+ raise Attributor::CoercionError, context: context, from: value.class, to: self.name, value: value
12
+ end
13
+
10
14
  headers = {'Content-Type' => content_type}
11
-
12
15
  parser = MultipartParser.new(headers, value)
13
16
  preamble, parts = parser.parse
14
17
 
@@ -60,9 +63,6 @@ module Praxis
60
63
  super
61
64
  end
62
65
 
63
- #def []=(k, v)
64
- #end
65
-
66
66
  end
67
67
 
68
68
 
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '0.14.0'
2
+ VERSION = '0.15.0'
3
3
  end
data/lib/praxis.rb CHANGED
@@ -50,7 +50,6 @@ module Praxis
50
50
  autoload :Router, 'praxis/router'
51
51
  autoload :SimpleMediaType, 'praxis/simple_media_type'
52
52
  autoload :Stage, 'praxis/stage'
53
- autoload :ContentTypeParser, 'praxis/content_type_parser'
54
53
 
55
54
  autoload :Stats, 'praxis/stats'
56
55
  autoload :Notifications, 'praxis/notifications'
@@ -65,6 +64,7 @@ module Praxis
65
64
  autoload :Links, 'praxis/links'
66
65
  autoload :MediaType, 'praxis/media_type'
67
66
  autoload :MediaTypeCollection, 'praxis/media_type_collection'
67
+ autoload :MediaTypeIdentifier, 'praxis/media_type_identifier'
68
68
  autoload :Multipart, 'praxis/types/multipart'
69
69
  autoload :Collection, 'praxis/collection'
70
70
 
@@ -91,6 +91,12 @@ module Praxis
91
91
  require 'praxis/responses/internal_server_error'
92
92
  require 'praxis/responses/validation_error'
93
93
 
94
+ module Handlers
95
+ autoload :JSON, 'praxis/handlers/json'
96
+ autoload :WWWForm, 'praxis/handlers/www_form'
97
+ autoload :XML, 'praxis/handlers/xml'
98
+ end
99
+
94
100
  module BootloaderStages
95
101
  autoload :FileLoader, 'praxis/bootloader_stages/file_loader'
96
102
  autoload :Environment, 'praxis/bootloader_stages/environment'
data/praxis.gemspec CHANGED
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_dependency 'mime', '~> 0'
27
27
  spec.add_dependency 'praxis-mapper', '~> 3.3'
28
28
  spec.add_dependency 'praxis-blueprints', '~> 1.3'
29
- spec.add_dependency 'attributor', '~> 2.6.0'
29
+ spec.add_dependency 'attributor', '~> 2.6'
30
30
  spec.add_dependency 'thor', '~> 0.18'
31
31
  spec.add_dependency 'terminal-table', '~> 1.4'
32
32
  spec.add_dependency 'harness', '~> 2'
@@ -58,7 +58,8 @@ describe 'Functional specs' do
58
58
  end
59
59
 
60
60
  it 'works' do
61
- get '/clouds/1/instances/2?junk=foo&api_version=1.0', nil, 'global_session' => session
61
+ the_body = StringIO.new("{}") # This is a funny, GET request expecting a body
62
+ get '/clouds/1/instances/2?junk=foo&api_version=1.0', nil,'rack.input' => the_body,'CONTENT_TYPE' => "application/json", 'global_session' => session
62
63
  expect(last_response.status).to eq(200)
63
64
  expected = {
64
65
  "cloud_id" => 1,
@@ -101,6 +102,7 @@ describe 'Functional specs' do
101
102
  let(:body) { form.body.to_s }
102
103
 
103
104
  it 'works' do
105
+
104
106
  post '/clouds/1/instances?api_version=1.0', body, 'CONTENT_TYPE' => content_type, 'global_session' => session
105
107
 
106
108
  _reponse_preamble, response = Praxis::MultipartParser.parse(last_response.headers, last_response.body)
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Praxis::Application do
4
- describe 'configuration' do
4
+ context 'configuration' do
5
5
  subject(:app) do
6
6
  app = Class.new(Praxis::Application).instance
7
7
 
@@ -43,4 +43,61 @@ describe Praxis::Application do
43
43
  end
44
44
  end
45
45
  end
46
+
47
+ context 'media type handlers' do
48
+ subject { Class.new(Praxis::Application).instance }
49
+
50
+ before do
51
+ bootloader = double('bootloader')
52
+ allow(bootloader).to receive(:setup!).and_return(true)
53
+
54
+ app = double('built Rack app')
55
+
56
+ builder = double('Rack builder')
57
+ allow(builder).to receive(:run)
58
+ allow(builder).to receive(:to_app).and_return(app)
59
+
60
+ subject.instance_variable_set(:@bootloader, bootloader)
61
+ subject.instance_variable_set(:@builder, builder)
62
+ end
63
+
64
+ describe '#handler' do
65
+ let(:new_handler_name) { 'awesomesauce' }
66
+ let(:new_handler_instance) { double('awesomesauce instance', generate: '', parse: {}) }
67
+ let(:new_handler_class) { double('awesomesauce', new: new_handler_instance) }
68
+ let(:bad_handler_instance) { double('bad handler instance', wokka: true, meep: false) }
69
+ let(:bad_handler_class) { double('bad handler', new: bad_handler_instance) }
70
+
71
+ context 'given a Class' do
72
+ it 'instantiates and registers an instance' do
73
+ expect(new_handler_class).to receive(:new)
74
+ subject.handler new_handler_name, new_handler_class
75
+ end
76
+ end
77
+
78
+ context 'given a non-Class' do
79
+ it 'raises' do
80
+ expect {
81
+ subject.handler('awesomesauce', 'hi') # no instances allowed
82
+ }.to raise_error(NoMethodError)
83
+
84
+ expect {
85
+ subject.handler('awesomesauce', ::Kernel) # no modules allowed
86
+ }.to raise_error(NoMethodError)
87
+ end
88
+ end
89
+
90
+ it 'overrides default handlers' do
91
+ subject.handler 'json', new_handler_class
92
+ subject.setup
93
+ expect(subject.handlers['json']).to eq(new_handler_instance)
94
+ end
95
+
96
+ it 'ensures that handlers will work' do
97
+ expect {
98
+ subject.handler new_handler_name, bad_handler_class
99
+ }.to raise_error(ArgumentError)
100
+ end
101
+ end
102
+ end
46
103
  end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe Praxis::Handlers::JSON do
4
+ let(:dictionary) { {"foo" => 1} }
5
+ let(:dictionary_json) { '{"foo":1}' }
6
+
7
+ let(:array) { [1,2,3] }
8
+ let(:array_json) { '[1,2,3]' }
9
+
10
+ describe '#parse' do
11
+ it 'handles dictionaries' do
12
+ expect(subject.parse(dictionary_json)).to eq(dictionary)
13
+ end
14
+
15
+ it 'handles arrays' do
16
+ expect(subject.parse(array_json)).to eq(array)
17
+
18
+ end
19
+ end
20
+
21
+ # slightly cheesy: use #parse to test #generate by round-tripping everything
22
+ describe '#generate' do
23
+ it 'pretty-prints' do
24
+ result = subject.generate({"foo" => 1})
25
+ expect(result).to include("\n")
26
+ expect(result).to match(/^ /m)
27
+ end
28
+
29
+ it 'handles dictionaries' do
30
+ expect(subject.parse(subject.generate(dictionary))).to eq(dictionary)
31
+ end
32
+
33
+ it 'handles arrays' do
34
+ expect(subject.parse(subject.generate(array))).to eq(array)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,155 @@
1
+ require 'spec_helper'
2
+
3
+ describe Praxis::Handlers::XML do
4
+
5
+ describe '#parse' do
6
+ after do
7
+ expect( subject.parse(xml) ).to eq(parsed)
8
+ end
9
+
10
+ # XML_TYPE_NAMES = {
11
+ # "Symbol" => "symbol",
12
+ # "Fixnum" => "integer",
13
+ # "Bignum" => "integer",
14
+ # "BigDecimal" => "decimal",
15
+ # "Float" => "float",
16
+ # "TrueClass" => "boolean",
17
+ # "FalseClass" => "boolean",
18
+ # "Date" => "date",
19
+ # "DateTime" => "dateTime",
20
+ # "Time" => "dateTime"
21
+ # }
22
+
23
+ context "Parsing symbols" do
24
+ let(:xml){ '<objects type="array"><object type="symbol">a_symbol</object></objects>' }
25
+ let(:parsed){ [:a_symbol] }
26
+ it 'works' do end
27
+ end
28
+
29
+ context "Parsing integers" do
30
+ let(:xml){ '<objects type="array"><object type="integer">1234</object></objects>' }
31
+ let(:parsed){ [1234] }
32
+ it 'works' do end
33
+ end
34
+
35
+ context "Parsing decimals" do
36
+ let(:xml){ '<objects type="array"><object type="decimal">0.1</object></objects>' }
37
+ let(:parsed){ [0.1] }
38
+ it 'works' do end
39
+ end
40
+
41
+ context "Parsing floats" do
42
+ let(:xml){ '<objects type="array"><object type="float">0.1</object></objects>' }
43
+ let(:parsed){ [0.1] }
44
+ it 'works' do end
45
+ end
46
+
47
+ context "Parsing booleans" do
48
+ context "that are true" do
49
+ let(:xml){ '<objects type="array"><object type="boolean">true</object></objects>' }
50
+ let(:parsed){ [true] }
51
+ it 'works' do end
52
+ end
53
+ context "that are false" do
54
+ let(:xml){ '<objects type="array"><object type="boolean">false</object></objects>' }
55
+ let(:parsed){ [false] }
56
+ it 'works' do end
57
+ end
58
+ end
59
+
60
+ context "Parsing dates" do
61
+ let(:xml){ '<objects type="array"><object type="date">2001-01-01</object></objects>' }
62
+ let(:parsed){ [Date.parse("2001-01-01")] }
63
+ it 'works' do end
64
+ end
65
+
66
+ context "Parsing dateTimes" do
67
+ let(:xml){ '<objects type="array"><object type="dateTime">2015-03-13T19:34:40-07:00</object></objects>' }
68
+ let(:parsed){ [DateTime.parse("2015-03-13T19:34:40-07:00")] }
69
+ it 'works' do end
70
+ end
71
+
72
+ context "Parsing hashes" do
73
+ context "that are empty" do
74
+ let(:xml){ '<hash></hash>' }
75
+ let(:parsed){ {} }
76
+ it 'works' do end
77
+ end
78
+ context "with a couple of elements" do
79
+ let(:xml){ '<hash><one type="integer">1</one><two type="integer">2</two></hash>' }
80
+ let(:parsed){ {"one"=>1, "two"=>2} }
81
+ it 'works' do end
82
+ end
83
+ context "with a nested hash" do
84
+ let(:xml){ '<hash><one type="integer">1</one><sub_hash><first>hello</first></sub_hash></hash>' }
85
+ let(:parsed){ {"one"=>1, "sub_hash"=>{"first"=>"hello"} } }
86
+ it 'works' do end
87
+ end
88
+ context "with a nested array" do
89
+ let(:xml){ '<hash><one type="integer">1</one><two type="array"><object>just text</object></two></hash>' }
90
+ let(:parsed){ {"one"=>1, "two" => ["just text"] } }
91
+ it 'works' do end
92
+ end
93
+
94
+ end
95
+
96
+ context "Parsing an Array" do
97
+ context 'with a couple of simple elements in it' do
98
+ let(:xml){ '<objects type="array"><object>just text</object><object type="integer">1</object></objects>' }
99
+ let(:parsed){ ["just text", 1] }
100
+ it 'works' do end
101
+ end
102
+ context "with a nested hash" do
103
+ let(:xml){ '<objects type="array"><object>just text</object><object><one type="integer">1</one></object></objects>' }
104
+ let(:parsed){ ["just text", { "one" => 1}] }
105
+ it 'works' do end
106
+ end
107
+ end
108
+
109
+ context "Parsing XML strings created with .to_xml" do
110
+ let(:xml){ parsed.to_xml }
111
+ context 'array with a couple of simple elements in it' do
112
+ let(:parsed){ ["just text", 1] }
113
+ it 'works' do end
114
+ end
115
+ context 'a hash with a couple of simple elements in it' do
116
+ let(:parsed){ { "one"=>"just text", "two"=> 1 } }
117
+ it 'works' do end
118
+ end
119
+ context 'a array with elements of all types' do
120
+ let(:parsed){ ["just text",:a,1,BigDecimal.new(100),0.1,true,Date.new] }
121
+ it 'works' do end
122
+ end
123
+ context 'a hash with a complex substructure' do
124
+ let(:parsed) do
125
+ Hash(
126
+ "text" => "just text",
127
+ "symbol" => :a,
128
+ "num" => 1,
129
+ "BD" => BigDecimal.new(100),
130
+ "float" => 0.1,
131
+ "truthyness" => true,
132
+ "day" => Date.new )
133
+ end
134
+ it 'works' do end
135
+ end
136
+
137
+ context "transformed characters when using .to_xml" do
138
+ context 'underscores become dashes' do
139
+ let(:xml){ {"one_thing"=>1, "two_things"=>2}.to_xml }
140
+ let(:parsed){ {"one-thing"=>1, "two-things"=>2} }
141
+ it 'works' do end
142
+ end
143
+ context 'spaces become dashes' do
144
+ let(:xml){ {"one thing"=>1, "two things"=>2}.to_xml }
145
+ let(:parsed){ {"one-thing"=>1, "two-things"=>2} }
146
+ it 'works' do end
147
+ end
148
+ end
149
+ end
150
+
151
+ describe '#generate' do
152
+ it 'has tests'
153
+ end
154
+ end
155
+ end