representable 3.0.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +17 -0
  3. data/CHANGES.md +25 -0
  4. data/Gemfile +4 -12
  5. data/LICENSE +1 -1
  6. data/README.md +6 -6
  7. data/Rakefile +1 -6
  8. data/TODO +1 -3
  9. data/TODO-4.0.md +72 -0
  10. data/lib/representable.rb +19 -25
  11. data/lib/representable/binding.rb +32 -12
  12. data/lib/representable/cached.rb +1 -1
  13. data/lib/representable/coercion.rb +8 -6
  14. data/lib/representable/config.rb +13 -3
  15. data/lib/representable/debug.rb +23 -15
  16. data/lib/representable/declarative.rb +12 -7
  17. data/lib/representable/decorator.rb +1 -1
  18. data/lib/representable/definition.rb +7 -3
  19. data/lib/representable/deserializer.rb +5 -4
  20. data/lib/representable/for_collection.rb +1 -1
  21. data/lib/representable/hash.rb +9 -2
  22. data/lib/representable/hash/allow_symbols.rb +9 -11
  23. data/lib/representable/hash/binding.rb +1 -0
  24. data/lib/representable/hash/collection.rb +4 -2
  25. data/lib/representable/hash_methods.rb +3 -2
  26. data/lib/representable/insert.rb +1 -1
  27. data/lib/representable/json.rb +8 -7
  28. data/lib/representable/json/collection.rb +3 -0
  29. data/lib/representable/object.rb +1 -1
  30. data/lib/representable/object/binding.rb +5 -1
  31. data/lib/representable/option.rb +19 -0
  32. data/lib/representable/pipeline.rb +3 -2
  33. data/lib/representable/pipeline_factories.rb +4 -2
  34. data/lib/representable/populator.rb +1 -1
  35. data/lib/representable/represent.rb +1 -0
  36. data/lib/representable/serializer.rb +3 -2
  37. data/lib/representable/version.rb +1 -1
  38. data/lib/representable/virtus_coercion.rb +38 -0
  39. data/lib/representable/xml.rb +12 -10
  40. data/lib/representable/xml/binding.rb +19 -13
  41. data/lib/representable/xml/namespace.rb +122 -0
  42. data/lib/representable/yaml.rb +6 -2
  43. data/lib/representable/yaml/binding.rb +1 -0
  44. data/representable.gemspec +8 -9
  45. data/test/as_test.rb +7 -7
  46. data/test/binding_test.rb +14 -14
  47. data/test/cached_test.rb +59 -49
  48. data/test/class_test.rb +9 -9
  49. data/test/coercion_test.rb +33 -22
  50. data/test/config/inherit_test.rb +14 -14
  51. data/test/config_test.rb +20 -20
  52. data/test/decorator_scope_test.rb +4 -4
  53. data/test/decorator_test.rb +33 -20
  54. data/test/default_test.rb +8 -8
  55. data/test/defaults_options_test.rb +3 -3
  56. data/test/definition_test.rb +38 -40
  57. data/test/{example.rb → examples/example.rb} +0 -1
  58. data/test/examples/object.rb +1 -5
  59. data/test/exec_context_test.rb +8 -8
  60. data/test/features_test.rb +6 -6
  61. data/test/filter_test.rb +8 -8
  62. data/test/for_collection_test.rb +10 -10
  63. data/test/generic_test.rb +13 -13
  64. data/test/getter_setter_test.rb +5 -5
  65. data/test/hash_bindings_test.rb +1 -1
  66. data/test/hash_test.rb +45 -23
  67. data/test/heritage_test.rb +16 -13
  68. data/test/if_test.rb +9 -9
  69. data/test/include_exclude_test.rb +14 -14
  70. data/test/inherit_test.rb +18 -18
  71. data/test/inline_test.rb +24 -24
  72. data/test/instance_test.rb +31 -31
  73. data/test/is_representable_test.rb +10 -10
  74. data/test/json_test.rb +29 -7
  75. data/test/lonely_test.rb +31 -31
  76. data/test/nested_test.rb +13 -13
  77. data/test/object_test.rb +9 -9
  78. data/test/option_test.rb +36 -0
  79. data/test/parse_pipeline_test.rb +3 -5
  80. data/test/pipeline_test.rb +50 -50
  81. data/test/populator_test.rb +18 -18
  82. data/test/prepare_test.rb +4 -4
  83. data/test/private_options_test.rb +2 -2
  84. data/test/reader_writer_test.rb +2 -2
  85. data/test/render_nil_test.rb +2 -2
  86. data/test/represent_test.rb +14 -14
  87. data/test/representable_test.rb +34 -36
  88. data/test/schema_test.rb +8 -11
  89. data/test/serialize_deserialize_test.rb +2 -2
  90. data/test/skip_test.rb +14 -14
  91. data/test/stringify_hash_test.rb +3 -3
  92. data/test/test_helper.rb +26 -14
  93. data/test/uncategorized_test.rb +10 -10
  94. data/test/user_options_test.rb +4 -4
  95. data/test/virtus_coercion_test.rb +52 -0
  96. data/test/wrap_test.rb +19 -19
  97. data/test/xml_bindings_test.rb +0 -4
  98. data/test/xml_namespace_test.rb +186 -0
  99. data/test/xml_test.rb +103 -43
  100. data/test/yaml_test.rb +51 -26
  101. metadata +101 -39
  102. data/.travis.yml +0 -7
  103. data/lib/representable/TODO.getting_serious +0 -11
  104. data/lib/representable/autoload.rb +0 -10
  105. data/test/mongoid_test.rb +0 -31
