mirror of
https://github.com/adnanh/webhook.git
synced 2025-05-10 23:54:42 +00:00
parent
3f4520da67
commit
3463804a7c
42 changed files with 5174 additions and 4 deletions
|
@ -424,3 +424,53 @@ Travis sends webhooks as `payload=<JSON_STRING>`, so the payload needs to be par
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## XML Payload
|
||||||
|
|
||||||
|
Given the following payload:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<app>
|
||||||
|
<users>
|
||||||
|
<user id="1" name="Jeff" />
|
||||||
|
<user id="2" name="Sally" />
|
||||||
|
</users>
|
||||||
|
<messages>
|
||||||
|
<message id="1" from_user="1" to_user="2">Hello!!</message>
|
||||||
|
</messages>
|
||||||
|
</app>
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "deploy",
|
||||||
|
"execute-command": "/root/my-server/deployment.sh",
|
||||||
|
"command-working-directory": "/root/my-server",
|
||||||
|
"trigger-rule": {
|
||||||
|
"and": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"type": "value",
|
||||||
|
"parameter": {
|
||||||
|
"source": "payload",
|
||||||
|
"name": "app.users.user.0.-name"
|
||||||
|
},
|
||||||
|
"value": "Jeff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"type": "value",
|
||||||
|
"parameter": {
|
||||||
|
"source": "payload",
|
||||||
|
"name": "app.messages.message.#text"
|
||||||
|
},
|
||||||
|
"value": "Hello!!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
|
@ -57,6 +57,31 @@ There are three types of request values:
|
||||||
|
|
||||||
If the payload contains a key with the specified name "commits.0.commit.id", then the value of that key has priority over the dot-notation referencing.
|
If the payload contains a key with the specified name "commits.0.commit.id", then the value of that key has priority over the dot-notation referencing.
|
||||||
|
|
||||||
|
3. XML Payload
|
||||||
|
|
||||||
|
Referencing XML payload parameters is much like the JSON examples above, but XML is more complex.
|
||||||
|
Take the following XML payload:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<app>
|
||||||
|
<users>
|
||||||
|
<user id="1" name="Jeff" />
|
||||||
|
<user id="2" name="Sally" />
|
||||||
|
</users>
|
||||||
|
<messages>
|
||||||
|
<message id="1" from_user="1" to_user="2">Hello!!</message>
|
||||||
|
</messages>
|
||||||
|
</app>
|
||||||
|
```
|
||||||
|
|
||||||
|
To access a given `user` tag, you must treat them as an array.
|
||||||
|
So `app.users.user.0.name` yields `Jeff`.
|
||||||
|
|
||||||
|
Since there's only one `message` tag, it's not treated as an array.
|
||||||
|
So `app.messages.message.id` yields `1`.
|
||||||
|
|
||||||
|
To access the text within the `message` tag, you would use: `app.messages.message.#text`.
|
||||||
|
|
||||||
If you are referencing values for environment, you can use `envname` property to set the name of the environment variable like so
|
If you are referencing values for environment, you can use `envname` property to set the name of the environment variable like so
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -3,6 +3,7 @@ module github.com/adnanh/webhook
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/clbanning/mxj v1.8.4
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/fsnotify/fsnotify v1.4.7 // indirect
|
github.com/fsnotify/fsnotify v1.4.7 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0
|
github.com/ghodss/yaml v1.0.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -1,3 +1,5 @@
|
||||||
|
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
|
||||||
|
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
|
||||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
|
|
|
@ -137,6 +137,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "xml",
|
||||||
|
"execute-command": "{{ .Hookecho }}",
|
||||||
|
"command-working-directory": "/",
|
||||||
|
"response-message": "success",
|
||||||
|
"trigger-rule": {
|
||||||
|
"and": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"type": "value",
|
||||||
|
"parameter": {
|
||||||
|
"source": "payload",
|
||||||
|
"name": "app.users.user.0.-name"
|
||||||
|
},
|
||||||
|
"value": "Jeff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"type": "value",
|
||||||
|
"parameter": {
|
||||||
|
"source": "payload",
|
||||||
|
"name": "app.messages.message.#text"
|
||||||
|
},
|
||||||
|
"value": "Hello!!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "capture-command-output-on-success-not-by-default",
|
"id": "capture-command-output-on-success-not-by-default",
|
||||||
"pass-arguments-to-command": [
|
"pass-arguments-to-command": [
|
||||||
|
|
|
@ -76,6 +76,25 @@
|
||||||
include-command-output-in-response: true
|
include-command-output-in-response: true
|
||||||
command-working-directory: /
|
command-working-directory: /
|
||||||
|
|
||||||
|
- id: xml
|
||||||
|
execute-command: '{{ .Hookecho }}'
|
||||||
|
command-working-directory: /
|
||||||
|
response-message: success
|
||||||
|
trigger-rule:
|
||||||
|
and:
|
||||||
|
- match:
|
||||||
|
type: value
|
||||||
|
parameter:
|
||||||
|
source: payload
|
||||||
|
name: app.users.user.0.-name
|
||||||
|
value: Jeff
|
||||||
|
- match:
|
||||||
|
type: value
|
||||||
|
parameter:
|
||||||
|
source: payload
|
||||||
|
name: "app.messages.message.#text"
|
||||||
|
value: "Hello!!"
|
||||||
|
|
||||||
- id: capture-command-output-on-success-not-by-default
|
- id: capture-command-output-on-success-not-by-default
|
||||||
pass-arguments-to-command:
|
pass-arguments-to-command:
|
||||||
- source: string
|
- source: string
|
||||||
|
|
55
vendor/github.com/clbanning/mxj/LICENSE
generated
vendored
Normal file
55
vendor/github.com/clbanning/mxj/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
Copyright (c) 2012-2016 Charles Banning <clbanning@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
Go Language Copyright & License -
|
||||||
|
|
||||||
|
Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
Use of this source code is governed by a BSD-style
|
||||||
|
license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
189
vendor/github.com/clbanning/mxj/anyxml.go
generated
vendored
Normal file
189
vendor/github.com/clbanning/mxj/anyxml.go
generated
vendored
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultElementTag = "element"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encode arbitrary value as XML.
|
||||||
|
//
|
||||||
|
// Note: unmarshaling the resultant
|
||||||
|
// XML may not return the original value, since tag labels may have been injected
|
||||||
|
// to create the XML representation of the value.
|
||||||
|
/*
|
||||||
|
Encode an arbitrary JSON object.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/clbanning/mxj"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
jsondata := []byte(`[
|
||||||
|
{ "somekey":"somevalue" },
|
||||||
|
"string",
|
||||||
|
3.14159265,
|
||||||
|
true
|
||||||
|
]`)
|
||||||
|
var i interface{}
|
||||||
|
err := json.Unmarshal(jsondata, &i)
|
||||||
|
if err != nil {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
x, err := mxj.AnyXmlIndent(i, "", " ", "mydoc")
|
||||||
|
if err != nil {
|
||||||
|
// do something else
|
||||||
|
}
|
||||||
|
fmt.Println(string(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
output:
|
||||||
|
<mydoc>
|
||||||
|
<somekey>somevalue</somekey>
|
||||||
|
<element>string</element>
|
||||||
|
<element>3.14159265</element>
|
||||||
|
<element>true</element>
|
||||||
|
</mydoc>
|
||||||
|
*/
|
||||||
|
// Alternative values for DefaultRootTag and DefaultElementTag can be set as:
|
||||||
|
// AnyXml( v, myRootTag, myElementTag).
|
||||||
|
func AnyXml(v interface{}, tags ...string) ([]byte, error) {
|
||||||
|
var rt, et string
|
||||||
|
if len(tags) == 1 || len(tags) == 2 {
|
||||||
|
rt = tags[0]
|
||||||
|
} else {
|
||||||
|
rt = DefaultRootTag
|
||||||
|
}
|
||||||
|
if len(tags) == 2 {
|
||||||
|
et = tags[1]
|
||||||
|
} else {
|
||||||
|
et = DefaultElementTag
|
||||||
|
}
|
||||||
|
|
||||||
|
if v == nil {
|
||||||
|
if useGoXmlEmptyElemSyntax {
|
||||||
|
return []byte("<" + rt + "></" + rt + ">"), nil
|
||||||
|
}
|
||||||
|
return []byte("<" + rt + "/>"), nil
|
||||||
|
}
|
||||||
|
if reflect.TypeOf(v).Kind() == reflect.Struct {
|
||||||
|
return xml.Marshal(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
s := new(string)
|
||||||
|
p := new(pretty)
|
||||||
|
|
||||||
|
var ss string
|
||||||
|
var b []byte
|
||||||
|
switch v.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
ss = "<" + rt + ">"
|
||||||
|
for _, vv := range v.([]interface{}) {
|
||||||
|
switch vv.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
m := vv.(map[string]interface{})
|
||||||
|
if len(m) == 1 {
|
||||||
|
for tag, val := range m {
|
||||||
|
err = mapToXmlIndent(false, s, tag, val, p)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = mapToXmlIndent(false, s, et, vv, p)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = mapToXmlIndent(false, s, et, vv, p)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ss += *s + "</" + rt + ">"
|
||||||
|
b = []byte(ss)
|
||||||
|
case map[string]interface{}:
|
||||||
|
m := Map(v.(map[string]interface{}))
|
||||||
|
b, err = m.Xml(rt)
|
||||||
|
default:
|
||||||
|
err = mapToXmlIndent(false, s, rt, v, p)
|
||||||
|
b = []byte(*s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode an arbitrary value as a pretty XML string.
|
||||||
|
// Alternative values for DefaultRootTag and DefaultElementTag can be set as:
|
||||||
|
// AnyXmlIndent( v, "", " ", myRootTag, myElementTag).
|
||||||
|
func AnyXmlIndent(v interface{}, prefix, indent string, tags ...string) ([]byte, error) {
|
||||||
|
var rt, et string
|
||||||
|
if len(tags) == 1 || len(tags) == 2 {
|
||||||
|
rt = tags[0]
|
||||||
|
} else {
|
||||||
|
rt = DefaultRootTag
|
||||||
|
}
|
||||||
|
if len(tags) == 2 {
|
||||||
|
et = tags[1]
|
||||||
|
} else {
|
||||||
|
et = DefaultElementTag
|
||||||
|
}
|
||||||
|
|
||||||
|
if v == nil {
|
||||||
|
if useGoXmlEmptyElemSyntax {
|
||||||
|
return []byte(prefix + "<" + rt + "></" + rt + ">"), nil
|
||||||
|
}
|
||||||
|
return []byte(prefix + "<" + rt + "/>"), nil
|
||||||
|
}
|
||||||
|
if reflect.TypeOf(v).Kind() == reflect.Struct {
|
||||||
|
return xml.MarshalIndent(v, prefix, indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
s := new(string)
|
||||||
|
p := new(pretty)
|
||||||
|
p.indent = indent
|
||||||
|
p.padding = prefix
|
||||||
|
|
||||||
|
var ss string
|
||||||
|
var b []byte
|
||||||
|
switch v.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
ss = "<" + rt + ">\n"
|
||||||
|
p.Indent()
|
||||||
|
for _, vv := range v.([]interface{}) {
|
||||||
|
switch vv.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
m := vv.(map[string]interface{})
|
||||||
|
if len(m) == 1 {
|
||||||
|
for tag, val := range m {
|
||||||
|
err = mapToXmlIndent(true, s, tag, val, p)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.start = 1 // we 1 tag in
|
||||||
|
err = mapToXmlIndent(true, s, et, vv, p)
|
||||||
|
*s += "\n"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
p.start = 0 // in case trailing p.start = 1
|
||||||
|
err = mapToXmlIndent(true, s, et, vv, p)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ss += *s + "</" + rt + ">"
|
||||||
|
b = []byte(ss)
|
||||||
|
case map[string]interface{}:
|
||||||
|
m := Map(v.(map[string]interface{}))
|
||||||
|
b, err = m.XmlIndent(prefix, indent, rt)
|
||||||
|
default:
|
||||||
|
err = mapToXmlIndent(true, s, rt, v, p)
|
||||||
|
b = []byte(*s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, err
|
||||||
|
}
|
54
vendor/github.com/clbanning/mxj/atomFeedString.xml
generated
vendored
Normal file
54
vendor/github.com/clbanning/mxj/atomFeedString.xml
generated
vendored
Normal 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<></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 &lt;link rel=&quot;hub&quot; href=&quot;hub-server&quot;&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&#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&#39;s actual URL in
|
||||||
|
the link rel=&quot;self&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&#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> `
|
||||||
|
|
134
vendor/github.com/clbanning/mxj/doc.go
generated
vendored
Normal file
134
vendor/github.com/clbanning/mxj/doc.go
generated
vendored
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
// mxj - A collection of map[string]interface{} and associated XML and JSON utilities.
|
||||||
|
// Copyright 2012-2015, 2018 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
|
||||||
|
|
||||||
|
/*
|
||||||
|
Marshal/Unmarshal XML to/from map[string]interface{} values (and JSON); extract/modify values from maps by key or key-path, including wildcards.
|
||||||
|
|
||||||
|
mxj supplants the legacy x2j and j2x packages. The subpackage x2j-wrapper is provided to facilitate migrating from the x2j package. The x2j and j2x subpackages provide similar functionality of the old packages but are not function-name compatible with them.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Related Packages:
|
||||||
|
checkxml: github.com/clbanning/checkxml provides functions for validating XML data.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
2018.04.18: mv.Xml/mv.XmlIndent encodes non-map[string]interface{} map values - map[string]string, map[int]uint, etc.
|
||||||
|
2018.03.29: mv.Gob/NewMapGob support gob encoding/decoding of Maps.
|
||||||
|
2018.03.26: Added mxj/x2j-wrapper sub-package for migrating from legacy x2j package.
|
||||||
|
2017.02.22: LeafNode paths can use ".N" syntax rather than "[N]" for list member indexing.
|
||||||
|
2017.02.21: github.com/clbanning/checkxml provides functions for validating XML data.
|
||||||
|
2017.02.10: SetFieldSeparator changes field separator for args in UpdateValuesForPath, ValuesFor... methods.
|
||||||
|
2017.02.06: Support XMPP stream processing - HandleXMPPStreamTag().
|
||||||
|
2016.11.07: Preserve name space prefix syntax in XmlSeq parser - NewMapXmlSeq(), etc.
|
||||||
|
2016.06.25: Support overriding default XML attribute prefix, "-", in Map keys - SetAttrPrefix().
|
||||||
|
2016.05.26: Support customization of xml.Decoder by exposing CustomDecoder variable.
|
||||||
|
2016.03.19: Escape invalid chars when encoding XML attribute and element values - XMLEscapeChars().
|
||||||
|
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 mv.Root(), mv.Elements(), mv.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: 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
|
||||||
|
|
||||||
|
type Map map[string]interface{}
|
||||||
|
|
||||||
|
Create a Map value, 'mv', from any map[string]interface{} value, 'v':
|
||||||
|
mv := Map(v)
|
||||||
|
|
||||||
|
Unmarshal / marshal XML as a Map value, 'mv':
|
||||||
|
mv, err := NewMapXml(xmlValue) // unmarshal
|
||||||
|
xmlValue, err := mv.Xml() // marshal
|
||||||
|
|
||||||
|
Unmarshal XML from an io.Reader as a Map value, 'mv':
|
||||||
|
mv, err := NewMapXmlReader(xmlReader) // repeated calls, as with an os.File Reader, will process stream
|
||||||
|
mv, raw, err := NewMapXmlReaderRaw(xmlReader) // 'raw' is the raw XML that was decoded
|
||||||
|
|
||||||
|
Marshal Map value, 'mv', to an XML Writer (io.Writer):
|
||||||
|
err := mv.XmlWriter(xmlWriter)
|
||||||
|
raw, err := mv.XmlWriterRaw(xmlWriter) // 'raw' is the raw XML that was written on xmlWriter
|
||||||
|
|
||||||
|
Also, for prettified output:
|
||||||
|
xmlValue, err := mv.XmlIndent(prefix, indent, ...)
|
||||||
|
err := mv.XmlIndentWriter(xmlWriter, prefix, indent, ...)
|
||||||
|
raw, err := mv.XmlIndentWriterRaw(xmlWriter, prefix, indent, ...)
|
||||||
|
|
||||||
|
Bulk process XML with error handling (note: handlers must return a boolean value):
|
||||||
|
err := HandleXmlReader(xmlReader, mapHandler(Map), errHandler(error))
|
||||||
|
err := HandleXmlReaderRaw(xmlReader, mapHandler(Map, []byte), errHandler(error, []byte))
|
||||||
|
|
||||||
|
Converting XML to JSON: see Examples for NewMapXml and HandleXmlReader.
|
||||||
|
|
||||||
|
There are comparable functions and methods for JSON processing.
|
||||||
|
|
||||||
|
Arbitrary structure values can be decoded to / encoded from Map values:
|
||||||
|
mv, err := NewMapStruct(structVal)
|
||||||
|
err := mv.Struct(structPointer)
|
||||||
|
|
||||||
|
To work with XML tag values, JSON or Map key values or structure field values, decode the XML, JSON
|
||||||
|
or structure to a Map value, 'mv', or cast a map[string]interface{} value to a Map value, 'mv', then:
|
||||||
|
paths := mv.PathsForKey(key)
|
||||||
|
path := mv.PathForKeyShortest(key)
|
||||||
|
values, err := mv.ValuesForKey(key, subkeys)
|
||||||
|
values, err := mv.ValuesForPath(path, subkeys) // 'path' can be dot-notation with wildcards and indexed arrays.
|
||||||
|
count, err := mv.UpdateValuesForPath(newVal, path, subkeys)
|
||||||
|
|
||||||
|
Get everything at once, irrespective of path depth:
|
||||||
|
leafnodes := mv.LeafNodes()
|
||||||
|
leafvalues := mv.LeafValues()
|
||||||
|
|
||||||
|
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, err := mv.NewMap("oldKey_1:newKey_1", "oldKey_2:newKey_2", ..., "oldKey_N:newKey_N")
|
||||||
|
newMap, err := mv.NewMap("oldKey1", "oldKey3", "oldKey5") // a subset of 'mv'; see "examples/partial.go"
|
||||||
|
newXml, err := newMap.Xml() // for example
|
||||||
|
newJson, err := newMap.Json() // ditto
|
||||||
|
|
||||||
|
XML PARSING CONVENTIONS
|
||||||
|
|
||||||
|
Using NewMapXml()
|
||||||
|
|
||||||
|
- Attributes are parsed to `map[string]interface{}` values by prefixing a hyphen, `-`,
|
||||||
|
to the attribute label. (Unless overridden by `PrependAttrWithHyphen(false)` or
|
||||||
|
`SetAttrPrefix()`.)
|
||||||
|
- 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 NewMapXmlSeq()
|
||||||
|
|
||||||
|
- 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.)
|
||||||
|
- Name space syntax is preserved:
|
||||||
|
- <ns:key>something</ns.key> parses to map["ns:key"]interface{}{"something"}
|
||||||
|
- xmlns:ns="http://myns.com/ns" parses to map["xmlns:ns"]interface{}{"http://myns.com/ns"}
|
||||||
|
|
||||||
|
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..
|
||||||
|
- 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
|
||||||
|
mv.XmlSeq() - these try to preserve the element sequencing but with added complexity when
|
||||||
|
working with the Map representation.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package mxj
|
54
vendor/github.com/clbanning/mxj/escapechars.go
generated
vendored
Normal file
54
vendor/github.com/clbanning/mxj/escapechars.go
generated
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var xmlEscapeChars bool
|
||||||
|
|
||||||
|
// XMLEscapeChars(true) forces escaping invalid characters in attribute and element values.
|
||||||
|
// NOTE: this is brute force with NO interrogation of '&' being escaped already; if it is
|
||||||
|
// then '&' will be re-escaped as '&amp;'.
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
The values are:
|
||||||
|
" "
|
||||||
|
' '
|
||||||
|
< <
|
||||||
|
> >
|
||||||
|
& &
|
||||||
|
*/
|
||||||
|
func XMLEscapeChars(b bool) {
|
||||||
|
xmlEscapeChars = b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan for '&' first, since 's' may contain "&" that is parsed to "&amp;"
|
||||||
|
// - or "<" that is parsed to "&lt;".
|
||||||
|
var escapechars = [][2][]byte{
|
||||||
|
{[]byte(`&`), []byte(`&`)},
|
||||||
|
{[]byte(`<`), []byte(`<`)},
|
||||||
|
{[]byte(`>`), []byte(`>`)},
|
||||||
|
{[]byte(`"`), []byte(`"`)},
|
||||||
|
{[]byte(`'`), []byte(`'`)},
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeChars(s string) string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
b := []byte(s)
|
||||||
|
for _, v := range escapechars {
|
||||||
|
n := bytes.Count(b, v[0])
|
||||||
|
if n == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b = bytes.Replace(b, v[0], v[1], n)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
7
vendor/github.com/clbanning/mxj/exists.go
generated
vendored
Normal file
7
vendor/github.com/clbanning/mxj/exists.go
generated
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
// Checks whether the path exists
|
||||||
|
func (mv Map) Exists(path string, subkeys ...string) bool {
|
||||||
|
v, err := mv.ValuesForPath(path, subkeys...)
|
||||||
|
return err == nil && len(v) > 0
|
||||||
|
}
|
287
vendor/github.com/clbanning/mxj/files.go
generated
vendored
Normal file
287
vendor/github.com/clbanning/mxj/files.go
generated
vendored
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Maps []Map
|
||||||
|
|
||||||
|
func NewMaps() Maps {
|
||||||
|
return make(Maps, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MapRaw struct {
|
||||||
|
M Map
|
||||||
|
R []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMapsFromXmlFile - creates an array from a file of JSON values.
|
||||||
|
func NewMapsFromJsonFile(name string) (Maps, error) {
|
||||||
|
fi, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !fi.Mode().IsRegular() {
|
||||||
|
return nil, fmt.Errorf("file %s is not a regular file", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fh, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
am := make([]Map, 0)
|
||||||
|
for {
|
||||||
|
m, raw, err := NewMapJsonReaderRaw(fh)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return am, fmt.Errorf("error: %s - reading: %s", err.Error(), string(raw))
|
||||||
|
}
|
||||||
|
if len(m) > 0 {
|
||||||
|
am = append(am, m)
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return am, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMapsFromJsonFileRaw - creates an array of MapRaw from a file of JSON values.
|
||||||
|
func NewMapsFromJsonFileRaw(name string) ([]MapRaw, error) {
|
||||||
|
fi, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !fi.Mode().IsRegular() {
|
||||||
|
return nil, fmt.Errorf("file %s is not a regular file", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fh, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
am := make([]MapRaw, 0)
|
||||||
|
for {
|
||||||
|
mr := new(MapRaw)
|
||||||
|
mr.M, mr.R, err = NewMapJsonReaderRaw(fh)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return am, fmt.Errorf("error: %s - reading: %s", err.Error(), string(mr.R))
|
||||||
|
}
|
||||||
|
if len(mr.M) > 0 {
|
||||||
|
am = append(am, *mr)
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return am, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMapsFromXmlFile - creates an array from a file of XML values.
|
||||||
|
func NewMapsFromXmlFile(name string) (Maps, error) {
|
||||||
|
fi, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !fi.Mode().IsRegular() {
|
||||||
|
return nil, fmt.Errorf("file %s is not a regular file", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fh, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
am := make([]Map, 0)
|
||||||
|
for {
|
||||||
|
m, raw, err := NewMapXmlReaderRaw(fh)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return am, fmt.Errorf("error: %s - reading: %s", err.Error(), string(raw))
|
||||||
|
}
|
||||||
|
if len(m) > 0 {
|
||||||
|
am = append(am, m)
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return am, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMapsFromXmlFileRaw - creates an array of MapRaw from a file of XML values.
|
||||||
|
// NOTE: the slice with the raw XML is clean with no extra capacity - unlike NewMapXmlReaderRaw().
|
||||||
|
// It is slow at parsing a file from disk and is intended for relatively small utility files.
|
||||||
|
func NewMapsFromXmlFileRaw(name string) ([]MapRaw, error) {
|
||||||
|
fi, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !fi.Mode().IsRegular() {
|
||||||
|
return nil, fmt.Errorf("file %s is not a regular file", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fh, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
am := make([]MapRaw, 0)
|
||||||
|
for {
|
||||||
|
mr := new(MapRaw)
|
||||||
|
mr.M, mr.R, err = NewMapXmlReaderRaw(fh)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return am, fmt.Errorf("error: %s - reading: %s", err.Error(), string(mr.R))
|
||||||
|
}
|
||||||
|
if len(mr.M) > 0 {
|
||||||
|
am = append(am, *mr)
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return am, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------ Maps writing -------------------------
|
||||||
|
// These are handy-dandy methods for dumping configuration data, etc.
|
||||||
|
|
||||||
|
// JsonString - analogous to mv.Json()
|
||||||
|
func (mvs Maps) JsonString(safeEncoding ...bool) (string, error) {
|
||||||
|
var s string
|
||||||
|
for _, v := range mvs {
|
||||||
|
j, err := v.Json()
|
||||||
|
if err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
s += string(j)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JsonStringIndent - analogous to mv.JsonIndent()
|
||||||
|
func (mvs Maps) JsonStringIndent(prefix, indent string, safeEncoding ...bool) (string, error) {
|
||||||
|
var s string
|
||||||
|
var haveFirst bool
|
||||||
|
for _, v := range mvs {
|
||||||
|
j, err := v.JsonIndent(prefix, indent)
|
||||||
|
if err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
if haveFirst {
|
||||||
|
s += "\n"
|
||||||
|
} else {
|
||||||
|
haveFirst = true
|
||||||
|
}
|
||||||
|
s += string(j)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// XmlString - analogous to mv.Xml()
|
||||||
|
func (mvs Maps) XmlString() (string, error) {
|
||||||
|
var s string
|
||||||
|
for _, v := range mvs {
|
||||||
|
x, err := v.Xml()
|
||||||
|
if err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
s += string(x)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// XmlStringIndent - analogous to mv.XmlIndent()
|
||||||
|
func (mvs Maps) XmlStringIndent(prefix, indent string) (string, error) {
|
||||||
|
var s string
|
||||||
|
for _, v := range mvs {
|
||||||
|
x, err := v.XmlIndent(prefix, indent)
|
||||||
|
if err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
s += string(x)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JsonFile - write Maps to named file as JSON
|
||||||
|
// Note: the file will be created, if necessary; if it exists it will be truncated.
|
||||||
|
// If you need to append to a file, open it and use JsonWriter method.
|
||||||
|
func (mvs Maps) JsonFile(file string, safeEncoding ...bool) error {
|
||||||
|
var encoding bool
|
||||||
|
if len(safeEncoding) == 1 {
|
||||||
|
encoding = safeEncoding[0]
|
||||||
|
}
|
||||||
|
s, err := mvs.JsonString(encoding)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fh, err := os.Create(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
fh.WriteString(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JsonFileIndent - write Maps to named file as pretty JSON
|
||||||
|
// Note: the file will be created, if necessary; if it exists it will be truncated.
|
||||||
|
// If you need to append to a file, open it and use JsonIndentWriter method.
|
||||||
|
func (mvs Maps) JsonFileIndent(file, prefix, indent string, safeEncoding ...bool) error {
|
||||||
|
var encoding bool
|
||||||
|
if len(safeEncoding) == 1 {
|
||||||
|
encoding = safeEncoding[0]
|
||||||
|
}
|
||||||
|
s, err := mvs.JsonStringIndent(prefix, indent, encoding)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fh, err := os.Create(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
fh.WriteString(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// XmlFile - write Maps to named file as XML
|
||||||
|
// Note: the file will be created, if necessary; if it exists it will be truncated.
|
||||||
|
// If you need to append to a file, open it and use XmlWriter method.
|
||||||
|
func (mvs Maps) XmlFile(file string) error {
|
||||||
|
s, err := mvs.XmlString()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fh, err := os.Create(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
fh.WriteString(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// XmlFileIndent - write Maps to named file as pretty XML
|
||||||
|
// Note: the file will be created,if necessary; if it exists it will be truncated.
|
||||||
|
// If you need to append to a file, open it and use XmlIndentWriter method.
|
||||||
|
func (mvs Maps) XmlFileIndent(file, prefix, indent string) error {
|
||||||
|
s, err := mvs.XmlStringIndent(prefix, indent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fh, err := os.Create(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
fh.WriteString(s)
|
||||||
|
return nil
|
||||||
|
}
|
2
vendor/github.com/clbanning/mxj/files_test.badjson
generated
vendored
Normal file
2
vendor/github.com/clbanning/mxj/files_test.badjson
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
{ "this":"is", "a":"test", "file":"for", "files_test.go":"case" }
|
||||||
|
{ "with":"some", "bad":JSON, "in":"it" }
|
9
vendor/github.com/clbanning/mxj/files_test.badxml
generated
vendored
Normal file
9
vendor/github.com/clbanning/mxj/files_test.badxml
generated
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<doc>
|
||||||
|
<some>test</some>
|
||||||
|
<data>for files.go</data>
|
||||||
|
</doc>
|
||||||
|
<msg>
|
||||||
|
<just>some</just>
|
||||||
|
<another>doc</other>
|
||||||
|
<for>test case</for>
|
||||||
|
</msg>
|
2
vendor/github.com/clbanning/mxj/files_test.json
generated
vendored
Normal file
2
vendor/github.com/clbanning/mxj/files_test.json
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
{ "this":"is", "a":"test", "file":"for", "files_test.go":"case" }
|
||||||
|
{ "with":"just", "two":2, "JSON":"values", "true":true }
|
9
vendor/github.com/clbanning/mxj/files_test.xml
generated
vendored
Normal file
9
vendor/github.com/clbanning/mxj/files_test.xml
generated
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<doc>
|
||||||
|
<some>test</some>
|
||||||
|
<data>for files.go</data>
|
||||||
|
</doc>
|
||||||
|
<msg>
|
||||||
|
<just>some</just>
|
||||||
|
<another>doc</another>
|
||||||
|
<for>test case</for>
|
||||||
|
</msg>
|
1
vendor/github.com/clbanning/mxj/files_test_dup.json
generated
vendored
Normal file
1
vendor/github.com/clbanning/mxj/files_test_dup.json
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"a":"test","file":"for","files_test.go":"case","this":"is"}{"JSON":"values","true":true,"two":2,"with":"just"}
|
1
vendor/github.com/clbanning/mxj/files_test_dup.xml
generated
vendored
Normal file
1
vendor/github.com/clbanning/mxj/files_test_dup.xml
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<doc><data>for files.go</data><some>test</some></doc><msg><another>doc</another><for>test case</for><just>some</just></msg>
|
12
vendor/github.com/clbanning/mxj/files_test_indent.json
generated
vendored
Normal file
12
vendor/github.com/clbanning/mxj/files_test_indent.json
generated
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"a": "test",
|
||||||
|
"file": "for",
|
||||||
|
"files_test.go": "case",
|
||||||
|
"this": "is"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"JSON": "values",
|
||||||
|
"true": true,
|
||||||
|
"two": 2,
|
||||||
|
"with": "just"
|
||||||
|
}
|
8
vendor/github.com/clbanning/mxj/files_test_indent.xml
generated
vendored
Normal file
8
vendor/github.com/clbanning/mxj/files_test_indent.xml
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<doc>
|
||||||
|
<data>for files.go</data>
|
||||||
|
<some>test</some>
|
||||||
|
</doc><msg>
|
||||||
|
<another>doc</another>
|
||||||
|
<for>test case</for>
|
||||||
|
<just>some</just>
|
||||||
|
</msg>
|
35
vendor/github.com/clbanning/mxj/gob.go
generated
vendored
Normal file
35
vendor/github.com/clbanning/mxj/gob.go
generated
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// gob.go - Encode/Decode a Map into a gob object.
|
||||||
|
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewMapGob returns a Map value for a gob object that has been
|
||||||
|
// encoded from a map[string]interface{} (or compatible type) value.
|
||||||
|
// It is intended to provide symmetric handling of Maps that have
|
||||||
|
// been encoded using mv.Gob.
|
||||||
|
func NewMapGob(gobj []byte) (Map, error) {
|
||||||
|
m := make(map[string]interface{}, 0)
|
||||||
|
if len(gobj) == 0 {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
r := bytes.NewReader(gobj)
|
||||||
|
dec := gob.NewDecoder(r)
|
||||||
|
if err := dec.Decode(&m); err != nil {
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gob returns a gob-encoded value for the Map 'mv'.
|
||||||
|
func (mv Map) Gob() ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
enc := gob.NewEncoder(&buf)
|
||||||
|
if err := enc.Encode(map[string]interface{}(mv)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
323
vendor/github.com/clbanning/mxj/json.go
generated
vendored
Normal file
323
vendor/github.com/clbanning/mxj/json.go
generated
vendored
Normal file
|
@ -0,0 +1,323 @@
|
||||||
|
// Copyright 2012-2014 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
|
||||||
|
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ------------------------------ write JSON -----------------------
|
||||||
|
|
||||||
|
// Just a wrapper on json.Marshal.
|
||||||
|
// If option safeEncoding is'true' then safe encoding of '<', '>' and '&'
|
||||||
|
// is preserved. (see encoding/json#Marshal, encoding/json#Encode)
|
||||||
|
func (mv Map) Json(safeEncoding ...bool) ([]byte, error) {
|
||||||
|
var s bool
|
||||||
|
if len(safeEncoding) == 1 {
|
||||||
|
s = safeEncoding[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(mv)
|
||||||
|
|
||||||
|
if !s {
|
||||||
|
b = bytes.Replace(b, []byte("\\u003c"), []byte("<"), -1)
|
||||||
|
b = bytes.Replace(b, []byte("\\u003e"), []byte(">"), -1)
|
||||||
|
b = bytes.Replace(b, []byte("\\u0026"), []byte("&"), -1)
|
||||||
|
}
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just a wrapper on json.MarshalIndent.
|
||||||
|
// If option safeEncoding is'true' then safe encoding of '<' , '>' and '&'
|
||||||
|
// is preserved. (see encoding/json#Marshal, encoding/json#Encode)
|
||||||
|
func (mv Map) JsonIndent(prefix, indent string, safeEncoding ...bool) ([]byte, error) {
|
||||||
|
var s bool
|
||||||
|
if len(safeEncoding) == 1 {
|
||||||
|
s = safeEncoding[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.MarshalIndent(mv, prefix, indent)
|
||||||
|
if !s {
|
||||||
|
b = bytes.Replace(b, []byte("\\u003c"), []byte("<"), -1)
|
||||||
|
b = bytes.Replace(b, []byte("\\u003e"), []byte(">"), -1)
|
||||||
|
b = bytes.Replace(b, []byte("\\u0026"), []byte("&"), -1)
|
||||||
|
}
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following implementation is provided for symmetry with NewMapJsonReader[Raw]
|
||||||
|
// The names will also provide a key for the number of return arguments.
|
||||||
|
|
||||||
|
// Writes the Map as JSON on the Writer.
|
||||||
|
// If 'safeEncoding' is 'true', then "safe" encoding of '<', '>' and '&' is preserved.
|
||||||
|
func (mv Map) JsonWriter(jsonWriter io.Writer, safeEncoding ...bool) error {
|
||||||
|
b, err := mv.Json(safeEncoding...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = jsonWriter.Write(b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes the Map as JSON on the Writer. []byte is the raw JSON that was written.
|
||||||
|
// If 'safeEncoding' is 'true', then "safe" encoding of '<', '>' and '&' is preserved.
|
||||||
|
func (mv Map) JsonWriterRaw(jsonWriter io.Writer, safeEncoding ...bool) ([]byte, error) {
|
||||||
|
b, err := mv.Json(safeEncoding...)
|
||||||
|
if err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = jsonWriter.Write(b)
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes the Map as pretty JSON on the Writer.
|
||||||
|
// If 'safeEncoding' is 'true', then "safe" encoding of '<', '>' and '&' is preserved.
|
||||||
|
func (mv Map) JsonIndentWriter(jsonWriter io.Writer, prefix, indent string, safeEncoding ...bool) error {
|
||||||
|
b, err := mv.JsonIndent(prefix, indent, safeEncoding...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = jsonWriter.Write(b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes the Map as pretty JSON on the Writer. []byte is the raw JSON that was written.
|
||||||
|
// If 'safeEncoding' is 'true', then "safe" encoding of '<', '>' and '&' is preserved.
|
||||||
|
func (mv Map) JsonIndentWriterRaw(jsonWriter io.Writer, prefix, indent string, safeEncoding ...bool) ([]byte, error) {
|
||||||
|
b, err := mv.JsonIndent(prefix, indent, safeEncoding...)
|
||||||
|
if err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = jsonWriter.Write(b)
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------- read JSON -----------------------------
|
||||||
|
|
||||||
|
// Decode numericvalues as json.Number type Map values - see encoding/json#Number.
|
||||||
|
// NOTE: this is for decoding JSON into a Map with NewMapJson(), NewMapJsonReader(),
|
||||||
|
// etc.; it does not affect NewMapXml(), etc. The XML encoders mv.Xml() and mv.XmlIndent()
|
||||||
|
// do recognize json.Number types; a JSON object can be decoded to a Map with json.Number
|
||||||
|
// value types and the resulting Map can be correctly encoded into a XML object.
|
||||||
|
var JsonUseNumber bool
|
||||||
|
|
||||||
|
// Just a wrapper on json.Unmarshal
|
||||||
|
// Converting JSON to XML is a simple as:
|
||||||
|
// ...
|
||||||
|
// mapVal, merr := mxj.NewMapJson(jsonVal)
|
||||||
|
// if merr != nil {
|
||||||
|
// // handle error
|
||||||
|
// }
|
||||||
|
// xmlVal, xerr := mapVal.Xml()
|
||||||
|
// if xerr != nil {
|
||||||
|
// // handle error
|
||||||
|
// }
|
||||||
|
// NOTE: as a special case, passing a list, e.g., [{"some-null-value":"", "a-non-null-value":"bar"}],
|
||||||
|
// will be interpreted as having the root key 'object' prepended - {"object":[ ... ]} - to unmarshal to a Map.
|
||||||
|
// See mxj/j2x/j2x_test.go.
|
||||||
|
func NewMapJson(jsonVal []byte) (Map, error) {
|
||||||
|
// empty or nil begets empty
|
||||||
|
if len(jsonVal) == 0 {
|
||||||
|
m := make(map[string]interface{}, 0)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
// handle a goofy case ...
|
||||||
|
if jsonVal[0] == '[' {
|
||||||
|
jsonVal = []byte(`{"object":` + string(jsonVal) + `}`)
|
||||||
|
}
|
||||||
|
m := make(map[string]interface{})
|
||||||
|
// err := json.Unmarshal(jsonVal, &m)
|
||||||
|
buf := bytes.NewReader(jsonVal)
|
||||||
|
dec := json.NewDecoder(buf)
|
||||||
|
if JsonUseNumber {
|
||||||
|
dec.UseNumber()
|
||||||
|
}
|
||||||
|
err := dec.Decode(&m)
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve a Map value from an io.Reader.
|
||||||
|
// NOTE: The raw JSON off the reader is buffered to []byte using a ByteReader. If the io.Reader is an
|
||||||
|
// os.File, there may be significant performance impact. 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 JSON object.
|
||||||
|
func NewMapJsonReader(jsonReader io.Reader) (Map, error) {
|
||||||
|
jb, err := getJson(jsonReader)
|
||||||
|
if err != nil || len(*jb) == 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal the 'presumed' JSON string
|
||||||
|
return NewMapJson(*jb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve a Map value and raw JSON - []byte - from an io.Reader.
|
||||||
|
// NOTE: The raw JSON off the reader is buffered to []byte using a ByteReader. If the io.Reader is an
|
||||||
|
// os.File, there may be significant performance impact. 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 JSON object and retrieve the raw JSON in a single call.
|
||||||
|
func NewMapJsonReaderRaw(jsonReader io.Reader) (Map, []byte, error) {
|
||||||
|
jb, err := getJson(jsonReader)
|
||||||
|
if err != nil || len(*jb) == 0 {
|
||||||
|
return nil, *jb, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal the 'presumed' JSON string
|
||||||
|
m, merr := NewMapJson(*jb)
|
||||||
|
return m, *jb, merr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the next JSON string off the stream: just read from first '{' to its closing '}'.
|
||||||
|
// Returning a pointer to the slice saves 16 bytes - maybe unnecessary, but internal to package.
|
||||||
|
func getJson(rdr io.Reader) (*[]byte, error) {
|
||||||
|
bval := make([]byte, 1)
|
||||||
|
jb := make([]byte, 0)
|
||||||
|
var inQuote, inJson bool
|
||||||
|
var parenCnt int
|
||||||
|
var previous byte
|
||||||
|
|
||||||
|
// scan the input for a matched set of {...}
|
||||||
|
// json.Unmarshal will handle syntax checking.
|
||||||
|
for {
|
||||||
|
_, err := rdr.Read(bval)
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF && inJson && parenCnt > 0 {
|
||||||
|
return &jb, fmt.Errorf("no closing } for JSON string: %s", string(jb))
|
||||||
|
}
|
||||||
|
return &jb, err
|
||||||
|
}
|
||||||
|
switch bval[0] {
|
||||||
|
case '{':
|
||||||
|
if !inQuote {
|
||||||
|
parenCnt++
|
||||||
|
inJson = true
|
||||||
|
}
|
||||||
|
case '}':
|
||||||
|
if !inQuote {
|
||||||
|
parenCnt--
|
||||||
|
}
|
||||||
|
if parenCnt < 0 {
|
||||||
|
return nil, fmt.Errorf("closing } without opening {: %s", string(jb))
|
||||||
|
}
|
||||||
|
case '"':
|
||||||
|
if inQuote {
|
||||||
|
if previous == '\\' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
inQuote = false
|
||||||
|
} else {
|
||||||
|
inQuote = true
|
||||||
|
}
|
||||||
|
case '\n', '\r', '\t', ' ':
|
||||||
|
if !inQuote {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inJson {
|
||||||
|
jb = append(jb, bval[0])
|
||||||
|
if parenCnt == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previous = bval[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return &jb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------- JSON Reader handler via Map values -----------------------
|
||||||
|
|
||||||
|
// Default poll delay to keep Handler from spinning on an open stream
|
||||||
|
// like sitting on os.Stdin waiting for imput.
|
||||||
|
var jhandlerPollInterval = time.Duration(1e6)
|
||||||
|
|
||||||
|
// While unnecessary, we make HandleJsonReader() have the same signature as HandleXmlReader().
|
||||||
|
// This avoids treating one or other as a special case and discussing the underlying stdlib logic.
|
||||||
|
|
||||||
|
// Bulk process JSON using handlers that process a Map value.
|
||||||
|
// 'rdr' is an io.Reader for the JSON (stream).
|
||||||
|
// 'mapHandler' is the Map processing handler. Return of 'false' stops io.Reader processing.
|
||||||
|
// 'errHandler' is the error processor. Return of 'false' stops io.Reader processing and returns the error.
|
||||||
|
// Note: mapHandler() and errHandler() calls are blocking, so reading and processing of messages is serialized.
|
||||||
|
// This means that you can stop reading the file on error or after processing a particular message.
|
||||||
|
// To have reading and handling run concurrently, pass argument to a go routine in handler and return 'true'.
|
||||||
|
func HandleJsonReader(jsonReader io.Reader, mapHandler func(Map) bool, errHandler func(error) bool) error {
|
||||||
|
var n int
|
||||||
|
for {
|
||||||
|
m, merr := NewMapJsonReader(jsonReader)
|
||||||
|
n++
|
||||||
|
|
||||||
|
// handle error condition with errhandler
|
||||||
|
if merr != nil && merr != io.EOF {
|
||||||
|
merr = fmt.Errorf("[jsonReader: %d] %s", n, merr.Error())
|
||||||
|
if ok := errHandler(merr); !ok {
|
||||||
|
// caused reader termination
|
||||||
|
return merr
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass to maphandler
|
||||||
|
if len(m) != 0 {
|
||||||
|
if ok := mapHandler(m); !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if merr != io.EOF {
|
||||||
|
<-time.After(jhandlerPollInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
if merr == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk process JSON using handlers that process a Map value and the raw JSON.
|
||||||
|
// 'rdr' is an io.Reader for the JSON (stream).
|
||||||
|
// 'mapHandler' is the Map and raw JSON - []byte - processor. Return of 'false' stops io.Reader processing.
|
||||||
|
// 'errHandler' is the error and raw JSON processor. Return of 'false' stops io.Reader processing and returns the error.
|
||||||
|
// Note: mapHandler() and errHandler() calls are blocking, so reading and processing of messages is serialized.
|
||||||
|
// This means that you can stop reading the file on error or after processing a particular message.
|
||||||
|
// To have reading and handling run concurrently, pass argument(s) to a go routine in handler and return 'true'.
|
||||||
|
func HandleJsonReaderRaw(jsonReader io.Reader, mapHandler func(Map, []byte) bool, errHandler func(error, []byte) bool) error {
|
||||||
|
var n int
|
||||||
|
for {
|
||||||
|
m, raw, merr := NewMapJsonReaderRaw(jsonReader)
|
||||||
|
n++
|
||||||
|
|
||||||
|
// handle error condition with errhandler
|
||||||
|
if merr != nil && merr != io.EOF {
|
||||||
|
merr = fmt.Errorf("[jsonReader: %d] %s", n, merr.Error())
|
||||||
|
if ok := errHandler(merr, raw); !ok {
|
||||||
|
// caused reader termination
|
||||||
|
return merr
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass to maphandler
|
||||||
|
if len(m) != 0 {
|
||||||
|
if ok := mapHandler(m, raw); !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if merr != io.EOF {
|
||||||
|
<-time.After(jhandlerPollInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
if merr == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
671
vendor/github.com/clbanning/mxj/keyvalues.go
generated
vendored
Normal file
671
vendor/github.com/clbanning/mxj/keyvalues.go
generated
vendored
Normal file
|
@ -0,0 +1,671 @@
|
||||||
|
// Copyright 2012-2014 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
|
||||||
|
|
||||||
|
// keyvalues.go: Extract values from an arbitrary XML doc. Tag path can include wildcard characters.
|
||||||
|
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----------------------------- get everything FOR a single key -------------------------
|
||||||
|
|
||||||
|
const (
|
||||||
|
minArraySize = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultArraySize int = minArraySize
|
||||||
|
|
||||||
|
// Adjust the buffers for expected number of values to return from ValuesForKey() and ValuesForPath().
|
||||||
|
// This can have the effect of significantly reducing memory allocation-copy functions for large data sets.
|
||||||
|
// Returns the initial buffer size.
|
||||||
|
func SetArraySize(size int) int {
|
||||||
|
if size > minArraySize {
|
||||||
|
defaultArraySize = size
|
||||||
|
} else {
|
||||||
|
defaultArraySize = minArraySize
|
||||||
|
}
|
||||||
|
return defaultArraySize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all values in Map, 'mv', associated with a 'key'. If len(returned_values) == 0, then no match.
|
||||||
|
// On error, the returned slice is 'nil'. NOTE: 'key' can be wildcard, "*".
|
||||||
|
// 'subkeys' (optional) are "key:val[:type]" strings representing attributes or elements in a list.
|
||||||
|
// - By default 'val' is of type string. "key:val:bool" and "key:val:float" to coerce them.
|
||||||
|
// - For attributes prefix the label with a hyphen, '-', e.g., "-seq:3".
|
||||||
|
// - If the 'key' refers to a list, then "key:value" could select a list member of the list.
|
||||||
|
// - The subkey can be wildcarded - "key:*" - to require that it's there with some value.
|
||||||
|
// - If a subkey is preceeded with the '!' character, the key:value[:type] entry is treated as an
|
||||||
|
// exclusion critera - e.g., "!author:William T. Gaddis".
|
||||||
|
// - If val contains ":" symbol, use SetFieldSeparator to a unused symbol, perhaps "|".
|
||||||
|
func (mv Map) ValuesForKey(key string, subkeys ...string) ([]interface{}, error) {
|
||||||
|
m := map[string]interface{}(mv)
|
||||||
|
var subKeyMap map[string]interface{}
|
||||||
|
if len(subkeys) > 0 {
|
||||||
|
var err error
|
||||||
|
subKeyMap, err = getSubKeyMap(subkeys...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make([]interface{}, 0, defaultArraySize)
|
||||||
|
var cnt int
|
||||||
|
hasKey(m, key, &ret, &cnt, subKeyMap)
|
||||||
|
return ret[:cnt], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var KeyNotExistError = errors.New("Key does not exist")
|
||||||
|
|
||||||
|
// ValueForKey is a wrapper on ValuesForKey. It returns the first member of []interface{}, if any.
|
||||||
|
// If there is no value, "nil, nil" is returned.
|
||||||
|
func (mv Map) ValueForKey(key string, subkeys ...string) (interface{}, error) {
|
||||||
|
vals, err := mv.ValuesForKey(key, subkeys...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(vals) == 0 {
|
||||||
|
return nil, KeyNotExistError
|
||||||
|
}
|
||||||
|
return vals[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasKey - if the map 'key' exists append it to array
|
||||||
|
// if it doesn't do nothing except scan array and map values
|
||||||
|
func hasKey(iv interface{}, key string, ret *[]interface{}, cnt *int, subkeys map[string]interface{}) {
|
||||||
|
// func hasKey(iv interface{}, key string, ret *[]interface{}, subkeys map[string]interface{}) {
|
||||||
|
switch iv.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
vv := iv.(map[string]interface{})
|
||||||
|
// see if the current value is of interest
|
||||||
|
if v, ok := vv[key]; ok {
|
||||||
|
switch v.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if hasSubKeys(v, subkeys) {
|
||||||
|
*ret = append(*ret, v)
|
||||||
|
*cnt++
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for _, av := range v.([]interface{}) {
|
||||||
|
if hasSubKeys(av, subkeys) {
|
||||||
|
*ret = append(*ret, av)
|
||||||
|
*cnt++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if len(subkeys) == 0 {
|
||||||
|
*ret = append(*ret, v)
|
||||||
|
*cnt++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wildcard case
|
||||||
|
if key == "*" {
|
||||||
|
for _, v := range vv {
|
||||||
|
switch v.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if hasSubKeys(v, subkeys) {
|
||||||
|
*ret = append(*ret, v)
|
||||||
|
*cnt++
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for _, av := range v.([]interface{}) {
|
||||||
|
if hasSubKeys(av, subkeys) {
|
||||||
|
*ret = append(*ret, av)
|
||||||
|
*cnt++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if len(subkeys) == 0 {
|
||||||
|
*ret = append(*ret, v)
|
||||||
|
*cnt++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scan the rest
|
||||||
|
for _, v := range vv {
|
||||||
|
hasKey(v, key, ret, cnt, subkeys)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for _, v := range iv.([]interface{}) {
|
||||||
|
hasKey(v, key, ret, cnt, subkeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------- get everything for a node in the Map ---------------------------
|
||||||
|
|
||||||
|
// Allow indexed arrays in "path" specification. (Request from Abhijit Kadam - abhijitk100@gmail.com.)
|
||||||
|
// 2014.04.28 - implementation note.
|
||||||
|
// Implemented as a wrapper of (old)ValuesForPath() because we need look-ahead logic to handle expansion
|
||||||
|
// of wildcards and unindexed arrays. Embedding such logic into valuesForKeyPath() would have made the
|
||||||
|
// code much more complicated; this wrapper is straightforward, easy to debug, and doesn't add significant overhead.
|
||||||
|
|
||||||
|
// Retrieve all values for a path from the Map. If len(returned_values) == 0, then no match.
|
||||||
|
// On error, the returned array is 'nil'.
|
||||||
|
// 'path' is a dot-separated path of key values.
|
||||||
|
// - If a node in the path is '*', then everything beyond is walked.
|
||||||
|
// - 'path' can contain indexed array references, such as, "*.data[1]" and "msgs[2].data[0].field" -
|
||||||
|
// even "*[2].*[0].field".
|
||||||
|
// 'subkeys' (optional) are "key:val[:type]" strings representing attributes or elements in a list.
|
||||||
|
// - By default 'val' is of type string. "key:val:bool" and "key:val:float" to coerce them.
|
||||||
|
// - For attributes prefix the label with a hyphen, '-', e.g., "-seq:3".
|
||||||
|
// - If the 'path' refers to a list, then "tag:value" would return member of the list.
|
||||||
|
// - The subkey can be wildcarded - "key:*" - to require that it's there with some value.
|
||||||
|
// - If a subkey is preceeded with the '!' character, the key:value[:type] entry is treated as an
|
||||||
|
// exclusion critera - e.g., "!author:William T. Gaddis".
|
||||||
|
// - If val contains ":" symbol, use SetFieldSeparator to a unused symbol, perhaps "|".
|
||||||
|
func (mv Map) ValuesForPath(path string, subkeys ...string) ([]interface{}, error) {
|
||||||
|
// If there are no array indexes in path, use legacy ValuesForPath() logic.
|
||||||
|
if strings.Index(path, "[") < 0 {
|
||||||
|
return mv.oldValuesForPath(path, subkeys...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var subKeyMap map[string]interface{}
|
||||||
|
if len(subkeys) > 0 {
|
||||||
|
var err error
|
||||||
|
subKeyMap, err = getSubKeyMap(subkeys...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, kerr := parsePath(path)
|
||||||
|
if kerr != nil {
|
||||||
|
return nil, kerr
|
||||||
|
}
|
||||||
|
|
||||||
|
vals, verr := valuesForArray(keys, mv)
|
||||||
|
if verr != nil {
|
||||||
|
return nil, verr // Vals may be nil, but return empty array.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to handle subkeys ... only return members of vals that satisfy conditions.
|
||||||
|
retvals := make([]interface{}, 0)
|
||||||
|
for _, v := range vals {
|
||||||
|
if hasSubKeys(v, subKeyMap) {
|
||||||
|
retvals = append(retvals, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retvals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func valuesForArray(keys []*key, m Map) ([]interface{}, error) {
|
||||||
|
var tmppath string
|
||||||
|
var haveFirst bool
|
||||||
|
var vals []interface{}
|
||||||
|
var verr error
|
||||||
|
|
||||||
|
lastkey := len(keys) - 1
|
||||||
|
for i := 0; i <= lastkey; i++ {
|
||||||
|
if !haveFirst {
|
||||||
|
tmppath = keys[i].name
|
||||||
|
haveFirst = true
|
||||||
|
} else {
|
||||||
|
tmppath += "." + keys[i].name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look-ahead: explode wildcards and unindexed arrays.
|
||||||
|
// Need to handle un-indexed list recursively:
|
||||||
|
// e.g., path is "stuff.data[0]" rather than "stuff[0].data[0]".
|
||||||
|
// Need to treat it as "stuff[0].data[0]", "stuff[1].data[0]", ...
|
||||||
|
if !keys[i].isArray && i < lastkey && keys[i+1].isArray {
|
||||||
|
// Can't pass subkeys because we may not be at literal end of path.
|
||||||
|
vv, vverr := m.oldValuesForPath(tmppath)
|
||||||
|
if vverr != nil {
|
||||||
|
return nil, vverr
|
||||||
|
}
|
||||||
|
for _, v := range vv {
|
||||||
|
// See if we can walk the value.
|
||||||
|
am, ok := v.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Work the backend.
|
||||||
|
nvals, nvalserr := valuesForArray(keys[i+1:], Map(am))
|
||||||
|
if nvalserr != nil {
|
||||||
|
return nil, nvalserr
|
||||||
|
}
|
||||||
|
vals = append(vals, nvals...)
|
||||||
|
}
|
||||||
|
break // have recursed the whole path - return
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys[i].isArray || i == lastkey {
|
||||||
|
// Don't pass subkeys because may not be at literal end of path.
|
||||||
|
vals, verr = m.oldValuesForPath(tmppath)
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if verr != nil {
|
||||||
|
return nil, verr
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == lastkey && !keys[i].isArray {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we're looking at an array - supposedly.
|
||||||
|
// Is index in range of vals?
|
||||||
|
if len(vals) <= keys[i].position {
|
||||||
|
vals = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the array member of interest, if at end of path.
|
||||||
|
if i == lastkey {
|
||||||
|
vals = vals[keys[i].position:(keys[i].position + 1)]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the array member of interest.
|
||||||
|
am := vals[keys[i].position:(keys[i].position + 1)]
|
||||||
|
|
||||||
|
// must be a map[string]interface{} value so we can keep walking the path
|
||||||
|
amm, ok := am[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
vals = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
m = Map(amm)
|
||||||
|
haveFirst = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type key struct {
|
||||||
|
name string
|
||||||
|
isArray bool
|
||||||
|
position int
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePath(s string) ([]*key, error) {
|
||||||
|
keys := strings.Split(s, ".")
|
||||||
|
|
||||||
|
ret := make([]*key, 0)
|
||||||
|
|
||||||
|
for i := 0; i < len(keys); i++ {
|
||||||
|
if keys[i] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newkey := new(key)
|
||||||
|
if strings.Index(keys[i], "[") < 0 {
|
||||||
|
newkey.name = keys[i]
|
||||||
|
ret = append(ret, newkey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
p := strings.Split(keys[i], "[")
|
||||||
|
newkey.name = p[0]
|
||||||
|
p = strings.Split(p[1], "]")
|
||||||
|
if p[0] == "" { // no right bracket
|
||||||
|
return nil, fmt.Errorf("no right bracket on key index: %s", keys[i])
|
||||||
|
}
|
||||||
|
// convert p[0] to a int value
|
||||||
|
pos, nerr := strconv.ParseInt(p[0], 10, 32)
|
||||||
|
if nerr != nil {
|
||||||
|
return nil, fmt.Errorf("cannot convert index to int value: %s", p[0])
|
||||||
|
}
|
||||||
|
newkey.position = int(pos)
|
||||||
|
newkey.isArray = true
|
||||||
|
ret = append(ret, newkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// legacy ValuesForPath() - now wrapped to handle special case of indexed arrays in 'path'.
|
||||||
|
func (mv Map) oldValuesForPath(path string, subkeys ...string) ([]interface{}, error) {
|
||||||
|
m := map[string]interface{}(mv)
|
||||||
|
var subKeyMap map[string]interface{}
|
||||||
|
if len(subkeys) > 0 {
|
||||||
|
var err error
|
||||||
|
subKeyMap, err = getSubKeyMap(subkeys...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := strings.Split(path, ".")
|
||||||
|
if keys[len(keys)-1] == "" {
|
||||||
|
keys = keys[:len(keys)-1]
|
||||||
|
}
|
||||||
|
ivals := make([]interface{}, 0, defaultArraySize)
|
||||||
|
var cnt int
|
||||||
|
valuesForKeyPath(&ivals, &cnt, m, keys, subKeyMap)
|
||||||
|
return ivals[:cnt], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func valuesForKeyPath(ret *[]interface{}, cnt *int, m interface{}, keys []string, subkeys map[string]interface{}) {
|
||||||
|
lenKeys := len(keys)
|
||||||
|
|
||||||
|
// load 'm' values into 'ret'
|
||||||
|
// expand any lists
|
||||||
|
if lenKeys == 0 {
|
||||||
|
switch m.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if subkeys != nil {
|
||||||
|
if ok := hasSubKeys(m, subkeys); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*ret = append(*ret, m)
|
||||||
|
*cnt++
|
||||||
|
case []interface{}:
|
||||||
|
for i, v := range m.([]interface{}) {
|
||||||
|
if subkeys != nil {
|
||||||
|
if ok := hasSubKeys(v, subkeys); !ok {
|
||||||
|
continue // only load list members with subkeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*ret = append(*ret, (m.([]interface{}))[i])
|
||||||
|
*cnt++
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if subkeys != nil {
|
||||||
|
return // must be map[string]interface{} if there are subkeys
|
||||||
|
}
|
||||||
|
*ret = append(*ret, m)
|
||||||
|
*cnt++
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// key of interest
|
||||||
|
key := keys[0]
|
||||||
|
switch key {
|
||||||
|
case "*": // wildcard - scan all values
|
||||||
|
switch m.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
for _, v := range m.(map[string]interface{}) {
|
||||||
|
// valuesForKeyPath(ret, v, keys[1:], subkeys)
|
||||||
|
valuesForKeyPath(ret, cnt, v, keys[1:], subkeys)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for _, v := range m.([]interface{}) {
|
||||||
|
switch v.(type) {
|
||||||
|
// flatten out a list of maps - keys are processed
|
||||||
|
case map[string]interface{}:
|
||||||
|
for _, vv := range v.(map[string]interface{}) {
|
||||||
|
// valuesForKeyPath(ret, vv, keys[1:], subkeys)
|
||||||
|
valuesForKeyPath(ret, cnt, vv, keys[1:], subkeys)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// valuesForKeyPath(ret, v, keys[1:], subkeys)
|
||||||
|
valuesForKeyPath(ret, cnt, v, keys[1:], subkeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: // key - must be map[string]interface{}
|
||||||
|
switch m.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if v, ok := m.(map[string]interface{})[key]; ok {
|
||||||
|
// valuesForKeyPath(ret, v, keys[1:], subkeys)
|
||||||
|
valuesForKeyPath(ret, cnt, v, keys[1:], subkeys)
|
||||||
|
}
|
||||||
|
case []interface{}: // may be buried in list
|
||||||
|
for _, v := range m.([]interface{}) {
|
||||||
|
switch v.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if vv, ok := v.(map[string]interface{})[key]; ok {
|
||||||
|
// valuesForKeyPath(ret, vv, keys[1:], subkeys)
|
||||||
|
valuesForKeyPath(ret, cnt, vv, keys[1:], subkeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasSubKeys() - interface{} equality works for string, float64, bool
|
||||||
|
// 'v' must be a map[string]interface{} value to have subkeys
|
||||||
|
// 'a' can have k:v pairs with v.(string) == "*", which is treated like a wildcard.
|
||||||
|
func hasSubKeys(v interface{}, subkeys map[string]interface{}) bool {
|
||||||
|
if len(subkeys) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
// do all subKey name:value pairs match?
|
||||||
|
mv := v.(map[string]interface{})
|
||||||
|
for skey, sval := range subkeys {
|
||||||
|
isNotKey := false
|
||||||
|
if skey[:1] == "!" { // a NOT-key
|
||||||
|
skey = skey[1:]
|
||||||
|
isNotKey = true
|
||||||
|
}
|
||||||
|
vv, ok := mv[skey]
|
||||||
|
if !ok { // key doesn't exist
|
||||||
|
if isNotKey { // key not there, but that's what we want
|
||||||
|
if kv, ok := sval.(string); ok && kv == "*" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// wildcard check
|
||||||
|
if kv, ok := sval.(string); ok && kv == "*" {
|
||||||
|
if isNotKey { // key is there, and we don't want it
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch sval.(type) {
|
||||||
|
case string:
|
||||||
|
if s, ok := vv.(string); ok && s == sval.(string) {
|
||||||
|
if isNotKey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case bool:
|
||||||
|
if b, ok := vv.(bool); ok && b == sval.(bool) {
|
||||||
|
if isNotKey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case float64:
|
||||||
|
if f, ok := vv.(float64); ok && f == sval.(float64) {
|
||||||
|
if isNotKey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// key there but didn't match subkey value
|
||||||
|
if isNotKey { // that's what we want
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// all subkeys matched
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// not a map[string]interface{} value, can't have subkeys
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate map of key:value entries as map[string]string.
|
||||||
|
// 'kv' arguments are "name:value" pairs: attribute keys are designated with prepended hyphen, '-'.
|
||||||
|
// If len(kv) == 0, the return is (nil, nil).
|
||||||
|
func getSubKeyMap(kv ...string) (map[string]interface{}, error) {
|
||||||
|
if len(kv) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
m := make(map[string]interface{}, 0)
|
||||||
|
for _, v := range kv {
|
||||||
|
vv := strings.Split(v, fieldSep)
|
||||||
|
switch len(vv) {
|
||||||
|
case 2:
|
||||||
|
m[vv[0]] = interface{}(vv[1])
|
||||||
|
case 3:
|
||||||
|
switch vv[2] {
|
||||||
|
case "string", "char", "text":
|
||||||
|
m[vv[0]] = interface{}(vv[1])
|
||||||
|
case "bool", "boolean":
|
||||||
|
// ParseBool treats "1"==true & "0"==false
|
||||||
|
b, err := strconv.ParseBool(vv[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't convert subkey value to bool: %s", vv[1])
|
||||||
|
}
|
||||||
|
m[vv[0]] = interface{}(b)
|
||||||
|
case "float", "float64", "num", "number", "numeric":
|
||||||
|
f, err := strconv.ParseFloat(vv[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't convert subkey value to float: %s", vv[1])
|
||||||
|
}
|
||||||
|
m[vv[0]] = interface{}(f)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown subkey conversion spec: %s", v)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown subkey spec: %s", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------- END of valuesFor ... ----------------------------
|
||||||
|
|
||||||
|
// ----------------------- locate where a key value is in the tree -------------------
|
||||||
|
|
||||||
|
//----------------------------- find all paths to a key --------------------------------
|
||||||
|
|
||||||
|
// Get all paths through Map, 'mv', (in dot-notation) that terminate with the specified key.
|
||||||
|
// Results can be used with ValuesForPath.
|
||||||
|
func (mv Map) PathsForKey(key string) []string {
|
||||||
|
m := map[string]interface{}(mv)
|
||||||
|
breadbasket := make(map[string]bool, 0)
|
||||||
|
breadcrumbs := ""
|
||||||
|
|
||||||
|
hasKeyPath(breadcrumbs, m, key, breadbasket)
|
||||||
|
if len(breadbasket) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unpack map keys to return
|
||||||
|
res := make([]string, len(breadbasket))
|
||||||
|
var i int
|
||||||
|
for k := range breadbasket {
|
||||||
|
res[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the shortest path from all possible paths - from PathsForKey() - in Map, 'mv'..
|
||||||
|
// Paths are strings using dot-notation.
|
||||||
|
func (mv Map) PathForKeyShortest(key string) string {
|
||||||
|
paths := mv.PathsForKey(key)
|
||||||
|
|
||||||
|
lp := len(paths)
|
||||||
|
if lp == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if lp == 1 {
|
||||||
|
return paths[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
shortest := paths[0]
|
||||||
|
shortestLen := len(strings.Split(shortest, "."))
|
||||||
|
|
||||||
|
for i := 1; i < len(paths); i++ {
|
||||||
|
vlen := len(strings.Split(paths[i], "."))
|
||||||
|
if vlen < shortestLen {
|
||||||
|
shortest = paths[i]
|
||||||
|
shortestLen = vlen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortest
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasKeyPath - if the map 'key' exists append it to KeyPath.path and increment KeyPath.depth
|
||||||
|
// This is really just a breadcrumber that saves all trails that hit the prescribed 'key'.
|
||||||
|
func hasKeyPath(crumbs string, iv interface{}, key string, basket map[string]bool) {
|
||||||
|
switch iv.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
vv := iv.(map[string]interface{})
|
||||||
|
if _, ok := vv[key]; ok {
|
||||||
|
// create a new breadcrumb, intialized with the one we have
|
||||||
|
var nbc string
|
||||||
|
if crumbs == "" {
|
||||||
|
nbc = key
|
||||||
|
} else {
|
||||||
|
nbc = crumbs + "." + key
|
||||||
|
}
|
||||||
|
basket[nbc] = true
|
||||||
|
}
|
||||||
|
// walk on down the path, key could occur again at deeper node
|
||||||
|
for k, v := range vv {
|
||||||
|
// create a new breadcrumb, intialized with the one we have
|
||||||
|
var nbc string
|
||||||
|
if crumbs == "" {
|
||||||
|
nbc = k
|
||||||
|
} else {
|
||||||
|
nbc = crumbs + "." + k
|
||||||
|
}
|
||||||
|
hasKeyPath(nbc, v, key, basket)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
// crumb-trail doesn't change, pass it on
|
||||||
|
for _, v := range iv.([]interface{}) {
|
||||||
|
hasKeyPath(crumbs, v, key, basket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var PathNotExistError = errors.New("Path does not exist")
|
||||||
|
|
||||||
|
// ValueForPath wrap ValuesFor Path and returns the first value returned.
|
||||||
|
// If no value is found it returns 'nil' and PathNotExistError.
|
||||||
|
func (mv Map) ValueForPath(path string) (interface{}, error) {
|
||||||
|
vals, err := mv.ValuesForPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(vals) == 0 {
|
||||||
|
return nil, PathNotExistError
|
||||||
|
}
|
||||||
|
return vals[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the first found value for the path as a string.
|
||||||
|
func (mv Map) ValueForPathString(path string) (string, error) {
|
||||||
|
vals, err := mv.ValuesForPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(vals) == 0 {
|
||||||
|
return "", errors.New("ValueForPath: path not found")
|
||||||
|
}
|
||||||
|
val := vals[0]
|
||||||
|
switch str := val.(type) {
|
||||||
|
case string:
|
||||||
|
return str, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("ValueForPath: unsupported type: %T", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the first found value for the path as a string.
|
||||||
|
// If the path is not found then it returns an empty string.
|
||||||
|
func (mv Map) ValueOrEmptyForPathString(path string) string {
|
||||||
|
str, _ := mv.ValueForPathString(path)
|
||||||
|
return str
|
||||||
|
}
|
112
vendor/github.com/clbanning/mxj/leafnode.go
generated
vendored
Normal file
112
vendor/github.com/clbanning/mxj/leafnode.go
generated
vendored
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
// leafnode.go - return leaf nodes with paths and values for the Map
|
||||||
|
// inspired by: https://groups.google.com/forum/#!topic/golang-nuts/3JhuVKRuBbw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NoAttributes = true // suppress LeafNode values that are attributes
|
||||||
|
)
|
||||||
|
|
||||||
|
// LeafNode - a terminal path value in a Map.
|
||||||
|
// For XML Map values it represents an attribute or simple element value - of type
|
||||||
|
// string unless Map was created using Cast flag. For JSON Map values it represents
|
||||||
|
// a string, numeric, boolean, or null value.
|
||||||
|
type LeafNode struct {
|
||||||
|
Path string // a dot-notation representation of the path with array subscripting
|
||||||
|
Value interface{} // the value at the path termination
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeafNodes - returns an array of all LeafNode values for the Map.
|
||||||
|
// The option no_attr argument suppresses attribute values (keys with prepended hyphen, '-')
|
||||||
|
// as well as the "#text" key for the associated simple element value.
|
||||||
|
//
|
||||||
|
// PrependAttrWithHypen(false) will result in attributes having .attr-name as
|
||||||
|
// terminal node in 'path' while the path for the element value, itself, will be
|
||||||
|
// the base path w/o "#text".
|
||||||
|
//
|
||||||
|
// LeafUseDotNotation(true) causes list members to be identified using ".N" syntax
|
||||||
|
// rather than "[N]" syntax.
|
||||||
|
func (mv Map) LeafNodes(no_attr ...bool) []LeafNode {
|
||||||
|
var a bool
|
||||||
|
if len(no_attr) == 1 {
|
||||||
|
a = no_attr[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
l := make([]LeafNode, 0)
|
||||||
|
getLeafNodes("", "", map[string]interface{}(mv), &l, a)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLeafNodes(path, node string, mv interface{}, l *[]LeafNode, noattr bool) {
|
||||||
|
// if stripping attributes, then also strip "#text" key
|
||||||
|
if !noattr || node != "#text" {
|
||||||
|
if path != "" && node[:1] != "[" {
|
||||||
|
path += "."
|
||||||
|
}
|
||||||
|
path += node
|
||||||
|
}
|
||||||
|
switch mv.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
for k, v := range mv.(map[string]interface{}) {
|
||||||
|
// if noattr && k[:1] == "-" {
|
||||||
|
if noattr && len(attrPrefix) > 0 && strings.Index(k, attrPrefix) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
getLeafNodes(path, k, v, l, noattr)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for i, v := range mv.([]interface{}) {
|
||||||
|
if useDotNotation {
|
||||||
|
getLeafNodes(path, strconv.Itoa(i), v, l, noattr)
|
||||||
|
} else {
|
||||||
|
getLeafNodes(path, "["+strconv.Itoa(i)+"]", v, l, noattr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// can't walk any further, so create leaf
|
||||||
|
n := LeafNode{path, mv}
|
||||||
|
*l = append(*l, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeafPaths - all paths that terminate in LeafNode values.
|
||||||
|
func (mv Map) LeafPaths(no_attr ...bool) []string {
|
||||||
|
ln := mv.LeafNodes()
|
||||||
|
ss := make([]string, len(ln))
|
||||||
|
for i := 0; i < len(ln); i++ {
|
||||||
|
ss[i] = ln[i].Path
|
||||||
|
}
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeafValues - all terminal values in the Map.
|
||||||
|
func (mv Map) LeafValues(no_attr ...bool) []interface{} {
|
||||||
|
ln := mv.LeafNodes()
|
||||||
|
vv := make([]interface{}, len(ln))
|
||||||
|
for i := 0; i < len(ln); i++ {
|
||||||
|
vv[i] = ln[i].Value
|
||||||
|
}
|
||||||
|
return vv
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== utilities ======================
|
||||||
|
|
||||||
|
// https://groups.google.com/forum/#!topic/golang-nuts/pj0C5IrZk4I
|
||||||
|
var useDotNotation bool
|
||||||
|
|
||||||
|
// LeafUseDotNotation sets a flag that list members in LeafNode paths
|
||||||
|
// should be identified using ".N" syntax rather than the default "[N]"
|
||||||
|
// syntax. Calling LeafUseDotNotation with no arguments toggles the
|
||||||
|
// flag on/off; otherwise, the argument sets the flag value 'true'/'false'.
|
||||||
|
func LeafUseDotNotation(b ...bool) {
|
||||||
|
if len(b) == 0 {
|
||||||
|
useDotNotation = !useDotNotation
|
||||||
|
return
|
||||||
|
}
|
||||||
|
useDotNotation = b[0]
|
||||||
|
}
|
86
vendor/github.com/clbanning/mxj/misc.go
generated
vendored
Normal file
86
vendor/github.com/clbanning/mxj/misc.go
generated
vendored
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
// 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"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return the root element of the Map. If there is not a single key in Map,
|
||||||
|
// then an error is returned.
|
||||||
|
func (mv Map) Root() (string, error) {
|
||||||
|
mm := map[string]interface{}(mv)
|
||||||
|
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 (mv Map) Elements(path string) ([]string, error) {
|
||||||
|
e, err := mv.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 len(attrPrefix) > 0 && strings.Index(k, attrPrefix) == 0 {
|
||||||
|
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). Also, if the
|
||||||
|
// attribute prefix is "" - SetAttrPrefix("") or PrependAttrWithHyphen(false) - then
|
||||||
|
// there are no identifiable attributes.
|
||||||
|
func (mv Map) Attributes(path string) ([]string, error) {
|
||||||
|
a, err := mv.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 len(attrPrefix) == 0 || strings.Index(k, attrPrefix) != 0 {
|
||||||
|
continue // skip non-attributes
|
||||||
|
}
|
||||||
|
attrs[i] = k[len(attrPrefix):]
|
||||||
|
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)
|
||||||
|
}
|
128
vendor/github.com/clbanning/mxj/mxj.go
generated
vendored
Normal file
128
vendor/github.com/clbanning/mxj/mxj.go
generated
vendored
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
// mxj - A collection of map[string]interface{} and associated XML and JSON utilities.
|
||||||
|
// Copyright 2012-2014 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
|
||||||
|
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Cast = true // for clarity - e.g., mxj.NewMapXml(doc, mxj.Cast)
|
||||||
|
SafeEncoding = true // ditto - e.g., mv.Json(mxj.SafeEncoding)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Map map[string]interface{}
|
||||||
|
|
||||||
|
// Allocate a Map.
|
||||||
|
func New() Map {
|
||||||
|
m := make(map[string]interface{}, 0)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cast a Map to map[string]interface{}
|
||||||
|
func (mv Map) Old() map[string]interface{} {
|
||||||
|
return mv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a copy of mv as a newly allocated Map. If the Map only contains string,
|
||||||
|
// numeric, map[string]interface{}, and []interface{} values, then it can be thought
|
||||||
|
// of as a "deep copy." Copying a structure (or structure reference) value is subject
|
||||||
|
// to the noted restrictions.
|
||||||
|
// NOTE: If 'mv' includes structure values with, possibly, JSON encoding tags
|
||||||
|
// then only public fields of the structure are in the new Map - and with
|
||||||
|
// keys that conform to any encoding tag instructions. The structure itself will
|
||||||
|
// be represented as a map[string]interface{} value.
|
||||||
|
func (mv Map) Copy() (Map, error) {
|
||||||
|
// this is the poor-man's deep copy
|
||||||
|
// not efficient, but it works
|
||||||
|
j, jerr := mv.Json()
|
||||||
|
// must handle, we don't know how mv got built
|
||||||
|
if jerr != nil {
|
||||||
|
return nil, jerr
|
||||||
|
}
|
||||||
|
return NewMapJson(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------- StringIndent ... from x2j.WriteMap -------------
|
||||||
|
|
||||||
|
// Pretty print a Map.
|
||||||
|
func (mv Map) StringIndent(offset ...int) string {
|
||||||
|
return writeMap(map[string]interface{}(mv), true, true, offset...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pretty print a Map without the value type information - just key:value entries.
|
||||||
|
func (mv Map) StringIndentNoTypeInfo(offset ...int) string {
|
||||||
|
return writeMap(map[string]interface{}(mv), false, true, offset...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeMap - dumps the map[string]interface{} for examination.
|
||||||
|
// 'typeInfo' causes value type to be printed.
|
||||||
|
// 'offset' is initial indentation count; typically: Write(m).
|
||||||
|
func writeMap(m interface{}, typeInfo, root bool, offset ...int) string {
|
||||||
|
var indent int
|
||||||
|
if len(offset) == 1 {
|
||||||
|
indent = offset[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var s string
|
||||||
|
switch m.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
if typeInfo {
|
||||||
|
s += "[[]interface{}]"
|
||||||
|
}
|
||||||
|
for _, v := range m.([]interface{}) {
|
||||||
|
s += "\n"
|
||||||
|
for i := 0; i < indent; i++ {
|
||||||
|
s += " "
|
||||||
|
}
|
||||||
|
s += writeMap(v, typeInfo, 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, typeInfo, false, indent+1)
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
sort.Sort(mapList(list))
|
||||||
|
for _, v := range list {
|
||||||
|
if root {
|
||||||
|
root = false
|
||||||
|
} else {
|
||||||
|
s += "\n"
|
||||||
|
}
|
||||||
|
for i := 0; i < indent; i++ {
|
||||||
|
s += " "
|
||||||
|
}
|
||||||
|
s += v[0] + " : " + v[1]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if typeInfo {
|
||||||
|
s += fmt.Sprintf("[%T] %+v", m, m)
|
||||||
|
} else {
|
||||||
|
s += fmt.Sprintf("%+v", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== utility ===============
|
||||||
|
|
||||||
|
type mapList [][2]string
|
||||||
|
|
||||||
|
func (ml mapList) Len() int {
|
||||||
|
return len(ml)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml mapList) Swap(i, j int) {
|
||||||
|
ml[i], ml[j] = ml[j], ml[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml mapList) Less(i, j int) bool {
|
||||||
|
return ml[i][0] <= ml[j][0]
|
||||||
|
}
|
184
vendor/github.com/clbanning/mxj/newmap.go
generated
vendored
Normal file
184
vendor/github.com/clbanning/mxj/newmap.go
generated
vendored
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
// mxj - A collection of map[string]interface{} and associated XML and JSON utilities.
|
||||||
|
// Copyright 2012-2014, 2018 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
|
||||||
|
|
||||||
|
// remap.go - build a new Map from the current Map based on keyOld:keyNew mapppings
|
||||||
|
// keys can use dot-notation, keyOld can use wildcard, '*'
|
||||||
|
//
|
||||||
|
// Computational strategy -
|
||||||
|
// Using the key path - []string - traverse a new map[string]interface{} and
|
||||||
|
// insert the oldVal as the newVal when we arrive at the end of the path.
|
||||||
|
// If the type at the end is nil, then that is newVal
|
||||||
|
// If the type at the end is a singleton (string, float64, bool) an array is created.
|
||||||
|
// If the type at the end is an array, newVal is just appended.
|
||||||
|
// If the type at the end is a map, it is inserted if possible or the map value
|
||||||
|
// is converted into an array if necessary.
|
||||||
|
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// (Map)NewMap - create a new Map from data in the current Map.
|
||||||
|
// 'keypairs' are key mappings "oldKey:newKey" and specify that the current value of 'oldKey'
|
||||||
|
// should be the value for 'newKey' in the returned Map.
|
||||||
|
// - 'oldKey' supports dot-notation as described for (Map)ValuesForPath()
|
||||||
|
// - 'newKey' supports dot-notation but with no wildcards, '*', or indexed arrays
|
||||||
|
// - "oldKey" is shorthand for the keypair value "oldKey:oldKey"
|
||||||
|
// - "oldKey:" and ":newKey" are invalid keypair values
|
||||||
|
// - if 'oldKey' does not exist in the current Map, it is not written to the new Map.
|
||||||
|
// "null" is not supported unless it is the current Map.
|
||||||
|
// - see newmap_test.go for several syntax examples
|
||||||
|
// - mv.NewMap() == mxj.New()
|
||||||
|
//
|
||||||
|
// NOTE: "examples/partial.go" shows how to create arbitrary sub-docs of an XML doc.
|
||||||
|
func (mv Map) NewMap(keypairs ...string) (Map, error) {
|
||||||
|
n := make(map[string]interface{}, 0)
|
||||||
|
if len(keypairs) == 0 {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through the pairs
|
||||||
|
var oldKey, newKey string
|
||||||
|
var path []string
|
||||||
|
for _, v := range keypairs {
|
||||||
|
if len(v) == 0 {
|
||||||
|
continue // just skip over empty keypair arguments
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize oldKey, newKey and check
|
||||||
|
vv := strings.Split(v, ":")
|
||||||
|
if len(vv) > 2 {
|
||||||
|
return n, errors.New("oldKey:newKey keypair value not valid - " + v)
|
||||||
|
}
|
||||||
|
if len(vv) == 1 {
|
||||||
|
oldKey, newKey = vv[0], vv[0]
|
||||||
|
} else {
|
||||||
|
oldKey, newKey = vv[0], vv[1]
|
||||||
|
}
|
||||||
|
strings.TrimSpace(oldKey)
|
||||||
|
strings.TrimSpace(newKey)
|
||||||
|
if i := strings.Index(newKey, "*"); i > -1 {
|
||||||
|
return n, errors.New("newKey value cannot contain wildcard character - " + v)
|
||||||
|
}
|
||||||
|
if i := strings.Index(newKey, "["); i > -1 {
|
||||||
|
return n, errors.New("newKey value cannot contain indexed arrays - " + v)
|
||||||
|
}
|
||||||
|
if oldKey == "" || newKey == "" {
|
||||||
|
return n, errors.New("oldKey or newKey is not specified - " + v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get oldKey value
|
||||||
|
oldVal, err := mv.ValuesForPath(oldKey)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
if len(oldVal) == 0 {
|
||||||
|
continue // oldKey has no value, may not exist in mv
|
||||||
|
}
|
||||||
|
|
||||||
|
// break down path
|
||||||
|
path = strings.Split(newKey, ".")
|
||||||
|
if path[len(path)-1] == "" { // ignore a trailing dot in newKey spec
|
||||||
|
path = path[:len(path)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
addNewVal(&n, path, oldVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// navigate 'n' to end of path and add val
|
||||||
|
func addNewVal(n *map[string]interface{}, path []string, val []interface{}) {
|
||||||
|
// newVal - either singleton or array
|
||||||
|
var newVal interface{}
|
||||||
|
if len(val) == 1 {
|
||||||
|
newVal = val[0] // is type interface{}
|
||||||
|
} else {
|
||||||
|
newVal = interface{}(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk to the position of interest, create it if necessary
|
||||||
|
m := (*n) // initialize map walker
|
||||||
|
var k string // key for m
|
||||||
|
lp := len(path) - 1 // when to stop looking
|
||||||
|
for i := 0; i < len(path); i++ {
|
||||||
|
k = path[i]
|
||||||
|
if i == lp {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var nm map[string]interface{} // holds position of next-map
|
||||||
|
switch m[k].(type) {
|
||||||
|
case nil: // need a map for next node in path, so go there
|
||||||
|
nm = make(map[string]interface{}, 0)
|
||||||
|
m[k] = interface{}(nm)
|
||||||
|
m = m[k].(map[string]interface{})
|
||||||
|
case map[string]interface{}:
|
||||||
|
// OK - got somewhere to walk to, go there
|
||||||
|
m = m[k].(map[string]interface{})
|
||||||
|
case []interface{}:
|
||||||
|
// add a map and nm points to new map unless there's already
|
||||||
|
// a map in the array, then nm points there
|
||||||
|
// The placement of the next value in the array is dependent
|
||||||
|
// on the sequence of members - could land on a map or a nil
|
||||||
|
// value first. TODO: how to test this.
|
||||||
|
a := make([]interface{}, 0)
|
||||||
|
var foundmap bool
|
||||||
|
for _, vv := range m[k].([]interface{}) {
|
||||||
|
switch vv.(type) {
|
||||||
|
case nil: // doesn't appear that this occurs, need a test case
|
||||||
|
if foundmap { // use the first one in array
|
||||||
|
a = append(a, vv)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nm = make(map[string]interface{}, 0)
|
||||||
|
a = append(a, interface{}(nm))
|
||||||
|
foundmap = true
|
||||||
|
case map[string]interface{}:
|
||||||
|
if foundmap { // use the first one in array
|
||||||
|
a = append(a, vv)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nm = vv.(map[string]interface{})
|
||||||
|
a = append(a, vv)
|
||||||
|
foundmap = true
|
||||||
|
default:
|
||||||
|
a = append(a, vv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// no map found in array
|
||||||
|
if !foundmap {
|
||||||
|
nm = make(map[string]interface{}, 0)
|
||||||
|
a = append(a, interface{}(nm))
|
||||||
|
}
|
||||||
|
m[k] = interface{}(a) // must insert in map
|
||||||
|
m = nm
|
||||||
|
default: // it's a string, float, bool, etc.
|
||||||
|
aa := make([]interface{}, 0)
|
||||||
|
nm = make(map[string]interface{}, 0)
|
||||||
|
aa = append(aa, m[k], nm)
|
||||||
|
m[k] = interface{}(aa)
|
||||||
|
m = nm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// value is nil, array or a singleton of some kind
|
||||||
|
// initially m.(type) == map[string]interface{}
|
||||||
|
v := m[k]
|
||||||
|
switch v.(type) {
|
||||||
|
case nil: // initialized
|
||||||
|
m[k] = newVal
|
||||||
|
case []interface{}:
|
||||||
|
a := m[k].([]interface{})
|
||||||
|
a = append(a, newVal)
|
||||||
|
m[k] = interface{}(a)
|
||||||
|
default: // v exists:string, float64, bool, map[string]interface, etc.
|
||||||
|
a := make([]interface{}, 0)
|
||||||
|
a = append(a, v, newVal)
|
||||||
|
m[k] = interface{}(a)
|
||||||
|
}
|
||||||
|
}
|
179
vendor/github.com/clbanning/mxj/readme.md
generated
vendored
Normal file
179
vendor/github.com/clbanning/mxj/readme.md
generated
vendored
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
<h2>mxj - to/from maps, XML and JSON</h2>
|
||||||
|
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>Related Packages</h4>
|
||||||
|
|
||||||
|
https://github.com/clbanning/checkxml provides functions for validating XML data.
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
2018.04.18: mv.Xml/mv.XmlIndent encodes non-map[string]interface{} map values - map[string]string, map[int]uint, etc.
|
||||||
|
2018.03.29: mv.Gob/NewMapGob support gob encoding/decoding of Maps.
|
||||||
|
2018.03.26: Added mxj/x2j-wrapper sub-package for migrating from legacy x2j package.
|
||||||
|
2017.02.22: LeafNode paths can use ".N" syntax rather than "[N]" for list member indexing.
|
||||||
|
2017.02.10: SetFieldSeparator changes field separator for args in UpdateValuesForPath, ValuesFor... methods.
|
||||||
|
2017.02.06: Support XMPP stream processing - HandleXMPPStreamTag().
|
||||||
|
2016.11.07: Preserve name space prefix syntax in XmlSeq parser - NewMapXmlSeq(), etc.
|
||||||
|
2016.06.25: Support overriding default XML attribute prefix, "-", in Map keys - SetAttrPrefix().
|
||||||
|
2016.05.26: Support customization of xml.Decoder by exposing CustomDecoder variable.
|
||||||
|
2016.03.19: Escape invalid chars when encoding XML attribute and element values - XMLEscapeChars().
|
||||||
|
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 mv.Root(), mv.Elements(), mv.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: 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().
|
||||||
|
2014-11-09: IncludeTagSeqNum() adds "_seq" key with XML doc positional information.
|
||||||
|
(NOTE: PreserveXmlList() is similar and will be here soon.)
|
||||||
|
2014-09-18: inspired by NYTimes fork, added PrependAttrWithHyphen() to allow stripping hyphen from attribute tag.
|
||||||
|
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 to map[string]interface{}</h4>
|
||||||
|
<pre>type Map map[string]interface{}</pre>
|
||||||
|
|
||||||
|
Create a `Map` value, 'mv', from any `map[string]interface{}` value, 'v':
|
||||||
|
<pre>mv := Map(v)</pre>
|
||||||
|
|
||||||
|
Unmarshal / marshal XML as a `Map` value, 'mv':
|
||||||
|
<pre>mv, err := NewMapXml(xmlValue) // unmarshal
|
||||||
|
xmlValue, err := mv.Xml() // marshal</pre>
|
||||||
|
|
||||||
|
Unmarshal XML from an `io.Reader` as a `Map` value, 'mv':
|
||||||
|
<pre>mv, err := NewMapXmlReader(xmlReader) // repeated calls, as with an os.File Reader, will process stream
|
||||||
|
mv, raw, err := NewMapXmlReaderRaw(xmlReader) // 'raw' is the raw XML that was decoded</pre>
|
||||||
|
|
||||||
|
Marshal `Map` value, 'mv', to an XML Writer (`io.Writer`):
|
||||||
|
<pre>err := mv.XmlWriter(xmlWriter)
|
||||||
|
raw, err := mv.XmlWriterRaw(xmlWriter) // 'raw' is the raw XML that was written on xmlWriter</pre>
|
||||||
|
|
||||||
|
Also, for prettified output:
|
||||||
|
<pre>xmlValue, err := mv.XmlIndent(prefix, indent, ...)
|
||||||
|
err := mv.XmlIndentWriter(xmlWriter, prefix, indent, ...)
|
||||||
|
raw, err := mv.XmlIndentWriterRaw(xmlWriter, prefix, indent, ...)</pre>
|
||||||
|
|
||||||
|
Bulk process XML with error handling (note: handlers must return a boolean value):
|
||||||
|
<pre>err := HandleXmlReader(xmlReader, mapHandler(Map), errHandler(error))
|
||||||
|
err := HandleXmlReaderRaw(xmlReader, mapHandler(Map, []byte), errHandler(error, []byte))</pre>
|
||||||
|
|
||||||
|
Converting XML to JSON: see Examples for `NewMapXml` and `HandleXmlReader`.
|
||||||
|
|
||||||
|
There are comparable functions and methods for JSON processing.
|
||||||
|
|
||||||
|
Arbitrary structure values can be decoded to / encoded from `Map` values:
|
||||||
|
<pre>mv, err := NewMapStruct(structVal)
|
||||||
|
err := mv.Struct(structPointer)</pre>
|
||||||
|
|
||||||
|
<h4>Extract / modify Map values</h4>
|
||||||
|
To work with XML tag values, JSON or Map key values or structure field values, decode the XML, JSON
|
||||||
|
or structure to a `Map` value, 'mv', or cast a `map[string]interface{}` value to a `Map` value, 'mv', then:
|
||||||
|
<pre>paths := mv.PathsForKey(key)
|
||||||
|
path := mv.PathForKeyShortest(key)
|
||||||
|
values, err := mv.ValuesForKey(key, subkeys)
|
||||||
|
values, err := mv.ValuesForPath(path, subkeys)
|
||||||
|
count, err := mv.UpdateValuesForPath(newVal, path, subkeys)</pre>
|
||||||
|
|
||||||
|
Get everything at once, irrespective of path depth:
|
||||||
|
<pre>leafnodes := mv.LeafNodes()
|
||||||
|
leafvalues := mv.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, err := mv.NewMap("oldKey_1:newKey_1", "oldKey_2:newKey_2", ..., "oldKey_N:newKey_N")
|
||||||
|
newMap, err := mv.NewMap("oldKey1", "oldKey3", "oldKey5") // a subset of 'mv'; see "examples/partial.go"
|
||||||
|
newXml, err := newMap.Xml() // for example
|
||||||
|
newJson, err := newMap.Json() // ditto</pre>
|
||||||
|
|
||||||
|
<h4>Usage</h4>
|
||||||
|
|
||||||
|
The package is fairly well [self-documented with examples](http://godoc.org/github.com/clbanning/mxj).
|
||||||
|
|
||||||
|
Also, the subdirectory "examples" contains a wide range of examples, several taken from golang-nuts discussions.
|
||||||
|
|
||||||
|
<h4>XML parsing conventions</h4>
|
||||||
|
|
||||||
|
Using NewMapXml()
|
||||||
|
|
||||||
|
- Attributes are parsed to `map[string]interface{}` values by prefixing a hyphen, `-`,
|
||||||
|
to the attribute label. (Unless overridden by `PrependAttrWithHyphen(false)` or
|
||||||
|
`SetAttrPrefix()`.)
|
||||||
|
- 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 NewMapXmlSeq()
|
||||||
|
|
||||||
|
- 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.)
|
||||||
|
- Name space syntax is preserved:
|
||||||
|
- `<ns:key>something</ns.key>` parses to `map["ns:key"]interface{}{"something"}`
|
||||||
|
- `xmlns:ns="http://myns.com/ns"` parses to `map["xmlns:ns"]interface{}{"http://myns.com/ns"}`
|
||||||
|
|
||||||
|
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.
|
||||||
|
- 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
|
||||||
|
mv.XmlSeq() - these try to preserve the element sequencing but with added complexity when
|
||||||
|
working with the Map representation.
|
||||||
|
|
||||||
|
<h4>Running "go test"</h4>
|
||||||
|
|
||||||
|
Because there are no guarantees on the sequence map elements are retrieved, the tests have been
|
||||||
|
written for visual verification in most cases. One advantage is that you can easily use the
|
||||||
|
output from running "go test" as examples of calling the various functions and methods.
|
||||||
|
|
||||||
|
<h4>Motivation</h4>
|
||||||
|
|
||||||
|
I make extensive use of JSON for messaging and typically unmarshal the messages into
|
||||||
|
`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{}` 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{}` 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
|
||||||
|
package was released to address XML encoding of arbitrary JSON and `map[string]interface{}`
|
||||||
|
values. As part of a refactoring of our production system and looking at how we had been
|
||||||
|
using the x2j and j2x packages we found that we rarely performed direct XML-to-JSON or
|
||||||
|
JSON-to_XML conversion and that working with the XML or JSON as `map[string]interface{}`
|
||||||
|
values was the primary value. Thus, everything was refactored into the mxj package.
|
||||||
|
|
37
vendor/github.com/clbanning/mxj/remove.go
generated
vendored
Normal file
37
vendor/github.com/clbanning/mxj/remove.go
generated
vendored
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// Removes the path.
|
||||||
|
func (mv Map) Remove(path string) error {
|
||||||
|
m := map[string]interface{}(mv)
|
||||||
|
return remove(m, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(m interface{}, path string) error {
|
||||||
|
val, err := prevValueByPath(m, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lastKey := lastKey(path)
|
||||||
|
delete(val, lastKey)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the last key of the path.
|
||||||
|
// lastKey("a.b.c") would had returned "c"
|
||||||
|
func lastKey(path string) string {
|
||||||
|
keys := strings.Split(path, ".")
|
||||||
|
key := keys[len(keys)-1]
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the path without the last key
|
||||||
|
// parentPath("a.b.c") whould had returned "a.b"
|
||||||
|
func parentPath(path string) string {
|
||||||
|
keys := strings.Split(path, ".")
|
||||||
|
parentPath := strings.Join(keys[0:len(keys)-1], ".")
|
||||||
|
return parentPath
|
||||||
|
}
|
54
vendor/github.com/clbanning/mxj/rename.go
generated
vendored
Normal file
54
vendor/github.com/clbanning/mxj/rename.go
generated
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenameKey renames a key in a Map.
|
||||||
|
// It works only for nested maps. It doesn't work for cases when it buried in a list.
|
||||||
|
func (mv Map) RenameKey(path string, newName string) error {
|
||||||
|
if !mv.Exists(path) {
|
||||||
|
return errors.New("RenameKey: path not found: " + path)
|
||||||
|
}
|
||||||
|
if mv.Exists(parentPath(path) + "." + newName) {
|
||||||
|
return errors.New("RenameKey: key already exists: " + newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := map[string]interface{}(mv)
|
||||||
|
return renameKey(m, path, newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renameKey(m interface{}, path string, newName string) error {
|
||||||
|
val, err := prevValueByPath(m, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldName := lastKey(path)
|
||||||
|
val[newName] = val[oldName]
|
||||||
|
delete(val, oldName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a value which contains a last key in the path
|
||||||
|
// For example: prevValueByPath("a.b.c", {a{b{c: 3}}}) returns {c: 3}
|
||||||
|
func prevValueByPath(m interface{}, path string) (map[string]interface{}, error) {
|
||||||
|
keys := strings.Split(path, ".")
|
||||||
|
|
||||||
|
switch mValue := m.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
for key, value := range mValue {
|
||||||
|
if key == keys[0] {
|
||||||
|
if len(keys) == 1 {
|
||||||
|
return mValue, nil
|
||||||
|
} else {
|
||||||
|
// keep looking for the full path to the key
|
||||||
|
return prevValueByPath(value, strings.Join(keys[1:], "."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("prevValueByPath: didn't find path – " + path)
|
||||||
|
}
|
26
vendor/github.com/clbanning/mxj/set.go
generated
vendored
Normal file
26
vendor/github.com/clbanning/mxj/set.go
generated
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sets the value for the path
|
||||||
|
func (mv Map) SetValueForPath(value interface{}, path string) error {
|
||||||
|
pathAry := strings.Split(path, ".")
|
||||||
|
parentPathAry := pathAry[0 : len(pathAry)-1]
|
||||||
|
parentPath := strings.Join(parentPathAry, ".")
|
||||||
|
|
||||||
|
val, err := mv.ValueForPath(parentPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if val == nil {
|
||||||
|
return nil // we just ignore the request if there's no val
|
||||||
|
}
|
||||||
|
|
||||||
|
key := pathAry[len(pathAry)-1]
|
||||||
|
cVal := val.(map[string]interface{})
|
||||||
|
cVal[key] = value
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
20
vendor/github.com/clbanning/mxj/setfieldsep.go
generated
vendored
Normal file
20
vendor/github.com/clbanning/mxj/setfieldsep.go
generated
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
// Per: https://github.com/clbanning/mxj/issues/37#issuecomment-278651862
|
||||||
|
var fieldSep string = ":"
|
||||||
|
|
||||||
|
// SetFieldSeparator changes the default field separator, ":", for the
|
||||||
|
// newVal argument in mv.UpdateValuesForPath and the optional 'subkey' arguments
|
||||||
|
// in mv.ValuesForKey and mv.ValuesForPath.
|
||||||
|
//
|
||||||
|
// E.g., if the newVal value is "http://blah/blah", setting the field separator
|
||||||
|
// to "|" will allow the newVal specification, "<key>|http://blah/blah" to parse
|
||||||
|
// properly. If called with no argument or an empty string value, the field
|
||||||
|
// separator is set to the default, ":".
|
||||||
|
func SetFieldSeparator(s ...string) {
|
||||||
|
if len(s) == 0 || s[0] == "" {
|
||||||
|
fieldSep = ":" // the default
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fieldSep = s[0]
|
||||||
|
}
|
29
vendor/github.com/clbanning/mxj/songtext.xml
generated
vendored
Normal file
29
vendor/github.com/clbanning/mxj/songtext.xml
generated
vendored
Normal 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>
|
30
vendor/github.com/clbanning/mxj/strict.go
generated
vendored
Normal file
30
vendor/github.com/clbanning/mxj/strict.go
generated
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// strict.go actually addresses setting xml.Decoder attribute
|
||||||
|
// values. This'll let you parse non-standard XML.
|
||||||
|
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CustomDecoder can be used to specify xml.Decoder attribute
|
||||||
|
// values, e.g., Strict:false, to be used. By default CustomDecoder
|
||||||
|
// is nil. If CustomeDecoder != nil, then mxj.XmlCharsetReader variable is
|
||||||
|
// ignored and must be set as part of the CustomDecoder value, if needed.
|
||||||
|
// Usage:
|
||||||
|
// mxj.CustomDecoder = &xml.Decoder{Strict:false}
|
||||||
|
var CustomDecoder *xml.Decoder
|
||||||
|
|
||||||
|
// useCustomDecoder copy over public attributes from customDecoder
|
||||||
|
func useCustomDecoder(d *xml.Decoder) {
|
||||||
|
d.Strict = CustomDecoder.Strict
|
||||||
|
d.AutoClose = CustomDecoder.AutoClose
|
||||||
|
d.Entity = CustomDecoder.Entity
|
||||||
|
d.CharsetReader = CustomDecoder.CharsetReader
|
||||||
|
d.DefaultSpace = CustomDecoder.DefaultSpace
|
||||||
|
}
|
||||||
|
|
54
vendor/github.com/clbanning/mxj/struct.go
generated
vendored
Normal file
54
vendor/github.com/clbanning/mxj/struct.go
generated
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright 2012-2017 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
|
||||||
|
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
// "github.com/fatih/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a new Map value from a structure. Error returned if argument is not a structure.
|
||||||
|
// Only public structure fields are decoded in the Map value. See github.com/fatih/structs#Map
|
||||||
|
// for handling of "structs" tags.
|
||||||
|
|
||||||
|
// DEPRECATED - import github.com/fatih/structs and cast result of structs.Map to mxj.Map.
|
||||||
|
// import "github.com/fatih/structs"
|
||||||
|
// ...
|
||||||
|
// sm, err := structs.Map(<some struct>)
|
||||||
|
// if err != nil {
|
||||||
|
// // handle error
|
||||||
|
// }
|
||||||
|
// m := mxj.Map(sm)
|
||||||
|
// Alernatively uncomment the old source and import in struct.go.
|
||||||
|
func NewMapStruct(structVal interface{}) (Map, error) {
|
||||||
|
return nil, errors.New("deprecated - see package documentation")
|
||||||
|
/*
|
||||||
|
if !structs.IsStruct(structVal) {
|
||||||
|
return nil, errors.New("NewMapStruct() error: argument is not type Struct")
|
||||||
|
}
|
||||||
|
return structs.Map(structVal), nil
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal a map[string]interface{} into a structure referenced by 'structPtr'. Error returned
|
||||||
|
// if argument is not a pointer or if json.Unmarshal returns an error.
|
||||||
|
// json.Unmarshal structure encoding rules are followed to encode public structure fields.
|
||||||
|
func (mv Map) Struct(structPtr interface{}) error {
|
||||||
|
// should check that we're getting a pointer.
|
||||||
|
if reflect.ValueOf(structPtr).Kind() != reflect.Ptr {
|
||||||
|
return errors.New("mv.Struct() error: argument is not type Ptr")
|
||||||
|
}
|
||||||
|
|
||||||
|
m := map[string]interface{}(mv)
|
||||||
|
j, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(j, structPtr)
|
||||||
|
}
|
256
vendor/github.com/clbanning/mxj/updatevalues.go
generated
vendored
Normal file
256
vendor/github.com/clbanning/mxj/updatevalues.go
generated
vendored
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
// Copyright 2012-2014, 2017 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
|
||||||
|
|
||||||
|
// updatevalues.go - modify a value based on path and possibly sub-keys
|
||||||
|
// TODO(clb): handle simple elements with attributes and NewMapXmlSeq Map values.
|
||||||
|
|
||||||
|
package mxj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update value based on path and possible sub-key values.
|
||||||
|
// A count of the number of values changed and any error are returned.
|
||||||
|
// If the count == 0, then no path (and subkeys) matched.
|
||||||
|
// 'newVal' can be a Map or map[string]interface{} value with a single 'key' that is the key to be modified
|
||||||
|
// or a string value "key:value[:type]" where type is "bool" or "num" to cast the value.
|
||||||
|
// 'path' is dot-notation list of keys to traverse; last key in path can be newVal key
|
||||||
|
// NOTE: 'path' spec does not currently support indexed array references.
|
||||||
|
// 'subkeys' are "key:value[:type]" entries that must match for path node
|
||||||
|
// The subkey can be wildcarded - "key:*" - to require that it's there with some value.
|
||||||
|
// If a subkey is preceeded with the '!' character, the key:value[:type] entry is treated as an
|
||||||
|
// exclusion critera - e.g., "!author:William T. Gaddis".
|
||||||
|
//
|
||||||
|
// NOTES:
|
||||||
|
// 1. Simple elements with attributes need a path terminated as ".#text" to modify the actual value.
|
||||||
|
// 2. Values in Maps created using NewMapXmlSeq are map[string]interface{} values with a "#text" key.
|
||||||
|
// 3. If values in 'newVal' or 'subkeys' args contain ":", use SetFieldSeparator to an unused symbol,
|
||||||
|
// perhaps "|".
|
||||||
|
func (mv Map) UpdateValuesForPath(newVal interface{}, path string, subkeys ...string) (int, error) {
|
||||||
|
m := map[string]interface{}(mv)
|
||||||
|
|
||||||
|
// extract the subkeys
|
||||||
|
var subKeyMap map[string]interface{}
|
||||||
|
if len(subkeys) > 0 {
|
||||||
|
var err error
|
||||||
|
subKeyMap, err = getSubKeyMap(subkeys...)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract key and value from newVal
|
||||||
|
var key string
|
||||||
|
var val interface{}
|
||||||
|
switch newVal.(type) {
|
||||||
|
case map[string]interface{}, Map:
|
||||||
|
switch newVal.(type) { // "fallthrough is not permitted in type switch" (Spec)
|
||||||
|
case Map:
|
||||||
|
newVal = newVal.(Map).Old()
|
||||||
|
}
|
||||||
|
if len(newVal.(map[string]interface{})) != 1 {
|
||||||
|
return 0, fmt.Errorf("newVal map can only have len == 1 - %+v", newVal)
|
||||||
|
}
|
||||||
|
for key, val = range newVal.(map[string]interface{}) {
|
||||||
|
}
|
||||||
|
case string: // split it as a key:value pair
|
||||||
|
ss := strings.Split(newVal.(string), fieldSep)
|
||||||
|
n := len(ss)
|
||||||
|
if n < 2 || n > 3 {
|
||||||
|
return 0, fmt.Errorf("unknown newVal spec - %+v", newVal)
|
||||||
|
}
|
||||||
|
key = ss[0]
|
||||||
|
if n == 2 {
|
||||||
|
val = interface{}(ss[1])
|
||||||
|
} else if n == 3 {
|
||||||
|
switch ss[2] {
|
||||||
|
case "bool", "boolean":
|
||||||
|
nv, err := strconv.ParseBool(ss[1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("can't convert newVal to bool - %+v", newVal)
|
||||||
|
}
|
||||||
|
val = interface{}(nv)
|
||||||
|
case "num", "numeric", "float", "int":
|
||||||
|
nv, err := strconv.ParseFloat(ss[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("can't convert newVal to float64 - %+v", newVal)
|
||||||
|
}
|
||||||
|
val = interface{}(nv)
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unknown type for newVal value - %+v", newVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("invalid newVal type - %+v", newVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse path
|
||||||
|
keys := strings.Split(path, ".")
|
||||||
|
|
||||||
|
var count int
|
||||||
|
updateValuesForKeyPath(key, val, m, keys, subKeyMap, &count)
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// navigate the path
|
||||||
|
func updateValuesForKeyPath(key string, value interface{}, m interface{}, keys []string, subkeys map[string]interface{}, cnt *int) {
|
||||||
|
// ----- at end node: looking at possible node to get 'key' ----
|
||||||
|
if len(keys) == 1 {
|
||||||
|
updateValue(key, value, m, keys[0], subkeys, cnt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- here we are navigating the path thru the penultimate node --------
|
||||||
|
// key of interest is keys[0] - the next in the path
|
||||||
|
switch keys[0] {
|
||||||
|
case "*": // wildcard - scan all values
|
||||||
|
switch m.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
for _, v := range m.(map[string]interface{}) {
|
||||||
|
updateValuesForKeyPath(key, value, v, keys[1:], subkeys, cnt)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for _, v := range m.([]interface{}) {
|
||||||
|
switch v.(type) {
|
||||||
|
// flatten out a list of maps - keys are processed
|
||||||
|
case map[string]interface{}:
|
||||||
|
for _, vv := range v.(map[string]interface{}) {
|
||||||
|
updateValuesForKeyPath(key, value, vv, keys[1:], subkeys, cnt)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
updateValuesForKeyPath(key, value, v, keys[1:], subkeys, cnt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: // key - must be map[string]interface{}
|
||||||
|
switch m.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if v, ok := m.(map[string]interface{})[keys[0]]; ok {
|
||||||
|
updateValuesForKeyPath(key, value, v, keys[1:], subkeys, cnt)
|
||||||
|
}
|
||||||
|
case []interface{}: // may be buried in list
|
||||||
|
for _, v := range m.([]interface{}) {
|
||||||
|
switch v.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if vv, ok := v.(map[string]interface{})[keys[0]]; ok {
|
||||||
|
updateValuesForKeyPath(key, value, vv, keys[1:], subkeys, cnt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// change value if key and subkeys are present
|
||||||
|
func updateValue(key string, value interface{}, m interface{}, keys0 string, subkeys map[string]interface{}, cnt *int) {
|
||||||
|
// there are two possible options for the value of 'keys0': map[string]interface, []interface{}
|
||||||
|
// and 'key' is a key in the map or is a key in a map in a list.
|
||||||
|
switch m.(type) {
|
||||||
|
case map[string]interface{}: // gotta have the last key
|
||||||
|
if keys0 == "*" {
|
||||||
|
for k := range m.(map[string]interface{}) {
|
||||||
|
updateValue(key, value, m, k, subkeys, cnt)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
endVal, _ := m.(map[string]interface{})[keys0]
|
||||||
|
|
||||||
|
// if newV key is the end of path, replace the value for path-end
|
||||||
|
// may be []interface{} - means replace just an entry w/ subkeys
|
||||||
|
// otherwise replace the keys0 value if subkeys are there
|
||||||
|
// NOTE: this will replace the subkeys, also
|
||||||
|
if key == keys0 {
|
||||||
|
switch endVal.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if hasSubKeys(m, subkeys) {
|
||||||
|
(m.(map[string]interface{}))[keys0] = value
|
||||||
|
(*cnt)++
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
// without subkeys can't select list member to modify
|
||||||
|
// so key:value spec is it ...
|
||||||
|
if hasSubKeys(m, subkeys) {
|
||||||
|
(m.(map[string]interface{}))[keys0] = value
|
||||||
|
(*cnt)++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nv := make([]interface{}, 0)
|
||||||
|
var valmodified bool
|
||||||
|
for _, v := range endVal.([]interface{}) {
|
||||||
|
// check entry subkeys
|
||||||
|
if hasSubKeys(v, subkeys) {
|
||||||
|
// replace v with value
|
||||||
|
nv = append(nv, value)
|
||||||
|
valmodified = true
|
||||||
|
(*cnt)++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nv = append(nv, v)
|
||||||
|
}
|
||||||
|
if valmodified {
|
||||||
|
(m.(map[string]interface{}))[keys0] = interface{}(nv)
|
||||||
|
}
|
||||||
|
default: // anything else is a strict replacement
|
||||||
|
if hasSubKeys(m, subkeys) {
|
||||||
|
(m.(map[string]interface{}))[keys0] = value
|
||||||
|
(*cnt)++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// so value is for an element of endVal
|
||||||
|
// if endVal is a map then 'key' must be there w/ subkeys
|
||||||
|
// if endVal is a list then 'key' must be in a list member w/ subkeys
|
||||||
|
switch endVal.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if !hasSubKeys(endVal, subkeys) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := (endVal.(map[string]interface{}))[key]; ok {
|
||||||
|
(endVal.(map[string]interface{}))[key] = value
|
||||||
|
(*cnt)++
|
||||||
|
}
|
||||||
|
case []interface{}: // keys0 points to a list, check subkeys
|
||||||
|
for _, v := range endVal.([]interface{}) {
|
||||||
|
// got to be a map so we can replace value for 'key'
|
||||||
|
vv, vok := v.(map[string]interface{})
|
||||||
|
if !vok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := vv[key]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !hasSubKeys(vv, subkeys) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vv[key] = value
|
||||||
|
(*cnt)++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []interface{}: // key may be in a list member
|
||||||
|
// don't need to handle keys0 == "*"; we're looking at everything, anyway.
|
||||||
|
for _, v := range m.([]interface{}) {
|
||||||
|
// only map values - we're looking for 'key'
|
||||||
|
mm, ok := v.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := mm[key]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !hasSubKeys(mm, subkeys) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mm[key] = value
|
||||||
|
(*cnt)++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return
|
||||||
|
}
|
1139
vendor/github.com/clbanning/mxj/xml.go
generated
vendored
Normal file
1139
vendor/github.com/clbanning/mxj/xml.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
828
vendor/github.com/clbanning/mxj/xmlseq.go
generated
vendored
Normal file
828
vendor/github.com/clbanning/mxj/xmlseq.go
generated
vendored
Normal file
|
@ -0,0 +1,828 @@
|
||||||
|
// 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.
|
||||||
|
// The xml.Decoder.RawToken method is used to parse the XML, so there is no checking for appropriate xml.EndElement values;
|
||||||
|
// thus, it is assumed that the XML is valid.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
// 3. If CoerceKeysToSnakeCase() has been called, then all key values will be converted to snake case.
|
||||||
|
//
|
||||||
|
// NAME SPACES:
|
||||||
|
// 1. Keys in the Map value that are parsed from a <name space prefix>:<local name> tag preserve the
|
||||||
|
// "<prefix>:" notation rather than stripping it as with NewMapXml().
|
||||||
|
// 2. Attribute keys for name space prefix declarations preserve "xmlns:<prefix>" notation.
|
||||||
|
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.
|
||||||
|
// 3. If CoerceKeysToSnakeCase() has been called, then all key values will be converted to snake case.
|
||||||
|
func NewMapXmlSeqReader(xmlReader io.Reader, cast ...bool) (Map, error) {
|
||||||
|
var r bool
|
||||||
|
if len(cast) == 1 {
|
||||||
|
r = cast[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to put an *os.File reader in a ByteReader or the xml.NewDecoder
|
||||||
|
// will wrap it in a bufio.Reader and seek on the file beyond where the
|
||||||
|
// xml.Decoder parses!
|
||||||
|
if _, ok := xmlReader.(io.ByteReader); !ok {
|
||||||
|
xmlReader = myByteReader(xmlReader) // see code at EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the map
|
||||||
|
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.
|
||||||
|
// 5. If CoerceKeysToSnakeCase() has been called, then all key values will be converted to snake case.
|
||||||
|
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, 0)
|
||||||
|
wb := bytes.NewBuffer(buf)
|
||||||
|
trdr := myTeeReader(xmlReader, wb)
|
||||||
|
|
||||||
|
m, err := xmlSeqReaderToMap(trdr, r)
|
||||||
|
|
||||||
|
// retrieve the raw XML that was decoded
|
||||||
|
b := wb.Bytes()
|
||||||
|
|
||||||
|
// err may be NoRoot
|
||||||
|
return m, b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if CustomDecoder != nil {
|
||||||
|
useCustomDecoder(p)
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
if CustomDecoder != nil {
|
||||||
|
useCustomDecoder(p)
|
||||||
|
} else {
|
||||||
|
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) {
|
||||||
|
if snakeCaseKeys {
|
||||||
|
skey = strings.Replace(skey, "-", "_", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: all attributes and sub-elements parsed into 'na', 'na' is returned as value for 'skey' in 'n'.
|
||||||
|
var n, na map[string]interface{}
|
||||||
|
var seq int // for including seq num when decoding
|
||||||
|
|
||||||
|
// Allocate maps and load attributes, if any.
|
||||||
|
// NOTE: on entry from NewMapXml(), etc., skey=="", and we fall through
|
||||||
|
// to get StartElement then recurse with skey==xml.StartElement.Name.Local
|
||||||
|
// where we begin allocating map[string]interface{} values 'n' and 'na'.
|
||||||
|
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 {
|
||||||
|
if snakeCaseKeys {
|
||||||
|
v.Name.Local = strings.Replace(v.Name.Local, "-", "_", -1)
|
||||||
|
}
|
||||||
|
if len(v.Name.Space) > 0 {
|
||||||
|
aa[v.Name.Space+`:`+v.Name.Local] = map[string]interface{}{"#text": cast(v.Value, r), "#seq": i}
|
||||||
|
} else {
|
||||||
|
aa[v.Name.Local] = map[string]interface{}{"#text": cast(v.Value, r), "#seq": i}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
na["#attr"] = aa
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return XMPP <stream:stream> message.
|
||||||
|
if handleXMPPStreamTag && skey == "stream:stream" {
|
||||||
|
n[skey] = na
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
t, err := p.RawToken()
|
||||||
|
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 == "" {
|
||||||
|
if len(tt.Name.Space) > 0 {
|
||||||
|
return xmlSeqToMapParser(tt.Name.Space+`:`+tt.Name.Local, tt.Attr, p, r)
|
||||||
|
} else {
|
||||||
|
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'.
|
||||||
|
var nn map[string]interface{}
|
||||||
|
if len(tt.Name.Space) > 0 {
|
||||||
|
nn, err = xmlSeqToMapParser(tt.Name.Space+`:`+tt.Name.Local, tt.Attr, p, r)
|
||||||
|
} else {
|
||||||
|
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:
|
||||||
|
if skey != "" {
|
||||||
|
tt := t.(xml.EndElement)
|
||||||
|
if snakeCaseKeys {
|
||||||
|
tt.Name.Local = strings.Replace(tt.Name.Local, "-", "_", -1)
|
||||||
|
}
|
||||||
|
var name string
|
||||||
|
if len(tt.Name.Space) > 0 {
|
||||||
|
name = tt.Name.Space + `:` + tt.Name.Local
|
||||||
|
} else {
|
||||||
|
name = tt.Name.Local
|
||||||
|
}
|
||||||
|
if skey != name {
|
||||||
|
return nil, fmt.Errorf("element %s not properly terminated, got %s at #%d",
|
||||||
|
skey, name, p.InputOffset())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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.XmlSeq & mv.XmlSeqWriter -------------------------
|
||||||
|
|
||||||
|
// 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's 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 XmlSeq() 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 XmlSeq() 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 XmlSeq() 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
|
||||||
|
var ss string
|
||||||
|
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:
|
||||||
|
if xmlEscapeChars {
|
||||||
|
ss = escapeChars(vv["#text"].(string))
|
||||||
|
} else {
|
||||||
|
ss = vv["#text"].(string)
|
||||||
|
}
|
||||||
|
*s += ` ` + a.k + `="` + ss + `"`
|
||||||
|
case float64, bool, int, int32, int64, float32:
|
||||||
|
*s += ` ` + a.k + `="` + fmt.Sprintf("%v", vv["#text"]) + `"`
|
||||||
|
case []byte:
|
||||||
|
if xmlEscapeChars {
|
||||||
|
ss = escapeChars(string(vv["#text"].([]byte)))
|
||||||
|
} else {
|
||||||
|
ss = string(vv["#text"].([]byte))
|
||||||
|
}
|
||||||
|
*s += ` ` + a.k + `="` + ss + `"`
|
||||||
|
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 != "" {
|
||||||
|
if xmlEscapeChars {
|
||||||
|
stmp = escapeChars(stmp)
|
||||||
|
}
|
||||||
|
*s += ">" + stmp
|
||||||
|
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++
|
||||||
|
sort.Sort(elemListSeq(kv))
|
||||||
|
i := 0
|
||||||
|
for _, v := range kv {
|
||||||
|
switch v.v.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
default:
|
||||||
|
if i == 0 && doIndent {
|
||||||
|
p.Indent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
if err := mapToXmlSeqIndent(doIndent, s, v.k, v.v, p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
if err := mapToXmlSeqIndent(doIndent, s, key, v, p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if doIndent {
|
||||||
|
p.Outdent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case nil:
|
||||||
|
// terminate the tag
|
||||||
|
if doIndent {
|
||||||
|
*s += p.padding
|
||||||
|
}
|
||||||
|
*s += "<" + key
|
||||||
|
endTag, isSimple = true, true
|
||||||
|
break
|
||||||
|
default: // handle anything - even goofy stuff
|
||||||
|
elen = 0
|
||||||
|
switch value.(type) {
|
||||||
|
case string:
|
||||||
|
if xmlEscapeChars {
|
||||||
|
ss = escapeChars(value.(string))
|
||||||
|
} else {
|
||||||
|
ss = value.(string)
|
||||||
|
}
|
||||||
|
elen = len(ss)
|
||||||
|
if elen > 0 {
|
||||||
|
*s += ">" + ss
|
||||||
|
}
|
||||||
|
case 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
|
||||||
|
if xmlEscapeChars {
|
||||||
|
ss = escapeChars(string(value.([]byte)))
|
||||||
|
} else {
|
||||||
|
ss = string(value.([]byte))
|
||||||
|
}
|
||||||
|
elen = len(ss)
|
||||||
|
if elen > 0 {
|
||||||
|
*s += ">" + ss
|
||||||
|
}
|
||||||
|
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 + ">"
|
||||||
|
// *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
|
||||||
|
}
|
||||||
|
|
||||||
|
return iseq <= jseq
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============== https://groups.google.com/forum/#!topic/golang-nuts/lHPOHD-8qio
|
||||||
|
|
||||||
|
// BeautifyXml (re)formats an XML doc similar to Map.XmlIndent().
|
||||||
|
func BeautifyXml(b []byte, prefix, indent string) ([]byte, error) {
|
||||||
|
x, err := NewMapXmlSeq(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return x.XmlSeqIndent(prefix, indent)
|
||||||
|
}
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
|
@ -1,3 +1,5 @@
|
||||||
|
# github.com/clbanning/mxj v1.8.4
|
||||||
|
github.com/clbanning/mxj
|
||||||
# github.com/dustin/go-humanize v1.0.0
|
# github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/dustin/go-humanize
|
github.com/dustin/go-humanize
|
||||||
# github.com/ghodss/yaml v1.0.0
|
# github.com/ghodss/yaml v1.0.0
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
|
@ -17,6 +18,7 @@ import (
|
||||||
|
|
||||||
"github.com/adnanh/webhook/internal/hook"
|
"github.com/adnanh/webhook/internal/hook"
|
||||||
"github.com/adnanh/webhook/internal/middleware"
|
"github.com/adnanh/webhook/internal/middleware"
|
||||||
|
"github.com/clbanning/mxj"
|
||||||
chimiddleware "github.com/go-chi/chi/middleware"
|
chimiddleware "github.com/go-chi/chi/middleware"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
fsnotify "gopkg.in/fsnotify.v1"
|
fsnotify "gopkg.in/fsnotify.v1"
|
||||||
|
@ -264,6 +266,11 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
} else {
|
} else {
|
||||||
payload = valuesToMap(fd)
|
payload = valuesToMap(fd)
|
||||||
}
|
}
|
||||||
|
case strings.Contains(contentType, "xml"):
|
||||||
|
payload, err = mxj.NewMapXmlReader(bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[%s] error parsing JSON payload %+v\n", rid, err)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
log.Printf("[%s] error parsing body payload due to unsupported content type header: %s\n", rid, contentType)
|
log.Printf("[%s] error parsing body payload due to unsupported content type header: %s\n", rid, contentType)
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,8 +109,10 @@ func TestWebhook(t *testing.T) {
|
||||||
if tt.urlencoded == true {
|
if tt.urlencoded == true {
|
||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
} else {
|
} else {
|
||||||
|
if req.Header.Get("Content-Type") == "" {
|
||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
res, err = client.Do(req)
|
res, err = client.Do(req)
|
||||||
|
@ -518,7 +520,24 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
|
||||||
`,
|
`,
|
||||||
``,
|
``,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"xml",
|
||||||
|
"xml",
|
||||||
|
map[string]string{"Content-Type": "application/xml"},
|
||||||
|
`<app>
|
||||||
|
<users>
|
||||||
|
<user id="1" name="Jeff" />
|
||||||
|
<user id="2" name="Sally" />
|
||||||
|
</users>
|
||||||
|
<messages>
|
||||||
|
<message id="1" from_user="1" to_user="2">Hello!!</message>
|
||||||
|
</messages>
|
||||||
|
</app>`,
|
||||||
|
false,
|
||||||
|
http.StatusOK,
|
||||||
|
`success`,
|
||||||
|
``,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"missing-cmd-arg", // missing head_commit.author.email
|
"missing-cmd-arg", // missing head_commit.author.email
|
||||||
"github",
|
"github",
|
||||||
|
|
Loading…
Add table
Reference in a new issue