Proper namespace support for elements.

This is still a bit rough on the edges but already way better than the broken
setup I had before.
This commit is contained in:
Yorick Peterse 2014-08-11 00:41:36 +02:00
parent 04cbbdcf9e
commit 33c28f633b
5 changed files with 207 additions and 71 deletions

View File

@ -8,47 +8,48 @@ module Oga
# The name of the element. # The name of the element.
# @return [String] # @return [String]
# #
# @!attribute [rw] namespace # @!attribute [ww] namespace_name
# The namespace of the element, if any. # The name of the namespace.
# @return [Oga::XML::Namespace] # @return [String]
# #
# @!attribute [rw] attributes # @!attribute [rw] attributes
# The attributes of the element. # The attributes of the element.
# @return [Array<Oga::XML::Attribute>] # @return [Array<Oga::XML::Attribute>]
# #
# @!attribute [rw] namespaces
# The registered namespaces.
# @return [Hash]
#
class Element < Node class Element < Node
attr_accessor :name, :namespace, :attributes attr_accessor :name, :namespace_name, :attributes, :namespaces
## ##
# List of options that can be passed to the constructor and the required # The attribute prefix/namespace used for registering element namespaces.
# types of their values.
# #
# @return [Hash] # @return [String]
# #
OPTION_TYPES = { XMLNS_PREFIX = 'xmlns'.freeze
:namespace => Namespace,
:attributes => Array
}
## ##
# @param [Hash] options # @param [Hash] options
# #
# @option options [String] :name The name of the element. # @option options [String] :name The name of the element.
# #
# @option options [Oga::XML::Namespace] :namespace The namespace of the # @option options [String] :namespace_name The name of the namespace.
# element.
# #
# @option options [Array<Oga::XML::Attribute>] :attributes The attributes # @option options [Array<Oga::XML::Attribute>] :attributes The attributes
# of the element as an Array. # of the element as an Array.
# #
def initialize(options = {}) def initialize(options = {})
validate_option_types!(options)
super super
@name = options[:name] @name = options[:name]
@namespace = options[:namespace] @namespace_name = options[:namespace_name]
@attributes = options[:attributes] || [] @attributes = options[:attributes] || []
@namespaces = options[:namespaces] || {}
link_attributes
register_namespaces_from_attributes
end end
## ##
@ -76,6 +77,15 @@ module Oga
alias_method :attr, :attribute alias_method :attr, :attribute
##
# Returns the namespace of the element.
#
# @return [Oga::XML::Namespace]
#
def namespace
return @namespace ||= available_namespaces[namespace_name]
end
## ##
# Returns the text of all child nodes joined together. # Returns the text of all child nodes joined together.
# #
@ -146,40 +156,63 @@ module Oga
end end
## ##
# Returns a node set of all the namespaces that are available to the # Registers a new namespace for the current element and its child
# current node. This includes the namespaces registered on the current # elements.
# node.
# #
# @return [Oga::XML::NodeSet] # @param [String] name
# @param [String] uri
# @see [Oga::XML::Namespace#initialize]
# #
def available_namespaces def register_namespace(name, uri)
if namespaces[name]
raise ArgumentError, "The namespace #{name.inspect} already exists"
end
namespaces[name] = Namespace.new(:name => name, :uri => uri)
end end
## ##
# Returns a node set of all the namespaces registered with the current # Returns a Hash containing all the namespaces available to the current
# node. # element.
# #
# @return [Oga::XML::NodeSet] # @return [Hash]
# #
def namespaces def available_namespaces
merged = namespaces
node = parent
while node && node.respond_to?(:namespaces)
merged = merged.merge(node.namespaces)
node = node.parent
end
return merged
end end
private private
## ##
# @param [Hash] options # Registers namespaces based on any "xmlns" attributes. Once a namespace
# @raise [TypeError] # has been registered the corresponding attribute is removed.
# #
def validate_option_types!(options) def register_namespaces_from_attributes
OPTION_TYPES.each do |key, type| self.attributes = attributes.reject do |attr|
if options[key] and !options[key].is_a?(type) # We're using `namespace_name` opposed to `namespace.name` as "xmlns"
raise( # is not a registered namespace.
TypeError, remove = attr.namespace_name && attr.namespace_name == XMLNS_PREFIX
"#{key.inspect} must be an instance of #{type}"
) register_namespace(attr.name, attr.value) if remove
end
remove
end
end
##
# Links all attributes to the current element.
#
def link_attributes
attributes.each do |attr|
attr.element = self
end end
end end
@ -206,7 +239,7 @@ module Oga
if ns if ns
ns_matches = attr.namespace.to_s == ns ns_matches = attr.namespace.to_s == ns
elsif name_matches elsif name_matches and !attr.namespace
ns_matches = true ns_matches = true
end end