@@ -4,11 +4,12 @@ module Representable
4
4
  class Pipeline < Array
5
5
  Stop = Class.new
6
6
 
7
- # options is mutuable.
7
+ # options is mutable.
8
8
  def call(input, options)
9
9
  inject(input) do |memo, block|
10
10
  res = evaluate(block, memo, options)
11
- return(Stop)if Stop == res
11
+ return(Stop) if Stop == res
12
+
12
13
  res
13
14
  end
14
15
  end
@@ -2,7 +2,8 @@
2
2
  module Representable
3
3
  module Binding::Factories
4
4
  def pipeline_for(name, input, options)
5
- return yield unless proc = @definition[name]
5
+ return yield unless (proc = @definition[name])
6
+
6
7
  # proc.(self, options)
7
8
  instance_exec(input, options, &proc)
8
9
  end
@@ -11,6 +12,7 @@ module Representable
11
12
  def collect_for(item_functions)
12
13
  return [Collect[*item_functions]] if array?
13
14
  return [Collect::Hash[*item_functions]] if self[:hash]
15
+
14
16
  item_functions
15
17
  end
16
18
 
@@ -92,4 +94,4 @@ module Representable
92
94
  funcs << (self[:setter] ? Setter : SetValue)
93
95
  end
94
96
  end
95
- end
97
+ end
@@ -21,7 +21,7 @@ module Representable
21
21
  def self.apply!(options)
22
22
  return unless populator = options[:populator]
23
23
 
24
- options[:parse_pipeline] = ->(input, options) do
24
+ options[:parse_pipeline] = ->(_input, _opts) do
25
25
  pipeline = Pipeline[*parse_functions] # TODO: AssignFragment
26
26
  pipeline = Pipeline::Insert.(pipeline, SetValue, delete: true) # remove the setter function.
27
27
  pipeline = Pipeline::Insert.(pipeline, populator, replace: CreateObject::Populator) # let the actual populator do the job.
@@ -1,6 +1,7 @@
1
1
  module Representable::Represent
2
2
  def represent(represented, array_class=Array)
3
3
  return for_collection.prepare(represented) if represented.is_a?(array_class)
4
+
4
5
  prepare(represented)
5
6
  end
6
7
  end
@@ -3,7 +3,7 @@ module Representable
3
3
  options[:binding].evaluate_option(:getter, input, options)
4
4
  end
5
5
 
6
- GetValue = ->(input, options) { options[:binding].send(:exec_context, options).send(options[:binding].getter) }
6
+ GetValue = ->(_input, options) { options[:binding].send(:exec_context, options).public_send(options[:binding].getter) }
7
7
 
8
8
  Writer = ->(input, options) do
9
9
  options[:binding].evaluate_option(:writer, input, options)
@@ -37,6 +37,7 @@ module Representable
37
37
 
38
38
  Serialize = ->(input, options) do
