praxis 0.14.0 → 0.15.0

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