rsxml 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -6,7 +6,7 @@ Why would you want to do this ? Well, s-expressions can be == compared natively
6
6
  and editors indent them nicely when embedded in code. These features make them very suitable for writing readable
7
7
  XML generation code and readable tests for XML generating code
8
8
 
9
- Rsxml uses Nokogiri[http://nokogiri.org/] for parsing XML, and Builder[http://builder.rubyforge.org/] to generate it. Rsxml is not intended to be a feature complete XML representation : It does not attempt to represent PIs, CDATA etc, but it does make it very easy to use and generate straightforward XML documents from Ruby
9
+ Rsxml uses Nokogiri[http://nokogiri.org/] for parsing XML, and Builder[http://builder.rubyforge.org/] to generate it. Rsxml is not intended to be a feature complete XML processor : It does not attempt to process PIs, CDATA etc, but it does make it very easy to use and generate straightforward XML documents from Ruby
10
10
 
11
11
  Rsxml represents XML documents as s-expressions thus :
12
12
 
@@ -25,9 +25,9 @@ It is easy to convert XML docuemnts to Rsxml representation and back again :
25
25
 
26
26
  === Namespaces
27
27
 
28
- XML namespaces are dealt with straightforwardly. When an XML document is converted to Rsxml, namespaces are preserved, and you can specify namespaces in an Rsxml structure in two ways
28
+ XML namespaces are dealt with straightforwardly. When an XML document is converted to Rsxml, namespaces are preserved, and you can specify namespaces in an Rsxml structure in two ways, which can be freely mixed
29
29
  * using QName prefixes and declarative attributes, exactly as with XML
30
- * using exploded QNames consisting of <tt>[local_name, prefix, uri]</tt> triples and <tt>[local_name, prefix]</tt> pairs
30
+ * using exploded QNames consisting of <tt>[local_part, prefix, uri]</tt> triples and <tt>[local_part, prefix]</tt> pairs
31
31
 
32
32
  === Converting to Rsxml
33
33
 
@@ -44,7 +44,7 @@ prefixed QNames, just as in XML
44
44
  ==== <tt>:exploded</tt> style
45
45
 
46
46
  In <tt>:exploded</tt> style namespaces are not declared using attributes, and QNames are specified
47
- using <tt>[local_name, prefix, uri]</tt> triples
47
+ using <tt>[local_part, prefix, uri]</tt> triples
48
48
 
49
49
  Rsxml.to_rsxml('<foo:foofoo xmlns:foo="http://foo.com/foo" foo:bar="barbar"/>', :style=>:exploded)
50
50
  => [["foofoo", "foo", "http://foo.com/foo"], {["bar", "foo", "http://foo.com/foo"]=>"barbar"}]
@@ -69,6 +69,18 @@ Fragments can be generated similarly :
69
69
  Rsxml.to_xml(["foo:foofoo", {"foo:bar"=>"barbar"}], :ns=>{"foo"=>"http://foo.com/foo"})
70
70
  => '<foo:foofoo foo:bar="barbar"></foo:foofoo>'
71
71
 
72
+ == Visitors
73
+
74
+ <tt>Rsxml.to_rsxml</tt> and <tt>Rsxml.to_xml</tt> are implemented with a simple Visitor pattern over the XML document structure. <tt>Rsxml::Sexp.traverse()</tt> traverses a Rsxml s-expression representation of an XML document and <tt>Rsxml::Xml.traverse()</tt> traverses a Nokogiri Node tree. The Visitor receives a
75
+
76
+ text(context, text)
77
+
78
+ invocation for each text node, and an
79
+
80
+ element(context, name, attrs, ns_decls)
81
+
82
+ method invocation for each element. If namespaces are used, element and attribute names in the <tt>element</tt> method invocations are exploded <tt>[local_part, prefix, uri]</tt> triples. The attributes are presented as a <tt>{name=>value}</tt> +Hash+ which contains no namespace-related attributes. Any namespace declarations for the element are provided as the <tt>{prefix=>uri}</tt> +ns_decls+ +Hash+. Namespace prefixes, URIs and declaration attributes are cleanly separated in this API, so it is easy for Visitor implementations to correctly process XML documents with namespaces
83
+
72
84
  == Install
73
85
 
74
86
  gem install rsxml
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 0.3.0
@@ -13,9 +13,9 @@ module Rsxml
13
13
  def explode_attr_qnames(ns_stack, attrs)
14
14
  Hash[attrs.map do |name, value|
15
15
  uq_name = explode_qname(ns_stack, name, true)
16
- local_name, prefix, uri = uq_name
16
+ local_part, prefix, uri = uq_name
17
17
  if !prefix || prefix==""
18
- [local_name, value]
18
+ [local_part, value]
19
19
  else
20
20
  [uq_name, value]
21
21
  end
@@ -39,15 +39,22 @@ module Rsxml
39
39
  end
40
40
  end
41
41
 
42
- # split a QName into [LocalPart, prefix, URI] triple
42
+ # split a QName into [LocalPart, prefix, URI] triple. fill out
43
+ # missing uris if necessary, and check QName is well specified
43
44
  def explode_qname(ns_stack, qname, attr=false)
44
45
  if qname.is_a?(Array)
45
- if qname.length>1 && !qname[1].nil?
46
- return qname
47
- elsif qname.length>1 && qname[1].nil? && !qname[2].nil?
48
- raise "invalid name: #{qname.inspect}"
46
+ local_part, prefix, uri = qname
47
+ if uri
48
+ raise "invalid name: #{qname.inspect}" if !prefix
49
+ bound_uri = find_namespace_uri(ns_stack, prefix)
50
+ raise "namespace conflict: prefix '#{prefix}' refers to '#{uri}' and '#{bound_uri}'" if bound_uri && uri != bound_uri
51
+ return [local_part, prefix, uri]
52
+ elsif prefix
53
+ uri = find_namespace_uri(ns_stack, prefix)
54
+ raise "namespace prefix not bound: '#{prefix}'" if !uri
55
+ return [local_part, prefix, uri]
49
56
  else
50
- return qname[0]
57
+ return local_part
51
58
  end
52
59
  end
53
60
 
@@ -93,24 +100,28 @@ module Rsxml
93
100
  uri
94
101
  end
95
102
 
96
-
97
- # extract a Hash of {prefix=>uri} mappings declared in attributes
98
- def extract_declared_namespace_bindings(attrs)
99
- Hash[attrs.map do |name,value|
100
- local_part, prefix, uri = split_qname(name)
101
- if (prefix && prefix == "xmlns")
102
- [local_part, value]
103
- elsif (!prefix && local_part == "xmlns")
104
- ["", value]
105
- end
106
- end.compact]
103
+ # split attributes into non-namespace related attrs and {prefix=>uri} namespace bindings
104
+ def partition_namespace_decls(attrs)
105
+ nonns_attrs = []
106
+ ns_bindings = []
107
+ attrs.each do |name, value|
108
+ local_part, prefix = split_qname(name)
109
+ if prefix && prefix=="xmlns"
110
+ ns_bindings << [local_part, value]
111
+ elsif !prefix && local_part=="xmlns"
112
+ ns_bindings << ["", value]
113
+ else
114
+ nonns_attrs << [name, value]
115
+ end
116
+ end
117
+ [Hash[nonns_attrs], Hash[ns_bindings]]
107
118
  end
108
119
 
109
- # extract a Hash of {prefix=>uri} mappings from exploded QName tag and attrs
110
- def extract_explicit_namespace_bindings(tag, attrs)
111
- tag_local_part, tag_prefix, tag_uri = tag
120
+ # extract a Hash of {prefix=>uri} mappings from exploded QName element and attrs
121
+ def extract_explicit_namespace_bindings(element_name, attrs)
122
+ element_name_local_part, element_name_prefix, element_name_uri = element_name
112
123
  ns = {}
113
- ns[tag_prefix] = tag_uri if tag_prefix && tag_uri
124
+ ns[element_name_prefix] = element_name_uri if element_name_prefix && element_name_uri
114
125
 
115
126
  attrs.each do |name, value|
116
127
  attr_local_part, attr_prefix, attr_uri = name
@@ -125,7 +136,7 @@ module Rsxml
125
136
  # figure out which explicit namespaces need declaring
126
137
  #
127
138
  # +ns_stack+ is the stack of namespace bindings
128
- # +ns_explicit+ is the explicit refs for a tag
139
+ # +ns_explicit+ is the explicit refs for a element
129
140
  def undeclared_namespace_bindings(ns_stack, ns_explicit)
130
141
  Hash[ns_explicit.map do |prefix,uri|
131
142
  [prefix, uri] if !find_namespace_uri(ns_stack, prefix, uri)
@@ -145,6 +156,18 @@ module Rsxml
145
156
  end]
146
157
  end
147
158
 
159
+ # produces a Hash of compact namespace attributes from a
160
+ # Hash of namespace bindings
161
+ def namespace_attributes(ns)
162
+ Hash[ns.map do |prefix, uri|
163
+ if prefix==""
164
+ ["xmlns", uri]
165
+ else
166
+ [["xmlns", prefix].join(":"), uri]
167
+ end
168
+ end]
169
+ end
170
+
148
171
  # merges two sets of namespace bindings, raising error on clash
149
172
  def merge_namespace_bindings(ns1, ns2)
150
173
  m = ns1.clone
@@ -155,20 +178,18 @@ module Rsxml
155
178
  m
156
179
  end
157
180
 
158
- # given the existing +ns_stack+ of ns bindings, a +tag+ and it's +attributes+,
159
- # return a pair <tt>[ns_bindings, ns_additional_decls]</tt> containing
160
- # ns bindings for the stack, and additional required (exploded) namespace
161
- # declarations to be added to the attributes
162
- def namespace_bindings_declarations(ns_stack, tag, attrs)
163
- ns_declared = extract_declared_namespace_bindings(attrs)
164
- ns_explicit = extract_explicit_namespace_bindings(tag, attrs)
181
+ # given the existing +ns_stack+ of ns bindings, a +element_name+ and it's +attributes+,
182
+ # return a pair <tt>[non_ns_attrs, ns_bindings]</tt> containing
183
+ # non-ns related attributes, and namespace bindings for the current element,
184
+ # both those declared in attributes and declared implicitly through exploded element names
185
+ def non_ns_attrs_ns_bindings(ns_stack, element_name, attrs)
186
+ non_ns_attrs, ns_declared = partition_namespace_decls(attrs)
187
+
188
+ ns_explicit = extract_explicit_namespace_bindings(element_name, attrs)
165
189
  ns_undeclared = undeclared_namespace_bindings(ns_stack + [ns_declared], ns_explicit)
166
190
  ns_bindings = merge_namespace_bindings(ns_declared, ns_undeclared)
167
191
 
168
- # and declarations for undeclared namespaces
169
- ns_additional_decls = exploded_namespace_declarations(ns_undeclared)
170
-
171
- [ns_bindings, ns_additional_decls]
192
+ [non_ns_attrs, ns_bindings]
172
193
  end
173
194
 
174
195
  end
data/lib/rsxml/sexp.rb CHANGED
@@ -6,34 +6,25 @@ module Rsxml
6
6
  # pre-order traversal of the sexp, calling methods on
7
7
  # the visitor with each node
8
8
  def traverse(sexp, visitor, context=Visitor::Context.new)
9
- tag, attrs, children = decompose_sexp(sexp)
9
+ element_name, attrs, children = decompose_sexp(sexp)
10
10
 
11
- ns_bindings, ns_additional_decls = Namespace::namespace_bindings_declarations(context.ns_stack, tag, attrs)
11
+ non_ns_attrs, ns_bindings = Namespace::non_ns_attrs_ns_bindings(context.ns_stack, element_name, attrs)
12
12
 
13
13
  context.ns_stack.push(ns_bindings)
14
14
 
15
- etag = Namespace::explode_qname(context.ns_stack, tag)
16
- eattrs = Namespace::explode_attr_qnames(context.ns_stack, attrs)
17
-
18
- eattrs = eattrs.merge(ns_additional_decls)
15
+ eelement_name = Namespace::explode_qname(context.ns_stack, element_name)
16
+ eattrs = Namespace::explode_attr_qnames(context.ns_stack, non_ns_attrs)
19
17
 
20
18
  begin
21
- visitor.tag(context, etag, eattrs) do
22
- context.push_node([etag, eattrs])
23
- begin
24
- children.each_with_index do |child, i|
25
- if child.is_a?(Array)
26
- traverse(child, visitor, context)
27
- else
28
- visitor.text(context, child)
29
- context.processed_node(child)
30
- end
19
+ visitor.element(context, eelement_name, eattrs, ns_bindings) do
20
+ children.each_with_index do |child, i|
21
+ if child.is_a?(Array)
22
+ traverse(child, visitor, context)
23
+ else
24
+ visitor.text(context, child)
31
25
  end
32
- ensure
33
- context.pop_node
34
26
  end
35
27
  end
36
-
37
28
  ensure
38
29
  context.ns_stack.pop
39
30
  end
@@ -41,13 +32,13 @@ module Rsxml
41
32
  visitor
42
33
  end
43
34
 
44
- # decompose a sexp to a [tag, attrs, children] list
35
+ # decompose a sexp to a [element_name, attrs, children] list
45
36
  def decompose_sexp(sexp)
46
37
  raise "invalid rsxml: #{rsxml.inspect}" if sexp.length<1
47
38
  if sexp[0].is_a?(Array)
48
- tag = sexp[0]
39
+ element_name = sexp[0]
49
40
  else
50
- tag = sexp[0].to_s
41
+ element_name = sexp[0].to_s
51
42
  end
52
43
  if sexp[1].is_a?(Hash)
53
44
  attrs = sexp[1]
@@ -56,7 +47,7 @@ module Rsxml
56
47
  attrs = {}
57
48
  children = sexp[1..-1]
58
49
  end
59
- [tag, attrs, children]
50
+ [element_name, attrs, children]
60
51
  end
61
52
 
62
53
  class ComparisonError < RuntimeError
@@ -68,14 +59,14 @@ module Rsxml
68
59
  end
69
60
 
70
61
  def compare(sexpa, sexpb, path=nil)
71
- taga, attrsa, childrena = decompose_sexp(sexpa)
72
- tagb, attrsb, childrenb = decompose_sexp(sexpb)
62
+ element_name_a, attrsa, childrena = decompose_sexp(sexpa)
63
+ element_name_b, attrsb, childrenb = decompose_sexp(sexpb)
73
64
 
74
- raise ComparisonError.new("element names differ: '#{taga}', '#{tagb}'", path) if taga != tagb
65
+ raise ComparisonError.new("element names differ: '#{element_name_a}', '#{element_name_b}'", path) if element_name_a != element_name_b
75
66
  raise ComparisonError.new("attributes differ", path) if attrsa != attrsb
76
67
  raise ComparisonError.new("child count differs", path) if childrena.length != childrenb.length
77
68
 
78
- path = [path, taga].compact.join("/")
69
+ path = [path, element_name_a].compact.join("/")
79
70
  (0...childrena.length).each do |i|
80
71
  if childrena[i].is_a?(Array) && childrenb[i].is_a?(Array)
81
72
  compare(childrena[i], childrenb[i], path)
data/lib/rsxml/visitor.rb CHANGED
@@ -6,30 +6,9 @@ module Rsxml
6
6
 
7
7
  class Context
8
8
  attr_reader :ns_stack
9
- attr_reader :node_stack
10
- attr_reader :prev_siblings
11
9
  def initialize(initial_ns_bindings = nil)
12
10
  @ns_stack=[]
13
11
  @ns_stack << initial_ns_bindings if initial_ns_bindings
14
- @node_stack=[]
15
- @prev_siblings=[]
16
- @sibling_stack=[]
17
- end
18
-
19
- def push_node(node)
20
- node_stack.push(node)
21
- @sibling_stack.push(@prev_siblings)
22
- @prev_siblings=[]
23
- end
24
-
25
- def pop_node
26
- n = node_stack.pop
27
- @prev_siblings = @sibling_stack.pop
28
- @prev_siblings << n
29
- end
30
-
31
- def processed_node(node)
32
- @prev_siblings << node
33
12
  end
34
13
  end
35
14
 
@@ -40,11 +19,13 @@ module Rsxml
40
19
  @xml = xml_builder || Builder::XmlMarkup.new
41
20
  end
42
21
 
43
- def tag(context, name, attrs)
22
+ def element(context, name, attrs, ns_decls)
44
23
  qname = Namespace::compact_qname(context.ns_stack, name)
45
24
  qattrs = Namespace::compact_attr_qnames(context.ns_stack, attrs)
25
+ ns_attrs = Namespace::namespace_attributes(ns_decls)
26
+ attrs = qattrs.merge(ns_attrs)
46
27
 
47
- xml.__send__(qname, qattrs) do
28
+ xml.__send__(qname, attrs) do
48
29
  yield
49
30
  end
50
31
  end
@@ -62,50 +43,42 @@ module Rsxml
62
43
  attr_reader :sexp
63
44
  attr_reader :cursor_stack
64
45
  attr_reader :opts
46
+ attr_reader :element_transformer
65
47
 
66
48
  # The <tt>:style</tt> option specifies how the Rsxml is to be produced
67
- # :xml style is with compact <tt>"prefix:local_name"</tt> Strings for QNames, and namespace declaration attributes
68
- # :exploded style is with <tt>[local_name, prefix, uri]</tt> triples for QNames, and no namespace declaration attributes
49
+ # :xml style is with compact <tt>"prefix:local_part"</tt> Strings for QNames, and namespace declaration attributes
50
+ # :exploded style is with <tt>[local_part, prefix, uri]</tt> triples for QNames, and no namespace declaration attributes
69
51
  OPTS = {:style=>[:xml, :exploded]}
70
52
 
71
- def initialize(opts=nil)
72
- @opts = opts || {}
73
- opts = Util.check_opts(OPTS, opts)
53
+ def initialize(opts=nil, &element_transformer)
54
+ @opts = Util.check_opts(OPTS, opts)
74
55
  @cursor_stack = []
75
- @sexp
56
+ @element_transformer = element_transformer
76
57
  end
77
58
 
78
59
  def compact_qname(qname)
79
- local_name, prefix, uri = qname
60
+ local_part, prefix, uri = qname
80
61
 
81
- [prefix, local_name].map{|s| (!s || s.empty?) ? nil : s}.compact.join(":")
62
+ [prefix, local_part].map{|s| (!s || s.empty?) ? nil : s}.compact.join(":")
82
63
  end
83
64
 
84
65
  def compact_attr_names(attrs)
85
66
  Hash[attrs.map{|qname,value| [compact_qname(qname), value]}]
86
67
  end
87
68
 
88
- # strip namespace decls from exploded attributes
89
- def strip_namespace_decls(attrs)
90
- Hash[attrs.map do |qname,value|
91
- local_name, prefix, uri = qname
92
- if !(prefix=="xmlns" || (prefix==nil && local_name=="xmlns"))
93
- [qname,value]
94
- end
95
- end.compact]
96
- end
69
+ def element(context, element_name, attrs, ns_decls)
97
70
 
98
- def tag(context, tag, attrs)
71
+ element_name, attrs = element_transformer.call(context, element_name, attrs) if element_transformer
99
72
 
100
73
  opts[:style] ||= :exploded
101
74
  if opts[:style] == :xml
102
- tag = compact_qname(tag)
75
+ element_name = compact_qname(element_name)
103
76
  attrs = compact_attr_names(attrs)
104
- elsif opts[:style] == :exploded
105
- attrs = strip_namespace_decls(attrs)
77
+ ns_attrs = Namespace.namespace_attributes(ns_decls)
78
+ attrs = attrs.merge(ns_attrs)
106
79
  end
107
80
 
108
- el = [tag, (attrs if attrs.size>0)].compact
81
+ el = [element_name, (attrs if attrs.size>0)].compact
109
82
 
110
83
  if !cursor_stack.last
111
84
  @sexp = el
data/lib/rsxml/xml.rb CHANGED
@@ -36,18 +36,8 @@ module Rsxml
36
36
  node = node_uri ? [node_name, node_prefix, node_uri] : node_name
37
37
  end
38
38
 
39
- def explode_ns_definitions(element)
40
- Hash[element.namespace_definitions.map do |ns|
41
- if ns.prefix
42
- [[ns.prefix, "xmlns"], ns.href]
43
- else
44
- ["xmlns", ns.href]
45
- end
46
- end]
47
- end
48
-
49
39
  # given a <tt>Nokogiri::XML::Element</tt> in +element+ , produce
50
- # a <tt>[[local_name, prefix, namespace], {[local_name, prefix, namespace]=>value}</tt>
40
+ # a <tt>[[local_part, prefix, namespace], {[local_part, prefix, namespace]=>value}</tt>
51
41
  # pair of exploded element name and attributes with exploded names
52
42
  def explode_element(element)
53
43
  eelement = explode_node(element)
@@ -56,36 +46,36 @@ module Rsxml
56
46
  [explode_node(attr), attr.value]
57
47
  end]
58
48
 
59
- ns_decl_attrs = explode_ns_definitions(element)
60
- eattrs = eattrs.merge(ns_decl_attrs)
61
-
62
49
  [eelement, eattrs]
63
50
  end
64
51
 
52
+ # give a list of <tt>Nokogiri::XML::Namespace</tt> definitions, produce
53
+ # a Hash <tt>{prefix=>uri}</tt> of namespace bindings
54
+ def namespace_bindings_from_defs(ns_defs)
55
+ (ns_defs||[]).reduce({}) do |h,ns_def|
56
+ h[ns_def.prefix||""] = ns_def.href
57
+ h
58
+ end
59
+ end
60
+
65
61
  # pre-order traversal of the Nokogiri Nodes, calling methods on
66
62
  # the visitor with each Node
67
63
  def traverse(element, visitor, context = Visitor::Context.new)
68
- eelement, eattrs = explode_element(element)
69
-
70
- ns_bindings = Rsxml::Namespace.extract_declared_namespace_bindings(element.namespaces)
64
+ ns_bindings = namespace_bindings_from_defs(element.namespace_definitions)
71
65
  context.ns_stack.push(ns_bindings)
72
66
 
67
+ eelement, eattrs = explode_element(element)
68
+
73
69
  begin
74
- visitor.tag(context, eelement, eattrs) do
75
- context.push_node([eelement, eattrs])
76
- begin
77
- element.children.each do |child|
78
- if child.element?
79
- traverse(child, visitor, context)
80
- elsif child.text?
81
- visitor.text(context, child.content)
82
- context.processed_node(child.content)
83
- else
84
- Rsxml.log{|logger| logger.warn("unknown Nokogiri Node type: #{child.inspect}")}
85
- end
70
+ visitor.element(context, eelement, eattrs, ns_bindings) do
71
+ element.children.each do |child|
72
+ if child.element?
73
+ traverse(child, visitor, context)
74
+ elsif child.text?
75
+ visitor.text(context, child.content)
76
+ else
77
+ Rsxml.log{|logger| logger.warn("unknown Nokogiri Node type: #{child.inspect}")}
86
78
  end
87
- ensure
88
- context.pop_node
89
79
  end
90
80
  end
91
81
  ensure
@@ -0,0 +1,43 @@
1
+ module Rsxml
2
+ module Visitor
3
+ # a mock visitor which can be used to check expectations
4
+ class MockVisitor
5
+ attr_reader :expectations
6
+
7
+ def initialize(expectations)
8
+ @expectations = expectations.clone
9
+ end
10
+
11
+ def __format_invocation__(method, args)
12
+ "#{method}(#{(args||[]).map(&:inspect).join(', ')})"
13
+ end
14
+
15
+ def __check_arg_expectation__(arg_xp, arg)
16
+ return true if arg_xp == :_
17
+ return arg_xp.call(arg) if arg_xp.is_a?(Proc)
18
+ return arg_xp == arg
19
+ end
20
+
21
+ def __check_expectation__(method, args)
22
+ xp_method, *xp_args = expectations.shift
23
+ msg = "unexpected invocation: #{__format_invocation__(method, args)}. expected: #{__format_invocation__(xp_method, xp_args)}"
24
+ raise msg if method!=xp_method || args.length != xp_args.length
25
+ (0...xp_args.length).each do |i|
26
+ raise msg if !__check_arg_expectation__(xp_args[i], args[i])
27
+ end
28
+ end
29
+
30
+ def __finalize__
31
+ raise "missing invocations: #{expectations.map{|xp_method, *xp_args| __format_invocation__(xp_method, xp_args)}.join('\n')}" if expectations && expectations.length>0
32
+ end
33
+
34
+ # check that there is an expectation for the invoked method, and if
35
+ # a block is given yield to that block
36
+ def method_missing(method, *args, &block)
37
+ __check_expectation__(method, args)
38
+ block.call if block
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -22,11 +22,26 @@ module Rsxml
22
22
  end
23
23
 
24
24
  describe "explode_qname" do
25
- it "should do nothing to an array with more than one element" do
25
+ it "should do nothing to an array with an exploded name" do
26
26
  Namespace.explode_qname([], ["bar", "foo", "http://foo.com/foo"]).should ==
27
27
  ["bar", "foo", "http://foo.com/foo"]
28
- Namespace.explode_qname([], ["bar", "foo"]).should ==
29
- ["bar", "foo"]
28
+ end
29
+
30
+ it "should fill in the uri if a bound prefix with no uri is given" do
31
+ Namespace.explode_qname([{"foo"=>"http://foo.com/foo"}], ["bar", "foo"]).should ==
32
+ ["bar", "foo", "http://foo.com/foo"]
33
+ end
34
+
35
+ it "should raise an exception if an unbound prefix and no uri is given" do
36
+ lambda{
37
+ Namespace.explode_qname([], ["bar", "foo"])
38
+ }.should raise_error(/not bound/)
39
+ end
40
+
41
+ it "should raise an execption if a prefix and uri are given which conflict with ns bindings" do
42
+ lambda{
43
+ Namespace.explode_qname([{"foo"=>"http://foo.com/foo"}], ["bar", "foo", "http://bar.com/bar"])
44
+ }.should raise_error
30
45
  end
31
46
 
32
47
  it "should return the first element of an array with only the first non-nil element" do
@@ -159,15 +174,15 @@ module Rsxml
159
174
 
160
175
  end
161
176
 
162
- describe "extract_declared_namespace_bindings" do
163
- it "should extract a hash of declared namespace bindings from a Hash of attributes" do
164
- Namespace.extract_declared_namespace_bindings({"xmlns"=>"http://default.com/default", "xmlns:foo"=>"http://foo.com/foo", "foo"=>"bar"}).should ==
165
- {""=>"http://default.com/default", "foo"=>"http://foo.com/foo"}
177
+ describe "partition_namespace_decls" do
178
+ it "should partition attributes into non-namespace attributes and a hash of namespace bindings" do
179
+ Namespace.partition_namespace_decls({"xmlns"=>"http://default.com/default", "xmlns:foo"=>"http://foo.com/foo", "foo"=>"bar"}).should ==
180
+ [{"foo"=>"bar"},{""=>"http://default.com/default", "foo"=>"http://foo.com/foo"}]
166
181
  end
167
182
  end
168
183
 
169
184
  describe "extract_explicit_namespace_bindings" do
170
- it "should extract a hash of explicit namespace bindings from expanded tags" do
185
+ it "should extract a hash of explicit namespace bindings from expanded QNames" do
171
186
  Namespace.extract_explicit_namespace_bindings(["bar", "foo", "http://foo.com/foo"],
172
187
  {["baz", "bar", "http://bar.com/bar"]=>"baz",
173
188
  ["boo", "", "http://default.com/default"]=>"boo"}).should ==
@@ -225,26 +240,38 @@ module Rsxml
225
240
  end
226
241
  end
227
242
 
228
- describe "namespace_bindings_declarations" do
229
- it "should extract declared and explicit bindings and additional declarations" do
230
- Namespace.namespace_bindings_declarations([{"foo"=>"http://foo.com/foo"}],
231
- [:bar, "foo", "http://foo.com/foo"],
232
- { [:b, "bar", "http://bar.com/bar"]=>"barbar",
233
- [:c, "baz", "http://baz.com/baz"]=>"bazbaz",
234
- [:d, "foo"]=>"booboo"}).should ==
235
- [{ "baz"=>"http://baz.com/baz",
236
- "bar"=>"http://bar.com/bar"},
237
- { ["baz", "xmlns"]=>"http://baz.com/baz",
238
- ["bar", "xmlns"]=>"http://bar.com/bar"}]
243
+ describe "namespace_attributes" do
244
+ it "should produce a Hash of namespace declaration attributes" do
245
+ Namespace.namespace_attributes({""=>"http://default.com/default", "foo"=>"http://foo.com/foo"}).should ==
246
+ {"xmlns"=>"http://default.com/default", "xmlns:foo"=>"http://foo.com/foo"}
247
+ end
248
+ end
249
+
250
+ describe "non_ns_attrs_ns_bindings" do
251
+ it "should extract non-ns attributes and explicit namespace bindings" do
252
+ Namespace.non_ns_attrs_ns_bindings([{"foo"=>"http://foo.com/foo"}],
253
+ [:bar, "foo", "http://foo.com/foo"],
254
+ { "xmlns:boo"=>"http://boo.com/boo",
255
+ [:b, "bar", "http://bar.com/bar"]=>"barbar",
256
+ [:c, "baz", "http://baz.com/baz"]=>"bazbaz",
257
+ [:d, "foo"]=>"booboo"}).should ==
258
+ [{[:b, "bar", "http://bar.com/bar"]=>"barbar",
259
+ [:c, "baz", "http://baz.com/baz"]=>"bazbaz",
260
+ [:d, "foo"]=>"booboo"},
261
+ { "baz"=>"http://baz.com/baz",
262
+ "bar"=>"http://bar.com/bar",
263
+ "boo"=>"http://boo.com/boo"}]
239
264
  end
240
265
 
241
266
  it "should extract explicit default namespace bindings" do
242
- Namespace.namespace_bindings_declarations([{"foo"=>"http://foo.com/foo"}],
243
- [:bar, "", "http://default.com/default"],
244
- { [:d, "foo"]=>"booboo"}).should ==
245
- [{ ""=>"http://default.com/default"},
246
- { "xmlns"=>"http://default.com/default"}]
267
+ Namespace.non_ns_attrs_ns_bindings([{"foo"=>"http://foo.com/foo"}],
268
+ [:bar, "", "http://default.com/default"],
269
+ { [:d, "foo"]=>"booboo"}).should ==
270
+ [{ [:d, "foo"]=>"booboo"},
271
+ { ""=>"http://default.com/default"}]
247
272
  end
273
+
274
+
248
275
  end
249
276
 
250
277
  end
@@ -1,13 +1,22 @@
1
1
  require File.expand_path("../../spec_helper", __FILE__)
2
2
 
3
3
  module Rsxml
4
-
5
-
6
4
  describe "traverse" do
7
-
5
+ # tests moved to visitor_spec... done in parallel with Xml traverse tests
8
6
  end
9
7
 
10
8
  describe "decompose_sexp" do
9
+ it "should decompose a [element_name] sexp" do
10
+ Sexp.decompose_sexp(["foo"]).should == ["foo", {}, []]
11
+ end
12
+
13
+ it "should decompose a [element_name, attrs] sexp" do
14
+ Sexp.decompose_sexp(["foo", {"foofoo"=>"a"}]).should == ["foo", {"foofoo"=>"a"}, []]
15
+ end
16
+
17
+ it "should decompose a [element_name, attrs, children] sexp" do
18
+ Sexp.decompose_sexp(["foo", {"foofoo"=>"a"}, ["bar"], ["baz"]]).should == ["foo", {"foofoo"=>"a"}, [["bar"], ["baz"]]]
19
+ end
11
20
  end
12
21
 
13
22
  describe "compare" do
@@ -1,6 +1,103 @@
1
1
  require File.expand_path("../../spec_helper", __FILE__)
2
2
 
3
3
  module Rsxml
4
+ # tests traverse methods on both Xml and Sexp together : they should produce identical visitation
5
+ # patterns
6
+ describe "traverse" do
7
+ def check_traverse(expectations, xml, sexp)
8
+ xml_visitor = Visitor::MockVisitor.new(expectations)
9
+ xml_root = Nokogiri::XML(xml).children.first
10
+ Xml.traverse(xml_root, xml_visitor)
11
+ xml_visitor.__finalize__
12
+
13
+ sexp_visitor = Visitor::MockVisitor.new(expectations)
14
+ Sexp.traverse(sexp, sexp_visitor)
15
+ sexp_visitor.__finalize__
16
+ end
17
+
18
+ it "should call the element function on the visitor" do
19
+ check_traverse([[:element, :_, "foo", {"bar"=>"barbar"}, {}]],
20
+ '<foo bar="barbar"/>',
21
+ [:foo, {"bar"=>"barbar"}])
22
+ end
23
+
24
+ it "should call the element function on the visitor with exploded element and attributes qnames" do
25
+ check_traverse([[:element, :_,
26
+ ["foofoo", "foo", "http://foo.com/foo"],
27
+ {["bar", "foo", "http://foo.com/foo"]=>"barbar"},
28
+ {"foo"=>"http://foo.com/foo"}]],
29
+ '<foo:foofoo foo:bar="barbar" xmlns:foo="http://foo.com/foo"/>',
30
+ [["foofoo", "foo"], {["bar", "foo", "http://foo.com/foo"]=>"barbar"}])
31
+ end
32
+
33
+ it "should call the test function on the visitor with textual content" do
34
+ check_traverse([[:element, :_,
35
+ ["foofoo", "foo", "http://foo.com/foo"],
36
+ {["bar", "foo", "http://foo.com/foo"]=>"barbar"},
37
+ {"foo"=>"http://foo.com/foo"}],
38
+ [:text, :_, "boohoo"]],
39
+ '<foo:foofoo foo:bar="barbar" xmlns:foo="http://foo.com/foo">boohoo</foo:foofoo>',
40
+ [["foofoo", "foo"], {["bar", "foo", "http://foo.com/foo"]=>"barbar"}, "boohoo"])
41
+ end
42
+
43
+ it "should call the element function in document order for each element in a hierarchic doc" do
44
+ check_traverse([[:element, :_,
45
+ ["foofoo", "foo", "http://foo.com/foo"],
46
+ {["bar", "foo", "http://foo.com/foo"]=>"barbar"},
47
+ {"foo"=>"http://foo.com/foo"}],
48
+ [:element, :_,
49
+ ["barbar", "foo", "http://foo.com/foo"],
50
+ {["baz", "foo", "http://foo.com/foo"]=>"bazbaz"},
51
+ {}]],
52
+ '<foo:foofoo foo:bar="barbar" xmlns:foo="http://foo.com/foo"><foo:barbar foo:baz="bazbaz"/></foo:foofoo>',
53
+ [["foofoo", "foo"], {["bar", "foo", "http://foo.com/foo"]=>"barbar"},
54
+ [["barbar", "foo"], {["baz", "foo"]=>"bazbaz"}]])
55
+ end
56
+
57
+ it "should call the element/text functions in a mixed document in document order" do
58
+ check_traverse([[:element, :_,
59
+ ["foofoo", "foo", "http://foo.com/foo"],
60
+ {["bar", "foo", "http://foo.com/foo"]=>"barbar"},
61
+ {"foo"=>"http://foo.com/foo"}],
62
+ [:element, :_,
63
+ ["barbar", "foo", "http://foo.com/foo"],
64
+ {["baz", "foo", "http://foo.com/foo"]=>"bazbaz"},
65
+ {}],
66
+ [:text, :_, "sometext"],
67
+ [:element, :_, "boo", {"hoo"=>"hoohoo"}, {"zoo"=>"http://zoo.com/zoo"}],
68
+ [:element, :_, ["bozo", "zoo", "http://zoo.com/zoo"], {}, {}]
69
+ ],
70
+ '<foo:foofoo foo:bar="barbar" xmlns:foo="http://foo.com/foo"><foo:barbar foo:baz="bazbaz"/>sometext<boo hoo="hoohoo" xmlns:zoo="http://zoo.com/zoo"><zoo:bozo/></boo></foo:foofoo>',
71
+ [["foofoo", "foo"], {["bar", "foo", "http://foo.com/foo"]=>"barbar"},
72
+ [["barbar", "foo"], {["baz", "foo"]=>"bazbaz"}],
73
+ "sometext",
74
+ ["boo", {"hoo"=>"hoohoo", "xmlns:zoo"=>"http://zoo.com/zoo"},
75
+ ["zoo:bozo"]]])
76
+ end
77
+
78
+ it "should work the same with compact sexp representations" do
79
+ check_traverse([[:element, :_,
80
+ ["foofoo", "foo", "http://foo.com/foo"],
81
+ {["bar", "foo", "http://foo.com/foo"]=>"barbar"},
82
+ {"foo"=>"http://foo.com/foo"}],
83
+ [:element, :_,
84
+ ["barbar", "foo", "http://foo.com/foo"],
85
+ {["baz", "foo", "http://foo.com/foo"]=>"bazbaz"},
86
+ {}],
87
+ [:text, :_, "sometext"],
88
+ [:element, :_, "boo", {"hoo"=>"hoohoo"}, {"zoo"=>"http://zoo.com/zoo"}],
89
+ [:element, :_, ["bozo", "zoo", "http://zoo.com/zoo"], {}, {}]
90
+ ],
91
+ '<foo:foofoo foo:bar="barbar" xmlns:foo="http://foo.com/foo"><foo:barbar foo:baz="bazbaz"/>sometext<boo hoo="hoohoo" xmlns:zoo="http://zoo.com/zoo"><zoo:bozo/></boo></foo:foofoo>',
92
+ ["foo:foofoo", {"foo:bar"=>"barbar", "xmlns:foo"=>"http://foo.com/foo"},
93
+ ["foo:barbar", {"foo:baz"=>"bazbaz"}],
94
+ "sometext",
95
+ ["boo", {"hoo"=>"hoohoo", "xmlns:zoo"=>"http://zoo.com/zoo"},
96
+ ["zoo:bozo"]]])
97
+ end
98
+
99
+ end
100
+
4
101
  describe Visitor::WriteXmlVisitor do
5
102
  it "should write single element xml" do
6
103
  Sexp::traverse(["item"], Visitor::WriteXmlVisitor.new).to_s.should == "<item></item>"
@@ -29,17 +126,6 @@ module Rsxml
29
126
  end
30
127
 
31
128
  describe Visitor::BuildRsxmlVisitor do
32
- describe "strip_namespace_decls" do
33
- it "should remove default and prefixed namespace decls from exploded attributes" do
34
- Visitor::BuildRsxmlVisitor.new.strip_namespace_decls({"xmlns"=>"http://default.com/default",
35
- ["foo", "xmlns"]=>"http://foo.com/foo",
36
- ["bar", "foo", "http://foo.com/foo"]=>"barbar",
37
- "baz"=>"bazbaz"}).should ==
38
- {["bar", "foo", "http://foo.com/foo"]=>"barbar",
39
- "baz"=>"bazbaz"}
40
- end
41
- end
42
-
43
129
  describe "compact_qname" do
44
130
  it "should compact exploded qnames" do
45
131
  Visitor::BuildRsxmlVisitor.new.compact_qname(["foo", "bar", "http://bar.com/bar"]).should ==
@@ -56,8 +142,8 @@ module Rsxml
56
142
  end
57
143
  end
58
144
 
59
- describe "tag" do
60
- it "should append the rsxml tag to the cursor element and yield" do
145
+ describe "element" do
146
+ it "should append the rsxml element to the cursor element and yield" do
61
147
  end
62
148
  end
63
149
 
@@ -77,6 +163,40 @@ module Rsxml
77
163
  rsxml = Rsxml::Xml.traverse(root, Visitor::BuildRsxmlVisitor.new).sexp
78
164
  rsxml.should == ["foo", {"bar"=>"10", "baz"=>"20"}]
79
165
  end
166
+
167
+ describe "element_transformer" do
168
+ def capitalize_local_part(qname)
169
+ local_part, prefix, uri = qname
170
+ if uri
171
+ [local_part.capitalize, prefix, uri]
172
+ else
173
+ local_part.capitalize
174
+ end
175
+ end
176
+
177
+ it "should call a element_transformer block to transform element_names and attrs" do
178
+ root = Nokogiri::XML('<foo bar="10" baz="20"></foo>').children.first
179
+ rsxml = Rsxml::Xml.traverse(root, Visitor::BuildRsxmlVisitor.new do |context,element_name,attrs|
180
+ celement_name = capitalize_local_part(element_name)
181
+ cattrs = Hash[attrs.map{|n,v| [capitalize_local_part(n), v]}]
182
+ [celement_name, cattrs]
183
+ end ).sexp
184
+
185
+ rsxml.should ==
186
+ ["Foo", {"Bar"=>"10", "Baz"=>"20"}]
187
+
188
+ root = Nokogiri::XML('<a:foo bar="10" baz="20" xmlns:a="http://a.com/a"></a:foo>').children.first
189
+ rsxml = Rsxml::Xml.traverse(root, Visitor::BuildRsxmlVisitor.new(:style=>:exploded) do |context,element_name,attrs|
190
+ celement_name = capitalize_local_part(element_name)
191
+ cattrs = Hash[attrs.map{|n,v| [capitalize_local_part(n), v]}]
192
+ [celement_name, cattrs]
193
+ end ).sexp
194
+
195
+ rsxml.should ==
196
+ [["Foo", "a", "http://a.com/a"], {"Bar"=>"10", "Baz"=>"20"}]
197
+
198
+ end
199
+ end
80
200
  end
81
201
 
82
202
  end
@@ -7,6 +7,7 @@ module Rsxml
7
7
 
8
8
  it "should wrap a fragment in a document with namespace declarations if there are ns prefixes" do
9
9
  end
10
+
10
11
  end
11
12
 
12
13
  describe "unwrap_fragment" do
@@ -18,6 +19,14 @@ module Rsxml
18
19
 
19
20
  it "should throw an exception if it unwraps and there is more than one child" do
20
21
  end
22
+
23
+ it "should return a node with no namespace definitions" do
24
+ root = Xml.unwrap_fragment(Nokogiri::XML(Xml.wrap_fragment("<foo:bar/>", {"foo"=>"http://foo.com/foo"})).children.first)
25
+ root.namespace_definitions.should == []
26
+ root.name.should == "bar"
27
+ root.namespace.prefix.should == "foo"
28
+ root.namespace.href.should == "http://foo.com/foo"
29
+ end
21
30
  end
22
31
 
23
32
  describe "explode_node" do
@@ -28,7 +37,7 @@ module Rsxml
28
37
  Xml.explode_node(node).should == "foo"
29
38
  end
30
39
 
31
- it "should return the [local_name, prefix, uri] triple if there is a namespace" do
40
+ it "should return the [local_part, prefix, uri] triple if there is a namespace" do
32
41
  node = Object.new
33
42
  stub(node).name{"foo"}
34
43
  namespace = Object.new
@@ -38,7 +47,7 @@ module Rsxml
38
47
  Xml.explode_node(node).should == ["foo", "bar", "http://bar.com/bar"]
39
48
  end
40
49
 
41
- it "should return a [local_name, "", uri] triple if there is a default namespace" do
50
+ it "should return a [local_part, "", uri] triple if there is a default namespace" do
42
51
  node = Object.new
43
52
  stub(node).name{"foo"}
44
53
  namespace = Object.new
@@ -56,13 +65,27 @@ module Rsxml
56
65
  eelement, eattrs = Rsxml::Xml.explode_element(root)
57
66
  eelement.should == ["bar", "foo", "http://foo.com/foo"]
58
67
  eattrs.should == {"a"=>"aa",
59
- ["b", "foo", "http://foo.com/foo"]=>"bb",
60
- ["foo", "xmlns"]=>"http://foo.com/foo",
61
- "xmlns"=>"http://default.com/default"}
68
+ ["b", "foo", "http://foo.com/foo"]=>"bb"}
69
+ end
70
+ end
71
+
72
+ describe "namespace_bindings_from_defs" do
73
+ it "should produce a hash of {prefix=>uri} bindings from a list of Nokogiri::XML::Namespace" do
74
+ default_def = Object.new
75
+ mock(default_def).prefix{nil}
76
+ mock(default_def).href{"http://default.com/default"}
77
+
78
+ foo_def = Object.new
79
+ mock(foo_def).prefix{"foo"}
80
+ mock(foo_def).href{"http://foo.com/foo"}
81
+
82
+ Xml.namespace_bindings_from_defs([default_def, foo_def]).should ==
83
+ {"foo"=>"http://foo.com/foo", ""=>"http://default.com/default"}
62
84
  end
63
85
  end
64
86
 
65
87
  describe "traverse" do
88
+ # tests moved to visitor_spec... done in parallel with Sexp traverse tests
66
89
  end
67
90
 
68
91
  end
data/spec/rsxml_spec.rb CHANGED
@@ -183,7 +183,7 @@ describe Rsxml do
183
183
  rsxml.should == org_no_ns
184
184
  end
185
185
 
186
- it "should return exploded namespaces if :compact is false when parsing a fragment" do
186
+ it "should return exploded namespaces if :style=>:exploded when parsing a fragment" do
187
187
  xml = '<foofoo foo:bar="1" foo:baz="baz"></foofoo>'
188
188
  rsxml = Rsxml.to_rsxml(xml, :ns=>{:foo=>"http://foo.com/foo", ""=>"http://baz.com/baz"}, :style=>:exploded)
189
189
 
data/spec/spec_helper.rb CHANGED
@@ -6,6 +6,7 @@ require 'spec/autorun'
6
6
  require 'rr'
7
7
  require 'nokogiri'
8
8
  require 'rsxml'
9
+ require 'rsxml/mock_visitor'
9
10
 
10
11
  Spec::Runner.configure do |config|
11
12
  config.mock_with RR::Adapters::Rspec
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsxml
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 1
10
- version: 0.2.1
8
+ - 3
9
+ - 0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Trampoline Systems Ltd
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-05-11 00:00:00 +01:00
18
+ date: 2011-05-27 00:00:00 +01:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -118,6 +118,7 @@ files:
118
118
  - lib/rsxml/util.rb
119
119
  - lib/rsxml/visitor.rb
120
120
  - lib/rsxml/xml.rb
121
+ - spec/rsxml/mock_visitor.rb
121
122
  - spec/rsxml/namespace_spec.rb
122
123
  - spec/rsxml/sexp_spec.rb
123
124
  - spec/rsxml/util_spec.rb
@@ -160,6 +161,7 @@ signing_key:
160
161
  specification_version: 3
161
162
  summary: an s-expression representation of XML documents in Ruby
162
163
  test_files:
164
+ - spec/rsxml/mock_visitor.rb
163
165
  - spec/rsxml/namespace_spec.rb
164
166
  - spec/rsxml/sexp_spec.rb
165
167
  - spec/rsxml/util_spec.rb