Update clbanning/mxj vendor library to fix dependency issue.

This commit is contained in:
Dan Willhite
2016-03-11 12:59:16 -08:00
parent b58ce977fa
commit 11806c4709
29 changed files with 3320 additions and 245 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
https://github.com/clbanning/mxj
87ef56c6f157ac80caf824a9779dddc6f69c6580
49e0b4242cb3c9d0fcd8bef7b8d9ecfe13fadba7
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2012-2015 Charles Banning <clbanning@gmail.com>. All rights reserved.
Copyright (c) 2012-2016 Charles Banning <clbanning@gmail.com>. All rights reserved.
The MIT License (MIT)
+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us" updated="2009-10-04T01:35:58+00:00"><title>Code Review - My issues</title><link href="http://codereview.appspot.com/" rel="alternate"></link><link href="http://codereview.appspot.com/rss/mine/rsc" rel="self"></link><id>http://codereview.appspot.com/</id><author><name>rietveld&lt;&gt;</name></author><entry><title>rietveld: an attempt at pubsubhubbub
</title><link href="http://codereview.appspot.com/126085" rel="alternate"></link><updated>2009-10-04T01:35:58+00:00</updated><author><name>email-address-removed</name></author><id>urn:md5:134d9179c41f806be79b3a5f7877d19a</id><summary type="html">
An attempt at adding pubsubhubbub support to Rietveld.
http://code.google.com/p/pubsubhubbub
http://code.google.com/p/rietveld/issues/detail?id=155
The server side of the protocol is trivial:
1. add a &amp;lt;link rel=&amp;quot;hub&amp;quot; href=&amp;quot;hub-server&amp;quot;&amp;gt; tag to all
feeds that will be pubsubhubbubbed.
2. every time one of those feeds changes, tell the hub
with a simple POST request.
I have tested this by adding debug prints to a local hub
server and checking that the server got the right publish
requests.
I can&amp;#39;t quite get the server to work, but I think the bug
is not in my code. I think that the server expects to be
able to grab the feed and see the feed&amp;#39;s actual URL in
the link rel=&amp;quot;self&amp;quot;, but the default value for that drops
the :port from the URL, and I cannot for the life of me
figure out how to get the Atom generator deep inside
django not to do that, or even where it is doing that,
or even what code is running to generate the Atom feed.
(I thought I knew but I added some assert False statements
and it kept running!)
Ignoring that particular problem, I would appreciate
feedback on the right way to get the two values at
the top of feeds.py marked NOTE(rsc).
</summary></entry><entry><title>rietveld: correct tab handling
</title><link href="http://codereview.appspot.com/124106" rel="alternate"></link><updated>2009-10-03T23:02:17+00:00</updated><author><name>email-address-removed</name></author><id>urn:md5:0a2a4f19bb815101f0ba2904aed7c35a</id><summary type="html">
This fixes the buggy tab rendering that can be seen at
http://codereview.appspot.com/116075/diff/1/2
The fundamental problem was that the tab code was
not being told what column the text began in, so it
didn&amp;#39;t know where to put the tab stops. Another problem
was that some of the code assumed that string byte
offsets were the same as column offsets, which is only
true if there are no tabs.
In the process of fixing this, I cleaned up the arguments
to Fold and ExpandTabs and renamed them Break and
_ExpandTabs so that I could be sure that I found all the
call sites. I also wanted to verify that ExpandTabs was
not being used from outside intra_region_diff.py.
</summary></entry></feed> `
+68
View File
@@ -0,0 +1,68 @@
// trying to recreate a panic
package mxj
import (
"bytes"
"fmt"
"testing"
)
var baddata = []byte(`
something strange
<Allitems>
<Item>
</Item>
<Item>
<link>http://www.something.com</link>
<description>Some description goes here.</description>
</Item>
</Allitems>
`)
func TestBadXml(t *testing.T) {
fmt.Println("\n---------------- badxml_test.go\n")
fmt.Println("TestBadXml ...")
m, err := NewMapXml(baddata)
if err != nil {
t.Fatalf("err: didn't find xml.StartElement")
}
fmt.Printf("m: %v\n", m)
j, _ := m.Xml()
fmt.Println("m:", string(j))
}
func TestBadXmlSeq(t *testing.T) {
fmt.Println("TestBadXmlSeq ...")
m, err := NewMapXmlSeq(baddata)
if err != nil {
t.Fatalf("err: didn't find xmlStartElement")
}
fmt.Printf("m: %v\n", m)
j, _ := m.XmlSeq()
fmt.Println("m:", string(j))
}
func TestBadXmlReader(t *testing.T) {
fmt.Println("TestBadXmlReader ...")
r := bytes.NewReader(baddata)
m, err := NewMapXmlReader(r)
if err != nil {
t.Fatalf("err: didn't find xml.StartElement")
}
fmt.Printf("m: %v\n", m)
j, _ := m.Xml()
fmt.Println("m:", string(j))
}
func TestBadXmlSeqReader(t *testing.T) {
fmt.Println("TestBadXmlSeqReader ...")
r := bytes.NewReader(baddata)
m, err := NewMapXmlSeqReader(r)
if err != nil {
t.Fatalf("err: didn't find xmlStartElement")
}
fmt.Printf("m: %v\n", m)
j, _ := m.XmlSeq()
fmt.Println("m:", string(j))
}
+88
View File
@@ -0,0 +1,88 @@
// bomxml.go - test handling Byte-Order-Mark headers
package mxj
import (
"bytes"
"fmt"
"io"
"testing"
)
// Check for Byte-Order-Mark header.
var boms = [][]byte{
{'\xef', '\xbb', '\xbf'},
{'\xfe', '\xff'},
{'\xff', '\xfe'},
{'\x00', '\x00', '\xfe', '\xff'},
{'\xff', '\xfe', '\x00', '\x00'},
}
func TestBom(t *testing.T) {
fmt.Println("\n--------------- bom_test.go \n")
fmt.Println("TestBom ...")
// use just UTF-8 BOM ... no alternative CharSetReader
if _, err := NewMapXml(boms[0]); err != io.EOF {
t.Fatalf("NewMapXml err; %v\n", err)
}
if _, err := NewMapXmlSeq(boms[0]); err != io.EOF {
t.Fatalf("NewMapXmlSeq err: %v\n", err)
}
}
var bomdata = append(boms[0], []byte(`<Allitems>
<Item>
</Item>
<Item>
<link>http://www.something.com</link>
<description>Some description goes here.</description>
</Item>
</Allitems>`)...)
func TestBomData(t *testing.T) {
fmt.Println("TestBomData ...")
m, err := NewMapXml(bomdata)
if err != nil {
t.Fatalf("err: didn't find xml.StartElement")
}
fmt.Printf("m: %v\n", m)
j, _ := m.Xml()
fmt.Println("m:", string(j))
}
func TestBomDataSeq(t *testing.T) {
fmt.Println("TestBomDataSeq ...")
m, err := NewMapXmlSeq(bomdata)
if err != nil {
t.Fatalf("err: didn't find xml.StartElement")
}
fmt.Printf("m: %v\n", m)
j, _ := m.XmlSeq()
fmt.Println("m:", string(j))
}
func TestBomDataReader(t *testing.T) {
fmt.Println("TestBomDataReader ...")
r := bytes.NewReader(bomdata)
m, err := NewMapXmlReader(r)
if err != nil {
t.Fatalf("err: didn't find xml.StartElement")
}
fmt.Printf("m: %v\n", m)
j, _ := m.Xml()
fmt.Println("m:", string(j))
}
func TestBomDataSeqReader(t *testing.T) {
fmt.Println("TestBomDataSeqReader ...")
r := bytes.NewReader(bomdata)
m, err := NewMapXmlSeqReader(r)
if err != nil {
t.Fatalf("err: didn't find xml.StartElement")
}
fmt.Printf("m: %v\n", m)
j, _ := m.XmlSeq()
fmt.Println("m:", string(j))
}
+36 -10
View File
@@ -1,5 +1,5 @@
// mxj - A collection of map[string]interface{} and associated XML and JSON utilities.
// Copyright 2012-2014 Charles Banning. All rights reserved.
// Copyright 2012-2015 Charles Banning. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file
@@ -11,6 +11,7 @@ mxj supplants the legacy x2j and j2x packages. If you want the old syntax, use m
Note: this library was designed for processing ad hoc anonymous messages. Bulk processing large data sets may be much more efficiently performed using the encoding/xml or encoding/json packages from Go's standard library directly.
Note:
2015-12-02: NewMapXmlSeq() with mv.XmlSeq() & co. will try to preserve structure of XML doc when re-encoding.
2014-08-02: AnyXml() and AnyXmlIndent() will try to marshal arbitrary values to XML.
SUMMARY
@@ -63,22 +64,47 @@ SUMMARY
A new Map with whatever keys are desired can be created from the current Map and then encoded in XML
or JSON. (Note: keys can use dot-notation. 'oldKey' can also use wildcards and indexed arrays.)
newMap := m.NewMap("oldKey_1:newKey_1", "oldKey_2:newKey_2", ..., "oldKey_N:newKey_N")
newXml := newMap.Xml() // for example
newJson := newMap.Json() // ditto
newMap, err := m.NewMap("oldKey_1:newKey_1", "oldKey_2:newKey_2", ..., "oldKey_N:newKey_N")
newXml, err := newMap.Xml() // for example
newJson, err := newMap.Json() // ditto
XML PARSING CONVENTIONS
- Attributes are parsed to map[string]interface{} values by prefixing a hyphen, '-',
to the attribute label. (PrependAttrWithHyphen(false) will override this.)
Using NewXml()
- Attributes are parsed to `map[string]interface{}` values by prefixing a hyphen, `-`,
to the attribute label. (Unless overridden by `PrependAttrWithHyphen(false)`.)
- If the element is a simple element and has attributes, the element value
is given the key '#text' for its map[string]interface{} representation.
is given the key `#text` for its `map[string]interface{}` representation. (See
the 'atomFeedString.xml' test data, below.)
- XML comments, directives, and process instructions are ignored.
- If CoerceKeysToLower() has been called, then the resultant keys will be lower case.
Using NewXmlSeq()
- Attributes are parsed to `map["#attr"]map[<attr_label>]map[string]interface{}`values
where the `<attr_label>` value has "#text" and "#seq" keys - the "#text" key holds the
value for `<attr_label>`.
- All elements, except for the root, have a "#seq" key.
- Comments, directives, and process instructions are unmarshalled into the Map using the
keys "#comment", "#directive", and "#procinst", respectively. (See documentation for more
specifics.)
Both
- By default, "Nan", "Inf", and "-Inf" values are not cast to float64. If you want them
to be cast, set a flag to cast them using CastNanInf(true).
XML ENCODING CONVENTIONS
- 'nil' Map values, which may represent 'null' JSON values, are encoded as "<tag/>".
NOTE: the operation is not symmetric as "<tag/>" elements are decoded as 'tag:""' Map values,
which, then, encode in JSON as '"tag":""' values..
NOTE: the operation is not symmetric as "<tag/>" elements are decoded as 'tag:""' Map values,
which, then, encode in JSON as '"tag":""' values..
- ALSO: there is no guarantee that the encoded XML doc will be the same as the decoded one. (Go
randomizes the walk through map[string]interface{} values.) If you plan to re-encode the
Map value to XML and want the same sequencing of elements look at NewMapXmlSeq() and
m.XmlSeq() - these try to preserve the element sequencing but with added complexity when
working with the Map representation.
*/
package mxj
+2
View File
@@ -1,3 +1,5 @@
// +test OMIT
// note - "// Output:" is a key for "go test" to match function ouput with the lines that follow.
// It is also use by "godoc" to build the Output block of the function / method documentation.
// To skip processing Example* functions, use: go test -run "Test*"
+20 -2
View File
@@ -41,6 +41,7 @@ import (
"github.com/clbanning/mxj"
"log"
"os"
"sort"
"time"
)
@@ -123,14 +124,14 @@ func main() {
// no guarantee that range on map will follow any sequence
lv := len(valueEntry)
type ev [2]string
list := make([]ev, lv)
list := make([][2]string, lv)
var i int
for k, v := range valueEntry {
list[i][0] = k
list[i][1] = v.(string)
i++
}
sort.Sort(mylist(list))
// extract keys as column header on first pass
if !gotKeys {
@@ -174,3 +175,20 @@ func main() {
mf.Close()
}
}
type mylist [][2]string
func (m mylist) Len() int {
return len(m)
}
func (m mylist) Less(i, j int) bool {
if m[i][0] > m[j][0] {
return false
}
return true
}
func (m mylist) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
+20 -2
View File
@@ -42,6 +42,7 @@ import (
"github.com/clbanning/mxj"
"log"
"os"
"sort"
"time"
)
@@ -120,14 +121,14 @@ func main() {
// no guarantee that range on map will follow any sequence
lv := len(valueEntry)
type ev [2]string
list := make([]ev, lv)
list := make([][2]string, lv)
var i int
for k, v := range valueEntry {
list[i][0] = k
list[i][1] = v.(string)
i++
}
sort.Sort(mylist(list))
// extract keys as column header on first pass
if !gotKeys {
@@ -171,3 +172,20 @@ func main() {
mf.Close()
}
}
type mylist [][2]string
func (m mylist) Len() int {
return len(m)
}
func (m mylist) Less(i, j int) bool {
if m[i][0] > m[j][0] {
return false
}
return true
}
func (m mylist) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
+21 -3
View File
@@ -43,6 +43,7 @@ import (
"github.com/clbanning/mxj"
"log"
"os"
"sort"
"time"
)
@@ -85,7 +86,7 @@ func main() {
log.Fatal("merr:", merr.Error())
}
fmt.Println(time.Now().String(), "... XML Unmarshaled - len:", len(m))
fmt.Println("raw XML buffer size (should be same as File size):", len(*raw))
fmt.Println("raw XML buffer size (should be same as File size):", len(raw))
// Get just the key values of interest.
// Could also use m.ValuesForKey("Metric"),
@@ -127,14 +128,14 @@ func main() {
// no guarantee that range on map will follow any sequence
lv := len(valueEntry)
type ev [2]string
list := make([]ev, lv)
list := make([][2]string, lv)
var i int
for k, v := range valueEntry {
list[i][0] = k
list[i][1] = v.(string)
i++
}
sort.Sort(mylist(list))
// extract keys as column header on first pass
if !gotKeys {
@@ -178,3 +179,20 @@ func main() {
mf.Close()
}
}
type mylist [][2]string
func (m mylist) Len() int {
return len(m)
}
func (m mylist) Less(i, j int) bool {
if m[i][0] > m[j][0] {
return false
}
return true
}
func (m mylist) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
+21 -3
View File
@@ -44,6 +44,7 @@ import (
"github.com/clbanning/mxj"
"log"
"os"
"sort"
"time"
)
@@ -84,7 +85,7 @@ func main() {
log.Fatal("merr:", merr.Error())
}
fmt.Println(time.Now().String(), "... XML Unmarshaled - len:", len(m))
fmt.Println("raw XML buffer size (should be same as File size):", len(*raw))
fmt.Println("raw XML buffer size (should be same as File size):", len(raw))
// Get just the key values of interest.
// Could also use m.ValuesForKey("Metric"),
@@ -126,14 +127,14 @@ func main() {
// no guarantee that range on map will follow any sequence
lv := len(valueEntry)
type ev [2]string
list := make([]ev, lv)
list := make([][2]string, lv)
var i int
for k, v := range valueEntry {
list[i][0] = k
list[i][1] = v.(string)
i++
}
sort.Sort(mylist(list))
// extract keys as column header on first pass
if !gotKeys {
@@ -177,3 +178,20 @@ func main() {
mf.Close()
}
}
type mylist [][2]string
func (m mylist) Len() int {
return len(m)
}
func (m mylist) Less(i, j int) bool {
if m[i][0] > m[j][0] {
return false
}
return true
}
func (m mylist) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
+1 -1
View File
@@ -55,7 +55,7 @@ func main() {
} else if merr == io.EOF {
break
}
fmt.Println("\nMessage to parse:", string(*raw))
fmt.Println("\nMessage to parse:", string(raw))
fmt.Println("Map value for XML message:", m.StringIndent())
// get the values for "netid" or "idnet" key using path == "data.*"
+108
View File
@@ -0,0 +1,108 @@
/* gonuts10.go - https://groups.google.com/forum/?fromgroups#!topic/golang-nuts/tf4aDQ1Hn_c
change:
<author>
<first-name effect_range="1999-2011">Sam</first-name>
<first-name effect_range="2012-">Kevin</first-name>
<last-name>Smith</last-name>
<full-name></full-name>
</author>
to:
<author>
<first-name effect_range="1999-2011">Sam</first-name>
<first-name effect_range="2012-">Kevin</first-name>
<last-name>Smith</last-name>
<full-name>Kevin Smith</full-name>
</author>
NOTE: sequence of elements NOT guaranteed due to use of map[string]interface{}.
Here we build the "full-name" element value from other values in the doc by selecting the
"first-name" value with the latest dates.
*/
package main
import (
"fmt"
"github.com/clbanning/mxj"
)
var data = []byte(`
<author>
<first-name effect_range="1999-2011">Sam</first-name>
<first-name effect_range="2012-">Kevin</first-name>
<last-name>Smith</last-name>
<full-name></full-name>
</author>
`)
func main() {
m, err := mxj.NewMapXml(data)
if err != nil {
fmt.Println("NewMapXml err:", err)
return
}
// vals, err := m.ValuesForPath("author.first-name") // full-path option
vals, err := m.ValuesForKey("first-name") // key-only alternatively
if err != nil {
fmt.Println("ValuesForPath err:", err)
return
} else if len(vals) == 0 {
fmt.Println("no first-name vals")
return
}
var fname, date string
for _, v := range vals {
vm, ok := v.(map[string]interface{})
if !ok {
fmt.Println("assertion failed")
return
}
fn, ok := vm["#text"].(string)
if !ok {
fmt.Println("no #text tag")
return
}
dt, ok := vm["-effect_range"].(string)
if !ok {
fmt.Println("no -effect_range attr")
return
}
if dt > date {
date = dt
fname = fn
}
}
/*
// alternatively:
//(however, this requires knowing what latest "effect_range" attribute value is)
vals, err := m.ValuesForKey("first-name", "-effect_range:2012-")
if len(vals) == 0 {
fmt.Println("no #text vals")
return
}
fname := vals[0].(map[string]interface{})["#text"].(string)
*/
// vals, err = m.ValuesForKey("last-name") // key-only option
vals, err = m.ValuesForPath("author.last-name") // full-path option
if err != nil {
fmt.Println("ValuesForPath err:", err)
return
} else if len(vals) == 0 {
fmt.Println("no last-name vals")
return
}
lname := vals[0].(string)
if err = m.SetValueForPath(fname+" "+lname, "author.full-name"); err != nil {
fmt.Println("SetValueForPath err:", err)
return
}
b, err := m.XmlIndent("", " ")
if err != nil {
fmt.Println("XmlIndent err:", err)
return
}
fmt.Println(string(b))
}
+106
View File
@@ -0,0 +1,106 @@
/* gonuts10.go - https://groups.google.com/forum/?fromgroups#!topic/golang-nuts/tf4aDQ1Hn_c
change:
<author>
<first-name effect_range="1999-2011">Sam</first-name>
<first-name effect_range="2012-">Kevin</first-name>
<last-name>Smith</last-name>
<full-name></full-name>
</author>
to:
<author>
<first-name effect_range="1999-2011">Sam</first-name>
<first-name effect_range="2012-">Kevin</first-name>
<last-name>Smith</last-name>
<full-name>Kevin Smith</full-name>
</author>
NOTE: use NewMapXmlSeq() and mv.XmlSeqIndent() to preserve structure.
Here we build the "full-name" element value from other values in the doc by selecting the
"first-name" value with the latest dates.
*/
package main
import (
"fmt"
"github.com/clbanning/mxj"
"strings"
)
var data = []byte(`
<author>
<first-name effect_range="1999-2011">Sam</first-name>
<first-name effect_range="2012-">Kevin</first-name>
<last-name>Smith</last-name>
<full-name></full-name>
</author>
`)
func main() {
fmt.Println(string(data))
m, err := mxj.NewMapXmlSeq(data)
if err != nil {
fmt.Println("NewMapXml err:", err)
return
}
vals, err := m.ValuesForPath("author.first-name") // full-path option
if err != nil {
fmt.Println("ValuesForPath err:", err)
return
} else if len(vals) == 0 {
fmt.Println("no first-name vals")
return
}
var fname, date string
var index int
for _, v := range vals {
vm, ok := v.(map[string]interface{})
if !ok {
fmt.Println("assertion failed")
return
}
fn, ok := vm["#text"].(string)
if !ok {
fmt.Println("no #text tag")
return
}
// extract the associated date
dt, _ := mxj.Map(vm).ValueForPathString("#attr.effect_range.#text")
if dt == "" {
fmt.Println("no effect_range attr")
return
}
dts := strings.Split(dt, "-")
if len(dts) > 1 && dts[len(dts)-1] == "" {
index = len(dts) - 2
} else if len(dts) > 0 {
index = len(dts) - 1
}
if len(dts) > 0 && dts[index] > date {
date = dts[index]
fname = fn
}
}
vals, err = m.ValuesForPath("author.last-name.#text") // full-path option
if err != nil {
fmt.Println("ValuesForPath err:", err)
return
} else if len(vals) == 0 {
fmt.Println("no last-name vals")
return
}
lname := vals[0].(string)
if err = m.SetValueForPath(fname+" "+lname, "author.full-name.#text"); err != nil {
fmt.Println("SetValueForPath err:", err)
return
}
b, err := m.XmlSeqIndent("", " ")
if err != nil {
fmt.Println("XmlIndent err:", err)
return
}
fmt.Println(string(b))
}
+510
View File
@@ -0,0 +1,510 @@
/* gonuts10seqB.go - https://groups.google.com/forum/?fromgroups#!topic/golang-nuts/tf4aDQ1Hn_c
Objective: assign Comment.CommentText attribute value to Request.ReportingName attribute that immediately follows Comment.
NOTE: use NewMapXmlSeq() and mv.XmlSeqIndent() to preserve structure.
See data value at EOF - from: https://gist.github.com/suntong/e4dcdc6c85dcf769eec4
*/
package main
import (
"bytes"
"fmt"
"github.com/clbanning/mxj"
"io"
)
func main() {
// fmt.Println(string(data))
rdr := bytes.NewReader(data)
var m mxj.Map
var err error
// We read processing docs sequentially.
// Un-rooted ProcInst or Comments are processed AND just reencoded. (XmlSeqIndent() knows how, now.)
for m, err = mxj.NewMapXmlSeqReader(rdr); m != nil || err != io.EOF; m, err = mxj.NewMapXmlSeqReader(rdr) {
if err != nil {
if err != mxj.NO_ROOT {
fmt.Println("NewMapXmlSeq err:", err)
fmt.Println("m:", m)
} else if m != nil {
x, _ := m.XmlSeqIndent("", " ")
fmt.Println(string(x))
}
continue
}
// fmt.Println(m.StringIndent())
// We have Two different arrays of Items in the XML doc, one nested in the other.
if err = copyCmts(m, "WebTest.Items"); err != nil {
fmt.Println("err:", err)
}
if err = copyCmts(m, "WebTest.Items.TransactionTimer.Items"); err != nil {
fmt.Println("err:", err)
}
// re-encode the map with the Items.Comment[#seq==n].#attr.CommentText
// values copied to the Items.Request[#seq==n+1].#attr.ReportingName elements.
b, err := m.XmlSeqIndent("", " ")
if err != nil {
fmt.Println("XmlIndent err:", err)
return
}
fmt.Println(string(b))
}
}
// Uncomment the print statements to details of the process here.
func copyCmts(m mxj.Map, path string) error {
// get the array of Items entries for the 'path'
vals, err := m.ValuesForPath(path)
if err != nil {
return fmt.Errorf("ValuesForPath err: %s", err.Error())
} else if len(vals) == 0 {
return fmt.Errorf("no vals for path: %s", path)
}
// process each Items entry
for _, v := range vals {
vm, ok := v.(map[string]interface{})
if !ok {
return fmt.Errorf("assertion failed")
}
// get the Comment list
c, ok := vm["Comment"]
if !ok { // --> no Items.Comment elements
continue
}
// Don't assume that Comment is an array.
// There may be just one value, in which case it will decode as map[string]interface{}.
switch c.(type) {
case map[string]interface{}:
c = []interface{}{c}
}
cmt := c.([]interface{})
// get the Request list
r, ok := vm["Request"]
if !ok { // --> no Items.Request elements
continue
}
// Don't assume the Request is an array.
// There may be just one value, in which case it will decode as map[string]interface{}.
switch r.(type) {
case map[string]interface{}:
r = []interface{}{r}
}
req := r.([]interface{})
// fmt.Println("Comment:", cmt)
// fmt.Println("Request:", req)
// Comment elements with #seq==n are followed by Request element with #seq==n+1.
// For each Comment.#seq==n extract the CommentText attribute value and use it to
// set the ReportingName attribute value in Request.#seq==n+1.
for _, v := range cmt {
vmap := v.(map[string]interface{})
seq := vmap["#seq"].(int) // type is int
// extract CommentText attr from "#attr"
acmt, _ := mxj.Map(vmap).ValueForPath("#attr.CommentText.#text")
if acmt == "" {
fmt.Println("no CommentText value in Comment attributes")
}
// fmt.Println(seq, acmt)
// find the request with the #seq==seq+1 value
var r map[string]interface{}
for _, vv := range req {
rt := vv.(map[string]interface{})
if rt["#seq"].(int) == seq+1 {
r = rt
break
}
}
if r == nil { // no Request with #seq==seq+1
continue
}
if err := mxj.Map(r).SetValueForPath(acmt, "#attr.ReportingName.#text"); err != nil {
fmt.Println("SetValueForPath err:", err)
break
}
}
}
return nil
}
var data = []byte(`
<?xml version="1.0" encoding="utf-8"?>
<WebTest Name="FirstAnonymousVisit" Id="ac766d08-f940-4b0a-b8f8-80675978894e" Owner="" Priority="0" Enabled="True" CssProjectStructure="" CssIteration="" Timeout="0" WorkItemIds="" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010" Description="" CredentialUserName="" CredentialPassword="" PreAuthenticate="True" Proxy="" StopOnError="False" RecordedResultFile="">
<Items>
<Comment CommentText="Visit Homepage and ensure new page setup is created" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Dropthings.Test.Rules.CookieValidationRule, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Check Cookie From Response" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="StopOnError" Value="False" />
<RuleParameter Name="CookieValueToMatch" Value="" />
<RuleParameter Name="MatchValue" Value="False" />
<RuleParameter Name="Exists" Value="True" />
<RuleParameter Name="CookieName" Value="{{Config.TestParameters.AnonCookieName}}" />
<RuleParameter Name="IsPersistent" Value="True" />
<RuleParameter Name="Domain" Value="" />
<RuleParameter Name="Index" Value="0" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Dropthings.Test.Rules.CookieValidationRule, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Check Cookie From Response" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="StopOnError" Value="False" />
<RuleParameter Name="CookieValueToMatch" Value="" />
<RuleParameter Name="MatchValue" Value="False" />
<RuleParameter Name="Exists" Value="False" />
<RuleParameter Name="CookieName" Value="{{Config.TestParameters.SessionCookieName}}" />
<RuleParameter Name="IsPersistent" Value="False" />
<RuleParameter Name="Domain" Value="" />
<RuleParameter Name="Index" Value="0" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Dropthings.Test.Rules.CacheHeaderValidation, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Cache Header Validation" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="Enabled" Value="True" />
<RuleParameter Name="DifferenceThresholdSec" Value="0" />
<RuleParameter Name="CacheControlPrivate" Value="False" />
<RuleParameter Name="CacheControlPublic" Value="False" />
<RuleParameter Name="CacheControlNoCache" Value="True" />
<RuleParameter Name="ExpiresAfterSeconds" Value="0" />
<RuleParameter Name="StopOnError" Value="False" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="How to of the Day" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="Weather" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="All rights reserved" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
</Request>
<TransactionTimer Name="Show Hide Widget List">
<Items>
<Comment CommentText="Show Widget List and expect Widget List to produce BBC Word widget link" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="BBC World" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="TabControlPanel$ShowAddContentPanel" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.OnPageMenuUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
<Comment CommentText="Hide Widget List and expect the outpu does not have the BBC World Widget" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="TabControlPanel$ShowAddContentPanel" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="TabControlPanel$HideAddContentPanel" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.OnPageMenuUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
</Items>
</TransactionTimer>
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/API/Proxy.svc/ajax/GetRss?url=%22http%3A%2F%2Ffeeds.feedburner.com%2FOmarAlZabirBlog%22&amp;count=10&amp;cacheDuration=10" ThinkTime="0" Timeout="300" ParseDependentRequests="True" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="{&quot;d&quot;:[{&quot;__type&quot;:&quot;RssItem:#Dropthings.Web.Util&quot;" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
</Request>
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/API/Proxy.svc/ajax/GetUrl?url=%22http%3A%2F%2Ffeeds.feedburner.com%2FOmarAlZabirBlog%22&amp;cacheDuration=10" ThinkTime="0" Timeout="300" ParseDependentRequests="True" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="&lt;channel&gt;" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
</Request>
<TransactionTimer Name="Edit Collapse Expand Widget">
<Items>
<Comment CommentText="Click edit on first widget &quot;How to of the Day&quot; and expect URL textbox to be present with Feed Url" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleRequiredAttributeValue, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Required Attribute Value" Description="Verifies the existence of a specified HTML tag that contains an attribute with a specified value." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="TagName" Value="input" />
<RuleParameter Name="AttributeName" Value="value" />
<RuleParameter Name="MatchAttributeName" Value="" />
<RuleParameter Name="MatchAttributeValue" Value="" />
<RuleParameter Name="ExpectedValue" Value="http://www.wikihow.com/feed.rss" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="Index" Value="-1" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<ExtractionRules>
<ExtractionRule Classname="Dropthings.Test.Rules.ExtractFormElements, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" VariableName="" DisplayName="Extract Form Elements" Description="">
<RuleParameters>
<RuleParameter Name="ContextParameterName" Value="" />
</RuleParameters>
</ExtractionRule>
</ExtractionRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="{{$POSTBACK.EditWidget.1}}" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.WidgetHeaderUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
<Comment CommentText="Change the Feed Count Dropdown list to 10 and expect 10 Feed Link controls are generated" />
<Request Method="POST" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="FeedList_ctl09_FeedLink" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="{{$POSTBACK.CancelEditWidget.1}}" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.WidgetHeaderUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
<FormPostHttpBody>
<FormPostParameter Name="{{$INPUT.FeedUrl.1}}" Value="http://www.wikihow.com/feed.rss" RecordedValue="" CorrelationBinding="" UrlEncode="True" />
<FormPostParameter Name="{{$SELECT.FeedCountDropDownList.1}}" Value="10" RecordedValue="" CorrelationBinding="" UrlEncode="True" />
</FormPostHttpBody>
</Request>
<Comment CommentText="Delete the How to of the Day widget and expect it's not found from response" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="How to of the Day" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="False" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="{{$POSTBACK.CloseWidget.1}}" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.WidgetHeaderUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
</Items>
</TransactionTimer>
<TransactionTimer Name="Add New Widget">
<Items>
<Comment CommentText="Show widget list and expect Digg to be there" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="Digg" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="TabControlPanel$ShowAddContentPanel" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.OnPageMenuUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
<Comment CommentText="Add New Widget" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="Digg" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="{{$POSTBACK.AddWidget.1}}" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.OnPageMenuUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
<Comment CommentText="Delete the newly added widget" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="How to of the Day" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="False" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="{{$POSTBACK.CloseWidget.1}}" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.WidgetHeaderUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
</Items>
</TransactionTimer>
<Comment CommentText="Revisit and ensure the Digg widget exists and How to widget does not exist" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Dropthings.Test.Rules.CookieValidationRule, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Check Cookie From Response" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="StopOnError" Value="False" />
<RuleParameter Name="CookieValueToMatch" Value="" />
<RuleParameter Name="MatchValue" Value="False" />
<RuleParameter Name="Exists" Value="False" />
<RuleParameter Name="CookieName" Value="{{Config.TestParameters.AnonCookieName}}" />
<RuleParameter Name="IsPersistent" Value="True" />
<RuleParameter Name="Domain" Value="" />
<RuleParameter Name="Index" Value="0" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Dropthings.Test.Rules.CookieValidationRule, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Check Cookie From Response" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="StopOnError" Value="False" />
<RuleParameter Name="CookieValueToMatch" Value="" />
<RuleParameter Name="MatchValue" Value="False" />
<RuleParameter Name="Exists" Value="False" />
<RuleParameter Name="CookieName" Value="{{Config.TestParameters.SessionCookieName}}" />
<RuleParameter Name="IsPersistent" Value="False" />
<RuleParameter Name="Domain" Value="" />
<RuleParameter Name="Index" Value="0" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Dropthings.Test.Rules.CacheHeaderValidation, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Cache Header Validation" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="Enabled" Value="True" />
<RuleParameter Name="DifferenceThresholdSec" Value="0" />
<RuleParameter Name="CacheControlPrivate" Value="False" />
<RuleParameter Name="CacheControlPublic" Value="False" />
<RuleParameter Name="CacheControlNoCache" Value="True" />
<RuleParameter Name="ExpiresAfterSeconds" Value="0" />
<RuleParameter Name="StopOnError" Value="False" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="How to of the Day" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="False" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="Digg" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="All rights reserved" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
</Request>
<Comment CommentText="- Logout and ensure Anon Cookie is set to expire" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Logout.ashx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="False" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="302" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Dropthings.Test.Rules.CookieSetToExpire, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Ensure Cookie Set to Expire" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="CookieName" Value="{{Config.TestParameters.AnonCookieName}}" />
<RuleParameter Name="Domain" Value="" />
<RuleParameter Name="StopOnError" Value="False" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
</Request>
</Items>
<DataSources>
<DataSource Name="Config" Provider="Microsoft.VisualStudio.TestTools.DataSource.XML" Connection="|DataDirectory|\Config\TestParameters.xml">
<Tables>
<DataSourceTable Name="TestParameters" SelectColumns="SelectOnlyBoundColumns" AccessMethod="Sequential" />
</Tables>
</DataSource>
</DataSources>
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidateResponseUrl, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Response URL" Description="Validates that the response URL after redirects are followed is the same as the recorded response URL. QueryString parameters are ignored." Level="Low" ExectuionOrder="BeforeDependents" />
</ValidationRules>
<WebTestPlugins>
<WebTestPlugin Classname="Dropthings.Test.Plugin.ASPNETWebTestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="ASPNETWebTestPlugin" Description="" />
</WebTestPlugins>
</WebTest>
`)
+537
View File
@@ -0,0 +1,537 @@
/* gonuts10seqB.go - https://groups.google.com/forum/?fromgroups#!topic/golang-nuts/tf4aDQ1Hn_c
Objective: to quote from email
================================ BEGIN QUOTE
I'm actually dealing with Microsoft webtest files. An example can be find at,
https://gist.github.com/suntong/e4dcdc6c85dcf769eec4
It is the same as our case -- we have comments before each "<Request", and the requests and comments are grouped into transactions. For *each* request, I need to change one of its sub-node with the content from its leading comments, and from the content of the grouping transaction as well.
Using the above example to explain in details, the first Request is,
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ...
The comments immediately before it, its leading comments, is,
<Comment CommentText="Visit Homepage ...
The fist Transaction is,
<TransactionTimer Name="Show Hide Widget List">
Under it, the requests and comments are grouped into this transaction.
Now the challenge is, for *each* request with a comment immediately before it, change it attribute "ReportingName="""''s value with the content from its leading comments, and from the content of the grouping transaction as well. Let's say, first 10 chars or first three words or each. So for the first Request under "<TransactionTimer Name="Show Hide Widget List">", which is "<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ...", it's attribute "ReportingName=""" should be changed to,
ReportingName="Show Hide Widget, Show Widget List"
Everything else should remain exactly the same.
========================== END OF QUOTE
NOTE: use NewMapXmlSeq() and mv.XmlSeqIndent() to preserve structure.
ALSO: we will ignore Comment/Request entiries in WebTest.Items list.
See data value at EOF - from: https://gist.github.com/suntong/e4dcdc6c85dcf769eec4
*/
package main
import (
"bytes"
"fmt"
"github.com/clbanning/mxj"
"io"
)
func main() {
// fmt.Println(string(data))
rdr := bytes.NewReader(data)
// We read processing docs sequentially.
// Un-rooted ProcInst or Comments are processed AND just re-encoded. (XmlSeqIndent() knows how, now.)
for m, err := mxj.NewMapXmlSeqReader(rdr); m != nil || err != io.EOF; m, err = mxj.NewMapXmlSeqReader(rdr) {
if err != nil {
if err != mxj.NoRoot {
fmt.Println("NewMapXmlSeq err:", err)
fmt.Println("m:", m)
} else if m != nil {
x, _ := m.XmlSeqIndent("", " ")
fmt.Println(string(x))
}
continue
}
// fmt.Println(m.StringIndent())
// get the array of TransactionTimer entries for the 'path'
vals, err := m.ValuesForPath("WebTest.Items.TransactionTimer")
if err != nil {
fmt.Printf("ValuesForPath err: %s", err.Error())
continue
} else if len(vals) == 0 {
fmt.Printf("no vals for WebTest.Items.TransactionTimer")
continue
}
// process each TransactionTimer element ...
for _, t := range vals {
tmap := t.(map[string]interface{})
// get Name from attrs
tname, _ := mxj.Map(tmap).ValueForPathString("#attr.Name.#text")
// now process TransactionTimer.Items value ... is a map[string]interface{} value
// with Comment and Request keys with array values
vm, ok := tmap["Items"].(map[string]interface{})
if !ok {
fmt.Println("assertion failed")
return
}
// get the Comment list
c, ok := vm["Comment"]
if !ok { // --> no Items.Comment elements
continue
}
// Don't assume that Comment is an array.
// There may be just one value, in which case it will decode as map[string]interface{}.
switch c.(type) {
case map[string]interface{}:
c = []interface{}{c}
}
cmt := c.([]interface{})
// get the Request list
r, ok := vm["Request"]
if !ok { // --> no Items.Request elements
continue
}
// Don't assume the Request is an array.
// There may be just one value, in which case it will decode as map[string]interface{}.
switch r.(type) {
case map[string]interface{}:
r = []interface{}{r}
}
req := r.([]interface{})
// fmt.Println("Comment:", cmt)
// fmt.Println("Request:", req)
// Comment elements with #seq==n are followed by Request element with #seq==n+1.
// For each Comment.#seq==n extract the CommentText attribute value and use it to
// set the ReportingName attribute value in Request.#seq==n+1.
for _, v := range cmt {
vmap := v.(map[string]interface{})
seq := vmap["#seq"].(int) // type is int
// extract CommentText attr from array of "#attr"
acmt, _ := mxj.Map(vmap).ValueForPathString("#attr.CommentText.#text")
if acmt == "" {
fmt.Println("no CommentText value in Comment attributes")
}
// fmt.Println(seq, acmt)
// find the request with the #seq==seq+1 value
var r map[string]interface{}
for _, vv := range req {
rt := vv.(map[string]interface{})
if rt["#seq"].(int) == seq+1 {
r = rt
break
}
}
if r == nil { // no Request with #seq==seq+1
continue
}
if err := mxj.Map(r).SetValueForPath(tname+", "+acmt, "#attr.ReportingName.#text"); err != nil {
fmt.Println("SetValueForPath err:", err)
break
}
}
}
// re-encode the map with the TransactionTimer.#attr.Name & Items.Comment[#seq==n].#attr.CommentText
// values copied to the Items.Request[#seq==n+1].#attr.ReportingName elements.
b, err := m.XmlSeqIndent("", " ")
if err != nil {
fmt.Println("XmlIndent err:", err)
return
}
fmt.Println(string(b))
}
}
var data = []byte(`
<?xml version="1.0" encoding="utf-8"?>
<WebTest Name="FirstAnonymousVisit" Id="ac766d08-f940-4b0a-b8f8-80675978894e" Owner="" Priority="0" Enabled="True" CssProjectStructure="" CssIteration="" Timeout="0" WorkItemIds="" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010" Description="" CredentialUserName="" CredentialPassword="" PreAuthenticate="True" Proxy="" StopOnError="False" RecordedResultFile="">
<Items>
<Comment CommentText="Visit Homepage and ensure new page setup is created" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Dropthings.Test.Rules.CookieValidationRule, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Check Cookie From Response" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="StopOnError" Value="False" />
<RuleParameter Name="CookieValueToMatch" Value="" />
<RuleParameter Name="MatchValue" Value="False" />
<RuleParameter Name="Exists" Value="True" />
<RuleParameter Name="CookieName" Value="{{Config.TestParameters.AnonCookieName}}" />
<RuleParameter Name="IsPersistent" Value="True" />
<RuleParameter Name="Domain" Value="" />
<RuleParameter Name="Index" Value="0" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Dropthings.Test.Rules.CookieValidationRule, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Check Cookie From Response" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="StopOnError" Value="False" />
<RuleParameter Name="CookieValueToMatch" Value="" />
<RuleParameter Name="MatchValue" Value="False" />
<RuleParameter Name="Exists" Value="False" />
<RuleParameter Name="CookieName" Value="{{Config.TestParameters.SessionCookieName}}" />
<RuleParameter Name="IsPersistent" Value="False" />
<RuleParameter Name="Domain" Value="" />
<RuleParameter Name="Index" Value="0" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Dropthings.Test.Rules.CacheHeaderValidation, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Cache Header Validation" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="Enabled" Value="True" />
<RuleParameter Name="DifferenceThresholdSec" Value="0" />
<RuleParameter Name="CacheControlPrivate" Value="False" />
<RuleParameter Name="CacheControlPublic" Value="False" />
<RuleParameter Name="CacheControlNoCache" Value="True" />
<RuleParameter Name="ExpiresAfterSeconds" Value="0" />
<RuleParameter Name="StopOnError" Value="False" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="How to of the Day" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="Weather" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="All rights reserved" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
</Request>
<TransactionTimer Name="Show Hide Widget List">
<Items>
<Comment CommentText="Show Widget List and expect Widget List to produce BBC Word widget link" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="BBC World" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="TabControlPanel$ShowAddContentPanel" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.OnPageMenuUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
<Comment CommentText="Hide Widget List and expect the outpu does not have the BBC World Widget" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="TabControlPanel$ShowAddContentPanel" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="TabControlPanel$HideAddContentPanel" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.OnPageMenuUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
</Items>
</TransactionTimer>
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/API/Proxy.svc/ajax/GetRss?url=%22http%3A%2F%2Ffeeds.feedburner.com%2FOmarAlZabirBlog%22&amp;count=10&amp;cacheDuration=10" ThinkTime="0" Timeout="300" ParseDependentRequests="True" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="{&quot;d&quot;:[{&quot;__type&quot;:&quot;RssItem:#Dropthings.Web.Util&quot;" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
</Request>
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/API/Proxy.svc/ajax/GetUrl?url=%22http%3A%2F%2Ffeeds.feedburner.com%2FOmarAlZabirBlog%22&amp;cacheDuration=10" ThinkTime="0" Timeout="300" ParseDependentRequests="True" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="&lt;channel&gt;" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
</Request>
<TransactionTimer Name="Edit Collapse Expand Widget">
<Items>
<Comment CommentText="Click edit on first widget &quot;How to of the Day&quot; and expect URL textbox to be present with Feed Url" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleRequiredAttributeValue, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Required Attribute Value" Description="Verifies the existence of a specified HTML tag that contains an attribute with a specified value." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="TagName" Value="input" />
<RuleParameter Name="AttributeName" Value="value" />
<RuleParameter Name="MatchAttributeName" Value="" />
<RuleParameter Name="MatchAttributeValue" Value="" />
<RuleParameter Name="ExpectedValue" Value="http://www.wikihow.com/feed.rss" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="Index" Value="-1" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<ExtractionRules>
<ExtractionRule Classname="Dropthings.Test.Rules.ExtractFormElements, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" VariableName="" DisplayName="Extract Form Elements" Description="">
<RuleParameters>
<RuleParameter Name="ContextParameterName" Value="" />
</RuleParameters>
</ExtractionRule>
</ExtractionRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="{{$POSTBACK.EditWidget.1}}" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.WidgetHeaderUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
<Comment CommentText="Change the Feed Count Dropdown list to 10 and expect 10 Feed Link controls are generated" />
<Request Method="POST" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="FeedList_ctl09_FeedLink" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="{{$POSTBACK.CancelEditWidget.1}}" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.WidgetHeaderUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
<FormPostHttpBody>
<FormPostParameter Name="{{$INPUT.FeedUrl.1}}" Value="http://www.wikihow.com/feed.rss" RecordedValue="" CorrelationBinding="" UrlEncode="True" />
<FormPostParameter Name="{{$SELECT.FeedCountDropDownList.1}}" Value="10" RecordedValue="" CorrelationBinding="" UrlEncode="True" />
</FormPostHttpBody>
</Request>
<Comment CommentText="Delete the How to of the Day widget and expect it's not found from response" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="How to of the Day" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="False" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="{{$POSTBACK.CloseWidget.1}}" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.WidgetHeaderUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
</Items>
</TransactionTimer>
<TransactionTimer Name="Add New Widget">
<Items>
<Comment CommentText="Show widget list and expect Digg to be there" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="Digg" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="TabControlPanel$ShowAddContentPanel" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.OnPageMenuUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
<Comment CommentText="Add New Widget" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="Digg" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="{{$POSTBACK.AddWidget.1}}" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.OnPageMenuUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
<Comment CommentText="Delete the newly added widget" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="How to of the Day" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="False" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
<RequestPlugins>
<RequestPlugin Classname="Dropthings.Test.Plugin.AsyncPostbackRequestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="AsyncPostbackRequestPlugin" Description="">
<RuleParameters>
<RuleParameter Name="ControlName" Value="{{$POSTBACK.CloseWidget.1}}" />
<RuleParameter Name="UpdatePanelName" Value="{{$UPDATEPANEL.WidgetHeaderUpdatePanel.1}}" />
</RuleParameters>
</RequestPlugin>
</RequestPlugins>
</Request>
</Items>
</TransactionTimer>
<Comment CommentText="Revisit and ensure the Digg widget exists and How to widget does not exist" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Default.aspx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="0" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Dropthings.Test.Rules.CookieValidationRule, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Check Cookie From Response" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="StopOnError" Value="False" />
<RuleParameter Name="CookieValueToMatch" Value="" />
<RuleParameter Name="MatchValue" Value="False" />
<RuleParameter Name="Exists" Value="False" />
<RuleParameter Name="CookieName" Value="{{Config.TestParameters.AnonCookieName}}" />
<RuleParameter Name="IsPersistent" Value="True" />
<RuleParameter Name="Domain" Value="" />
<RuleParameter Name="Index" Value="0" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Dropthings.Test.Rules.CookieValidationRule, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Check Cookie From Response" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="StopOnError" Value="False" />
<RuleParameter Name="CookieValueToMatch" Value="" />
<RuleParameter Name="MatchValue" Value="False" />
<RuleParameter Name="Exists" Value="False" />
<RuleParameter Name="CookieName" Value="{{Config.TestParameters.SessionCookieName}}" />
<RuleParameter Name="IsPersistent" Value="False" />
<RuleParameter Name="Domain" Value="" />
<RuleParameter Name="Index" Value="0" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Dropthings.Test.Rules.CacheHeaderValidation, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Cache Header Validation" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="Enabled" Value="True" />
<RuleParameter Name="DifferenceThresholdSec" Value="0" />
<RuleParameter Name="CacheControlPrivate" Value="False" />
<RuleParameter Name="CacheControlPublic" Value="False" />
<RuleParameter Name="CacheControlNoCache" Value="True" />
<RuleParameter Name="ExpiresAfterSeconds" Value="0" />
<RuleParameter Name="StopOnError" Value="False" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="How to of the Day" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="False" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="Digg" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidationRuleFindText, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Find Text" Description="Verifies the existence of the specified text in the response." Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="FindText" Value="All rights reserved" />
<RuleParameter Name="IgnoreCase" Value="False" />
<RuleParameter Name="UseRegularExpression" Value="False" />
<RuleParameter Name="PassIfTextFound" Value="True" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
</Request>
<Comment CommentText="- Logout and ensure Anon Cookie is set to expire" />
<Request Method="GET" Version="1.1" Url="{{Config.TestParameters.ServerURL}}/Logout.ashx" ThinkTime="0" Timeout="300" ParseDependentRequests="False" FollowRedirects="False" RecordResult="True" Cache="False" ResponseTimeGoal="0.5" Encoding="utf-8" ExpectedHttpStatusCode="302" ExpectedResponseUrl="" ReportingName="">
<ValidationRules>
<ValidationRule Classname="Dropthings.Test.Rules.CookieSetToExpire, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Ensure Cookie Set to Expire" Description="" Level="High" ExectuionOrder="BeforeDependents">
<RuleParameters>
<RuleParameter Name="CookieName" Value="{{Config.TestParameters.AnonCookieName}}" />
<RuleParameter Name="Domain" Value="" />
<RuleParameter Name="StopOnError" Value="False" />
</RuleParameters>
</ValidationRule>
</ValidationRules>
</Request>
</Items>
<DataSources>
<DataSource Name="Config" Provider="Microsoft.VisualStudio.TestTools.DataSource.XML" Connection="|DataDirectory|\Config\TestParameters.xml">
<Tables>
<DataSourceTable Name="TestParameters" SelectColumns="SelectOnlyBoundColumns" AccessMethod="Sequential" />
</Tables>
</DataSource>
</DataSources>
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidateResponseUrl, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Response URL" Description="Validates that the response URL after redirects are followed is the same as the recorded response URL. QueryString parameters are ignored." Level="Low" ExectuionOrder="BeforeDependents" />
</ValidationRules>
<WebTestPlugins>
<WebTestPlugin Classname="Dropthings.Test.Plugin.ASPNETWebTestPlugin, Dropthings.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="ASPNETWebTestPlugin" Description="" />
</WebTestPlugins>
</WebTest>
`)
+8 -8
View File
@@ -5,7 +5,7 @@
// <GetClaimStatusCodesResult>... list of ClaimStatusCodeRecord ...</GetClaimStatusCodeResult>
// <GetClaimStatusCodesResult>... one instance of ClaimStatusCodeRecord ...</GetClaimStatusCodeResult>
// <GetClaimStatusCodesResult>... empty element ...</GetClaimStatusCodeResult>
// ValueForPath
// ValuesForPath
package main
@@ -79,7 +79,7 @@ func fullPath(xmldata [][]byte) {
// get the value for the key path of interest
path := "Envelope.Body.GetClaimStatusCodesResponse.GetClaimStatusCodesResult.ClaimStatusCodeRecord"
values, err := m.ValueForPath(path)
values, err := m.ValuesForPath(path)
if err != nil {
fmt.Println("err:", err.Error())
return
@@ -112,7 +112,7 @@ func partPath1(msg []byte) {
m, _ := mxj.NewMapXml(msg)
fmt.Println("m:", m.StringIndent())
path := "Envelope.Body.*.*.ClaimStatusCodeRecord"
values, err := m.ValueForPath(path)
values, err := m.ValuesForPath(path)
if err != nil {
fmt.Println("err:", err.Error())
return
@@ -134,7 +134,7 @@ func partPath2(msg []byte) {
m, _ := mxj.NewMapXml(msg)
fmt.Println("m:", m.StringIndent())
path := "Envelope.Body.*.*.*"
values, err := m.ValueForPath(path)
values, err := m.ValuesForPath(path)
if err != nil {
fmt.Println("err:", err.Error())
return
@@ -156,7 +156,7 @@ func partPath3(msg []byte) {
m, _ := mxj.NewMapXml(msg)
fmt.Println("m:", m.StringIndent())
path := "*.*.*.*.*"
values, err := m.ValueForPath(path)
values, err := m.ValuesForPath(path)
if err != nil {
fmt.Println("err:", err.Error())
return
@@ -178,7 +178,7 @@ func partPath4(msg []byte) {
m, _ := mxj.NewMapXml(msg)
fmt.Println("m:", m.StringIndent())
path := "*.*.*.*.*.Description"
values, err := m.ValueForPath(path)
values, err := m.ValuesForPath(path)
if err != nil {
fmt.Println("err:", err.Error())
return
@@ -200,7 +200,7 @@ func partPath5(msg []byte) {
m, _ := mxj.NewMapXml(msg)
fmt.Println("m:", m.StringIndent())
path := "*.*.*.*.*.*"
values, err := m.ValueForPath(path)
values, err := m.ValuesForPath(path)
if err != nil {
fmt.Println("err:", err.Error())
return
@@ -222,7 +222,7 @@ func partPath6(msg []byte) {
m, _ := mxj.NewMapXml(msg)
fmt.Println("m:", m.StringIndent())
path := "*.*.*.*.*.*.*"
values, err := m.ValueForPath(path)
values, err := m.ValuesForPath(path)
if err != nil {
fmt.Println("err:", err.Error())
return
+1 -2
View File
@@ -4,8 +4,7 @@ package main
import (
"fmt"
// "github.com/clbanning/mxj"
"tamgroup/mxj"
"github.com/clbanning/mxj"
)
var data = []byte(`<root>
+59
View File
@@ -0,0 +1,59 @@
// keystolower_test.go
package mxj
import (
"fmt"
"testing"
)
var tolowerdata1 = []byte(`
<doc>
<element attr="attrValue">value</element>
</doc>
`)
var tolowerdata2 = []byte(`
<DOC>
<Element attR="attrValue">value</Element>
</DOC>
`)
func TestToLower(t *testing.T) {
fmt.Println("\n-------------- keystolower_test.go")
fmt.Println("\nTestToLower ...")
CoerceKeysToLower()
m1, err := NewMapXml(tolowerdata1)
if err != nil {
t.Fatal(err)
}
m2, err := NewMapXml(tolowerdata2)
if err != nil {
t.Fatal(err)
}
v1, err := m1.ValuesForPath("doc.element")
if err != nil {
t.Fatal(err)
}
v2, err := m2.ValuesForPath("doc.element")
if err != nil {
t.Fatal(err)
}
if len(v1) != len(v2) {
t.Fatal(err, len(v1), len(v2))
}
m := v1[0].(map[string]interface{})
mm := v2[0].(map[string]interface{})
for k, v := range m {
if vv, ok := mm[k]; !ok {
t.Fatal("key:", k, "not in mm")
} else if v.(string) != vv.(string) {
t.Fatal(v.(string), "not in v2:", vv.(string))
}
}
}
+83
View File
@@ -0,0 +1,83 @@
// Copyright 2016 Charles Banning. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file
// misc.go - mimic functions (+others) called out in:
// https://groups.google.com/forum/#!topic/golang-nuts/jm_aGsJNbdQ
// Primarily these methods let you retrive XML structure information.
package mxj
import (
"fmt"
"sort"
)
// Return the root element of the Map. If there is not a single key in Map,
// then an error is returned.
func (m Map) Root() (string, error) {
mm := map[string]interface{}(m)
if len(mm) != 1 {
return "", fmt.Errorf("Map does not have singleton root. Len: %d.", len(mm))
}
for k, _ := range mm {
return k, nil
}
return "", nil
}
// If the path is an element with sub-elements, return a list of the sub-element
// keys. (The list is alphabeticly sorted.) NOTE: Map keys that are prefixed with
// '-', a hyphen, are considered attributes; see m.Attributes(path).
func (m Map) Elements(path string) ([]string, error) {
e, err := m.ValueForPath(path)
if err != nil {
return nil, err
}
switch e.(type) {
case map[string]interface{}:
ee := e.(map[string]interface{})
elems := make([]string, len(ee))
var i int
for k, _ := range ee {
if k[:1] == "-" {
continue // skip attributes
}
elems[i] = k
i++
}
elems = elems[:i]
// alphabetic sort keeps things tidy
sort.Strings(elems)
return elems, nil
}
return nil, fmt.Errorf("no elements for path: %s", path)
}
// If the path is an element with attributes, return a list of the attribute
// keys. (The list is alphabeticly sorted.) NOTE: Map keys that are not prefixed with
// '-', a hyphen, are not treated as attributes; see m.Elements(path).
func (m Map) Attributes(path string) ([]string, error) {
a, err := m.ValueForPath(path)
if err != nil {
return nil, err
}
switch a.(type) {
case map[string]interface{}:
aa := a.(map[string]interface{})
attrs := make([]string, len(aa))
var i int
for k, _ := range aa {
if k[:1] != "-" {
continue // skip non-attributes
}
attrs[i] = k[1:]
i++
}
attrs = attrs[:i]
// alphabetic sort keeps things tidy
sort.Strings(attrs)
return attrs, nil
}
return nil, fmt.Errorf("no attributes for path: %s", path)
}
+92
View File
@@ -0,0 +1,92 @@
// misc_test.go
package mxj
import (
"fmt"
"testing"
)
var miscdata = []byte(`
<doc>
<elem1 name="elem1" seq="1">
<sub1 name="sub1" seq="1">sub_value_1</sub1>
<sub2 name="sub2" seq="2">sub_value_2</sub2>
</elem1>
<elem2 name="elem2" seq="2">element_2</elem2>
</doc>
`)
func TestMisc(t *testing.T) {
fmt.Println("\n------------------ misc_test.go ...")
}
func TestRoot(t *testing.T) {
m, err := NewMapXml(miscdata)
if err != nil {
t.Fatal(err)
}
r, err := m.Root()
if err != nil {
t.Fatal(err)
}
if r != "doc" {
t.Fatal("Root not doc:", r)
}
}
func TestElements(t *testing.T) {
m, err := NewMapXml(miscdata)
if err != nil {
t.Fatal(err)
}
e, err := m.Elements("doc")
if err != nil {
t.Fatal(err)
}
elist := []string{"elem1", "elem2"}
for i, ee := range e {
if ee != elist[i] {
t.Fatal("error in list, elem#:", i, "-", ee, ":", elist[i])
}
}
e, err = m.Elements("doc.elem1")
if err != nil {
t.Fatal(err)
}
elist = []string{"sub1", "sub2"}
for i, ee := range e {
if ee != elist[i] {
t.Fatal("error in list, elem#:", i, "-", ee, ":", elist[i])
}
}
}
func TestAttributes(t *testing.T) {
m, err := NewMapXml(miscdata)
if err != nil {
t.Fatal(err)
}
a, err := m.Attributes("doc.elem2")
if err != nil {
t.Fatal(err)
}
alist := []string{"name", "seq"}
for i, aa := range a {
if aa != alist[i] {
t.Fatal("error in list, elem#:", i, "-", aa, ":", alist[i])
}
}
a, err = m.Attributes("doc.elem1.sub2")
if err != nil {
t.Fatal(err)
}
alist = []string{"name", "seq"}
for i, aa := range a {
if aa != alist[i] {
t.Fatal("error in list, elem#:", i, "-", aa, ":", alist[i])
}
}
}
+17 -11
View File
@@ -52,17 +52,17 @@ func (mv Map) Copy() (Map, error) {
// Pretty print a Map.
func (mv Map) StringIndent(offset ...int) string {
return writeMap(map[string]interface{}(mv), offset...)
return writeMap(map[string]interface{}(mv), true, offset...)
}
// Pretty print a Map without the value type information - just key:value entries.
func (mv Map) StringIndentNoTypeInfo(offset ...int) string {
return writeMapNoTypes(map[string]interface{}(mv), offset...)
return writeMapNoTypes(map[string]interface{}(mv), true, offset...)
}
// writeMap - dumps the map[string]interface{} for examination.
// 'offset' is initial indentation count; typically: Write(m).
func writeMap(m interface{}, offset ...int) string {
func writeMap(m interface{}, root bool, offset ...int) string {
var indent int
if len(offset) == 1 {
indent = offset[0]
@@ -74,7 +74,9 @@ func writeMap(m interface{}, offset ...int) string {
return "[nil] nil"
case string:
return "[string] " + m.(string)
case float64:
case int, int32, int64:
return "[int] " + strconv.Itoa(m.(int))
case float64, float32:
return "[float64] " + strconv.FormatFloat(m.(float64), 'e', 2, 64)
case bool:
return "[bool] " + strconv.FormatBool(m.(bool))
@@ -95,19 +97,21 @@ func writeMap(m interface{}, offset ...int) string {
for i := 0; i < indent; i++ {
s += " "
}
s += writeMap(v, indent+1)
s += writeMap(v, false, indent+1)
}
case map[string]interface{}:
list := make([][2]string, len(m.(map[string]interface{})))
var n int
for k, v := range m.(map[string]interface{}) {
list[n][0] = k
list[n][1] = writeMap(v, indent+1)
list[n][1] = writeMap(v, false, indent+1)
n++
}
sort.Sort(mapList(list))
for _, v := range list {
s += "\n"
if !root {
s += "\n"
}
for i := 0; i < indent; i++ {
s += " "
}
@@ -122,7 +126,7 @@ func writeMap(m interface{}, offset ...int) string {
// writeMapNoTypes - dumps the map[string]interface{} for examination.
// 'offset' is initial indentation count; typically: Write(m).
func writeMapNoTypes(m interface{}, offset ...int) string {
func writeMapNoTypes(m interface{}, root bool, offset ...int) string {
var indent int
if len(offset) == 1 {
indent = offset[0]
@@ -155,19 +159,21 @@ func writeMapNoTypes(m interface{}, offset ...int) string {
for i := 0; i < indent; i++ {
s += " "
}
s += writeMapNoTypes(v, indent+1)
s += writeMapNoTypes(v, false, indent+1)
}
case map[string]interface{}:
list := make([][2]string, len(m.(map[string]interface{})))
var n int
for k, v := range m.(map[string]interface{}) {
list[n][0] = k
list[n][1] = writeMapNoTypes(v, indent+1)
list[n][1] = writeMapNoTypes(v, false, indent+1)
n++
}
sort.Sort(mapList(list))
for _, v := range list {
s += "\n"
if !root {
s += "\n"
}
for i := 0; i < indent; i++ {
s += " "
}
+81
View File
@@ -0,0 +1,81 @@
// nan_test.go
package mxj
import (
"fmt"
"testing"
)
func TestNan(t *testing.T) {
fmt.Println("\n------------ TestNan\n")
data := []byte("<foo><bar>NAN</bar></foo>")
m, err := NewMapXml(data, true)
if err != nil {
t.Fatal("err:", err)
}
v, err := m.ValueForPath("foo.bar")
if err != nil {
t.Fatal("err:", err)
}
if _, ok := v.(string); !ok {
t.Fatal("v not string")
}
fmt.Println("foo.bar:", v)
}
func TestInf(t *testing.T) {
data := []byte("<foo><bar>INF</bar></foo>")
m, err := NewMapXml(data, true)
if err != nil {
t.Fatal("err:", err)
}
v, err := m.ValueForPath("foo.bar")
if err != nil {
t.Fatal("err:", err)
}
if _, ok := v.(string); !ok {
t.Fatal("v not string")
}
fmt.Println("foo.bar:", v)
}
func TestMinusInf(t *testing.T) {
data := []byte("<foo><bar>-INF</bar></foo>")
m, err := NewMapXml(data, true)
if err != nil {
t.Fatal("err:", err)
}
v, err := m.ValueForPath("foo.bar")
if err != nil {
t.Fatal("err:", err)
}
if _, ok := v.(string); !ok {
t.Fatal("v not string")
}
fmt.Println("foo.bar:", v)
}
func TestCastNanInf(t *testing.T) {
data := []byte("<foo><bar>NAN</bar></foo>")
CastNanInf(true)
m, err := NewMapXml(data, true)
if err != nil {
t.Fatal("err:", err)
}
v, err := m.ValueForPath("foo.bar")
if err != nil {
t.Fatal("err:", err)
}
if _, ok := v.(float64); !ok {
fmt.Printf("%#v\n", v)
t.Fatal("v not float64")
}
fmt.Println("foo.bar:", v)
}
+53 -10
View File
@@ -1,9 +1,28 @@
<h2>mxj - to/from maps, XML and JSON</h2>
Marshal/Unmarshal XML to/from JSON and `map[string]interface{}` values, and extract/modify values from maps by key or key-path, including wildcards.
Decode/encode XML to/from map[string]interface{} (or JSON) values, and extract/modify values from maps by key or key-path, including wildcards.
mxj supplants the legacy x2j and j2x packages. If you want the old syntax, use mxj/x2j and mxj/j2x packages.
<h4>Refactor Decoder - 2015.11.15</h4>
For over a year I've wanted to refactor the XML-to-map[string]interface{} decoder to make it more performant. I recently took the time to do that, since we were using github.com/clbanning/mxj in a production system that could be deployed on a Raspberry Pi. Now the decoder is comparable to the stdlib JSON-to-map[string]interface{} decoder in terms of its additional processing overhead relative to decoding to a structure value. As shown by:
BenchmarkNewMapXml-4 100000 18043 ns/op
BenchmarkNewStructXml-4 100000 14892 ns/op
BenchmarkNewMapJson-4 300000 4633 ns/op
BenchmarkNewStructJson-4 300000 3427 ns/op
BenchmarkNewMapXmlBooks-4 20000 82850 ns/op
BenchmarkNewStructXmlBooks-4 20000 67822 ns/op
BenchmarkNewMapJsonBooks-4 100000 17222 ns/op
BenchmarkNewStructJsonBooks-4 100000 15309 ns/op
<h4>Notices</h4>
2016.03.02: By default decoding XML with float64 and bool value casting will not cast "NaN", "Inf", and "-Inf".
To cast them to float64, first set flag with CastNanInf(true).
2016.02.22: New m.Root(), m.Elements(), m.Attributes methods let you examine XML document structure.
2016.02.16: Add CoerceKeysToLower() option to handle tags with mixed capitalization.
2016.02.12: Seek for first xml.StartElement token; only return error if io.EOF is reached first (handles BOM).
2015.12.02: EXPERIMENTAL XML decoding/encoding that preserves original structure of document. See
NewMapXmlSeq() and mv.XmlSeq() / mv.XmlSeqIndent().
2015-05-20: New: mv.StringIndentNoTypeInfo().
Also, alphabetically sort map[string]interface{} values by key to prettify output for mv.Xml(),
mv.XmlIndent(), mv.StringIndent(), mv.StringIndentNoTypeInfo().
@@ -13,7 +32,7 @@ mxj supplants the legacy x2j and j2x packages. If you want the old syntax, use m
2014-08-02: AnyXml() and AnyXmlIndent() will try to marshal arbitrary values to XML.
2014-04-28: ValuesForPath() and NewMap() now accept path with indexed array references.
<h4>Basic Unmarshal XML / JSON / struct</h4>
<h4>Basic Unmarshal XML to map[string]interface{}</h4>
<pre>type Map map[string]interface{}</pre>
Create a `Map` value, 'm', from any `map[string]interface{}` value, 'v':
@@ -63,9 +82,9 @@ leafvalues := m.LeafValues()</pre>
A new `Map` with whatever keys are desired can be created from the current `Map` and then encoded in XML
or JSON. (Note: keys can use dot-notation.)
<pre>newMap := m.NewMap("oldKey_1:newKey_1", "oldKey_2:newKey_2", ..., "oldKey_N:newKey_N")
newXml := newMap.Xml() // for example
newJson := newMap.Json() // ditto</pre>
<pre>newMap, err := m.NewMap("oldKey_1:newKey_1", "oldKey_2:newKey_2", ..., "oldKey_N:newKey_N")
newXml, err := newMap.Xml() // for example
newJson, err := newMap.Json() // ditto</pre>
<h4>Usage</h4>
@@ -75,17 +94,41 @@ Also, the subdirectory "examples" contains a wide range of examples, several tak
<h4>XML parsing conventions</h4>
Using NewXml()
- Attributes are parsed to `map[string]interface{}` values by prefixing a hyphen, `-`,
to the attribute label. (Unless overridden by `PrependAttrWithHyphen(false)`.)
- If the element is a simple element and has attributes, the element value
is given the key `#text` for its `map[string]interface{}` representation. (See
the 'atomFeedString.xml' test data, below.)
- XML comments, directives, and process instructions are ignored.
- If CoerceKeysToLower() has been called, then the resultant keys will be lower case.
Using NewXmlSeq()
- Attributes are parsed to `map["#attr"]map[<attr_label>]map[string]interface{}`values
where the `<attr_label>` value has "#text" and "#seq" keys - the "#text" key holds the
value for `<attr_label>`.
- All elements, except for the root, have a "#seq" key.
- Comments, directives, and process instructions are unmarshalled into the Map using the
keys "#comment", "#directive", and "#procinst", respectively. (See documentation for more
specifics.)
Both
- By default, "Nan", "Inf", and "-Inf" values are not cast to float64. If you want them
to be cast, set a flag to cast them using CastNanInf(true).
<h4>XML encoding conventions</h4>
- 'nil' `Map` values, which may represent 'null' JSON values, are encoded as `<tag/>`.
NOTE: the operation is not symmetric as `<tag/>` elements are decoded as `tag:""` `Map` values,
which, then, encode in JSON as `"tag":""` values.
NOTE: the operation is not symmetric as `<tag/>` elements are decoded as `tag:""` `Map` values,
which, then, encode in JSON as `"tag":""` values.
- ALSO: there is no guarantee that the encoded XML doc will be the same as the decoded one. (Go
randomizes the walk through map[string]interface{} values.) If you plan to re-encode the
Map value to XML and want the same sequencing of elements look at NewMapXmlSeq() and
m.XmlSeq() - these try to preserve the element sequencing but with added complexity when
working with the Map representation.
<h4>Running "go test"</h4>
@@ -96,15 +139,15 @@ output from running "go test" as examples of calling the various functions and m
<h4>Motivation</h4>
I make extensive use of JSON for messaging and typically unmarshal the messages into
`map[string]interface{}` variables. This is easily done using `json.Unmarshal` from the
`map[string]interface{}` values. This is easily done using `json.Unmarshal` from the
standard Go libraries. Unfortunately, many legacy solutions use structured
XML messages; in those environments the applications would have to be refactored to
interoperate with my components.
The better solution is to just provide an alternative HTTP handler that receives
XML messages and parses it into a `map[string]interface{}` variable and then reuse
XML messages and parses it into a `map[string]interface{}` value and then reuse
all the JSON-based code. The Go `xml.Unmarshal()` function does not provide the same
option of unmarshaling XML messages into `map[string]interface{}` variables. So I wrote
option of unmarshaling XML messages into `map[string]interface{}` values. So I wrote
a couple of small functions to fill this gap and released them as the x2j package.
Over the next year and a half additional features were added, and the companion j2x
+29
View File
@@ -0,0 +1,29 @@
<msg mtype="alert" mpriority="1">
<text>help me!</text>
<song title="A Long Time" author="Mayer Hawthorne">
<verses>
<verse name="verse 1" no="1">
<line no="1">Henry was a renegade</line>
<line no="2">Didn't like to play it safe</line>
<line no="3">One component at a time</line>
<line no="4">There's got to be a better way</line>
<line no="5">Oh, people came from miles around</line>
<line no="6">Searching for a steady job</line>
<line no="7">Welcome to the Motor Town</line>
<line no="8">Booming like an atom bomb</line>
</verse>
<verse name="verse 2" no="2">
<line no="1">Oh, Henry was the end of the story</line>
<line no="2">Then everything went wrong</line>
<line no="3">And we'll return it to its former glory</line>
<line no="4">But it just takes so long</line>
</verse>
</verses>
<chorus>
<line no="1">It's going to take a long time</line>
<line no="2">It's going to take it, but we'll make it one day</line>
<line no="3">It's going to take a long time</line>
<line no="4">It's going to take it, but we'll make it one day</line>
</chorus>
</song>
</msg>
+191 -191
View File
@@ -1,4 +1,4 @@
// Copyright 2012-2014 Charles Banning. All rights reserved.
// Copyright 2012-2016 Charles Banning. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file
@@ -14,14 +14,13 @@ import (
"errors"
"fmt"
"io"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
// ------------------- NewMapXml & NewMapXmlReader ... from x2j2 -------------------------
// ------------------- NewMapXml & NewMapXmlReader ... -------------------------
// If XmlCharsetReader != nil, it will be used to decode the XML, if required.
// import (
@@ -47,23 +46,24 @@ var XmlCharsetReader func(charset string, input io.Reader) (io.Reader, error)
// if jerr != nil {
// // handle error
// }
//
// NOTES:
// 1. The 'xmlVal' will be parsed looking for an xml.StartElement, so BOM and other
// extraneous xml.CharData will be ignored unless io.EOF is reached first.
// 2. If CoerceKeysToLower() has been called, then all key values will be lower case.
func NewMapXml(xmlVal []byte, cast ...bool) (Map, error) {
var r bool
if len(cast) == 1 {
r = cast[0]
}
n, err := xmlToTree(xmlVal)
if err != nil {
return nil, err
}
m := make(map[string]interface{}, 0)
m[n.key] = n.treeToMap(r)
return m, nil
return xmlToMap(xmlVal, r)
}
// Get next XML doc from an io.Reader as a Map value. Returns Map value.
// NOTES:
// 1. The 'xmlReader' will be parsed looking for an xml.StartElement, so BOM and other
// extraneous xml.CharData will be ignored unless io.EOF is reached first.
// 2. If CoerceKeysToLower() has been called, then all key values will be lower case.
func NewMapXmlReader(xmlReader io.Reader, cast ...bool) (Map, error) {
var r bool
if len(cast) == 1 {
@@ -71,29 +71,26 @@ func NewMapXmlReader(xmlReader io.Reader, cast ...bool) (Map, error) {
}
// build the node tree
n, err := xmlReaderToTree(xmlReader)
if err != nil {
return nil, err
}
// create the Map value
m := make(map[string]interface{})
m[n.key] = n.treeToMap(r)
return m, nil
return xmlReaderToMap(xmlReader, r)
}
// XmlWriterBufSize - set the size of io.Writer for the TeeReader used by NewMapXmlReaderRaw()
// and HandleXmlReaderRaw(). This reduces repeated memory allocations and copy() calls in most cases.
// NOTE: the 'xmlVal' will be parsed looking for an xml.StartElement, so BOM and other
// extraneous xml.CharData will be ignored unless io.EOF is reached first.
var XmlWriterBufSize int = 256
// Get next XML doc from an io.Reader as a Map value. Returns Map value and slice with the raw XML.
// NOTES: 1. Due to the implementation of xml.Decoder, the raw XML off the reader is buffered to []byte
// using a ByteReader. If the io.Reader is an os.File, there may be significant performance impact.
// See the examples - getmetrics1.go through getmetrics4.go - for comparative use cases on a large
// data set. If the io.Reader is wrapping a []byte value in-memory, however, such as http.Request.Body
// you CAN use it to efficiently unmarshal a XML doc and retrieve the raw XML in a single call.
// 2. The 'raw' return value may be larger than the XML text value. To log it, cast it to a string.
// NOTES:
// 1. Due to the implementation of xml.Decoder, the raw XML off the reader is buffered to []byte
// using a ByteReader. If the io.Reader is an os.File, there may be significant performance impact.
// See the examples - getmetrics1.go through getmetrics4.go - for comparative use cases on a large
// data set. If the io.Reader is wrapping a []byte value in-memory, however, such as http.Request.Body
// you CAN use it to efficiently unmarshal a XML doc and retrieve the raw XML in a single call.
// 2. The 'raw' return value may be larger than the XML text value.
// 3. The 'xmlReader' will be parsed looking for an xml.StartElement, so BOM and other
// extraneous xml.CharData will be ignored unless io.EOF is reached first.
// 4. If CoerceKeysToLower() has been called, then all key values will be lower case.
func NewMapXmlReaderRaw(xmlReader io.Reader, cast ...bool) (Map, []byte, error) {
var r bool
if len(cast) == 1 {
@@ -105,7 +102,7 @@ func NewMapXmlReaderRaw(xmlReader io.Reader, cast ...bool) (Map, []byte, error)
trdr := myTeeReader(xmlReader, wb) // see code at EOF
// build the node tree
n, err := xmlReaderToTree(trdr)
m, err := xmlReaderToMap(trdr, r)
// retrieve the raw XML that was decoded
b := make([]byte, wb.Len())
@@ -115,49 +112,28 @@ func NewMapXmlReaderRaw(xmlReader io.Reader, cast ...bool) (Map, []byte, error)
return nil, b, err
}
// create the Map value
m := make(map[string]interface{})
m[n.key] = n.treeToMap(r)
return m, b, nil
}
// xmlReaderToTree() - parse a XML io.Reader to a tree of nodes
func xmlReaderToTree(rdr io.Reader) (*node, error) {
// xmlReaderToMap() - parse a XML io.Reader to a map[string]interface{} value
func xmlReaderToMap(rdr io.Reader, r bool) (map[string]interface{}, error) {
// parse the Reader
p := xml.NewDecoder(rdr)
p.CharsetReader = XmlCharsetReader
return xmlToTreeParser("", nil, p)
return xmlToMapParser("", nil, p, r)
}
// for building the parse tree
type node struct {
dup bool // is member of a list
attr bool // is an attribute
key string // XML tag
val string // element value
nodes []*node
}
// xmlToTree - convert a XML doc into a tree of nodes.
func xmlToTree(doc []byte) (*node, error) {
// xml.Decoder doesn't properly handle whitespace in some doc
// see songTextString.xml test case ...
reg, _ := regexp.Compile("[ \t\n\r]*<")
doc = reg.ReplaceAll(doc, []byte("<"))
// xmlToMap - convert a XML doc into map[string]interface{} value
func xmlToMap(doc []byte, r bool) (map[string]interface{}, error) {
b := bytes.NewReader(doc)
p := xml.NewDecoder(b)
p.CharsetReader = XmlCharsetReader
n, berr := xmlToTreeParser("", nil, p)
if berr != nil {
return nil, berr
}
return n, nil
return xmlToMapParser("", nil, p, r)
}
// we allow people to drop hyphen when unmarshaling the XML doc.
// ===================================== where the work happens =============================
// Allow people to drop hyphen when unmarshaling the XML doc.
var useHyphen bool = true
// PrependAttrWithHyphen. Prepend attribute tags with a hyphen.
@@ -173,7 +149,10 @@ func PrependAttrWithHyphen(v bool) {
var includeTagSeqNum bool
// IncludeTagSeqNum - include a "_seq":N key:value pair with each inner tag, denoting
// its position when parsed. E.g.,
// its position when parsed. This is of limited usefulness, since list values cannot
// be tagged with "_seq" without changing their depth in the Map.
// So THIS SHOULD BE USED WITH CAUTION - see the test cases. Here's a sample of what
// you get.
/*
<Obj c="la" x="dee" h="da">
<IntObj id="3"/>
@@ -213,25 +192,58 @@ func IncludeTagSeqNum(b bool) {
includeTagSeqNum = b
}
// xmlToTreeParser - load a 'clean' XML doc into a tree of *node.
func xmlToTreeParser(skey string, a []xml.Attr, p *xml.Decoder) (*node, error) {
n := new(node)
n.nodes = make([]*node, 0)
// all keys will be "lower case"
var lowerCase bool
// Coerce all tag values to keys in lower case. This is useful if you've got sources with variable
// tag capitalization, and you want to use m.ValuesForKeys(), etc., with the key or path spec
// in lower case.
// CoerceKeysToLower() will toggle the coercion flag true|false - on|off
// CoerceKeysToLower(true|false) will set the coercion flag on|off
//
// NOTE: only recognized by NewMapXml, NewMapXmlReader, and NewMapXmlReaderRaw functions as well as
// the associated HandleXmlReader and HandleXmlReaderRaw.
func CoerceKeysToLower(b ...bool) {
if len(b) == 1 {
lowerCase = b[0]
return
}
if !lowerCase {
lowerCase = true
} else {
lowerCase = false
}
}
// xmlToMapParser (2015.11.12) - load a 'clean' XML doc into a map[string]interface{} directly.
// A refactoring of xmlToTreeParser(), markDuplicate() and treeToMap() - here, all-in-one.
// We've removed the intermediate *node tree with the allocation and subsequent rescanning.
func xmlToMapParser(skey string, a []xml.Attr, p *xml.Decoder, r bool) (map[string]interface{}, error) {
if lowerCase {
skey = strings.ToLower(skey)
}
// NOTE: all attributes and sub-elements parsed into 'na', 'na' is returned as value for 'skey'
// Unless 'skey' is a simple element w/o attributes, in which case the xml.CharData value is the value.
var n, na map[string]interface{}
var seq int // for includeTagSeqNum
// Allocate maps and load attributes, if any.
if skey != "" {
n.key = skey
n = make(map[string]interface{}) // old n
na = make(map[string]interface{}) // old n.nodes
if len(a) > 0 {
for _, v := range a {
na := new(node)
na.attr = true
var key string
if useHyphen {
na.key = `-` + v.Name.Local
key = `-` + v.Name.Local
} else {
na.key = v.Name.Local
key = v.Name.Local
}
na.val = v.Value
n.nodes = append(n.nodes, na)
if lowerCase {
key = strings.ToLower(key)
}
na[key] = cast(v.Value, r)
}
}
}
@@ -246,132 +258,128 @@ func xmlToTreeParser(skey string, a []xml.Attr, p *xml.Decoder) (*node, error) {
switch t.(type) {
case xml.StartElement:
tt := t.(xml.StartElement)
// handle root
if n.key == "" {
n.key = tt.Name.Local
if len(tt.Attr) > 0 {
for _, v := range tt.Attr {
na := new(node)
na.attr = true
if useHyphen {
na.key = `-` + v.Name.Local
} else {
na.key = v.Name.Local
}
na.val = v.Value
n.nodes = append(n.nodes, na)
}
}
} else {
nn, nnerr := xmlToTreeParser(tt.Name.Local, tt.Attr, p)
if nnerr != nil {
return nil, nnerr
}
n.nodes = append(n.nodes, nn)
if includeTagSeqNum { // 2014.11.09
sn := &node{false, false, "_seq", strconv.Itoa(seq), nil}
nn.nodes = append(nn.nodes, sn)
// First call to xmlToMapParser() doesn't pass xml.StartElement - the map key.
// So when the loop is first entered, the first token is the root tag along
// with any attributes, which we process here.
//
// Subsequent calls to xmlToMapParser() will pass in tag+attributes for
// processing before getting the next token which is the element value,
// which is done above.
if skey == "" {
return xmlToMapParser(tt.Name.Local, tt.Attr, p, r)
}
// If not initializing the map, parse the element.
// len(nn) == 1, necessarily - it is just an 'n'.
nn, err := xmlToMapParser(tt.Name.Local, tt.Attr, p, r)
if err != nil {
return nil, err
}
// The nn map[string]interface{} value is a na[nn_key] value.
// We need to see if nn_key already exists - means we're parsing a list.
// This may require converting na[nn_key] value into []interface{} type.
// First, extract the key:val for the map - it's a singleton.
// Note: if CoerceKeysToLower() called, then key will be lower case.
var key string
var val interface{}
for key, val = range nn {
break
}
// IncludeTagSeqNum requests that the element be augmented with a "_seq" sub-element.
// In theory, we don't need this if len(na) == 1. But, we don't know what might
// come next - we're only parsing forward. So if you ask for 'includeTagSeqNum' you
// get it on every element. (Personally, I never liked this, but I added it on request
// and did get a $50 Amazon gift card in return - now we support it for backwards compatibility!)
if includeTagSeqNum {
switch val.(type) {
case []interface{}:
// noop - There's no clean way to handle this w/o changing message structure.
case map[string]interface{}:
val.(map[string]interface{})["_seq"] = seq // will overwrite an "_seq" XML tag
seq++
case interface{}: // a non-nil simple element: string, float64, bool
v := map[string]interface{}{"#text": val}
v["_seq"] = seq
seq++
val = v
}
}
// 'na' holding sub-elements of n.
// See if 'key' already exists.
// If 'key' exists, then this is a list, if not just add key:val to na.
if v, ok := na[key]; ok {
var a []interface{}
switch v.(type) {
case []interface{}:
a = v.([]interface{})
default: // anything else - note: v.(type) != nil
a = []interface{}{v}
}
a = append(a, val)
na[key] = a
} else {
na[key] = val // save it as a singleton
}
case xml.EndElement:
// scan n.nodes for duplicate n.key values
n.markDuplicateKeys()
// len(n) > 0 if this is a simple element w/o xml.Attrs - see xml.CharData case.
if len(n) == 0 {
// If len(na)==0 we have an empty element == "";
// it has no xml.Attr nor xml.CharData.
// Note: in original node-tree parser, val defaulted to "";
// so we always had the default if len(node.nodes) == 0.
if len(na) > 0 {
n[skey] = na
} else {
n[skey] = "" // empty element
}
}
return n, nil
case xml.CharData:
tt := string(t.(xml.CharData))
// clean up possible noise
tt = strings.Trim(tt, "\t\r\b\n ")
if len(n.nodes) > 0 && len(tt) > 0 {
// if len(n.nodes) > 0 {
nn := new(node)
nn.key = "#text"
nn.val = tt
n.nodes = append(n.nodes, nn)
} else {
n.val = tt
}
if includeTagSeqNum { // 2014.11.09
if len(n.nodes) == 0 { // treat like a simple element with attributes
nn := new(node)
nn.key = "#text"
nn.val = tt
n.nodes = append(n.nodes, nn)
tt := strings.Trim(string(t.(xml.CharData)), "\t\r\b\n ")
if len(tt) > 0 {
if len(na) > 0 {
na["#text"] = cast(tt, r)
} else if skey != "" {
n[skey] = cast(tt, r)
} else {
// per Adrian (http://www.adrianlungu.com/) catch stray text
// in decoder stream -
// https://github.com/clbanning/mxj/pull/14#issuecomment-182816374
// NOTE: CharSetReader must be set to non-UTF-8 CharSet or you'll get
// a p.Token() decoding error when the BOM is UTF-16 or UTF-32.
continue
}
sn := &node{false, false, "_seq", strconv.Itoa(seq), nil}
n.nodes = append(n.nodes, sn)
seq++
}
default:
// noop
}
}
// Logically we can't get here, but provide an error message anyway.
return nil, fmt.Errorf("Unknown parse error in xmlToTree() for: %s", n.key)
}
// (*node)markDuplicateKeys - set node.dup flag for loading map[string]interface{}.
func (n *node) markDuplicateKeys() {
l := len(n.nodes)
for i := 0; i < l; i++ {
if n.nodes[i].dup {
continue
}
for j := i + 1; j < l; j++ {
if n.nodes[i].key == n.nodes[j].key {
n.nodes[i].dup = true
n.nodes[j].dup = true
}
}
}
}
var castNanInf bool
// (*node)treeToMap - convert a tree of nodes into a map[string]interface{}.
// (Parses to map that is structurally the same as from json.Unmarshal().)
// Note: root is not instantiated; call with: "m[n.key] = n.treeToMap(cast)".
func (n *node) treeToMap(r bool) interface{} {
if len(n.nodes) == 0 {
return cast(n.val, r)
}
m := make(map[string]interface{}, 0)
for _, v := range n.nodes {
// 2014.11.9 - may have to back out
if includeTagSeqNum {
if len(v.nodes) == 1 {
m[v.key] = cast(v.val, r)
continue
}
}
// just a value
if !v.dup && len(v.nodes) == 0 {
m[v.key] = cast(v.val, r)
continue
}
// a list of values
if v.dup {
var a []interface{}
if vv, ok := m[v.key]; ok {
a = vv.([]interface{})
} else {
a = make([]interface{}, 0)
}
a = append(a, v.treeToMap(r))
m[v.key] = interface{}(a)
continue
}
// it's a unique key
m[v.key] = v.treeToMap(r)
}
return interface{}(m)
// Cast "Nan", "Inf", "-Inf" XML values to 'float64'.
// By default, these values will be decoded as 'string'.
func CastNanInf(b bool) {
castNanInf = b
}
// cast - try to cast string values to bool or float64
func cast(s string, r bool) interface{} {
if r {
// handle nan and inf
if !castNanInf {
switch strings.ToLower(s) {
case "nan", "inf", "-inf":
return interface{}(s)
}
}
// handle numeric strings ahead of boolean
if f, err := strconv.ParseFloat(s, 64); err == nil {
return interface{}(f)
@@ -518,7 +526,7 @@ func (mv Map) XmlIndentWriterRaw(xmlWriter io.Writer, prefix, indent string, roo
// Default poll delay to keep Handler from spinning on an open stream
// like sitting on os.Stdin waiting for imput.
var xhandlerPollInterval = time.Duration(1e6)
var xhandlerPollInterval = time.Millisecond
// Bulk process XML using handlers that process a Map value.
// 'rdr' is an io.Reader for XML (stream)
@@ -549,7 +557,7 @@ func HandleXmlReader(xmlReader io.Reader, mapHandler func(Map) bool, errHandler
break
}
} else if merr != io.EOF {
<-time.After(xhandlerPollInterval)
time.Sleep(xhandlerPollInterval)
}
if merr == io.EOF {
@@ -589,7 +597,7 @@ func HandleXmlReaderRaw(xmlReader io.Reader, mapHandler func(Map, []byte) bool,
break
}
} else if merr != io.EOF {
<-time.After(xhandlerPollInterval)
time.Sleep(xhandlerPollInterval)
}
if merr == io.EOF {
@@ -741,12 +749,11 @@ func mapToXmlIndent(doIndent bool, s *string, key string, value interface{}, pp
break
}
// simple element? Note: '#text" is an invalid XML tag.
if v, ok := vv["#text"]; ok {
if cntAttr+1 < lenvv {
return errors.New("#text key occurs with other non-attribute keys")
}
if v, ok := vv["#text"]; ok && cntAttr+1 == lenvv {
*s += ">" + fmt.Sprintf("%v", v)
endTag = true
elen = 1
isSimple = true
break
}
// close tag with possible attributes
@@ -771,9 +778,6 @@ func mapToXmlIndent(doIndent bool, s *string, key string, value interface{}, pp
sort.Sort(elemList(elemlist))
var i int
for _, v := range elemlist {
// if k[:1] == "-" {
// continue
// }
switch v[1].(type) {
case []interface{}:
default:
@@ -846,19 +850,15 @@ func mapToXmlIndent(doIndent bool, s *string, key string, value interface{}, pp
isSimple = true
endTag = true
}
if endTag {
if doIndent {
if !isSimple {
// if p.mapDepth == 0 {
// p.Outdent()
// }
*s += p.padding
}
}
switch value.(type) {
case map[string]interface{}, []byte, string, float64, bool, int, int32, int64, float32:
if elen > 0 || useGoXmlEmptyElemSyntax {
if elen > 0 || useGoXmlEmptyElemSyntax {
if elen == 0 {
*s += ">"
}
+322
View File
@@ -0,0 +1,322 @@
package mxj
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"os"
"testing"
)
func TestXml2Header(t *testing.T) {
fmt.Println("\n---------------- xml2_test.go ...\n")
}
func TestNewMapXml4(t *testing.T) {
x := []byte(`<doc>
<books>
<book seq="1">
<author>William T. Gaddis</author>
<title>The Recognitions</title>
<review>One of the great seminal American novels of the 20th century.</review>
</book>
<book seq="2">
<author>Austin Tappan Wright</author>
<title>Islandia</title>
<review>An example of earlier 20th century American utopian fiction.</review>
</book>
<book seq="3">
<author>John Hawkes</author>
<title>The Beetle Leg</title>
<review>A lyrical novel about the construction of Ft. Peck Dam in Montana.</review>
</book>
<book seq="4">
<author>
<first_name>T.E.</first_name>
<last_name>Porter</last_name>
</author>
<title>King's Day</title>
<review>A magical novella.</review>
</book>
</books>
</doc>`)
m, err := NewMapXml(x)
if err != nil && err != io.EOF {
t.Fatal("err:", err.Error())
}
fmt.Println("NewMapXml4, x:\n", string(x))
fmt.Println("NewMapXml4, m:\n", m)
fmt.Println("NewMapXml4, s:\n", m.StringIndent())
b, err := m.XmlIndent("", " ")
if err != nil {
t.Fatal("err:", err)
}
fmt.Println("NewMapXml4, b:\n", string(b))
}
func TestNewMapXml5(t *testing.T) {
fh, err := os.Open("songtext.xml")
if err != nil {
t.Fatal("err:", err.Error())
}
defer fh.Close()
m, raw, err := NewMapXmlReaderRaw(fh)
if err != nil && err != io.EOF {
t.Fatal("err:", err.Error())
}
fmt.Println("NewMapXml5, raw:\n", string(raw))
fmt.Println("NewMapXml5, m:\n", m)
fmt.Println("NewMapXml5, s:\n", m.StringIndent())
b, err := m.Xml()
if err != nil {
t.Fatal("err:", err)
}
fmt.Println("NewMapXml5, b:\n", string(b))
b, err = m.XmlIndent("", " ")
if err != nil {
t.Fatal("err:", err)
}
fmt.Println("NewMapXml5, b:\n", string(b))
}
func TestNewMapXml6(t *testing.T) {
fh, err := os.Open("atomFeedString.xml")
if err != nil {
t.Fatal("err:", err.Error())
}
defer fh.Close()
m, raw, err := NewMapXmlReaderRaw(fh)
if err != nil && err != io.EOF {
t.Fatal("err:", err.Error())
}
fmt.Println("NewMapXml6, raw:\n", string(raw))
fmt.Println("NewMapXml6, m:\n", m)
fmt.Println("NewMapXml6, s:\n", m.StringIndent())
b, err := m.Xml()
if err != nil {
t.Fatal("err:", err)
}
fmt.Println("NewMapXml6, b:\n", string(b))
b, err = m.XmlIndent("", " ")
if err != nil {
t.Fatal("err:", err)
}
fmt.Println("NewMapXml6, b:\n", string(b))
}
// ===================================== benchmarks ============================
var smallxml = []byte(`
<doc>
<words>
<word1>this</word1>
<word2>is</word2>
<word3>the</word3>
<word4>end</word4>
</words>
</doc>
`)
var smalljson = []byte(`{"doc":{"words":{"word1":"this","word2":"is","word3":"the","word4":"end"}}}`)
type words struct {
Word1 string `xml:"word1"`
Word2 string `xml:"word2"`
Word3 string `xml:"word3"`
Word4 string `xml:"word4"`
}
type xmldoc struct {
Words words `xml:"words"`
}
type jsondoc struct {
Doc xmldoc
}
func BenchmarkNewMapXml(b *testing.B) {
// var m Map
var err error
for i := 0; i < b.N; i++ {
if _, err = NewMapXml(smallxml); err != nil {
b.Fatal("err:", err)
}
}
// fmt.Println("m Map:", m)
}
func BenchmarkNewStructXml(b *testing.B) {
var s *xmldoc
var err error
for i := 0; i < b.N; i++ {
s = new(xmldoc)
if err = xml.Unmarshal(smallxml, s); err != nil {
b.Fatal("err:", err)
}
}
// fmt.Println("s xmldoc:", *s)
}
func BenchmarkNewMapJson(b *testing.B) {
var m map[string]interface{}
var err error
for i := 0; i < b.N; i++ {
m = make(map[string]interface{})
if err = json.Unmarshal(smalljson, &m); err != nil {
b.Fatal("err:", err)
}
}
// fmt.Println("m map:", m)
}
func BenchmarkNewStructJson(b *testing.B) {
var s *jsondoc
var err error
for i := 0; i < b.N; i++ {
s = new(jsondoc)
if err = json.Unmarshal(smalljson, s); err != nil {
b.Fatal("err:", err)
}
}
// fmt.Println("s jsondoc:", *s)
}
// ================== something with a little more content ... ===================
var xmlbooks = []byte(`
<doc>
<books>
<book seq="1">
<author>William T. Gaddis</author>
<title>The Recognitions</title>
<review>One of the great seminal American novels of the 20th century.</review>
</book>
<book seq="2">
<author>Austin Tappan Wright</author>
<title>Islandia</title>
<review>An example of earlier 20th century American utopian fiction.</review>
</book>
<book seq="3">
<author>John Hawkes</author>
<title>The Beetle Leg</title>
<review>A lyrical novel set during the construction of Ft. Peck Dam in Montana.</review>
</book>
<book seq="4">
<author>
<first_name>T.E.</first_name>
<last_name>Porter</last_name>
</author>
<title>King's Day</title>
<review>A magical novella.</review>
</book>
</books>
</doc>
`)
var jsonbooks = []byte(`
{"doc":
{"books":
{"book":[
{ "author":"William T. Gaddis",
"title":"The Recognitions",
"review":"One of the great seminal American novels of the 20th century."
},
{ "author":"Austin Tappan Wright",
"title":"Islandia",
"review":"An example of earlier 20th century American utopian fiction."
},
{ "author":"John Hawkes",
"title":"The Beetle Leg",
"review":"A lyrical novel set during the construction of Ft. Peck Dam in Montana."
},
{ "author":{"first_name":"T.E.", "last_name":"Porter"},
"title":"King's Day",
"review":"A magical novella."
}]
}
}
}
`)
type book struct {
Author string `xml:"author"`
Title string `xml:"title"`
Review string `xml:"review"`
}
type books struct {
Book []book `xml:"book"`
}
type doc struct {
Books books `xml:"books"`
}
type jsonbook struct {
Author json.RawMessage
Title string
Review string
}
type jsonbooks2 struct {
Book []jsonbook
}
type jsondoc1 struct {
Books jsonbooks2
}
type jsondoc2 struct {
Doc jsondoc1
}
func BenchmarkNewMapXmlBooks(b *testing.B) {
// var m Map
var err error
for i := 0; i < b.N; i++ {
if _, err = NewMapXml(xmlbooks); err != nil {
b.Fatal("err:", err)
}
}
// fmt.Println("m Map:", m)
}
func BenchmarkNewStructXmlBooks(b *testing.B) {
var s *doc
var err error
for i := 0; i < b.N; i++ {
s = new(doc)
if err = xml.Unmarshal(xmlbooks, s); err != nil {
b.Fatal("err:", err)
}
}
// fmt.Println("s doc:", *s)
}
func BenchmarkNewMapJsonBooks(b *testing.B) {
var m map[string]interface{}
var err error
for i := 0; i < b.N; i++ {
m = make(map[string]interface{})
if err = json.Unmarshal(jsonbooks, &m); err != nil {
b.Fatal("err:", err)
}
}
// fmt.Println("m map:", m)
}
func BenchmarkNewStructJsonBooks(b *testing.B) {
var s *jsondoc2
var err error
for i := 0; i < b.N; i++ {
s = new(jsondoc2)
if err = json.Unmarshal(jsonbooks, s); err != nil {
b.Fatal("err:", err)
}
}
// fmt.Println("s jsondoc2:", *s)
}
+723
View File
@@ -0,0 +1,723 @@
// Copyright 2012-2016 Charles Banning. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file
// xmlseq.go - version of xml.go with sequence # injection on Decoding and sorting on Encoding.
// Also, handles comments, directives and process instructions.
package mxj
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"sort"
"strings"
)
var NoRoot = errors.New("no root key")
var NO_ROOT = NoRoot // maintain backwards compatibility
// ------------------- NewMapXmlSeq & NewMapXmlSeqReader ... -------------------------
// This is only useful if you want to re-encode the Map as XML using mv.XmlSeq(), etc., to preserve the original structure.
//
// NewMapXmlSeq - convert a XML doc into a Map with elements id'd with decoding sequence int - #seq.
// If the optional argument 'cast' is 'true', then values will be converted to boolean or float64 if possible.
// NOTE: "#seq" key/value pairs are removed on encoding with mv.XmlSeq() / mv.XmlSeqIndent().
// • attributes are a map - map["#attr"]map["attr_key"]map[string]interface{}{"#text":<aval>, "#seq":<num>}
// • all simple elements are decoded as map["#text"]interface{} with a "#seq" k:v pair, as well.
// • lists always decode as map["list_tag"][]map[string]interface{} where the array elements are maps that
// include a "#seq" k:v pair based on sequence they are decoded. Thus, XML like:
// <doc>
// <ltag>value 1</ltag>
// <newtag>value 2</newtag>
// <ltag>value 3</ltag>
// </doc>
// is decoded as:
// doc :
// ltag :[[]interface{}]
// [item: 0]
// #seq :[int] 0
// #text :[string] value 1
// [item: 1]
// #seq :[int] 2
// #text :[string] value 3
// newtag :
// #seq :[int] 1
// #text :[string] value 2
// It will encode in proper sequence even though the Map representation merges all "ltag" elements in an array.
// • comments - "<!--comment-->" - are decoded as map["#comment"]map["#text"]"cmnt_text" with a "#seq" k:v pair.
// • directives - "<!text>" - are decoded as map["#directive"]map[#text"]"directive_text" with a "#seq" k:v pair.
// • process instructions - "<?instr?>" - are decoded as map["#procinst"]interface{} where the #procinst value
// is of map[string]interface{} type with the following keys: #target, #inst, and #seq.
// • comments, directives, and procinsts that are NOT part of a document with a root key will be returned as
// map[string]interface{} and the error value 'NoRoot'.
// • note: "<![CDATA[" syntax is lost in xml.Decode parser - and is not handled here, either.
// and: "\r\n" is converted to "\n"
//
// NOTES:
// 1. The 'xmlVal' will be parsed looking for an xml.StartElement, xml.Comment, etc., so BOM and other
// extraneous xml.CharData will be ignored unless io.EOF is reached first.
// 2. CoerceKeysToLower() is NOT recognized, since the intent here is to eventually call m.XmlSeq() to
// re-encode the message in its original structure.
func NewMapXmlSeq(xmlVal []byte, cast ...bool) (Map, error) {
var r bool
if len(cast) == 1 {
r = cast[0]
}
return xmlSeqToMap(xmlVal, r)
}
// This is only useful if you want to re-encode the Map as XML using mv.XmlSeq(), etc., to preserve the original structure.
//
// Get next XML doc from an io.Reader as a Map value. Returns Map value.
// NOTES:
// 1. The 'xmlReader' will be parsed looking for an xml.StartElement, xml.Comment, etc., so BOM and other
// extraneous xml.CharData will be ignored unless io.EOF is reached first.
// 2. CoerceKeysToLower() is NOT recognized, since the intent here is to eventually call m.XmlSeq() to
// re-encode the message in its original structure.
func NewMapXmlSeqReader(xmlReader io.Reader, cast ...bool) (Map, error) {
var r bool
if len(cast) == 1 {
r = cast[0]
}
// build the node tree
return xmlSeqReaderToMap(xmlReader, r)
}
// This is only useful if you want to re-encode the Map as XML using mv.XmlSeq(), etc., to preserve the original structure.
//
// Get next XML doc from an io.Reader as a Map value. Returns Map value and slice with the raw XML.
// NOTES:
// 1. Due to the implementation of xml.Decoder, the raw XML off the reader is buffered to []byte
// using a ByteReader. If the io.Reader is an os.File, there may be significant performance impact.
// See the examples - getmetrics1.go through getmetrics4.go - for comparative use cases on a large
// data set. If the io.Reader is wrapping a []byte value in-memory, however, such as http.Request.Body
// you CAN use it to efficiently unmarshal a XML doc and retrieve the raw XML in a single call.
// 2. The 'raw' return value may be larger than the XML text value.
// 3. The 'xmlReader' will be parsed looking for an xml.StartElement, xml.Comment, etc., so BOM and other
// extraneous xml.CharData will be ignored unless io.EOF is reached first.
// 4. CoerceKeysToLower() is NOT recognized, since the intent here is to eventually call m.XmlSeq() to
// re-encode the message in its original structure.
func NewMapXmlSeqReaderRaw(xmlReader io.Reader, cast ...bool) (Map, []byte, error) {
var r bool
if len(cast) == 1 {
r = cast[0]
}
// create TeeReader so we can retrieve raw XML
buf := make([]byte, XmlWriterBufSize)
wb := bytes.NewBuffer(buf)
trdr := myTeeReader(xmlReader, wb)
// build the node tree
m, err := xmlSeqReaderToMap(trdr, r)
// retrieve the raw XML that was decoded
b := make([]byte, wb.Len())
_, _ = wb.Read(b)
if err != nil {
return nil, b, err
}
return m, b, nil
}
// xmlSeqReaderToMap() - parse a XML io.Reader to a map[string]interface{} value
func xmlSeqReaderToMap(rdr io.Reader, r bool) (map[string]interface{}, error) {
// parse the Reader
p := xml.NewDecoder(rdr)
p.CharsetReader = XmlCharsetReader
return xmlSeqToMapParser("", nil, p, r)
}
// xmlSeqToMap - convert a XML doc into map[string]interface{} value
func xmlSeqToMap(doc []byte, r bool) (map[string]interface{}, error) {
b := bytes.NewReader(doc)
p := xml.NewDecoder(b)
p.CharsetReader = XmlCharsetReader
return xmlSeqToMapParser("", nil, p, r)
}
// ===================================== where the work happens =============================
// xmlSeqToMapParser - load a 'clean' XML doc into a map[string]interface{} directly.
// Add #seq tag value for each element decoded - to be used for Encoding later.
func xmlSeqToMapParser(skey string, a []xml.Attr, p *xml.Decoder, r bool) (map[string]interface{}, error) {
// NOTE: all attributes and sub-elements parsed into 'na', 'na' is returned as value for 'skey'
var n, na map[string]interface{}
var seq int // for including seq num when decoding
// Allocate maps and load attributes, if any.
if skey != "" {
// 'n' only needs one slot - save call to runtime•hashGrow()
// 'na' we don't know
n = make(map[string]interface{}, 1)
na = make(map[string]interface{})
if len(a) > 0 {
// xml.Attr is decoded into: map["#attr"]map[<attr_label>]interface{}
// where interface{} is map[string]interface{}{"#text":<attr_val>, "#seq":<attr_seq>}
aa := make(map[string]interface{}, len(a))
for i, v := range a {
aa[v.Name.Local] = map[string]interface{}{"#text": cast(v.Value, r), "#seq": i}
}
na["#attr"] = aa
}
}
for {
t, err := p.Token()
if err != nil {
if err != io.EOF {
return nil, errors.New("xml.Decoder.Token() - " + err.Error())
}
return nil, err
}
switch t.(type) {
case xml.StartElement:
tt := t.(xml.StartElement)
// First call to xmlSeqToMapParser() doesn't pass xml.StartElement - the map key.
// So when the loop is first entered, the first token is the root tag along
// with any attributes, which we process here.
//
// Subsequent calls to xmlSeqToMapParser() will pass in tag+attributes for
// processing before getting the next token which is the element value,
// which is done above.
if skey == "" {
return xmlSeqToMapParser(tt.Name.Local, tt.Attr, p, r)
}
// If not initializing the map, parse the element.
// len(nn) == 1, necessarily - it is just an 'n'.
nn, err := xmlSeqToMapParser(tt.Name.Local, tt.Attr, p, r)
if err != nil {
return nil, err
}
// The nn map[string]interface{} value is a na[nn_key] value.
// We need to see if nn_key already exists - means we're parsing a list.
// This may require converting na[nn_key] value into []interface{} type.
// First, extract the key:val for the map - it's a singleton.
var key string
var val interface{}
for key, val = range nn {
break
}
// add "#seq" k:v pair -
// Sequence number included even in list elements - this should allow us
// to properly resequence even something goofy like:
// <list>item 1</list>
// <subelement>item 2</subelement>
// <list>item 3</list>
// where all the "list" subelements are decoded into an array.
switch val.(type) {
case map[string]interface{}:
val.(map[string]interface{})["#seq"] = seq
seq++
case interface{}: // a non-nil simple element: string, float64, bool
v := map[string]interface{}{"#text": val, "#seq": seq}
seq++
val = v
}
// 'na' holding sub-elements of n.
// See if 'key' already exists.
// If 'key' exists, then this is a list, if not just add key:val to na.
if v, ok := na[key]; ok {
var a []interface{}
switch v.(type) {
case []interface{}:
a = v.([]interface{})
default: // anything else - note: v.(type) != nil
a = []interface{}{v}
}
a = append(a, val)
na[key] = a
} else {
na[key] = val // save it as a singleton
}
case xml.EndElement:
// len(n) > 0 if this is a simple element w/o xml.Attrs - see xml.CharData case.
if len(n) == 0 {
// If len(na)==0 we have an empty element == "";
// it has no xml.Attr nor xml.CharData.
// Empty element content will be map["etag"]map["#text"]""
// after #seq injection - map["etag"]map["#seq"]seq - after return.
if len(na) > 0 {
n[skey] = na
} else {
n[skey] = "" // empty element
}
}
return n, nil
case xml.CharData:
// clean up possible noise
tt := strings.Trim(string(t.(xml.CharData)), "\t\r\b\n ")
if skey == "" {
// per Adrian (http://www.adrianlungu.com/) catch stray text
// in decoder stream -
// https://github.com/clbanning/mxj/pull/14#issuecomment-182816374
// NOTE: CharSetReader must be set to non-UTF-8 CharSet or you'll get
// a p.Token() decoding error when the BOM is UTF-16 or UTF-32.
continue
}
if len(tt) > 0 {
// every simple element is a #text and has #seq associated with it
na["#text"] = cast(tt, r)
na["#seq"] = seq
seq++
}
case xml.Comment:
if n == nil { // no root 'key'
n = map[string]interface{}{"#comment": string(t.(xml.Comment))}
return n, NoRoot
}
cm := make(map[string]interface{}, 2)
cm["#text"] = string(t.(xml.Comment))
cm["#seq"] = seq
seq++
na["#comment"] = cm
case xml.Directive:
if n == nil { // no root 'key'
n = map[string]interface{}{"#directive": string(t.(xml.Directive))}
return n, NoRoot
}
dm := make(map[string]interface{}, 2)
dm["#text"] = string(t.(xml.Directive))
dm["#seq"] = seq
seq++
na["#directive"] = dm
case xml.ProcInst:
if n == nil {
na = map[string]interface{}{"#target": t.(xml.ProcInst).Target, "#inst": string(t.(xml.ProcInst).Inst)}
n = map[string]interface{}{"#procinst": na}
return n, NoRoot
}
pm := make(map[string]interface{}, 3)
pm["#target"] = t.(xml.ProcInst).Target
pm["#inst"] = string(t.(xml.ProcInst).Inst)
pm["#seq"] = seq
seq++
na["#procinst"] = pm
default:
// noop - shouldn't ever get here, now, since we handle all token types
}
}
}
// ------------------ END: NewMapXml & NewMapXmlReader -------------------------
// ------------------ mv.Xml & mv.XmlWriter - from j2x ------------------------
// This should ONLY be used on Map values that were decoded using NewMapXmlSeq() & co.
//
// Encode a Map as XML with elements sorted on #seq. The companion of NewMapXmlSeq().
// The following rules apply.
// - The key label "#text" is treated as the value for a simple element with attributes.
// - The "#seq" key is used to seqence the subelements or attributes but is ignored for writing.
// - The "#attr" map key identifies the map of attribute map[string]interface{} values with "#text" key.
// - The "#comment" map key identifies a comment in the value "#text" map entry - <!--comment-->.
// - The "#directive" map key identifies a directive in the value "#text" map entry - <!directive>.
// - The "#procinst" map key identifies a process instruction in the value "#target" and "#inst"
// map entries - <?target inst?>.
// - Value type encoding:
// > string, bool, float64, int, int32, int64, float32: per "%v" formating
// > []bool, []uint8: by casting to string
// > structures, etc.: handed to xml.Marshal() - if there is an error, the element
// value is "UNKNOWN"
// - Elements with only attribute values or are null are terminated using "/>" unless XmlGoEmptyElemSystax() called.
// - If len(mv) == 1 and no rootTag is provided, then the map key is used as the root tag, possible.
// Thus, `{ "key":"value" }` encodes as "<key>value</key>".
func (mv Map) XmlSeq(rootTag ...string) ([]byte, error) {
m := map[string]interface{}(mv)
var err error
s := new(string)
p := new(pretty) // just a stub
if len(m) == 1 && len(rootTag) == 0 {
for key, value := range m {
// if it an array, see if all values are map[string]interface{}
// we force a new root tag if we'll end up with no key:value in the list
// so: key:[string_val, bool:true] --> <doc><key>string_val</key><bool>true</bool></doc>
switch value.(type) {
case []interface{}:
for _, v := range value.([]interface{}) {
switch v.(type) {
case map[string]interface{}: // noop
default: // anything else
err = mapToXmlSeqIndent(false, s, DefaultRootTag, m, p)
goto done
}
}
}
err = mapToXmlSeqIndent(false, s, key, value, p)
}
} else if len(rootTag) == 1 {
err = mapToXmlSeqIndent(false, s, rootTag[0], m, p)
} else {
err = mapToXmlSeqIndent(false, s, DefaultRootTag, m, p)
}
done:
return []byte(*s), err
}
// The following implementation is provided only for symmetry with NewMapXmlReader[Raw]
// The names will also provide a key for the number of return arguments.
// This should ONLY be used on Map values that were decoded using NewMapXmlSeq() & co.
//
// Writes the Map as XML on the Writer.
// See Xml() for encoding rules.
func (mv Map) XmlSeqWriter(xmlWriter io.Writer, rootTag ...string) error {
x, err := mv.XmlSeq(rootTag...)
if err != nil {
return err
}
_, err = xmlWriter.Write(x)
return err
}
// This should ONLY be used on Map values that were decoded using NewMapXmlSeq() & co.
//
// Writes the Map as XML on the Writer. []byte is the raw XML that was written.
// See Xml() for encoding rules.
func (mv Map) XmlSeqWriterRaw(xmlWriter io.Writer, rootTag ...string) ([]byte, error) {
x, err := mv.XmlSeq(rootTag...)
if err != nil {
return x, err
}
_, err = xmlWriter.Write(x)
return x, err
}
// This should ONLY be used on Map values that were decoded using NewMapXmlSeq() & co.
//
// Writes the Map as pretty XML on the Writer.
// See Xml() for encoding rules.
func (mv Map) XmlSeqIndentWriter(xmlWriter io.Writer, prefix, indent string, rootTag ...string) error {
x, err := mv.XmlSeqIndent(prefix, indent, rootTag...)
if err != nil {
return err
}
_, err = xmlWriter.Write(x)
return err
}
// This should ONLY be used on Map values that were decoded using NewMapXmlSeq() & co.
//
// Writes the Map as pretty XML on the Writer. []byte is the raw XML that was written.
// See Xml() for encoding rules.
func (mv Map) XmlSeqIndentWriterRaw(xmlWriter io.Writer, prefix, indent string, rootTag ...string) ([]byte, error) {
x, err := mv.XmlSeqIndent(prefix, indent, rootTag...)
if err != nil {
return x, err
}
_, err = xmlWriter.Write(x)
return x, err
}
// -------------------- END: mv.Xml & mv.XmlWriter -------------------------------
// ---------------------- XmlSeqIndent ----------------------------
// This should ONLY be used on Map values that were decoded using NewMapXmlSeq() & co.
//
// Encode a map[string]interface{} as a pretty XML string.
// See mv.XmlSeq() for encoding rules.
func (mv Map) XmlSeqIndent(prefix, indent string, rootTag ...string) ([]byte, error) {
m := map[string]interface{}(mv)
var err error
s := new(string)
p := new(pretty)
p.indent = indent
p.padding = prefix
if len(m) == 1 && len(rootTag) == 0 {
// this can extract the key for the single map element
// use it if it isn't a key for a list
for key, value := range m {
if _, ok := value.([]interface{}); ok {
err = mapToXmlSeqIndent(true, s, DefaultRootTag, m, p)
} else {
err = mapToXmlSeqIndent(true, s, key, value, p)
}
}
} else if len(rootTag) == 1 {
err = mapToXmlSeqIndent(true, s, rootTag[0], m, p)
} else {
err = mapToXmlSeqIndent(true, s, DefaultRootTag, m, p)
}
return []byte(*s), err
}
// where the work actually happens
// returns an error if an attribute is not atomic
func mapToXmlSeqIndent(doIndent bool, s *string, key string, value interface{}, pp *pretty) error {
var endTag bool
var isSimple bool
var noEndTag bool
var elen int
p := &pretty{pp.indent, pp.cnt, pp.padding, pp.mapDepth, pp.start}
switch value.(type) {
case map[string]interface{}, []byte, string, float64, bool, int, int32, int64, float32:
if doIndent {
*s += p.padding
}
if key != "#comment" && key != "#directive" && key != "#procinst" {
*s += `<` + key
}
}
switch value.(type) {
case map[string]interface{}:
val := value.(map[string]interface{})
if key == "#comment" {
*s += `<!--` + val["#text"].(string) + `-->`
noEndTag = true
break
}
if key == "#directive" {
*s += `<!` + val["#text"].(string) + `>`
noEndTag = true
break
}
if key == "#procinst" {
*s += `<?` + val["#target"].(string) + ` ` + val["#inst"].(string) + `?>`
noEndTag = true
break
}
haveAttrs := false
// process attributes first
if v, ok := val["#attr"].(map[string]interface{}); ok {
// First, unroll the map[string]interface{} into a []keyval array.
// Then sequence it.
kv := make([]keyval, len(v))
n := 0
for ak, av := range v {
kv[n] = keyval{ak, av}
n++
}
sort.Sort(elemListSeq(kv))
// Now encode the attributes in original decoding sequence, using keyval array.
for _, a := range kv {
vv := a.v.(map[string]interface{})
switch vv["#text"].(type) {
case string, float64, bool, int, int32, int64, float32:
*s += ` ` + a.k + `="` + fmt.Sprintf("%v", vv["#text"]) + `"`
case []byte:
*s += ` ` + a.k + `="` + fmt.Sprintf("%v", string(vv["#text"].([]byte))) + `"`
default:
return fmt.Errorf("invalid attribute value for: %s", a.k)
}
}
haveAttrs = true
}
// simple element?
// every map value has, at least, "#seq" and, perhaps, "#text" and/or "#attr"
_, seqOK := val["#seq"] // have key
if v, ok := val["#text"]; ok && ((len(val) == 3 && haveAttrs) || (len(val) == 2 && !haveAttrs)) && seqOK {
if stmp, ok := v.(string); ok && stmp != "" {
*s += ">" + fmt.Sprintf("%v", v)
endTag = true
elen = 1
}
isSimple = true
break
} else if !ok && ((len(val) == 2 && haveAttrs) || (len(val) == 1 && !haveAttrs)) && seqOK {
// here no #text but have #seq or #seq+#attr
endTag = false
break
}
// we now need to sequence everything except attributes
// 'kv' will hold everything that needs to be written
kv := make([]keyval, 0)
for k, v := range val {
if k == "#attr" { // already processed
continue
}
if k == "#seq" { // ignore - just for sorting
continue
}
switch v.(type) {
case []interface{}:
// unwind the array as separate entries
for _, vv := range v.([]interface{}) {
kv = append(kv, keyval{k, vv})
}
default:
kv = append(kv, keyval{k, v})
}
}
// close tag with possible attributes
*s += ">"
if doIndent {
*s += "\n"
}
// something more complex
p.mapDepth++
// PrintElemListSeq(elemListSeq(kv))
sort.Sort(elemListSeq(kv))
// PrintElemListSeq(elemListSeq(kv))
i := 0
for _, v := range kv {
switch v.v.(type) {
case []interface{}:
default:
if i == 0 && doIndent {
p.Indent()
}
}
i++
mapToXmlSeqIndent(doIndent, s, v.k, v.v, p)
switch v.v.(type) {
case []interface{}: // handled in []interface{} case
default:
if doIndent {
p.Outdent()
}
}
i--
}
p.mapDepth--
endTag = true
elen = 1 // we do have some content other than attrs
case []interface{}:
for _, v := range value.([]interface{}) {
if doIndent {
p.Indent()
}
mapToXmlSeqIndent(doIndent, s, key, v, p)
if doIndent {
p.Outdent()
}
}
return nil
case nil:
// terminate the tag
*s += "<" + key
break
default: // handle anything - even goofy stuff
elen = 0
switch value.(type) {
case string, float64, bool, int, int32, int64, float32:
v := fmt.Sprintf("%v", value)
elen = len(v)
if elen > 0 {
*s += ">" + v
}
case []byte: // NOTE: byte is just an alias for uint8
// similar to how xml.Marshal handles []byte structure members
v := string(value.([]byte))
elen = len(v)
if elen > 0 {
*s += ">" + v
}
default:
var v []byte
var err error
if doIndent {
v, err = xml.MarshalIndent(value, p.padding, p.indent)
} else {
v, err = xml.Marshal(value)
}
if err != nil {
*s += ">UNKNOWN"
} else {
elen = len(v)
if elen > 0 {
*s += string(v)
}
}
}
isSimple = true
endTag = true
}
if endTag && !noEndTag {
if doIndent {
if !isSimple {
*s += p.padding
}
}
switch value.(type) {
case map[string]interface{}, []byte, string, float64, bool, int, int32, int64, float32:
if elen > 0 || useGoXmlEmptyElemSyntax {
if elen == 0 {
*s += ">"
}
*s += `</` + key + ">"
} else {
*s += `/>`
}
}
} else if !noEndTag {
if useGoXmlEmptyElemSyntax {
*s += "></" + key + ">"
} else {
*s += "/>"
}
}
if doIndent {
if p.cnt > p.start {
*s += "\n"
}
p.Outdent()
}
return nil
}
// the element sort implementation
type keyval struct {
k string
v interface{}
}
type elemListSeq []keyval
func (e elemListSeq) Len() int {
return len(e)
}
func (e elemListSeq) Swap(i, j int) {
e[i], e[j] = e[j], e[i]
}
func (e elemListSeq) Less(i, j int) bool {
var iseq, jseq int
var ok bool
if iseq, ok = e[i].v.(map[string]interface{})["#seq"].(int); !ok {
iseq = 9999999
}
if jseq, ok = e[j].v.(map[string]interface{})["#seq"].(int); !ok {
jseq = 9999999
}
if iseq > jseq {
return false
}
return true
}
func PrintElemListSeq(e elemListSeq) {
for n, v := range e {
fmt.Printf("%d: %v\n", n, v)
}
}
+67
View File
@@ -0,0 +1,67 @@
package mxj
import (
"fmt"
"io"
"testing"
)
func TestXmlSeqHeader(t *testing.T) {
fmt.Println("\n---------------- xmlseq_test.go ...\n")
}
func TestNewMapXmlSeq(t *testing.T) {
x := []byte(`<doc>
<books>
<book seq="1">
<author>William T. Gaddis</author>
<review>Gaddis is one of the most influential but little know authors in America.</review>
<title>The Recognitions</title>
<!-- here's the rest of the review -->
<review>One of the great seminal American novels of the 20th century.</review>
<review>Without it Thomas Pynchon probably wouldn't have written Gravity's Rainbow.</review>
</book>
<book seq="2">
<author>Austin Tappan Wright</author>
<title>Islandia</title>
<review>An example of earlier 20th century American utopian fiction.</review>
</book>
<book>
<author>John Hawkes</author>
<title>The Beetle Leg</title>
<!throw in a directive here>
<review>A lyrical novel about the construction of Ft. Peck Dam in Montana.</review>
</book>
<book>
<author>
<?cat first_name last_name?>
<first_name>T.E.</first_name>
<last_name>Porter</last_name>
</author>
<title>King's Day</title>
<review>A magical novella.</review>
</book>
</books>
</doc>`)
m, err := NewMapXmlSeq(x)
if err != nil && err != io.EOF {
t.Fatal("err:", err.Error())
}
fmt.Println("NewMapXmlSeq, x:\n", string(x))
// fmt.Println("NewMapXmlSeq, m:\n", m)
fmt.Println("NewMapXmlSeq, s:\n", m.StringIndent())
b, err := m.XmlIndent("", " ")
if err != nil {
t.Fatal("err:", err)
}
fmt.Println("NewMapXmlSeq, mv.XmlIndent():\n", string(b))
b, err = m.XmlSeqIndent("", " ")
if err != nil {
t.Fatal("err:", err)
}
fmt.Println("NewMapXmlSeq, mv.XmlSeqIndent():\n", string(b))
}