View File

@ -336,14 +336,10 @@ Unexpected #{name} with value #{value.inspect} on line #{@line}:
# @return [Oga::XML::Element] # @return [Oga::XML::Element]
# #
def on_element(namespace, name, attributes = {}) def on_element(namespace, name, attributes = {})
if namespace
namespace = Namespace.new(:name => namespace)
end
element = Element.new( element = Element.new(
:namespace => namespace, :namespace_name => namespace,
:name => name, :name => name,
:attributes => attributes :attributes => attributes
) )
return element return element

View File

@ -6,18 +6,6 @@ describe Oga::XML::Element do
described_class.new(:name => 'p').name.should == 'p' described_class.new(:name => 'p').name.should == 'p'
end end
example 'raise TypeError when the namespace is not a Namespace' do
block = lambda { described_class.new(:namespace => 'x') }
block.should raise_error(TypeError)
end
example 'raise TypeError when the attributes are not an Array' do
block = lambda { described_class.new(:attributes => 'foo') }
block.should raise_error(TypeError)
end
example 'set the name via a setter' do example 'set the name via a setter' do
instance = described_class.new instance = described_class.new
instance.name = 'p' instance.name = 'p'
@ -30,18 +18,42 @@ describe Oga::XML::Element do
end end
end end
context 'setting namespaces via attributes' do
before do
attr = Oga::XML::Attribute.new(:name => 'foo', :namespace_name => 'xmlns')
@element = described_class.new(:attributes => [attr])
end
example 'register the "foo" namespace' do
@element.namespaces['foo'].is_a?(Oga::XML::Namespace).should == true
end
example 'remove the namespace attribute from the list of attributes' do
@element.attributes.empty?.should == true
end
end
context '#attribute' do context '#attribute' do
before do before do
attributes = [ attributes = [
Oga::XML::Attribute.new(:name => 'key', :value => 'value'), Oga::XML::Attribute.new(:name => 'key', :value => 'value'),
Oga::XML::Attribute.new( Oga::XML::Attribute.new(
:name => 'key', :name => 'bar',
:value => 'foo', :value => 'baz',
:namespace => Oga::XML::Namespace.new(:name => 'x') :namespace_name => 'x'
),
Oga::XML::Attribute.new(
:name => 'key',
:value => 'foo',
:namespace_name => 'x'
) )
] ]
@instance = described_class.new(:attributes => attributes) @instance = described_class.new(
:attributes => attributes,
:namespaces => {'x' => Oga::XML::Namespace.new(:name => 'x')}
)
end end
example 'return an attribute with only a name' do example 'return an attribute with only a name' do
@ -71,6 +83,24 @@ describe Oga::XML::Element do
example 'return nil for a non existing attribute' do example 'return nil for a non existing attribute' do
@instance.attribute('foobar').nil?.should == true @instance.attribute('foobar').nil?.should == true
end end
example 'return nil if an attribute has a namespace that is not given' do
@instance.attribute('bar').nil?.should == true
end
end
context '#namespace' do
before do
@namespace = Oga::XML::Namespace.new(:name => 'x')
@element = described_class.new(
:namespace_name => 'x',
:namespaces => {'x' => @namespace}
)
end
example 'return the namespace' do
@element.namespace.should == @namespace
end
end end
context '#text' do context '#text' do
@ -116,8 +146,9 @@ describe Oga::XML::Element do
example 'include the namespace if present' do example 'include the namespace if present' do
instance = described_class.new( instance = described_class.new(
:name => 'p', :name => 'p',
:namespace => Oga::XML::Namespace.new(:name => 'foo') :namespace_name => 'foo',
:namespaces => {'foo' => Oga::XML::Namespace.new(:name => 'foo')}
) )
instance.to_xml.should == '<foo:p></foo:p>' instance.to_xml.should == '<foo:p></foo:p>'
@ -164,11 +195,13 @@ describe Oga::XML::Element do
example 'inspect a node with a namespace' do example 'inspect a node with a namespace' do
node = described_class.new( node = described_class.new(
:name => 'p', :name => 'p',
:namespace => Oga::XML::Namespace.new(:name => 'x') :namespace_name => 'x',
:namespaces => {'x' => Oga::XML::Namespace.new(:name => 'x')}
) )
node.inspect.should == 'Element(name: "p" namespace: Namespace(name: "x"))' node.inspect.should == 'Element(name: "p" ' \
'namespace: Namespace(name: "x" uri: nil))'
end end
end end
@ -178,7 +211,53 @@ describe Oga::XML::Element do
end end
end end
context '#register_namespace' do
before do
@element = described_class.new
@element.register_namespace('foo', 'http://example.com')
end
example 'return a Namespace instance' do
@element.namespaces['foo'].is_a?(Oga::XML::Namespace).should == true
end
example 'set the name of the namespace' do
@element.namespaces['foo'].name.should == 'foo'
end
example 'set the URI of the namespace' do
@element.namespaces['foo'].uri.should == 'http://example.com'
end
example 'raise ArgumentError if the namespace already exists' do
block = lambda { @element.register_namespace('foo', 'bar') }
block.should raise_error(ArgumentError)
end
end
context '#available_namespaces' do context '#available_namespaces' do
# TODO: write me before do
@parent = described_class.new
@child = described_class.new
@child.node_set = Oga::XML::NodeSet.new([@child], @parent)
@parent.register_namespace('foo', 'bar')
@child.register_namespace('baz', 'xxx')
@parent_ns = @parent.available_namespaces
@child_ns = @child.available_namespaces
end
example 'return the available namespaces of the child node' do
@child_ns['foo'].is_a?(Oga::XML::Namespace).should == true
@child_ns['baz'].is_a?(Oga::XML::Namespace).should == true
end
example 'return the available namespaces of the parent node' do
@parent_ns['foo'].is_a?(Oga::XML::Namespace).should == true
end
end end
end end

