mirror of
https://github.com/adnanh/webhook.git
synced 2025-07-04 18:28:31 +00:00
Merge branch 'development' into feature/context-provider-command
This commit is contained in:
commit
49b375f625
451 changed files with 162922 additions and 42024 deletions
16
.travis.yml
16
.travis.yml
|
@ -1,23 +1,29 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
- 1.13.x
|
||||
- tip
|
||||
- master
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
- windows
|
||||
|
||||
arch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- go: tip
|
||||
- go: master
|
||||
exclude:
|
||||
- os: windows
|
||||
go: tip
|
||||
go: master
|
||||
- os: windows
|
||||
arch: arm64
|
||||
- os: osx
|
||||
arch: arm64
|
||||
|
||||
install:
|
||||
- go get -d -v -t ./...
|
||||
|
|
40
Godeps/Godeps.json
generated
40
Godeps/Godeps.json
generated
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"ImportPath": "github.com/adnanh/webhook",
|
||||
"GoVersion": "go1.9",
|
||||
"GodepVersion": "v79",
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/codegangsta/negroni",
|
||||
"Comment": "v0.2.0-151-g5bc66cf",
|
||||
"Rev": "5bc66cf1ad89af58511e07e108a31f219ed61012"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/ghodss/yaml",
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "0ca9ea5df5451ffdf184b4428c902747c2c11cd7"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/mux",
|
||||
"Comment": "v1.5.0-2-gbdd5a5a",
|
||||
"Rev": "bdd5a5a1b0b489d297b73eb62b5f6328df198bfc"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/satori/go.uuid",
|
||||
"Comment": "v1.1.0-8-g5bf94b6",
|
||||
"Rev": "5bf94b69c6b68ee1b541973bb8e1144db23a194b"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/sys/unix",
|
||||
"Rev": "ebfc5b4631820b793c9010c87fd8fef0f39eb082"
|
||||
},
|
||||
{
|
||||
"ImportPath": "gopkg.in/fsnotify.v1",
|
||||
"Comment": "v1.4.2",
|
||||
"Rev": "629574ca2a5df945712d3079857300b5e4da0236"
|
||||
},
|
||||
{
|
||||
"ImportPath": "gopkg.in/yaml.v2",
|
||||
"Rev": "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
||||
}
|
||||
]
|
||||
}
|
5
Godeps/Readme
generated
5
Godeps/Readme
generated
|
@ -1,5 +0,0 @@
|
|||
This directory tree is generated automatically by godep.
|
||||
|
||||
Please do not edit.
|
||||
|
||||
See https://github.com/tools/godep for more information.
|
16
README.md
16
README.md
|
@ -26,7 +26,7 @@ If you don't have time to waste configuring, hosting, debugging and maintaining
|
|||
# Getting started
|
||||
## Installation
|
||||
### Building from source
|
||||
To get started, first make sure you've properly set up your [Golang](http://golang.org/doc/install) environment and then run the
|
||||
To get started, first make sure you've properly set up your [Go](http://golang.org/doc/install) 1.12 or newer environment and then run
|
||||
```bash
|
||||
$ go get github.com/adnanh/webhook
|
||||
```
|
||||
|
@ -77,12 +77,26 @@ By performing a simple HTTP GET or POST request to that endpoint, your specified
|
|||
|
||||
However, hook defined like that could pose a security threat to your system, because anyone who knows your endpoint, can send a request and execute your command. To prevent that, you can use the `"trigger-rule"` property for your hook, to specify the exact circumstances under which the hook would be triggered. For example, you can use them to add a secret that you must supply as a parameter in order to successfully trigger the hook. Please check out the [Hook rules page](docs/Hook-Rules.md) for detailed list of available rules and their usage.
|
||||
|
||||
## Multipart Form Data
|
||||
[webhook][w] provides limited support the parsing of multipart form data.
|
||||
Multipart form data can contain two types of parts: values and files.
|
||||
All form _values_ are automatically added to the `payload` scope.
|
||||
Use the `parse-parameters-as-json` settings to parse a given value as JSON.
|
||||
All files are ignored unless they match one of the following criteria:
|
||||
|
||||
1. The `Content-Type` header is `application/json`.
|
||||
1. The part is named in the `parse-parameters-as-json` setting.
|
||||
|
||||
In either case, the given file part will be parsed as JSON and added to the `payload` map.
|
||||
|
||||
## Templates
|
||||
[webhook][w] can parse the `hooks.json` input file as a Go template when given the `-template` [CLI parameter](docs/Webhook-Parameters.md). See the [Templates page](docs/Templates.md) for more details on template usage.
|
||||
|
||||
## Using HTTPS
|
||||
[webhook][w] by default serves hooks using http. If you want [webhook][w] to serve secure content using https, you can use the `-secure` flag while starting [webhook][w]. Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate.
|
||||
|
||||
TLS version and cipher suite selection flags are available from the command line. To list available cipher suites, use the `-list-cipher-suites` flag. The `-tls-min-version` flag can be used with `-list-cipher-suites`.
|
||||
|
||||
## CORS Headers
|
||||
If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook][w] to set the appropriate CORS headers that will be returned with each response.
|
||||
|
||||
|
|
102
cipher_suites.go
Normal file
102
cipher_suites.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
// Copied from Go 1.14 tip src/crypto/tls/cipher_suites.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CipherSuite is a TLS cipher suite. Note that most functions in this package
|
||||
// accept and expose cipher suite IDs instead of this type.
|
||||
type CipherSuite struct {
|
||||
ID uint16
|
||||
Name string
|
||||
|
||||
// Supported versions is the list of TLS protocol versions that can
|
||||
// negotiate this cipher suite.
|
||||
SupportedVersions []uint16
|
||||
|
||||
// Insecure is true if the cipher suite has known security issues
|
||||
// due to its primitives, design, or implementation.
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
var (
|
||||
supportedUpToTLS12 = []uint16{tls.VersionTLS10, tls.VersionTLS11, tls.VersionTLS12}
|
||||
supportedOnlyTLS12 = []uint16{tls.VersionTLS12}
|
||||
supportedOnlyTLS13 = []uint16{tls.VersionTLS13}
|
||||
)
|
||||
|
||||
// CipherSuites returns a list of cipher suites currently implemented by this
|
||||
// package, excluding those with security issues, which are returned by
|
||||
// InsecureCipherSuites.
|
||||
//
|
||||
// The list is sorted by ID. Note that the default cipher suites selected by
|
||||
// this package might depend on logic that can't be captured by a static list.
|
||||
func CipherSuites() []*CipherSuite {
|
||||
return []*CipherSuite{
|
||||
{tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, "TLS_RSA_WITH_3DES_EDE_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_RSA_WITH_AES_128_CBC_SHA, "TLS_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_RSA_WITH_AES_256_CBC_SHA, "TLS_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_RSA_WITH_AES_128_GCM_SHA256, "TLS_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
|
||||
{tls.TLS_RSA_WITH_AES_256_GCM_SHA384, "TLS_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
|
||||
|
||||
{tls.TLS_AES_128_GCM_SHA256, "TLS_AES_128_GCM_SHA256", supportedOnlyTLS13, false},
|
||||
{tls.TLS_AES_256_GCM_SHA384, "TLS_AES_256_GCM_SHA384", supportedOnlyTLS13, false},
|
||||
{tls.TLS_CHACHA20_POLY1305_SHA256, "TLS_CHACHA20_POLY1305_SHA256", supportedOnlyTLS13, false},
|
||||
|
||||
{tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
|
||||
{tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
|
||||
{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
|
||||
{tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
|
||||
|
||||
// go1.14
|
||||
// {tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
|
||||
// {tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
|
||||
}
|
||||
}
|
||||
|
||||
// InsecureCipherSuites returns a list of cipher suites currently implemented by
|
||||
// this package and which have security issues.
|
||||
//
|
||||
// Most applications should not use the cipher suites in this list, and should
|
||||
// only use those returned by CipherSuites.
|
||||
func InsecureCipherSuites() []*CipherSuite {
|
||||
// RC4 suites are broken because RC4 is.
|
||||
// CBC-SHA256 suites have no Lucky13 countermeasures.
|
||||
return []*CipherSuite{
|
||||
{tls.TLS_RSA_WITH_RC4_128_SHA, "TLS_RSA_WITH_RC4_128_SHA", supportedUpToTLS12, true},
|
||||
{tls.TLS_RSA_WITH_AES_128_CBC_SHA256, "TLS_RSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true},
|
||||
{tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", supportedUpToTLS12, true},
|
||||
{tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, "TLS_ECDHE_RSA_WITH_RC4_128_SHA", supportedUpToTLS12, true},
|
||||
{tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true},
|
||||
{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true},
|
||||
}
|
||||
}
|
||||
|
||||
// CipherSuiteName returns the standard name for the passed cipher suite ID
|
||||
// (e.g. "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"), or a fallback representation
|
||||
// of the ID value if the cipher suite is not implemented by this package.
|
||||
func CipherSuiteName(id uint16) string {
|
||||
for _, c := range CipherSuites() {
|
||||
if c.ID == id {
|
||||
return c.Name
|
||||
}
|
||||
}
|
||||
for _, c := range InsecureCipherSuites() {
|
||||
if c.ID == id {
|
||||
return c.Name
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("0x%04X", id)
|
||||
}
|
|
@ -10,6 +10,7 @@ Hooks are defined as JSON objects. Please note that in order to be considered va
|
|||
* `response-headers` - specifies the list of headers in format `{"name": "X-Example-Header", "value": "it works"}` that will be returned in HTTP response for the hook
|
||||
* `success-http-response-code` - specifies the HTTP status code to be returned upon success
|
||||
* `incoming-payload-content-type` - sets the `Content-Type` of the incoming HTTP request (ie. `application/json`); useful when the request lacks a `Content-Type` or sends an erroneous value
|
||||
* `http-methods` - a list of allowed HTTP methods, such as `POST` and `GET`
|
||||
* `include-command-output-in-response` - boolean whether webhook should wait for the command to finish and return the raw output as a response to the hook initiator. If the command fails to execute or encounters any errors while executing the response will result in 500 Internal Server Error HTTP status code, otherwise the 200 OK status code will be returned.
|
||||
* `include-command-output-in-response-on-error` - boolean whether webhook should include command stdout & stderror as a response in failed executions. It only works if `include-command-output-in-response` is set to `true`.
|
||||
* `parse-parameters-as-json` - specifies the list of arguments that contain JSON strings. These parameters will be decoded by webhook and you can access them like regular objects in rules and `pass-arguments-to-command`.
|
||||
|
|
|
@ -287,12 +287,12 @@ __Not recommended in production due to low security__
|
|||
]
|
||||
```
|
||||
|
||||
# JIRA Webhooks
|
||||
## JIRA Webhooks
|
||||
[Guide by @perfecto25](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks)
|
||||
|
||||
# Pass File-to-command sample
|
||||
## Pass File-to-command sample
|
||||
|
||||
## Webhook configuration
|
||||
### Webhook configuration
|
||||
|
||||
<pre>
|
||||
[
|
||||
|
@ -315,7 +315,7 @@ __Not recommended in production due to low security__
|
|||
]
|
||||
</pre>
|
||||
|
||||
## Sample client usage
|
||||
### Sample client usage
|
||||
|
||||
Store the following file as `testRequest.json`.
|
||||
|
||||
|
@ -424,3 +424,97 @@ 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!!"
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Multipart Form Data
|
||||
|
||||
Example of a [Plex Media Server webhook](https://support.plex.tv/articles/115002267687-webhooks/).
|
||||
The Plex Media Server will send two parts: payload and thumb.
|
||||
We only care about the payload part.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "plex",
|
||||
"execute-command": "play-command.sh",
|
||||
"parse-parameters-as-json": [
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "payload"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "payload.event"
|
||||
},
|
||||
"value": "media.play"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
Each part of a multipart form data body will have a `Content-Disposition` header.
|
||||
Some example headers:
|
||||
|
||||
```
|
||||
Content-Disposition: form-data; name="payload"
|
||||
Content-Disposition: form-data; name="thumb"; filename="thumb.jpg"
|
||||
```
|
||||
|
||||
We key off of the `name` attribute in the `Content-Disposition` value.
|
||||
|
|
|
@ -186,7 +186,60 @@ For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
|
|||
}
|
||||
```
|
||||
|
||||
### 4. Match Whitelisted IP range
|
||||
Note that if multiple signatures were passed via a comma separated string, each
|
||||
will be tried unless a match is found. For example:
|
||||
|
||||
```
|
||||
X-Hub-Signature: sha1=the-first-signature,sha1=the-second-signature
|
||||
```
|
||||
|
||||
### 4. Match payload-hash-sha256
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha256",
|
||||
"secret": "yoursecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that if multiple signatures were passed via a comma separated string, each
|
||||
will be tried unless a match is found. For example:
|
||||
|
||||
```
|
||||
X-Hub-Signature: sha256=the-first-signature,sha256=the-second-signature
|
||||
```
|
||||
|
||||
### 5. Match payload-hash-sha512
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha512",
|
||||
"secret": "yoursecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that if multiple signatures were passed via a comma separated string, each
|
||||
will be tried unless a match is found. For example:
|
||||
|
||||
```
|
||||
X-Hub-Signature: sha512=the-first-signature,sha512=the-second-signature
|
||||
```
|
||||
|
||||
### 6. Match Whitelisted IP range
|
||||
|
||||
The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_blocks). To match a single IP address only, use `/32`.
|
||||
|
||||
|
@ -200,7 +253,7 @@ The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedi
|
|||
}
|
||||
```
|
||||
|
||||
### 5. Match scalr-signature
|
||||
### 7. Match scalr-signature
|
||||
|
||||
The trigger rule checks the scalr signature and also checks that the request was signed less than 5 minutes before it was received.
|
||||
A unqiue signing key is generated for each webhook endpoint URL you register in Scalr.
|
||||
|
|
|
@ -68,6 +68,34 @@ There are four 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.
|
||||
|
||||
3. XML Payload
|
||||
|
||||
Referencing XML payload parameters is much like the JSON examples above, but XML is more complex.
|
||||
Element attributes are prefixed by a hyphen (`-`).
|
||||
Element values are prefixed by a pound (`#`).
|
||||
|
||||
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` element, 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
|
||||
```json
|
||||
{
|
||||
|
|
|
@ -3,36 +3,60 @@
|
|||
Usage of webhook:
|
||||
-cert string
|
||||
path to the HTTPS certificate pem file (default "cert.pem")
|
||||
-cipher-suites string
|
||||
comma-separated list of supported TLS cipher suites
|
||||
-debug
|
||||
show debug output
|
||||
-header value
|
||||
response header to return, specified in format name=value, use multiple times to set multiple headers
|
||||
-hooks value
|
||||
path to the json file containing defined hooks the webhook should serve, use multiple times to load from different files
|
||||
-hotreload
|
||||
watch hooks file for changes and reload them automatically
|
||||
-http-methods string
|
||||
globally restrict allowed HTTP methods; separate methods with comma
|
||||
-ip string
|
||||
ip the webhook should serve hooks on (default "0.0.0.0")
|
||||
-key string
|
||||
path to the HTTPS certificate private key pem file (default "key.pem")
|
||||
-list-cipher-suites
|
||||
list available TLS cipher suites
|
||||
-logfile string
|
||||
send log output to a file; implicitly enables verbose logging
|
||||
-nopanic
|
||||
do not panic if hooks cannot be loaded when webhook is not running in verbose mode
|
||||
-pidfile string
|
||||
create PID file at the given path
|
||||
-port int
|
||||
port the webhook should serve hooks on (default 9000)
|
||||
-secure
|
||||
use HTTPS instead of HTTP
|
||||
-setgid int
|
||||
set group ID after opening listening port; must be used with setuid
|
||||
-setuid int
|
||||
set user ID after opening listening port; must be used with setgid
|
||||
-template
|
||||
parse hooks file as a Go template
|
||||
-tls-min-version string
|
||||
minimum TLS version (1.0, 1.1, 1.2, 1.3) (default "1.2")
|
||||
-urlprefix string
|
||||
url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id) (default "hooks")
|
||||
-verbose
|
||||
show verbose output
|
||||
-version
|
||||
display webhook version and quit
|
||||
-x-request-id
|
||||
use X-Request-Id header, if present, as request ID
|
||||
-x-request-id-limit int
|
||||
truncate X-Request-Id header to limit; default no limit
|
||||
```
|
||||
|
||||
Use any of the above specified flags to override their default behavior.
|
||||
|
||||
# Live reloading hooks
|
||||
If you are running an OS that supports USR1 signal, you can use it to trigger hooks reload from hooks file, without restarting the webhook instance.
|
||||
If you are running an OS that supports the HUP or USR1 signal, you can use it to trigger hooks reload from hooks file, without restarting the webhook instance.
|
||||
```bash
|
||||
kill -USR1 webhookpid
|
||||
|
||||
kill -HUP webhookpid
|
||||
```
|
12
droppriv_nope.go
Normal file
12
droppriv_nope.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
// +build linux windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func dropPrivileges(uid, gid int) error {
|
||||
return errors.New("setuid and setgid not supported on " + runtime.GOOS)
|
||||
}
|
21
droppriv_unix.go
Normal file
21
droppriv_unix.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
// +build !windows,!linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func dropPrivileges(uid, gid int) error {
|
||||
err := syscall.Setgid(gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = syscall.Setuid(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
19
go.mod
Normal file
19
go.mod
Normal file
|
@ -0,0 +1,19 @@
|
|||
module github.com/adnanh/webhook
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/clbanning/mxj v1.8.4
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fsnotify/fsnotify v1.4.7 // indirect
|
||||
github.com/ghodss/yaml v1.0.0
|
||||
github.com/go-chi/chi v4.0.2+incompatible
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/fsnotify.v1 v1.4.2
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7 // indirect
|
||||
)
|
34
go.sum
Normal file
34
go.sum
Normal file
|
@ -0,0 +1,34 @@
|
|||
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/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
|
||||
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.2 h1:AwZiD/bIUttYJ+n/k1UwlSUsM+VSE6id7UAnSKqQ+Tc=
|
||||
gopkg.in/fsnotify.v1 v1.4.2/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7 h1:+t9dhfO+GNOIGJof6kPOAenx7YgrZMTdRPV+EsnPabk=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
|
@ -5,11 +5,14 @@ import (
|
|||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
|
@ -47,15 +50,43 @@ const (
|
|||
EnvNamespace string = "HOOK_"
|
||||
)
|
||||
|
||||
// ParameterNodeError describes an error walking a parameter node.
|
||||
type ParameterNodeError struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (e *ParameterNodeError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("parameter node not found: %s", e.key)
|
||||
}
|
||||
|
||||
// IsParameterNodeError returns whether err is of type ParameterNodeError.
|
||||
func IsParameterNodeError(err error) bool {
|
||||
switch err.(type) {
|
||||
case *ParameterNodeError:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// SignatureError describes an invalid payload signature passed to Hook.
|
||||
type SignatureError struct {
|
||||
Signature string
|
||||
Signatures []string
|
||||
}
|
||||
|
||||
func (e *SignatureError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
if e.Signatures != nil {
|
||||
return fmt.Sprintf("invalid payload signatures %s", e.Signatures)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("invalid payload signature %s", e.Signature)
|
||||
}
|
||||
|
||||
|
@ -95,25 +126,67 @@ func (e *ParseError) Error() string {
|
|||
return e.Err.Error()
|
||||
}
|
||||
|
||||
// ExtractCommaSeparatedValues will extract the values matching the key.
|
||||
func ExtractCommaSeparatedValues(source, prefix string) []string {
|
||||
parts := strings.Split(source, ",")
|
||||
values := make([]string, 0)
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, prefix) {
|
||||
values = append(values, strings.TrimPrefix(part, prefix))
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
// ExtractSignatures will extract all the signatures from the source.
|
||||
func ExtractSignatures(source, prefix string) []string {
|
||||
// If there are multiple possible matches, let the comma seperated extractor
|
||||
// do it's work.
|
||||
if strings.Contains(source, ",") {
|
||||
return ExtractCommaSeparatedValues(source, prefix)
|
||||
}
|
||||
|
||||
// There were no commas, so just trim the prefix (if it even exists) and
|
||||
// pass it back.
|
||||
return []string{
|
||||
strings.TrimPrefix(source, prefix),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateMAC will verify that the expected mac for the given hash will match
|
||||
// the one provided.
|
||||
func ValidateMAC(payload []byte, mac hash.Hash, signatures []string) (string, error) {
|
||||
// Write the payload to the provided hash.
|
||||
_, err := mac.Write(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
for _, signature := range signatures {
|
||||
if hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||
return expectedMAC, err
|
||||
}
|
||||
}
|
||||
|
||||
return expectedMAC, &SignatureError{
|
||||
Signatures: signatures,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
||||
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
signature = strings.TrimPrefix(signature, "sha1=")
|
||||
// Extract the signatures.
|
||||
signatures := ExtractSignatures(signature, "sha1=")
|
||||
|
||||
mac := hmac.New(sha1.New, []byte(secret))
|
||||
_, err := mac.Write(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||
return expectedMAC, &SignatureError{signature}
|
||||
}
|
||||
return expectedMAC, err
|
||||
// Validate the MAC.
|
||||
return ValidateMAC(payload, hmac.New(sha1.New, []byte(secret)), signatures)
|
||||
}
|
||||
|
||||
// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload
|
||||
|
@ -122,19 +195,24 @@ func CheckPayloadSignature256(payload []byte, secret string, signature string) (
|
|||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
signature = strings.TrimPrefix(signature, "sha256=")
|
||||
// Extract the signatures.
|
||||
signatures := ExtractSignatures(signature, "sha256=")
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, err := mac.Write(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
// Validate the MAC.
|
||||
return ValidateMAC(payload, hmac.New(sha256.New, []byte(secret)), signatures)
|
||||
}
|
||||
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||
return expectedMAC, &SignatureError{signature}
|
||||
// CheckPayloadSignature512 calculates and verifies SHA512 signature of the given payload
|
||||
func CheckPayloadSignature512(payload []byte, secret string, signature string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
return expectedMAC, err
|
||||
|
||||
// Extract the signatures.
|
||||
signatures := ExtractSignatures(signature, "sha512=")
|
||||
|
||||
// Validate the MAC.
|
||||
return ValidateMAC(payload, hmac.New(sha512.New, []byte(secret)), signatures)
|
||||
}
|
||||
|
||||
func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey string, checkDate bool) (bool, error) {
|
||||
|
@ -157,7 +235,7 @@ func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey
|
|||
expectedSignature := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(providedSignature), []byte(expectedSignature)) {
|
||||
return false, &SignatureError{providedSignature}
|
||||
return false, &SignatureError{Signature: providedSignature}
|
||||
}
|
||||
|
||||
if !checkDate {
|
||||
|
@ -172,7 +250,7 @@ func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey
|
|||
delta := math.Abs(now.Sub(date).Seconds())
|
||||
|
||||
if delta > 300 {
|
||||
return false, &SignatureError{"outdated"}
|
||||
return false, &SignatureError{Signature: "outdated"}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
@ -254,9 +332,9 @@ func ReplaceParameter(s string, params interface{}, value interface{}) bool {
|
|||
}
|
||||
|
||||
// GetParameter extracts interface{} value based on the passed string
|
||||
func GetParameter(s string, params interface{}) (interface{}, bool) {
|
||||
func GetParameter(s string, params interface{}) (interface{}, error) {
|
||||
if params == nil {
|
||||
return nil, false
|
||||
return nil, errors.New("no parameters")
|
||||
}
|
||||
|
||||
paramsValue := reflect.ValueOf(params)
|
||||
|
@ -270,7 +348,7 @@ func GetParameter(s string, params interface{}) (interface{}, bool) {
|
|||
index, err := strconv.ParseUint(p[0], 10, 64)
|
||||
|
||||
if err != nil || paramsValueSliceLength <= int(index) {
|
||||
return nil, false
|
||||
return nil, &ParameterNodeError{s}
|
||||
}
|
||||
|
||||
return GetParameter(p[1], params.([]interface{})[index])
|
||||
|
@ -279,18 +357,18 @@ func GetParameter(s string, params interface{}) (interface{}, bool) {
|
|||
index, err := strconv.ParseUint(s, 10, 64)
|
||||
|
||||
if err != nil || paramsValueSliceLength <= int(index) {
|
||||
return nil, false
|
||||
return nil, &ParameterNodeError{s}
|
||||
}
|
||||
|
||||
return params.([]interface{})[index], true
|
||||
return params.([]interface{})[index], nil
|
||||
}
|
||||
|
||||
return nil, false
|
||||
return nil, &ParameterNodeError{s}
|
||||
|
||||
case reflect.Map:
|
||||
// Check for raw key
|
||||
if v, ok := params.(map[string]interface{})[s]; ok {
|
||||
return v, true
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Checked for dotted references
|
||||
|
@ -300,19 +378,21 @@ func GetParameter(s string, params interface{}) (interface{}, bool) {
|
|||
return GetParameter(p[1], pValue)
|
||||
}
|
||||
|
||||
return pValue, true
|
||||
return pValue, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
return nil, &ParameterNodeError{s}
|
||||
}
|
||||
|
||||
// ExtractParameterAsString extracts value from interface{} as string based on the passed string
|
||||
func ExtractParameterAsString(s string, params interface{}) (string, bool) {
|
||||
if pValue, ok := GetParameter(s, params); ok {
|
||||
return fmt.Sprintf("%v", pValue), true
|
||||
func ExtractParameterAsString(s string, params interface{}) (string, error) {
|
||||
pValue, err := GetParameter(s, params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", false
|
||||
|
||||
return fmt.Sprintf("%v", pValue), nil
|
||||
}
|
||||
|
||||
// Argument type specifies the parameter key name and the source it should
|
||||
|
@ -326,7 +406,7 @@ type Argument struct {
|
|||
|
||||
// Get Argument method returns the value for the Argument's key name
|
||||
// based on the Argument's source
|
||||
func (ha *Argument) Get(headers, query, payload *map[string]interface{}, context *map[string]interface{}) (string, bool) {
|
||||
func (ha *Argument) Get(headers, query, payload *map[string]interface{}, context *map[string]interface{}) (string, error) {
|
||||
var source *map[string]interface{}
|
||||
key := ha.Name
|
||||
|
||||
|
@ -341,35 +421,35 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}, context
|
|||
case SourceContext:
|
||||
source = context
|
||||
case SourceString:
|
||||
return ha.Name, true
|
||||
return ha.Name, nil
|
||||
case SourceEntirePayload:
|
||||
r, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", false
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(r), true
|
||||
return string(r), nil
|
||||
case SourceEntireHeaders:
|
||||
r, err := json.Marshal(headers)
|
||||
if err != nil {
|
||||
return "", false
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(r), true
|
||||
return string(r), nil
|
||||
case SourceEntireQuery:
|
||||
r, err := json.Marshal(query)
|
||||
if err != nil {
|
||||
return "", false
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(r), true
|
||||
return string(r), nil
|
||||
}
|
||||
|
||||
if source != nil {
|
||||
return ExtractParameterAsString(key, *source)
|
||||
}
|
||||
|
||||
return "", false
|
||||
return "", errors.New("no source for value retrieval")
|
||||
}
|
||||
|
||||
// Header is a structure containing header name and it's value
|
||||
|
@ -455,6 +535,7 @@ type Hook struct {
|
|||
TriggerRuleMismatchHttpResponseCode int `json:"trigger-rule-mismatch-http-response-code,omitempty"`
|
||||
IncomingPayloadContentType string `json:"incoming-payload-content-type,omitempty"`
|
||||
SuccessHttpResponseCode int `json:"success-http-response-code,omitempty"`
|
||||
HTTPMethods []string `json:"http-methods"`
|
||||
}
|
||||
|
||||
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
||||
|
@ -463,7 +544,10 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
|
|||
errors := make([]error, 0)
|
||||
|
||||
for i := range h.JSONStringParameters {
|
||||
if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload, context); ok {
|
||||
arg, err := h.JSONStringParameters[i].Get(headers, query, payload, context)
|
||||
if err != nil {
|
||||
errors = append(errors, &ArgumentError{h.JSONStringParameters[i]})
|
||||
} else {
|
||||
var newArg map[string]interface{}
|
||||
|
||||
decoder := json.NewDecoder(strings.NewReader(string(arg)))
|
||||
|
@ -499,8 +583,6 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
|
|||
} else {
|
||||
errors = append(errors, &SourceError{h.JSONStringParameters[i]})
|
||||
}
|
||||
} else {
|
||||
errors = append(errors, &ArgumentError{h.JSONStringParameters[i]})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -520,12 +602,14 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter
|
|||
args = append(args, h.ExecuteCommand)
|
||||
|
||||
for i := range h.PassArgumentsToCommand {
|
||||
if arg, ok := h.PassArgumentsToCommand[i].Get(headers, query, payload, context); ok {
|
||||
args = append(args, arg)
|
||||
} else {
|
||||
arg, err := h.PassArgumentsToCommand[i].Get(headers, query, payload, context)
|
||||
if err != nil {
|
||||
args = append(args, "")
|
||||
errors = append(errors, &ArgumentError{h.PassArgumentsToCommand[i]})
|
||||
continue
|
||||
}
|
||||
|
||||
args = append(args, arg)
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
|
@ -542,7 +626,12 @@ func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string
|
|||
args := make([]string, 0)
|
||||
errors := make([]error, 0)
|
||||
for i := range h.PassEnvironmentToCommand {
|
||||
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload, context); ok {
|
||||
arg, err := h.PassEnvironmentToCommand[i].Get(headers, query, payload, context)
|
||||
if err != nil {
|
||||
errors = append(errors, &ArgumentError{h.PassEnvironmentToCommand[i]})
|
||||
continue
|
||||
}
|
||||
|
||||
if h.PassEnvironmentToCommand[i].EnvName != "" {
|
||||
// first try to use the EnvName if specified
|
||||
args = append(args, h.PassEnvironmentToCommand[i].EnvName+"="+arg)
|
||||
|
@ -550,9 +639,6 @@ func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string
|
|||
// then fallback on the name
|
||||
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
||||
}
|
||||
} else {
|
||||
errors = append(errors, &ArgumentError{h.PassEnvironmentToCommand[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
|
@ -576,7 +662,11 @@ func (h *Hook) ExtractCommandArgumentsForFile(headers, query, payload *map[strin
|
|||
args := make([]FileParameter, 0)
|
||||
errors := make([]error, 0)
|
||||
for i := range h.PassFileToCommand {
|
||||
if arg, ok := h.PassFileToCommand[i].Get(headers, query, payload, context); ok {
|
||||
arg, err := h.PassFileToCommand[i].Get(headers, query, payload, context)
|
||||
if err != nil {
|
||||
errors = append(errors, &ArgumentError{h.PassFileToCommand[i]})
|
||||
continue
|
||||
}
|
||||
|
||||
if h.PassFileToCommand[i].EnvName == "" {
|
||||
// if no environment-variable name is set, fall-back on the name
|
||||
|
@ -596,10 +686,6 @@ func (h *Hook) ExtractCommandArgumentsForFile(headers, query, payload *map[strin
|
|||
}
|
||||
|
||||
args = append(args, FileParameter{EnvName: h.PassFileToCommand[i].EnvName, Data: fileContent})
|
||||
|
||||
} else {
|
||||
errors = append(errors, &ArgumentError{h.PassFileToCommand[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
|
@ -645,8 +731,7 @@ func (h *Hooks) LoadFromFile(path string, asTemplate bool) error {
|
|||
file = buf.Bytes()
|
||||
}
|
||||
|
||||
e = yaml.Unmarshal(file, h)
|
||||
return e
|
||||
return yaml.Unmarshal(file, h)
|
||||
}
|
||||
|
||||
// Append appends hooks unless the new hooks contain a hook with an ID that already exists
|
||||
|
@ -768,6 +853,7 @@ const (
|
|||
MatchRegex string = "regex"
|
||||
MatchHashSHA1 string = "payload-hash-sha1"
|
||||
MatchHashSHA256 string = "payload-hash-sha256"
|
||||
MatchHashSHA512 string = "payload-hash-sha512"
|
||||
IPWhitelist string = "ip-whitelist"
|
||||
ScalrSignature string = "scalr-signature"
|
||||
)
|
||||
|
@ -782,10 +868,11 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, con
|
|||
return CheckScalrSignature(*headers, *body, r.Secret, true)
|
||||
}
|
||||
|
||||
if arg, ok := r.Parameter.Get(headers, query, payload, context); ok {
|
||||
arg, err := r.Parameter.Get(headers, query, payload, context)
|
||||
if err == nil {
|
||||
switch r.Type {
|
||||
case MatchValue:
|
||||
return arg == r.Value, nil
|
||||
return compare(arg, r.Value), nil
|
||||
case MatchRegex:
|
||||
return regexp.MatchString(r.Regex, arg)
|
||||
case MatchHashSHA1:
|
||||
|
@ -794,9 +881,17 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, con
|
|||
case MatchHashSHA256:
|
||||
_, err := CheckPayloadSignature256(*body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
case MatchHashSHA512:
|
||||
_, err := CheckPayloadSignature512(*body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
return false, err
|
||||
}
|
||||
|
||||
// compare is a helper function for constant time string comparisons.
|
||||
func compare(a, b string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
|
||||
// getenv provides a template function to retrieve OS environment variables.
|
|
@ -28,9 +28,9 @@ func TestGetParameter(t *testing.T) {
|
|||
{"z.b", map[string]interface{}{"a": map[string]interface{}{"z": 2}}, nil, false},
|
||||
{"a.2", map[string]interface{}{"a": []interface{}{"a", "b"}}, nil, false},
|
||||
} {
|
||||
res, ok := GetParameter(test.key, test.val)
|
||||
if ok != test.ok {
|
||||
t.Errorf("unexpected result given {%q, %q}: %t\n", test.key, test.val, ok)
|
||||
res, err := GetParameter(test.key, test.val)
|
||||
if (err == nil) != test.ok {
|
||||
t.Errorf("unexpected result given {%q, %q}: %s\n", test.key, test.val, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(res, test.expect) {
|
||||
|
@ -48,8 +48,11 @@ var checkPayloadSignatureTests = []struct {
|
|||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
|
||||
{[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false},
|
||||
}
|
||||
|
@ -76,8 +79,11 @@ var checkPayloadSignature256Tests = []struct {
|
|||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
||||
{[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false},
|
||||
}
|
||||
|
||||
|
@ -94,6 +100,33 @@ func TestCheckPayloadSignature256(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
var checkPayloadSignature512Tests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha512=4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", false},
|
||||
{[]byte(`{"a": "z"}`), "", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature512(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignature512Tests {
|
||||
mac, err := CheckPayloadSignature512(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkScalrSignatureTests = []struct {
|
||||
description string
|
||||
headers map[string]interface{}
|
||||
|
@ -198,9 +231,9 @@ var extractParameterTests = []struct {
|
|||
|
||||
func TestExtractParameter(t *testing.T) {
|
||||
for _, tt := range extractParameterTests {
|
||||
value, ok := ExtractParameterAsString(tt.s, tt.params)
|
||||
if ok != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to extract parameter %q:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.s, tt.value, tt.ok, value, ok)
|
||||
value, err := ExtractParameterAsString(tt.s, tt.params)
|
||||
if (err == nil) != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to extract parameter %q:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%s}", tt.s, tt.value, tt.ok, value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -225,9 +258,9 @@ var argumentGetTests = []struct {
|
|||
func TestArgumentGet(t *testing.T) {
|
||||
for _, tt := range argumentGetTests {
|
||||
a := Argument{tt.source, tt.name, "", false}
|
||||
value, ok := a.Get(tt.headers, tt.query, tt.payload)
|
||||
if ok != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.source, tt.name, tt.value, tt.ok, value, ok)
|
||||
value, err := a.Get(tt.headers, tt.query, tt.payload)
|
||||
if (err == nil) != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%s}", tt.source, tt.name, tt.value, tt.ok, value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -346,10 +379,10 @@ var hooksLoadFromFileTests = []struct {
|
|||
asTemplate bool
|
||||
ok bool
|
||||
}{
|
||||
{"../hooks.json.example", false, true},
|
||||
{"../hooks.yaml.example", false, true},
|
||||
{"../hooks.json.tmpl.example", true, true},
|
||||
{"../hooks.yaml.tmpl.example", true, true},
|
||||
{"../../hooks.json.example", false, true},
|
||||
{"../../hooks.yaml.example", false, true},
|
||||
{"../../hooks.json.tmpl.example", true, true},
|
||||
{"../../hooks.yaml.tmpl.example", true, true},
|
||||
{"", false, true},
|
||||
// failures
|
||||
{"missing.json", false, false},
|
||||
|
@ -425,7 +458,7 @@ var matchRuleTests = []struct {
|
|||
// failures
|
||||
{"value", "", "", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"regex", "^X", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"value", "", "2", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, false}, // reference invalid header
|
||||
{"value", "", "2", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, true}, // reference invalid header
|
||||
// errors
|
||||
{"regex", "*", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
|
@ -450,7 +483,7 @@ func TestMatchRule(t *testing.T) {
|
|||
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param, tt.ipRange}
|
||||
ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, tt.remoteAddr)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, (err != nil))
|
||||
t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -513,7 +546,7 @@ var andRuleTests = []struct {
|
|||
"invalid rule",
|
||||
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}},
|
||||
&map[string]interface{}{"Y": "z"}, nil, nil, nil,
|
||||
false, false,
|
||||
false, true,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -568,7 +601,7 @@ var orRuleTests = []struct {
|
|||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"Y": "Z"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
false, true,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -601,3 +634,17 @@ func TestNotRule(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
a, b string
|
||||
ok bool
|
||||
}{
|
||||
{"abcd", "abcd", true},
|
||||
{"zyxw", "abcd", false},
|
||||
} {
|
||||
if ok := compare(tt.a, tt.b); ok != tt.ok {
|
||||
t.Errorf("compare failed for %q and %q: got %v\n", tt.a, tt.b, ok)
|
||||
}
|
||||
}
|
||||
}
|
124
internal/middleware/dumper.go
Normal file
124
internal/middleware/dumper.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package middleware
|
||||
|
||||
// Derived from from the Goa project, MIT Licensed
|
||||
// https://github.com/goadesign/goa/blob/v3/http/middleware/debug.go
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// responseDupper tees the response to a buffer and a response writer.
|
||||
type responseDupper struct {
|
||||
http.ResponseWriter
|
||||
Buffer *bytes.Buffer
|
||||
Status int
|
||||
}
|
||||
|
||||
// Dumper returns a debug middleware which prints detailed information about
|
||||
// incoming requests and outgoing responses including all headers, parameters
|
||||
// and bodies.
|
||||
func Dumper(w io.Writer) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
buf := &bytes.Buffer{}
|
||||
// Request ID
|
||||
rid := r.Context().Value(RequestIDKey)
|
||||
|
||||
// Request URL
|
||||
buf.WriteString(fmt.Sprintf("> [%s] %s %s", rid, r.Method, r.URL.String()))
|
||||
|
||||
// Request Headers
|
||||
keys := make([]string, len(r.Header))
|
||||
i := 0
|
||||
for k := range r.Header {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
buf.WriteString(fmt.Sprintf("\n> [%s] %s: %s", rid, k, strings.Join(r.Header[k], ", ")))
|
||||
}
|
||||
|
||||
// Request parameters
|
||||
params := mux.Vars(r)
|
||||
keys = make([]string, len(params))
|
||||
i = 0
|
||||
for k := range params {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
buf.WriteString(fmt.Sprintf("\n> [%s] %s: %s", rid, k, strings.Join(r.Header[k], ", ")))
|
||||
}
|
||||
|
||||
// Request body
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
b = []byte("failed to read body: " + err.Error())
|
||||
}
|
||||
if len(b) > 0 {
|
||||
buf.WriteByte('\n')
|
||||
lines := strings.Split(string(b), "\n")
|
||||
for _, line := range lines {
|
||||
buf.WriteString(fmt.Sprintf("> [%s] %s\n", rid, line))
|
||||
}
|
||||
}
|
||||
r.Body = ioutil.NopCloser(bytes.NewBuffer(b))
|
||||
|
||||
dupper := &responseDupper{ResponseWriter: rw, Buffer: &bytes.Buffer{}}
|
||||
h.ServeHTTP(dupper, r)
|
||||
|
||||
buf.WriteString(fmt.Sprintf("\n< [%s] %s", rid, http.StatusText(dupper.Status)))
|
||||
keys = make([]string, len(dupper.Header()))
|
||||
i = 0
|
||||
for k := range dupper.Header() {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
buf.WriteString(fmt.Sprintf("\n< [%s] %s: %s", rid, k, strings.Join(dupper.Header()[k], ", ")))
|
||||
}
|
||||
if dupper.Buffer.Len() > 0 {
|
||||
buf.WriteByte('\n')
|
||||
lines := strings.Split(dupper.Buffer.String(), "\n")
|
||||
for _, line := range lines {
|
||||
buf.WriteString(fmt.Sprintf("< [%s] %s\n", rid, line))
|
||||
}
|
||||
}
|
||||
buf.WriteByte('\n')
|
||||
w.Write(buf.Bytes())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes the data to the buffer and connection as part of an HTTP reply.
|
||||
func (r *responseDupper) Write(b []byte) (int, error) {
|
||||
r.Buffer.Write(b)
|
||||
return r.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// WriteHeader records the status and sends an HTTP response header with status code.
|
||||
func (r *responseDupper) WriteHeader(s int) {
|
||||
r.Status = s
|
||||
r.ResponseWriter.WriteHeader(s)
|
||||
}
|
||||
|
||||
// Hijack supports the http.Hijacker interface.
|
||||
func (r *responseDupper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hijacker, ok := r.ResponseWriter.(http.Hijacker); ok {
|
||||
return hijacker.Hijack()
|
||||
}
|
||||
return nil, nil, fmt.Errorf("dumper middleware: inner ResponseWriter cannot be hijacked: %T", r.ResponseWriter)
|
||||
}
|
59
internal/middleware/logger.go
Normal file
59
internal/middleware/logger.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
)
|
||||
|
||||
// Logger is a middleware that logs useful data about each HTTP request.
|
||||
type Logger struct {
|
||||
Logger middleware.LoggerInterface
|
||||
}
|
||||
|
||||
// NewLogger creates a new RequestLogger Handler.
|
||||
func NewLogger() func(next http.Handler) http.Handler {
|
||||
return middleware.RequestLogger(&Logger{})
|
||||
}
|
||||
|
||||
// NewLogEntry creates a new LogEntry for the request.
|
||||
func (l *Logger) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
e := &LogEntry{
|
||||
req: r,
|
||||
buf: &bytes.Buffer{},
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// LogEntry represents an individual log entry.
|
||||
type LogEntry struct {
|
||||
*Logger
|
||||
req *http.Request
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
// Write constructs and writes the final log entry.
|
||||
func (l *LogEntry) Write(status, bytes int, elapsed time.Duration) {
|
||||
rid := GetReqID(l.req.Context())
|
||||
if rid != "" {
|
||||
fmt.Fprintf(l.buf, "[%s] ", rid)
|
||||
}
|
||||
|
||||
fmt.Fprintf(l.buf, "%03d | %s | %s | ", status, humanize.IBytes(uint64(bytes)), elapsed)
|
||||
l.buf.WriteString(l.req.Host + " | " + l.req.Method + " " + l.req.RequestURI)
|
||||
log.Print(l.buf.String())
|
||||
}
|
||||
|
||||
/// Panic prints the call stack for a panic.
|
||||
func (l *LogEntry) Panic(v interface{}, stack []byte) {
|
||||
e := l.NewLogEntry(l.req).(*LogEntry)
|
||||
fmt.Fprintf(e.buf, "panic: %#v", v)
|
||||
log.Print(e.buf.String())
|
||||
log.Print(string(stack))
|
||||
}
|
98
internal/middleware/request_id.go
Normal file
98
internal/middleware/request_id.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package middleware
|
||||
|
||||
// Derived from Goa project, MIT Licensed
|
||||
// https://github.com/goadesign/goa/blob/v3/http/middleware/requestid.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
// Key to use when setting the request ID.
|
||||
type ctxKeyRequestID int
|
||||
|
||||
// RequestIDKey is the key that holds the unique request ID in a request context.
|
||||
const RequestIDKey ctxKeyRequestID = 0
|
||||
|
||||
// RequestID is a middleware that injects a request ID into the context of each
|
||||
// request.
|
||||
func RequestID(options ...RequestIDOption) func(http.Handler) http.Handler {
|
||||
o := newRequestIDOptions(options...)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var id string
|
||||
|
||||
if o.UseRequestID() {
|
||||
id = r.Header.Get("X-Request-Id")
|
||||
if o.requestIDLimit > 0 && len(id) > o.requestIDLimit {
|
||||
id = id[:o.requestIDLimit]
|
||||
}
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
id = uuid.Must(uuid.NewV4()).String()[:6]
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, RequestIDKey, id)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetReqID returns a request ID from the given context if one is present.
|
||||
// Returns the empty string if a request ID cannot be found.
|
||||
func GetReqID(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
|
||||
return reqID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func UseXRequestIDHeaderOption(f bool) RequestIDOption {
|
||||
return func(o *RequestIDOptions) *RequestIDOptions {
|
||||
o.useXRequestID = f
|
||||
return o
|
||||
}
|
||||
}
|
||||
|
||||
func XRequestIDLimitOption(limit int) RequestIDOption {
|
||||
return func(o *RequestIDOptions) *RequestIDOptions {
|
||||
o.requestIDLimit = limit
|
||||
return o
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
RequestIDOption func(*RequestIDOptions) *RequestIDOptions
|
||||
|
||||
RequestIDOptions struct {
|
||||
// useXRequestID enabled the use of the X-Request-Id request header as
|
||||
// the request ID.
|
||||
useXRequestID bool
|
||||
|
||||
// requestIDLimit is the maximum length of the X-Request-Id header
|
||||
// allowed. Values longer than this value are truncated. Zero value
|
||||
// means no limit.
|
||||
requestIDLimit int
|
||||
}
|
||||
)
|
||||
|
||||
func newRequestIDOptions(options ...RequestIDOption) *RequestIDOptions {
|
||||
o := new(RequestIDOptions)
|
||||
for _, opt := range options {
|
||||
o = opt(o)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *RequestIDOptions) UseRequestID() bool {
|
||||
return o.useXRequestID
|
||||
}
|
4
internal/pidfile/README.md
Normal file
4
internal/pidfile/README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
Package pidfile is derived from github.com/moby/moby/pkg/pidfile.
|
||||
|
||||
Moby is licensed under the Apache License, Version 2.0.
|
||||
Copyright 2012-2017 Docker, Inc.
|
11
internal/pidfile/mkdirall.go
Normal file
11
internal/pidfile/mkdirall.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
// +build !windows
|
||||
|
||||
package pidfile
|
||||
|
||||
import "os"
|
||||
|
||||
// MkdirAll creates a directory named path along with any necessary parents,
|
||||
// with permission specified by attribute perm for all dir created.
|
||||
func MkdirAll(path string, perm os.FileMode) error {
|
||||
return os.MkdirAll(path, perm)
|
||||
}
|
109
internal/pidfile/mkdirall_windows.go
Normal file
109
internal/pidfile/mkdirall_windows.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
// +build windows
|
||||
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// MkdirAll implementation that is volume path aware for Windows. It can be used
|
||||
// as a drop-in replacement for os.MkdirAll()
|
||||
func MkdirAll(path string, _ os.FileMode) error {
|
||||
return mkdirall(path, false, "")
|
||||
}
|
||||
|
||||
// mkdirall is a custom version of os.MkdirAll modified for use on Windows
|
||||
// so that it is both volume path aware, and can create a directory with
|
||||
// a DACL.
|
||||
func mkdirall(path string, applyACL bool, sddl string) error {
|
||||
if re := regexp.MustCompile(`^\\\\\?\\Volume{[a-z0-9-]+}$`); re.MatchString(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The rest of this method is largely copied from os.MkdirAll and should be kept
|
||||
// as-is to ensure compatibility.
|
||||
|
||||
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
|
||||
dir, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return &os.PathError{
|
||||
Op: "mkdir",
|
||||
Path: path,
|
||||
Err: syscall.ENOTDIR,
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: make sure parent exists and then call Mkdir for path.
|
||||
i := len(path)
|
||||
for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator.
|
||||
i--
|
||||
}
|
||||
|
||||
j := i
|
||||
for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element.
|
||||
j--
|
||||
}
|
||||
|
||||
if j > 1 {
|
||||
// Create parent
|
||||
err = mkdirall(path[0:j-1], false, sddl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parent now exists; invoke os.Mkdir or mkdirWithACL and use its result.
|
||||
if applyACL {
|
||||
err = mkdirWithACL(path, sddl)
|
||||
} else {
|
||||
err = os.Mkdir(path, 0)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Handle arguments like "foo/." by
|
||||
// double-checking that directory doesn't exist.
|
||||
dir, err1 := os.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mkdirWithACL creates a new directory. If there is an error, it will be of
|
||||
// type *PathError. .
|
||||
//
|
||||
// This is a modified and combined version of os.Mkdir and windows.Mkdir
|
||||
// in golang to cater for creating a directory am ACL permitting full
|
||||
// access, with inheritance, to any subfolder/file for Built-in Administrators
|
||||
// and Local System.
|
||||
func mkdirWithACL(name string, sddl string) error {
|
||||
sa := windows.SecurityAttributes{Length: 0}
|
||||
sd, err := windows.SecurityDescriptorFromString(sddl)
|
||||
if err != nil {
|
||||
return &os.PathError{Op: "mkdir", Path: name, Err: err}
|
||||
}
|
||||
sa.Length = uint32(unsafe.Sizeof(sa))
|
||||
sa.InheritHandle = 1
|
||||
sa.SecurityDescriptor = sd
|
||||
|
||||
namep, err := windows.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return &os.PathError{Op: "mkdir", Path: name, Err: err}
|
||||
}
|
||||
|
||||
e := windows.CreateDirectory(namep, &sa)
|
||||
if e != nil {
|
||||
return &os.PathError{Op: "mkdir", Path: name, Err: e}
|
||||
}
|
||||
return nil
|
||||
}
|
51
internal/pidfile/pidfile.go
Normal file
51
internal/pidfile/pidfile.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
// Package pidfile provides structure and helper functions to create and remove
|
||||
// PID file. A PID file is usually a file used to store the process ID of a
|
||||
// running process.
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PIDFile is a file used to store the process ID of a running process.
|
||||
type PIDFile struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func checkPIDFileAlreadyExists(path string) error {
|
||||
if pidByte, err := ioutil.ReadFile(path); err == nil {
|
||||
pidString := strings.TrimSpace(string(pidByte))
|
||||
if pid, err := strconv.Atoi(pidString); err == nil {
|
||||
if processExists(pid) {
|
||||
return fmt.Errorf("pid file found, ensure webhook is not running or delete %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// New creates a PIDfile using the specified path.
|
||||
func New(path string) (*PIDFile, error) {
|
||||
if err := checkPIDFileAlreadyExists(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Note MkdirAll returns nil if a directory already exists
|
||||
if err := MkdirAll(filepath.Dir(path), os.FileMode(0755)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(path, []byte(fmt.Sprintf("%d", os.Getpid())), 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PIDFile{path: path}, nil
|
||||
}
|
||||
|
||||
// Remove removes the PIDFile.
|
||||
func (file PIDFile) Remove() error {
|
||||
return os.Remove(file.path)
|
||||
}
|
14
internal/pidfile/pidfile_darwin.go
Normal file
14
internal/pidfile/pidfile_darwin.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
// +build darwin
|
||||
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func processExists(pid int) bool {
|
||||
// OS X does not have a proc filesystem.
|
||||
// Use kill -0 pid to judge if the process exists.
|
||||
err := unix.Kill(pid, 0)
|
||||
return err == nil
|
||||
}
|
38
internal/pidfile/pidfile_test.go
Normal file
38
internal/pidfile/pidfile_test.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package pidfile
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewAndRemove(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "test-pidfile")
|
||||
if err != nil {
|
||||
t.Fatal("Could not create test directory")
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, "testfile")
|
||||
file, err := New(path)
|
||||
if err != nil {
|
||||
t.Fatal("Could not create test file", err)
|
||||
}
|
||||
|
||||
_, err = New(path)
|
||||
if err == nil {
|
||||
t.Fatal("Test file creation not blocked")
|
||||
}
|
||||
|
||||
if err := file.Remove(); err != nil {
|
||||
t.Fatal("Could not delete created test file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveInvalidPath(t *testing.T) {
|
||||
file := PIDFile{path: filepath.Join("foo", "bar")}
|
||||
|
||||
if err := file.Remove(); err == nil {
|
||||
t.Fatal("Non-existing file doesn't give an error on delete")
|
||||
}
|
||||
}
|
16
internal/pidfile/pidfile_unix.go
Normal file
16
internal/pidfile/pidfile_unix.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// +build !windows,!darwin
|
||||
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func processExists(pid int) bool {
|
||||
if _, err := os.Stat(filepath.Join("/proc", strconv.Itoa(pid))); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
25
internal/pidfile/pidfile_windows.go
Normal file
25
internal/pidfile/pidfile_windows.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package pidfile
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
processQueryLimitedInformation = 0x1000
|
||||
|
||||
stillActive = 259
|
||||
)
|
||||
|
||||
func processExists(pid int) bool {
|
||||
h, err := windows.OpenProcess(processQueryLimitedInformation, false, uint32(pid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var c uint32
|
||||
err = windows.GetExitCodeProcess(h, &c)
|
||||
windows.Close(h)
|
||||
if err != nil {
|
||||
return c == stillActive
|
||||
}
|
||||
return true
|
||||
}
|
24
signals.go
24
signals.go
|
@ -14,6 +14,9 @@ func setupSignals() {
|
|||
|
||||
signals = make(chan os.Signal, 1)
|
||||
signal.Notify(signals, syscall.SIGUSR1)
|
||||
signal.Notify(signals, syscall.SIGHUP)
|
||||
signal.Notify(signals, syscall.SIGTERM)
|
||||
signal.Notify(signals, os.Interrupt)
|
||||
|
||||
go watchForSignals()
|
||||
}
|
||||
|
@ -23,11 +26,26 @@ func watchForSignals() {
|
|||
|
||||
for {
|
||||
sig := <-signals
|
||||
if sig == syscall.SIGUSR1 {
|
||||
switch sig {
|
||||
case syscall.SIGUSR1:
|
||||
log.Println("caught USR1 signal")
|
||||
|
||||
reloadAllHooks()
|
||||
} else {
|
||||
|
||||
case syscall.SIGHUP:
|
||||
log.Println("caught HUP signal")
|
||||
reloadAllHooks()
|
||||
|
||||
case os.Interrupt, syscall.SIGTERM:
|
||||
log.Printf("caught %s signal; exiting\n", sig)
|
||||
if pidFile != nil {
|
||||
err := pidFile.Remove()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
|
||||
default:
|
||||
log.Printf("caught unhandled signal %+v\n", sig)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"id": "github",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"http-methods": ["Post "],
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule-mismatch-http-response-code": 400,
|
||||
"pass-environment-to-command":
|
||||
|
@ -137,6 +138,60 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": "plex",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"response-message": "success",
|
||||
"parse-parameters-as-json": [
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "payload"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "payload.event"
|
||||
},
|
||||
"value": "media.play"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "capture-command-output-on-success-not-by-default",
|
||||
"pass-arguments-to-command": [
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
- id: github
|
||||
http-methods:
|
||||
- "Post "
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
|
@ -76,6 +78,40 @@
|
|||
include-command-output-in-response: true
|
||||
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: plex
|
||||
trigger-rule:
|
||||
match:
|
||||
type: value
|
||||
parameter:
|
||||
source: payload
|
||||
name: payload.event
|
||||
value: media.play
|
||||
parse-parameters-as-json:
|
||||
- source: payload
|
||||
name: payload
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
command-working-directory: /
|
||||
|
||||
- id: capture-command-output-on-success-not-by-default
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
|
|
85
tls.go
Normal file
85
tls.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func writeTLSSupportedCipherStrings(w io.Writer, min uint16) error {
|
||||
for _, c := range CipherSuites() {
|
||||
var found bool
|
||||
|
||||
for _, v := range c.SupportedVersions {
|
||||
if v >= min {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := w.Write([]byte(c.Name + "\n"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTLSMinVersion converts a version string into a TLS version ID.
|
||||
func getTLSMinVersion(v string) uint16 {
|
||||
switch v {
|
||||
case "1.0":
|
||||
return tls.VersionTLS10
|
||||
case "1.1":
|
||||
return tls.VersionTLS11
|
||||
case "1.2", "":
|
||||
return tls.VersionTLS12
|
||||
case "1.3":
|
||||
return tls.VersionTLS13
|
||||
default:
|
||||
log.Fatalln("error: unknown minimum TLS version:", v)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// getTLSCipherSuites converts a comma separated list of cipher suites into a
|
||||
// slice of TLS cipher suite IDs.
|
||||
func getTLSCipherSuites(v string) []uint16 {
|
||||
supported := CipherSuites()
|
||||
|
||||
if v == "" {
|
||||
suites := make([]uint16, len(supported))
|
||||
|
||||
for _, cs := range supported {
|
||||
suites = append(suites, cs.ID)
|
||||
}
|
||||
|
||||
return suites
|
||||
}
|
||||
|
||||
var found bool
|
||||
txts := strings.Split(v, ",")
|
||||
suites := make([]uint16, len(txts))
|
||||
|
||||
for _, want := range txts {
|
||||
found = false
|
||||
|
||||
for _, cs := range supported {
|
||||
if want == cs.Name {
|
||||
suites = append(suites, cs.ID)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Fatalln("error: unknown TLS cipher suite:", want)
|
||||
}
|
||||
}
|
||||
|
||||
return suites
|
||||
}
|
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)
|
||||
}
|
1
vendor/github.com/codegangsta/negroni/.gitignore
generated
vendored
1
vendor/github.com/codegangsta/negroni/.gitignore
generated
vendored
|
@ -1 +0,0 @@
|
|||
/coverage.txt
|
27
vendor/github.com/codegangsta/negroni/.travis.yml
generated
vendored
27
vendor/github.com/codegangsta/negroni/.travis.yml
generated
vendored
|
@ -1,27 +0,0 @@
|
|||
language: go
|
||||
|
||||
sudo: false
|
||||
dist: trusty
|
||||
|
||||
go:
|
||||
- 1.x
|
||||
- 1.2.x
|
||||
- 1.3.x
|
||||
- 1.4.x
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- master
|
||||
|
||||
before_install:
|
||||
- find "${GOPATH%%:*}" -name '*.a' -delete
|
||||
- rm -rf "${GOPATH%%:*}/src/golang.org"
|
||||
- go get golang.org/x/tools/cover
|
||||
- go get golang.org/x/tools/cmd/cover
|
||||
|
||||
script:
|
||||
- go test -race -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s "https://codecov.io/bash")
|
37
vendor/github.com/codegangsta/negroni/CHANGELOG.md
generated
vendored
37
vendor/github.com/codegangsta/negroni/CHANGELOG.md
generated
vendored
|
@ -1,37 +0,0 @@
|
|||
# Change Log
|
||||
|
||||
**ATTN**: This project uses [semantic versioning](http://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- `Recovery.ErrorHandlerFunc` for custom error handling during recovery
|
||||
- `With()` helper for building a new `Negroni` struct chaining handlers from
|
||||
existing `Negroni` structs
|
||||
|
||||
### Fixed
|
||||
- `Written()` correct returns `false` if no response header has been written
|
||||
|
||||
### Changed
|
||||
- Set default status to `0` in the case that no handler writes status -- was
|
||||
previously `200` (in 0.2.0, before that it was `0` so this reestablishes that
|
||||
behavior)
|
||||
- Catch `panic`s thrown by callbacks provided to the `Recovery` handler
|
||||
|
||||
## [0.2.0] - 2016-05-10
|
||||
### Added
|
||||
- Support for variadic handlers in `New()`
|
||||
- Added `Negroni.Handlers()` to fetch all of the handlers for a given chain
|
||||
- Allowed size in `Recovery` handler was bumped to 8k
|
||||
- `Negroni.UseFunc` to push another handler onto the chain
|
||||
|
||||
### Changed
|
||||
- Set the status before calling `beforeFuncs` so the information is available to them
|
||||
- Set default status to `200` in the case that no handler writes status -- was previously `0`
|
||||
- Panic if `nil` handler is given to `negroni.Use`
|
||||
|
||||
## 0.1.0 - 2013-07-22
|
||||
### Added
|
||||
- Initial implementation.
|
||||
|
||||
[Unreleased]: https://github.com/urfave/negroni/compare/v0.2.0...HEAD
|
||||
[0.2.0]: https://github.com/urfave/negroni/compare/v0.1.0...v0.2.0
|
546
vendor/github.com/codegangsta/negroni/README.md
generated
vendored
546
vendor/github.com/codegangsta/negroni/README.md
generated
vendored
|
@ -1,546 +0,0 @@
|
|||
# Negroni
|
||||
[](http://godoc.org/github.com/urfave/negroni)
|
||||
[](https://travis-ci.org/urfave/negroni)
|
||||
[](https://codebeat.co/projects/github-com-urfave-negroni)
|
||||
[](https://codecov.io/gh/urfave/negroni)
|
||||
|
||||
**Notice:** This is the library formerly known as
|
||||
`github.com/codegangsta/negroni` -- Github will automatically redirect requests
|
||||
to this repository, but we recommend updating your references for clarity.
|
||||
|
||||
Negroni is an idiomatic approach to web middleware in Go. It is tiny,
|
||||
non-intrusive, and encourages use of `net/http` Handlers.
|
||||
|
||||
If you like the idea of [Martini](https://github.com/go-martini/martini), but
|
||||
you think it contains too much magic, then Negroni is a great fit.
|
||||
|
||||
Language Translations:
|
||||
* [Deutsch (de_DE)](translations/README_de_de.md)
|
||||
* [Português Brasileiro (pt_BR)](translations/README_pt_br.md)
|
||||
* [简体中文 (zh_cn)](translations/README_zh_cn.md)
|
||||
* [繁體中文 (zh_tw)](translations/README_zh_tw.md)
|
||||
* [日本語 (ja_JP)](translations/README_ja_JP.md)
|
||||
* [Français (fr_FR)](translations/README_fr_FR.md)
|
||||
|
||||
## Getting Started
|
||||
|
||||
After installing Go and setting up your
|
||||
[GOPATH](http://golang.org/doc/code.html#GOPATH), create your first `.go` file.
|
||||
We'll call it `server.go`.
|
||||
|
||||
<!-- { "interrupt": true } -->
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(w, "Welcome to the home page!")
|
||||
})
|
||||
|
||||
n := negroni.Classic() // Includes some default middlewares
|
||||
n.UseHandler(mux)
|
||||
|
||||
http.ListenAndServe(":3000", n)
|
||||
}
|
||||
```
|
||||
|
||||
Then install the Negroni package (**NOTE**: >= **go 1.1** is required):
|
||||
|
||||
```
|
||||
go get github.com/urfave/negroni
|
||||
```
|
||||
|
||||
Then run your server:
|
||||
|
||||
```
|
||||
go run server.go
|
||||
```
|
||||
|
||||
You will now have a Go `net/http` webserver running on `localhost:3000`.
|
||||
|
||||
### Packaging
|
||||
|
||||
If you are on Debian, `negroni` is also available as [a
|
||||
package](https://packages.debian.org/sid/golang-github-urfave-negroni-dev) that
|
||||
you can install via `apt install golang-github-urfave-negroni-dev` (at the time
|
||||
of writing, it is in the `sid` repositories).
|
||||
|
||||
## Is Negroni a Framework?
|
||||
|
||||
Negroni is **not** a framework. It is a middleware-focused library that is
|
||||
designed to work directly with `net/http`.
|
||||
|
||||
## Routing?
|
||||
|
||||
Negroni is BYOR (Bring your own Router). The Go community already has a number
|
||||
of great http routers available, and Negroni tries to play well with all of them
|
||||
by fully supporting `net/http`. For instance, integrating with [Gorilla Mux]
|
||||
looks like so:
|
||||
|
||||
``` go
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/", HomeHandler)
|
||||
|
||||
n := negroni.New(Middleware1, Middleware2)
|
||||
// Or use a middleware with the Use() function
|
||||
n.Use(Middleware3)
|
||||
// router goes last
|
||||
n.UseHandler(router)
|
||||
|
||||
http.ListenAndServe(":3001", n)
|
||||
```
|
||||
|
||||
## `negroni.Classic()`
|
||||
|
||||
`negroni.Classic()` provides some default middleware that is useful for most
|
||||
applications:
|
||||
|
||||
* [`negroni.Recovery`](#recovery) - Panic Recovery Middleware.
|
||||
* [`negroni.Logger`](#logger) - Request/Response Logger Middleware.
|
||||
* [`negroni.Static`](#static) - Static File serving under the "public"
|
||||
directory.
|
||||
|
||||
This makes it really easy to get started with some useful features from Negroni.
|
||||
|
||||
## Handlers
|
||||
|
||||
Negroni provides a bidirectional middleware flow. This is done through the
|
||||
`negroni.Handler` interface:
|
||||
|
||||
``` go
|
||||
type Handler interface {
|
||||
ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc)
|
||||
}
|
||||
```
|
||||
|
||||
If a middleware hasn't already written to the `ResponseWriter`, it should call
|
||||
the next `http.HandlerFunc` in the chain to yield to the next middleware
|
||||
handler. This can be used for great good:
|
||||
|
||||
``` go
|
||||
func MyMiddleware(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
// do some stuff before
|
||||
next(rw, r)
|
||||
// do some stuff after
|
||||
}
|
||||
```
|
||||
|
||||
And you can map it to the handler chain with the `Use` function:
|
||||
|
||||
``` go
|
||||
n := negroni.New()
|
||||
n.Use(negroni.HandlerFunc(MyMiddleware))
|
||||
```
|
||||
|
||||
You can also map plain old `http.Handler`s:
|
||||
|
||||
``` go
|
||||
n := negroni.New()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
// map your routes
|
||||
|
||||
n.UseHandler(mux)
|
||||
|
||||
http.ListenAndServe(":3000", n)
|
||||
```
|
||||
|
||||
## `With()`
|
||||
|
||||
Negroni has a convenience function called `With`. `With` takes one or more
|
||||
`Handler` instances and returns a new `Negroni` with the combination of the
|
||||
receiver's handlers and the new handlers.
|
||||
|
||||
```go
|
||||
// middleware we want to reuse
|
||||
common := negroni.New()
|
||||
common.Use(MyMiddleware1)
|
||||
common.Use(MyMiddleware2)
|
||||
|
||||
// `specific` is a new negroni with the handlers from `common` combined with the
|
||||
// the handlers passed in
|
||||
specific := common.With(
|
||||
SpecificMiddleware1,
|
||||
SpecificMiddleware2
|
||||
)
|
||||
```
|
||||
|
||||
## `Run()`
|
||||
|
||||
Negroni has a convenience function called `Run`. `Run` takes an addr string
|
||||
identical to [`http.ListenAndServe`](https://godoc.org/net/http#ListenAndServe).
|
||||
|
||||
<!-- { "interrupt": true } -->
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
func main() {
|
||||
n := negroni.Classic()
|
||||
n.Run(":8080")
|
||||
}
|
||||
```
|
||||
If no address is provided, the `PORT` environment variable is used instead.
|
||||
If the `PORT` environment variable is not defined, the default address will be used.
|
||||
See [Run](https://godoc.org/github.com/urfave/negroni#Negroni.Run) for a complete description.
|
||||
|
||||
In general, you will want to use `net/http` methods and pass `negroni` as a
|
||||
`Handler`, as this is more flexible, e.g.:
|
||||
|
||||
<!-- { "interrupt": true } -->
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(w, "Welcome to the home page!")
|
||||
})
|
||||
|
||||
n := negroni.Classic() // Includes some default middlewares
|
||||
n.UseHandler(mux)
|
||||
|
||||
s := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: n,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
log.Fatal(s.ListenAndServe())
|
||||
}
|
||||
```
|
||||
|
||||
## Route Specific Middleware
|
||||
|
||||
If you have a route group of routes that need specific middleware to be
|
||||
executed, you can simply create a new Negroni instance and use it as your route
|
||||
handler.
|
||||
|
||||
``` go
|
||||
router := mux.NewRouter()
|
||||
adminRoutes := mux.NewRouter()
|
||||
// add admin routes here
|
||||
|
||||
// Create a new negroni for the admin middleware
|
||||
router.PathPrefix("/admin").Handler(negroni.New(
|
||||
Middleware1,
|
||||
Middleware2,
|
||||
negroni.Wrap(adminRoutes),
|
||||
))
|
||||
```
|
||||
|
||||
If you are using [Gorilla Mux], here is an example using a subrouter:
|
||||
|
||||
``` go
|
||||
router := mux.NewRouter()
|
||||
subRouter := mux.NewRouter().PathPrefix("/subpath").Subrouter().StrictSlash(true)
|
||||
subRouter.HandleFunc("/", someSubpathHandler) // "/subpath/"
|
||||
subRouter.HandleFunc("/:id", someSubpathHandler) // "/subpath/:id"
|
||||
|
||||
// "/subpath" is necessary to ensure the subRouter and main router linkup
|
||||
router.PathPrefix("/subpath").Handler(negroni.New(
|
||||
Middleware1,
|
||||
Middleware2,
|
||||
negroni.Wrap(subRouter),
|
||||
))
|
||||
```
|
||||
|
||||
`With()` can be used to eliminate redundancy for middlewares shared across
|
||||
routes.
|
||||
|
||||
``` go
|
||||
router := mux.NewRouter()
|
||||
apiRoutes := mux.NewRouter()
|
||||
// add api routes here
|
||||
webRoutes := mux.NewRouter()
|
||||
// add web routes here
|
||||
|
||||
// create common middleware to be shared across routes
|
||||
common := negroni.New(
|
||||
Middleware1,
|
||||
Middleware2,
|
||||
)
|
||||
|
||||
// create a new negroni for the api middleware
|
||||
// using the common middleware as a base
|
||||
router.PathPrefix("/api").Handler(common.With(
|
||||
APIMiddleware1,
|
||||
negroni.Wrap(apiRoutes),
|
||||
))
|
||||
// create a new negroni for the web middleware
|
||||
// using the common middleware as a base
|
||||
router.PathPrefix("/web").Handler(common.With(
|
||||
WebMiddleware1,
|
||||
negroni.Wrap(webRoutes),
|
||||
))
|
||||
```
|
||||
|
||||
## Bundled Middleware
|
||||
|
||||
### Static
|
||||
|
||||
This middleware will serve files on the filesystem. If the files do not exist,
|
||||
it proxies the request to the next middleware. If you want the requests for
|
||||
non-existent files to return a `404 File Not Found` to the user you should look
|
||||
at using [http.FileServer](https://golang.org/pkg/net/http/#FileServer) as
|
||||
a handler.
|
||||
|
||||
Example:
|
||||
|
||||
<!-- { "interrupt": true } -->
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(w, "Welcome to the home page!")
|
||||
})
|
||||
|
||||
// Example of using a http.FileServer if you want "server-like" rather than "middleware" behavior
|
||||
// mux.Handle("/public", http.FileServer(http.Dir("/home/public")))
|
||||
|
||||
n := negroni.New()
|
||||
n.Use(negroni.NewStatic(http.Dir("/tmp")))
|
||||
n.UseHandler(mux)
|
||||
|
||||
http.ListenAndServe(":3002", n)
|
||||
}
|
||||
```
|
||||
|
||||
Will serve files from the `/tmp` directory first, but proxy calls to the next
|
||||
handler if the request does not match a file on the filesystem.
|
||||
|
||||
### Recovery
|
||||
|
||||
This middleware catches `panic`s and responds with a `500` response code. If
|
||||
any other middleware has written a response code or body, this middleware will
|
||||
fail to properly send a 500 to the client, as the client has already received
|
||||
the HTTP response code. Additionally, an `ErrorHandlerFunc` can be attached
|
||||
to report 500's to an error reporting service such as Sentry or Airbrake.
|
||||
|
||||
Example:
|
||||
|
||||
<!-- { "interrupt": true } -->
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
panic("oh no")
|
||||
})
|
||||
|
||||
n := negroni.New()
|
||||
n.Use(negroni.NewRecovery())
|
||||
n.UseHandler(mux)
|
||||
|
||||
http.ListenAndServe(":3003", n)
|
||||
}
|
||||
```
|
||||
|
||||
Will return a `500 Internal Server Error` to each request. It will also log the
|
||||
stack traces as well as print the stack trace to the requester if `PrintStack`
|
||||
is set to `true` (the default).
|
||||
|
||||
Example with error handler:
|
||||
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
panic("oh no")
|
||||
})
|
||||
|
||||
n := negroni.New()
|
||||
recovery := negroni.NewRecovery()
|
||||
recovery.ErrorHandlerFunc = reportToSentry
|
||||
n.Use(recovery)
|
||||
n.UseHandler(mux)
|
||||
|
||||
http.ListenAndServe(":3003", n)
|
||||
}
|
||||
|
||||
func reportToSentry(error interface{}) {
|
||||
// write code here to report error to Sentry
|
||||
}
|
||||
```
|
||||
|
||||
The middleware simply output the informations on STDOUT by default.
|
||||
You can customize the output process by using the `SetFormatter()` function.
|
||||
|
||||
You can use also the `HTMLPanicFormatter` to display a pretty HTML when a crash occurs.
|
||||
|
||||
<!-- { "interrupt": true } -->
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
panic("oh no")
|
||||
})
|
||||
|
||||
n := negroni.New()
|
||||
recovery := negroni.NewRecovery()
|
||||
recovery.Formatter = &negroni.HTMLPanicFormatter{}
|
||||
n.Use(recovery)
|
||||
n.UseHandler(mux)
|
||||
|
||||
http.ListenAndServe(":3003", n)
|
||||
}
|
||||
```
|
||||
|
||||
## Logger
|
||||
|
||||
This middleware logs each incoming request and response.
|
||||
|
||||
Example:
|
||||
|
||||
<!-- { "interrupt": true } -->
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(w, "Welcome to the home page!")
|
||||
})
|
||||
|
||||
n := negroni.New()
|
||||
n.Use(negroni.NewLogger())
|
||||
n.UseHandler(mux)
|
||||
|
||||
http.ListenAndServe(":3004", n)
|
||||
}
|
||||
```
|
||||
|
||||
Will print a log similar to:
|
||||
|
||||
```
|
||||
[negroni] 2017-10-04T14:56:25+02:00 | 200 | 378µs | localhost:3004 | GET /
|
||||
```
|
||||
|
||||
on each request.
|
||||
|
||||
You can also set your own log format by calling the `SetFormat` function. The format is a template string with fields as mentioned in the `LoggerEntry` struct. So, as an example -
|
||||
|
||||
```go
|
||||
l.SetFormat("[{{.Status}} {{.Duration}}] - {{.Request.UserAgent}}")
|
||||
```
|
||||
|
||||
will show something like - `[200 18.263µs] - Go-User-Agent/1.1 `
|
||||
|
||||
## Third Party Middleware
|
||||
|
||||
Here is a current list of Negroni compatible middlware. Feel free to put up a PR
|
||||
linking your middleware if you have built one:
|
||||
|
||||
| Middleware | Author | Description |
|
||||
| -----------|--------|-------------|
|
||||
| [authz](https://github.com/casbin/negroni-authz) | [Yang Luo](https://github.com/hsluoyz) | ACL, RBAC, ABAC Authorization middlware based on [Casbin](https://github.com/casbin/casbin) |
|
||||
| [binding](https://github.com/mholt/binding) | [Matt Holt](https://github.com/mholt) | Data binding from HTTP requests into structs |
|
||||
| [cloudwatch](https://github.com/cvillecsteele/negroni-cloudwatch) | [Colin Steele](https://github.com/cvillecsteele) | AWS cloudwatch metrics middleware |
|
||||
| [cors](https://github.com/rs/cors) | [Olivier Poitrey](https://github.com/rs) | [Cross Origin Resource Sharing](http://www.w3.org/TR/cors/) (CORS) support |
|
||||
| [csp](https://github.com/awakenetworks/csp) | [Awake Networks](https://github.com/awakenetworks) | [Content Security Policy](https://www.w3.org/TR/CSP2/) (CSP) support |
|
||||
| [delay](https://github.com/jeffbmartinez/delay) | [Jeff Martinez](https://github.com/jeffbmartinez) | Add delays/latency to endpoints. Useful when testing effects of high latency |
|
||||
| [New Relic Go Agent](https://github.com/yadvendar/negroni-newrelic-go-agent) | [Yadvendar Champawat](https://github.com/yadvendar) | Official [New Relic Go Agent](https://github.com/newrelic/go-agent) (currently in beta) |
|
||||
| [gorelic](https://github.com/jingweno/negroni-gorelic) | [Jingwen Owen Ou](https://github.com/jingweno) | New Relic agent for Go runtime |
|
||||
| [Graceful](https://github.com/tylerb/graceful) | [Tyler Bunnell](https://github.com/tylerb) | Graceful HTTP Shutdown |
|
||||
| [gzip](https://github.com/phyber/negroni-gzip) | [phyber](https://github.com/phyber) | GZIP response compression |
|
||||
| [JWT Middleware](https://github.com/auth0/go-jwt-middleware) | [Auth0](https://github.com/auth0) | Middleware checks for a JWT on the `Authorization` header on incoming requests and decodes it|
|
||||
| [logrus](https://github.com/meatballhat/negroni-logrus) | [Dan Buch](https://github.com/meatballhat) | Logrus-based logger |
|
||||
| [oauth2](https://github.com/goincremental/negroni-oauth2) | [David Bochenski](https://github.com/bochenski) | oAuth2 middleware |
|
||||
| [onthefly](https://github.com/xyproto/onthefly) | [Alexander Rødseth](https://github.com/xyproto) | Generate TinySVG, HTML and CSS on the fly |
|
||||
| [permissions2](https://github.com/xyproto/permissions2) | [Alexander Rødseth](https://github.com/xyproto) | Cookies, users and permissions |
|
||||
| [prometheus](https://github.com/zbindenren/negroni-prometheus) | [Rene Zbinden](https://github.com/zbindenren) | Easily create metrics endpoint for the [prometheus](http://prometheus.io) instrumentation tool |
|
||||
| [render](https://github.com/unrolled/render) | [Cory Jacobsen](https://github.com/unrolled) | Render JSON, XML and HTML templates |
|
||||
| [RestGate](https://github.com/pjebs/restgate) | [Prasanga Siripala](https://github.com/pjebs) | Secure authentication for REST API endpoints |
|
||||
| [secure](https://github.com/unrolled/secure) | [Cory Jacobsen](https://github.com/unrolled) | Middleware that implements a few quick security wins |
|
||||
| [sessions](https://github.com/goincremental/negroni-sessions) | [David Bochenski](https://github.com/bochenski) | Session Management |
|
||||
| [stats](https://github.com/thoas/stats) | [Florent Messa](https://github.com/thoas) | Store information about your web application (response time, etc.) |
|
||||
| [VanGoH](https://github.com/auroratechnologies/vangoh) | [Taylor Wrobel](https://github.com/twrobel3) | Configurable [AWS-Style](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html) HMAC authentication middleware |
|
||||
| [xrequestid](https://github.com/pilu/xrequestid) | [Andrea Franz](https://github.com/pilu) | Middleware that assigns a random X-Request-Id header to each request |
|
||||
| [mgo session](https://github.com/joeljames/nigroni-mgo-session) | [Joel James](https://github.com/joeljames) | Middleware that handles creating and closing mgo sessions per request |
|
||||
| [digits](https://github.com/bamarni/digits) | [Bilal Amarni](https://github.com/bamarni) | Middleware that handles [Twitter Digits](https://get.digits.com/) authentication |
|
||||
|
||||
## Examples
|
||||
|
||||
[Alexander Rødseth](https://github.com/xyproto) created
|
||||
[mooseware](https://github.com/xyproto/mooseware), a skeleton for writing a
|
||||
Negroni middleware handler.
|
||||
|
||||
[Prasanga Siripala](https://github.com/pjebs) created an effective skeleton structure for web-based Go/Negroni projects: [Go-Skeleton](https://github.com/pjebs/go-skeleton)
|
||||
|
||||
## Live code reload?
|
||||
|
||||
[gin](https://github.com/codegangsta/gin) and
|
||||
[fresh](https://github.com/pilu/fresh) both live reload negroni apps.
|
||||
|
||||
## Essential Reading for Beginners of Go & Negroni
|
||||
|
||||
* [Using a Context to pass information from middleware to end handler](http://elithrar.github.io/article/map-string-interface/)
|
||||
* [Understanding middleware](https://mattstauffer.co/blog/laravel-5.0-middleware-filter-style)
|
||||
|
||||
## About
|
||||
|
||||
Negroni is obsessively designed by none other than the [Code
|
||||
Gangsta](https://codegangsta.io/)
|
||||
|
||||
[Gorilla Mux]: https://github.com/gorilla/mux
|
||||
[`http.FileSystem`]: https://godoc.org/net/http#FileSystem
|
25
vendor/github.com/codegangsta/negroni/doc.go
generated
vendored
25
vendor/github.com/codegangsta/negroni/doc.go
generated
vendored
|
@ -1,25 +0,0 @@
|
|||
// Package negroni is an idiomatic approach to web middleware in Go. It is tiny, non-intrusive, and encourages use of net/http Handlers.
|
||||
//
|
||||
// If you like the idea of Martini, but you think it contains too much magic, then Negroni is a great fit.
|
||||
//
|
||||
// For a full guide visit http://github.com/urfave/negroni
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "github.com/urfave/negroni"
|
||||
// "net/http"
|
||||
// "fmt"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
// mux := http.NewServeMux()
|
||||
// mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
// fmt.Fprintf(w, "Welcome to the home page!")
|
||||
// })
|
||||
//
|
||||
// n := negroni.Classic()
|
||||
// n.UseHandler(mux)
|
||||
// n.Run(":3000")
|
||||
// }
|
||||
package negroni
|
82
vendor/github.com/codegangsta/negroni/logger.go
generated
vendored
82
vendor/github.com/codegangsta/negroni/logger.go
generated
vendored
|
@ -1,82 +0,0 @@
|
|||
package negroni
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoggerEntry is the structure
|
||||
// passed to the template.
|
||||
type LoggerEntry struct {
|
||||
StartTime string
|
||||
Status int
|
||||
Duration time.Duration
|
||||
Hostname string
|
||||
Method string
|
||||
Path string
|
||||
Request *http.Request
|
||||
}
|
||||
|
||||
// LoggerDefaultFormat is the format
|
||||
// logged used by the default Logger instance.
|
||||
var LoggerDefaultFormat = "{{.StartTime}} | {{.Status}} | \t {{.Duration}} | {{.Hostname}} | {{.Method}} {{.Path}} \n"
|
||||
|
||||
// LoggerDefaultDateFormat is the
|
||||
// format used for date by the
|
||||
// default Logger instance.
|
||||
var LoggerDefaultDateFormat = time.RFC3339
|
||||
|
||||
// ALogger interface
|
||||
type ALogger interface {
|
||||
Println(v ...interface{})
|
||||
Printf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// Logger is a middleware handler that logs the request as it goes in and the response as it goes out.
|
||||
type Logger struct {
|
||||
// ALogger implements just enough log.Logger interface to be compatible with other implementations
|
||||
ALogger
|
||||
dateFormat string
|
||||
template *template.Template
|
||||
}
|
||||
|
||||
// NewLogger returns a new Logger instance
|
||||
func NewLogger() *Logger {
|
||||
logger := &Logger{ALogger: log.New(os.Stdout, "[negroni] ", 0), dateFormat: LoggerDefaultDateFormat}
|
||||
logger.SetFormat(LoggerDefaultFormat)
|
||||
return logger
|
||||
}
|
||||
|
||||
func (l *Logger) SetFormat(format string) {
|
||||
l.template = template.Must(template.New("negroni_parser").Parse(format))
|
||||
}
|
||||
|
||||
func (l *Logger) SetDateFormat(format string) {
|
||||
l.dateFormat = format
|
||||
}
|
||||
|
||||
func (l *Logger) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
start := time.Now()
|
||||
|
||||
next(rw, r)
|
||||
|
||||
res := rw.(ResponseWriter)
|
||||
log := LoggerEntry{
|
||||
StartTime: start.Format(l.dateFormat),
|
||||
Status: res.Status(),
|
||||
Duration: time.Since(start),
|
||||
Hostname: r.Host,
|
||||
Method: r.Method,
|
||||
Path: r.URL.Path,
|
||||
Request: r,
|
||||
}
|
||||
|
||||
buff := &bytes.Buffer{}
|
||||
l.template.Execute(buff, log)
|
||||
l.Printf(buff.String())
|
||||
}
|
169
vendor/github.com/codegangsta/negroni/negroni.go
generated
vendored
169
vendor/github.com/codegangsta/negroni/negroni.go
generated
vendored
|
@ -1,169 +0,0 @@
|
|||
package negroni
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultAddress is used if no other is specified.
|
||||
DefaultAddress = ":8080"
|
||||
)
|
||||
|
||||
// Handler handler is an interface that objects can implement to be registered to serve as middleware
|
||||
// in the Negroni middleware stack.
|
||||
// ServeHTTP should yield to the next middleware in the chain by invoking the next http.HandlerFunc
|
||||
// passed in.
|
||||
//
|
||||
// If the Handler writes to the ResponseWriter, the next http.HandlerFunc should not be invoked.
|
||||
type Handler interface {
|
||||
ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc)
|
||||
}
|
||||
|
||||
// HandlerFunc is an adapter to allow the use of ordinary functions as Negroni handlers.
|
||||
// If f is a function with the appropriate signature, HandlerFunc(f) is a Handler object that calls f.
|
||||
type HandlerFunc func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc)
|
||||
|
||||
func (h HandlerFunc) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
h(rw, r, next)
|
||||
}
|
||||
|
||||
type middleware struct {
|
||||
handler Handler
|
||||
next *middleware
|
||||
}
|
||||
|
||||
func (m middleware) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
m.handler.ServeHTTP(rw, r, m.next.ServeHTTP)
|
||||
}
|
||||
|
||||
// Wrap converts a http.Handler into a negroni.Handler so it can be used as a Negroni
|
||||
// middleware. The next http.HandlerFunc is automatically called after the Handler
|
||||
// is executed.
|
||||
func Wrap(handler http.Handler) Handler {
|
||||
return HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
handler.ServeHTTP(rw, r)
|
||||
next(rw, r)
|
||||
})
|
||||
}
|
||||
|
||||
// WrapFunc converts a http.HandlerFunc into a negroni.Handler so it can be used as a Negroni
|
||||
// middleware. The next http.HandlerFunc is automatically called after the Handler
|
||||
// is executed.
|
||||
func WrapFunc(handlerFunc http.HandlerFunc) Handler {
|
||||
return HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
handlerFunc(rw, r)
|
||||
next(rw, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Negroni is a stack of Middleware Handlers that can be invoked as an http.Handler.
|
||||
// Negroni middleware is evaluated in the order that they are added to the stack using
|
||||
// the Use and UseHandler methods.
|
||||
type Negroni struct {
|
||||
middleware middleware
|
||||
handlers []Handler
|
||||
}
|
||||
|
||||
// New returns a new Negroni instance with no middleware preconfigured.
|
||||
func New(handlers ...Handler) *Negroni {
|
||||
return &Negroni{
|
||||
handlers: handlers,
|
||||
middleware: build(handlers),
|
||||
}
|
||||
}
|
||||
|
||||
// With returns a new Negroni instance that is a combination of the negroni
|
||||
// receiver's handlers and the provided handlers.
|
||||
func (n *Negroni) With(handlers ...Handler) *Negroni {
|
||||
return New(
|
||||
append(n.handlers, handlers...)...,
|
||||
)
|
||||
}
|
||||
|
||||
// Classic returns a new Negroni instance with the default middleware already
|
||||
// in the stack.
|
||||
//
|
||||
// Recovery - Panic Recovery Middleware
|
||||
// Logger - Request/Response Logging
|
||||
// Static - Static File Serving
|
||||
func Classic() *Negroni {
|
||||
return New(NewRecovery(), NewLogger(), NewStatic(http.Dir("public")))
|
||||
}
|
||||
|
||||
func (n *Negroni) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
n.middleware.ServeHTTP(NewResponseWriter(rw), r)
|
||||
}
|
||||
|
||||
// Use adds a Handler onto the middleware stack. Handlers are invoked in the order they are added to a Negroni.
|
||||
func (n *Negroni) Use(handler Handler) {
|
||||
if handler == nil {
|
||||
panic("handler cannot be nil")
|
||||
}
|
||||
|
||||
n.handlers = append(n.handlers, handler)
|
||||
n.middleware = build(n.handlers)
|
||||
}
|
||||
|
||||
// UseFunc adds a Negroni-style handler function onto the middleware stack.
|
||||
func (n *Negroni) UseFunc(handlerFunc func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc)) {
|
||||
n.Use(HandlerFunc(handlerFunc))
|
||||
}
|
||||
|
||||
// UseHandler adds a http.Handler onto the middleware stack. Handlers are invoked in the order they are added to a Negroni.
|
||||
func (n *Negroni) UseHandler(handler http.Handler) {
|
||||
n.Use(Wrap(handler))
|
||||
}
|
||||
|
||||
// UseHandlerFunc adds a http.HandlerFunc-style handler function onto the middleware stack.
|
||||
func (n *Negroni) UseHandlerFunc(handlerFunc func(rw http.ResponseWriter, r *http.Request)) {
|
||||
n.UseHandler(http.HandlerFunc(handlerFunc))
|
||||
}
|
||||
|
||||
// Run is a convenience function that runs the negroni stack as an HTTP
|
||||
// server. The addr string, if provided, takes the same format as http.ListenAndServe.
|
||||
// If no address is provided but the PORT environment variable is set, the PORT value is used.
|
||||
// If neither is provided, the address' value will equal the DefaultAddress constant.
|
||||
func (n *Negroni) Run(addr ...string) {
|
||||
l := log.New(os.Stdout, "[negroni] ", 0)
|
||||
finalAddr := detectAddress(addr...)
|
||||
l.Printf("listening on %s", finalAddr)
|
||||
l.Fatal(http.ListenAndServe(finalAddr, n))
|
||||
}
|
||||
|
||||
func detectAddress(addr ...string) string {
|
||||
if len(addr) > 0 {
|
||||
return addr[0]
|
||||
}
|
||||
if port := os.Getenv("PORT"); port != "" {
|
||||
return ":" + port
|
||||
}
|
||||
return DefaultAddress
|
||||
}
|
||||
|
||||
// Returns a list of all the handlers in the current Negroni middleware chain.
|
||||
func (n *Negroni) Handlers() []Handler {
|
||||
return n.handlers
|
||||
}
|
||||
|
||||
func build(handlers []Handler) middleware {
|
||||
var next middleware
|
||||
|
||||
if len(handlers) == 0 {
|
||||
return voidMiddleware()
|
||||
} else if len(handlers) > 1 {
|
||||
next = build(handlers[1:])
|
||||
} else {
|
||||
next = voidMiddleware()
|
||||
}
|
||||
|
||||
return middleware{handlers[0], &next}
|
||||
}
|
||||
|
||||
func voidMiddleware() middleware {
|
||||
return middleware{
|
||||
HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {}),
|
||||
&middleware{},
|
||||
}
|
||||
}
|
179
vendor/github.com/codegangsta/negroni/recovery.go
generated
vendored
179
vendor/github.com/codegangsta/negroni/recovery.go
generated
vendored
|
@ -1,179 +0,0 @@
|
|||
package negroni
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const (
|
||||
panicText = "PANIC: %s\n%s"
|
||||
panicHTML = `<html>
|
||||
<head><title>PANIC: {{.RecoveredPanic}}</title></head>
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
font-family: Helvetica, Arial, Sans;
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
margin: 0px;
|
||||
}
|
||||
h1 {
|
||||
color: #ffffff;
|
||||
background-color: #f14c4c;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #2b3848;
|
||||
}
|
||||
.block {
|
||||
margin: 2em;
|
||||
}
|
||||
.panic-interface {
|
||||
}
|
||||
|
||||
.panic-stack-raw pre {
|
||||
padding: 1em;
|
||||
background: #f6f8fa;
|
||||
border: dashed 1px;
|
||||
}
|
||||
.panic-interface-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<h1>Negroni - PANIC</h1>
|
||||
|
||||
<div class="panic-interface block">
|
||||
<h3>{{.RequestDescription}}</h3>
|
||||
<span class="panic-interface-title">Runtime error:</span> <span class="panic-interface-element">{{.RecoveredPanic}}</span>
|
||||
</div>
|
||||
|
||||
{{ if .Stack }}
|
||||
<div class="panic-stack-raw block">
|
||||
<h3>Runtime Stack</h3>
|
||||
<pre>{{.StackAsString}}</pre>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
nilRequestMessage = "Request is nil"
|
||||
)
|
||||
|
||||
var panicHTMLTemplate = template.Must(template.New("PanicPage").Parse(panicHTML))
|
||||
|
||||
// PanicInformation contains all
|
||||
// elements for printing stack informations.
|
||||
type PanicInformation struct {
|
||||
RecoveredPanic interface{}
|
||||
Stack []byte
|
||||
Request *http.Request
|
||||
}
|
||||
|
||||
// StackAsString returns a printable version of the stack
|
||||
func (p *PanicInformation) StackAsString() string {
|
||||
return string(p.Stack)
|
||||
}
|
||||
|
||||
// RequestDescription returns a printable description of the url
|
||||
func (p *PanicInformation) RequestDescription() string {
|
||||
|
||||
if p.Request == nil {
|
||||
return nilRequestMessage
|
||||
}
|
||||
|
||||
var queryOutput string
|
||||
if p.Request.URL.RawQuery != "" {
|
||||
queryOutput = "?" + p.Request.URL.RawQuery
|
||||
}
|
||||
return fmt.Sprintf("%s %s%s", p.Request.Method, p.Request.URL.Path, queryOutput)
|
||||
}
|
||||
|
||||
// PanicFormatter is an interface on object can implement
|
||||
// to be able to output the stack trace
|
||||
type PanicFormatter interface {
|
||||
// FormatPanicError output the stack for a given answer/response.
|
||||
// In case the the middleware should not output the stack trace,
|
||||
// the field `Stack` of the passed `PanicInformation` instance equals `[]byte{}`.
|
||||
FormatPanicError(rw http.ResponseWriter, r *http.Request, infos *PanicInformation)
|
||||
}
|
||||
|
||||
// TextPanicFormatter output the stack
|
||||
// as simple text on os.Stdout. If no `Content-Type` is set,
|
||||
// it will output the data as `text/plain; charset=utf-8`.
|
||||
// Otherwise, the origin `Content-Type` is kept.
|
||||
type TextPanicFormatter struct{}
|
||||
|
||||
func (t *TextPanicFormatter) FormatPanicError(rw http.ResponseWriter, r *http.Request, infos *PanicInformation) {
|
||||
if rw.Header().Get("Content-Type") == "" {
|
||||
rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
}
|
||||
fmt.Fprintf(rw, panicText, infos.RecoveredPanic, infos.Stack)
|
||||
}
|
||||
|
||||
// HTMLPanicFormatter output the stack inside
|
||||
// an HTML page. This has been largely inspired by
|
||||
// https://github.com/go-martini/martini/pull/156/commits.
|
||||
type HTMLPanicFormatter struct{}
|
||||
|
||||
func (t *HTMLPanicFormatter) FormatPanicError(rw http.ResponseWriter, r *http.Request, infos *PanicInformation) {
|
||||
if rw.Header().Get("Content-Type") == "" {
|
||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
}
|
||||
panicHTMLTemplate.Execute(rw, infos)
|
||||
}
|
||||
|
||||
// Recovery is a Negroni middleware that recovers from any panics and writes a 500 if there was one.
|
||||
type Recovery struct {
|
||||
Logger ALogger
|
||||
PrintStack bool
|
||||
ErrorHandlerFunc func(interface{})
|
||||
StackAll bool
|
||||
StackSize int
|
||||
Formatter PanicFormatter
|
||||
}
|
||||
|
||||
// NewRecovery returns a new instance of Recovery
|
||||
func NewRecovery() *Recovery {
|
||||
return &Recovery{
|
||||
Logger: log.New(os.Stdout, "[negroni] ", 0),
|
||||
PrintStack: true,
|
||||
StackAll: false,
|
||||
StackSize: 1024 * 8,
|
||||
Formatter: &TextPanicFormatter{},
|
||||
}
|
||||
}
|
||||
|
||||
func (rec *Recovery) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
stack := make([]byte, rec.StackSize)
|
||||
stack = stack[:runtime.Stack(stack, rec.StackAll)]
|
||||
infos := &PanicInformation{RecoveredPanic: err, Request: r}
|
||||
|
||||
if rec.PrintStack {
|
||||
infos.Stack = stack
|
||||
}
|
||||
rec.Logger.Printf(panicText, err, stack)
|
||||
rec.Formatter.FormatPanicError(rw, r, infos)
|
||||
|
||||
if rec.ErrorHandlerFunc != nil {
|
||||
func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
rec.Logger.Printf("provided ErrorHandlerFunc panic'd: %s, trace:\n%s", err, debug.Stack())
|
||||
rec.Logger.Printf("%s\n", debug.Stack())
|
||||
}
|
||||
}()
|
||||
rec.ErrorHandlerFunc(err)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
next(rw, r)
|
||||
}
|
113
vendor/github.com/codegangsta/negroni/response_writer.go
generated
vendored
113
vendor/github.com/codegangsta/negroni/response_writer.go
generated
vendored
|
@ -1,113 +0,0 @@
|
|||
package negroni
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ResponseWriter is a wrapper around http.ResponseWriter that provides extra information about
|
||||
// the response. It is recommended that middleware handlers use this construct to wrap a responsewriter
|
||||
// if the functionality calls for it.
|
||||
type ResponseWriter interface {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
// Status returns the status code of the response or 0 if the response has
|
||||
// not been written
|
||||
Status() int
|
||||
// Written returns whether or not the ResponseWriter has been written.
|
||||
Written() bool
|
||||
// Size returns the size of the response body.
|
||||
Size() int
|
||||
// Before allows for a function to be called before the ResponseWriter has been written to. This is
|
||||
// useful for setting headers or any other operations that must happen before a response has been written.
|
||||
Before(func(ResponseWriter))
|
||||
}
|
||||
|
||||
type beforeFunc func(ResponseWriter)
|
||||
|
||||
// NewResponseWriter creates a ResponseWriter that wraps an http.ResponseWriter
|
||||
func NewResponseWriter(rw http.ResponseWriter) ResponseWriter {
|
||||
nrw := &responseWriter{
|
||||
ResponseWriter: rw,
|
||||
}
|
||||
|
||||
if _, ok := rw.(http.CloseNotifier); ok {
|
||||
return &responseWriterCloseNotifer{nrw}
|
||||
}
|
||||
|
||||
return nrw
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
size int
|
||||
beforeFuncs []beforeFunc
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(s int) {
|
||||
rw.status = s
|
||||
rw.callBefore()
|
||||
rw.ResponseWriter.WriteHeader(s)
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Write(b []byte) (int, error) {
|
||||
if !rw.Written() {
|
||||
// The status will be StatusOK if WriteHeader has not been called yet
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
size, err := rw.ResponseWriter.Write(b)
|
||||
rw.size += size
|
||||
return size, err
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Status() int {
|
||||
return rw.status
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Size() int {
|
||||
return rw.size
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Written() bool {
|
||||
return rw.status != 0
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Before(before func(ResponseWriter)) {
|
||||
rw.beforeFuncs = append(rw.beforeFuncs, before)
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
hijacker, ok := rw.ResponseWriter.(http.Hijacker)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("the ResponseWriter doesn't support the Hijacker interface")
|
||||
}
|
||||
return hijacker.Hijack()
|
||||
}
|
||||
|
||||
func (rw *responseWriter) callBefore() {
|
||||
for i := len(rw.beforeFuncs) - 1; i >= 0; i-- {
|
||||
rw.beforeFuncs[i](rw)
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Flush() {
|
||||
flusher, ok := rw.ResponseWriter.(http.Flusher)
|
||||
if ok {
|
||||
if !rw.Written() {
|
||||
// The status will be StatusOK if WriteHeader has not been called yet
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
type responseWriterCloseNotifer struct {
|
||||
*responseWriter
|
||||
}
|
||||
|
||||
func (rw *responseWriterCloseNotifer) CloseNotify() <-chan bool {
|
||||
return rw.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
||||
}
|
16
vendor/github.com/codegangsta/negroni/response_writer_pusher.go
generated
vendored
16
vendor/github.com/codegangsta/negroni/response_writer_pusher.go
generated
vendored
|
@ -1,16 +0,0 @@
|
|||
//+build go1.8
|
||||
|
||||
package negroni
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (rw *responseWriter) Push(target string, opts *http.PushOptions) error {
|
||||
pusher, ok := rw.ResponseWriter.(http.Pusher)
|
||||
if ok {
|
||||
return pusher.Push(target, opts)
|
||||
}
|
||||
return fmt.Errorf("the ResponseWriter doesn't support the Pusher interface")
|
||||
}
|
88
vendor/github.com/codegangsta/negroni/static.go
generated
vendored
88
vendor/github.com/codegangsta/negroni/static.go
generated
vendored
|
@ -1,88 +0,0 @@
|
|||
package negroni
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Static is a middleware handler that serves static files in the given
|
||||
// directory/filesystem. If the file does not exist on the filesystem, it
|
||||
// passes along to the next middleware in the chain. If you desire "fileserver"
|
||||
// type behavior where it returns a 404 for unfound files, you should consider
|
||||
// using http.FileServer from the Go stdlib.
|
||||
type Static struct {
|
||||
// Dir is the directory to serve static files from
|
||||
Dir http.FileSystem
|
||||
// Prefix is the optional prefix used to serve the static directory content
|
||||
Prefix string
|
||||
// IndexFile defines which file to serve as index if it exists.
|
||||
IndexFile string
|
||||
}
|
||||
|
||||
// NewStatic returns a new instance of Static
|
||||
func NewStatic(directory http.FileSystem) *Static {
|
||||
return &Static{
|
||||
Dir: directory,
|
||||
Prefix: "",
|
||||
IndexFile: "index.html",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Static) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
if r.Method != "GET" && r.Method != "HEAD" {
|
||||
next(rw, r)
|
||||
return
|
||||
}
|
||||
file := r.URL.Path
|
||||
// if we have a prefix, filter requests by stripping the prefix
|
||||
if s.Prefix != "" {
|
||||
if !strings.HasPrefix(file, s.Prefix) {
|
||||
next(rw, r)
|
||||
return
|
||||
}
|
||||
file = file[len(s.Prefix):]
|
||||
if file != "" && file[0] != '/' {
|
||||
next(rw, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
f, err := s.Dir.Open(file)
|
||||
if err != nil {
|
||||
// discard the error?
|
||||
next(rw, r)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
next(rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
// try to serve index file
|
||||
if fi.IsDir() {
|
||||
// redirect if missing trailing slash
|
||||
if !strings.HasSuffix(r.URL.Path, "/") {
|
||||
http.Redirect(rw, r, r.URL.Path+"/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
file = path.Join(file, s.IndexFile)
|
||||
f, err = s.Dir.Open(file)
|
||||
if err != nil {
|
||||
next(rw, r)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err = f.Stat()
|
||||
if err != nil || fi.IsDir() {
|
||||
next(rw, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.ServeContent(rw, r, file, fi.ModTime(), f)
|
||||
}
|
21
vendor/github.com/dustin/go-humanize/.travis.yml
generated
vendored
Normal file
21
vendor/github.com/dustin/go-humanize/.travis.yml
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.3.x
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- master
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: master
|
||||
fast_finish: true
|
||||
install:
|
||||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
||||
- go tool vet .
|
||||
- go test -v -race ./...
|
|
@ -1,6 +1,4 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Jeremy Saenz
|
||||
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -9,8 +7,8 @@ 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 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,
|
||||
|
@ -19,3 +17,5 @@ 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.
|
||||
|
||||
<http://www.opensource.org/licenses/mit-license.php>
|
124
vendor/github.com/dustin/go-humanize/README.markdown
generated
vendored
Normal file
124
vendor/github.com/dustin/go-humanize/README.markdown
generated
vendored
Normal file
|
@ -0,0 +1,124 @@
|
|||
# Humane Units [](https://travis-ci.org/dustin/go-humanize) [](https://godoc.org/github.com/dustin/go-humanize)
|
||||
|
||||
Just a few functions for helping humanize times and sizes.
|
||||
|
||||
`go get` it as `github.com/dustin/go-humanize`, import it as
|
||||
`"github.com/dustin/go-humanize"`, use it as `humanize`.
|
||||
|
||||
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for
|
||||
complete documentation.
|
||||
|
||||
## Sizes
|
||||
|
||||
This lets you take numbers like `82854982` and convert them to useful
|
||||
strings like, `83 MB` or `79 MiB` (whichever you prefer).
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
|
||||
```
|
||||
|
||||
## Times
|
||||
|
||||
This lets you take a `time.Time` and spit it out in relative terms.
|
||||
For example, `12 seconds ago` or `3 days from now`.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.
|
||||
```
|
||||
|
||||
Thanks to Kyle Lemons for the time implementation from an IRC
|
||||
conversation one day. It's pretty neat.
|
||||
|
||||
## Ordinals
|
||||
|
||||
From a [mailing list discussion][odisc] where a user wanted to be able
|
||||
to label ordinals.
|
||||
|
||||
0 -> 0th
|
||||
1 -> 1st
|
||||
2 -> 2nd
|
||||
3 -> 3rd
|
||||
4 -> 4th
|
||||
[...]
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.
|
||||
```
|
||||
|
||||
## Commas
|
||||
|
||||
Want to shove commas into numbers? Be my guest.
|
||||
|
||||
0 -> 0
|
||||
100 -> 100
|
||||
1000 -> 1,000
|
||||
1000000000 -> 1,000,000,000
|
||||
-100000 -> -100,000
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
|
||||
```
|
||||
|
||||
## Ftoa
|
||||
|
||||
Nicer float64 formatter that removes trailing zeros.
|
||||
|
||||
```go
|
||||
fmt.Printf("%f", 2.24) // 2.240000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
|
||||
fmt.Printf("%f", 2.0) // 2.000000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
|
||||
```
|
||||
|
||||
## SI notation
|
||||
|
||||
Format numbers with [SI notation][sinotation].
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
humanize.SI(0.00000000223, "M") // 2.23 nM
|
||||
```
|
||||
|
||||
## English-specific functions
|
||||
|
||||
The following functions are in the `humanize/english` subpackage.
|
||||
|
||||
### Plurals
|
||||
|
||||
Simple English pluralization
|
||||
|
||||
```go
|
||||
english.PluralWord(1, "object", "") // object
|
||||
english.PluralWord(42, "object", "") // objects
|
||||
english.PluralWord(2, "bus", "") // buses
|
||||
english.PluralWord(99, "locus", "loci") // loci
|
||||
|
||||
english.Plural(1, "object", "") // 1 object
|
||||
english.Plural(42, "object", "") // 42 objects
|
||||
english.Plural(2, "bus", "") // 2 buses
|
||||
english.Plural(99, "locus", "loci") // 99 loci
|
||||
```
|
||||
|
||||
### Word series
|
||||
|
||||
Format comma-separated words lists with conjuctions:
|
||||
|
||||
```go
|
||||
english.WordSeries([]string{"foo"}, "and") // foo
|
||||
english.WordSeries([]string{"foo", "bar"}, "and") // foo and bar
|
||||
english.WordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar and baz
|
||||
|
||||
english.OxfordWordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar, and baz
|
||||
```
|
||||
|
||||
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
|
||||
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix
|
31
vendor/github.com/dustin/go-humanize/big.go
generated
vendored
Normal file
31
vendor/github.com/dustin/go-humanize/big.go
generated
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// order of magnitude (to a max order)
|
||||
func oomm(n, b *big.Int, maxmag int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
if mag == maxmag && maxmag >= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
|
||||
// total order of magnitude
|
||||
// (same as above, but with no upper limit)
|
||||
func oom(n, b *big.Int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
173
vendor/github.com/dustin/go-humanize/bigbytes.go
generated
vendored
Normal file
173
vendor/github.com/dustin/go-humanize/bigbytes.go
generated
vendored
Normal file
|
@ -0,0 +1,173 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
bigIECExp = big.NewInt(1024)
|
||||
|
||||
// BigByte is one byte in bit.Ints
|
||||
BigByte = big.NewInt(1)
|
||||
// BigKiByte is 1,024 bytes in bit.Ints
|
||||
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
|
||||
// BigMiByte is 1,024 k bytes in bit.Ints
|
||||
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
|
||||
// BigGiByte is 1,024 m bytes in bit.Ints
|
||||
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
|
||||
// BigTiByte is 1,024 g bytes in bit.Ints
|
||||
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
|
||||
// BigPiByte is 1,024 t bytes in bit.Ints
|
||||
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
|
||||
// BigEiByte is 1,024 p bytes in bit.Ints
|
||||
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
|
||||
// BigZiByte is 1,024 e bytes in bit.Ints
|
||||
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
|
||||
// BigYiByte is 1,024 z bytes in bit.Ints
|
||||
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
|
||||
)
|
||||
|
||||
var (
|
||||
bigSIExp = big.NewInt(1000)
|
||||
|
||||
// BigSIByte is one SI byte in big.Ints
|
||||
BigSIByte = big.NewInt(1)
|
||||
// BigKByte is 1,000 SI bytes in big.Ints
|
||||
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
|
||||
// BigMByte is 1,000 SI k bytes in big.Ints
|
||||
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
|
||||
// BigGByte is 1,000 SI m bytes in big.Ints
|
||||
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
|
||||
// BigTByte is 1,000 SI g bytes in big.Ints
|
||||
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
|
||||
// BigPByte is 1,000 SI t bytes in big.Ints
|
||||
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
|
||||
// BigEByte is 1,000 SI p bytes in big.Ints
|
||||
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
|
||||
// BigZByte is 1,000 SI e bytes in big.Ints
|
||||
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
|
||||
// BigYByte is 1,000 SI z bytes in big.Ints
|
||||
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
|
||||
)
|
||||
|
||||
var bigBytesSizeTable = map[string]*big.Int{
|
||||
"b": BigByte,
|
||||
"kib": BigKiByte,
|
||||
"kb": BigKByte,
|
||||
"mib": BigMiByte,
|
||||
"mb": BigMByte,
|
||||
"gib": BigGiByte,
|
||||
"gb": BigGByte,
|
||||
"tib": BigTiByte,
|
||||
"tb": BigTByte,
|
||||
"pib": BigPiByte,
|
||||
"pb": BigPByte,
|
||||
"eib": BigEiByte,
|
||||
"eb": BigEByte,
|
||||
"zib": BigZiByte,
|
||||
"zb": BigZByte,
|
||||
"yib": BigYiByte,
|
||||
"yb": BigYByte,
|
||||
// Without suffix
|
||||
"": BigByte,
|
||||
"ki": BigKiByte,
|
||||
"k": BigKByte,
|
||||
"mi": BigMiByte,
|
||||
"m": BigMByte,
|
||||
"gi": BigGiByte,
|
||||
"g": BigGByte,
|
||||
"ti": BigTiByte,
|
||||
"t": BigTByte,
|
||||
"pi": BigPiByte,
|
||||
"p": BigPByte,
|
||||
"ei": BigEiByte,
|
||||
"e": BigEByte,
|
||||
"z": BigZByte,
|
||||
"zi": BigZiByte,
|
||||
"y": BigYByte,
|
||||
"yi": BigYiByte,
|
||||
}
|
||||
|
||||
var ten = big.NewInt(10)
|
||||
|
||||
func humanateBigBytes(s, base *big.Int, sizes []string) string {
|
||||
if s.Cmp(ten) < 0 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
c := (&big.Int{}).Set(s)
|
||||
val, mag := oomm(c, base, len(sizes)-1)
|
||||
suffix := sizes[mag]
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
|
||||
}
|
||||
|
||||
// BigBytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigBytes(82854982) -> 83 MB
|
||||
func BigBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
return humanateBigBytes(s, bigSIExp, sizes)
|
||||
}
|
||||
|
||||
// BigIBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigIBytes(82854982) -> 79 MiB
|
||||
func BigIBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
|
||||
return humanateBigBytes(s, bigIECExp, sizes)
|
||||
}
|
||||
|
||||
// ParseBigBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See also: BigBytes, BigIBytes.
|
||||
//
|
||||
// ParseBigBytes("42 MB") -> 42000000, nil
|
||||
// ParseBigBytes("42 mib") -> 44040192, nil
|
||||
func ParseBigBytes(s string) (*big.Int, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
val := &big.Rat{}
|
||||
_, err := fmt.Sscanf(num, "%f", val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bigBytesSizeTable[extra]; ok {
|
||||
mv := (&big.Rat{}).SetInt(m)
|
||||
val.Mul(val, mv)
|
||||
rv := &big.Int{}
|
||||
rv.Div(val.Num(), val.Denom())
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
143
vendor/github.com/dustin/go-humanize/bytes.go
generated
vendored
Normal file
143
vendor/github.com/dustin/go-humanize/bytes.go
generated
vendored
Normal file
|
@ -0,0 +1,143 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// IEC Sizes.
|
||||
// kibis of bits
|
||||
const (
|
||||
Byte = 1 << (iota * 10)
|
||||
KiByte
|
||||
MiByte
|
||||
GiByte
|
||||
TiByte
|
||||
PiByte
|
||||
EiByte
|
||||
)
|
||||
|
||||
// SI Sizes.
|
||||
const (
|
||||
IByte = 1
|
||||
KByte = IByte * 1000
|
||||
MByte = KByte * 1000
|
||||
GByte = MByte * 1000
|
||||
TByte = GByte * 1000
|
||||
PByte = TByte * 1000
|
||||
EByte = PByte * 1000
|
||||
)
|
||||
|
||||
var bytesSizeTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"kib": KiByte,
|
||||
"kb": KByte,
|
||||
"mib": MiByte,
|
||||
"mb": MByte,
|
||||
"gib": GiByte,
|
||||
"gb": GByte,
|
||||
"tib": TiByte,
|
||||
"tb": TByte,
|
||||
"pib": PiByte,
|
||||
"pb": PByte,
|
||||
"eib": EiByte,
|
||||
"eb": EByte,
|
||||
// Without suffix
|
||||
"": Byte,
|
||||
"ki": KiByte,
|
||||
"k": KByte,
|
||||
"mi": MiByte,
|
||||
"m": MByte,
|
||||
"gi": GiByte,
|
||||
"g": GByte,
|
||||
"ti": TiByte,
|
||||
"t": TByte,
|
||||
"pi": PiByte,
|
||||
"p": PByte,
|
||||
"ei": EiByte,
|
||||
"e": EByte,
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
// Bytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// Bytes(82854982) -> 83 MB
|
||||
func Bytes(s uint64) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
return humanateBytes(s, 1000, sizes)
|
||||
}
|
||||
|
||||
// IBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// IBytes(82854982) -> 79 MiB
|
||||
func IBytes(s uint64) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
return humanateBytes(s, 1024, sizes)
|
||||
}
|
||||
|
||||
// ParseBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See Also: Bytes, IBytes.
|
||||
//
|
||||
// ParseBytes("42 MB") -> 42000000, nil
|
||||
// ParseBytes("42 mib") -> 44040192, nil
|
||||
func ParseBytes(s string) (uint64, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(num, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bytesSizeTable[extra]; ok {
|
||||
f *= float64(m)
|
||||
if f >= math.MaxUint64 {
|
||||
return 0, fmt.Errorf("too large: %v", s)
|
||||
}
|
||||
return uint64(f), nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
116
vendor/github.com/dustin/go-humanize/comma.go
generated
vendored
Normal file
116
vendor/github.com/dustin/go-humanize/comma.go
generated
vendored
Normal file
|
@ -0,0 +1,116 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Comma produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Comma(834142) -> 834,142
|
||||
func Comma(v int64) string {
|
||||
sign := ""
|
||||
|
||||
// Min int64 can't be negated to a usable value, so it has to be special cased.
|
||||
if v == math.MinInt64 {
|
||||
return "-9,223,372,036,854,775,808"
|
||||
}
|
||||
|
||||
if v < 0 {
|
||||
sign = "-"
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
parts := []string{"", "", "", "", "", "", ""}
|
||||
j := len(parts) - 1
|
||||
|
||||
for v > 999 {
|
||||
parts[j] = strconv.FormatInt(v%1000, 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
v = v / 1000
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(v))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
|
||||
// Commaf produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Commaf(834142.32) -> 834,142.32
|
||||
func Commaf(v float64) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// CommafWithDigits works like the Commaf but limits the resulting
|
||||
// string to the given number of decimal places.
|
||||
//
|
||||
// e.g. CommafWithDigits(834142.32, 1) -> 834,142.3
|
||||
func CommafWithDigits(f float64, decimals int) string {
|
||||
return stripTrailingDigits(Commaf(f), decimals)
|
||||
}
|
||||
|
||||
// BigComma produces a string form of the given big.Int in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigComma(b *big.Int) string {
|
||||
sign := ""
|
||||
if b.Sign() < 0 {
|
||||
sign = "-"
|
||||
b.Abs(b)
|
||||
}
|
||||
|
||||
athousand := big.NewInt(1000)
|
||||
c := (&big.Int{}).Set(b)
|
||||
_, m := oom(c, athousand)
|
||||
parts := make([]string, m+1)
|
||||
j := len(parts) - 1
|
||||
|
||||
mod := &big.Int{}
|
||||
for b.Cmp(athousand) >= 0 {
|
||||
b.DivMod(b, athousand, mod)
|
||||
parts[j] = strconv.FormatInt(mod.Int64(), 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(b.Int64()))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
40
vendor/github.com/dustin/go-humanize/commaf.go
generated
vendored
Normal file
40
vendor/github.com/dustin/go-humanize/commaf.go
generated
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
// +build go1.6
|
||||
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BigCommaf produces a string form of the given big.Float in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigCommaf(v *big.Float) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v.Sign() < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v.Abs(v)
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(v.Text('f', -1), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
46
vendor/github.com/dustin/go-humanize/ftoa.go
generated
vendored
Normal file
46
vendor/github.com/dustin/go-humanize/ftoa.go
generated
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func stripTrailingZeros(s string) string {
|
||||
offset := len(s) - 1
|
||||
for offset > 0 {
|
||||
if s[offset] == '.' {
|
||||
offset--
|
||||
break
|
||||
}
|
||||
if s[offset] != '0' {
|
||||
break
|
||||
}
|
||||
offset--
|
||||
}
|
||||
return s[:offset+1]
|
||||
}
|
||||
|
||||
func stripTrailingDigits(s string, digits int) string {
|
||||
if i := strings.Index(s, "."); i >= 0 {
|
||||
if digits <= 0 {
|
||||
return s[:i]
|
||||
}
|
||||
i++
|
||||
if i+digits >= len(s) {
|
||||
return s
|
||||
}
|
||||
return s[:i+digits]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Ftoa converts a float to a string with no trailing zeros.
|
||||
func Ftoa(num float64) string {
|
||||
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
|
||||
}
|
||||
|
||||
// FtoaWithDigits converts a float to a string but limits the resulting string
|
||||
// to the given number of decimal places, and no trailing zeros.
|
||||
func FtoaWithDigits(num float64, digits int) string {
|
||||
return stripTrailingZeros(stripTrailingDigits(strconv.FormatFloat(num, 'f', 6, 64), digits))
|
||||
}
|
8
vendor/github.com/dustin/go-humanize/humanize.go
generated
vendored
Normal file
8
vendor/github.com/dustin/go-humanize/humanize.go
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
Package humanize converts boring ugly numbers to human-friendly strings and back.
|
||||
|
||||
Durations can be turned into strings such as "3 days ago", numbers
|
||||
representing sizes like 82854982 into useful strings like, "83 MB" or
|
||||
"79 MiB" (whichever you prefer).
|
||||
*/
|
||||
package humanize
|
192
vendor/github.com/dustin/go-humanize/number.go
generated
vendored
Normal file
192
vendor/github.com/dustin/go-humanize/number.go
generated
vendored
Normal file
|
@ -0,0 +1,192 @@
|
|||
package humanize
|
||||
|
||||
/*
|
||||
Slightly adapted from the source to fit go-humanize.
|
||||
|
||||
Author: https://github.com/gorhill
|
||||
Source: https://gist.github.com/gorhill/5285193
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
renderFloatPrecisionMultipliers = [...]float64{
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
100000,
|
||||
1000000,
|
||||
10000000,
|
||||
100000000,
|
||||
1000000000,
|
||||
}
|
||||
|
||||
renderFloatPrecisionRounders = [...]float64{
|
||||
0.5,
|
||||
0.05,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.00005,
|
||||
0.000005,
|
||||
0.0000005,
|
||||
0.00000005,
|
||||
0.000000005,
|
||||
0.0000000005,
|
||||
}
|
||||
)
|
||||
|
||||
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
|
||||
// * thousands separator
|
||||
// * decimal separator
|
||||
// * decimal precision
|
||||
//
|
||||
// Usage: s := RenderFloat(format, n)
|
||||
// The format parameter tells how to render the number n.
|
||||
//
|
||||
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
|
||||
//
|
||||
// Examples of format strings, given n = 12345.6789:
|
||||
// "#,###.##" => "12,345.67"
|
||||
// "#,###." => "12,345"
|
||||
// "#,###" => "12345,678"
|
||||
// "#\u202F###,##" => "12 345,68"
|
||||
// "#.###,###### => 12.345,678900
|
||||
// "" (aka default format) => 12,345.67
|
||||
//
|
||||
// The highest precision allowed is 9 digits after the decimal symbol.
|
||||
// There is also a version for integer number, FormatInteger(),
|
||||
// which is convenient for calls within template.
|
||||
func FormatFloat(format string, n float64) string {
|
||||
// Special cases:
|
||||
// NaN = "NaN"
|
||||
// +Inf = "+Infinity"
|
||||
// -Inf = "-Infinity"
|
||||
if math.IsNaN(n) {
|
||||
return "NaN"
|
||||
}
|
||||
if n > math.MaxFloat64 {
|
||||
return "Infinity"
|
||||
}
|
||||
if n < -math.MaxFloat64 {
|
||||
return "-Infinity"
|
||||
}
|
||||
|
||||
// default format
|
||||
precision := 2
|
||||
decimalStr := "."
|
||||
thousandStr := ","
|
||||
positiveStr := ""
|
||||
negativeStr := "-"
|
||||
|
||||
if len(format) > 0 {
|
||||
format := []rune(format)
|
||||
|
||||
// If there is an explicit format directive,
|
||||
// then default values are these:
|
||||
precision = 9
|
||||
thousandStr = ""
|
||||
|
||||
// collect indices of meaningful formatting directives
|
||||
formatIndx := []int{}
|
||||
for i, char := range format {
|
||||
if char != '#' && char != '0' {
|
||||
formatIndx = append(formatIndx, i)
|
||||
}
|
||||
}
|
||||
|
||||
if len(formatIndx) > 0 {
|
||||
// Directive at index 0:
|
||||
// Must be a '+'
|
||||
// Raise an error if not the case
|
||||
// index: 0123456789
|
||||
// +0.000,000
|
||||
// +000,000.0
|
||||
// +0000.00
|
||||
// +0000
|
||||
if formatIndx[0] == 0 {
|
||||
if format[formatIndx[0]] != '+' {
|
||||
panic("RenderFloat(): invalid positive sign directive")
|
||||
}
|
||||
positiveStr = "+"
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// Two directives:
|
||||
// First is thousands separator
|
||||
// Raise an error if not followed by 3-digit
|
||||
// 0123456789
|
||||
// 0.000,000
|
||||
// 000,000.00
|
||||
if len(formatIndx) == 2 {
|
||||
if (formatIndx[1] - formatIndx[0]) != 4 {
|
||||
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
|
||||
}
|
||||
thousandStr = string(format[formatIndx[0]])
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// One directive:
|
||||
// Directive is decimal separator
|
||||
// The number of digit-specifier following the separator indicates wanted precision
|
||||
// 0123456789
|
||||
// 0.00
|
||||
// 000,0000
|
||||
if len(formatIndx) == 1 {
|
||||
decimalStr = string(format[formatIndx[0]])
|
||||
precision = len(format) - formatIndx[0] - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate sign part
|
||||
var signStr string
|
||||
if n >= 0.000000001 {
|
||||
signStr = positiveStr
|
||||
} else if n <= -0.000000001 {
|
||||
signStr = negativeStr
|
||||
n = -n
|
||||
} else {
|
||||
signStr = ""
|
||||
n = 0.0
|
||||
}
|
||||
|
||||
// split number into integer and fractional parts
|
||||
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
|
||||
|
||||
// generate integer part string
|
||||
intStr := strconv.FormatInt(int64(intf), 10)
|
||||
|
||||
// add thousand separator if required
|
||||
if len(thousandStr) > 0 {
|
||||
for i := len(intStr); i > 3; {
|
||||
i -= 3
|
||||
intStr = intStr[:i] + thousandStr + intStr[i:]
|
||||
}
|
||||
}
|
||||
|
||||
// no fractional part, we can leave now
|
||||
if precision == 0 {
|
||||
return signStr + intStr
|
||||
}
|
||||
|
||||
// generate fractional part
|
||||
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
|
||||
// may need padding
|
||||
if len(fracStr) < precision {
|
||||
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
|
||||
}
|
||||
|
||||
return signStr + intStr + decimalStr + fracStr
|
||||
}
|
||||
|
||||
// FormatInteger produces a formatted number as string.
|
||||
// See FormatFloat.
|
||||
func FormatInteger(format string, n int) string {
|
||||
return FormatFloat(format, float64(n))
|
||||
}
|
25
vendor/github.com/dustin/go-humanize/ordinals.go
generated
vendored
Normal file
25
vendor/github.com/dustin/go-humanize/ordinals.go
generated
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
package humanize
|
||||
|
||||
import "strconv"
|
||||
|
||||
// Ordinal gives you the input number in a rank/ordinal format.
|
||||
//
|
||||
// Ordinal(3) -> 3rd
|
||||
func Ordinal(x int) string {
|
||||
suffix := "th"
|
||||
switch x % 10 {
|
||||
case 1:
|
||||
if x%100 != 11 {
|
||||
suffix = "st"
|
||||
}
|
||||
case 2:
|
||||
if x%100 != 12 {
|
||||
suffix = "nd"
|
||||
}
|
||||
case 3:
|
||||
if x%100 != 13 {
|
||||
suffix = "rd"
|
||||
}
|
||||
}
|
||||
return strconv.Itoa(x) + suffix
|
||||
}
|
123
vendor/github.com/dustin/go-humanize/si.go
generated
vendored
Normal file
123
vendor/github.com/dustin/go-humanize/si.go
generated
vendored
Normal file
|
@ -0,0 +1,123 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var siPrefixTable = map[float64]string{
|
||||
-24: "y", // yocto
|
||||
-21: "z", // zepto
|
||||
-18: "a", // atto
|
||||
-15: "f", // femto
|
||||
-12: "p", // pico
|
||||
-9: "n", // nano
|
||||
-6: "µ", // micro
|
||||
-3: "m", // milli
|
||||
0: "",
|
||||
3: "k", // kilo
|
||||
6: "M", // mega
|
||||
9: "G", // giga
|
||||
12: "T", // tera
|
||||
15: "P", // peta
|
||||
18: "E", // exa
|
||||
21: "Z", // zetta
|
||||
24: "Y", // yotta
|
||||
}
|
||||
|
||||
var revSIPrefixTable = revfmap(siPrefixTable)
|
||||
|
||||
// revfmap reverses the map and precomputes the power multiplier
|
||||
func revfmap(in map[float64]string) map[string]float64 {
|
||||
rv := map[string]float64{}
|
||||
for k, v := range in {
|
||||
rv[v] = math.Pow(10, k)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
var riParseRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
ri := `^([\-0-9.]+)\s?([`
|
||||
for _, v := range siPrefixTable {
|
||||
ri += v
|
||||
}
|
||||
ri += `]?)(.*)`
|
||||
|
||||
riParseRegex = regexp.MustCompile(ri)
|
||||
}
|
||||
|
||||
// ComputeSI finds the most appropriate SI prefix for the given number
|
||||
// and returns the prefix along with the value adjusted to be within
|
||||
// that prefix.
|
||||
//
|
||||
// See also: SI, ParseSI.
|
||||
//
|
||||
// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p")
|
||||
func ComputeSI(input float64) (float64, string) {
|
||||
if input == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
mag := math.Abs(input)
|
||||
exponent := math.Floor(logn(mag, 10))
|
||||
exponent = math.Floor(exponent/3) * 3
|
||||
|
||||
value := mag / math.Pow(10, exponent)
|
||||
|
||||
// Handle special case where value is exactly 1000.0
|
||||
// Should return 1 M instead of 1000 k
|
||||
if value == 1000.0 {
|
||||
exponent += 3
|
||||
value = mag / math.Pow(10, exponent)
|
||||
}
|
||||
|
||||
value = math.Copysign(value, input)
|
||||
|
||||
prefix := siPrefixTable[exponent]
|
||||
return value, prefix
|
||||
}
|
||||
|
||||
// SI returns a string with default formatting.
|
||||
//
|
||||
// SI uses Ftoa to format float value, removing trailing zeros.
|
||||
//
|
||||
// See also: ComputeSI, ParseSI.
|
||||
//
|
||||
// e.g. SI(1000000, "B") -> 1 MB
|
||||
// e.g. SI(2.2345e-12, "F") -> 2.2345 pF
|
||||
func SI(input float64, unit string) string {
|
||||
value, prefix := ComputeSI(input)
|
||||
return Ftoa(value) + " " + prefix + unit
|
||||
}
|
||||
|
||||
// SIWithDigits works like SI but limits the resulting string to the
|
||||
// given number of decimal places.
|
||||
//
|
||||
// e.g. SIWithDigits(1000000, 0, "B") -> 1 MB
|
||||
// e.g. SIWithDigits(2.2345e-12, 2, "F") -> 2.23 pF
|
||||
func SIWithDigits(input float64, decimals int, unit string) string {
|
||||
value, prefix := ComputeSI(input)
|
||||
return FtoaWithDigits(value, decimals) + " " + prefix + unit
|
||||
}
|
||||
|
||||
var errInvalid = errors.New("invalid input")
|
||||
|
||||
// ParseSI parses an SI string back into the number and unit.
|
||||
//
|
||||
// See also: SI, ComputeSI.
|
||||
//
|
||||
// e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil)
|
||||
func ParseSI(input string) (float64, string, error) {
|
||||
found := riParseRegex.FindStringSubmatch(input)
|
||||
if len(found) != 4 {
|
||||
return 0, "", errInvalid
|
||||
}
|
||||
mag := revSIPrefixTable[found[2]]
|
||||
unit := found[3]
|
||||
|
||||
base, err := strconv.ParseFloat(found[1], 64)
|
||||
return base * mag, unit, err
|
||||
}
|
117
vendor/github.com/dustin/go-humanize/times.go
generated
vendored
Normal file
117
vendor/github.com/dustin/go-humanize/times.go
generated
vendored
Normal file
|
@ -0,0 +1,117 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Seconds-based time units
|
||||
const (
|
||||
Day = 24 * time.Hour
|
||||
Week = 7 * Day
|
||||
Month = 30 * Day
|
||||
Year = 12 * Month
|
||||
LongTime = 37 * Year
|
||||
)
|
||||
|
||||
// Time formats a time into a relative string.
|
||||
//
|
||||
// Time(someT) -> "3 weeks ago"
|
||||
func Time(then time.Time) string {
|
||||
return RelTime(then, time.Now(), "ago", "from now")
|
||||
}
|
||||
|
||||
// A RelTimeMagnitude struct contains a relative time point at which
|
||||
// the relative format of time will switch to a new format string. A
|
||||
// slice of these in ascending order by their "D" field is passed to
|
||||
// CustomRelTime to format durations.
|
||||
//
|
||||
// The Format field is a string that may contain a "%s" which will be
|
||||
// replaced with the appropriate signed label (e.g. "ago" or "from
|
||||
// now") and a "%d" that will be replaced by the quantity.
|
||||
//
|
||||
// The DivBy field is the amount of time the time difference must be
|
||||
// divided by in order to display correctly.
|
||||
//
|
||||
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
|
||||
// DivBy should be time.Minute so whatever the duration is will be
|
||||
// expressed in minutes.
|
||||
type RelTimeMagnitude struct {
|
||||
D time.Duration
|
||||
Format string
|
||||
DivBy time.Duration
|
||||
}
|
||||
|
||||
var defaultMagnitudes = []RelTimeMagnitude{
|
||||
{time.Second, "now", time.Second},
|
||||
{2 * time.Second, "1 second %s", 1},
|
||||
{time.Minute, "%d seconds %s", time.Second},
|
||||
{2 * time.Minute, "1 minute %s", 1},
|
||||
{time.Hour, "%d minutes %s", time.Minute},
|
||||
{2 * time.Hour, "1 hour %s", 1},
|
||||
{Day, "%d hours %s", time.Hour},
|
||||
{2 * Day, "1 day %s", 1},
|
||||
{Week, "%d days %s", Day},
|
||||
{2 * Week, "1 week %s", 1},
|
||||
{Month, "%d weeks %s", Week},
|
||||
{2 * Month, "1 month %s", 1},
|
||||
{Year, "%d months %s", Month},
|
||||
{18 * Month, "1 year %s", 1},
|
||||
{2 * Year, "2 years %s", 1},
|
||||
{LongTime, "%d years %s", Year},
|
||||
{math.MaxInt64, "a long while %s", 1},
|
||||
}
|
||||
|
||||
// RelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times and two labels. In addition to the generic time
|
||||
// delta string (e.g. 5 minutes), the labels are used applied so that
|
||||
// the label corresponding to the smaller time is applied.
|
||||
//
|
||||
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
|
||||
func RelTime(a, b time.Time, albl, blbl string) string {
|
||||
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
|
||||
}
|
||||
|
||||
// CustomRelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times two labels and a table of relative time formats.
|
||||
// In addition to the generic time delta string (e.g. 5 minutes), the
|
||||
// labels are used applied so that the label corresponding to the
|
||||
// smaller time is applied.
|
||||
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
|
||||
lbl := albl
|
||||
diff := b.Sub(a)
|
||||
|
||||
if a.After(b) {
|
||||
lbl = blbl
|
||||
diff = a.Sub(b)
|
||||
}
|
||||
|
||||
n := sort.Search(len(magnitudes), func(i int) bool {
|
||||
return magnitudes[i].D > diff
|
||||
})
|
||||
|
||||
if n >= len(magnitudes) {
|
||||
n = len(magnitudes) - 1
|
||||
}
|
||||
mag := magnitudes[n]
|
||||
args := []interface{}{}
|
||||
escaped := false
|
||||
for _, ch := range mag.Format {
|
||||
if escaped {
|
||||
switch ch {
|
||||
case 's':
|
||||
args = append(args, lbl)
|
||||
case 'd':
|
||||
args = append(args, diff/mag.DivBy)
|
||||
}
|
||||
escaped = false
|
||||
} else {
|
||||
escaped = ch == '%'
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(mag.Format, args...)
|
||||
}
|
3
vendor/github.com/go-chi/chi/.gitignore
generated
vendored
Normal file
3
vendor/github.com/go-chi/chi/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
.idea
|
||||
*.sw?
|
||||
.vscode
|
18
vendor/github.com/go-chi/chi/.travis.yml
generated
vendored
Normal file
18
vendor/github.com/go-chi/chi/.travis.yml
generated
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
|
||||
script:
|
||||
- go get -d -t ./...
|
||||
- go vet ./...
|
||||
- go test ./...
|
||||
- >
|
||||
go_version=$(go version);
|
||||
if [ ${go_version:13:4} = "1.12" ]; then
|
||||
go get -u golang.org/x/tools/cmd/goimports;
|
||||
goimports -d -e ./ | grep '.*' && { echo; echo "Aborting due to non-empty goimports output."; exit 1; } || :;
|
||||
fi
|
||||
|
139
vendor/github.com/go-chi/chi/CHANGELOG.md
generated
vendored
Normal file
139
vendor/github.com/go-chi/chi/CHANGELOG.md
generated
vendored
Normal file
|
@ -0,0 +1,139 @@
|
|||
# Changelog
|
||||
|
||||
## v4.0.0 (2019-01-10)
|
||||
|
||||
- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8
|
||||
- router: respond with 404 on router with no routes (#362)
|
||||
- router: additional check to ensure wildcard is at the end of a url pattern (#333)
|
||||
- middleware: deprecate use of http.CloseNotifier (#347)
|
||||
- middleware: fix RedirectSlashes to include query params on redirect (#334)
|
||||
- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0
|
||||
|
||||
|
||||
## v3.3.4 (2019-01-07)
|
||||
|
||||
- Minor middleware improvements. No changes to core library/router. Moving v3 into its
|
||||
- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11
|
||||
- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4
|
||||
|
||||
|
||||
## v3.3.3 (2018-08-27)
|
||||
|
||||
- Minor release
|
||||
- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3
|
||||
|
||||
|
||||
## v3.3.2 (2017-12-22)
|
||||
|
||||
- Support to route trailing slashes on mounted sub-routers (#281)
|
||||
- middleware: new `ContentCharset` to check matching charsets. Thank you
|
||||
@csucu for your community contribution!
|
||||
|
||||
|
||||
## v3.3.1 (2017-11-20)
|
||||
|
||||
- middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types
|
||||
- middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value
|
||||
- Minor bug fixes
|
||||
|
||||
|
||||
## v3.3.0 (2017-10-10)
|
||||
|
||||
- New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage
|
||||
- Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function
|
||||
|
||||
|
||||
## v3.2.1 (2017-08-31)
|
||||
|
||||
- Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface
|
||||
and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path
|
||||
- Add new `RouteMethod` to `*Context`
|
||||
- Add new `Routes` pointer to `*Context`
|
||||
- Add new `middleware.GetHead` to route missing HEAD requests to GET handler
|
||||
- Updated benchmarks (see README)
|
||||
|
||||
|
||||
## v3.1.5 (2017-08-02)
|
||||
|
||||
- Setup golint and go vet for the project
|
||||
- As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler`
|
||||
to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler`
|
||||
|
||||
|
||||
## v3.1.0 (2017-07-10)
|
||||
|
||||
- Fix a few minor issues after v3 release
|
||||
- Move `docgen` sub-pkg to https://github.com/go-chi/docgen
|
||||
- Move `render` sub-pkg to https://github.com/go-chi/render
|
||||
- Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime
|
||||
suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in
|
||||
https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage.
|
||||
|
||||
|
||||
## v3.0.0 (2017-06-21)
|
||||
|
||||
- Major update to chi library with many exciting updates, but also some *breaking changes*
|
||||
- URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as
|
||||
`/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the
|
||||
same router
|
||||
- Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example:
|
||||
`r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")`
|
||||
- Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as
|
||||
`r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like
|
||||
in `_examples/custom-handler`
|
||||
- Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their
|
||||
own using file handler with the stdlib, see `_examples/fileserver` for an example
|
||||
- Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()`
|
||||
- Moved the chi project to its own organization, to allow chi-related community packages to
|
||||
be easily discovered and supported, at: https://github.com/go-chi
|
||||
- *NOTE:* please update your import paths to `"github.com/go-chi/chi"`
|
||||
- *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2
|
||||
|
||||
|
||||
## v2.1.0 (2017-03-30)
|
||||
|
||||
- Minor improvements and update to the chi core library
|
||||
- Introduced a brand new `chi/render` sub-package to complete the story of building
|
||||
APIs to offer a pattern for managing well-defined request / response payloads. Please
|
||||
check out the updated `_examples/rest` example for how it works.
|
||||
- Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface
|
||||
|
||||
|
||||
## v2.0.0 (2017-01-06)
|
||||
|
||||
- After many months of v2 being in an RC state with many companies and users running it in
|
||||
production, the inclusion of some improvements to the middlewares, we are very pleased to
|
||||
announce v2.0.0 of chi.
|
||||
|
||||
|
||||
## v2.0.0-rc1 (2016-07-26)
|
||||
|
||||
- Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular
|
||||
community `"net/context"` package has been included in the standard library as `"context"` and
|
||||
utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other
|
||||
request-scoped values. We're very excited about the new context addition and are proud to
|
||||
introduce chi v2, a minimal and powerful routing package for building large HTTP services,
|
||||
with zero external dependencies. Chi focuses on idiomatic design and encourages the use of
|
||||
stdlib HTTP handlers and middlwares.
|
||||
- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc`
|
||||
- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()`
|
||||
- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`,
|
||||
which provides direct access to URL routing parameters, the routing path and the matching
|
||||
routing patterns.
|
||||
- Users upgrading from chi v1 to v2, need to:
|
||||
1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to
|
||||
the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)`
|
||||
2. Use `chi.URLParam(r *http.Request, paramKey string) string`
|
||||
or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value
|
||||
|
||||
|
||||
## v1.0.0 (2016-07-01)
|
||||
|
||||
- Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older.
|
||||
|
||||
|
||||
## v0.9.0 (2016-03-31)
|
||||
|
||||
- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33)
|
||||
- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters
|
||||
has changed to: `chi.URLParam(ctx, "id")`
|
31
vendor/github.com/go-chi/chi/CONTRIBUTING.md
generated
vendored
Normal file
31
vendor/github.com/go-chi/chi/CONTRIBUTING.md
generated
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Contributing
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. [Install Go][go-install].
|
||||
2. Download the sources and switch the working directory:
|
||||
|
||||
```bash
|
||||
go get -u -d github.com/go-chi/chi
|
||||
cd $GOPATH/src/github.com/go-chi/chi
|
||||
```
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
A typical workflow is:
|
||||
|
||||
1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip]
|
||||
2. [Create a topic branch.][branch]
|
||||
3. Add tests for your change.
|
||||
4. Run `go test`. If your tests pass, return to the step 3.
|
||||
5. Implement the change and ensure the steps from the previous step pass.
|
||||
6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline.
|
||||
7. [Add, commit and push your changes.][git-help]
|
||||
8. [Submit a pull request.][pull-req]
|
||||
|
||||
[go-install]: https://golang.org/doc/install
|
||||
[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html
|
||||
[fork]: https://help.github.com/articles/fork-a-repo
|
||||
[branch]: http://learn.github.com/p/branching.html
|
||||
[git-help]: https://guides.github.com
|
||||
[pull-req]: https://help.github.com/articles/using-pull-requests
|
20
vendor/github.com/go-chi/chi/LICENSE
generated
vendored
Normal file
20
vendor/github.com/go-chi/chi/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc.
|
||||
|
||||
MIT License
|
||||
|
||||
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.
|
438
vendor/github.com/go-chi/chi/README.md
generated
vendored
Normal file
438
vendor/github.com/go-chi/chi/README.md
generated
vendored
Normal file
|
@ -0,0 +1,438 @@
|
|||
# <img alt="chi" src="https://cdn.rawgit.com/go-chi/chi/master/_examples/chi.svg" width="220" />
|
||||
|
||||
|
||||
[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis]
|
||||
|
||||
`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's
|
||||
especially good at helping you write large REST API services that are kept maintainable as your
|
||||
project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to
|
||||
handle signaling, cancelation and request-scoped values across a handler chain.
|
||||
|
||||
The focus of the project has been to seek out an elegant and comfortable design for writing
|
||||
REST API servers, written during the development of the Pressly API service that powers our
|
||||
public API service, which in turn powers all of our client-side applications.
|
||||
|
||||
The key considerations of chi's design are: project structure, maintainability, standard http
|
||||
handlers (stdlib-only), developer productivity, and deconstructing a large system into many small
|
||||
parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also
|
||||
included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too!
|
||||
|
||||
## Install
|
||||
|
||||
`go get -u github.com/go-chi/chi`
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* **Lightweight** - cloc'd in ~1000 LOC for the chi router
|
||||
* **Fast** - yes, see [benchmarks](#benchmarks)
|
||||
* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http`
|
||||
* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and subrouter mounting
|
||||
* **Context control** - built on new `context` package, providing value chaining, cancelations and timeouts
|
||||
* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91))
|
||||
* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown
|
||||
* **No external dependencies** - plain ol' Go stdlib + net/http
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples.
|
||||
|
||||
|
||||
**As easy as:**
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("welcome"))
|
||||
})
|
||||
http.ListenAndServe(":3000", r)
|
||||
}
|
||||
```
|
||||
|
||||
**REST Preview:**
|
||||
|
||||
Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs
|
||||
in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in
|
||||
Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)).
|
||||
|
||||
I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed
|
||||
above, they will show you all the features of chi and serve as a good form of documentation.
|
||||
|
||||
```go
|
||||
import (
|
||||
//...
|
||||
"context"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// A good base middleware stack
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
// Set a timeout value on the request context (ctx), that will signal
|
||||
// through ctx.Done() that the request has timed out and further
|
||||
// processing should be stopped.
|
||||
r.Use(middleware.Timeout(60 * time.Second))
|
||||
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("hi"))
|
||||
})
|
||||
|
||||
// RESTy routes for "articles" resource
|
||||
r.Route("/articles", func(r chi.Router) {
|
||||
r.With(paginate).Get("/", listArticles) // GET /articles
|
||||
r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017
|
||||
|
||||
r.Post("/", createArticle) // POST /articles
|
||||
r.Get("/search", searchArticles) // GET /articles/search
|
||||
|
||||
// Regexp url parameters:
|
||||
r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto
|
||||
|
||||
// Subrouters:
|
||||
r.Route("/{articleID}", func(r chi.Router) {
|
||||
r.Use(ArticleCtx)
|
||||
r.Get("/", getArticle) // GET /articles/123
|
||||
r.Put("/", updateArticle) // PUT /articles/123
|
||||
r.Delete("/", deleteArticle) // DELETE /articles/123
|
||||
})
|
||||
})
|
||||
|
||||
// Mount the admin sub-router
|
||||
r.Mount("/admin", adminRouter())
|
||||
|
||||
http.ListenAndServe(":3333", r)
|
||||
}
|
||||
|
||||
func ArticleCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
articleID := chi.URLParam(r, "articleID")
|
||||
article, err := dbGetArticle(articleID)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), "article", article)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func getArticle(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
article, ok := ctx.Value("article").(*Article)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(422), 422)
|
||||
return
|
||||
}
|
||||
w.Write([]byte(fmt.Sprintf("title:%s", article.Title)))
|
||||
}
|
||||
|
||||
// A completely separate router for administrator routes
|
||||
func adminRouter() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Use(AdminOnly)
|
||||
r.Get("/", adminIndex)
|
||||
r.Get("/accounts", adminListAccounts)
|
||||
return r
|
||||
}
|
||||
|
||||
func AdminOnly(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
perm, ok := ctx.Value("acl.permission").(YourPermissionType)
|
||||
if !ok || !perm.IsAdmin() {
|
||||
http.Error(w, http.StatusText(403), 403)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Router design
|
||||
|
||||
chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree).
|
||||
The router is fully compatible with `net/http`.
|
||||
|
||||
Built on top of the tree is the `Router` interface:
|
||||
|
||||
```go
|
||||
// Router consisting of the core routing methods used by chi's Mux,
|
||||
// using only the standard net/http.
|
||||
type Router interface {
|
||||
http.Handler
|
||||
Routes
|
||||
|
||||
// Use appends one of more middlewares onto the Router stack.
|
||||
Use(middlewares ...func(http.Handler) http.Handler)
|
||||
|
||||
// With adds inline middlewares for an endpoint handler.
|
||||
With(middlewares ...func(http.Handler) http.Handler) Router
|
||||
|
||||
// Group adds a new inline-Router along the current routing
|
||||
// path, with a fresh middleware stack for the inline-Router.
|
||||
Group(fn func(r Router)) Router
|
||||
|
||||
// Route mounts a sub-Router along a `pattern`` string.
|
||||
Route(pattern string, fn func(r Router)) Router
|
||||
|
||||
// Mount attaches another http.Handler along ./pattern/*
|
||||
Mount(pattern string, h http.Handler)
|
||||
|
||||
// Handle and HandleFunc adds routes for `pattern` that matches
|
||||
// all HTTP methods.
|
||||
Handle(pattern string, h http.Handler)
|
||||
HandleFunc(pattern string, h http.HandlerFunc)
|
||||
|
||||
// Method and MethodFunc adds routes for `pattern` that matches
|
||||
// the `method` HTTP method.
|
||||
Method(method, pattern string, h http.Handler)
|
||||
MethodFunc(method, pattern string, h http.HandlerFunc)
|
||||
|
||||
// HTTP-method routing along `pattern`
|
||||
Connect(pattern string, h http.HandlerFunc)
|
||||
Delete(pattern string, h http.HandlerFunc)
|
||||
Get(pattern string, h http.HandlerFunc)
|
||||
Head(pattern string, h http.HandlerFunc)
|
||||
Options(pattern string, h http.HandlerFunc)
|
||||
Patch(pattern string, h http.HandlerFunc)
|
||||
Post(pattern string, h http.HandlerFunc)
|
||||
Put(pattern string, h http.HandlerFunc)
|
||||
Trace(pattern string, h http.HandlerFunc)
|
||||
|
||||
// NotFound defines a handler to respond whenever a route could
|
||||
// not be found.
|
||||
NotFound(h http.HandlerFunc)
|
||||
|
||||
// MethodNotAllowed defines a handler to respond whenever a method is
|
||||
// not allowed.
|
||||
MethodNotAllowed(h http.HandlerFunc)
|
||||
}
|
||||
|
||||
// Routes interface adds two methods for router traversal, which is also
|
||||
// used by the github.com/go-chi/docgen package to generate documentation for Routers.
|
||||
type Routes interface {
|
||||
// Routes returns the routing tree in an easily traversable structure.
|
||||
Routes() []Route
|
||||
|
||||
// Middlewares returns the list of middlewares in use by the router.
|
||||
Middlewares() Middlewares
|
||||
|
||||
// Match searches the routing tree for a handler that matches
|
||||
// the method/path - similar to routing a http request, but without
|
||||
// executing the handler thereafter.
|
||||
Match(rctx *Context, method, path string) bool
|
||||
}
|
||||
```
|
||||
|
||||
Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern
|
||||
supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters
|
||||
can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters
|
||||
and `chi.URLParam(r, "*")` for a wildcard parameter.
|
||||
|
||||
|
||||
### Middleware handlers
|
||||
|
||||
chi's middlewares are just stdlib net/http middleware handlers. There is nothing special
|
||||
about them, which means the router and all the tooling is designed to be compatible and
|
||||
friendly with any middleware in the community. This offers much better extensibility and reuse
|
||||
of packages and is at the heart of chi's purpose.
|
||||
|
||||
Here is an example of a standard net/http middleware handler using the new request context
|
||||
available in Go. This middleware sets a hypothetical user identifier on the request
|
||||
context and calls the next handler in the chain.
|
||||
|
||||
```go
|
||||
// HTTP middleware setting a value on the request context
|
||||
func MyMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.WithValue(r.Context(), "user", "123")
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Request handlers
|
||||
|
||||
chi uses standard net/http request handlers. This little snippet is an example of a http.Handler
|
||||
func that reads a user identifier from the request context - hypothetically, identifying
|
||||
the user sending an authenticated request, validated+set by a previous middleware handler.
|
||||
|
||||
```go
|
||||
// HTTP handler accessing data from the request context.
|
||||
func MyRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user := r.Context().Value("user").(string)
|
||||
w.Write([]byte(fmt.Sprintf("hi %s", user)))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### URL parameters
|
||||
|
||||
chi's router parses and stores URL parameters right onto the request context. Here is
|
||||
an example of how to access URL params in your net/http handlers. And of course, middlewares
|
||||
are able to access the same information.
|
||||
|
||||
```go
|
||||
// HTTP handler accessing the url routing parameters.
|
||||
func MyRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "userID") // from a route like /users/{userID}
|
||||
|
||||
ctx := r.Context()
|
||||
key := ctx.Value("key").(string)
|
||||
|
||||
w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key)))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Middlewares
|
||||
|
||||
chi comes equipped with an optional `middleware` package, providing a suite of standard
|
||||
`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible
|
||||
with `net/http` can be used with chi's mux.
|
||||
|
||||
### Core middlewares
|
||||
|
||||
-----------------------------------------------------------------------------------------------------------
|
||||
| chi/middleware Handler | description |
|
||||
|:----------------------|:---------------------------------------------------------------------------------
|
||||
| AllowContentType | Explicit whitelist of accepted request Content-Types |
|
||||
| Compress | Gzip compression for clients that accept compressed responses |
|
||||
| GetHead | Automatically route undefined HEAD requests to GET handlers |
|
||||
| Heartbeat | Monitoring endpoint to check the servers pulse |
|
||||
| Logger | Logs the start and end of each request with the elapsed processing time |
|
||||
| NoCache | Sets response headers to prevent clients from caching |
|
||||
| Profiler | Easily attach net/http/pprof to your routers |
|
||||
| RealIP | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP |
|
||||
| Recoverer | Gracefully absorb panics and prints the stack trace |
|
||||
| RequestID | Injects a request ID into the context of each request |
|
||||
| RedirectSlashes | Redirect slashes on routing paths |
|
||||
| SetHeader | Short-hand middleware to set a response header key/value |
|
||||
| StripSlashes | Strip slashes on routing paths |
|
||||
| Throttle | Puts a ceiling on the number of concurrent requests |
|
||||
| Timeout | Signals to the request context when the timeout deadline is reached |
|
||||
| URLFormat | Parse extension from url and put it on request context |
|
||||
| WithValue | Short-hand middleware to set a key/value on the request context |
|
||||
-----------------------------------------------------------------------------------------------------------
|
||||
|
||||
### Auxiliary middlewares & packages
|
||||
|
||||
Please see https://github.com/go-chi for additional packages.
|
||||
|
||||
--------------------------------------------------------------------------------------------------------------------
|
||||
| package | description |
|
||||
|:---------------------------------------------------|:-------------------------------------------------------------
|
||||
| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) |
|
||||
| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime |
|
||||
| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication |
|
||||
| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing |
|
||||
| [httpcoala](https://github.com/go-chi/httpcoala) | HTTP request coalescer |
|
||||
| [chi-authz](https://github.com/casbin/chi-authz) | Request ACL via https://github.com/hsluoyz/casbin |
|
||||
| [phi](https://github.com/fate-lovely/phi) | Port chi to [fasthttp](https://github.com/valyala/fasthttp) |
|
||||
--------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
please [submit a PR](./CONTRIBUTING.md) if you'd like to include a link to a chi-compatible middleware
|
||||
|
||||
|
||||
## context?
|
||||
|
||||
`context` is a tiny pkg that provides simple interface to signal context across call stacks
|
||||
and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani)
|
||||
and is available in stdlib since go1.7.
|
||||
|
||||
Learn more at https://blog.golang.org/context
|
||||
|
||||
and..
|
||||
* Docs: https://golang.org/pkg/context
|
||||
* Source: https://github.com/golang/go/tree/master/src/context
|
||||
|
||||
|
||||
## Benchmarks
|
||||
|
||||
The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark
|
||||
|
||||
Results as of Jan 9, 2019 with Go 1.11.4 on Linux X1 Carbon laptop
|
||||
|
||||
```shell
|
||||
BenchmarkChi_Param 3000000 475 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_Param5 2000000 696 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_Param20 1000000 1275 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_ParamWrite 3000000 505 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GithubStatic 3000000 508 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GithubParam 2000000 669 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GithubAll 10000 134627 ns/op 87699 B/op 609 allocs/op
|
||||
BenchmarkChi_GPlusStatic 3000000 402 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GPlusParam 3000000 500 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GPlus2Params 3000000 586 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GPlusAll 200000 7237 ns/op 5616 B/op 39 allocs/op
|
||||
BenchmarkChi_ParseStatic 3000000 408 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_ParseParam 3000000 488 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_Parse2Params 3000000 551 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_ParseAll 100000 13508 ns/op 11232 B/op 78 allocs/op
|
||||
BenchmarkChi_StaticAll 20000 81933 ns/op 67826 B/op 471 allocs/op
|
||||
```
|
||||
|
||||
Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc
|
||||
|
||||
NOTE: the allocs in the benchmark above are from the calls to http.Request's
|
||||
`WithContext(context.Context)` method that clones the http.Request, sets the `Context()`
|
||||
on the duplicated (alloc'd) request and returns it the new request object. This is just
|
||||
how setting context on a request in Go works.
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
* Carl Jackson for https://github.com/zenazn/goji
|
||||
* Parts of chi's thinking comes from goji, and chi's middleware package
|
||||
sources from goji.
|
||||
* Armon Dadgar for https://github.com/armon/go-radix
|
||||
* Contributions: [@VojtechVitek](https://github.com/VojtechVitek)
|
||||
|
||||
We'll be more than happy to see [your contributions](./CONTRIBUTING.md)!
|
||||
|
||||
|
||||
## Beyond REST
|
||||
|
||||
chi is just a http router that lets you decompose request handling into many smaller layers.
|
||||
Many companies including Pressly.com (of course) use chi to write REST services for their public
|
||||
APIs. But, REST is just a convention for managing state via HTTP, and there's a lot of other pieces
|
||||
required to write a complete client-server system or network of microservices.
|
||||
|
||||
Looking ahead beyond REST, I also recommend some newer works in the field coming from
|
||||
[gRPC](https://github.com/grpc/grpc-go), [NATS](https://nats.io), [go-kit](https://github.com/go-kit/kit)
|
||||
and even [graphql](https://github.com/graphql-go/graphql). They're all pretty cool with their
|
||||
own unique approaches and benefits. Specifically, I'd look at gRPC since it makes client-server
|
||||
communication feel like a single program on a single computer, no need to hand-write a client library
|
||||
and the request/response payloads are typed contracts. NATS is pretty amazing too as a super
|
||||
fast and lightweight pub-sub transport that can speak protobufs, with nice service discovery -
|
||||
an excellent combination with gRPC.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka)
|
||||
|
||||
Licensed under [MIT License](./LICENSE)
|
||||
|
||||
[GoDoc]: https://godoc.org/github.com/go-chi/chi
|
||||
[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg
|
||||
[Travis]: https://travis-ci.org/go-chi/chi
|
||||
[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master
|
49
vendor/github.com/go-chi/chi/chain.go
generated
vendored
Normal file
49
vendor/github.com/go-chi/chi/chain.go
generated
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
package chi
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Chain returns a Middlewares type from a slice of middleware handlers.
|
||||
func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares {
|
||||
return Middlewares(middlewares)
|
||||
}
|
||||
|
||||
// Handler builds and returns a http.Handler from the chain of middlewares,
|
||||
// with `h http.Handler` as the final handler.
|
||||
func (mws Middlewares) Handler(h http.Handler) http.Handler {
|
||||
return &ChainHandler{mws, h, chain(mws, h)}
|
||||
}
|
||||
|
||||
// HandlerFunc builds and returns a http.Handler from the chain of middlewares,
|
||||
// with `h http.Handler` as the final handler.
|
||||
func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler {
|
||||
return &ChainHandler{mws, h, chain(mws, h)}
|
||||
}
|
||||
|
||||
// ChainHandler is a http.Handler with support for handler composition and
|
||||
// execution.
|
||||
type ChainHandler struct {
|
||||
Middlewares Middlewares
|
||||
Endpoint http.Handler
|
||||
chain http.Handler
|
||||
}
|
||||
|
||||
func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c.chain.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// chain builds a http.Handler composed of an inline middleware stack and endpoint
|
||||
// handler in the order they are passed.
|
||||
func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler {
|
||||
// Return ahead of time if there aren't any middlewares for the chain
|
||||
if len(middlewares) == 0 {
|
||||
return endpoint
|
||||
}
|
||||
|
||||
// Wrap the end handler with the middleware chain
|
||||
h := middlewares[len(middlewares)-1](endpoint)
|
||||
for i := len(middlewares) - 2; i >= 0; i-- {
|
||||
h = middlewares[i](h)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
134
vendor/github.com/go-chi/chi/chi.go
generated
vendored
Normal file
134
vendor/github.com/go-chi/chi/chi.go
generated
vendored
Normal file
|
@ -0,0 +1,134 @@
|
|||
//
|
||||
// Package chi is a small, idiomatic and composable router for building HTTP services.
|
||||
//
|
||||
// chi requires Go 1.7 or newer.
|
||||
//
|
||||
// Example:
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "net/http"
|
||||
//
|
||||
// "github.com/go-chi/chi"
|
||||
// "github.com/go-chi/chi/middleware"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
// r := chi.NewRouter()
|
||||
// r.Use(middleware.Logger)
|
||||
// r.Use(middleware.Recoverer)
|
||||
//
|
||||
// r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// w.Write([]byte("root."))
|
||||
// })
|
||||
//
|
||||
// http.ListenAndServe(":3333", r)
|
||||
// }
|
||||
//
|
||||
// See github.com/go-chi/chi/_examples/ for more in-depth examples.
|
||||
//
|
||||
// URL patterns allow for easy matching of path components in HTTP
|
||||
// requests. The matching components can then be accessed using
|
||||
// chi.URLParam(). All patterns must begin with a slash.
|
||||
//
|
||||
// A simple named placeholder {name} matches any sequence of characters
|
||||
// up to the next / or the end of the URL. Trailing slashes on paths must
|
||||
// be handled explicitly.
|
||||
//
|
||||
// A placeholder with a name followed by a colon allows a regular
|
||||
// expression match, for example {number:\\d+}. The regular expression
|
||||
// syntax is Go's normal regexp RE2 syntax, except that regular expressions
|
||||
// including { or } are not supported, and / will never be
|
||||
// matched. An anonymous regexp pattern is allowed, using an empty string
|
||||
// before the colon in the placeholder, such as {:\\d+}
|
||||
//
|
||||
// The special placeholder of asterisk matches the rest of the requested
|
||||
// URL. Any trailing characters in the pattern are ignored. This is the only
|
||||
// placeholder which will match / characters.
|
||||
//
|
||||
// Examples:
|
||||
// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/"
|
||||
// "/user/{name}/info" matches "/user/jsmith/info"
|
||||
// "/page/*" matches "/page/intro/latest"
|
||||
// "/page/*/index" also matches "/page/intro/latest"
|
||||
// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01"
|
||||
//
|
||||
package chi
|
||||
|
||||
import "net/http"
|
||||
|
||||
// NewRouter returns a new Mux object that implements the Router interface.
|
||||
func NewRouter() *Mux {
|
||||
return NewMux()
|
||||
}
|
||||
|
||||
// Router consisting of the core routing methods used by chi's Mux,
|
||||
// using only the standard net/http.
|
||||
type Router interface {
|
||||
http.Handler
|
||||
Routes
|
||||
|
||||
// Use appends one of more middlewares onto the Router stack.
|
||||
Use(middlewares ...func(http.Handler) http.Handler)
|
||||
|
||||
// With adds inline middlewares for an endpoint handler.
|
||||
With(middlewares ...func(http.Handler) http.Handler) Router
|
||||
|
||||
// Group adds a new inline-Router along the current routing
|
||||
// path, with a fresh middleware stack for the inline-Router.
|
||||
Group(fn func(r Router)) Router
|
||||
|
||||
// Route mounts a sub-Router along a `pattern`` string.
|
||||
Route(pattern string, fn func(r Router)) Router
|
||||
|
||||
// Mount attaches another http.Handler along ./pattern/*
|
||||
Mount(pattern string, h http.Handler)
|
||||
|
||||
// Handle and HandleFunc adds routes for `pattern` that matches
|
||||
// all HTTP methods.
|
||||
Handle(pattern string, h http.Handler)
|
||||
HandleFunc(pattern string, h http.HandlerFunc)
|
||||
|
||||
// Method and MethodFunc adds routes for `pattern` that matches
|
||||
// the `method` HTTP method.
|
||||
Method(method, pattern string, h http.Handler)
|
||||
MethodFunc(method, pattern string, h http.HandlerFunc)
|
||||
|
||||
// HTTP-method routing along `pattern`
|
||||
Connect(pattern string, h http.HandlerFunc)
|
||||
Delete(pattern string, h http.HandlerFunc)
|
||||
Get(pattern string, h http.HandlerFunc)
|
||||
Head(pattern string, h http.HandlerFunc)
|
||||
Options(pattern string, h http.HandlerFunc)
|
||||
Patch(pattern string, h http.HandlerFunc)
|
||||
Post(pattern string, h http.HandlerFunc)
|
||||
Put(pattern string, h http.HandlerFunc)
|
||||
Trace(pattern string, h http.HandlerFunc)
|
||||
|
||||
// NotFound defines a handler to respond whenever a route could
|
||||
// not be found.
|
||||
NotFound(h http.HandlerFunc)
|
||||
|
||||
// MethodNotAllowed defines a handler to respond whenever a method is
|
||||
// not allowed.
|
||||
MethodNotAllowed(h http.HandlerFunc)
|
||||
}
|
||||
|
||||
// Routes interface adds two methods for router traversal, which is also
|
||||
// used by the `docgen` subpackage to generation documentation for Routers.
|
||||
type Routes interface {
|
||||
// Routes returns the routing tree in an easily traversable structure.
|
||||
Routes() []Route
|
||||
|
||||
// Middlewares returns the list of middlewares in use by the router.
|
||||
Middlewares() Middlewares
|
||||
|
||||
// Match searches the routing tree for a handler that matches
|
||||
// the method/path - similar to routing a http request, but without
|
||||
// executing the handler thereafter.
|
||||
Match(rctx *Context, method, path string) bool
|
||||
}
|
||||
|
||||
// Middlewares type is a slice of standard middleware handlers with methods
|
||||
// to compose middleware chains and http.Handler's.
|
||||
type Middlewares []func(http.Handler) http.Handler
|
161
vendor/github.com/go-chi/chi/context.go
generated
vendored
Normal file
161
vendor/github.com/go-chi/chi/context.go
generated
vendored
Normal file
|
@ -0,0 +1,161 @@
|
|||
package chi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// RouteCtxKey is the context.Context key to store the request context.
|
||||
RouteCtxKey = &contextKey{"RouteContext"}
|
||||
)
|
||||
|
||||
// Context is the default routing context set on the root node of a
|
||||
// request context to track route patterns, URL parameters and
|
||||
// an optional routing path.
|
||||
type Context struct {
|
||||
Routes Routes
|
||||
|
||||
// Routing path/method override used during the route search.
|
||||
// See Mux#routeHTTP method.
|
||||
RoutePath string
|
||||
RouteMethod string
|
||||
|
||||
// Routing pattern stack throughout the lifecycle of the request,
|
||||
// across all connected routers. It is a record of all matching
|
||||
// patterns across a stack of sub-routers.
|
||||
RoutePatterns []string
|
||||
|
||||
// URLParams are the stack of routeParams captured during the
|
||||
// routing lifecycle across a stack of sub-routers.
|
||||
URLParams RouteParams
|
||||
|
||||
// The endpoint routing pattern that matched the request URI path
|
||||
// or `RoutePath` of the current sub-router. This value will update
|
||||
// during the lifecycle of a request passing through a stack of
|
||||
// sub-routers.
|
||||
routePattern string
|
||||
|
||||
// Route parameters matched for the current sub-router. It is
|
||||
// intentionally unexported so it cant be tampered.
|
||||
routeParams RouteParams
|
||||
|
||||
// methodNotAllowed hint
|
||||
methodNotAllowed bool
|
||||
}
|
||||
|
||||
// NewRouteContext returns a new routing Context object.
|
||||
func NewRouteContext() *Context {
|
||||
return &Context{}
|
||||
}
|
||||
|
||||
// Reset a routing context to its initial state.
|
||||
func (x *Context) Reset() {
|
||||
x.Routes = nil
|
||||
x.RoutePath = ""
|
||||
x.RouteMethod = ""
|
||||
x.RoutePatterns = x.RoutePatterns[:0]
|
||||
x.URLParams.Keys = x.URLParams.Keys[:0]
|
||||
x.URLParams.Values = x.URLParams.Values[:0]
|
||||
|
||||
x.routePattern = ""
|
||||
x.routeParams.Keys = x.routeParams.Keys[:0]
|
||||
x.routeParams.Values = x.routeParams.Values[:0]
|
||||
x.methodNotAllowed = false
|
||||
}
|
||||
|
||||
// URLParam returns the corresponding URL parameter value from the request
|
||||
// routing context.
|
||||
func (x *Context) URLParam(key string) string {
|
||||
for k := len(x.URLParams.Keys) - 1; k >= 0; k-- {
|
||||
if x.URLParams.Keys[k] == key {
|
||||
return x.URLParams.Values[k]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RoutePattern builds the routing pattern string for the particular
|
||||
// request, at the particular point during routing. This means, the value
|
||||
// will change throughout the execution of a request in a router. That is
|
||||
// why its advised to only use this value after calling the next handler.
|
||||
//
|
||||
// For example,
|
||||
//
|
||||
// func Instrument(next http.Handler) http.Handler {
|
||||
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// next.ServeHTTP(w, r)
|
||||
// routePattern := chi.RouteContext(r.Context()).RoutePattern()
|
||||
// measure(w, r, routePattern)
|
||||
// })
|
||||
// }
|
||||
func (x *Context) RoutePattern() string {
|
||||
routePattern := strings.Join(x.RoutePatterns, "")
|
||||
return strings.Replace(routePattern, "/*/", "/", -1)
|
||||
}
|
||||
|
||||
// RouteContext returns chi's routing Context object from a
|
||||
// http.Request Context.
|
||||
func RouteContext(ctx context.Context) *Context {
|
||||
return ctx.Value(RouteCtxKey).(*Context)
|
||||
}
|
||||
|
||||
// URLParam returns the url parameter from a http.Request object.
|
||||
func URLParam(r *http.Request, key string) string {
|
||||
if rctx := RouteContext(r.Context()); rctx != nil {
|
||||
return rctx.URLParam(key)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// URLParamFromCtx returns the url parameter from a http.Request Context.
|
||||
func URLParamFromCtx(ctx context.Context, key string) string {
|
||||
if rctx := RouteContext(ctx); rctx != nil {
|
||||
return rctx.URLParam(key)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RouteParams is a structure to track URL routing parameters efficiently.
|
||||
type RouteParams struct {
|
||||
Keys, Values []string
|
||||
}
|
||||
|
||||
// Add will append a URL parameter to the end of the route param
|
||||
func (s *RouteParams) Add(key, value string) {
|
||||
(*s).Keys = append((*s).Keys, key)
|
||||
(*s).Values = append((*s).Values, value)
|
||||
}
|
||||
|
||||
// ServerBaseContext wraps an http.Handler to set the request context to the
|
||||
// `baseCtx`.
|
||||
func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler {
|
||||
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
baseCtx := baseCtx
|
||||
|
||||
// Copy over default net/http server context keys
|
||||
if v, ok := ctx.Value(http.ServerContextKey).(*http.Server); ok {
|
||||
baseCtx = context.WithValue(baseCtx, http.ServerContextKey, v)
|
||||
}
|
||||
if v, ok := ctx.Value(http.LocalAddrContextKey).(net.Addr); ok {
|
||||
baseCtx = context.WithValue(baseCtx, http.LocalAddrContextKey, v)
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r.WithContext(baseCtx))
|
||||
})
|
||||
return fn
|
||||
}
|
||||
|
||||
// contextKey is a value for use with context.WithValue. It's used as
|
||||
// a pointer so it fits in an interface{} without allocation. This technique
|
||||
// for defining context keys was copied from Go 1.7's new use of context in net/http.
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (k *contextKey) String() string {
|
||||
return "chi context value " + k.name
|
||||
}
|
275
vendor/github.com/go-chi/chi/middleware/compress.go
generated
vendored
Normal file
275
vendor/github.com/go-chi/chi/middleware/compress.go
generated
vendored
Normal file
|
@ -0,0 +1,275 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/flate"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var encoders = map[string]EncoderFunc{}
|
||||
|
||||
var encodingPrecedence = []string{"br", "gzip", "deflate"}
|
||||
|
||||
func init() {
|
||||
// TODO:
|
||||
// lzma: Opera.
|
||||
// sdch: Chrome, Android. Gzip output + dictionary header.
|
||||
// br: Brotli, see https://github.com/go-chi/chi/pull/326
|
||||
|
||||
// TODO: Exception for old MSIE browsers that can't handle non-HTML?
|
||||
// https://zoompf.com/blog/2012/02/lose-the-wait-http-compression
|
||||
SetEncoder("gzip", encoderGzip)
|
||||
|
||||
// HTTP 1.1 "deflate" (RFC 2616) stands for DEFLATE data (RFC 1951)
|
||||
// wrapped with zlib (RFC 1950). The zlib wrapper uses Adler-32
|
||||
// checksum compared to CRC-32 used in "gzip" and thus is faster.
|
||||
//
|
||||
// But.. some old browsers (MSIE, Safari 5.1) incorrectly expect
|
||||
// raw DEFLATE data only, without the mentioned zlib wrapper.
|
||||
// Because of this major confusion, most modern browsers try it
|
||||
// both ways, first looking for zlib headers.
|
||||
// Quote by Mark Adler: http://stackoverflow.com/a/9186091/385548
|
||||
//
|
||||
// The list of browsers having problems is quite big, see:
|
||||
// http://zoompf.com/blog/2012/02/lose-the-wait-http-compression
|
||||
// https://web.archive.org/web/20120321182910/http://www.vervestudios.co/projects/compression-tests/results
|
||||
//
|
||||
// That's why we prefer gzip over deflate. It's just more reliable
|
||||
// and not significantly slower than gzip.
|
||||
SetEncoder("deflate", encoderDeflate)
|
||||
|
||||
// NOTE: Not implemented, intentionally:
|
||||
// case "compress": // LZW. Deprecated.
|
||||
// case "bzip2": // Too slow on-the-fly.
|
||||
// case "zopfli": // Too slow on-the-fly.
|
||||
// case "xz": // Too slow on-the-fly.
|
||||
}
|
||||
|
||||
// An EncoderFunc is a function that wraps the provided ResponseWriter with a
|
||||
// streaming compression algorithm and returns it.
|
||||
//
|
||||
// In case of failure, the function should return nil.
|
||||
type EncoderFunc func(w http.ResponseWriter, level int) io.Writer
|
||||
|
||||
// SetEncoder can be used to set the implementation of a compression algorithm.
|
||||
//
|
||||
// The encoding should be a standardised identifier. See:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
|
||||
//
|
||||
// For example, add the Brotli algortithm:
|
||||
//
|
||||
// import brotli_enc "gopkg.in/kothar/brotli-go.v0/enc"
|
||||
//
|
||||
// middleware.SetEncoder("br", func(w http.ResponseWriter, level int) io.Writer {
|
||||
// params := brotli_enc.NewBrotliParams()
|
||||
// params.SetQuality(level)
|
||||
// return brotli_enc.NewBrotliWriter(params, w)
|
||||
// })
|
||||
func SetEncoder(encoding string, fn EncoderFunc) {
|
||||
encoding = strings.ToLower(encoding)
|
||||
if encoding == "" {
|
||||
panic("the encoding can not be empty")
|
||||
}
|
||||
if fn == nil {
|
||||
panic("attempted to set a nil encoder function")
|
||||
}
|
||||
encoders[encoding] = fn
|
||||
|
||||
var e string
|
||||
for _, v := range encodingPrecedence {
|
||||
if v == encoding {
|
||||
e = v
|
||||
}
|
||||
}
|
||||
|
||||
if e == "" {
|
||||
encodingPrecedence = append([]string{e}, encodingPrecedence...)
|
||||
}
|
||||
}
|
||||
|
||||
var defaultContentTypes = map[string]struct{}{
|
||||
"text/html": {},
|
||||
"text/css": {},
|
||||
"text/plain": {},
|
||||
"text/javascript": {},
|
||||
"application/javascript": {},
|
||||
"application/x-javascript": {},
|
||||
"application/json": {},
|
||||
"application/atom+xml": {},
|
||||
"application/rss+xml": {},
|
||||
"image/svg+xml": {},
|
||||
}
|
||||
|
||||
// DefaultCompress is a middleware that compresses response
|
||||
// body of predefined content types to a data format based
|
||||
// on Accept-Encoding request header. It uses a default
|
||||
// compression level.
|
||||
func DefaultCompress(next http.Handler) http.Handler {
|
||||
return Compress(flate.DefaultCompression)(next)
|
||||
}
|
||||
|
||||
// Compress is a middleware that compresses response
|
||||
// body of a given content types to a data format based
|
||||
// on Accept-Encoding request header. It uses a given
|
||||
// compression level.
|
||||
//
|
||||
// NOTE: make sure to set the Content-Type header on your response
|
||||
// otherwise this middleware will not compress the response body. For ex, in
|
||||
// your handler you should set w.Header().Set("Content-Type", http.DetectContentType(yourBody))
|
||||
// or set it manually.
|
||||
func Compress(level int, types ...string) func(next http.Handler) http.Handler {
|
||||
contentTypes := defaultContentTypes
|
||||
if len(types) > 0 {
|
||||
contentTypes = make(map[string]struct{}, len(types))
|
||||
for _, t := range types {
|
||||
contentTypes[t] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
encoder, encoding := selectEncoder(r.Header)
|
||||
|
||||
cw := &compressResponseWriter{
|
||||
ResponseWriter: w,
|
||||
w: w,
|
||||
contentTypes: contentTypes,
|
||||
encoder: encoder,
|
||||
encoding: encoding,
|
||||
level: level,
|
||||
}
|
||||
defer cw.Close()
|
||||
|
||||
next.ServeHTTP(cw, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func selectEncoder(h http.Header) (EncoderFunc, string) {
|
||||
header := h.Get("Accept-Encoding")
|
||||
|
||||
// Parse the names of all accepted algorithms from the header.
|
||||
accepted := strings.Split(strings.ToLower(header), ",")
|
||||
|
||||
// Find supported encoder by accepted list by precedence
|
||||
for _, name := range encodingPrecedence {
|
||||
if fn, ok := encoders[name]; ok && matchAcceptEncoding(accepted, name) {
|
||||
return fn, name
|
||||
}
|
||||
}
|
||||
|
||||
// No encoder found to match the accepted encoding
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
func matchAcceptEncoding(accepted []string, encoding string) bool {
|
||||
for _, v := range accepted {
|
||||
if strings.Index(v, encoding) >= 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type compressResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
w io.Writer
|
||||
encoder EncoderFunc
|
||||
encoding string
|
||||
contentTypes map[string]struct{}
|
||||
level int
|
||||
wroteHeader bool
|
||||
}
|
||||
|
||||
func (cw *compressResponseWriter) WriteHeader(code int) {
|
||||
if cw.wroteHeader {
|
||||
return
|
||||
}
|
||||
cw.wroteHeader = true
|
||||
defer cw.ResponseWriter.WriteHeader(code)
|
||||
|
||||
// Already compressed data?
|
||||
if cw.Header().Get("Content-Encoding") != "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the first part of the Content-Type response header.
|
||||
contentType := ""
|
||||
parts := strings.Split(cw.Header().Get("Content-Type"), ";")
|
||||
if len(parts) > 0 {
|
||||
contentType = parts[0]
|
||||
}
|
||||
|
||||
// Is the content type compressable?
|
||||
if _, ok := cw.contentTypes[contentType]; !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cw.encoder != nil && cw.encoding != "" {
|
||||
if wr := cw.encoder(cw.ResponseWriter, cw.level); wr != nil {
|
||||
cw.w = wr
|
||||
cw.Header().Set("Content-Encoding", cw.encoding)
|
||||
|
||||
// The content-length after compression is unknown
|
||||
cw.Header().Del("Content-Length")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cw *compressResponseWriter) Write(p []byte) (int, error) {
|
||||
if !cw.wroteHeader {
|
||||
cw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
return cw.w.Write(p)
|
||||
}
|
||||
|
||||
func (cw *compressResponseWriter) Flush() {
|
||||
if f, ok := cw.w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (cw *compressResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := cw.w.(http.Hijacker); ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
return nil, nil, errors.New("chi/middleware: http.Hijacker is unavailable on the writer")
|
||||
}
|
||||
|
||||
func (cw *compressResponseWriter) Push(target string, opts *http.PushOptions) error {
|
||||
if ps, ok := cw.w.(http.Pusher); ok {
|
||||
return ps.Push(target, opts)
|
||||
}
|
||||
return errors.New("chi/middleware: http.Pusher is unavailable on the writer")
|
||||
}
|
||||
|
||||
func (cw *compressResponseWriter) Close() error {
|
||||
if c, ok := cw.w.(io.WriteCloser); ok {
|
||||
return c.Close()
|
||||
}
|
||||
return errors.New("chi/middleware: io.WriteCloser is unavailable on the writer")
|
||||
}
|
||||
|
||||
func encoderGzip(w http.ResponseWriter, level int) io.Writer {
|
||||
gw, err := gzip.NewWriterLevel(w, level)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return gw
|
||||
}
|
||||
|
||||
func encoderDeflate(w http.ResponseWriter, level int) io.Writer {
|
||||
dw, err := flate.NewWriter(w, level)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return dw
|
||||
}
|
51
vendor/github.com/go-chi/chi/middleware/content_charset.go
generated
vendored
Normal file
51
vendor/github.com/go-chi/chi/middleware/content_charset.go
generated
vendored
Normal file
|
@ -0,0 +1,51 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ContentCharset generates a handler that writes a 415 Unsupported Media Type response if none of the charsets match.
|
||||
// An empty charset will allow requests with no Content-Type header or no specified charset.
|
||||
func ContentCharset(charsets ...string) func(next http.Handler) http.Handler {
|
||||
for i, c := range charsets {
|
||||
charsets[i] = strings.ToLower(c)
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !contentEncoding(r.Header.Get("Content-Type"), charsets...) {
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check the content encoding against a list of acceptable values.
|
||||
func contentEncoding(ce string, charsets ...string) bool {
|
||||
_, ce = split(strings.ToLower(ce), ";")
|
||||
_, ce = split(ce, "charset=")
|
||||
ce, _ = split(ce, ";")
|
||||
for _, c := range charsets {
|
||||
if ce == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Split a string in two parts, cleaning any whitespace.
|
||||
func split(str, sep string) (string, string) {
|
||||
var a, b string
|
||||
var parts = strings.SplitN(str, sep, 2)
|
||||
a = strings.TrimSpace(parts[0])
|
||||
if len(parts) == 2 {
|
||||
b = strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
return a, b
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue