mirror of
https://github.com/Threnklyn/jira.git
synced 2026-05-19 20:53:27 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 009103e6e4 | |||
| 8922d89f74 | |||
| 10661e2fc4 |
@@ -3,7 +3,6 @@ config:
|
||||
password-source: pass
|
||||
endpoint: https://go-jira.atlassian.net
|
||||
user: admin
|
||||
login: atlassian@corybennett.org
|
||||
|
||||
queries:
|
||||
todo: |
|
||||
|
||||
@@ -1,40 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.20 - 2018-08-04
|
||||
|
||||
* [[#201](https://github.com/Netflix-Skunkworks/go-jira/issues/201)] update required library, failing to populate cookiejar from cookies file [Cory Bennett] [[ee69afa](https://github.com/Netflix-Skunkworks/go-jira/commit/ee69afa)]
|
||||
|
||||
## 1.0.19 - 2018-08-02
|
||||
|
||||
* [[#199](https://github.com/Netflix-Skunkworks/go-jira/issues/199)] [[#198](https://github.com/Netflix-Skunkworks/go-jira/issues/198)] update http client library, fix issues with internal login retries [Cory Bennett] [[0cba806](https://github.com/Netflix-Skunkworks/go-jira/commit/0cba806)]
|
||||
|
||||
## 1.0.18 - 2018-07-29
|
||||
|
||||
* [[#196](https://github.com/Netflix-Skunkworks/go-jira/issues/196)] add `jira session` command to show session information if user is authenticated [Cory Bennett] [[f592107](https://github.com/Netflix-Skunkworks/go-jira/commit/f592107)]
|
||||
* [[#192](https://github.com/Netflix-Skunkworks/go-jira/issues/192)] fix template usage for 'fixVersions' in transition template [Cory Bennett] [[c9a4e30](https://github.com/Netflix-Skunkworks/go-jira/commit/c9a4e30)]
|
||||
* move HandleExit to the jiracli package [Cory Bennett] [[ef9b731](https://github.com/Netflix-Skunkworks/go-jira/commit/ef9b731)]
|
||||
* they broke golang.org/x/net: ERROR: vendor/golang.org/x/net/proxy/socks5.go:11:2: use of internal package not allowed [Cory Bennett] [[7191c77](https://github.com/Netflix-Skunkworks/go-jira/commit/7191c77)]
|
||||
* udpate deps [Cory Bennett] [[d16bcc2](https://github.com/Netflix-Skunkworks/go-jira/commit/d16bcc2)]
|
||||
* udpate for figtree api changes [Cory Bennett] [[07ba74b](https://github.com/Netflix-Skunkworks/go-jira/commit/07ba74b)]
|
||||
* udpate to use latest dep, update figtree [Cory Bennett] [[462ef1c](https://github.com/Netflix-Skunkworks/go-jira/commit/462ef1c)]
|
||||
* [[#171](https://github.com/Netflix-Skunkworks/go-jira/issues/171)] change proposed PasswordPath to PasswordName [Cory Bennett] [[213a7e0](https://github.com/Netflix-Skunkworks/go-jira/commit/213a7e0)]
|
||||
* add pass path to config [dvogt23] [[fa01ff5](https://github.com/Netflix-Skunkworks/go-jira/commit/fa01ff5)]
|
||||
|
||||
## 1.0.17 - 2018-04-15
|
||||
|
||||
* fix IsTerminal usage for windows [Cory Bennett] [[7f9595c](https://github.com/Netflix-Skunkworks/go-jira/commit/7f9595c)]
|
||||
* [[#166](https://github.com/Netflix-Skunkworks/go-jira/issues/166)] fix issue when editing templates specified with full path [Cory Bennett] [[d787ac0](https://github.com/Netflix-Skunkworks/go-jira/commit/d787ac0)]
|
||||
* only prompt on logout if stdin and stdout are terminals [Cory Bennett] [[09a61c3](https://github.com/Netflix-Skunkworks/go-jira/commit/09a61c3)]
|
||||
* [[#163](https://github.com/Netflix-Skunkworks/go-jira/issues/163)] fix url path join logic [Cory Bennett] [[9146346](https://github.com/Netflix-Skunkworks/go-jira/commit/9146346)]
|
||||
* [[#160](https://github.com/Netflix-Skunkworks/go-jira/issues/160)] prompt when api-token is invalid to get new token [Cory Bennett] [[e639cce](https://github.com/Netflix-Skunkworks/go-jira/commit/e639cce)]
|
||||
* [[#157](https://github.com/Netflix-Skunkworks/go-jira/issues/157)] add `password-directory: path` to allow overriding PASSWORD_STORE_DIR from configs [Cory Bennett] [[06b26c9](https://github.com/Netflix-Skunkworks/go-jira/commit/06b26c9)]
|
||||
* [[#160](https://github.com/Netflix-Skunkworks/go-jira/issues/160)] allow `jira logout` to delete your api-token from keychain [Cory Bennett] [[bd3cf99](https://github.com/Netflix-Skunkworks/go-jira/commit/bd3cf99)]
|
||||
|
||||
## 1.0.16 - 2018-04-01
|
||||
|
||||
* [[#159](https://github.com/Netflix-Skunkworks/go-jira/issues/159)] fix `slice bounds out of range` error in `abbrev` template function [Cory Bennett] [[359bec2](https://github.com/Netflix-Skunkworks/go-jira/commit/359bec2)]
|
||||
* [[#158](https://github.com/Netflix-Skunkworks/go-jira/issues/158)] always print usage to stdout [Cory Bennett] [[79c83f6](https://github.com/Netflix-Skunkworks/go-jira/commit/79c83f6)]
|
||||
|
||||
## 1.0.15 - 2018-03-08
|
||||
|
||||
* [[#147](https://github.com/Netflix-Skunkworks/go-jira/issues/147)] [[#148](https://github.com/Netflix-Skunkworks/go-jira/issues/148)] add support for api token based authentication [Cory Bennett] [[edb0662](https://github.com/Netflix-Skunkworks/go-jira/commit/edb0662)]
|
||||
|
||||
Generated
+37
-41
@@ -4,10 +4,7 @@
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/alecthomas/template"
|
||||
packages = [
|
||||
".",
|
||||
"parse"
|
||||
]
|
||||
packages = [".","parse"]
|
||||
revision = "a0175ee3bccc567396460bf5acd36800cb10c49c"
|
||||
|
||||
[[projects]]
|
||||
@@ -20,14 +17,13 @@
|
||||
branch = "master"
|
||||
name = "github.com/cheekybits/genny"
|
||||
packages = ["generic"]
|
||||
revision = "c546fedd85a9b2291805f7a2933a3564cbdda989"
|
||||
source = "github.com/coryb/genny"
|
||||
revision = "9127e812e1e9e501ce899a18121d316ecb52e4ba"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/coryb/figtree"
|
||||
packages = ["."]
|
||||
revision = "071d1ef303dfb7738166ba62aac71e5ee10ce218"
|
||||
revision = "c7d8fbf1d7746b5864b8262fabffec813b5a43fa"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -39,7 +35,7 @@
|
||||
branch = "master"
|
||||
name = "github.com/coryb/oreo"
|
||||
packages = ["."]
|
||||
revision = "3e1b88fc08f134aa91ccfb5d58c983ca8ab42589"
|
||||
revision = "95687d61c95ee1522c1140e2af59b0c1846abfc1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
@@ -51,7 +47,7 @@
|
||||
branch = "master"
|
||||
name = "github.com/fatih/camelcase"
|
||||
packages = ["."]
|
||||
revision = "44e46d280b43ec1531bb25252440e34f1b800b65"
|
||||
revision = "f6a740d52f961c60348ebb109adde9f4635d7540"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -63,25 +59,25 @@
|
||||
branch = "master"
|
||||
name = "github.com/jinzhu/copier"
|
||||
packages = ["."]
|
||||
revision = "7e38e58719c33e0d44d585c4ab477a30f8cb82dd"
|
||||
revision = "32e0d0db1dcd4373fb9eb0f9d727b1fe1a723e54"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kballard/go-shellquote"
|
||||
packages = ["."]
|
||||
revision = "95032a82bc518f77982ea72343cc1ade730072f0"
|
||||
revision = "cd60e84ee657ff3dc51de0b4f55dd299a3e136f2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
revision = "ad5389df28cdac544c99bd7b9161a0b5b6ca9d1b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
revision = "fc9e8d8ef48496124e79ae0df75490096eccf6fe"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -108,42 +104,49 @@
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
branch = "master"
|
||||
name = "github.com/sethgrid/pester"
|
||||
packages = ["."]
|
||||
revision = "a86a2d88f4dc3c7dbf3a6a6bbbfb095690b834b6"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
||||
version = "v1.1.4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/theckman/go-flock"
|
||||
packages = ["."]
|
||||
revision = "b139a2487364247d91814e4a7c7b8fdc69e342b2"
|
||||
version = "v0.4.0"
|
||||
revision = "6de226b0d5f040ed85b88c82c381709b98277f3d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tidwall/gjson"
|
||||
packages = ["."]
|
||||
revision = "ba784d767ac7d937cf2439f237e50ec04a381c8b"
|
||||
revision = "be96719f990978a867f52c48f29d43f6b591da28"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tidwall/match"
|
||||
packages = ["."]
|
||||
revision = "1731857f09b1f38450e2c12409748407822dc6be"
|
||||
revision = "173748da739a410c5b0b813b956f89ff94730b4c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tmc/keyring"
|
||||
packages = ["."]
|
||||
revision = "839169085ae146fc7a34bcb34dfd7ab216d23991"
|
||||
revision = "06e6283d50adc5f8fcdb3cdf33ee1244d4400ae1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["ssh/terminal"]
|
||||
revision = "c126467f60eb25f8f27e5a981f32a87e3965053f"
|
||||
revision = "9ba3862cf6a5452ae579de98f9364dd2e544844c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["proxy"]
|
||||
revision = "01c190206fbdffa42f334f4b2bf2220f50e64920"
|
||||
@@ -151,33 +154,26 @@
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "bd9dbc187b6e1dacfdd2722a87e83093c2d7bd6e"
|
||||
packages = ["unix","windows"]
|
||||
revision = "a5054c7c1385fd50d9394475365355a87a7873ec"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/AlecAivazis/survey.v1"
|
||||
packages = [
|
||||
".",
|
||||
"core",
|
||||
"terminal"
|
||||
]
|
||||
revision = "17861e192dc11fd2f5081df1932c94cce262fa1e"
|
||||
version = "v1.6.1"
|
||||
packages = [".","core","terminal"]
|
||||
revision = "9d910423e24aa6d7c7950160658c295e0734c7e0"
|
||||
version = "1.3.1"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/alecthomas/kingpin.v2"
|
||||
packages = ["."]
|
||||
revision = "947dcec5ba9c011838740e680966fd7087a71d0d"
|
||||
version = "v2.2.6"
|
||||
revision = "1087e65c9441605df944fb12c33f0fe7072d18ca"
|
||||
version = "v2.2.5"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/coryb/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "0e40e46f7153ceb79ebbfdd075233d57f9611bd1"
|
||||
revision = "fb7cb9628c6e3bdd76c29fb91798d51a09832470"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/op/go-logging.v1"
|
||||
@@ -188,6 +184,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "e087b3c5e03a82796f3bbc9d67c366dd794718c12f9ef9252e6172a6344c4fd7"
|
||||
inputs-digest = "50016720a1e2509a915e4465a53ffa957f977d2145e831b81d946ef87f7a8f48"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
+3
-18
@@ -20,50 +20,40 @@
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
non-go = true
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coryb/figtree"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coryb/kingpeon"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coryb/oreo"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/jinzhu/copier"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/kballard/go-shellquote"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mgutz/ansi"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/browser"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/savaki/jq"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/tmc/keyring"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/crypto"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/AlecAivazis/survey.v1"
|
||||
@@ -75,7 +65,6 @@
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/coryb/yaml.v2"
|
||||
branch = "v2"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/op/go-logging.v1"
|
||||
@@ -84,7 +73,3 @@
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/tidwall/gjson"
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/net"
|
||||
revision = "01c190206fbdffa42f334f4b2bf2220f50e64920"
|
||||
@@ -1,5 +1,4 @@
|
||||
NAME=jira
|
||||
GO?=go
|
||||
|
||||
OS=$(shell uname -s)
|
||||
ifeq ($(filter CYGWIN%,$(OS)),$(OS))
|
||||
@@ -21,17 +20,17 @@ CURVER ?= $(patsubst v%,%,$(shell [ -d .git ] && git describe --abbrev=0 --tags
|
||||
LDFLAGS:= -w
|
||||
|
||||
build:
|
||||
$(GO) build -gcflags="-e" -v -ldflags "$(LDFLAGS) -s" -o '$(BIN)' cmd/jira/main.go
|
||||
go build -gcflags="-e" -v -ldflags "$(LDFLAGS) -s" -o '$(BIN)' cmd/jira/main.go
|
||||
|
||||
vet:
|
||||
@$(GO) vet .
|
||||
@$(GO) vet ./jiracli
|
||||
@$(GO) vet ./jiracmd
|
||||
@$(GO) vet ./jiradata
|
||||
@$(GO) vet ./cmd/jira
|
||||
@go vet .
|
||||
@go vet ./jiracli
|
||||
@go vet ./jiracmd
|
||||
@go vet ./jiradata
|
||||
@go vet ./cmd/jira
|
||||
|
||||
lint:
|
||||
@$(GO) get github.com/golang/lint/golint
|
||||
@go get github.com/golang/lint/golint
|
||||
@golint .
|
||||
@golint ./jiracli
|
||||
@golint ./jiracmd
|
||||
@@ -39,10 +38,10 @@ lint:
|
||||
@golint ./cmd/jira
|
||||
|
||||
all:
|
||||
$(GO) get -u github.com/karalabe/xgo
|
||||
go get -u github.com/karalabe/xgo
|
||||
rm -rf dist
|
||||
mkdir -p dist
|
||||
xgo --targets="freebsd/amd64,linux/386,linux/amd64,windows/386,windows/amd64,darwin/amd64" -dest ./dist -ldflags="-w -s" ./cmd/jira
|
||||
xgo --go 1.9.0 --targets="freebsd/amd64,linux/386,linux/amd64,windows/386,windows/amd64,darwin/amd64" -dest ./dist -ldflags="-w -s" ./cmd/jira
|
||||
|
||||
install:
|
||||
${MAKE} GOBIN=$$HOME/bin build
|
||||
@@ -66,11 +65,11 @@ update-changelog:
|
||||
mv CHANGELOG.md.new CHANGELOG.md; \
|
||||
$(NULL)
|
||||
|
||||
update-usage:
|
||||
@perl -pi -e 'undef $$/; s|\n```\nusage.*?```|"\n```\n".qx{./jira --help}."```"|esg' README.md
|
||||
|
||||
release:
|
||||
make update-usage
|
||||
perl -pi -e 'undef $$/; s/\n```\nusage.*```//sg' README.md
|
||||
echo '```' >> README.md
|
||||
./jira --help >> README.md 2>&1 || true
|
||||
echo '```' >> README.md
|
||||
git diff --exit-code --quiet README.md || git commit -m "Updated Usage" README.md
|
||||
git commit -m "Updated Changelog" CHANGELOG.md
|
||||
git commit -m "version bump" jira.go
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[](https://gitter.im/go-jira-cli/help?utm_source=badge&utm_medium=badge&utm_content=badge)
|
||||
g[](https://gitter.im/go-jira-cli/help?utm_source=badge&utm_medium=badge&utm_content=badge)
|
||||
[](https://travis-ci.org/Netflix-Skunkworks/go-jira)
|
||||
[](https://godoc.org/gopkg.in/Netflix-Skunkworks/go-jira.v1)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
@@ -9,8 +9,6 @@
|
||||
* [Install](#install)
|
||||
* [Download](#download)
|
||||
* [Build](#build)
|
||||
* [Usage](#usage)
|
||||
* [TAB completion](#setting-up-tab-completion)
|
||||
* [v1 vs v0 changes](#v1-vs-v0-changes)
|
||||
* [<strong>Golang library import</strong>](#golang-library-import)
|
||||
* [<strong>Configs per command</strong>](#configs-per-command)
|
||||
@@ -32,6 +30,7 @@
|
||||
* [user vs login](#user-vs-login)
|
||||
* [keyring password source](#keyring-password-source)
|
||||
* [pass password source](#pass-password-source)
|
||||
* [Usage](#usage)
|
||||
|
||||
# go-jira
|
||||
simple command line client for Atlassian's Jira service written in Go
|
||||
@@ -49,7 +48,7 @@ You can build and install the official repository with [Go](https://golang.org/d
|
||||
```
|
||||
go get gopkg.in/Netflix-Skunkworks/go-jira.v1/cmd/jira
|
||||
```
|
||||
This will checkout this repository into `$GOPATH/src/gopkg.in/Netflix-Skunkworks/go-jira.v1`, build, and install it.
|
||||
This will checkout this repository into `$GOPATH/src/gopkg.in/Netflix-Skunkworks/go-jira.v1`, build, and install it.
|
||||
|
||||
Because golang likes fully qualified import paths, forking and contributing can be a bit tricky.
|
||||
|
||||
@@ -59,23 +58,391 @@ If you want to tinker or hack on go-jira, the [easiest way to do so](http://code
|
||||
|
||||
From within that source dir you can build and install modifications from within that directory like:
|
||||
|
||||
`go install ./...`
|
||||
`go install ./...`
|
||||
|
||||
|
||||
## v1 vs v0 changes
|
||||
|
||||
###### **Golang library import**
|
||||
For the new version of go-jira you should use:
|
||||
```
|
||||
import "gopkg.in/Netflix-Skunkworks/go-jira.v1"
|
||||
```
|
||||
|
||||
If you have code that depends on the old apis, you can still use them with this import:
|
||||
```
|
||||
import "gopkg.in/Netflix-Skunkworks/go-jira.v0"
|
||||
```
|
||||
|
||||
###### **Configs per command**
|
||||
Instead of requiring a exectuable template to get configs for a given command now you can create a config to be applied to a command. So if you want to use `template: table` by default for yor `jira list` you can now do:
|
||||
```
|
||||
$ cat $HOME/.jira.d/list.yml
|
||||
template: table
|
||||
```
|
||||
Where previously you needed something like:
|
||||
```
|
||||
# cat $HOME/.jira.d/config.yml
|
||||
#!/bin/sh
|
||||
case $JIRA_OPERATION in
|
||||
list)
|
||||
echo "template: table";;
|
||||
esac
|
||||
```
|
||||
|
||||
###### **Custom Commands**
|
||||
Now you can create your own custom commands to do common operations with jira. Please see the details **Custom Commands** section below for more details. If you want to create a command `jira mine` that lists all the issues assigned to you now you can modify your `.jira.d/config.yml` file to add a `custom-commands` section like this:
|
||||
```
|
||||
custom-commands:
|
||||
- name: mine
|
||||
help: display issues assigned to me
|
||||
script: |-
|
||||
{{jira}} list --query "resolution = unresolved and assignee=currentuser() ORDER BY created"
|
||||
```
|
||||
Then the next time you run `jira help` you will see your usage:
|
||||
```
|
||||
$ jira mine --help
|
||||
usage: jira mine
|
||||
|
||||
display issues assigned to me
|
||||
|
||||
Flags:
|
||||
--help Show context-sensitive help (also try --help-long and --help-man).
|
||||
-v, --verbose ... Increase verbosity for debugging
|
||||
-e, --endpoint=ENDPOINT Base URI to use for Jira
|
||||
-u, --user=USER Login name used for authentication with Jira service
|
||||
--unixproxy=UNIXPROXY Path for a unix-socket proxy
|
||||
-k, --insecure Disable TLS certificate verification
|
||||
```
|
||||
|
||||
###### **Incompatible command changes**
|
||||
Unfortunately during the rewrite between v0 and v1 there were some necessary changes that broke backwards compatibility with existing commands. Specifically the `dups`, `blocks`, `add worklog` and `add|remove|set labels` commands have had the command word swapped around:
|
||||
* `jira DUPLICATE dups ISSUE` => `jira dup DUPLICATE ISSUE`
|
||||
* `jira BLOCKER blocks ISSUE` => `jira block BLOCKER ISSUE`
|
||||
* `jira add worklog` => `jira worklog add`
|
||||
* `jira add labels` => `jira labels add`
|
||||
* `jira remove labels` => `jira labels remove`
|
||||
* `jira set labels` => `jira labels set`
|
||||
|
||||
###### **Login process change**
|
||||
We have, once again, changed how login happens for Jira. When authenticating against Atlassian Cloud Jira [API Tokens are now required](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/). Please read [the Authentication section](#authentication) below for more information.
|
||||
|
||||
If you use a privately hosted Jira service, you can chose to use the API Token method or continue using the session login api. Please read [the Authentication section](#authentication) below for more information.
|
||||
|
||||
Previously `jira` used attempt to get a `JSESSION` cookies by authenticating with the webservice standard GUI login process. This has been especially problematic as users need to authenticate with various credential providers (google auth, etc). We now attempt to authenticate via the [session login api](https://docs.atlassian.com/jira/REST/cloud/#auth/1/session-login). This may be problematic for users if admins have locked down the session-login api, so we might have to bring back the error-prone Basic-Auth approach. For users that are unable to authenticate via `jira` hopefully someone in your organization can provide me with details on a process for you to authenticate and we can try to update `jira`.
|
||||
|
||||
## Configuration
|
||||
|
||||
**go-jira** uses a configuration hierarchy. When loading the configuration from disk it will recursively look through all parent directories in your current path looking for a **.jira.d** directory. If your current directory is not a child directory of your homedir, then your homedir will also be inspected for a **.jira.d** directory. From all of **.jira.d** directories discovered **go-jira** will load a **<command>.yml** file (ie for `jira list` it will load `.jira.d/list.yml`) then it will merge in any properties from the **config.yml** if found. The configuration properties found in a file closests to your current working directory will have precedence. Properties overriden with command line options will have final precedence.
|
||||
|
||||
The complicated configuration hierarchy is used because **go-jira** attempts to be context aware. For example, if you are working on a "foo" project and you `cd` into your project workspace, wouldn't it be nice if `jira ls` automatically knew to list only issues related to the "foo" project? Likewise when you `cd` to the "bar" project then `jira ls` should only list issues related to "bar" project. You can do this with by creating a configuration under your project workspace at **./.jira.d/config.yml** that looks like:
|
||||
|
||||
```
|
||||
project: foo
|
||||
```
|
||||
|
||||
You will need to specify your local jira endpoint first, typically in your homedir like:
|
||||
|
||||
```bash
|
||||
mkdir ~/.jira.d
|
||||
|
||||
cat <<EOM >~/.jira.d/config.yml
|
||||
endpoint: https://jira.mycompany.com
|
||||
EOM
|
||||
```
|
||||
|
||||
Then use `jira login` to authenticate yourself as $USER. To change your username, use the `-u` CLI flag or set `user:` in your config.yml
|
||||
|
||||
### Dynamic Configuration
|
||||
|
||||
If the **.jira.d/config.yml** file is executable, then **go-jira** will attempt to execute the file and use the stdout for configuration. You can use this to customize templates or other overrides depending on what type of operation you are running. For example if you would like to use the "table" template when ever you run `jira ls`, then you can create a template like this:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
echo "endpoint: https://jira.mycompany.com"
|
||||
echo "editor: emacs -nw"
|
||||
|
||||
case $JIRA_OPERATION in
|
||||
list)
|
||||
echo "template: table";;
|
||||
esac
|
||||
```
|
||||
|
||||
Or if you always set the same overrides when you create an issue for your project you can do something like this:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
echo "project: GOJIRA"
|
||||
|
||||
case $JIRA_OPERATION in
|
||||
create)
|
||||
echo "assignee: $USER"
|
||||
echo "watchers: mothra"
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
### Custom Commands
|
||||
You can now create custom commands for `jira` just by editing your `.jira.d/config.yml` config file. These commands are effectively shell-scripts that can have documented options and arguments. The basic format is like:
|
||||
```
|
||||
custom-commands:
|
||||
- command1
|
||||
- command2
|
||||
```
|
||||
##### Commands
|
||||
Where the individual commands are maps with these keys:
|
||||
* `name: string` [**required**] This is the command name, so for `jira foobar` you would have `name: foobar`
|
||||
* `help: string` This is help message displayed in the usage for the command
|
||||
* `hidden: bool` This command will be hidden from users, but still executable. Sometimes useful for constructing complex commands where one custom command might call another.
|
||||
* `default: bool` Use this for compound command groups. If you wanted to have `jira foo bar` and `jira foo baz` you would have two commands with `name: foo bar` and `name: foo baz`. Then if you wanted `jira foo baz` to be called by default when you type `jira foo` you would set `default: true` for that custom command.
|
||||
* `options: list` This is the list of possible option flags that the command will accept
|
||||
* `args: list` This is the list of command arguments (like the ISSUE) that the command will accept.
|
||||
* `aliases: string list`: This is a list of alternate names that the user can provide on the command line to run the same command. Typically used to shorten the command name or provide alternatives that users might expect.
|
||||
* `script: string` [**required**] This is the script that will be executed as the action for this command. The value will be treated as a template and substitutions for options and arguments will be made before executing.
|
||||
|
||||
##### Options
|
||||
These are possible keys under the command `options` property:
|
||||
* `name: string` [**required**] Name of the option, so `name: foobar` will result in `--foobar` option.
|
||||
* `help: string` The help messsage displayed in usage for the option.
|
||||
* `type: string`: The type of the option, can be one of these values: `BOOL`, `COUNTER`, `ENUM`, `FLOAT32`, `FLOAT64`, `INT8`, `INT16`, `INT32`, `INT64`, `INT`, `STRING`, `STRINGMAP`, `UINT8`, `UINT16`, `UINT32`, `UINT64` and `UINT`. Most of these are primitive data types an should be self-explanitory. The default type is `STRING`. There are some special types:
|
||||
* `COUNTER` will be an integer type that increments each time the option is used. So something like `--count --count` will results in `{{options.count}}` of `2`.
|
||||
* `ENUM` type is used with the `enum` property. The raw type is a string and **must** be one of the values listed in the `enum` property.
|
||||
* `STRINGMAP` is a `string => string` map with the format of `KEY=VALUE`. So `--override foo=bar --override bin=baz` will allow for `{{options.override.foo}}` to be `bar` and `{{options.override.bin}}` to be `baz`.
|
||||
* `short: char` The single character option to be used so `short: c` will allow for `-c`.
|
||||
* `required: bool` Indicate that this option must be provided on the command line. Conflicts with the `default` property.
|
||||
* `default: any` Specify the default value for the option. Conflicts with the `required` property.
|
||||
* `hidden: bool` Hide the option from the usage help message, but otherwise works fine. Sometimes useful for developer options that user should not play with.
|
||||
* `repeat: bool` Indicate that this option can be repeated. Not applicable for `COUNTER` and `STRINGMAP` types. This will turn the option value into an array that you can iterate over. So `--day Monday --day Thursday` can be used like `{{range options.day}}Day: {{.}}{{end}}`
|
||||
* `enum: string list` Used with the `type: ENUM` property, it is a list of strings values that represent the set of possible values the option accepts.
|
||||
|
||||
##### Arguments
|
||||
These are possible keys under the command `args` property:
|
||||
* `name: string` [**required**] Name of the option, so `name: ISSUE` will show in the usasge as `jira <command> ISSUE`. This also represents the name of the argument to be used in the script template, so `{{args.ISSUE}}`.
|
||||
* `help: string` The help messsage displayed in usage for the argument.
|
||||
* `type: string`: The type of the argumemnt, can be one of these values: `BOOL`, `COUNTER`, `ENUM`, `FLOAT32`, `FLOAT64`, `INT8`, `INT16`, `INT32`, `INT64`, `INT`, `STRING`, `STRINGMAP`, `UINT8`, `UINT16`, `UINT32`, `UINT64` and `UINT`. Most of these are primitive data types an should be self-explanitory. The default type is `STRING`. There are some special types:
|
||||
* `COUNTER` will be an integer type that increments each the argument is provided So something like `jira <command> ISSUE-12 ISSUE-23` will results in `{{args.ISSUE}}` of `2`.
|
||||
* `ENUM` type is used with the `enum` property. The raw type is a string and **must** be one of the values listed in the `enum` property.
|
||||
* `STRINGMAP` is a `string => string` map with the format of `KEY=VALUE`. So `jira <command> foo=bar bin=baz` along with a `name: OVERRIDE` property will allow for `{{args.OVERRIDE.foo}}` to be `bar` and `{{args.OVERRIDE.bin}}` to be `baz`.
|
||||
* `required: bool` Indicate that this argument must be provided on the command line. Conflicts with the `default` property.
|
||||
* `default: any` Specify the default value for the argument. Conflicts with the `required` property.
|
||||
* `repeat: bool` Indicate that this argument can be repeated. Not applicable for `COUNTER` and `STRINGMAP` types. This will turn the template value into an array that you can iterate over. So `jira <command> ISSUE-12 ISSUE-23` can be used like `{{range args.ISSUE}}Issue: {{.}}{{end}}`
|
||||
* `enum: string list` Used with the `type: ENUM` property, it is a list of strings values that represent the set of possible values for the argument.
|
||||
|
||||
##### Script Template
|
||||
The `script` property is a template that whould produce `/bin/sh` compatible syntax after the template has been processed. There are 2 key template functions `{{args}}` and `{{options}}` that return the parsed arguments and option flags as a map.
|
||||
|
||||
To demonstrate how you might use args and options here is a `custom-test` command:
|
||||
```
|
||||
custom-commands:
|
||||
- name: custom-test
|
||||
help: Testing the custom commands
|
||||
options:
|
||||
- name: abc
|
||||
short: a
|
||||
default: default
|
||||
- name: day
|
||||
type: ENUM
|
||||
enum:
|
||||
- Monday
|
||||
- Tuesday
|
||||
- Wednesday
|
||||
- Thursday
|
||||
- Friday
|
||||
required: true
|
||||
args:
|
||||
- name: ARG
|
||||
required: true
|
||||
- name: MORE
|
||||
repeat: true
|
||||
script: |
|
||||
echo COMMAND {{args.ARG}} --abc {{options.abc}} --day {{options.day}} {{range $more := args.MORE}}{{$more}} {{end}}
|
||||
```
|
||||
|
||||
Then to run it:
|
||||
```
|
||||
$ jira custom-test
|
||||
ERROR Invalid Usage: required flag --day not provided
|
||||
|
||||
$ jira custom-test --day Sunday
|
||||
ERROR Invalid Usage: enum value must be one of Monday,Tuesday,Wednesday,Thursday,Friday, got 'Sunday'
|
||||
|
||||
$ jira custom-test --day Tuesday
|
||||
ERROR Invalid Usage: required argument 'ARG' not provided
|
||||
|
||||
$ jira custom-test --day Tuesday arg1
|
||||
COMMAND arg1 --abc default --day Tuesday
|
||||
|
||||
$ jira custom-test --day Tuesday arg1 more1 more2 more3
|
||||
COMMAND arg1 --abc default --day Tuesday more1 more2 more3
|
||||
|
||||
$ jira custom-test --day Tuesday arg1 more1 more2 more3 --abc non-default
|
||||
COMMAND arg1 --abc non-default --day Tuesday more1 more2 more3
|
||||
|
||||
$ jira custom-test --day Tuesday arg1 more1 more2 more3 -a short-non-default
|
||||
COMMAND arg1 --abc short-non-default --day Tuesday more1 more2 more3
|
||||
```
|
||||
|
||||
The script has access to all the environment variables that are in your current environment plus those that `jira` will set. `jira` sets environment variables for each config property it has parsed from `.jira.d/config.yml` or the command configs at `.jira.d/<command>.yml`. It might be useful to see all environment variables that `jira` is producing, so here is a simple custom command to list them:
|
||||
```
|
||||
custom-commands:
|
||||
- name: env
|
||||
help: print the JIRA environment variables available to custom commands
|
||||
script: |
|
||||
env | grep JIRA
|
||||
```
|
||||
|
||||
You could use the environment variables automatically, so if your `.jira.d/config.yml` looks something like this:
|
||||
```
|
||||
project: PROJECT
|
||||
custom-commands:
|
||||
- name: print-project
|
||||
help: print the name of the configured project
|
||||
script: "echo $JIRA_PROJECT"
|
||||
```
|
||||
|
||||
##### Examples
|
||||
|
||||
* `jira mine` for listing issues assigned to you
|
||||
```
|
||||
custom-commands:
|
||||
- name: mine
|
||||
help: display issues assigned to me
|
||||
script: |-
|
||||
if [ -n "$JIRA_PROJECT" ]; then
|
||||
# if `project: ...` configured just list the issues for current project
|
||||
{{jira}} list --template table --query "resolution = unresolved and assignee=currentuser() and project = $JIRA_PROJECT ORDER BY priority asc, created"
|
||||
else
|
||||
# otherwise list issues for all project
|
||||
{{jira}} list --template table --query "resolution = unresolved and assignee=currentuser() ORDER BY priority asc, created"
|
||||
fi
|
||||
```
|
||||
* `jira sprint` for listing issues in your current sprint
|
||||
```
|
||||
custom-commands:
|
||||
- name: sprint
|
||||
help: display issues for active sprint
|
||||
script: |-
|
||||
if [ -n "$JIRA_PROJECT" ]; then
|
||||
# if `project: ...` configured just list the issues for current project
|
||||
{{jira}} list --template table --query "sprint in openSprints() and type != epic and resolution = unresolved and project=$JIRA_PROJECT ORDER BY rank asc, created"
|
||||
else
|
||||
# otherwise list issues for all project
|
||||
echo "\"project: ...\" configuration missing from .jira.d/config.yml"
|
||||
fi
|
||||
```
|
||||
|
||||
### Editing
|
||||
|
||||
When you run command like `jira edit` it will open up your favorite editor with the templatized output so you can quickly edit. When the editor
|
||||
closes **go-jira** will submit the completed form. The order which **go-jira** attempts to determine your prefered editor is:
|
||||
|
||||
* **editor** property in any config.yml file
|
||||
* **JIRA_EDITOR** environment variable
|
||||
* **EDITOR** environment variable
|
||||
* vim
|
||||
|
||||
### Templates
|
||||
|
||||
**go-jira** has the ability to customize most output (and editor input) via templates. There are default templates available for all operations,
|
||||
which may or may not work for your actual jira implementation. Jira is endlessly customizable, so it is hard to provide default templates
|
||||
that will work for all issue types.
|
||||
|
||||
When running a command like `jira edit` it will look through the current directory hierarchy trying to find a file that matches **.jira.d/templates/edit**,
|
||||
if found it will use that file as the template, otherwise it will use the default **edit** template hard-coded into **go-jira**. You can export the default
|
||||
hard-coded templates with `jira export-templates` which will write them to **~/.jira.d/templates/**.
|
||||
|
||||
#### Writing/Editing Templates
|
||||
|
||||
First the basic templating functionality is defined by the Go language 'text/template' library. The library reference documentation can be found [here](https://golang.org/pkg/text/template/), and there is a good primer document [here](https://gohugo.io/templates/go-templates/). `go-jira` also provides a few extra helper functions to make it a bit easlier to format the data, those functions are defined [here](https://github.com/Netflix-Skunkworks/go-jira/blob/master/jiracli/templates.go#L64).
|
||||
|
||||
Knowing what data and fields are available to any given template is not obvious. The easiest approach to determine what is available is to use the `debug` template on any given operation. For eample to find out what is available to the "view" templates, you can use:
|
||||
```
|
||||
jira view GOJIRA-321 -t debug
|
||||
```
|
||||
|
||||
This will print out the data in JSON format that is available to the template. You can do this for any other operation, like "list":
|
||||
```
|
||||
jira list -t debug
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
For Atlassian Cloud hosted Jira [API Tokens are now required](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/). You will automatically be prompted for an API Token if your jira endoint ends in `.atlassian.net`. If you are using a private Jira service, you can force `jira` to use an api-token by setting the `authentication-method: api-token` property in your `$HOME/.jira.d/config.yml` file. The API Token needs to be presented to the Jira service on every request, so it is recommended to store this API Token security within your OS's keyring, or using the `pass` service as documented below so that it can be programatically accessed via `jira` and not prompt you every time. For a less-secure option you can also provide the API token via a `JIRA_API_TOKEN` environment variable. If you are unable to use an api-token for an Atlassian Cloud hosted Jira then you can still force `jira` to use the old session based authentication (until it the hosted system stops accepting it) by setting `authentication-method: session`.
|
||||
|
||||
If your Jira service still allows you to use the Session based authention method then `jira` will prompt for a password automatically when get a response header from the Jira service that indicates you do not have an active session (ie the `X-Ausername` header is set to `anonymous`). Then after authentication we cache the `cloud.session.token` cookie returned by the service [session login api](https://docs.atlassian.com/jira/REST/cloud/#auth/1/session-login) and reuse that on subsequent requests. Typically this cookie will be valid for several hours (depending on the service configuration). To automatically securely store your password for easy reuse by jira You can enable a `password-source` via `.jira.d/config.yml` with possible values of `keyring` or `pass`.
|
||||
|
||||
#### User vs Login
|
||||
The Jira service has sometimes differing opinions about how a user is identified. In other words the ID you login with might not be ID that the jira system recognized you as. This matters when trying to identify a user via various Jira REST APIs (like issue assignment). This is especially relevent when trying to authenticate with an API Token where the authentication user is usually an email address, but within the Jira system the user is identified by a user name. To accomodate this `jira` now supports two different properties in the config file. So when authentication using the API Tokens you will likely want something like this in your `$HOME/.jira.d/config.yml` file:
|
||||
```
|
||||
user: person
|
||||
login: person@example.com
|
||||
```
|
||||
|
||||
You can also override these values on the command line with `jira --user person --login person@example.com`. The `login` value will be used only for authentication purposes, the `user` value will be used when a user name is required for any Jira service API calls.
|
||||
|
||||
#### keyring password source
|
||||
On OSX and Linux there are a few keyring providers that `go-jira` can use (via this [golang module](https://github.com/tmc/keyring)). To integrate `go-jira` with a supported keyring just add this configuration to `$HOME/.jira.d/config.yml`:
|
||||
```yaml
|
||||
password-source: keyring
|
||||
```
|
||||
After setting this and issuing a `jira login`, your credentials will be stored in your platform's backend (e.g. Keychain for Mac OS X) automatically. Subsequent operations, like a `jira ls`, should automatically login.
|
||||
|
||||
#### `pass` password source
|
||||
An alternative to the keyring password source is the `pass` tool (documentation [here](https://www.passwordstore.org/)). This uses gpg to encrypt/decrypt passwords on demand and by using `gpg-agent` you can cache the gpg credentials for a period of time so you will not be prompted repeatedly for decrypting the passwords. The advantage over the keyring integration is that `pass` can be used on more platforms than OSX and Linux, although it does require more setup. To use `pass` for password storage and retrieval via `go-jira` just add this configuration to `$HOME/.jira.d/config.yml`:
|
||||
```yaml
|
||||
password-source: pass
|
||||
```
|
||||
|
||||
This assumes you have already setup `pass` correctly on your system. Specifically you will need to have created a gpg key like this:
|
||||
|
||||
```
|
||||
$ gpg --gen-key
|
||||
```
|
||||
|
||||
Then you will need the GPG Key ID you want associated with `pass`. First list the available keys:
|
||||
```
|
||||
$ gpg --list-keys
|
||||
/home/gojira/.gnupg/pubring.gpg
|
||||
-------------------------------------------------
|
||||
pub 2048R/A307D709 2016-12-18
|
||||
uid Go Jira <gojira@example.com>
|
||||
sub 2048R/F9A047B8 2016-12-18
|
||||
```
|
||||
|
||||
Then initialize the `pass` tool to use the correct key:
|
||||
```
|
||||
$ pass init "Go Jira <gojira@example.com>"
|
||||
```
|
||||
|
||||
You probably want to setup gpg-agent so that you dont have to type in your gpg passphrase all the time. You can get `gpg-agent` to automatically start by adding something like this to your `$HOME/.bashrc`
|
||||
```bash
|
||||
if [ -f $HOME/.gpg-agent-info ]; then
|
||||
. $HOME/.gpg-agent-info
|
||||
export GPG_AGENT_INFO
|
||||
fi
|
||||
|
||||
if [ ! -f $HOME/.gpg-agent.conf ]; then
|
||||
cat <<EOM >$HOME/.gpg-agent.conf
|
||||
default-cache-ttl 604800
|
||||
max-cache-ttl 604800
|
||||
default-cache-ttl-ssh 604800
|
||||
max-cache-ttl-ssh 604800
|
||||
EOM
|
||||
fi
|
||||
|
||||
if [ -n "${GPG_AGENT_INFO}" ]; then
|
||||
nc -U "${GPG_AGENT_INFO%%:*}" >/dev/null </dev/null
|
||||
if [ ! -S "${GPG_AGENT_INFO%%:*}" -o $? != 0 ]; then
|
||||
# set passphrase cache so I only have to type my passphrase once a day
|
||||
eval $(gpg-agent --options $HOME/.gpg-agent.conf --daemon --write-env-file $HOME/.gpg-agent-info --use-standard-socket --log-file $HOME/tmp/gpg-agent.log --verbose)
|
||||
fi
|
||||
fi
|
||||
export GPG_TTY=$(tty)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
#### Setting up TAB completion
|
||||
|
||||
Since go-jira is build with the "kingpin" golang command line library we supports bash/zsh shell completion automatically:
|
||||
|
||||
* <https://github.com/alecthomas/kingpin/tree/v2.2.5#bashzsh-shell-completion>
|
||||
|
||||
For example, in bash, adding something along the lines of:
|
||||
|
||||
`eval "$(jira --completion-script-bash)"`
|
||||
|
||||
to your bashrc, or .profile (assuming go-jira binary is already in your path) will cause jira to offer tab completion behavior.
|
||||
|
||||
```
|
||||
usage: jira [<flags>] <command> [<args> ...]
|
||||
|
||||
@@ -149,386 +516,5 @@ Commands:
|
||||
watch: Add/Remove watcher to issue
|
||||
worklog add: Add a worklog to an issue
|
||||
worklog list: Prints the worklog data for given issue
|
||||
session: Attempt to login into jira server
|
||||
|
||||
```
|
||||
|
||||
## v1 vs v0 changes
|
||||
|
||||
###### **Golang library import**
|
||||
For the new version of go-jira you should use:
|
||||
```
|
||||
import "gopkg.in/Netflix-Skunkworks/go-jira.v1"
|
||||
```
|
||||
|
||||
If you have code that depends on the old apis, you can still use them with this import:
|
||||
```
|
||||
import "gopkg.in/Netflix-Skunkworks/go-jira.v0"
|
||||
```
|
||||
|
||||
###### **Configs per command**
|
||||
Instead of requiring a executable template to get configs for a given command now you can create a config to be applied to a command. So if you want to use `template: table` by default for your `jira list` you can now do:
|
||||
```
|
||||
$ cat $HOME/.jira.d/list.yml
|
||||
template: table
|
||||
```
|
||||
Where previously you needed something like:
|
||||
```
|
||||
# cat $HOME/.jira.d/config.yml
|
||||
#!/bin/sh
|
||||
case $JIRA_OPERATION in
|
||||
list)
|
||||
echo "template: table";;
|
||||
esac
|
||||
```
|
||||
|
||||
###### **Custom Commands**
|
||||
Now you can create your own custom commands to do common operations with jira. Please see the details **Custom Commands** section below for more details. If you want to create a command `jira mine` that lists all the issues assigned to you now you can modify your `.jira.d/config.yml` file to add a `custom-commands` section like this:
|
||||
```yaml
|
||||
custom-commands:
|
||||
- name: mine
|
||||
help: display issues assigned to me
|
||||
script: |-
|
||||
{{jira}} list --query "resolution = unresolved and assignee=currentuser() ORDER BY created"
|
||||
```
|
||||
Then the next time you run `jira help` you will see your usage:
|
||||
```
|
||||
$ jira mine --help
|
||||
usage: jira mine
|
||||
|
||||
display issues assigned to me
|
||||
|
||||
Flags:
|
||||
--help Show context-sensitive help (also try --help-long and --help-man).
|
||||
-v, --verbose ... Increase verbosity for debugging
|
||||
-e, --endpoint=ENDPOINT Base URI to use for Jira
|
||||
-u, --user=USER Login name used for authentication with Jira service
|
||||
--unixproxy=UNIXPROXY Path for a unix-socket proxy
|
||||
-k, --insecure Disable TLS certificate verification
|
||||
```
|
||||
|
||||
###### **Incompatible command changes**
|
||||
Unfortunately during the rewrite between v0 and v1 there were some necessary changes that broke backwards compatibility with existing commands. Specifically the `dups`, `blocks`, `add worklog` and `add|remove|set labels` commands have had the command word swapped around:
|
||||
* `jira DUPLICATE dups ISSUE` => `jira dup DUPLICATE ISSUE`
|
||||
* `jira BLOCKER blocks ISSUE` => `jira block BLOCKER ISSUE`
|
||||
* `jira add worklog` => `jira worklog add`
|
||||
* `jira add labels` => `jira labels add`
|
||||
* `jira remove labels` => `jira labels remove`
|
||||
* `jira set labels` => `jira labels set`
|
||||
|
||||
###### **Login process change**
|
||||
We have, once again, changed how login happens for Jira. When authenticating against Atlassian Cloud Jira [API Tokens are now required](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/). Please read [the Authentication section](#authentication) below for more information.
|
||||
|
||||
If you use a privately hosted Jira service, you can chose to use the API Token method or continue using the session login api. Please read [the Authentication section](#authentication) below for more information.
|
||||
|
||||
Previously `jira` used attempt to get a `JSESSION` cookies by authenticating with the webservice standard GUI login process. This has been especially problematic as users need to authenticate with various credential providers (google auth, etc). We now attempt to authenticate via the [session login api](https://docs.atlassian.com/jira/REST/cloud/#auth/1/session-login). This may be problematic for users if admins have locked down the session-login api, so we might have to bring back the error-prone Basic-Auth approach. For users that are unable to authenticate via `jira` hopefully someone in your organization can provide me with details on a process for you to authenticate and we can try to update `jira`.
|
||||
|
||||
## Configuration
|
||||
|
||||
**go-jira** uses a configuration hierarchy. When loading the configuration from disk it will recursively look through all parent directories in your current path looking for a **.jira.d** directory. If your current directory is not a child directory of your homedir, then your homedir will also be inspected for a **.jira.d** directory. From all of **.jira.d** directories discovered **go-jira** will load a **<command>.yml** file (ie for `jira list` it will load `.jira.d/list.yml`) then it will merge in any properties from the **config.yml** if found. The configuration properties found in a file closest to your current working directory will have precedence. Properties overridden with command line options will have final precedence.
|
||||
|
||||
The complicated configuration hierarchy is used because **go-jira** attempts to be context aware. For example, if you are working on a "foo" project and you `cd` into your project workspace, wouldn't it be nice if `jira ls` automatically knew to list only issues related to the "foo" project? Likewise when you `cd` to the "bar" project then `jira ls` should only list issues related to "bar" project. You can do this with by creating a configuration under your project workspace at **./.jira.d/config.yml** that looks like:
|
||||
|
||||
```yaml
|
||||
project: foo
|
||||
```
|
||||
|
||||
You will need to specify your local jira endpoint first, typically in your homedir like:
|
||||
|
||||
```bash
|
||||
mkdir ~/.jira.d
|
||||
|
||||
cat <<EOM >~/.jira.d/config.yml
|
||||
endpoint: https://jira.mycompany.com
|
||||
EOM
|
||||
```
|
||||
|
||||
Then use `jira login` to authenticate yourself as $USER. To change your username, use the `-u` CLI flag or set `user:` in your config.yml
|
||||
|
||||
### Dynamic Configuration
|
||||
|
||||
If the **.jira.d/config.yml** file is executable, then **go-jira** will attempt to execute the file and use the stdout for configuration. You can use this to customize templates or other overrides depending on what type of operation you are running. For example if you would like to use the "table" template when ever you run `jira ls`, then you can create a template like this:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
echo "endpoint: https://jira.mycompany.com"
|
||||
echo "editor: emacs -nw"
|
||||
|
||||
case $JIRA_OPERATION in
|
||||
list)
|
||||
echo "template: table";;
|
||||
esac
|
||||
```
|
||||
|
||||
Or if you always set the same overrides when you create an issue for your project you can do something like this:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
echo "project: GOJIRA"
|
||||
|
||||
case $JIRA_OPERATION in
|
||||
create)
|
||||
echo "assignee: $USER"
|
||||
echo "watchers: mothra"
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
### Custom Commands
|
||||
You can now create custom commands for `jira` just by editing your `.jira.d/config.yml` config file. These commands are effectively shell-scripts that can have documented options and arguments. The basic format is like:
|
||||
```yaml
|
||||
custom-commands:
|
||||
- command1
|
||||
- command2
|
||||
```
|
||||
##### Commands
|
||||
Where the individual commands are maps with these keys:
|
||||
* `name: string` [**required**] This is the command name, so for `jira foobar` you would have `name: foobar`
|
||||
* `help: string` This is help message displayed in the usage for the command
|
||||
* `hidden: bool` This command will be hidden from users, but still executable. Sometimes useful for constructing complex commands where one custom command might call another.
|
||||
* `default: bool` Use this for compound command groups. If you wanted to have `jira foo bar` and `jira foo baz` you would have two commands with `name: foo bar` and `name: foo baz`. Then if you wanted `jira foo baz` to be called by default when you type `jira foo` you would set `default: true` for that custom command.
|
||||
* `options: list` This is the list of possible option flags that the command will accept
|
||||
* `args: list` This is the list of command arguments (like the ISSUE) that the command will accept.
|
||||
* `aliases: string list`: This is a list of alternate names that the user can provide on the command line to run the same command. Typically used to shorten the command name or provide alternatives that users might expect.
|
||||
* `script: string` [**required**] This is the script that will be executed as the action for this command. The value will be treated as a template and substitutions for options and arguments will be made before executing.
|
||||
|
||||
##### Options
|
||||
These are possible keys under the command `options` property:
|
||||
* `name: string` [**required**] Name of the option, so `name: foobar` will result in `--foobar` option.
|
||||
* `help: string` The help message displayed in usage for the option.
|
||||
* `type: string`: The type of the option, can be one of these values: `BOOL`, `COUNTER`, `ENUM`, `FLOAT32`, `FLOAT64`, `INT8`, `INT16`, `INT32`, `INT64`, `INT`, `STRING`, `STRINGMAP`, `UINT8`, `UINT16`, `UINT32`, `UINT64` and `UINT`. Most of these are primitive data types an should be self-explanatory. The default type is `STRING`. There are some special types:
|
||||
* `COUNTER` will be an integer type that increments each time the option is used. So something like `--count --count` will results in `{{options.count}}` of `2`.
|
||||
* `ENUM` type is used with the `enum` property. The raw type is a string and **must** be one of the values listed in the `enum` property.
|
||||
* `STRINGMAP` is a `string => string` map with the format of `KEY=VALUE`. So `--override foo=bar --override bin=baz` will allow for `{{options.override.foo}}` to be `bar` and `{{options.override.bin}}` to be `baz`.
|
||||
* `short: char` The single character option to be used so `short: c` will allow for `-c`.
|
||||
* `required: bool` Indicate that this option must be provided on the command line. Conflicts with the `default` property.
|
||||
* `default: any` Specify the default value for the option. Conflicts with the `required` property.
|
||||
* `hidden: bool` Hide the option from the usage help message, but otherwise works fine. Sometimes useful for developer options that user should not play with.
|
||||
* `repeat: bool` Indicate that this option can be repeated. Not applicable for `COUNTER` and `STRINGMAP` types. This will turn the option value into an array that you can iterate over. So `--day Monday --day Thursday` can be used like `{{range options.day}}Day: {{.}}{{end}}`
|
||||
* `enum: string list` Used with the `type: ENUM` property, it is a list of strings values that represent the set of possible values the option accepts.
|
||||
|
||||
##### Arguments
|
||||
These are possible keys under the command `args` property:
|
||||
* `name: string` [**required**] Name of the option, so `name: ISSUE` will show in the usage as `jira <command> ISSUE`. This also represents the name of the argument to be used in the script template, so `{{args.ISSUE}}`.
|
||||
* `help: string` The help message displayed in usage for the argument.
|
||||
* `type: string`: The type of the argument, can be one of these values: `BOOL`, `COUNTER`, `ENUM`, `FLOAT32`, `FLOAT64`, `INT8`, `INT16`, `INT32`, `INT64`, `INT`, `STRING`, `STRINGMAP`, `UINT8`, `UINT16`, `UINT32`, `UINT64` and `UINT`. Most of these are primitive data types an should be self-explanatory. The default type is `STRING`. There are some special types:
|
||||
* `COUNTER` will be an integer type that increments each the argument is provided So something like `jira <command> ISSUE-12 ISSUE-23` will results in `{{args.ISSUE}}` of `2`.
|
||||
* `ENUM` type is used with the `enum` property. The raw type is a string and **must** be one of the values listed in the `enum` property.
|
||||
* `STRINGMAP` is a `string => string` map with the format of `KEY=VALUE`. So `jira <command> foo=bar bin=baz` along with a `name: OVERRIDE` property will allow for `{{args.OVERRIDE.foo}}` to be `bar` and `{{args.OVERRIDE.bin}}` to be `baz`.
|
||||
* `required: bool` Indicate that this argument must be provided on the command line. Conflicts with the `default` property.
|
||||
* `default: any` Specify the default value for the argument. Conflicts with the `required` property.
|
||||
* `repeat: bool` Indicate that this argument can be repeated. Not applicable for `COUNTER` and `STRINGMAP` types. This will turn the template value into an array that you can iterate over. So `jira <command> ISSUE-12 ISSUE-23` can be used like `{{range args.ISSUE}}Issue: {{.}}{{end}}`
|
||||
* `enum: string list` Used with the `type: ENUM` property, it is a list of strings values that represent the set of possible values for the argument.
|
||||
|
||||
##### Script Template
|
||||
The `script` property is a template that would produce `/bin/sh` compatible syntax after the template has been processed. There are 2 key template functions `{{args}}` and `{{options}}` that return the parsed arguments and option flags as a map.
|
||||
|
||||
To demonstrate how you might use args and options here is a `custom-test` command:
|
||||
```yaml
|
||||
custom-commands:
|
||||
- name: custom-test
|
||||
help: Testing the custom commands
|
||||
options:
|
||||
- name: abc
|
||||
short: a
|
||||
default: default
|
||||
- name: day
|
||||
type: ENUM
|
||||
enum:
|
||||
- Monday
|
||||
- Tuesday
|
||||
- Wednesday
|
||||
- Thursday
|
||||
- Friday
|
||||
required: true
|
||||
args:
|
||||
- name: ARG
|
||||
required: true
|
||||
- name: MORE
|
||||
repeat: true
|
||||
script: |
|
||||
echo COMMAND {{args.ARG}} --abc {{options.abc}} --day {{options.day}} {{range $more := args.MORE}}{{$more}} {{end}}
|
||||
```
|
||||
|
||||
Then to run it:
|
||||
```
|
||||
$ jira custom-test
|
||||
ERROR Invalid Usage: required flag --day not provided
|
||||
|
||||
$ jira custom-test --day Sunday
|
||||
ERROR Invalid Usage: enum value must be one of Monday,Tuesday,Wednesday,Thursday,Friday, got 'Sunday'
|
||||
|
||||
$ jira custom-test --day Tuesday
|
||||
ERROR Invalid Usage: required argument 'ARG' not provided
|
||||
|
||||
$ jira custom-test --day Tuesday arg1
|
||||
COMMAND arg1 --abc default --day Tuesday
|
||||
|
||||
$ jira custom-test --day Tuesday arg1 more1 more2 more3
|
||||
COMMAND arg1 --abc default --day Tuesday more1 more2 more3
|
||||
|
||||
$ jira custom-test --day Tuesday arg1 more1 more2 more3 --abc non-default
|
||||
COMMAND arg1 --abc non-default --day Tuesday more1 more2 more3
|
||||
|
||||
$ jira custom-test --day Tuesday arg1 more1 more2 more3 -a short-non-default
|
||||
COMMAND arg1 --abc short-non-default --day Tuesday more1 more2 more3
|
||||
```
|
||||
|
||||
The script has access to all the environment variables that are in your current environment plus those that `jira` will set. `jira` sets environment variables for each config property it has parsed from `.jira.d/config.yml` or the command configs at `.jira.d/<command>.yml`. It might be useful to see all environment variables that `jira` is producing, so here is a simple custom command to list them:
|
||||
```yaml
|
||||
custom-commands:
|
||||
- name: env
|
||||
help: print the JIRA environment variables available to custom commands
|
||||
script: |
|
||||
env | grep JIRA
|
||||
```
|
||||
|
||||
You could use the environment variables automatically, so if your `.jira.d/config.yml` looks something like this:
|
||||
```yaml
|
||||
project: PROJECT
|
||||
custom-commands:
|
||||
- name: print-project
|
||||
help: print the name of the configured project
|
||||
script: "echo $JIRA_PROJECT"
|
||||
```
|
||||
|
||||
##### Examples
|
||||
|
||||
* `jira mine` for listing issues assigned to you
|
||||
```yaml
|
||||
custom-commands:
|
||||
- name: mine
|
||||
help: display issues assigned to me
|
||||
script: |-
|
||||
if [ -n "$JIRA_PROJECT" ]; then
|
||||
# if `project: ...` configured just list the issues for current project
|
||||
{{jira}} list --template table --query "resolution = unresolved and assignee=currentuser() and project = $JIRA_PROJECT ORDER BY priority asc, created"
|
||||
else
|
||||
# otherwise list issues for all project
|
||||
{{jira}} list --template table --query "resolution = unresolved and assignee=currentuser() ORDER BY priority asc, created"
|
||||
fi
|
||||
```
|
||||
* `jira sprint` for listing issues in your current sprint
|
||||
```yaml
|
||||
custom-commands:
|
||||
- name: sprint
|
||||
help: display issues for active sprint
|
||||
script: |-
|
||||
if [ -n "$JIRA_PROJECT" ]; then
|
||||
# if `project: ...` configured just list the issues for current project
|
||||
{{jira}} list --template table --query "sprint in openSprints() and type != epic and resolution = unresolved and project=$JIRA_PROJECT ORDER BY rank asc, created"
|
||||
else
|
||||
# otherwise list issues for all project
|
||||
echo "\"project: ...\" configuration missing from .jira.d/config.yml"
|
||||
fi
|
||||
```
|
||||
|
||||
### Editing
|
||||
|
||||
When you run command like `jira edit` it will open up your favorite editor with the templatized output so you can quickly edit. When the editor
|
||||
closes **go-jira** will submit the completed form. The order which **go-jira** attempts to determine your preferred editor is:
|
||||
|
||||
* **editor** property in any config.yml file
|
||||
* **JIRA_EDITOR** environment variable
|
||||
* **EDITOR** environment variable
|
||||
* vim
|
||||
|
||||
### Templates
|
||||
|
||||
**go-jira** has the ability to customize most output (and editor input) via templates. There are default templates available for all operations,
|
||||
which may or may not work for your actual jira implementation. Jira is endlessly customizable, so it is hard to provide default templates
|
||||
that will work for all issue types.
|
||||
|
||||
When running a command like `jira edit` it will look through the current directory hierarchy trying to find a file that matches **.jira.d/templates/edit**,
|
||||
if found it will use that file as the template, otherwise it will use the default **edit** template hard-coded into **go-jira**. You can export the default
|
||||
hard-coded templates with `jira export-templates` which will write them to **~/.jira.d/templates/**.
|
||||
|
||||
#### Writing/Editing Templates
|
||||
|
||||
First the basic templating functionality is defined by the Go language 'text/template' library. The library reference documentation can be found [here](https://golang.org/pkg/text/template/), and there is a good primer document [here](https://gohugo.io/templates/go-templates/). `go-jira` also provides a few extra helper functions to make it a bit easier to format the data, those functions are defined [here](https://github.com/Netflix-Skunkworks/go-jira/blob/master/jiracli/templates.go#L64).
|
||||
|
||||
Knowing what data and fields are available to any given template is not obvious. The easiest approach to determine what is available is to use the `debug` template on any given operation. For example to find out what is available to the "view" templates, you can use:
|
||||
```
|
||||
jira view GOJIRA-321 -t debug
|
||||
```
|
||||
|
||||
This will print out the data in JSON format that is available to the template. You can do this for any other operation, like "list":
|
||||
```
|
||||
jira list -t debug
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
For Atlassian Cloud hosted Jira [API Tokens are now required](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/). You will automatically be prompted for an API Token if your jira endpoint ends in `.atlassian.net`. If you are using a private Jira service, you can force `jira` to use an api-token by setting the `authentication-method: api-token` property in your `$HOME/.jira.d/config.yml` file. The API Token needs to be presented to the Jira service on every request, so it is recommended to store this API Token security within your OS's keyring, or using the `pass` service as documented below so that it can be programmatically accessed via `jira` and not prompt you every time. For a less-secure option you can also provide the API token via a `JIRA_API_TOKEN` environment variable. If you are unable to use an api-token for an Atlassian Cloud hosted Jira then you can still force `jira` to use the old session based authentication (until it the hosted system stops accepting it) by setting `authentication-method: session`.
|
||||
|
||||
If your Jira service still allows you to use the Session based authentication method then `jira` will prompt for a password automatically when get a response header from the Jira service that indicates you do not have an active session (ie the `X-Ausername` header is set to `anonymous`). Then after authentication we cache the `cloud.session.token` cookie returned by the service [session login api](https://docs.atlassian.com/jira/REST/cloud/#auth/1/session-login) and reuse that on subsequent requests. Typically this cookie will be valid for several hours (depending on the service configuration). To automatically securely store your password for easy reuse by jira You can enable a `password-source` via `.jira.d/config.yml` with possible values of `keyring` or `pass`.
|
||||
|
||||
#### User vs Login
|
||||
The Jira service has sometimes differing opinions about how a user is identified. In other words the ID you login with might not be ID that the jira system recognized you as. This matters when trying to identify a user via various Jira REST APIs (like issue assignment). This is especially relevant when trying to authenticate with an API Token where the authentication user is usually an email address, but within the Jira system the user is identified by a user name. To accommodate this `jira` now supports two different properties in the config file. So when authentication using the API Tokens you will likely want something like this in your `$HOME/.jira.d/config.yml` file:
|
||||
```yaml
|
||||
user: person
|
||||
login: person@example.com
|
||||
```
|
||||
|
||||
You can also override these values on the command line with `jira --user person --login person@example.com`. The `login` value will be used only for authentication purposes, the `user` value will be used when a user name is required for any Jira service API calls.
|
||||
|
||||
#### keyring password source
|
||||
On OSX and Linux there are a few keyring providers that `go-jira` can use (via this [golang module](https://github.com/tmc/keyring)). To integrate `go-jira` with a supported keyring just add this configuration to `$HOME/.jira.d/config.yml`:
|
||||
```yaml
|
||||
password-source: keyring
|
||||
```
|
||||
After setting this and issuing a `jira login`, your credentials will be stored in your platform's backend (e.g. Keychain for Mac OS X) automatically. Subsequent operations, like a `jira ls`, should automatically login.
|
||||
|
||||
#### `pass` password source
|
||||
An alternative to the keyring password source is the `pass` tool (documentation [here](https://www.passwordstore.org/)). This uses gpg to encrypt/decrypt passwords on demand and by using `gpg-agent` you can cache the gpg credentials for a period of time so you will not be prompted repeatedly for decrypting the passwords. The advantage over the keyring integration is that `pass` can be used on more platforms than OSX and Linux, although it does require more setup. To use `pass` for password storage and retrieval via `go-jira` just add this configuration to `$HOME/.jira.d/config.yml`:
|
||||
```yaml
|
||||
password-source: pass
|
||||
```
|
||||
|
||||
This assumes you have already setup `pass` correctly on your system. Specifically you will need to have created a gpg key like this:
|
||||
|
||||
```
|
||||
$ gpg --gen-key
|
||||
```
|
||||
|
||||
Then you will need the GPG Key ID you want associated with `pass`. First list the available keys:
|
||||
```
|
||||
$ gpg --list-keys
|
||||
/home/gojira/.gnupg/pubring.gpg
|
||||
-------------------------------------------------
|
||||
pub 2048R/A307D709 2016-12-18
|
||||
uid Go Jira <gojira@example.com>
|
||||
sub 2048R/F9A047B8 2016-12-18
|
||||
```
|
||||
|
||||
Then initialize the `pass` tool to use the correct key:
|
||||
```
|
||||
$ pass init "Go Jira <gojira@example.com>"
|
||||
```
|
||||
|
||||
You probably want to setup gpg-agent so that you don't have to type in your gpg passphrase all the time. You can get `gpg-agent` to automatically start by adding something like this to your `$HOME/.bashrc`
|
||||
```bash
|
||||
if [ -f $HOME/.gpg-agent-info ]; then
|
||||
. $HOME/.gpg-agent-info
|
||||
export GPG_AGENT_INFO
|
||||
fi
|
||||
|
||||
if [ ! -f $HOME/.gpg-agent.conf ]; then
|
||||
cat <<EOM >$HOME/.gpg-agent.conf
|
||||
default-cache-ttl 604800
|
||||
max-cache-ttl 604800
|
||||
default-cache-ttl-ssh 604800
|
||||
max-cache-ttl-ssh 604800
|
||||
EOM
|
||||
fi
|
||||
|
||||
if [ -n "${GPG_AGENT_INFO}" ]; then
|
||||
nc -U "${GPG_AGENT_INFO%%:*}" >/dev/null </dev/null
|
||||
if [ ! -S "${GPG_AGENT_INFO%%:*}" -o $? != 0 ]; then
|
||||
# set passphrase cache so I only have to type my passphrase once a day
|
||||
eval $(gpg-agent --options $HOME/.gpg-agent.conf --daemon --write-env-file $HOME/.gpg-agent-info --use-standard-socket --log-file $HOME/tmp/gpg-agent.log --verbose)
|
||||
fi
|
||||
fi
|
||||
export GPG_TTY=$(tty)
|
||||
```
|
||||
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
|
||||
)
|
||||
|
||||
@@ -10,7 +12,7 @@ func (j *Jira) GetAttachment(id string) (*jiradata.Attachment, error) {
|
||||
}
|
||||
|
||||
func GetAttachment(ua HttpClient, endpoint string, id string) (*jiradata.Attachment, error) {
|
||||
uri := URLJoin(endpoint, "rest/api/2/attachment", id)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/attachment/%s", endpoint, id)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -30,7 +32,7 @@ func (j *Jira) RemoveAttachment(id string) error {
|
||||
}
|
||||
|
||||
func RemoveAttachment(ua HttpClient, endpoint string, id string) error {
|
||||
uri := URLJoin(endpoint, "rest/api/2/attachment", id)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/attachment/%s", endpoint, id)
|
||||
resp, err := ua.Delete(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+20
-16
@@ -1,8 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/coryb/figtree"
|
||||
"github.com/coryb/oreo"
|
||||
@@ -12,34 +14,36 @@ import (
|
||||
"gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
type oreoLogger struct {
|
||||
logger *logging.Logger
|
||||
}
|
||||
var (
|
||||
log = logging.MustGetLogger("jira")
|
||||
)
|
||||
|
||||
var log = logging.MustGetLogger("jira")
|
||||
|
||||
func (ol *oreoLogger) Printf(format string, args ...interface{}) {
|
||||
ol.logger.Debugf(format, args...)
|
||||
func handleExit() {
|
||||
if e := recover(); e != nil {
|
||||
if exit, ok := e.(jiracli.Exit); ok {
|
||||
os.Exit(exit.Code)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "%s\n%s", e, debug.Stack())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
defer jiracli.HandleExit()
|
||||
defer handleExit()
|
||||
|
||||
jiracli.InitLogging()
|
||||
|
||||
configDir := ".jira.d"
|
||||
fig := figtree.NewFigTree(
|
||||
figtree.WithHome(jiracli.Homedir()),
|
||||
figtree.WithEnvPrefix("JIRA"),
|
||||
figtree.WithConfigDir(configDir),
|
||||
)
|
||||
fig := figtree.NewFigTree()
|
||||
fig.EnvPrefix = "JIRA"
|
||||
fig.ConfigDir = ".jira.d"
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(jiracli.Homedir(), configDir), 0755); err != nil {
|
||||
if err := os.MkdirAll(filepath.Join(jiracli.Homedir(), fig.ConfigDir), 0755); err != nil {
|
||||
log.Errorf("%s", err)
|
||||
panic(jiracli.Exit{Code: 1})
|
||||
}
|
||||
|
||||
o := oreo.New().WithCookieFile(filepath.Join(jiracli.Homedir(), configDir, "cookies.js")).WithLogger(&oreoLogger{log})
|
||||
o := oreo.New().WithCookieFile(filepath.Join(jiracli.Homedir(), fig.ConfigDir, "cookies.js"))
|
||||
|
||||
jiracmd.RegisterAllCommands()
|
||||
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ package jira
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
|
||||
)
|
||||
@@ -22,7 +23,7 @@ func CreateComponent(ua HttpClient, endpoint string, cp ComponentProvider) (*jir
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := URLJoin(endpoint, "rest/api/2/component")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/component", endpoint)
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -23,7 +23,7 @@ func EpicSearch(ua HttpClient, endpoint string, epic string, sp SearchProvider)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
uri, err := url.Parse(URLJoin(endpoint, "rest/agile/1.0/epic", epic, "issue"))
|
||||
uri, err := url.Parse(fmt.Sprintf("%s/rest/agile/1.0/epic/%s/issue", endpoint, epic))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func EpicAddIssues(ua HttpClient, endpoint string, epic string, eip EpicIssuesPr
|
||||
return err
|
||||
}
|
||||
|
||||
uri := URLJoin(endpoint, "rest/agile/1.0/epic", epic, "issue")
|
||||
uri := fmt.Sprintf("%s/rest/agile/1.0/epic/%s/issue", endpoint, epic)
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -99,7 +99,7 @@ func EpicRemoveIssues(ua HttpClient, endpoint string, eip EpicIssuesProvider) er
|
||||
return err
|
||||
}
|
||||
|
||||
uri := URLJoin(endpoint, "rest/agile/1.0/epic/none/issue")
|
||||
uri := fmt.Sprintf("%s/rest/agile/1.0/epic/none/issue", endpoint)
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
|
||||
)
|
||||
|
||||
@@ -10,7 +12,7 @@ func (j *Jira) GetFields() ([]jiradata.Field, error) {
|
||||
}
|
||||
|
||||
func GetFields(ua HttpClient, endpoint string) ([]jiradata.Field, error) {
|
||||
uri := URLJoin(endpoint, "rest/api/2/field")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/field", endpoint)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -59,8 +59,7 @@ func GetIssue(ua HttpClient, endpoint string, issue string, iqg IssueQueryProvid
|
||||
if iqg != nil {
|
||||
query = iqg.ProvideIssueQueryString()
|
||||
}
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue)
|
||||
uri += query
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s%s", endpoint, issue, query)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -85,8 +84,7 @@ func GetIssueWorklog(ua HttpClient, endpoint string, issue string) (*jiradata.Wo
|
||||
maxResults := 100
|
||||
worklogs := jiradata.Worklogs{}
|
||||
for startAt < total {
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "worklog")
|
||||
uri += fmt.Sprintf("?startAt=%d&maxResults=%d", startAt, maxResults)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/worklog?startAt=%d&maxResults=%d", endpoint, issue, startAt, maxResults)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -126,7 +124,7 @@ func AddIssueWorklog(ua HttpClient, endpoint string, issue string, wp WorklogPro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "worklog")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/worklog", endpoint, issue)
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -146,7 +144,7 @@ func (j *Jira) GetIssueEditMeta(issue string) (*jiradata.EditMeta, error) {
|
||||
}
|
||||
|
||||
func GetIssueEditMeta(ua HttpClient, endpoint string, issue string) (*jiradata.EditMeta, error) {
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "editmeta")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/editmeta", endpoint, issue)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -175,7 +173,7 @@ func EditIssue(ua HttpClient, endpoint string, issue string, iup IssueUpdateProv
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s", endpoint, issue)
|
||||
resp, err := ua.Put(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -199,7 +197,7 @@ func CreateIssue(ua HttpClient, endpoint string, iup IssueUpdateProvider) (*jira
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue", endpoint)
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -219,8 +217,7 @@ func (j *Jira) GetIssueCreateMetaProject(projectKey string) (*jiradata.CreateMet
|
||||
}
|
||||
|
||||
func GetIssueCreateMetaProject(ua HttpClient, endpoint string, projectKey string) (*jiradata.CreateMetaProject, error) {
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue/createmeta")
|
||||
uri += fmt.Sprintf("?projectKeys=%s&expand=projects.issuetypes.fields", projectKey)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&expand=projects.issuetypes.fields", endpoint, projectKey)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -249,8 +246,7 @@ func (j *Jira) GetIssueCreateMetaIssueType(projectKey, issueTypeName string) (*j
|
||||
}
|
||||
|
||||
func GetIssueCreateMetaIssueType(ua HttpClient, endpoint string, projectKey, issueTypeName string) (*jiradata.IssueType, error) {
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue/createmeta")
|
||||
uri += fmt.Sprintf("?projectKeys=%s&issuetypeNames=%s&expand=projects.issuetypes.fields", projectKey, url.QueryEscape(issueTypeName))
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&issuetypeNames=%s&expand=projects.issuetypes.fields", endpoint, projectKey, url.QueryEscape(issueTypeName))
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -292,7 +288,7 @@ func LinkIssues(ua HttpClient, endpoint string, lip LinkIssueProvider) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := URLJoin(endpoint, "rest/api/2/issueLink")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issueLink", endpoint)
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -311,8 +307,7 @@ func (j *Jira) GetIssueTransitions(issue string) (*jiradata.TransitionsMeta, err
|
||||
}
|
||||
|
||||
func GetIssueTransitions(ua HttpClient, endpoint string, issue string) (*jiradata.TransitionsMeta, error) {
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "transitions")
|
||||
uri += "?expand=transitions.fields"
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", endpoint, issue)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -337,7 +332,7 @@ func TransitionIssue(ua HttpClient, endpoint string, issue string, iup IssueUpda
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "transitions")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", endpoint, issue)
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -356,7 +351,7 @@ func (j *Jira) GetIssueLinkTypes() (*jiradata.IssueLinkTypes, error) {
|
||||
}
|
||||
|
||||
func GetIssueLinkTypes(ua HttpClient, endpoint string) (*jiradata.IssueLinkTypes, error) {
|
||||
uri := URLJoin(endpoint, "rest/api/2/issueLinkType")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issueLinkType", endpoint)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -380,7 +375,7 @@ func (j *Jira) IssueAddVote(issue string) error {
|
||||
}
|
||||
|
||||
func IssueAddVote(ua HttpClient, endpoint string, issue string) error {
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "votes")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", endpoint, issue)
|
||||
resp, err := ua.Post(uri, "application/json", strings.NewReader("{}"))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -399,7 +394,7 @@ func (j *Jira) IssueRemoveVote(issue string) error {
|
||||
}
|
||||
|
||||
func IssueRemoveVote(ua HttpClient, endpoint string, issue string) error {
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "votes")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", endpoint, issue)
|
||||
resp, err := ua.Delete(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -427,7 +422,7 @@ func RankIssues(ua HttpClient, endpoint string, rrp RankRequestProvider) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := URLJoin(endpoint, "rest/agile/1.0/issue/rank")
|
||||
uri := fmt.Sprintf("%s/rest/agile/1.0/issue/rank", endpoint)
|
||||
resp, err := ua.Put(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -446,7 +441,7 @@ func (j *Jira) IssueAddWatcher(issue, user string) error {
|
||||
}
|
||||
|
||||
func IssueAddWatcher(ua HttpClient, endpoint string, issue, user string) error {
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "watchers")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/watchers", endpoint, issue)
|
||||
resp, err := ua.Post(uri, "application/json", strings.NewReader(fmt.Sprintf("%q", user)))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -465,8 +460,7 @@ func (j *Jira) IssueRemoveWatcher(issue, user string) error {
|
||||
}
|
||||
|
||||
func IssueRemoveWatcher(ua HttpClient, endpoint string, issue, user string) error {
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "watchers")
|
||||
uri += fmt.Sprintf("?username=%s", user)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/watchers?username=%s", endpoint, issue, user)
|
||||
resp, err := ua.Delete(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -494,7 +488,7 @@ func IssueAddComment(ua HttpClient, endpoint string, issue string, cp CommentPro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "comment")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/comment", endpoint, issue)
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -532,7 +526,7 @@ func IssueAssign(ua HttpClient, endpoint string, issue, name string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "assignee")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/assignee", endpoint, issue)
|
||||
resp, err := ua.Put(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -562,7 +556,7 @@ func IssueAttachFile(ua HttpClient, endpoint string, issue, filename string, con
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri, err := url.Parse(URLJoin(endpoint, "rest/api/2/issue", issue, "attachments"))
|
||||
uri, err := url.Parse(fmt.Sprintf("%s/rest/api/2/issue/%s/attachments", endpoint, issue))
|
||||
req := oreo.RequestBuilder(uri).WithMethod("POST").WithHeader(
|
||||
"X-Atlassian-Token", "no-check",
|
||||
).WithHeader(
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
var log = logging.MustGetLogger("jira")
|
||||
|
||||
const VERSION = "1.0.20"
|
||||
const VERSION = "1.0.15"
|
||||
|
||||
type Jira struct {
|
||||
Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
||||
|
||||
+8
-72
@@ -11,7 +11,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/coryb/figtree"
|
||||
@@ -22,79 +21,22 @@ import (
|
||||
"gopkg.in/AlecAivazis/survey.v1"
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
yaml "gopkg.in/coryb/yaml.v2"
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
type Exit struct {
|
||||
Code int
|
||||
}
|
||||
|
||||
// HandleExit will unwind any panics and check to see if they are jiracli.Exit
|
||||
// and exit accordingly.
|
||||
//
|
||||
// Example:
|
||||
// func main() {
|
||||
// defer jiracli.HandleExit()
|
||||
// ...
|
||||
// }
|
||||
func HandleExit() {
|
||||
if e := recover(); e != nil {
|
||||
if exit, ok := e.(Exit); ok {
|
||||
os.Exit(exit.Code)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "%s\n%s", e, debug.Stack())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type GlobalOptions struct {
|
||||
// AuthenticationMethod is the method we use to authenticate with the jira serivce. Possible values are "api-token" or "session".
|
||||
// The default is "api-token" when the service endpoint ends with "atlassian.net", otherwise it "session". Session authentication
|
||||
// will promt for user password and use the /auth/1/session-login endpoint.
|
||||
AuthenticationMethod figtree.StringOption `yaml:"authentication-method,omitempty" json:"authentication-method,omitempty"`
|
||||
|
||||
// Endpoint is the URL for the Jira service. Something like: https://go-jira.atlassian.net
|
||||
Endpoint figtree.StringOption `yaml:"endpoint,omitempty" json:"endpoint,omitempty"`
|
||||
|
||||
// Insecure will allow you to connect to an https endpoint with a self-signed SSL certificate
|
||||
Insecure figtree.BoolOption `yaml:"insecure,omitempty" json:"insecure,omitempty"`
|
||||
|
||||
// Login is the id used for authenticating with the Jira service. For "api-token" AuthenticationMethod this is usually a
|
||||
// full email address, something like "user@example.com". For "session" AuthenticationMethod this will be something
|
||||
// like "user", which by default will use the same value in the `User` field.
|
||||
Login figtree.StringOption `yaml:"login,omitempty" json:"login,omitempty"`
|
||||
|
||||
// PasswordSource specificies the method that we fetch the password. Possible values are "keyring" or "pass".
|
||||
// If this is unset we will just prompt the user. For "keyring" this will look in the OS keychain, if missing
|
||||
// then prompt the user and store the password in the OS keychain. For "pass" this will look in the PasswordDirectory
|
||||
// location using the `pass` tool, if missing prompt the user and store in the PasswordDirectory
|
||||
PasswordSource figtree.StringOption `yaml:"password-source,omitempty" json:"password-source,omitempty"`
|
||||
|
||||
// PasswordDirectory is only used for the "pass" PasswordSource. It is the location for the encrypted password
|
||||
// files used by `pass`. Effectively this overrides the "PASSWORD_STORE_DIR" environment variable
|
||||
PasswordDirectory figtree.StringOption `yaml:"password-directory,omitempty" json:"password-directory,omitempty"`
|
||||
|
||||
// PasswordName is the the name of the password key entry stored used with PasswordSource `pass`.
|
||||
PasswordName figtree.StringOption `yaml:"password-name,omitempty" json:"password-name,omitempty"`
|
||||
|
||||
// Quiet will lower the defalt log level to suppress the standard output for commands
|
||||
Quiet figtree.BoolOption `yaml:"quiet,omitempty" json:"quiet,omitempty"`
|
||||
|
||||
// SocksProxy is used to configure the http client to access the Endpoint via a socks proxy. The value
|
||||
// should be a ip address and port string, something like "127.0.0.1:1080"
|
||||
SocksProxy figtree.StringOption `yaml:"socksproxy,omitempty" json:"socksproxy,omitempty"`
|
||||
|
||||
// UnixProxy is use to configure the http client to access the Endpoint via a local unix domain socket used
|
||||
// to proxy requests
|
||||
UnixProxy figtree.StringOption `yaml:"unixproxy,omitempty" json:"unixproxy,omitempty"`
|
||||
|
||||
// User is use to represent the user on the Jira service. This can be different from the username used to
|
||||
// authenticate with the service. For example when using AuthenticationMethod `api-token` the Login is
|
||||
// typically an email address like `username@example.com` and the User property would be someting like
|
||||
// `username` The User property is used on Jira service API calls that require a user to associate with
|
||||
// an Issue (like assigning a Issue to yourself)
|
||||
User figtree.StringOption `yaml:"user,omitempty" json:"user,omitempty"`
|
||||
Endpoint figtree.StringOption `yaml:"endpoint,omitempty" json:"endpoint,omitempty"`
|
||||
Insecure figtree.BoolOption `yaml:"insecure,omitempty" json:"insecure,omitempty"`
|
||||
Login figtree.StringOption `yaml:"login,omitempty" json:"login,omitempty"`
|
||||
PasswordSource figtree.StringOption `yaml:"password-source,omitempty" json:"password-source,omitempty"`
|
||||
Quiet figtree.BoolOption `yaml:"quiet,omitempty" json:"quiet,omitempty"`
|
||||
SocksProxy figtree.StringOption `yaml:"socksproxy,omitempty" json:"socksproxy,omitempty"`
|
||||
UnixProxy figtree.StringOption `yaml:"unixproxy,omitempty" json:"unixproxy,omitempty"`
|
||||
User figtree.StringOption `yaml:"user,omitempty" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type CommonOptions struct {
|
||||
@@ -180,9 +122,6 @@ func register(app *kingpin.Application, o *oreo.Client, fig *figtree.FigTree) {
|
||||
// rerun the original request
|
||||
return o.Do(req)
|
||||
}
|
||||
} else if globals.AuthMethod() == "api-token" && resp.StatusCode == 401 {
|
||||
globals.SetPass("")
|
||||
return o.Do(req)
|
||||
}
|
||||
return resp, nil
|
||||
},
|
||||
@@ -240,9 +179,6 @@ func register(app *kingpin.Application, o *oreo.Client, fig *figtree.FigTree) {
|
||||
|
||||
cmd.Action(
|
||||
func(_ *kingpin.ParseContext) error {
|
||||
if logging.GetLevel("") > logging.DEBUG {
|
||||
o = o.WithTrace(true)
|
||||
}
|
||||
return copy.Entry.ExecuteFunc(o, &globals)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/coryb/oreo"
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
@@ -13,6 +14,10 @@ var (
|
||||
|
||||
func IncreaseLogLevel(verbosity int) {
|
||||
logging.SetLevel(logging.GetLevel("")+logging.Level(verbosity), "")
|
||||
if logging.GetLevel("") > logging.DEBUG {
|
||||
oreo.TraceRequestBody = true
|
||||
oreo.TraceResponseBody = true
|
||||
}
|
||||
}
|
||||
|
||||
func InitLogging() {
|
||||
|
||||
@@ -25,9 +25,6 @@ func (o *GlobalOptions) keyName() string {
|
||||
}
|
||||
|
||||
if o.PasswordSource.Value == "pass" {
|
||||
if o.PasswordName.Value != "" {
|
||||
return o.PasswordName.Value
|
||||
}
|
||||
return fmt.Sprintf("GoJira/%s", user)
|
||||
}
|
||||
return user
|
||||
@@ -43,11 +40,6 @@ func (o *GlobalOptions) GetPass() string {
|
||||
panic(err)
|
||||
}
|
||||
} else if o.PasswordSource.Value == "pass" {
|
||||
if o.PasswordDirectory.Value != "" {
|
||||
orig := os.Getenv("PASSWORD_STORE_DIR")
|
||||
os.Setenv("PASSWORD_STORE_DIR", o.PasswordDirectory.Value)
|
||||
defer os.Setenv("PASSWORD_STORE_DIR", orig)
|
||||
}
|
||||
if bin, err := exec.LookPath("pass"); err == nil {
|
||||
buf := bytes.NewBufferString("")
|
||||
cmd := exec.Command(bin, o.keyName())
|
||||
@@ -103,11 +95,6 @@ func (o *GlobalOptions) SetPass(passwd string) error {
|
||||
return err
|
||||
}
|
||||
} else if o.PasswordSource.Value == "pass" {
|
||||
if o.PasswordDirectory.Value != "" {
|
||||
orig := os.Getenv("PASSWORD_STORE_DIR")
|
||||
os.Setenv("PASSWORD_STORE_DIR", o.PasswordDirectory.Value)
|
||||
defer os.Setenv("PASSWORD_STORE_DIR", orig)
|
||||
}
|
||||
if bin, err := exec.LookPath("pass"); err == nil {
|
||||
log.Debugf("using %s", bin)
|
||||
passName := o.keyName()
|
||||
|
||||
@@ -158,7 +158,7 @@ func TemplateProcessor() *template.Template {
|
||||
return strings.Join(vals, sep)
|
||||
},
|
||||
"abbrev": func(max int, content string) string {
|
||||
if len(content) > max && max > 2 {
|
||||
if len(content) > max {
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(content[:max-3])
|
||||
buffer.WriteString("...")
|
||||
@@ -290,7 +290,7 @@ const defaultTableTemplate = `{{/* table template */ -}}
|
||||
| {{ "Issue" | printf "%-14s" }} | {{ "Summary" | printf (printf "%%-%ds" (sub $w 2)) }} | {{ "Type" | printf "%-12s"}} | {{ "Priority" | printf "%-12s" }} | {{ "Status" | printf "%-12s" }} | {{ "Age" | printf "%-10s" }} | {{ "Reporter" | printf "%-12s" }} | {{ "Assignee" | printf "%-12s" }} |
|
||||
+{{ "-" | rep 16 }}+{{ "-" | rep $w }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
|
||||
{{ range .issues -}}
|
||||
| {{ .key | printf "%-14s"}} | {{ .fields.summary | abbrev (sub $w 2) | printf (printf "%%-%ds" (sub $w 2)) }} | {{.fields.issuetype.name | printf "%-12s" }} | {{if .fields.priority}}{{.fields.priority.name | printf "%-12s" }}{{else}}<unassigned>{{end}} | {{.fields.status.name | printf "%-12s" }} | {{.fields.created | age | printf "%-10s" }} | {{if .fields.reporter}}{{ .fields.reporter.name | printf "%-12s"}}{{else}}<unassigned>{{end}} | {{if .fields.assignee }}{{.fields.assignee.name | printf "%-12s" }}{{else}}<unassigned>{{end}} |
|
||||
| {{ .key | printf "%-14s"}} | {{ .fields.summary | abbrev (sub $w 2) | printf (printf "%%-%ds" (sub $w 2)) }} | {{.fields.issuetype.name | printf "%-12s" }} | {{.fields.priority.name | printf "%-12s" }} | {{.fields.status.name | printf "%-12s" }} | {{.fields.created | age | printf "%-10s" }} | {{if .fields.reporter}}{{ .fields.reporter.name | printf "%-12s"}}{{else}}<unassigned>{{end}} | {{if .fields.assignee }}{{.fields.assignee.name | printf "%-12s" }}{{else}}<unassigned>{{end}} |
|
||||
{{ end -}}
|
||||
+{{ "-" | rep 16 }}+{{ "-" | rep $w }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
|
||||
`
|
||||
@@ -504,7 +504,7 @@ fields:
|
||||
{{if .meta.fields.fixVersions -}}
|
||||
{{if .meta.fields.fixVersions.allowedValues}}
|
||||
fixVersions: # Values: {{ range .meta.fields.fixVersions.allowedValues }}{{.name}}, {{end}}{{if .overrides.fixVersions}}{{ range (split "," .overrides.fixVersions)}}
|
||||
- name: {{.}}{{end}}{{else}}{{range .fields.fixVersions}}
|
||||
- name: {{.name}}{{end}}{{else}}{{range .fields.fixVersions}}
|
||||
- name: {{.name}}{{end}}{{end}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
+1
-2
@@ -103,8 +103,7 @@ Commands:
|
||||
|
||||
func CommandLine(fig *figtree.FigTree, o *oreo.Client) *kingpin.Application {
|
||||
app := kingpin.New("jira", "Jira Command Line Interface")
|
||||
app.UsageWriter(os.Stdout)
|
||||
app.ErrorWriter(os.Stderr)
|
||||
|
||||
app.Command("version", "Prints version").PreAction(func(*kingpin.ParseContext) error {
|
||||
fmt.Println(jira.VERSION)
|
||||
panic(Exit{Code: 0})
|
||||
|
||||
+2
-7
@@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
@@ -23,11 +22,7 @@ func Homedir() string {
|
||||
}
|
||||
|
||||
func findClosestParentPath(fileName string) (string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
paths := figtree.FindParentPaths(Homedir(), cwd, fileName)
|
||||
paths := figtree.FindParentPaths(fileName)
|
||||
if len(paths) > 0 {
|
||||
return paths[len(paths)-1], nil
|
||||
}
|
||||
@@ -35,7 +30,7 @@ func findClosestParentPath(fileName string) (string, error) {
|
||||
}
|
||||
|
||||
func tmpYml(tmpFilePrefix string) (*os.File, error) {
|
||||
fh, err := ioutil.TempFile("", filepath.Base(tmpFilePrefix))
|
||||
fh, err := ioutil.TempFile("", tmpFilePrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ func CmdAssign(o *oreo.Client, globals *jiracli.GlobalOptions, opts *AssignOptio
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
+2
-2
@@ -66,8 +66,8 @@ func CmdBlock(o *oreo.Client, globals *jiracli.GlobalOptions, opts *BlockOptions
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.InwardIssue.Key, jira.URLJoin(globals.Endpoint.Value, "browse", opts.InwardIssue.Key))
|
||||
fmt.Printf("OK %s %s\n", opts.OutwardIssue.Key, jira.URLJoin(globals.Endpoint.Value, "browse", opts.OutwardIssue.Key))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.InwardIssue.Key, globals.Endpoint.Value, opts.InwardIssue.Key)
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.OutwardIssue.Key, globals.Endpoint.Value, opts.OutwardIssue.Key)
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
package jiracmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/coryb/figtree"
|
||||
"github.com/coryb/oreo"
|
||||
"github.com/pkg/browser"
|
||||
jira "gopkg.in/Netflix-Skunkworks/go-jira.v1"
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
@@ -26,5 +27,5 @@ func CmdBrowseRegistry() *jiracli.CommandRegistryEntry {
|
||||
|
||||
// CmdBrowse open the default system browser to the provided issue
|
||||
func CmdBrowse(globals *jiracli.GlobalOptions, issue string) error {
|
||||
return browser.OpenURL(jira.URLJoin(globals.Endpoint.Value, "browse", issue))
|
||||
return browser.OpenURL(fmt.Sprintf("%s/browse/%s", globals.Endpoint.Value, issue))
|
||||
}
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ func CmdComment(o *oreo.Client, globals *jiracli.GlobalOptions, opts *CommentOpt
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
+2
-3
@@ -93,9 +93,8 @@ func CmdCreate(o *oreo.Client, globals *jiracli.GlobalOptions, opts *CreateOptio
|
||||
return err
|
||||
}
|
||||
|
||||
browseLink := jira.URLJoin(globals.Endpoint.Value, "browse", issueResp.Key)
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", issueResp.Key, browseLink)
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issueResp.Key, globals.Endpoint.Value, issueResp.Key)
|
||||
}
|
||||
|
||||
if opts.SaveFile != "" {
|
||||
@@ -106,7 +105,7 @@ func CmdCreate(o *oreo.Client, globals *jiracli.GlobalOptions, opts *CreateOptio
|
||||
defer fh.Close()
|
||||
out, err := yaml.Marshal(map[string]string{
|
||||
"issue": issueResp.Key,
|
||||
"link": browseLink,
|
||||
"link": fmt.Sprintf("%s/browse/%s", globals.Endpoint.Value, issueResp.Key),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+2
-2
@@ -67,7 +67,7 @@ func CmdDup(o *oreo.Client, globals *jiracli.GlobalOptions, opts *DupOptions) er
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.OutwardIssue.Key, jira.URLJoin(globals.Endpoint.Value, "browse", opts.OutwardIssue.Key))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.OutwardIssue.Key, globals.Endpoint.Value, opts.OutwardIssue.Key)
|
||||
}
|
||||
|
||||
meta, err := jira.GetIssueTransitions(o, globals.Endpoint.Value, opts.InwardIssue.Key)
|
||||
@@ -103,7 +103,7 @@ func CmdDup(o *oreo.Client, globals *jiracli.GlobalOptions, opts *DupOptions) er
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.InwardIssue.Key, jira.URLJoin(globals.Endpoint.Value, "browse", opts.InwardIssue.Key))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.InwardIssue.Key, globals.Endpoint.Value, opts.InwardIssue.Key)
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
+2
-2
@@ -98,7 +98,7 @@ func CmdEdit(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EditOptions)
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, opts.Issue)
|
||||
@@ -145,7 +145,7 @@ func CmdEdit(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EditOptions)
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", issueData.Key, jira.URLJoin(globals.Endpoint.Value, "browse", issueData.Key))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issueData.Key, globals.Endpoint.Value, issueData.Key)
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, issueData.Key)
|
||||
|
||||
+2
-2
@@ -44,9 +44,9 @@ func CmdEpicAdd(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EpicAddOpt
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.Epic, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Epic))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Epic, globals.Endpoint.Value, opts.Epic)
|
||||
for _, issue := range opts.Issues {
|
||||
fmt.Printf("OK %s %s\n", issue, jira.URLJoin(globals.Endpoint.Value, "browse", issue))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, globals.Endpoint.Value, issue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ func CmdEpicRemove(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EpicRem
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
for _, issue := range opts.Issues {
|
||||
fmt.Printf("OK %s %s\n", issue, jira.URLJoin(globals.Endpoint.Value, "browse", issue))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, globals.Endpoint.Value, issue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@ func CmdIssueLink(o *oreo.Client, globals *jiracli.GlobalOptions, opts *IssueLin
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.InwardIssue.Key, jira.URLJoin(globals.Endpoint.Value, "browse", opts.InwardIssue.Key))
|
||||
fmt.Printf("OK %s %s\n", opts.OutwardIssue.Key, jira.URLJoin(globals.Endpoint.Value, "browse", opts.OutwardIssue.Key))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.InwardIssue.Key, globals.Endpoint.Value, opts.InwardIssue.Key)
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.OutwardIssue.Key, globals.Endpoint.Value, opts.OutwardIssue.Key)
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
@@ -57,7 +57,7 @@ func CmdLabelsAdd(o *oreo.Client, globals *jiracli.GlobalOptions, opts *LabelsAd
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, opts.Issue)
|
||||
|
||||
@@ -58,7 +58,7 @@ func CmdLabelsRemove(o *oreo.Client, globals *jiracli.GlobalOptions, opts *Label
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, opts.Issue)
|
||||
|
||||
@@ -55,7 +55,7 @@ func CmdLabelsSet(o *oreo.Client, globals *jiracli.GlobalOptions, opts *LabelsSe
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, opts.Issue)
|
||||
|
||||
@@ -2,13 +2,10 @@ package jiracmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/coryb/figtree"
|
||||
"github.com/coryb/oreo"
|
||||
"github.com/mgutz/ansi"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
survey "gopkg.in/AlecAivazis/survey.v1"
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
@@ -32,24 +29,6 @@ func CmdLogoutRegistry() *jiracli.CommandRegistryEntry {
|
||||
func CmdLogout(o *oreo.Client, globals *jiracli.GlobalOptions, opts *jiracli.CommonOptions) error {
|
||||
if globals.AuthMethod() == "api-token" {
|
||||
log.Noticef("No need to logout when using api-token authentication method")
|
||||
if globals.GetPass() != "" && terminal.IsTerminal(int(os.Stdin.Fd())) && terminal.IsTerminal(int(os.Stdout.Fd())) {
|
||||
delete := false
|
||||
err := survey.AskOne(
|
||||
&survey.Confirm{
|
||||
Message: fmt.Sprintf("Delete api-token from password provider [%s]: ", globals.PasswordSource),
|
||||
Default: false,
|
||||
},
|
||||
&delete,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("%s", err)
|
||||
panic(jiracli.Exit{Code: 1})
|
||||
}
|
||||
if delete {
|
||||
globals.SetPass("")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
ua := o.WithoutRedirect().WithRetries(0).WithoutCallbacks()
|
||||
|
||||
+2
-2
@@ -59,8 +59,8 @@ func CmdRank(o *oreo.Client, globals *jiracli.GlobalOptions, opts *RankOptions)
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.First, jira.URLJoin(globals.Endpoint.Value, "browse", opts.First))
|
||||
fmt.Printf("OK %s %s\n", opts.Second, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Second))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.First, globals.Endpoint.Value, opts.First)
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Second, globals.Endpoint.Value, opts.Second)
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
@@ -57,5 +57,4 @@ func RegisterAllCommands() {
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "watch", Entry: CmdWatchRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "worklog add", Entry: CmdWorklogAddRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "worklog list", Entry: CmdWorklogListRegistry(), Default: true})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "session", Entry: CmdSessionRegistry()})
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package jiracmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/coryb/figtree"
|
||||
"github.com/coryb/oreo"
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
yaml "gopkg.in/coryb/yaml.v2"
|
||||
)
|
||||
|
||||
func CmdSessionRegistry() *jiracli.CommandRegistryEntry {
|
||||
opts := jiracli.CommonOptions{}
|
||||
return &jiracli.CommandRegistryEntry{
|
||||
"Attempt to login into jira server",
|
||||
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
|
||||
jiracli.LoadConfigs(cmd, fig, &opts)
|
||||
return nil
|
||||
},
|
||||
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
|
||||
return CmdSession(o, globals, &opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CmdSession will attempt to login into jira server
|
||||
func CmdSession(o *oreo.Client, globals *jiracli.GlobalOptions, opts *jiracli.CommonOptions) error {
|
||||
ua := o.WithoutRedirect().WithRetries(0).WithoutPostCallbacks()
|
||||
session, err := jira.GetSession(ua, globals.Endpoint.Value)
|
||||
var output []byte
|
||||
if err != nil {
|
||||
defer panic(jiracli.Exit{1})
|
||||
output, err = yaml.Marshal(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
output, err = yaml.Marshal(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Print(string(output))
|
||||
return nil
|
||||
}
|
||||
+1
-1
@@ -108,7 +108,7 @@ func CmdSubtask(o *oreo.Client, globals *jiracli.GlobalOptions, opts *SubtaskOpt
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", issueResp.Key, jira.URLJoin(globals.Endpoint.Value, "browse", issueResp.Key))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issueResp.Key, globals.Endpoint.Value, issueResp.Key)
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
@@ -157,7 +157,7 @@ func CmdTransition(o *oreo.Client, globals *jiracli.GlobalOptions, opts *Transit
|
||||
return jiracli.CliError(err)
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", issueData.Key, jira.URLJoin(globals.Endpoint.Value, "browse", issueData.Key))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issueData.Key, globals.Endpoint.Value, issueData.Key)
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ func CmdVote(o *oreo.Client, globals *jiracli.GlobalOptions, opts *VoteOptions)
|
||||
}
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, opts.Issue)
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ func CmdWatch(o *oreo.Client, globals *jiracli.GlobalOptions, opts *WatchOptions
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
@@ -59,7 +59,7 @@ func CmdWorklogAdd(o *oreo.Client, globals *jiracli.GlobalOptions, opts *Worklog
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, opts.Issue)
|
||||
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
|
||||
)
|
||||
|
||||
@@ -10,7 +12,7 @@ func (j *Jira) GetProjectComponents(project string) (*jiradata.Components, error
|
||||
}
|
||||
|
||||
func GetProjectComponents(ua HttpClient, endpoint string, project string) (*jiradata.Components, error) {
|
||||
uri := URLJoin(endpoint, "rest/api/2/project", project, "components")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/project/%s/components", endpoint, project)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -83,7 +83,7 @@ func Search(ua HttpClient, endpoint string, sp SearchProvider) (*jiradata.Search
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := URLJoin(endpoint, "rest/api/2/search")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/search", endpoint)
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+4
-3
@@ -3,6 +3,7 @@ package jira
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
|
||||
)
|
||||
@@ -34,7 +35,7 @@ func NewSession(ua HttpClient, endpoint string, ap AuthProvider) (*jiradata.Auth
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := URLJoin(endpoint, "rest/auth/1/session")
|
||||
uri := fmt.Sprintf("%s/rest/auth/1/session", endpoint)
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -54,7 +55,7 @@ func (j *Jira) GetSession() (*jiradata.CurrentUser, error) {
|
||||
}
|
||||
|
||||
func GetSession(ua HttpClient, endpoint string) (*jiradata.CurrentUser, error) {
|
||||
uri := URLJoin(endpoint, "rest/auth/1/session")
|
||||
uri := fmt.Sprintf("%s/rest/auth/1/session", endpoint)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -74,7 +75,7 @@ func (j *Jira) DeleteSession() error {
|
||||
}
|
||||
|
||||
func DeleteSession(ua HttpClient, endpoint string) error {
|
||||
uri := URLJoin(endpoint, "rest/auth/1/session")
|
||||
uri := fmt.Sprintf("%s/rest/auth/1/session", endpoint)
|
||||
resp, err := ua.Delete(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
|
||||
<properties>
|
||||
<jira.version>7.2.0</jira.version>
|
||||
<amps.version>6.3.15</amps.version>
|
||||
<amps.version>6.2.6</amps.version>
|
||||
<plugin.testrunner.version>1.2.3</plugin.testrunner.version>
|
||||
<atlassian.spring.scanner.version>1.2.13</atlassian.spring.scanner.version>
|
||||
<!-- This key is used to keep the consistency between the key in atlassian-plugin.xml and the key to generate bundle. -->
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
|
||||
func readJSON(input io.Reader, data interface{}) error {
|
||||
@@ -23,13 +21,3 @@ func readJSON(input io.Reader, data interface{}) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func URLJoin(endpoint string, paths ...string) string {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Unable to parse endpoint: %s", endpoint))
|
||||
}
|
||||
paths = append([]string{u.Path}, paths...)
|
||||
u.Path = path.Join(paths...)
|
||||
return u.String()
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Go's `text/template` package with newline elision
|
||||
|
||||
This is a fork of Go 1.4's [text/template](http://golang.org/pkg/text/template/) package with one addition: a backslash immediately after a closing delimiter will delete all subsequent newlines until a non-newline.
|
||||
|
||||
eg.
|
||||
|
||||
```
|
||||
{{if true}}\
|
||||
hello
|
||||
{{end}}\
|
||||
```
|
||||
|
||||
Will result in:
|
||||
|
||||
```
|
||||
hello\n
|
||||
```
|
||||
|
||||
Rather than:
|
||||
|
||||
```
|
||||
\n
|
||||
hello\n
|
||||
\n
|
||||
```
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// Copyright 2011 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.
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/template"
|
||||
)
|
||||
|
||||
func ExampleTemplate() {
|
||||
// Define a template.
|
||||
const letter = `
|
||||
Dear {{.Name}},
|
||||
{{if .Attended}}
|
||||
It was a pleasure to see you at the wedding.{{else}}
|
||||
It is a shame you couldn't make it to the wedding.{{end}}
|
||||
{{with .Gift}}Thank you for the lovely {{.}}.
|
||||
{{end}}
|
||||
Best wishes,
|
||||
Josie
|
||||
`
|
||||
|
||||
// Prepare some data to insert into the template.
|
||||
type Recipient struct {
|
||||
Name, Gift string
|
||||
Attended bool
|
||||
}
|
||||
var recipients = []Recipient{
|
||||
{"Aunt Mildred", "bone china tea set", true},
|
||||
{"Uncle John", "moleskin pants", false},
|
||||
{"Cousin Rodney", "", false},
|
||||
}
|
||||
|
||||
// Create a new template and parse the letter into it.
|
||||
t := template.Must(template.New("letter").Parse(letter))
|
||||
|
||||
// Execute the template for each recipient.
|
||||
for _, r := range recipients {
|
||||
err := t.Execute(os.Stdout, r)
|
||||
if err != nil {
|
||||
log.Println("executing template:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Dear Aunt Mildred,
|
||||
//
|
||||
// It was a pleasure to see you at the wedding.
|
||||
// Thank you for the lovely bone china tea set.
|
||||
//
|
||||
// Best wishes,
|
||||
// Josie
|
||||
//
|
||||
// Dear Uncle John,
|
||||
//
|
||||
// It is a shame you couldn't make it to the wedding.
|
||||
// Thank you for the lovely moleskin pants.
|
||||
//
|
||||
// Best wishes,
|
||||
// Josie
|
||||
//
|
||||
// Dear Cousin Rodney,
|
||||
//
|
||||
// It is a shame you couldn't make it to the wedding.
|
||||
//
|
||||
// Best wishes,
|
||||
// Josie
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
// Copyright 2012 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.
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/alecthomas/template"
|
||||
)
|
||||
|
||||
// templateFile defines the contents of a template to be stored in a file, for testing.
|
||||
type templateFile struct {
|
||||
name string
|
||||
contents string
|
||||
}
|
||||
|
||||
func createTestDir(files []templateFile) string {
|
||||
dir, err := ioutil.TempDir("", "template")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
f, err := os.Create(filepath.Join(dir, file.name))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.WriteString(f, file.contents)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// Here we demonstrate loading a set of templates from a directory.
|
||||
func ExampleTemplate_glob() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T0.tmpl is a plain template file that just invokes T1.
|
||||
{"T0.tmpl", `T0 invokes T1: ({{template "T1"}})`},
|
||||
// T1.tmpl defines a template, T1 that invokes T2.
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
// T2.tmpl defines a template T2.
|
||||
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// T0.tmpl is the first name matched, so it becomes the starting template,
|
||||
// the value returned by ParseGlob.
|
||||
tmpl := template.Must(template.ParseGlob(pattern))
|
||||
|
||||
err := tmpl.Execute(os.Stdout, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("template execution: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// T0 invokes T1: (T1 invokes T2: (This is T2))
|
||||
}
|
||||
|
||||
// This example demonstrates one way to share some templates
|
||||
// and use them in different contexts. In this variant we add multiple driver
|
||||
// templates by hand to an existing bundle of templates.
|
||||
func ExampleTemplate_helpers() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T1.tmpl defines a template, T1 that invokes T2.
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
// T2.tmpl defines a template T2.
|
||||
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// Load the helpers.
|
||||
templates := template.Must(template.ParseGlob(pattern))
|
||||
// Add one driver template to the bunch; we do this with an explicit template definition.
|
||||
_, err := templates.Parse("{{define `driver1`}}Driver 1 calls T1: ({{template `T1`}})\n{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing driver1: ", err)
|
||||
}
|
||||
// Add another driver template.
|
||||
_, err = templates.Parse("{{define `driver2`}}Driver 2 calls T2: ({{template `T2`}})\n{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing driver2: ", err)
|
||||
}
|
||||
// We load all the templates before execution. This package does not require
|
||||
// that behavior but html/template's escaping does, so it's a good habit.
|
||||
err = templates.ExecuteTemplate(os.Stdout, "driver1", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("driver1 execution: %s", err)
|
||||
}
|
||||
err = templates.ExecuteTemplate(os.Stdout, "driver2", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("driver2 execution: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// Driver 1 calls T1: (T1 invokes T2: (This is T2))
|
||||
// Driver 2 calls T2: (This is T2)
|
||||
}
|
||||
|
||||
// This example demonstrates how to use one group of driver
|
||||
// templates with distinct sets of helper templates.
|
||||
func ExampleTemplate_share() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T0.tmpl is a plain template file that just invokes T1.
|
||||
{"T0.tmpl", "T0 ({{.}} version) invokes T1: ({{template `T1`}})\n"},
|
||||
// T1.tmpl defines a template, T1 that invokes T2. Note T2 is not defined
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// Load the drivers.
|
||||
drivers := template.Must(template.ParseGlob(pattern))
|
||||
|
||||
// We must define an implementation of the T2 template. First we clone
|
||||
// the drivers, then add a definition of T2 to the template name space.
|
||||
|
||||
// 1. Clone the helper set to create a new name space from which to run them.
|
||||
first, err := drivers.Clone()
|
||||
if err != nil {
|
||||
log.Fatal("cloning helpers: ", err)
|
||||
}
|
||||
// 2. Define T2, version A, and parse it.
|
||||
_, err = first.Parse("{{define `T2`}}T2, version A{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing T2: ", err)
|
||||
}
|
||||
|
||||
// Now repeat the whole thing, using a different version of T2.
|
||||
// 1. Clone the drivers.
|
||||
second, err := drivers.Clone()
|
||||
if err != nil {
|
||||
log.Fatal("cloning drivers: ", err)
|
||||
}
|
||||
// 2. Define T2, version B, and parse it.
|
||||
_, err = second.Parse("{{define `T2`}}T2, version B{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing T2: ", err)
|
||||
}
|
||||
|
||||
// Execute the templates in the reverse order to verify the
|
||||
// first is unaffected by the second.
|
||||
err = second.ExecuteTemplate(os.Stdout, "T0.tmpl", "second")
|
||||
if err != nil {
|
||||
log.Fatalf("second execution: %s", err)
|
||||
}
|
||||
err = first.ExecuteTemplate(os.Stdout, "T0.tmpl", "first")
|
||||
if err != nil {
|
||||
log.Fatalf("first: execution: %s", err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// T0 (second version) invokes T1: (T1 invokes T2: (T2, version B))
|
||||
// T0 (first version) invokes T1: (T1 invokes T2: (T2, version A))
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// Copyright 2012 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.
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/template"
|
||||
)
|
||||
|
||||
// This example demonstrates a custom function to process template text.
|
||||
// It installs the strings.Title function and uses it to
|
||||
// Make Title Text Look Good In Our Template's Output.
|
||||
func ExampleTemplate_func() {
|
||||
// First we create a FuncMap with which to register the function.
|
||||
funcMap := template.FuncMap{
|
||||
// The name "title" is what the function will be called in the template text.
|
||||
"title": strings.Title,
|
||||
}
|
||||
|
||||
// A simple template definition to test our function.
|
||||
// We print the input text several ways:
|
||||
// - the original
|
||||
// - title-cased
|
||||
// - title-cased and then printed with %q
|
||||
// - printed with %q and then title-cased.
|
||||
const templateText = `
|
||||
Input: {{printf "%q" .}}
|
||||
Output 0: {{title .}}
|
||||
Output 1: {{title . | printf "%q"}}
|
||||
Output 2: {{printf "%q" . | title}}
|
||||
`
|
||||
|
||||
// Create a template, add the function map, and parse the text.
|
||||
tmpl, err := template.New("titleTest").Funcs(funcMap).Parse(templateText)
|
||||
if err != nil {
|
||||
log.Fatalf("parsing: %s", err)
|
||||
}
|
||||
|
||||
// Run the template to verify the output.
|
||||
err = tmpl.Execute(os.Stdout, "the go programming language")
|
||||
if err != nil {
|
||||
log.Fatalf("execution: %s", err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Input: "the go programming language"
|
||||
// Output 0: The Go Programming Language
|
||||
// Output 1: "The Go Programming Language"
|
||||
// Output 2: "The Go Programming Language"
|
||||
}
|
||||
+1044
File diff suppressed because it is too large
Load Diff
+293
@@ -0,0 +1,293 @@
|
||||
// Copyright 2011 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.
|
||||
|
||||
package template
|
||||
|
||||
// Tests for mulitple-template parsing and execution.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/alecthomas/template/parse"
|
||||
)
|
||||
|
||||
const (
|
||||
noError = true
|
||||
hasError = false
|
||||
)
|
||||
|
||||
type multiParseTest struct {
|
||||
name string
|
||||
input string
|
||||
ok bool
|
||||
names []string
|
||||
results []string
|
||||
}
|
||||
|
||||
var multiParseTests = []multiParseTest{
|
||||
{"empty", "", noError,
|
||||
nil,
|
||||
nil},
|
||||
{"one", `{{define "foo"}} FOO {{end}}`, noError,
|
||||
[]string{"foo"},
|
||||
[]string{" FOO "}},
|
||||
{"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
|
||||
[]string{"foo", "bar"},
|
||||
[]string{" FOO ", " BAR "}},
|
||||
// errors
|
||||
{"missing end", `{{define "foo"}} FOO `, hasError,
|
||||
nil,
|
||||
nil},
|
||||
{"malformed name", `{{define "foo}} FOO `, hasError,
|
||||
nil,
|
||||
nil},
|
||||
}
|
||||
|
||||
func TestMultiParse(t *testing.T) {
|
||||
for _, test := range multiParseTests {
|
||||
template, err := New("root").Parse(test.input)
|
||||
switch {
|
||||
case err == nil && !test.ok:
|
||||
t.Errorf("%q: expected error; got none", test.name)
|
||||
continue
|
||||
case err != nil && test.ok:
|
||||
t.Errorf("%q: unexpected error: %v", test.name, err)
|
||||
continue
|
||||
case err != nil && !test.ok:
|
||||
// expected error, got one
|
||||
if *debug {
|
||||
fmt.Printf("%s: %s\n\t%s\n", test.name, test.input, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if template == nil {
|
||||
continue
|
||||
}
|
||||
if len(template.tmpl) != len(test.names)+1 { // +1 for root
|
||||
t.Errorf("%s: wrong number of templates; wanted %d got %d", test.name, len(test.names), len(template.tmpl))
|
||||
continue
|
||||
}
|
||||
for i, name := range test.names {
|
||||
tmpl, ok := template.tmpl[name]
|
||||
if !ok {
|
||||
t.Errorf("%s: can't find template %q", test.name, name)
|
||||
continue
|
||||
}
|
||||
result := tmpl.Root.String()
|
||||
if result != test.results[i] {
|
||||
t.Errorf("%s=(%q): got\n\t%v\nexpected\n\t%v", test.name, test.input, result, test.results[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var multiExecTests = []execTest{
|
||||
{"empty", "", "", nil, true},
|
||||
{"text", "some text", "some text", nil, true},
|
||||
{"invoke x", `{{template "x" .SI}}`, "TEXT", tVal, true},
|
||||
{"invoke x no args", `{{template "x"}}`, "TEXT", tVal, true},
|
||||
{"invoke dot int", `{{template "dot" .I}}`, "17", tVal, true},
|
||||
{"invoke dot []int", `{{template "dot" .SI}}`, "[3 4 5]", tVal, true},
|
||||
{"invoke dotV", `{{template "dotV" .U}}`, "v", tVal, true},
|
||||
{"invoke nested int", `{{template "nested" .I}}`, "17", tVal, true},
|
||||
{"variable declared by template", `{{template "nested" $x:=.SI}},{{index $x 1}}`, "[3 4 5],4", tVal, true},
|
||||
|
||||
// User-defined function: test argument evaluator.
|
||||
{"testFunc literal", `{{oneArg "joe"}}`, "oneArg=joe", tVal, true},
|
||||
{"testFunc .", `{{oneArg .}}`, "oneArg=joe", "joe", true},
|
||||
}
|
||||
|
||||
// These strings are also in testdata/*.
|
||||
const multiText1 = `
|
||||
{{define "x"}}TEXT{{end}}
|
||||
{{define "dotV"}}{{.V}}{{end}}
|
||||
`
|
||||
|
||||
const multiText2 = `
|
||||
{{define "dot"}}{{.}}{{end}}
|
||||
{{define "nested"}}{{template "dot" .}}{{end}}
|
||||
`
|
||||
|
||||
func TestMultiExecute(t *testing.T) {
|
||||
// Declare a couple of templates first.
|
||||
template, err := New("root").Parse(multiText1)
|
||||
if err != nil {
|
||||
t.Fatalf("parse error for 1: %s", err)
|
||||
}
|
||||
_, err = template.Parse(multiText2)
|
||||
if err != nil {
|
||||
t.Fatalf("parse error for 2: %s", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseFiles(t *testing.T) {
|
||||
_, err := ParseFiles("DOES NOT EXIST")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent file; got none")
|
||||
}
|
||||
template := New("root")
|
||||
_, err = template.ParseFiles("testdata/file1.tmpl", "testdata/file2.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseGlob(t *testing.T) {
|
||||
_, err := ParseGlob("DOES NOT EXIST")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent file; got none")
|
||||
}
|
||||
_, err = New("error").ParseGlob("[x")
|
||||
if err == nil {
|
||||
t.Error("expected error for bad pattern; got none")
|
||||
}
|
||||
template := New("root")
|
||||
_, err = template.ParseGlob("testdata/file*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
// In these tests, actual content (not just template definitions) comes from the parsed files.
|
||||
|
||||
var templateFileExecTests = []execTest{
|
||||
{"test", `{{template "tmpl1.tmpl"}}{{template "tmpl2.tmpl"}}`, "template1\n\ny\ntemplate2\n\nx\n", 0, true},
|
||||
}
|
||||
|
||||
func TestParseFilesWithData(t *testing.T) {
|
||||
template, err := New("root").ParseFiles("testdata/tmpl1.tmpl", "testdata/tmpl2.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(templateFileExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseGlobWithData(t *testing.T) {
|
||||
template, err := New("root").ParseGlob("testdata/tmpl*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(templateFileExecTests, template, t)
|
||||
}
|
||||
|
||||
const (
|
||||
cloneText1 = `{{define "a"}}{{template "b"}}{{template "c"}}{{end}}`
|
||||
cloneText2 = `{{define "b"}}b{{end}}`
|
||||
cloneText3 = `{{define "c"}}root{{end}}`
|
||||
cloneText4 = `{{define "c"}}clone{{end}}`
|
||||
)
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
// Create some templates and clone the root.
|
||||
root, err := New("root").Parse(cloneText1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = root.Parse(cloneText2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clone := Must(root.Clone())
|
||||
// Add variants to both.
|
||||
_, err = root.Parse(cloneText3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = clone.Parse(cloneText4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Verify that the clone is self-consistent.
|
||||
for k, v := range clone.tmpl {
|
||||
if k == clone.name && v.tmpl[k] != clone {
|
||||
t.Error("clone does not contain root")
|
||||
}
|
||||
if v != v.tmpl[v.name] {
|
||||
t.Errorf("clone does not contain self for %q", k)
|
||||
}
|
||||
}
|
||||
// Execute root.
|
||||
var b bytes.Buffer
|
||||
err = root.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.String() != "broot" {
|
||||
t.Errorf("expected %q got %q", "broot", b.String())
|
||||
}
|
||||
// Execute copy.
|
||||
b.Reset()
|
||||
err = clone.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.String() != "bclone" {
|
||||
t.Errorf("expected %q got %q", "bclone", b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddParseTree(t *testing.T) {
|
||||
// Create some templates.
|
||||
root, err := New("root").Parse(cloneText1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = root.Parse(cloneText2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Add a new parse tree.
|
||||
tree, err := parse.Parse("cloneText3", cloneText3, "", "", nil, builtins)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
added, err := root.AddParseTree("c", tree["c"])
|
||||
// Execute.
|
||||
var b bytes.Buffer
|
||||
err = added.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.String() != "broot" {
|
||||
t.Errorf("expected %q got %q", "broot", b.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 7032
|
||||
func TestAddParseTreeToUnparsedTemplate(t *testing.T) {
|
||||
master := "{{define \"master\"}}{{end}}"
|
||||
tmpl := New("master")
|
||||
tree, err := parse.Parse("master", master, "", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parse err: %v", err)
|
||||
}
|
||||
masterTree := tree["master"]
|
||||
tmpl.AddParseTree("master", masterTree) // used to panic
|
||||
}
|
||||
|
||||
func TestRedefinition(t *testing.T) {
|
||||
var tmpl *Template
|
||||
var err error
|
||||
if tmpl, err = New("tmpl1").Parse(`{{define "test"}}foo{{end}}`); err != nil {
|
||||
t.Fatalf("parse 1: %v", err)
|
||||
}
|
||||
if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "redefinition") {
|
||||
t.Fatalf("expected redefinition error; got %v", err)
|
||||
}
|
||||
if _, err = tmpl.New("tmpl2").Parse(`{{define "test"}}bar{{end}}`); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "redefinition") {
|
||||
t.Fatalf("expected redefinition error; got %v", err)
|
||||
}
|
||||
}
|
||||
+468
@@ -0,0 +1,468 @@
|
||||
// Copyright 2011 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.
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Make the types prettyprint.
|
||||
var itemName = map[itemType]string{
|
||||
itemError: "error",
|
||||
itemBool: "bool",
|
||||
itemChar: "char",
|
||||
itemCharConstant: "charconst",
|
||||
itemComplex: "complex",
|
||||
itemColonEquals: ":=",
|
||||
itemEOF: "EOF",
|
||||
itemField: "field",
|
||||
itemIdentifier: "identifier",
|
||||
itemLeftDelim: "left delim",
|
||||
itemLeftParen: "(",
|
||||
itemNumber: "number",
|
||||
itemPipe: "pipe",
|
||||
itemRawString: "raw string",
|
||||
itemRightDelim: "right delim",
|
||||
itemElideNewline: "elide newline",
|
||||
itemRightParen: ")",
|
||||
itemSpace: "space",
|
||||
itemString: "string",
|
||||
itemVariable: "variable",
|
||||
|
||||
// keywords
|
||||
itemDot: ".",
|
||||
itemDefine: "define",
|
||||
itemElse: "else",
|
||||
itemIf: "if",
|
||||
itemEnd: "end",
|
||||
itemNil: "nil",
|
||||
itemRange: "range",
|
||||
itemTemplate: "template",
|
||||
itemWith: "with",
|
||||
}
|
||||
|
||||
func (i itemType) String() string {
|
||||
s := itemName[i]
|
||||
if s == "" {
|
||||
return fmt.Sprintf("item%d", int(i))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type lexTest struct {
|
||||
name string
|
||||
input string
|
||||
items []item
|
||||
}
|
||||
|
||||
var (
|
||||
tEOF = item{itemEOF, 0, ""}
|
||||
tFor = item{itemIdentifier, 0, "for"}
|
||||
tLeft = item{itemLeftDelim, 0, "{{"}
|
||||
tLpar = item{itemLeftParen, 0, "("}
|
||||
tPipe = item{itemPipe, 0, "|"}
|
||||
tQuote = item{itemString, 0, `"abc \n\t\" "`}
|
||||
tRange = item{itemRange, 0, "range"}
|
||||
tRight = item{itemRightDelim, 0, "}}"}
|
||||
tElideNewline = item{itemElideNewline, 0, "\\"}
|
||||
tRpar = item{itemRightParen, 0, ")"}
|
||||
tSpace = item{itemSpace, 0, " "}
|
||||
raw = "`" + `abc\n\t\" ` + "`"
|
||||
tRawQuote = item{itemRawString, 0, raw}
|
||||
)
|
||||
|
||||
var lexTests = []lexTest{
|
||||
{"empty", "", []item{tEOF}},
|
||||
{"spaces", " \t\n", []item{{itemText, 0, " \t\n"}, tEOF}},
|
||||
{"text", `now is the time`, []item{{itemText, 0, "now is the time"}, tEOF}},
|
||||
{"elide newline", "{{}}\\", []item{tLeft, tRight, tElideNewline, tEOF}},
|
||||
{"text with comment", "hello-{{/* this is a comment */}}-world", []item{
|
||||
{itemText, 0, "hello-"},
|
||||
{itemText, 0, "-world"},
|
||||
tEOF,
|
||||
}},
|
||||
{"punctuation", "{{,@% }}", []item{
|
||||
tLeft,
|
||||
{itemChar, 0, ","},
|
||||
{itemChar, 0, "@"},
|
||||
{itemChar, 0, "%"},
|
||||
tSpace,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"parens", "{{((3))}}", []item{
|
||||
tLeft,
|
||||
tLpar,
|
||||
tLpar,
|
||||
{itemNumber, 0, "3"},
|
||||
tRpar,
|
||||
tRpar,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
|
||||
{"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}},
|
||||
{"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
|
||||
{"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
|
||||
{"numbers", "{{1 02 0x14 -7.2i 1e3 +1.2e-4 4.2i 1+2i}}", []item{
|
||||
tLeft,
|
||||
{itemNumber, 0, "1"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "02"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "0x14"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "-7.2i"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "1e3"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "+1.2e-4"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "4.2i"},
|
||||
tSpace,
|
||||
{itemComplex, 0, "1+2i"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"characters", `{{'a' '\n' '\'' '\\' '\u00FF' '\xFF' '本'}}`, []item{
|
||||
tLeft,
|
||||
{itemCharConstant, 0, `'a'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\n'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\''`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\\'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\u00FF'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\xFF'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'本'`},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"bools", "{{true false}}", []item{
|
||||
tLeft,
|
||||
{itemBool, 0, "true"},
|
||||
tSpace,
|
||||
{itemBool, 0, "false"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"dot", "{{.}}", []item{
|
||||
tLeft,
|
||||
{itemDot, 0, "."},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"nil", "{{nil}}", []item{
|
||||
tLeft,
|
||||
{itemNil, 0, "nil"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"dots", "{{.x . .2 .x.y.z}}", []item{
|
||||
tLeft,
|
||||
{itemField, 0, ".x"},
|
||||
tSpace,
|
||||
{itemDot, 0, "."},
|
||||
tSpace,
|
||||
{itemNumber, 0, ".2"},
|
||||
tSpace,
|
||||
{itemField, 0, ".x"},
|
||||
{itemField, 0, ".y"},
|
||||
{itemField, 0, ".z"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"keywords", "{{range if else end with}}", []item{
|
||||
tLeft,
|
||||
{itemRange, 0, "range"},
|
||||
tSpace,
|
||||
{itemIf, 0, "if"},
|
||||
tSpace,
|
||||
{itemElse, 0, "else"},
|
||||
tSpace,
|
||||
{itemEnd, 0, "end"},
|
||||
tSpace,
|
||||
{itemWith, 0, "with"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{
|
||||
tLeft,
|
||||
{itemVariable, 0, "$c"},
|
||||
tSpace,
|
||||
{itemColonEquals, 0, ":="},
|
||||
tSpace,
|
||||
{itemIdentifier, 0, "printf"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$hello"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$23"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$var"},
|
||||
{itemField, 0, ".Field"},
|
||||
tSpace,
|
||||
{itemField, 0, ".Method"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"variable invocation", "{{$x 23}}", []item{
|
||||
tLeft,
|
||||
{itemVariable, 0, "$x"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "23"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"pipeline", `intro {{echo hi 1.2 |noargs|args 1 "hi"}} outro`, []item{
|
||||
{itemText, 0, "intro "},
|
||||
tLeft,
|
||||
{itemIdentifier, 0, "echo"},
|
||||
tSpace,
|
||||
{itemIdentifier, 0, "hi"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "1.2"},
|
||||
tSpace,
|
||||
tPipe,
|
||||
{itemIdentifier, 0, "noargs"},
|
||||
tPipe,
|
||||
{itemIdentifier, 0, "args"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "1"},
|
||||
tSpace,
|
||||
{itemString, 0, `"hi"`},
|
||||
tRight,
|
||||
{itemText, 0, " outro"},
|
||||
tEOF,
|
||||
}},
|
||||
{"declaration", "{{$v := 3}}", []item{
|
||||
tLeft,
|
||||
{itemVariable, 0, "$v"},
|
||||
tSpace,
|
||||
{itemColonEquals, 0, ":="},
|
||||
tSpace,
|
||||
{itemNumber, 0, "3"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"2 declarations", "{{$v , $w := 3}}", []item{
|
||||
tLeft,
|
||||
{itemVariable, 0, "$v"},
|
||||
tSpace,
|
||||
{itemChar, 0, ","},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$w"},
|
||||
tSpace,
|
||||
{itemColonEquals, 0, ":="},
|
||||
tSpace,
|
||||
{itemNumber, 0, "3"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"field of parenthesized expression", "{{(.X).Y}}", []item{
|
||||
tLeft,
|
||||
tLpar,
|
||||
{itemField, 0, ".X"},
|
||||
tRpar,
|
||||
{itemField, 0, ".Y"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
// errors
|
||||
{"badchar", "#{{\x01}}", []item{
|
||||
{itemText, 0, "#"},
|
||||
tLeft,
|
||||
{itemError, 0, "unrecognized character in action: U+0001"},
|
||||
}},
|
||||
{"unclosed action", "{{\n}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, "unclosed action"},
|
||||
}},
|
||||
{"EOF in action", "{{range", []item{
|
||||
tLeft,
|
||||
tRange,
|
||||
{itemError, 0, "unclosed action"},
|
||||
}},
|
||||
{"unclosed quote", "{{\"\n\"}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, "unterminated quoted string"},
|
||||
}},
|
||||
{"unclosed raw quote", "{{`xx\n`}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, "unterminated raw quoted string"},
|
||||
}},
|
||||
{"unclosed char constant", "{{'\n}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, "unterminated character constant"},
|
||||
}},
|
||||
{"bad number", "{{3k}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, `bad number syntax: "3k"`},
|
||||
}},
|
||||
{"unclosed paren", "{{(3}}", []item{
|
||||
tLeft,
|
||||
tLpar,
|
||||
{itemNumber, 0, "3"},
|
||||
{itemError, 0, `unclosed left paren`},
|
||||
}},
|
||||
{"extra right paren", "{{3)}}", []item{
|
||||
tLeft,
|
||||
{itemNumber, 0, "3"},
|
||||
tRpar,
|
||||
{itemError, 0, `unexpected right paren U+0029 ')'`},
|
||||
}},
|
||||
|
||||
// Fixed bugs
|
||||
// Many elements in an action blew the lookahead until
|
||||
// we made lexInsideAction not loop.
|
||||
{"long pipeline deadlock", "{{|||||}}", []item{
|
||||
tLeft,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"text with bad comment", "hello-{{/*/}}-world", []item{
|
||||
{itemText, 0, "hello-"},
|
||||
{itemError, 0, `unclosed comment`},
|
||||
}},
|
||||
{"text with comment close separted from delim", "hello-{{/* */ }}-world", []item{
|
||||
{itemText, 0, "hello-"},
|
||||
{itemError, 0, `comment ends before closing delimiter`},
|
||||
}},
|
||||
// This one is an error that we can't catch because it breaks templates with
|
||||
// minimized JavaScript. Should have fixed it before Go 1.1.
|
||||
{"unmatched right delimiter", "hello-{.}}-world", []item{
|
||||
{itemText, 0, "hello-{.}}-world"},
|
||||
tEOF,
|
||||
}},
|
||||
}
|
||||
|
||||
// collect gathers the emitted items into a slice.
|
||||
func collect(t *lexTest, left, right string) (items []item) {
|
||||
l := lex(t.name, t.input, left, right)
|
||||
for {
|
||||
item := l.nextItem()
|
||||
items = append(items, item)
|
||||
if item.typ == itemEOF || item.typ == itemError {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func equal(i1, i2 []item, checkPos bool) bool {
|
||||
if len(i1) != len(i2) {
|
||||
return false
|
||||
}
|
||||
for k := range i1 {
|
||||
if i1[k].typ != i2[k].typ {
|
||||
return false
|
||||
}
|
||||
if i1[k].val != i2[k].val {
|
||||
return false
|
||||
}
|
||||
if checkPos && i1[k].pos != i2[k].pos {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestLex(t *testing.T) {
|
||||
for _, test := range lexTests {
|
||||
items := collect(&test, "", "")
|
||||
if !equal(items, test.items, false) {
|
||||
t.Errorf("%s: got\n\t%+v\nexpected\n\t%v", test.name, items, test.items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some easy cases from above, but with delimiters $$ and @@
|
||||
var lexDelimTests = []lexTest{
|
||||
{"punctuation", "$$,@%{{}}@@", []item{
|
||||
tLeftDelim,
|
||||
{itemChar, 0, ","},
|
||||
{itemChar, 0, "@"},
|
||||
{itemChar, 0, "%"},
|
||||
{itemChar, 0, "{"},
|
||||
{itemChar, 0, "{"},
|
||||
{itemChar, 0, "}"},
|
||||
{itemChar, 0, "}"},
|
||||
tRightDelim,
|
||||
tEOF,
|
||||
}},
|
||||
{"empty action", `$$@@`, []item{tLeftDelim, tRightDelim, tEOF}},
|
||||
{"for", `$$for@@`, []item{tLeftDelim, tFor, tRightDelim, tEOF}},
|
||||
{"quote", `$$"abc \n\t\" "@@`, []item{tLeftDelim, tQuote, tRightDelim, tEOF}},
|
||||
{"raw quote", "$$" + raw + "@@", []item{tLeftDelim, tRawQuote, tRightDelim, tEOF}},
|
||||
}
|
||||
|
||||
var (
|
||||
tLeftDelim = item{itemLeftDelim, 0, "$$"}
|
||||
tRightDelim = item{itemRightDelim, 0, "@@"}
|
||||
)
|
||||
|
||||
func TestDelims(t *testing.T) {
|
||||
for _, test := range lexDelimTests {
|
||||
items := collect(&test, "$$", "@@")
|
||||
if !equal(items, test.items, false) {
|
||||
t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lexPosTests = []lexTest{
|
||||
{"empty", "", []item{tEOF}},
|
||||
{"punctuation", "{{,@%#}}", []item{
|
||||
{itemLeftDelim, 0, "{{"},
|
||||
{itemChar, 2, ","},
|
||||
{itemChar, 3, "@"},
|
||||
{itemChar, 4, "%"},
|
||||
{itemChar, 5, "#"},
|
||||
{itemRightDelim, 6, "}}"},
|
||||
{itemEOF, 8, ""},
|
||||
}},
|
||||
{"sample", "0123{{hello}}xyz", []item{
|
||||
{itemText, 0, "0123"},
|
||||
{itemLeftDelim, 4, "{{"},
|
||||
{itemIdentifier, 6, "hello"},
|
||||
{itemRightDelim, 11, "}}"},
|
||||
{itemText, 13, "xyz"},
|
||||
{itemEOF, 16, ""},
|
||||
}},
|
||||
}
|
||||
|
||||
// The other tests don't check position, to make the test cases easier to construct.
|
||||
// This one does.
|
||||
func TestPos(t *testing.T) {
|
||||
for _, test := range lexPosTests {
|
||||
items := collect(&test, "", "")
|
||||
if !equal(items, test.items, true) {
|
||||
t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
|
||||
if len(items) == len(test.items) {
|
||||
// Detailed print; avoid item.String() to expose the position value.
|
||||
for i := range items {
|
||||
if !equal(items[i:i+1], test.items[i:i+1], true) {
|
||||
i1 := items[i]
|
||||
i2 := test.items[i]
|
||||
t.Errorf("\t#%d: got {%v %d %q} expected {%v %d %q}", i, i1.typ, i1.pos, i1.val, i2.typ, i2.pos, i2.val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
// Copyright 2011 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.
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var debug = flag.Bool("debug", false, "show the errors produced by the main tests")
|
||||
|
||||
type numberTest struct {
|
||||
text string
|
||||
isInt bool
|
||||
isUint bool
|
||||
isFloat bool
|
||||
isComplex bool
|
||||
int64
|
||||
uint64
|
||||
float64
|
||||
complex128
|
||||
}
|
||||
|
||||
var numberTests = []numberTest{
|
||||
// basics
|
||||
{"0", true, true, true, false, 0, 0, 0, 0},
|
||||
{"-0", true, true, true, false, 0, 0, 0, 0}, // check that -0 is a uint.
|
||||
{"73", true, true, true, false, 73, 73, 73, 0},
|
||||
{"073", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0x73", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
{"-73", true, false, true, false, -73, 0, -73, 0},
|
||||
{"+73", true, false, true, false, 73, 0, 73, 0},
|
||||
{"100", true, true, true, false, 100, 100, 100, 0},
|
||||
{"1e9", true, true, true, false, 1e9, 1e9, 1e9, 0},
|
||||
{"-1e9", true, false, true, false, -1e9, 0, -1e9, 0},
|
||||
{"-1.2", false, false, true, false, 0, 0, -1.2, 0},
|
||||
{"1e19", false, true, true, false, 0, 1e19, 1e19, 0},
|
||||
{"-1e19", false, false, true, false, 0, 0, -1e19, 0},
|
||||
{"4i", false, false, false, true, 0, 0, 0, 4i},
|
||||
{"-1.2+4.2i", false, false, false, true, 0, 0, 0, -1.2 + 4.2i},
|
||||
{"073i", false, false, false, true, 0, 0, 0, 73i}, // not octal!
|
||||
// complex with 0 imaginary are float (and maybe integer)
|
||||
{"0i", true, true, true, true, 0, 0, 0, 0},
|
||||
{"-1.2+0i", false, false, true, true, 0, 0, -1.2, -1.2},
|
||||
{"-12+0i", true, false, true, true, -12, 0, -12, -12},
|
||||
{"13+0i", true, true, true, true, 13, 13, 13, 13},
|
||||
// funny bases
|
||||
{"0123", true, true, true, false, 0123, 0123, 0123, 0},
|
||||
{"-0x0", true, true, true, false, 0, 0, 0, 0},
|
||||
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
|
||||
// character constants
|
||||
{`'a'`, true, true, true, false, 'a', 'a', 'a', 0},
|
||||
{`'\n'`, true, true, true, false, '\n', '\n', '\n', 0},
|
||||
{`'\\'`, true, true, true, false, '\\', '\\', '\\', 0},
|
||||
{`'\''`, true, true, true, false, '\'', '\'', '\'', 0},
|
||||
{`'\xFF'`, true, true, true, false, 0xFF, 0xFF, 0xFF, 0},
|
||||
{`'パ'`, true, true, true, false, 0x30d1, 0x30d1, 0x30d1, 0},
|
||||
{`'\u30d1'`, true, true, true, false, 0x30d1, 0x30d1, 0x30d1, 0},
|
||||
{`'\U000030d1'`, true, true, true, false, 0x30d1, 0x30d1, 0x30d1, 0},
|
||||
// some broken syntax
|
||||
{text: "+-2"},
|
||||
{text: "0x123."},
|
||||
{text: "1e."},
|
||||
{text: "0xi."},
|
||||
{text: "1+2."},
|
||||
{text: "'x"},
|
||||
{text: "'xx'"},
|
||||
// Issue 8622 - 0xe parsed as floating point. Very embarrassing.
|
||||
{"0xef", true, true, true, false, 0xef, 0xef, 0xef, 0},
|
||||
}
|
||||
|
||||
func TestNumberParse(t *testing.T) {
|
||||
for _, test := range numberTests {
|
||||
// If fmt.Sscan thinks it's complex, it's complex. We can't trust the output
|
||||
// because imaginary comes out as a number.
|
||||
var c complex128
|
||||
typ := itemNumber
|
||||
var tree *Tree
|
||||
if test.text[0] == '\'' {
|
||||
typ = itemCharConstant
|
||||
} else {
|
||||
_, err := fmt.Sscan(test.text, &c)
|
||||
if err == nil {
|
||||
typ = itemComplex
|
||||
}
|
||||
}
|
||||
n, err := tree.newNumber(0, test.text, typ)
|
||||
ok := test.isInt || test.isUint || test.isFloat || test.isComplex
|
||||
if ok && err != nil {
|
||||
t.Errorf("unexpected error for %q: %s", test.text, err)
|
||||
continue
|
||||
}
|
||||
if !ok && err == nil {
|
||||
t.Errorf("expected error for %q", test.text)
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
if *debug {
|
||||
fmt.Printf("%s\n\t%s\n", test.text, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if n.IsComplex != test.isComplex {
|
||||
t.Errorf("complex incorrect for %q; should be %t", test.text, test.isComplex)
|
||||
}
|
||||
if test.isInt {
|
||||
if !n.IsInt {
|
||||
t.Errorf("expected integer for %q", test.text)
|
||||
}
|
||||
if n.Int64 != test.int64 {
|
||||
t.Errorf("int64 for %q should be %d Is %d", test.text, test.int64, n.Int64)
|
||||
}
|
||||
} else if n.IsInt {
|
||||
t.Errorf("did not expect integer for %q", test.text)
|
||||
}
|
||||
if test.isUint {
|
||||
if !n.IsUint {
|
||||
t.Errorf("expected unsigned integer for %q", test.text)
|
||||
}
|
||||
if n.Uint64 != test.uint64 {
|
||||
t.Errorf("uint64 for %q should be %d Is %d", test.text, test.uint64, n.Uint64)
|
||||
}
|
||||
} else if n.IsUint {
|
||||
t.Errorf("did not expect unsigned integer for %q", test.text)
|
||||
}
|
||||
if test.isFloat {
|
||||
if !n.IsFloat {
|
||||
t.Errorf("expected float for %q", test.text)
|
||||
}
|
||||
if n.Float64 != test.float64 {
|
||||
t.Errorf("float64 for %q should be %g Is %g", test.text, test.float64, n.Float64)
|
||||
}
|
||||
} else if n.IsFloat {
|
||||
t.Errorf("did not expect float for %q", test.text)
|
||||
}
|
||||
if test.isComplex {
|
||||
if !n.IsComplex {
|
||||
t.Errorf("expected complex for %q", test.text)
|
||||
}
|
||||
if n.Complex128 != test.complex128 {
|
||||
t.Errorf("complex128 for %q should be %g Is %g", test.text, test.complex128, n.Complex128)
|
||||
}
|
||||
} else if n.IsComplex {
|
||||
t.Errorf("did not expect complex for %q", test.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type parseTest struct {
|
||||
name string
|
||||
input string
|
||||
ok bool
|
||||
result string // what the user would see in an error message.
|
||||
}
|
||||
|
||||
const (
|
||||
noError = true
|
||||
hasError = false
|
||||
)
|
||||
|
||||
var parseTests = []parseTest{
|
||||
{"empty", "", noError,
|
||||
``},
|
||||
{"comment", "{{/*\n\n\n*/}}", noError,
|
||||
``},
|
||||
{"spaces", " \t\n", noError,
|
||||
`" \t\n"`},
|
||||
{"text", "some text", noError,
|
||||
`"some text"`},
|
||||
{"emptyAction", "{{}}", hasError,
|
||||
`{{}}`},
|
||||
{"field", "{{.X}}", noError,
|
||||
`{{.X}}`},
|
||||
{"simple command", "{{printf}}", noError,
|
||||
`{{printf}}`},
|
||||
{"$ invocation", "{{$}}", noError,
|
||||
"{{$}}"},
|
||||
{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
|
||||
"{{with $x := 3}}{{$x 23}}{{end}}"},
|
||||
{"variable with fields", "{{$.I}}", noError,
|
||||
"{{$.I}}"},
|
||||
{"multi-word command", "{{printf `%d` 23}}", noError,
|
||||
"{{printf `%d` 23}}"},
|
||||
{"pipeline", "{{.X|.Y}}", noError,
|
||||
`{{.X | .Y}}`},
|
||||
{"pipeline with decl", "{{$x := .X|.Y}}", noError,
|
||||
`{{$x := .X | .Y}}`},
|
||||
{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
|
||||
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
|
||||
{"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
|
||||
`{{(.Y .Z).Field}}`},
|
||||
{"simple if", "{{if .X}}hello{{end}}", noError,
|
||||
`{{if .X}}"hello"{{end}}`},
|
||||
{"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}"false"{{end}}`},
|
||||
{"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`},
|
||||
{"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
|
||||
`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`},
|
||||
{"simple range", "{{range .X}}hello{{end}}", noError,
|
||||
`{{range .X}}"hello"{{end}}`},
|
||||
{"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
|
||||
`{{range .X.Y.Z}}"hello"{{end}}`},
|
||||
{"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
|
||||
`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`},
|
||||
{"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X}}"true"{{else}}"false"{{end}}`},
|
||||
{"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X | .M}}"true"{{else}}"false"{{end}}`},
|
||||
{"range []int", "{{range .SI}}{{.}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{end}}`},
|
||||
{"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x := .SI}}{{.}}{{end}}`},
|
||||
{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x, $y := .SI}}{{.}}{{end}}`},
|
||||
{"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
|
||||
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
|
||||
{"template", "{{template `x`}}", noError,
|
||||
`{{template "x"}}`},
|
||||
{"template with arg", "{{template `x` .Y}}", noError,
|
||||
`{{template "x" .Y}}`},
|
||||
{"with", "{{with .X}}hello{{end}}", noError,
|
||||
`{{with .X}}"hello"{{end}}`},
|
||||
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
|
||||
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`},
|
||||
{"elide newline", "{{true}}\\\n ", noError,
|
||||
`{{true}}" "`},
|
||||
// Errors.
|
||||
{"unclosed action", "hello{{range", hasError, ""},
|
||||
{"unmatched end", "{{end}}", hasError, ""},
|
||||
{"missing end", "hello{{range .x}}", hasError, ""},
|
||||
{"missing end after else", "hello{{range .x}}{{else}}", hasError, ""},
|
||||
{"undefined function", "hello{{undefined}}", hasError, ""},
|
||||
{"undefined variable", "{{$x}}", hasError, ""},
|
||||
{"variable undefined after end", "{{with $x := 4}}{{end}}{{$x}}", hasError, ""},
|
||||
{"variable undefined in template", "{{template $v}}", hasError, ""},
|
||||
{"declare with field", "{{with $x.Y := 4}}{{end}}", hasError, ""},
|
||||
{"template with field ref", "{{template .X}}", hasError, ""},
|
||||
{"template with var", "{{template $v}}", hasError, ""},
|
||||
{"invalid punctuation", "{{printf 3, 4}}", hasError, ""},
|
||||
{"multidecl outside range", "{{with $v, $u := 3}}{{end}}", hasError, ""},
|
||||
{"too many decls in range", "{{range $u, $v, $w := 3}}{{end}}", hasError, ""},
|
||||
{"dot applied to parentheses", "{{printf (printf .).}}", hasError, ""},
|
||||
{"adjacent args", "{{printf 3`x`}}", hasError, ""},
|
||||
{"adjacent args with .", "{{printf `x`.}}", hasError, ""},
|
||||
{"extra end after if", "{{if .X}}a{{else if .Y}}b{{end}}{{end}}", hasError, ""},
|
||||
{"invalid newline elision", "{{true}}\\{{true}}", hasError, ""},
|
||||
// Equals (and other chars) do not assignments make (yet).
|
||||
{"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"},
|
||||
{"bug0b", "{{$x = 1}}{{$x}}", hasError, ""},
|
||||
{"bug0c", "{{$x ! 2}}{{$x}}", hasError, ""},
|
||||
{"bug0d", "{{$x % 3}}{{$x}}", hasError, ""},
|
||||
// Check the parse fails for := rather than comma.
|
||||
{"bug0e", "{{range $x := $y := 3}}{{end}}", hasError, ""},
|
||||
// Another bug: variable read must ignore following punctuation.
|
||||
{"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""}, // ! is just illegal here.
|
||||
{"bug1b", "{{$x:=.}}{{$x+2}}", hasError, ""}, // $x+2 should not parse as ($x) (+2).
|
||||
{"bug1c", "{{$x:=.}}{{$x +2}}", noError, "{{$x := .}}{{$x +2}}"}, // It's OK with a space.
|
||||
}
|
||||
|
||||
var builtins = map[string]interface{}{
|
||||
"printf": fmt.Sprintf,
|
||||
}
|
||||
|
||||
func testParse(doCopy bool, t *testing.T) {
|
||||
textFormat = "%q"
|
||||
defer func() { textFormat = "%s" }()
|
||||
for _, test := range parseTests {
|
||||
tmpl, err := New(test.name).Parse(test.input, "", "", make(map[string]*Tree), builtins)
|
||||
switch {
|
||||
case err == nil && !test.ok:
|
||||
t.Errorf("%q: expected error; got none", test.name)
|
||||
continue
|
||||
case err != nil && test.ok:
|
||||
t.Errorf("%q: unexpected error: %v", test.name, err)
|
||||
continue
|
||||
case err != nil && !test.ok:
|
||||
// expected error, got one
|
||||
if *debug {
|
||||
fmt.Printf("%s: %s\n\t%s\n", test.name, test.input, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
var result string
|
||||
if doCopy {
|
||||
result = tmpl.Root.Copy().String()
|
||||
} else {
|
||||
result = tmpl.Root.String()
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("%s=(%q): got\n\t%v\nexpected\n\t%v", test.name, test.input, result, test.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
testParse(false, t)
|
||||
}
|
||||
|
||||
// Same as TestParse, but we copy the node first
|
||||
func TestParseCopy(t *testing.T) {
|
||||
testParse(true, t)
|
||||
}
|
||||
|
||||
type isEmptyTest struct {
|
||||
name string
|
||||
input string
|
||||
empty bool
|
||||
}
|
||||
|
||||
var isEmptyTests = []isEmptyTest{
|
||||
{"empty", ``, true},
|
||||
{"nonempty", `hello`, false},
|
||||
{"spaces only", " \t\n \t\n", true},
|
||||
{"definition", `{{define "x"}}something{{end}}`, true},
|
||||
{"definitions and space", "{{define `x`}}something{{end}}\n\n{{define `y`}}something{{end}}\n\n", true},
|
||||
{"definitions and text", "{{define `x`}}something{{end}}\nx\n{{define `y`}}something{{end}}\ny\n", false},
|
||||
{"definition and action", "{{define `x`}}something{{end}}{{if 3}}foo{{end}}", false},
|
||||
}
|
||||
|
||||
func TestIsEmpty(t *testing.T) {
|
||||
if !IsEmptyTree(nil) {
|
||||
t.Errorf("nil tree is not empty")
|
||||
}
|
||||
for _, test := range isEmptyTests {
|
||||
tree, err := New("root").Parse(test.input, "", "", make(map[string]*Tree), nil)
|
||||
if err != nil {
|
||||
t.Errorf("%q: unexpected error: %v", test.name, err)
|
||||
continue
|
||||
}
|
||||
if empty := IsEmptyTree(tree.Root); empty != test.empty {
|
||||
t.Errorf("%q: expected %t got %t", test.name, test.empty, empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorContextWithTreeCopy(t *testing.T) {
|
||||
tree, err := New("root").Parse("{{if true}}{{end}}", "", "", make(map[string]*Tree), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected tree parse failure: %v", err)
|
||||
}
|
||||
treeCopy := tree.Copy()
|
||||
wantLocation, wantContext := tree.ErrorContext(tree.Root.Nodes[0])
|
||||
gotLocation, gotContext := treeCopy.ErrorContext(treeCopy.Root.Nodes[0])
|
||||
if wantLocation != gotLocation {
|
||||
t.Errorf("wrong error location want %q got %q", wantLocation, gotLocation)
|
||||
}
|
||||
if wantContext != gotContext {
|
||||
t.Errorf("wrong error location want %q got %q", wantContext, gotContext)
|
||||
}
|
||||
}
|
||||
|
||||
// All failures, and the result is a string that must appear in the error message.
|
||||
var errorTests = []parseTest{
|
||||
// Check line numbers are accurate.
|
||||
{"unclosed1",
|
||||
"line1\n{{",
|
||||
hasError, `unclosed1:2: unexpected unclosed action in command`},
|
||||
{"unclosed2",
|
||||
"line1\n{{define `x`}}line2\n{{",
|
||||
hasError, `unclosed2:3: unexpected unclosed action in command`},
|
||||
// Specific errors.
|
||||
{"function",
|
||||
"{{foo}}",
|
||||
hasError, `function "foo" not defined`},
|
||||
{"comment",
|
||||
"{{/*}}",
|
||||
hasError, `unclosed comment`},
|
||||
{"lparen",
|
||||
"{{.X (1 2 3}}",
|
||||
hasError, `unclosed left paren`},
|
||||
{"rparen",
|
||||
"{{.X 1 2 3)}}",
|
||||
hasError, `unexpected ")"`},
|
||||
{"space",
|
||||
"{{`x`3}}",
|
||||
hasError, `missing space?`},
|
||||
{"idchar",
|
||||
"{{a#}}",
|
||||
hasError, `'#'`},
|
||||
{"charconst",
|
||||
"{{'a}}",
|
||||
hasError, `unterminated character constant`},
|
||||
{"stringconst",
|
||||
`{{"a}}`,
|
||||
hasError, `unterminated quoted string`},
|
||||
{"rawstringconst",
|
||||
"{{`a}}",
|
||||
hasError, `unterminated raw quoted string`},
|
||||
{"number",
|
||||
"{{0xi}}",
|
||||
hasError, `number syntax`},
|
||||
{"multidefine",
|
||||
"{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
|
||||
hasError, `multiple definition of template`},
|
||||
{"eof",
|
||||
"{{range .X}}",
|
||||
hasError, `unexpected EOF`},
|
||||
{"variable",
|
||||
// Declare $x so it's defined, to avoid that error, and then check we don't parse a declaration.
|
||||
"{{$x := 23}}{{with $x.y := 3}}{{$x 23}}{{end}}",
|
||||
hasError, `unexpected ":="`},
|
||||
{"multidecl",
|
||||
"{{$a,$b,$c := 23}}",
|
||||
hasError, `too many declarations`},
|
||||
{"undefvar",
|
||||
"{{$a}}",
|
||||
hasError, `undefined variable`},
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
for _, test := range errorTests {
|
||||
_, err := New(test.name).Parse(test.input, "", "", make(map[string]*Tree))
|
||||
if err == nil {
|
||||
t.Errorf("%q: expected error", test.name)
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(err.Error(), test.result) {
|
||||
t.Errorf("%q: error %q does not contain %q", test.name, err, test.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# Units - Helpful unit multipliers and functions for Go
|
||||
|
||||
The goal of this package is to have functionality similar to the [time](http://golang.org/pkg/time/) package.
|
||||
|
||||
It allows for code like this:
|
||||
|
||||
```go
|
||||
n, err := ParseBase2Bytes("1KB")
|
||||
// n == 1024
|
||||
n = units.Mebibyte * 512
|
||||
```
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package units
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBase2BytesString(t *testing.T) {
|
||||
assert.Equal(t, Base2Bytes(0).String(), "0B")
|
||||
assert.Equal(t, Base2Bytes(1025).String(), "1KiB1B")
|
||||
assert.Equal(t, Base2Bytes(1048577).String(), "1MiB1B")
|
||||
}
|
||||
|
||||
func TestParseBase2Bytes(t *testing.T) {
|
||||
n, err := ParseBase2Bytes("0B")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, int(n))
|
||||
n, err = ParseBase2Bytes("1KB")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1024, int(n))
|
||||
n, err = ParseBase2Bytes("1MB1KB25B")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1049625, int(n))
|
||||
n, err = ParseBase2Bytes("1.5MB")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1572864, int(n))
|
||||
}
|
||||
|
||||
func TestMetricBytesString(t *testing.T) {
|
||||
assert.Equal(t, MetricBytes(0).String(), "0B")
|
||||
assert.Equal(t, MetricBytes(1001).String(), "1KB1B")
|
||||
assert.Equal(t, MetricBytes(1001025).String(), "1MB1KB25B")
|
||||
}
|
||||
|
||||
func TestParseMetricBytes(t *testing.T) {
|
||||
n, err := ParseMetricBytes("0B")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, int(n))
|
||||
n, err = ParseMetricBytes("1KB1B")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1001, int(n))
|
||||
n, err = ParseMetricBytes("1MB1KB25B")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1001025, int(n))
|
||||
n, err = ParseMetricBytes("1.5MB")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1500000, int(n))
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
genny
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
# - 1.0
|
||||
# - 1.1
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
# genny - Generics for Go
|
||||
|
||||
[](https://travis-ci.org/cheekybits/genny) [](http://godoc.org/github.com/cheekybits/genny/parse)
|
||||
|
||||
Install:
|
||||
|
||||
```
|
||||
go get github.com/cheekybits/genny
|
||||
```
|
||||
|
||||
=====
|
||||
|
||||
(pron. Jenny) by Mat Ryer ([@matryer](https://twitter.com/matryer)) and Tyler Bunnell ([@TylerJBunnell](https://twitter.com/TylerJBunnell)).
|
||||
|
||||
Until the Go core team include support for [generics in Go](http://golang.org/doc/faq#generics), `genny` is a code-generation generics solution. It allows you write normal buildable and testable Go code which, when processed by the `genny gen` tool, will replace the generics with specific types.
|
||||
|
||||
* Generic code is valid Go code
|
||||
* Generic code compiles and can be tested
|
||||
* Use `stdin` and `stdout` or specify in and out files
|
||||
* Supports Go 1.4's [go generate](http://tip.golang.org/doc/go1.4#gogenerate)
|
||||
* Multiple specific types will generate every permutation
|
||||
* Use `BUILTINS` and `NUMBERS` wildtype to generate specific code for all built-in (and number) Go types
|
||||
* Function names and comments also get updated
|
||||
|
||||
## Library
|
||||
|
||||
We have started building a [library of common things](https://github.com/cheekybits/gennylib), and you can use `genny get` to generate the specific versions you need.
|
||||
|
||||
For example: `genny get maps/concurrentmap.go "KeyType=BUILTINS ValueType=BUILTINS"` will print out generated code for all types for a concurrent map. Any file in the library may be generated locally in this way using all the same options given to `genny gen`.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
genny [{flags}] gen "{types}"
|
||||
|
||||
gen - generates type specific code from generic code.
|
||||
get <package/file> - fetch a generic template from the online library and gen it.
|
||||
|
||||
{types} - (optional) Command line flags (see below)
|
||||
{types} - (required) Specific types for each generic type in the source
|
||||
{types} format: {generic}={specific}[,another][ {generic2}={specific2}]
|
||||
|
||||
Examples:
|
||||
Generic=Specific
|
||||
Generic1=Specific1 Generic2=Specific2
|
||||
Generic1=Specific1,Specific2 Generic2=Specific3,Specific4
|
||||
|
||||
Flags:
|
||||
-in="": file to parse instead of stdin
|
||||
-out="": file to save output to instead of stdout
|
||||
-pkg="": package name for generated files
|
||||
```
|
||||
|
||||
* Comma separated type lists will generate code for each type
|
||||
|
||||
### Flags
|
||||
|
||||
* `-in` - specify the input file (rather than using stdin)
|
||||
* `-out` - specify the output file (rather than using stdout)
|
||||
|
||||
### go generate
|
||||
|
||||
To use Go 1.4's `go generate` capability, insert the following comment in your source code file:
|
||||
|
||||
```
|
||||
//go:generate genny -in=$GOFILE -out=gen-$GOFILE gen "KeyType=string,int ValueType=string,int"
|
||||
```
|
||||
|
||||
* Start the line with `//go:generate `
|
||||
* Use the `-in` and `-out` flags to specify the files to work on
|
||||
* Use the `genny` command as usual after the flags
|
||||
|
||||
Now, running `go generate` (in a shell) for the package will cause the generic versions of the files to be generated.
|
||||
|
||||
* The output file will be overwritten, so it's safe to call `go generate` many times
|
||||
* Use `$GOFILE` to refer to the current file
|
||||
* The `//go:generate` line will be removed from the output
|
||||
|
||||
To see a real example of how to use `genny` with `go generate`, look in the [example/go-generate directory](https://github.com/cheekybits/genny/tree/master/examples/go-generate).
|
||||
|
||||
## How it works
|
||||
|
||||
Define your generic types using the special `generic.Type` placeholder type:
|
||||
|
||||
```go
|
||||
type KeyType generic.Type
|
||||
type ValueType generic.Type
|
||||
```
|
||||
|
||||
* You can use as many as you like
|
||||
* Give them meaningful names
|
||||
|
||||
Then write the generic code referencing the types as your normally would:
|
||||
|
||||
```go
|
||||
func SetValueTypeForKeyType(key KeyType, value ValueType) { /* ... */ }
|
||||
```
|
||||
|
||||
* Generic type names will also be replaced in comments and function names (see Real example below)
|
||||
|
||||
Since `generic.Type` is a real Go type, your code will compile, and you can even write unit tests against your generic code.
|
||||
|
||||
#### Generating specific versions
|
||||
|
||||
Pass the file through the `genny gen` tool with the specific types as the argument:
|
||||
|
||||
```
|
||||
cat generic.go | genny gen "KeyType=string ValueType=interface{}"
|
||||
```
|
||||
|
||||
The output will be the complete Go source file with the generic types replaced with the types specified in the arguments.
|
||||
|
||||
## Real example
|
||||
|
||||
Given [this generic Go code](https://github.com/cheekybits/genny/tree/master/examples/queue) which compiles and is tested:
|
||||
|
||||
```go
|
||||
package queue
|
||||
|
||||
import "github.com/cheekybits/genny/generic"
|
||||
|
||||
// NOTE: this is how easy it is to define a generic type
|
||||
type Something generic.Type
|
||||
|
||||
// SomethingQueue is a queue of Somethings.
|
||||
type SomethingQueue struct {
|
||||
items []Something
|
||||
}
|
||||
|
||||
func NewSomethingQueue() *SomethingQueue {
|
||||
return &SomethingQueue{items: make([]Something, 0)}
|
||||
}
|
||||
func (q *SomethingQueue) Push(item Something) {
|
||||
q.items = append(q.items, item)
|
||||
}
|
||||
func (q *SomethingQueue) Pop() Something {
|
||||
item := q.items[0]
|
||||
q.items = q.items[1:]
|
||||
return item
|
||||
}
|
||||
```
|
||||
|
||||
When `genny gen` is invoked like this:
|
||||
|
||||
```
|
||||
cat source.go | genny gen "Something=string"
|
||||
```
|
||||
|
||||
It outputs:
|
||||
|
||||
```go
|
||||
// This file was automatically generated by genny.
|
||||
// Any changes will be lost if this file is regenerated.
|
||||
// see https://github.com/cheekybits/genny
|
||||
|
||||
package queue
|
||||
|
||||
// StringQueue is a queue of Strings.
|
||||
type StringQueue struct {
|
||||
items []string
|
||||
}
|
||||
|
||||
func NewStringQueue() *StringQueue {
|
||||
return &StringQueue{items: make([]string, 0)}
|
||||
}
|
||||
func (q *StringQueue) Push(item string) {
|
||||
q.items = append(q.items, item)
|
||||
}
|
||||
func (q *StringQueue) Pop() string {
|
||||
item := q.items[0]
|
||||
q.items = q.items[1:]
|
||||
return item
|
||||
}
|
||||
```
|
||||
|
||||
To get a _something_ for every built-in Go type plus one of your own types, you could run:
|
||||
|
||||
```
|
||||
cat source.go | genny gen "Something=BUILTINS,*MyType"
|
||||
```
|
||||
|
||||
#### More examples
|
||||
|
||||
Check out the [test code files](https://github.com/cheekybits/genny/tree/master/parse/test) for more real examples.
|
||||
|
||||
## Writing test code
|
||||
|
||||
Once you have defined a generic type with some code worth testing:
|
||||
|
||||
```go
|
||||
package slice
|
||||
|
||||
import (
|
||||
"log"
|
||||
"reflect"
|
||||
|
||||
"github.com/stretchr/gogen/generic"
|
||||
)
|
||||
|
||||
type MyType generic.Type
|
||||
|
||||
func EnsureMyTypeSlice(objectOrSlice interface{}) []MyType {
|
||||
log.Printf("%v", reflect.TypeOf(objectOrSlice))
|
||||
switch obj := objectOrSlice.(type) {
|
||||
case []MyType:
|
||||
log.Println(" returning it untouched")
|
||||
return obj
|
||||
case MyType:
|
||||
log.Println(" wrapping in slice")
|
||||
return []MyType{obj}
|
||||
default:
|
||||
panic("ensure slice needs MyType or []MyType")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can treat it like any normal Go type in your test code:
|
||||
|
||||
```go
|
||||
func TestEnsureMyTypeSlice(t *testing.T) {
|
||||
|
||||
myType := new(MyType)
|
||||
slice := EnsureMyTypeSlice(myType)
|
||||
if assert.NotNil(t, slice) {
|
||||
assert.Equal(t, slice[0], myType)
|
||||
}
|
||||
|
||||
slice = EnsureMyTypeSlice(slice)
|
||||
log.Printf("%#v", slice[0])
|
||||
if assert.NotNil(t, slice) {
|
||||
assert.Equal(t, slice[0], myType)
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Understanding what `generic.Type` is
|
||||
|
||||
Because `generic.Type` is an empty interface type (literally `interface{}`) every other type will be considered to be a `generic.Type` if you are switching on the type of an object. Of course, once the specific versions are generated, this issue goes away but it's worth knowing when you are writing your tests against generic code.
|
||||
|
||||
### Contributions
|
||||
|
||||
* See the [API documentation for the parse package](http://godoc.org/github.com/cheekybits/genny/parse)
|
||||
* Please do TDD
|
||||
* All input welcome
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Package main is the command line tool for Genny.
|
||||
package main
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/cheekybits/genny/parse"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
source | genny gen [-in=""] [-out=""] [-pkg=""] "KeyType=string,int ValueType=string,int"
|
||||
|
||||
*/
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
exitcodeInvalidArgs
|
||||
exitcodeInvalidTypeSet
|
||||
exitcodeStdinFailed
|
||||
exitcodeGenFailed
|
||||
exitcodeGetFailed
|
||||
exitcodeSourceFileInvalid
|
||||
exitcodeDestFileFailed
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
in = flag.String("in", "", "file to parse instead of stdin")
|
||||
out = flag.String("out", "", "file to save output to instead of stdout")
|
||||
pkgName = flag.String("pkg", "", "package name for generated files")
|
||||
prefix = "https://github.com/metabition/gennylib/raw/master/"
|
||||
)
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
if len(args) < 2 {
|
||||
usage()
|
||||
os.Exit(exitcodeInvalidArgs)
|
||||
}
|
||||
|
||||
if strings.ToLower(args[0]) != "gen" && strings.ToLower(args[0]) != "get" {
|
||||
usage()
|
||||
os.Exit(exitcodeInvalidArgs)
|
||||
}
|
||||
|
||||
// parse the typesets
|
||||
var setsArg = args[1]
|
||||
if strings.ToLower(args[0]) == "get" {
|
||||
setsArg = args[2]
|
||||
}
|
||||
typeSets, err := parse.TypeSet(setsArg)
|
||||
if err != nil {
|
||||
fatal(exitcodeInvalidTypeSet, err)
|
||||
}
|
||||
|
||||
var outWriter io.Writer
|
||||
if len(*out) > 0 {
|
||||
err := os.MkdirAll(path.Dir(*out), 0755)
|
||||
if err != nil {
|
||||
fatal(exitcodeDestFileFailed, err)
|
||||
}
|
||||
|
||||
outFile, err := os.Create(*out)
|
||||
if err != nil {
|
||||
fatal(exitcodeDestFileFailed, err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
outWriter = outFile
|
||||
} else {
|
||||
outWriter = os.Stdout
|
||||
}
|
||||
|
||||
if strings.ToLower(args[0]) == "get" {
|
||||
if len(args) != 3 {
|
||||
fmt.Println("not enough arguments to get")
|
||||
usage()
|
||||
os.Exit(exitcodeInvalidArgs)
|
||||
}
|
||||
r, err := http.Get(prefix + args[1])
|
||||
if err != nil {
|
||||
fatal(exitcodeGetFailed, err)
|
||||
}
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
fatal(exitcodeGetFailed, err)
|
||||
}
|
||||
r.Body.Close()
|
||||
br := bytes.NewReader(b)
|
||||
err = gen(*in, *pkgName, br, typeSets, outWriter)
|
||||
} else if len(*in) > 0 {
|
||||
var file *os.File
|
||||
file, err = os.Open(*in)
|
||||
if err != nil {
|
||||
fatal(exitcodeSourceFileInvalid, err)
|
||||
}
|
||||
defer file.Close()
|
||||
err = gen(*in, *pkgName, file, typeSets, outWriter)
|
||||
} else {
|
||||
var source []byte
|
||||
source, err = ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fatal(exitcodeStdinFailed, err)
|
||||
}
|
||||
reader := bytes.NewReader(source)
|
||||
err = gen("stdin", *pkgName, reader, typeSets, outWriter)
|
||||
}
|
||||
|
||||
// do the work
|
||||
if err != nil {
|
||||
fatal(exitcodeGenFailed, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, `usage: genny [{flags}] gen "{types}"
|
||||
|
||||
gen - generates type specific code from generic code.
|
||||
get <package/file> - fetch a generic template from the online library and gen it.
|
||||
|
||||
{flags} - (optional) Command line flags (see below)
|
||||
{types} - (required) Specific types for each generic type in the source
|
||||
{types} format: {generic}={specific}[,another][ {generic2}={specific2}]
|
||||
|
||||
Examples:
|
||||
Generic=Specific
|
||||
Generic1=Specific1 Generic2=Specific2
|
||||
Generic1=Specific1,Specific2 Generic2=Specific3,Specific4
|
||||
|
||||
Flags:`)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func fatal(code int, a ...interface{}) {
|
||||
fmt.Println(a...)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// gen performs the generic generation.
|
||||
func gen(filename, pkgName string, in io.ReadSeeker, typesets []map[string]string, out io.Writer) error {
|
||||
|
||||
var output []byte
|
||||
var err error
|
||||
|
||||
output, err = parse.Generics(filename, pkgName, in, typesets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out.Write(output)
|
||||
return nil
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
package main_test
|
||||
+1
@@ -0,0 +1 @@
|
||||
language: go
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/alecthomas/template"
|
||||
packages = [".","parse"]
|
||||
revision = "a0175ee3bccc567396460bf5acd36800cb10c49c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/alecthomas/units"
|
||||
packages = ["."]
|
||||
revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cheekybits/genny"
|
||||
packages = ["generic"]
|
||||
revision = "9127e812e1e9e501ce899a18121d316ecb52e4ba"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "6d212800a42e8ab5c146b8ace3490ee17e5225f9"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/fatih/camelcase"
|
||||
packages = ["."]
|
||||
revision = "f6a740d52f961c60348ebb109adde9f4635d7540"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "d8ed2627bdf02c080bf22230dbb337003b7aba2d"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
||||
version = "v1.1.4"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/alecthomas/kingpin.v2"
|
||||
packages = ["."]
|
||||
revision = "7f0871f2e17818990e4eed73f9b5c2f429501228"
|
||||
version = "v2.2.4"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/coryb/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "fb7cb9628c6e3bdd76c29fb91798d51a09832470"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/op/go-logging.v1"
|
||||
packages = ["."]
|
||||
revision = "b2cb9fa56473e98db8caba80237377e83fe44db5"
|
||||
version = "v1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "1879d7c016fcc8e210ca7fdeed4b099f4e7fc931d34ff47a170222c60eea8aab"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/cheekybits/genny"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fatih/camelcase"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.1.4"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/alecthomas/kingpin.v2"
|
||||
version = "2.2.4"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/coryb/yaml.v2"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/op/go-logging.v1"
|
||||
version = "1.0.0"
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
GENERATOR_SRC = \
|
||||
rawoption.go \
|
||||
$(NULL)
|
||||
|
||||
GENERATED_SRC = $(GENERATOR_SRC:%.go=gen-%.go)
|
||||
|
||||
test: $(GENERATED_SRC)
|
||||
go get -t -v
|
||||
go get github.com/kr/pretty
|
||||
go get gopkg.in/alecthomas/kingpin.v2
|
||||
go test
|
||||
|
||||
gen-%.go: %.go
|
||||
# use github.com/cheekybits/genny after https://github.com/cheekybits/genny/pull/42 is merged
|
||||
go get github.com/coryb/genny
|
||||
go generate
|
||||
|
||||
.PHONY: test
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
[](https://travis-ci.org/coryb/figtree)
|
||||
[](https://godoc.org/github.com/coryb/figtree)
|
||||
|
||||
Figtree is a go library to recusively parse and merge yaml based config files.
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptionsEnv(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
StringifyValue = true
|
||||
defer func() {
|
||||
StringifyValue = false
|
||||
}()
|
||||
|
||||
os.Clearenv()
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
got := []string{}
|
||||
for _, env := range os.Environ() {
|
||||
if strings.HasPrefix(env, "FIGTREE_") {
|
||||
got = append(got, env)
|
||||
}
|
||||
}
|
||||
|
||||
sort.StringSlice(got).Sort()
|
||||
|
||||
expected := []string{
|
||||
"FIGTREE_ARRAY_1=[\"d1arr1val1\",\"d1arr1val2\",\"dupval\"]",
|
||||
"FIGTREE_BOOL_1=true",
|
||||
"FIGTREE_FLOAT_1=1.11",
|
||||
"FIGTREE_INT_1=111",
|
||||
"FIGTREE_MAP_1={\"dup\":\"d1dupval\",\"key0\":\"d1map1val0\",\"key1\":\"d1map1val1\"}",
|
||||
"FIGTREE_STRING_1=d1str1val1",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, got)
|
||||
}
|
||||
|
||||
func TestOptionsNamedEnv(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
StringifyValue = true
|
||||
defer func() {
|
||||
StringifyValue = false
|
||||
}()
|
||||
|
||||
os.Clearenv()
|
||||
fig := NewFigTree()
|
||||
fig.EnvPrefix = "TEST"
|
||||
err := fig.LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
got := []string{}
|
||||
for _, env := range os.Environ() {
|
||||
if strings.HasPrefix(env, "FIGTREE_") || strings.HasPrefix(env, "TEST_") {
|
||||
got = append(got, env)
|
||||
}
|
||||
}
|
||||
|
||||
sort.StringSlice(got).Sort()
|
||||
|
||||
expected := []string{
|
||||
"TEST_ARRAY_1=[\"d1arr1val1\",\"d1arr1val2\",\"dupval\"]",
|
||||
"TEST_BOOL_1=true",
|
||||
"TEST_FLOAT_1=1.11",
|
||||
"TEST_INT_1=111",
|
||||
"TEST_MAP_1={\"dup\":\"d1dupval\",\"key0\":\"d1map1val0\",\"key1\":\"d1map1val1\"}",
|
||||
"TEST_STRING_1=d1str1val1",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, got)
|
||||
}
|
||||
|
||||
func TestBuiltinEnv(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
os.Clearenv()
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
got := []string{}
|
||||
for _, env := range os.Environ() {
|
||||
if strings.HasPrefix(env, "FIGTREE_") {
|
||||
got = append(got, env)
|
||||
}
|
||||
}
|
||||
|
||||
sort.StringSlice(got).Sort()
|
||||
|
||||
expected := []string{
|
||||
"FIGTREE_ARRAY_1=[\"d1arr1val1\",\"d1arr1val2\",\"dupval\"]",
|
||||
"FIGTREE_BOOL_1=true",
|
||||
"FIGTREE_FLOAT_1=1.11",
|
||||
"FIGTREE_INT_1=111",
|
||||
"FIGTREE_LEAVE_EMPTY=",
|
||||
"FIGTREE_MAP_1={\"dup\":\"d1dupval\",\"key0\":\"d1map1val0\",\"key1\":\"d1map1val1\"}",
|
||||
"FIGTREE_STRING_1=d1str1val1",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, got)
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptionsExecConfigD3(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"exec.yml", true, "d3arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"exec.yml", true, "d3arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../exec.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../exec.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../../exec.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../../exec.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"exec.yml", true, "d3str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../../exec.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"../exec.yml", true, "d2map1val1"},
|
||||
"key2": StringOption{"exec.yml", true, "d3map1val2"},
|
||||
"key3": StringOption{"exec.yml", true, "d3map1val3"},
|
||||
},
|
||||
Int1: IntOption{"exec.yml", true, 333},
|
||||
Float1: Float32Option{"exec.yml", true, 3.33},
|
||||
Bool1: BoolOption{"exec.yml", true, true},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("exec.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsExecConfigD2(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"exec.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"exec.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../exec.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../exec.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"exec.yml", true, "d2str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../exec.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"exec.yml", true, "d2map1val1"},
|
||||
"key2": StringOption{"exec.yml", true, "d2map1val2"},
|
||||
},
|
||||
Int1: IntOption{"exec.yml", true, 222},
|
||||
Float1: Float32Option{"exec.yml", true, 2.22},
|
||||
Bool1: BoolOption{"exec.yml", true, false},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("exec.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsExecConfigD1(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"exec.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"exec.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"exec.yml", true, "d1str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"exec.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"exec.yml", true, "d1map1val1"},
|
||||
},
|
||||
Int1: IntOption{"exec.yml", true, 111},
|
||||
Float1: Float32Option{"exec.yml", true, 1.11},
|
||||
Bool1: BoolOption{"exec.yml", true, true},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("exec.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinExecConfigD3(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d3arr1val1")
|
||||
arr1 = append(arr1, "d3arr1val2")
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d3str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d2map1val1",
|
||||
"key2": "d3map1val2",
|
||||
"key3": "d3map1val3",
|
||||
},
|
||||
Int1: 333,
|
||||
Float1: 3.33,
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("exec.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinExecConfigD2(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d2str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d2map1val1",
|
||||
"key2": "d2map1val2",
|
||||
},
|
||||
Int1: 222,
|
||||
Float1: 2.22,
|
||||
// note this will be true from d1/exec.yml since the
|
||||
// d1/d2/exec.yml set it to false which is a zero value
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("exec.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinExecConfigD1(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d1str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d1map1val1",
|
||||
},
|
||||
Int1: 111,
|
||||
Float1: 1.11,
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("exec.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
+137
-633
@@ -10,8 +10,6 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@@ -19,159 +17,69 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
yaml "gopkg.in/coryb/yaml.v2"
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Debugf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
type nullLogger struct{}
|
||||
|
||||
func (*nullLogger) Debugf(string, ...interface{}) {}
|
||||
|
||||
var Log Logger = &nullLogger{}
|
||||
|
||||
func defaultApplyChangeSet(changeSet map[string]*string) error {
|
||||
for k, v := range changeSet {
|
||||
if v != nil {
|
||||
os.Setenv(k, *v)
|
||||
} else {
|
||||
os.Unsetenv(k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Option func(*FigTree)
|
||||
|
||||
func WithHome(home string) Option {
|
||||
return func(f *FigTree) {
|
||||
f.home = home
|
||||
}
|
||||
}
|
||||
|
||||
func WithCwd(cwd string) Option {
|
||||
return func(f *FigTree) {
|
||||
f.workDir = cwd
|
||||
}
|
||||
}
|
||||
|
||||
func WithEnvPrefix(env string) Option {
|
||||
return func(f *FigTree) {
|
||||
f.envPrefix = env
|
||||
}
|
||||
}
|
||||
|
||||
func WithConfigDir(dir string) Option {
|
||||
return func(f *FigTree) {
|
||||
f.configDir = dir
|
||||
}
|
||||
}
|
||||
|
||||
type ChangeSetFunc func(map[string]*string) error
|
||||
|
||||
func WithApplyChangeSet(apply ChangeSetFunc) Option {
|
||||
return func(f *FigTree) {
|
||||
f.applyChangeSet = apply
|
||||
}
|
||||
}
|
||||
|
||||
type PreProcessor func([]byte) ([]byte, error)
|
||||
|
||||
func WithPreProcessor(pp PreProcessor) Option {
|
||||
return func(f *FigTree) {
|
||||
f.preProcessor = pp
|
||||
}
|
||||
}
|
||||
var log = logging.MustGetLogger("figtree")
|
||||
|
||||
type FigTree struct {
|
||||
home string
|
||||
workDir string
|
||||
configDir string
|
||||
envPrefix string
|
||||
preProcessor PreProcessor
|
||||
stop bool
|
||||
applyChangeSet ChangeSetFunc
|
||||
ConfigDir string
|
||||
Defaults interface{}
|
||||
EnvPrefix string
|
||||
stop bool
|
||||
}
|
||||
|
||||
func NewFigTree(opts ...Option) *FigTree {
|
||||
wd, _ := os.Getwd()
|
||||
fig := &FigTree{
|
||||
home: os.Getenv("HOME"),
|
||||
workDir: wd,
|
||||
envPrefix: "FIGTREE",
|
||||
applyChangeSet: defaultApplyChangeSet,
|
||||
func NewFigTree() *FigTree {
|
||||
return &FigTree{
|
||||
EnvPrefix: "FIGTREE",
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(fig)
|
||||
}
|
||||
return fig
|
||||
}
|
||||
|
||||
func (f *FigTree) WithHome(home string) {
|
||||
WithHome(home)(f)
|
||||
func LoadAllConfigs(configFile string, options interface{}) error {
|
||||
return NewFigTree().LoadAllConfigs(configFile, options)
|
||||
}
|
||||
|
||||
func (f *FigTree) WithCwd(cwd string) {
|
||||
WithCwd(cwd)(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) WithEnvPrefix(env string) {
|
||||
WithEnvPrefix(env)(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) WithConfigDir(dir string) {
|
||||
WithConfigDir(dir)(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) WithPreProcessor(pp PreProcessor) {
|
||||
WithPreProcessor(pp)(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) WithApplyChangeSet(apply ChangeSetFunc) {
|
||||
WithApplyChangeSet(apply)(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) WithIgnoreChangeSet() {
|
||||
WithApplyChangeSet(func(_ map[string]*string) error {
|
||||
return nil
|
||||
})(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) Copy() *FigTree {
|
||||
cp := *f
|
||||
return &cp
|
||||
func LoadConfig(configFile string, options interface{}) error {
|
||||
return NewFigTree().LoadConfig(configFile, options)
|
||||
}
|
||||
|
||||
func (f *FigTree) LoadAllConfigs(configFile string, options interface{}) error {
|
||||
// reset from any previous config parsing runs
|
||||
f.stop = false
|
||||
|
||||
if f.configDir != "" {
|
||||
configFile = path.Join(f.configDir, configFile)
|
||||
if f.ConfigDir != "" {
|
||||
configFile = path.Join(f.ConfigDir, configFile)
|
||||
}
|
||||
|
||||
paths := FindParentPaths(f.home, f.workDir, configFile)
|
||||
paths := FindParentPaths(configFile)
|
||||
paths = append([]string{fmt.Sprintf("/etc/%s", configFile)}, paths...)
|
||||
|
||||
// iterate paths in reverse
|
||||
for i := len(paths) - 1; i >= 0; i-- {
|
||||
file := paths[i]
|
||||
if err := f.LoadConfig(file, options); err != nil {
|
||||
err := f.LoadConfig(file, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.stop {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// apply defaults at the end to set any undefined fields
|
||||
if f.Defaults != nil {
|
||||
m := &merger{sourceFile: "default"}
|
||||
m.mergeStructs(
|
||||
reflect.ValueOf(options),
|
||||
reflect.ValueOf(f.Defaults),
|
||||
)
|
||||
f.populateEnv(options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FigTree) LoadConfigBytes(config []byte, source string, options interface{}) error {
|
||||
if !reflect.ValueOf(options).IsValid() {
|
||||
return fmt.Errorf("options argument [%#v] is not valid", options)
|
||||
}
|
||||
func (f *FigTree) LoadConfigBytes(config []byte, source string, options interface{}) (err error) {
|
||||
f.populateEnv(options)
|
||||
|
||||
defer func(mapType, iface reflect.Type) {
|
||||
yaml.DefaultMapType = mapType
|
||||
@@ -181,15 +89,7 @@ func (f *FigTree) LoadConfigBytes(config []byte, source string, options interfac
|
||||
yaml.DefaultMapType = reflect.TypeOf(map[string]interface{}{})
|
||||
yaml.IfaceType = yaml.DefaultMapType.Elem()
|
||||
|
||||
var err error
|
||||
if f.preProcessor != nil {
|
||||
config, err = f.preProcessor(config)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to process config file: %s", source)
|
||||
}
|
||||
}
|
||||
|
||||
m := NewMerger(WithSourceFile(source))
|
||||
m := &merger{sourceFile: source}
|
||||
type tmpOpts struct {
|
||||
Config ConfigOptions
|
||||
}
|
||||
@@ -198,13 +98,13 @@ func (f *FigTree) LoadConfigBytes(config []byte, source string, options interfac
|
||||
// look for config settings first
|
||||
err = yaml.Unmarshal(config, m)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Unable to parse %s", source)
|
||||
return errors.Wrap(err, fmt.Sprintf("Unable to parse %s", source))
|
||||
}
|
||||
|
||||
// then parse document into requested struct
|
||||
err = yaml.Unmarshal(config, tmp)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Unable to parse %s", source)
|
||||
return errors.Wrap(err, fmt.Sprintf("Unable to parse %s", source))
|
||||
}
|
||||
|
||||
m.setSource(reflect.ValueOf(tmp))
|
||||
@@ -212,35 +112,40 @@ func (f *FigTree) LoadConfigBytes(config []byte, source string, options interfac
|
||||
reflect.ValueOf(options),
|
||||
reflect.ValueOf(tmp),
|
||||
)
|
||||
changeSet := f.PopulateEnv(options)
|
||||
f.populateEnv(options)
|
||||
if m.Config.Stop {
|
||||
f.stop = true
|
||||
return f.applyChangeSet(changeSet)
|
||||
return nil
|
||||
}
|
||||
return f.applyChangeSet(changeSet)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FigTree) LoadConfig(file string, options interface{}) error {
|
||||
rel, err := filepath.Rel(f.workDir, file)
|
||||
func (f *FigTree) LoadConfig(file string, options interface{}) (err error) {
|
||||
basePath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(basePath, file)
|
||||
if err != nil {
|
||||
rel = file
|
||||
}
|
||||
|
||||
if stat, err := os.Stat(file); err == nil {
|
||||
if stat.Mode()&0111 == 0 {
|
||||
Log.Debugf("Loading config %s", file)
|
||||
log.Debugf("Loading config %s", file)
|
||||
if data, err := ioutil.ReadFile(file); err == nil {
|
||||
return f.LoadConfigBytes(data, rel, options)
|
||||
}
|
||||
} else {
|
||||
Log.Debugf("Found Executable Config file: %s", file)
|
||||
log.Debugf("Found Executable Config file: %s", file)
|
||||
// it is executable, so run it and try to parse the output
|
||||
cmd := exec.Command(file)
|
||||
stdout := bytes.NewBufferString("")
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = bytes.NewBufferString("")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return errors.Wrapf(err, "%s is exectuable, but it failed to execute:\n%s", file, cmd.Stderr)
|
||||
return errors.Wrap(err, fmt.Sprintf("%s is exectuable, but it failed to execute:\n%s", file, cmd.Stderr))
|
||||
}
|
||||
return f.LoadConfigBytes(stdout.Bytes(), rel, options)
|
||||
}
|
||||
@@ -248,261 +153,17 @@ func (f *FigTree) LoadConfig(file string, options interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindParentPaths(homedir, cwd, fileName string) []string {
|
||||
paths := make([]string, 0)
|
||||
|
||||
// special case if homedir is not in current path then check there anyway
|
||||
if !strings.HasPrefix(cwd, homedir) {
|
||||
file := path.Join(homedir, fileName)
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
paths = append(paths, filepath.FromSlash(file))
|
||||
}
|
||||
}
|
||||
|
||||
var dir string
|
||||
for _, part := range strings.Split(cwd, string(os.PathSeparator)) {
|
||||
if part == "" && dir == "" {
|
||||
dir = "/"
|
||||
} else {
|
||||
dir = path.Join(dir, part)
|
||||
}
|
||||
file := path.Join(dir, fileName)
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
paths = append(paths, filepath.FromSlash(file))
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func (f *FigTree) FindParentPaths(fileName string) []string {
|
||||
return FindParentPaths(f.home, f.workDir, fileName)
|
||||
}
|
||||
|
||||
var camelCaseWords = regexp.MustCompile("[0-9A-Za-z]+")
|
||||
|
||||
func camelCase(name string) string {
|
||||
words := camelCaseWords.FindAllString(name, -1)
|
||||
for i, word := range words {
|
||||
words[i] = strings.Title(word)
|
||||
}
|
||||
return strings.Join(words, "")
|
||||
|
||||
}
|
||||
|
||||
type Merger struct {
|
||||
sourceFile string
|
||||
preserveMap map[string]struct{}
|
||||
Config ConfigOptions `json:"config,omitempty" yaml:"config,omitempty"`
|
||||
}
|
||||
|
||||
type MergeOption func(*Merger)
|
||||
|
||||
func WithSourceFile(source string) MergeOption {
|
||||
return func(m *Merger) {
|
||||
m.sourceFile = source
|
||||
}
|
||||
}
|
||||
|
||||
func PreserveMap(keys ...string) MergeOption {
|
||||
return func(m *Merger) {
|
||||
for _, key := range keys {
|
||||
m.preserveMap[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewMerger(options ...MergeOption) *Merger {
|
||||
m := &Merger{
|
||||
sourceFile: "merge",
|
||||
preserveMap: make(map[string]struct{}),
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Merge will attempt to merge the data from src into dst. They shoud be either both maps or both structs.
|
||||
// The structs do not need to have the same structure, but any field name that exists in both
|
||||
// structs will must be the same type.
|
||||
func Merge(dst, src interface{}) {
|
||||
m := NewMerger()
|
||||
m.mergeStructs(reflect.ValueOf(dst), reflect.ValueOf(src))
|
||||
}
|
||||
|
||||
// MakeMergeStruct will take multiple structs and return a pointer to a zero value for the
|
||||
// anonymous struct that has all the public fields from all the structs merged into one struct.
|
||||
// If there are multiple structs with the same field names, the first appearance of that name
|
||||
// will be used.
|
||||
func MakeMergeStruct(structs ...interface{}) interface{} {
|
||||
m := NewMerger()
|
||||
return m.MakeMergeStruct(structs...)
|
||||
}
|
||||
|
||||
func (m *Merger) MakeMergeStruct(structs ...interface{}) interface{} {
|
||||
values := []reflect.Value{}
|
||||
for _, data := range structs {
|
||||
values = append(values, reflect.ValueOf(data))
|
||||
}
|
||||
return m.makeMergeStruct(values...).Interface()
|
||||
}
|
||||
|
||||
func inlineField(field reflect.StructField) bool {
|
||||
if tag := field.Tag.Get("figtree"); tag != "" {
|
||||
return strings.HasSuffix(tag, ",inline")
|
||||
}
|
||||
if tag := field.Tag.Get("yaml"); tag != "" {
|
||||
return strings.HasSuffix(tag, ",inline")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Merger) makeMergeStruct(values ...reflect.Value) reflect.Value {
|
||||
foundFields := map[string]reflect.StructField{}
|
||||
for i := 0; i < len(values); i++ {
|
||||
v := values[i]
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
typ := v.Type()
|
||||
var field reflect.StructField
|
||||
if typ.Kind() == reflect.Struct {
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field = typ.Field(i)
|
||||
if field.PkgPath != "" {
|
||||
// unexported field, skip
|
||||
continue
|
||||
}
|
||||
if f, ok := foundFields[field.Name]; ok {
|
||||
if f.Type.Kind() == reflect.Struct && field.Type.Kind() == reflect.Struct {
|
||||
if fName, fieldName := f.Type.Name(), field.Type.Name(); fName == "" || fieldName == "" || fName != fieldName {
|
||||
// we have 2 fields with the same name and they are both structs, so we need
|
||||
// to merge the existing struct with the new one in case they are different
|
||||
newval := m.makeMergeStruct(reflect.New(f.Type).Elem(), reflect.New(field.Type).Elem()).Elem()
|
||||
f.Type = newval.Type()
|
||||
foundFields[field.Name] = f
|
||||
}
|
||||
}
|
||||
// field already found, skip
|
||||
continue
|
||||
}
|
||||
if inlineField(field) {
|
||||
values = append(values, v.Field(i))
|
||||
continue
|
||||
}
|
||||
foundFields[field.Name] = field
|
||||
}
|
||||
} else if typ.Kind() == reflect.Map {
|
||||
for _, key := range v.MapKeys() {
|
||||
keyval := reflect.ValueOf(v.MapIndex(key).Interface())
|
||||
if _, ok := m.preserveMap[key.String()]; !ok {
|
||||
if keyval.Kind() == reflect.Ptr && keyval.Elem().Kind() == reflect.Map {
|
||||
keyval = m.makeMergeStruct(keyval.Elem())
|
||||
} else if keyval.Kind() == reflect.Map {
|
||||
keyval = m.makeMergeStruct(keyval).Elem()
|
||||
}
|
||||
}
|
||||
var t reflect.Type
|
||||
if !keyval.IsValid() {
|
||||
// this nonsense is to create a generic `interface{}` type. There is
|
||||
// probably an easier to do this, but it eludes me at the moment.
|
||||
var dummy interface{}
|
||||
t = reflect.ValueOf(&dummy).Elem().Type()
|
||||
} else {
|
||||
t = reflect.ValueOf(keyval.Interface()).Type()
|
||||
}
|
||||
field = reflect.StructField{
|
||||
Name: camelCase(key.String()),
|
||||
Type: t,
|
||||
Tag: reflect.StructTag(fmt.Sprintf(`json:"%s" yaml:"%s"`, key.String(), key.String())),
|
||||
}
|
||||
if f, ok := foundFields[field.Name]; ok {
|
||||
if f.Type.Kind() == reflect.Struct && t.Kind() == reflect.Struct {
|
||||
if fName, tName := f.Type.Name(), t.Name(); fName == "" || tName == "" || fName != tName {
|
||||
// we have 2 fields with the same name and they are both structs, so we need
|
||||
// to merge the existig struct with the new one in case they are different
|
||||
newval := m.makeMergeStruct(reflect.New(f.Type).Elem(), reflect.New(t).Elem()).Elem()
|
||||
f.Type = newval.Type()
|
||||
foundFields[field.Name] = f
|
||||
}
|
||||
}
|
||||
// field already found, skip
|
||||
continue
|
||||
}
|
||||
foundFields[field.Name] = field
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fields := []reflect.StructField{}
|
||||
for _, value := range foundFields {
|
||||
fields = append(fields, value)
|
||||
}
|
||||
sort.Slice(fields, func(i, j int) bool {
|
||||
return fields[i].Name < fields[j].Name
|
||||
})
|
||||
newType := reflect.StructOf(fields)
|
||||
return reflect.New(newType)
|
||||
}
|
||||
|
||||
func (m *Merger) mapToStruct(src reflect.Value) reflect.Value {
|
||||
if src.Kind() != reflect.Map {
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
dest := m.makeMergeStruct(src)
|
||||
if dest.Kind() == reflect.Ptr {
|
||||
dest = dest.Elem()
|
||||
}
|
||||
|
||||
for _, key := range src.MapKeys() {
|
||||
structFieldName := camelCase(key.String())
|
||||
keyval := reflect.ValueOf(src.MapIndex(key).Interface())
|
||||
// skip invalid (ie nil) key values
|
||||
if !keyval.IsValid() {
|
||||
continue
|
||||
}
|
||||
if keyval.Kind() == reflect.Ptr && keyval.Elem().Kind() == reflect.Map {
|
||||
keyval = m.mapToStruct(keyval.Elem()).Addr()
|
||||
m.mergeStructs(dest.FieldByName(structFieldName), reflect.ValueOf(keyval.Interface()))
|
||||
} else if keyval.Kind() == reflect.Map {
|
||||
keyval = m.mapToStruct(keyval)
|
||||
m.mergeStructs(dest.FieldByName(structFieldName), reflect.ValueOf(keyval.Interface()))
|
||||
} else {
|
||||
dest.FieldByName(structFieldName).Set(reflect.ValueOf(keyval.Interface()))
|
||||
}
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
func structToMap(src reflect.Value) reflect.Value {
|
||||
if src.Kind() != reflect.Struct {
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
dest := reflect.ValueOf(map[string]interface{}{})
|
||||
|
||||
typ := src.Type()
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
structField := typ.Field(i)
|
||||
if structField.PkgPath != "" {
|
||||
// skip private fields
|
||||
continue
|
||||
}
|
||||
name := yamlFieldName(structField)
|
||||
dest.SetMapIndex(reflect.ValueOf(name), src.Field(i))
|
||||
}
|
||||
|
||||
return dest
|
||||
}
|
||||
|
||||
type ConfigOptions struct {
|
||||
Overwrite []string `json:"overwrite,omitempty" yaml:"overwrite,omitempty"`
|
||||
Stop bool `json:"stop,omitempty" yaml:"stop,omitempty"`
|
||||
// Merge bool `json:"merge,omitempty" yaml:"merge,omitempty"`
|
||||
}
|
||||
|
||||
type merger struct {
|
||||
sourceFile string
|
||||
Config ConfigOptions `json:"config,omitempty" yaml:"config,omitempty"`
|
||||
}
|
||||
|
||||
func yamlFieldName(sf reflect.StructField) string {
|
||||
if tag, ok := sf.Tag.Lookup("yaml"); ok {
|
||||
// with yaml:"foobar,omitempty"
|
||||
@@ -510,16 +171,10 @@ func yamlFieldName(sf reflect.StructField) string {
|
||||
parts := strings.Split(tag, ",")
|
||||
return parts[0]
|
||||
}
|
||||
// guess the field name from reversing camel case
|
||||
// so "FooBar" becomes "foo-bar"
|
||||
parts := camelcase.Split(sf.Name)
|
||||
for i := range parts {
|
||||
parts[i] = strings.ToLower(parts[i])
|
||||
}
|
||||
return strings.Join(parts, "-")
|
||||
return sf.Name
|
||||
}
|
||||
|
||||
func (m *Merger) mustOverwrite(name string) bool {
|
||||
func (m *merger) mustOverwrite(name string) bool {
|
||||
for _, prop := range m.Config.Overwrite {
|
||||
if name == prop {
|
||||
return true
|
||||
@@ -530,7 +185,7 @@ func (m *Merger) mustOverwrite(name string) bool {
|
||||
|
||||
func isDefault(v reflect.Value) bool {
|
||||
if v.CanAddr() {
|
||||
if option, ok := v.Addr().Interface().(option); ok {
|
||||
if option, ok := v.Addr().Interface().(Option); ok {
|
||||
if option.GetSource() == "default" {
|
||||
return true
|
||||
}
|
||||
@@ -539,10 +194,7 @@ func isDefault(v reflect.Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func isZero(v reflect.Value) bool {
|
||||
if !v.IsValid() {
|
||||
return true
|
||||
}
|
||||
func isEmpty(v reflect.Value) bool {
|
||||
return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
|
||||
}
|
||||
|
||||
@@ -551,7 +203,7 @@ func isSame(v1, v2 reflect.Value) bool {
|
||||
}
|
||||
|
||||
// recursively set the Source attribute of the Options
|
||||
func (m *Merger) setSource(v reflect.Value) {
|
||||
func (m *merger) setSource(v reflect.Value) {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
@@ -570,7 +222,7 @@ func (m *Merger) setSource(v reflect.Value) {
|
||||
}
|
||||
case reflect.Struct:
|
||||
if v.CanAddr() {
|
||||
if option, ok := v.Addr().Interface().(option); ok {
|
||||
if option, ok := v.Addr().Interface().(Option); ok {
|
||||
if option.IsDefined() {
|
||||
option.SetSource(m.sourceFile)
|
||||
}
|
||||
@@ -595,117 +247,23 @@ func (m *Merger) setSource(v reflect.Value) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Merger) assignValue(dest, src reflect.Value, overwrite bool) {
|
||||
if src.Type().AssignableTo(dest.Type()) {
|
||||
if (isZero(dest) || isDefault(dest) || overwrite) && !isZero(src) {
|
||||
dest.Set(src)
|
||||
return
|
||||
}
|
||||
return
|
||||
func (m *merger) mergeStructs(ov, nv reflect.Value) {
|
||||
if ov.Kind() == reflect.Ptr {
|
||||
ov = ov.Elem()
|
||||
}
|
||||
if dest.CanAddr() {
|
||||
if option, ok := dest.Addr().Interface().(option); ok {
|
||||
destOptionValue := reflect.ValueOf(option.GetValue())
|
||||
// map interface type to real-ish type:
|
||||
src = reflect.ValueOf(src.Interface())
|
||||
if !src.IsValid() {
|
||||
Log.Debugf("assignValue: src isValid: %t", src.IsValid())
|
||||
return
|
||||
}
|
||||
if src.Type().AssignableTo(destOptionValue.Type()) {
|
||||
option.SetValue(src.Interface())
|
||||
option.SetSource(m.sourceFile)
|
||||
Log.Debugf("assignValue: assigned %#v to %#v", destOptionValue, src)
|
||||
return
|
||||
} else {
|
||||
panic(fmt.Errorf("%s is not assinable to %s", src.Type(), destOptionValue.Type()))
|
||||
}
|
||||
}
|
||||
if nv.Kind() == reflect.Ptr {
|
||||
nv = nv.Elem()
|
||||
}
|
||||
// make copy so we can reliably Addr it to see if it fits the
|
||||
// Option interface.
|
||||
srcCopy := reflect.New(src.Type()).Elem()
|
||||
srcCopy.Set(src)
|
||||
if option, ok := srcCopy.Addr().Interface().(option); ok {
|
||||
srcOptionValue := reflect.ValueOf(option.GetValue())
|
||||
if srcOptionValue.Type().AssignableTo(dest.Type()) {
|
||||
m.assignValue(dest, srcOptionValue, overwrite)
|
||||
return
|
||||
} else {
|
||||
panic(fmt.Errorf("%s is not assinable to %s", srcOptionValue.Type(), dest.Type()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fromInterface(v reflect.Value) (reflect.Value, func()) {
|
||||
if v.Kind() == reflect.Interface {
|
||||
realV := reflect.ValueOf(v.Interface())
|
||||
if !realV.IsValid() {
|
||||
realV = reflect.New(v.Type()).Elem()
|
||||
v.Set(realV)
|
||||
return v, func() {}
|
||||
}
|
||||
tmp := reflect.New(realV.Type()).Elem()
|
||||
tmp.Set(realV)
|
||||
return tmp, func() {
|
||||
v.Set(tmp)
|
||||
}
|
||||
}
|
||||
return v, func() {}
|
||||
}
|
||||
|
||||
func (m *Merger) mergeStructs(ov, nv reflect.Value) {
|
||||
ov = reflect.Indirect(ov)
|
||||
nv = reflect.Indirect(nv)
|
||||
|
||||
ov, restore := fromInterface(ov)
|
||||
defer restore()
|
||||
|
||||
if nv.Kind() == reflect.Interface {
|
||||
nv = reflect.ValueOf(nv.Interface())
|
||||
}
|
||||
|
||||
if ov.Kind() == reflect.Map {
|
||||
if nv.Kind() == reflect.Struct {
|
||||
nv = structToMap(nv)
|
||||
}
|
||||
if ov.Kind() == reflect.Map && nv.Kind() == reflect.Map {
|
||||
m.mergeMaps(ov, nv)
|
||||
return
|
||||
}
|
||||
|
||||
if ov.Kind() == reflect.Struct && nv.Kind() == reflect.Map {
|
||||
nv = m.mapToStruct(nv)
|
||||
}
|
||||
|
||||
if !ov.IsValid() || !nv.IsValid() {
|
||||
Log.Debugf("Valid: ov:%v nv:%t", ov.IsValid(), nv.IsValid())
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < nv.NumField(); i++ {
|
||||
nvField := nv.Field(i)
|
||||
if nvField.Kind() == reflect.Interface {
|
||||
nvField = reflect.ValueOf(nvField.Interface())
|
||||
}
|
||||
if !nvField.IsValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
ovStructField := ov.Type().Field(i)
|
||||
nvStructField := nv.Type().Field(i)
|
||||
ovStructField, ok := ov.Type().FieldByName(nvStructField.Name)
|
||||
if !ok {
|
||||
if nvStructField.Anonymous {
|
||||
// this is an embedded struct, and the destination does not contain
|
||||
// the same embeded struct, so try to merge the embedded struct
|
||||
// directly with the destination
|
||||
m.mergeStructs(ov, nvField)
|
||||
continue
|
||||
}
|
||||
// if original value does not have the same struct field
|
||||
// then just skip this field.
|
||||
continue
|
||||
}
|
||||
|
||||
// PkgPath is empty for upper case (exported) field names.
|
||||
if ovStructField.PkgPath != "" || nvStructField.PkgPath != "" {
|
||||
// unexported field, skipping
|
||||
@@ -713,133 +271,95 @@ func (m *Merger) mergeStructs(ov, nv reflect.Value) {
|
||||
}
|
||||
fieldName := yamlFieldName(ovStructField)
|
||||
|
||||
ovField := ov.FieldByName(nvStructField.Name)
|
||||
ovField, restore := fromInterface(ovField)
|
||||
defer restore()
|
||||
|
||||
if (isZero(ovField) || isDefault(ovField) || m.mustOverwrite(fieldName)) && !isSame(ovField, nvField) {
|
||||
Log.Debugf("Setting %s to %#v", nv.Type().Field(i).Name, nvField.Interface())
|
||||
m.assignValue(ovField, nvField, m.mustOverwrite(fieldName))
|
||||
}
|
||||
switch ovField.Kind() {
|
||||
case reflect.Map:
|
||||
Log.Debugf("Merging Map: %#v with %#v", ovField, nvField)
|
||||
m.mergeStructs(ovField, nvField)
|
||||
case reflect.Slice:
|
||||
if nvField.Len() > 0 {
|
||||
Log.Debugf("Merging Slice: %#v with %#v", ovField, nvField)
|
||||
ovField.Set(m.mergeArrays(ovField, nvField))
|
||||
}
|
||||
case reflect.Array:
|
||||
if nvField.Len() > 0 {
|
||||
Log.Debugf("Merging Array: %v with %v", ovField, nvField)
|
||||
ovField.Set(m.mergeArrays(ovField, nvField))
|
||||
}
|
||||
case reflect.Struct:
|
||||
// only merge structs if they are not an Option type:
|
||||
if _, ok := ovField.Addr().Interface().(option); !ok {
|
||||
Log.Debugf("Merging Struct: %v with %v", ovField, nvField)
|
||||
m.mergeStructs(ovField, nvField)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Merger) mergeMaps(ov, nv reflect.Value) {
|
||||
for _, key := range nv.MapKeys() {
|
||||
if !ov.MapIndex(key).IsValid() {
|
||||
ovElem := reflect.New(ov.Type().Elem()).Elem()
|
||||
m.assignValue(ovElem, nv.MapIndex(key), false)
|
||||
if ov.IsNil() {
|
||||
if !ov.CanSet() {
|
||||
continue
|
||||
}
|
||||
ov.Set(reflect.MakeMap(ov.Type()))
|
||||
}
|
||||
Log.Debugf("Setting %v to %#v", key.Interface(), ovElem.Interface())
|
||||
ov.SetMapIndex(key, ovElem)
|
||||
if (isEmpty(ov.Field(i)) || isDefault(ov.Field(i)) || m.mustOverwrite(fieldName)) && !isEmpty(nv.Field(i)) && !isSame(ov.Field(i), nv.Field(i)) {
|
||||
log.Debugf("Setting %s to %#v", nv.Type().Field(i).Name, nv.Field(i).Interface())
|
||||
ov.Field(i).Set(nv.Field(i))
|
||||
} else {
|
||||
ovi := reflect.ValueOf(ov.MapIndex(key).Interface())
|
||||
nvi := reflect.ValueOf(nv.MapIndex(key).Interface())
|
||||
if !nvi.IsValid() {
|
||||
continue
|
||||
}
|
||||
switch ovi.Kind() {
|
||||
switch ov.Field(i).Kind() {
|
||||
case reflect.Map:
|
||||
Log.Debugf("Merging: %v with %v", ovi.Interface(), nvi.Interface())
|
||||
m.mergeStructs(ovi, nvi)
|
||||
if nv.Field(i).Len() > 0 {
|
||||
log.Debugf("Merging: %v with %v", ov.Field(i), nv.Field(i))
|
||||
m.mergeMaps(ov.Field(i), nv.Field(i))
|
||||
}
|
||||
case reflect.Slice:
|
||||
Log.Debugf("Merging: %v with %v", ovi.Interface(), nvi.Interface())
|
||||
ov.SetMapIndex(key, m.mergeArrays(ovi, nvi))
|
||||
case reflect.Array:
|
||||
Log.Debugf("Merging: %v with %v", ovi.Interface(), nvi.Interface())
|
||||
ov.SetMapIndex(key, m.mergeArrays(ovi, nvi))
|
||||
default:
|
||||
if isZero(ovi) {
|
||||
if !ovi.IsValid() || nvi.Type().AssignableTo(ovi.Type()) {
|
||||
ov.SetMapIndex(key, nvi)
|
||||
} else {
|
||||
// to check for the Option interface we need the Addr of the value, but
|
||||
// we cannot take the Addr of a map value, so we have to first copy
|
||||
// it, meh not optimal
|
||||
newVal := reflect.New(nvi.Type())
|
||||
newVal.Elem().Set(nvi)
|
||||
if nOption, ok := newVal.Interface().(option); ok {
|
||||
ov.SetMapIndex(key, reflect.ValueOf(nOption.GetValue()))
|
||||
continue
|
||||
if nv.Field(i).Len() > 0 {
|
||||
log.Debugf("Merging: %v with %v", ov.Field(i), nv.Field(i))
|
||||
if ov.Field(i).CanSet() {
|
||||
if ov.Field(i).Len() == 0 {
|
||||
ov.Field(i).Set(nv.Field(i))
|
||||
} else {
|
||||
log.Debugf("Merging: %v with %v", ov.Field(i), nv.Field(i))
|
||||
ov.Field(i).Set(m.mergeArrays(ov.Field(i), nv.Field(i)))
|
||||
}
|
||||
panic(fmt.Errorf("map value %T is not assignable to %T", nvi.Interface(), ovi.Interface()))
|
||||
}
|
||||
|
||||
}
|
||||
case reflect.Array:
|
||||
if nv.Field(i).Len() > 0 {
|
||||
log.Debugf("Merging: %v with %v", ov.Field(i), nv.Field(i))
|
||||
ov.Field(i).Set(m.mergeArrays(ov.Field(i), nv.Field(i)))
|
||||
}
|
||||
case reflect.Struct:
|
||||
// only merge structs if they are not an Option type:
|
||||
if _, ok := ov.Field(i).Addr().Interface().(Option); !ok {
|
||||
log.Debugf("Merging: %v with %v", ov.Field(i), nv.Field(i))
|
||||
m.mergeStructs(ov.Field(i), nv.Field(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Merger) mergeArrays(ov, nv reflect.Value) reflect.Value {
|
||||
var zero interface{}
|
||||
func (m *merger) mergeMaps(ov, nv reflect.Value) {
|
||||
for _, key := range nv.MapKeys() {
|
||||
if !ov.MapIndex(key).IsValid() {
|
||||
log.Debugf("Setting %v to %#v", key.Interface(), nv.MapIndex(key).Interface())
|
||||
ov.SetMapIndex(key, nv.MapIndex(key))
|
||||
} else {
|
||||
ovi := reflect.ValueOf(ov.MapIndex(key).Interface())
|
||||
nvi := reflect.ValueOf(nv.MapIndex(key).Interface())
|
||||
switch ovi.Kind() {
|
||||
case reflect.Map:
|
||||
log.Debugf("Merging: %v with %v", ovi.Interface(), nvi.Interface())
|
||||
m.mergeMaps(ovi, nvi)
|
||||
case reflect.Slice:
|
||||
log.Debugf("Merging: %v with %v", ovi.Interface(), nvi.Interface())
|
||||
ov.SetMapIndex(key, m.mergeArrays(ovi, nvi))
|
||||
case reflect.Array:
|
||||
log.Debugf("Merging: %v with %v", ovi.Interface(), nvi.Interface())
|
||||
ov.SetMapIndex(key, m.mergeArrays(ovi, nvi))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *merger) mergeArrays(ov, nv reflect.Value) reflect.Value {
|
||||
Outer:
|
||||
for ni := 0; ni < nv.Len(); ni++ {
|
||||
niv := nv.Index(ni)
|
||||
|
||||
n := niv
|
||||
if n.CanAddr() {
|
||||
if nOption, ok := n.Addr().Interface().(option); ok {
|
||||
if !nOption.IsDefined() {
|
||||
continue
|
||||
}
|
||||
n = reflect.ValueOf(nOption.GetValue())
|
||||
}
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(n.Interface(), zero) {
|
||||
continue
|
||||
}
|
||||
|
||||
for oi := 0; oi < ov.Len(); oi++ {
|
||||
o := ov.Index(oi)
|
||||
if o.CanAddr() {
|
||||
if oOption, ok := o.Addr().Interface().(option); ok {
|
||||
o = reflect.ValueOf(oOption.GetValue())
|
||||
oiv := ov.Index(oi)
|
||||
if oiv.CanAddr() && niv.CanAddr() {
|
||||
if oOption, ok := oiv.Addr().Interface().(Option); ok {
|
||||
if nOption, ok := niv.Addr().Interface().(Option); ok {
|
||||
if reflect.DeepEqual(oOption.GetValue(), nOption.GetValue()) {
|
||||
continue Outer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if reflect.DeepEqual(n.Interface(), o.Interface()) {
|
||||
if reflect.DeepEqual(niv.Interface(), oiv.Interface()) {
|
||||
continue Outer
|
||||
}
|
||||
}
|
||||
|
||||
nvElem := reflect.New(ov.Type().Elem()).Elem()
|
||||
m.assignValue(nvElem, niv, false)
|
||||
|
||||
Log.Debugf("Appending %v to %v", nvElem.Interface(), ov)
|
||||
ov = reflect.Append(ov, nvElem)
|
||||
log.Debugf("Appending %v to %v", niv.Interface(), ov)
|
||||
ov = reflect.Append(ov, niv)
|
||||
}
|
||||
return ov
|
||||
}
|
||||
|
||||
func (f *FigTree) formatEnvName(name string) string {
|
||||
name = fmt.Sprintf("%s_%s", f.envPrefix, strings.ToUpper(name))
|
||||
name = fmt.Sprintf("%s_%s", f.EnvPrefix, strings.ToUpper(name))
|
||||
|
||||
return strings.Map(func(r rune) rune {
|
||||
if unicode.IsDigit(r) || unicode.IsLetter(r) {
|
||||
@@ -892,9 +412,7 @@ func (f *FigTree) formatEnvValue(value reflect.Value) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (f *FigTree) PopulateEnv(data interface{}) (changeSet map[string]*string) {
|
||||
changeSet = make(map[string]*string)
|
||||
|
||||
func (f *FigTree) populateEnv(data interface{}) {
|
||||
options := reflect.ValueOf(data)
|
||||
if options.Kind() == reflect.Ptr {
|
||||
options = reflect.ValueOf(options.Elem().Interface())
|
||||
@@ -917,9 +435,7 @@ func (f *FigTree) PopulateEnv(data interface{}) (changeSet map[string]*string) {
|
||||
envName := f.formatEnvName(name)
|
||||
val, ok := f.formatEnvValue(options.MapIndex(key))
|
||||
if ok {
|
||||
changeSet[envName] = &val
|
||||
} else {
|
||||
changeSet[envName] = nil
|
||||
os.Setenv(envName, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -932,40 +448,28 @@ func (f *FigTree) PopulateEnv(data interface{}) (changeSet map[string]*string) {
|
||||
continue
|
||||
}
|
||||
|
||||
envNames := []string{strings.Join(camelcase.Split(structField.Name), "_")}
|
||||
name := strings.Join(camelcase.Split(structField.Name), "_")
|
||||
|
||||
if tag := structField.Tag.Get("figtree"); tag != "" {
|
||||
if strings.HasSuffix(tag, ",inline") {
|
||||
// if we have a tag like: `figtree:",inline"` then we
|
||||
// want to the field as a top level member and not serialize
|
||||
// the raw struct to json, so just recurse here
|
||||
nestedEnvSet := f.PopulateEnv(options.Field(i).Interface())
|
||||
for k, v := range nestedEnvSet {
|
||||
changeSet[k] = v
|
||||
}
|
||||
f.populateEnv(options.Field(i).Interface())
|
||||
continue
|
||||
}
|
||||
// next look for `figtree:"env,..."` to set the env name to that
|
||||
parts := strings.Split(tag, ",")
|
||||
if len(parts) > 0 {
|
||||
// if the env name is "-" then we should not populate this data into the env
|
||||
if parts[0] == "-" {
|
||||
continue
|
||||
}
|
||||
envNames = strings.Split(parts[0], ";")
|
||||
name = parts[0]
|
||||
}
|
||||
}
|
||||
for _, name := range envNames {
|
||||
envName := f.formatEnvName(name)
|
||||
val, ok := f.formatEnvValue(options.Field(i))
|
||||
if ok {
|
||||
changeSet[envName] = &val
|
||||
} else {
|
||||
changeSet[envName] = nil
|
||||
}
|
||||
|
||||
envName := f.formatEnvName(name)
|
||||
val, ok := f.formatEnvValue(options.Field(i))
|
||||
if ok {
|
||||
os.Setenv(envName, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changeSet
|
||||
}
|
||||
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
StringifyValue = false
|
||||
logging.SetLevel(logging.NOTICE, "")
|
||||
}
|
||||
|
||||
type TestOptions struct {
|
||||
String1 StringOption `json:"str1,omitempty" yaml:"str1,omitempty"`
|
||||
LeaveEmpty StringOption `json:"leave-empty,omitempty" yaml:"leave-empty,omitempty"`
|
||||
Array1 ListStringOption `json:"arr1,omitempty" yaml:"arr1,omitempty"`
|
||||
Map1 MapStringOption `json:"map1,omitempty" yaml:"map1,omitempty"`
|
||||
Int1 IntOption `json:"int1,omitempty" yaml:"int1,omitempty"`
|
||||
Float1 Float32Option `json:"float1,omitempty" yaml:"float1,omitempty"`
|
||||
Bool1 BoolOption `json:"bool1,omitempty" yaml:"bool1,omitempty"`
|
||||
}
|
||||
|
||||
type TestBuiltin struct {
|
||||
String1 string `yaml:"str1,omitempty"`
|
||||
LeaveEmpty string `yaml:"leave-empty,omitempty"`
|
||||
Array1 []string `yaml:"arr1,omitempty"`
|
||||
Map1 map[string]string `yaml:"map1,omitempty"`
|
||||
Int1 int `yaml:"int1,omitempty"`
|
||||
Float1 float32 `yaml:"float1,omitempty"`
|
||||
Bool1 bool `yaml:"bool1,omitempty"`
|
||||
}
|
||||
|
||||
func TestOptionsLoadConfigD3(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d3arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d3arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "dupval"})
|
||||
arr1 = append(arr1, StringOption{"../figtree.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../figtree.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../../figtree.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../../figtree.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"figtree.yml", true, "d3str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../../figtree.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"../figtree.yml", true, "d2map1val1"},
|
||||
"key2": StringOption{"figtree.yml", true, "d3map1val2"},
|
||||
"key3": StringOption{"figtree.yml", true, "d3map1val3"},
|
||||
"dup": StringOption{"figtree.yml", true, "d3dupval"},
|
||||
},
|
||||
Int1: IntOption{"figtree.yml", true, 333},
|
||||
Float1: Float32Option{"figtree.yml", true, 3.33},
|
||||
Bool1: BoolOption{"figtree.yml", true, true},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsLoadConfigD2(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "dupval"})
|
||||
arr1 = append(arr1, StringOption{"../figtree.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../figtree.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"figtree.yml", true, "d2str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../figtree.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"figtree.yml", true, "d2map1val1"},
|
||||
"key2": StringOption{"figtree.yml", true, "d2map1val2"},
|
||||
"dup": StringOption{"figtree.yml", true, "d2dupval"},
|
||||
},
|
||||
Int1: IntOption{"figtree.yml", true, 222},
|
||||
Float1: Float32Option{"figtree.yml", true, 2.22},
|
||||
Bool1: BoolOption{"figtree.yml", true, false},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsLoadConfigD1(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d1arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "dupval"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"figtree.yml", true, "d1str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"figtree.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"figtree.yml", true, "d1map1val1"},
|
||||
"dup": StringOption{"figtree.yml", true, "d1dupval"},
|
||||
},
|
||||
Int1: IntOption{"figtree.yml", true, 111},
|
||||
Float1: Float32Option{"figtree.yml", true, 1.11},
|
||||
Bool1: BoolOption{"figtree.yml", true, true},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsCorrupt(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
err := LoadAllConfigs("corrupt.yml", &opts)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestBuiltinLoadConfigD3(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d3arr1val1")
|
||||
arr1 = append(arr1, "d3arr1val2")
|
||||
arr1 = append(arr1, "dupval")
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d3str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d2map1val1",
|
||||
"key2": "d3map1val2",
|
||||
"key3": "d3map1val3",
|
||||
"dup": "d3dupval",
|
||||
},
|
||||
Int1: 333,
|
||||
Float1: 3.33,
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinLoadConfigD2(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
arr1 = append(arr1, "dupval")
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d2str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d2map1val1",
|
||||
"key2": "d2map1val2",
|
||||
"dup": "d2dupval",
|
||||
},
|
||||
Int1: 222,
|
||||
Float1: 2.22,
|
||||
// note this will be true from d1/figtree.yml since the
|
||||
// d1/d2/figtree.yml set it to false which is a zero value
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinLoadConfigD1(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
arr1 = append(arr1, "dupval")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d1str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d1map1val1",
|
||||
"dup": "d1dupval",
|
||||
},
|
||||
Int1: 111,
|
||||
Float1: 1.11,
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinCorrupt(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
err := LoadAllConfigs("corrupt.yml", &opts)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestOptionsLoadConfigDefaults(t *testing.T) {
|
||||
opts := TestOptions{
|
||||
String1: NewStringOption("defaultVal1"),
|
||||
LeaveEmpty: NewStringOption("emptyVal1"),
|
||||
Int1: NewIntOption(999),
|
||||
Float1: NewFloat32Option(9.99),
|
||||
Bool1: NewBoolOption(false),
|
||||
}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d1arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "dupval"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"figtree.yml", true, "d1str1val1"},
|
||||
LeaveEmpty: StringOption{"default", true, "emptyVal1"},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"figtree.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"figtree.yml", true, "d1map1val1"},
|
||||
"dup": StringOption{"figtree.yml", true, "d1dupval"},
|
||||
},
|
||||
Int1: IntOption{"figtree.yml", true, 111},
|
||||
Float1: Float32Option{"figtree.yml", true, 1.11},
|
||||
Bool1: BoolOption{"figtree.yml", true, true},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
-40
@@ -74,7 +74,6 @@ func (o *BoolOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -83,7 +82,6 @@ func (o *BoolOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -307,7 +305,6 @@ func (o *ByteOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -316,7 +313,6 @@ func (o *ByteOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -540,7 +536,6 @@ func (o *Complex128Option) UnmarshalYAML(unmarshal func(interface{}) error) erro
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -549,7 +544,6 @@ func (o *Complex128Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -773,7 +767,6 @@ func (o *Complex64Option) UnmarshalYAML(unmarshal func(interface{}) error) error
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -782,7 +775,6 @@ func (o *Complex64Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1006,7 +998,6 @@ func (o *ErrorOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1015,7 +1006,6 @@ func (o *ErrorOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1239,7 +1229,6 @@ func (o *Float32Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1248,7 +1237,6 @@ func (o *Float32Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1472,7 +1460,6 @@ func (o *Float64Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1481,7 +1468,6 @@ func (o *Float64Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1705,7 +1691,6 @@ func (o *IntOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1714,7 +1699,6 @@ func (o *IntOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1938,7 +1922,6 @@ func (o *Int16Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1947,7 +1930,6 @@ func (o *Int16Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2171,7 +2153,6 @@ func (o *Int32Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2180,7 +2161,6 @@ func (o *Int32Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2404,7 +2384,6 @@ func (o *Int64Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2413,7 +2392,6 @@ func (o *Int64Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2637,7 +2615,6 @@ func (o *Int8Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2646,7 +2623,6 @@ func (o *Int8Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2870,7 +2846,6 @@ func (o *RuneOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2879,7 +2854,6 @@ func (o *RuneOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3103,7 +3077,6 @@ func (o *StringOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3112,7 +3085,6 @@ func (o *StringOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3336,7 +3308,6 @@ func (o *UintOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3345,7 +3316,6 @@ func (o *UintOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3569,7 +3539,6 @@ func (o *Uint16Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3578,7 +3547,6 @@ func (o *Uint16Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3802,7 +3770,6 @@ func (o *Uint32Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3811,7 +3778,6 @@ func (o *Uint32Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -4035,7 +4001,6 @@ func (o *Uint64Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -4044,7 +4009,6 @@ func (o *Uint64Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -4268,7 +4232,6 @@ func (o *Uint8Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -4277,7 +4240,6 @@ func (o *Uint8Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -4501,7 +4463,6 @@ func (o *UintptrOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -4510,7 +4471,6 @@ func (o *UintptrOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
func TestCommandLine(t *testing.T) {
|
||||
type CommandLineOptions struct {
|
||||
Str1 StringOption `yaml:"str1,omitempty"`
|
||||
Int1 IntOption `yaml:"int1,omitempty"`
|
||||
Map1 MapStringOption `yaml:"map1,omitempty"`
|
||||
Arr1 ListStringOption `yaml:"arr1,omitempty"`
|
||||
}
|
||||
|
||||
opts := CommandLineOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
app := kingpin.New("test", "testing")
|
||||
app.Flag("str1", "Str1").SetValue(&opts.Str1)
|
||||
app.Flag("int1", "Int1").SetValue(&opts.Int1)
|
||||
app.Flag("map1", "Map1").SetValue(&opts.Map1)
|
||||
app.Flag("arr1", "Arr1").SetValue(&opts.Arr1)
|
||||
_, err = app.Parse([]string{"--int1", "999", "--map1", "k1=v1", "--map1", "k2=v2", "--arr1", "v1", "--arr1", "v2"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
arr1 := ListStringOption{}
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d3arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d3arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "dupval"})
|
||||
arr1 = append(arr1, StringOption{"../figtree.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../figtree.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../../figtree.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../../figtree.yml", true, "d1arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"override", true, "v1"})
|
||||
arr1 = append(arr1, StringOption{"override", true, "v2"})
|
||||
|
||||
expected := CommandLineOptions{
|
||||
Str1: StringOption{"figtree.yml", true, "d3str1val1"},
|
||||
Int1: IntOption{"override", true, 999},
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../../figtree.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"../figtree.yml", true, "d2map1val1"},
|
||||
"key2": StringOption{"figtree.yml", true, "d3map1val2"},
|
||||
"key3": StringOption{"figtree.yml", true, "d3map1val3"},
|
||||
"dup": StringOption{"figtree.yml", true, "d3dupval"},
|
||||
"k1": StringOption{"override", true, "v1"},
|
||||
"k2": StringOption{"override", true, "v2"},
|
||||
},
|
||||
Arr1: arr1,
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, opts)
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
yaml "gopkg.in/coryb/yaml.v2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptionsMarshalYAML(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
StringifyValue = true
|
||||
defer func() {
|
||||
StringifyValue = false
|
||||
}()
|
||||
got, err := yaml.Marshal(&opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
expected := `str1: d3str1val1
|
||||
arr1:
|
||||
- d3arr1val1
|
||||
- d3arr1val2
|
||||
- dupval
|
||||
- d2arr1val1
|
||||
- d2arr1val2
|
||||
- d1arr1val1
|
||||
- d1arr1val2
|
||||
map1:
|
||||
dup: d3dupval
|
||||
key0: d1map1val0
|
||||
key1: d2map1val1
|
||||
key2: d3map1val2
|
||||
key3: d3map1val3
|
||||
int1: 333
|
||||
float1: 3.33
|
||||
bool1: true
|
||||
`
|
||||
assert.Equal(t, expected, string(got))
|
||||
}
|
||||
|
||||
func TestOptionsMarshalJSON(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
StringifyValue = true
|
||||
defer func() {
|
||||
StringifyValue = false
|
||||
}()
|
||||
got, err := json.Marshal(&opts)
|
||||
assert.Nil(t, err)
|
||||
// note that "leave-empty" is serialized even though "omitempty" tag is set
|
||||
// this is because json always assumes structs are not empty and there
|
||||
// is no interface to override this behavior
|
||||
expected := `{"str1":"d3str1val1","leave-empty":"","arr1":["d3arr1val1","d3arr1val2","dupval","d2arr1val1","d2arr1val2","d1arr1val1","d1arr1val2"],"map1":{"dup":"d3dupval","key0":"d1map1val0","key1":"d2map1val1","key2":"d3map1val2","key3":"d3map1val3"},"int1":333,"float1":3.33,"bool1":true}`
|
||||
assert.Equal(t, expected, string(got))
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ package figtree
|
||||
|
||||
import "regexp"
|
||||
|
||||
type option interface {
|
||||
type Option interface {
|
||||
IsDefined() bool
|
||||
GetValue() interface{}
|
||||
SetValue(interface{}) error
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
StringifyValue = false
|
||||
}
|
||||
|
||||
func TestOptionsOverwriteConfigD3(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"../overwrite.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../overwrite.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../../overwrite.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../../overwrite.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"../overwrite.yml", true, "d2str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../../overwrite.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"../../overwrite.yml", true, "d1map1val1"},
|
||||
},
|
||||
Int1: IntOption{"../../overwrite.yml", true, 111},
|
||||
Float1: Float32Option{"../../overwrite.yml", true, 1.11},
|
||||
Bool1: BoolOption{"../overwrite.yml", true, false},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("overwrite.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsOverwriteConfigD2(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"overwrite.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"overwrite.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../overwrite.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../overwrite.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"overwrite.yml", true, "d2str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../overwrite.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"../overwrite.yml", true, "d1map1val1"},
|
||||
},
|
||||
Int1: IntOption{"../overwrite.yml", true, 111},
|
||||
Float1: Float32Option{"../overwrite.yml", true, 1.11},
|
||||
Bool1: BoolOption{"overwrite.yml", true, false},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("overwrite.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinOverwriteConfigD3(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d2str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d1map1val1",
|
||||
},
|
||||
Int1: 111,
|
||||
Float1: 1.11,
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("overwrite.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinOverwriteConfigD2(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d2str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d1map1val1",
|
||||
},
|
||||
Int1: 111,
|
||||
Float1: 1.11,
|
||||
// note this will be true from d1/overwrite.yml since the
|
||||
// d1/d2/overwrite.yml set it to false which is a zero value
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("overwrite.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
-2
@@ -76,7 +76,6 @@ func (o *RawTypeOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -85,7 +84,6 @@ func (o *RawTypeOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
yaml "gopkg.in/coryb/yaml.v2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptionInterface(t *testing.T) {
|
||||
f := func(_ Option) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
assert.True(t, f(&BoolOption{}))
|
||||
assert.True(t, f(&ByteOption{}))
|
||||
assert.True(t, f(&Complex128Option{}))
|
||||
assert.True(t, f(&Complex64Option{}))
|
||||
assert.True(t, f(&ErrorOption{}))
|
||||
assert.True(t, f(&Float32Option{}))
|
||||
assert.True(t, f(&Float64Option{}))
|
||||
assert.True(t, f(&IntOption{}))
|
||||
assert.True(t, f(&Int16Option{}))
|
||||
assert.True(t, f(&Int32Option{}))
|
||||
assert.True(t, f(&Int64Option{}))
|
||||
assert.True(t, f(&Int8Option{}))
|
||||
assert.True(t, f(&RuneOption{}))
|
||||
assert.True(t, f(&StringOption{}))
|
||||
assert.True(t, f(&UintOption{}))
|
||||
assert.True(t, f(&Uint16Option{}))
|
||||
assert.True(t, f(&Uint32Option{}))
|
||||
assert.True(t, f(&Uint64Option{}))
|
||||
assert.True(t, f(&Uint8Option{}))
|
||||
assert.True(t, f(&UintptrOption{}))
|
||||
}
|
||||
|
||||
func TestStringOptionYAML(t *testing.T) {
|
||||
s := ""
|
||||
err := yaml.Unmarshal([]byte(`""`), &s)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, s, "")
|
||||
|
||||
type testType struct {
|
||||
String StringOption `yaml:"string,omitempty"`
|
||||
}
|
||||
tt := testType{}
|
||||
|
||||
err = yaml.Unmarshal([]byte(`string: ""`), &tt)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tt.String, StringOption{Value: "", Defined: true})
|
||||
|
||||
tt = testType{}
|
||||
err = yaml.Unmarshal([]byte(`string: "value"`), &tt)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tt.String, StringOption{Value: "value", Defined: true})
|
||||
}
|
||||
|
||||
func TestStringOptionJSON(t *testing.T) {
|
||||
type testType struct {
|
||||
String StringOption `json:"string,omitempty"`
|
||||
}
|
||||
tt := testType{}
|
||||
|
||||
err := json.Unmarshal([]byte(`{"string": ""}`), &tt)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tt.String, StringOption{Value: "", Defined: true})
|
||||
|
||||
tt = testType{}
|
||||
err = json.Unmarshal([]byte(`{"string": "value"}`), &tt)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tt.String, StringOption{Value: "value", Defined: true})
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptionsStopConfigD3(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"stop.yml", true, "d3arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"stop.yml", true, "d3arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../stop.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../stop.yml", true, "d2arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"stop.yml", true, "d3str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key1": StringOption{"../stop.yml", true, "d2map1val1"},
|
||||
"key2": StringOption{"stop.yml", true, "d3map1val2"},
|
||||
"key3": StringOption{"stop.yml", true, "d3map1val3"},
|
||||
},
|
||||
Int1: IntOption{"stop.yml", true, 333},
|
||||
Float1: Float32Option{"stop.yml", true, 3.33},
|
||||
Bool1: BoolOption{"stop.yml", true, true},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("stop.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsStopConfigD2(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"stop.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"stop.yml", true, "d2arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"stop.yml", true, "d2str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key1": StringOption{"stop.yml", true, "d2map1val1"},
|
||||
"key2": StringOption{"stop.yml", true, "d2map1val2"},
|
||||
},
|
||||
Int1: IntOption{"stop.yml", true, 222},
|
||||
Float1: Float32Option{"stop.yml", true, 2.22},
|
||||
Bool1: BoolOption{"stop.yml", true, false},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("stop.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinStopConfigD3(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d3arr1val1")
|
||||
arr1 = append(arr1, "d3arr1val2")
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d3str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key1": "d2map1val1",
|
||||
"key2": "d3map1val2",
|
||||
"key3": "d3map1val3",
|
||||
},
|
||||
Int1: 333,
|
||||
Float1: 3.33,
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("stop.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinStopConfigD2(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d2str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key1": "d2map1val1",
|
||||
"key2": "d2map1val2",
|
||||
},
|
||||
Int1: 222,
|
||||
Float1: 2.22,
|
||||
Bool1: false,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("stop.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func homedir() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return os.Getenv("USERPROFILE")
|
||||
}
|
||||
return os.Getenv("HOME")
|
||||
}
|
||||
|
||||
func FindParentPaths(fileName string) []string {
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
paths := make([]string, 0)
|
||||
|
||||
// special case if homedir is not in current path then check there anyway
|
||||
homedir := homedir()
|
||||
if !strings.HasPrefix(cwd, homedir) {
|
||||
file := path.Join(homedir, fileName)
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
paths = append(paths, filepath.FromSlash(file))
|
||||
}
|
||||
}
|
||||
|
||||
var dir string
|
||||
for _, part := range strings.Split(cwd, string(os.PathSeparator)) {
|
||||
if part == "" && dir == "" {
|
||||
dir = "/"
|
||||
} else {
|
||||
dir = path.Join(dir, part)
|
||||
}
|
||||
file := path.Join(dir, fileName)
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
paths = append(paths, filepath.FromSlash(file))
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
language: go
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
test:
|
||||
go get -t -v
|
||||
go test
|
||||
|
||||
.PHONY: test
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
[](https://travis-ci.org/coryb/kingpeon)
|
||||
[](https://godoc.org/github.com/coryb/kingpeon)
|
||||
|
||||
Kingpeon is a Go library to generate [kingpin](https://godoc.org/gopkg.in/alecthomas/kingpin.v2) command usage from a configuration file. It is useful for creating allowing for user-defined aliases for Go command line tools.
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
hash: 69f929047be51886aecd667c9eedf56f84e06024391695c787f5d2a82238b185
|
||||
updated: 2017-08-28T14:04:17.250162442-07:00
|
||||
imports:
|
||||
- name: github.com/alecthomas/template
|
||||
version: a0175ee3bccc567396460bf5acd36800cb10c49c
|
||||
subpackages:
|
||||
- parse
|
||||
- name: github.com/alecthomas/units
|
||||
version: 2efee857e7cfd4f3d0138cc3cbb1b4966962b93a
|
||||
- name: gopkg.in/alecthomas/kingpin.v2
|
||||
version: 1087e65c9441605df944fb12c33f0fe7072d18ca
|
||||
testImports: []
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package: github.com/coryb/kingpeon
|
||||
import:
|
||||
- package: gopkg.in/alecthomas/kingpin.v2
|
||||
version: ^2.2.5
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
package kingpeon
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestRegisterDynamicCommands(t *testing.T) {
|
||||
data := struct {
|
||||
DynamicCommands []DynamicCommand `yaml:"dynamic-commands"`
|
||||
}{}
|
||||
|
||||
config, err := ioutil.ReadFile("./sample.yml")
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = yaml.Unmarshal(config, &data)
|
||||
assert.Nil(t, err)
|
||||
|
||||
tmpl := template.New("test")
|
||||
app := kingpin.New("kingpeon", "Testing Aliases")
|
||||
|
||||
var expectedShell string
|
||||
run := func(bin string, cmd []string, env []string) error {
|
||||
assert.Equal(t, "/bin/sh", bin)
|
||||
assert.Equal(t, []string{"sh", "-c", expectedShell}, cmd)
|
||||
assert.NotEmpty(t, env)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = RegisterDynamicCommandsWithRunner(run, app, data.DynamicCommands, tmpl)
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo hello world"
|
||||
_, err = app.Parse([]string{"echo"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo -n hello world"
|
||||
_, err = app.Parse([]string{"echo", "--no-newline"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo hello test"
|
||||
_, err = app.Parse([]string{"echo", "test", "--newline"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo -n hello test"
|
||||
_, err = app.Parse([]string{"echo", "test", "--no-newline"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo true"
|
||||
_, err = app.Parse([]string{"test", "bool", "arg", "true"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo true"
|
||||
_, err = app.Parse([]string{"test", "bool", "opt", "--BOOL"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 2"
|
||||
_, err = app.Parse([]string{"test", "counter", "arg", "foo", "bar"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 2"
|
||||
_, err = app.Parse([]string{"test", "counter", "opt", "--COUNTER", "--COUNTER"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo foo"
|
||||
_, err = app.Parse([]string{"test", "enum", "arg", "foo"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo foo"
|
||||
_, err = app.Parse([]string{"test", "enum", "opt", "--ENUM", "foo"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, err = app.Parse([]string{"test", "enum", "opt", "--ENUM", "bogus"})
|
||||
assert.EqualError(t, err, "enum value must be one of foo,bar, got 'bogus'")
|
||||
|
||||
expectedShell = "echo 1.23"
|
||||
_, err = app.Parse([]string{"test", "float32", "arg", "1.23"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 1.23"
|
||||
_, err = app.Parse([]string{"test", "float32", "opt", "--FLOAT32", "1.23"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 1.23"
|
||||
_, err = app.Parse([]string{"test", "float64", "arg", "1.23"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 1.23"
|
||||
_, err = app.Parse([]string{"test", "float64", "opt", "--FLOAT64", "1.23"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 127"
|
||||
_, err = app.Parse([]string{"test", "int8", "arg", "127"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 127"
|
||||
_, err = app.Parse([]string{"test", "int8", "opt", "--INT8", "127"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 127"
|
||||
_, err = app.Parse([]string{"test", "int8", "arg", "127"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 127"
|
||||
_, err = app.Parse([]string{"test", "int8", "opt", "--INT8", "127"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 127"
|
||||
_, err = app.Parse([]string{"test", "int16", "arg", "127"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 127"
|
||||
_, err = app.Parse([]string{"test", "int16", "opt", "--INT16", "127"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 127"
|
||||
_, err = app.Parse([]string{"test", "int32", "arg", "127"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 127"
|
||||
_, err = app.Parse([]string{"test", "int32", "opt", "--INT32", "127"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 127"
|
||||
_, err = app.Parse([]string{"test", "int64", "arg", "127"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 127"
|
||||
_, err = app.Parse([]string{"test", "int64", "opt", "--INT64", "127"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 127"
|
||||
_, err = app.Parse([]string{"test", "int", "arg", "127"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 127"
|
||||
_, err = app.Parse([]string{"test", "int", "opt", "--INT", "127"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo hello"
|
||||
_, err = app.Parse([]string{"test", "string", "arg", "hello"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo hello"
|
||||
_, err = app.Parse([]string{"test", "string", "opt", "--STRING", "hello"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo [abc: def][foo: bar]"
|
||||
_, err = app.Parse([]string{"test", "stringmap", "arg", "foo=bar", "abc=def"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo [abc: def][foo: bar]"
|
||||
_, err = app.Parse([]string{"test", "stringmap", "opt", "--STRINGMAP", "foo=bar", "--STRINGMAP", "abc=def"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 255"
|
||||
_, err = app.Parse([]string{"test", "uint8", "arg", "255"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 255"
|
||||
_, err = app.Parse([]string{"test", "uint8", "opt", "--UINT8", "255"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 255"
|
||||
_, err = app.Parse([]string{"test", "uint8", "arg", "255"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 255"
|
||||
_, err = app.Parse([]string{"test", "uint8", "opt", "--UINT8", "255"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 255"
|
||||
_, err = app.Parse([]string{"test", "uint16", "arg", "255"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 255"
|
||||
_, err = app.Parse([]string{"test", "uint16", "opt", "--UINT16", "255"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 255"
|
||||
_, err = app.Parse([]string{"test", "uint32", "arg", "255"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 255"
|
||||
_, err = app.Parse([]string{"test", "uint32", "opt", "--UINT32", "255"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 255"
|
||||
_, err = app.Parse([]string{"test", "uint64", "arg", "255"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 255"
|
||||
_, err = app.Parse([]string{"test", "uint64", "opt", "--UINT64", "255"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 255"
|
||||
_, err = app.Parse([]string{"test", "uint", "arg", "255"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedShell = "echo 255"
|
||||
_, err = app.Parse([]string{"test", "uint", "opt", "--UINT", "255"})
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
dynamic-commands:
|
||||
- name: echo
|
||||
help: echo stuff
|
||||
script: |-
|
||||
echo {{if not options.newline}}-n {{end}}hello {{args.WORD}}
|
||||
args:
|
||||
- name: WORD
|
||||
default: world
|
||||
options:
|
||||
- name: newline
|
||||
type: BOOL
|
||||
default: true
|
||||
- name: test bool arg
|
||||
help: test bool arg
|
||||
script: |-
|
||||
echo {{args.BOOL}}
|
||||
args:
|
||||
- name: BOOL
|
||||
type: BOOL
|
||||
required: true
|
||||
- name: test bool opt
|
||||
help: test bool opt
|
||||
script: |-
|
||||
echo {{options.BOOL}}
|
||||
options:
|
||||
- name: BOOL
|
||||
type: BOOL
|
||||
required: true
|
||||
- name: test counter arg
|
||||
help: test counter arg
|
||||
script: |-
|
||||
echo {{args.COUNTER}}
|
||||
args:
|
||||
- name: COUNTER
|
||||
type: COUNTER
|
||||
required: true
|
||||
- name: test counter opt
|
||||
help: test counter opt
|
||||
script: |-
|
||||
echo {{options.COUNTER}}
|
||||
options:
|
||||
- name: COUNTER
|
||||
type: COUNTER
|
||||
required: true
|
||||
- name: test enum arg
|
||||
help: test enum arg
|
||||
script: |-
|
||||
echo {{args.ENUM}}
|
||||
args:
|
||||
- name: ENUM
|
||||
type: ENUM
|
||||
enum:
|
||||
- foo
|
||||
- bar
|
||||
required: true
|
||||
- name: test enum opt
|
||||
help: test enum opt
|
||||
script: |-
|
||||
echo {{options.ENUM}}
|
||||
options:
|
||||
- name: ENUM
|
||||
type: ENUM
|
||||
enum:
|
||||
- foo
|
||||
- bar
|
||||
required: true
|
||||
- name: test float32 arg
|
||||
help: test float32 arg
|
||||
script: |-
|
||||
echo {{args.FLOAT32}}
|
||||
args:
|
||||
- name: FLOAT32
|
||||
type: FLOAT32
|
||||
required: true
|
||||
- name: test float32 opt
|
||||
help: test float32 opt
|
||||
script: |-
|
||||
echo {{options.FLOAT32}}
|
||||
options:
|
||||
- name: FLOAT32
|
||||
type: FLOAT32
|
||||
required: true
|
||||
- name: test float64 arg
|
||||
help: test float64 arg
|
||||
script: |-
|
||||
echo {{args.FLOAT64}}
|
||||
args:
|
||||
- name: FLOAT64
|
||||
type: FLOAT64
|
||||
required: true
|
||||
- name: test float64 opt
|
||||
help: test float64 opt
|
||||
script: |-
|
||||
echo {{options.FLOAT64}}
|
||||
options:
|
||||
- name: FLOAT64
|
||||
type: FLOAT64
|
||||
required: true
|
||||
- name: test int8 arg
|
||||
help: test int8 arg
|
||||
script: |-
|
||||
echo {{args.INT8}}
|
||||
args:
|
||||
- name: INT8
|
||||
type: INT8
|
||||
required: true
|
||||
- name: test int8 opt
|
||||
help: test int8 opt
|
||||
script: |-
|
||||
echo {{options.INT8}}
|
||||
options:
|
||||
- name: INT8
|
||||
type: INT8
|
||||
required: true
|
||||
- name: test int16 arg
|
||||
help: test int16 arg
|
||||
script: |-
|
||||
echo {{args.INT16}}
|
||||
args:
|
||||
- name: INT16
|
||||
type: INT16
|
||||
required: true
|
||||
- name: test int16 opt
|
||||
help: test int16 opt
|
||||
script: |-
|
||||
echo {{options.INT16}}
|
||||
options:
|
||||
- name: INT16
|
||||
type: INT16
|
||||
required: true
|
||||
- name: test int32 arg
|
||||
help: test int32 arg
|
||||
script: |-
|
||||
echo {{args.INT32}}
|
||||
args:
|
||||
- name: INT32
|
||||
type: INT32
|
||||
required: true
|
||||
- name: test int32 opt
|
||||
help: test int32 opt
|
||||
script: |-
|
||||
echo {{options.INT32}}
|
||||
options:
|
||||
- name: INT32
|
||||
type: INT32
|
||||
required: true
|
||||
- name: test int64 arg
|
||||
help: test int64 arg
|
||||
script: |-
|
||||
echo {{args.INT64}}
|
||||
args:
|
||||
- name: INT64
|
||||
type: INT64
|
||||
required: true
|
||||
- name: test int64 opt
|
||||
help: test int64 opt
|
||||
script: |-
|
||||
echo {{options.INT64}}
|
||||
options:
|
||||
- name: INT64
|
||||
type: INT64
|
||||
required: true
|
||||
- name: test int arg
|
||||
help: test int arg
|
||||
script: |-
|
||||
echo {{args.INT}}
|
||||
args:
|
||||
- name: INT
|
||||
type: INT
|
||||
required: true
|
||||
- name: test int opt
|
||||
help: test int opt
|
||||
script: |-
|
||||
echo {{options.INT}}
|
||||
options:
|
||||
- name: INT
|
||||
type: INT
|
||||
required: true
|
||||
- name: test string arg
|
||||
help: test string arg
|
||||
script: |-
|
||||
echo {{args.STRING}}
|
||||
args:
|
||||
- name: STRING
|
||||
required: true
|
||||
- name: test string opt
|
||||
help: test string opt
|
||||
script: |-
|
||||
echo {{options.STRING}}
|
||||
options:
|
||||
- name: STRING
|
||||
required: true
|
||||
- name: test stringmap arg
|
||||
help: test stringmap arg
|
||||
script: |-
|
||||
echo {{range $key, $val := args.STRINGMAP}}[{{$key}}: {{$val}}]{{end}}
|
||||
args:
|
||||
- name: STRINGMAP
|
||||
type: STRINGMAP
|
||||
required: true
|
||||
- name: test stringmap opt
|
||||
help: test stringmap opt
|
||||
script: |-
|
||||
echo {{range $key, $val := options.STRINGMAP}}[{{$key}}: {{$val}}]{{end}}
|
||||
options:
|
||||
- name: STRINGMAP
|
||||
type: STRINGMAP
|
||||
required: true
|
||||
- name: test uint8 arg
|
||||
help: test uint8 arg
|
||||
script: |-
|
||||
echo {{args.UINT8}}
|
||||
args:
|
||||
- name: UINT8
|
||||
type: UINT8
|
||||
required: true
|
||||
- name: test uint8 opt
|
||||
help: test uint8 opt
|
||||
script: |-
|
||||
echo {{options.UINT8}}
|
||||
options:
|
||||
- name: UINT8
|
||||
type: UINT8
|
||||
required: true
|
||||
- name: test uint16 arg
|
||||
help: test uint16 arg
|
||||
script: |-
|
||||
echo {{args.UINT16}}
|
||||
args:
|
||||
- name: UINT16
|
||||
type: UINT16
|
||||
required: true
|
||||
- name: test uint16 opt
|
||||
help: test uint16 opt
|
||||
script: |-
|
||||
echo {{options.UINT16}}
|
||||
options:
|
||||
- name: UINT16
|
||||
type: UINT16
|
||||
required: true
|
||||
- name: test uint32 arg
|
||||
help: test uint32 arg
|
||||
script: |-
|
||||
echo {{args.UINT32}}
|
||||
args:
|
||||
- name: UINT32
|
||||
type: UINT32
|
||||
required: true
|
||||
- name: test uint32 opt
|
||||
help: test uint32 opt
|
||||
script: |-
|
||||
echo {{options.UINT32}}
|
||||
options:
|
||||
- name: UINT32
|
||||
type: UINT32
|
||||
required: true
|
||||
- name: test uint64 arg
|
||||
help: test uint64 arg
|
||||
script: |-
|
||||
echo {{args.UINT64}}
|
||||
args:
|
||||
- name: UINT64
|
||||
type: UINT64
|
||||
required: true
|
||||
- name: test uint64 opt
|
||||
help: test uint64 opt
|
||||
script: |-
|
||||
echo {{options.UINT64}}
|
||||
options:
|
||||
- name: UINT64
|
||||
type: UINT64
|
||||
required: true
|
||||
- name: test uint arg
|
||||
help: test uint arg
|
||||
script: |-
|
||||
echo {{args.UINT}}
|
||||
args:
|
||||
- name: UINT
|
||||
type: UINT
|
||||
required: true
|
||||
- name: test uint opt
|
||||
help: test uint opt
|
||||
script: |-
|
||||
echo {{options.UINT}}
|
||||
options:
|
||||
- name: UINT
|
||||
type: UINT
|
||||
required: true
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
language: go
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
test:
|
||||
go get -t -v
|
||||
go test
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
[](https://travis-ci.org/coryb/oreo)
|
||||
[](https://godoc.org/github.com/coryb/oreo)
|
||||
|
||||
Oreo is a simple wrapper build on top of [github.com/sethgrid/pester](http://github.com/sethgrid/pester) http client library. Oreo handles cookies and persist them to disk to be reused between client invocations.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
hash: 36198c10af5880bbc485f4e52d35f5d97a2b3765e1b04a06a057a4496f5502b6
|
||||
updated: 2017-06-20T14:10:57.557488284-07:00
|
||||
imports:
|
||||
- name: github.com/sethgrid/pester
|
||||
version: 8053687f99650573b28fb75cddf3f295082704d7
|
||||
- name: github.com/theckman/go-flock
|
||||
version: 6de226b0d5f040ed85b88c82c381709b98277f3d
|
||||
- name: gopkg.in/op/go-logging.v1
|
||||
version: b2cb9fa56473e98db8caba80237377e83fe44db5
|
||||
testImports:
|
||||
- name: github.com/davecgh/go-spew
|
||||
version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9
|
||||
subpackages:
|
||||
- spew
|
||||
- name: github.com/pmezard/go-difflib
|
||||
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
|
||||
subpackages:
|
||||
- difflib
|
||||
- name: github.com/stretchr/testify
|
||||
version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0
|
||||
subpackages:
|
||||
- assert
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package: github.com/coryb/oreo
|
||||
import:
|
||||
- package: github.com/sethgrid/pester
|
||||
- package: gopkg.in/op/go-logging.v1
|
||||
version: ^1.0.0
|
||||
- package: github.com/theckman/go-flock
|
||||
+77
-147
@@ -15,45 +15,36 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sethgrid/pester"
|
||||
flock "github.com/theckman/go-flock"
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Printf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
type nullLogger struct{}
|
||||
|
||||
func (n *nullLogger) Printf(format string, args ...interface{}) {}
|
||||
|
||||
var DefaultLogger Logger = &nullLogger{}
|
||||
|
||||
type PreRequestCallback func(*http.Request) (*http.Request, error)
|
||||
type PostRequestCallback func(*http.Request, *http.Response) (*http.Response, error)
|
||||
|
||||
type Client struct {
|
||||
http.Client
|
||||
backoff BackoffStrategy
|
||||
maxRetries int
|
||||
var log = logging.MustGetLogger("oreo")
|
||||
|
||||
// var CookieFile = filepath.Join(os.Getenv("HOME"), ".oreo-cookies.js")
|
||||
|
||||
var TraceRequestBody = false
|
||||
var TraceResponseBody = false
|
||||
|
||||
type Client struct {
|
||||
pester.Client
|
||||
preCallbacks []PreRequestCallback
|
||||
postCallbacks []PostRequestCallback
|
||||
|
||||
cookieFile string
|
||||
handlingPostCallback bool
|
||||
log Logger
|
||||
traceCookies bool
|
||||
traceRequestBody bool
|
||||
traceResponseBody bool
|
||||
}
|
||||
|
||||
func New() *Client {
|
||||
return &Client{
|
||||
maxRetries: 3,
|
||||
Client: *pester.New(),
|
||||
handlingPostCallback: false,
|
||||
preCallbacks: []PreRequestCallback{},
|
||||
postCallbacks: []PostRequestCallback{},
|
||||
log: DefaultLogger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +54,8 @@ func (c *Client) WithCookieFile(file string) *Client {
|
||||
if cp.Jar != nil {
|
||||
cp.Jar = nil
|
||||
}
|
||||
// need to reset cached http client with embedded jar
|
||||
cp.Client.EmbedHTTPClient(nil)
|
||||
return &cp
|
||||
}
|
||||
|
||||
@@ -70,33 +63,50 @@ func (c *Client) WithRetries(retries int) *Client {
|
||||
cp := *c
|
||||
// pester MaxRetries is really a MaxAttempts, so if you
|
||||
// want 2 retries that means 3 attempts
|
||||
cp.maxRetries = retries + 1
|
||||
cp.MaxRetries = retries + 1
|
||||
return &cp
|
||||
}
|
||||
|
||||
func (c *Client) WithTimeout(duration time.Duration) *Client {
|
||||
cp := *c
|
||||
cp.Timeout = duration
|
||||
// need to reset cached http client with embedded timeout
|
||||
cp.Client.EmbedHTTPClient(nil)
|
||||
return &cp
|
||||
}
|
||||
|
||||
type BackoffStrategy int
|
||||
|
||||
const (
|
||||
CONSTANT_BACKOFF BackoffStrategy = iota
|
||||
LINEAR_BACKOFF BackoffStrategy = iota
|
||||
NO_BACKOFF BackoffStrategy = iota
|
||||
CONSTANT_BACKOFF BackoffStrategy = iota
|
||||
EXPONENTIAL_BACKOFF BackoffStrategy = iota
|
||||
EXPONENTIAL_JITTER_BACKOFF BackoffStrategy = iota
|
||||
LINEAR_BACKOFF BackoffStrategy = iota
|
||||
LINEAR_JITTER_BACKOFF BackoffStrategy = iota
|
||||
)
|
||||
|
||||
func (c *Client) WithBackoff(backoff BackoffStrategy) *Client {
|
||||
cp := *c
|
||||
cp.backoff = backoff
|
||||
switch backoff {
|
||||
case CONSTANT_BACKOFF:
|
||||
cp.Backoff = pester.DefaultBackoff
|
||||
case EXPONENTIAL_BACKOFF:
|
||||
cp.Backoff = pester.ExponentialBackoff
|
||||
case EXPONENTIAL_JITTER_BACKOFF:
|
||||
cp.Backoff = pester.ExponentialJitterBackoff
|
||||
case LINEAR_BACKOFF:
|
||||
cp.Backoff = pester.LinearBackoff
|
||||
case LINEAR_JITTER_BACKOFF:
|
||||
cp.Backoff = pester.LinearJitterBackoff
|
||||
}
|
||||
return &cp
|
||||
}
|
||||
|
||||
func (c *Client) WithTransport(transport http.RoundTripper) *Client {
|
||||
cp := *c
|
||||
cp.Transport = transport
|
||||
// need to reset cached http client with embedded tranport
|
||||
cp.Client.EmbedHTTPClient(nil)
|
||||
return &cp
|
||||
}
|
||||
|
||||
@@ -135,6 +145,8 @@ func (c *Client) WithoutCallbacks() *Client {
|
||||
func (c *Client) WithCheckRedirect(checkFunc func(*http.Request, []*http.Request) error) *Client {
|
||||
cp := *c
|
||||
cp.CheckRedirect = checkFunc
|
||||
// need to reset cached http client with embedded CheckRedirect
|
||||
cp.Client.EmbedHTTPClient(nil)
|
||||
return &cp
|
||||
}
|
||||
|
||||
@@ -142,32 +154,6 @@ func (c *Client) WithoutRedirect() *Client {
|
||||
return c.WithCheckRedirect(NoRedirect)
|
||||
}
|
||||
|
||||
func (c *Client) WithLogger(l Logger) *Client {
|
||||
cp := *c
|
||||
cp.log = l
|
||||
return &cp
|
||||
}
|
||||
|
||||
func (c *Client) WithRequestTrace(b bool) *Client {
|
||||
cp := *c
|
||||
cp.traceRequestBody = b
|
||||
return &cp
|
||||
}
|
||||
|
||||
func (c *Client) WithResponseTrace(b bool) *Client {
|
||||
cp := *c
|
||||
cp.traceResponseBody = b
|
||||
return &cp
|
||||
}
|
||||
|
||||
func (c *Client) WithTrace(b bool) *Client {
|
||||
cp := *c
|
||||
cp.traceRequestBody = b
|
||||
cp.traceResponseBody = b
|
||||
cp.traceCookies = b
|
||||
return &cp
|
||||
}
|
||||
|
||||
func (c *Client) initCookieJar() (err error) {
|
||||
if c.Jar != nil {
|
||||
return nil
|
||||
@@ -182,14 +168,11 @@ func (c *Client) initCookieJar() (err error) {
|
||||
return err
|
||||
}
|
||||
for _, cookie := range cookies {
|
||||
// this is dumb, cookie.Domain *must not* have a scheme or port url.Parse will parse strings like "localhost"
|
||||
// into the Path variable, not Host. So lets just force Host. We also need to set arbitrary http/https Scheme
|
||||
// as Jar.SetCookies will ignore cookies where the url does not have a http/https Scheme
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: cookie.Domain,
|
||||
url, err := url.Parse(fmt.Sprintf("http://%s", cookie.Domain))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Jar.SetCookies(u, []*http.Cookie{cookie})
|
||||
c.Jar.SetCookies(url, []*http.Cookie{cookie})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -206,20 +189,17 @@ func (c *Client) saveCookies(resp *http.Response) error {
|
||||
if c.cookieFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, ok := resp.Header["Set-Cookie"]; !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
cookies := resp.Cookies()
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Domain == "" {
|
||||
// if it is host:port then we need to split off port
|
||||
parts := strings.Split(resp.Request.URL.Host, ":")
|
||||
host := parts[0]
|
||||
c.log.Printf("Setting DOMAIN to %s for Cookie: %s", host, cookie)
|
||||
cookie.Domain = host
|
||||
}
|
||||
// if it is host:port then we need to split off port
|
||||
parts := strings.Split(resp.Request.URL.Host, ":")
|
||||
host := parts[0]
|
||||
log.Debugf("Setting DOMAIN to %s for Cookie: %s", host, cookie)
|
||||
cookie.Domain = host
|
||||
}
|
||||
|
||||
// expiry in one week from now
|
||||
@@ -298,23 +278,15 @@ func (c *Client) loadCookies() ([]*http.Cookie, error) {
|
||||
cookies := []*http.Cookie{}
|
||||
err = json.Unmarshal(bytes, &cookies)
|
||||
if err != nil {
|
||||
c.log.Printf("Failed to parse cookie file: %s", err)
|
||||
log.Debugf("Failed to parse cookie file: %s", err)
|
||||
}
|
||||
|
||||
if c.traceCookies {
|
||||
c.log.Printf("Loading Cookies: %s", cookies)
|
||||
if log.IsEnabledFor(logging.DEBUG) && os.Getenv("LOG_TRACE") != "" {
|
||||
log.Debugf("Loading Cookies: %s", cookies)
|
||||
}
|
||||
return cookies, nil
|
||||
}
|
||||
|
||||
type bytesReaderCloser struct {
|
||||
bytes.Reader
|
||||
}
|
||||
|
||||
func (b *bytesReaderCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Do(req *http.Request) (resp *http.Response, err error) {
|
||||
for _, cb := range c.preCallbacks {
|
||||
req, err = cb(req)
|
||||
@@ -330,77 +302,41 @@ func (c *Client) Do(req *http.Request) (resp *http.Response, err error) {
|
||||
|
||||
// Callback may want to resubmit the request, so we
|
||||
// will need to rewind (Seek) the Reader back to start.
|
||||
if (c.maxRetries != 0 || (c.traceRequestBody || len(c.postCallbacks) > 0)) && req.Body != nil {
|
||||
bites, err := ioutil.ReadAll(req.Body)
|
||||
var bodyCache []byte
|
||||
if len(c.postCallbacks) > 0 && !c.handlingPostCallback && req.Body != nil {
|
||||
bodyCache, err = ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader := bytes.NewReader(bites)
|
||||
req.Body = &bytesReaderCloser{*reader}
|
||||
req.Body = ioutil.NopCloser(bytes.NewReader(bodyCache))
|
||||
}
|
||||
|
||||
attempt := 1
|
||||
for {
|
||||
resp, err = c.Client.Do(req)
|
||||
if err != nil {
|
||||
if c.traceRequestBody {
|
||||
rewindRequest(req)
|
||||
out, _ := httputil.DumpRequestOut(req, true)
|
||||
c.log.Printf("Request %d: %s", attempt, out)
|
||||
}
|
||||
} else {
|
||||
// we log this after the request is made because http.send
|
||||
// will modify the request to append cookies, so to see the
|
||||
// cookies sent we need to log post-send.
|
||||
if c.traceRequestBody {
|
||||
rewindRequest(req)
|
||||
out, _ := httputil.DumpRequestOut(req, true)
|
||||
c.log.Printf("Request %d: %s", attempt, out)
|
||||
}
|
||||
|
||||
if c.traceResponseBody {
|
||||
out, _ := httputil.DumpResponse(resp, true)
|
||||
c.log.Printf("Response %d: %s", attempt, out)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil || resp.StatusCode >= 500 {
|
||||
if c.maxRetries < 0 || c.maxRetries < attempt+1 {
|
||||
break
|
||||
}
|
||||
|
||||
var idle time.Duration
|
||||
if c.backoff == CONSTANT_BACKOFF {
|
||||
idle = time.Duration(1 * time.Second)
|
||||
} else if c.backoff == LINEAR_BACKOFF {
|
||||
idle = time.Duration(attempt) * time.Second
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.log.Printf("Attempt %d error: %s, retry in %s", attempt, err, idle)
|
||||
} else {
|
||||
c.log.Printf("Attempt %d failed: %s, retry in %s", attempt, resp.Status, idle)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
c.log.Printf("Request Context timeout after attempt %d", attempt)
|
||||
return
|
||||
case <-time.After(idle):
|
||||
}
|
||||
|
||||
// need to reset body for the retry
|
||||
rewindRequest(req)
|
||||
|
||||
attempt++
|
||||
continue
|
||||
}
|
||||
break
|
||||
log.Debugf("%s %s", req.Method, req.URL.String())
|
||||
if log.IsEnabledFor(logging.DEBUG) && TraceRequestBody {
|
||||
out, _ := httputil.DumpRequest(req, true)
|
||||
log.Debugf("Request: %s", out)
|
||||
}
|
||||
|
||||
resp, err = c.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// log any cookies sent b/c they will not be present until
|
||||
// afater we call the `Do` func
|
||||
if log.IsEnabledFor(logging.DEBUG) && TraceRequestBody {
|
||||
for key, values := range req.Header {
|
||||
if key == "Cookie" {
|
||||
for _, cookie := range values {
|
||||
log.Debugf("Cookie: %s", cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if log.IsEnabledFor(logging.DEBUG) && TraceResponseBody {
|
||||
out, _ := httputil.DumpResponse(resp, true)
|
||||
log.Debugf("Response: %s", out)
|
||||
}
|
||||
|
||||
err = c.saveCookies(resp)
|
||||
if err != nil {
|
||||
@@ -408,7 +344,9 @@ func (c *Client) Do(req *http.Request) (resp *http.Response, err error) {
|
||||
}
|
||||
|
||||
if len(c.postCallbacks) > 0 && !c.handlingPostCallback {
|
||||
rewindRequest(req)
|
||||
if len(bodyCache) > 0 {
|
||||
req.Body = ioutil.NopCloser(bytes.NewReader(bodyCache))
|
||||
}
|
||||
c.handlingPostCallback = true
|
||||
defer func() {
|
||||
c.handlingPostCallback = false
|
||||
@@ -424,14 +362,6 @@ func (c *Client) Do(req *http.Request) (resp *http.Response, err error) {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func rewindRequest(req *http.Request) {
|
||||
if req.Body != nil {
|
||||
if rs, ok := req.Body.(io.ReadSeeker); ok {
|
||||
rs.Seek(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Get(urlStr string) (resp *http.Response, err error) {
|
||||
parsed, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
|
||||
+456
@@ -0,0 +1,456 @@
|
||||
package oreo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TraceRequestBody = true
|
||||
TraceResponseBody = true
|
||||
}
|
||||
|
||||
func TestOreoGet(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New()
|
||||
resp, err := c.Get(ts.URL)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
}
|
||||
|
||||
func TestOreoHead(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "HEAD", r.Method)
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New()
|
||||
resp, err := c.Head(ts.URL)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Equal(t, []byte(""), body)
|
||||
assert.Equal(t, int64(2), resp.ContentLength)
|
||||
}
|
||||
|
||||
func TestOreoPost(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
assert.Equal(t, []byte("DATA"), body)
|
||||
contentLength := r.Header["Content-Type"][0]
|
||||
assert.Equal(t, "text/plain", contentLength)
|
||||
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New()
|
||||
resp, err := c.Post(ts.URL, "text/plain", strings.NewReader("DATA"))
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
assert.Equal(t, int64(2), resp.ContentLength)
|
||||
}
|
||||
|
||||
func TestOreoPostForm(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
assert.Equal(t, []byte("key=value"), body)
|
||||
contentLength := r.Header["Content-Type"][0]
|
||||
assert.Equal(t, "application/x-www-form-urlencoded", contentLength)
|
||||
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New()
|
||||
data := url.Values{}
|
||||
data.Add("key", "value")
|
||||
resp, err := c.PostForm(ts.URL, data)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
assert.Equal(t, int64(2), resp.ContentLength)
|
||||
}
|
||||
|
||||
func TestOreoPostJSON(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
assert.Equal(t, []byte(`{"key":"value"}`), body)
|
||||
contentLength := r.Header["Content-Type"][0]
|
||||
assert.Equal(t, "application/json", contentLength)
|
||||
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New()
|
||||
resp, err := c.PostJSON(ts.URL, `{"key":"value"}`)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
assert.Equal(t, int64(2), resp.ContentLength)
|
||||
}
|
||||
|
||||
func TestOreoPut(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "PUT", r.Method)
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
assert.Equal(t, []byte("DATA"), body)
|
||||
contentLength := r.Header["Content-Type"][0]
|
||||
assert.Equal(t, "text/plain", contentLength)
|
||||
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New()
|
||||
resp, err := c.Put(ts.URL, "text/plain", strings.NewReader("DATA"))
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
assert.Equal(t, int64(2), resp.ContentLength)
|
||||
}
|
||||
|
||||
func TestOreoPutJSON(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "PUT", r.Method)
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
assert.Equal(t, []byte(`{"key":"value"}`), body)
|
||||
contentLength := r.Header["Content-Type"][0]
|
||||
assert.Equal(t, "application/json", contentLength)
|
||||
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New()
|
||||
resp, err := c.PutJSON(ts.URL, `{"key":"value"}`)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
assert.Equal(t, int64(2), resp.ContentLength)
|
||||
}
|
||||
|
||||
func TestOreoDelete(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "DELETE", r.Method)
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New()
|
||||
resp, err := c.Delete(ts.URL)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
}
|
||||
|
||||
func TestOreoWithRetries(t *testing.T) {
|
||||
attempts := 0
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
attempts++
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New().WithRetries(2)
|
||||
resp, err := c.Get(ts.URL)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 3, attempts)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, 500, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestOreoWithTimeout(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(2 * time.Second)
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New().WithTimeout(1 * time.Second).WithRetries(2)
|
||||
start := time.Now().Unix()
|
||||
resp, err := c.Get(ts.URL)
|
||||
end := time.Now().Unix()
|
||||
assert.Nil(t, resp)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, end-start >= 2, "duration more than 2x timeout")
|
||||
}
|
||||
|
||||
func TestOreoWithLinearTimeout(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(2 * time.Second)
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New().WithTimeout(1 * time.Second).WithBackoff(LINEAR_BACKOFF).WithRetries(2)
|
||||
|
||||
start := time.Now().Unix()
|
||||
resp, err := c.Get(ts.URL)
|
||||
end := time.Now().Unix()
|
||||
assert.Nil(t, resp)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, end-start >= 3, "duration more than 1*timeout + 2*timeout")
|
||||
}
|
||||
|
||||
func TestOreoWithCookieFile(t *testing.T) {
|
||||
request := 0
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
request++
|
||||
switch request {
|
||||
case 1:
|
||||
cookie := &http.Cookie{
|
||||
Name: "key1",
|
||||
Value: "val1",
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
case 2:
|
||||
cookie := r.Header["Cookie"][0]
|
||||
assert.Equal(t, "key1=val1", cookie)
|
||||
}
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
tmpFile, err := ioutil.TempFile("", "oreo-cookies")
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpFile.Name())
|
||||
|
||||
c := New().WithCookieFile(tmpFile.Name())
|
||||
// first request will get a cookie set on response
|
||||
resp, err := c.Get(ts.URL)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Nil(t, err)
|
||||
|
||||
/// this request should automatically send cookie back to server
|
||||
resp, err = c.Get(ts.URL)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestOreoWithTransport(t *testing.T) {
|
||||
// set tcp connect timeout to 5s
|
||||
var netTransport = &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
}).Dial,
|
||||
}
|
||||
|
||||
c := New().WithTransport(netTransport).WithRetries(0)
|
||||
|
||||
// test against google dns servers, we will get a tcp connection
|
||||
// failure (timeout due to firewall) to a non dns port on those hosts
|
||||
start := time.Now().Unix()
|
||||
resp, err := c.Get("http://8.8.8.8:9999")
|
||||
end := time.Now().Unix()
|
||||
assert.Nil(t, resp)
|
||||
assert.Error(t, err)
|
||||
lapse := end - start
|
||||
msg := fmt.Sprintf("duration between 5-6s timeout, got: %d", lapse)
|
||||
assert.True(t, lapse >= 5 && lapse <= 6, msg)
|
||||
|
||||
}
|
||||
|
||||
func TestOreoWithPostCallback(t *testing.T) {
|
||||
requests := 0
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
_, ok := r.Header["Authorization"]
|
||||
if ok {
|
||||
fmt.Fprintf(w, "OK")
|
||||
} else {
|
||||
http.Error(w, "error", http.StatusUnauthorized)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
var c *Client
|
||||
called := 0
|
||||
callback := func(req *http.Request, resp *http.Response) (*http.Response, error) {
|
||||
called++
|
||||
// if we get a 401 then add auth headers and try the request again
|
||||
if resp.StatusCode == 401 {
|
||||
req.SetBasicAuth("user", "pass")
|
||||
return c.Do(req)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
c = New().WithPostCallback(callback)
|
||||
|
||||
resp, err := c.Get(ts.URL)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, called)
|
||||
assert.Equal(t, 2, requests)
|
||||
}
|
||||
|
||||
func TestOreoWithPreCallback(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, ok := r.Header["Authorization"]
|
||||
assert.True(t, ok)
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
callback := func(req *http.Request) (*http.Request, error) {
|
||||
req.SetBasicAuth("user", "pass")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
c := New().WithPreCallback(callback)
|
||||
|
||||
resp, err := c.Get(ts.URL)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestOreoWithRedirect(t *testing.T) {
|
||||
requests := 0
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
if requests == 1 {
|
||||
http.Redirect(w, r, "/redirect", http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New()
|
||||
|
||||
resp, err := c.Get(ts.URL)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, requests)
|
||||
}
|
||||
|
||||
func TestOreoWithNoRedirect(t *testing.T) {
|
||||
requests := 0
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
if requests == 1 {
|
||||
http.Redirect(w, r, "/redirect/", http.StatusMovedPermanently)
|
||||
} else {
|
||||
fmt.Fprintf(w, "OK")
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New().WithCheckRedirect(NoRedirect)
|
||||
|
||||
resp, err := c.Get(ts.URL)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, requests)
|
||||
}
|
||||
|
||||
func TestOreoWithImmutability(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
result := ""
|
||||
callback1 := func(req *http.Request) (*http.Request, error) {
|
||||
result = "callback1"
|
||||
return req, nil
|
||||
}
|
||||
|
||||
callback2 := func(req *http.Request) (*http.Request, error) {
|
||||
result = "callback2"
|
||||
return req, nil
|
||||
}
|
||||
|
||||
c1 := New().WithPreCallback(callback1)
|
||||
c2 := c1.WithPreCallback(callback2)
|
||||
|
||||
resp, err := c1.Get(ts.URL)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "callback1", result)
|
||||
|
||||
resp, err = c2.Get(ts.URL)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "callback2", result)
|
||||
|
||||
resp, err = c1.Get(ts.URL)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "callback1", result)
|
||||
}
|
||||
|
||||
func TestOreoPostCompressed(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, "gzip", r.Header.Get("Content-Encoding"))
|
||||
|
||||
reader, err := gzip.NewReader(r.Body)
|
||||
assert.Nil(t, err)
|
||||
defer reader.Close()
|
||||
buf := bytes.NewBufferString("")
|
||||
_, err = io.Copy(buf, reader)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, []byte("DATA"), buf.Bytes())
|
||||
contentLength := r.Header["Content-Type"][0]
|
||||
assert.Equal(t, "text/plain", contentLength)
|
||||
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := New()
|
||||
parsed, _ := url.Parse(ts.URL)
|
||||
req := RequestBuilder(parsed).WithMethod("POST").WithContentType("text/plain").WithBody(strings.NewReader("DATA")).WithCompression().Build()
|
||||
resp, err := c.Do(req)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
assert.Equal(t, int64(2), resp.ContentLength)
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.5.4
|
||||
- 1.6.3
|
||||
- 1.7
|
||||
install:
|
||||
- go get -v golang.org/x/tools/cmd/cover
|
||||
script:
|
||||
- go test -v -tags=safe ./spew
|
||||
- go test -v -tags=testcgo ./spew -covermode=count -coverprofile=profile.cov
|
||||
after_success:
|
||||
- go get -v github.com/mattn/goveralls
|
||||
- export PATH=$PATH:$HOME/gopath/bin
|
||||
- goveralls -coverprofile=profile.cov -service=travis-ci
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
go-spew
|
||||
=======
|
||||
|
||||
[]
|
||||
(https://travis-ci.org/davecgh/go-spew) [![ISC License]
|
||||
(http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) [![Coverage Status]
|
||||
(https://img.shields.io/coveralls/davecgh/go-spew.svg)]
|
||||
(https://coveralls.io/r/davecgh/go-spew?branch=master)
|
||||
|
||||
|
||||
Go-spew implements a deep pretty printer for Go data structures to aid in
|
||||
debugging. A comprehensive suite of tests with 100% test coverage is provided
|
||||
to ensure proper functionality. See `test_coverage.txt` for the gocov coverage
|
||||
report. Go-spew is licensed under the liberal ISC license, so it may be used in
|
||||
open source or commercial projects.
|
||||
|
||||
If you're interested in reading about how this package came to life and some
|
||||
of the challenges involved in providing a deep pretty printer, there is a blog
|
||||
post about it
|
||||
[here](https://web.archive.org/web/20160304013555/https://blog.cyphertite.com/go-spew-a-journey-into-dumping-go-data-structures/).
|
||||
|
||||
## Documentation
|
||||
|
||||
[]
|
||||
(http://godoc.org/github.com/davecgh/go-spew/spew)
|
||||
|
||||
Full `go doc` style documentation for the project can be viewed online without
|
||||
installing this package by using the excellent GoDoc site here:
|
||||
http://godoc.org/github.com/davecgh/go-spew/spew
|
||||
|
||||
You can also view the documentation locally once the package is installed with
|
||||
the `godoc` tool by running `godoc -http=":6060"` and pointing your browser to
|
||||
http://localhost:6060/pkg/github.com/davecgh/go-spew/spew
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ go get -u github.com/davecgh/go-spew/spew
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
Add this import line to the file you're working in:
|
||||
|
||||
```Go
|
||||
import "github.com/davecgh/go-spew/spew"
|
||||
```
|
||||
|
||||
To dump a variable with full newlines, indentation, type, and pointer
|
||||
information use Dump, Fdump, or Sdump:
|
||||
|
||||
```Go
|
||||
spew.Dump(myVar1, myVar2, ...)
|
||||
spew.Fdump(someWriter, myVar1, myVar2, ...)
|
||||
str := spew.Sdump(myVar1, myVar2, ...)
|
||||
```
|
||||
|
||||
Alternatively, if you would prefer to use format strings with a compacted inline
|
||||
printing style, use the convenience wrappers Printf, Fprintf, etc with %v (most
|
||||
compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types
|
||||
and pointer addresses):
|
||||
|
||||
```Go
|
||||
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||
```
|
||||
|
||||
## Debugging a Web Application Example
|
||||
|
||||
Here is an example of how you can use `spew.Sdump()` to help debug a web application. Please be sure to wrap your output using the `html.EscapeString()` function for safety reasons. You should also only use this debugging technique in a development environment, never in production.
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, "Hi there, %s!", r.URL.Path[1:])
|
||||
fmt.Fprintf(w, "<!--\n" + html.EscapeString(spew.Sdump(w)) + "\n-->")
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", handler)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
||||
```
|
||||
|
||||
## Sample Dump Output
|
||||
|
||||
```
|
||||
(main.Foo) {
|
||||
unexportedField: (*main.Bar)(0xf84002e210)({
|
||||
flag: (main.Flag) flagTwo,
|
||||
data: (uintptr) <nil>
|
||||
}),
|
||||
ExportedField: (map[interface {}]interface {}) {
|
||||
(string) "one": (bool) true
|
||||
}
|
||||
}
|
||||
([]uint8) {
|
||||
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
|
||||
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
|
||||
00000020 31 32 |12|
|
||||
}
|
||||
```
|
||||
|
||||
## Sample Formatter Output
|
||||
|
||||
Double pointer to a uint8:
|
||||
```
|
||||
%v: <**>5
|
||||
%+v: <**>(0xf8400420d0->0xf8400420c8)5
|
||||
%#v: (**uint8)5
|
||||
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
|
||||
```
|
||||
|
||||
Pointer to circular struct with a uint8 field and a pointer to itself:
|
||||
```
|
||||
%v: <*>{1 <*><shown>}
|
||||
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
|
||||
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
|
||||
%#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)<shown>}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Configuration of spew is handled by fields in the ConfigState type. For
|
||||
convenience, all of the top-level functions use a global state available via the
|
||||
spew.Config global.
|
||||
|
||||
It is also possible to create a ConfigState instance that provides methods
|
||||
equivalent to the top-level functions. This allows concurrent configuration
|
||||
options. See the ConfigState documentation for more details.
|
||||
|
||||
```
|
||||
* Indent
|
||||
String to use for each indentation level for Dump functions.
|
||||
It is a single space by default. A popular alternative is "\t".
|
||||
|
||||
* MaxDepth
|
||||
Maximum number of levels to descend into nested data structures.
|
||||
There is no limit by default.
|
||||
|
||||
* DisableMethods
|
||||
Disables invocation of error and Stringer interface methods.
|
||||
Method invocation is enabled by default.
|
||||
|
||||
* DisablePointerMethods
|
||||
Disables invocation of error and Stringer interface methods on types
|
||||
which only accept pointer receivers from non-pointer variables. This option
|
||||
relies on access to the unsafe package, so it will not have any effect when
|
||||
running in environments without access to the unsafe package such as Google
|
||||
App Engine or with the "safe" build tag specified.
|
||||
Pointer method invocation is enabled by default.
|
||||
|
||||
* DisablePointerAddresses
|
||||
DisablePointerAddresses specifies whether to disable the printing of
|
||||
pointer addresses. This is useful when diffing data structures in tests.
|
||||
|
||||
* DisableCapacities
|
||||
DisableCapacities specifies whether to disable the printing of capacities
|
||||
for arrays, slices, maps and channels. This is useful when diffing data
|
||||
structures in tests.
|
||||
|
||||
* ContinueOnMethod
|
||||
Enables recursion into types after invoking error and Stringer interface
|
||||
methods. Recursion after method invocation is disabled by default.
|
||||
|
||||
* SortKeys
|
||||
Specifies map keys should be sorted before being printed. Use
|
||||
this to have a more deterministic, diffable output. Note that
|
||||
only native types (bool, int, uint, floats, uintptr and string)
|
||||
and types which implement error or Stringer interfaces are supported,
|
||||
with other types sorted according to the reflect.Value.String() output
|
||||
which guarantees display stability. Natural map order is used by
|
||||
default.
|
||||
|
||||
* SpewKeys
|
||||
SpewKeys specifies that, as a last resort attempt, map keys should be
|
||||
spewed to strings and sorted by those strings. This is only considered
|
||||
if SortKeys is true.
|
||||
|
||||
```
|
||||
|
||||
## Unsafe Package Dependency
|
||||
|
||||
This package relies on the unsafe package to perform some of the more advanced
|
||||
features, however it also supports a "limited" mode which allows it to work in
|
||||
environments where the unsafe package is not available. By default, it will
|
||||
operate in this mode on Google App Engine and when compiled with GopherJS. The
|
||||
"safe" build tag may also be specified to force the package to build without
|
||||
using the unsafe package.
|
||||
|
||||
## License
|
||||
|
||||
Go-spew is licensed under the [copyfree](http://copyfree.org) ISC License.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This script uses gocov to generate a test coverage report.
|
||||
# The gocov tool my be obtained with the following command:
|
||||
# go get github.com/axw/gocov/gocov
|
||||
#
|
||||
# It will be installed to $GOPATH/bin, so ensure that location is in your $PATH.
|
||||
|
||||
# Check for gocov.
|
||||
if ! type gocov >/dev/null 2>&1; then
|
||||
echo >&2 "This script requires the gocov tool."
|
||||
echo >&2 "You may obtain it with the following command:"
|
||||
echo >&2 "go get github.com/axw/gocov/gocov"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Only run the cgo tests if gcc is installed.
|
||||
if type gcc >/dev/null 2>&1; then
|
||||
(cd spew && gocov test -tags testcgo | gocov report)
|
||||
else
|
||||
(cd spew && gocov test | gocov report)
|
||||
fi
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package spew_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// custom type to test Stinger interface on non-pointer receiver.
|
||||
type stringer string
|
||||
|
||||
// String implements the Stringer interface for testing invocation of custom
|
||||
// stringers on types with non-pointer receivers.
|
||||
func (s stringer) String() string {
|
||||
return "stringer " + string(s)
|
||||
}
|
||||
|
||||
// custom type to test Stinger interface on pointer receiver.
|
||||
type pstringer string
|
||||
|
||||
// String implements the Stringer interface for testing invocation of custom
|
||||
// stringers on types with only pointer receivers.
|
||||
func (s *pstringer) String() string {
|
||||
return "stringer " + string(*s)
|
||||
}
|
||||
|
||||
// xref1 and xref2 are cross referencing structs for testing circular reference
|
||||
// detection.
|
||||
type xref1 struct {
|
||||
ps2 *xref2
|
||||
}
|
||||
type xref2 struct {
|
||||
ps1 *xref1
|
||||
}
|
||||
|
||||
// indirCir1, indirCir2, and indirCir3 are used to generate an indirect circular
|
||||
// reference for testing detection.
|
||||
type indirCir1 struct {
|
||||
ps2 *indirCir2
|
||||
}
|
||||
type indirCir2 struct {
|
||||
ps3 *indirCir3
|
||||
}
|
||||
type indirCir3 struct {
|
||||
ps1 *indirCir1
|
||||
}
|
||||
|
||||
// embed is used to test embedded structures.
|
||||
type embed struct {
|
||||
a string
|
||||
}
|
||||
|
||||
// embedwrap is used to test embedded structures.
|
||||
type embedwrap struct {
|
||||
*embed
|
||||
e *embed
|
||||
}
|
||||
|
||||
// panicer is used to intentionally cause a panic for testing spew properly
|
||||
// handles them
|
||||
type panicer int
|
||||
|
||||
func (p panicer) String() string {
|
||||
panic("test panic")
|
||||
}
|
||||
|
||||
// customError is used to test custom error interface invocation.
|
||||
type customError int
|
||||
|
||||
func (e customError) Error() string {
|
||||
return fmt.Sprintf("error: %d", int(e))
|
||||
}
|
||||
|
||||
// stringizeWants converts a slice of wanted test output into a format suitable
|
||||
// for a test error message.
|
||||
func stringizeWants(wants []string) string {
|
||||
s := ""
|
||||
for i, want := range wants {
|
||||
if i > 0 {
|
||||
s += fmt.Sprintf("want%d: %s", i+1, want)
|
||||
} else {
|
||||
s += "want: " + want
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// testFailed returns whether or not a test failed by checking if the result
|
||||
// of the test is in the slice of wanted strings.
|
||||
func testFailed(result string, wants []string) bool {
|
||||
for _, want := range wants {
|
||||
if result == want {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type sortableStruct struct {
|
||||
x int
|
||||
}
|
||||
|
||||
func (ss sortableStruct) String() string {
|
||||
return fmt.Sprintf("ss.%d", ss.x)
|
||||
}
|
||||
|
||||
type unsortableStruct struct {
|
||||
x int
|
||||
}
|
||||
|
||||
type sortTestCase struct {
|
||||
input []reflect.Value
|
||||
expected []reflect.Value
|
||||
}
|
||||
|
||||
func helpTestSortValues(tests []sortTestCase, cs *spew.ConfigState, t *testing.T) {
|
||||
getInterfaces := func(values []reflect.Value) []interface{} {
|
||||
interfaces := []interface{}{}
|
||||
for _, v := range values {
|
||||
interfaces = append(interfaces, v.Interface())
|
||||
}
|
||||
return interfaces
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
spew.SortValues(test.input, cs)
|
||||
// reflect.DeepEqual cannot really make sense of reflect.Value,
|
||||
// probably because of all the pointer tricks. For instance,
|
||||
// v(2.0) != v(2.0) on a 32-bits system. Turn them into interface{}
|
||||
// instead.
|
||||
input := getInterfaces(test.input)
|
||||
expected := getInterfaces(test.expected)
|
||||
if !reflect.DeepEqual(input, expected) {
|
||||
t.Errorf("Sort mismatch:\n %v != %v", input, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSortValues ensures the sort functionality for relect.Value based sorting
|
||||
// works as intended.
|
||||
func TestSortValues(t *testing.T) {
|
||||
v := reflect.ValueOf
|
||||
|
||||
a := v("a")
|
||||
b := v("b")
|
||||
c := v("c")
|
||||
embedA := v(embed{"a"})
|
||||
embedB := v(embed{"b"})
|
||||
embedC := v(embed{"c"})
|
||||
tests := []sortTestCase{
|
||||
// No values.
|
||||
{
|
||||
[]reflect.Value{},
|
||||
[]reflect.Value{},
|
||||
},
|
||||
// Bools.
|
||||
{
|
||||
[]reflect.Value{v(false), v(true), v(false)},
|
||||
[]reflect.Value{v(false), v(false), v(true)},
|
||||
},
|
||||
// Ints.
|
||||
{
|
||||
[]reflect.Value{v(2), v(1), v(3)},
|
||||
[]reflect.Value{v(1), v(2), v(3)},
|
||||
},
|
||||
// Uints.
|
||||
{
|
||||
[]reflect.Value{v(uint8(2)), v(uint8(1)), v(uint8(3))},
|
||||
[]reflect.Value{v(uint8(1)), v(uint8(2)), v(uint8(3))},
|
||||
},
|
||||
// Floats.
|
||||
{
|
||||
[]reflect.Value{v(2.0), v(1.0), v(3.0)},
|
||||
[]reflect.Value{v(1.0), v(2.0), v(3.0)},
|
||||
},
|
||||
// Strings.
|
||||
{
|
||||
[]reflect.Value{b, a, c},
|
||||
[]reflect.Value{a, b, c},
|
||||
},
|
||||
// Array
|
||||
{
|
||||
[]reflect.Value{v([3]int{3, 2, 1}), v([3]int{1, 3, 2}), v([3]int{1, 2, 3})},
|
||||
[]reflect.Value{v([3]int{1, 2, 3}), v([3]int{1, 3, 2}), v([3]int{3, 2, 1})},
|
||||
},
|
||||
// Uintptrs.
|
||||
{
|
||||
[]reflect.Value{v(uintptr(2)), v(uintptr(1)), v(uintptr(3))},
|
||||
[]reflect.Value{v(uintptr(1)), v(uintptr(2)), v(uintptr(3))},
|
||||
},
|
||||
// SortableStructs.
|
||||
{
|
||||
// Note: not sorted - DisableMethods is set.
|
||||
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
|
||||
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
|
||||
},
|
||||
// UnsortableStructs.
|
||||
{
|
||||
// Note: not sorted - SpewKeys is false.
|
||||
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||
},
|
||||
// Invalid.
|
||||
{
|
||||
[]reflect.Value{embedB, embedA, embedC},
|
||||
[]reflect.Value{embedB, embedA, embedC},
|
||||
},
|
||||
}
|
||||
cs := spew.ConfigState{DisableMethods: true, SpewKeys: false}
|
||||
helpTestSortValues(tests, &cs, t)
|
||||
}
|
||||
|
||||
// TestSortValuesWithMethods ensures the sort functionality for relect.Value
|
||||
// based sorting works as intended when using string methods.
|
||||
func TestSortValuesWithMethods(t *testing.T) {
|
||||
v := reflect.ValueOf
|
||||
|
||||
a := v("a")
|
||||
b := v("b")
|
||||
c := v("c")
|
||||
tests := []sortTestCase{
|
||||
// Ints.
|
||||
{
|
||||
[]reflect.Value{v(2), v(1), v(3)},
|
||||
[]reflect.Value{v(1), v(2), v(3)},
|
||||
},
|
||||
// Strings.
|
||||
{
|
||||
[]reflect.Value{b, a, c},
|
||||
[]reflect.Value{a, b, c},
|
||||
},
|
||||
// SortableStructs.
|
||||
{
|
||||
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
|
||||
[]reflect.Value{v(sortableStruct{1}), v(sortableStruct{2}), v(sortableStruct{3})},
|
||||
},
|
||||
// UnsortableStructs.
|
||||
{
|
||||
// Note: not sorted - SpewKeys is false.
|
||||
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||
},
|
||||
}
|
||||
cs := spew.ConfigState{DisableMethods: false, SpewKeys: false}
|
||||
helpTestSortValues(tests, &cs, t)
|
||||
}
|
||||
|
||||
// TestSortValuesWithSpew ensures the sort functionality for relect.Value
|
||||
// based sorting works as intended when using spew to stringify keys.
|
||||
func TestSortValuesWithSpew(t *testing.T) {
|
||||
v := reflect.ValueOf
|
||||
|
||||
a := v("a")
|
||||
b := v("b")
|
||||
c := v("c")
|
||||
tests := []sortTestCase{
|
||||
// Ints.
|
||||
{
|
||||
[]reflect.Value{v(2), v(1), v(3)},
|
||||
[]reflect.Value{v(1), v(2), v(3)},
|
||||
},
|
||||
// Strings.
|
||||
{
|
||||
[]reflect.Value{b, a, c},
|
||||
[]reflect.Value{a, b, c},
|
||||
},
|
||||
// SortableStructs.
|
||||
{
|
||||
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
|
||||
[]reflect.Value{v(sortableStruct{1}), v(sortableStruct{2}), v(sortableStruct{3})},
|
||||
},
|
||||
// UnsortableStructs.
|
||||
{
|
||||
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||
[]reflect.Value{v(unsortableStruct{1}), v(unsortableStruct{2}), v(unsortableStruct{3})},
|
||||
},
|
||||
}
|
||||
cs := spew.ConfigState{DisableMethods: true, SpewKeys: true}
|
||||
helpTestSortValues(tests, &cs, t)
|
||||
}
|
||||
+1042
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user