From 24376f2f1fa12af12aa352e62d78a11ca0f29a8a Mon Sep 17 00:00:00 2001 From: rubiii Date: Tue, 4 Jun 2013 21:19:16 +0200 Subject: [PATCH] first code and specs to support attributes example Hashes contain attributes prefixed with _ (underscore). the @-sign would be more natural for this, but it doesn't work with the new ruby 1.9 hash syntax. /cc savonrb/wasabi#27 --- lib/savon/attribute.rb | 11 + lib/savon/element.rb | 28 +- lib/savon/example_message.rb | 11 + lib/savon/message.rb | 19 +- lib/savon/wsdl/message_builder.rb | 47 ++ lib/savon/xs/schema.rb | 30 +- lib/savon/xs/schema_collection.rb | 8 + lib/savon/xs/types.rb | 74 +++ spec/fixtures/wsdl/equifax.wsdl | 794 ++++++++++++++++++++++++++++++ spec/integration/equifax_spec.rb | 212 ++++++++ 10 files changed, 1214 insertions(+), 20 deletions(-) create mode 100644 lib/savon/attribute.rb create mode 100644 spec/fixtures/wsdl/equifax.wsdl create mode 100644 spec/integration/equifax_spec.rb 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