39
39
  return if input.nil? # DISCUSS: how can we prevent that?
40
+
40
41
  binding, options = options[:binding], options[:options] # FIXME: rename to :local_options.
41
42
 
42
43
  options_for_nested = OptionsForNested.(options, binding)
@@ -51,4 +52,4 @@ module Representable
51
52
  # Warning: don't rely on AssignAs/AssignName, i am not sure if i leave that as functions.
52
53
  AssignAs = ->(input, options) { options[:as] = As.(input, options); input }
53
54
  AssignName = ->(input, options) { options[:as] = options[:binding].name; input }
54
- end
55
+ end
@@ -1,3 +1,3 @@
1
1
  module Representable
2
- VERSION = "3.0.0"
2
+ VERSION = "3.1.0"
3
3
  end
@@ -0,0 +1,38 @@
1
+ gem 'virtus'
2
+ require "virtus"
3
+
4
+ module Representable
5
+ module VirtusCoercion
6
+ class Coercer
7
+ def initialize(type)
8
+ @type = type
9
+ end
10
+
11
+ # This gets called when the :render_filter or :parse_filter option is evaluated.
12
+ # Usually the Coercer instance is an element in a Pipeline to allow >1 filters per property.
13
+ def call(input, options)
14
+ Virtus::Attribute.build(@type).coerce(input)
15
+ end
16
+ end
17
+
18
+
19
+ def self.included(base)
20
+ base.class_eval do
21
+ extend ClassMethods
22
+ register_feature VirtusCoercion
23
+ end
24
+ end
25
+
26
+
27
+ module ClassMethods
28
+ def property(name, options={}, &block)
29
+ super.tap do |definition|
30
+ return definition unless type = options[:type]
31
+
32
+ definition.merge!(render_filter: coercer = Coercer.new(type))
33
+ definition.merge!(parse_filter: coercer)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,15 +1,14 @@
1
- require 'representable'
2
- require 'representable/xml/binding'
3
- require 'representable/xml/collection'
1
+ gem 'nokogiri', '> 1.10.8'
2
+ require 'nokogiri'
4
3
 
5
- begin
6
- require 'nokogiri'
7
- rescue LoadError => _
8
- abort "Missing dependency 'nokogiri' for Representable::XML. See dependencies section in README.md for details."
9
- end
4
+ require 'representable'
10
5
 
11
6
  module Representable
12
7
  module XML
8
+ autoload :Binding, 'representable/xml/binding'
9
+ autoload :Collection, 'representable/xml/collection'
10
+ autoload :Namespace, 'representable/xml/namespace'
11
+
13
12
  def self.included(base)
14
13
  base.class_eval do
15
14
  include Representable
@@ -46,16 +45,19 @@ module Representable
46
45
 
47
46
  # Returns a Nokogiri::XML object representing this object.
48
47
  def to_node(options={})
49
- options[:doc] ||= Nokogiri::XML::Document.new
48
+ options[:doc] = Nokogiri::XML::Document.new # DISCUSS: why do we need a fresh Document here?
50
49
  root_tag = options[:wrap] || representation_wrap(options)
51
50
 
52
- create_representation_with(Nokogiri::XML::Node.new(root_tag.to_s, options[:doc]), options, Binding)
51
+ create_representation_with(Node(options[:doc], root_tag.to_s), options, Binding)
53
52
  end
54
53
 
55
54
  def to_xml(*args)
56
55
  to_node(*args).to_s
57
56
  end
58
57
 
58
+ alias_method :render, :to_xml
59
+ alias_method :parse, :from_xml
60
+
59
61
  private
60
62
  def remove_namespaces?
61
63
  # TODO: make local Config easily extendable so you get Config#remove_ns? etc.
@@ -1,8 +1,14 @@
1
1
  require 'representable/binding'
2
- require 'representable/hash/binding.rb'
3
2
 
4
3
  module Representable
5
4
  module XML
5
+ module_function def Node(document, name, attributes={})
6
+ node = Nokogiri::XML::Node.new(name.to_s, document) # Java::OrgW3cDom::DOMException: NAMESPACE_ERR: An attempt is made to create or change an object in a way which is incorrect with regard to namespaces.
7
+
8
+ attributes.each { |k, v| node[k] = v } # TODO: benchmark.
9
+ node
10
+ end
11
+
6
12
  class Binding < Representable::Binding
