Compare commits
220 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 568c711625 | |||
| e329b6d9ff | |||
| 181672afcc | |||
| d523af1b6c | |||
| 390e3bd772 | |||
| 21549749c0 | |||
| 6184509494 | |||
| b1f69564a3 | |||
| 159cb4a911 | |||
| b5af9a3968 | |||
| 2e4aea4cbc | |||
| b6e5b11174 | |||
| 9dec52c727 | |||
| f2b536dbad | |||
| 62f9c01cab | |||
| 6d2f26d952 | |||
| c2ffd465c4 | |||
| 3e18a060ae | |||
| 6f5962f8f2 | |||
| 346c761ef6 | |||
| e513eb4bf4 | |||
| 22c8a1670b | |||
| 9c7f8c1ac4 | |||
| 4fadb1171f | |||
| dc184d2737 | |||
| 7467933680 | |||
| fd50118712 | |||
| 67c317e741 | |||
| ab3ff0343e | |||
| f007fa5280 | |||
| a904537367 | |||
| 0814b10a16 | |||
| d279505930 | |||
| 0f4bbfac9f | |||
| 6bbf14f7d9 | |||
| 6797bf7cf7 | |||
| c6603894c1 | |||
| b8498c564d | |||
| dd5fa20415 | |||
| c7a8fbc929 | |||
| 04ca211531 | |||
| ae5e9e7894 | |||
| 47e5ae5527 | |||
| 534e99bf13 | |||
| 0e90ccb441 | |||
| f692da2465 | |||
| fb9b22a118 | |||
| eefcd7f7d5 | |||
| c4f29b5d8b | |||
| dd84a68483 | |||
| c9199d62e4 | |||
| 3d824b47b7 | |||
| cc98de88ce | |||
| e71b45b28f | |||
| 41ac427a89 | |||
| 7b3c5fd028 | |||
| 526c9a20ac | |||
| a75ab4f92f | |||
| 345bf3d409 | |||
| e6e324235d | |||
| 6c8d2e6b6d | |||
| c8ea86f6ce | |||
| 4f437e4642 | |||
| 7267733aa8 | |||
| 95bd1b3072 | |||
| 9cb199c8b3 | |||
| 4407c0190b | |||
| 4897bea79f | |||
| 38294cd0c6 | |||
| dc4f42bb26 | |||
| 472ce4863f | |||
| de626ab2bb | |||
| f8c8932866 | |||
| 6d3b81fc61 | |||
| 11e0031a9f | |||
| 53f63a7614 | |||
| 8c5b2e0c17 | |||
| 7fa3a8900c | |||
| e1634fe669 | |||
| 876c853073 | |||
| 569921cd72 | |||
| fda328dc23 | |||
| aa03daeff8 | |||
| 811481298a | |||
| 5af6e4d1ec | |||
| 725fda68dc | |||
| 157f468e0c | |||
| e1249a9ddb | |||
| a03e812615 | |||
| c38778ba62 | |||
| 3414f34025 | |||
| 66562fdb41 | |||
| 77159d9db6 | |||
| 35d1cedc24 | |||
| f38dfbbf78 | |||
| 78b0610218 | |||
| c6c270c7dd | |||
| 1c779a0d75 | |||
| 8702b37430 | |||
| 5b4e60e7d7 | |||
| 93632d077c | |||
| cc5cbae14f | |||
| 779ff0ad10 | |||
| d8bd2662ff | |||
| 28e0012470 | |||
| 3463804a7c | |||
| 3f4520da67 | |||
| be815d0a41 | |||
| 93ce24d3f3 | |||
| e72a7d2e22 | |||
| 40d9dcd6d4 | |||
| c872aae7e8 | |||
| 8ff3848ea3 | |||
| 7b87d6092f | |||
| 669414ca70 | |||
| 3f5fee20c0 | |||
| c6e809a1a2 | |||
| 2088f61cba | |||
| a818e29113 | |||
| 8fe6c9a05d | |||
| 7c4e6e94fc | |||
| 31e76bcd00 | |||
| c47c06e822 | |||
| bf3d042da6 | |||
| d05911cdcb | |||
| 634ca84807 | |||
| 8c46a8343b | |||
| 13d5630e80 | |||
| f1003560f1 | |||
| 997db04b9f | |||
| 769e743563 | |||
| 43f519a712 | |||
| a617b1a6ac | |||
| 9117f4f6d6 | |||
| b53996f175 | |||
| 154177e46a | |||
| d4e98281d7 | |||
| ce186487f4 | |||
| 1110f82443 | |||
| a99abd4e6f | |||
| 8728ec4786 | |||
| 34ae132930 | |||
| f993aaa11c | |||
| d82e838554 | |||
| 9c35aa070c | |||
| 14ee68a06e | |||
| e0e1dd8ade | |||
| 9852f0f0a5 | |||
| 2cf3f4e1a8 | |||
| 1cf531b1c3 | |||
| 74e55e3089 | |||
| 6c77ff0a2c | |||
| 002c332b68 | |||
| ffba396523 | |||
| b016e99ea6 | |||
| e47f9afb11 | |||
| d3fd9bddd9 | |||
| 42b72b028b | |||
| c6939d57dc | |||
| 90f751a61d | |||
| e86c2cf610 | |||
| 0aa7395e21 | |||
| 3f834f5c3d | |||
| 5bca86cdb2 | |||
| 896d1608ca | |||
| e5c18aa87e | |||
| f056f94305 | |||
| 1a17dc83fe | |||
| 753734428f | |||
| f76426e9b0 | |||
| 5803d5e849 | |||
| 385898b21f | |||
| ce7f8d5d28 | |||
| b2899d1d3e | |||
| e8628cd662 | |||
| 4e1719d966 | |||
| 98f86cf044 | |||
| fc0544e4a2 | |||
| 537f5c21bc | |||
| a0880ab82d | |||
| 5636ead921 | |||
| 01e0c9e972 | |||
| 54a7190113 | |||
| b65bdbbb24 | |||
| ef3f43f89f | |||
| 22073d8847 | |||
| c05ca8c528 | |||
| f9e799fea0 | |||
| 9b99452b60 | |||
| b93cdc346e | |||
| d59f6228ad | |||
| 78c8c61bf2 | |||
| 7ed5d4af9b | |||
| 7905c74687 | |||
| 2fb08ab579 | |||
| 681e8b6459 | |||
| 8a3770db29 | |||
| ae54669c02 | |||
| b449793825 | |||
| 66a9e48e39 | |||
| d85ee5e068 | |||
| 7da4d8ba9f | |||
| 8d260c6a7e | |||
| 48061f1508 | |||
| cfed5cfe4b | |||
| 471c849c50 | |||
| 337621998e | |||
| 0feeb945fc | |||
| 4f9ed434c5 | |||
| 0934b9414c | |||
| 356870358d | |||
| 6dc331726d | |||
| 3f8dbf09dc | |||
| dcda096b5d | |||
| 7079128eca | |||
| 6e3ec89ce1 | |||
| 10396a5434 | |||
| d009919755 | |||
| b595694658 | |||
| f84edae99d |
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
open_collective: webhook
|
||||
@@ -0,0 +1,23 @@
|
||||
name: build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.14.x, 1.15.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Build
|
||||
run: go build -v
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
@@ -3,3 +3,4 @@
|
||||
coverage
|
||||
webhook
|
||||
/test/hookecho
|
||||
build
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.14.x
|
||||
- master
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
- windows
|
||||
|
||||
arch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- go: master
|
||||
exclude:
|
||||
- os: windows
|
||||
go: master
|
||||
- os: windows
|
||||
arch: arm64
|
||||
- os: osx
|
||||
arch: arm64
|
||||
|
||||
install:
|
||||
- go get -d -v -t ./...
|
||||
|
||||
script:
|
||||
- go test -v -race ./...
|
||||
Generated
-40
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"ImportPath": "github.com/adnanh/webhook",
|
||||
"GoVersion": "go1.9",
|
||||
"GodepVersion": "v79",
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/codegangsta/negroni",
|
||||
"Comment": "v0.2.0-151-g5bc66cf",
|
||||
"Rev": "5bc66cf1ad89af58511e07e108a31f219ed61012"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/ghodss/yaml",
|
||||
"Comment": "v1.0.0",
|
||||
"Rev": "0ca9ea5df5451ffdf184b4428c902747c2c11cd7"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/mux",
|
||||
"Comment": "v1.5.0-2-gbdd5a5a",
|
||||
"Rev": "bdd5a5a1b0b489d297b73eb62b5f6328df198bfc"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/satori/go.uuid",
|
||||
"Comment": "v1.1.0-8-g5bf94b6",
|
||||
"Rev": "5bf94b69c6b68ee1b541973bb8e1144db23a194b"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/sys/unix",
|
||||
"Rev": "ebfc5b4631820b793c9010c87fd8fef0f39eb082"
|
||||
},
|
||||
{
|
||||
"ImportPath": "gopkg.in/fsnotify.v1",
|
||||
"Comment": "v1.4.2",
|
||||
"Rev": "629574ca2a5df945712d3079857300b5e4da0236"
|
||||
},
|
||||
{
|
||||
"ImportPath": "gopkg.in/yaml.v2",
|
||||
"Rev": "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
-5
@@ -1,5 +0,0 @@
|
||||
This directory tree is generated automatically by godep.
|
||||
|
||||
Please do not edit.
|
||||
|
||||
See https://github.com/tools/godep for more information.
|
||||
@@ -1,12 +1,18 @@
|
||||
OS = darwin freebsd linux openbsd windows
|
||||
OS = darwin freebsd linux openbsd
|
||||
ARCHS = 386 arm amd64 arm64
|
||||
|
||||
all: build release
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
build: deps
|
||||
.PHONY: help
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-16s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
all: build release release-windows
|
||||
|
||||
build: deps ## Build the project
|
||||
go build
|
||||
|
||||
release: clean deps
|
||||
release: clean deps ## Generate releases for unix systems
|
||||
@for arch in $(ARCHS);\
|
||||
do \
|
||||
for os in $(OS);\
|
||||
@@ -18,12 +24,21 @@ release: clean deps
|
||||
done \
|
||||
done
|
||||
|
||||
test: deps
|
||||
release-windows: clean deps ## Generate release for windows
|
||||
@for arch in $(ARCHS);\
|
||||
do \
|
||||
echo "Building windows-$$arch"; \
|
||||
mkdir -p build/webhook-windows-$$arch/; \
|
||||
GOOS=windows GOARCH=$$arch go build -o build/webhook-windows-$$arch/webhook.exe; \
|
||||
tar cz -C build -f build/webhook-windows-$$arch.tar.gz webhook-windows-$$arch; \
|
||||
done
|
||||
|
||||
test: deps ## Execute tests
|
||||
go test ./...
|
||||
|
||||
deps:
|
||||
deps: ## Install dependencies using go get
|
||||
go get -d -v -t ./...
|
||||
|
||||
clean:
|
||||
clean: ## Remove building artifacts
|
||||
rm -rf build
|
||||
rm -f webhook
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# What is webhook?
|
||||
# What is webhook? ![build-status][badge]
|
||||
|
||||
<img src="https://github.com/adnanh/webhook/raw/development/docs/logo/logo-128x128.png" alt="Webhook" align="left" />
|
||||
|
||||
@@ -18,21 +18,27 @@ If you use Mattermost or Slack, you can set up an "Outgoing webhook integration"
|
||||
Everything else is the responsibility of the command's author.
|
||||
|
||||
# Hookdoo
|
||||
<a href="https://www.hookdoo.com/?github"><img src="https://my.hookdoo.com/logo/logo-dark-96.png" alt="hookdoo" align="left" /></a>
|
||||
<a href="https://www.hookdoo.com/?github"><img src="https://www.hookdoo.com/logo/logo.svg" height="96" alt="hookdoo" align="left" /></a>
|
||||
|
||||
If you don't have time to waste configuring, hosting, debugging and maintaining your webhook instance, we offer a __SaaS__ solution that has all of the capabilities webhook provides, plus a lot more, and all that packaged in a nice friendly web interface. If you are interested, find out more at [hookdoo website](https://www.hookdoo.com/?ref=github-webhook-readme). If you have any questions, you can contact us at info@hookdoo.com
|
||||
|
||||
#
|
||||
|
||||
<a href="https://www.hookdeck.io/?ref=adnanh-webhook"><img src="http://hajdarevic.net/hookdeck-logo.svg" height="17" alt="hookdeck" align="left" /></a> If you need a way of inspecting, monitoring and replaying webhooks without the back and forth troubleshooting, [give Hookdeck a try!](https://www.hookdeck.io/?ref=adnanh-webhook)
|
||||
|
||||
# Getting started
|
||||
## Installation
|
||||
### Building from source
|
||||
To get started, first make sure you've properly set up your [Golang](http://golang.org/doc/install) environment and then run the
|
||||
To get started, first make sure you've properly set up your [Go](http://golang.org/doc/install) 1.14 or newer environment and then run
|
||||
```bash
|
||||
$ go get github.com/adnanh/webhook
|
||||
$ go build github.com/adnanh/webhook
|
||||
```
|
||||
to get the latest version of the [webhook][w].
|
||||
to build the latest version of the [webhook][w].
|
||||
|
||||
### Using package manager
|
||||
#### Snap store
|
||||
[](https://snapcraft.io/webhook)
|
||||
|
||||
#### Ubuntu
|
||||
If you are using Ubuntu linux (17.04 or later), you can install webhook using `sudo apt-get install webhook` which will install community packaged version.
|
||||
|
||||
@@ -43,9 +49,11 @@ If you are using Debian linux ("stretch" or later), you can install webhook usin
|
||||
Prebuilt binaries for different architectures are available at [GitHub Releases](https://github.com/adnanh/webhook/releases).
|
||||
|
||||
## Configuration
|
||||
Next step is to define some hooks you want [webhook][w] to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook][w] will serve. Check [Hook definition page](docs/Hook-Definition.md) to see the detailed description of what properties a hook can contain, and how to use them.
|
||||
Next step is to define some hooks you want [webhook][w] to serve.
|
||||
[webhook][w] supports JSON or YAML configuration files, but we'll focus primarily on JSON in the following example.
|
||||
Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook][w] will serve. Check [Hook definition page](docs/Hook-Definition.md) to see the detailed description of what properties a hook can contain, and how to use them.
|
||||
|
||||
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`.
|
||||
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`. Make sure that your bash script has `#!/bin/sh` shebang on top.
|
||||
|
||||
Our `hooks.json` file will now look like this:
|
||||
```json
|
||||
@@ -58,6 +66,13 @@ Our `hooks.json` file will now look like this:
|
||||
]
|
||||
```
|
||||
|
||||
**NOTE:** If you prefer YAML, the equivalent `hooks.yaml` file would be:
|
||||
```yaml
|
||||
- id: redeploy-webhook
|
||||
execute-command: "/var/scripts/redeploy.sh"
|
||||
command-working-directory: "/var/webhook"
|
||||
```
|
||||
|
||||
You can now run [webhook][w] using
|
||||
```bash
|
||||
$ /path/to/webhook -hooks hooks.json -verbose
|
||||
@@ -74,25 +89,58 @@ By performing a simple HTTP GET or POST request to that endpoint, your specified
|
||||
|
||||
However, hook defined like that could pose a security threat to your system, because anyone who knows your endpoint, can send a request and execute your command. To prevent that, you can use the `"trigger-rule"` property for your hook, to specify the exact circumstances under which the hook would be triggered. For example, you can use them to add a secret that you must supply as a parameter in order to successfully trigger the hook. Please check out the [Hook rules page](docs/Hook-Rules.md) for detailed list of available rules and their usage.
|
||||
|
||||
## Multipart Form Data
|
||||
[webhook][w] provides limited support the parsing of multipart form data.
|
||||
Multipart form data can contain two types of parts: values and files.
|
||||
All form _values_ are automatically added to the `payload` scope.
|
||||
Use the `parse-parameters-as-json` settings to parse a given value as JSON.
|
||||
All files are ignored unless they match one of the following criteria:
|
||||
|
||||
1. The `Content-Type` header is `application/json`.
|
||||
1. The part is named in the `parse-parameters-as-json` setting.
|
||||
|
||||
In either case, the given file part will be parsed as JSON and added to the `payload` map.
|
||||
|
||||
## Templates
|
||||
[webhook][w] can parse the `hooks.json` input file as a Go template when given the `-template` [CLI parameter](docs/Webhook-Parameters.md). See the [Templates page](docs/Templates.md) for more details on template usage.
|
||||
[webhook][w] can parse the hooks configuration file as a Go template when given the `-template` [CLI parameter](docs/Webhook-Parameters.md). See the [Templates page](docs/Templates.md) for more details on template usage.
|
||||
|
||||
## Using HTTPS
|
||||
[webhook][w] by default serves hooks using http. If you want [webhook][w] to serve secure content using https, you can use the `-secure` flag while starting [webhook][w]. Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate.
|
||||
|
||||
TLS version and cipher suite selection flags are available from the command line. To list available cipher suites, use the `-list-cipher-suites` flag. The `-tls-min-version` flag can be used with `-list-cipher-suites`.
|
||||
|
||||
## CORS Headers
|
||||
If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook][w] to set the appropriate CORS headers that will be returned with each response.
|
||||
|
||||
## Interested in running webhook inside of a Docker container?
|
||||
You can use [almir/webhook](https://hub.docker.com/r/almir/webhook/) docker image, or create your own (please read [this discussion](https://github.com/adnanh/webhook/issues/63)).
|
||||
You can use one of the following Docker images, or create your own (please read [this discussion](https://github.com/adnanh/webhook/issues/63)):
|
||||
- [almir/webhook](https://github.com/almir/docker-webhook)
|
||||
- [roxedus/webhook](https://github.com/Roxedus/docker-webhook)
|
||||
- [thecatlady/webhook](https://github.com/thecatlady/docker-webhook)
|
||||
|
||||
## Examples
|
||||
Check out [Hook examples page](docs/Hook-Examples.md) for more complex examples of hooks.
|
||||
|
||||
### Guides featuring webhook
|
||||
- [Plex 2 Telegram](https://gitlab.com/-/snippets/1972594) by [@psyhomb](https://github.com/psyhomb)
|
||||
- [Webhook & JIRA](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks) by [@perfecto25](https://github.com/perfecto25)
|
||||
- [Trigger Ansible AWX job runs on SCM (e.g. git) commit](http://jpmens.net/2017/10/23/trigger-awx-job-runs-on-scm-commit/) by [@jpmens](http://mens.de/)
|
||||
|
||||
- [Deploy using GitHub webhooks](https://davidauthier.wearemd.com/blog/deploy-using-github-webhooks.html) by [@awea](https://davidauthier.wearemd.com)
|
||||
- [Setting up Automatic Deployment and Builds Using Webhooks](https://willbrowning.me/setting-up-automatic-deployment-and-builds-using-webhooks/) by [Will Browning](https://willbrowning.me/about/)
|
||||
- [Auto deploy your Node.js app on push to GitHub in 3 simple steps](https://webhookrelay.com/blog/2018/07/17/auto-deploy-on-git-push/) by Karolis Rusenas
|
||||
- [Automate Static Site Deployments with Salt, Git, and Webhooks](https://www.linode.com/docs/applications/configuration-management/automate-a-static-site-deployment-with-salt/) by [Linode](https://www.linode.com)
|
||||
- [Using Prometheus to Automatically Scale WebLogic Clusters on Kubernetes](https://blogs.oracle.com/weblogicserver/using-prometheus-to-automatically-scale-weblogic-clusters-on-kubernetes-v5) by [Marina Kogan](https://blogs.oracle.com/author/9a4fe754-1cc2-4c64-95fc-360642b62927)
|
||||
- [Github Pages and Jekyll - A New Platform for LACNIC Labs](https://labs.lacnic.net/a-new-platform-for-lacniclabs/) by [Carlos Martínez Cagnazzo](https://twitter.com/carlosm3011)
|
||||
- [How to Deploy React Apps Using Webhooks and Integrating Slack on Ubuntu](https://www.alibabacloud.com/blog/how-to-deploy-react-apps-using-webhooks-and-integrating-slack-on-ubuntu_594116) by Arslan Ud Din Shafiq
|
||||
- [Private webhooks](https://ihateithe.re/2018/01/private-webhooks/) by [Thomas](https://ihateithe.re/colophon/)
|
||||
- [Adventures in webhooks](https://medium.com/@draketech/adventures-in-webhooks-2d6584501c62) by [Drake](https://medium.com/@draketech)
|
||||
- [GitHub pro tips](http://notes.spencerlyon.com/2016/01/04/github-pro-tips/) by [Spencer Lyon](http://notes.spencerlyon.com/)
|
||||
- [XiaoMi Vacuum + Amazon Button = Dash Cleaning](https://www.instructables.com/id/XiaoMi-Vacuum-Amazon-Button-Dash-Cleaning/) by [c0mmensal](https://www.instructables.com/member/c0mmensal/)
|
||||
- [Set up Automated Deployments From Github With Webhook](https://maximorlov.com/automated-deployments-from-github-with-webhook/) by [Maxim Orlov](https://twitter.com/_maximization)
|
||||
- VIDEO: [Gitlab CI/CD configuration using Docker and adnanh/webhook to deploy on VPS - Tutorial #1](https://www.youtube.com/watch?v=Qhn-lXjyrZA&feature=youtu.be) by [Yes! Let's Learn Software Engineering](https://www.youtube.com/channel/UCH4XJf2BZ_52fbf8fOBMF3w)
|
||||
- ...
|
||||
- Want to add your own? Open an Issue or create a PR :-)
|
||||
|
||||
## Community Contributions
|
||||
See the [webhook-contrib][wc] repository for a collections of tools and helpers related to [webhook][w] that have been contributed by the [webhook][w] community.
|
||||
|
||||
@@ -102,7 +150,7 @@ Check out [existing issues](https://github.com/adnanh/webhook/issues) to see if
|
||||
# Support active development
|
||||
|
||||
## Sponsors
|
||||
## <a href="https://www.digitalocean.com/?ref=webhook"><img src="https://www.digitalocean.com/assets/media/logos-badges/png/DO_Logo_Horizontal_Blue-3db19536.png" alt="DigitalOcean" width="250"/></a>
|
||||
## <a href="https://www.digitalocean.com/?ref=webhook"><img src="http://www.hajdarevic.net/DO_Logo_Horizontal_Blue.png" alt="DigitalOcean" width="250"/></a>
|
||||
[DigitalOcean](https://www.digitalocean.com/?ref=webhook) is a simple and robust cloud computing platform, designed for developers.
|
||||
|
||||
|
||||
@@ -171,3 +219,4 @@ THE SOFTWARE.
|
||||
|
||||
[w]: https://github.com/adnanh/webhook
|
||||
[wc]: https://github.com/adnanh/webhook-contrib
|
||||
[badge]: https://github.com/adnanh/webhook/workflows/build/badge.svg
|
||||
|
||||
Binary file not shown.
@@ -1,5 +1,6 @@
|
||||
# Hook definition
|
||||
Hooks are defined as JSON objects. Please note that in order to be considered valid, a hook object must contain the `id` and `execute-command` properties. All other properties are considered optional.
|
||||
|
||||
Hooks are defined as objects in the JSON or YAML hooks configuration file. Please note that in order to be considered valid, a hook object must contain the `id` and `execute-command` properties. All other properties are considered optional.
|
||||
|
||||
## Properties (keys)
|
||||
|
||||
@@ -8,16 +9,20 @@ Hooks are defined as JSON objects. Please note that in order to be considered va
|
||||
* `command-working-directory` - specifies the working directory that will be used for the script when it's executed
|
||||
* `response-message` - specifies the string that will be returned to the hook initiator
|
||||
* `response-headers` - specifies the list of headers in format `{"name": "X-Example-Header", "value": "it works"}` that will be returned in HTTP response for the hook
|
||||
* `success-http-response-code` - specifies the HTTP status code to be returned upon success
|
||||
* `incoming-payload-content-type` - sets the `Content-Type` of the incoming HTTP request (ie. `application/json`); useful when the request lacks a `Content-Type` or sends an erroneous value
|
||||
* `http-methods` - a list of allowed HTTP methods, such as `POST` and `GET`
|
||||
* `include-command-output-in-response` - boolean whether webhook should wait for the command to finish and return the raw output as a response to the hook initiator. If the command fails to execute or encounters any errors while executing the response will result in 500 Internal Server Error HTTP status code, otherwise the 200 OK status code will be returned.
|
||||
* `include-command-output-in-response-on-error` - boolean whether webhook should include command stdout & stderror as a response in failed executions. It only works if `include-command-output-in-response` is set to `true`.
|
||||
* `parse-parameters-as-json` - specifies the list of arguments that contain JSON strings. These parameters will be decoded by webhook and you can access them like regular objects in rules and `pass-arguments-to-command`.
|
||||
* `pass-arguments-to-command` - specifies the list of arguments that will be passed to the command. Check [Referencing request values page](Referencing-Request-Values) to see how to reference the values from the request. If you want to pass a static string value to your command you can specify it as
|
||||
* `pass-arguments-to-command` - specifies the list of arguments that will be passed to the command. Check [Referencing request values page](Referencing-Request-Values.md) to see how to reference the values from the request. If you want to pass a static string value to your command you can specify it as
|
||||
`{ "source": "string", "name": "argumentvalue" }`
|
||||
* `pass-environment-to-command` - specifies the list of arguments that will be passed to the command as environment variables. If you do not specify the `"envname"` field in the referenced value, the hook will be in format "HOOK_argumentname", otherwise "envname" field will be used as it's name. Check [Referencing request values page](Referencing-Request-Values) to see how to reference the values from the request. If you want to pass a static string value to your command you can specify it as
|
||||
* `pass-environment-to-command` - specifies the list of arguments that will be passed to the command as environment variables. If you do not specify the `"envname"` field in the referenced value, the hook will be in format "HOOK_argumentname", otherwise "envname" field will be used as it's name. Check [Referencing request values page](Referencing-Request-Values.md) to see how to reference the values from the request. If you want to pass a static string value to your command you can specify it as
|
||||
`{ "source": "string", "envname": "SOMETHING", "name": "argumentvalue" }`
|
||||
* `pass-file-to-command` - specifies a list of entries that will be serialized as a file. Incoming [data](Referencing-Request-Values.md) will be serialized in a request-temporary-file (otherwise parallel calls of the hook would lead to concurrent overwritings of the file). The filename to be addressed within the subsequent script is provided via an environment variable. Use `envname` to specify the name of the environment variable. If `envname` is not provided `HOOK_` and the name used to reference the request value are used. Defining `command-working-directory` will store the file relative to this location, if not provided, the systems temporary file directory will be used. If `base64decode` is true, the incoming binary data will be base 64 decoded prior to storing it into the file. By default the corresponding file will be removed after the webhook exited.
|
||||
* `trigger-rule` - specifies the rule that will be evaluated in order to determine should the hook be triggered. Check [Hook rules page](Hook-Rules) to see the list of valid rules and their usage
|
||||
* `trigger-rule` - specifies the rule that will be evaluated in order to determine should the hook be triggered. Check [Hook rules page](Hook-Rules.md) to see the list of valid rules and their usage
|
||||
* `trigger-rule-mismatch-http-response-code` - specifies the HTTP status code to be returned when the trigger rule is not satisfied
|
||||
* `trigger-signature-soft-failures` - allow signature validation failures within Or rules; by default, signature failures are treated as errors.
|
||||
|
||||
## Examples
|
||||
Check out [Hook examples page](Hook-Examples.md) for more complex examples of hooks.
|
||||
|
||||
+623
-283
@@ -1,283 +1,623 @@
|
||||
# Hook examples
|
||||
This page is still work in progress. Feel free to contribute!
|
||||
|
||||
## Incoming Github webhook
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Incoming Bitbucket webhook
|
||||
|
||||
Bitbucket does not pass any secrets back to the webhook. [Per their documentation](https://confluence.atlassian.com/bitbucket/manage-webhooks-735643732.html#Managewebhooks-trigger_webhookTriggeringwebhooks), in order to verify that the webhook came from Bitbucket you must whitelist the IP range `104.192.143.0/24`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "actor.username"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "ip-whitelist",
|
||||
"ip-range": "104.192.143.0/24"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Incoming Gitlab Webhook
|
||||
Gitlab provides webhooks for many kinds of events.
|
||||
Refer to this URL for example request body content: [gitlab-ce/integrations/webhooks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/project/integrations/webhooks.md)
|
||||
Values in the request body can be accessed in the command or to the match rule by referencing 'payload' as the source:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "redeploy-webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "user_name"
|
||||
}
|
||||
],
|
||||
"response-message": "Executing redeploy script",
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "<YOUR-GENERATED-TOKEN>",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Gitlab-Token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Incoming Gogs webhook
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha256",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Gogs-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Slack slash command
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "redeploy-webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"response-message": "Executing redeploy script",
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "<YOUR-GENERATED-TOKEN>",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## A simple webhook with a secret key in GET query
|
||||
|
||||
__Not recommended in production due to low security__
|
||||
|
||||
`example.com:9000/hooks/simple-one` - won't work
|
||||
`example.com:9000/hooks/simple-one?token=42` - will work
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "simple-one",
|
||||
"execute-command": "/path/to/command.sh",
|
||||
"response-message": "Executing simple webhook...",
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "42",
|
||||
"parameter":
|
||||
{
|
||||
"source": "url",
|
||||
"name": "token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
# JIRA Webhooks
|
||||
[Guide by @perfecto25](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks)
|
||||
|
||||
# Pass File-to-command sample
|
||||
|
||||
## Webhook configuration
|
||||
|
||||
<pre>
|
||||
[
|
||||
{
|
||||
"id": "test-file-webhook",
|
||||
"execute-command": "/bin/ls",
|
||||
"command-working-directory": "/tmp",
|
||||
"pass-file-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "binary",
|
||||
"envname": "ENV_VARIABLE", // to use $ENV_VARIABLE in execute-command
|
||||
// if not defined, $HOOK_BINARY will be provided
|
||||
"base64decode": true, // defaults to false
|
||||
}
|
||||
],
|
||||
"include-command-output-in-response": true
|
||||
}
|
||||
]
|
||||
</pre>
|
||||
|
||||
## Sample client usage
|
||||
|
||||
Store the following file as `testRequest.json`.
|
||||
|
||||
<pre>
|
||||
{"binary":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2lpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wUmlnaHRzPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvcmlnaHRzLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcFJpZ2h0czpNYXJrZWQ9IkZhbHNlIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjEzMTA4RDI0QzMxQjExRTBCMzYzRjY1QUQ1Njc4QzFBIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjEzMTA4RDIzQzMxQjExRTBCMzYzRjY1QUQ1Njc4QzFBIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzMgV2luZG93cyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ1dWlkOkFDMUYyRTgzMzI0QURGMTFBQUI4QzUzOTBEODVCNUIzIiBzdFJlZjpkb2N1bWVudElEPSJ1dWlkOkM5RDM0OTY2NEEzQ0REMTFCMDhBQkJCQ0ZGMTcyMTU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+IBFgEwAAAmJJREFUeNqkk89rE1EQx2d/NNq0xcYYayPYJDWC9ODBsKIgAREjBmvEg2cvHnr05KHQ9iB49SL+/BMEfxBQKHgwCEbTNNIYaqgaoanFJi+rcXezye4689jYkIMIDnx47837zrx583YFx3Hgf0xA6/dJyAkkgUy4vgryAnmNWH9L4EVmotFoKplMHgoGg6PkrFarjXQ6/bFcLj/G5W1E+3NaX4KZeDx+dX5+7kg4HBlmrC6JoiDFYrGhROLM/mp1Y6JSqdCd3/SW0GUqEAjkl5ZyHTSHKBQKnO6a9khD2m5cr91IJBJ1VVWdiM/n6LruNJtNDs3JR3ukIW03SHTHi8iVsbG9I51OG1bW16HVasHQZopDc/JZVgdIQ1o3BmTkEnJXURS/KIpgGAYPkCQJPi0u8uzDKQN0XQPbtgE1MmrHs9nsfSqAEjxCNtHxZHLy4G4smUQgyzL4LzOegDGGp1ucVqsNqKVrpJCM7F4hg6iaZvhqtZrg8XjA4xnAU3XeKLqWaRImoIZeQXVjQO5pYp4xNVirsR1erxer2O4yfa227WCwhtWoJmn7m0h270NxmemFW4706zMm8GCgxBGEASCfhnukIW03iFdQnOPz0LNKp3362JqQzSw4u2LXBe+Bs3xD+/oc1NxN55RiC9fOme0LEQiRf2rBzaKEeJJ37ZWTVunBeGN2WmQjg/DeLTVP89nzAive2dMwlo9bpFVC2xWMZr+A720FVn88fAUb3wDMOjyN7YNc6TvUSHQ4AH6TOUdLL7em68UtWPsJqxgTpgeiLu1EBt1R+Me/mF7CQPTfAgwAGxY2vOTrR3oAAAAASUVORK5CYII="}
|
||||
</pre>
|
||||
|
||||
use then the curl tool to execute a request to the webhook.
|
||||
|
||||
<pre>
|
||||
#!/bin/bash
|
||||
curl -H "Content-Type:application/json" -X POST -d @testRequest.json \
|
||||
http://localhost:9000/hooks/test-file-webhook
|
||||
</pre>
|
||||
|
||||
or in a single line, using https://github.com/jpmens/jo to generate the JSON code
|
||||
<pre>
|
||||
jo binary=%filename.zip | curl -H "Content-Type:application/json" -X POST -d @- \
|
||||
http://localhost:9000/hooks/test-file-webhook
|
||||
</pre>
|
||||
# Hook Examples
|
||||
|
||||
Hooks are defined in a hooks configuration file in either JSON or YAML format,
|
||||
although the examples on this page all use the JSON format.
|
||||
|
||||
🌱 This page is still a work in progress. Feel free to contribute!
|
||||
|
||||
### Table of Contents
|
||||
|
||||
* [Incoming Github webhook](#incoming-github-webhook)
|
||||
* [Incoming Bitbucket webhook](#incoming-bitbucket-webhook)
|
||||
* [Incoming Gitlab webhook](#incoming-gitlab-webhook)
|
||||
* [Incoming Gogs webhook](#incoming-gogs-webhook)
|
||||
* [Incoming Gitea webhook](#incoming-gitea-webhook)
|
||||
* [Slack slash command](#slack-slash-command)
|
||||
* [A simple webhook with a secret key in GET query](#a-simple-webhook-with-a-secret-key-in-get-query)
|
||||
* [JIRA Webhooks](#jira-webhooks)
|
||||
* [Pass File-to-command sample](#pass-file-to-command-sample)
|
||||
* [Incoming Scalr Webhook](#incoming-scalr-webhook)
|
||||
* [Travis CI webhook](#travis-ci-webhook)
|
||||
* [XML Payload](#xml-payload)
|
||||
* [Multipart Form Data](#multipart-form-data)
|
||||
* [Pass string arguments to command](#pass-string-arguments-to-command)
|
||||
|
||||
## Incoming Github webhook
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Incoming Bitbucket webhook
|
||||
|
||||
Bitbucket does not pass any secrets back to the webhook. [Per their documentation](https://confluence.atlassian.com/bitbucket/manage-webhooks-735643732.html#Managewebhooks-trigger_webhookTriggeringwebhooks), in order to verify that the webhook came from Bitbucket you must whitelist the IP range `104.192.143.0/24`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "actor.username"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "ip-whitelist",
|
||||
"ip-range": "104.192.143.0/24"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Incoming Gitlab Webhook
|
||||
Gitlab provides webhooks for many kinds of events.
|
||||
Refer to this URL for example request body content: [gitlab-ce/integrations/webhooks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/project/integrations/webhooks.md)
|
||||
Values in the request body can be accessed in the command or to the match rule by referencing 'payload' as the source:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "redeploy-webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "user_name"
|
||||
}
|
||||
],
|
||||
"response-message": "Executing redeploy script",
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "<YOUR-GENERATED-TOKEN>",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Gitlab-Token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Incoming Gogs webhook
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha256",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Gogs-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
## Incoming Gitea webhook
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "secret"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Slack slash command
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "redeploy-webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"response-message": "Executing redeploy script",
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "<YOUR-GENERATED-TOKEN>",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## A simple webhook with a secret key in GET query
|
||||
|
||||
__Not recommended in production due to low security__
|
||||
|
||||
`example.com:9000/hooks/simple-one` - won't work
|
||||
`example.com:9000/hooks/simple-one?token=42` - will work
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "simple-one",
|
||||
"execute-command": "/path/to/command.sh",
|
||||
"response-message": "Executing simple webhook...",
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "42",
|
||||
"parameter":
|
||||
{
|
||||
"source": "url",
|
||||
"name": "token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## JIRA Webhooks
|
||||
[Guide by @perfecto25](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks)
|
||||
|
||||
## Pass File-to-command sample
|
||||
|
||||
### Webhook configuration
|
||||
|
||||
<pre>
|
||||
[
|
||||
{
|
||||
"id": "test-file-webhook",
|
||||
"execute-command": "/bin/ls",
|
||||
"command-working-directory": "/tmp",
|
||||
"pass-file-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "binary",
|
||||
"envname": "ENV_VARIABLE", // to use $ENV_VARIABLE in execute-command
|
||||
// if not defined, $HOOK_BINARY will be provided
|
||||
"base64decode": true, // defaults to false
|
||||
}
|
||||
],
|
||||
"include-command-output-in-response": true
|
||||
}
|
||||
]
|
||||
</pre>
|
||||
|
||||
### Sample client usage
|
||||
|
||||
Store the following file as `testRequest.json`.
|
||||
|
||||
<pre>
|
||||
{"binary":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2lpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wUmlnaHRzPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvcmlnaHRzLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcFJpZ2h0czpNYXJrZWQ9IkZhbHNlIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjEzMTA4RDI0QzMxQjExRTBCMzYzRjY1QUQ1Njc4QzFBIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjEzMTA4RDIzQzMxQjExRTBCMzYzRjY1QUQ1Njc4QzFBIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzMgV2luZG93cyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ1dWlkOkFDMUYyRTgzMzI0QURGMTFBQUI4QzUzOTBEODVCNUIzIiBzdFJlZjpkb2N1bWVudElEPSJ1dWlkOkM5RDM0OTY2NEEzQ0REMTFCMDhBQkJCQ0ZGMTcyMTU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+IBFgEwAAAmJJREFUeNqkk89rE1EQx2d/NNq0xcYYayPYJDWC9ODBsKIgAREjBmvEg2cvHnr05KHQ9iB49SL+/BMEfxBQKHgwCEbTNNIYaqgaoanFJi+rcXezye4689jYkIMIDnx47837zrx583YFx3Hgf0xA6/dJyAkkgUy4vgryAnmNWH9L4EVmotFoKplMHgoGg6PkrFarjXQ6/bFcLj/G5W1E+3NaX4KZeDx+dX5+7kg4HBlmrC6JoiDFYrGhROLM/mp1Y6JSqdCd3/SW0GUqEAjkl5ZyHTSHKBQKnO6a9khD2m5cr91IJBJ1VVWdiM/n6LruNJtNDs3JR3ukIW03SHTHi8iVsbG9I51OG1bW16HVasHQZopDc/JZVgdIQ1o3BmTkEnJXURS/KIpgGAYPkCQJPi0u8uzDKQN0XQPbtgE1MmrHs9nsfSqAEjxCNtHxZHLy4G4smUQgyzL4LzOegDGGp1ucVqsNqKVrpJCM7F4hg6iaZvhqtZrg8XjA4xnAU3XeKLqWaRImoIZeQXVjQO5pYp4xNVirsR1erxer2O4yfa227WCwhtWoJmn7m0h270NxmemFW4706zMm8GCgxBGEASCfhnukIW03iFdQnOPz0LNKp3362JqQzSw4u2LXBe+Bs3xD+/oc1NxN55RiC9fOme0LEQiRf2rBzaKEeJJ37ZWTVunBeGN2WmQjg/DeLTVP89nzAive2dMwlo9bpFVC2xWMZr+A720FVn88fAUb3wDMOjyN7YNc6TvUSHQ4AH6TOUdLL7em68UtWPsJqxgTpgeiLu1EBt1R+Me/mF7CQPTfAgwAGxY2vOTrR3oAAAAASUVORK5CYII="}
|
||||
</pre>
|
||||
|
||||
use then the curl tool to execute a request to the webhook.
|
||||
|
||||
<pre>
|
||||
#!/bin/bash
|
||||
curl -H "Content-Type:application/json" -X POST -d @testRequest.json \
|
||||
http://localhost:9000/hooks/test-file-webhook
|
||||
</pre>
|
||||
|
||||
or in a single line, using https://github.com/jpmens/jo to generate the JSON code
|
||||
<pre>
|
||||
jo binary=%filename.zip | curl -H "Content-Type:application/json" -X POST -d @- \
|
||||
http://localhost:9000/hooks/test-file-webhook
|
||||
</pre>
|
||||
|
||||
|
||||
## Incoming Scalr Webhook
|
||||
[Guide by @hassanbabaie]
|
||||
Scalr makes webhook calls based on an event to a configured webhook endpoint (for example Host Down, Host Up). Webhook endpoints are URLs where Scalr will deliver Webhook notifications.
|
||||
Scalr assigns a unique signing key for every configured webhook endpoint.
|
||||
Refer to this URL for information on how to setup the webhook call on the Scalr side: [Scalr Wiki Webhooks](https://scalr-wiki.atlassian.net/wiki/spaces/docs/pages/6193173/Webhooks)
|
||||
In order to leverage the Signing Key for addtional authentication/security you must configure the trigger rule with a match type of "scalr-signature".
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "redeploy-webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "scalr-signature",
|
||||
"secret": "Scalr-provided signing key"
|
||||
}
|
||||
},
|
||||
"pass-environment-to-command":
|
||||
[
|
||||
{
|
||||
"envname": "EVENT_NAME",
|
||||
"source": "payload",
|
||||
"name": "eventName"
|
||||
},
|
||||
{
|
||||
"envname": "SERVER_HOSTNAME",
|
||||
"source": "payload",
|
||||
"name": "data.SCALR_SERVER_HOSTNAME"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
## Travis CI webhook
|
||||
Travis sends webhooks as `payload=<JSON_STRING>`, so the payload needs to be parsed as JSON. Here is an example to run on successful builds of the master branch.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "deploy",
|
||||
"execute-command": "/root/my-server/deployment.sh",
|
||||
"command-working-directory": "/root/my-server",
|
||||
"parse-parameters-as-json": [
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "payload"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "passed",
|
||||
"parameter": {
|
||||
"name": "payload.state",
|
||||
"source": "payload"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "master",
|
||||
"parameter": {
|
||||
"name": "payload.branch",
|
||||
"source": "payload"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## JSON Array Payload
|
||||
|
||||
If the JSON payload is an array instead of an object, `webhook` will process the payload and place it into a "root" object.
|
||||
Therefore, references to payload values must begin with `root.`.
|
||||
|
||||
For example, given the following payload (taken from the Sendgrid Event Webhook documentation):
|
||||
```json
|
||||
[
|
||||
{
|
||||
"email": "example@test.com",
|
||||
"timestamp": 1513299569,
|
||||
"smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
|
||||
"event": "processed",
|
||||
"category": "cat facts",
|
||||
"sg_event_id": "sg_event_id",
|
||||
"sg_message_id": "sg_message_id"
|
||||
},
|
||||
{
|
||||
"email": "example@test.com",
|
||||
"timestamp": 1513299569,
|
||||
"smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
|
||||
"event": "deferred",
|
||||
"category": "cat facts",
|
||||
"sg_event_id": "sg_event_id",
|
||||
"sg_message_id": "sg_message_id",
|
||||
"response": "400 try again later",
|
||||
"attempt": "5"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
A reference to the second item in the array would look like this:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "sendgrid",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"trigger-rule": {
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "root.1.event"
|
||||
},
|
||||
"value": "deferred"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## XML Payload
|
||||
|
||||
Given the following payload:
|
||||
|
||||
```xml
|
||||
<app>
|
||||
<users>
|
||||
<user id="1" name="Jeff" />
|
||||
<user id="2" name="Sally" />
|
||||
</users>
|
||||
<messages>
|
||||
<message id="1" from_user="1" to_user="2">Hello!!</message>
|
||||
</messages>
|
||||
</app>
|
||||
```
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "deploy",
|
||||
"execute-command": "/root/my-server/deployment.sh",
|
||||
"command-working-directory": "/root/my-server",
|
||||
"trigger-rule": {
|
||||
"and": [
|
||||
{
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "app.users.user.0.-name"
|
||||
},
|
||||
"value": "Jeff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "app.messages.message.#text"
|
||||
},
|
||||
"value": "Hello!!"
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Multipart Form Data
|
||||
|
||||
Example of a [Plex Media Server webhook](https://support.plex.tv/articles/115002267687-webhooks/).
|
||||
The Plex Media Server will send two parts: payload and thumb.
|
||||
We only care about the payload part.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "plex",
|
||||
"execute-command": "play-command.sh",
|
||||
"parse-parameters-as-json": [
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "payload"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "payload.event"
|
||||
},
|
||||
"value": "media.play"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
Each part of a multipart form data body will have a `Content-Disposition` header.
|
||||
Some example headers:
|
||||
|
||||
```
|
||||
Content-Disposition: form-data; name="payload"
|
||||
Content-Disposition: form-data; name="thumb"; filename="thumb.jpg"
|
||||
```
|
||||
|
||||
We key off of the `name` attribute in the `Content-Disposition` value.
|
||||
|
||||
## Pass string arguments to command
|
||||
|
||||
To pass simple string arguments to a command, use the `string` parameter source.
|
||||
The following example will pass two static string parameters ("-e 123123") to the
|
||||
`execute-command` before appending the `pusher.email` value from the payload:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "string",
|
||||
"name": "-e"
|
||||
},
|
||||
{
|
||||
"source": "string",
|
||||
"name": "123123"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
+94
-9
@@ -1,5 +1,20 @@
|
||||
# Hook rules
|
||||
|
||||
### Table of Contents
|
||||
|
||||
* [And](#and)
|
||||
* [Or](#or)
|
||||
* [Not](#not)
|
||||
* [Multi-level](#multi-level)
|
||||
* [Match](#match)
|
||||
* [Match value](#match-value)
|
||||
* [Match regex](#match-regex)
|
||||
* [Match payload-hmac-sha1](#match-payload-hmac-sha1)
|
||||
* [Match payload-hmac-sha256](#match-payload-hmac-sha256)
|
||||
* [Match payload-hmac-sha512](#match-payload-hmac-sha512)
|
||||
* [Match Whitelisted IP range](#match-whitelisted-ip-range)
|
||||
* [Match scalr-signature](#match-scalr-signature)
|
||||
|
||||
## And
|
||||
*And rule* will evaluate to _true_, if and only if all of the sub rules evaluate to _true_.
|
||||
```json
|
||||
@@ -95,7 +110,7 @@
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
},
|
||||
"type": "payload-hash-sha1",
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret"
|
||||
}
|
||||
},
|
||||
@@ -135,9 +150,7 @@
|
||||
|
||||
*Please note:* Due to technical reasons, _number_ and _boolean_ values in the _match rule_ must be wrapped around with a pair of quotes.
|
||||
|
||||
There are three different match rules:
|
||||
|
||||
### 1. Match value
|
||||
### Match value
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
@@ -153,7 +166,7 @@ There are three different match rules:
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Match regex
|
||||
### Match regex
|
||||
For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
|
||||
```json
|
||||
{
|
||||
@@ -170,12 +183,13 @@ For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Match payload-hash-sha1
|
||||
### Match payload-hmac-sha1
|
||||
Validate the HMAC of the payload using the SHA1 hash and the given *secret*.
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "yoursecret",
|
||||
"parameter":
|
||||
{
|
||||
@@ -186,7 +200,62 @@ For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Match Whitelisted IP range
|
||||
Note that if multiple signatures were passed via a comma separated string, each
|
||||
will be tried unless a match is found. For example:
|
||||
|
||||
```
|
||||
X-Hub-Signature: sha1=the-first-signature,sha1=the-second-signature
|
||||
```
|
||||
|
||||
### Match payload-hmac-sha256
|
||||
Validate the HMAC of the payload using the SHA256 hash and the given *secret*.
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha256",
|
||||
"secret": "yoursecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that if multiple signatures were passed via a comma separated string, each
|
||||
will be tried unless a match is found. For example:
|
||||
|
||||
```
|
||||
X-Hub-Signature: sha256=the-first-signature,sha256=the-second-signature
|
||||
```
|
||||
|
||||
### Match payload-hmac-sha512
|
||||
Validate the HMAC of the payload using the SHA512 hash and the given *secret*.
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha512",
|
||||
"secret": "yoursecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that if multiple signatures were passed via a comma separated string, each
|
||||
will be tried unless a match is found. For example:
|
||||
|
||||
```
|
||||
X-Hub-Signature: sha512=the-first-signature,sha512=the-second-signature
|
||||
```
|
||||
|
||||
### Match Whitelisted IP range
|
||||
|
||||
The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_blocks). To match a single IP address only, use `/32`.
|
||||
|
||||
@@ -198,4 +267,20 @@ The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedi
|
||||
"ip-range": "192.168.0.1/24"
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
### Match scalr-signature
|
||||
|
||||
The trigger rule checks the scalr signature and also checks that the request was signed less than 5 minutes before it was received.
|
||||
A unqiue signing key is generated for each webhook endpoint URL you register in Scalr.
|
||||
Given the time check make sure that NTP is enabled on both your Scalr and webhook server to prevent any issues
|
||||
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "scalr-signature",
|
||||
"secret": "Scalr-provided signing key"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Referencing request values
|
||||
There are three types of request values:
|
||||
There are four types of request values:
|
||||
|
||||
1. HTTP Request Header values
|
||||
|
||||
@@ -19,7 +19,23 @@ There are three types of request values:
|
||||
}
|
||||
```
|
||||
|
||||
3. Payload (JSON or form-value encoded)
|
||||
3. HTTP Request parameters
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "request",
|
||||
"name": "method"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "request",
|
||||
"name": "remote-addr"
|
||||
}
|
||||
```
|
||||
|
||||
4. Payload (JSON or form-value encoded)
|
||||
```json
|
||||
{
|
||||
"source": "payload",
|
||||
@@ -57,6 +73,34 @@ There are three types of request values:
|
||||
|
||||
If the payload contains a key with the specified name "commits.0.commit.id", then the value of that key has priority over the dot-notation referencing.
|
||||
|
||||
4. XML Payload
|
||||
|
||||
Referencing XML payload parameters is much like the JSON examples above, but XML is more complex.
|
||||
Element attributes are prefixed by a hyphen (`-`).
|
||||
Element values are prefixed by a pound (`#`).
|
||||
|
||||
Take the following XML payload:
|
||||
|
||||
```xml
|
||||
<app>
|
||||
<users>
|
||||
<user id="1" name="Jeff" />
|
||||
<user id="2" name="Sally" />
|
||||
</users>
|
||||
<messages>
|
||||
<message id="1" from_user="1" to_user="2">Hello!!</message>
|
||||
</messages>
|
||||
</app>
|
||||
```
|
||||
|
||||
To access a given `user` element, you must treat them as an array.
|
||||
So `app.users.user.0.name` yields `Jeff`.
|
||||
|
||||
Since there's only one `message` tag, it's not treated as an array.
|
||||
So `app.messages.message.id` yields `1`.
|
||||
|
||||
To access the text within the `message` tag, you would use: `app.messages.message.#text`.
|
||||
|
||||
If you are referencing values for environment, you can use `envname` property to set the name of the environment variable like so
|
||||
```json
|
||||
{
|
||||
@@ -87,4 +131,4 @@ and for query variables you can use
|
||||
{
|
||||
"source": "entire-query"
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
# Templates in Webhook
|
||||
|
||||
[`webhook`][w] can parse the `hooks.json` input file as a Go template when given the `-template` [CLI parameter](Webhook-Parameters.md).
|
||||
[`webhook`][w] can parse a hooks configuration file as a Go template when given the `-template` [CLI parameter](Webhook-Parameters.md).
|
||||
|
||||
In additional to the [built-in Go template functions and features][tt], `webhook` provides a `getenv` template function for inserting environment variables into a `hooks.json` file.
|
||||
In additional to the [built-in Go template functions and features][tt], `webhook` provides a `getenv` template function for inserting environment variables into a templated configuration file.
|
||||
|
||||
## Example Usage
|
||||
|
||||
In the example `hooks.json` file below, the `payload-hash-sha1` matching rule looks up the secret hash from the environment using the `getenv` template function.
|
||||
In the example JSON template file below (YAML is also supported), the `payload-hmac-sha1` matching rule looks up the HMAC secret from the environment using the `getenv` template function.
|
||||
Additionally, the result is piped through the built-in Go template function `js` to ensure that the result is a well-formed Javascript/JSON string.
|
||||
|
||||
```
|
||||
@@ -44,7 +44,7 @@ Additionally, the result is piped through the built-in Go template function `js`
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "{{ getenv "XXXTEST_SECRET" | js }}",
|
||||
"parameter":
|
||||
{
|
||||
|
||||
@@ -3,36 +3,60 @@
|
||||
Usage of webhook:
|
||||
-cert string
|
||||
path to the HTTPS certificate pem file (default "cert.pem")
|
||||
-cipher-suites string
|
||||
comma-separated list of supported TLS cipher suites
|
||||
-debug
|
||||
show debug output
|
||||
-header value
|
||||
response header to return, specified in format name=value, use multiple times to set multiple headers
|
||||
-hooks value
|
||||
path to the json file containing defined hooks the webhook should serve, use multiple times to load from different files
|
||||
-hotreload
|
||||
watch hooks file for changes and reload them automatically
|
||||
-http-methods string
|
||||
globally restrict allowed HTTP methods; separate methods with comma
|
||||
-ip string
|
||||
ip the webhook should serve hooks on (default "0.0.0.0")
|
||||
-key string
|
||||
path to the HTTPS certificate private key pem file (default "key.pem")
|
||||
-list-cipher-suites
|
||||
list available TLS cipher suites
|
||||
-logfile string
|
||||
send log output to a file; implicitly enables verbose logging
|
||||
-nopanic
|
||||
do not panic if hooks cannot be loaded when webhook is not running in verbose mode
|
||||
-pidfile string
|
||||
create PID file at the given path
|
||||
-port int
|
||||
port the webhook should serve hooks on (default 9000)
|
||||
-secure
|
||||
use HTTPS instead of HTTP
|
||||
-setgid int
|
||||
set group ID after opening listening port; must be used with setuid
|
||||
-setuid int
|
||||
set user ID after opening listening port; must be used with setgid
|
||||
-template
|
||||
parse hooks file as a Go template
|
||||
-tls-min-version string
|
||||
minimum TLS version (1.0, 1.1, 1.2, 1.3) (default "1.2")
|
||||
-urlprefix string
|
||||
url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id) (default "hooks")
|
||||
-verbose
|
||||
show verbose output
|
||||
-version
|
||||
display webhook version and quit
|
||||
-x-request-id
|
||||
use X-Request-Id header, if present, as request ID
|
||||
-x-request-id-limit int
|
||||
truncate X-Request-Id header to limit; default no limit
|
||||
```
|
||||
|
||||
Use any of the above specified flags to override their default behavior.
|
||||
|
||||
# Live reloading hooks
|
||||
If you are running an OS that supports USR1 signal, you can use it to trigger hooks reload from hooks file, without restarting the webhook instance.
|
||||
If you are running an OS that supports the HUP or USR1 signal, you can use it to trigger hooks reload from hooks file, without restarting the webhook instance.
|
||||
```bash
|
||||
kill -USR1 webhookpid
|
||||
```
|
||||
|
||||
kill -HUP webhookpid
|
||||
```
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// +build linux windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func dropPrivileges(uid, gid int) error {
|
||||
return errors.New("setuid and setgid not supported on " + runtime.GOOS)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// +build !windows,!linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func dropPrivileges(uid, gid int) error {
|
||||
err := syscall.Setgid(gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = syscall.Setuid(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
module github.com/adnanh/webhook
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/antonmedv/expr v1.8.9
|
||||
github.com/clbanning/mxj v1.8.4
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fsnotify/fsnotify v1.4.7 // indirect
|
||||
github.com/ghodss/yaml v1.0.0
|
||||
github.com/go-chi/chi v4.0.2+incompatible
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/google/cel-go v0.7.0 // indirect
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/fsnotify.v1 v1.4.2
|
||||
)
|
||||
@@ -0,0 +1,142 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f h1:0cEys61Sr2hUBEXfNV8eyQP01oZuBgoMeHunebPirK8=
|
||||
github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
|
||||
github.com/antonmedv/expr v1.8.9 h1:O9stiHmHHww9b4ozhPx7T6BK7fXfOCHJ8ybxf0833zw=
|
||||
github.com/antonmedv/expr v1.8.9/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
|
||||
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
|
||||
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/cel-go v0.7.0 h1:J0J8RSCJW+SdB53YPwPSm2m1Kfz1tqGXdwMuIAVRU9o=
|
||||
github.com/google/cel-go v0.7.0/go.mod h1:4EtyFAHT5xNr0Msu0MJjyGxPUgdr9DlcaPyzLt/kkt8=
|
||||
github.com/google/cel-spec v0.5.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
|
||||
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0 h1:d0rYPqjQfVuFe+tZgv4PHt2hNxK79MRXX7PaD/A5ynA=
|
||||
google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.2 h1:AwZiD/bIUttYJ+n/k1UwlSUsM+VSE6id7UAnSKqQ+Tc=
|
||||
gopkg.in/fsnotify.v1 v1.4.2/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7 h1:+t9dhfO+GNOIGJof6kPOAenx7YgrZMTdRPV+EsnPabk=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1,491 +0,0 @@
|
||||
package hook
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var checkPayloadSignatureTests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignatureTests {
|
||||
mac, err := CheckPayloadSignature(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkPayloadSignature256Tests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature256(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignature256Tests {
|
||||
mac, err := CheckPayloadSignature256(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var extractParameterTests = []struct {
|
||||
s string
|
||||
params interface{}
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"a", map[string]interface{}{"a": "z"}, "z", true},
|
||||
{"a.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "z", true},
|
||||
{"a.b.c", map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}, "z", true},
|
||||
{"a.b.0", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "x", true},
|
||||
{"a.1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "z", true},
|
||||
{"a.1.b.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": map[string]interface{}{"c": "y"}}, map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}}, "z", true},
|
||||
// failures
|
||||
{"check_nil", nil, "", false},
|
||||
{"a.X", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent parameter reference
|
||||
{"a.X.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-integer slice index
|
||||
{"a.-1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // negative slice index
|
||||
{"a.500.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent slice
|
||||
{"a.501.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-existent slice index
|
||||
{"a.502.b", map[string]interface{}{"a": []interface{}{}}, "", false}, // non-existent slice index
|
||||
{"a.b.503", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "", false}, // trailing, non-existent slice index
|
||||
{"a.b", interface{}("a"), "", false}, // non-map, non-slice input
|
||||
}
|
||||
|
||||
func TestExtractParameter(t *testing.T) {
|
||||
for _, tt := range extractParameterTests {
|
||||
value, ok := ExtractParameterAsString(tt.s, tt.params)
|
||||
if ok != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to extract parameter %q:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.s, tt.value, tt.ok, value, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var argumentGetTests = []struct {
|
||||
source, name string
|
||||
headers, query, payload *map[string]interface{}
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"header", "a", &map[string]interface{}{"A": "z"}, nil, nil, "z", true},
|
||||
{"url", "a", nil, &map[string]interface{}{"a": "z"}, nil, "z", true},
|
||||
{"payload", "a", nil, nil, &map[string]interface{}{"a": "z"}, "z", true},
|
||||
{"string", "a", nil, nil, &map[string]interface{}{"a": "z"}, "a", true},
|
||||
// failures
|
||||
{"header", "a", nil, &map[string]interface{}{"a": "z"}, &map[string]interface{}{"a": "z"}, "", false}, // nil headers
|
||||
{"url", "a", &map[string]interface{}{"A": "z"}, nil, &map[string]interface{}{"a": "z"}, "", false}, // nil query
|
||||
{"payload", "a", &map[string]interface{}{"A": "z"}, &map[string]interface{}{"a": "z"}, nil, "", false}, // nil payload
|
||||
{"foo", "a", &map[string]interface{}{"A": "z"}, nil, nil, "", false}, // invalid source
|
||||
}
|
||||
|
||||
func TestArgumentGet(t *testing.T) {
|
||||
for _, tt := range argumentGetTests {
|
||||
a := Argument{tt.source, tt.name, "", false}
|
||||
value, ok := a.Get(tt.headers, tt.query, tt.payload)
|
||||
if ok != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.source, tt.name, tt.value, tt.ok, value, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hookParseJSONParametersTests = []struct {
|
||||
params []Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
rheaders, rquery, rpayload *map[string]interface{}
|
||||
ok bool
|
||||
}{
|
||||
{[]Argument{Argument{"header", "a", "", false}}, &map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
|
||||
{[]Argument{Argument{"url", "a", "", false}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
|
||||
{[]Argument{Argument{"payload", "a", "", false}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
|
||||
{[]Argument{Argument{"header", "z", "", false}}, &map[string]interface{}{"Z": `{}`}, nil, nil, &map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
|
||||
// failures
|
||||
{[]Argument{Argument{"header", "z", "", false}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
|
||||
{[]Argument{Argument{"header", "y", "", false}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
|
||||
{[]Argument{Argument{"string", "z", "", false}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
|
||||
}
|
||||
|
||||
func TestHookParseJSONParameters(t *testing.T) {
|
||||
for _, tt := range hookParseJSONParametersTests {
|
||||
h := &Hook{JSONStringParameters: tt.params}
|
||||
err := h.ParseJSONParameters(tt.headers, tt.query, tt.payload)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(tt.headers, tt.rheaders) {
|
||||
t.Errorf("failed to parse %v:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.params, *tt.rheaders, tt.ok, *tt.headers, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hookExtractCommandArgumentsTests = []struct {
|
||||
exec string
|
||||
args []Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
{"test", []Argument{Argument{"header", "a", "", false}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
|
||||
// failures
|
||||
{"fail", []Argument{Argument{"payload", "a", "", false}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArguments(t *testing.T) {
|
||||
for _, tt := range hookExtractCommandArgumentsTests {
|
||||
h := &Hook{ExecuteCommand: tt.exec, PassArgumentsToCommand: tt.args}
|
||||
value, err := h.ExtractCommandArguments(tt.headers, tt.query, tt.payload)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
||||
t.Errorf("failed to extract args {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here we test the extraction of env variables when the user defined a hook
|
||||
// with the "pass-environment-to-command" directive
|
||||
// we test both cases where the name of the data is used as the name of the
|
||||
// env key & the case where the hook definition sets the env var name to a
|
||||
// fixed value using the envname construct like so::
|
||||
// [
|
||||
// {
|
||||
// "id": "push",
|
||||
// "execute-command": "bb2mm",
|
||||
// "command-working-directory": "/tmp",
|
||||
// "pass-environment-to-command":
|
||||
// [
|
||||
// {
|
||||
// "source": "entire-payload",
|
||||
// "envname": "PAYLOAD"
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
var hookExtractCommandArgumentsForEnvTests = []struct {
|
||||
exec string
|
||||
args []Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
// successes
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", "", false}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"HOOK_a=z"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", "MYKEY", false}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"MYKEY=z"},
|
||||
true,
|
||||
},
|
||||
// failures
|
||||
{
|
||||
"fail",
|
||||
[]Argument{Argument{"payload", "a", "", false}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArgumentsForEnv(t *testing.T) {
|
||||
for _, tt := range hookExtractCommandArgumentsForEnvTests {
|
||||
h := &Hook{ExecuteCommand: tt.exec, PassEnvironmentToCommand: tt.args}
|
||||
value, err := h.ExtractCommandArgumentsForEnv(tt.headers, tt.query, tt.payload)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
||||
t.Errorf("failed to extract args for env {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hooksLoadFromFileTests = []struct {
|
||||
path string
|
||||
asTemplate bool
|
||||
ok bool
|
||||
}{
|
||||
{"../hooks.json.example", false, true},
|
||||
{"../hooks.yaml.example", false, true},
|
||||
{"../hooks.json.tmpl.example", true, true},
|
||||
{"../hooks.yaml.tmpl.example", true, true},
|
||||
{"", false, true},
|
||||
// failures
|
||||
{"missing.json", false, false},
|
||||
}
|
||||
|
||||
func TestHooksLoadFromFile(t *testing.T) {
|
||||
secret := `foo"123`
|
||||
os.Setenv("XXXTEST_SECRET", secret)
|
||||
|
||||
for _, tt := range hooksLoadFromFileTests {
|
||||
h := &Hooks{}
|
||||
err := h.LoadFromFile(tt.path, tt.asTemplate)
|
||||
if (err == nil) != tt.ok {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHooksTemplateLoadFromFile(t *testing.T) {
|
||||
secret := `foo"123`
|
||||
os.Setenv("XXXTEST_SECRET", secret)
|
||||
|
||||
for _, tt := range hooksLoadFromFileTests {
|
||||
if !tt.asTemplate {
|
||||
continue
|
||||
}
|
||||
|
||||
h := &Hooks{}
|
||||
err := h.LoadFromFile(tt.path, tt.asTemplate)
|
||||
if (err == nil) != tt.ok {
|
||||
t.Errorf(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
s := (*h.Match("webhook").TriggerRule.And)[0].Match.Secret
|
||||
if s != secret {
|
||||
t.Errorf("Expected secret of %q, got %q", secret, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hooksMatchTests = []struct {
|
||||
id string
|
||||
hooks Hooks
|
||||
value *Hook
|
||||
}{
|
||||
{"a", Hooks{Hook{ID: "a"}}, &Hook{ID: "a"}},
|
||||
{"X", Hooks{Hook{ID: "a"}}, new(Hook)},
|
||||
}
|
||||
|
||||
func TestHooksMatch(t *testing.T) {
|
||||
for _, tt := range hooksMatchTests {
|
||||
value := tt.hooks.Match(tt.id)
|
||||
if reflect.DeepEqual(reflect.ValueOf(value), reflect.ValueOf(tt.value)) {
|
||||
t.Errorf("failed to match %q:\nexpected %#v,\ngot %#v", tt.id, tt.value, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var matchRuleTests = []struct {
|
||||
typ, regex, secret, value, ipRange string
|
||||
param Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
body []byte
|
||||
remoteAddr string
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"value", "", "", "z", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
||||
{"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
// failures
|
||||
{"value", "", "", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"regex", "^X", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"value", "", "2", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, false}, // reference invalid header
|
||||
// errors
|
||||
{"regex", "*", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
// IP whitelisting, valid cases
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1", Argument{}, nil, nil, nil, []byte{}, "192.168.0.1:9000", true, false}, // valid IPv4, no range
|
||||
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, with range
|
||||
{"ip-whitelist", "", "", "", "::1", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, no range
|
||||
// IP whitelisting, invalid cases
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, no range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.a:9000", false, true}, // invalid IPv4 address
|
||||
{"ip-whitelist", "", "", "", "::1/a", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, with range
|
||||
{"ip-whitelist", "", "", "", "::z", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, no range
|
||||
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::z]:9000", false, true}, // invalid IPv6 address
|
||||
}
|
||||
|
||||
func TestMatchRule(t *testing.T) {
|
||||
for i, tt := range matchRuleTests {
|
||||
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param, tt.ipRange}
|
||||
ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, tt.remoteAddr)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, (err != nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var andRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule AndRule
|
||||
headers, query, payload *map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
"(a=z, b=y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "y"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=z, b=Y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
},
|
||||
// Complex test to cover Rules.Evaluate
|
||||
{
|
||||
"(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{
|
||||
And: &AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", "", false}, ""}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Or: &OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", "", false}, ""}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Not: &NotRule{
|
||||
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", "", false}, ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
|
||||
// failures
|
||||
{
|
||||
"invalid rule",
|
||||
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}},
|
||||
&map[string]interface{}{"Y": "z"}, nil, nil, nil,
|
||||
false, false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestAndRule(t *testing.T) {
|
||||
for _, tt := range andRuleTests {
|
||||
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "")
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var orRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule OrRule
|
||||
headers, query, payload *map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
"(a=z, b=X): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "X"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=X, b=y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "X", "B": "y"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=Z, b=Y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
},
|
||||
// failures
|
||||
{
|
||||
"invalid rule",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"Y": "Z"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestOrRule(t *testing.T) {
|
||||
for _, tt := range orRuleTests {
|
||||
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "")
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("%#v:\nexpected ok: %#v, err: %v\ngot ok: %#v err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var notRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule NotRule
|
||||
headers, query, payload *map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||
}
|
||||
|
||||
func TestNotRule(t *testing.T) {
|
||||
for _, tt := range notRuleTests {
|
||||
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "")
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.rule, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"response-message": "I got the payload!",
|
||||
"response-headers":
|
||||
[
|
||||
{
|
||||
"name": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
],
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"or":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "expression",
|
||||
"expression": "\"ada\" in Payload"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
+1
-1
@@ -33,7 +33,7 @@
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "{{ getenv "XXXTEST_SECRET" | js }}",
|
||||
"parameter":
|
||||
{
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
type: payload-hash-sha1
|
||||
type: payload-hmac-sha1
|
||||
secret: mysecret
|
||||
parameter:
|
||||
source: header
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
type: payload-hash-sha1
|
||||
type: payload-hmac-sha1
|
||||
secret: "{{ getenv "XXXTEST_SECRET" | js }}"
|
||||
parameter:
|
||||
source: header
|
||||
|
||||
@@ -5,13 +5,17 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"os"
|
||||
@@ -20,20 +24,24 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/antonmedv/expr"
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
// Constants used to specify the parameter source
|
||||
const (
|
||||
SourceHeader string = "header"
|
||||
SourceQuery string = "url"
|
||||
SourceQueryAlias string = "query"
|
||||
SourcePayload string = "payload"
|
||||
SourceString string = "string"
|
||||
SourceEntirePayload string = "entire-payload"
|
||||
SourceEntireQuery string = "entire-query"
|
||||
SourceEntireHeaders string = "entire-headers"
|
||||
SourceHeader string = "header"
|
||||
SourceQuery string = "url"
|
||||
SourceQueryAlias string = "query"
|
||||
SourcePayload string = "payload"
|
||||
SourceRawRequestBody string = "raw-request-body"
|
||||
SourceRequest string = "request"
|
||||
SourceString string = "string"
|
||||
SourceEntirePayload string = "entire-payload"
|
||||
SourceEntireQuery string = "entire-query"
|
||||
SourceEntireHeaders string = "entire-headers"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -42,16 +50,61 @@ const (
|
||||
EnvNamespace string = "HOOK_"
|
||||
)
|
||||
|
||||
// ParameterNodeError describes an error walking a parameter node.
|
||||
type ParameterNodeError struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (e *ParameterNodeError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("parameter node not found: %s", e.key)
|
||||
}
|
||||
|
||||
// IsParameterNodeError returns whether err is of type ParameterNodeError.
|
||||
func IsParameterNodeError(err error) bool {
|
||||
switch err.(type) {
|
||||
case *ParameterNodeError:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// SignatureError describes an invalid payload signature passed to Hook.
|
||||
type SignatureError struct {
|
||||
Signature string
|
||||
Signature string
|
||||
Signatures []string
|
||||
|
||||
emptyPayload bool
|
||||
}
|
||||
|
||||
func (e *SignatureError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("invalid payload signature %s", e.Signature)
|
||||
|
||||
var empty string
|
||||
if e.emptyPayload {
|
||||
empty = " on empty payload"
|
||||
}
|
||||
|
||||
if e.Signatures != nil {
|
||||
return fmt.Sprintf("invalid payload signatures %s%s", e.Signatures, empty)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("invalid payload signature %s%s", e.Signature, empty)
|
||||
}
|
||||
|
||||
// IsSignatureError returns whether err is of type SignatureError.
|
||||
func IsSignatureError(err error) bool {
|
||||
switch err.(type) {
|
||||
case *SignatureError:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ArgumentError describes an invalid argument passed to Hook.
|
||||
@@ -90,97 +143,213 @@ func (e *ParseError) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
||||
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, error) {
|
||||
if strings.HasPrefix(signature, "sha1=") {
|
||||
signature = signature[5:]
|
||||
// ExtractCommaSeparatedValues will extract the values matching the key.
|
||||
func ExtractCommaSeparatedValues(source, prefix string) []string {
|
||||
parts := strings.Split(source, ",")
|
||||
values := make([]string, 0)
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, prefix) {
|
||||
values = append(values, strings.TrimPrefix(part, prefix))
|
||||
}
|
||||
}
|
||||
|
||||
mac := hmac.New(sha1.New, []byte(secret))
|
||||
return values
|
||||
}
|
||||
|
||||
// ExtractSignatures will extract all the signatures from the source.
|
||||
func ExtractSignatures(source, prefix string) []string {
|
||||
// If there are multiple possible matches, let the comma seperated extractor
|
||||
// do it's work.
|
||||
if strings.Contains(source, ",") {
|
||||
return ExtractCommaSeparatedValues(source, prefix)
|
||||
}
|
||||
|
||||
// There were no commas, so just trim the prefix (if it even exists) and
|
||||
// pass it back.
|
||||
return []string{
|
||||
strings.TrimPrefix(source, prefix),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateMAC will verify that the expected mac for the given hash will match
|
||||
// the one provided.
|
||||
func ValidateMAC(payload []byte, mac hash.Hash, signatures []string) (string, error) {
|
||||
// Write the payload to the provided hash.
|
||||
_, err := mac.Write(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||
return expectedMAC, &SignatureError{signature}
|
||||
actualMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
for _, signature := range signatures {
|
||||
if hmac.Equal([]byte(signature), []byte(actualMAC)) {
|
||||
return actualMAC, err
|
||||
}
|
||||
}
|
||||
return expectedMAC, err
|
||||
|
||||
e := &SignatureError{Signatures: signatures}
|
||||
if len(payload) == 0 {
|
||||
e.emptyPayload = true
|
||||
}
|
||||
|
||||
return actualMAC, e
|
||||
}
|
||||
|
||||
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
||||
func CheckPayloadSignature(payload []byte, secret, signature string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
// Extract the signatures.
|
||||
signatures := ExtractSignatures(signature, "sha1=")
|
||||
|
||||
// Validate the MAC.
|
||||
return ValidateMAC(payload, hmac.New(sha1.New, []byte(secret)), signatures)
|
||||
}
|
||||
|
||||
// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload
|
||||
func CheckPayloadSignature256(payload []byte, secret string, signature string) (string, error) {
|
||||
if strings.HasPrefix(signature, "sha256=") {
|
||||
signature = signature[7:]
|
||||
func CheckPayloadSignature256(payload []byte, secret, signature string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, err := mac.Write(payload)
|
||||
// Extract the signatures.
|
||||
signatures := ExtractSignatures(signature, "sha256=")
|
||||
|
||||
// Validate the MAC.
|
||||
return ValidateMAC(payload, hmac.New(sha256.New, []byte(secret)), signatures)
|
||||
}
|
||||
|
||||
// CheckPayloadSignature512 calculates and verifies SHA512 signature of the given payload
|
||||
func CheckPayloadSignature512(payload []byte, secret, signature string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
// Extract the signatures.
|
||||
signatures := ExtractSignatures(signature, "sha512=")
|
||||
|
||||
// Validate the MAC.
|
||||
return ValidateMAC(payload, hmac.New(sha512.New, []byte(secret)), signatures)
|
||||
}
|
||||
|
||||
func CheckScalrSignature(r *Request, signingKey string, checkDate bool) (bool, error) {
|
||||
if r.Headers == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check for the signature and date headers
|
||||
if _, ok := r.Headers["X-Signature"]; !ok {
|
||||
return false, nil
|
||||
}
|
||||
if _, ok := r.Headers["Date"]; !ok {
|
||||
return false, nil
|
||||
}
|
||||
if signingKey == "" {
|
||||
return false, errors.New("signature validation signing key can not be empty")
|
||||
}
|
||||
|
||||
providedSignature := r.Headers["X-Signature"].(string)
|
||||
dateHeader := r.Headers["Date"].(string)
|
||||
mac := hmac.New(sha1.New, []byte(signingKey))
|
||||
mac.Write(r.Body)
|
||||
mac.Write([]byte(dateHeader))
|
||||
expectedSignature := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(providedSignature), []byte(expectedSignature)) {
|
||||
return false, &SignatureError{Signature: providedSignature}
|
||||
}
|
||||
|
||||
if !checkDate {
|
||||
return true, nil
|
||||
}
|
||||
// Example format: Fri 08 Sep 2017 11:24:32 UTC
|
||||
date, err := time.Parse("Mon 02 Jan 2006 15:04:05 MST", dateHeader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return false, err
|
||||
}
|
||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
now := time.Now()
|
||||
delta := math.Abs(now.Sub(date).Seconds())
|
||||
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||
return expectedMAC, &SignatureError{signature}
|
||||
if delta > 300 {
|
||||
return false, &SignatureError{Signature: "outdated"}
|
||||
}
|
||||
return expectedMAC, err
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckIPWhitelist makes sure the provided remote address (of the form IP:port) falls within the provided IP range
|
||||
// (in CIDR form or a single IP address).
|
||||
func CheckIPWhitelist(remoteAddr string, ipRange string) (bool, error) {
|
||||
func CheckIPWhitelist(remoteAddr, ipRange string) (bool, error) {
|
||||
// Extract IP address from remote address.
|
||||
|
||||
ip := remoteAddr
|
||||
// IPv6 addresses will likely be surrounded by [].
|
||||
ip := strings.Trim(remoteAddr, " []")
|
||||
|
||||
if strings.LastIndex(remoteAddr, ":") != -1 {
|
||||
ip = remoteAddr[0:strings.LastIndex(remoteAddr, ":")]
|
||||
if i := strings.LastIndex(ip, ":"); i != -1 {
|
||||
ip = ip[:i]
|
||||
ip = strings.Trim(ip, " []")
|
||||
}
|
||||
|
||||
ip = strings.TrimSpace(ip)
|
||||
|
||||
// IPv6 addresses will likely be surrounded by [], so don't forget to remove those.
|
||||
|
||||
if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") {
|
||||
ip = ip[1 : len(ip)-1]
|
||||
}
|
||||
|
||||
parsedIP := net.ParseIP(strings.TrimSpace(ip))
|
||||
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return false, fmt.Errorf("invalid IP address found in remote address '%s'", remoteAddr)
|
||||
}
|
||||
|
||||
// Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form.
|
||||
for _, r := range strings.Fields(ipRange) {
|
||||
// Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form.
|
||||
|
||||
ipRange = strings.TrimSpace(ipRange)
|
||||
if !strings.Contains(r, "/") {
|
||||
r = r + "/32"
|
||||
}
|
||||
|
||||
if strings.Index(ipRange, "/") == -1 {
|
||||
ipRange = ipRange + "/32"
|
||||
_, cidr, err := net.ParseCIDR(r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if cidr.Contains(parsedIP) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
_, cidr, err := net.ParseCIDR(ipRange)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
type ExpressionEnv struct {
|
||||
Headers, Query, Payload map[string]interface{}
|
||||
}
|
||||
|
||||
func CheckExpression(request *Request, expression string) (bool, error) {
|
||||
program, err := expr.Compile(expression, expr.Env(ExpressionEnv{}))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
out, err := expr.Run(program, ExpressionEnv{
|
||||
Headers: request.Headers,
|
||||
Query: request.Query,
|
||||
Payload: request.Payload,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return cidr.Contains(parsedIP), nil
|
||||
return out == true, nil
|
||||
}
|
||||
|
||||
// ReplaceParameter replaces parameter value with the passed value in the passed map
|
||||
// (please note you should pass pointer to the map, because we're modifying it)
|
||||
// based on the passed string
|
||||
func ReplaceParameter(s string, params interface{}, value interface{}) bool {
|
||||
func ReplaceParameter(s string, params, value interface{}) bool {
|
||||
if params == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
||||
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
||||
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
index, err := strconv.ParseUint(p[0], 10, 64)
|
||||
|
||||
@@ -210,19 +379,23 @@ func ReplaceParameter(s string, params interface{}, value interface{}) bool {
|
||||
}
|
||||
|
||||
// GetParameter extracts interface{} value based on the passed string
|
||||
func GetParameter(s string, params interface{}) (interface{}, bool) {
|
||||
func GetParameter(s string, params interface{}) (interface{}, error) {
|
||||
if params == nil {
|
||||
return nil, false
|
||||
return nil, errors.New("no parameters")
|
||||
}
|
||||
|
||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
||||
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
||||
paramsValue := reflect.ValueOf(params)
|
||||
|
||||
switch paramsValue.Kind() {
|
||||
case reflect.Slice:
|
||||
paramsValueSliceLength := paramsValue.Len()
|
||||
if paramsValueSliceLength > 0 {
|
||||
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
index, err := strconv.ParseUint(p[0], 10, 64)
|
||||
|
||||
if err != nil || paramsValueSliceLength <= int(index) {
|
||||
return nil, false
|
||||
return nil, &ParameterNodeError{s}
|
||||
}
|
||||
|
||||
return GetParameter(p[1], params.([]interface{})[index])
|
||||
@@ -231,38 +404,55 @@ func GetParameter(s string, params interface{}) (interface{}, bool) {
|
||||
index, err := strconv.ParseUint(s, 10, 64)
|
||||
|
||||
if err != nil || paramsValueSliceLength <= int(index) {
|
||||
return nil, false
|
||||
return nil, &ParameterNodeError{s}
|
||||
}
|
||||
|
||||
return params.([]interface{})[index], true
|
||||
return params.([]interface{})[index], nil
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
return nil, &ParameterNodeError{s}
|
||||
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Map {
|
||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||
case reflect.Map:
|
||||
// Check for raw key
|
||||
if v, ok := params.(map[string]interface{})[s]; ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Checked for dotted references
|
||||
p := strings.SplitN(s, ".", 2)
|
||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||
if len(p) > 1 {
|
||||
return GetParameter(p[1], pValue)
|
||||
}
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
} else {
|
||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||
return pValue, true
|
||||
|
||||
return pValue, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
return nil, &ParameterNodeError{s}
|
||||
}
|
||||
|
||||
// ExtractParameterAsString extracts value from interface{} as string based on the passed string
|
||||
func ExtractParameterAsString(s string, params interface{}) (string, bool) {
|
||||
if pValue, ok := GetParameter(s, params); ok {
|
||||
return fmt.Sprintf("%v", pValue), true
|
||||
// ExtractParameterAsString extracts value from interface{} as string based on
|
||||
// the passed string. Complex data types are rendered as JSON instead of the Go
|
||||
// Stringer format.
|
||||
func ExtractParameterAsString(s string, params interface{}) (string, error) {
|
||||
pValue, err := GetParameter(s, params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch v := reflect.ValueOf(pValue); v.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice:
|
||||
r, err := json.Marshal(pValue)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(r), nil
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("%v", pValue), nil
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Argument type specifies the parameter key name and the source it should
|
||||
@@ -276,51 +466,71 @@ type Argument struct {
|
||||
|
||||
// Get Argument method returns the value for the Argument's key name
|
||||
// based on the Argument's source
|
||||
func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string, bool) {
|
||||
func (ha *Argument) Get(r *Request) (string, error) {
|
||||
var source *map[string]interface{}
|
||||
key := ha.Name
|
||||
|
||||
switch ha.Source {
|
||||
case SourceHeader:
|
||||
source = headers
|
||||
source = &r.Headers
|
||||
key = textproto.CanonicalMIMEHeaderKey(ha.Name)
|
||||
|
||||
case SourceQuery, SourceQueryAlias:
|
||||
source = query
|
||||
source = &r.Query
|
||||
|
||||
case SourcePayload:
|
||||
source = payload
|
||||
source = &r.Payload
|
||||
|
||||
case SourceString:
|
||||
return ha.Name, true
|
||||
return ha.Name, nil
|
||||
|
||||
case SourceRawRequestBody:
|
||||
return string(r.Body), nil
|
||||
|
||||
case SourceRequest:
|
||||
if r == nil || r.RawRequest == nil {
|
||||
return "", errors.New("request is nil")
|
||||
}
|
||||
|
||||
switch strings.ToLower(ha.Name) {
|
||||
case "remote-addr":
|
||||
return r.RawRequest.RemoteAddr, nil
|
||||
case "method":
|
||||
return r.RawRequest.Method, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported request key: %q", ha.Name)
|
||||
}
|
||||
|
||||
case SourceEntirePayload:
|
||||
r, err := json.Marshal(payload)
|
||||
|
||||
res, err := json.Marshal(&r.Payload)
|
||||
if err != nil {
|
||||
return "", false
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(r), true
|
||||
return string(res), nil
|
||||
|
||||
case SourceEntireHeaders:
|
||||
r, err := json.Marshal(headers)
|
||||
|
||||
res, err := json.Marshal(&r.Headers)
|
||||
if err != nil {
|
||||
return "", false
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(r), true
|
||||
return string(res), nil
|
||||
|
||||
case SourceEntireQuery:
|
||||
r, err := json.Marshal(query)
|
||||
|
||||
res, err := json.Marshal(&r.Query)
|
||||
if err != nil {
|
||||
return "", false
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(r), true
|
||||
return string(res), nil
|
||||
}
|
||||
|
||||
if source != nil {
|
||||
return ExtractParameterAsString(key, *source)
|
||||
}
|
||||
|
||||
return "", false
|
||||
return "", errors.New("no source for value retrieval")
|
||||
}
|
||||
|
||||
// Header is a structure containing header name and it's value
|
||||
@@ -391,22 +601,28 @@ type Hook struct {
|
||||
JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
|
||||
TriggerRule *Rules `json:"trigger-rule,omitempty"`
|
||||
TriggerRuleMismatchHttpResponseCode int `json:"trigger-rule-mismatch-http-response-code,omitempty"`
|
||||
TriggerSignatureSoftFailures bool `json:"trigger-signature-soft-failures,omitempty"`
|
||||
IncomingPayloadContentType string `json:"incoming-payload-content-type,omitempty"`
|
||||
SuccessHttpResponseCode int `json:"success-http-response-code,omitempty"`
|
||||
HTTPMethods []string `json:"http-methods"`
|
||||
}
|
||||
|
||||
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
||||
// string with the newly created object
|
||||
func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface{}) []error {
|
||||
var errors = make([]error, 0)
|
||||
func (h *Hook) ParseJSONParameters(r *Request) []error {
|
||||
errors := make([]error, 0)
|
||||
|
||||
for i := range h.JSONStringParameters {
|
||||
if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok {
|
||||
arg, err := h.JSONStringParameters[i].Get(r)
|
||||
if err != nil {
|
||||
errors = append(errors, &ArgumentError{h.JSONStringParameters[i]})
|
||||
} else {
|
||||
var newArg map[string]interface{}
|
||||
|
||||
decoder := json.NewDecoder(strings.NewReader(string(arg)))
|
||||
decoder.UseNumber()
|
||||
|
||||
err := decoder.Decode(&newArg)
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, &ParseError{err})
|
||||
continue
|
||||
@@ -416,11 +632,11 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
|
||||
|
||||
switch h.JSONStringParameters[i].Source {
|
||||
case SourceHeader:
|
||||
source = headers
|
||||
source = &r.Headers
|
||||
case SourcePayload:
|
||||
source = payload
|
||||
source = &r.Payload
|
||||
case SourceQuery, SourceQueryAlias:
|
||||
source = query
|
||||
source = &r.Query
|
||||
}
|
||||
|
||||
if source != nil {
|
||||
@@ -434,8 +650,6 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
|
||||
} else {
|
||||
errors = append(errors, &SourceError{h.JSONStringParameters[i]})
|
||||
}
|
||||
} else {
|
||||
errors = append(errors, &ArgumentError{h.JSONStringParameters[i]})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,19 +662,21 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
|
||||
|
||||
// ExtractCommandArguments creates a list of arguments, based on the
|
||||
// PassArgumentsToCommand property that is ready to be used with exec.Command()
|
||||
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, []error) {
|
||||
var args = make([]string, 0)
|
||||
var errors = make([]error, 0)
|
||||
func (h *Hook) ExtractCommandArguments(r *Request) ([]string, []error) {
|
||||
args := make([]string, 0)
|
||||
errors := make([]error, 0)
|
||||
|
||||
args = append(args, h.ExecuteCommand)
|
||||
|
||||
for i := range h.PassArgumentsToCommand {
|
||||
if arg, ok := h.PassArgumentsToCommand[i].Get(headers, query, payload); ok {
|
||||
args = append(args, arg)
|
||||
} else {
|
||||
arg, err := h.PassArgumentsToCommand[i].Get(r)
|
||||
if err != nil {
|
||||
args = append(args, "")
|
||||
errors = append(errors, &ArgumentError{h.PassArgumentsToCommand[i]})
|
||||
continue
|
||||
}
|
||||
|
||||
args = append(args, arg)
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
@@ -473,20 +689,22 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter
|
||||
// ExtractCommandArgumentsForEnv creates a list of arguments in key=value
|
||||
// format, based on the PassEnvironmentToCommand property that is ready to be used
|
||||
// with exec.Command().
|
||||
func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, []error) {
|
||||
var args = make([]string, 0)
|
||||
var errors = make([]error, 0)
|
||||
func (h *Hook) ExtractCommandArgumentsForEnv(r *Request) ([]string, []error) {
|
||||
args := make([]string, 0)
|
||||
errors := make([]error, 0)
|
||||
for i := range h.PassEnvironmentToCommand {
|
||||
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
|
||||
if h.PassEnvironmentToCommand[i].EnvName != "" {
|
||||
// first try to use the EnvName if specified
|
||||
args = append(args, h.PassEnvironmentToCommand[i].EnvName+"="+arg)
|
||||
} else {
|
||||
// then fallback on the name
|
||||
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
||||
}
|
||||
} else {
|
||||
arg, err := h.PassEnvironmentToCommand[i].Get(r)
|
||||
if err != nil {
|
||||
errors = append(errors, &ArgumentError{h.PassEnvironmentToCommand[i]})
|
||||
continue
|
||||
}
|
||||
|
||||
if h.PassEnvironmentToCommand[i].EnvName != "" {
|
||||
// first try to use the EnvName if specified
|
||||
args = append(args, h.PassEnvironmentToCommand[i].EnvName+"="+arg)
|
||||
} else {
|
||||
// then fallback on the name
|
||||
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,34 +725,34 @@ type FileParameter struct {
|
||||
// ExtractCommandArgumentsForFile creates a list of arguments in key=value
|
||||
// format, based on the PassFileToCommand property that is ready to be used
|
||||
// with exec.Command().
|
||||
func (h *Hook) ExtractCommandArgumentsForFile(headers, query, payload *map[string]interface{}) ([]FileParameter, []error) {
|
||||
var args = make([]FileParameter, 0)
|
||||
var errors = make([]error, 0)
|
||||
func (h *Hook) ExtractCommandArgumentsForFile(r *Request) ([]FileParameter, []error) {
|
||||
args := make([]FileParameter, 0)
|
||||
errors := make([]error, 0)
|
||||
for i := range h.PassFileToCommand {
|
||||
if arg, ok := h.PassFileToCommand[i].Get(headers, query, payload); ok {
|
||||
|
||||
if h.PassFileToCommand[i].EnvName == "" {
|
||||
// if no environment-variable name is set, fall-back on the name
|
||||
log.Printf("no ENVVAR name specified, falling back to [%s]", EnvNamespace+strings.ToUpper(h.PassFileToCommand[i].Name))
|
||||
h.PassFileToCommand[i].EnvName = EnvNamespace + strings.ToUpper(h.PassFileToCommand[i].Name)
|
||||
}
|
||||
|
||||
var fileContent []byte
|
||||
if h.PassFileToCommand[i].Base64Decode {
|
||||
dec, err := base64.StdEncoding.DecodeString(arg)
|
||||
if err != nil {
|
||||
log.Printf("error decoding string [%s]", err)
|
||||
}
|
||||
fileContent = []byte(dec)
|
||||
} else {
|
||||
fileContent = []byte(arg)
|
||||
}
|
||||
|
||||
args = append(args, FileParameter{EnvName: h.PassFileToCommand[i].EnvName, Data: fileContent})
|
||||
|
||||
} else {
|
||||
arg, err := h.PassFileToCommand[i].Get(r)
|
||||
if err != nil {
|
||||
errors = append(errors, &ArgumentError{h.PassFileToCommand[i]})
|
||||
continue
|
||||
}
|
||||
|
||||
if h.PassFileToCommand[i].EnvName == "" {
|
||||
// if no environment-variable name is set, fall-back on the name
|
||||
log.Printf("no ENVVAR name specified, falling back to [%s]", EnvNamespace+strings.ToUpper(h.PassFileToCommand[i].Name))
|
||||
h.PassFileToCommand[i].EnvName = EnvNamespace + strings.ToUpper(h.PassFileToCommand[i].Name)
|
||||
}
|
||||
|
||||
var fileContent []byte
|
||||
if h.PassFileToCommand[i].Base64Decode {
|
||||
dec, err := base64.StdEncoding.DecodeString(arg)
|
||||
if err != nil {
|
||||
log.Printf("error decoding string [%s]", err)
|
||||
}
|
||||
fileContent = []byte(dec)
|
||||
} else {
|
||||
fileContent = []byte(arg)
|
||||
}
|
||||
|
||||
args = append(args, FileParameter{EnvName: h.PassFileToCommand[i].EnvName, Data: fileContent})
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
@@ -580,8 +798,7 @@ func (h *Hooks) LoadFromFile(path string, asTemplate bool) error {
|
||||
file = buf.Bytes()
|
||||
}
|
||||
|
||||
e = yaml.Unmarshal(file, h)
|
||||
return e
|
||||
return yaml.Unmarshal(file, h)
|
||||
}
|
||||
|
||||
// Append appends hooks unless the new hooks contain a hook with an ID that already exists
|
||||
@@ -619,16 +836,16 @@ type Rules struct {
|
||||
|
||||
// Evaluate finds the first rule property that is not nil and returns the value
|
||||
// it evaluates to
|
||||
func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
|
||||
func (r Rules) Evaluate(req *Request) (bool, error) {
|
||||
switch {
|
||||
case r.And != nil:
|
||||
return r.And.Evaluate(headers, query, payload, body, remoteAddr)
|
||||
return r.And.Evaluate(req)
|
||||
case r.Or != nil:
|
||||
return r.Or.Evaluate(headers, query, payload, body, remoteAddr)
|
||||
return r.Or.Evaluate(req)
|
||||
case r.Not != nil:
|
||||
return r.Not.Evaluate(headers, query, payload, body, remoteAddr)
|
||||
return r.Not.Evaluate(req)
|
||||
case r.Match != nil:
|
||||
return r.Match.Evaluate(headers, query, payload, body, remoteAddr)
|
||||
return r.Match.Evaluate(req)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
@@ -638,17 +855,17 @@ func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[
|
||||
type AndRule []Rules
|
||||
|
||||
// Evaluate AndRule will return true if and only if all of ChildRules evaluate to true
|
||||
func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
|
||||
func (r AndRule) Evaluate(req *Request) (bool, error) {
|
||||
res := true
|
||||
|
||||
for _, v := range r {
|
||||
rv, err := v.Evaluate(headers, query, payload, body, remoteAddr)
|
||||
rv, err := v.Evaluate(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
res = res && rv
|
||||
if res == false {
|
||||
if !res {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
@@ -660,17 +877,21 @@ func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body
|
||||
type OrRule []Rules
|
||||
|
||||
// Evaluate OrRule will return true if any of ChildRules evaluate to true
|
||||
func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
|
||||
func (r OrRule) Evaluate(req *Request) (bool, error) {
|
||||
res := false
|
||||
|
||||
for _, v := range r {
|
||||
rv, err := v.Evaluate(headers, query, payload, body, remoteAddr)
|
||||
rv, err := v.Evaluate(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
if !IsParameterNodeError(err) {
|
||||
if !req.AllowSignatureErrors || (req.AllowSignatureErrors && !IsSignatureError(err)) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res = res || rv
|
||||
if res == true {
|
||||
if res {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
@@ -682,51 +903,82 @@ func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *
|
||||
type NotRule Rules
|
||||
|
||||
// Evaluate NotRule will return true if and only if ChildRule evaluates to false
|
||||
func (r NotRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
|
||||
rv, err := Rules(r).Evaluate(headers, query, payload, body, remoteAddr)
|
||||
func (r NotRule) Evaluate(req *Request) (bool, error) {
|
||||
rv, err := Rules(r).Evaluate(req)
|
||||
return !rv, err
|
||||
}
|
||||
|
||||
// MatchRule will evaluate to true based on the type
|
||||
type MatchRule struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Regex string `json:"regex,omitempty"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Parameter Argument `json:"parameter,omitempty"`
|
||||
IPRange string `json:"ip-range,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Regex string `json:"regex,omitempty"`
|
||||
Expression string `json:"expression,omitempty"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Parameter Argument `json:"parameter,omitempty"`
|
||||
IPRange string `json:"ip-range,omitempty"`
|
||||
}
|
||||
|
||||
// Constants for the MatchRule type
|
||||
const (
|
||||
MatchValue string = "value"
|
||||
MatchRegex string = "regex"
|
||||
MatchExpression string = "expression"
|
||||
MatchHMACSHA1 string = "payload-hmac-sha1"
|
||||
MatchHMACSHA256 string = "payload-hmac-sha256"
|
||||
MatchHMACSHA512 string = "payload-hmac-sha512"
|
||||
MatchHashSHA1 string = "payload-hash-sha1"
|
||||
MatchHashSHA256 string = "payload-hash-sha256"
|
||||
MatchHashSHA512 string = "payload-hash-sha512"
|
||||
IPWhitelist string = "ip-whitelist"
|
||||
ScalrSignature string = "scalr-signature"
|
||||
)
|
||||
|
||||
// Evaluate MatchRule will return based on the type
|
||||
func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
|
||||
func (r MatchRule) Evaluate(req *Request) (bool, error) {
|
||||
if r.Type == IPWhitelist {
|
||||
return CheckIPWhitelist(remoteAddr, r.IPRange)
|
||||
return CheckIPWhitelist(req.RawRequest.RemoteAddr, r.IPRange)
|
||||
}
|
||||
if r.Type == ScalrSignature {
|
||||
return CheckScalrSignature(req, r.Secret, true)
|
||||
}
|
||||
if r.Type == MatchExpression {
|
||||
return CheckExpression(req, r.Expression)
|
||||
}
|
||||
|
||||
if arg, ok := r.Parameter.Get(headers, query, payload); ok {
|
||||
arg, err := r.Parameter.Get(req)
|
||||
if err == nil {
|
||||
switch r.Type {
|
||||
case MatchValue:
|
||||
return arg == r.Value, nil
|
||||
return compare(arg, r.Value), nil
|
||||
case MatchRegex:
|
||||
return regexp.MatchString(r.Regex, arg)
|
||||
case MatchHashSHA1:
|
||||
_, err := CheckPayloadSignature(*body, r.Secret, arg)
|
||||
log.Print(`warn: use of deprecated option payload-hash-sha1; use payload-hmac-sha1 instead`)
|
||||
fallthrough
|
||||
case MatchHMACSHA1:
|
||||
_, err := CheckPayloadSignature(req.Body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
case MatchHashSHA256:
|
||||
_, err := CheckPayloadSignature256(*body, r.Secret, arg)
|
||||
log.Print(`warn: use of deprecated option payload-hash-sha256: use payload-hmac-sha256 instead`)
|
||||
fallthrough
|
||||
case MatchHMACSHA256:
|
||||
_, err := CheckPayloadSignature256(req.Body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
case MatchHashSHA512:
|
||||
log.Print(`warn: use of deprecated option payload-hash-sha512: use payload-hmac-sha512 instead`)
|
||||
fallthrough
|
||||
case MatchHMACSHA512:
|
||||
_, err := CheckPayloadSignature512(req.Body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
return false, err
|
||||
}
|
||||
|
||||
// compare is a helper function for constant time string comparisons.
|
||||
func compare(a, b string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
|
||||
// getenv provides a template function to retrieve OS environment variables.
|
||||
@@ -0,0 +1,728 @@
|
||||
package hook
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetParameter(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
key string
|
||||
val interface{}
|
||||
expect interface{}
|
||||
ok bool
|
||||
}{
|
||||
// True
|
||||
{"a", map[string]interface{}{"a": "1"}, "1", true},
|
||||
{"a.b", map[string]interface{}{"a.b": "1"}, "1", true},
|
||||
{"a.c", map[string]interface{}{"a": map[string]interface{}{"c": 2}}, 2, true},
|
||||
{"a.1", map[string]interface{}{"a": map[string]interface{}{"1": 3}}, 3, true},
|
||||
{"a.1", map[string]interface{}{"a": []interface{}{"a", "b"}}, "b", true},
|
||||
{"0", []interface{}{"a", "b"}, "a", true},
|
||||
|
||||
// False
|
||||
{"z", map[string]interface{}{"a": "1"}, nil, false},
|
||||
{"a.z", map[string]interface{}{"a": map[string]interface{}{"b": 2}}, nil, false},
|
||||
{"z.b", map[string]interface{}{"a": map[string]interface{}{"z": 2}}, nil, false},
|
||||
{"a.2", map[string]interface{}{"a": []interface{}{"a", "b"}}, nil, false},
|
||||
} {
|
||||
res, err := GetParameter(test.key, test.val)
|
||||
if (err == nil) != test.ok {
|
||||
t.Errorf("unexpected result given {%q, %q}: %s\n", test.key, test.val, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(res, test.expect) {
|
||||
t.Errorf("failed given {%q, %q}:\nexpected {%#v}\ngot {%#v}\n", test.key, test.val, test.expect, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkPayloadSignatureTests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
{[]byte(``), "secret", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
|
||||
{[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false},
|
||||
{[]byte(``), "secret", "XXXf6174a0fcecc4d346680a72b7ce644b9a88e8", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignatureTests {
|
||||
mac, err := CheckPayloadSignature(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkPayloadSignature256Tests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
{[]byte(``), "secret", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
||||
{[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false},
|
||||
{[]byte(``), "secret", "XXX66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature256(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignature256Tests {
|
||||
mac, err := CheckPayloadSignature256(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkPayloadSignature512Tests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha512=4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
|
||||
{[]byte(``), "secret", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", false},
|
||||
{[]byte(`{"a": "z"}`), "", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "", false},
|
||||
{[]byte(``), "secret", "XXX9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature512(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignature512Tests {
|
||||
mac, err := CheckPayloadSignature512(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkScalrSignatureTests = []struct {
|
||||
description string
|
||||
headers map[string]interface{}
|
||||
body []byte
|
||||
secret string
|
||||
expectedSignature string
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
"Valid signature",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "48e395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", true,
|
||||
},
|
||||
{
|
||||
"Wrong signature",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "999395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
{
|
||||
"Missing Date header",
|
||||
map[string]interface{}{"X-Signature": "999395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
{
|
||||
"Missing X-Signature header",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
{
|
||||
"Missing signing key",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "48e395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestCheckScalrSignature(t *testing.T) {
|
||||
for _, testCase := range checkScalrSignatureTests {
|
||||
r := &Request{
|
||||
Headers: testCase.headers,
|
||||
Body: testCase.body,
|
||||
}
|
||||
valid, err := CheckScalrSignature(r, testCase.secret, false)
|
||||
if valid != testCase.ok {
|
||||
t.Errorf("failed to check scalr signature fot test case: %s\nexpected ok:%#v, got ok:%#v}",
|
||||
testCase.description, testCase.ok, valid)
|
||||
}
|
||||
|
||||
if err != nil && testCase.secret != "" && strings.Contains(err.Error(), testCase.expectedSignature) {
|
||||
t.Errorf("error message should not disclose expected mac: %s on test case %s", err, testCase.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkIPWhitelistTests = []struct {
|
||||
addr string
|
||||
ipRange string
|
||||
expect bool
|
||||
ok bool
|
||||
}{
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.1 ", true, true},
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.0 ", false, true},
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.1 10.0.0.1 ", true, true},
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.0/31 ", true, true},
|
||||
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/48 ", true, true},
|
||||
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/48 2001:db8:1::/64", true, true},
|
||||
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/64 ", false, true},
|
||||
}
|
||||
|
||||
func TestCheckIPWhitelist(t *testing.T) {
|
||||
for _, tt := range checkIPWhitelistTests {
|
||||
result, err := CheckIPWhitelist(tt.addr, tt.ipRange)
|
||||
if (err == nil) != tt.ok || result != tt.expect {
|
||||
t.Errorf("ip whitelist test failed {%q, %q}:\nwant {expect:%#v, ok:%#v},\ngot {result:%#v, ok:%#v}", tt.addr, tt.ipRange, tt.expect, tt.ok, result, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var extractParameterTests = []struct {
|
||||
s string
|
||||
params interface{}
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"a", map[string]interface{}{"a": "z"}, "z", true},
|
||||
{"a.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "z", true},
|
||||
{"a.b.c", map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}, "z", true},
|
||||
{"a.b.0", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "x", true},
|
||||
{"a.1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "z", true},
|
||||
{"a.1.b.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": map[string]interface{}{"c": "y"}}, map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}}, "z", true},
|
||||
{"b", map[string]interface{}{"b": map[string]interface{}{"z": 1}}, `{"z":1}`, true},
|
||||
{"c", map[string]interface{}{"c": []interface{}{"y", "z"}}, `["y","z"]`, true},
|
||||
{"d", map[string]interface{}{"d": [2]interface{}{"y", "z"}}, `["y","z"]`, true},
|
||||
// failures
|
||||
{"check_nil", nil, "", false},
|
||||
{"a.X", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent parameter reference
|
||||
{"a.X.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-integer slice index
|
||||
{"a.-1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // negative slice index
|
||||
{"a.500.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent slice
|
||||
{"a.501.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-existent slice index
|
||||
{"a.502.b", map[string]interface{}{"a": []interface{}{}}, "", false}, // non-existent slice index
|
||||
{"a.b.503", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "", false}, // trailing, non-existent slice index
|
||||
{"a.b", interface{}("a"), "", false}, // non-map, non-slice input
|
||||
}
|
||||
|
||||
func TestExtractParameter(t *testing.T) {
|
||||
for _, tt := range extractParameterTests {
|
||||
value, err := ExtractParameterAsString(tt.s, tt.params)
|
||||
if (err == nil) != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to extract parameter %q:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%v}", tt.s, tt.value, tt.ok, value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var argumentGetTests = []struct {
|
||||
source, name string
|
||||
headers, query, payload map[string]interface{}
|
||||
request *http.Request
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"header", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "z", true},
|
||||
{"url", "a", nil, map[string]interface{}{"a": "z"}, nil, nil, "z", true},
|
||||
{"payload", "a", nil, nil, map[string]interface{}{"a": "z"}, nil, "z", true},
|
||||
{"request", "METHOD", nil, nil, map[string]interface{}{"a": "z"}, &http.Request{Method: "POST", RemoteAddr: "127.0.0.1:1234"}, "POST", true},
|
||||
{"request", "remote-addr", nil, nil, map[string]interface{}{"a": "z"}, &http.Request{Method: "POST", RemoteAddr: "127.0.0.1:1234"}, "127.0.0.1:1234", true},
|
||||
{"string", "a", nil, nil, map[string]interface{}{"a": "z"}, nil, "a", true},
|
||||
// failures
|
||||
{"header", "a", nil, map[string]interface{}{"a": "z"}, map[string]interface{}{"a": "z"}, nil, "", false}, // nil headers
|
||||
{"url", "a", map[string]interface{}{"A": "z"}, nil, map[string]interface{}{"a": "z"}, nil, "", false}, // nil query
|
||||
{"payload", "a", map[string]interface{}{"A": "z"}, map[string]interface{}{"a": "z"}, nil, nil, "", false}, // nil payload
|
||||
{"foo", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "", false}, // invalid source
|
||||
}
|
||||
|
||||
func TestArgumentGet(t *testing.T) {
|
||||
for _, tt := range argumentGetTests {
|
||||
a := Argument{tt.source, tt.name, "", false}
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
RawRequest: tt.request,
|
||||
}
|
||||
value, err := a.Get(r)
|
||||
if (err == nil) != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%v}", tt.source, tt.name, tt.value, tt.ok, value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hookParseJSONParametersTests = []struct {
|
||||
params []Argument
|
||||
headers, query, payload map[string]interface{}
|
||||
rheaders, rquery, rpayload map[string]interface{}
|
||||
ok bool
|
||||
}{
|
||||
{[]Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
|
||||
{[]Argument{Argument{"url", "a", "", false}}, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
|
||||
{[]Argument{Argument{"payload", "a", "", false}}, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
|
||||
{[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": `{}`}, nil, nil, map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
|
||||
// failures
|
||||
{[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
|
||||
{[]Argument{Argument{"header", "y", "", false}}, map[string]interface{}{"X": `{}`}, nil, nil, map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
|
||||
{[]Argument{Argument{"string", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
|
||||
}
|
||||
|
||||
func TestHookParseJSONParameters(t *testing.T) {
|
||||
for _, tt := range hookParseJSONParametersTests {
|
||||
h := &Hook{JSONStringParameters: tt.params}
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
}
|
||||
err := h.ParseJSONParameters(r)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(tt.headers, tt.rheaders) {
|
||||
t.Errorf("failed to parse %v:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.params, tt.rheaders, tt.ok, tt.headers, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hookExtractCommandArgumentsTests = []struct {
|
||||
exec string
|
||||
args []Argument
|
||||
headers, query, payload map[string]interface{}
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
{"test", []Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
|
||||
// failures
|
||||
{"fail", []Argument{Argument{"payload", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArguments(t *testing.T) {
|
||||
for _, tt := range hookExtractCommandArgumentsTests {
|
||||
h := &Hook{ExecuteCommand: tt.exec, PassArgumentsToCommand: tt.args}
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
}
|
||||
value, err := h.ExtractCommandArguments(r)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
||||
t.Errorf("failed to extract args {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here we test the extraction of env variables when the user defined a hook
|
||||
// with the "pass-environment-to-command" directive
|
||||
// we test both cases where the name of the data is used as the name of the
|
||||
// env key & the case where the hook definition sets the env var name to a
|
||||
// fixed value using the envname construct like so::
|
||||
// [
|
||||
// {
|
||||
// "id": "push",
|
||||
// "execute-command": "bb2mm",
|
||||
// "command-working-directory": "/tmp",
|
||||
// "pass-environment-to-command":
|
||||
// [
|
||||
// {
|
||||
// "source": "entire-payload",
|
||||
// "envname": "PAYLOAD"
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
var hookExtractCommandArgumentsForEnvTests = []struct {
|
||||
exec string
|
||||
args []Argument
|
||||
headers, query, payload map[string]interface{}
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
// successes
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", "", false}},
|
||||
map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"HOOK_a=z"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", "MYKEY", false}},
|
||||
map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"MYKEY=z"},
|
||||
true,
|
||||
},
|
||||
// failures
|
||||
{
|
||||
"fail",
|
||||
[]Argument{Argument{"payload", "a", "", false}},
|
||||
map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArgumentsForEnv(t *testing.T) {
|
||||
for _, tt := range hookExtractCommandArgumentsForEnvTests {
|
||||
h := &Hook{ExecuteCommand: tt.exec, PassEnvironmentToCommand: tt.args}
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
}
|
||||
value, err := h.ExtractCommandArgumentsForEnv(r)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
||||
t.Errorf("failed to extract args for env {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hooksLoadFromFileTests = []struct {
|
||||
path string
|
||||
asTemplate bool
|
||||
ok bool
|
||||
}{
|
||||
{"../../hooks.json.example", false, true},
|
||||
{"../../hooks.yaml.example", false, true},
|
||||
{"../../hooks.json.tmpl.example", true, true},
|
||||
{"../../hooks.yaml.tmpl.example", true, true},
|
||||
{"", false, true},
|
||||
// failures
|
||||
{"missing.json", false, false},
|
||||
}
|
||||
|
||||
func TestHooksLoadFromFile(t *testing.T) {
|
||||
secret := `foo"123`
|
||||
os.Setenv("XXXTEST_SECRET", secret)
|
||||
|
||||
for _, tt := range hooksLoadFromFileTests {
|
||||
h := &Hooks{}
|
||||
err := h.LoadFromFile(tt.path, tt.asTemplate)
|
||||
if (err == nil) != tt.ok {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHooksTemplateLoadFromFile(t *testing.T) {
|
||||
secret := `foo"123`
|
||||
os.Setenv("XXXTEST_SECRET", secret)
|
||||
|
||||
for _, tt := range hooksLoadFromFileTests {
|
||||
if !tt.asTemplate {
|
||||
continue
|
||||
}
|
||||
|
||||
h := &Hooks{}
|
||||
err := h.LoadFromFile(tt.path, tt.asTemplate)
|
||||
if (err == nil) != tt.ok {
|
||||
t.Errorf(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
s := (*h.Match("webhook").TriggerRule.And)[0].Match.Secret
|
||||
if s != secret {
|
||||
t.Errorf("Expected secret of %q, got %q", secret, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hooksMatchTests = []struct {
|
||||
id string
|
||||
hooks Hooks
|
||||
value *Hook
|
||||
}{
|
||||
{"a", Hooks{Hook{ID: "a"}}, &Hook{ID: "a"}},
|
||||
{"X", Hooks{Hook{ID: "a"}}, new(Hook)},
|
||||
}
|
||||
|
||||
func TestHooksMatch(t *testing.T) {
|
||||
for _, tt := range hooksMatchTests {
|
||||
value := tt.hooks.Match(tt.id)
|
||||
if reflect.DeepEqual(reflect.ValueOf(value), reflect.ValueOf(tt.value)) {
|
||||
t.Errorf("failed to match %q:\nexpected %#v,\ngot %#v", tt.id, tt.value, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var matchRuleTests = []struct {
|
||||
typ, regex, secret, value, ipRange string
|
||||
param Argument
|
||||
headers, query, payload map[string]interface{}
|
||||
body []byte
|
||||
remoteAddr string
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"value", "", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
||||
{"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
||||
{"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
{"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
// failures
|
||||
{"value", "", "", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"regex", "^X", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"value", "", "2", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, true}, // reference invalid header
|
||||
// errors
|
||||
{"regex", "*", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
|
||||
{"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hmac-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hash-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
// IP whitelisting, valid cases
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1", Argument{}, nil, nil, nil, []byte{}, "192.168.0.1:9000", true, false}, // valid IPv4, no range
|
||||
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, with range
|
||||
{"ip-whitelist", "", "", "", "::1", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, no range
|
||||
// IP whitelisting, invalid cases
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, no range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.a:9000", false, true}, // invalid IPv4 address
|
||||
{"ip-whitelist", "", "", "", "::1/a", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, with range
|
||||
{"ip-whitelist", "", "", "", "::z", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, no range
|
||||
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::z]:9000", false, true}, // invalid IPv6 address
|
||||
}
|
||||
|
||||
func TestMatchRule(t *testing.T) {
|
||||
for i, tt := range matchRuleTests {
|
||||
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param, tt.ipRange}
|
||||
req := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
Body: tt.body,
|
||||
RawRequest: &http.Request{
|
||||
RemoteAddr: tt.remoteAddr,
|
||||
},
|
||||
}
|
||||
ok, err := r.Evaluate(req)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var andRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule AndRule
|
||||
headers, query, payload map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
"(a=z, b=y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
map[string]interface{}{"A": "z", "B": "y"}, nil, nil,
|
||||
[]byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=z, b=Y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
map[string]interface{}{"A": "z", "B": "Y"}, nil, nil,
|
||||
[]byte{},
|
||||
false, false,
|
||||
},
|
||||
// Complex test to cover Rules.Evaluate
|
||||
{
|
||||
"(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{
|
||||
And: &AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", "", false}, ""}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Or: &OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", "", false}, ""}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Not: &NotRule{
|
||||
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", "", false}, ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil,
|
||||
[]byte{},
|
||||
true, false,
|
||||
},
|
||||
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
|
||||
// failures
|
||||
{
|
||||
"invalid rule",
|
||||
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}},
|
||||
map[string]interface{}{"Y": "z"}, nil, nil, nil,
|
||||
false, true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestAndRule(t *testing.T) {
|
||||
for _, tt := range andRuleTests {
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
Body: tt.body,
|
||||
}
|
||||
ok, err := tt.rule.Evaluate(r)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var orRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule OrRule
|
||||
headers, query, payload map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
"(a=z, b=X): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
map[string]interface{}{"A": "z", "B": "X"}, nil, nil,
|
||||
[]byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=X, b=y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
map[string]interface{}{"A": "X", "B": "y"}, nil, nil,
|
||||
[]byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=Z, b=Y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil,
|
||||
[]byte{},
|
||||
false, false,
|
||||
},
|
||||
// failures
|
||||
{
|
||||
"missing parameter node",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
},
|
||||
map[string]interface{}{"Y": "Z"}, nil, nil,
|
||||
[]byte{},
|
||||
false, false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestOrRule(t *testing.T) {
|
||||
for _, tt := range orRuleTests {
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
Body: tt.body,
|
||||
}
|
||||
ok, err := tt.rule.Evaluate(r)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("%#v:\nexpected ok: %#v, err: %v\ngot ok: %#v err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var notRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule NotRule
|
||||
headers, query, payload map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||
}
|
||||
|
||||
func TestNotRule(t *testing.T) {
|
||||
for _, tt := range notRuleTests {
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
Body: tt.body,
|
||||
}
|
||||
ok, err := tt.rule.Evaluate(r)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.rule, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
a, b string
|
||||
ok bool
|
||||
}{
|
||||
{"abcd", "abcd", true},
|
||||
{"zyxw", "abcd", false},
|
||||
} {
|
||||
if ok := compare(tt.a, tt.b); ok != tt.ok {
|
||||
t.Errorf("compare failed for %q and %q: got %v\n", tt.a, tt.b, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package hook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"unicode"
|
||||
|
||||
"github.com/clbanning/mxj"
|
||||
)
|
||||
|
||||
// Request represents a webhook request.
|
||||
type Request struct {
|
||||
// The request ID set by the RequestID middleware.
|
||||
ID string
|
||||
|
||||
// The Content-Type of the request.
|
||||
ContentType string
|
||||
|
||||
// The raw request body.
|
||||
Body []byte
|
||||
|
||||
// Headers is a map of the parsed headers.
|
||||
Headers map[string]interface{}
|
||||
|
||||
// Query is a map of the parsed URL query values.
|
||||
Query map[string]interface{}
|
||||
|
||||
// Payload is a map of the parsed payload.
|
||||
Payload map[string]interface{}
|
||||
|
||||
// The underlying HTTP request.
|
||||
RawRequest *http.Request
|
||||
|
||||
// Treat signature errors as simple validate failures.
|
||||
AllowSignatureErrors bool
|
||||
}
|
||||
|
||||
func (r *Request) ParseJSONPayload() error {
|
||||
decoder := json.NewDecoder(bytes.NewReader(r.Body))
|
||||
decoder.UseNumber()
|
||||
|
||||
var firstChar byte
|
||||
for i := 0; i < len(r.Body); i++ {
|
||||
if unicode.IsSpace(rune(r.Body[i])) {
|
||||
continue
|
||||
}
|
||||
firstChar = r.Body[i]
|
||||
break
|
||||
}
|
||||
|
||||
if firstChar == byte('[') {
|
||||
var arrayPayload interface{}
|
||||
err := decoder.Decode(&arrayPayload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing JSON array payload %+v", err)
|
||||
}
|
||||
|
||||
r.Payload = make(map[string]interface{}, 1)
|
||||
r.Payload["root"] = arrayPayload
|
||||
} else {
|
||||
err := decoder.Decode(&r.Payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing JSON payload %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Request) ParseHeaders(headers map[string][]string) {
|
||||
r.Headers = make(map[string]interface{}, len(headers))
|
||||
|
||||
for k, v := range headers {
|
||||
if len(v) > 0 {
|
||||
r.Headers[k] = v[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Request) ParseQuery(query map[string][]string) {
|
||||
r.Query = make(map[string]interface{}, len(query))
|
||||
|
||||
for k, v := range query {
|
||||
if len(v) > 0 {
|
||||
r.Query[k] = v[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Request) ParseFormPayload() error {
|
||||
fd, err := url.ParseQuery(string(r.Body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing form payload %+v", err)
|
||||
}
|
||||
|
||||
r.Payload = make(map[string]interface{}, len(fd))
|
||||
|
||||
for k, v := range fd {
|
||||
if len(v) > 0 {
|
||||
r.Payload[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Request) ParseXMLPayload() error {
|
||||
var err error
|
||||
|
||||
r.Payload, err = mxj.NewMapXmlReader(bytes.NewReader(r.Body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing XML payload: %+v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package middleware
|
||||
|
||||
// Derived from from the Goa project, MIT Licensed
|
||||
// https://github.com/goadesign/goa/blob/v3/http/middleware/debug.go
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// responseDupper tees the response to a buffer and a response writer.
|
||||
type responseDupper struct {
|
||||
http.ResponseWriter
|
||||
Buffer *bytes.Buffer
|
||||
Status int
|
||||
}
|
||||
|
||||
// Dumper returns a debug middleware which prints detailed information about
|
||||
// incoming requests and outgoing responses including all headers, parameters
|
||||
// and bodies.
|
||||
func Dumper(w io.Writer) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
buf := &bytes.Buffer{}
|
||||
// Request ID
|
||||
rid := r.Context().Value(RequestIDKey)
|
||||
|
||||
// Dump request
|
||||
|
||||
bd, err := httputil.DumpRequest(r, true)
|
||||
if err != nil {
|
||||
buf.WriteString(fmt.Sprintf("[%s] Error dumping request for debugging: %s\n", rid, err))
|
||||
}
|
||||
|
||||
sc := bufio.NewScanner(bytes.NewBuffer(bd))
|
||||
sc.Split(bufio.ScanLines)
|
||||
for sc.Scan() {
|
||||
buf.WriteString(fmt.Sprintf("> [%s] ", rid))
|
||||
buf.WriteString(sc.Text() + "\n")
|
||||
}
|
||||
|
||||
w.Write(buf.Bytes())
|
||||
buf.Reset()
|
||||
|
||||
// Dump Response
|
||||
|
||||
dupper := &responseDupper{ResponseWriter: rw, Buffer: &bytes.Buffer{}}
|
||||
h.ServeHTTP(dupper, r)
|
||||
|
||||
// Response Status
|
||||
buf.WriteString(fmt.Sprintf("< [%s] %d %s\n", rid, dupper.Status, http.StatusText(dupper.Status)))
|
||||
|
||||
// Response Headers
|
||||
keys := make([]string, len(dupper.Header()))
|
||||
i := 0
|
||||
for k := range dupper.Header() {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
buf.WriteString(fmt.Sprintf("< [%s] %s: %s\n", rid, k, strings.Join(dupper.Header()[k], ", ")))
|
||||
}
|
||||
|
||||
// Response Body
|
||||
if dupper.Buffer.Len() > 0 {
|
||||
buf.WriteString(fmt.Sprintf("< [%s]\n", rid))
|
||||
sc = bufio.NewScanner(dupper.Buffer)
|
||||
sc.Split(bufio.ScanLines)
|
||||
for sc.Scan() {
|
||||
buf.WriteString(fmt.Sprintf("< [%s] ", rid))
|
||||
buf.WriteString(sc.Text() + "\n")
|
||||
}
|
||||
}
|
||||
w.Write(buf.Bytes())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes the data to the buffer and connection as part of an HTTP reply.
|
||||
func (r *responseDupper) Write(b []byte) (int, error) {
|
||||
r.Buffer.Write(b)
|
||||
return r.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// WriteHeader records the status and sends an HTTP response header with status code.
|
||||
func (r *responseDupper) WriteHeader(s int) {
|
||||
r.Status = s
|
||||
r.ResponseWriter.WriteHeader(s)
|
||||
}
|
||||
|
||||
// Hijack supports the http.Hijacker interface.
|
||||
func (r *responseDupper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hijacker, ok := r.ResponseWriter.(http.Hijacker); ok {
|
||||
return hijacker.Hijack()
|
||||
}
|
||||
return nil, nil, fmt.Errorf("dumper middleware: inner ResponseWriter cannot be hijacked: %T", r.ResponseWriter)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
)
|
||||
|
||||
// Logger is a middleware that logs useful data about each HTTP request.
|
||||
type Logger struct {
|
||||
Logger middleware.LoggerInterface
|
||||
}
|
||||
|
||||
// NewLogger creates a new RequestLogger Handler.
|
||||
func NewLogger() func(next http.Handler) http.Handler {
|
||||
return middleware.RequestLogger(&Logger{})
|
||||
}
|
||||
|
||||
// NewLogEntry creates a new LogEntry for the request.
|
||||
func (l *Logger) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
e := &LogEntry{
|
||||
req: r,
|
||||
buf: &bytes.Buffer{},
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// LogEntry represents an individual log entry.
|
||||
type LogEntry struct {
|
||||
*Logger
|
||||
req *http.Request
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
// Write constructs and writes the final log entry.
|
||||
func (l *LogEntry) Write(status, totalBytes int, elapsed time.Duration) {
|
||||
rid := GetReqID(l.req.Context())
|
||||
if rid != "" {
|
||||
fmt.Fprintf(l.buf, "[%s] ", rid)
|
||||
}
|
||||
|
||||
fmt.Fprintf(l.buf, "%03d | %s | %s | ", status, humanize.IBytes(uint64(totalBytes)), elapsed)
|
||||
l.buf.WriteString(l.req.Host + " | " + l.req.Method + " " + l.req.RequestURI)
|
||||
log.Print(l.buf.String())
|
||||
}
|
||||
|
||||
// Panic prints the call stack for a panic.
|
||||
func (l *LogEntry) Panic(v interface{}, stack []byte) {
|
||||
e := l.NewLogEntry(l.req).(*LogEntry)
|
||||
fmt.Fprintf(e.buf, "panic: %#v", v)
|
||||
log.Print(e.buf.String())
|
||||
log.Print(string(stack))
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package middleware
|
||||
|
||||
// Derived from Goa project, MIT Licensed
|
||||
// https://github.com/goadesign/goa/blob/v3/http/middleware/requestid.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
// Key to use when setting the request ID.
|
||||
type ctxKeyRequestID int
|
||||
|
||||
// RequestIDKey is the key that holds the unique request ID in a request context.
|
||||
const RequestIDKey ctxKeyRequestID = 0
|
||||
|
||||
// RequestID is a middleware that injects a request ID into the context of each
|
||||
// request.
|
||||
func RequestID(options ...RequestIDOption) func(http.Handler) http.Handler {
|
||||
o := newRequestIDOptions(options...)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var id string
|
||||
|
||||
if o.UseRequestID() {
|
||||
id = r.Header.Get("X-Request-Id")
|
||||
if o.requestIDLimit > 0 && len(id) > o.requestIDLimit {
|
||||
id = id[:o.requestIDLimit]
|
||||
}
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
id = uuid.Must(uuid.NewV4()).String()[:6]
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, RequestIDKey, id)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetReqID returns a request ID from the given context if one is present.
|
||||
// Returns the empty string if a request ID cannot be found.
|
||||
func GetReqID(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
|
||||
return reqID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func UseXRequestIDHeaderOption(f bool) RequestIDOption {
|
||||
return func(o *RequestIDOptions) *RequestIDOptions {
|
||||
o.useXRequestID = f
|
||||
return o
|
||||
}
|
||||
}
|
||||
|
||||
func XRequestIDLimitOption(limit int) RequestIDOption {
|
||||
return func(o *RequestIDOptions) *RequestIDOptions {
|
||||
o.requestIDLimit = limit
|
||||
return o
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
RequestIDOption func(*RequestIDOptions) *RequestIDOptions
|
||||
|
||||
RequestIDOptions struct {
|
||||
// useXRequestID enabled the use of the X-Request-Id request header as
|
||||
// the request ID.
|
||||
useXRequestID bool
|
||||
|
||||
// requestIDLimit is the maximum length of the X-Request-Id header
|
||||
// allowed. Values longer than this value are truncated. Zero value
|
||||
// means no limit.
|
||||
requestIDLimit int
|
||||
}
|
||||
)
|
||||
|
||||
func newRequestIDOptions(options ...RequestIDOption) *RequestIDOptions {
|
||||
o := new(RequestIDOptions)
|
||||
for _, opt := range options {
|
||||
o = opt(o)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *RequestIDOptions) UseRequestID() bool {
|
||||
return o.useXRequestID
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
Package pidfile is derived from github.com/moby/moby/pkg/pidfile.
|
||||
|
||||
Moby is licensed under the Apache License, Version 2.0.
|
||||
Copyright 2012-2017 Docker, Inc.
|
||||
@@ -0,0 +1,11 @@
|
||||
// +build !windows
|
||||
|
||||
package pidfile
|
||||
|
||||
import "os"
|
||||
|
||||
// MkdirAll creates a directory named path along with any necessary parents,
|
||||
// with permission specified by attribute perm for all dir created.
|
||||
func MkdirAll(path string, perm os.FileMode) error {
|
||||
return os.MkdirAll(path, perm)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// +build windows
|
||||
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// MkdirAll implementation that is volume path aware for Windows. It can be used
|
||||
// as a drop-in replacement for os.MkdirAll()
|
||||
func MkdirAll(path string, _ os.FileMode) error {
|
||||
return mkdirall(path, false, "")
|
||||
}
|
||||
|
||||
// mkdirall is a custom version of os.MkdirAll modified for use on Windows
|
||||
// so that it is both volume path aware, and can create a directory with
|
||||
// a DACL.
|
||||
func mkdirall(path string, applyACL bool, sddl string) error {
|
||||
if re := regexp.MustCompile(`^\\\\\?\\Volume{[a-z0-9-]+}$`); re.MatchString(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The rest of this method is largely copied from os.MkdirAll and should be kept
|
||||
// as-is to ensure compatibility.
|
||||
|
||||
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
|
||||
dir, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return &os.PathError{
|
||||
Op: "mkdir",
|
||||
Path: path,
|
||||
Err: syscall.ENOTDIR,
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: make sure parent exists and then call Mkdir for path.
|
||||
i := len(path)
|
||||
for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator.
|
||||
i--
|
||||
}
|
||||
|
||||
j := i
|
||||
for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element.
|
||||
j--
|
||||
}
|
||||
|
||||
if j > 1 {
|
||||
// Create parent
|
||||
err = mkdirall(path[0:j-1], false, sddl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parent now exists; invoke os.Mkdir or mkdirWithACL and use its result.
|
||||
if applyACL {
|
||||
err = mkdirWithACL(path, sddl)
|
||||
} else {
|
||||
err = os.Mkdir(path, 0)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Handle arguments like "foo/." by
|
||||
// double-checking that directory doesn't exist.
|
||||
dir, err1 := os.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mkdirWithACL creates a new directory. If there is an error, it will be of
|
||||
// type *PathError. .
|
||||
//
|
||||
// This is a modified and combined version of os.Mkdir and windows.Mkdir
|
||||
// in golang to cater for creating a directory am ACL permitting full
|
||||
// access, with inheritance, to any subfolder/file for Built-in Administrators
|
||||
// and Local System.
|
||||
func mkdirWithACL(name string, sddl string) error {
|
||||
sa := windows.SecurityAttributes{Length: 0}
|
||||
sd, err := windows.SecurityDescriptorFromString(sddl)
|
||||
if err != nil {
|
||||
return &os.PathError{Op: "mkdir", Path: name, Err: err}
|
||||
}
|
||||
sa.Length = uint32(unsafe.Sizeof(sa))
|
||||
sa.InheritHandle = 1
|
||||
sa.SecurityDescriptor = sd
|
||||
|
||||
namep, err := windows.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return &os.PathError{Op: "mkdir", Path: name, Err: err}
|
||||
}
|
||||
|
||||
e := windows.CreateDirectory(namep, &sa)
|
||||
if e != nil {
|
||||
return &os.PathError{Op: "mkdir", Path: name, Err: e}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Package pidfile provides structure and helper functions to create and remove
|
||||
// PID file. A PID file is usually a file used to store the process ID of a
|
||||
// running process.
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PIDFile is a file used to store the process ID of a running process.
|
||||
type PIDFile struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func checkPIDFileAlreadyExists(path string) error {
|
||||
if pidByte, err := ioutil.ReadFile(path); err == nil {
|
||||
pidString := strings.TrimSpace(string(pidByte))
|
||||
if pid, err := strconv.Atoi(pidString); err == nil {
|
||||
if processExists(pid) {
|
||||
return fmt.Errorf("pid file found, ensure webhook is not running or delete %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// New creates a PIDfile using the specified path.
|
||||
func New(path string) (*PIDFile, error) {
|
||||
if err := checkPIDFileAlreadyExists(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Note MkdirAll returns nil if a directory already exists
|
||||
if err := MkdirAll(filepath.Dir(path), os.FileMode(0o755)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(path, []byte(fmt.Sprintf("%d", os.Getpid())), 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PIDFile{path: path}, nil
|
||||
}
|
||||
|
||||
// Remove removes the PIDFile.
|
||||
func (file PIDFile) Remove() error {
|
||||
return os.Remove(file.path)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// +build darwin
|
||||
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func processExists(pid int) bool {
|
||||
// OS X does not have a proc filesystem.
|
||||
// Use kill -0 pid to judge if the process exists.
|
||||
err := unix.Kill(pid, 0)
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewAndRemove(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "test-pidfile")
|
||||
if err != nil {
|
||||
t.Fatal("Could not create test directory")
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, "testfile")
|
||||
file, err := New(path)
|
||||
if err != nil {
|
||||
t.Fatal("Could not create test file", err)
|
||||
}
|
||||
|
||||
_, err = New(path)
|
||||
if err == nil {
|
||||
t.Fatal("Test file creation not blocked")
|
||||
}
|
||||
|
||||
if err := file.Remove(); err != nil {
|
||||
t.Fatal("Could not delete created test file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveInvalidPath(t *testing.T) {
|
||||
file := PIDFile{path: filepath.Join("foo", "bar")}
|
||||
|
||||
if err := file.Remove(); err == nil {
|
||||
t.Fatal("Non-existing file doesn't give an error on delete")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// +build !windows,!darwin
|
||||
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func processExists(pid int) bool {
|
||||
if _, err := os.Stat(filepath.Join("/proc", strconv.Itoa(pid))); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
processQueryLimitedInformation = 0x1000
|
||||
|
||||
stillActive = 259
|
||||
)
|
||||
|
||||
func processExists(pid int) bool {
|
||||
h, err := windows.OpenProcess(processQueryLimitedInformation, false, uint32(pid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var c uint32
|
||||
err = windows.GetExitCodeProcess(h, &c)
|
||||
windows.Close(h)
|
||||
if err != nil {
|
||||
return c == stillActive
|
||||
}
|
||||
return true
|
||||
}
|
||||
+21
-3
@@ -14,6 +14,9 @@ func setupSignals() {
|
||||
|
||||
signals = make(chan os.Signal, 1)
|
||||
signal.Notify(signals, syscall.SIGUSR1)
|
||||
signal.Notify(signals, syscall.SIGHUP)
|
||||
signal.Notify(signals, syscall.SIGTERM)
|
||||
signal.Notify(signals, os.Interrupt)
|
||||
|
||||
go watchForSignals()
|
||||
}
|
||||
@@ -23,11 +26,26 @@ func watchForSignals() {
|
||||
|
||||
for {
|
||||
sig := <-signals
|
||||
if sig == syscall.SIGUSR1 {
|
||||
switch sig {
|
||||
case syscall.SIGUSR1:
|
||||
log.Println("caught USR1 signal")
|
||||
|
||||
reloadAllHooks()
|
||||
} else {
|
||||
|
||||
case syscall.SIGHUP:
|
||||
log.Println("caught HUP signal")
|
||||
reloadAllHooks()
|
||||
|
||||
case os.Interrupt, syscall.SIGTERM:
|
||||
log.Printf("caught %s signal; exiting\n", sig)
|
||||
if pidFile != nil {
|
||||
err := pidFile.Remove()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
|
||||
default:
|
||||
log.Printf("caught unhandled signal %+v\n", sig)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,8 +5,8 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
+375
-1
@@ -3,6 +3,7 @@
|
||||
"id": "github",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"http-methods": ["Post "],
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule-mismatch-http-response-code": 400,
|
||||
"pass-environment-to-command":
|
||||
@@ -30,7 +31,7 @@
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
@@ -54,6 +55,149 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "github-multi-sig",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"http-methods": ["Post "],
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule-mismatch-http-response-code": 400,
|
||||
"trigger-signature-soft-failures": true,
|
||||
"pass-environment-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.timestamp"
|
||||
}
|
||||
],
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.author.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
"or":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecretFAIL",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "github-multi-sig-fail",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"http-methods": ["Post "],
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule-mismatch-http-response-code": 400,
|
||||
"pass-environment-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.timestamp"
|
||||
}
|
||||
],
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.author.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
"or":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecretFAIL",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bitbucket",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
@@ -137,6 +281,103 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "xml",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"response-message": "success",
|
||||
"trigger-rule": {
|
||||
"and": [
|
||||
{
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "app.users.user.0.-name"
|
||||
},
|
||||
"value": "Jeff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "app.messages.message.#text"
|
||||
},
|
||||
"value": "Hello!!"
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "txt-raw",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"include-command-output-in-response": true,
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "raw-request-body"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sendgrid",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"response-message": "success",
|
||||
"trigger-rule": {
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "root.0.event"
|
||||
},
|
||||
"value": "processed"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sendgrid/dir",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"response-message": "success",
|
||||
"trigger-rule": {
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "root.0.event"
|
||||
},
|
||||
"value": "it worked!"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "plex",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"response-message": "success",
|
||||
"parse-parameters-as-json": [
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "payload"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "payload.event"
|
||||
},
|
||||
"value": "media.play"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "capture-command-output-on-success-not-by-default",
|
||||
"pass-arguments-to-command": [
|
||||
@@ -180,5 +421,138 @@
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"include-command-output-in-response": true,
|
||||
"include-command-output-in-response-on-error": true
|
||||
},
|
||||
{
|
||||
"id": "request-source",
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "request",
|
||||
"name": "method"
|
||||
},
|
||||
{
|
||||
"source": "request",
|
||||
"name": "remote-addr"
|
||||
}
|
||||
],
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"include-command-output-in-response": true
|
||||
},
|
||||
{
|
||||
"id": "static-params-ok",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"response-message": "success",
|
||||
"include-command-output-in-response": true,
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "passed"
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "warn-on-space",
|
||||
"execute-command": "{{ .Hookecho }} foo",
|
||||
"response-message": "success",
|
||||
"include-command-output-in-response": true,
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "passed"
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "issue-471",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"response-message": "success",
|
||||
"trigger-rule":
|
||||
{
|
||||
"or":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "foo"
|
||||
},
|
||||
"type": "value",
|
||||
"value": "bar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "exists"
|
||||
},
|
||||
"type": "value",
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "issue-471-and",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"response-message": "success",
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "foo"
|
||||
},
|
||||
"type": "value",
|
||||
"value": "bar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "exists"
|
||||
},
|
||||
"type": "value",
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "empty-payload-signature",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+224
-7
@@ -1,11 +1,14 @@
|
||||
- trigger-rule:
|
||||
- id: github
|
||||
http-methods:
|
||||
- "Post "
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
secret: mysecret
|
||||
type: payload-hash-sha1
|
||||
type: payload-hmac-sha1
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
@@ -23,9 +26,85 @@
|
||||
pass-environment-to-command:
|
||||
- source: payload
|
||||
name: head_commit.timestamp
|
||||
id: github
|
||||
command-working-directory: /
|
||||
- trigger-rule:
|
||||
|
||||
- id: github-multi-sig
|
||||
http-methods:
|
||||
- "Post "
|
||||
trigger-rule:
|
||||
and:
|
||||
- or:
|
||||
- match:
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
secret: mysecretFAIL
|
||||
type: payload-hmac-sha1
|
||||
- match:
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
secret: mysecret
|
||||
type: payload-hmac-sha1
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: ref
|
||||
type: value
|
||||
value: refs/heads/master
|
||||
include-command-output-in-response: true
|
||||
trigger-rule-mismatch-http-response-code: 400
|
||||
trigger-signature-soft-failures: true
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
pass-arguments-to-command:
|
||||
- source: payload
|
||||
name: head_commit.id
|
||||
- source: payload
|
||||
name: head_commit.author.email
|
||||
pass-environment-to-command:
|
||||
- source: payload
|
||||
name: head_commit.timestamp
|
||||
command-working-directory: /
|
||||
|
||||
- id: github-multi-sig-fail
|
||||
http-methods:
|
||||
- "Post "
|
||||
trigger-rule:
|
||||
and:
|
||||
- or:
|
||||
- match:
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
secret: mysecretFAIL
|
||||
type: payload-hmac-sha1
|
||||
- match:
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
secret: mysecret
|
||||
type: payload-hmac-sha1
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: ref
|
||||
type: value
|
||||
value: refs/heads/master
|
||||
include-command-output-in-response: true
|
||||
trigger-rule-mismatch-http-response-code: 400
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
pass-arguments-to-command:
|
||||
- source: payload
|
||||
name: head_commit.id
|
||||
- source: payload
|
||||
name: head_commit.author.email
|
||||
pass-environment-to-command:
|
||||
- source: payload
|
||||
name: head_commit.timestamp
|
||||
command-working-directory: /
|
||||
|
||||
- id: bitbucket
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
parameter:
|
||||
@@ -52,9 +131,10 @@
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
include-command-output-in-response: false
|
||||
id: bitbucket
|
||||
command-working-directory: /
|
||||
- trigger-rule:
|
||||
|
||||
- id: gitlab
|
||||
trigger-rule:
|
||||
match:
|
||||
parameter:
|
||||
source: payload
|
||||
@@ -71,25 +151,93 @@
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
include-command-output-in-response: true
|
||||
id: gitlab
|
||||
command-working-directory: /
|
||||
|
||||
- id: xml
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
command-working-directory: /
|
||||
response-message: success
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
type: value
|
||||
parameter:
|
||||
source: payload
|
||||
name: app.users.user.0.-name
|
||||
value: Jeff
|
||||
- match:
|
||||
type: value
|
||||
parameter:
|
||||
source: payload
|
||||
name: "app.messages.message.#text"
|
||||
value: "Hello!!"
|
||||
|
||||
- id: txt-raw
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
command-working-directory: /
|
||||
include-command-output-in-response: true
|
||||
pass-arguments-to-command:
|
||||
- source: raw-request-body
|
||||
|
||||
- id: sendgrid
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
command-working-directory: /
|
||||
response-message: success
|
||||
trigger-rule:
|
||||
match:
|
||||
type: value
|
||||
parameter:
|
||||
source: payload
|
||||
name: root.0.event
|
||||
value: processed
|
||||
|
||||
- id: sendgrid/dir
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
command-working-directory: /
|
||||
response-message: success
|
||||
trigger-rule:
|
||||
match:
|
||||
type: value
|
||||
parameter:
|
||||
source: payload
|
||||
name: root.0.event
|
||||
value: it worked!
|
||||
|
||||
- id: plex
|
||||
trigger-rule:
|
||||
match:
|
||||
type: value
|
||||
parameter:
|
||||
source: payload
|
||||
name: payload.event
|
||||
value: media.play
|
||||
parse-parameters-as-json:
|
||||
- source: payload
|
||||
name: payload
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
command-working-directory: /
|
||||
|
||||
- id: capture-command-output-on-success-not-by-default
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: exit=0
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
|
||||
- id: capture-command-output-on-success-yes-with-flag
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: exit=0
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
|
||||
- id: capture-command-output-on-error-not-by-default
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: exit=1
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
|
||||
- id: capture-command-output-on-error-yes-with-extra-flag
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
@@ -97,3 +245,72 @@
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
include-command-output-in-response-on-error: true
|
||||
|
||||
- id: request-source
|
||||
pass-arguments-to-command:
|
||||
- source: request
|
||||
name: method
|
||||
- source: request
|
||||
name: remote-addr
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
|
||||
- id: static-params-ok
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: passed
|
||||
|
||||
- id: warn-on-space
|
||||
execute-command: '{{ .Hookecho }} foo'
|
||||
include-command-output-in-response: true
|
||||
|
||||
- id: issue-471
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
trigger-rule:
|
||||
or:
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: foo
|
||||
type: value
|
||||
value: bar
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: exists
|
||||
type: value
|
||||
value: 1
|
||||
|
||||
- id: issue-471-and
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: foo
|
||||
type: value
|
||||
value: bar
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: exists
|
||||
type: value
|
||||
value: 1
|
||||
|
||||
- id: empty-payload-signature
|
||||
include-command-output-in-response: true
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
command-working-directory: /
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
secret: mysecret
|
||||
type: payload-hmac-sha1
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func writeTLSSupportedCipherStrings(w io.Writer, min uint16) error {
|
||||
for _, c := range tls.CipherSuites() {
|
||||
var found bool
|
||||
|
||||
for _, v := range c.SupportedVersions {
|
||||
if v >= min {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := w.Write([]byte(c.Name + "\n"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTLSMinVersion converts a version string into a TLS version ID.
|
||||
func getTLSMinVersion(v string) uint16 {
|
||||
switch v {
|
||||
case "1.0":
|
||||
return tls.VersionTLS10
|
||||
case "1.1":
|
||||
return tls.VersionTLS11
|
||||
case "1.2", "":
|
||||
return tls.VersionTLS12
|
||||
case "1.3":
|
||||
return tls.VersionTLS13
|
||||
default:
|
||||
log.Fatalln("error: unknown minimum TLS version:", v)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// getTLSCipherSuites converts a comma separated list of cipher suites into a
|
||||
// slice of TLS cipher suite IDs.
|
||||
func getTLSCipherSuites(v string) []uint16 {
|
||||
supported := tls.CipherSuites()
|
||||
|
||||
if v == "" {
|
||||
suites := make([]uint16, len(supported))
|
||||
|
||||
for _, cs := range supported {
|
||||
suites = append(suites, cs.ID)
|
||||
}
|
||||
|
||||
return suites
|
||||
}
|
||||
|
||||
var found bool
|
||||
txts := strings.Split(v, ",")
|
||||
suites := make([]uint16, len(txts))
|
||||
|
||||
for _, want := range txts {
|
||||
found = false
|
||||
|
||||
for _, cs := range supported {
|
||||
if want == cs.Name {
|
||||
suites = append(suites, cs.ID)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Fatalln("error: unknown TLS cipher suite:", want)
|
||||
}
|
||||
}
|
||||
|
||||
return suites
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.13.x
|
||||
Generated
Vendored
+2
-2
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014 Jeremy Saenz
|
||||
Copyright (c) 2019 Anton Medvedev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# Expr
|
||||
[](https://travis-ci.org/antonmedv/expr)
|
||||
[](https://goreportcard.com/report/github.com/antonmedv/expr)
|
||||
[](https://godoc.org/github.com/antonmedv/expr)
|
||||
|
||||
<img src="docs/images/logo-small.png" width="150" alt="expr logo" align="right">
|
||||
|
||||
**Expr** package provides an engine that can compile and evaluate expressions.
|
||||
An expression is a one-liner that returns a value (mostly, but not limited to, booleans).
|
||||
It is designed for simplicity, speed and safety.
|
||||
|
||||
The purpose of the package is to allow users to use expressions inside configuration for more complex logic.
|
||||
It is a perfect candidate for the foundation of a _business rule engine_.
|
||||
The idea is to let configure things in a dynamic way without recompile of a program:
|
||||
|
||||
```coffeescript
|
||||
# Get the special price if
|
||||
user.Group in ["good_customers", "collaborator"]
|
||||
|
||||
# Promote article to the homepage when
|
||||
len(article.Comments) > 100 and article.Category not in ["misc"]
|
||||
|
||||
# Send an alert when
|
||||
product.Stock < 15
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
* Seamless integration with Go (no need to redefine types)
|
||||
* Static typing ([example](https://godoc.org/github.com/antonmedv/expr#example-Env)).
|
||||
```go
|
||||
out, err := expr.Compile(`name + age`)
|
||||
// err: invalid operation + (mismatched types string and int)
|
||||
// | name + age
|
||||
// | .....^
|
||||
```
|
||||
* User-friendly error messages.
|
||||
* Reasonable set of basic operators.
|
||||
* Builtins `all`, `none`, `any`, `one`, `filter`, `map`.
|
||||
```coffeescript
|
||||
all(Tweets, {.Size <= 280})
|
||||
```
|
||||
* Fast ([benchmarks](https://github.com/antonmedv/golang-expression-evaluation-comparison#readme)): uses bytecode virtual machine and optimizing compiler.
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
go get github.com/antonmedv/expr
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
* See [Getting Started](docs/Getting-Started.md) page for developer documentation.
|
||||
* See [Language Definition](docs/Language-Definition.md) page to learn the syntax.
|
||||
|
||||
## Expr Code Editor
|
||||
|
||||
<a href="http://bit.ly/expr-code-editor">
|
||||
<img src="https://antonmedv.github.io/expr/ogimage.png" align="center" alt="Expr Code Editor" width="1200">
|
||||
</a>
|
||||
|
||||
Also, I have an embeddable code editor written in JavaScript which allows editing expressions with syntax highlighting and autocomplete based on your types declaration.
|
||||
|
||||
[Learn more →](https://antonmedv.github.io/expr/)
|
||||
|
||||
## Examples
|
||||
|
||||
[Play Online](https://play.golang.org/p/z7T8ytJ1T1d)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/antonmedv/expr"
|
||||
)
|
||||
|
||||
func main() {
|
||||
env := map[string]interface{}{
|
||||
"greet": "Hello, %v!",
|
||||
"names": []string{"world", "you"},
|
||||
"sprintf": fmt.Sprintf,
|
||||
}
|
||||
|
||||
code := `sprintf(greet, names[0])`
|
||||
|
||||
program, err := expr.Compile(code, expr.Env(env))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(output)
|
||||
}
|
||||
```
|
||||
|
||||
[Play Online](https://play.golang.org/p/4S4brsIvU4i)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/antonmedv/expr"
|
||||
)
|
||||
|
||||
type Tweet struct {
|
||||
Len int
|
||||
}
|
||||
|
||||
type Env struct {
|
||||
Tweets []Tweet
|
||||
}
|
||||
|
||||
func main() {
|
||||
code := `all(Tweets, {.Len <= 240})`
|
||||
|
||||
program, err := expr.Compile(code, expr.Env(Env{}))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
env := Env{
|
||||
Tweets: []Tweet{{42}, {98}, {69}},
|
||||
}
|
||||
output, err := expr.Run(program, env)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(output)
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
**Expr** consist of a few packages for parsing source code to AST, type checking AST, compiling to bytecode and VM for running bytecode program.
|
||||
|
||||
Also expr provides powerful tool [exe](cmd/exe) for debugging. It has interactive terminal debugger for our bytecode virtual machine.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/debug.gif" alt="debugger" width="605">
|
||||
</p>
|
||||
|
||||
|
||||
## Who is using Expr?
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<p align="center"><a href="https://aviasales.ru"><img alt="Aviasales Logo" height="100" src="https://cdn.worldvectorlogo.com/logos/aviasales-4.svg"></a></p>
|
||||
<p><a href="https://aviasales.ru">Aviasales</a> are actively using Expr for different parts of the search engine.</p>
|
||||
</td>
|
||||
<td>
|
||||
<p align="center"><a href="https://www.mysteryminds.com/en/"><img alt="Mystery Minds Logo" height="100" src="https://www.mysteryminds.com/wp-content/uploads/2018/07/MM_logo_colours.png"></a></p>
|
||||
<p><a href="https://www.mysteryminds.com/en/">Mystery Minds</a> uses Expr to allow easy yet powerful customization of its matching algorithm.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
[Add you company too](https://github.com/antonmedv/expr/edit/master/README.md)
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
|
||||
"github.com/antonmedv/expr/file"
|
||||
)
|
||||
|
||||
// Node represents items of abstract syntax tree.
|
||||
type Node interface {
|
||||
Location() file.Location
|
||||
SetLocation(file.Location)
|
||||
Type() reflect.Type
|
||||
SetType(reflect.Type)
|
||||
}
|
||||
|
||||
func Patch(node *Node, newNode Node) {
|
||||
newNode.SetType((*node).Type())
|
||||
newNode.SetLocation((*node).Location())
|
||||
*node = newNode
|
||||
}
|
||||
|
||||
type base struct {
|
||||
loc file.Location
|
||||
nodeType reflect.Type
|
||||
}
|
||||
|
||||
func (n *base) Location() file.Location {
|
||||
return n.loc
|
||||
}
|
||||
|
||||
func (n *base) SetLocation(loc file.Location) {
|
||||
n.loc = loc
|
||||
}
|
||||
|
||||
func (n *base) Type() reflect.Type {
|
||||
return n.nodeType
|
||||
}
|
||||
|
||||
func (n *base) SetType(t reflect.Type) {
|
||||
n.nodeType = t
|
||||
}
|
||||
|
||||
type NilNode struct {
|
||||
base
|
||||
}
|
||||
|
||||
type IdentifierNode struct {
|
||||
base
|
||||
Value string
|
||||
}
|
||||
|
||||
type IntegerNode struct {
|
||||
base
|
||||
Value int
|
||||
}
|
||||
|
||||
type FloatNode struct {
|
||||
base
|
||||
Value float64
|
||||
}
|
||||
|
||||
type BoolNode struct {
|
||||
base
|
||||
Value bool
|
||||
}
|
||||
|
||||
type StringNode struct {
|
||||
base
|
||||
Value string
|
||||
}
|
||||
|
||||
type ConstantNode struct {
|
||||
base
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
type UnaryNode struct {
|
||||
base
|
||||
Operator string
|
||||
Node Node
|
||||
}
|
||||
|
||||
type BinaryNode struct {
|
||||
base
|
||||
Operator string
|
||||
Left Node
|
||||
Right Node
|
||||
}
|
||||
|
||||
type MatchesNode struct {
|
||||
base
|
||||
Regexp *regexp.Regexp
|
||||
Left Node
|
||||
Right Node
|
||||
}
|
||||
|
||||
type PropertyNode struct {
|
||||
base
|
||||
Node Node
|
||||
Property string
|
||||
}
|
||||
|
||||
type IndexNode struct {
|
||||
base
|
||||
Node Node
|
||||
Index Node
|
||||
}
|
||||
|
||||
type SliceNode struct {
|
||||
base
|
||||
Node Node
|
||||
From Node
|
||||
To Node
|
||||
}
|
||||
|
||||
type MethodNode struct {
|
||||
base
|
||||
Node Node
|
||||
Method string
|
||||
Arguments []Node
|
||||
}
|
||||
|
||||
type FunctionNode struct {
|
||||
base
|
||||
Name string
|
||||
Arguments []Node
|
||||
Fast bool
|
||||
}
|
||||
|
||||
type BuiltinNode struct {
|
||||
base
|
||||
Name string
|
||||
Arguments []Node
|
||||
}
|
||||
|
||||
type ClosureNode struct {
|
||||
base
|
||||
Node Node
|
||||
}
|
||||
|
||||
type PointerNode struct {
|
||||
base
|
||||
}
|
||||
|
||||
type ConditionalNode struct {
|
||||
base
|
||||
Cond Node
|
||||
Exp1 Node
|
||||
Exp2 Node
|
||||
}
|
||||
|
||||
type ArrayNode struct {
|
||||
base
|
||||
Nodes []Node
|
||||
}
|
||||
|
||||
type MapNode struct {
|
||||
base
|
||||
Pairs []Node
|
||||
}
|
||||
|
||||
type PairNode struct {
|
||||
base
|
||||
Key Node
|
||||
Value Node
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func Dump(node Node) string {
|
||||
return dump(reflect.ValueOf(node), "")
|
||||
}
|
||||
|
||||
func dump(v reflect.Value, ident string) string {
|
||||
if !v.IsValid() {
|
||||
return "nil"
|
||||
}
|
||||
t := v.Type()
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
out := t.Name() + "{\n"
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
if isPrivate(f.Name) {
|
||||
continue
|
||||
}
|
||||
s := v.Field(i)
|
||||
out += fmt.Sprintf("%v%v: %v,\n", ident+"\t", f.Name, dump(s, ident+"\t"))
|
||||
}
|
||||
return out + ident + "}"
|
||||
case reflect.Slice:
|
||||
if v.Len() == 0 {
|
||||
return "[]"
|
||||
}
|
||||
out := "[\n"
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
s := v.Index(i)
|
||||
out += fmt.Sprintf("%v%v,", ident+"\t", dump(s, ident+"\t"))
|
||||
if i+1 < v.Len() {
|
||||
out += "\n"
|
||||
}
|
||||
}
|
||||
return out + "\n" + ident + "]"
|
||||
case reflect.Ptr:
|
||||
return dump(v.Elem(), ident)
|
||||
case reflect.Interface:
|
||||
return dump(reflect.ValueOf(v.Interface()), ident)
|
||||
|
||||
case reflect.String:
|
||||
return fmt.Sprintf("%q", v)
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
var isCapital = regexp.MustCompile("^[A-Z]")
|
||||
|
||||
func isPrivate(s string) bool {
|
||||
return !isCapital.Match([]byte(s))
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package ast
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Visitor interface {
|
||||
Enter(node *Node)
|
||||
Exit(node *Node)
|
||||
}
|
||||
|
||||
type walker struct {
|
||||
visitor Visitor
|
||||
}
|
||||
|
||||
func Walk(node *Node, visitor Visitor) {
|
||||
w := walker{
|
||||
visitor: visitor,
|
||||
}
|
||||
w.walk(node)
|
||||
}
|
||||
|
||||
func (w *walker) walk(node *Node) {
|
||||
w.visitor.Enter(node)
|
||||
|
||||
switch n := (*node).(type) {
|
||||
case *NilNode:
|
||||
w.visitor.Exit(node)
|
||||
case *IdentifierNode:
|
||||
w.visitor.Exit(node)
|
||||
case *IntegerNode:
|
||||
w.visitor.Exit(node)
|
||||
case *FloatNode:
|
||||
w.visitor.Exit(node)
|
||||
case *BoolNode:
|
||||
w.visitor.Exit(node)
|
||||
case *StringNode:
|
||||
w.visitor.Exit(node)
|
||||
case *ConstantNode:
|
||||
w.visitor.Exit(node)
|
||||
case *UnaryNode:
|
||||
w.walk(&n.Node)
|
||||
w.visitor.Exit(node)
|
||||
case *BinaryNode:
|
||||
w.walk(&n.Left)
|
||||
w.walk(&n.Right)
|
||||
w.visitor.Exit(node)
|
||||
case *MatchesNode:
|
||||
w.walk(&n.Left)
|
||||
w.walk(&n.Right)
|
||||
w.visitor.Exit(node)
|
||||
case *PropertyNode:
|
||||
w.walk(&n.Node)
|
||||
w.visitor.Exit(node)
|
||||
case *IndexNode:
|
||||
w.walk(&n.Node)
|
||||
w.walk(&n.Index)
|
||||
w.visitor.Exit(node)
|
||||
case *SliceNode:
|
||||
if n.From != nil {
|
||||
w.walk(&n.From)
|
||||
}
|
||||
if n.To != nil {
|
||||
w.walk(&n.To)
|
||||
}
|
||||
w.visitor.Exit(node)
|
||||
case *MethodNode:
|
||||
w.walk(&n.Node)
|
||||
for i := range n.Arguments {
|
||||
w.walk(&n.Arguments[i])
|
||||
}
|
||||
w.visitor.Exit(node)
|
||||
case *FunctionNode:
|
||||
for i := range n.Arguments {
|
||||
w.walk(&n.Arguments[i])
|
||||
}
|
||||
w.visitor.Exit(node)
|
||||
case *BuiltinNode:
|
||||
for i := range n.Arguments {
|
||||
w.walk(&n.Arguments[i])
|
||||
}
|
||||
w.visitor.Exit(node)
|
||||
case *ClosureNode:
|
||||
w.walk(&n.Node)
|
||||
w.visitor.Exit(node)
|
||||
case *PointerNode:
|
||||
w.visitor.Exit(node)
|
||||
case *ConditionalNode:
|
||||
w.walk(&n.Cond)
|
||||
w.walk(&n.Exp1)
|
||||
w.walk(&n.Exp2)
|
||||
w.visitor.Exit(node)
|
||||
case *ArrayNode:
|
||||
for i := range n.Nodes {
|
||||
w.walk(&n.Nodes[i])
|
||||
}
|
||||
w.visitor.Exit(node)
|
||||
case *MapNode:
|
||||
for i := range n.Pairs {
|
||||
w.walk(&n.Pairs[i])
|
||||
}
|
||||
w.visitor.Exit(node)
|
||||
case *PairNode:
|
||||
w.walk(&n.Key)
|
||||
w.walk(&n.Value)
|
||||
w.visitor.Exit(node)
|
||||
default:
|
||||
panic(fmt.Sprintf("undefined node type (%T)", node))
|
||||
}
|
||||
}
|
||||
+597
@@ -0,0 +1,597 @@
|
||||
package checker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/conf"
|
||||
"github.com/antonmedv/expr/file"
|
||||
"github.com/antonmedv/expr/parser"
|
||||
)
|
||||
|
||||
func Check(tree *parser.Tree, config *conf.Config) (reflect.Type, error) {
|
||||
v := &visitor{
|
||||
collections: make([]reflect.Type, 0),
|
||||
}
|
||||
if config != nil {
|
||||
v.types = config.Types
|
||||
v.operators = config.Operators
|
||||
v.expect = config.Expect
|
||||
v.strict = config.Strict
|
||||
v.defaultType = config.DefaultType
|
||||
}
|
||||
|
||||
t := v.visit(tree.Node)
|
||||
|
||||
if v.expect != reflect.Invalid {
|
||||
switch v.expect {
|
||||
case reflect.Int64, reflect.Float64:
|
||||
if !isNumber(t) {
|
||||
return nil, fmt.Errorf("expected %v, but got %v", v.expect, t)
|
||||
}
|
||||
default:
|
||||
if t.Kind() != v.expect {
|
||||
return nil, fmt.Errorf("expected %v, but got %v", v.expect, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v.err != nil {
|
||||
return t, v.err.Bind(tree.Source)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
type visitor struct {
|
||||
types conf.TypesTable
|
||||
operators conf.OperatorsTable
|
||||
expect reflect.Kind
|
||||
collections []reflect.Type
|
||||
strict bool
|
||||
defaultType reflect.Type
|
||||
err *file.Error
|
||||
}
|
||||
|
||||
func (v *visitor) visit(node ast.Node) reflect.Type {
|
||||
var t reflect.Type
|
||||
switch n := node.(type) {
|
||||
case *ast.NilNode:
|
||||
t = v.NilNode(n)
|
||||
case *ast.IdentifierNode:
|
||||
t = v.IdentifierNode(n)
|
||||
case *ast.IntegerNode:
|
||||
t = v.IntegerNode(n)
|
||||
case *ast.FloatNode:
|
||||
t = v.FloatNode(n)
|
||||
case *ast.BoolNode:
|
||||
t = v.BoolNode(n)
|
||||
case *ast.StringNode:
|
||||
t = v.StringNode(n)
|
||||
case *ast.UnaryNode:
|
||||
t = v.UnaryNode(n)
|
||||
case *ast.BinaryNode:
|
||||
t = v.BinaryNode(n)
|
||||
case *ast.MatchesNode:
|
||||
t = v.MatchesNode(n)
|
||||
case *ast.PropertyNode:
|
||||
t = v.PropertyNode(n)
|
||||
case *ast.IndexNode:
|
||||
t = v.IndexNode(n)
|
||||
case *ast.SliceNode:
|
||||
t = v.SliceNode(n)
|
||||
case *ast.MethodNode:
|
||||
t = v.MethodNode(n)
|
||||
case *ast.FunctionNode:
|
||||
t = v.FunctionNode(n)
|
||||
case *ast.BuiltinNode:
|
||||
t = v.BuiltinNode(n)
|
||||
case *ast.ClosureNode:
|
||||
t = v.ClosureNode(n)
|
||||
case *ast.PointerNode:
|
||||
t = v.PointerNode(n)
|
||||
case *ast.ConditionalNode:
|
||||
t = v.ConditionalNode(n)
|
||||
case *ast.ArrayNode:
|
||||
t = v.ArrayNode(n)
|
||||
case *ast.MapNode:
|
||||
t = v.MapNode(n)
|
||||
case *ast.PairNode:
|
||||
t = v.PairNode(n)
|
||||
default:
|
||||
panic(fmt.Sprintf("undefined node type (%T)", node))
|
||||
}
|
||||
node.SetType(t)
|
||||
return t
|
||||
}
|
||||
|
||||
func (v *visitor) error(node ast.Node, format string, args ...interface{}) reflect.Type {
|
||||
if v.err == nil { // show first error
|
||||
v.err = &file.Error{
|
||||
Location: node.Location(),
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
}
|
||||
}
|
||||
return interfaceType // interface represent undefined type
|
||||
}
|
||||
|
||||
func (v *visitor) NilNode(*ast.NilNode) reflect.Type {
|
||||
return nilType
|
||||
}
|
||||
|
||||
func (v *visitor) IdentifierNode(node *ast.IdentifierNode) reflect.Type {
|
||||
if v.types == nil {
|
||||
return interfaceType
|
||||
}
|
||||
if t, ok := v.types[node.Value]; ok {
|
||||
if t.Ambiguous {
|
||||
return v.error(node, "ambiguous identifier %v", node.Value)
|
||||
}
|
||||
return t.Type
|
||||
}
|
||||
if !v.strict {
|
||||
if v.defaultType != nil {
|
||||
return v.defaultType
|
||||
}
|
||||
return interfaceType
|
||||
}
|
||||
return v.error(node, "unknown name %v", node.Value)
|
||||
}
|
||||
|
||||
func (v *visitor) IntegerNode(*ast.IntegerNode) reflect.Type {
|
||||
return integerType
|
||||
}
|
||||
|
||||
func (v *visitor) FloatNode(*ast.FloatNode) reflect.Type {
|
||||
return floatType
|
||||
}
|
||||
|
||||
func (v *visitor) BoolNode(*ast.BoolNode) reflect.Type {
|
||||
return boolType
|
||||
}
|
||||
|
||||
func (v *visitor) StringNode(*ast.StringNode) reflect.Type {
|
||||
return stringType
|
||||
}
|
||||
|
||||
func (v *visitor) UnaryNode(node *ast.UnaryNode) reflect.Type {
|
||||
t := v.visit(node.Node)
|
||||
|
||||
switch node.Operator {
|
||||
|
||||
case "!", "not":
|
||||
if isBool(t) {
|
||||
return boolType
|
||||
}
|
||||
|
||||
case "+", "-":
|
||||
if isNumber(t) {
|
||||
return t
|
||||
}
|
||||
|
||||
default:
|
||||
return v.error(node, "unknown operator (%v)", node.Operator)
|
||||
}
|
||||
|
||||
return v.error(node, `invalid operation: %v (mismatched type %v)`, node.Operator, t)
|
||||
}
|
||||
|
||||
func (v *visitor) BinaryNode(node *ast.BinaryNode) reflect.Type {
|
||||
l := v.visit(node.Left)
|
||||
r := v.visit(node.Right)
|
||||
|
||||
// check operator overloading
|
||||
if fns, ok := v.operators[node.Operator]; ok {
|
||||
t, _, ok := conf.FindSuitableOperatorOverload(fns, v.types, l, r)
|
||||
if ok {
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
switch node.Operator {
|
||||
case "==", "!=":
|
||||
if isNumber(l) && isNumber(r) {
|
||||
return boolType
|
||||
}
|
||||
if isComparable(l, r) {
|
||||
return boolType
|
||||
}
|
||||
|
||||
case "or", "||", "and", "&&":
|
||||
if isBool(l) && isBool(r) {
|
||||
return boolType
|
||||
}
|
||||
|
||||
case "in", "not in":
|
||||
if isString(l) && isStruct(r) {
|
||||
return boolType
|
||||
}
|
||||
if isMap(r) {
|
||||
return boolType
|
||||
}
|
||||
if isArray(r) {
|
||||
return boolType
|
||||
}
|
||||
|
||||
case "<", ">", ">=", "<=":
|
||||
if isNumber(l) && isNumber(r) {
|
||||
return boolType
|
||||
}
|
||||
if isString(l) && isString(r) {
|
||||
return boolType
|
||||
}
|
||||
|
||||
case "/", "-", "*":
|
||||
if isNumber(l) && isNumber(r) {
|
||||
return combined(l, r)
|
||||
}
|
||||
|
||||
case "**":
|
||||
if isNumber(l) && isNumber(r) {
|
||||
return floatType
|
||||
}
|
||||
|
||||
case "%":
|
||||
if isInteger(l) && isInteger(r) {
|
||||
return combined(l, r)
|
||||
}
|
||||
|
||||
case "+":
|
||||
if isNumber(l) && isNumber(r) {
|
||||
return combined(l, r)
|
||||
}
|
||||
if isString(l) && isString(r) {
|
||||
return stringType
|
||||
}
|
||||
|
||||
case "contains", "startsWith", "endsWith":
|
||||
if isString(l) && isString(r) {
|
||||
return boolType
|
||||
}
|
||||
|
||||
case "..":
|
||||
if isInteger(l) && isInteger(r) {
|
||||
return reflect.SliceOf(integerType)
|
||||
}
|
||||
|
||||
default:
|
||||
return v.error(node, "unknown operator (%v)", node.Operator)
|
||||
|
||||
}
|
||||
|
||||
return v.error(node, `invalid operation: %v (mismatched types %v and %v)`, node.Operator, l, r)
|
||||
}
|
||||
|
||||
func (v *visitor) MatchesNode(node *ast.MatchesNode) reflect.Type {
|
||||
l := v.visit(node.Left)
|
||||
r := v.visit(node.Right)
|
||||
|
||||
if isString(l) && isString(r) {
|
||||
return boolType
|
||||
}
|
||||
|
||||
return v.error(node, `invalid operation: matches (mismatched types %v and %v)`, l, r)
|
||||
}
|
||||
|
||||
func (v *visitor) PropertyNode(node *ast.PropertyNode) reflect.Type {
|
||||
t := v.visit(node.Node)
|
||||
|
||||
if t, ok := fieldType(t, node.Property); ok {
|
||||
return t
|
||||
}
|
||||
|
||||
return v.error(node, "type %v has no field %v", t, node.Property)
|
||||
}
|
||||
|
||||
func (v *visitor) IndexNode(node *ast.IndexNode) reflect.Type {
|
||||
t := v.visit(node.Node)
|
||||
i := v.visit(node.Index)
|
||||
|
||||
if t, ok := indexType(t); ok {
|
||||
if !isInteger(i) && !isString(i) {
|
||||
return v.error(node, "invalid operation: cannot use %v as index to %v", i, t)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
return v.error(node, "invalid operation: type %v does not support indexing", t)
|
||||
}
|
||||
|
||||
func (v *visitor) SliceNode(node *ast.SliceNode) reflect.Type {
|
||||
t := v.visit(node.Node)
|
||||
|
||||
_, isIndex := indexType(t)
|
||||
|
||||
if isIndex || isString(t) {
|
||||
if node.From != nil {
|
||||
from := v.visit(node.From)
|
||||
if !isInteger(from) {
|
||||
return v.error(node.From, "invalid operation: non-integer slice index %v", from)
|
||||
}
|
||||
}
|
||||
if node.To != nil {
|
||||
to := v.visit(node.To)
|
||||
if !isInteger(to) {
|
||||
return v.error(node.To, "invalid operation: non-integer slice index %v", to)
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
return v.error(node, "invalid operation: cannot slice %v", t)
|
||||
}
|
||||
|
||||
func (v *visitor) FunctionNode(node *ast.FunctionNode) reflect.Type {
|
||||
if f, ok := v.types[node.Name]; ok {
|
||||
if fn, ok := isFuncType(f.Type); ok {
|
||||
|
||||
inputParamsCount := 1 // for functions
|
||||
if f.Method {
|
||||
inputParamsCount = 2 // for methods
|
||||
}
|
||||
|
||||
if !isInterface(fn) &&
|
||||
fn.IsVariadic() &&
|
||||
fn.NumIn() == inputParamsCount &&
|
||||
fn.NumOut() == 1 &&
|
||||
fn.Out(0).Kind() == reflect.Interface {
|
||||
rest := fn.In(fn.NumIn() - 1) // function has only one param for functions and two for methods
|
||||
if rest.Kind() == reflect.Slice && rest.Elem().Kind() == reflect.Interface {
|
||||
node.Fast = true
|
||||
}
|
||||
}
|
||||
|
||||
return v.checkFunc(fn, f.Method, node, node.Name, node.Arguments)
|
||||
}
|
||||
}
|
||||
if !v.strict {
|
||||
if v.defaultType != nil {
|
||||
return v.defaultType
|
||||
}
|
||||
return interfaceType
|
||||
}
|
||||
return v.error(node, "unknown func %v", node.Name)
|
||||
}
|
||||
|
||||
func (v *visitor) MethodNode(node *ast.MethodNode) reflect.Type {
|
||||
t := v.visit(node.Node)
|
||||
if f, method, ok := methodType(t, node.Method); ok {
|
||||
if fn, ok := isFuncType(f); ok {
|
||||
return v.checkFunc(fn, method, node, node.Method, node.Arguments)
|
||||
}
|
||||
}
|
||||
return v.error(node, "type %v has no method %v", t, node.Method)
|
||||
}
|
||||
|
||||
// checkFunc checks func arguments and returns "return type" of func or method.
|
||||
func (v *visitor) checkFunc(fn reflect.Type, method bool, node ast.Node, name string, arguments []ast.Node) reflect.Type {
|
||||
if isInterface(fn) {
|
||||
return interfaceType
|
||||
}
|
||||
|
||||
if fn.NumOut() == 0 {
|
||||
return v.error(node, "func %v doesn't return value", name)
|
||||
}
|
||||
if fn.NumOut() != 1 {
|
||||
return v.error(node, "func %v returns more then one value", name)
|
||||
}
|
||||
|
||||
numIn := fn.NumIn()
|
||||
|
||||
// If func is method on an env, first argument should be a receiver,
|
||||
// and actual arguments less then numIn by one.
|
||||
if method {
|
||||
numIn--
|
||||
}
|
||||
|
||||
if fn.IsVariadic() {
|
||||
if len(arguments) < numIn-1 {
|
||||
return v.error(node, "not enough arguments to call %v", name)
|
||||
}
|
||||
} else {
|
||||
if len(arguments) > numIn {
|
||||
return v.error(node, "too many arguments to call %v", name)
|
||||
}
|
||||
if len(arguments) < numIn {
|
||||
return v.error(node, "not enough arguments to call %v", name)
|
||||
}
|
||||
}
|
||||
|
||||
offset := 0
|
||||
|
||||
// Skip first argument in case of the receiver.
|
||||
if method {
|
||||
offset = 1
|
||||
}
|
||||
|
||||
for i, arg := range arguments {
|
||||
t := v.visit(arg)
|
||||
|
||||
var in reflect.Type
|
||||
if fn.IsVariadic() && i >= numIn-1 {
|
||||
// For variadic arguments fn(xs ...int), go replaces type of xs (int) with ([]int).
|
||||
// As we compare arguments one by one, we need underling type.
|
||||
in = fn.In(fn.NumIn() - 1)
|
||||
in, _ = indexType(in)
|
||||
} else {
|
||||
in = fn.In(i + offset)
|
||||
}
|
||||
|
||||
if isIntegerOrArithmeticOperation(arg) {
|
||||
t = in
|
||||
setTypeForIntegers(arg, t)
|
||||
}
|
||||
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !t.AssignableTo(in) && t.Kind() != reflect.Interface {
|
||||
return v.error(arg, "cannot use %v as argument (type %v) to call %v ", t, in, name)
|
||||
}
|
||||
}
|
||||
|
||||
return fn.Out(0)
|
||||
}
|
||||
|
||||
func (v *visitor) BuiltinNode(node *ast.BuiltinNode) reflect.Type {
|
||||
switch node.Name {
|
||||
|
||||
case "len":
|
||||
param := v.visit(node.Arguments[0])
|
||||
if isArray(param) || isMap(param) || isString(param) {
|
||||
return integerType
|
||||
}
|
||||
return v.error(node, "invalid argument for len (type %v)", param)
|
||||
|
||||
case "all", "none", "any", "one":
|
||||
collection := v.visit(node.Arguments[0])
|
||||
if !isArray(collection) {
|
||||
return v.error(node.Arguments[0], "builtin %v takes only array (got %v)", node.Name, collection)
|
||||
}
|
||||
|
||||
v.collections = append(v.collections, collection)
|
||||
closure := v.visit(node.Arguments[1])
|
||||
v.collections = v.collections[:len(v.collections)-1]
|
||||
|
||||
if isFunc(closure) &&
|
||||
closure.NumOut() == 1 &&
|
||||
closure.NumIn() == 1 && isInterface(closure.In(0)) {
|
||||
|
||||
if !isBool(closure.Out(0)) {
|
||||
return v.error(node.Arguments[1], "closure should return boolean (got %v)", closure.Out(0).String())
|
||||
}
|
||||
return boolType
|
||||
}
|
||||
return v.error(node.Arguments[1], "closure should has one input and one output param")
|
||||
|
||||
case "filter":
|
||||
collection := v.visit(node.Arguments[0])
|
||||
if !isArray(collection) {
|
||||
return v.error(node.Arguments[0], "builtin %v takes only array (got %v)", node.Name, collection)
|
||||
}
|
||||
|
||||
v.collections = append(v.collections, collection)
|
||||
closure := v.visit(node.Arguments[1])
|
||||
v.collections = v.collections[:len(v.collections)-1]
|
||||
|
||||
if isFunc(closure) &&
|
||||
closure.NumOut() == 1 &&
|
||||
closure.NumIn() == 1 && isInterface(closure.In(0)) {
|
||||
|
||||
if !isBool(closure.Out(0)) {
|
||||
return v.error(node.Arguments[1], "closure should return boolean (got %v)", closure.Out(0).String())
|
||||
}
|
||||
if isInterface(collection) {
|
||||
return arrayType
|
||||
}
|
||||
return reflect.SliceOf(collection.Elem())
|
||||
}
|
||||
return v.error(node.Arguments[1], "closure should has one input and one output param")
|
||||
|
||||
case "map":
|
||||
collection := v.visit(node.Arguments[0])
|
||||
if !isArray(collection) {
|
||||
return v.error(node.Arguments[0], "builtin %v takes only array (got %v)", node.Name, collection)
|
||||
}
|
||||
|
||||
v.collections = append(v.collections, collection)
|
||||
closure := v.visit(node.Arguments[1])
|
||||
v.collections = v.collections[:len(v.collections)-1]
|
||||
|
||||
if isFunc(closure) &&
|
||||
closure.NumOut() == 1 &&
|
||||
closure.NumIn() == 1 && isInterface(closure.In(0)) {
|
||||
|
||||
return reflect.SliceOf(closure.Out(0))
|
||||
}
|
||||
return v.error(node.Arguments[1], "closure should has one input and one output param")
|
||||
|
||||
case "count":
|
||||
collection := v.visit(node.Arguments[0])
|
||||
if !isArray(collection) {
|
||||
return v.error(node.Arguments[0], "builtin %v takes only array (got %v)", node.Name, collection)
|
||||
}
|
||||
|
||||
v.collections = append(v.collections, collection)
|
||||
closure := v.visit(node.Arguments[1])
|
||||
v.collections = v.collections[:len(v.collections)-1]
|
||||
|
||||
if isFunc(closure) &&
|
||||
closure.NumOut() == 1 &&
|
||||
closure.NumIn() == 1 && isInterface(closure.In(0)) {
|
||||
if !isBool(closure.Out(0)) {
|
||||
return v.error(node.Arguments[1], "closure should return boolean (got %v)", closure.Out(0).String())
|
||||
}
|
||||
|
||||
return integerType
|
||||
}
|
||||
return v.error(node.Arguments[1], "closure should has one input and one output param")
|
||||
|
||||
default:
|
||||
return v.error(node, "unknown builtin %v", node.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *visitor) ClosureNode(node *ast.ClosureNode) reflect.Type {
|
||||
t := v.visit(node.Node)
|
||||
return reflect.FuncOf([]reflect.Type{interfaceType}, []reflect.Type{t}, false)
|
||||
}
|
||||
|
||||
func (v *visitor) PointerNode(node *ast.PointerNode) reflect.Type {
|
||||
if len(v.collections) == 0 {
|
||||
return v.error(node, "cannot use pointer accessor outside closure")
|
||||
}
|
||||
|
||||
collection := v.collections[len(v.collections)-1]
|
||||
|
||||
if t, ok := indexType(collection); ok {
|
||||
return t
|
||||
}
|
||||
return v.error(node, "cannot use %v as array", collection)
|
||||
}
|
||||
|
||||
func (v *visitor) ConditionalNode(node *ast.ConditionalNode) reflect.Type {
|
||||
c := v.visit(node.Cond)
|
||||
if !isBool(c) {
|
||||
return v.error(node.Cond, "non-bool expression (type %v) used as condition", c)
|
||||
}
|
||||
|
||||
t1 := v.visit(node.Exp1)
|
||||
t2 := v.visit(node.Exp2)
|
||||
|
||||
if t1 == nil && t2 != nil {
|
||||
return t2
|
||||
}
|
||||
if t1 != nil && t2 == nil {
|
||||
return t1
|
||||
}
|
||||
if t1 == nil && t2 == nil {
|
||||
return nilType
|
||||
}
|
||||
if t1.AssignableTo(t2) {
|
||||
return t1
|
||||
}
|
||||
return interfaceType
|
||||
}
|
||||
|
||||
func (v *visitor) ArrayNode(node *ast.ArrayNode) reflect.Type {
|
||||
for _, node := range node.Nodes {
|
||||
_ = v.visit(node)
|
||||
}
|
||||
return arrayType
|
||||
}
|
||||
|
||||
func (v *visitor) MapNode(node *ast.MapNode) reflect.Type {
|
||||
for _, pair := range node.Pairs {
|
||||
v.visit(pair)
|
||||
}
|
||||
return mapType
|
||||
}
|
||||
|
||||
func (v *visitor) PairNode(node *ast.PairNode) reflect.Type {
|
||||
v.visit(node.Key)
|
||||
v.visit(node.Value)
|
||||
return nilType
|
||||
}
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
package checker
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/antonmedv/expr/ast"
|
||||
)
|
||||
|
||||
var (
|
||||
nilType = reflect.TypeOf(nil)
|
||||
boolType = reflect.TypeOf(true)
|
||||
integerType = reflect.TypeOf(int(0))
|
||||
floatType = reflect.TypeOf(float64(0))
|
||||
stringType = reflect.TypeOf("")
|
||||
arrayType = reflect.TypeOf([]interface{}{})
|
||||
mapType = reflect.TypeOf(map[string]interface{}{})
|
||||
interfaceType = reflect.TypeOf(new(interface{})).Elem()
|
||||
)
|
||||
|
||||
func typeWeight(t reflect.Type) int {
|
||||
switch t.Kind() {
|
||||
case reflect.Uint:
|
||||
return 1
|
||||
case reflect.Uint8:
|
||||
return 2
|
||||
case reflect.Uint16:
|
||||
return 3
|
||||
case reflect.Uint32:
|
||||
return 4
|
||||
case reflect.Uint64:
|
||||
return 5
|
||||
case reflect.Int:
|
||||
return 6
|
||||
case reflect.Int8:
|
||||
return 7
|
||||
case reflect.Int16:
|
||||
return 8
|
||||
case reflect.Int32:
|
||||
return 9
|
||||
case reflect.Int64:
|
||||
return 10
|
||||
case reflect.Float32:
|
||||
return 11
|
||||
case reflect.Float64:
|
||||
return 12
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func combined(a, b reflect.Type) reflect.Type {
|
||||
if typeWeight(a) > typeWeight(b) {
|
||||
return a
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
func dereference(t reflect.Type) reflect.Type {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = dereference(t.Elem())
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func isComparable(l, r reflect.Type) bool {
|
||||
l = dereference(l)
|
||||
r = dereference(r)
|
||||
|
||||
if l == nil || r == nil { // It is possible to compare with nil.
|
||||
return true
|
||||
}
|
||||
if l.Kind() == r.Kind() {
|
||||
return true
|
||||
}
|
||||
if isInterface(l) || isInterface(r) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isInterface(t reflect.Type) bool {
|
||||
t = dereference(t)
|
||||
if t != nil {
|
||||
switch t.Kind() {
|
||||
case reflect.Interface:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isInteger(t reflect.Type) bool {
|
||||
t = dereference(t)
|
||||
if t != nil {
|
||||
switch t.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
fallthrough
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return true
|
||||
case reflect.Interface:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isFloat(t reflect.Type) bool {
|
||||
t = dereference(t)
|
||||
if t != nil {
|
||||
switch t.Kind() {
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return true
|
||||
case reflect.Interface:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isNumber(t reflect.Type) bool {
|
||||
return isInteger(t) || isFloat(t)
|
||||
}
|
||||
|
||||
func isBool(t reflect.Type) bool {
|
||||
t = dereference(t)
|
||||
if t != nil {
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
return true
|
||||
case reflect.Interface:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isString(t reflect.Type) bool {
|
||||
t = dereference(t)
|
||||
if t != nil {
|
||||
switch t.Kind() {
|
||||
case reflect.String:
|
||||
return true
|
||||
case reflect.Interface:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isArray(t reflect.Type) bool {
|
||||
t = dereference(t)
|
||||
if t != nil {
|
||||
switch t.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
return true
|
||||
case reflect.Interface:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isMap(t reflect.Type) bool {
|
||||
t = dereference(t)
|
||||
if t != nil {
|
||||
switch t.Kind() {
|
||||
case reflect.Map:
|
||||
return true
|
||||
case reflect.Interface:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isStruct(t reflect.Type) bool {
|
||||
t = dereference(t)
|
||||
if t != nil {
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isFunc(t reflect.Type) bool {
|
||||
t = dereference(t)
|
||||
if t != nil {
|
||||
switch t.Kind() {
|
||||
case reflect.Func:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fieldType(ntype reflect.Type, name string) (reflect.Type, bool) {
|
||||
ntype = dereference(ntype)
|
||||
if ntype != nil {
|
||||
switch ntype.Kind() {
|
||||
case reflect.Interface:
|
||||
return interfaceType, true
|
||||
case reflect.Struct:
|
||||
// First check all struct's fields.
|
||||
for i := 0; i < ntype.NumField(); i++ {
|
||||
f := ntype.Field(i)
|
||||
if f.Name == name {
|
||||
return f.Type, true
|
||||
}
|
||||
}
|
||||
|
||||
// Second check fields of embedded structs.
|
||||
for i := 0; i < ntype.NumField(); i++ {
|
||||
f := ntype.Field(i)
|
||||
if f.Anonymous {
|
||||
if t, ok := fieldType(f.Type, name); ok {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
}
|
||||
case reflect.Map:
|
||||
return ntype.Elem(), true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func methodType(t reflect.Type, name string) (reflect.Type, bool, bool) {
|
||||
if t != nil {
|
||||
// First, check methods defined on type itself,
|
||||
// independent of which type it is.
|
||||
if m, ok := t.MethodByName(name); ok {
|
||||
if t.Kind() == reflect.Interface {
|
||||
// In case of interface type method will not have a receiver,
|
||||
// and to prevent checker decreasing numbers of in arguments
|
||||
// return method type as not method (second argument is false).
|
||||
return m.Type, false, true
|
||||
} else {
|
||||
return m.Type, true, true
|
||||
}
|
||||
}
|
||||
|
||||
d := t
|
||||
if t.Kind() == reflect.Ptr {
|
||||
d = t.Elem()
|
||||
}
|
||||
|
||||
switch d.Kind() {
|
||||
case reflect.Interface:
|
||||
return interfaceType, false, true
|
||||
case reflect.Struct:
|
||||
// First, check all struct's fields.
|
||||
for i := 0; i < d.NumField(); i++ {
|
||||
f := d.Field(i)
|
||||
if !f.Anonymous && f.Name == name {
|
||||
return f.Type, false, true
|
||||
}
|
||||
}
|
||||
|
||||
// Second, check fields of embedded structs.
|
||||
for i := 0; i < d.NumField(); i++ {
|
||||
f := d.Field(i)
|
||||
if f.Anonymous {
|
||||
if t, method, ok := methodType(f.Type, name); ok {
|
||||
return t, method, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
return d.Elem(), false, true
|
||||
}
|
||||
}
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
func indexType(ntype reflect.Type) (reflect.Type, bool) {
|
||||
ntype = dereference(ntype)
|
||||
if ntype == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
switch ntype.Kind() {
|
||||
case reflect.Interface:
|
||||
return interfaceType, true
|
||||
case reflect.Map, reflect.Array, reflect.Slice:
|
||||
return ntype.Elem(), true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func isFuncType(ntype reflect.Type) (reflect.Type, bool) {
|
||||
ntype = dereference(ntype)
|
||||
if ntype == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
switch ntype.Kind() {
|
||||
case reflect.Interface:
|
||||
return interfaceType, true
|
||||
case reflect.Func:
|
||||
return ntype, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func isIntegerOrArithmeticOperation(node ast.Node) bool {
|
||||
switch n := node.(type) {
|
||||
case *ast.IntegerNode:
|
||||
return true
|
||||
case *ast.UnaryNode:
|
||||
switch n.Operator {
|
||||
case "+", "-":
|
||||
return true
|
||||
}
|
||||
case *ast.BinaryNode:
|
||||
switch n.Operator {
|
||||
case "+", "/", "-", "*":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setTypeForIntegers(node ast.Node, t reflect.Type) {
|
||||
switch n := node.(type) {
|
||||
case *ast.IntegerNode:
|
||||
n.SetType(t)
|
||||
case *ast.UnaryNode:
|
||||
switch n.Operator {
|
||||
case "+", "-":
|
||||
setTypeForIntegers(n.Node, t)
|
||||
}
|
||||
case *ast.BinaryNode:
|
||||
switch n.Operator {
|
||||
case "+", "/", "-", "*":
|
||||
setTypeForIntegers(n.Left, t)
|
||||
setTypeForIntegers(n.Right, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
+663
@@ -0,0 +1,663 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
|
||||
"github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/conf"
|
||||
"github.com/antonmedv/expr/file"
|
||||
"github.com/antonmedv/expr/parser"
|
||||
. "github.com/antonmedv/expr/vm"
|
||||
)
|
||||
|
||||
func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
c := &compiler{
|
||||
index: make(map[interface{}]uint16),
|
||||
locations: make(map[int]file.Location),
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
c.mapEnv = config.MapEnv
|
||||
c.cast = config.Expect
|
||||
}
|
||||
|
||||
c.compile(tree.Node)
|
||||
|
||||
switch c.cast {
|
||||
case reflect.Int64:
|
||||
c.emit(OpCast, encode(0)...)
|
||||
case reflect.Float64:
|
||||
c.emit(OpCast, encode(1)...)
|
||||
}
|
||||
|
||||
program = &Program{
|
||||
Source: tree.Source,
|
||||
Locations: c.locations,
|
||||
Constants: c.constants,
|
||||
Bytecode: c.bytecode,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type compiler struct {
|
||||
locations map[int]file.Location
|
||||
constants []interface{}
|
||||
bytecode []byte
|
||||
index map[interface{}]uint16
|
||||
mapEnv bool
|
||||
cast reflect.Kind
|
||||
nodes []ast.Node
|
||||
}
|
||||
|
||||
func (c *compiler) emit(op byte, b ...byte) int {
|
||||
c.bytecode = append(c.bytecode, op)
|
||||
current := len(c.bytecode)
|
||||
c.bytecode = append(c.bytecode, b...)
|
||||
|
||||
var loc file.Location
|
||||
if len(c.nodes) > 0 {
|
||||
loc = c.nodes[len(c.nodes)-1].Location()
|
||||
}
|
||||
c.locations[current-1] = loc
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
func (c *compiler) emitPush(value interface{}) int {
|
||||
return c.emit(OpPush, c.makeConstant(value)...)
|
||||
}
|
||||
|
||||
func (c *compiler) makeConstant(i interface{}) []byte {
|
||||
hashable := true
|
||||
switch reflect.TypeOf(i).Kind() {
|
||||
case reflect.Slice, reflect.Map:
|
||||
hashable = false
|
||||
}
|
||||
|
||||
if hashable {
|
||||
if p, ok := c.index[i]; ok {
|
||||
return encode(p)
|
||||
}
|
||||
}
|
||||
|
||||
c.constants = append(c.constants, i)
|
||||
if len(c.constants) > math.MaxUint16 {
|
||||
panic("exceeded constants max space limit")
|
||||
}
|
||||
|
||||
p := uint16(len(c.constants) - 1)
|
||||
if hashable {
|
||||
c.index[i] = p
|
||||
}
|
||||
return encode(p)
|
||||
}
|
||||
|
||||
func (c *compiler) placeholder() []byte {
|
||||
return []byte{0xFF, 0xFF}
|
||||
}
|
||||
|
||||
func (c *compiler) patchJump(placeholder int) {
|
||||
offset := len(c.bytecode) - 2 - placeholder
|
||||
b := encode(uint16(offset))
|
||||
c.bytecode[placeholder] = b[0]
|
||||
c.bytecode[placeholder+1] = b[1]
|
||||
}
|
||||
|
||||
func (c *compiler) calcBackwardJump(to int) []byte {
|
||||
return encode(uint16(len(c.bytecode) + 1 + 2 - to))
|
||||
}
|
||||
|
||||
func (c *compiler) compile(node ast.Node) {
|
||||
c.nodes = append(c.nodes, node)
|
||||
defer func() {
|
||||
c.nodes = c.nodes[:len(c.nodes)-1]
|
||||
}()
|
||||
|
||||
switch n := node.(type) {
|
||||
case *ast.NilNode:
|
||||
c.NilNode(n)
|
||||
case *ast.IdentifierNode:
|
||||
c.IdentifierNode(n)
|
||||
case *ast.IntegerNode:
|
||||
c.IntegerNode(n)
|
||||
case *ast.FloatNode:
|
||||
c.FloatNode(n)
|
||||
case *ast.BoolNode:
|
||||
c.BoolNode(n)
|
||||
case *ast.StringNode:
|
||||
c.StringNode(n)
|
||||
case *ast.ConstantNode:
|
||||
c.ConstantNode(n)
|
||||
case *ast.UnaryNode:
|
||||
c.UnaryNode(n)
|
||||
case *ast.BinaryNode:
|
||||
c.BinaryNode(n)
|
||||
case *ast.MatchesNode:
|
||||
c.MatchesNode(n)
|
||||
case *ast.PropertyNode:
|
||||
c.PropertyNode(n)
|
||||
case *ast.IndexNode:
|
||||
c.IndexNode(n)
|
||||
case *ast.SliceNode:
|
||||
c.SliceNode(n)
|
||||
case *ast.MethodNode:
|
||||
c.MethodNode(n)
|
||||
case *ast.FunctionNode:
|
||||
c.FunctionNode(n)
|
||||
case *ast.BuiltinNode:
|
||||
c.BuiltinNode(n)
|
||||
case *ast.ClosureNode:
|
||||
c.ClosureNode(n)
|
||||
case *ast.PointerNode:
|
||||
c.PointerNode(n)
|
||||
case *ast.ConditionalNode:
|
||||
c.ConditionalNode(n)
|
||||
case *ast.ArrayNode:
|
||||
c.ArrayNode(n)
|
||||
case *ast.MapNode:
|
||||
c.MapNode(n)
|
||||
case *ast.PairNode:
|
||||
c.PairNode(n)
|
||||
default:
|
||||
panic(fmt.Sprintf("undefined node type (%T)", node))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) NilNode(node *ast.NilNode) {
|
||||
c.emit(OpNil)
|
||||
}
|
||||
|
||||
func (c *compiler) IdentifierNode(node *ast.IdentifierNode) {
|
||||
v := c.makeConstant(node.Value)
|
||||
if c.mapEnv {
|
||||
c.emit(OpFetchMap, v...)
|
||||
} else {
|
||||
c.emit(OpFetch, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) IntegerNode(node *ast.IntegerNode) {
|
||||
t := node.Type()
|
||||
if t == nil {
|
||||
c.emitPush(node.Value)
|
||||
return
|
||||
}
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Float32:
|
||||
c.emitPush(float32(node.Value))
|
||||
case reflect.Float64:
|
||||
c.emitPush(float64(node.Value))
|
||||
|
||||
case reflect.Int:
|
||||
c.emitPush(int(node.Value))
|
||||
case reflect.Int8:
|
||||
c.emitPush(int8(node.Value))
|
||||
case reflect.Int16:
|
||||
c.emitPush(int16(node.Value))
|
||||
case reflect.Int32:
|
||||
c.emitPush(int32(node.Value))
|
||||
case reflect.Int64:
|
||||
c.emitPush(int64(node.Value))
|
||||
|
||||
case reflect.Uint:
|
||||
c.emitPush(uint(node.Value))
|
||||
case reflect.Uint8:
|
||||
c.emitPush(uint8(node.Value))
|
||||
case reflect.Uint16:
|
||||
c.emitPush(uint16(node.Value))
|
||||
case reflect.Uint32:
|
||||
c.emitPush(uint32(node.Value))
|
||||
case reflect.Uint64:
|
||||
c.emitPush(uint64(node.Value))
|
||||
|
||||
default:
|
||||
c.emitPush(node.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) FloatNode(node *ast.FloatNode) {
|
||||
c.emitPush(node.Value)
|
||||
}
|
||||
|
||||
func (c *compiler) BoolNode(node *ast.BoolNode) {
|
||||
if node.Value {
|
||||
c.emit(OpTrue)
|
||||
} else {
|
||||
c.emit(OpFalse)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) StringNode(node *ast.StringNode) {
|
||||
c.emitPush(node.Value)
|
||||
}
|
||||
|
||||
func (c *compiler) ConstantNode(node *ast.ConstantNode) {
|
||||
c.emitPush(node.Value)
|
||||
}
|
||||
|
||||
func (c *compiler) UnaryNode(node *ast.UnaryNode) {
|
||||
c.compile(node.Node)
|
||||
|
||||
switch node.Operator {
|
||||
|
||||
case "!", "not":
|
||||
c.emit(OpNot)
|
||||
|
||||
case "+":
|
||||
// Do nothing
|
||||
|
||||
case "-":
|
||||
c.emit(OpNegate)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown operator (%v)", node.Operator))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) BinaryNode(node *ast.BinaryNode) {
|
||||
l := kind(node.Left)
|
||||
r := kind(node.Right)
|
||||
|
||||
switch node.Operator {
|
||||
case "==":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
|
||||
if l == r && l == reflect.Int {
|
||||
c.emit(OpEqualInt)
|
||||
} else if l == r && l == reflect.String {
|
||||
c.emit(OpEqualString)
|
||||
} else {
|
||||
c.emit(OpEqual)
|
||||
}
|
||||
|
||||
case "!=":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpEqual)
|
||||
c.emit(OpNot)
|
||||
|
||||
case "or", "||":
|
||||
c.compile(node.Left)
|
||||
end := c.emit(OpJumpIfTrue, c.placeholder()...)
|
||||
c.emit(OpPop)
|
||||
c.compile(node.Right)
|
||||
c.patchJump(end)
|
||||
|
||||
case "and", "&&":
|
||||
c.compile(node.Left)
|
||||
end := c.emit(OpJumpIfFalse, c.placeholder()...)
|
||||
c.emit(OpPop)
|
||||
c.compile(node.Right)
|
||||
c.patchJump(end)
|
||||
|
||||
case "in":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpIn)
|
||||
|
||||
case "not in":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpIn)
|
||||
c.emit(OpNot)
|
||||
|
||||
case "<":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpLess)
|
||||
|
||||
case ">":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpMore)
|
||||
|
||||
case "<=":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpLessOrEqual)
|
||||
|
||||
case ">=":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpMoreOrEqual)
|
||||
|
||||
case "+":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpAdd)
|
||||
|
||||
case "-":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpSubtract)
|
||||
|
||||
case "*":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpMultiply)
|
||||
|
||||
case "/":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpDivide)
|
||||
|
||||
case "%":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpModulo)
|
||||
|
||||
case "**":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpExponent)
|
||||
|
||||
case "contains":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpContains)
|
||||
|
||||
case "startsWith":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpStartsWith)
|
||||
|
||||
case "endsWith":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpEndsWith)
|
||||
|
||||
case "..":
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpRange)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown operator (%v)", node.Operator))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) MatchesNode(node *ast.MatchesNode) {
|
||||
if node.Regexp != nil {
|
||||
c.compile(node.Left)
|
||||
c.emit(OpMatchesConst, c.makeConstant(node.Regexp)...)
|
||||
return
|
||||
}
|
||||
c.compile(node.Left)
|
||||
c.compile(node.Right)
|
||||
c.emit(OpMatches)
|
||||
}
|
||||
|
||||
func (c *compiler) PropertyNode(node *ast.PropertyNode) {
|
||||
c.compile(node.Node)
|
||||
c.emit(OpProperty, c.makeConstant(node.Property)...)
|
||||
}
|
||||
|
||||
func (c *compiler) IndexNode(node *ast.IndexNode) {
|
||||
c.compile(node.Node)
|
||||
c.compile(node.Index)
|
||||
c.emit(OpIndex)
|
||||
}
|
||||
|
||||
func (c *compiler) SliceNode(node *ast.SliceNode) {
|
||||
c.compile(node.Node)
|
||||
if node.To != nil {
|
||||
c.compile(node.To)
|
||||
} else {
|
||||
c.emit(OpLen)
|
||||
}
|
||||
if node.From != nil {
|
||||
c.compile(node.From)
|
||||
} else {
|
||||
c.emitPush(0)
|
||||
}
|
||||
c.emit(OpSlice)
|
||||
}
|
||||
|
||||
func (c *compiler) MethodNode(node *ast.MethodNode) {
|
||||
c.compile(node.Node)
|
||||
for _, arg := range node.Arguments {
|
||||
c.compile(arg)
|
||||
}
|
||||
c.emit(OpMethod, c.makeConstant(Call{Name: node.Method, Size: len(node.Arguments)})...)
|
||||
}
|
||||
|
||||
func (c *compiler) FunctionNode(node *ast.FunctionNode) {
|
||||
for _, arg := range node.Arguments {
|
||||
c.compile(arg)
|
||||
}
|
||||
op := OpCall
|
||||
if node.Fast {
|
||||
op = OpCallFast
|
||||
}
|
||||
c.emit(op, c.makeConstant(Call{Name: node.Name, Size: len(node.Arguments)})...)
|
||||
}
|
||||
|
||||
func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
|
||||
switch node.Name {
|
||||
case "len":
|
||||
c.compile(node.Arguments[0])
|
||||
c.emit(OpLen)
|
||||
c.emit(OpRot)
|
||||
c.emit(OpPop)
|
||||
|
||||
case "all":
|
||||
c.compile(node.Arguments[0])
|
||||
c.emit(OpBegin)
|
||||
var loopBreak int
|
||||
c.emitLoop(func() {
|
||||
c.compile(node.Arguments[1])
|
||||
loopBreak = c.emit(OpJumpIfFalse, c.placeholder()...)
|
||||
c.emit(OpPop)
|
||||
})
|
||||
c.emit(OpTrue)
|
||||
c.patchJump(loopBreak)
|
||||
c.emit(OpEnd)
|
||||
|
||||
case "none":
|
||||
c.compile(node.Arguments[0])
|
||||
c.emit(OpBegin)
|
||||
var loopBreak int
|
||||
c.emitLoop(func() {
|
||||
c.compile(node.Arguments[1])
|
||||
c.emit(OpNot)
|
||||
loopBreak = c.emit(OpJumpIfFalse, c.placeholder()...)
|
||||
c.emit(OpPop)
|
||||
})
|
||||
c.emit(OpTrue)
|
||||
c.patchJump(loopBreak)
|
||||
c.emit(OpEnd)
|
||||
|
||||
case "any":
|
||||
c.compile(node.Arguments[0])
|
||||
c.emit(OpBegin)
|
||||
var loopBreak int
|
||||
c.emitLoop(func() {
|
||||
c.compile(node.Arguments[1])
|
||||
loopBreak = c.emit(OpJumpIfTrue, c.placeholder()...)
|
||||
c.emit(OpPop)
|
||||
})
|
||||
c.emit(OpFalse)
|
||||
c.patchJump(loopBreak)
|
||||
c.emit(OpEnd)
|
||||
|
||||
case "one":
|
||||
count := c.makeConstant("count")
|
||||
c.compile(node.Arguments[0])
|
||||
c.emit(OpBegin)
|
||||
c.emitPush(0)
|
||||
c.emit(OpStore, count...)
|
||||
c.emitLoop(func() {
|
||||
c.compile(node.Arguments[1])
|
||||
c.emitCond(func() {
|
||||
c.emit(OpInc, count...)
|
||||
})
|
||||
})
|
||||
c.emit(OpLoad, count...)
|
||||
c.emitPush(1)
|
||||
c.emit(OpEqual)
|
||||
c.emit(OpEnd)
|
||||
|
||||
case "filter":
|
||||
count := c.makeConstant("count")
|
||||
c.compile(node.Arguments[0])
|
||||
c.emit(OpBegin)
|
||||
c.emitPush(0)
|
||||
c.emit(OpStore, count...)
|
||||
c.emitLoop(func() {
|
||||
c.compile(node.Arguments[1])
|
||||
c.emitCond(func() {
|
||||
c.emit(OpInc, count...)
|
||||
|
||||
c.emit(OpLoad, c.makeConstant("array")...)
|
||||
c.emit(OpLoad, c.makeConstant("i")...)
|
||||
c.emit(OpIndex)
|
||||
})
|
||||
})
|
||||
c.emit(OpLoad, count...)
|
||||
c.emit(OpEnd)
|
||||
c.emit(OpArray)
|
||||
|
||||
case "map":
|
||||
c.compile(node.Arguments[0])
|
||||
c.emit(OpBegin)
|
||||
size := c.emitLoop(func() {
|
||||
c.compile(node.Arguments[1])
|
||||
})
|
||||
c.emit(OpLoad, size...)
|
||||
c.emit(OpEnd)
|
||||
c.emit(OpArray)
|
||||
|
||||
case "count":
|
||||
count := c.makeConstant("count")
|
||||
c.compile(node.Arguments[0])
|
||||
c.emit(OpBegin)
|
||||
c.emitPush(0)
|
||||
c.emit(OpStore, count...)
|
||||
c.emitLoop(func() {
|
||||
c.compile(node.Arguments[1])
|
||||
c.emitCond(func() {
|
||||
c.emit(OpInc, count...)
|
||||
})
|
||||
})
|
||||
c.emit(OpLoad, count...)
|
||||
c.emit(OpEnd)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown builtin %v", node.Name))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) emitCond(body func()) {
|
||||
noop := c.emit(OpJumpIfFalse, c.placeholder()...)
|
||||
c.emit(OpPop)
|
||||
|
||||
body()
|
||||
|
||||
jmp := c.emit(OpJump, c.placeholder()...)
|
||||
c.patchJump(noop)
|
||||
c.emit(OpPop)
|
||||
c.patchJump(jmp)
|
||||
}
|
||||
|
||||
func (c *compiler) emitLoop(body func()) []byte {
|
||||
i := c.makeConstant("i")
|
||||
size := c.makeConstant("size")
|
||||
array := c.makeConstant("array")
|
||||
|
||||
c.emit(OpLen)
|
||||
c.emit(OpStore, size...)
|
||||
c.emit(OpStore, array...)
|
||||
c.emitPush(0)
|
||||
c.emit(OpStore, i...)
|
||||
|
||||
cond := len(c.bytecode)
|
||||
c.emit(OpLoad, i...)
|
||||
c.emit(OpLoad, size...)
|
||||
c.emit(OpLess)
|
||||
end := c.emit(OpJumpIfFalse, c.placeholder()...)
|
||||
c.emit(OpPop)
|
||||
|
||||
body()
|
||||
|
||||
c.emit(OpInc, i...)
|
||||
c.emit(OpJumpBackward, c.calcBackwardJump(cond)...)
|
||||
|
||||
c.patchJump(end)
|
||||
c.emit(OpPop)
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
func (c *compiler) ClosureNode(node *ast.ClosureNode) {
|
||||
c.compile(node.Node)
|
||||
}
|
||||
|
||||
func (c *compiler) PointerNode(node *ast.PointerNode) {
|
||||
c.emit(OpLoad, c.makeConstant("array")...)
|
||||
c.emit(OpLoad, c.makeConstant("i")...)
|
||||
c.emit(OpIndex)
|
||||
}
|
||||
|
||||
func (c *compiler) ConditionalNode(node *ast.ConditionalNode) {
|
||||
c.compile(node.Cond)
|
||||
otherwise := c.emit(OpJumpIfFalse, c.placeholder()...)
|
||||
|
||||
c.emit(OpPop)
|
||||
c.compile(node.Exp1)
|
||||
end := c.emit(OpJump, c.placeholder()...)
|
||||
|
||||
c.patchJump(otherwise)
|
||||
c.emit(OpPop)
|
||||
c.compile(node.Exp2)
|
||||
|
||||
c.patchJump(end)
|
||||
}
|
||||
|
||||
func (c *compiler) ArrayNode(node *ast.ArrayNode) {
|
||||
for _, node := range node.Nodes {
|
||||
c.compile(node)
|
||||
}
|
||||
|
||||
c.emitPush(len(node.Nodes))
|
||||
c.emit(OpArray)
|
||||
}
|
||||
|
||||
func (c *compiler) MapNode(node *ast.MapNode) {
|
||||
for _, pair := range node.Pairs {
|
||||
c.compile(pair)
|
||||
}
|
||||
|
||||
c.emitPush(len(node.Pairs))
|
||||
c.emit(OpMap)
|
||||
}
|
||||
|
||||
func (c *compiler) PairNode(node *ast.PairNode) {
|
||||
c.compile(node.Key)
|
||||
c.compile(node.Value)
|
||||
}
|
||||
|
||||
func encode(i uint16) []byte {
|
||||
b := make([]byte, 2)
|
||||
binary.LittleEndian.PutUint16(b, i)
|
||||
return b
|
||||
}
|
||||
|
||||
func kind(node ast.Node) reflect.Kind {
|
||||
t := node.Type()
|
||||
if t == nil {
|
||||
return reflect.Invalid
|
||||
}
|
||||
return t.Kind()
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/conf"
|
||||
)
|
||||
|
||||
type operatorPatcher struct {
|
||||
ops map[string][]string
|
||||
types conf.TypesTable
|
||||
}
|
||||
|
||||
func (p *operatorPatcher) Enter(node *ast.Node) {}
|
||||
func (p *operatorPatcher) Exit(node *ast.Node) {
|
||||
binaryNode, ok := (*node).(*ast.BinaryNode)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
fns, ok := p.ops[binaryNode.Operator]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
leftType := binaryNode.Left.Type()
|
||||
rightType := binaryNode.Right.Type()
|
||||
|
||||
_, fn, ok := conf.FindSuitableOperatorOverload(fns, p.types, leftType, rightType)
|
||||
if ok {
|
||||
newNode := &ast.FunctionNode{
|
||||
Name: fn,
|
||||
Arguments: []ast.Node{binaryNode.Left, binaryNode.Right},
|
||||
}
|
||||
ast.Patch(node, newNode)
|
||||
}
|
||||
}
|
||||
|
||||
func PatchOperators(node *ast.Node, config *conf.Config) {
|
||||
if len(config.Operators) == 0 {
|
||||
return
|
||||
}
|
||||
patcher := &operatorPatcher{ops: config.Operators, types: config.Types}
|
||||
ast.Walk(node, patcher)
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/vm"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Env interface{}
|
||||
MapEnv bool
|
||||
Types TypesTable
|
||||
Operators OperatorsTable
|
||||
Expect reflect.Kind
|
||||
Optimize bool
|
||||
Strict bool
|
||||
DefaultType reflect.Type
|
||||
ConstExprFns map[string]reflect.Value
|
||||
Visitors []ast.Visitor
|
||||
err error
|
||||
}
|
||||
|
||||
func New(env interface{}) *Config {
|
||||
var mapEnv bool
|
||||
var mapValueType reflect.Type
|
||||
if _, ok := env.(map[string]interface{}); ok {
|
||||
mapEnv = true
|
||||
} else {
|
||||
if reflect.ValueOf(env).Kind() == reflect.Map {
|
||||
mapValueType = reflect.TypeOf(env).Elem()
|
||||
}
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Env: env,
|
||||
MapEnv: mapEnv,
|
||||
Types: CreateTypesTable(env),
|
||||
Optimize: true,
|
||||
Strict: true,
|
||||
DefaultType: mapValueType,
|
||||
ConstExprFns: make(map[string]reflect.Value),
|
||||
}
|
||||
}
|
||||
|
||||
// Check validates the compiler configuration.
|
||||
func (c *Config) Check() error {
|
||||
// Check that all functions that define operator overloading
|
||||
// exist in environment and have correct signatures.
|
||||
for op, fns := range c.Operators {
|
||||
for _, fn := range fns {
|
||||
fnType, ok := c.Types[fn]
|
||||
if !ok || fnType.Type.Kind() != reflect.Func {
|
||||
return fmt.Errorf("function %s for %s operator does not exist in environment", fn, op)
|
||||
}
|
||||
requiredNumIn := 2
|
||||
if fnType.Method {
|
||||
requiredNumIn = 3 // As first argument of method is receiver.
|
||||
}
|
||||
if fnType.Type.NumIn() != requiredNumIn || fnType.Type.NumOut() != 1 {
|
||||
return fmt.Errorf("function %s for %s operator does not have a correct signature", fn, op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check that all ConstExprFns are functions.
|
||||
for name, fn := range c.ConstExprFns {
|
||||
if fn.Kind() != reflect.Func {
|
||||
return fmt.Errorf("const expression %q must be a function", name)
|
||||
}
|
||||
}
|
||||
|
||||
return c.err
|
||||
}
|
||||
|
||||
func (c *Config) ConstExpr(name string) {
|
||||
if c.Env == nil {
|
||||
c.Error(fmt.Errorf("no environment for const expression: %v", name))
|
||||
return
|
||||
}
|
||||
c.ConstExprFns[name] = vm.FetchFn(c.Env, name)
|
||||
}
|
||||
|
||||
func (c *Config) Error(err error) {
|
||||
if c.err == nil {
|
||||
c.err = err
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package conf
|
||||
|
||||
import "reflect"
|
||||
|
||||
// OperatorsTable maps binary operators to corresponding list of functions.
|
||||
// Functions should be provided in the environment to allow operator overloading.
|
||||
type OperatorsTable map[string][]string
|
||||
|
||||
func FindSuitableOperatorOverload(fns []string, types TypesTable, l, r reflect.Type) (reflect.Type, string, bool) {
|
||||
for _, fn := range fns {
|
||||
fnType := types[fn]
|
||||
firstInIndex := 0
|
||||
if fnType.Method {
|
||||
firstInIndex = 1 // As first argument to method is receiver.
|
||||
}
|
||||
firstArgType := fnType.Type.In(firstInIndex)
|
||||
secondArgType := fnType.Type.In(firstInIndex + 1)
|
||||
|
||||
firstArgumentFit := l == firstArgType || (firstArgType.Kind() == reflect.Interface && (l == nil || l.Implements(firstArgType)))
|
||||
secondArgumentFit := r == secondArgType || (secondArgType.Kind() == reflect.Interface && (r == nil || r.Implements(secondArgType)))
|
||||
if firstArgumentFit && secondArgumentFit {
|
||||
return fnType.Type.Out(0), fn, true
|
||||
}
|
||||
}
|
||||
return nil, "", false
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package conf
|
||||
|
||||
import "reflect"
|
||||
|
||||
type Tag struct {
|
||||
Type reflect.Type
|
||||
Method bool
|
||||
Ambiguous bool
|
||||
}
|
||||
|
||||
type TypesTable map[string]Tag
|
||||
|
||||
// CreateTypesTable creates types table for type checks during parsing.
|
||||
// If struct is passed, all fields will be treated as variables,
|
||||
// as well as all fields of embedded structs and struct itself.
|
||||
//
|
||||
// If map is passed, all items will be treated as variables
|
||||
// (key as name, value as type).
|
||||
func CreateTypesTable(i interface{}) TypesTable {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
types := make(TypesTable)
|
||||
v := reflect.ValueOf(i)
|
||||
t := reflect.TypeOf(i)
|
||||
|
||||
d := t
|
||||
if t.Kind() == reflect.Ptr {
|
||||
d = t.Elem()
|
||||
}
|
||||
|
||||
switch d.Kind() {
|
||||
case reflect.Struct:
|
||||
types = FieldsFromStruct(d)
|
||||
|
||||
// Methods of struct should be gathered from original struct with pointer,
|
||||
// as methods maybe declared on pointer receiver. Also this method retrieves
|
||||
// all embedded structs methods as well, no need to recursion.
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
m := t.Method(i)
|
||||
types[m.Name] = Tag{Type: m.Type, Method: true}
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
for _, key := range v.MapKeys() {
|
||||
value := v.MapIndex(key)
|
||||
if key.Kind() == reflect.String && value.IsValid() && value.CanInterface() {
|
||||
types[key.String()] = Tag{Type: reflect.TypeOf(value.Interface())}
|
||||
}
|
||||
}
|
||||
|
||||
// A map may have method too.
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
m := t.Method(i)
|
||||
types[m.Name] = Tag{Type: m.Type, Method: true}
|
||||
}
|
||||
}
|
||||
|
||||
return types
|
||||
}
|
||||
|
||||
func FieldsFromStruct(t reflect.Type) TypesTable {
|
||||
types := make(TypesTable)
|
||||
t = dereference(t)
|
||||
if t == nil {
|
||||
return types
|
||||
}
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
|
||||
if f.Anonymous {
|
||||
for name, typ := range FieldsFromStruct(f.Type) {
|
||||
if _, ok := types[name]; ok {
|
||||
types[name] = Tag{Ambiguous: true}
|
||||
} else {
|
||||
types[name] = typ
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
types[f.Name] = Tag{Type: f.Type}
|
||||
}
|
||||
}
|
||||
|
||||
return types
|
||||
}
|
||||
|
||||
func dereference(t reflect.Type) reflect.Type {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = dereference(t.Elem())
|
||||
}
|
||||
return t
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/file"
|
||||
"reflect"
|
||||
|
||||
"github.com/antonmedv/expr/checker"
|
||||
"github.com/antonmedv/expr/compiler"
|
||||
"github.com/antonmedv/expr/conf"
|
||||
"github.com/antonmedv/expr/optimizer"
|
||||
"github.com/antonmedv/expr/parser"
|
||||
"github.com/antonmedv/expr/vm"
|
||||
)
|
||||
|
||||
// Option for configuring config.
|
||||
type Option func(c *conf.Config)
|
||||
|
||||
// Eval parses, compiles and runs given input.
|
||||
func Eval(input string, env interface{}) (interface{}, error) {
|
||||
if _, ok := env.(Option); ok {
|
||||
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
|
||||
}
|
||||
|
||||
tree, err := parser.Parse(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
program, err := compiler.Compile(tree, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := vm.Run(program, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// Env specifies expected input of env for type checks.
|
||||
// If struct is passed, all fields will be treated as variables,
|
||||
// as well as all fields of embedded structs and struct itself.
|
||||
// If map is passed, all items will be treated as variables.
|
||||
// Methods defined on this type will be available as functions.
|
||||
func Env(env interface{}) Option {
|
||||
return func(c *conf.Config) {
|
||||
if _, ok := env.(map[string]interface{}); ok {
|
||||
c.MapEnv = true
|
||||
} else {
|
||||
if reflect.ValueOf(env).Kind() == reflect.Map {
|
||||
c.DefaultType = reflect.TypeOf(env).Elem()
|
||||
}
|
||||
}
|
||||
c.Strict = true
|
||||
c.Types = conf.CreateTypesTable(env)
|
||||
c.Env = env
|
||||
}
|
||||
}
|
||||
|
||||
// AllowUndefinedVariables allows to use undefined variables inside expressions.
|
||||
// This can be used with expr.Env option to partially define a few variables.
|
||||
// Note what this option is only works in map environment are used, otherwise
|
||||
// runtime.fetch will panic as there is no way to get missing field zero value.
|
||||
func AllowUndefinedVariables() Option {
|
||||
return func(c *conf.Config) {
|
||||
c.Strict = false
|
||||
}
|
||||
}
|
||||
|
||||
// Operator allows to override binary operator with function.
|
||||
func Operator(operator string, fn ...string) Option {
|
||||
return func(c *conf.Config) {
|
||||
c.Operators[operator] = append(c.Operators[operator], fn...)
|
||||
}
|
||||
}
|
||||
|
||||
// ConstExpr defines func expression as constant. If all argument to this function is constants,
|
||||
// then it can be replaced by result of this func call on compile step.
|
||||
func ConstExpr(fn string) Option {
|
||||
return func(c *conf.Config) {
|
||||
c.ConstExpr(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// AsBool tells the compiler to expect boolean result.
|
||||
func AsBool() Option {
|
||||
return func(c *conf.Config) {
|
||||
c.Expect = reflect.Bool
|
||||
}
|
||||
}
|
||||
|
||||
// AsInt64 tells the compiler to expect int64 result.
|
||||
func AsInt64() Option {
|
||||
return func(c *conf.Config) {
|
||||
c.Expect = reflect.Int64
|
||||
}
|
||||
}
|
||||
|
||||
// AsFloat64 tells the compiler to expect float64 result.
|
||||
func AsFloat64() Option {
|
||||
return func(c *conf.Config) {
|
||||
c.Expect = reflect.Float64
|
||||
}
|
||||
}
|
||||
|
||||
// Optimize turns optimizations on or off.
|
||||
func Optimize(b bool) Option {
|
||||
return func(c *conf.Config) {
|
||||
c.Optimize = b
|
||||
}
|
||||
}
|
||||
|
||||
// Patch adds visitor to list of visitors what will be applied before compiling AST to bytecode.
|
||||
func Patch(visitor ast.Visitor) Option {
|
||||
return func(c *conf.Config) {
|
||||
c.Visitors = append(c.Visitors, visitor)
|
||||
}
|
||||
}
|
||||
|
||||
// Compile parses and compiles given input expression to bytecode program.
|
||||
func Compile(input string, ops ...Option) (*vm.Program, error) {
|
||||
config := &conf.Config{
|
||||
Operators: make(map[string][]string),
|
||||
ConstExprFns: make(map[string]reflect.Value),
|
||||
Optimize: true,
|
||||
}
|
||||
|
||||
for _, op := range ops {
|
||||
op(config)
|
||||
}
|
||||
|
||||
if err := config.Check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tree, err := parser.Parse(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = checker.Check(tree, config)
|
||||
|
||||
// If we have a patch to apply, it may fix out error and
|
||||
// second type check is needed. Otherwise it is an error.
|
||||
if err != nil && len(config.Visitors) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Patch operators before Optimize, as we may also mark it as ConstExpr.
|
||||
compiler.PatchOperators(&tree.Node, config)
|
||||
|
||||
if len(config.Visitors) >= 0 {
|
||||
for _, v := range config.Visitors {
|
||||
ast.Walk(&tree.Node, v)
|
||||
}
|
||||
_, err = checker.Check(tree, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if config.Optimize {
|
||||
err = optimizer.Optimize(&tree.Node, config)
|
||||
if err != nil {
|
||||
if fileError, ok := err.(*file.Error); ok {
|
||||
return nil, fileError.Bind(tree.Source)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
program, err := compiler.Compile(tree, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return program, nil
|
||||
}
|
||||
|
||||
// Run evaluates given bytecode program.
|
||||
func Run(program *vm.Program, env interface{}) (interface{}, error) {
|
||||
return vm.Run(program, env)
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Location
|
||||
Message string
|
||||
Snippet string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.format()
|
||||
}
|
||||
|
||||
func (e *Error) Bind(source *Source) *Error {
|
||||
if snippet, found := source.Snippet(e.Location.Line); found {
|
||||
snippet := strings.Replace(snippet, "\t", " ", -1)
|
||||
srcLine := "\n | " + snippet
|
||||
var bytes = []byte(snippet)
|
||||
var indLine = "\n | "
|
||||
for i := 0; i < e.Location.Column && len(bytes) > 0; i++ {
|
||||
_, sz := utf8.DecodeRune(bytes)
|
||||
bytes = bytes[sz:]
|
||||
if sz > 1 {
|
||||
goto noind
|
||||
} else {
|
||||
indLine += "."
|
||||
}
|
||||
}
|
||||
if _, sz := utf8.DecodeRune(bytes); sz > 1 {
|
||||
goto noind
|
||||
} else {
|
||||
indLine += "^"
|
||||
}
|
||||
srcLine += indLine
|
||||
|
||||
noind:
|
||||
e.Snippet = srcLine
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Error) format() string {
|
||||
if e.Location.Empty() {
|
||||
return e.Message
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%s (%d:%d)%s",
|
||||
e.Message,
|
||||
e.Line,
|
||||
e.Column+1, // add one to the 0-based column for display
|
||||
e.Snippet,
|
||||
)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package file
|
||||
|
||||
type Location struct {
|
||||
Line int // The 1-based line of the location.
|
||||
Column int // The 0-based column number of the location.
|
||||
}
|
||||
|
||||
func (l Location) Empty() bool {
|
||||
return l.Column == 0 && l.Line == 0
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type Source struct {
|
||||
contents []rune
|
||||
lineOffsets []int32
|
||||
}
|
||||
|
||||
func NewSource(contents string) *Source {
|
||||
s := &Source{
|
||||
contents: []rune(contents),
|
||||
}
|
||||
s.updateOffsets()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Source) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.contents)
|
||||
}
|
||||
|
||||
func (s *Source) UnmarshalJSON(b []byte) error {
|
||||
contents := make([]rune, 0)
|
||||
err := json.Unmarshal(b, &contents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.contents = contents
|
||||
s.updateOffsets()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Source) Content() string {
|
||||
return string(s.contents)
|
||||
}
|
||||
|
||||
func (s *Source) Snippet(line int) (string, bool) {
|
||||
charStart, found := s.findLineOffset(line)
|
||||
if !found || len(s.contents) == 0 {
|
||||
return "", false
|
||||
}
|
||||
charEnd, found := s.findLineOffset(line + 1)
|
||||
if found {
|
||||
return string(s.contents[charStart : charEnd-1]), true
|
||||
}
|
||||
return string(s.contents[charStart:]), true
|
||||
}
|
||||
|
||||
// updateOffsets compute line offsets up front as they are referred to frequently.
|
||||
func (s *Source) updateOffsets() {
|
||||
lines := strings.Split(string(s.contents), "\n")
|
||||
offsets := make([]int32, len(lines))
|
||||
var offset int32
|
||||
for i, line := range lines {
|
||||
offset = offset + int32(utf8.RuneCountInString(line)) + 1
|
||||
offsets[int32(i)] = offset
|
||||
}
|
||||
s.lineOffsets = offsets
|
||||
}
|
||||
|
||||
// findLineOffset returns the offset where the (1-indexed) line begins,
|
||||
// or false if line doesn't exist.
|
||||
func (s *Source) findLineOffset(line int) (int32, bool) {
|
||||
if line == 1 {
|
||||
return 0, true
|
||||
} else if line > 1 && line <= len(s.lineOffsets) {
|
||||
offset := s.lineOffsets[line-2]
|
||||
return offset, true
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
// findLine finds the line that contains the given character offset and
|
||||
// returns the line number and offset of the beginning of that line.
|
||||
// Note that the last line is treated as if it contains all offsets
|
||||
// beyond the end of the actual source.
|
||||
func (s *Source) findLine(characterOffset int32) (int32, int32) {
|
||||
var line int32 = 1
|
||||
for _, lineOffset := range s.lineOffsets {
|
||||
if lineOffset > characterOffset {
|
||||
break
|
||||
} else {
|
||||
line++
|
||||
}
|
||||
}
|
||||
if line == 1 {
|
||||
return line, 0
|
||||
}
|
||||
return line, s.lineOffsets[line-2]
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
module github.com/antonmedv/expr
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell v1.3.0
|
||||
github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498
|
||||
github.com/sanity-io/litter v1.2.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
)
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
|
||||
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498 h1:4CFNy7/q7P06AsIONZzuWy7jcdqEmYQvOZ9FAFZdbls=
|
||||
github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
|
||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sanity-io/litter v1.2.0 h1:DGJO0bxH/+C2EukzOSBmAlxmkhVMGqzvcx/rvySYw9M=
|
||||
github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package optimizer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/file"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type constExpr struct {
|
||||
applied bool
|
||||
err error
|
||||
fns map[string]reflect.Value
|
||||
}
|
||||
|
||||
func (*constExpr) Enter(*Node) {}
|
||||
func (c *constExpr) Exit(node *Node) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
msg := fmt.Sprintf("%v", r)
|
||||
// Make message more actual, it's a runtime error, but at compile step.
|
||||
msg = strings.Replace(msg, "runtime error:", "compile error:", 1)
|
||||
c.err = &file.Error{
|
||||
Location: (*node).Location(),
|
||||
Message: msg,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
patch := func(newNode Node) {
|
||||
c.applied = true
|
||||
Patch(node, newNode)
|
||||
}
|
||||
|
||||
switch n := (*node).(type) {
|
||||
case *FunctionNode:
|
||||
fn, ok := c.fns[n.Name]
|
||||
if ok {
|
||||
in := make([]reflect.Value, len(n.Arguments))
|
||||
for i := 0; i < len(n.Arguments); i++ {
|
||||
arg := n.Arguments[i]
|
||||
var param interface{}
|
||||
|
||||
switch a := arg.(type) {
|
||||
case *NilNode:
|
||||
param = nil
|
||||
case *IntegerNode:
|
||||
param = a.Value
|
||||
case *FloatNode:
|
||||
param = a.Value
|
||||
case *BoolNode:
|
||||
param = a.Value
|
||||
case *StringNode:
|
||||
param = a.Value
|
||||
case *ConstantNode:
|
||||
param = a.Value
|
||||
|
||||
default:
|
||||
return // Const expr optimization not applicable.
|
||||
}
|
||||
|
||||
if param == nil && reflect.TypeOf(param) == nil {
|
||||
// In case of nil value and nil type use this hack,
|
||||
// otherwise reflect.Call will panic on zero value.
|
||||
in[i] = reflect.ValueOf(¶m).Elem()
|
||||
} else {
|
||||
in[i] = reflect.ValueOf(param)
|
||||
}
|
||||
}
|
||||
|
||||
out := fn.Call(in)
|
||||
constNode := &ConstantNode{Value: out[0].Interface()}
|
||||
patch(constNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package optimizer
|
||||
|
||||
import (
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
)
|
||||
|
||||
type constRange struct{}
|
||||
|
||||
func (*constRange) Enter(*Node) {}
|
||||
func (*constRange) Exit(node *Node) {
|
||||
switch n := (*node).(type) {
|
||||
case *BinaryNode:
|
||||
if n.Operator == ".." {
|
||||
if min, ok := n.Left.(*IntegerNode); ok {
|
||||
if max, ok := n.Right.(*IntegerNode); ok {
|
||||
size := max.Value - min.Value + 1
|
||||
// In this case array is too big. Skip generation,
|
||||
// and wait for memory budget detection on runtime.
|
||||
if size > 1e6 {
|
||||
return
|
||||
}
|
||||
value := make([]int, size)
|
||||
for i := range value {
|
||||
value[i] = min.Value + i
|
||||
}
|
||||
Patch(node, &ConstantNode{
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package optimizer
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/file"
|
||||
)
|
||||
|
||||
type fold struct {
|
||||
applied bool
|
||||
err *file.Error
|
||||
}
|
||||
|
||||
func (*fold) Enter(*Node) {}
|
||||
func (fold *fold) Exit(node *Node) {
|
||||
patch := func(newNode Node) {
|
||||
fold.applied = true
|
||||
Patch(node, newNode)
|
||||
}
|
||||
// for IntegerNode the type may have been changed from int->float
|
||||
// preserve this information by setting the type after the Patch
|
||||
patchWithType := func(newNode Node, leafType reflect.Type) {
|
||||
patch(newNode)
|
||||
newNode.SetType(leafType)
|
||||
}
|
||||
|
||||
switch n := (*node).(type) {
|
||||
case *UnaryNode:
|
||||
switch n.Operator {
|
||||
case "-":
|
||||
if i, ok := n.Node.(*IntegerNode); ok {
|
||||
patchWithType(&IntegerNode{Value: -i.Value}, n.Node.Type())
|
||||
}
|
||||
case "+":
|
||||
if i, ok := n.Node.(*IntegerNode); ok {
|
||||
patchWithType(&IntegerNode{Value: i.Value}, n.Node.Type())
|
||||
}
|
||||
}
|
||||
|
||||
case *BinaryNode:
|
||||
switch n.Operator {
|
||||
case "+":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patchWithType(&IntegerNode{Value: a.Value + b.Value}, a.Type())
|
||||
}
|
||||
}
|
||||
if a, ok := n.Left.(*StringNode); ok {
|
||||
if b, ok := n.Right.(*StringNode); ok {
|
||||
patch(&StringNode{Value: a.Value + b.Value})
|
||||
}
|
||||
}
|
||||
case "-":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patchWithType(&IntegerNode{Value: a.Value - b.Value}, a.Type())
|
||||
}
|
||||
}
|
||||
case "*":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patchWithType(&IntegerNode{Value: a.Value * b.Value}, a.Type())
|
||||
}
|
||||
}
|
||||
case "/":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
if b.Value == 0 {
|
||||
fold.err = &file.Error{
|
||||
Location: (*node).Location(),
|
||||
Message: "integer divide by zero",
|
||||
}
|
||||
return
|
||||
}
|
||||
patchWithType(&IntegerNode{Value: a.Value / b.Value}, a.Type())
|
||||
}
|
||||
}
|
||||
case "%":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
if b.Value == 0 {
|
||||
fold.err = &file.Error{
|
||||
Location: (*node).Location(),
|
||||
Message: "integer divide by zero",
|
||||
}
|
||||
return
|
||||
}
|
||||
patch(&IntegerNode{Value: a.Value % b.Value})
|
||||
}
|
||||
}
|
||||
case "**":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&FloatNode{Value: math.Pow(float64(a.Value), float64(b.Value))})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *ArrayNode:
|
||||
if len(n.Nodes) > 0 {
|
||||
|
||||
for _, a := range n.Nodes {
|
||||
if _, ok := a.(*IntegerNode); !ok {
|
||||
goto string
|
||||
}
|
||||
}
|
||||
{
|
||||
value := make([]int, len(n.Nodes))
|
||||
for i, a := range n.Nodes {
|
||||
value[i] = a.(*IntegerNode).Value
|
||||
}
|
||||
patch(&ConstantNode{Value: value})
|
||||
}
|
||||
|
||||
string:
|
||||
for _, a := range n.Nodes {
|
||||
if _, ok := a.(*StringNode); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
{
|
||||
value := make([]string, len(n.Nodes))
|
||||
for i, a := range n.Nodes {
|
||||
value[i] = a.(*StringNode).Value
|
||||
}
|
||||
patch(&ConstantNode{Value: value})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package optimizer
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
)
|
||||
|
||||
type inArray struct{}
|
||||
|
||||
func (*inArray) Enter(*Node) {}
|
||||
func (*inArray) Exit(node *Node) {
|
||||
switch n := (*node).(type) {
|
||||
case *BinaryNode:
|
||||
if n.Operator == "in" || n.Operator == "not in" {
|
||||
if array, ok := n.Right.(*ArrayNode); ok {
|
||||
if len(array.Nodes) > 0 {
|
||||
t := n.Left.Type()
|
||||
if t == nil || t.Kind() != reflect.Int {
|
||||
// This optimization can be only performed if left side is int type,
|
||||
// as runtime.in func uses reflect.Map.MapIndex and keys of map must,
|
||||
// be same as checked value type.
|
||||
goto string
|
||||
}
|
||||
|
||||
for _, a := range array.Nodes {
|
||||
if _, ok := a.(*IntegerNode); !ok {
|
||||
goto string
|
||||
}
|
||||
}
|
||||
{
|
||||
value := make(map[int]struct{})
|
||||
for _, a := range array.Nodes {
|
||||
value[a.(*IntegerNode).Value] = struct{}{}
|
||||
}
|
||||
Patch(node, &BinaryNode{
|
||||
Operator: n.Operator,
|
||||
Left: n.Left,
|
||||
Right: &ConstantNode{Value: value},
|
||||
})
|
||||
}
|
||||
|
||||
string:
|
||||
for _, a := range array.Nodes {
|
||||
if _, ok := a.(*StringNode); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
{
|
||||
value := make(map[string]struct{})
|
||||
for _, a := range array.Nodes {
|
||||
value[a.(*StringNode).Value] = struct{}{}
|
||||
}
|
||||
Patch(node, &BinaryNode{
|
||||
Operator: n.Operator,
|
||||
Left: n.Left,
|
||||
Right: &ConstantNode{Value: value},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package optimizer
|
||||
|
||||
import (
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
)
|
||||
|
||||
type inRange struct{}
|
||||
|
||||
func (*inRange) Enter(*Node) {}
|
||||
func (*inRange) Exit(node *Node) {
|
||||
switch n := (*node).(type) {
|
||||
case *BinaryNode:
|
||||
if n.Operator == "in" || n.Operator == "not in" {
|
||||
if rng, ok := n.Right.(*BinaryNode); ok && rng.Operator == ".." {
|
||||
if from, ok := rng.Left.(*IntegerNode); ok {
|
||||
if to, ok := rng.Right.(*IntegerNode); ok {
|
||||
Patch(node, &BinaryNode{
|
||||
Operator: "and",
|
||||
Left: &BinaryNode{
|
||||
Operator: ">=",
|
||||
Left: n.Left,
|
||||
Right: from,
|
||||
},
|
||||
Right: &BinaryNode{
|
||||
Operator: "<=",
|
||||
Left: n.Left,
|
||||
Right: to,
|
||||
},
|
||||
})
|
||||
if n.Operator == "not in" {
|
||||
Patch(node, &UnaryNode{
|
||||
Operator: "not",
|
||||
Node: *node,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package optimizer
|
||||
|
||||
import (
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/conf"
|
||||
)
|
||||
|
||||
func Optimize(node *Node, config *conf.Config) error {
|
||||
Walk(node, &inArray{})
|
||||
for limit := 1000; limit >= 0; limit-- {
|
||||
fold := &fold{}
|
||||
Walk(node, fold)
|
||||
if fold.err != nil {
|
||||
return fold.err
|
||||
}
|
||||
if !fold.applied {
|
||||
break
|
||||
}
|
||||
}
|
||||
if config != nil && len(config.ConstExprFns) > 0 {
|
||||
for limit := 100; limit >= 0; limit-- {
|
||||
constExpr := &constExpr{
|
||||
fns: config.ConstExprFns,
|
||||
}
|
||||
Walk(node, constExpr)
|
||||
if constExpr.err != nil {
|
||||
return constExpr.err
|
||||
}
|
||||
if !constExpr.applied {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Walk(node, &inRange{})
|
||||
Walk(node, &constRange{})
|
||||
return nil
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/antonmedv/expr/file"
|
||||
)
|
||||
|
||||
func Lex(source *file.Source) ([]Token, error) {
|
||||
l := &lexer{
|
||||
input: source.Content(),
|
||||
tokens: make([]Token, 0),
|
||||
}
|
||||
|
||||
l.loc = file.Location{1, 0}
|
||||
l.prev = l.loc
|
||||
l.startLoc = l.loc
|
||||
|
||||
for state := root; state != nil; {
|
||||
state = state(l)
|
||||
}
|
||||
|
||||
if l.err != nil {
|
||||
return nil, l.err.Bind(source)
|
||||
}
|
||||
|
||||
return l.tokens, nil
|
||||
}
|
||||
|
||||
type lexer struct {
|
||||
input string
|
||||
state stateFn
|
||||
tokens []Token
|
||||
start, end int // current position in input
|
||||
width int // last rune with
|
||||
startLoc file.Location // start location
|
||||
prev, loc file.Location // prev location of end location, end location
|
||||
err *file.Error
|
||||
}
|
||||
|
||||
const eof rune = -1
|
||||
|
||||
func (l *lexer) next() rune {
|
||||
if l.end >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
r, w := utf8.DecodeRuneInString(l.input[l.end:])
|
||||
l.width = w
|
||||
l.end += w
|
||||
|
||||
l.prev = l.loc
|
||||
if r == '\n' {
|
||||
l.loc.Line++
|
||||
l.loc.Column = 0
|
||||
} else {
|
||||
l.loc.Column++
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *lexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *lexer) backup() {
|
||||
l.end -= l.width
|
||||
l.loc = l.prev
|
||||
}
|
||||
|
||||
func (l *lexer) emit(t Kind) {
|
||||
l.emitValue(t, l.word())
|
||||
}
|
||||
|
||||
func (l *lexer) emitValue(t Kind, value string) {
|
||||
l.tokens = append(l.tokens, Token{
|
||||
Location: l.startLoc,
|
||||
Kind: t,
|
||||
Value: value,
|
||||
})
|
||||
l.start = l.end
|
||||
l.startLoc = l.loc
|
||||
}
|
||||
|
||||
func (l *lexer) emitEOF() {
|
||||
l.tokens = append(l.tokens, Token{
|
||||
Location: l.prev, // Point to previous position for better error messages.
|
||||
Kind: EOF,
|
||||
})
|
||||
l.start = l.end
|
||||
l.startLoc = l.loc
|
||||
}
|
||||
|
||||
func (l *lexer) word() string {
|
||||
return l.input[l.start:l.end]
|
||||
}
|
||||
|
||||
func (l *lexer) ignore() {
|
||||
l.start = l.end
|
||||
l.startLoc = l.loc
|
||||
}
|
||||
|
||||
func (l *lexer) accept(valid string) bool {
|
||||
if strings.ContainsRune(valid, l.next()) {
|
||||
return true
|
||||
}
|
||||
l.backup()
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *lexer) acceptRun(valid string) {
|
||||
for strings.ContainsRune(valid, l.next()) {
|
||||
}
|
||||
l.backup()
|
||||
}
|
||||
|
||||
func (l *lexer) acceptWord(word string) bool {
|
||||
pos := l.end
|
||||
loc := l.loc
|
||||
prev := l.prev
|
||||
for _, ch := range word {
|
||||
if l.next() != ch {
|
||||
l.end = pos
|
||||
l.loc = loc
|
||||
l.prev = prev
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *lexer) error(format string, args ...interface{}) stateFn {
|
||||
if l.err == nil { // show first error
|
||||
l.err = &file.Error{
|
||||
Location: l.loc,
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func digitVal(ch rune) int {
|
||||
switch {
|
||||
case '0' <= ch && ch <= '9':
|
||||
return int(ch - '0')
|
||||
case 'a' <= lower(ch) && lower(ch) <= 'f':
|
||||
return int(lower(ch) - 'a' + 10)
|
||||
}
|
||||
return 16 // larger than any legal digit val
|
||||
}
|
||||
|
||||
func lower(ch rune) rune { return ('a' - 'A') | ch } // returns lower-case ch iff ch is ASCII letter
|
||||
|
||||
func (l *lexer) scanDigits(ch rune, base, n int) rune {
|
||||
for n > 0 && digitVal(ch) < base {
|
||||
ch = l.next()
|
||||
n--
|
||||
}
|
||||
if n > 0 {
|
||||
l.error("invalid char escape")
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (l *lexer) scanEscape(quote rune) rune {
|
||||
ch := l.next() // read character after '/'
|
||||
switch ch {
|
||||
case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', quote:
|
||||
// nothing to do
|
||||
ch = l.next()
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7':
|
||||
ch = l.scanDigits(ch, 8, 3)
|
||||
case 'x':
|
||||
ch = l.scanDigits(l.next(), 16, 2)
|
||||
case 'u':
|
||||
ch = l.scanDigits(l.next(), 16, 4)
|
||||
case 'U':
|
||||
ch = l.scanDigits(l.next(), 16, 8)
|
||||
default:
|
||||
l.error("invalid char escape")
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (l *lexer) scanString(quote rune) (n int) {
|
||||
ch := l.next() // read character after quote
|
||||
for ch != quote {
|
||||
if ch == '\n' || ch == eof {
|
||||
l.error("literal not terminated")
|
||||
return
|
||||
}
|
||||
if ch == '\\' {
|
||||
ch = l.scanEscape(quote)
|
||||
} else {
|
||||
ch = l.next()
|
||||
}
|
||||
n++
|
||||
}
|
||||
return
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type stateFn func(*lexer) stateFn
|
||||
|
||||
func root(l *lexer) stateFn {
|
||||
switch r := l.next(); {
|
||||
case r == eof:
|
||||
l.emitEOF()
|
||||
return nil
|
||||
case IsSpace(r):
|
||||
l.ignore()
|
||||
return root
|
||||
case r == '\'' || r == '"':
|
||||
l.scanString(r)
|
||||
str, err := unescape(l.word())
|
||||
if err != nil {
|
||||
l.error("%v", err)
|
||||
}
|
||||
l.emitValue(String, str)
|
||||
case '0' <= r && r <= '9':
|
||||
l.backup()
|
||||
return number
|
||||
case strings.ContainsRune("([{", r):
|
||||
l.emit(Bracket)
|
||||
case strings.ContainsRune(")]}", r):
|
||||
l.emit(Bracket)
|
||||
case strings.ContainsRune("#,?:%+-/", r): // single rune operator
|
||||
l.emit(Operator)
|
||||
case strings.ContainsRune("&|!=*<>", r): // possible double rune operator
|
||||
l.accept("&|=*")
|
||||
l.emit(Operator)
|
||||
case r == '.':
|
||||
l.backup()
|
||||
return dot
|
||||
case IsAlphaNumeric(r):
|
||||
l.backup()
|
||||
return identifier
|
||||
default:
|
||||
return l.error("unrecognized character: %#U", r)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
func number(l *lexer) stateFn {
|
||||
if !l.scanNumber() {
|
||||
return l.error("bad number syntax: %q", l.word())
|
||||
}
|
||||
l.emit(Number)
|
||||
return root
|
||||
}
|
||||
|
||||
func (l *lexer) scanNumber() bool {
|
||||
digits := "0123456789_"
|
||||
// Is it hex?
|
||||
if l.accept("0") {
|
||||
// Note: Leading 0 does not mean octal in floats.
|
||||
if l.accept("xX") {
|
||||
digits = "0123456789abcdefABCDEF_"
|
||||
} else if l.accept("oO") {
|
||||
digits = "01234567_"
|
||||
} else if l.accept("bB") {
|
||||
digits = "01_"
|
||||
}
|
||||
}
|
||||
l.acceptRun(digits)
|
||||
loc, prev, end := l.loc, l.prev, l.end
|
||||
if l.accept(".") {
|
||||
// Lookup for .. operator: if after dot there is another dot (1..2), it maybe a range operator.
|
||||
if l.peek() == '.' {
|
||||
// We can't backup() here, as it would require two backups,
|
||||
// and backup() func supports only one for now. So, save and
|
||||
// restore it here.
|
||||
l.loc, l.prev, l.end = loc, prev, end
|
||||
return true
|
||||
}
|
||||
l.acceptRun(digits)
|
||||
}
|
||||
if l.accept("eE") {
|
||||
l.accept("+-")
|
||||
l.acceptRun(digits)
|
||||
}
|
||||
// Next thing mustn't be alphanumeric.
|
||||
if IsAlphaNumeric(l.peek()) {
|
||||
l.next()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func dot(l *lexer) stateFn {
|
||||
l.next()
|
||||
if l.accept("0123456789") {
|
||||
l.backup()
|
||||
return number
|
||||
}
|
||||
l.accept(".")
|
||||
l.emit(Operator)
|
||||
return root
|
||||
}
|
||||
|
||||
func identifier(l *lexer) stateFn {
|
||||
loop:
|
||||
for {
|
||||
switch r := l.next(); {
|
||||
case IsAlphaNumeric(r):
|
||||
// absorb
|
||||
default:
|
||||
l.backup()
|
||||
switch l.word() {
|
||||
case "not":
|
||||
return not
|
||||
case "in", "or", "and", "matches", "contains", "startsWith", "endsWith":
|
||||
l.emit(Operator)
|
||||
default:
|
||||
l.emit(Identifier)
|
||||
}
|
||||
break loop
|
||||
}
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
func not(l *lexer) stateFn {
|
||||
if l.acceptWord(" in") {
|
||||
l.emit(Operator)
|
||||
} else {
|
||||
l.emit(Operator)
|
||||
}
|
||||
return root
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/antonmedv/expr/file"
|
||||
)
|
||||
|
||||
type Kind string
|
||||
|
||||
const (
|
||||
Identifier Kind = "Identifier"
|
||||
Number = "Number"
|
||||
String = "String"
|
||||
Operator = "Operator"
|
||||
Bracket = "Bracket"
|
||||
EOF = "EOF"
|
||||
)
|
||||
|
||||
type Token struct {
|
||||
file.Location
|
||||
Kind Kind
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t Token) String() string {
|
||||
if t.Value == "" {
|
||||
return string(t.Kind)
|
||||
}
|
||||
return fmt.Sprintf("%s(%#v)", t.Kind, t.Value)
|
||||
}
|
||||
|
||||
func (t Token) Is(kind Kind, values ...string) bool {
|
||||
if len(values) == 0 {
|
||||
return kind == t.Kind
|
||||
}
|
||||
|
||||
for _, v := range values {
|
||||
if v == t.Value {
|
||||
goto found
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
found:
|
||||
return kind == t.Kind
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func IsSpace(r rune) bool {
|
||||
return unicode.IsSpace(r)
|
||||
}
|
||||
|
||||
func IsAlphaNumeric(r rune) bool {
|
||||
return IsAlphabetic(r) || unicode.IsDigit(r)
|
||||
}
|
||||
|
||||
func IsAlphabetic(r rune) bool {
|
||||
return r == '_' || r == '$' || unicode.IsLetter(r)
|
||||
}
|
||||
|
||||
var (
|
||||
newlineNormalizer = strings.NewReplacer("\r\n", "\n", "\r", "\n")
|
||||
)
|
||||
|
||||
// Unescape takes a quoted string, unquotes, and unescapes it.
|
||||
func unescape(value string) (string, error) {
|
||||
// All strings normalize newlines to the \n representation.
|
||||
value = newlineNormalizer.Replace(value)
|
||||
n := len(value)
|
||||
|
||||
// Nothing to unescape / decode.
|
||||
if n < 2 {
|
||||
return value, fmt.Errorf("unable to unescape string")
|
||||
}
|
||||
|
||||
// Quoted string of some form, must have same first and last char.
|
||||
if value[0] != value[n-1] || (value[0] != '"' && value[0] != '\'') {
|
||||
return value, fmt.Errorf("unable to unescape string")
|
||||
}
|
||||
|
||||
value = value[1 : n-1]
|
||||
|
||||
// The string contains escape characters.
|
||||
// The following logic is adapted from `strconv/quote.go`
|
||||
var runeTmp [utf8.UTFMax]byte
|
||||
buf := make([]byte, 0, 3*n/2)
|
||||
for len(value) > 0 {
|
||||
c, multibyte, rest, err := unescapeChar(value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
value = rest
|
||||
if c < utf8.RuneSelf || !multibyte {
|
||||
buf = append(buf, byte(c))
|
||||
} else {
|
||||
n := utf8.EncodeRune(runeTmp[:], c)
|
||||
buf = append(buf, runeTmp[:n]...)
|
||||
}
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
// unescapeChar takes a string input and returns the following info:
|
||||
//
|
||||
// value - the escaped unicode rune at the front of the string.
|
||||
// multibyte - whether the rune value might require multiple bytes to represent.
|
||||
// tail - the remainder of the input string.
|
||||
// err - error value, if the character could not be unescaped.
|
||||
//
|
||||
// When multibyte is true the return value may still fit within a single byte,
|
||||
// but a multibyte conversion is attempted which is more expensive than when the
|
||||
// value is known to fit within one byte.
|
||||
func unescapeChar(s string) (value rune, multibyte bool, tail string, err error) {
|
||||
// 1. Character is not an escape sequence.
|
||||
switch c := s[0]; {
|
||||
case c >= utf8.RuneSelf:
|
||||
r, size := utf8.DecodeRuneInString(s)
|
||||
return r, true, s[size:], nil
|
||||
case c != '\\':
|
||||
return rune(s[0]), false, s[1:], nil
|
||||
}
|
||||
|
||||
// 2. Last character is the start of an escape sequence.
|
||||
if len(s) <= 1 {
|
||||
err = fmt.Errorf("unable to unescape string, found '\\' as last character")
|
||||
return
|
||||
}
|
||||
|
||||
c := s[1]
|
||||
s = s[2:]
|
||||
// 3. Common escape sequences shared with Google SQL
|
||||
switch c {
|
||||
case 'a':
|
||||
value = '\a'
|
||||
case 'b':
|
||||
value = '\b'
|
||||
case 'f':
|
||||
value = '\f'
|
||||
case 'n':
|
||||
value = '\n'
|
||||
case 'r':
|
||||
value = '\r'
|
||||
case 't':
|
||||
value = '\t'
|
||||
case 'v':
|
||||
value = '\v'
|
||||
case '\\':
|
||||
value = '\\'
|
||||
case '\'':
|
||||
value = '\''
|
||||
case '"':
|
||||
value = '"'
|
||||
case '`':
|
||||
value = '`'
|
||||
case '?':
|
||||
value = '?'
|
||||
|
||||
// 4. Unicode escape sequences, reproduced from `strconv/quote.go`
|
||||
case 'x', 'X', 'u', 'U':
|
||||
n := 0
|
||||
switch c {
|
||||
case 'x', 'X':
|
||||
n = 2
|
||||
case 'u':
|
||||
n = 4
|
||||
case 'U':
|
||||
n = 8
|
||||
}
|
||||
var v rune
|
||||
if len(s) < n {
|
||||
err = fmt.Errorf("unable to unescape string")
|
||||
return
|
||||
}
|
||||
for j := 0; j < n; j++ {
|
||||
x, ok := unhex(s[j])
|
||||
if !ok {
|
||||
err = fmt.Errorf("unable to unescape string")
|
||||
return
|
||||
}
|
||||
v = v<<4 | x
|
||||
}
|
||||
s = s[n:]
|
||||
if v > utf8.MaxRune {
|
||||
err = fmt.Errorf("unable to unescape string")
|
||||
return
|
||||
}
|
||||
value = v
|
||||
multibyte = true
|
||||
|
||||
// 5. Octal escape sequences, must be three digits \[0-3][0-7][0-7]
|
||||
case '0', '1', '2', '3':
|
||||
if len(s) < 2 {
|
||||
err = fmt.Errorf("unable to unescape octal sequence in string")
|
||||
return
|
||||
}
|
||||
v := rune(c - '0')
|
||||
for j := 0; j < 2; j++ {
|
||||
x := s[j]
|
||||
if x < '0' || x > '7' {
|
||||
err = fmt.Errorf("unable to unescape octal sequence in string")
|
||||
return
|
||||
}
|
||||
v = v*8 + rune(x-'0')
|
||||
}
|
||||
if v > utf8.MaxRune {
|
||||
err = fmt.Errorf("unable to unescape string")
|
||||
return
|
||||
}
|
||||
value = v
|
||||
s = s[2:]
|
||||
multibyte = true
|
||||
|
||||
// Unknown escape sequence.
|
||||
default:
|
||||
err = fmt.Errorf("unable to unescape string")
|
||||
}
|
||||
|
||||
tail = s
|
||||
return
|
||||
}
|
||||
|
||||
func unhex(b byte) (rune, bool) {
|
||||
c := rune(b)
|
||||
switch {
|
||||
case '0' <= c && c <= '9':
|
||||
return c - '0', true
|
||||
case 'a' <= c && c <= 'f':
|
||||
return c - 'a' + 10, true
|
||||
case 'A' <= c && c <= 'F':
|
||||
return c - 'A' + 10, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
+579
@@ -0,0 +1,579 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/file"
|
||||
. "github.com/antonmedv/expr/parser/lexer"
|
||||
)
|
||||
|
||||
type associativity int
|
||||
|
||||
const (
|
||||
left associativity = iota + 1
|
||||
right
|
||||
)
|
||||
|
||||
type operator struct {
|
||||
precedence int
|
||||
associativity associativity
|
||||
}
|
||||
|
||||
type builtin struct {
|
||||
arity int
|
||||
}
|
||||
|
||||
var unaryOperators = map[string]operator{
|
||||
"not": {50, left},
|
||||
"!": {50, left},
|
||||
"-": {500, left},
|
||||
"+": {500, left},
|
||||
}
|
||||
|
||||
var binaryOperators = map[string]operator{
|
||||
"or": {10, left},
|
||||
"||": {10, left},
|
||||
"and": {15, left},
|
||||
"&&": {15, left},
|
||||
"==": {20, left},
|
||||
"!=": {20, left},
|
||||
"<": {20, left},
|
||||
">": {20, left},
|
||||
">=": {20, left},
|
||||
"<=": {20, left},
|
||||
"not in": {20, left},
|
||||
"in": {20, left},
|
||||
"matches": {20, left},
|
||||
"contains": {20, left},
|
||||
"startsWith": {20, left},
|
||||
"endsWith": {20, left},
|
||||
"..": {25, left},
|
||||
"+": {30, left},
|
||||
"-": {30, left},
|
||||
"*": {60, left},
|
||||
"/": {60, left},
|
||||
"%": {60, left},
|
||||
"**": {70, right},
|
||||
}
|
||||
|
||||
var builtins = map[string]builtin{
|
||||
"len": {1},
|
||||
"all": {2},
|
||||
"none": {2},
|
||||
"any": {2},
|
||||
"one": {2},
|
||||
"filter": {2},
|
||||
"map": {2},
|
||||
"count": {2},
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
tokens []Token
|
||||
current Token
|
||||
pos int
|
||||
err *file.Error
|
||||
depth int // closure call depth
|
||||
}
|
||||
|
||||
type Tree struct {
|
||||
Node Node
|
||||
Source *file.Source
|
||||
}
|
||||
|
||||
func Parse(input string) (*Tree, error) {
|
||||
source := file.NewSource(input)
|
||||
|
||||
tokens, err := Lex(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &parser{
|
||||
tokens: tokens,
|
||||
current: tokens[0],
|
||||
}
|
||||
|
||||
node := p.parseExpression(0)
|
||||
|
||||
if !p.current.Is(EOF) {
|
||||
p.error("unexpected token %v", p.current)
|
||||
}
|
||||
|
||||
if p.err != nil {
|
||||
return nil, p.err.Bind(source)
|
||||
}
|
||||
|
||||
return &Tree{
|
||||
Node: node,
|
||||
Source: source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *parser) error(format string, args ...interface{}) {
|
||||
if p.err == nil { // show first error
|
||||
p.err = &file.Error{
|
||||
Location: p.current.Location,
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) next() {
|
||||
p.pos++
|
||||
if p.pos >= len(p.tokens) {
|
||||
p.error("unexpected end of expression")
|
||||
return
|
||||
}
|
||||
p.current = p.tokens[p.pos]
|
||||
}
|
||||
|
||||
func (p *parser) expect(kind Kind, values ...string) {
|
||||
if p.current.Is(kind, values...) {
|
||||
p.next()
|
||||
return
|
||||
}
|
||||
p.error("unexpected token %v", p.current)
|
||||
}
|
||||
|
||||
// parse functions
|
||||
|
||||
func (p *parser) parseExpression(precedence int) Node {
|
||||
nodeLeft := p.parsePrimary()
|
||||
|
||||
token := p.current
|
||||
for token.Is(Operator) && p.err == nil {
|
||||
if op, ok := binaryOperators[token.Value]; ok {
|
||||
if op.precedence >= precedence {
|
||||
p.next()
|
||||
|
||||
var nodeRight Node
|
||||
if op.associativity == left {
|
||||
nodeRight = p.parseExpression(op.precedence + 1)
|
||||
} else {
|
||||
nodeRight = p.parseExpression(op.precedence)
|
||||
}
|
||||
|
||||
if token.Is(Operator, "matches") {
|
||||
var r *regexp.Regexp
|
||||
var err error
|
||||
|
||||
if s, ok := nodeRight.(*StringNode); ok {
|
||||
r, err = regexp.Compile(s.Value)
|
||||
if err != nil {
|
||||
p.error("%v", err)
|
||||
}
|
||||
}
|
||||
nodeLeft = &MatchesNode{
|
||||
Regexp: r,
|
||||
Left: nodeLeft,
|
||||
Right: nodeRight,
|
||||
}
|
||||
nodeLeft.SetLocation(token.Location)
|
||||
} else {
|
||||
nodeLeft = &BinaryNode{
|
||||
Operator: token.Value,
|
||||
Left: nodeLeft,
|
||||
Right: nodeRight,
|
||||
}
|
||||
nodeLeft.SetLocation(token.Location)
|
||||
}
|
||||
token = p.current
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if precedence == 0 {
|
||||
nodeLeft = p.parseConditionalExpression(nodeLeft)
|
||||
}
|
||||
|
||||
return nodeLeft
|
||||
}
|
||||
|
||||
func (p *parser) parsePrimary() Node {
|
||||
token := p.current
|
||||
|
||||
if token.Is(Operator) {
|
||||
if op, ok := unaryOperators[token.Value]; ok {
|
||||
p.next()
|
||||
expr := p.parseExpression(op.precedence)
|
||||
node := &UnaryNode{
|
||||
Operator: token.Value,
|
||||
Node: expr,
|
||||
}
|
||||
node.SetLocation(token.Location)
|
||||
return p.parsePostfixExpression(node)
|
||||
}
|
||||
}
|
||||
|
||||
if token.Is(Bracket, "(") {
|
||||
p.next()
|
||||
expr := p.parseExpression(0)
|
||||
p.expect(Bracket, ")") // "an opened parenthesis is not properly closed"
|
||||
return p.parsePostfixExpression(expr)
|
||||
}
|
||||
|
||||
if p.depth > 0 {
|
||||
if token.Is(Operator, "#") || token.Is(Operator, ".") {
|
||||
if token.Is(Operator, "#") {
|
||||
p.next()
|
||||
}
|
||||
node := &PointerNode{}
|
||||
node.SetLocation(token.Location)
|
||||
return p.parsePostfixExpression(node)
|
||||
}
|
||||
} else {
|
||||
if token.Is(Operator, "#") || token.Is(Operator, ".") {
|
||||
p.error("cannot use pointer accessor outside closure")
|
||||
}
|
||||
}
|
||||
|
||||
return p.parsePrimaryExpression()
|
||||
}
|
||||
|
||||
func (p *parser) parseConditionalExpression(node Node) Node {
|
||||
var expr1, expr2 Node
|
||||
for p.current.Is(Operator, "?") && p.err == nil {
|
||||
p.next()
|
||||
|
||||
if !p.current.Is(Operator, ":") {
|
||||
expr1 = p.parseExpression(0)
|
||||
p.expect(Operator, ":")
|
||||
expr2 = p.parseExpression(0)
|
||||
} else {
|
||||
p.next()
|
||||
expr1 = node
|
||||
expr2 = p.parseExpression(0)
|
||||
}
|
||||
|
||||
node = &ConditionalNode{
|
||||
Cond: node,
|
||||
Exp1: expr1,
|
||||
Exp2: expr2,
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func (p *parser) parsePrimaryExpression() Node {
|
||||
var node Node
|
||||
token := p.current
|
||||
|
||||
switch token.Kind {
|
||||
|
||||
case Identifier:
|
||||
p.next()
|
||||
switch token.Value {
|
||||
case "true":
|
||||
node := &BoolNode{Value: true}
|
||||
node.SetLocation(token.Location)
|
||||
return node
|
||||
case "false":
|
||||
node := &BoolNode{Value: false}
|
||||
node.SetLocation(token.Location)
|
||||
return node
|
||||
case "nil":
|
||||
node := &NilNode{}
|
||||
node.SetLocation(token.Location)
|
||||
return node
|
||||
default:
|
||||
node = p.parseIdentifierExpression(token)
|
||||
}
|
||||
|
||||
case Number:
|
||||
p.next()
|
||||
value := strings.Replace(token.Value, "_", "", -1)
|
||||
if strings.ContainsAny(value, ".eE") {
|
||||
number, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
p.error("invalid float literal: %v", err)
|
||||
}
|
||||
node := &FloatNode{Value: number}
|
||||
node.SetLocation(token.Location)
|
||||
return node
|
||||
} else if strings.Contains(value, "x") {
|
||||
number, err := strconv.ParseInt(value, 0, 64)
|
||||
if err != nil {
|
||||
p.error("invalid hex literal: %v", err)
|
||||
}
|
||||
node := &IntegerNode{Value: int(number)}
|
||||
node.SetLocation(token.Location)
|
||||
return node
|
||||
} else {
|
||||
number, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
p.error("invalid integer literal: %v", err)
|
||||
}
|
||||
node := &IntegerNode{Value: int(number)}
|
||||
node.SetLocation(token.Location)
|
||||
return node
|
||||
}
|
||||
|
||||
case String:
|
||||
p.next()
|
||||
node := &StringNode{Value: token.Value}
|
||||
node.SetLocation(token.Location)
|
||||
return node
|
||||
|
||||
default:
|
||||
if token.Is(Bracket, "[") {
|
||||
node = p.parseArrayExpression(token)
|
||||
} else if token.Is(Bracket, "{") {
|
||||
node = p.parseMapExpression(token)
|
||||
} else {
|
||||
p.error("unexpected token %v", token)
|
||||
}
|
||||
}
|
||||
|
||||
return p.parsePostfixExpression(node)
|
||||
}
|
||||
|
||||
func (p *parser) parseIdentifierExpression(token Token) Node {
|
||||
var node Node
|
||||
if p.current.Is(Bracket, "(") {
|
||||
var arguments []Node
|
||||
|
||||
if b, ok := builtins[token.Value]; ok {
|
||||
p.expect(Bracket, "(")
|
||||
// TODO: Add builtins signatures.
|
||||
if b.arity == 1 {
|
||||
arguments = make([]Node, 1)
|
||||
arguments[0] = p.parseExpression(0)
|
||||
} else if b.arity == 2 {
|
||||
arguments = make([]Node, 2)
|
||||
arguments[0] = p.parseExpression(0)
|
||||
p.expect(Operator, ",")
|
||||
arguments[1] = p.parseClosure()
|
||||
}
|
||||
p.expect(Bracket, ")")
|
||||
|
||||
node = &BuiltinNode{
|
||||
Name: token.Value,
|
||||
Arguments: arguments,
|
||||
}
|
||||
node.SetLocation(token.Location)
|
||||
} else {
|
||||
arguments = p.parseArguments()
|
||||
node = &FunctionNode{
|
||||
Name: token.Value,
|
||||
Arguments: arguments,
|
||||
}
|
||||
node.SetLocation(token.Location)
|
||||
}
|
||||
} else {
|
||||
node = &IdentifierNode{Value: token.Value}
|
||||
node.SetLocation(token.Location)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func (p *parser) parseClosure() Node {
|
||||
token := p.current
|
||||
p.expect(Bracket, "{")
|
||||
|
||||
p.depth++
|
||||
node := p.parseExpression(0)
|
||||
p.depth--
|
||||
|
||||
p.expect(Bracket, "}")
|
||||
closure := &ClosureNode{
|
||||
Node: node,
|
||||
}
|
||||
closure.SetLocation(token.Location)
|
||||
return closure
|
||||
}
|
||||
|
||||
func (p *parser) parseArrayExpression(token Token) Node {
|
||||
nodes := make([]Node, 0)
|
||||
|
||||
p.expect(Bracket, "[")
|
||||
for !p.current.Is(Bracket, "]") && p.err == nil {
|
||||
if len(nodes) > 0 {
|
||||
p.expect(Operator, ",")
|
||||
if p.current.Is(Bracket, "]") {
|
||||
goto end
|
||||
}
|
||||
}
|
||||
node := p.parseExpression(0)
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
end:
|
||||
p.expect(Bracket, "]")
|
||||
|
||||
node := &ArrayNode{Nodes: nodes}
|
||||
node.SetLocation(token.Location)
|
||||
return node
|
||||
}
|
||||
|
||||
func (p *parser) parseMapExpression(token Token) Node {
|
||||
p.expect(Bracket, "{")
|
||||
|
||||
nodes := make([]Node, 0)
|
||||
for !p.current.Is(Bracket, "}") && p.err == nil {
|
||||
if len(nodes) > 0 {
|
||||
p.expect(Operator, ",")
|
||||
if p.current.Is(Bracket, "}") {
|
||||
goto end
|
||||
}
|
||||
if p.current.Is(Operator, ",") {
|
||||
p.error("unexpected token %v", p.current)
|
||||
}
|
||||
}
|
||||
|
||||
var key Node
|
||||
// a map key can be:
|
||||
// * a number
|
||||
// * a string
|
||||
// * a identifier, which is equivalent to a string
|
||||
// * an expression, which must be enclosed in parentheses -- (1 + 2)
|
||||
if p.current.Is(Number) || p.current.Is(String) || p.current.Is(Identifier) {
|
||||
key = &StringNode{Value: p.current.Value}
|
||||
key.SetLocation(token.Location)
|
||||
p.next()
|
||||
} else if p.current.Is(Bracket, "(") {
|
||||
key = p.parseExpression(0)
|
||||
} else {
|
||||
p.error("a map key must be a quoted string, a number, a identifier, or an expression enclosed in parentheses (unexpected token %v)", p.current)
|
||||
}
|
||||
|
||||
p.expect(Operator, ":")
|
||||
|
||||
node := p.parseExpression(0)
|
||||
pair := &PairNode{Key: key, Value: node}
|
||||
pair.SetLocation(token.Location)
|
||||
nodes = append(nodes, pair)
|
||||
}
|
||||
|
||||
end:
|
||||
p.expect(Bracket, "}")
|
||||
|
||||
node := &MapNode{Pairs: nodes}
|
||||
node.SetLocation(token.Location)
|
||||
return node
|
||||
}
|
||||
|
||||
func (p *parser) parsePostfixExpression(node Node) Node {
|
||||
token := p.current
|
||||
for (token.Is(Operator) || token.Is(Bracket)) && p.err == nil {
|
||||
if token.Value == "." {
|
||||
p.next()
|
||||
|
||||
token = p.current
|
||||
p.next()
|
||||
|
||||
if token.Kind != Identifier &&
|
||||
// Operators like "not" and "matches" are valid methods or property names.
|
||||
(token.Kind != Operator || !isValidIdentifier(token.Value)) {
|
||||
p.error("expected name")
|
||||
}
|
||||
|
||||
if p.current.Is(Bracket, "(") {
|
||||
arguments := p.parseArguments()
|
||||
node = &MethodNode{
|
||||
Node: node,
|
||||
Method: token.Value,
|
||||
Arguments: arguments,
|
||||
}
|
||||
node.SetLocation(token.Location)
|
||||
} else {
|
||||
node = &PropertyNode{
|
||||
Node: node,
|
||||
Property: token.Value,
|
||||
}
|
||||
node.SetLocation(token.Location)
|
||||
}
|
||||
|
||||
} else if token.Value == "[" {
|
||||
p.next()
|
||||
var from, to Node
|
||||
|
||||
if p.current.Is(Operator, ":") { // slice without from [:1]
|
||||
p.next()
|
||||
|
||||
if !p.current.Is(Bracket, "]") { // slice without from and to [:]
|
||||
to = p.parseExpression(0)
|
||||
}
|
||||
|
||||
node = &SliceNode{
|
||||
Node: node,
|
||||
To: to,
|
||||
}
|
||||
node.SetLocation(token.Location)
|
||||
p.expect(Bracket, "]")
|
||||
|
||||
} else {
|
||||
|
||||
from = p.parseExpression(0)
|
||||
|
||||
if p.current.Is(Operator, ":") {
|
||||
p.next()
|
||||
|
||||
if !p.current.Is(Bracket, "]") { // slice without to [1:]
|
||||
to = p.parseExpression(0)
|
||||
}
|
||||
|
||||
node = &SliceNode{
|
||||
Node: node,
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
node.SetLocation(token.Location)
|
||||
p.expect(Bracket, "]")
|
||||
|
||||
} else {
|
||||
// Slice operator [:] was not found, it should by just index node.
|
||||
|
||||
node = &IndexNode{
|
||||
Node: node,
|
||||
Index: from,
|
||||
}
|
||||
node.SetLocation(token.Location)
|
||||
p.expect(Bracket, "]")
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
token = p.current
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func isValidIdentifier(str string) bool {
|
||||
if len(str) == 0 {
|
||||
return false
|
||||
}
|
||||
h, w := utf8.DecodeRuneInString(str)
|
||||
if !IsAlphabetic(h) {
|
||||
return false
|
||||
}
|
||||
for _, r := range str[w:] {
|
||||
if !IsAlphaNumeric(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *parser) parseArguments() []Node {
|
||||
p.expect(Bracket, "(")
|
||||
nodes := make([]Node, 0)
|
||||
for !p.current.Is(Bracket, ")") && p.err == nil {
|
||||
if len(nodes) > 0 {
|
||||
p.expect(Operator, ",")
|
||||
}
|
||||
node := p.parseExpression(0)
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
p.expect(Bracket, ")")
|
||||
|
||||
return nodes
|
||||
}
|
||||
+3247
File diff suppressed because it is too large
Load Diff
+53
@@ -0,0 +1,53 @@
|
||||
package vm
|
||||
|
||||
const (
|
||||
OpPush byte = iota
|
||||
OpPop
|
||||
OpRot
|
||||
OpFetch
|
||||
OpFetchMap
|
||||
OpTrue
|
||||
OpFalse
|
||||
OpNil
|
||||
OpNegate
|
||||
OpNot
|
||||
OpEqual
|
||||
OpEqualInt
|
||||
OpEqualString
|
||||
OpJump
|
||||
OpJumpIfTrue
|
||||
OpJumpIfFalse
|
||||
OpJumpBackward
|
||||
OpIn
|
||||
OpLess
|
||||
OpMore
|
||||
OpLessOrEqual
|
||||
OpMoreOrEqual
|
||||
OpAdd
|
||||
OpSubtract
|
||||
OpMultiply
|
||||
OpDivide
|
||||
OpModulo
|
||||
OpExponent
|
||||
OpRange
|
||||
OpMatches
|
||||
OpMatchesConst
|
||||
OpContains
|
||||
OpStartsWith
|
||||
OpEndsWith
|
||||
OpIndex
|
||||
OpSlice
|
||||
OpProperty
|
||||
OpCall
|
||||
OpCallFast
|
||||
OpMethod
|
||||
OpArray
|
||||
OpMap
|
||||
OpLen
|
||||
OpCast
|
||||
OpStore
|
||||
OpLoad
|
||||
OpInc
|
||||
OpBegin
|
||||
OpEnd // This opcode must be at the end of this list.
|
||||
)
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/antonmedv/expr/file"
|
||||
)
|
||||
|
||||
type Program struct {
|
||||
Source *file.Source
|
||||
Locations map[int]file.Location
|
||||
Constants []interface{}
|
||||
Bytecode []byte
|
||||
}
|
||||
|
||||
func (program *Program) Disassemble() string {
|
||||
out := ""
|
||||
ip := 0
|
||||
for ip < len(program.Bytecode) {
|
||||
pp := ip
|
||||
op := program.Bytecode[ip]
|
||||
ip++
|
||||
|
||||
readArg := func() uint16 {
|
||||
if ip+1 >= len(program.Bytecode) {
|
||||
return 0
|
||||
}
|
||||
|
||||
i := binary.LittleEndian.Uint16([]byte{program.Bytecode[ip], program.Bytecode[ip+1]})
|
||||
ip += 2
|
||||
return i
|
||||
}
|
||||
|
||||
code := func(label string) {
|
||||
out += fmt.Sprintf("%v\t%v\n", pp, label)
|
||||
}
|
||||
jump := func(label string) {
|
||||
a := readArg()
|
||||
out += fmt.Sprintf("%v\t%v\t%v\t(%v)\n", pp, label, a, ip+int(a))
|
||||
}
|
||||
back := func(label string) {
|
||||
a := readArg()
|
||||
out += fmt.Sprintf("%v\t%v\t%v\t(%v)\n", pp, label, a, ip-int(a))
|
||||
}
|
||||
argument := func(label string) {
|
||||
a := readArg()
|
||||
out += fmt.Sprintf("%v\t%v\t%v\n", pp, label, a)
|
||||
}
|
||||
constant := func(label string) {
|
||||
a := readArg()
|
||||
var c interface{}
|
||||
if int(a) < len(program.Constants) {
|
||||
c = program.Constants[a]
|
||||
}
|
||||
if r, ok := c.(*regexp.Regexp); ok {
|
||||
c = r.String()
|
||||
}
|
||||
out += fmt.Sprintf("%v\t%v\t%v\t%#v\n", pp, label, a, c)
|
||||
}
|
||||
|
||||
switch op {
|
||||
case OpPush:
|
||||
constant("OpPush")
|
||||
|
||||
case OpPop:
|
||||
code("OpPop")
|
||||
|
||||
case OpRot:
|
||||
code("OpRot")
|
||||
|
||||
case OpFetch:
|
||||
constant("OpFetch")
|
||||
|
||||
case OpFetchMap:
|
||||
constant("OpFetchMap")
|
||||
|
||||
case OpTrue:
|
||||
code("OpTrue")
|
||||
|
||||
case OpFalse:
|
||||
code("OpFalse")
|
||||
|
||||
case OpNil:
|
||||
code("OpNil")
|
||||
|
||||
case OpNegate:
|
||||
code("OpNegate")
|
||||
|
||||
case OpNot:
|
||||
code("OpNot")
|
||||
|
||||
case OpEqual:
|
||||
code("OpEqual")
|
||||
|
||||
case OpEqualInt:
|
||||
code("OpEqualInt")
|
||||
|
||||
case OpEqualString:
|
||||
code("OpEqualString")
|
||||
|
||||
case OpJump:
|
||||
jump("OpJump")
|
||||
|
||||
case OpJumpIfTrue:
|
||||
jump("OpJumpIfTrue")
|
||||
|
||||
case OpJumpIfFalse:
|
||||
jump("OpJumpIfFalse")
|
||||
|
||||
case OpJumpBackward:
|
||||
back("OpJumpBackward")
|
||||
|
||||
case OpIn:
|
||||
code("OpIn")
|
||||
|
||||
case OpLess:
|
||||
code("OpLess")
|
||||
|
||||
case OpMore:
|
||||
code("OpMore")
|
||||
|
||||
case OpLessOrEqual:
|
||||
code("OpLessOrEqual")
|
||||
|
||||
case OpMoreOrEqual:
|
||||
code("OpMoreOrEqual")
|
||||
|
||||
case OpAdd:
|
||||
code("OpAdd")
|
||||
|
||||
case OpSubtract:
|
||||
code("OpSubtract")
|
||||
|
||||
case OpMultiply:
|
||||
code("OpMultiply")
|
||||
|
||||
case OpDivide:
|
||||
code("OpDivide")
|
||||
|
||||
case OpModulo:
|
||||
code("OpModulo")
|
||||
|
||||
case OpExponent:
|
||||
code("OpExponent")
|
||||
|
||||
case OpRange:
|
||||
code("OpRange")
|
||||
|
||||
case OpMatches:
|
||||
code("OpMatches")
|
||||
|
||||
case OpMatchesConst:
|
||||
constant("OpMatchesConst")
|
||||
|
||||
case OpContains:
|
||||
code("OpContains")
|
||||
|
||||
case OpStartsWith:
|
||||
code("OpStartsWith")
|
||||
|
||||
case OpEndsWith:
|
||||
code("OpEndsWith")
|
||||
|
||||
case OpIndex:
|
||||
code("OpIndex")
|
||||
|
||||
case OpSlice:
|
||||
code("OpSlice")
|
||||
|
||||
case OpProperty:
|
||||
constant("OpProperty")
|
||||
|
||||
case OpCall:
|
||||
constant("OpCall")
|
||||
|
||||
case OpCallFast:
|
||||
constant("OpCallFast")
|
||||
|
||||
case OpMethod:
|
||||
constant("OpMethod")
|
||||
|
||||
case OpArray:
|
||||
code("OpArray")
|
||||
|
||||
case OpMap:
|
||||
code("OpMap")
|
||||
|
||||
case OpLen:
|
||||
code("OpLen")
|
||||
|
||||
case OpCast:
|
||||
argument("OpCast")
|
||||
|
||||
case OpStore:
|
||||
constant("OpStore")
|
||||
|
||||
case OpLoad:
|
||||
constant("OpLoad")
|
||||
|
||||
case OpInc:
|
||||
constant("OpInc")
|
||||
|
||||
case OpBegin:
|
||||
code("OpBegin")
|
||||
|
||||
case OpEnd:
|
||||
code("OpEnd")
|
||||
|
||||
default:
|
||||
out += fmt.Sprintf("%v\t%#x\n", pp, op)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
package vm
|
||||
|
||||
//go:generate go run ./generate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type Call struct {
|
||||
Name string
|
||||
Size int
|
||||
}
|
||||
|
||||
type Scope map[string]interface{}
|
||||
|
||||
func fetch(from interface{}, i interface{}) interface{} {
|
||||
v := reflect.ValueOf(from)
|
||||
kind := v.Kind()
|
||||
|
||||
// Structures can be access through a pointer or through a value, when they
|
||||
// are accessed through a pointer we don't want to copy them to a value.
|
||||
if kind == reflect.Ptr && reflect.Indirect(v).Kind() == reflect.Struct {
|
||||
v = reflect.Indirect(v)
|
||||
kind = v.Kind()
|
||||
}
|
||||
|
||||
switch kind {
|
||||
|
||||
case reflect.Array, reflect.Slice, reflect.String:
|
||||
value := v.Index(toInt(i))
|
||||
if value.IsValid() && value.CanInterface() {
|
||||
return value.Interface()
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
value := v.MapIndex(reflect.ValueOf(i))
|
||||
if value.IsValid() {
|
||||
if value.CanInterface() {
|
||||
return value.Interface()
|
||||
}
|
||||
} else {
|
||||
elem := reflect.TypeOf(from).Elem()
|
||||
return reflect.Zero(elem).Interface()
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
value := v.FieldByName(reflect.ValueOf(i).String())
|
||||
if value.IsValid() && value.CanInterface() {
|
||||
return value.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("cannot fetch %v from %T", i, from))
|
||||
}
|
||||
|
||||
func slice(array, from, to interface{}) interface{} {
|
||||
v := reflect.ValueOf(array)
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.String:
|
||||
length := v.Len()
|
||||
a, b := toInt(from), toInt(to)
|
||||
|
||||
if b > length {
|
||||
b = length
|
||||
}
|
||||
if a > b {
|
||||
a = b
|
||||
}
|
||||
|
||||
value := v.Slice(a, b)
|
||||
if value.IsValid() && value.CanInterface() {
|
||||
return value.Interface()
|
||||
}
|
||||
|
||||
case reflect.Ptr:
|
||||
value := v.Elem()
|
||||
if value.IsValid() && value.CanInterface() {
|
||||
return slice(value.Interface(), from, to)
|
||||
}
|
||||
|
||||
}
|
||||
panic(fmt.Sprintf("cannot slice %v", from))
|
||||
}
|
||||
|
||||
func FetchFn(from interface{}, name string) reflect.Value {
|
||||
v := reflect.ValueOf(from)
|
||||
|
||||
// Methods can be defined on any type.
|
||||
if v.NumMethod() > 0 {
|
||||
method := v.MethodByName(name)
|
||||
if method.IsValid() {
|
||||
return method
|
||||
}
|
||||
}
|
||||
|
||||
d := v
|
||||
if v.Kind() == reflect.Ptr {
|
||||
d = v.Elem()
|
||||
}
|
||||
|
||||
switch d.Kind() {
|
||||
case reflect.Map:
|
||||
value := d.MapIndex(reflect.ValueOf(name))
|
||||
if value.IsValid() && value.CanInterface() {
|
||||
return value.Elem()
|
||||
}
|
||||
case reflect.Struct:
|
||||
// If struct has not method, maybe it has func field.
|
||||
// To access this field we need dereference value.
|
||||
value := d.FieldByName(name)
|
||||
if value.IsValid() {
|
||||
return value
|
||||
}
|
||||
}
|
||||
panic(fmt.Sprintf(`cannot get "%v" from %T`, name, from))
|
||||
}
|
||||
|
||||
func in(needle interface{}, array interface{}) bool {
|
||||
if array == nil {
|
||||
return false
|
||||
}
|
||||
v := reflect.ValueOf(array)
|
||||
|
||||
switch v.Kind() {
|
||||
|
||||
case reflect.Array, reflect.Slice:
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
value := v.Index(i)
|
||||
if value.IsValid() && value.CanInterface() {
|
||||
if equal(value.Interface(), needle).(bool) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
case reflect.Map:
|
||||
n := reflect.ValueOf(needle)
|
||||
if !n.IsValid() {
|
||||
panic(fmt.Sprintf("cannot use %T as index to %T", needle, array))
|
||||
}
|
||||
value := v.MapIndex(n)
|
||||
if value.IsValid() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
case reflect.Struct:
|
||||
n := reflect.ValueOf(needle)
|
||||
if !n.IsValid() || n.Kind() != reflect.String {
|
||||
panic(fmt.Sprintf("cannot use %T as field name of %T", needle, array))
|
||||
}
|
||||
value := v.FieldByName(n.String())
|
||||
if value.IsValid() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
case reflect.Ptr:
|
||||
value := v.Elem()
|
||||
if value.IsValid() && value.CanInterface() {
|
||||
return in(needle, value.Interface())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf(`operator "in"" not defined on %T`, array))
|
||||
}
|
||||
|
||||
func length(a interface{}) int {
|
||||
v := reflect.ValueOf(a)
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
||||
return v.Len()
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid argument for len (type %T)", a))
|
||||
}
|
||||
}
|
||||
|
||||
func negate(i interface{}) interface{} {
|
||||
switch v := i.(type) {
|
||||
case float32:
|
||||
return -v
|
||||
case float64:
|
||||
return -v
|
||||
|
||||
case int:
|
||||
return -v
|
||||
case int8:
|
||||
return -v
|
||||
case int16:
|
||||
return -v
|
||||
case int32:
|
||||
return -v
|
||||
case int64:
|
||||
return -v
|
||||
|
||||
case uint:
|
||||
return -v
|
||||
case uint8:
|
||||
return -v
|
||||
case uint16:
|
||||
return -v
|
||||
case uint32:
|
||||
return -v
|
||||
case uint64:
|
||||
return -v
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid operation: - %T", v))
|
||||
}
|
||||
}
|
||||
|
||||
func exponent(a, b interface{}) float64 {
|
||||
return math.Pow(toFloat64(a), toFloat64(b))
|
||||
}
|
||||
|
||||
func makeRange(min, max int) []int {
|
||||
size := max - min + 1
|
||||
rng := make([]int, size)
|
||||
for i := range rng {
|
||||
rng[i] = min + i
|
||||
}
|
||||
return rng
|
||||
}
|
||||
|
||||
func toInt(a interface{}) int {
|
||||
switch x := a.(type) {
|
||||
case float32:
|
||||
return int(x)
|
||||
case float64:
|
||||
return int(x)
|
||||
|
||||
case int:
|
||||
return x
|
||||
case int8:
|
||||
return int(x)
|
||||
case int16:
|
||||
return int(x)
|
||||
case int32:
|
||||
return int(x)
|
||||
case int64:
|
||||
return int(x)
|
||||
|
||||
case uint:
|
||||
return int(x)
|
||||
case uint8:
|
||||
return int(x)
|
||||
case uint16:
|
||||
return int(x)
|
||||
case uint32:
|
||||
return int(x)
|
||||
case uint64:
|
||||
return int(x)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid operation: int(%T)", x))
|
||||
}
|
||||
}
|
||||
|
||||
func toInt64(a interface{}) int64 {
|
||||
switch x := a.(type) {
|
||||
case float32:
|
||||
return int64(x)
|
||||
case float64:
|
||||
return int64(x)
|
||||
|
||||
case int:
|
||||
return int64(x)
|
||||
case int8:
|
||||
return int64(x)
|
||||
case int16:
|
||||
return int64(x)
|
||||
case int32:
|
||||
return int64(x)
|
||||
case int64:
|
||||
return x
|
||||
|
||||
case uint:
|
||||
return int64(x)
|
||||
case uint8:
|
||||
return int64(x)
|
||||
case uint16:
|
||||
return int64(x)
|
||||
case uint32:
|
||||
return int64(x)
|
||||
case uint64:
|
||||
return int64(x)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid operation: int64(%T)", x))
|
||||
}
|
||||
}
|
||||
|
||||
func toFloat64(a interface{}) float64 {
|
||||
switch x := a.(type) {
|
||||
case float32:
|
||||
return float64(x)
|
||||
case float64:
|
||||
return x
|
||||
|
||||
case int:
|
||||
return float64(x)
|
||||
case int8:
|
||||
return float64(x)
|
||||
case int16:
|
||||
return float64(x)
|
||||
case int32:
|
||||
return float64(x)
|
||||
case int64:
|
||||
return float64(x)
|
||||
|
||||
case uint:
|
||||
return float64(x)
|
||||
case uint8:
|
||||
return float64(x)
|
||||
case uint16:
|
||||
return float64(x)
|
||||
case uint32:
|
||||
return float64(x)
|
||||
case uint64:
|
||||
return float64(x)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid operation: float64(%T)", x))
|
||||
}
|
||||
}
|
||||
|
||||
func isNil(v interface{}) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
r := reflect.ValueOf(v)
|
||||
switch r.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice:
|
||||
return r.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
+439
@@ -0,0 +1,439 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/antonmedv/expr/file"
|
||||
)
|
||||
|
||||
var (
|
||||
MemoryBudget int = 1e6
|
||||
)
|
||||
|
||||
func Run(program *Program, env interface{}) (interface{}, error) {
|
||||
if program == nil {
|
||||
return nil, fmt.Errorf("program is nil")
|
||||
}
|
||||
|
||||
vm := VM{}
|
||||
return vm.Run(program, env)
|
||||
}
|
||||
|
||||
type VM struct {
|
||||
stack []interface{}
|
||||
constants []interface{}
|
||||
bytecode []byte
|
||||
ip int
|
||||
pp int
|
||||
scopes []Scope
|
||||
debug bool
|
||||
step chan struct{}
|
||||
curr chan int
|
||||
memory int
|
||||
limit int
|
||||
}
|
||||
|
||||
func Debug() *VM {
|
||||
vm := &VM{
|
||||
debug: true,
|
||||
step: make(chan struct{}, 0),
|
||||
curr: make(chan int, 0),
|
||||
}
|
||||
return vm
|
||||
}
|
||||
|
||||
func (vm *VM) Run(program *Program, env interface{}) (out interface{}, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
f := &file.Error{
|
||||
Location: program.Locations[vm.pp],
|
||||
Message: fmt.Sprintf("%v", r),
|
||||
}
|
||||
err = f.Bind(program.Source)
|
||||
}
|
||||
}()
|
||||
|
||||
vm.limit = MemoryBudget
|
||||
vm.ip = 0
|
||||
vm.pp = 0
|
||||
|
||||
if vm.stack == nil {
|
||||
vm.stack = make([]interface{}, 0, 2)
|
||||
} else {
|
||||
vm.stack = vm.stack[0:0]
|
||||
}
|
||||
|
||||
if vm.scopes != nil {
|
||||
vm.scopes = vm.scopes[0:0]
|
||||
}
|
||||
|
||||
vm.bytecode = program.Bytecode
|
||||
vm.constants = program.Constants
|
||||
|
||||
for vm.ip < len(vm.bytecode) {
|
||||
|
||||
if vm.debug {
|
||||
<-vm.step
|
||||
}
|
||||
|
||||
vm.pp = vm.ip
|
||||
vm.ip++
|
||||
op := vm.bytecode[vm.pp]
|
||||
|
||||
switch op {
|
||||
|
||||
case OpPush:
|
||||
vm.push(vm.constant())
|
||||
|
||||
case OpPop:
|
||||
vm.pop()
|
||||
|
||||
case OpRot:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(b)
|
||||
vm.push(a)
|
||||
|
||||
case OpFetch:
|
||||
vm.push(fetch(env, vm.constant()))
|
||||
|
||||
case OpFetchMap:
|
||||
vm.push(env.(map[string]interface{})[vm.constant().(string)])
|
||||
|
||||
case OpTrue:
|
||||
vm.push(true)
|
||||
|
||||
case OpFalse:
|
||||
vm.push(false)
|
||||
|
||||
case OpNil:
|
||||
vm.push(nil)
|
||||
|
||||
case OpNegate:
|
||||
v := negate(vm.pop())
|
||||
vm.push(v)
|
||||
|
||||
case OpNot:
|
||||
v := vm.pop().(bool)
|
||||
vm.push(!v)
|
||||
|
||||
case OpEqual:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(equal(a, b))
|
||||
|
||||
case OpEqualInt:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(a.(int) == b.(int))
|
||||
|
||||
case OpEqualString:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(a.(string) == b.(string))
|
||||
|
||||
case OpJump:
|
||||
offset := vm.arg()
|
||||
vm.ip += int(offset)
|
||||
|
||||
case OpJumpIfTrue:
|
||||
offset := vm.arg()
|
||||
if vm.current().(bool) {
|
||||
vm.ip += int(offset)
|
||||
}
|
||||
|
||||
case OpJumpIfFalse:
|
||||
offset := vm.arg()
|
||||
if !vm.current().(bool) {
|
||||
vm.ip += int(offset)
|
||||
}
|
||||
|
||||
case OpJumpBackward:
|
||||
offset := vm.arg()
|
||||
vm.ip -= int(offset)
|
||||
|
||||
case OpIn:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(in(a, b))
|
||||
|
||||
case OpLess:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(less(a, b))
|
||||
|
||||
case OpMore:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(more(a, b))
|
||||
|
||||
case OpLessOrEqual:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(lessOrEqual(a, b))
|
||||
|
||||
case OpMoreOrEqual:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(moreOrEqual(a, b))
|
||||
|
||||
case OpAdd:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(add(a, b))
|
||||
|
||||
case OpSubtract:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(subtract(a, b))
|
||||
|
||||
case OpMultiply:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(multiply(a, b))
|
||||
|
||||
case OpDivide:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(divide(a, b))
|
||||
|
||||
case OpModulo:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(modulo(a, b))
|
||||
|
||||
case OpExponent:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(exponent(a, b))
|
||||
|
||||
case OpRange:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
min := toInt(a)
|
||||
max := toInt(b)
|
||||
size := max - min + 1
|
||||
if vm.memory+size >= vm.limit {
|
||||
panic("memory budget exceeded")
|
||||
}
|
||||
vm.push(makeRange(min, max))
|
||||
vm.memory += size
|
||||
|
||||
case OpMatches:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
match, err := regexp.MatchString(b.(string), a.(string))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
vm.push(match)
|
||||
|
||||
case OpMatchesConst:
|
||||
a := vm.pop()
|
||||
r := vm.constant().(*regexp.Regexp)
|
||||
vm.push(r.MatchString(a.(string)))
|
||||
|
||||
case OpContains:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(strings.Contains(a.(string), b.(string)))
|
||||
|
||||
case OpStartsWith:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(strings.HasPrefix(a.(string), b.(string)))
|
||||
|
||||
case OpEndsWith:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(strings.HasSuffix(a.(string), b.(string)))
|
||||
|
||||
case OpIndex:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(fetch(a, b))
|
||||
|
||||
case OpSlice:
|
||||
from := vm.pop()
|
||||
to := vm.pop()
|
||||
node := vm.pop()
|
||||
vm.push(slice(node, from, to))
|
||||
|
||||
case OpProperty:
|
||||
a := vm.pop()
|
||||
b := vm.constant()
|
||||
vm.push(fetch(a, b))
|
||||
|
||||
case OpCall:
|
||||
call := vm.constant().(Call)
|
||||
in := make([]reflect.Value, call.Size)
|
||||
for i := call.Size - 1; i >= 0; i-- {
|
||||
param := vm.pop()
|
||||
if param == nil && reflect.TypeOf(param) == nil {
|
||||
// In case of nil value and nil type use this hack,
|
||||
// otherwise reflect.Call will panic on zero value.
|
||||
in[i] = reflect.ValueOf(¶m).Elem()
|
||||
} else {
|
||||
in[i] = reflect.ValueOf(param)
|
||||
}
|
||||
}
|
||||
out := FetchFn(env, call.Name).Call(in)
|
||||
vm.push(out[0].Interface())
|
||||
|
||||
case OpCallFast:
|
||||
call := vm.constant().(Call)
|
||||
in := make([]interface{}, call.Size)
|
||||
for i := call.Size - 1; i >= 0; i-- {
|
||||
in[i] = vm.pop()
|
||||
}
|
||||
fn := FetchFn(env, call.Name).Interface()
|
||||
vm.push(fn.(func(...interface{}) interface{})(in...))
|
||||
|
||||
case OpMethod:
|
||||
call := vm.constants[vm.arg()].(Call)
|
||||
in := make([]reflect.Value, call.Size)
|
||||
for i := call.Size - 1; i >= 0; i-- {
|
||||
param := vm.pop()
|
||||
if param == nil && reflect.TypeOf(param) == nil {
|
||||
// In case of nil value and nil type use this hack,
|
||||
// otherwise reflect.Call will panic on zero value.
|
||||
in[i] = reflect.ValueOf(¶m).Elem()
|
||||
} else {
|
||||
in[i] = reflect.ValueOf(param)
|
||||
}
|
||||
}
|
||||
out := FetchFn(vm.pop(), call.Name).Call(in)
|
||||
vm.push(out[0].Interface())
|
||||
|
||||
case OpArray:
|
||||
size := vm.pop().(int)
|
||||
array := make([]interface{}, size)
|
||||
for i := size - 1; i >= 0; i-- {
|
||||
array[i] = vm.pop()
|
||||
}
|
||||
vm.push(array)
|
||||
vm.memory += size
|
||||
if vm.memory >= vm.limit {
|
||||
panic("memory budget exceeded")
|
||||
}
|
||||
|
||||
case OpMap:
|
||||
size := vm.pop().(int)
|
||||
m := make(map[string]interface{})
|
||||
for i := size - 1; i >= 0; i-- {
|
||||
value := vm.pop()
|
||||
key := vm.pop()
|
||||
m[key.(string)] = value
|
||||
}
|
||||
vm.push(m)
|
||||
vm.memory += size
|
||||
if vm.memory >= vm.limit {
|
||||
panic("memory budget exceeded")
|
||||
}
|
||||
|
||||
case OpLen:
|
||||
vm.push(length(vm.current()))
|
||||
|
||||
case OpCast:
|
||||
t := vm.arg()
|
||||
switch t {
|
||||
case 0:
|
||||
vm.push(toInt64(vm.pop()))
|
||||
case 1:
|
||||
vm.push(toFloat64(vm.pop()))
|
||||
}
|
||||
|
||||
case OpStore:
|
||||
scope := vm.Scope()
|
||||
key := vm.constant().(string)
|
||||
value := vm.pop()
|
||||
scope[key] = value
|
||||
|
||||
case OpLoad:
|
||||
scope := vm.Scope()
|
||||
key := vm.constant().(string)
|
||||
vm.push(scope[key])
|
||||
|
||||
case OpInc:
|
||||
scope := vm.Scope()
|
||||
key := vm.constant().(string)
|
||||
i := scope[key].(int)
|
||||
i++
|
||||
scope[key] = i
|
||||
|
||||
case OpBegin:
|
||||
scope := make(Scope)
|
||||
vm.scopes = append(vm.scopes, scope)
|
||||
|
||||
case OpEnd:
|
||||
vm.scopes = vm.scopes[:len(vm.scopes)-1]
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown bytecode %#x", op))
|
||||
}
|
||||
|
||||
if vm.debug {
|
||||
vm.curr <- vm.ip
|
||||
}
|
||||
}
|
||||
|
||||
if vm.debug {
|
||||
close(vm.curr)
|
||||
close(vm.step)
|
||||
}
|
||||
|
||||
if len(vm.stack) > 0 {
|
||||
return vm.pop(), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (vm *VM) push(value interface{}) {
|
||||
vm.stack = append(vm.stack, value)
|
||||
}
|
||||
|
||||
func (vm *VM) current() interface{} {
|
||||
return vm.stack[len(vm.stack)-1]
|
||||
}
|
||||
|
||||
func (vm *VM) pop() interface{} {
|
||||
value := vm.stack[len(vm.stack)-1]
|
||||
vm.stack = vm.stack[:len(vm.stack)-1]
|
||||
return value
|
||||
}
|
||||
|
||||
func (vm *VM) arg() uint16 {
|
||||
b0, b1 := vm.bytecode[vm.ip], vm.bytecode[vm.ip+1]
|
||||
vm.ip += 2
|
||||
return uint16(b0) | uint16(b1)<<8
|
||||
}
|
||||
|
||||
func (vm *VM) constant() interface{} {
|
||||
return vm.constants[vm.arg()]
|
||||
}
|
||||
|
||||
func (vm *VM) Stack() []interface{} {
|
||||
return vm.stack
|
||||
}
|
||||
|
||||
func (vm *VM) Scope() Scope {
|
||||
if len(vm.scopes) > 0 {
|
||||
return vm.scopes[len(vm.scopes)-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vm *VM) Step() {
|
||||
if vm.ip < len(vm.bytecode) {
|
||||
vm.step <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (vm *VM) Position() chan int {
|
||||
return vm.curr
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
Copyright (c) 2012-2016 Charles Banning <clbanning@gmail.com>. All rights reserved.
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
===============================================================================
|
||||
|
||||
Go Language Copyright & License -
|
||||
|
||||
Copyright 2009 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package mxj
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultElementTag = "element"
|
||||
)
|
||||
|
||||
// Encode arbitrary value as XML.
|
||||
//
|
||||
// Note: unmarshaling the resultant
|
||||
// XML may not return the original value, since tag labels may have been injected
|
||||
// to create the XML representation of the value.
|
||||
/*
|
||||
Encode an arbitrary JSON object.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/clbanning/mxj"
|
||||
)
|
||||
|
||||
func main() {
|
||||
jsondata := []byte(`[
|
||||
{ "somekey":"somevalue" },
|
||||
"string",
|
||||
3.14159265,
|
||||
true
|
||||
]`)
|
||||
var i interface{}
|
||||
err := json.Unmarshal(jsondata, &i)
|
||||
if err != nil {
|
||||
// do something
|
||||
}
|
||||
x, err := mxj.AnyXmlIndent(i, "", " ", "mydoc")
|
||||
if err != nil {
|
||||
// do something else
|
||||
}
|
||||
fmt.Println(string(x))
|
||||
}
|
||||
|
||||
output:
|
||||
<mydoc>
|
||||
<somekey>somevalue</somekey>
|
||||
<element>string</element>
|
||||
<element>3.14159265</element>
|
||||
<element>true</element>
|
||||
</mydoc>
|
||||
*/
|
||||
// Alternative values for DefaultRootTag and DefaultElementTag can be set as:
|
||||
// AnyXml( v, myRootTag, myElementTag).
|
||||
func AnyXml(v interface{}, tags ...string) ([]byte, error) {
|
||||
var rt, et string
|
||||
if len(tags) == 1 || len(tags) == 2 {
|
||||
rt = tags[0]
|
||||
} else {
|
||||
rt = DefaultRootTag
|
||||
}
|
||||
if len(tags) == 2 {
|
||||
et = tags[1]
|
||||
} else {
|
||||
et = DefaultElementTag
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
if useGoXmlEmptyElemSyntax {
|
||||
return []byte("<" + rt + "></" + rt + ">"), nil
|
||||
}
|
||||
return []byte("<" + rt + "/>"), nil
|
||||
}
|
||||
if reflect.TypeOf(v).Kind() == reflect.Struct {
|
||||
return xml.Marshal(v)
|
||||
}
|
||||
|
||||
var err error
|
||||
s := new(string)
|
||||
p := new(pretty)
|
||||
|
||||
var ss string
|
||||
var b []byte
|
||||
switch v.(type) {
|
||||
case []interface{}:
|
||||
ss = "<" + rt + ">"
|
||||
for _, vv := range v.([]interface{}) {
|
||||
switch vv.(type) {
|
||||
case map[string]interface{}:
|
||||
m := vv.(map[string]interface{})
|
||||
if len(m) == 1 {
|
||||
for tag, val := range m {
|
||||
err = mapToXmlIndent(false, s, tag, val, p)
|
||||
}
|
||||
} else {
|
||||
err = mapToXmlIndent(false, s, et, vv, p)
|
||||
}
|
||||
default:
|
||||
err = mapToXmlIndent(false, s, et, vv, p)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
ss += *s + "</" + rt + ">"
|
||||
b = []byte(ss)
|
||||
case map[string]interface{}:
|
||||
m := Map(v.(map[string]interface{}))
|
||||
b, err = m.Xml(rt)
|
||||
default:
|
||||
err = mapToXmlIndent(false, s, rt, v, p)
|
||||
b = []byte(*s)
|
||||
}
|
||||
|
||||
return b, err
|
||||
}
|
||||
|
||||
// Encode an arbitrary value as a pretty XML string.
|
||||
// Alternative values for DefaultRootTag and DefaultElementTag can be set as:
|
||||
// AnyXmlIndent( v, "", " ", myRootTag, myElementTag).
|
||||
func AnyXmlIndent(v interface{}, prefix, indent string, tags ...string) ([]byte, error) {
|
||||
var rt, et string
|
||||
if len(tags) == 1 || len(tags) == 2 {
|
||||
rt = tags[0]
|
||||
} else {
|
||||
rt = DefaultRootTag
|
||||
}
|
||||
if len(tags) == 2 {
|
||||
et = tags[1]
|
||||
} else {
|
||||
et = DefaultElementTag
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
if useGoXmlEmptyElemSyntax {
|
||||
return []byte(prefix + "<" + rt + "></" + rt + ">"), nil
|
||||
}
|
||||
return []byte(prefix + "<" + rt + "/>"), nil
|
||||
}
|
||||
if reflect.TypeOf(v).Kind() == reflect.Struct {
|
||||
return xml.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
var err error
|
||||
s := new(string)
|
||||
p := new(pretty)
|
||||
p.indent = indent
|
||||
p.padding = prefix
|
||||
|
||||
var ss string
|
||||
var b []byte
|
||||
switch v.(type) {
|
||||
case []interface{}:
|
||||
ss = "<" + rt + ">\n"
|
||||
p.Indent()
|
||||
for _, vv := range v.([]interface{}) {
|
||||
switch vv.(type) {
|
||||
case map[string]interface{}:
|
||||
m := vv.(map[string]interface{})
|
||||
if len(m) == 1 {
|
||||
for tag, val := range m {
|
||||
err = mapToXmlIndent(true, s, tag, val, p)
|
||||
}
|
||||
} else {
|
||||
p.start = 1 // we 1 tag in
|
||||
err = mapToXmlIndent(true, s, et, vv, p)
|
||||
*s += "\n"
|
||||
}
|
||||
default:
|
||||
p.start = 0 // in case trailing p.start = 1
|
||||
err = mapToXmlIndent(true, s, et, vv, p)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
ss += *s + "</" + rt + ">"
|
||||
b = []byte(ss)
|
||||
case map[string]interface{}:
|
||||
m := Map(v.(map[string]interface{}))
|
||||
b, err = m.XmlIndent(prefix, indent, rt)
|
||||
default:
|
||||
err = mapToXmlIndent(true, s, rt, v, p)
|
||||
b = []byte(*s)
|
||||
}
|
||||
|
||||
return b, err
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us" updated="2009-10-04T01:35:58+00:00"><title>Code Review - My issues</title><link href="http://codereview.appspot.com/" rel="alternate"></link><link href="http://codereview.appspot.com/rss/mine/rsc" rel="self"></link><id>http://codereview.appspot.com/</id><author><name>rietveld<></name></author><entry><title>rietveld: an attempt at pubsubhubbub
|
||||
</title><link href="http://codereview.appspot.com/126085" rel="alternate"></link><updated>2009-10-04T01:35:58+00:00</updated><author><name>email-address-removed</name></author><id>urn:md5:134d9179c41f806be79b3a5f7877d19a</id><summary type="html">
|
||||
An attempt at adding pubsubhubbub support to Rietveld.
|
||||
http://code.google.com/p/pubsubhubbub
|
||||
http://code.google.com/p/rietveld/issues/detail?id=155
|
||||
|
||||
The server side of the protocol is trivial:
|
||||
1. add a &lt;link rel=&quot;hub&quot; href=&quot;hub-server&quot;&gt; tag to all
|
||||
feeds that will be pubsubhubbubbed.
|
||||
2. every time one of those feeds changes, tell the hub
|
||||
with a simple POST request.
|
||||
|
||||
I have tested this by adding debug prints to a local hub
|
||||
server and checking that the server got the right publish
|
||||
requests.
|
||||
|
||||
I can&#39;t quite get the server to work, but I think the bug
|
||||
is not in my code. I think that the server expects to be
|
||||
able to grab the feed and see the feed&#39;s actual URL in
|
||||
the link rel=&quot;self&quot;, but the default value for that drops
|
||||
the :port from the URL, and I cannot for the life of me
|
||||
figure out how to get the Atom generator deep inside
|
||||
django not to do that, or even where it is doing that,
|
||||
or even what code is running to generate the Atom feed.
|
||||
(I thought I knew but I added some assert False statements
|
||||
and it kept running!)
|
||||
|
||||
Ignoring that particular problem, I would appreciate
|
||||
feedback on the right way to get the two values at
|
||||
the top of feeds.py marked NOTE(rsc).
|
||||
|
||||
|
||||
</summary></entry><entry><title>rietveld: correct tab handling
|
||||
</title><link href="http://codereview.appspot.com/124106" rel="alternate"></link><updated>2009-10-03T23:02:17+00:00</updated><author><name>email-address-removed</name></author><id>urn:md5:0a2a4f19bb815101f0ba2904aed7c35a</id><summary type="html">
|
||||
This fixes the buggy tab rendering that can be seen at
|
||||
http://codereview.appspot.com/116075/diff/1/2
|
||||
|
||||
The fundamental problem was that the tab code was
|
||||
not being told what column the text began in, so it
|
||||
didn&#39;t know where to put the tab stops. Another problem
|
||||
was that some of the code assumed that string byte
|
||||
offsets were the same as column offsets, which is only
|
||||
true if there are no tabs.
|
||||
|
||||
In the process of fixing this, I cleaned up the arguments
|
||||
to Fold and ExpandTabs and renamed them Break and
|
||||
_ExpandTabs so that I could be sure that I found all the
|
||||
call sites. I also wanted to verify that ExpandTabs was
|
||||
not being used from outside intra_region_diff.py.
|
||||
|
||||
|
||||
</summary></entry></feed> `
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
// mxj - A collection of map[string]interface{} and associated XML and JSON utilities.
|
||||
// Copyright 2012-2015, 2018 Charles Banning. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file
|
||||
|
||||
/*
|
||||
Marshal/Unmarshal XML to/from map[string]interface{} values (and JSON); extract/modify values from maps by key or key-path, including wildcards.
|
||||
|
||||
mxj supplants the legacy x2j and j2x packages. The subpackage x2j-wrapper is provided to facilitate migrating from the x2j package. The x2j and j2x subpackages provide similar functionality of the old packages but are not function-name compatible with them.
|
||||
|
||||
Note: this library was designed for processing ad hoc anonymous messages. Bulk processing large data sets may be much more efficiently performed using the encoding/xml or encoding/json packages from Go's standard library directly.
|
||||
|
||||
Related Packages:
|
||||
checkxml: github.com/clbanning/checkxml provides functions for validating XML data.
|
||||
|
||||
Notes:
|
||||
2018.04.18: mv.Xml/mv.XmlIndent encodes non-map[string]interface{} map values - map[string]string, map[int]uint, etc.
|
||||
2018.03.29: mv.Gob/NewMapGob support gob encoding/decoding of Maps.
|
||||
2018.03.26: Added mxj/x2j-wrapper sub-package for migrating from legacy x2j package.
|
||||
2017.02.22: LeafNode paths can use ".N" syntax rather than "[N]" for list member indexing.
|
||||
2017.02.21: github.com/clbanning/checkxml provides functions for validating XML data.
|
||||
2017.02.10: SetFieldSeparator changes field separator for args in UpdateValuesForPath, ValuesFor... methods.
|
||||
2017.02.06: Support XMPP stream processing - HandleXMPPStreamTag().
|
||||
2016.11.07: Preserve name space prefix syntax in XmlSeq parser - NewMapXmlSeq(), etc.
|
||||
2016.06.25: Support overriding default XML attribute prefix, "-", in Map keys - SetAttrPrefix().
|
||||
2016.05.26: Support customization of xml.Decoder by exposing CustomDecoder variable.
|
||||
2016.03.19: Escape invalid chars when encoding XML attribute and element values - XMLEscapeChars().
|
||||
2016.03.02: By default decoding XML with float64 and bool value casting will not cast "NaN", "Inf", and "-Inf".
|
||||
To cast them to float64, first set flag with CastNanInf(true).
|
||||
2016.02.22: New mv.Root(), mv.Elements(), mv.Attributes methods let you examine XML document structure.
|
||||
2016.02.16: Add CoerceKeysToLower() option to handle tags with mixed capitalization.
|
||||
2016.02.12: Seek for first xml.StartElement token; only return error if io.EOF is reached first (handles BOM).
|
||||
2015-12-02: NewMapXmlSeq() with mv.XmlSeq() & co. will try to preserve structure of XML doc when re-encoding.
|
||||
2014-08-02: AnyXml() and AnyXmlIndent() will try to marshal arbitrary values to XML.
|
||||
|
||||
SUMMARY
|
||||
|
||||
type Map map[string]interface{}
|
||||
|
||||
Create a Map value, 'mv', from any map[string]interface{} value, 'v':
|
||||
mv := Map(v)
|
||||
|
||||
Unmarshal / marshal XML as a Map value, 'mv':
|
||||
mv, err := NewMapXml(xmlValue) // unmarshal
|
||||
xmlValue, err := mv.Xml() // marshal
|
||||
|
||||
Unmarshal XML from an io.Reader as a Map value, 'mv':
|
||||
mv, err := NewMapXmlReader(xmlReader) // repeated calls, as with an os.File Reader, will process stream
|
||||
mv, raw, err := NewMapXmlReaderRaw(xmlReader) // 'raw' is the raw XML that was decoded
|
||||
|
||||
Marshal Map value, 'mv', to an XML Writer (io.Writer):
|
||||
err := mv.XmlWriter(xmlWriter)
|
||||
raw, err := mv.XmlWriterRaw(xmlWriter) // 'raw' is the raw XML that was written on xmlWriter
|
||||
|
||||
Also, for prettified output:
|
||||
xmlValue, err := mv.XmlIndent(prefix, indent, ...)
|
||||
err := mv.XmlIndentWriter(xmlWriter, prefix, indent, ...)
|
||||
raw, err := mv.XmlIndentWriterRaw(xmlWriter, prefix, indent, ...)
|
||||
|
||||
Bulk process XML with error handling (note: handlers must return a boolean value):
|
||||
err := HandleXmlReader(xmlReader, mapHandler(Map), errHandler(error))
|
||||
err := HandleXmlReaderRaw(xmlReader, mapHandler(Map, []byte), errHandler(error, []byte))
|
||||
|
||||
Converting XML to JSON: see Examples for NewMapXml and HandleXmlReader.
|
||||
|
||||
There are comparable functions and methods for JSON processing.
|
||||
|
||||
Arbitrary structure values can be decoded to / encoded from Map values:
|
||||
mv, err := NewMapStruct(structVal)
|
||||
err := mv.Struct(structPointer)
|
||||
|
||||
To work with XML tag values, JSON or Map key values or structure field values, decode the XML, JSON
|
||||
or structure to a Map value, 'mv', or cast a map[string]interface{} value to a Map value, 'mv', then:
|
||||
paths := mv.PathsForKey(key)
|
||||
path := mv.PathForKeyShortest(key)
|
||||
values, err := mv.ValuesForKey(key, subkeys)
|
||||
values, err := mv.ValuesForPath(path, subkeys) // 'path' can be dot-notation with wildcards and indexed arrays.
|
||||
count, err := mv.UpdateValuesForPath(newVal, path, subkeys)
|
||||
|
||||
Get everything at once, irrespective of path depth:
|
||||
leafnodes := mv.LeafNodes()
|
||||
leafvalues := mv.LeafValues()
|
||||
|
||||
A new Map with whatever keys are desired can be created from the current Map and then encoded in XML
|
||||
or JSON. (Note: keys can use dot-notation. 'oldKey' can also use wildcards and indexed arrays.)
|
||||
newMap, err := mv.NewMap("oldKey_1:newKey_1", "oldKey_2:newKey_2", ..., "oldKey_N:newKey_N")
|
||||
newMap, err := mv.NewMap("oldKey1", "oldKey3", "oldKey5") // a subset of 'mv'; see "examples/partial.go"
|
||||
newXml, err := newMap.Xml() // for example
|
||||
newJson, err := newMap.Json() // ditto
|
||||
|
||||
XML PARSING CONVENTIONS
|
||||
|
||||
Using NewMapXml()
|
||||
|
||||
- Attributes are parsed to `map[string]interface{}` values by prefixing a hyphen, `-`,
|
||||
to the attribute label. (Unless overridden by `PrependAttrWithHyphen(false)` or
|
||||
`SetAttrPrefix()`.)
|
||||
- If the element is a simple element and has attributes, the element value
|
||||
is given the key `#text` for its `map[string]interface{}` representation. (See
|
||||
the 'atomFeedString.xml' test data, below.)
|
||||
- XML comments, directives, and process instructions are ignored.
|
||||
- If CoerceKeysToLower() has been called, then the resultant keys will be lower case.
|
||||
|
||||
Using NewMapXmlSeq()
|
||||
|
||||
- Attributes are parsed to `map["#attr"]map[<attr_label>]map[string]interface{}`values
|
||||
where the `<attr_label>` value has "#text" and "#seq" keys - the "#text" key holds the
|
||||
value for `<attr_label>`.
|
||||
- All elements, except for the root, have a "#seq" key.
|
||||
- Comments, directives, and process instructions are unmarshalled into the Map using the
|
||||
keys "#comment", "#directive", and "#procinst", respectively. (See documentation for more
|
||||
specifics.)
|
||||
- Name space syntax is preserved:
|
||||
- <ns:key>something</ns.key> parses to map["ns:key"]interface{}{"something"}
|
||||
- xmlns:ns="http://myns.com/ns" parses to map["xmlns:ns"]interface{}{"http://myns.com/ns"}
|
||||
|
||||
Both
|
||||
|
||||
- By default, "Nan", "Inf", and "-Inf" values are not cast to float64. If you want them
|
||||
to be cast, set a flag to cast them using CastNanInf(true).
|
||||
|
||||
XML ENCODING CONVENTIONS
|
||||
|
||||
- 'nil' Map values, which may represent 'null' JSON values, are encoded as "<tag/>".
|
||||
NOTE: the operation is not symmetric as "<tag/>" elements are decoded as 'tag:""' Map values,
|
||||
which, then, encode in JSON as '"tag":""' values..
|
||||
- ALSO: there is no guarantee that the encoded XML doc will be the same as the decoded one. (Go
|
||||
randomizes the walk through map[string]interface{} values.) If you plan to re-encode the
|
||||
Map value to XML and want the same sequencing of elements look at NewMapXmlSeq() and
|
||||
mv.XmlSeq() - these try to preserve the element sequencing but with added complexity when
|
||||
working with the Map representation.
|
||||
|
||||
*/
|
||||
package mxj
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Copyright 2016 Charles Banning. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file
|
||||
|
||||
package mxj
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
var xmlEscapeChars bool
|
||||
|
||||
// XMLEscapeChars(true) forces escaping invalid characters in attribute and element values.
|
||||
// NOTE: this is brute force with NO interrogation of '&' being escaped already; if it is
|
||||
// then '&' will be re-escaped as '&amp;'.
|
||||
//
|
||||
/*
|
||||
The values are:
|
||||
" "
|
||||
' '
|
||||
< <
|
||||
> >
|
||||
& &
|
||||
*/
|
||||
func XMLEscapeChars(b bool) {
|
||||
xmlEscapeChars = b
|
||||
}
|
||||
|
||||
// Scan for '&' first, since 's' may contain "&" that is parsed to "&amp;"
|
||||
// - or "<" that is parsed to "&lt;".
|
||||
var escapechars = [][2][]byte{
|
||||
{[]byte(`&`), []byte(`&`)},
|
||||
{[]byte(`<`), []byte(`<`)},
|
||||
{[]byte(`>`), []byte(`>`)},
|
||||
{[]byte(`"`), []byte(`"`)},
|
||||
{[]byte(`'`), []byte(`'`)},
|
||||
}
|
||||
|
||||
func escapeChars(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
b := []byte(s)
|
||||
for _, v := range escapechars {
|
||||
n := bytes.Count(b, v[0])
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
b = bytes.Replace(b, v[0], v[1], n)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package mxj
|
||||
|
||||
// Checks whether the path exists
|
||||
func (mv Map) Exists(path string, subkeys ...string) bool {
|
||||
v, err := mv.ValuesForPath(path, subkeys...)
|
||||
return err == nil && len(v) > 0
|
||||
}
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
package mxj
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Maps []Map
|
||||
|
||||
func NewMaps() Maps {
|
||||
return make(Maps, 0)
|
||||
}
|
||||
|
||||
type MapRaw struct {
|
||||
M Map
|
||||
R []byte
|
||||
}
|
||||
|
||||
// NewMapsFromXmlFile - creates an array from a file of JSON values.
|
||||
func NewMapsFromJsonFile(name string) (Maps, error) {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("file %s is not a regular file", name)
|
||||
}
|
||||
|
||||
fh, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
am := make([]Map, 0)
|
||||
for {
|
||||
m, raw, err := NewMapJsonReaderRaw(fh)
|
||||
if err != nil && err != io.EOF {
|
||||
return am, fmt.Errorf("error: %s - reading: %s", err.Error(), string(raw))
|
||||
}
|
||||
if len(m) > 0 {
|
||||
am = append(am, m)
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return am, nil
|
||||
}
|
||||
|
||||
// ReadMapsFromJsonFileRaw - creates an array of MapRaw from a file of JSON values.
|
||||
func NewMapsFromJsonFileRaw(name string) ([]MapRaw, error) {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("file %s is not a regular file", name)
|
||||
}
|
||||
|
||||
fh, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
am := make([]MapRaw, 0)
|
||||
for {
|
||||
mr := new(MapRaw)
|
||||
mr.M, mr.R, err = NewMapJsonReaderRaw(fh)
|
||||
if err != nil && err != io.EOF {
|
||||
return am, fmt.Errorf("error: %s - reading: %s", err.Error(), string(mr.R))
|
||||
}
|
||||
if len(mr.M) > 0 {
|
||||
am = append(am, *mr)
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return am, nil
|
||||
}
|
||||
|
||||
// NewMapsFromXmlFile - creates an array from a file of XML values.
|
||||
func NewMapsFromXmlFile(name string) (Maps, error) {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("file %s is not a regular file", name)
|
||||
}
|
||||
|
||||
fh, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
am := make([]Map, 0)
|
||||
for {
|
||||
m, raw, err := NewMapXmlReaderRaw(fh)
|
||||
if err != nil && err != io.EOF {
|
||||
return am, fmt.Errorf("error: %s - reading: %s", err.Error(), string(raw))
|
||||
}
|
||||
if len(m) > 0 {
|
||||
am = append(am, m)
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return am, nil
|
||||
}
|
||||
|
||||
// NewMapsFromXmlFileRaw - creates an array of MapRaw from a file of XML values.
|
||||
// NOTE: the slice with the raw XML is clean with no extra capacity - unlike NewMapXmlReaderRaw().
|
||||
// It is slow at parsing a file from disk and is intended for relatively small utility files.
|
||||
func NewMapsFromXmlFileRaw(name string) ([]MapRaw, error) {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("file %s is not a regular file", name)
|
||||
}
|
||||
|
||||
fh, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
am := make([]MapRaw, 0)
|
||||
for {
|
||||
mr := new(MapRaw)
|
||||
mr.M, mr.R, err = NewMapXmlReaderRaw(fh)
|
||||
if err != nil && err != io.EOF {
|
||||
return am, fmt.Errorf("error: %s - reading: %s", err.Error(), string(mr.R))
|
||||
}
|
||||
if len(mr.M) > 0 {
|
||||
am = append(am, *mr)
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return am, nil
|
||||
}
|
||||
|
||||
// ------------------------ Maps writing -------------------------
|
||||
// These are handy-dandy methods for dumping configuration data, etc.
|
||||
|
||||
// JsonString - analogous to mv.Json()
|
||||
func (mvs Maps) JsonString(safeEncoding ...bool) (string, error) {
|
||||
var s string
|
||||
for _, v := range mvs {
|
||||
j, err := v.Json()
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
s += string(j)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// JsonStringIndent - analogous to mv.JsonIndent()
|
||||
func (mvs Maps) JsonStringIndent(prefix, indent string, safeEncoding ...bool) (string, error) {
|
||||
var s string
|
||||
var haveFirst bool
|
||||
for _, v := range mvs {
|
||||
j, err := v.JsonIndent(prefix, indent)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
if haveFirst {
|
||||
s += "\n"
|
||||
} else {
|
||||
haveFirst = true
|
||||
}
|
||||
s += string(j)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// XmlString - analogous to mv.Xml()
|
||||
func (mvs Maps) XmlString() (string, error) {
|
||||
var s string
|
||||
for _, v := range mvs {
|
||||
x, err := v.Xml()
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
s += string(x)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// XmlStringIndent - analogous to mv.XmlIndent()
|
||||
func (mvs Maps) XmlStringIndent(prefix, indent string) (string, error) {
|
||||
var s string
|
||||
for _, v := range mvs {
|
||||
x, err := v.XmlIndent(prefix, indent)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
s += string(x)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// JsonFile - write Maps to named file as JSON
|
||||
// Note: the file will be created, if necessary; if it exists it will be truncated.
|
||||
// If you need to append to a file, open it and use JsonWriter method.
|
||||
func (mvs Maps) JsonFile(file string, safeEncoding ...bool) error {
|
||||
var encoding bool
|
||||
if len(safeEncoding) == 1 {
|
||||
encoding = safeEncoding[0]
|
||||
}
|
||||
s, err := mvs.JsonString(encoding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fh, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
fh.WriteString(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// JsonFileIndent - write Maps to named file as pretty JSON
|
||||
// Note: the file will be created, if necessary; if it exists it will be truncated.
|
||||
// If you need to append to a file, open it and use JsonIndentWriter method.
|
||||
func (mvs Maps) JsonFileIndent(file, prefix, indent string, safeEncoding ...bool) error {
|
||||
var encoding bool
|
||||
if len(safeEncoding) == 1 {
|
||||
encoding = safeEncoding[0]
|
||||
}
|
||||
s, err := mvs.JsonStringIndent(prefix, indent, encoding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fh, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
fh.WriteString(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// XmlFile - write Maps to named file as XML
|
||||
// Note: the file will be created, if necessary; if it exists it will be truncated.
|
||||
// If you need to append to a file, open it and use XmlWriter method.
|
||||
func (mvs Maps) XmlFile(file string) error {
|
||||
s, err := mvs.XmlString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fh, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
fh.WriteString(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// XmlFileIndent - write Maps to named file as pretty XML
|
||||
// Note: the file will be created,if necessary; if it exists it will be truncated.
|
||||
// If you need to append to a file, open it and use XmlIndentWriter method.
|
||||
func (mvs Maps) XmlFileIndent(file, prefix, indent string) error {
|
||||
s, err := mvs.XmlStringIndent(prefix, indent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fh, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
fh.WriteString(s)
|
||||
return nil
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
{ "this":"is", "a":"test", "file":"for", "files_test.go":"case" }
|
||||
{ "with":"some", "bad":JSON, "in":"it" }
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<doc>
|
||||
<some>test</some>
|
||||
<data>for files.go</data>
|
||||
</doc>
|
||||
<msg>
|
||||
<just>some</just>
|
||||
<another>doc</other>
|
||||
<for>test case</for>
|
||||
</msg>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
{ "this":"is", "a":"test", "file":"for", "files_test.go":"case" }
|
||||
{ "with":"just", "two":2, "JSON":"values", "true":true }
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<doc>
|
||||
<some>test</some>
|
||||
<data>for files.go</data>
|
||||
</doc>
|
||||
<msg>
|
||||
<just>some</just>
|
||||
<another>doc</another>
|
||||
<for>test case</for>
|
||||
</msg>
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"a":"test","file":"for","files_test.go":"case","this":"is"}{"JSON":"values","true":true,"two":2,"with":"just"}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<doc><data>for files.go</data><some>test</some></doc><msg><another>doc</another><for>test case</for><just>some</just></msg>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"a": "test",
|
||||
"file": "for",
|
||||
"files_test.go": "case",
|
||||
"this": "is"
|
||||
}
|
||||
{
|
||||
"JSON": "values",
|
||||
"true": true,
|
||||
"two": 2,
|
||||
"with": "just"
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<doc>
|
||||
<data>for files.go</data>
|
||||
<some>test</some>
|
||||
</doc><msg>
|
||||
<another>doc</another>
|
||||
<for>test case</for>
|
||||
<just>some</just>
|
||||
</msg>
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// gob.go - Encode/Decode a Map into a gob object.
|
||||
|
||||
package mxj
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
)
|
||||
|
||||
// NewMapGob returns a Map value for a gob object that has been
|
||||
// encoded from a map[string]interface{} (or compatible type) value.
|
||||
// It is intended to provide symmetric handling of Maps that have
|
||||
// been encoded using mv.Gob.
|
||||
func NewMapGob(gobj []byte) (Map, error) {
|
||||
m := make(map[string]interface{}, 0)
|
||||
if len(gobj) == 0 {
|
||||
return m, nil
|
||||
}
|
||||
r := bytes.NewReader(gobj)
|
||||
dec := gob.NewDecoder(r)
|
||||
if err := dec.Decode(&m); err != nil {
|
||||
return m, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Gob returns a gob-encoded value for the Map 'mv'.
|
||||
func (mv Map) Gob() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
if err := enc.Encode(map[string]interface{}(mv)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
// Copyright 2012-2014 Charles Banning. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file
|
||||
|
||||
package mxj
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ------------------------------ write JSON -----------------------
|
||||
|
||||
// Just a wrapper on json.Marshal.
|
||||
// If option safeEncoding is'true' then safe encoding of '<', '>' and '&'
|
||||
// is preserved. (see encoding/json#Marshal, encoding/json#Encode)
|
||||
func (mv Map) Json(safeEncoding ...bool) ([]byte, error) {
|
||||
var s bool
|
||||
if len(safeEncoding) == 1 {
|
||||
s = safeEncoding[0]
|
||||
}
|
||||
|
||||
b, err := json.Marshal(mv)
|
||||
|
||||
if !s {
|
||||
b = bytes.Replace(b, []byte("\\u003c"), []byte("<"), -1)
|
||||
b = bytes.Replace(b, []byte("\\u003e"), []byte(">"), -1)
|
||||
b = bytes.Replace(b, []byte("\\u0026"), []byte("&"), -1)
|
||||
}
|
||||
return b, err
|
||||
}
|
||||
|
||||
// Just a wrapper on json.MarshalIndent.
|
||||
// If option safeEncoding is'true' then safe encoding of '<' , '>' and '&'
|
||||
// is preserved. (see encoding/json#Marshal, encoding/json#Encode)
|
||||
func (mv Map) JsonIndent(prefix, indent string, safeEncoding ...bool) ([]byte, error) {
|
||||
var s bool
|
||||
if len(safeEncoding) == 1 {
|
||||
s = safeEncoding[0]
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(mv, prefix, indent)
|
||||
if !s {
|
||||
b = bytes.Replace(b, []byte("\\u003c"), []byte("<"), -1)
|
||||
b = bytes.Replace(b, []byte("\\u003e"), []byte(">"), -1)
|
||||
b = bytes.Replace(b, []byte("\\u0026"), []byte("&"), -1)
|
||||
}
|
||||
return b, err
|
||||
}
|
||||
|
||||
// The following implementation is provided for symmetry with NewMapJsonReader[Raw]
|
||||
// The names will also provide a key for the number of return arguments.
|
||||
|
||||
// Writes the Map as JSON on the Writer.
|
||||
// If 'safeEncoding' is 'true', then "safe" encoding of '<', '>' and '&' is preserved.
|
||||
func (mv Map) JsonWriter(jsonWriter io.Writer, safeEncoding ...bool) error {
|
||||
b, err := mv.Json(safeEncoding...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = jsonWriter.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
// Writes the Map as JSON on the Writer. []byte is the raw JSON that was written.
|
||||
// If 'safeEncoding' is 'true', then "safe" encoding of '<', '>' and '&' is preserved.
|
||||
func (mv Map) JsonWriterRaw(jsonWriter io.Writer, safeEncoding ...bool) ([]byte, error) {
|
||||
b, err := mv.Json(safeEncoding...)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
_, err = jsonWriter.Write(b)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// Writes the Map as pretty JSON on the Writer.
|
||||
// If 'safeEncoding' is 'true', then "safe" encoding of '<', '>' and '&' is preserved.
|
||||
func (mv Map) JsonIndentWriter(jsonWriter io.Writer, prefix, indent string, safeEncoding ...bool) error {
|
||||
b, err := mv.JsonIndent(prefix, indent, safeEncoding...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = jsonWriter.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
// Writes the Map as pretty JSON on the Writer. []byte is the raw JSON that was written.
|
||||
// If 'safeEncoding' is 'true', then "safe" encoding of '<', '>' and '&' is preserved.
|
||||
func (mv Map) JsonIndentWriterRaw(jsonWriter io.Writer, prefix, indent string, safeEncoding ...bool) ([]byte, error) {
|
||||
b, err := mv.JsonIndent(prefix, indent, safeEncoding...)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
_, err = jsonWriter.Write(b)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// --------------------------- read JSON -----------------------------
|
||||
|
||||
// Decode numericvalues as json.Number type Map values - see encoding/json#Number.
|
||||
// NOTE: this is for decoding JSON into a Map with NewMapJson(), NewMapJsonReader(),
|
||||
// etc.; it does not affect NewMapXml(), etc. The XML encoders mv.Xml() and mv.XmlIndent()
|
||||
// do recognize json.Number types; a JSON object can be decoded to a Map with json.Number
|
||||
// value types and the resulting Map can be correctly encoded into a XML object.
|
||||
var JsonUseNumber bool
|
||||
|
||||
// Just a wrapper on json.Unmarshal
|
||||
// Converting JSON to XML is a simple as:
|
||||
// ...
|
||||
// mapVal, merr := mxj.NewMapJson(jsonVal)
|
||||
// if merr != nil {
|
||||
// // handle error
|
||||
// }
|
||||
// xmlVal, xerr := mapVal.Xml()
|
||||
// if xerr != nil {
|
||||
// // handle error
|
||||
// }
|
||||
// NOTE: as a special case, passing a list, e.g., [{"some-null-value":"", "a-non-null-value":"bar"}],
|
||||
// will be interpreted as having the root key 'object' prepended - {"object":[ ... ]} - to unmarshal to a Map.
|
||||
// See mxj/j2x/j2x_test.go.
|
||||
func NewMapJson(jsonVal []byte) (Map, error) {
|
||||
// empty or nil begets empty
|
||||
if len(jsonVal) == 0 {
|
||||
m := make(map[string]interface{}, 0)
|
||||
return m, nil
|
||||
}
|
||||
// handle a goofy case ...
|
||||
if jsonVal[0] == '[' {
|
||||
jsonVal = []byte(`{"object":` + string(jsonVal) + `}`)
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
// err := json.Unmarshal(jsonVal, &m)
|
||||
buf := bytes.NewReader(jsonVal)
|
||||
dec := json.NewDecoder(buf)
|
||||
if JsonUseNumber {
|
||||
dec.UseNumber()
|
||||
}
|
||||
err := dec.Decode(&m)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Retrieve a Map value from an io.Reader.
|
||||
// NOTE: The raw JSON off the reader is buffered to []byte using a ByteReader. If the io.Reader is an
|
||||
// os.File, there may be significant performance impact. If the io.Reader is wrapping a []byte
|
||||
// value in-memory, however, such as http.Request.Body you CAN use it to efficiently unmarshal
|
||||
// a JSON object.
|
||||
func NewMapJsonReader(jsonReader io.Reader) (Map, error) {
|
||||
jb, err := getJson(jsonReader)
|
||||
if err != nil || len(*jb) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal the 'presumed' JSON string
|
||||
return NewMapJson(*jb)
|
||||
}
|
||||
|
||||
// Retrieve a Map value and raw JSON - []byte - from an io.Reader.
|
||||
// NOTE: The raw JSON off the reader is buffered to []byte using a ByteReader. If the io.Reader is an
|
||||
// os.File, there may be significant performance impact. If the io.Reader is wrapping a []byte
|
||||
// value in-memory, however, such as http.Request.Body you CAN use it to efficiently unmarshal
|
||||
// a JSON object and retrieve the raw JSON in a single call.
|
||||
func NewMapJsonReaderRaw(jsonReader io.Reader) (Map, []byte, error) {
|
||||
jb, err := getJson(jsonReader)
|
||||
if err != nil || len(*jb) == 0 {
|
||||
return nil, *jb, err
|
||||
}
|
||||
|
||||
// Unmarshal the 'presumed' JSON string
|
||||
m, merr := NewMapJson(*jb)
|
||||
return m, *jb, merr
|
||||
}
|
||||
|
||||
// Pull the next JSON string off the stream: just read from first '{' to its closing '}'.
|
||||
// Returning a pointer to the slice saves 16 bytes - maybe unnecessary, but internal to package.
|
||||
func getJson(rdr io.Reader) (*[]byte, error) {
|
||||
bval := make([]byte, 1)
|
||||
jb := make([]byte, 0)
|
||||
var inQuote, inJson bool
|
||||
var parenCnt int
|
||||
var previous byte
|
||||
|
||||
// scan the input for a matched set of {...}
|
||||
// json.Unmarshal will handle syntax checking.
|
||||
for {
|
||||
_, err := rdr.Read(bval)
|
||||
if err != nil {
|
||||
if err == io.EOF && inJson && parenCnt > 0 {
|
||||
return &jb, fmt.Errorf("no closing } for JSON string: %s", string(jb))
|
||||
}
|
||||
return &jb, err
|
||||
}
|
||||
switch bval[0] {
|
||||
case '{':
|
||||
if !inQuote {
|
||||
parenCnt++
|
||||
inJson = true
|
||||
}
|
||||
case '}':
|
||||
if !inQuote {
|
||||
parenCnt--
|
||||
}
|
||||
if parenCnt < 0 {
|
||||
return nil, fmt.Errorf("closing } without opening {: %s", string(jb))
|
||||
}
|
||||
case '"':
|
||||
if inQuote {
|
||||
if previous == '\\' {
|
||||
break
|
||||
}
|
||||
inQuote = false
|
||||
} else {
|
||||
inQuote = true
|
||||
}
|
||||
case '\n', '\r', '\t', ' ':
|
||||
if !inQuote {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if inJson {
|
||||
jb = append(jb, bval[0])
|
||||
if parenCnt == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
previous = bval[0]
|
||||
}
|
||||
|
||||
return &jb, nil
|
||||
}
|
||||
|
||||
// ------------------------------- JSON Reader handler via Map values -----------------------
|
||||
|
||||
// Default poll delay to keep Handler from spinning on an open stream
|
||||
// like sitting on os.Stdin waiting for imput.
|
||||
var jhandlerPollInterval = time.Duration(1e6)
|
||||
|
||||
// While unnecessary, we make HandleJsonReader() have the same signature as HandleXmlReader().
|
||||
// This avoids treating one or other as a special case and discussing the underlying stdlib logic.
|
||||
|
||||
// Bulk process JSON using handlers that process a Map value.
|
||||
// 'rdr' is an io.Reader for the JSON (stream).
|
||||
// 'mapHandler' is the Map processing handler. Return of 'false' stops io.Reader processing.
|
||||
// 'errHandler' is the error processor. Return of 'false' stops io.Reader processing and returns the error.
|
||||
// Note: mapHandler() and errHandler() calls are blocking, so reading and processing of messages is serialized.
|
||||
// This means that you can stop reading the file on error or after processing a particular message.
|
||||
// To have reading and handling run concurrently, pass argument to a go routine in handler and return 'true'.
|
||||
func HandleJsonReader(jsonReader io.Reader, mapHandler func(Map) bool, errHandler func(error) bool) error {
|
||||
var n int
|
||||
for {
|
||||
m, merr := NewMapJsonReader(jsonReader)
|
||||
n++
|
||||
|
||||
// handle error condition with errhandler
|
||||
if merr != nil && merr != io.EOF {
|
||||
merr = fmt.Errorf("[jsonReader: %d] %s", n, merr.Error())
|
||||
if ok := errHandler(merr); !ok {
|
||||
// caused reader termination
|
||||
return merr
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// pass to maphandler
|
||||
if len(m) != 0 {
|
||||
if ok := mapHandler(m); !ok {
|
||||
break
|
||||
}
|
||||
} else if merr != io.EOF {
|
||||
<-time.After(jhandlerPollInterval)
|
||||
}
|
||||
|
||||
if merr == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bulk process JSON using handlers that process a Map value and the raw JSON.
|
||||
// 'rdr' is an io.Reader for the JSON (stream).
|
||||
// 'mapHandler' is the Map and raw JSON - []byte - processor. Return of 'false' stops io.Reader processing.
|
||||
// 'errHandler' is the error and raw JSON processor. Return of 'false' stops io.Reader processing and returns the error.
|
||||
// Note: mapHandler() and errHandler() calls are blocking, so reading and processing of messages is serialized.
|
||||
// This means that you can stop reading the file on error or after processing a particular message.
|
||||
// To have reading and handling run concurrently, pass argument(s) to a go routine in handler and return 'true'.
|
||||
func HandleJsonReaderRaw(jsonReader io.Reader, mapHandler func(Map, []byte) bool, errHandler func(error, []byte) bool) error {
|
||||
var n int
|
||||
for {
|
||||
m, raw, merr := NewMapJsonReaderRaw(jsonReader)
|
||||
n++
|
||||
|
||||
// handle error condition with errhandler
|
||||
if merr != nil && merr != io.EOF {
|
||||
merr = fmt.Errorf("[jsonReader: %d] %s", n, merr.Error())
|
||||
if ok := errHandler(merr, raw); !ok {
|
||||
// caused reader termination
|
||||
return merr
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// pass to maphandler
|
||||
if len(m) != 0 {
|
||||
if ok := mapHandler(m, raw); !ok {
|
||||
break
|
||||
}
|
||||
} else if merr != io.EOF {
|
||||
<-time.After(jhandlerPollInterval)
|
||||
}
|
||||
|
||||
if merr == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+671
@@ -0,0 +1,671 @@
|
||||
// Copyright 2012-2014 Charles Banning. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file
|
||||
|
||||
// keyvalues.go: Extract values from an arbitrary XML doc. Tag path can include wildcard characters.
|
||||
|
||||
package mxj
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ----------------------------- get everything FOR a single key -------------------------
|
||||
|
||||
const (
|
||||
minArraySize = 32
|
||||
)
|
||||
|
||||
var defaultArraySize int = minArraySize
|
||||
|
||||
// Adjust the buffers for expected number of values to return from ValuesForKey() and ValuesForPath().
|
||||
// This can have the effect of significantly reducing memory allocation-copy functions for large data sets.
|
||||
// Returns the initial buffer size.
|
||||
func SetArraySize(size int) int {
|
||||
if size > minArraySize {
|
||||
defaultArraySize = size
|
||||
} else {
|
||||
defaultArraySize = minArraySize
|
||||
}
|
||||
return defaultArraySize
|
||||
}
|
||||
|
||||
// Return all values in Map, 'mv', associated with a 'key'. If len(returned_values) == 0, then no match.
|
||||
// On error, the returned slice is 'nil'. NOTE: 'key' can be wildcard, "*".
|
||||
// 'subkeys' (optional) are "key:val[:type]" strings representing attributes or elements in a list.
|
||||
// - By default 'val' is of type string. "key:val:bool" and "key:val:float" to coerce them.
|
||||
// - For attributes prefix the label with a hyphen, '-', e.g., "-seq:3".
|
||||
// - If the 'key' refers to a list, then "key:value" could select a list member of the list.
|
||||
// - The subkey can be wildcarded - "key:*" - to require that it's there with some value.
|
||||
// - If a subkey is preceeded with the '!' character, the key:value[:type] entry is treated as an
|
||||
// exclusion critera - e.g., "!author:William T. Gaddis".
|
||||
// - If val contains ":" symbol, use SetFieldSeparator to a unused symbol, perhaps "|".
|
||||
func (mv Map) ValuesForKey(key string, subkeys ...string) ([]interface{}, error) {
|
||||
m := map[string]interface{}(mv)
|
||||
var subKeyMap map[string]interface{}
|
||||
if len(subkeys) > 0 {
|
||||
var err error
|
||||
subKeyMap, err = getSubKeyMap(subkeys...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ret := make([]interface{}, 0, defaultArraySize)
|
||||
var cnt int
|
||||
hasKey(m, key, &ret, &cnt, subKeyMap)
|
||||
return ret[:cnt], nil
|
||||
}
|
||||
|
||||
var KeyNotExistError = errors.New("Key does not exist")
|
||||
|
||||
// ValueForKey is a wrapper on ValuesForKey. It returns the first member of []interface{}, if any.
|
||||
// If there is no value, "nil, nil" is returned.
|
||||
func (mv Map) ValueForKey(key string, subkeys ...string) (interface{}, error) {
|
||||
vals, err := mv.ValuesForKey(key, subkeys...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(vals) == 0 {
|
||||
return nil, KeyNotExistError
|
||||
}
|
||||
return vals[0], nil
|
||||
}
|
||||
|
||||
// hasKey - if the map 'key' exists append it to array
|
||||
// if it doesn't do nothing except scan array and map values
|
||||
func hasKey(iv interface{}, key string, ret *[]interface{}, cnt *int, subkeys map[string]interface{}) {
|
||||
// func hasKey(iv interface{}, key string, ret *[]interface{}, subkeys map[string]interface{}) {
|
||||
switch iv.(type) {
|
||||
case map[string]interface{}:
|
||||
vv := iv.(map[string]interface{})
|
||||
// see if the current value is of interest
|
||||
if v, ok := vv[key]; ok {
|
||||
switch v.(type) {
|
||||
case map[string]interface{}:
|
||||
if hasSubKeys(v, subkeys) {
|
||||
*ret = append(*ret, v)
|
||||
*cnt++
|
||||
}
|
||||
case []interface{}:
|
||||
for _, av := range v.([]interface{}) {
|
||||
if hasSubKeys(av, subkeys) {
|
||||
*ret = append(*ret, av)
|
||||
*cnt++
|
||||
}
|
||||
}
|
||||
default:
|
||||
if len(subkeys) == 0 {
|
||||
*ret = append(*ret, v)
|
||||
*cnt++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wildcard case
|
||||
if key == "*" {
|
||||
for _, v := range vv {
|
||||
switch v.(type) {
|
||||
case map[string]interface{}:
|
||||
if hasSubKeys(v, subkeys) {
|
||||
*ret = append(*ret, v)
|
||||
*cnt++
|
||||
}
|
||||
case []interface{}:
|
||||
for _, av := range v.([]interface{}) {
|
||||
if hasSubKeys(av, subkeys) {
|
||||
*ret = append(*ret, av)
|
||||
*cnt++
|
||||
}
|
||||
}
|
||||
default:
|
||||
if len(subkeys) == 0 {
|
||||
*ret = append(*ret, v)
|
||||
*cnt++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scan the rest
|
||||
for _, v := range vv {
|
||||
hasKey(v, key, ret, cnt, subkeys)
|
||||
}
|
||||
case []interface{}:
|
||||
for _, v := range iv.([]interface{}) {
|
||||
hasKey(v, key, ret, cnt, subkeys)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------- get everything for a node in the Map ---------------------------
|
||||
|
||||
// Allow indexed arrays in "path" specification. (Request from Abhijit Kadam - abhijitk100@gmail.com.)
|
||||
// 2014.04.28 - implementation note.
|
||||
// Implemented as a wrapper of (old)ValuesForPath() because we need look-ahead logic to handle expansion
|
||||
// of wildcards and unindexed arrays. Embedding such logic into valuesForKeyPath() would have made the
|
||||
// code much more complicated; this wrapper is straightforward, easy to debug, and doesn't add significant overhead.
|
||||
|
||||
// Retrieve all values for a path from the Map. If len(returned_values) == 0, then no match.
|
||||
// On error, the returned array is 'nil'.
|
||||
// 'path' is a dot-separated path of key values.
|
||||
// - If a node in the path is '*', then everything beyond is walked.
|
||||
// - 'path' can contain indexed array references, such as, "*.data[1]" and "msgs[2].data[0].field" -
|
||||
// even "*[2].*[0].field".
|
||||
// 'subkeys' (optional) are "key:val[:type]" strings representing attributes or elements in a list.
|
||||
// - By default 'val' is of type string. "key:val:bool" and "key:val:float" to coerce them.
|
||||
// - For attributes prefix the label with a hyphen, '-', e.g., "-seq:3".
|
||||
// - If the 'path' refers to a list, then "tag:value" would return member of the list.
|
||||
// - The subkey can be wildcarded - "key:*" - to require that it's there with some value.
|
||||
// - If a subkey is preceeded with the '!' character, the key:value[:type] entry is treated as an
|
||||
// exclusion critera - e.g., "!author:William T. Gaddis".
|
||||
// - If val contains ":" symbol, use SetFieldSeparator to a unused symbol, perhaps "|".
|
||||
func (mv Map) ValuesForPath(path string, subkeys ...string) ([]interface{}, error) {
|
||||
// If there are no array indexes in path, use legacy ValuesForPath() logic.
|
||||
if strings.Index(path, "[") < 0 {
|
||||
return mv.oldValuesForPath(path, subkeys...)
|
||||
}
|
||||
|
||||
var subKeyMap map[string]interface{}
|
||||
if len(subkeys) > 0 {
|
||||
var err error
|
||||
subKeyMap, err = getSubKeyMap(subkeys...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
keys, kerr := parsePath(path)
|
||||
if kerr != nil {
|
||||
return nil, kerr
|
||||
}
|
||||
|
||||
vals, verr := valuesForArray(keys, mv)
|
||||
if verr != nil {
|
||||
return nil, verr // Vals may be nil, but return empty array.
|
||||
}
|
||||
|
||||
// Need to handle subkeys ... only return members of vals that satisfy conditions.
|
||||
retvals := make([]interface{}, 0)
|
||||
for _, v := range vals {
|
||||
if hasSubKeys(v, subKeyMap) {
|
||||
retvals = append(retvals, v)
|
||||
}
|
||||
}
|
||||
return retvals, nil
|
||||
}
|
||||
|
||||
func valuesForArray(keys []*key, m Map) ([]interface{}, error) {
|
||||
var tmppath string
|
||||
var haveFirst bool
|
||||
var vals []interface{}
|
||||
var verr error
|
||||
|
||||
lastkey := len(keys) - 1
|
||||
for i := 0; i <= lastkey; i++ {
|
||||
if !haveFirst {
|
||||
tmppath = keys[i].name
|
||||
haveFirst = true
|
||||
} else {
|
||||
tmppath += "." + keys[i].name
|
||||
}
|
||||
|
||||
// Look-ahead: explode wildcards and unindexed arrays.
|
||||
// Need to handle un-indexed list recursively:
|
||||
// e.g., path is "stuff.data[0]" rather than "stuff[0].data[0]".
|
||||
// Need to treat it as "stuff[0].data[0]", "stuff[1].data[0]", ...
|
||||
if !keys[i].isArray && i < lastkey && keys[i+1].isArray {
|
||||
// Can't pass subkeys because we may not be at literal end of path.
|
||||
vv, vverr := m.oldValuesForPath(tmppath)
|
||||
if vverr != nil {
|
||||
return nil, vverr
|
||||
}
|
||||
for _, v := range vv {
|
||||
// See if we can walk the value.
|
||||
am, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Work the backend.
|
||||
nvals, nvalserr := valuesForArray(keys[i+1:], Map(am))
|
||||
if nvalserr != nil {
|
||||
return nil, nvalserr
|
||||
}
|
||||
vals = append(vals, nvals...)
|
||||
}
|
||||
break // have recursed the whole path - return
|
||||
}
|
||||
|
||||
if keys[i].isArray || i == lastkey {
|
||||
// Don't pass subkeys because may not be at literal end of path.
|
||||
vals, verr = m.oldValuesForPath(tmppath)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if verr != nil {
|
||||
return nil, verr
|
||||
}
|
||||
|
||||
if i == lastkey && !keys[i].isArray {
|
||||
break
|
||||
}
|
||||
|
||||
// Now we're looking at an array - supposedly.
|
||||
// Is index in range of vals?
|
||||
if len(vals) <= keys[i].position {
|
||||
vals = nil
|
||||
break
|
||||
}
|
||||
|
||||
// Return the array member of interest, if at end of path.
|
||||
if i == lastkey {
|
||||
vals = vals[keys[i].position:(keys[i].position + 1)]
|
||||
break
|
||||
}
|
||||
|
||||
// Extract the array member of interest.
|
||||
am := vals[keys[i].position:(keys[i].position + 1)]
|
||||
|
||||
// must be a map[string]interface{} value so we can keep walking the path
|
||||
amm, ok := am[0].(map[string]interface{})
|
||||
if !ok {
|
||||
vals = nil
|
||||
break
|
||||
}
|
||||
|
||||
m = Map(amm)
|
||||
haveFirst = false
|
||||
}
|
||||
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
type key struct {
|
||||
name string
|
||||
isArray bool
|
||||
position int
|
||||
}
|
||||
|
||||
func parsePath(s string) ([]*key, error) {
|
||||
keys := strings.Split(s, ".")
|
||||
|
||||
ret := make([]*key, 0)
|
||||
|
||||
for i := 0; i < len(keys); i++ {
|
||||
if keys[i] == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
newkey := new(key)
|
||||
if strings.Index(keys[i], "[") < 0 {
|
||||
newkey.name = keys[i]
|
||||
ret = append(ret, newkey)
|
||||
continue
|
||||
}
|
||||
|
||||
p := strings.Split(keys[i], "[")
|
||||
newkey.name = p[0]
|
||||
p = strings.Split(p[1], "]")
|
||||
if p[0] == "" { // no right bracket
|
||||
return nil, fmt.Errorf("no right bracket on key index: %s", keys[i])
|
||||
}
|
||||
// convert p[0] to a int value
|
||||
pos, nerr := strconv.ParseInt(p[0], 10, 32)
|
||||
if nerr != nil {
|
||||
return nil, fmt.Errorf("cannot convert index to int value: %s", p[0])
|
||||
}
|
||||
newkey.position = int(pos)
|
||||
newkey.isArray = true
|
||||
ret = append(ret, newkey)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// legacy ValuesForPath() - now wrapped to handle special case of indexed arrays in 'path'.
|
||||
func (mv Map) oldValuesForPath(path string, subkeys ...string) ([]interface{}, error) {
|
||||
m := map[string]interface{}(mv)
|
||||
var subKeyMap map[string]interface{}
|
||||
if len(subkeys) > 0 {
|
||||
var err error
|
||||
subKeyMap, err = getSubKeyMap(subkeys...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
keys := strings.Split(path, ".")
|
||||
if keys[len(keys)-1] == "" {
|
||||
keys = keys[:len(keys)-1]
|
||||
}
|
||||
ivals := make([]interface{}, 0, defaultArraySize)
|
||||
var cnt int
|
||||
valuesForKeyPath(&ivals, &cnt, m, keys, subKeyMap)
|
||||
return ivals[:cnt], nil
|
||||
}
|
||||
|
||||
func valuesForKeyPath(ret *[]interface{}, cnt *int, m interface{}, keys []string, subkeys map[string]interface{}) {
|
||||
lenKeys := len(keys)
|
||||
|
||||
// load 'm' values into 'ret'
|
||||
// expand any lists
|
||||
if lenKeys == 0 {
|
||||
switch m.(type) {
|
||||
case map[string]interface{}:
|
||||
if subkeys != nil {
|
||||
if ok := hasSubKeys(m, subkeys); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
*ret = append(*ret, m)
|
||||
*cnt++
|
||||
case []interface{}:
|
||||
for i, v := range m.([]interface{}) {
|
||||
if subkeys != nil {
|
||||
if ok := hasSubKeys(v, subkeys); !ok {
|
||||
continue // only load list members with subkeys
|
||||
}
|
||||
}
|
||||
*ret = append(*ret, (m.([]interface{}))[i])
|
||||
*cnt++
|
||||
}
|
||||
default:
|
||||
if subkeys != nil {
|
||||
return // must be map[string]interface{} if there are subkeys
|
||||
}
|
||||
*ret = append(*ret, m)
|
||||
*cnt++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// key of interest
|
||||
key := keys[0]
|
||||
switch key {
|
||||
case "*": // wildcard - scan all values
|
||||
switch m.(type) {
|
||||
case map[string]interface{}:
|
||||
for _, v := range m.(map[string]interface{}) {
|
||||
// valuesForKeyPath(ret, v, keys[1:], subkeys)
|
||||
valuesForKeyPath(ret, cnt, v, keys[1:], subkeys)
|
||||
}
|
||||
case []interface{}:
|
||||
for _, v := range m.([]interface{}) {
|
||||
switch v.(type) {
|
||||
// flatten out a list of maps - keys are processed
|
||||
case map[string]interface{}:
|
||||
for _, vv := range v.(map[string]interface{}) {
|
||||
// valuesForKeyPath(ret, vv, keys[1:], subkeys)
|
||||
valuesForKeyPath(ret, cnt, vv, keys[1:], subkeys)
|
||||
}
|
||||
default:
|
||||
// valuesForKeyPath(ret, v, keys[1:], subkeys)
|
||||
valuesForKeyPath(ret, cnt, v, keys[1:], subkeys)
|
||||
}
|
||||
}
|
||||
}
|
||||
default: // key - must be map[string]interface{}
|
||||
switch m.(type) {
|
||||
case map[string]interface{}:
|
||||
if v, ok := m.(map[string]interface{})[key]; ok {
|
||||
// valuesForKeyPath(ret, v, keys[1:], subkeys)
|
||||
valuesForKeyPath(ret, cnt, v, keys[1:], subkeys)
|
||||
}
|
||||
case []interface{}: // may be buried in list
|
||||
for _, v := range m.([]interface{}) {
|
||||
switch v.(type) {
|
||||
case map[string]interface{}:
|
||||
if vv, ok := v.(map[string]interface{})[key]; ok {
|
||||
// valuesForKeyPath(ret, vv, keys[1:], subkeys)
|
||||
valuesForKeyPath(ret, cnt, vv, keys[1:], subkeys)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hasSubKeys() - interface{} equality works for string, float64, bool
|
||||
// 'v' must be a map[string]interface{} value to have subkeys
|
||||
// 'a' can have k:v pairs with v.(string) == "*", which is treated like a wildcard.
|
||||
func hasSubKeys(v interface{}, subkeys map[string]interface{}) bool {
|
||||
if len(subkeys) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
switch v.(type) {
|
||||
case map[string]interface{}:
|
||||
// do all subKey name:value pairs match?
|
||||
mv := v.(map[string]interface{})
|
||||
for skey, sval := range subkeys {
|
||||
isNotKey := false
|
||||
if skey[:1] == "!" { // a NOT-key
|
||||
skey = skey[1:]
|
||||
isNotKey = true
|
||||
}
|
||||
vv, ok := mv[skey]
|
||||
if !ok { // key doesn't exist
|
||||
if isNotKey { // key not there, but that's what we want
|
||||
if kv, ok := sval.(string); ok && kv == "*" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// wildcard check
|
||||
if kv, ok := sval.(string); ok && kv == "*" {
|
||||
if isNotKey { // key is there, and we don't want it
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch sval.(type) {
|
||||
case string:
|
||||
if s, ok := vv.(string); ok && s == sval.(string) {
|
||||
if isNotKey {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
case bool:
|
||||
if b, ok := vv.(bool); ok && b == sval.(bool) {
|
||||
if isNotKey {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
case float64:
|
||||
if f, ok := vv.(float64); ok && f == sval.(float64) {
|
||||
if isNotKey {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
// key there but didn't match subkey value
|
||||
if isNotKey { // that's what we want
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
// all subkeys matched
|
||||
return true
|
||||
}
|
||||
|
||||
// not a map[string]interface{} value, can't have subkeys
|
||||
return false
|
||||
}
|
||||
|
||||
// Generate map of key:value entries as map[string]string.
|
||||
// 'kv' arguments are "name:value" pairs: attribute keys are designated with prepended hyphen, '-'.
|
||||
// If len(kv) == 0, the return is (nil, nil).
|
||||
func getSubKeyMap(kv ...string) (map[string]interface{}, error) {
|
||||
if len(kv) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
m := make(map[string]interface{}, 0)
|
||||
for _, v := range kv {
|
||||
vv := strings.Split(v, fieldSep)
|
||||
switch len(vv) {
|
||||
case 2:
|
||||
m[vv[0]] = interface{}(vv[1])
|
||||
case 3:
|
||||
switch vv[2] {
|
||||
case "string", "char", "text":
|
||||
m[vv[0]] = interface{}(vv[1])
|
||||
case "bool", "boolean":
|
||||
// ParseBool treats "1"==true & "0"==false
|
||||
b, err := strconv.ParseBool(vv[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't convert subkey value to bool: %s", vv[1])
|
||||
}
|
||||
m[vv[0]] = interface{}(b)
|
||||
case "float", "float64", "num", "number", "numeric":
|
||||
f, err := strconv.ParseFloat(vv[1], 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't convert subkey value to float: %s", vv[1])
|
||||
}
|
||||
m[vv[0]] = interface{}(f)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown subkey conversion spec: %s", v)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown subkey spec: %s", v)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ------------------------------- END of valuesFor ... ----------------------------
|
||||
|
||||
// ----------------------- locate where a key value is in the tree -------------------
|
||||
|
||||
//----------------------------- find all paths to a key --------------------------------
|
||||
|
||||
// Get all paths through Map, 'mv', (in dot-notation) that terminate with the specified key.
|
||||
// Results can be used with ValuesForPath.
|
||||
func (mv Map) PathsForKey(key string) []string {
|
||||
m := map[string]interface{}(mv)
|
||||
breadbasket := make(map[string]bool, 0)
|
||||
breadcrumbs := ""
|
||||
|
||||
hasKeyPath(breadcrumbs, m, key, breadbasket)
|
||||
if len(breadbasket) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unpack map keys to return
|
||||
res := make([]string, len(breadbasket))
|
||||
var i int
|
||||
for k := range breadbasket {
|
||||
res[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Extract the shortest path from all possible paths - from PathsForKey() - in Map, 'mv'..
|
||||
// Paths are strings using dot-notation.
|
||||
func (mv Map) PathForKeyShortest(key string) string {
|
||||
paths := mv.PathsForKey(key)
|
||||
|
||||
lp := len(paths)
|
||||
if lp == 0 {
|
||||
return ""
|
||||
}
|
||||
if lp == 1 {
|
||||
return paths[0]
|
||||
}
|
||||
|
||||
shortest := paths[0]
|
||||
shortestLen := len(strings.Split(shortest, "."))
|
||||
|
||||
for i := 1; i < len(paths); i++ {
|
||||
vlen := len(strings.Split(paths[i], "."))
|
||||
if vlen < shortestLen {
|
||||
shortest = paths[i]
|
||||
shortestLen = vlen
|
||||
}
|
||||
}
|
||||
|
||||
return shortest
|
||||
}
|
||||
|
||||
// hasKeyPath - if the map 'key' exists append it to KeyPath.path and increment KeyPath.depth
|
||||
// This is really just a breadcrumber that saves all trails that hit the prescribed 'key'.
|
||||
func hasKeyPath(crumbs string, iv interface{}, key string, basket map[string]bool) {
|
||||
switch iv.(type) {
|
||||
case map[string]interface{}:
|
||||
vv := iv.(map[string]interface{})
|
||||
if _, ok := vv[key]; ok {
|
||||
// create a new breadcrumb, intialized with the one we have
|
||||
var nbc string
|
||||
if crumbs == "" {
|
||||
nbc = key
|
||||
} else {
|
||||
nbc = crumbs + "." + key
|
||||
}
|
||||
basket[nbc] = true
|
||||
}
|
||||
// walk on down the path, key could occur again at deeper node
|
||||
for k, v := range vv {
|
||||
// create a new breadcrumb, intialized with the one we have
|
||||
var nbc string
|
||||
if crumbs == "" {
|
||||
nbc = k
|
||||
} else {
|
||||
nbc = crumbs + "." + k
|
||||
}
|
||||
hasKeyPath(nbc, v, key, basket)
|
||||
}
|
||||
case []interface{}:
|
||||
// crumb-trail doesn't change, pass it on
|
||||
for _, v := range iv.([]interface{}) {
|
||||
hasKeyPath(crumbs, v, key, basket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var PathNotExistError = errors.New("Path does not exist")
|
||||
|
||||
// ValueForPath wrap ValuesFor Path and returns the first value returned.
|
||||
// If no value is found it returns 'nil' and PathNotExistError.
|
||||
func (mv Map) ValueForPath(path string) (interface{}, error) {
|
||||
vals, err := mv.ValuesForPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(vals) == 0 {
|
||||
return nil, PathNotExistError
|
||||
}
|
||||
return vals[0], nil
|
||||
}
|
||||
|
||||
// Returns the first found value for the path as a string.
|
||||
func (mv Map) ValueForPathString(path string) (string, error) {
|
||||
vals, err := mv.ValuesForPath(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(vals) == 0 {
|
||||
return "", errors.New("ValueForPath: path not found")
|
||||
}
|
||||
val := vals[0]
|
||||
switch str := val.(type) {
|
||||
case string:
|
||||
return str, nil
|
||||
default:
|
||||
return "", fmt.Errorf("ValueForPath: unsupported type: %T", str)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the first found value for the path as a string.
|
||||
// If the path is not found then it returns an empty string.
|
||||
func (mv Map) ValueOrEmptyForPathString(path string) string {
|
||||
str, _ := mv.ValueForPathString(path)
|
||||
return str
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package mxj
|
||||
|
||||
// leafnode.go - return leaf nodes with paths and values for the Map
|
||||
// inspired by: https://groups.google.com/forum/#!topic/golang-nuts/3JhuVKRuBbw
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
NoAttributes = true // suppress LeafNode values that are attributes
|
||||
)
|
||||
|
||||
// LeafNode - a terminal path value in a Map.
|
||||
// For XML Map values it represents an attribute or simple element value - of type
|
||||
// string unless Map was created using Cast flag. For JSON Map values it represents
|
||||
// a string, numeric, boolean, or null value.
|
||||
type LeafNode struct {
|
||||
Path string // a dot-notation representation of the path with array subscripting
|
||||
Value interface{} // the value at the path termination
|
||||
}
|
||||
|
||||
// LeafNodes - returns an array of all LeafNode values for the Map.
|
||||
// The option no_attr argument suppresses attribute values (keys with prepended hyphen, '-')
|
||||
// as well as the "#text" key for the associated simple element value.
|
||||
//
|
||||
// PrependAttrWithHypen(false) will result in attributes having .attr-name as
|
||||
// terminal node in 'path' while the path for the element value, itself, will be
|
||||
// the base path w/o "#text".
|
||||
//
|
||||
// LeafUseDotNotation(true) causes list members to be identified using ".N" syntax
|
||||
// rather than "[N]" syntax.
|
||||
func (mv Map) LeafNodes(no_attr ...bool) []LeafNode {
|
||||
var a bool
|
||||
if len(no_attr) == 1 {
|
||||
a = no_attr[0]
|
||||
}
|
||||
|
||||
l := make([]LeafNode, 0)
|
||||
getLeafNodes("", "", map[string]interface{}(mv), &l, a)
|
||||
return l
|
||||
}
|
||||
|
||||
func getLeafNodes(path, node string, mv interface{}, l *[]LeafNode, noattr bool) {
|
||||
// if stripping attributes, then also strip "#text" key
|
||||
if !noattr || node != "#text" {
|
||||
if path != "" && node[:1] != "[" {
|
||||
path += "."
|
||||
}
|
||||
path += node
|
||||
}
|
||||
switch mv.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, v := range mv.(map[string]interface{}) {
|
||||
// if noattr && k[:1] == "-" {
|
||||
if noattr && len(attrPrefix) > 0 && strings.Index(k, attrPrefix) == 0 {
|
||||
continue
|
||||
}
|
||||
getLeafNodes(path, k, v, l, noattr)
|
||||
}
|
||||
case []interface{}:
|
||||
for i, v := range mv.([]interface{}) {
|
||||
if useDotNotation {
|
||||
getLeafNodes(path, strconv.Itoa(i), v, l, noattr)
|
||||
} else {
|
||||
getLeafNodes(path, "["+strconv.Itoa(i)+"]", v, l, noattr)
|
||||
}
|
||||
}
|
||||
default:
|
||||
// can't walk any further, so create leaf
|
||||
n := LeafNode{path, mv}
|
||||
*l = append(*l, n)
|
||||
}
|
||||
}
|
||||
|
||||
// LeafPaths - all paths that terminate in LeafNode values.
|
||||
func (mv Map) LeafPaths(no_attr ...bool) []string {
|
||||
ln := mv.LeafNodes()
|
||||
ss := make([]string, len(ln))
|
||||
for i := 0; i < len(ln); i++ {
|
||||
ss[i] = ln[i].Path
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
// LeafValues - all terminal values in the Map.
|
||||
func (mv Map) LeafValues(no_attr ...bool) []interface{} {
|
||||
ln := mv.LeafNodes()
|
||||
vv := make([]interface{}, len(ln))
|
||||
for i := 0; i < len(ln); i++ {
|
||||
vv[i] = ln[i].Value
|
||||
}
|
||||
return vv
|
||||
}
|
||||
|
||||
// ====================== utilities ======================
|
||||
|
||||
// https://groups.google.com/forum/#!topic/golang-nuts/pj0C5IrZk4I
|
||||
var useDotNotation bool
|
||||
|
||||
// LeafUseDotNotation sets a flag that list members in LeafNode paths
|
||||
// should be identified using ".N" syntax rather than the default "[N]"
|
||||
// syntax. Calling LeafUseDotNotation with no arguments toggles the
|
||||
// flag on/off; otherwise, the argument sets the flag value 'true'/'false'.
|
||||
func LeafUseDotNotation(b ...bool) {
|
||||
if len(b) == 0 {
|
||||
useDotNotation = !useDotNotation
|
||||
return
|
||||
}
|
||||
useDotNotation = b[0]
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
// Copyright 2016 Charles Banning. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file
|
||||
|
||||
// misc.go - mimic functions (+others) called out in:
|
||||
// https://groups.google.com/forum/#!topic/golang-nuts/jm_aGsJNbdQ
|
||||
// Primarily these methods let you retrive XML structure information.
|
||||
|
||||
package mxj
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Return the root element of the Map. If there is not a single key in Map,
|
||||
// then an error is returned.
|
||||
func (mv Map) Root() (string, error) {
|
||||
mm := map[string]interface{}(mv)
|
||||
if len(mm) != 1 {
|
||||
return "", fmt.Errorf("Map does not have singleton root. Len: %d.", len(mm))
|
||||
}
|
||||
for k, _ := range mm {
|
||||
return k, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// If the path is an element with sub-elements, return a list of the sub-element
|
||||
// keys. (The list is alphabeticly sorted.) NOTE: Map keys that are prefixed with
|
||||
// '-', a hyphen, are considered attributes; see m.Attributes(path).
|
||||
func (mv Map) Elements(path string) ([]string, error) {
|
||||
e, err := mv.ValueForPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch e.(type) {
|
||||
case map[string]interface{}:
|
||||
ee := e.(map[string]interface{})
|
||||
elems := make([]string, len(ee))
|
||||
var i int
|
||||
for k, _ := range ee {
|
||||
if len(attrPrefix) > 0 && strings.Index(k, attrPrefix) == 0 {
|
||||
continue // skip attributes
|
||||
}
|
||||
elems[i] = k
|
||||
i++
|
||||
}
|
||||
elems = elems[:i]
|
||||
// alphabetic sort keeps things tidy
|
||||
sort.Strings(elems)
|
||||
return elems, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no elements for path: %s", path)
|
||||
}
|
||||
|
||||
// If the path is an element with attributes, return a list of the attribute
|
||||
// keys. (The list is alphabeticly sorted.) NOTE: Map keys that are not prefixed with
|
||||
// '-', a hyphen, are not treated as attributes; see m.Elements(path). Also, if the
|
||||
// attribute prefix is "" - SetAttrPrefix("") or PrependAttrWithHyphen(false) - then
|
||||
// there are no identifiable attributes.
|
||||
func (mv Map) Attributes(path string) ([]string, error) {
|
||||
a, err := mv.ValueForPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch a.(type) {
|
||||
case map[string]interface{}:
|
||||
aa := a.(map[string]interface{})
|
||||
attrs := make([]string, len(aa))
|
||||
var i int
|
||||
for k, _ := range aa {
|
||||
if len(attrPrefix) == 0 || strings.Index(k, attrPrefix) != 0 {
|
||||
continue // skip non-attributes
|
||||
}
|
||||
attrs[i] = k[len(attrPrefix):]
|
||||
i++
|
||||
}
|
||||
attrs = attrs[:i]
|
||||
// alphabetic sort keeps things tidy
|
||||
sort.Strings(attrs)
|
||||
return attrs, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no attributes for path: %s", path)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user