View File

@ -15,9 +15,9 @@ describe Oga::XML::Namespace do
context '#inspect' do context '#inspect' do
example 'return the inspect value' do example 'return the inspect value' do
ns = described_class.new(:name => 'x') ns = described_class.new(:name => 'x', :uri => 'y')
ns.inspect.should == 'Namespace(name: "x")' ns.inspect.should == 'Namespace(name: "x" uri: "y")'
end end
end end
end end

View File

@ -21,7 +21,7 @@ describe Oga::XML::Parser do
context 'elements with namespaces' do context 'elements with namespaces' do
before :all do before :all do
@element = parse('<foo:p></foo:p>').children[0] @element = parse('<foo:p xmlns:foo="bar"></foo:p>').children[0]
end end
example 'return an Element instance' do example 'return an Element instance' do
@ -57,7 +57,7 @@ describe Oga::XML::Parser do
context 'elements with namespaced attributes' do context 'elements with namespaced attributes' do
before :all do before :all do
@element = parse('<foo x:bar="baz"></foo>').children[0] @element = parse('<foo xmlns:x="x" x:bar="baz"></foo>').children[0]
end end
example 'return an Element instance' do example 'return an Element instance' do
@ -108,4 +108,32 @@ describe Oga::XML::Parser do
@element.children[1].children[0].is_a?(Oga::XML::Text).should == true @element.children[1].children[0].is_a?(Oga::XML::Text).should == true
end end
end end
context 'elements with namespace registrations' do
before :all do
document = parse('<root xmlns:a="1"><foo xmlns:b="2"></foo></root>')
@root = document.children[0]
@foo = @root.children[0]
end
example 'return the namespaces of the <root> node' do
@root.namespaces['a'].name.should == 'a'
@root.namespaces['a'].uri.should == '1'
end
example 'return the namespaces of the <foo> node' do
@foo.namespaces['b'].name.should == 'b'
@foo.namespaces['b'].uri.should == '2'
end
example 'return the available namespaces of the <root> node' do
@root.available_namespaces['a'].name.should == 'a'
end
example 'return the available namespaces of the <foo> node' do
@foo.available_namespaces['a'].name.should == 'a'
@foo.available_namespaces['b'].name.should == 'b'
end
end
end end