7
13
  def self.build_for(definition)
8
14
  return Collection.new(definition) if definition.array?
@@ -10,6 +16,7 @@ module Representable
10
16
  return AttributeHash.new(definition) if definition.hash? and definition[:use_attributes]
11
17
  return Attribute.new(definition) if definition[:attribute]
12
18
  return Content.new(definition) if definition[:content]
19
+
13
20
  new(definition)
14
21
  end
15
22
 
@@ -17,7 +24,7 @@ module Representable
17
24
  wrap_node = parent
18
25
 
19
26
  if wrap = self[:wrap]
20
- parent << wrap_node = node_for(parent, wrap)
27
+ parent << wrap_node = XML::Node(parent, wrap)
21
28
  end
22
29
 
23
30
  wrap_node << serialize_for(fragments, parent, as)
@@ -32,12 +39,15 @@ module Representable
32
39
 
33
40
  # Creates wrapped node for the property.
34
41
  def serialize_for(value, parent, as)
35
- node = node_for(parent, as)
36
- serialize_node(node, value)
42
+ node = XML::Node(parent, as) # node doesn't have attr="" attributes!!!
43
+ serialize_node(node, value, as)
37
44
  end
38
45
 
39
- def serialize_node(node, value)
40
- return value if typed?
46
+ def serialize_node(node, value, as)
47
+ if typed?
48
+ value.name = as if as != self[:name]
49
+ return value
50
+ end
41
51
 
42
52
  node.content = value
43
53
  node
@@ -60,11 +70,7 @@ module Representable
60
70
  def find_nodes(doc, as)
61
71
  selector = as
62
72
  selector = "#{self[:wrap]}/#{as}" if self[:wrap]
63
- nodes = doc.xpath(selector)
64
- end
65
-
66
- def node_for(parent, name)
67
- Nokogiri::XML::Node.new(name.to_s, parent.document)
73
+ doc.xpath(selector) # nodes
68
74
  end
69
75
 
70
76
  def content_for(node) # TODO: move this into a ScalarDecorator.
@@ -100,8 +106,8 @@ module Representable
100
106
  class Hash < Collection
101
107
  def serialize_for(value, parent, as)
102
108
  set_for(parent, value.collect do |k, v|
103
- node = node_for(parent, k)
104
- serialize_node(node, v)
109
+ node = XML::Node(parent, k)
110
+ serialize_node(node, v, as)
105
111
  end)
106
112
  end
107
113
 
