Compare commits

..

3 Commits

Author SHA1 Message Date
Cory Bennett 009103e6e4 version bump 2018-03-08 11:42:14 -08:00
Cory Bennett 8922d89f74 Updated Changelog 2018-03-08 11:42:14 -08:00
Cory Bennett 10661e2fc4 Updated Usage 2018-03-08 11:42:14 -08:00
553 changed files with 53854 additions and 35610 deletions
-1
View File
@@ -3,7 +3,6 @@ config:
password-source: pass
endpoint: https://go-jira.atlassian.net
user: admin
login: atlassian@corybennett.org
queries:
todo: |
-35
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+13 -14
View File
@@ -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
+385 -399
View File
@@ -1,4 +1,4 @@
[![Join the chat at https://gitter.im/go-jira-cli/help](https://badges.gitter.im/go-jira-cli/help.svg)](https://gitter.im/go-jira-cli/help?utm_source=badge&utm_medium=badge&utm_content=badge)
g[![Join the chat at https://gitter.im/go-jira-cli/help](https://badges.gitter.im/go-jira-cli/help.svg)](https://gitter.im/go-jira-cli/help?utm_source=badge&utm_medium=badge&utm_content=badge)
[![Build Status](https://travis-ci.org/Netflix-Skunkworks/go-jira.svg?branch=master)](https://travis-ci.org/Netflix-Skunkworks/go-jira)
[![GoDoc](https://godoc.org/gopkg.in/Netflix-Skunkworks/go-jira.v1?status.svg)](https://godoc.org/gopkg.in/Netflix-Skunkworks/go-jira.v1)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](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 **&lt;command&gt;.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 **&lt;command&gt;.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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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
+3 -1
View File
@@ -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
+20 -26
View File
@@ -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(
+1 -1
View File
@@ -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
View File
@@ -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)
},
)
+5
View File
@@ -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() {
-13
View File
@@ -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()
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
+1 -1
View File
@@ -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)
}
}
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
-21
View File
@@ -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
View File
@@ -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 {
-1
View File
@@ -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()})
}
-47
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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. -->
-12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
File diff suppressed because it is too large Load Diff
+293
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,10 @@
language: go
go:
# - 1.0
# - 1.1
- 1.2
- 1.3
- 1.4
- 1.5
- 1.6
+245
View File
@@ -0,0 +1,245 @@
# genny - Generics for Go
[![Build Status](https://travis-ci.org/cheekybits/genny.svg?branch=master)](https://travis-ci.org/cheekybits/genny) [![GoDoc](https://godoc.org/github.com/cheekybits/genny/parse?status.png)](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
View File
@@ -0,0 +1,2 @@
// Package main is the command line tool for Genny.
package main
+160
View File
@@ -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
View File
@@ -0,0 +1 @@
package main_test
+1
View File
@@ -0,0 +1 @@
language: go
+73
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,4 @@
[![Build Status](https://travis-ci.org/coryb/figtree.svg?branch=master)](https://travis-ci.org/coryb/figtree)
[![GoDoc](https://godoc.org/github.com/coryb/figtree?status.png)](https://godoc.org/github.com/coryb/figtree)
Figtree is a go library to recusively parse and merge yaml based config files.
+113
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -2,7 +2,7 @@ package figtree
import "regexp"
type option interface {
type Option interface {
IsDefined() bool
GetValue() interface{}
SetValue(interface{}) error
+130
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1 @@
language: go
+5
View File
@@ -0,0 +1,5 @@
test:
go get -t -v
go test
.PHONY: test
+4
View File
@@ -0,0 +1,4 @@
[![Build Status](https://travis-ci.org/coryb/kingpeon.svg?branch=master)](https://travis-ci.org/coryb/kingpeon)
[![GoDoc](https://godoc.org/github.com/coryb/kingpeon?status.png)](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
View File
@@ -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
View File
@@ -0,0 +1,4 @@
package: github.com/coryb/kingpeon
import:
- package: gopkg.in/alecthomas/kingpin.v2
version: ^2.2.5
+208
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1 @@
language: go
+3
View File
@@ -0,0 +1,3 @@
test:
go get -t -v
go test
+4
View File
@@ -0,0 +1,4 @@
[![Build Status](https://travis-ci.org/coryb/oreo.svg?branch=master)](https://travis-ci.org/coryb/oreo)
[![GoDoc](https://godoc.org/github.com/coryb/oreo?status.png)](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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,205 @@
go-spew
=======
[![Build Status](https://img.shields.io/travis/davecgh/go-spew.svg)]
(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
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)]
(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
View File
@@ -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
View File
@@ -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)
}
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