diff --git a/lib/savon/attribute.rb b/lib/savon/attribute.rb
new file mode 100644
index 00000000..c4b49ce5
--- /dev/null
+++ b/lib/savon/attribute.rb
@@ -0,0 +1,11 @@
+class Savon
+ class Attribute
+
+ attr_accessor :name, :base_type, :use
+
+ def optional?
+ use == 'optional'
+ end
+
+ end
+end
diff --git a/lib/savon/element.rb b/lib/savon/element.rb
index db03edce..c97bce28 100644
--- a/lib/savon/element.rb
+++ b/lib/savon/element.rb
@@ -2,13 +2,15 @@ class Savon
class Element
def initialize
- @children = []
- @recursive = false
- @singular = true
+ @children = []
+ @attributes = {}
+ @recursive = false
+ @singular = true
end
attr_accessor :parent, :name, :form, :namespace, :singular, :recursive,
- :base_type, :children, :complex_type_id, :recursive_type
+ :base_type, :children, :complex_type_id, :recursive_type,
+ :attributes
alias_method :singular?, :singular
@@ -26,18 +28,24 @@ def complex_type?
def to_a(memo = [], stack = [])
new_stack = stack + [name]
- attributes = { namespace: namespace, form: form, singular: singular? }
+ data = { namespace: namespace, form: form, singular: singular? }
+
+ unless attributes.empty?
+ data[:attributes] = attributes.each_with_object({}) do |attribute, memo|
+ memo[attribute.name] = { optional: attribute.optional? }
+ end
+ end
if recursive?
- attributes[:recursive_type] = recursive_type
- memo << [new_stack, attributes]
+ data[:recursive_type] = recursive_type
+ memo << [new_stack, data]
elsif simple_type?
- attributes[:type] = base_type
- memo << [new_stack, attributes]
+ data[:type] = base_type
+ memo << [new_stack, data]
elsif complex_type?
- memo << [new_stack, attributes]
+ memo << [new_stack, data]
children.each do |child|
child.to_a(memo, new_stack)
diff --git a/lib/savon/example_message.rb b/lib/savon/example_message.rb
index 621b4b1f..08c59185 100644
--- a/lib/savon/example_message.rb
+++ b/lib/savon/example_message.rb
@@ -25,6 +25,11 @@ def build(elements)
when element.complex_type?
value = build(element.children)
+
+ unless element.attributes.empty?
+ value.merge! collect_attributes(element)
+ end
+
value = [value] unless element.singular?
memo[name] = value
@@ -34,5 +39,11 @@ def build(elements)
memo
end
+ def collect_attributes(element)
+ element.attributes.each_with_object({}) { |attribute, memo|
+ memo["_#{attribute.name}".to_sym] = attribute.base_type
+ }
+ end
+
end
end
diff --git a/lib/savon/message.rb b/lib/savon/message.rb
index 2c1b0681..a0b72529 100644
--- a/lib/savon/message.rb
+++ b/lib/savon/message.rb
@@ -3,6 +3,8 @@
class Savon
class Message
+ ATTRIBUTE_PREFIX = '_'
+
def initialize(envelope, parts)
@logger = Logging.logger[self]
@@ -73,7 +75,9 @@ def build_complex_type_element(element, xml, tag, value)
raise ArgumentError, "Expected a Hash for the #{tag.last.inspect} complex type"
end
- xml.tag! *tag do |xml|
+ attributes, value = extract_attributes(value)
+
+ xml.tag! *tag, attributes do |xml|
build_elements(element.children, value, xml)
end
else
@@ -101,5 +105,18 @@ def extract_value(name, symbol_name, message)
end
end
+ def extract_attributes(hash)
+ attributes = {}
+
+ hash.dup.each do |k, v|
+ next unless k.to_s[0, 1] == ATTRIBUTE_PREFIX
+
+ attributes[k.to_s[1..-1]] = v
+ hash.delete(k)
+ end
+
+ [attributes, hash]
+ end
+
end
end
diff --git a/lib/savon/wsdl/message_builder.rb b/lib/savon/wsdl/message_builder.rb
index f7371d39..1052fafc 100644
--- a/lib/savon/wsdl/message_builder.rb
+++ b/lib/savon/wsdl/message_builder.rb
@@ -1,10 +1,12 @@
require 'savon/element'
+require 'savon/attribute'
class Savon
class WSDL
class MessageBuilder
def initialize(wsdl)
+ @logger = Logging.logger[self]
@wsdl = wsdl
end
@@ -53,16 +55,54 @@ def build_element(part)
def handle_type(element, type)
case type
+
when XS::ComplexType
element.complex_type_id = type.id
element.children = child_elements(element, type)
+ element.attributes = element_attributes(type)
+
when XS::SimpleType
element.base_type = type.base
+
when String
element.base_type = type
+
end
end
+ def handle_simple_type(attribute, type)
+ case type
+ when XS::SimpleType then attribute.base_type = type.base
+ when String then attribute.base_type = type
+ end
+ end
+
+ def element_attributes(type)
+ type.attributes.map { |attribute|
+ attr = Attribute.new
+
+ if attribute.ref
+ local, namespace = expand_qname(attribute.ref, attribute.namespaces)
+ schema = find_schema(namespace)
+
+ if schema
+ attribute = schema.attributes[local]
+ else
+ @logger.debug("Unable to find schema for attribute@ref #{attribute.ref.inspect}")
+ next
+ end
+ end
+
+ type = find_type_for_attribute(attribute)
+ handle_simple_type(attr, type)
+
+ attr.name = attribute.name
+ attr.use = attribute.use
+
+ attr
+ }.compact
+ end
+
def child_elements(parent, type)
type.elements.map { |child_element|
el = Element.new
@@ -120,6 +160,8 @@ def find_type_for_element(element)
end
end
+ alias_method :find_type_for_attribute, :find_type_for_element
+
def find_type(qname, namespaces)
local, namespace = expand_qname(qname, namespaces)
@@ -154,6 +196,11 @@ def find_element(qname, namespaces)
@wsdl.schemas.element(namespace, local)
end
+ def find_attribute(qname, namespaces)
+ local, namespace = expand_qname(qname, namespaces)
+ @wsdl.schemas.attribute(namespace, local)
+ end
+
def find_schema(namespace)
@wsdl.schemas.find_by_namespace(namespace)
end
diff --git a/lib/savon/xs/schema.rb b/lib/savon/xs/schema.rb
index b757793b..f58bd77e 100644
--- a/lib/savon/xs/schema.rb
+++ b/lib/savon/xs/schema.rb
@@ -11,16 +11,18 @@ def initialize(schema, wsdl)
@target_namespace = @schema['targetNamespace']
@element_form_default = @schema['elementFormDefault'] || 'unqualified'
- @elements = {}
- @complex_types = {}
- @simple_types = {}
- @imports = {}
+ @attributes = {}
+ @attribute_groups = {}
+ @elements = {}
+ @complex_types = {}
+ @simple_types = {}
+ @imports = {}
parse
end
attr_accessor :target_namespace, :element_form_default, :imports,
- :elements, :complex_types, :simple_types
+ :attributes, :attribute_groups, :elements, :complex_types, :simple_types
private
@@ -32,14 +34,24 @@ def parse
@schema.element_children.each do |node|
case node.name
- when 'element' then @elements[node['name']] = XS::Element.new(node, @wsdl, schema)
- when 'complexType' then @complex_types[node['name']] = XS::ComplexType.new(node, @wsdl, schema)
- when 'simpleType' then @simple_types[node['name']] = XS::SimpleType.new(node, @wsdl, schema)
- when 'import' then @imports[node['namespace']] = node['schemaLocation']
+ when 'attribute' then store_element(@attributes, node, schema)
+ when 'attributeGroup' then store_element(@attribute_groups, node, schema)
+ when 'element' then store_element(@elements, node, schema)
+ when 'complexType' then store_element(@complex_types, node, schema)
+ when 'simpleType' then store_element(@simple_types, node, schema)
+ when 'import' then store_import(node)
end
end
end
+ def store_element(collection, node, schema)
+ collection[node['name']] = XS.build(node, @wsdl, schema)
+ end
+
+ def store_import(node)
+ @imports[node['namespace']] = node['schemaLocation']
+ end
+
end
end
end
diff --git a/lib/savon/xs/schema_collection.rb b/lib/savon/xs/schema_collection.rb
index 36449483..933de0c0 100644
--- a/lib/savon/xs/schema_collection.rb
+++ b/lib/savon/xs/schema_collection.rb
@@ -19,6 +19,14 @@ def each(&block)
@schemas.each(&block)
end
+ def attribute(namespace, name)
+ find_by_namespace(namespace).attributes[name]
+ end
+
+ def attribute_group(namespace, name)
+ find_by_namespace(namespace).attribute_groups[name]
+ end
+
def element(namespace, name)
find_by_namespace(namespace).elements[name]
end
diff --git a/lib/savon/xs/types.rb b/lib/savon/xs/types.rb
index d0572aeb..7e8cfa3e 100644
--- a/lib/savon/xs/types.rb
+++ b/lib/savon/xs/types.rb
@@ -35,6 +35,18 @@ def collect_child_elements(memo = [])
memo
end
+ def collect_attributes(memo = [])
+ children.each do |child|
+ if child.kind_of? Attribute
+ memo << child
+ else
+ memo = child.collect_attributes(memo)
+ end
+ end
+
+ memo
+ end
+
def inspect
attributes = @node.attributes.
inject({}) { |memo, (k, attr)| memo[k.to_s] = attr.value; memo }.
@@ -100,6 +112,7 @@ def inline_type
class ComplexType < PrimaryType
alias_method :elements, :collect_child_elements
+ alias_method :attributes, :collect_attributes
def id
[namespace, name].join(':')
@@ -116,6 +129,8 @@ def collect_child_elements(memo = [])
if complex_type = @wsdl.schemas.complex_type(namespace, local)
memo += complex_type.elements
+
+ # TODO: can we find a testcase for this?
else #if simple_type = @wsdl.schemas.simple_type(namespace, local)
raise 'simple type extension?!'
#memo << simple_type
@@ -135,8 +150,60 @@ class Sequence < BaseType; end
class Choice < BaseType; end
class Enumeration < BaseType; end
+ class Attribute < BaseType
+
+ def initialize(node, wsdl, schema = {})
+ super
+
+ @name = node['name']
+ @type = node['type']
+ @ref = node['ref']
+
+ @use = node['use'] || 'optional'
+ @default = node['default']
+ @fixed = node['fixed']
+
+ @namespaces = node.namespaces
+ end
+
+ attr_reader :name, :type, :ref, :namespaces,
+ :use, :default, :fixed
+
+ def inline_type
+ children.first
+ end
+
+ # stop searching for child elements
+ def collect_child_elements(memo = [])
+ memo
+ end
+
+ end
+
+ class AttributeGroup < BaseType
+
+ alias_method :attributes, :collect_attributes
+
+ def collect_attributes(memo = [])
+ if @node['ref']
+ local, nsid = @node['ref'].split(':').reverse
+ namespace = @node.namespaces["xmlns:#{nsid}"]
+
+ attribute_group = @wsdl.schemas.attribute_group(namespace, local)
+ memo += attribute_group.attributes
+ else
+ super
+ end
+ end
+ end
+
class SimpleContent < BaseType
+ # stop searching for attributes
+ def collect_attributes(memo = [])
+ memo
+ end
+
# stop searching for child elements
def collect_child_elements(memo = [])
memo
@@ -146,6 +213,11 @@ def collect_child_elements(memo = [])
class Annotation < BaseType
+ # stop searching for attributes
+ def collect_attributes(memo = [])
+ memo
+ end
+
# stop searching for child elements
def collect_child_elements(memo = [])
memo
@@ -154,6 +226,8 @@ def collect_child_elements(memo = [])
end
TYPE_MAPPING = {
+ 'attribute' => Attribute,
+ 'attributeGroup' => AttributeGroup,
'element' => Element,
'complexType' => ComplexType,
'simpleType' => SimpleType,
diff --git a/spec/fixtures/wsdl/equifax.wsdl b/spec/fixtures/wsdl/equifax.wsdl
new file mode 100644
index 00000000..ccc2f623
--- /dev/null
+++ b/spec/fixtures/wsdl/equifax.wsdl
@@ -0,0 +1,794 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spec/integration/equifax_spec.rb b/spec/integration/equifax_spec.rb
new file mode 100644
index 00000000..3d737666
--- /dev/null
+++ b/spec/integration/equifax_spec.rb
@@ -0,0 +1,212 @@
+require 'spec_helper'
+
+describe 'Integration with Equifax' do
+
+ subject(:client) { Savon.new fixture('wsdl/equifax') }
+
+ let(:service_name) { :canadav2 }
+ let(:port_name) { :canadaHttpPortV2 }
+
+ it 'returns a map of services and ports' do
+ expect(client.services).to eq(
+ 'canadav2' => {
+ ports: {
+ 'canadaHttpPortV2' => {
+ type: 'http://schemas.xmlsoap.org/wsdl/soap/',
+ location: 'https://pilot.eidverifier.com/uru/soap/cert/canadav2'
+ }
+ }
+ }
+ )
+ end
+
+ it 'knows operations with attributes and attribute groups' do
+ operation = client.operation(service_name, port_name, 'startTransaction')
+
+ expect(operation.soap_action).to eq('')
+ expect(operation.endpoint).to eq('https://pilot.eidverifier.com/uru/soap/cert/canadav2')
+
+ ns1 = 'http://eid.equifax.com/soap/schema/canada/v2'
+
+ expect(operation.body_parts).to eq([
+ [['InitialRequest'], { namespace: ns1, form: 'qualified', singular: true }],
+ [['InitialRequest', 'Identity'], { namespace: ns1, form: 'qualified', singular: true }],
+ [['InitialRequest', 'Identity', 'Name'], { namespace: ns1, form: 'qualified', singular: true }],
+ [['InitialRequest', 'Identity', 'Name', 'FirstName'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'Name', 'MiddleName'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'Name', 'MiddleInitial'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'Name', 'LastName'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'Name', 'Suffix'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+
+ [['InitialRequest', 'Identity', 'Address'], { namespace: ns1, form: 'qualified', singular: true,
+ attributes: {
+ 'timeAtAddress' => { optional: true },
+ 'addressType' => { optional: false }
+ }
+ }],
+
+ [['InitialRequest', 'Identity', 'Address', 'FreeFormAddress'], { namespace: ns1, form: 'qualified', singular: true }],
+ [['InitialRequest', 'Identity', 'Address', 'FreeFormAddress', 'AddressLine'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'Address', 'HybridAddress'], { namespace: ns1, form: 'qualified', singular: true }],
+ [['InitialRequest', 'Identity', 'Address', 'HybridAddress', 'AddressLine'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'Address', 'HybridAddress', 'City'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'Address', 'HybridAddress', 'Province'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'Address', 'HybridAddress', 'PostalCode'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'SIN'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'DateOfBirth'], { namespace: ns1, form: 'qualified', singular: true }],
+ [['InitialRequest', 'Identity', 'DateOfBirth', 'Day'], { namespace: ns1, form: 'qualified', singular: true, type: 'positiveInteger' }],
+ [['InitialRequest', 'Identity', 'DateOfBirth', 'Month'], { namespace: ns1, form: 'qualified', singular: true, type: 'positiveInteger' }],
+ [['InitialRequest', 'Identity', 'DateOfBirth', 'Year'], { namespace: ns1, form: 'qualified', singular: true, type: 'positiveInteger' }],
+
+ [['InitialRequest', 'Identity', 'DriversLicense'], { namespace: ns1, form: 'qualified', singular: true,
+ attributes: {
+ 'driversLicenseAddressType'=> { optional: true }
+ }
+ }],
+
+ [['InitialRequest', 'Identity', 'DriversLicense', 'Number'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'DriversLicense', 'Province'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+
+ [['InitialRequest', 'Identity', 'PhoneNumber'], { namespace: ns1, form: 'qualified', singular: true,
+ attributes: {
+ 'phoneType' => { optional: true }
+ }
+ }],
+
+ [['InitialRequest', 'Identity', 'PhoneNumber', 'AreaCode'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'PhoneNumber', 'Exchange'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'PhoneNumber', 'Number'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'PhoneNumber', 'PhoneNumber'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'Email'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'IPAddress'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'CreditCardNumber'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'Identity', 'CustomerId'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'ProcessingOptions'], { namespace: ns1, form: 'qualified', singular: true }],
+ [['InitialRequest', 'ProcessingOptions', 'Language'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }],
+ [['InitialRequest', 'ProcessingOptions', 'EnvironmentOverride'], { namespace: ns1, form: 'qualified', singular: true, type: 'string' }]
+ ])
+ end
+
+ it 'creates an example body with attributes' do
+ operation = client.operation(service_name, port_name, :startTransaction)
+
+ expect(operation.example_body).to eq(
+ InitialRequest: {
+ Identity: {
+ Name: {
+ FirstName: 'string',
+ MiddleName: 'string',
+ MiddleInitial: 'string',
+ LastName: 'string',
+ Suffix: 'string'
+ },
+
+ # TODO: shouldn't this be an Array?!
+ Address: {
+ FreeFormAddress: {
+ AddressLine: 'string'
+ },
+ HybridAddress: {
+ AddressLine: 'string',
+ City: 'string',
+ Province: 'string',
+ PostalCode: 'string'
+ },
+
+ # attributes are prefixed with an underscore.
+ _timeAtAddress: 'nonNegativeInteger',
+ _addressType: 'string'
+
+ },
+ SIN: 'string',
+ DateOfBirth: {
+ Day: 'positiveInteger',
+ Month: 'positiveInteger',
+ Year: 'positiveInteger'
+ },
+ DriversLicense: {
+ Number: 'string',
+ Province: 'string',
+
+ # another attribute
+ _driversLicenseAddressType: 'string'
+ },
+ PhoneNumber: {
+ AreaCode: 'string',
+ Exchange: 'string',
+ Number: 'string',
+ PhoneNumber: 'string',
+
+ # another attribute
+ _phoneType: 'string'
+ },
+ Email: 'string',
+ IPAddress: 'string',
+ CreditCardNumber: 'string',
+ CustomerId: 'string'
+ },
+ ProcessingOptions: {
+ Language: 'string',
+ EnvironmentOverride: 'string'
+ }
+ }
+ )
+ end
+
+ it 'creates a request with attributes' do
+ operation = client.operation(service_name, port_name, :startTransaction)
+
+ operation.body = {
+ InitialRequest: {
+ Identity: {
+ Address: {
+ FreeFormAddress: {
+ AddressLine: 'Abbey Road, London'
+ },
+ HybridAddress: {
+ AddressLine: 'Abbey Road',
+ City: 'London',
+ Province: 'Camden',
+ PostalCode: 'NW8 9BS'
+ },
+
+ # attributes are prefixed with an underscore
+ _timeAtAddress: 3,
+ _addressType: 'public'
+
+ }
+ }
+ }
+ }
+
+ expected = Nokogiri.XML('
+
+
+
+
+
+
+
+
+ Abbey Road, London
+
+
+ Abbey Road
+ London
+ Camden
+ NW8 9BS
+
+
+
+
+
+
+ ')
+
+ expect(Nokogiri.XML operation.build).
+ to be_equivalent_to(expected).respecting_element_order
+ end
+
+end