@@ -0,0 +1,122 @@
1
+ module Representable::XML
2
+ # Experimental!
3
+ # Best explanation so far: http://books.xmlschemata.org/relaxng/relax-CHP-11-SECT-1.html
4
+ #
5
+ # Note: This module doesn't work with JRuby because Nokogiri uses a completely
6
+ # different implementation in Java which has other requirements that we couldn't fulfil.
7
+ # Please wait for Representable 4 where we replace Nokogiri with Oga.
8
+ module Namespace
9
+ def self.included(includer)
10
+ includer.extend(DSL)
11
+ end
12
+
13
+ module DSL
14
+ def namespace(namespace)
15
+ representable_attrs.options[:local_namespace] = namespace
16
+ representable_attrs.options[:namespace_mappings] ||= {}
17
+ representable_attrs.options[:namespace_mappings][namespace] = nil # this might get overwritten via #namespace_def later.
18
+ end
19
+
20
+ def namespace_def(mapping)
21
+ namespace_defs.merge!(mapping.invert)
22
+ end
23
+
24
+ # :private:
25
+ def namespace_defs
26
+ representable_attrs.options[:namespace_mappings] ||= {}
27
+ end
28
+
29
+ def property(name, options={})
30
+ uri = representable_attrs.options[:local_namespace] # per default, a property belongs to the local namespace.
31
+ options[:namespace] ||= uri # don't override if already set.
32
+
33
+ # a nested representer is automatically assigned "its" local namespace. It's like saying
34
+ # property :author, namespace: "http://ns/author" do ... end
35
+
36
+ super.tap do |dfn|
37
+ if dfn.typed? # FIXME: ouch, this should be doable with property's API to hook into the creation process.
38
+ dfn.merge!( namespace: dfn.representer_module.representable_attrs.options[:local_namespace] )
39
+
40
+ update_namespace_defs!(namespace_defs)
41
+ end
42
+ end
43
+ end
44
+
45
+ # :private:
46
+ # super ugly hack
47
+ # recursively injects the namespace_defs into all representers of this tree. will be done better in 4.0.
48
+ def update_namespace_defs!(namespace_defs)
49
+ representable_attrs.each do |dfn|
50
+ dfn.merge!(namespace_defs: namespace_defs) # this only helps with scalars
51
+
52
+ if dfn.typed?
53
+ representer = Class.new(dfn.representer_module) # don't pollute classes.
54
+ representer.update_namespace_defs!(namespace_defs)
55
+ dfn.merge!(extend: representer)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ module AsWithNamespace
62
+ def write(doc, fragment, as)
63
+ super(doc, fragment, prefixed(self, as))
64
+ end
65
+
66
+ # FIXME: this is shit, the NestedOptions is executed too late here!
67
+ def read(node, as)
68
+ super(node, prefixed(self, as))
69
+ end
70
+
71
+ private
72
+ def prefixed(dfn, as)
73
+ uri = dfn[:namespace] # this is generic behavior and per property
74
+ prefix = dfn[:namespace_defs][uri]
75
+ as = Namespace::Namespaced(prefix, as)
76
+ end
77
+ end
78
+
79
+ # FIXME: some "bug" in Representable's XML doesn't consider the container tag, so we could theoretically pick the
80
+ # wrong namespaced tag here :O
81
+ def from_node(node, options={})
82
+ super
83
+ end
84
+
85
+ def to_node(options={})
86
+ local_uri = representable_attrs.options[:local_namespace] # every decorator MUST have a local namespace.
87
+ prefix = self.class.namespace_defs[local_uri]
88
+
89
+ root_tag = [prefix, representation_wrap(options)].compact.join(":")
90
+
91
+ options = { wrap: root_tag }.merge(options)
92
+
93
+ # TODO: there should be an easier way to pass a set of options to all nested #to_node decorators.
94
+ representable_attrs.keys.each do |property|
95
+ options[property.to_sym] = { show_definition: false, namespaces: options[:namespaces] }
96
+ end
97
+
98
+ super(options).tap do |node|
99
+ add_namespace_definitions!(node, self.class.namespace_defs) unless options[:show_definition] == false
100
+ end
101
+ end
102
+
103
+ # "Physically" add `xmlns` attributes to `node`.
104
+ def add_namespace_definitions!(node, namespaces)
105
+ namespaces.each do |uri, prefix|
106
+ prefix = prefix.nil? ? nil : prefix.to_s
107
+ node.add_namespace_definition(prefix, uri)
108
+ end
109
+ end
110
+
111
+ def self.Namespaced(prefix, name)
112
+ [ prefix, name ].compact.join(":")
113
+ end
114
+
115
+ # FIXME: this is a PoC, we need a better API to inject code.
116
+ def representable_map(options, format)
117
+ super.tap do |map|
118
+ map.each { |bin| bin.extend(AsWithNamespace) unless bin.is_a?(Binding::Attribute) }
119
+ end
120
+ end
121
+ end
122
+ end
@@ -1,8 +1,9 @@
1
- require 'representable/hash'
2
- require 'representable/yaml/binding'
1
+ require 'psych'
2
+ require 'representable'
3
3
 
4
4
  module Representable
5
5
  module YAML
6
+ autoload :Binding, 'representable/yaml/binding'
6
7
  include Hash
7
8
 
8
9
  def self.included(base)
@@ -38,5 +39,8 @@ module Representable
38
39
  doc.children << to_ast(*args)
39
40
  stream.to_yaml
40
41
  end
42
+
43
+ alias_method :render, :to_yaml
44
+ alias_method :parse, :from_yaml
41
45
  end
42
46
  end
@@ -5,6 +5,7 @@ module Representable
5
5
  class Binding < Representable::Hash::Binding
6
6
  def self.build_for(definition)
7
7
  return Collection.new(definition) if definition.array?
8
+
8
9
  new(definition)
9
10
  end
10
11
 
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.platform = Gem::Platform::RUBY
10
10
  spec.authors = ["Nick Sutterer"]
11
11
  spec.email = ["apotonick@gmail.com"]
12
- spec.homepage = "https://github.com/apotonick/representable/"
12
+ spec.homepage = "https://github.com/trailblazer/representable/"
13
13
  spec.summary = %q{Renders and parses JSON/XML/YAML documents from and to Ruby objects. Includes plain properties, collections, nesting, coercion and more.}
14
14
  spec.description = spec.summary
15
15
 
@@ -19,17 +19,16 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
  spec.license = "MIT"
21
21
 
22
- spec.required_ruby_version = '>= 1.9.3'
22
+ spec.required_ruby_version = '>= 2.4.0'
23
23
 
24
- spec.add_dependency "uber", "~> 0.0.15"
25
- spec.add_dependency "declarative", "~> 0.0.5"
24
+ spec.add_dependency "uber", "< 0.2.0"
25
+ spec.add_dependency "declarative", "< 0.1.0"
26
+ spec.add_dependency "trailblazer-option", "~> 0.1.0"
26
27
 
27
28
  spec.add_development_dependency "rake"
28
- spec.add_development_dependency "test_xml", "0.1.6"
29
+ spec.add_development_dependency "test_xml", ">= 0.1.6"
29
30
  spec.add_development_dependency "minitest"
30
- spec.add_development_dependency "mongoid"
31
31
  spec.add_development_dependency "virtus"
32
- spec.add_development_dependency "json", '>= 1.7.7'
33
-
34
- spec.add_development_dependency "ruby-prof"
32
+ spec.add_development_dependency "dry-types"
33
+ spec.add_development_dependency "ruby-prof" if RUBY_ENGINE == "ruby" # mri
35
34
  end
data/test/as_test.rb CHANGED
@@ -7,8 +7,8 @@ class AsTest < MiniTest::Spec
7
7
  # :yaml => [Representable::YAML, "---\nsong:\n name: Alive\n", "---\nsong:\n name: You've Taken Everything\n"],
8
8
  ) do |format, mod, input, output|
9
9
 
10
- let (:song) { representer.prepare(Song.new("Revolution")) }
11
- let (:format) { format }
10
+ let(:song) { representer.prepare(Song.new("Revolution")) }
11
+ let(:format) { format }
12
12
 
13
13
 
14
14
  describe "as: with :symbol" do
@@ -17,7 +17,7 @@ class AsTest < MiniTest::Spec
17
17
  end
18
18
 
19
19
  it { render(song).must_equal_document output }
20
- it { parse(song, input).name.must_equal "Wie Es Geht" }
20
+ it { _(parse(song, input).name).must_equal "Wie Es Geht" }
21
21
  end
22
22
 
23
23
 
@@ -27,7 +27,7 @@ class AsTest < MiniTest::Spec
27
27
  end
28
28
 
29
29
  it { render(song).must_equal_document({"Song" => "Revolution"}) }
30
- it { parse(song, {"Song" => "Wie Es Geht"}).name.must_equal "Wie Es Geht" }
30
+ it { _(parse(song, {"Song" => "Wie Es Geht"}).name).must_equal "Wie Es Geht" }
31
31
  end
32
32
 
33
33
 
@@ -37,7 +37,7 @@ class AsTest < MiniTest::Spec
37
37
  end
38
38
 
39
39
  it { render(song, user_options:{volume: 1}).must_equal_document({"{:volume=>1}" => "Revolution"}) }
40
- it { parse(song, {"{:volume=>1}" => "Wie Es Geht"}, user_options: {volume: 1}).name.must_equal "Wie Es Geht" }
40
+ it { _(parse(song, {"{:volume=>1}" => "Wie Es Geht"}, user_options: {volume: 1}).name).must_equal "Wie Es Geht" }
41
41
  end
42
42
  end
43
43
  end
@@ -60,6 +60,6 @@ class AsXmlTest < MiniTest::Spec
60
60
 
61
61
  it do
62
62
  skip
63
- representer.new(Album.new(Band.new("Offspring"))).to_xml.must_equal ""
63
+ _(representer.new(Album.new(Band.new("Offspring"))).to_xml).must_equal ""
64
64
  end
65
- end
65
+ end