Merge pull request #5 from drone-plugins/feature/unify
Some unification with other plugins
This commit is contained in:
commit
200e836397
15
.drone.yml
15
.drone.yml
|
@ -1,14 +1,9 @@
|
||||||
build:
|
build:
|
||||||
image: golang:1.5
|
image: golang:1.5
|
||||||
environment:
|
|
||||||
- GO15VENDOREXPERIMENT=1
|
|
||||||
- GOOS=linux
|
|
||||||
- GOARCH=amd64
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
commands:
|
commands:
|
||||||
- go get
|
- make deps
|
||||||
- go build
|
- make build
|
||||||
- go test
|
- make test
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
docker:
|
docker:
|
||||||
|
@ -21,10 +16,12 @@ publish:
|
||||||
|
|
||||||
plugin:
|
plugin:
|
||||||
name: Webhook
|
name: Webhook
|
||||||
desc: Send webhook notifications when your build completes.
|
desc: Send build status notifications via Webhook
|
||||||
type: notify
|
type: notify
|
||||||
image: plugins/drone-webhook
|
image: plugins/drone-webhook
|
||||||
labels:
|
labels:
|
||||||
|
- notify
|
||||||
|
- webhook
|
||||||
- rest
|
- rest
|
||||||
- json
|
- json
|
||||||
- hook
|
- hook
|
||||||
|
|
|
@ -1 +1,26 @@
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
|
||||||
drone-webhook
|
drone-webhook
|
||||||
|
|
89
DOCS.md
89
DOCS.md
|
@ -1,13 +1,14 @@
|
||||||
Use the Webhook plugin to notify services via Webhook when a build completes.
|
Use the Webhook plugin to notify services via Webhook when a build completes.
|
||||||
You will need to supply Drone with outgoing Webhook URLs.
|
You will need to supply Drone with outgoing Webhook URLs. You can override the
|
||||||
|
default configuration with the following parameters:
|
||||||
|
|
||||||
The following parameters are used to configure outgoing Webhooks:
|
* `urls` - JSON payloads are sent to each URL
|
||||||
|
|
||||||
* `urls` - JSON payloads are sent to each URL listed here
|
|
||||||
* `method` - HTTP request method. Defaults to `POST`
|
* `method` - HTTP request method. Defaults to `POST`
|
||||||
* `header` - HTTP request header map
|
* `header` - HTTP request header map
|
||||||
|
|
||||||
The following is a sample Webhook configuration in your .drone.yml file:
|
## Example
|
||||||
|
|
||||||
|
The following is a sample configuration in your .drone.yml file:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
notify:
|
notify:
|
||||||
|
@ -19,53 +20,10 @@ notify:
|
||||||
Authorization: pa55word
|
Authorization: pa55word
|
||||||
```
|
```
|
||||||
|
|
||||||
The following is an example Webhook payload (whitespace added):
|
### Custom Body
|
||||||
|
|
||||||
```json
|
In some cases you may want to submit a custom payload in the body of your hook.
|
||||||
{
|
For the use case we expose the following additional parameters:
|
||||||
"build": {
|
|
||||||
"author": "johnsmith",
|
|
||||||
"author_avatar": "",
|
|
||||||
"author_email": "john.smith@gmail.com",
|
|
||||||
"branch": "master",
|
|
||||||
"commit": "9f2849d5",
|
|
||||||
"created_at": 0,
|
|
||||||
"enqueued_at": 0,
|
|
||||||
"event": "",
|
|
||||||
"finished_at": 1421029813,
|
|
||||||
"link_url": "",
|
|
||||||
"message": "Update the Readme",
|
|
||||||
"number": 22,
|
|
||||||
"ref": "",
|
|
||||||
"refspec": "",
|
|
||||||
"remote": "",
|
|
||||||
"started_at": 1421029603,
|
|
||||||
"status": "success",
|
|
||||||
"timestamp": 0,
|
|
||||||
"title": ""
|
|
||||||
},
|
|
||||||
"repo": {
|
|
||||||
"allow_deploys": false,
|
|
||||||
"allow_pr": false,
|
|
||||||
"allow_push": false,
|
|
||||||
"allow_tags": false,
|
|
||||||
"avatar_url": "",
|
|
||||||
"clone_url": "",
|
|
||||||
"default_branch": "",
|
|
||||||
"full_name": "foo/bar",
|
|
||||||
"link_url": "",
|
|
||||||
"name": "bar",
|
|
||||||
"owner": "foo",
|
|
||||||
"private": false,
|
|
||||||
"timeout": 0,
|
|
||||||
"trusted": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Custom Body
|
|
||||||
|
|
||||||
In some cases you may want to submit a custom payload in the body of your hook. For the use case we expose the following additional parameters:
|
|
||||||
|
|
||||||
* `template` - Handlebars template to create a custom payload body. See [docs](http://handlebarsjs.com/)
|
* `template` - Handlebars template to create a custom payload body. See [docs](http://handlebarsjs.com/)
|
||||||
* `content_type` - HTTP request content type
|
* `content_type` - HTTP request content type
|
||||||
|
@ -85,15 +43,18 @@ notify:
|
||||||
commit: {{build.commit}}
|
commit: {{build.commit}}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Basic Authentication
|
### Basic Authentication
|
||||||
|
|
||||||
>It is important to note that with HTTP Basic Authentication the provided username and password are not encrypted.
|
> It is important to note that with HTTP Basic Authentication the provided
|
||||||
|
> username and password are not encrypted.
|
||||||
|
|
||||||
In some cases your webhook may need to authenticate with another service. You can set the basic `Authentication` header with a username and password. For these use cases we expose the following additional parameters:
|
In some cases your webhook may need to authenticate with another service. You
|
||||||
|
can set the basic `Authentication` header with a username and password. For
|
||||||
|
these use cases we expose the following additional parameters:
|
||||||
|
|
||||||
* `auth` - Sets the request's `Authorization` header to use HTTP Basic Authentication with the provided username and password below.
|
* `auth` - Sets the request's `Authorization` header to use HTTP Basic Authentication with the provided username and password below
|
||||||
* `username` - The username as a string.
|
* `username` - The username as a string
|
||||||
* `password` - The password as a string.
|
* `password` - The password as a string
|
||||||
|
|
||||||
Example configuration to include HTTP Basic Authentication:
|
Example configuration to include HTTP Basic Authentication:
|
||||||
|
|
||||||
|
@ -108,13 +69,17 @@ notify:
|
||||||
- https://tower.example.com/...
|
- https://tower.example.com/...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Debugging Webhooks
|
### Debugging Webhooks
|
||||||
|
|
||||||
>If you have private variables that are encrypted and hidden in `.drone.sec`, remember that the `debug` flag may print out those sensitive values. Please use `dubug: true` wisely.
|
> If you have private variables that are encrypted and hidden in `.drone.sec`,
|
||||||
|
> remember that the `debug` flag may print out those sensitive values. Please
|
||||||
|
> use `debug: true` wisely.
|
||||||
|
|
||||||
In some cases complicated webhooks may need debugging to ensure `urls`, `template`, `auth` and more a properly configured. For these use cases we expose the following `debug` parameter:
|
In some cases complicated webhooks may need debugging to ensure `urls`,
|
||||||
|
`template`, `auth` and more a properly configured. For these use cases we expose
|
||||||
|
the following `debug` parameter:
|
||||||
|
|
||||||
* `debug` - If `debug: true` it will print out each URL request and response information.
|
* `debug` - If `true` it will print out each URL request and response information
|
||||||
|
|
||||||
Example configuration to include the `debug` parameter:
|
Example configuration to include the `debug` parameter:
|
||||||
|
|
||||||
|
@ -135,7 +100,7 @@ notify:
|
||||||
|
|
||||||
Example of a debug print result:
|
Example of a debug print result:
|
||||||
|
|
||||||
```yaml
|
```
|
||||||
[debug] Webhook 1
|
[debug] Webhook 1
|
||||||
URL: http://tower.example.com/api/v1/job_templates/44/launch/
|
URL: http://tower.example.com/api/v1/job_templates/44/launch/
|
||||||
METHOD: POST
|
METHOD: POST
|
||||||
|
|
14
Dockerfile
14
Dockerfile
|
@ -1,9 +1,15 @@
|
||||||
# Docker image for Drone's webhook notification plugin
|
# Docker image for the Drone Webhook plugin
|
||||||
#
|
#
|
||||||
# CGO_ENABLED=0 go build -a -tags netgo
|
# cd $GOPATH/src/github.com/drone-plugins/drone-webhook
|
||||||
|
# make deps build
|
||||||
# docker build --rm=true -t plugins/drone-webhook .
|
# docker build --rm=true -t plugins/drone-webhook .
|
||||||
|
|
||||||
FROM gliderlabs/alpine:3.2
|
FROM alpine:3.2
|
||||||
RUN apk-install ca-certificates
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk add \
|
||||||
|
ca-certificates && \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
ADD drone-webhook /bin/
|
ADD drone-webhook /bin/
|
||||||
ENTRYPOINT ["/bin/drone-webhook"]
|
ENTRYPOINT ["/bin/drone-webhook"]
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
.PHONY: clean deps test build
|
||||||
|
|
||||||
|
export GOOS ?= linux
|
||||||
|
export GOARCH ?= amd64
|
||||||
|
export CGO_ENABLED ?= 0
|
||||||
|
|
||||||
|
CI_BUILD_NUMBER ?= 0
|
||||||
|
|
||||||
|
LDFLAGS += -X "main.buildDate=$(shell date -u '+%Y-%m-%d %H:%M:%S %Z')"
|
||||||
|
LDFLAGS += -X "main.build=$(CI_BUILD_NUMBER)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
go clean -i ./...
|
||||||
|
|
||||||
|
deps:
|
||||||
|
go get -t ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -cover ./...
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -ldflags '-s -w $(LDFLAGS)'
|
18
README.md
18
README.md
|
@ -1,12 +1,11 @@
|
||||||
# drone-webhook
|
# drone-webhook
|
||||||
Drone plugin for sending Webhook notifications.
|
|
||||||
|
|
||||||
## Overview
|
Drone plugin for sending notifications via Webhook
|
||||||
|
|
||||||
This plugin is responsible for sending build notifications via Webhooks:
|
## Usage
|
||||||
|
|
||||||
```sh
|
```
|
||||||
./drone-webhooks <<EOF
|
./drone-webhook <<EOF
|
||||||
{
|
{
|
||||||
"repo" : {
|
"repo" : {
|
||||||
"owner": "foo",
|
"owner": "foo",
|
||||||
|
@ -44,15 +43,14 @@ EOF
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
Build the Docker container. Note that we need to use the `-netgo` tag so that
|
Build the Docker container using `make`:
|
||||||
the binary is built without a CGO dependency:
|
|
||||||
|
|
||||||
```sh
|
```
|
||||||
CGO_ENABLED=0 go build -a -tags netgo
|
make deps build
|
||||||
docker build --rm=true -t plugins/drone-webhook .
|
docker build --rm=true -t plugins/drone-webhook .
|
||||||
```
|
```
|
||||||
|
|
||||||
Send a Webhook notification:
|
### Example
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run -i plugins/drone-webhook <<EOF
|
docker run -i plugins/drone-webhook <<EOF
|
||||||
|
|
51
main.go
51
main.go
|
@ -14,45 +14,45 @@ import (
|
||||||
"github.com/drone/drone-go/template"
|
"github.com/drone/drone-go/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
build string
|
||||||
|
buildDate string
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
fmt.Printf("Drone Webhook Plugin built at %s\n", buildDate)
|
||||||
|
|
||||||
// plugin settings
|
system := drone.System{}
|
||||||
var sys = drone.System{}
|
repo := drone.Repo{}
|
||||||
var repo = drone.Repo{}
|
build := drone.Build{}
|
||||||
var build = drone.Build{}
|
vargs := Params{}
|
||||||
var vargs = Webhook{}
|
|
||||||
|
|
||||||
// set plugin parameters
|
plugin.Param("system", &system)
|
||||||
plugin.Param("system", &sys)
|
|
||||||
plugin.Param("repo", &repo)
|
plugin.Param("repo", &repo)
|
||||||
plugin.Param("build", &build)
|
plugin.Param("build", &build)
|
||||||
plugin.Param("vargs", &vargs)
|
plugin.Param("vargs", &vargs)
|
||||||
|
plugin.MustParse()
|
||||||
|
|
||||||
// parse the parameters
|
|
||||||
if err := plugin.Parse(); err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set default values
|
|
||||||
if len(vargs.Method) == 0 {
|
if len(vargs.Method) == 0 {
|
||||||
vargs.Method = "POST"
|
vargs.Method = "POST"
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(vargs.ContentType) == 0 {
|
if len(vargs.ContentType) == 0 {
|
||||||
vargs.ContentType = "application/json"
|
vargs.ContentType = "application/json"
|
||||||
}
|
}
|
||||||
|
|
||||||
// data structure
|
|
||||||
data := struct {
|
data := struct {
|
||||||
System drone.System `json:"system"`
|
System drone.System `json:"system"`
|
||||||
Repo drone.Repo `json:"repo"`
|
Repo drone.Repo `json:"repo"`
|
||||||
Build drone.Build `json:"build"`
|
Build drone.Build `json:"build"`
|
||||||
}{sys, repo, build}
|
}{system, repo, build}
|
||||||
|
|
||||||
// creates the payload. by default the payload
|
// creates the payload. by default the payload
|
||||||
// is the build details in json format, but a custom
|
// is the build details in json format, but a custom
|
||||||
// template may also be used.
|
// template may also be used.
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
if len(vargs.Template) == 0 {
|
if len(vargs.Template) == 0 {
|
||||||
if err := json.NewEncoder(&buf).Encode(&data); err != nil {
|
if err := json.NewEncoder(&buf).Encode(&data); err != nil {
|
||||||
fmt.Printf("Error encoding json payload. %s\n", err)
|
fmt.Printf("Error encoding json payload. %s\n", err)
|
||||||
|
@ -62,8 +62,9 @@ func main() {
|
||||||
err := template.Write(&buf, vargs.Template, &drone.Payload{
|
err := template.Write(&buf, vargs.Template, &drone.Payload{
|
||||||
Build: &build,
|
Build: &build,
|
||||||
Repo: &repo,
|
Repo: &repo,
|
||||||
System: &sys,
|
System: &system,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error executing content template. %s\n", err)
|
fmt.Printf("Error executing content template. %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -74,30 +75,31 @@ func main() {
|
||||||
// all auth, headers, method, template (payload),
|
// all auth, headers, method, template (payload),
|
||||||
// and content_type values will be applied to
|
// and content_type values will be applied to
|
||||||
// every webhook request.
|
// every webhook request.
|
||||||
for i, rawurl := range vargs.Urls {
|
|
||||||
|
|
||||||
|
for i, rawurl := range vargs.Urls {
|
||||||
uri, err := url.Parse(rawurl)
|
uri, err := url.Parse(rawurl)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error parsing hook url. %s\n", err)
|
fmt.Printf("Error parsing hook url. %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// vargs.Method defaults to POST, no need to check
|
|
||||||
b := buf.Bytes()
|
b := buf.Bytes()
|
||||||
r := bytes.NewReader(b)
|
r := bytes.NewReader(b)
|
||||||
|
|
||||||
req, err := http.NewRequest(vargs.Method, uri.String(), r)
|
req, err := http.NewRequest(vargs.Method, uri.String(), r)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error creating http request. %s\n", err)
|
fmt.Printf("Error creating http request. %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// vargs.ContentType defaults to application/json, no need to check
|
|
||||||
req.Header.Set("Content-Type", vargs.ContentType)
|
req.Header.Set("Content-Type", vargs.ContentType)
|
||||||
|
|
||||||
for key, value := range vargs.Headers {
|
for key, value := range vargs.Headers {
|
||||||
req.Header.Set(key, value)
|
req.Header.Set(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set basic auth if a user or user and pass is provided
|
|
||||||
if len(vargs.Auth.Username) > 0 {
|
if len(vargs.Auth.Username) > 0 {
|
||||||
if len(vargs.Auth.Password) > 0 {
|
if len(vargs.Auth.Password) > 0 {
|
||||||
req.SetBasicAuth(vargs.Auth.Username, vargs.Auth.Password)
|
req.SetBasicAuth(vargs.Auth.Username, vargs.Auth.Password)
|
||||||
|
@ -107,24 +109,23 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error executing http request. %s\n", err)
|
fmt.Printf("Error executing http request. %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// if debug is on or response status code is bad
|
|
||||||
if vargs.Debug || resp.StatusCode >= http.StatusBadRequest {
|
if vargs.Debug || resp.StatusCode >= http.StatusBadRequest {
|
||||||
|
|
||||||
// read the response body
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// I do not think we need to os.Exit(1) if we are
|
// I do not think we need to os.Exit(1) if we are
|
||||||
// unable to read a http response body.
|
// unable to read a http response body.
|
||||||
fmt.Printf("Error reading http response body. %s\n", err)
|
fmt.Printf("Error reading http response body. %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// debug/info print
|
|
||||||
if vargs.Debug {
|
if vargs.Debug {
|
||||||
fmt.Printf("[debug] Webhook %d\n URL: %s\n METHOD: %s\n HEADERS: %s\n REQUEST BODY: %s\n RESPONSE STATUS: %s\n RESPONSE BODY: %s\n", i+1, req.URL, req.Method, req.Header, string(b), resp.Status, string(body))
|
fmt.Printf("[debug] Webhook %d\n URL: %s\n METHOD: %s\n HEADERS: %s\n REQUEST BODY: %s\n RESPONSE STATUS: %s\n RESPONSE BODY: %s\n", i+1, req.URL, req.Method, req.Header, string(b), resp.Status, string(body))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
// Webhook represents a webhook.
|
type Params struct {
|
||||||
type Webhook struct {
|
|
||||||
Urls []string `json:"urls"`
|
Urls []string `json:"urls"`
|
||||||
Debug bool `json:"debug"`
|
Debug bool `json:"debug"`
|
||||||
Auth BasicAuth `json:"auth"`
|
Auth Auth `json:"auth"`
|
||||||
Headers map[string]string `json:"header"`
|
Headers map[string]string `json:"header"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Template string `json:"template"`
|
Template string `json:"template"`
|
||||||
ContentType string `json:"content_type"`
|
ContentType string `json:"content_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BasicAuth represents a HTTP basic authentication username and password.
|
type Auth struct {
|
||||||
type BasicAuth struct {
|
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
|
@ -1,46 +0,0 @@
|
||||||
# Benchmarks
|
|
||||||
|
|
||||||
Hardware: MacBookPro11,1 - Intel Core i5 - 2,6 GHz - 8 Go RAM
|
|
||||||
|
|
||||||
With:
|
|
||||||
|
|
||||||
- handlebars.js #8cba84df119c317fcebc49fb285518542ca9c2d0
|
|
||||||
- raymond #7bbaaf50ed03c96b56687d7fa6c6e04e02375a98
|
|
||||||
|
|
||||||
|
|
||||||
## handlebars.js (ops/ms)
|
|
||||||
|
|
||||||
arguments 198 ±4 (5)
|
|
||||||
array-each 568 ±23 (5)
|
|
||||||
array-mustache 522 ±18 (4)
|
|
||||||
complex 71 ±7 (3)
|
|
||||||
data 67 ±2 (3)
|
|
||||||
depth-1 47 ±2 (3)
|
|
||||||
depth-2 14 ±1 (2)
|
|
||||||
object-mustache 1099 ±47 (5)
|
|
||||||
object 907 ±58 (4)
|
|
||||||
partial-recursion 46 ±3 (4)
|
|
||||||
partial 68 ±3 (3)
|
|
||||||
paths 1650 ±50 (3)
|
|
||||||
string 2552 ±157 (3)
|
|
||||||
subexpression 141 ±2 (4)
|
|
||||||
variables 2671 ±83 (4)
|
|
||||||
|
|
||||||
|
|
||||||
## raymond
|
|
||||||
|
|
||||||
BenchmarkArguments 200000 6642 ns/op 151 ops/ms
|
|
||||||
BenchmarkArrayEach 100000 19584 ns/op 51 ops/ms
|
|
||||||
BenchmarkArrayMustache 100000 17305 ns/op 58 ops/ms
|
|
||||||
BenchmarkComplex 30000 50270 ns/op 20 ops/ms
|
|
||||||
BenchmarkData 50000 25551 ns/op 39 ops/ms
|
|
||||||
BenchmarkDepth1 100000 20162 ns/op 50 ops/ms
|
|
||||||
BenchmarkDepth2 30000 47782 ns/op 21 ops/ms
|
|
||||||
BenchmarkObjectMustache 200000 7668 ns/op 130 ops/ms
|
|
||||||
BenchmarkObject 200000 8843 ns/op 113 ops/ms
|
|
||||||
BenchmarkPartialRecursion 50000 23139 ns/op 43 ops/ms
|
|
||||||
BenchmarkPartial 50000 31015 ns/op 32 ops/ms
|
|
||||||
BenchmarkPath 200000 8997 ns/op 111 ops/ms
|
|
||||||
BenchmarkString 1000000 1879 ns/op 532 ops/ms
|
|
||||||
BenchmarkSubExpression 300000 4935 ns/op 203 ops/ms
|
|
||||||
BenchmarkVariables 200000 6478 ns/op 154 ops/ms
|
|
|
@ -1,13 +0,0 @@
|
||||||
# Raymond Changelog
|
|
||||||
|
|
||||||
### Raymond 1.1.0 _(June 15, 2015)_
|
|
||||||
|
|
||||||
- Permits templates references with lowercase versions of struct fields.
|
|
||||||
- Adds `ParseFile()` function.
|
|
||||||
- Adds `RegisterPartialFile()`, `RegisterPartialFiles()` and `Clone()` methods on `Template`.
|
|
||||||
- Helpers can now be struct methods.
|
|
||||||
- Ensures safe concurrent access to helpers and partials.
|
|
||||||
|
|
||||||
### Raymond 1.0.0 _(June 09, 2015)_
|
|
||||||
|
|
||||||
- This is the first release. Raymond supports almost all handlebars features. See https://github.com/aymerick/raymond#limitations for a list of differences with the javascript implementation.
|
|
|
@ -1,22 +0,0 @@
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015 Aymerick JEHANNE
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1 +0,0 @@
|
||||||
1.1.0
|
|
|
@ -1,767 +0,0 @@
|
||||||
// Package ast provides structures to represent a handlebars Abstract Syntax Tree, and a Visitor interface to visit that tree.
|
|
||||||
package ast
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// References:
|
|
||||||
// - https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/ast.js
|
|
||||||
// - https://github.com/wycats/handlebars.js/blob/master/docs/compiler-api.md
|
|
||||||
// - https://github.com/golang/go/blob/master/src/text/template/parse/node.go
|
|
||||||
|
|
||||||
// Node is an element in the AST.
|
|
||||||
type Node interface {
|
|
||||||
// node type
|
|
||||||
Type() NodeType
|
|
||||||
|
|
||||||
// location of node in original input string
|
|
||||||
Location() Loc
|
|
||||||
|
|
||||||
// string representation, used for debugging
|
|
||||||
String() string
|
|
||||||
|
|
||||||
// accepts visitor
|
|
||||||
Accept(Visitor) interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Visitor is the interface to visit an AST.
|
|
||||||
type Visitor interface {
|
|
||||||
VisitProgram(*Program) interface{}
|
|
||||||
|
|
||||||
// statements
|
|
||||||
VisitMustache(*MustacheStatement) interface{}
|
|
||||||
VisitBlock(*BlockStatement) interface{}
|
|
||||||
VisitPartial(*PartialStatement) interface{}
|
|
||||||
VisitContent(*ContentStatement) interface{}
|
|
||||||
VisitComment(*CommentStatement) interface{}
|
|
||||||
|
|
||||||
// expressions
|
|
||||||
VisitExpression(*Expression) interface{}
|
|
||||||
VisitSubExpression(*SubExpression) interface{}
|
|
||||||
VisitPath(*PathExpression) interface{}
|
|
||||||
|
|
||||||
// literals
|
|
||||||
VisitString(*StringLiteral) interface{}
|
|
||||||
VisitBoolean(*BooleanLiteral) interface{}
|
|
||||||
VisitNumber(*NumberLiteral) interface{}
|
|
||||||
|
|
||||||
// miscellaneous
|
|
||||||
VisitHash(*Hash) interface{}
|
|
||||||
VisitHashPair(*HashPair) interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeType represents an AST Node type.
|
|
||||||
type NodeType int
|
|
||||||
|
|
||||||
// Type returns itself, and permits struct includers to satisfy that part of Node interface.
|
|
||||||
func (t NodeType) Type() NodeType {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// program
|
|
||||||
NodeProgram NodeType = iota
|
|
||||||
|
|
||||||
// statements
|
|
||||||
NodeMustache
|
|
||||||
NodeBlock
|
|
||||||
NodePartial
|
|
||||||
NodeContent
|
|
||||||
NodeComment
|
|
||||||
|
|
||||||
// expressions
|
|
||||||
NodeExpression
|
|
||||||
NodeSubExpression
|
|
||||||
NodePath
|
|
||||||
|
|
||||||
// literals
|
|
||||||
NodeBoolean
|
|
||||||
NodeNumber
|
|
||||||
NodeString
|
|
||||||
|
|
||||||
// miscellaneous
|
|
||||||
NodeHash
|
|
||||||
NodeHashPair
|
|
||||||
)
|
|
||||||
|
|
||||||
// Loc represents the position of a parsed node in source file.
|
|
||||||
type Loc struct {
|
|
||||||
Pos int // Byte position
|
|
||||||
Line int // Line number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Location returns itself, and permits struct includers to satisfy that part of Node interface.
|
|
||||||
func (l Loc) Location() Loc {
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip describes node whitespace management.
|
|
||||||
type Strip struct {
|
|
||||||
Open bool
|
|
||||||
Close bool
|
|
||||||
|
|
||||||
OpenStandalone bool
|
|
||||||
CloseStandalone bool
|
|
||||||
InlineStandalone bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStrip instanciates a Strip for given open and close mustaches.
|
|
||||||
func NewStrip(openStr, closeStr string) *Strip {
|
|
||||||
return &Strip{
|
|
||||||
Open: (len(openStr) > 2) && openStr[2] == '~',
|
|
||||||
Close: (len(closeStr) > 2) && closeStr[len(closeStr)-3] == '~',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStripForStr instanciates a Strip for given tag.
|
|
||||||
func NewStripForStr(str string) *Strip {
|
|
||||||
return &Strip{
|
|
||||||
Open: (len(str) > 2) && str[2] == '~',
|
|
||||||
Close: (len(str) > 2) && str[len(str)-3] == '~',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (s *Strip) String() string {
|
|
||||||
return fmt.Sprintf("Open: %t, Close: %t, OpenStandalone: %t, CloseStandalone: %t, InlineStandalone: %t", s.Open, s.Close, s.OpenStandalone, s.CloseStandalone, s.InlineStandalone)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Program
|
|
||||||
//
|
|
||||||
|
|
||||||
// Program represents a program node.
|
|
||||||
type Program struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Body []Node // [ Statement ... ]
|
|
||||||
BlockParams []string
|
|
||||||
Chained bool
|
|
||||||
|
|
||||||
// whitespace management
|
|
||||||
Strip *Strip
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewProgram instanciates a new program node.
|
|
||||||
func NewProgram(pos int, line int) *Program {
|
|
||||||
return &Program{
|
|
||||||
NodeType: NodeProgram,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *Program) String() string {
|
|
||||||
return fmt.Sprintf("Program{Pos: %d}", node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *Program) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitProgram(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddStatement adds given statement to program.
|
|
||||||
func (node *Program) AddStatement(statement Node) {
|
|
||||||
node.Body = append(node.Body, statement)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Mustache Statement
|
|
||||||
//
|
|
||||||
|
|
||||||
// MustacheStatement represents a mustache node.
|
|
||||||
type MustacheStatement struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Unescaped bool
|
|
||||||
Expression *Expression
|
|
||||||
|
|
||||||
// whitespace management
|
|
||||||
Strip *Strip
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMustacheStatement instanciates a new mustache node.
|
|
||||||
func NewMustacheStatement(pos int, line int, unescaped bool) *MustacheStatement {
|
|
||||||
return &MustacheStatement{
|
|
||||||
NodeType: NodeMustache,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
Unescaped: unescaped,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *MustacheStatement) String() string {
|
|
||||||
return fmt.Sprintf("Mustache{Pos: %d}", node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *MustacheStatement) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitMustache(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Block Statement
|
|
||||||
//
|
|
||||||
|
|
||||||
// BlockStatement represents a block node.
|
|
||||||
type BlockStatement struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Expression *Expression
|
|
||||||
|
|
||||||
Program *Program
|
|
||||||
Inverse *Program
|
|
||||||
|
|
||||||
// whitespace management
|
|
||||||
OpenStrip *Strip
|
|
||||||
InverseStrip *Strip
|
|
||||||
CloseStrip *Strip
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBlockStatement instanciates a new block node.
|
|
||||||
func NewBlockStatement(pos int, line int) *BlockStatement {
|
|
||||||
return &BlockStatement{
|
|
||||||
NodeType: NodeBlock,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *BlockStatement) String() string {
|
|
||||||
return fmt.Sprintf("Block{Pos: %d}", node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *BlockStatement) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitBlock(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Partial Statement
|
|
||||||
//
|
|
||||||
|
|
||||||
// PartialStatement represents a partial node.
|
|
||||||
type PartialStatement struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Name Node // PathExpression | SubExpression
|
|
||||||
Params []Node // [ Expression ... ]
|
|
||||||
Hash *Hash
|
|
||||||
|
|
||||||
// whitespace management
|
|
||||||
Strip *Strip
|
|
||||||
Indent string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPartialStatement instanciates a new partial node.
|
|
||||||
func NewPartialStatement(pos int, line int) *PartialStatement {
|
|
||||||
return &PartialStatement{
|
|
||||||
NodeType: NodePartial,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *PartialStatement) String() string {
|
|
||||||
return fmt.Sprintf("Partial{Name:%s, Pos:%d}", node.Name, node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *PartialStatement) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitPartial(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Content Statement
|
|
||||||
//
|
|
||||||
|
|
||||||
// ContentStatement represents a content node.
|
|
||||||
type ContentStatement struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Value string
|
|
||||||
Original string
|
|
||||||
|
|
||||||
// whitespace management
|
|
||||||
RightStripped bool
|
|
||||||
LeftStripped bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewContentStatement instanciates a new content node.
|
|
||||||
func NewContentStatement(pos int, line int, val string) *ContentStatement {
|
|
||||||
return &ContentStatement{
|
|
||||||
NodeType: NodeContent,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
|
|
||||||
Value: val,
|
|
||||||
Original: val,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *ContentStatement) String() string {
|
|
||||||
return fmt.Sprintf("Content{Value:'%s', Pos:%d}", node.Value, node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *ContentStatement) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitContent(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Comment Statement
|
|
||||||
//
|
|
||||||
|
|
||||||
// CommentStatement represents a comment node.
|
|
||||||
type CommentStatement struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Value string
|
|
||||||
|
|
||||||
// whitespace management
|
|
||||||
Strip *Strip
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCommentStatement instanciates a new comment node.
|
|
||||||
func NewCommentStatement(pos int, line int, val string) *CommentStatement {
|
|
||||||
return &CommentStatement{
|
|
||||||
NodeType: NodeComment,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
|
|
||||||
Value: val,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *CommentStatement) String() string {
|
|
||||||
return fmt.Sprintf("Comment{Value:'%s', Pos:%d}", node.Value, node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *CommentStatement) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitComment(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Expression
|
|
||||||
//
|
|
||||||
|
|
||||||
// Expression represents an expression node.
|
|
||||||
type Expression struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Path Node // PathExpression | StringLiteral | BooleanLiteral | NumberLiteral
|
|
||||||
Params []Node // [ Expression ... ]
|
|
||||||
Hash *Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExpression instanciates a new expression node.
|
|
||||||
func NewExpression(pos int, line int) *Expression {
|
|
||||||
return &Expression{
|
|
||||||
NodeType: NodeExpression,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *Expression) String() string {
|
|
||||||
return fmt.Sprintf("Expr{Path:%s, Pos:%d}", node.Path, node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *Expression) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitExpression(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HelperName returns helper name, or an empty string if this expression can't be a helper.
|
|
||||||
func (node *Expression) HelperName() string {
|
|
||||||
path, ok := node.Path.(*PathExpression)
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if path.Data || (len(path.Parts) != 1) || (path.Depth > 0) || path.Scoped {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.Parts[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// FieldPath returns path expression representing a field path, or nil if this is not a field path.
|
|
||||||
func (node *Expression) FieldPath() *PathExpression {
|
|
||||||
path, ok := node.Path.(*PathExpression)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
// LiteralStr returns the string representation of literal value, with a boolean set to false if this is not a literal.
|
|
||||||
func (node *Expression) LiteralStr() (string, bool) {
|
|
||||||
return LiteralStr(node.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Canonical returns the canonical form of expression node as a string.
|
|
||||||
func (node *Expression) Canonical() string {
|
|
||||||
if str, ok := HelperNameStr(node.Path); ok {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// HelperNameStr returns the string representation of a helper name, with a boolean set to false if this is not a valid helper name.
|
|
||||||
//
|
|
||||||
// helperName : path | dataName | STRING | NUMBER | BOOLEAN | UNDEFINED | NULL
|
|
||||||
func HelperNameStr(node Node) (string, bool) {
|
|
||||||
// PathExpression
|
|
||||||
if str, ok := PathExpressionStr(node); ok {
|
|
||||||
return str, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Literal
|
|
||||||
if str, ok := LiteralStr(node); ok {
|
|
||||||
return str, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathExpressionStr returns the string representation of path expression value, with a boolean set to false if this is not a path expression.
|
|
||||||
func PathExpressionStr(node Node) (string, bool) {
|
|
||||||
if path, ok := node.(*PathExpression); ok {
|
|
||||||
result := path.Original
|
|
||||||
|
|
||||||
// "[foo bar]"" => "foo bar"
|
|
||||||
if (len(result) >= 2) && (result[0] == '[') && (result[len(result)-1] == ']') {
|
|
||||||
result = result[1 : len(result)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// LiteralStr returns the string representation of literal value, with a boolean set to false if this is not a literal.
|
|
||||||
func LiteralStr(node Node) (string, bool) {
|
|
||||||
if lit, ok := node.(*StringLiteral); ok {
|
|
||||||
return lit.Value, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if lit, ok := node.(*BooleanLiteral); ok {
|
|
||||||
return lit.Canonical(), true
|
|
||||||
}
|
|
||||||
|
|
||||||
if lit, ok := node.(*NumberLiteral); ok {
|
|
||||||
return lit.Canonical(), true
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// SubExpression
|
|
||||||
//
|
|
||||||
|
|
||||||
// SubExpression represents a subexpression node.
|
|
||||||
type SubExpression struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Expression *Expression
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSubExpression instanciates a new subexpression node.
|
|
||||||
func NewSubExpression(pos int, line int) *SubExpression {
|
|
||||||
return &SubExpression{
|
|
||||||
NodeType: NodeSubExpression,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *SubExpression) String() string {
|
|
||||||
return fmt.Sprintf("Sexp{Path:%s, Pos:%d}", node.Expression.Path, node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *SubExpression) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitSubExpression(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Path Expression
|
|
||||||
//
|
|
||||||
|
|
||||||
// PathExpression represents a path expression node.
|
|
||||||
type PathExpression struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Original string
|
|
||||||
Depth int
|
|
||||||
Parts []string
|
|
||||||
Data bool
|
|
||||||
Scoped bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPathExpression instanciates a new path expression node.
|
|
||||||
func NewPathExpression(pos int, line int, data bool) *PathExpression {
|
|
||||||
result := &PathExpression{
|
|
||||||
NodeType: NodePath,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
|
|
||||||
if data {
|
|
||||||
result.Original = "@"
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *PathExpression) String() string {
|
|
||||||
return fmt.Sprintf("Path{Original:'%s', Pos:%d}", node.Original, node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *PathExpression) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitPath(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Part adds path part.
|
|
||||||
func (node *PathExpression) Part(part string) {
|
|
||||||
node.Original += part
|
|
||||||
|
|
||||||
switch part {
|
|
||||||
case "..":
|
|
||||||
node.Depth += 1
|
|
||||||
node.Scoped = true
|
|
||||||
case ".", "this":
|
|
||||||
node.Scoped = true
|
|
||||||
default:
|
|
||||||
node.Parts = append(node.Parts, part)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sep adds path separator.
|
|
||||||
func (node *PathExpression) Sep(separator string) {
|
|
||||||
node.Original += separator
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDataRoot returns true if path expression is @root.
|
|
||||||
func (node *PathExpression) IsDataRoot() bool {
|
|
||||||
return node.Data && (node.Parts[0] == "root")
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// String Literal
|
|
||||||
//
|
|
||||||
|
|
||||||
// StringLiteral represents a string node.
|
|
||||||
type StringLiteral struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStringLiteral instanciates a new string node.
|
|
||||||
func NewStringLiteral(pos int, line int, val string) *StringLiteral {
|
|
||||||
return &StringLiteral{
|
|
||||||
NodeType: NodeString,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
|
|
||||||
Value: val,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *StringLiteral) String() string {
|
|
||||||
return fmt.Sprintf("String{Value:'%s', Pos:%d}", node.Value, node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *StringLiteral) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitString(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Boolean Literal
|
|
||||||
//
|
|
||||||
|
|
||||||
// BooleanLiteral represents a boolean node.
|
|
||||||
type BooleanLiteral struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Value bool
|
|
||||||
Original string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBooleanLiteral instanciates a new boolean node.
|
|
||||||
func NewBooleanLiteral(pos int, line int, val bool, original string) *BooleanLiteral {
|
|
||||||
return &BooleanLiteral{
|
|
||||||
NodeType: NodeBoolean,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
|
|
||||||
Value: val,
|
|
||||||
Original: original,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *BooleanLiteral) String() string {
|
|
||||||
return fmt.Sprintf("Boolean{Value:%s, Pos:%d}", node.Canonical(), node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *BooleanLiteral) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitBoolean(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Canonical returns the canonical form of boolean node as a string (ie. "true" | "false").
|
|
||||||
func (node *BooleanLiteral) Canonical() string {
|
|
||||||
if node.Value {
|
|
||||||
return "true"
|
|
||||||
} else {
|
|
||||||
return "false"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Number Literal
|
|
||||||
//
|
|
||||||
|
|
||||||
// NumberLiteral represents a number node.
|
|
||||||
type NumberLiteral struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Value float64
|
|
||||||
IsInt bool
|
|
||||||
Original string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewNumberLiteral instanciates a new number node.
|
|
||||||
func NewNumberLiteral(pos int, line int, val float64, isInt bool, original string) *NumberLiteral {
|
|
||||||
return &NumberLiteral{
|
|
||||||
NodeType: NodeNumber,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
|
|
||||||
Value: val,
|
|
||||||
IsInt: isInt,
|
|
||||||
Original: original,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *NumberLiteral) String() string {
|
|
||||||
return fmt.Sprintf("Number{Value:%s, Pos:%d}", node.Canonical(), node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *NumberLiteral) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitNumber(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Canonical returns the canonical form of number node as a string (eg: "12", "-1.51").
|
|
||||||
func (node *NumberLiteral) Canonical() string {
|
|
||||||
prec := -1
|
|
||||||
if node.IsInt {
|
|
||||||
prec = 0
|
|
||||||
}
|
|
||||||
return strconv.FormatFloat(node.Value, 'f', prec, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Number returns an integer or a float.
|
|
||||||
func (node *NumberLiteral) Number() interface{} {
|
|
||||||
if node.IsInt {
|
|
||||||
return int(node.Value)
|
|
||||||
} else {
|
|
||||||
return node.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Hash
|
|
||||||
//
|
|
||||||
|
|
||||||
// Hash represents a hash node.
|
|
||||||
type Hash struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Pairs []*HashPair
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewNumberLiteral instanciates a new hash node.
|
|
||||||
func NewHash(pos int, line int) *Hash {
|
|
||||||
return &Hash{
|
|
||||||
NodeType: NodeHash,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *Hash) String() string {
|
|
||||||
result := fmt.Sprintf("Hash{[", node.Loc.Pos)
|
|
||||||
|
|
||||||
for i, p := range node.Pairs {
|
|
||||||
if i > 0 {
|
|
||||||
result += ", "
|
|
||||||
}
|
|
||||||
result += p.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result + fmt.Sprintf("], Pos:%d}", node.Loc.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *Hash) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitHash(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// HashPair
|
|
||||||
//
|
|
||||||
|
|
||||||
// HashPair represents a hash pair node.
|
|
||||||
type HashPair struct {
|
|
||||||
NodeType
|
|
||||||
Loc
|
|
||||||
|
|
||||||
Key string
|
|
||||||
Val Node // Expression
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHashPair instanciates a new hash pair node.
|
|
||||||
func NewHashPair(pos int, line int) *HashPair {
|
|
||||||
return &HashPair{
|
|
||||||
NodeType: NodeHashPair,
|
|
||||||
Loc: Loc{pos, line},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of receiver that can be used for debugging.
|
|
||||||
func (node *HashPair) String() string {
|
|
||||||
return node.Key + "=" + node.Val.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept is the receiver entry point for visitors.
|
|
||||||
func (node *HashPair) Accept(visitor Visitor) interface{} {
|
|
||||||
return visitor.VisitHashPair(node)
|
|
||||||
}
|
|
|
@ -1,279 +0,0 @@
|
||||||
package ast
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// printVisitor implements the Visitor interface to print a AST.
|
|
||||||
type printVisitor struct {
|
|
||||||
buf string
|
|
||||||
depth int
|
|
||||||
|
|
||||||
original bool
|
|
||||||
inBlock bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPrintVisitor() *printVisitor {
|
|
||||||
return &printVisitor{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print returns a string representation of given AST, that can be used for debugging purpose.
|
|
||||||
func Print(node Node) string {
|
|
||||||
visitor := newPrintVisitor()
|
|
||||||
node.Accept(visitor)
|
|
||||||
return visitor.output()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *printVisitor) output() string {
|
|
||||||
return v.buf
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *printVisitor) indent() {
|
|
||||||
for i := 0; i < v.depth; {
|
|
||||||
v.buf += " "
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *printVisitor) str(val string) {
|
|
||||||
v.buf += val
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *printVisitor) nl() {
|
|
||||||
v.str("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *printVisitor) line(val string) {
|
|
||||||
v.indent()
|
|
||||||
v.str(val)
|
|
||||||
v.nl()
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Visitor interface
|
|
||||||
//
|
|
||||||
|
|
||||||
// Statements
|
|
||||||
|
|
||||||
// VisitProgram implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitProgram(node *Program) interface{} {
|
|
||||||
if len(node.BlockParams) > 0 {
|
|
||||||
v.line("BLOCK PARAMS: [ " + strings.Join(node.BlockParams, " ") + " ]")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, n := range node.Body {
|
|
||||||
n.Accept(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitMustache implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitMustache(node *MustacheStatement) interface{} {
|
|
||||||
v.indent()
|
|
||||||
v.str("{{ ")
|
|
||||||
|
|
||||||
node.Expression.Accept(v)
|
|
||||||
|
|
||||||
v.str(" }}")
|
|
||||||
v.nl()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitBlock implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitBlock(node *BlockStatement) interface{} {
|
|
||||||
v.inBlock = true
|
|
||||||
|
|
||||||
v.line("BLOCK:")
|
|
||||||
v.depth++
|
|
||||||
|
|
||||||
node.Expression.Accept(v)
|
|
||||||
|
|
||||||
if node.Program != nil {
|
|
||||||
v.line("PROGRAM:")
|
|
||||||
v.depth++
|
|
||||||
node.Program.Accept(v)
|
|
||||||
v.depth--
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.Inverse != nil {
|
|
||||||
// if node.Program != nil {
|
|
||||||
// v.depth++
|
|
||||||
// }
|
|
||||||
|
|
||||||
v.line("{{^}}")
|
|
||||||
v.depth++
|
|
||||||
node.Inverse.Accept(v)
|
|
||||||
v.depth--
|
|
||||||
|
|
||||||
// if node.Program != nil {
|
|
||||||
// v.depth--
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
v.inBlock = false
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitPartial implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitPartial(node *PartialStatement) interface{} {
|
|
||||||
v.indent()
|
|
||||||
v.str("{{> PARTIAL:")
|
|
||||||
|
|
||||||
v.original = true
|
|
||||||
node.Name.Accept(v)
|
|
||||||
v.original = false
|
|
||||||
|
|
||||||
if len(node.Params) > 0 {
|
|
||||||
v.str(" ")
|
|
||||||
node.Params[0].Accept(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hash
|
|
||||||
if node.Hash != nil {
|
|
||||||
v.str(" ")
|
|
||||||
node.Hash.Accept(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
v.str(" }}")
|
|
||||||
v.nl()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitContent implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitContent(node *ContentStatement) interface{} {
|
|
||||||
v.line("CONTENT[ '" + node.Value + "' ]")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitComment implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitComment(node *CommentStatement) interface{} {
|
|
||||||
v.line("{{! '" + node.Value + "' }}")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expressions
|
|
||||||
|
|
||||||
// VisitExpression implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitExpression(node *Expression) interface{} {
|
|
||||||
if v.inBlock {
|
|
||||||
v.indent()
|
|
||||||
}
|
|
||||||
|
|
||||||
// path
|
|
||||||
node.Path.Accept(v)
|
|
||||||
|
|
||||||
// params
|
|
||||||
v.str(" [")
|
|
||||||
for i, n := range node.Params {
|
|
||||||
if i > 0 {
|
|
||||||
v.str(", ")
|
|
||||||
}
|
|
||||||
n.Accept(v)
|
|
||||||
}
|
|
||||||
v.str("]")
|
|
||||||
|
|
||||||
// hash
|
|
||||||
if node.Hash != nil {
|
|
||||||
v.str(" ")
|
|
||||||
node.Hash.Accept(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.inBlock {
|
|
||||||
v.nl()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitSubExpression implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitSubExpression(node *SubExpression) interface{} {
|
|
||||||
node.Expression.Accept(v)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitPath implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitPath(node *PathExpression) interface{} {
|
|
||||||
if v.original {
|
|
||||||
v.str(node.Original)
|
|
||||||
} else {
|
|
||||||
path := strings.Join(node.Parts, "/")
|
|
||||||
|
|
||||||
result := ""
|
|
||||||
if node.Data {
|
|
||||||
result += "@"
|
|
||||||
}
|
|
||||||
|
|
||||||
v.str(result + "PATH:" + path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Literals
|
|
||||||
|
|
||||||
// VisitString implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitString(node *StringLiteral) interface{} {
|
|
||||||
if v.original {
|
|
||||||
v.str(node.Value)
|
|
||||||
} else {
|
|
||||||
v.str("\"" + node.Value + "\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitBoolean implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitBoolean(node *BooleanLiteral) interface{} {
|
|
||||||
if v.original {
|
|
||||||
v.str(node.Original)
|
|
||||||
} else {
|
|
||||||
v.str(fmt.Sprintf("BOOLEAN{%s}", node.Canonical()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitNumber implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitNumber(node *NumberLiteral) interface{} {
|
|
||||||
if v.original {
|
|
||||||
v.str(node.Original)
|
|
||||||
} else {
|
|
||||||
v.str(fmt.Sprintf("NUMBER{%s}", node.Canonical()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Miscellaneous
|
|
||||||
|
|
||||||
// VisitHash implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitHash(node *Hash) interface{} {
|
|
||||||
v.str("HASH{")
|
|
||||||
|
|
||||||
for i, p := range node.Pairs {
|
|
||||||
if i > 0 {
|
|
||||||
v.str(", ")
|
|
||||||
}
|
|
||||||
p.Accept(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
v.str("}")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitHashPair implements corresponding Visitor interface method
|
|
||||||
func (v *printVisitor) VisitHashPair(node *HashPair) interface{} {
|
|
||||||
v.str(node.Key + "=")
|
|
||||||
node.Val.Accept(v)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,167 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Test struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
data interface{}
|
|
||||||
privData map[string]interface{}
|
|
||||||
helpers map[string]interface{}
|
|
||||||
partials map[string]string
|
|
||||||
output interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func launchTests(t *testing.T, tests []Test) {
|
|
||||||
// NOTE: TestMustache() makes Parallel testing fail
|
|
||||||
// t.Parallel()
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
var err error
|
|
||||||
var tpl *Template
|
|
||||||
|
|
||||||
// parse template
|
|
||||||
tpl, err = Parse(test.input)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err)
|
|
||||||
} else {
|
|
||||||
if len(test.helpers) > 0 {
|
|
||||||
// register helpers
|
|
||||||
tpl.RegisterHelpers(test.helpers)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(test.partials) > 0 {
|
|
||||||
// register partials
|
|
||||||
tpl.RegisterPartials(test.partials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup private data frame
|
|
||||||
var privData *DataFrame
|
|
||||||
if test.privData != nil {
|
|
||||||
privData = NewDataFrame()
|
|
||||||
for k, v := range test.privData {
|
|
||||||
privData.Set(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// render template
|
|
||||||
output, err := tpl.ExecWith(test.data, privData)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\nerror:\n\t%s\nAST:\n\t%s", test.name, test.input, Str(test.data), err, tpl.PrintAST())
|
|
||||||
} else {
|
|
||||||
// check output
|
|
||||||
var expectedArr []string
|
|
||||||
expectedArr, ok := test.output.([]string)
|
|
||||||
if ok {
|
|
||||||
match := false
|
|
||||||
for _, expectedStr := range expectedArr {
|
|
||||||
if expectedStr == output {
|
|
||||||
match = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !match {
|
|
||||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, Str(test.data), Str(test.partials), expectedArr, output, tpl.PrintAST())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
expectedStr, ok := test.output.(string)
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Errorf("Erroneous test output description: %q", test.output))
|
|
||||||
}
|
|
||||||
|
|
||||||
if expectedStr != output {
|
|
||||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, Str(test.data), Str(test.partials), expectedStr, output, tpl.PrintAST())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func launchErrorTests(t *testing.T, tests []Test) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
var err error
|
|
||||||
var tpl *Template
|
|
||||||
|
|
||||||
// parse template
|
|
||||||
tpl, err = Parse(test.input)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err)
|
|
||||||
} else {
|
|
||||||
if len(test.helpers) > 0 {
|
|
||||||
// register helpers
|
|
||||||
tpl.RegisterHelpers(test.helpers)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(test.partials) > 0 {
|
|
||||||
// register partials
|
|
||||||
tpl.RegisterPartials(test.partials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup private data frame
|
|
||||||
var privData *DataFrame
|
|
||||||
if test.privData != nil {
|
|
||||||
privData := NewDataFrame()
|
|
||||||
for k, v := range test.privData {
|
|
||||||
privData.Set(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// render template
|
|
||||||
output, err := tpl.ExecWith(test.data, privData)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Test '%s' failed - Error expected\ninput:\n\t'%s'\ngot\n\t%q\nAST:\n%q", test.name, test.input, output, tpl.PrintAST())
|
|
||||||
} else {
|
|
||||||
var errMatch error
|
|
||||||
match := false
|
|
||||||
|
|
||||||
// check output
|
|
||||||
var expectedArr []string
|
|
||||||
expectedArr, ok := test.output.([]string)
|
|
||||||
if ok {
|
|
||||||
if len(expectedArr) > 0 {
|
|
||||||
for _, expectedStr := range expectedArr {
|
|
||||||
match, errMatch = regexp.MatchString(regexp.QuoteMeta(expectedStr), fmt.Sprint(err))
|
|
||||||
if errMatch != nil {
|
|
||||||
panic("Failed to match regexp")
|
|
||||||
}
|
|
||||||
|
|
||||||
if match {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// nothing to test
|
|
||||||
match = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
expectedStr, ok := test.output.(string)
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Errorf("Erroneous test output description: %q", test.output))
|
|
||||||
}
|
|
||||||
|
|
||||||
if expectedStr != "" {
|
|
||||||
match, errMatch = regexp.MatchString(regexp.QuoteMeta(expectedStr), fmt.Sprint(err))
|
|
||||||
if errMatch != nil {
|
|
||||||
panic("Failed to match regexp")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// nothing to test
|
|
||||||
match = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !match {
|
|
||||||
t.Errorf("Test '%s' failed - Incorrect error returned\ninput:\n\t'%s'\ndata:\n\t%s\nexpected\n\t%q\ngot\n\t%q", test.name, test.input, Str(test.data), test.output, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,316 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
//
|
|
||||||
// Those tests come from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/bench/
|
|
||||||
//
|
|
||||||
// Note that handlebars.js does NOT benchmark template compilation, it only benchmarks evaluation.
|
|
||||||
//
|
|
||||||
|
|
||||||
func BenchmarkArguments(b *testing.B) {
|
|
||||||
source := `{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}`
|
|
||||||
|
|
||||||
ctx := map[string]bool{
|
|
||||||
"bar": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
tpl.RegisterHelper("foo", func(a, b, c, d interface{}) string { return "" })
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkArrayEach(b *testing.B) {
|
|
||||||
source := `{{#each names}}{{name}}{{/each}}`
|
|
||||||
|
|
||||||
ctx := map[string][]map[string]string{
|
|
||||||
"names": {
|
|
||||||
{"name": "Moe"},
|
|
||||||
{"name": "Larry"},
|
|
||||||
{"name": "Curly"},
|
|
||||||
{"name": "Shemp"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkArrayMustache(b *testing.B) {
|
|
||||||
source := `{{#names}}{{name}}{{/names}}`
|
|
||||||
|
|
||||||
ctx := map[string][]map[string]string{
|
|
||||||
"names": {
|
|
||||||
{"name": "Moe"},
|
|
||||||
{"name": "Larry"},
|
|
||||||
{"name": "Curly"},
|
|
||||||
{"name": "Shemp"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkComplex(b *testing.B) {
|
|
||||||
source := `<h1>{{header}}</h1>
|
|
||||||
{{#if items}}
|
|
||||||
<ul>
|
|
||||||
{{#each items}}
|
|
||||||
{{#if current}}
|
|
||||||
<li><strong>{{name}}</strong></li>
|
|
||||||
{{^}}
|
|
||||||
<li><a href="{{url}}">{{name}}</a></li>
|
|
||||||
{{/if}}
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
{{^}}
|
|
||||||
<p>The list is empty.</p>
|
|
||||||
{{/if}}
|
|
||||||
`
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"header": func() string { return "Colors" },
|
|
||||||
"hasItems": true,
|
|
||||||
"items": []map[string]interface{}{
|
|
||||||
{"name": "red", "current": true, "url": "#Red"},
|
|
||||||
{"name": "green", "current": false, "url": "#Green"},
|
|
||||||
{"name": "blue", "current": false, "url": "#Blue"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkData(b *testing.B) {
|
|
||||||
source := `{{#each names}}{{@index}}{{name}}{{/each}}`
|
|
||||||
|
|
||||||
ctx := map[string][]map[string]string{
|
|
||||||
"names": {
|
|
||||||
{"name": "Moe"},
|
|
||||||
{"name": "Larry"},
|
|
||||||
{"name": "Curly"},
|
|
||||||
{"name": "Shemp"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkDepth1(b *testing.B) {
|
|
||||||
source := `{{#each names}}{{../foo}}{{/each}}`
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"names": []map[string]string{
|
|
||||||
{"name": "Moe"},
|
|
||||||
{"name": "Larry"},
|
|
||||||
{"name": "Curly"},
|
|
||||||
{"name": "Shemp"},
|
|
||||||
},
|
|
||||||
"foo": "bar",
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkDepth2(b *testing.B) {
|
|
||||||
source := `{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}`
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"names": []map[string]interface{}{
|
|
||||||
{"bat": "foo", "name": []string{"Moe"}},
|
|
||||||
{"bat": "foo", "name": []string{"Larry"}},
|
|
||||||
{"bat": "foo", "name": []string{"Curly"}},
|
|
||||||
{"bat": "foo", "name": []string{"Shemp"}},
|
|
||||||
},
|
|
||||||
"foo": "bar",
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkObjectMustache(b *testing.B) {
|
|
||||||
source := `{{#person}}{{name}}{{age}}{{/person}}`
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"person": map[string]interface{}{
|
|
||||||
"name": "Larry",
|
|
||||||
"age": 45,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkObject(b *testing.B) {
|
|
||||||
source := `{{#with person}}{{name}}{{age}}{{/with}}`
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"person": map[string]interface{}{
|
|
||||||
"name": "Larry",
|
|
||||||
"age": 45,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkPartialRecursion(b *testing.B) {
|
|
||||||
source := `{{name}}{{#each kids}}{{>recursion}}{{/each}}`
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"name": 1,
|
|
||||||
"kids": []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"name": "1.1",
|
|
||||||
"kids": []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"name": "1.1.1",
|
|
||||||
"kids": []map[string]interface{}{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
partial := MustParse(`{{name}}{{#each kids}}{{>recursion}}{{/each}}`)
|
|
||||||
tpl.RegisterPartialTemplate("recursion", partial)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkPartial(b *testing.B) {
|
|
||||||
source := `{{#each peeps}}{{>variables}}{{/each}}`
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"peeps": []map[string]interface{}{
|
|
||||||
{"name": "Moe", "count": 15},
|
|
||||||
{"name": "Moe", "count": 5},
|
|
||||||
{"name": "Curly", "count": 1},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
partial := MustParse(`Hello {{name}}! You have {{count}} new messages.`)
|
|
||||||
tpl.RegisterPartialTemplate("variables", partial)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkPath(b *testing.B) {
|
|
||||||
source := `{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}`
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"person": map[string]interface{}{
|
|
||||||
"name": map[string]interface{}{
|
|
||||||
"bar": map[string]string{
|
|
||||||
"baz": "Larry",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"age": 45,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkString(b *testing.B) {
|
|
||||||
source := `Hello world`
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkSubExpression(b *testing.B) {
|
|
||||||
source := `{{echo (header)}}`
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
tpl.RegisterHelpers(map[string]interface{}{
|
|
||||||
"echo": func(v string) string { return "foo " + v },
|
|
||||||
"header": func() string { return "Colors" },
|
|
||||||
})
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkVariables(b *testing.B) {
|
|
||||||
source := `Hello {{name}}! You have {{count}} new messages.`
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"name": "Mick",
|
|
||||||
"count": 30,
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
tpl.MustExec(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import "reflect"
|
|
||||||
|
|
||||||
// DataFrame represents a private data frame.
|
|
||||||
//
|
|
||||||
// Cf. private variables documentation at: http://handlebarsjs.com/block_helpers.html
|
|
||||||
type DataFrame struct {
|
|
||||||
parent *DataFrame
|
|
||||||
data map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDataFrame instanciates a new private data frame.
|
|
||||||
func NewDataFrame() *DataFrame {
|
|
||||||
return &DataFrame{
|
|
||||||
data: make(map[string]interface{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy instanciates a new private data frame with receiver as parent.
|
|
||||||
func (p *DataFrame) Copy() *DataFrame {
|
|
||||||
result := NewDataFrame()
|
|
||||||
|
|
||||||
for k, v := range p.data {
|
|
||||||
result.data[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
result.parent = p
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// newIterDataFrame instanciates a new private data frame with receiver as parent and with iteration data set (@index, @key, @first, @last)
|
|
||||||
func (p *DataFrame) newIterDataFrame(length int, i int, key interface{}) *DataFrame {
|
|
||||||
result := p.Copy()
|
|
||||||
|
|
||||||
result.Set("index", i)
|
|
||||||
result.Set("key", key)
|
|
||||||
result.Set("first", i == 0)
|
|
||||||
result.Set("last", i == length-1)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set sets a data value.
|
|
||||||
func (p *DataFrame) Set(key string, val interface{}) {
|
|
||||||
p.data[key] = val
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get gets a data value.
|
|
||||||
func (p *DataFrame) Get(key string) interface{} {
|
|
||||||
return p.find([]string{key})
|
|
||||||
}
|
|
||||||
|
|
||||||
// find gets a deep data value
|
|
||||||
//
|
|
||||||
// @todo This is NOT consistent with the way we resolve data in template (cf. `evalDataPathExpression()`) ! FIX THAT !
|
|
||||||
func (p *DataFrame) find(parts []string) interface{} {
|
|
||||||
data := p.data
|
|
||||||
|
|
||||||
for i, part := range parts {
|
|
||||||
val := data[part]
|
|
||||||
if val == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == len(parts)-1 {
|
|
||||||
// found
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
valValue := reflect.ValueOf(val)
|
|
||||||
if valValue.Kind() != reflect.Map {
|
|
||||||
// not found
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// continue
|
|
||||||
data = mapStringInterface(valValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// not found
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// mapStringInterface converts any `map` to `map[string]interface{}`
|
|
||||||
func mapStringInterface(value reflect.Value) map[string]interface{} {
|
|
||||||
result := make(map[string]interface{})
|
|
||||||
|
|
||||||
for _, key := range value.MapKeys() {
|
|
||||||
result[strValue(key)] = value.MapIndex(key).Interface()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
|
||||||
// That whole file is borrowed from https://github.com/golang/go/tree/master/src/html/escape.go
|
|
||||||
//
|
|
||||||
// With changes:
|
|
||||||
// ' => '
|
|
||||||
// " => "
|
|
||||||
//
|
|
||||||
// To stay in sync with JS implementation, and make mustache tests pass.
|
|
||||||
//
|
|
||||||
|
|
||||||
type writer interface {
|
|
||||||
WriteString(string) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const escapedChars = `&'<>"`
|
|
||||||
|
|
||||||
func escape(w writer, s string) error {
|
|
||||||
i := strings.IndexAny(s, escapedChars)
|
|
||||||
for i != -1 {
|
|
||||||
if _, err := w.WriteString(s[:i]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var esc string
|
|
||||||
switch s[i] {
|
|
||||||
case '&':
|
|
||||||
esc = "&"
|
|
||||||
case '\'':
|
|
||||||
esc = "'"
|
|
||||||
case '<':
|
|
||||||
esc = "<"
|
|
||||||
case '>':
|
|
||||||
esc = ">"
|
|
||||||
case '"':
|
|
||||||
esc = """
|
|
||||||
default:
|
|
||||||
panic("unrecognized escape character")
|
|
||||||
}
|
|
||||||
s = s[i+1:]
|
|
||||||
if _, err := w.WriteString(esc); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
i = strings.IndexAny(s, escapedChars)
|
|
||||||
}
|
|
||||||
_, err := w.WriteString(s)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape escapes special HTML characters.
|
|
||||||
//
|
|
||||||
// It can be used by helpers that return a SafeString and that need to escape some content by themselves.
|
|
||||||
func Escape(s string) string {
|
|
||||||
if strings.IndexAny(s, escapedChars) == -1 {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
escape(&buf, s)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func ExampleEscape() {
|
|
||||||
tpl := MustParse("{{link url text}}")
|
|
||||||
|
|
||||||
tpl.RegisterHelper("link", func(url string, text string) SafeString {
|
|
||||||
return SafeString("<a href='" + Escape(url) + "'>" + Escape(text) + "</a>")
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx := map[string]string{
|
|
||||||
"url": "http://www.aymerick.com/",
|
|
||||||
"text": "This is a <em>cool</em> website",
|
|
||||||
}
|
|
||||||
|
|
||||||
result := tpl.MustExec(ctx)
|
|
||||||
fmt.Print(result)
|
|
||||||
// Output: <a href='http://www.aymerick.com/'>This is a <em>cool</em> website</a>
|
|
||||||
}
|
|
|
@ -1,984 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/aymerick/raymond/ast"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// @note borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
|
||||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
|
||||||
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
|
||||||
|
|
||||||
zero reflect.Value
|
|
||||||
)
|
|
||||||
|
|
||||||
// evalVisitor evaluates a handlebars template with context
|
|
||||||
type evalVisitor struct {
|
|
||||||
tpl *Template
|
|
||||||
|
|
||||||
// contexts stack
|
|
||||||
ctx []reflect.Value
|
|
||||||
|
|
||||||
// current data frame (chained with parent)
|
|
||||||
dataFrame *DataFrame
|
|
||||||
|
|
||||||
// block parameters stack
|
|
||||||
blockParams []map[string]interface{}
|
|
||||||
|
|
||||||
// block statements stack
|
|
||||||
blocks []*ast.BlockStatement
|
|
||||||
|
|
||||||
// expressions stack
|
|
||||||
exprs []*ast.Expression
|
|
||||||
|
|
||||||
// memoize expressions that were function calls
|
|
||||||
exprFunc map[*ast.Expression]bool
|
|
||||||
|
|
||||||
// used for info on panic
|
|
||||||
curNode ast.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEvalVisitor instanciate a new evaluation visitor with given context and initial private data frame
|
|
||||||
//
|
|
||||||
// If privData is nil, then a default data frame is created
|
|
||||||
func newEvalVisitor(tpl *Template, ctx interface{}, privData *DataFrame) *evalVisitor {
|
|
||||||
frame := privData
|
|
||||||
if frame == nil {
|
|
||||||
frame = NewDataFrame()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &evalVisitor{
|
|
||||||
tpl: tpl,
|
|
||||||
ctx: []reflect.Value{reflect.ValueOf(ctx)},
|
|
||||||
dataFrame: frame,
|
|
||||||
exprFunc: make(map[*ast.Expression]bool),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// at sets current node
|
|
||||||
func (v *evalVisitor) at(node ast.Node) {
|
|
||||||
v.curNode = node
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Contexts stack
|
|
||||||
//
|
|
||||||
|
|
||||||
// pushCtx pushes new context to the stack
|
|
||||||
func (v *evalVisitor) pushCtx(ctx reflect.Value) {
|
|
||||||
v.ctx = append(v.ctx, ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// popCtx pops last context from stack
|
|
||||||
func (v *evalVisitor) popCtx() reflect.Value {
|
|
||||||
if len(v.ctx) == 0 {
|
|
||||||
return zero
|
|
||||||
}
|
|
||||||
|
|
||||||
var result reflect.Value
|
|
||||||
result, v.ctx = v.ctx[len(v.ctx)-1], v.ctx[:len(v.ctx)-1]
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// rootCtx returns root context
|
|
||||||
func (v *evalVisitor) rootCtx() reflect.Value {
|
|
||||||
return v.ctx[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// curCtx returns current context
|
|
||||||
func (v *evalVisitor) curCtx() reflect.Value {
|
|
||||||
return v.ancestorCtx(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ancestorCtx returns ancestor context
|
|
||||||
func (v *evalVisitor) ancestorCtx(depth int) reflect.Value {
|
|
||||||
index := len(v.ctx) - 1 - depth
|
|
||||||
if index < 0 {
|
|
||||||
return zero
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.ctx[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Private data frame
|
|
||||||
//
|
|
||||||
|
|
||||||
// setDataFrame sets new data frame
|
|
||||||
func (v *evalVisitor) setDataFrame(frame *DataFrame) {
|
|
||||||
v.dataFrame = frame
|
|
||||||
}
|
|
||||||
|
|
||||||
// popDataFrame sets back parent data frame
|
|
||||||
func (v *evalVisitor) popDataFrame() {
|
|
||||||
v.dataFrame = v.dataFrame.parent
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Block Parameters stack
|
|
||||||
//
|
|
||||||
|
|
||||||
// pushBlockParams pushes new block params to the stack
|
|
||||||
func (v *evalVisitor) pushBlockParams(params map[string]interface{}) {
|
|
||||||
v.blockParams = append(v.blockParams, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// popBlockParams pops last block params from stack
|
|
||||||
func (v *evalVisitor) popBlockParams() map[string]interface{} {
|
|
||||||
var result map[string]interface{}
|
|
||||||
|
|
||||||
if len(v.blockParams) == 0 {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
result, v.blockParams = v.blockParams[len(v.blockParams)-1], v.blockParams[:len(v.blockParams)-1]
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// blockParam iterates on stack to find given block parameter, and returns its value or nil if not founc
|
|
||||||
func (v *evalVisitor) blockParam(name string) interface{} {
|
|
||||||
for i := len(v.blockParams) - 1; i >= 0; i-- {
|
|
||||||
for k, v := range v.blockParams[i] {
|
|
||||||
if name == k {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Blocks stack
|
|
||||||
//
|
|
||||||
|
|
||||||
// pushBlock pushes new block statement to stack
|
|
||||||
func (v *evalVisitor) pushBlock(block *ast.BlockStatement) {
|
|
||||||
v.blocks = append(v.blocks, block)
|
|
||||||
}
|
|
||||||
|
|
||||||
// popBlock pops last block statement from stack
|
|
||||||
func (v *evalVisitor) popBlock() *ast.BlockStatement {
|
|
||||||
if len(v.blocks) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var result *ast.BlockStatement
|
|
||||||
result, v.blocks = v.blocks[len(v.blocks)-1], v.blocks[:len(v.blocks)-1]
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// curBlock returns current block statement
|
|
||||||
func (v *evalVisitor) curBlock() *ast.BlockStatement {
|
|
||||||
if len(v.blocks) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.blocks[len(v.blocks)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Expressions stack
|
|
||||||
//
|
|
||||||
|
|
||||||
// pushExpr pushes new expression to stack
|
|
||||||
func (v *evalVisitor) pushExpr(expression *ast.Expression) {
|
|
||||||
v.exprs = append(v.exprs, expression)
|
|
||||||
}
|
|
||||||
|
|
||||||
// popExpr pops last expression from stack
|
|
||||||
func (v *evalVisitor) popExpr() *ast.Expression {
|
|
||||||
if len(v.exprs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var result *ast.Expression
|
|
||||||
result, v.exprs = v.exprs[len(v.exprs)-1], v.exprs[:len(v.exprs)-1]
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// curExpr returns current expression
|
|
||||||
func (v *evalVisitor) curExpr() *ast.Expression {
|
|
||||||
if len(v.exprs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.exprs[len(v.exprs)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Error functions
|
|
||||||
//
|
|
||||||
|
|
||||||
// errPanic panics
|
|
||||||
func (v *evalVisitor) errPanic(err error) {
|
|
||||||
panic(fmt.Errorf("Evaluation error: %s\nCurrent node:\n\t%s", err, v.curNode))
|
|
||||||
}
|
|
||||||
|
|
||||||
// errorf panics with a custom message
|
|
||||||
func (v *evalVisitor) errorf(format string, args ...interface{}) {
|
|
||||||
v.errPanic(fmt.Errorf(format, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Evaluation
|
|
||||||
//
|
|
||||||
|
|
||||||
// evalProgram eEvaluates program with given context and returns string result
|
|
||||||
func (v *evalVisitor) evalProgram(program *ast.Program, ctx interface{}, data *DataFrame, key interface{}) string {
|
|
||||||
blockParams := make(map[string]interface{})
|
|
||||||
|
|
||||||
// compute block params
|
|
||||||
if len(program.BlockParams) > 0 {
|
|
||||||
blockParams[program.BlockParams[0]] = ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
if (len(program.BlockParams) > 1) && (key != nil) {
|
|
||||||
blockParams[program.BlockParams[1]] = key
|
|
||||||
}
|
|
||||||
|
|
||||||
// push contexts
|
|
||||||
if len(blockParams) > 0 {
|
|
||||||
v.pushBlockParams(blockParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxVal := reflect.ValueOf(ctx)
|
|
||||||
if ctxVal.IsValid() {
|
|
||||||
v.pushCtx(ctxVal)
|
|
||||||
}
|
|
||||||
|
|
||||||
if data != nil {
|
|
||||||
v.setDataFrame(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// evaluate program
|
|
||||||
result, _ := program.Accept(v).(string)
|
|
||||||
|
|
||||||
// pop contexts
|
|
||||||
if data != nil {
|
|
||||||
v.popDataFrame()
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctxVal.IsValid() {
|
|
||||||
v.popCtx()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(blockParams) > 0 {
|
|
||||||
v.popBlockParams()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalPath evaluates all path parts with given context
|
|
||||||
func (v *evalVisitor) evalPath(ctx reflect.Value, parts []string, exprRoot bool) (reflect.Value, bool) {
|
|
||||||
partResolved := false
|
|
||||||
|
|
||||||
for i := 0; i < len(parts); i++ {
|
|
||||||
part := parts[i]
|
|
||||||
|
|
||||||
// "[foo bar]"" => "foo bar"
|
|
||||||
if (len(part) >= 2) && (part[0] == '[') && (part[len(part)-1] == ']') {
|
|
||||||
part = part[1 : len(part)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = v.evalField(ctx, part, exprRoot)
|
|
||||||
if !ctx.IsValid() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// we resolved at least one part of path
|
|
||||||
partResolved = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx, partResolved
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalField evaluates field with given context
|
|
||||||
func (v *evalVisitor) evalField(ctx reflect.Value, fieldName string, exprRoot bool) reflect.Value {
|
|
||||||
result := zero
|
|
||||||
|
|
||||||
ctx, _ = indirect(ctx)
|
|
||||||
if !ctx.IsValid() {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if this is a method call
|
|
||||||
result, isMeth := v.evalMethod(ctx, fieldName, exprRoot)
|
|
||||||
if !isMeth {
|
|
||||||
switch ctx.Kind() {
|
|
||||||
case reflect.Struct:
|
|
||||||
// example: firstName => FirstName
|
|
||||||
expFieldName := strings.Title(fieldName)
|
|
||||||
|
|
||||||
// check if struct have this field and that it is exported
|
|
||||||
if tField, ok := ctx.Type().FieldByName(expFieldName); ok && (tField.PkgPath == "") {
|
|
||||||
// struct field
|
|
||||||
result = ctx.FieldByIndex(tField.Index)
|
|
||||||
}
|
|
||||||
case reflect.Map:
|
|
||||||
nameVal := reflect.ValueOf(fieldName)
|
|
||||||
if nameVal.Type().AssignableTo(ctx.Type().Key()) {
|
|
||||||
// map key
|
|
||||||
result = ctx.MapIndex(nameVal)
|
|
||||||
}
|
|
||||||
case reflect.Array, reflect.Slice:
|
|
||||||
if i, err := strconv.Atoi(fieldName); (err == nil) && (i < ctx.Len()) {
|
|
||||||
result = ctx.Index(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if result is a function
|
|
||||||
result, _ = indirect(result)
|
|
||||||
if result.Kind() == reflect.Func {
|
|
||||||
result = v.evalFieldFunc(fieldName, result, exprRoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalFieldFunc tries to evaluate given method name, and a boolean to indicate if this was a method call
|
|
||||||
func (v *evalVisitor) evalMethod(ctx reflect.Value, name string, exprRoot bool) (reflect.Value, bool) {
|
|
||||||
if ctx.Kind() != reflect.Interface && ctx.CanAddr() {
|
|
||||||
ctx = ctx.Addr()
|
|
||||||
}
|
|
||||||
|
|
||||||
method := ctx.MethodByName(name)
|
|
||||||
if !method.IsValid() {
|
|
||||||
// example: subject() => Subject()
|
|
||||||
method = ctx.MethodByName(strings.Title(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !method.IsValid() {
|
|
||||||
return zero, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.evalFieldFunc(name, method, exprRoot), true
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalFieldFunc evaluates given function
|
|
||||||
func (v *evalVisitor) evalFieldFunc(name string, funcVal reflect.Value, exprRoot bool) reflect.Value {
|
|
||||||
ensureValidHelper(name, funcVal)
|
|
||||||
|
|
||||||
var options *Options
|
|
||||||
if exprRoot {
|
|
||||||
// create function arg with all params/hash
|
|
||||||
expr := v.curExpr()
|
|
||||||
options = v.helperOptions(expr)
|
|
||||||
|
|
||||||
// ok, that expression was a function call
|
|
||||||
v.exprFunc[expr] = true
|
|
||||||
} else {
|
|
||||||
// we are not at root of expression, so we are a parameter... and we don't like
|
|
||||||
// infinite loops caused by trying to parse ourself forever
|
|
||||||
options = newEmptyOptions(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.callFunc(name, funcVal, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// findBlockParam returns node's block parameter
|
|
||||||
func (v *evalVisitor) findBlockParam(node *ast.PathExpression) (string, interface{}) {
|
|
||||||
if len(node.Parts) > 0 {
|
|
||||||
name := node.Parts[0]
|
|
||||||
if value := v.blockParam(name); value != nil {
|
|
||||||
return name, value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalPathExpression evaluates a path expression
|
|
||||||
func (v *evalVisitor) evalPathExpression(node *ast.PathExpression, exprRoot bool) interface{} {
|
|
||||||
var result interface{}
|
|
||||||
|
|
||||||
if name, value := v.findBlockParam(node); value != nil {
|
|
||||||
// block parameter value
|
|
||||||
|
|
||||||
// We push a new context so we can evaluate the path expression (note: this may be a bad idea).
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
// {{#foo as |bar|}}
|
|
||||||
// {{bar.baz}}
|
|
||||||
// {{/foo}}
|
|
||||||
//
|
|
||||||
// With data:
|
|
||||||
// {"foo": {"baz": "bat"}}
|
|
||||||
newCtx := map[string]interface{}{name: value}
|
|
||||||
|
|
||||||
v.pushCtx(reflect.ValueOf(newCtx))
|
|
||||||
result = v.evalCtxPathExpression(node, exprRoot)
|
|
||||||
v.popCtx()
|
|
||||||
} else {
|
|
||||||
ctxTried := false
|
|
||||||
|
|
||||||
if node.IsDataRoot() {
|
|
||||||
// context path
|
|
||||||
result = v.evalCtxPathExpression(node, exprRoot)
|
|
||||||
|
|
||||||
ctxTried = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == nil) && node.Data {
|
|
||||||
// if it is @root, then we tried to evaluate with root context but nothing was found
|
|
||||||
// so let's try with private data
|
|
||||||
|
|
||||||
// private data
|
|
||||||
result = v.evalDataPathExpression(node, exprRoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == nil) && !ctxTried {
|
|
||||||
// context path
|
|
||||||
result = v.evalCtxPathExpression(node, exprRoot)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalDataPathExpression evaluates a private data path expression
|
|
||||||
func (v *evalVisitor) evalDataPathExpression(node *ast.PathExpression, exprRoot bool) interface{} {
|
|
||||||
// find data frame
|
|
||||||
frame := v.dataFrame
|
|
||||||
for i := node.Depth; i > 0; i-- {
|
|
||||||
if frame.parent == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
frame = frame.parent
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolve data
|
|
||||||
// @note Can be changed to v.evalCtx() as context can't be an array
|
|
||||||
result, _ := v.evalCtxPath(reflect.ValueOf(frame.data), node.Parts, exprRoot)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalCtxPathExpression evaluates a context path expression
|
|
||||||
func (v *evalVisitor) evalCtxPathExpression(node *ast.PathExpression, exprRoot bool) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
if node.IsDataRoot() {
|
|
||||||
// `@root` - remove the first part
|
|
||||||
parts := node.Parts[1:len(node.Parts)]
|
|
||||||
|
|
||||||
result, _ := v.evalCtxPath(v.rootCtx(), parts, exprRoot)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.evalDepthPath(node.Depth, node.Parts, exprRoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalDepthPath iterates on contexts, starting at given depth, until there is one that resolve given path parts
|
|
||||||
func (v *evalVisitor) evalDepthPath(depth int, parts []string, exprRoot bool) interface{} {
|
|
||||||
var result interface{}
|
|
||||||
partResolved := false
|
|
||||||
|
|
||||||
ctx := v.ancestorCtx(depth)
|
|
||||||
|
|
||||||
for (result == nil) && ctx.IsValid() && (depth <= len(v.ctx) && !partResolved) {
|
|
||||||
// try with context
|
|
||||||
result, partResolved = v.evalCtxPath(ctx, parts, exprRoot)
|
|
||||||
|
|
||||||
// As soon as we find the first part of a path, we must not try to resolve with parent context if result is finally `nil`
|
|
||||||
// Reference: "Dotted Names - Context Precedence" mustache test
|
|
||||||
if !partResolved && (result == nil) {
|
|
||||||
// try with previous context
|
|
||||||
depth++
|
|
||||||
ctx = v.ancestorCtx(depth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalCtxPath evaluates path with given context
|
|
||||||
func (v *evalVisitor) evalCtxPath(ctx reflect.Value, parts []string, exprRoot bool) (interface{}, bool) {
|
|
||||||
var result interface{}
|
|
||||||
partResolved := false
|
|
||||||
|
|
||||||
switch ctx.Kind() {
|
|
||||||
case reflect.Array, reflect.Slice:
|
|
||||||
// Array context
|
|
||||||
var results []interface{}
|
|
||||||
|
|
||||||
for i := 0; i < ctx.Len(); i++ {
|
|
||||||
value, _ := v.evalPath(ctx.Index(i), parts, exprRoot)
|
|
||||||
if value.IsValid() {
|
|
||||||
results = append(results, value.Interface())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result = results
|
|
||||||
default:
|
|
||||||
// NOT array context
|
|
||||||
var value reflect.Value
|
|
||||||
|
|
||||||
value, partResolved = v.evalPath(ctx, parts, exprRoot)
|
|
||||||
if value.IsValid() {
|
|
||||||
result = value.Interface()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, partResolved
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Helpers
|
|
||||||
//
|
|
||||||
|
|
||||||
// isHelperCall returns true if given expression is a helper call
|
|
||||||
func (v *evalVisitor) isHelperCall(node *ast.Expression) bool {
|
|
||||||
if helperName := node.HelperName(); helperName != "" {
|
|
||||||
return v.findHelper(helperName) != zero
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// findHelper finds given helper
|
|
||||||
func (v *evalVisitor) findHelper(name string) reflect.Value {
|
|
||||||
// check template helpers
|
|
||||||
if h := v.tpl.findHelper(name); h != zero {
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// check global helpers
|
|
||||||
return findHelper(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callFunc calls function with given options
|
|
||||||
func (v *evalVisitor) callFunc(name string, funcVal reflect.Value, options *Options) reflect.Value {
|
|
||||||
params := options.Params()
|
|
||||||
|
|
||||||
funcType := funcVal.Type()
|
|
||||||
|
|
||||||
// @todo Is there a better way to do that ?
|
|
||||||
strType := reflect.TypeOf("")
|
|
||||||
boolType := reflect.TypeOf(true)
|
|
||||||
|
|
||||||
// check parameters number
|
|
||||||
addOptions := false
|
|
||||||
numIn := funcType.NumIn()
|
|
||||||
|
|
||||||
if numIn == len(params)+1 {
|
|
||||||
lastArgType := funcType.In(numIn - 1)
|
|
||||||
if reflect.TypeOf(options).AssignableTo(lastArgType) {
|
|
||||||
addOptions = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !addOptions && (len(params) != numIn) {
|
|
||||||
v.errorf("Helper '%s' called with wrong number of arguments, needed %d but got %d", name, numIn, len(params))
|
|
||||||
}
|
|
||||||
|
|
||||||
// check and collect arguments
|
|
||||||
args := make([]reflect.Value, numIn)
|
|
||||||
for i, param := range params {
|
|
||||||
arg := reflect.ValueOf(param)
|
|
||||||
argType := funcType.In(i)
|
|
||||||
|
|
||||||
if !arg.IsValid() {
|
|
||||||
if canBeNil(argType) {
|
|
||||||
arg = reflect.Zero(argType)
|
|
||||||
} else if argType.Kind() == reflect.String {
|
|
||||||
arg = reflect.ValueOf("")
|
|
||||||
} else {
|
|
||||||
// @todo Maybe we can panic on that
|
|
||||||
return reflect.Zero(strType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !arg.Type().AssignableTo(argType) {
|
|
||||||
if strType.AssignableTo(argType) {
|
|
||||||
// convert parameter to string
|
|
||||||
arg = reflect.ValueOf(strValue(arg))
|
|
||||||
} else if boolType.AssignableTo(argType) {
|
|
||||||
// convert parameter to bool
|
|
||||||
val, _ := isTrueValue(arg)
|
|
||||||
arg = reflect.ValueOf(val)
|
|
||||||
} else {
|
|
||||||
v.errorf("Helper %s called with argument %d with type %s but it should be %s", name, i, arg.Type(), argType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args[i] = arg
|
|
||||||
}
|
|
||||||
|
|
||||||
if addOptions {
|
|
||||||
args[numIn-1] = reflect.ValueOf(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := funcVal.Call(args)
|
|
||||||
|
|
||||||
return result[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// callHelper invoqs helper function for given expression node
|
|
||||||
func (v *evalVisitor) callHelper(name string, helper reflect.Value, node *ast.Expression) interface{} {
|
|
||||||
result := v.callFunc(name, helper, v.helperOptions(node))
|
|
||||||
if !result.IsValid() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// @todo We maybe want to ensure here that helper returned a string or a SafeString
|
|
||||||
return result.Interface()
|
|
||||||
}
|
|
||||||
|
|
||||||
// helperOptions computes helper options argument from an expression
|
|
||||||
func (v *evalVisitor) helperOptions(node *ast.Expression) *Options {
|
|
||||||
var params []interface{}
|
|
||||||
var hash map[string]interface{}
|
|
||||||
|
|
||||||
for _, paramNode := range node.Params {
|
|
||||||
param := paramNode.Accept(v)
|
|
||||||
params = append(params, param)
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.Hash != nil {
|
|
||||||
hash, _ = node.Hash.Accept(v).(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
return newOptions(v, params, hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Partials
|
|
||||||
//
|
|
||||||
|
|
||||||
// findPartial finds given partial
|
|
||||||
func (v *evalVisitor) findPartial(name string) *partial {
|
|
||||||
// check template partials
|
|
||||||
if p := v.tpl.findPartial(name); p != nil {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// check global partials
|
|
||||||
return findPartial(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// partialContext computes partial context
|
|
||||||
func (v *evalVisitor) partialContext(node *ast.PartialStatement) reflect.Value {
|
|
||||||
if nb := len(node.Params); nb > 1 {
|
|
||||||
v.errorf("Unsupported number of partial arguments: %d", nb)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (len(node.Params) > 0) && (node.Hash != nil) {
|
|
||||||
v.errorf("Passing both context and named parameters to a partial is not allowed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(node.Params) == 1 {
|
|
||||||
return reflect.ValueOf(node.Params[0].Accept(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.Hash != nil {
|
|
||||||
hash, _ := node.Hash.Accept(v).(map[string]interface{})
|
|
||||||
return reflect.ValueOf(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
return zero
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalPartial evaluates a partial
|
|
||||||
func (v *evalVisitor) evalPartial(p *partial, node *ast.PartialStatement) string {
|
|
||||||
// get partial template
|
|
||||||
partialTpl, err := p.template()
|
|
||||||
if err != nil {
|
|
||||||
v.errPanic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// push partial context
|
|
||||||
ctx := v.partialContext(node)
|
|
||||||
if ctx.IsValid() {
|
|
||||||
v.pushCtx(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// evaluate partial template
|
|
||||||
result, _ := partialTpl.program.Accept(v).(string)
|
|
||||||
|
|
||||||
// ident partial
|
|
||||||
result = indentLines(result, node.Indent)
|
|
||||||
|
|
||||||
if ctx.IsValid() {
|
|
||||||
v.popCtx()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// indentLines indents all lines of given string
|
|
||||||
func indentLines(str string, indent string) string {
|
|
||||||
if indent == "" {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
var indented []string
|
|
||||||
|
|
||||||
lines := strings.Split(str, "\n")
|
|
||||||
for i, line := range lines {
|
|
||||||
if (i == (len(lines) - 1)) && (line == "") {
|
|
||||||
// input string ends with a new line
|
|
||||||
indented = append(indented, line)
|
|
||||||
} else {
|
|
||||||
indented = append(indented, indent+line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(indented, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Functions
|
|
||||||
//
|
|
||||||
|
|
||||||
// wasFuncCall returns true if given expression was a function call
|
|
||||||
func (v *evalVisitor) wasFuncCall(node *ast.Expression) bool {
|
|
||||||
// check if expression was tagged as a function call
|
|
||||||
return v.exprFunc[node]
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Visitor interface
|
|
||||||
//
|
|
||||||
|
|
||||||
// Statements
|
|
||||||
|
|
||||||
// VisitProgram implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitProgram(node *ast.Program) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
|
|
||||||
for _, n := range node.Body {
|
|
||||||
if str := Str(n.Accept(v)); str != "" {
|
|
||||||
if _, err := buf.Write([]byte(str)); err != nil {
|
|
||||||
v.errPanic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitMustache implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitMustache(node *ast.MustacheStatement) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
// evaluate expression
|
|
||||||
expr := node.Expression.Accept(v)
|
|
||||||
|
|
||||||
// check if this is a safe string
|
|
||||||
isSafe := isSafeString(expr)
|
|
||||||
|
|
||||||
// get string value
|
|
||||||
str := Str(expr)
|
|
||||||
if !isSafe && !node.Unescaped {
|
|
||||||
// escape html
|
|
||||||
str = Escape(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitBlock implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitBlock(node *ast.BlockStatement) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
v.pushBlock(node)
|
|
||||||
|
|
||||||
var result interface{}
|
|
||||||
|
|
||||||
// evaluate expression
|
|
||||||
expr := node.Expression.Accept(v)
|
|
||||||
|
|
||||||
if v.isHelperCall(node.Expression) || v.wasFuncCall(node.Expression) {
|
|
||||||
// it is the responsability of the helper/function to evaluate block
|
|
||||||
result = expr
|
|
||||||
} else {
|
|
||||||
val := reflect.ValueOf(expr)
|
|
||||||
|
|
||||||
truth, _ := isTrueValue(val)
|
|
||||||
if truth {
|
|
||||||
if node.Program != nil {
|
|
||||||
switch val.Kind() {
|
|
||||||
case reflect.Array, reflect.Slice:
|
|
||||||
concat := ""
|
|
||||||
|
|
||||||
// Array context
|
|
||||||
for i := 0; i < val.Len(); i++ {
|
|
||||||
// Computes new private data frame
|
|
||||||
frame := v.dataFrame.newIterDataFrame(val.Len(), i, nil)
|
|
||||||
|
|
||||||
// Evaluate program
|
|
||||||
concat += v.evalProgram(node.Program, val.Index(i).Interface(), frame, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
result = concat
|
|
||||||
default:
|
|
||||||
// NOT array
|
|
||||||
result = v.evalProgram(node.Program, expr, nil, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if node.Inverse != nil {
|
|
||||||
result, _ = node.Inverse.Accept(v).(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v.popBlock()
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitPartial implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitPartial(node *ast.PartialStatement) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
// partialName: helperName | sexpr
|
|
||||||
name, ok := ast.HelperNameStr(node.Name)
|
|
||||||
if !ok {
|
|
||||||
if subExpr, ok := node.Name.(*ast.SubExpression); ok {
|
|
||||||
name, _ = subExpr.Accept(v).(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if name == "" {
|
|
||||||
v.errorf("Unexpected partial name: %q", node.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
partial := v.findPartial(name)
|
|
||||||
if partial == nil {
|
|
||||||
v.errorf("Partial not found: %s", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.evalPartial(partial, node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitContent implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitContent(node *ast.ContentStatement) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
// write content as is
|
|
||||||
return node.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitComment implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitComment(node *ast.CommentStatement) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
// ignore comments
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expressions
|
|
||||||
|
|
||||||
// VisitExpression implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitExpression(node *ast.Expression) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
var result interface{}
|
|
||||||
done := false
|
|
||||||
|
|
||||||
v.pushExpr(node)
|
|
||||||
|
|
||||||
// helper call
|
|
||||||
if helperName := node.HelperName(); helperName != "" {
|
|
||||||
if helper := v.findHelper(helperName); helper != zero {
|
|
||||||
result = v.callHelper(helperName, helper, node)
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !done {
|
|
||||||
// literal
|
|
||||||
if literal, ok := node.LiteralStr(); ok {
|
|
||||||
if val := v.evalField(v.curCtx(), literal, true); val.IsValid() {
|
|
||||||
result = val.Interface()
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !done {
|
|
||||||
// field path
|
|
||||||
if path := node.FieldPath(); path != nil {
|
|
||||||
// @todo Find a cleaner way ! Don't break the pattern !
|
|
||||||
// this is an exception to visitor pattern, because we need to pass the info
|
|
||||||
// that this path is at root of current expression
|
|
||||||
if val := v.evalPathExpression(path, true); val != nil {
|
|
||||||
result = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v.popExpr()
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitSubExpression implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitSubExpression(node *ast.SubExpression) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
return node.Expression.Accept(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitPath implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitPath(node *ast.PathExpression) interface{} {
|
|
||||||
return v.evalPathExpression(node, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Literals
|
|
||||||
|
|
||||||
// VisitString implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitString(node *ast.StringLiteral) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
return node.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitBoolean implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitBoolean(node *ast.BooleanLiteral) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
return node.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitNumber implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitNumber(node *ast.NumberLiteral) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
return node.Number()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Miscellaneous
|
|
||||||
|
|
||||||
// VisitHash implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitHash(node *ast.Hash) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
result := make(map[string]interface{})
|
|
||||||
|
|
||||||
for _, pair := range node.Pairs {
|
|
||||||
if value := pair.Accept(v); value != nil {
|
|
||||||
result[pair.Key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitHashPair implements corresponding Visitor interface method
|
|
||||||
func (v *evalVisitor) VisitHashPair(node *ast.HashPair) interface{} {
|
|
||||||
v.at(node)
|
|
||||||
|
|
||||||
return node.Val.Accept(v)
|
|
||||||
}
|
|
|
@ -1,215 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
var evalTests = []Test{
|
|
||||||
{
|
|
||||||
"only content",
|
|
||||||
"this is content",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"this is content",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"checks path in parent contexts",
|
|
||||||
"{{#a}}{{one}}{{#b}}{{one}}{{two}}{{one}}{{/b}}{{/a}}",
|
|
||||||
map[string]interface{}{"a": map[string]int{"one": 1}, "b": map[string]int{"two": 2}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"1121",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block params",
|
|
||||||
"{{#foo as |bar|}}{{bar}}{{/foo}}{{bar}}",
|
|
||||||
map[string]string{"foo": "baz", "bar": "bat"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bazbat",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block params on array",
|
|
||||||
"{{#foo as |bar i|}}{{i}}.{{bar}} {{/foo}}",
|
|
||||||
map[string][]string{"foo": {"baz", "bar", "bat"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"0.baz 1.bar 2.bat ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nested block params",
|
|
||||||
"{{#foos as |foo iFoo|}}{{#wats as |wat iWat|}}{{iFoo}}.{{iWat}}.{{foo}}-{{wat}} {{/wats}}{{/foos}}",
|
|
||||||
map[string][]string{"foos": {"baz", "bar"}, "wats": {"the", "phoque"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"0.0.baz-the 0.1.baz-phoque 1.0.bar-the 1.1.bar-phoque ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block params with path reference",
|
|
||||||
"{{#foo as |bar|}}{{bar.baz}}{{/foo}}",
|
|
||||||
map[string]map[string]string{"foo": {"baz": "bat"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bat",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"falsy block evaluation",
|
|
||||||
"{{#foo}}bar{{/foo}} baz",
|
|
||||||
map[string]interface{}{"foo": false},
|
|
||||||
nil, nil, nil,
|
|
||||||
" baz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helper returns a SafeString",
|
|
||||||
"{{title}} - {{#bold}}{{body}}{{/bold}}",
|
|
||||||
map[string]string{
|
|
||||||
"title": "My new blog post",
|
|
||||||
"body": "I have so many things to say!",
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"bold": func(options *Options) SafeString {
|
|
||||||
return SafeString(`<div class="mybold">` + options.Fn() + "</div>")
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
`My new blog post - <div class="mybold">I have so many things to say!</div>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"chained blocks",
|
|
||||||
"{{#if a}}A{{else if b}}B{{else}}C{{/if}}",
|
|
||||||
map[string]interface{}{"b": false},
|
|
||||||
nil, nil, nil,
|
|
||||||
"C",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo Test with a "../../path" (depth 2 path) while context is only depth 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEval(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
launchTests(t, evalTests)
|
|
||||||
}
|
|
||||||
|
|
||||||
var evalErrors = []Test{
|
|
||||||
{
|
|
||||||
"functions with wrong number of arguments",
|
|
||||||
`{{foo "bar"}}`,
|
|
||||||
map[string]interface{}{"foo": func(a string, b string) string { return "foo" }},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Helper 'foo' called with wrong number of arguments, needed 2 but got 1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"functions with wrong number of returned values (1)",
|
|
||||||
"{{foo}}",
|
|
||||||
map[string]interface{}{"foo": func() {}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Helper function must return a string or a SafeString",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"functions with wrong number of returned values (2)",
|
|
||||||
"{{foo}}",
|
|
||||||
map[string]interface{}{"foo": func() (string, bool, string) { return "foo", true, "bar" }},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Helper function must return a string or a SafeString",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvalErrors(t *testing.T) {
|
|
||||||
launchErrorTests(t, evalErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvalStruct(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
source := `<div class="post">
|
|
||||||
<h1>By {{author.FirstName}} {{Author.lastName}}</h1>
|
|
||||||
<div class="body">{{Body}}</div>
|
|
||||||
|
|
||||||
<h1>Comments</h1>
|
|
||||||
|
|
||||||
{{#each comments}}
|
|
||||||
<h2>By {{Author.FirstName}} {{author.LastName}}</h2>
|
|
||||||
<div class="body">{{body}}</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>`
|
|
||||||
|
|
||||||
expected := `<div class="post">
|
|
||||||
<h1>By Jean Valjean</h1>
|
|
||||||
<div class="body">Life is difficult</div>
|
|
||||||
|
|
||||||
<h1>Comments</h1>
|
|
||||||
|
|
||||||
<h2>By Marcel Beliveau</h2>
|
|
||||||
<div class="body">LOL!</div>
|
|
||||||
</div>`
|
|
||||||
|
|
||||||
type Person struct {
|
|
||||||
FirstName string
|
|
||||||
LastName string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Comment struct {
|
|
||||||
Author Person
|
|
||||||
Body string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Post struct {
|
|
||||||
Author Person
|
|
||||||
Body string
|
|
||||||
Comments []Comment
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := Post{
|
|
||||||
Person{"Jean", "Valjean"},
|
|
||||||
"Life is difficult",
|
|
||||||
[]Comment{
|
|
||||||
Comment{
|
|
||||||
Person{"Marcel", "Beliveau"},
|
|
||||||
"LOL!",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
output := MustRender(source, ctx)
|
|
||||||
if output != expected {
|
|
||||||
t.Errorf("Failed to evaluate with struct context")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TestFoo struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TestFoo) Subject() string {
|
|
||||||
return "foo"
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvalMethod(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
source := `Subject is {{subject}}! YES I SAID {{Subject}}!`
|
|
||||||
expected := `Subject is foo! YES I SAID foo!`
|
|
||||||
|
|
||||||
ctx := &TestFoo{}
|
|
||||||
|
|
||||||
output := MustRender(source, ctx)
|
|
||||||
if output != expected {
|
|
||||||
t.Errorf("Failed to evaluate struct method: %s", output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TestBar struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TestBar) Subject() interface{} {
|
|
||||||
return testBar
|
|
||||||
}
|
|
||||||
|
|
||||||
func testBar() string {
|
|
||||||
return "bar"
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvalMethodReturningFunc(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
source := `Subject is {{subject}}! YES I SAID {{Subject}}!`
|
|
||||||
expected := `Subject is bar! YES I SAID bar!`
|
|
||||||
|
|
||||||
ctx := &TestBar{}
|
|
||||||
|
|
||||||
output := MustRender(source, ctx)
|
|
||||||
if output != expected {
|
|
||||||
t.Errorf("Failed to evaluate struct method: %s", output)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
package handlebars
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/aymerick/raymond"
|
|
||||||
)
|
|
||||||
|
|
||||||
// cf. https://github.com/aymerick/go-fuzz-tests/raymond
|
|
||||||
const DUMP_TPL = false
|
|
||||||
|
|
||||||
var dump_tpl_nb = 0
|
|
||||||
|
|
||||||
type Test struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
data interface{}
|
|
||||||
privData map[string]interface{}
|
|
||||||
helpers map[string]interface{}
|
|
||||||
partials map[string]string
|
|
||||||
output interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func launchTests(t *testing.T, tests []Test) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
var err error
|
|
||||||
var tpl *raymond.Template
|
|
||||||
|
|
||||||
if DUMP_TPL {
|
|
||||||
filename := strconv.Itoa(dump_tpl_nb)
|
|
||||||
if err := ioutil.WriteFile(path.Join(".", "dump_tpl", filename), []byte(test.input), 0644); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
dump_tpl_nb += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse template
|
|
||||||
tpl, err = raymond.Parse(test.input)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err)
|
|
||||||
} else {
|
|
||||||
if len(test.helpers) > 0 {
|
|
||||||
// register helpers
|
|
||||||
tpl.RegisterHelpers(test.helpers)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(test.partials) > 0 {
|
|
||||||
// register partials
|
|
||||||
tpl.RegisterPartials(test.partials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup private data frame
|
|
||||||
var privData *raymond.DataFrame
|
|
||||||
if test.privData != nil {
|
|
||||||
privData = raymond.NewDataFrame()
|
|
||||||
for k, v := range test.privData {
|
|
||||||
privData.Set(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// render template
|
|
||||||
output, err := tpl.ExecWith(test.data, privData)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\nerror:\n\t%s\nAST:\n\t%s", test.name, test.input, raymond.Str(test.data), err, tpl.PrintAST())
|
|
||||||
} else {
|
|
||||||
// check output
|
|
||||||
var expectedArr []string
|
|
||||||
expectedArr, ok := test.output.([]string)
|
|
||||||
if ok {
|
|
||||||
match := false
|
|
||||||
for _, expectedStr := range expectedArr {
|
|
||||||
if expectedStr == output {
|
|
||||||
match = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !match {
|
|
||||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, raymond.Str(test.data), raymond.Str(test.partials), expectedArr, output, tpl.PrintAST())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
expectedStr, ok := test.output.(string)
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Errorf("Erroneous test output description: %q", test.output))
|
|
||||||
}
|
|
||||||
|
|
||||||
if expectedStr != output {
|
|
||||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, raymond.Str(test.data), raymond.Str(test.partials), expectedStr, output, tpl.PrintAST())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,651 +0,0 @@
|
||||||
package handlebars
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/aymerick/raymond"
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Those tests come from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/spec/basic.js
|
|
||||||
//
|
|
||||||
var basicTests = []Test{
|
|
||||||
{
|
|
||||||
"most basic",
|
|
||||||
"{{foo}}",
|
|
||||||
map[string]string{"foo": "foo"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping (1)",
|
|
||||||
"\\{{foo}}",
|
|
||||||
map[string]string{"foo": "food"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"{{foo}}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping (2)",
|
|
||||||
"content \\{{foo}}",
|
|
||||||
map[string]string{},
|
|
||||||
nil, nil, nil,
|
|
||||||
"content {{foo}}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping (3)",
|
|
||||||
"\\\\{{foo}}",
|
|
||||||
map[string]string{"foo": "food"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"\\food",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping (4)",
|
|
||||||
"content \\\\{{foo}}",
|
|
||||||
map[string]string{"foo": "food"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"content \\food",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping (5)",
|
|
||||||
"\\\\ {{foo}}",
|
|
||||||
map[string]string{"foo": "food"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"\\\\ food",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"compiling with a basic context",
|
|
||||||
"Goodbye\n{{cruel}}\n{{world}}!",
|
|
||||||
map[string]string{"cruel": "cruel", "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Goodbye\ncruel\nworld!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"compiling with an undefined context (1)",
|
|
||||||
"Goodbye\n{{cruel}}\n{{world.bar}}!",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"Goodbye\n\n!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"compiling with an undefined context (2)",
|
|
||||||
"{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"Goodbye",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"comments (1)",
|
|
||||||
"{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!",
|
|
||||||
map[string]string{"cruel": "cruel", "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Goodbye\ncruel\nworld!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"comments (2)",
|
|
||||||
" {{~! comment ~}} blah",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"blah",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"comments (3)",
|
|
||||||
" {{~!-- long-comment --~}} blah",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"blah",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"comments (4)",
|
|
||||||
" {{! comment ~}} blah",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
" blah",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"comments (5)",
|
|
||||||
" {{!-- long-comment --~}} blah",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
" blah",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"comments (6)",
|
|
||||||
" {{~! comment}} blah",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
" blah",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"comments (7)",
|
|
||||||
" {{~!-- long-comment --}} blah",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
" blah",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"boolean (1)",
|
|
||||||
"{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbye": true, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"GOODBYE cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"boolean (2)",
|
|
||||||
"{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbye": false, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"zeros (1)",
|
|
||||||
"num1: {{num1}}, num2: {{num2}}",
|
|
||||||
map[string]interface{}{"num1": 42, "num2": 0},
|
|
||||||
nil, nil, nil,
|
|
||||||
"num1: 42, num2: 0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"zeros (2)",
|
|
||||||
"num: {{.}}",
|
|
||||||
0,
|
|
||||||
nil, nil, nil,
|
|
||||||
"num: 0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"zeros (3)",
|
|
||||||
"num: {{num1/num2}}",
|
|
||||||
map[string]map[string]interface{}{"num1": {"num2": 0}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"num: 0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"false (1)",
|
|
||||||
"val1: {{val1}}, val2: {{val2}}",
|
|
||||||
map[string]interface{}{"val1": false, "val2": false},
|
|
||||||
nil, nil, nil,
|
|
||||||
"val1: false, val2: false",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"false (2)",
|
|
||||||
"val: {{.}}",
|
|
||||||
false,
|
|
||||||
nil, nil, nil,
|
|
||||||
"val: false",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"false (3)",
|
|
||||||
"val: {{val1/val2}}",
|
|
||||||
map[string]map[string]interface{}{"val1": {"val2": false}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"val: false",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"false (4)",
|
|
||||||
"val1: {{{val1}}}, val2: {{{val2}}}",
|
|
||||||
map[string]interface{}{"val1": false, "val2": false},
|
|
||||||
nil, nil, nil,
|
|
||||||
"val1: false, val2: false",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"false (5)",
|
|
||||||
"val: {{{val1/val2}}}",
|
|
||||||
map[string]map[string]interface{}{"val1": {"val2": false}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"val: false",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"newlines (1)",
|
|
||||||
"Alan's\nTest",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"Alan's\nTest",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"newlines (2)",
|
|
||||||
"Alan's\rTest",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"Alan's\rTest",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping text (1)",
|
|
||||||
"Awesome's",
|
|
||||||
map[string]string{},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Awesome's",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping text (2)",
|
|
||||||
"Awesome\\",
|
|
||||||
map[string]string{},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Awesome\\",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping text (3)",
|
|
||||||
"Awesome\\\\ foo",
|
|
||||||
map[string]string{},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Awesome\\\\ foo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping text (4)",
|
|
||||||
"Awesome {{foo}}",
|
|
||||||
map[string]string{"foo": "\\"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Awesome \\",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping text (5)",
|
|
||||||
" ' ' ",
|
|
||||||
map[string]string{},
|
|
||||||
nil, nil, nil,
|
|
||||||
" ' ' ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping expressions (6)",
|
|
||||||
"{{{awesome}}}",
|
|
||||||
map[string]string{"awesome": "&'\\<>"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"&'\\<>",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping expressions (7)",
|
|
||||||
"{{&awesome}}",
|
|
||||||
map[string]string{"awesome": "&'\\<>"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"&'\\<>",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping expressions (8)",
|
|
||||||
"{{awesome}}",
|
|
||||||
map[string]string{"awesome": "&\"'`\\<>"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"&"'`\\<>",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"escaping expressions (9)",
|
|
||||||
"{{awesome}}",
|
|
||||||
map[string]string{"awesome": "Escaped, <b> looks like: <b>"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Escaped, <b> looks like: &lt;b&gt;",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"functions returning safestrings shouldn't be escaped",
|
|
||||||
"{{awesome}}",
|
|
||||||
map[string]interface{}{"awesome": func() raymond.SafeString { return raymond.SafeString("&'\\<>") }},
|
|
||||||
nil, nil, nil,
|
|
||||||
"&'\\<>",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"functions (1)",
|
|
||||||
"{{awesome}}",
|
|
||||||
map[string]interface{}{"awesome": func() string { return "Awesome" }},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Awesome",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"functions (2)",
|
|
||||||
"{{awesome}}",
|
|
||||||
map[string]interface{}{"awesome": func(options *raymond.Options) string {
|
|
||||||
return options.ValueStr("more")
|
|
||||||
}, "more": "More awesome"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"More awesome",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"functions with context argument",
|
|
||||||
"{{awesome frank}}",
|
|
||||||
map[string]interface{}{"awesome": func(context string) string {
|
|
||||||
return context
|
|
||||||
}, "frank": "Frank"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Frank",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pathed functions with context argument",
|
|
||||||
"{{bar.awesome frank}}",
|
|
||||||
map[string]interface{}{"bar": map[string]interface{}{"awesome": func(context string) string {
|
|
||||||
return context
|
|
||||||
}}, "frank": "Frank"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Frank",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depthed functions with context argument",
|
|
||||||
"{{#with frank}}{{../awesome .}}{{/with}}",
|
|
||||||
map[string]interface{}{"awesome": func(context string) string {
|
|
||||||
return context
|
|
||||||
}, "frank": "Frank"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Frank",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block functions with context argument",
|
|
||||||
"{{#awesome 1}}inner {{.}}{{/awesome}}",
|
|
||||||
map[string]interface{}{"awesome": func(context interface{}, options *raymond.Options) string {
|
|
||||||
return options.FnWith(context)
|
|
||||||
}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"inner 1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depthed block functions with context argument",
|
|
||||||
"{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}",
|
|
||||||
map[string]interface{}{
|
|
||||||
"awesome": func(context interface{}, options *raymond.Options) string {
|
|
||||||
return options.FnWith(context)
|
|
||||||
},
|
|
||||||
"value": true,
|
|
||||||
},
|
|
||||||
nil, nil, nil,
|
|
||||||
"inner 1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block functions without context argument",
|
|
||||||
"{{#awesome}}inner{{/awesome}}",
|
|
||||||
map[string]interface{}{
|
|
||||||
"awesome": func(options *raymond.Options) string {
|
|
||||||
return options.Fn()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil, nil, nil,
|
|
||||||
"inner",
|
|
||||||
},
|
|
||||||
// // @note I don't even understand why this test passes with the JS implementation... it should be
|
|
||||||
// // the responsability of the function to evaluate the block
|
|
||||||
// {
|
|
||||||
// "pathed block functions without context argument",
|
|
||||||
// "{{#foo.awesome}}inner{{/foo.awesome}}",
|
|
||||||
// map[string]map[string]interface{}{
|
|
||||||
// "foo": {
|
|
||||||
// "awesome": func(options *raymond.Options) interface{} {
|
|
||||||
// return options.Ctx()
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// nil, nil, nil,
|
|
||||||
// "inner",
|
|
||||||
// },
|
|
||||||
// // @note I don't even understand why this test passes with the JS implementation... it should be
|
|
||||||
// // the responsability of the function to evaluate the block
|
|
||||||
// {
|
|
||||||
// "depthed block functions without context argument",
|
|
||||||
// "{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}",
|
|
||||||
// map[string]interface{}{
|
|
||||||
// "value": true,
|
|
||||||
// "awesome": func(options *raymond.Options) interface{} {
|
|
||||||
// return options.Ctx()
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// nil, nil, nil,
|
|
||||||
// "inner",
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
"paths with hyphens (1)",
|
|
||||||
"{{foo-bar}}",
|
|
||||||
map[string]string{"foo-bar": "baz"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"baz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"paths with hyphens (2)",
|
|
||||||
"{{foo.foo-bar}}",
|
|
||||||
map[string]map[string]string{"foo": {"foo-bar": "baz"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"baz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"paths with hyphens (3)",
|
|
||||||
"{{foo/foo-bar}}",
|
|
||||||
map[string]map[string]string{"foo": {"foo-bar": "baz"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"baz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nested paths",
|
|
||||||
"Goodbye {{alan/expression}} world!",
|
|
||||||
map[string]map[string]string{"alan": {"expression": "beautiful"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Goodbye beautiful world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nested paths with empty string value",
|
|
||||||
"Goodbye {{alan/expression}} world!",
|
|
||||||
map[string]map[string]string{"alan": {"expression": ""}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Goodbye world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"literal paths (1)",
|
|
||||||
"Goodbye {{[@alan]/expression}} world!",
|
|
||||||
map[string]map[string]string{"@alan": {"expression": "beautiful"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Goodbye beautiful world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"literal paths (2)",
|
|
||||||
"Goodbye {{[foo bar]/expression}} world!",
|
|
||||||
map[string]map[string]string{"foo bar": {"expression": "beautiful"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Goodbye beautiful world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"literal references",
|
|
||||||
"Goodbye {{[foo bar]}} world!",
|
|
||||||
map[string]string{"foo bar": "beautiful"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Goodbye beautiful world!",
|
|
||||||
},
|
|
||||||
// @note MMm ok, well... no... I don't see the purpose of that test
|
|
||||||
{
|
|
||||||
"that current context path ({{.}}) doesn't hit helpers",
|
|
||||||
"test: {{.}}",
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"helper": func() string {
|
|
||||||
panic("fail")
|
|
||||||
return ""
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"test: ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"complex but empty paths (1)",
|
|
||||||
"{{person/name}}",
|
|
||||||
map[string]map[string]interface{}{"person": {"name": nil}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"complex but empty paths (2)",
|
|
||||||
"{{person/name}}",
|
|
||||||
map[string]map[string]string{"person": {}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"this keyword in paths (1)",
|
|
||||||
"{{#goodbyes}}{{this}}{{/goodbyes}}",
|
|
||||||
map[string]interface{}{"goodbyes": []string{"goodbye", "Goodbye", "GOODBYE"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"goodbyeGoodbyeGOODBYE",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"this keyword in paths (2)",
|
|
||||||
"{{#hellos}}{{this/text}}{{/hellos}}",
|
|
||||||
map[string]interface{}{"hellos": []interface{}{
|
|
||||||
map[string]string{"text": "hello"},
|
|
||||||
map[string]string{"text": "Hello"},
|
|
||||||
map[string]string{"text": "HELLO"},
|
|
||||||
}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"helloHelloHELLO",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"this keyword nested inside path' (1)",
|
|
||||||
"{{[this]}}",
|
|
||||||
map[string]string{"this": "bar"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"this keyword nested inside path' (2)",
|
|
||||||
"{{text/[this]}}",
|
|
||||||
map[string]map[string]string{"text": {"this": "bar"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"this keyword in helpers (1)",
|
|
||||||
"{{#goodbyes}}{{foo this}}{{/goodbyes}}",
|
|
||||||
map[string]interface{}{"goodbyes": []string{"goodbye", "Goodbye", "GOODBYE"}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"foo": barSuffixHelper},
|
|
||||||
nil,
|
|
||||||
"bar goodbyebar Goodbyebar GOODBYE",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"this keyword in helpers (2)",
|
|
||||||
"{{#hellos}}{{foo this/text}}{{/hellos}}",
|
|
||||||
map[string]interface{}{"hellos": []map[string]string{{"text": "hello"}, {"text": "Hello"}, {"text": "HELLO"}}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"foo": barSuffixHelper},
|
|
||||||
nil,
|
|
||||||
"bar hellobar Hellobar HELLO",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"this keyword nested inside helpers param (1)",
|
|
||||||
"{{foo [this]}}",
|
|
||||||
map[string]interface{}{"this": "bar"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"foo": echoHelper},
|
|
||||||
nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"this keyword nested inside helpers param (2)",
|
|
||||||
"{{foo text/[this]}}",
|
|
||||||
map[string]map[string]string{"text": {"this": "bar"}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"foo": echoHelper},
|
|
||||||
nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pass string literals (1)",
|
|
||||||
`{{"foo"}}`,
|
|
||||||
map[string]string{},
|
|
||||||
nil, nil, nil,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pass string literals (2)",
|
|
||||||
`{{"foo"}}`,
|
|
||||||
map[string]string{"foo": "bar"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pass string literals (3)",
|
|
||||||
`{{#"foo"}}{{.}}{{/"foo"}}`,
|
|
||||||
map[string]interface{}{"foo": []string{"bar", "baz"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"barbaz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pass number literals (1)",
|
|
||||||
"{{12}}",
|
|
||||||
map[string]string{},
|
|
||||||
nil, nil, nil,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pass number literals (2)",
|
|
||||||
"{{12}}",
|
|
||||||
map[string]string{"12": "bar"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pass number literals (3)",
|
|
||||||
"{{12.34}}",
|
|
||||||
map[string]string{},
|
|
||||||
nil, nil, nil,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pass number literals (4)",
|
|
||||||
"{{12.34}}",
|
|
||||||
map[string]string{"12.34": "bar"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pass number literals (5)",
|
|
||||||
"{{12.34 1}}",
|
|
||||||
map[string]interface{}{"12.34": func(context string) string {
|
|
||||||
return "bar" + context
|
|
||||||
}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pass boolean literals (1)",
|
|
||||||
"{{true}}",
|
|
||||||
map[string]string{},
|
|
||||||
nil, nil, nil,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pass boolean literals (2)",
|
|
||||||
"{{true}}",
|
|
||||||
map[string]string{"": "foo"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pass boolean literals (3)",
|
|
||||||
"{{false}}",
|
|
||||||
map[string]string{"false": "foo"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should handle literals in subexpression",
|
|
||||||
"{{foo (false)}}",
|
|
||||||
map[string]interface{}{"false": func() string { return "bar" }},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"foo": func(context string) string {
|
|
||||||
return context
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBasic(t *testing.T) {
|
|
||||||
launchTests(t, basicTests)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBasicErrors(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
inputs := []string{
|
|
||||||
// this keyword nested inside path
|
|
||||||
"{{#hellos}}{{text/this/foo}}{{/hellos}}",
|
|
||||||
// this keyword nested inside helpers param
|
|
||||||
"{{#hellos}}{{foo text/this/foo}}{{/hellos}}",
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedError := regexp.QuoteMeta("Invalid path: text/this")
|
|
||||||
|
|
||||||
for _, input := range inputs {
|
|
||||||
_, err = raymond.Parse(input)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Test failed - Error expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
match, errMatch := regexp.MatchString(expectedError, fmt.Sprint(err))
|
|
||||||
if errMatch != nil {
|
|
||||||
panic("Failed to match regexp")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !match {
|
|
||||||
t.Errorf("Test failed - Expected error:\n\t%s\n\nGot:\n\t%s", expectedError, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,208 +0,0 @@
|
||||||
package handlebars
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
//
|
|
||||||
// Those tests come from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/spec/blocks.js
|
|
||||||
//
|
|
||||||
var blocksTests = []Test{
|
|
||||||
{
|
|
||||||
"array (1) - Arrays iterate over the contents when not empty",
|
|
||||||
"{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"goodbye! Goodbye! GOODBYE! cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"array (2) - Arrays ignore the contents when empty",
|
|
||||||
"{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"array without data",
|
|
||||||
"{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"array with @index - The @index variable is used",
|
|
||||||
"{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"empty block (1) - Arrays iterate over the contents when not empty",
|
|
||||||
"{{#goodbyes}}{{/goodbyes}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"empty block (1) - Arrays ignore the contents when empty",
|
|
||||||
"{{#goodbyes}}{{/goodbyes}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block with complex lookup - Templates can access variables in contexts up the stack with relative path syntax",
|
|
||||||
"{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "name": "Alan"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"multiple blocks with complex lookup",
|
|
||||||
"{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "name": "Alan"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"AlanAlanAlanAlanAlanAlan",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}" should throw error
|
|
||||||
|
|
||||||
{
|
|
||||||
"block with deep nested complex lookup",
|
|
||||||
"{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}",
|
|
||||||
map[string]interface{}{"omg": "OMG!", "outer": []map[string]interface{}{{"sibling": "sad", "inner": []map[string]string{{"text": "goodbye"}}}}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Goodbye cruel sad OMG!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"inverted sections with unset value - Inverted section rendered when value isn't set.",
|
|
||||||
"{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}",
|
|
||||||
map[string]interface{}{},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Right On!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"inverted sections with false value - Inverted section rendered when value is false.",
|
|
||||||
"{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}",
|
|
||||||
map[string]interface{}{"goodbyes": false},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Right On!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"inverted section with empty set - Inverted section rendered when value is empty set.",
|
|
||||||
"{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}",
|
|
||||||
map[string]interface{}{"goodbyes": []interface{}{}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Right On!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block inverted sections",
|
|
||||||
"{{#people}}{{name}}{{^}}{{none}}{{/people}}",
|
|
||||||
map[string]interface{}{"none": "No people"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"No people",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"chained inverted sections (1)",
|
|
||||||
"{{#people}}{{name}}{{else if none}}{{none}}{{/people}}",
|
|
||||||
map[string]interface{}{"none": "No people"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"No people",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"chained inverted sections (2)",
|
|
||||||
"{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}",
|
|
||||||
map[string]interface{}{"none": "No people"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"No people",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"chained inverted sections (3)",
|
|
||||||
"{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}",
|
|
||||||
map[string]interface{}{"none": "No people"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"No people",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "{{#people}}{{name}}{{else if none}}{{none}}{{/if}}" should throw error
|
|
||||||
|
|
||||||
{
|
|
||||||
"block inverted sections with empty arrays",
|
|
||||||
"{{#people}}{{name}}{{^}}{{none}}{{/people}}",
|
|
||||||
map[string]interface{}{"none": "No people", "people": map[string]interface{}{}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"No people",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block standalone else sections (1)",
|
|
||||||
"{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n",
|
|
||||||
map[string]interface{}{"none": "No people"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"No people\n",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block standalone else sections (2)",
|
|
||||||
"{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n",
|
|
||||||
map[string]interface{}{"none": "No people"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"No people\n",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block standalone else sections (3)",
|
|
||||||
"{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n",
|
|
||||||
map[string]interface{}{"none": "No people"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"No people\n",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block standalone chained else sections (1)",
|
|
||||||
"{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n",
|
|
||||||
map[string]interface{}{"none": "No people"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"No people\n",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block standalone chained else sections (2)",
|
|
||||||
"{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n",
|
|
||||||
map[string]interface{}{"none": "No people"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"No people\n",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should handle nesting",
|
|
||||||
"{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.",
|
|
||||||
map[string]interface{}{"data": []int{1, 3, 5}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"1\n3\n5\nOK.",
|
|
||||||
},
|
|
||||||
// // @todo compat mode
|
|
||||||
// {
|
|
||||||
// "block with deep recursive lookup lookup",
|
|
||||||
// "{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}",
|
|
||||||
// map[string]interface{}{"omg": "OMG!", "outer": []map[string]interface{}{{"inner": []map[string]string{{"text": "goodbye"}}}}},
|
|
||||||
// nil,
|
|
||||||
// nil,
|
|
||||||
// nil,
|
|
||||||
// "Goodbye cruel OMG!",
|
|
||||||
// },
|
|
||||||
// // @todo compat mode
|
|
||||||
// {
|
|
||||||
// "block with deep recursive pathed lookup",
|
|
||||||
// "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}",
|
|
||||||
// map[string]interface{}{"omg": map[string]string{"yes": "OMG!"}, "outer": []map[string]interface{}{{"inner": []map[string]string{{"yes": "no", "text": "goodbye"}}}}},
|
|
||||||
// nil,
|
|
||||||
// nil,
|
|
||||||
// nil,
|
|
||||||
// "Goodbye cruel OMG!",
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
"block with missed recursive lookup",
|
|
||||||
"{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}",
|
|
||||||
map[string]interface{}{"omg": map[string]string{"no": "OMG!"}, "outer": []map[string]interface{}{{"inner": []map[string]string{{"yes": "no", "text": "goodbye"}}}}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Goodbye cruel ",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlocks(t *testing.T) {
|
|
||||||
launchTests(t, blocksTests)
|
|
||||||
}
|
|
|
@ -1,341 +0,0 @@
|
||||||
package handlebars
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
//
|
|
||||||
// Those tests come from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/spec/builtin.js
|
|
||||||
//
|
|
||||||
var builtinsTests = []Test{
|
|
||||||
{
|
|
||||||
"#if - if with boolean argument shows the contents when true",
|
|
||||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbye": true, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"GOODBYE cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if - if with string argument shows the contents",
|
|
||||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbye": "dummy", "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"GOODBYE cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if - if with boolean argument does not show the contents when false",
|
|
||||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbye": false, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if - if with undefined does not show the contents",
|
|
||||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if - if with non-empty array shows the contents",
|
|
||||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbye": []string{"foo"}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"GOODBYE cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if - if with empty array does not show the contents",
|
|
||||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbye": []string{}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if - if with zero does not show the contents",
|
|
||||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbye": 0, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if - if with zero and includeZero option shows the contents",
|
|
||||||
"{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbye": 0, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"GOODBYE cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if - if with function shows the contents when function returns true",
|
|
||||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{
|
|
||||||
"goodbye": func() bool { return true },
|
|
||||||
"world": "world",
|
|
||||||
},
|
|
||||||
nil, nil, nil,
|
|
||||||
"GOODBYE cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if - if with function shows the contents when function returns string",
|
|
||||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{
|
|
||||||
"goodbye": func() string { return "world" },
|
|
||||||
"world": "world",
|
|
||||||
},
|
|
||||||
nil, nil, nil,
|
|
||||||
"GOODBYE cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if - if with function does not show the contents when returns false",
|
|
||||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{
|
|
||||||
"goodbye": func() bool { return false },
|
|
||||||
"world": "world",
|
|
||||||
},
|
|
||||||
nil, nil, nil,
|
|
||||||
"cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if - if with function does not show the contents when returns undefined",
|
|
||||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{
|
|
||||||
"goodbye": func() interface{} { return nil },
|
|
||||||
"world": "world",
|
|
||||||
},
|
|
||||||
nil, nil, nil,
|
|
||||||
"cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#with",
|
|
||||||
"{{#with person}}{{first}} {{last}}{{/with}}",
|
|
||||||
map[string]interface{}{"person": map[string]string{"first": "Alan", "last": "Johnson"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Alan Johnson",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#with - with with function argument",
|
|
||||||
"{{#with person}}{{first}} {{last}}{{/with}}",
|
|
||||||
map[string]interface{}{
|
|
||||||
"person": func() map[string]string { return map[string]string{"first": "Alan", "last": "Johnson"} },
|
|
||||||
}, nil, nil, nil,
|
|
||||||
"Alan Johnson",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#with - with with else",
|
|
||||||
"{{#with person}}Person is present{{else}}Person is not present{{/with}}",
|
|
||||||
map[string]interface{}{},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Person is not present",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"#each - each with array argument iterates over the contents when not empty",
|
|
||||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"goodbye! Goodbye! GOODBYE! cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#each - each with array argument ignores the contents when empty",
|
|
||||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#each - each without data (1)",
|
|
||||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"goodbye! Goodbye! GOODBYE! cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#each - each without data (2)",
|
|
||||||
"{{#each .}}{{.}}{{/each}}",
|
|
||||||
map[string]interface{}{"goodbyes": "cruel", "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
// note: a go hash is not ordered, so result may vary, this behaviour differs from the JS implementation
|
|
||||||
[]string{"cruelworld", "worldcruel"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#each - each without context",
|
|
||||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"cruel !",
|
|
||||||
},
|
|
||||||
|
|
||||||
// NOTE: we test with a map instead of an object
|
|
||||||
{
|
|
||||||
"#each - each with an object and @key (map)",
|
|
||||||
"{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": map[interface{}]map[string]string{"<b>#1</b>": {"text": "goodbye"}, 2: {"text": "GOODBYE"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
[]string{"<b>#1</b>. goodbye! 2. GOODBYE! cruel world!", "2. GOODBYE! <b>#1</b>. goodbye! cruel world!"},
|
|
||||||
},
|
|
||||||
// NOTE: An additional test with a struct, but without an html stuff for the key, because it is impossible
|
|
||||||
{
|
|
||||||
"#each - each with an object and @key (struct)",
|
|
||||||
"{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{
|
|
||||||
"goodbyes": struct {
|
|
||||||
Foo map[string]string
|
|
||||||
Bar map[string]int
|
|
||||||
}{map[string]string{"text": "baz"}, map[string]int{"text": 10}},
|
|
||||||
"world": "world",
|
|
||||||
},
|
|
||||||
nil, nil, nil,
|
|
||||||
[]string{"Foo. baz! Bar. 10! cruel world!", "Bar. 10! Foo. baz! cruel world!"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#each - each with @index",
|
|
||||||
"{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#each - each with nested @index",
|
|
||||||
"{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#each - each with block params",
|
|
||||||
"{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!",
|
|
||||||
},
|
|
||||||
// @note: That test differs from JS impl because maps and structs are not ordered in go
|
|
||||||
{
|
|
||||||
"#each - each object with @index",
|
|
||||||
"{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": map[string]map[string]string{"a": {"text": "goodbye"}, "b": {"text": "Goodbye"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
[]string{"0. goodbye! 1. Goodbye! cruel world!", "0. Goodbye! 1. goodbye! cruel world!"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#each - each with nested @first",
|
|
||||||
"{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!",
|
|
||||||
},
|
|
||||||
// @note: That test differs from JS impl because maps and structs are not ordered in go
|
|
||||||
{
|
|
||||||
"#each - each object with @first",
|
|
||||||
"{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": map[string]map[string]string{"foo": {"text": "goodbye"}, "bar": {"text": "Goodbye"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
[]string{"goodbye! cruel world!", "Goodbye! cruel world!"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#each - each with @last",
|
|
||||||
"{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"GOODBYE! cruel world!",
|
|
||||||
},
|
|
||||||
// @note: That test differs from JS impl because maps and structs are not ordered in go
|
|
||||||
{
|
|
||||||
"#each - each object with @last",
|
|
||||||
"{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": map[string]map[string]string{"foo": {"text": "goodbye"}, "bar": {"text": "Goodbye"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
[]string{"goodbye! cruel world!", "Goodbye! cruel world!"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#each - each with nested @last",
|
|
||||||
"{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"#each - each with function argument (1)",
|
|
||||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": func() []map[string]string {
|
|
||||||
return []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}
|
|
||||||
}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"goodbye! Goodbye! GOODBYE! cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#each - each with function argument (2)",
|
|
||||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#each - data passed to helpers",
|
|
||||||
"{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}",
|
|
||||||
map[string][]string{"letters": {"a", "b", "c"}},
|
|
||||||
map[string]interface{}{"exclaim": "!"},
|
|
||||||
map[string]interface{}{"detectDataInsideEach": detectDataHelper},
|
|
||||||
nil,
|
|
||||||
"a!b!c!",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "each on implicit context" should throw error
|
|
||||||
|
|
||||||
// SKIP: #log - "should call logger at default level"
|
|
||||||
// SKIP: #log - "should call logger at data level"
|
|
||||||
// SKIP: #log - "should output to info"
|
|
||||||
// SKIP: #log - "should log at data level"
|
|
||||||
// SKIP: #log - "should handle missing logger"
|
|
||||||
|
|
||||||
// @note Test added
|
|
||||||
// @todo Check log output
|
|
||||||
{
|
|
||||||
"#log",
|
|
||||||
"{{log blah}}",
|
|
||||||
map[string]string{"blah": "whee"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @note Test added
|
|
||||||
{
|
|
||||||
"#lookup - should lookup array element",
|
|
||||||
"{{#each goodbyes}}{{lookup ../data @index}}{{/each}}",
|
|
||||||
map[string]interface{}{"goodbyes": []int{0, 1}, "data": []string{"foo", "bar"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"foobar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#lookup - should lookup map element",
|
|
||||||
"{{#each goodbyes}}{{lookup ../data .}}{{/each}}",
|
|
||||||
map[string]interface{}{"goodbyes": []string{"foo", "bar"}, "data": map[string]string{"foo": "baz", "bar": "bat"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bazbat",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#lookup - should lookup struct field",
|
|
||||||
"{{#each goodbyes}}{{lookup ../data .}}{{/each}}",
|
|
||||||
map[string]interface{}{"goodbyes": []string{"Foo", "Bar"}, "data": struct {
|
|
||||||
Foo string
|
|
||||||
Bar string
|
|
||||||
}{"baz", "bat"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bazbat",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#lookup - should lookup arbitrary content",
|
|
||||||
"{{#each goodbyes}}{{lookup ../data .}}{{/each}}",
|
|
||||||
map[string]interface{}{"goodbyes": []int{0, 1}, "data": []string{"foo", "bar"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"foobar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#lookup - should not fail on undefined value",
|
|
||||||
"{{#each goodbyes}}{{lookup ../bar .}}{{/each}}",
|
|
||||||
map[string]interface{}{"goodbyes": []int{0, 1}, "data": []string{"foo", "bar"}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuiltins(t *testing.T) {
|
|
||||||
launchTests(t, builtinsTests)
|
|
||||||
}
|
|
|
@ -1,300 +0,0 @@
|
||||||
package handlebars
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/aymerick/raymond"
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Those tests come from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/spec/data.js
|
|
||||||
//
|
|
||||||
var dataTests = []Test{
|
|
||||||
{
|
|
||||||
"passing in data to a compiled function that expects data - works with helpers",
|
|
||||||
"{{hello}}",
|
|
||||||
map[string]string{"noun": "cat"},
|
|
||||||
map[string]interface{}{"adjective": "happy"},
|
|
||||||
map[string]interface{}{"hello": func(options *raymond.Options) string {
|
|
||||||
return options.DataStr("adjective") + " " + options.ValueStr("noun")
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"happy cat",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data can be looked up via @foo",
|
|
||||||
"{{@hello}}",
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"hello": "hello"},
|
|
||||||
nil, nil,
|
|
||||||
"hello",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deep @foo triggers automatic top-level data",
|
|
||||||
`{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}`,
|
|
||||||
map[string]bool{"foo": true},
|
|
||||||
map[string]interface{}{"hello": "hello"},
|
|
||||||
map[string]interface{}{"let": func(options *raymond.Options) string {
|
|
||||||
frame := options.NewDataFrame()
|
|
||||||
|
|
||||||
for k, v := range options.Hash() {
|
|
||||||
frame.Set(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return options.FnData(frame)
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"Hello world",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameter data can be looked up via @foo",
|
|
||||||
`{{hello @world}}`,
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"world": "world"},
|
|
||||||
map[string]interface{}{"hello": func(context string) string {
|
|
||||||
return "Hello " + context
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"Hello world",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hash values can be looked up via @foo",
|
|
||||||
`{{hello noun=@world}}`,
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"world": "world"},
|
|
||||||
map[string]interface{}{"hello": func(options *raymond.Options) string {
|
|
||||||
return "Hello " + options.HashStr("noun")
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"Hello world",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nested parameter data can be looked up via @foo.bar",
|
|
||||||
`{{hello @world.bar}}`,
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"world": map[string]string{"bar": "world"}},
|
|
||||||
map[string]interface{}{"hello": func(context string) string {
|
|
||||||
return "Hello " + context
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"Hello world",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nested parameter data does not fail with @world.bar",
|
|
||||||
`{{hello @world.bar}}`,
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"foo": map[string]string{"bar": "world"}},
|
|
||||||
map[string]interface{}{"hello": func(context string) string {
|
|
||||||
return "Hello " + context
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
// @todo Test differs with JS implementation: we don't output `undefined`
|
|
||||||
"Hello ",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "parameter data throws when using complex scope references",
|
|
||||||
|
|
||||||
{
|
|
||||||
"data can be functions",
|
|
||||||
`{{@hello}}`,
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"hello": func() string { return "hello" }},
|
|
||||||
nil, nil,
|
|
||||||
"hello",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data can be functions with params",
|
|
||||||
`{{@hello "hello"}}`,
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"hello": func(context string) string { return context }},
|
|
||||||
nil, nil,
|
|
||||||
"hello",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"data is inherited downstream",
|
|
||||||
`{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}`,
|
|
||||||
map[string]map[string]string{"bar": {"baz": "hello world"}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"let": func(options *raymond.Options) string {
|
|
||||||
frame := options.NewDataFrame()
|
|
||||||
|
|
||||||
for k, v := range options.Hash() {
|
|
||||||
frame.Set(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return options.FnData(frame)
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"2hello world1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"passing in data to a compiled function that expects data - works with helpers in partials",
|
|
||||||
`{{>myPartial}}`,
|
|
||||||
map[string]string{"noun": "cat"},
|
|
||||||
map[string]interface{}{"adjective": "happy"},
|
|
||||||
map[string]interface{}{"hello": func(options *raymond.Options) string {
|
|
||||||
return options.DataStr("adjective") + " " + options.ValueStr("noun")
|
|
||||||
}},
|
|
||||||
map[string]string{
|
|
||||||
"myPartial": "{{hello}}",
|
|
||||||
},
|
|
||||||
"happy cat",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"passing in data to a compiled function that expects data - works with helpers and parameters",
|
|
||||||
`{{hello world}}`,
|
|
||||||
map[string]interface{}{"exclaim": true, "world": "world"},
|
|
||||||
map[string]interface{}{"adjective": "happy"},
|
|
||||||
map[string]interface{}{"hello": func(context string, options *raymond.Options) string {
|
|
||||||
str := "error"
|
|
||||||
if b, ok := options.Value("exclaim").(bool); ok {
|
|
||||||
if b {
|
|
||||||
str = "!"
|
|
||||||
} else {
|
|
||||||
str = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options.DataStr("adjective") + " " + context + str
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"happy world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"passing in data to a compiled function that expects data - works with block helpers",
|
|
||||||
`{{#hello}}{{world}}{{/hello}}`,
|
|
||||||
map[string]bool{"exclaim": true},
|
|
||||||
map[string]interface{}{"adjective": "happy"},
|
|
||||||
map[string]interface{}{
|
|
||||||
"hello": func(options *raymond.Options) string {
|
|
||||||
return options.Fn()
|
|
||||||
},
|
|
||||||
"world": func(options *raymond.Options) string {
|
|
||||||
str := "error"
|
|
||||||
if b, ok := options.Value("exclaim").(bool); ok {
|
|
||||||
if b {
|
|
||||||
str = "!"
|
|
||||||
} else {
|
|
||||||
str = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options.DataStr("adjective") + " world" + str
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"happy world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"passing in data to a compiled function that expects data - works with block helpers that use ..",
|
|
||||||
`{{#hello}}{{world ../zomg}}{{/hello}}`,
|
|
||||||
map[string]interface{}{"exclaim": true, "zomg": "world"},
|
|
||||||
map[string]interface{}{"adjective": "happy"},
|
|
||||||
map[string]interface{}{
|
|
||||||
"hello": func(options *raymond.Options) string {
|
|
||||||
return options.FnWith(map[string]string{"exclaim": "?"})
|
|
||||||
},
|
|
||||||
"world": func(context string, options *raymond.Options) string {
|
|
||||||
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"happy world?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..",
|
|
||||||
`{{#hello}}{{world ../zomg}}{{/hello}}`,
|
|
||||||
map[string]interface{}{"exclaim": true, "zomg": "world"},
|
|
||||||
map[string]interface{}{"adjective": "happy", "accessData": "#win"},
|
|
||||||
map[string]interface{}{
|
|
||||||
"hello": func(options *raymond.Options) string {
|
|
||||||
return options.DataStr("accessData") + " " + options.FnWith(map[string]string{"exclaim": "?"})
|
|
||||||
},
|
|
||||||
"world": func(context string, options *raymond.Options) string {
|
|
||||||
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"#win happy world?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"you can override inherited data when invoking a helper",
|
|
||||||
`{{#hello}}{{world zomg}}{{/hello}}`,
|
|
||||||
map[string]interface{}{"exclaim": true, "zomg": "planet"},
|
|
||||||
map[string]interface{}{"adjective": "happy"},
|
|
||||||
map[string]interface{}{
|
|
||||||
"hello": func(options *raymond.Options) string {
|
|
||||||
ctx := map[string]string{"exclaim": "?", "zomg": "world"}
|
|
||||||
data := options.NewDataFrame()
|
|
||||||
data.Set("adjective", "sad")
|
|
||||||
|
|
||||||
return options.FnCtxData(ctx, data)
|
|
||||||
},
|
|
||||||
"world": func(context string, options *raymond.Options) string {
|
|
||||||
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"sad world?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"you can override inherited data when invoking a helper with depth",
|
|
||||||
`{{#hello}}{{world ../zomg}}{{/hello}}`,
|
|
||||||
map[string]interface{}{"exclaim": true, "zomg": "world"},
|
|
||||||
map[string]interface{}{"adjective": "happy"},
|
|
||||||
map[string]interface{}{
|
|
||||||
"hello": func(options *raymond.Options) string {
|
|
||||||
ctx := map[string]string{"exclaim": "?"}
|
|
||||||
data := options.NewDataFrame()
|
|
||||||
data.Set("adjective", "sad")
|
|
||||||
|
|
||||||
return options.FnCtxData(ctx, data)
|
|
||||||
},
|
|
||||||
"world": func(context string, options *raymond.Options) string {
|
|
||||||
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"sad world?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@root - the root context can be looked up via @root",
|
|
||||||
`{{@root.foo}}`,
|
|
||||||
map[string]interface{}{"foo": "hello"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"hello",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@root - passed root values take priority",
|
|
||||||
`{{@root.foo}}`,
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"root": map[string]string{"foo": "hello"}},
|
|
||||||
nil, nil,
|
|
||||||
"hello",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nesting - the root context can be looked up via @root",
|
|
||||||
`{{#helper}}{{#helper}}{{@./depth}} {{@../depth}} {{@../../depth}}{{/helper}}{{/helper}}`,
|
|
||||||
map[string]interface{}{"foo": "hello"},
|
|
||||||
map[string]interface{}{"depth": 0},
|
|
||||||
map[string]interface{}{
|
|
||||||
"helper": func(options *raymond.Options) string {
|
|
||||||
data := options.NewDataFrame()
|
|
||||||
|
|
||||||
if depth, ok := options.Data("depth").(int); ok {
|
|
||||||
data.Set("depth", depth+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return options.FnData(data)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"2 1 0",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestData(t *testing.T) {
|
|
||||||
launchTests(t, dataTests)
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
// Package handlebars contains all the tests that come from handlebars.js project.
|
|
||||||
package handlebars
|
|
|
@ -1,670 +0,0 @@
|
||||||
package handlebars
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/aymerick/raymond"
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Helpers
|
|
||||||
//
|
|
||||||
|
|
||||||
func barSuffixHelper(context string) string {
|
|
||||||
return "bar " + context
|
|
||||||
}
|
|
||||||
|
|
||||||
func echoHelper(str string) string {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
func echoNbHelper(str string, nb int) string {
|
|
||||||
result := ""
|
|
||||||
for i := 0; i < nb; i++ {
|
|
||||||
result += str
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func linkHelper(prefix string, options *raymond.Options) string {
|
|
||||||
return fmt.Sprintf(`<a href="%s/%s">%s</a>`, prefix, options.ValueStr("url"), options.ValueStr("text"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func rawHelper(options *raymond.Options) string {
|
|
||||||
return options.Fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
func rawThreeHelper(a, b, c string, options *raymond.Options) string {
|
|
||||||
return options.Fn() + a + b + c
|
|
||||||
}
|
|
||||||
|
|
||||||
func formHelper(options *raymond.Options) string {
|
|
||||||
return "<form>" + options.Fn() + "</form>"
|
|
||||||
}
|
|
||||||
|
|
||||||
func formCtxHelper(context interface{}, options *raymond.Options) string {
|
|
||||||
return "<form>" + options.FnWith(context) + "</form>"
|
|
||||||
}
|
|
||||||
|
|
||||||
func listHelper(context interface{}, options *raymond.Options) string {
|
|
||||||
val := reflect.ValueOf(context)
|
|
||||||
switch val.Kind() {
|
|
||||||
case reflect.Array, reflect.Slice:
|
|
||||||
if val.Len() > 0 {
|
|
||||||
result := "<ul>"
|
|
||||||
for i := 0; i < val.Len(); i++ {
|
|
||||||
result += "<li>"
|
|
||||||
result += options.FnWith(val.Index(i).Interface())
|
|
||||||
result += "</li>"
|
|
||||||
}
|
|
||||||
result += "</ul>"
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "<p>" + options.Inverse() + "</p>"
|
|
||||||
}
|
|
||||||
|
|
||||||
func blogHelper(val string) string {
|
|
||||||
return "val is " + val
|
|
||||||
}
|
|
||||||
|
|
||||||
func equalHelper(a, b string) string {
|
|
||||||
return raymond.Str(a == b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func dashHelper(a, b string) string {
|
|
||||||
return a + "-" + b
|
|
||||||
}
|
|
||||||
|
|
||||||
func concatHelper(a, b string) string {
|
|
||||||
return a + b
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectDataHelper(options *raymond.Options) string {
|
|
||||||
if val, ok := options.DataFrame().Get("exclaim").(string); ok {
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Those tests come from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/spec/helper.js
|
|
||||||
//
|
|
||||||
var helpersTests = []Test{
|
|
||||||
{
|
|
||||||
"helper with complex lookup",
|
|
||||||
"{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}",
|
|
||||||
map[string]interface{}{"prefix": "/root", "goodbyes": []map[string]string{{"text": "Goodbye", "url": "goodbye"}}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"link": linkHelper},
|
|
||||||
nil,
|
|
||||||
`<a href="/root/goodbye">Goodbye</a>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helper for raw block gets raw content",
|
|
||||||
"{{{{raw}}}} {{test}} {{{{/raw}}}}",
|
|
||||||
map[string]interface{}{"test": "hello"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"raw": rawHelper},
|
|
||||||
nil,
|
|
||||||
" {{test}} ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helper for raw block gets parameters",
|
|
||||||
"{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}",
|
|
||||||
map[string]interface{}{"test": "hello"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"raw": rawThreeHelper},
|
|
||||||
nil,
|
|
||||||
" {{test}} 123",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helper block with complex lookup expression",
|
|
||||||
"{{#goodbyes}}{{../name}}{{/goodbyes}}",
|
|
||||||
map[string]interface{}{"name": "Alan"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"goodbyes": func(options *raymond.Options) string {
|
|
||||||
out := ""
|
|
||||||
for _, str := range []string{"Goodbye", "goodbye", "GOODBYE"} {
|
|
||||||
out += str + " " + options.FnWith(str) + "! "
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"Goodbye Alan! goodbye Alan! GOODBYE Alan! ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helper with complex lookup and nested template",
|
|
||||||
"{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}",
|
|
||||||
map[string]interface{}{"prefix": "/root", "goodbyes": []map[string]string{{"text": "Goodbye", "url": "goodbye"}}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"link": linkHelper},
|
|
||||||
nil,
|
|
||||||
`<a href="/root/goodbye">Goodbye</a>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// note: The JS implementation returns undefined, we return empty string
|
|
||||||
"helper returning undefined value (1)",
|
|
||||||
" {{nothere}}",
|
|
||||||
map[string]interface{}{},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"nothere": func() string {
|
|
||||||
return ""
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
" ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// note: The JS implementation returns undefined, we return empty string
|
|
||||||
"helper returning undefined value (2)",
|
|
||||||
" {{#nothere}}{{/nothere}}",
|
|
||||||
map[string]interface{}{},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"nothere": func() string {
|
|
||||||
return ""
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
" ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helper",
|
|
||||||
"{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!",
|
|
||||||
map[string]interface{}{"world": "world"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"goodbyes": func(options *raymond.Options) string {
|
|
||||||
return options.FnWith(map[string]string{"text": "GOODBYE"})
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"GOODBYE! cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helper staying in the same context",
|
|
||||||
"{{#form}}<p>{{name}}</p>{{/form}}",
|
|
||||||
map[string]interface{}{"name": "Yehuda"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"form": formHelper},
|
|
||||||
nil,
|
|
||||||
"<form><p>Yehuda</p></form>",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helper should have context in this",
|
|
||||||
"<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>",
|
|
||||||
map[string]interface{}{"people": []map[string]interface{}{{"name": "Alan", "id": 1}, {"name": "Yehuda", "id": 2}}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"link": func(options *raymond.Options) string {
|
|
||||||
return fmt.Sprintf("<a href=\"/people/%s\">%s</a>", options.ValueStr("id"), options.Fn())
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
`<ul><li><a href="/people/1">Alan</a></li><li><a href="/people/2">Yehuda</a></li></ul>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helper for undefined value",
|
|
||||||
"{{#empty}}shouldn't render{{/empty}}",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helper passing a new context",
|
|
||||||
"{{#form yehuda}}<p>{{name}}</p>{{/form}}",
|
|
||||||
map[string]map[string]string{"yehuda": {"name": "Yehuda"}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"form": formCtxHelper},
|
|
||||||
nil,
|
|
||||||
"<form><p>Yehuda</p></form>",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helper passing a complex path context",
|
|
||||||
"{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}",
|
|
||||||
map[string]map[string]interface{}{"yehuda": {"name": "Yehuda", "cat": map[string]string{"name": "Harold"}}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"form": formCtxHelper},
|
|
||||||
nil,
|
|
||||||
"<form><p>Harold</p></form>",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nested block helpers",
|
|
||||||
"{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}",
|
|
||||||
map[string]map[string]string{"yehuda": {"name": "Yehuda"}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"link": func(options *raymond.Options) string {
|
|
||||||
return fmt.Sprintf("<a href=\"%s\">%s</a>", options.ValueStr("name"), options.Fn())
|
|
||||||
}, "form": formCtxHelper},
|
|
||||||
nil,
|
|
||||||
`<form><p>Yehuda</p><a href="Yehuda">Hello</a></form>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helper inverted sections (1) - an inverse wrapper is passed in as a new context",
|
|
||||||
"{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}",
|
|
||||||
map[string][]map[string]string{"people": {{"name": "Alan"}, {"name": "Yehuda"}}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"list": listHelper},
|
|
||||||
nil,
|
|
||||||
`<ul><li>Alan</li><li>Yehuda</li></ul>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helper inverted sections (2) - an inverse wrapper can be optionally called",
|
|
||||||
"{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}",
|
|
||||||
map[string][]map[string]string{"people": {}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"list": listHelper},
|
|
||||||
nil,
|
|
||||||
`<p><em>Nobody's here</em></p>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helper inverted sections (3) - the context of an inverse is the parent of the block",
|
|
||||||
"{{#list people}}Hello{{^}}{{message}}{{/list}}",
|
|
||||||
map[string]interface{}{"people": []interface{}{}, "message": "Nobody's here"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"list": listHelper},
|
|
||||||
nil,
|
|
||||||
`<p>Nobody's here</p>`,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"pathed lambdas with parameters (1)",
|
|
||||||
"{{./helper 1}}",
|
|
||||||
map[string]interface{}{
|
|
||||||
"helper": func(param int) string { return "winning" },
|
|
||||||
"hash": map[string]interface{}{
|
|
||||||
"helper": func(param int) string { return "winning" },
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"./helper": func(param int) string { return "fail" }},
|
|
||||||
nil,
|
|
||||||
"winning",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pathed lambdas with parameters (2)",
|
|
||||||
"{{hash/helper 1}}",
|
|
||||||
map[string]interface{}{
|
|
||||||
"helper": func(param int) string { return "winning" },
|
|
||||||
"hash": map[string]interface{}{
|
|
||||||
"helper": func(param int) string { return "winning" },
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"./helper": func(param int) string { return "fail" }},
|
|
||||||
nil,
|
|
||||||
"winning",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"helpers hash - providing a helpers hash (1)",
|
|
||||||
"Goodbye {{cruel}} {{world}}!",
|
|
||||||
map[string]interface{}{"cruel": "cruel"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"world": func() string { return "world" }},
|
|
||||||
nil,
|
|
||||||
"Goodbye cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helpers hash - providing a helpers hash (2)",
|
|
||||||
"Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!",
|
|
||||||
map[string]interface{}{"iter": []map[string]string{{"cruel": "cruel"}}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"world": func() string { return "world" }},
|
|
||||||
nil,
|
|
||||||
"Goodbye cruel world!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helpers hash - in cases of conflict, helpers win (1)",
|
|
||||||
"{{{lookup}}}",
|
|
||||||
map[string]interface{}{"lookup": "Explicit"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"lookup": func() string { return "helpers" }},
|
|
||||||
nil,
|
|
||||||
"helpers",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helpers hash - in cases of conflict, helpers win (2)",
|
|
||||||
"{{lookup}}",
|
|
||||||
map[string]interface{}{"lookup": "Explicit"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"lookup": func() string { return "helpers" }},
|
|
||||||
nil,
|
|
||||||
"helpers",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helpers hash - the helpers hash is available is nested contexts",
|
|
||||||
"{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}",
|
|
||||||
map[string]interface{}{"outer": map[string]interface{}{"inner": map[string]interface{}{"unused": []string{}}}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"helper": func() string { return "helper" }},
|
|
||||||
nil,
|
|
||||||
"helper",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "helpers hash - the helper hash should augment the global hash"
|
|
||||||
|
|
||||||
// @todo "registration"
|
|
||||||
|
|
||||||
{
|
|
||||||
"decimal number literals work",
|
|
||||||
"Message: {{hello -1.2 1.2}}",
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"hello": func(times, times2 interface{}) string {
|
|
||||||
ts, t2s := "NaN", "NaN"
|
|
||||||
|
|
||||||
if v, ok := times.(float64); ok {
|
|
||||||
ts = raymond.Str(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := times2.(float64); ok {
|
|
||||||
t2s = raymond.Str(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Hello " + ts + " " + t2s + " times"
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"Message: Hello -1.2 1.2 times",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"negative number literals work",
|
|
||||||
"Message: {{hello -12}}",
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"hello": func(times interface{}) string {
|
|
||||||
ts := "NaN"
|
|
||||||
|
|
||||||
if v, ok := times.(int); ok {
|
|
||||||
ts = raymond.Str(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Hello " + ts + " times"
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"Message: Hello -12 times",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"String literal parameters - simple literals work",
|
|
||||||
`Message: {{hello "world" 12 true false}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"hello": func(p, t, b, b2 interface{}) string {
|
|
||||||
times, bool1, bool2 := "NaN", "NaB", "NaB"
|
|
||||||
|
|
||||||
param, ok := p.(string)
|
|
||||||
if !ok {
|
|
||||||
param = "NaN"
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := t.(int); ok {
|
|
||||||
times = raymond.Str(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := b.(bool); ok {
|
|
||||||
bool1 = raymond.Str(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := b2.(bool); ok {
|
|
||||||
bool2 = raymond.Str(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"Message: Hello world 12 times: true false",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "using a quote in the middle of a parameter raises an error"
|
|
||||||
|
|
||||||
{
|
|
||||||
"String literal parameters - escaping a String is possible",
|
|
||||||
"Message: {{{hello \"\\\"world\\\"\"}}}",
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"hello": func(param string) string {
|
|
||||||
return "Hello " + param
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
`Message: Hello "world"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"String literal parameters - it works with ' marks",
|
|
||||||
"Message: {{{hello \"Alan's world\"}}}",
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"hello": func(param string) string {
|
|
||||||
return "Hello " + param
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
`Message: Hello Alan's world`,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"multiple parameters - simple multi-params work",
|
|
||||||
"Message: {{goodbye cruel world}}",
|
|
||||||
map[string]string{"cruel": "cruel", "world": "world"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"goodbye": func(cruel, world string) string {
|
|
||||||
return "Goodbye " + cruel + " " + world
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"Message: Goodbye cruel world",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"multiple parameters - block multi-params work",
|
|
||||||
"Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}",
|
|
||||||
map[string]string{"cruel": "cruel", "world": "world"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"goodbye": func(cruel, world string, options *raymond.Options) string {
|
|
||||||
return options.FnWith(map[string]interface{}{"greeting": "Goodbye", "adj": cruel, "noun": world})
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"Message: Goodbye cruel world",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"hash - helpers can take an optional hash",
|
|
||||||
`{{goodbye cruel="CRUEL" world="WORLD" times=12}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
|
||||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.HashStr("world") + " " + options.HashStr("times") + " TIMES"
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"GOODBYE CRUEL WORLD 12 TIMES",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hash - helpers can take an optional hash with booleans (1)",
|
|
||||||
`{{goodbye cruel="CRUEL" world="WORLD" print=true}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
|
||||||
p, ok := options.HashProp("print").(bool)
|
|
||||||
if ok {
|
|
||||||
if p {
|
|
||||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.HashStr("world")
|
|
||||||
} else {
|
|
||||||
return "NOT PRINTING"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "THIS SHOULD NOT HAPPEN"
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"GOODBYE CRUEL WORLD",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hash - helpers can take an optional hash with booleans (2)",
|
|
||||||
`{{goodbye cruel="CRUEL" world="WORLD" print=false}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
|
||||||
p, ok := options.HashProp("print").(bool)
|
|
||||||
if ok {
|
|
||||||
if p {
|
|
||||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.HashStr("world")
|
|
||||||
} else {
|
|
||||||
return "NOT PRINTING"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "THIS SHOULD NOT HAPPEN"
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"NOT PRINTING",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helpers can take an optional hash",
|
|
||||||
`{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
|
||||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn() + " " + options.HashStr("times") + " TIMES"
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"GOODBYE CRUEL world 12 TIMES",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helpers can take an optional hash with single quoted stings",
|
|
||||||
`{{#goodbye cruel='CRUEL' times=12}}world{{/goodbye}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
|
||||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn() + " " + options.HashStr("times") + " TIMES"
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"GOODBYE CRUEL world 12 TIMES",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helpers can take an optional hash with booleans (1)",
|
|
||||||
`{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
|
||||||
p, ok := options.HashProp("print").(bool)
|
|
||||||
if ok {
|
|
||||||
if p {
|
|
||||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn()
|
|
||||||
} else {
|
|
||||||
return "NOT PRINTING"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "THIS SHOULD NOT HAPPEN"
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"GOODBYE CRUEL world",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block helpers can take an optional hash with booleans (1)",
|
|
||||||
`{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
|
||||||
p, ok := options.HashProp("print").(bool)
|
|
||||||
if ok {
|
|
||||||
if p {
|
|
||||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn()
|
|
||||||
} else {
|
|
||||||
return "NOT PRINTING"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "THIS SHOULD NOT HAPPEN"
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"NOT PRINTING",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "helperMissing - if a context is not found, helperMissing is used" throw error
|
|
||||||
|
|
||||||
// @todo "helperMissing - if a context is not found, custom helperMissing is used"
|
|
||||||
|
|
||||||
// @todo "helperMissing - if a value is not found, custom helperMissing is used"
|
|
||||||
|
|
||||||
{
|
|
||||||
"block helpers can take an optional hash with booleans (1)",
|
|
||||||
`{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
|
||||||
p, ok := options.HashProp("print").(bool)
|
|
||||||
if ok {
|
|
||||||
if p {
|
|
||||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn()
|
|
||||||
} else {
|
|
||||||
return "NOT PRINTING"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "THIS SHOULD NOT HAPPEN"
|
|
||||||
}},
|
|
||||||
nil,
|
|
||||||
"NOT PRINTING",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "knownHelpers/knownHelpersOnly" tests
|
|
||||||
|
|
||||||
// @todo "blockHelperMissing" tests
|
|
||||||
|
|
||||||
// @todo "name field" tests
|
|
||||||
|
|
||||||
{
|
|
||||||
"name conflicts - helpers take precedence over same-named context properties",
|
|
||||||
`{{goodbye}} {{cruel world}}`,
|
|
||||||
map[string]string{"goodbye": "goodbye", "world": "world"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"goodbye": func(options *raymond.Options) string {
|
|
||||||
return strings.ToUpper(options.ValueStr("goodbye"))
|
|
||||||
},
|
|
||||||
"cruel": func(world string) string {
|
|
||||||
return "cruel " + strings.ToUpper(world)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"GOODBYE cruel WORLD",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name conflicts - helpers take precedence over same-named context properties",
|
|
||||||
`{{#goodbye}} {{cruel world}}{{/goodbye}}`,
|
|
||||||
map[string]string{"goodbye": "goodbye", "world": "world"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"goodbye": func(options *raymond.Options) string {
|
|
||||||
return strings.ToUpper(options.ValueStr("goodbye")) + options.Fn()
|
|
||||||
},
|
|
||||||
"cruel": func(world string) string {
|
|
||||||
return "cruel " + strings.ToUpper(world)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"GOODBYE cruel WORLD",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name conflicts - Scoped names take precedence over helpers",
|
|
||||||
`{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}`,
|
|
||||||
map[string]string{"goodbye": "goodbye", "world": "world"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"goodbye": func(options *raymond.Options) string {
|
|
||||||
return strings.ToUpper(options.ValueStr("goodbye"))
|
|
||||||
},
|
|
||||||
"cruel": func(world string) string {
|
|
||||||
return "cruel " + strings.ToUpper(world)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"goodbye cruel WORLD cruel GOODBYE",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name conflicts - Scoped names take precedence over block helpers",
|
|
||||||
`{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}`,
|
|
||||||
map[string]string{"goodbye": "goodbye", "world": "world"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"goodbye": func(options *raymond.Options) string {
|
|
||||||
return strings.ToUpper(options.ValueStr("goodbye")) + options.Fn()
|
|
||||||
},
|
|
||||||
"cruel": func(world string) string {
|
|
||||||
return "cruel " + strings.ToUpper(world)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"GOODBYE cruel WORLD goodbye",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "block params" tests
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHelpers(t *testing.T) {
|
|
||||||
launchTests(t, helpersTests)
|
|
||||||
}
|
|
|
@ -1,182 +0,0 @@
|
||||||
package handlebars
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
//
|
|
||||||
// Those tests come from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/spec/partials.js
|
|
||||||
//
|
|
||||||
var partialsTests = []Test{
|
|
||||||
{
|
|
||||||
"basic partials",
|
|
||||||
"Dudes: {{#dudes}}{{> dude}}{{/dudes}}",
|
|
||||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"dude": "{{name}} ({{url}}) "},
|
|
||||||
"Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dynamic partials",
|
|
||||||
"Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}",
|
|
||||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"partial": func() string {
|
|
||||||
return "dude"
|
|
||||||
}},
|
|
||||||
map[string]string{"dude": "{{name}} ({{url}}) "},
|
|
||||||
"Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "failing dynamic partials"
|
|
||||||
|
|
||||||
{
|
|
||||||
"partials with context",
|
|
||||||
"Dudes: {{>dude dudes}}",
|
|
||||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"dude": "{{#this}}{{name}} ({{url}}) {{/this}}"},
|
|
||||||
"Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"partials with undefined context",
|
|
||||||
"Dudes: {{>dude dudes}}",
|
|
||||||
map[string]interface{}{},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"dude": "{{foo}} Empty"},
|
|
||||||
"Dudes: Empty",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "partials with duplicate parameters"
|
|
||||||
|
|
||||||
{
|
|
||||||
"partials with parameters",
|
|
||||||
"Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}",
|
|
||||||
map[string]interface{}{"foo": "bar", "dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"dude": "{{others.foo}}{{name}} ({{url}}) "},
|
|
||||||
"Dudes: barYehuda (http://yehuda) barAlan (http://alan) ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"partial in a partial",
|
|
||||||
"Dudes: {{#dudes}}{{>dude}}{{/dudes}}",
|
|
||||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"dude": "{{name}} {{> url}} ", "url": `<a href="{{url}}">{{url}}</a>`},
|
|
||||||
`Dudes: Yehuda <a href="http://yehuda">http://yehuda</a> Alan <a href="http://alan">http://alan</a> `,
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "rendering undefined partial throws an exception"
|
|
||||||
|
|
||||||
// @todo "registering undefined partial throws an exception"
|
|
||||||
|
|
||||||
// SKIP: "rendering template partial in vm mode throws an exception"
|
|
||||||
// SKIP: "rendering function partial in vm mode"
|
|
||||||
|
|
||||||
{
|
|
||||||
"GH-14: a partial preceding a selector",
|
|
||||||
"Dudes: {{>dude}} {{anotherDude}}",
|
|
||||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"dude": "{{name}}"},
|
|
||||||
"Dudes: Jeepers Creepers",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Partials with slash paths",
|
|
||||||
"Dudes: {{> shared/dude}}",
|
|
||||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"shared/dude": "{{name}}"},
|
|
||||||
"Dudes: Jeepers",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Partials with slash and point paths",
|
|
||||||
"Dudes: {{> shared/dude.thing}}",
|
|
||||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"shared/dude.thing": "{{name}}"},
|
|
||||||
"Dudes: Jeepers",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "Global Partials"
|
|
||||||
|
|
||||||
// @todo "Multiple partial registration"
|
|
||||||
|
|
||||||
{
|
|
||||||
"Partials with integer path",
|
|
||||||
"Dudes: {{> 404}}",
|
|
||||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"404": "{{name}}"}, // @note Difference with JS test: partial name is a string
|
|
||||||
"Dudes: Jeepers",
|
|
||||||
},
|
|
||||||
// @note This is not supported by our implementation. But really... who cares ?
|
|
||||||
// {
|
|
||||||
// "Partials with complex path",
|
|
||||||
// "Dudes: {{> 404/asdf?.bar}}",
|
|
||||||
// map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
|
||||||
// nil, nil,
|
|
||||||
// map[string]string{"404/asdf?.bar": "{{name}}"},
|
|
||||||
// "Dudes: Jeepers",
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
"Partials with escaped",
|
|
||||||
"Dudes: {{> [+404/asdf?.bar]}}",
|
|
||||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"+404/asdf?.bar": "{{name}}"},
|
|
||||||
"Dudes: Jeepers",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Partials with string",
|
|
||||||
"Dudes: {{> '+404/asdf?.bar'}}",
|
|
||||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"+404/asdf?.bar": "{{name}}"},
|
|
||||||
"Dudes: Jeepers",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should handle empty partial",
|
|
||||||
"Dudes: {{#dudes}}{{> dude}}{{/dudes}}",
|
|
||||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"dude": ""},
|
|
||||||
"Dudes: ",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "throw on missing partial"
|
|
||||||
|
|
||||||
// SKIP: "should pass compiler flags"
|
|
||||||
|
|
||||||
{
|
|
||||||
"standalone partials (1) - indented partials",
|
|
||||||
"Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}",
|
|
||||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"dude": "{{name}}\n"},
|
|
||||||
"Dudes:\n Yehuda\n Alan\n",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"standalone partials (2) - nested indented partials",
|
|
||||||
"Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}",
|
|
||||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
|
||||||
nil, nil,
|
|
||||||
map[string]string{"dude": "{{name}}\n {{> url}}", "url": "{{url}}!\n"},
|
|
||||||
"Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n",
|
|
||||||
},
|
|
||||||
|
|
||||||
// // @todo preventIndent option
|
|
||||||
// {
|
|
||||||
// "standalone partials (3) - prevent nested indented partials",
|
|
||||||
// "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}",
|
|
||||||
// map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
|
||||||
// nil, nil,
|
|
||||||
// map[string]string{"dude": "{{name}}\n {{> url}}", "url": "{{url}}!\n"},
|
|
||||||
// "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n",
|
|
||||||
// },
|
|
||||||
|
|
||||||
// @todo "compat mode"
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPartials(t *testing.T) {
|
|
||||||
launchTests(t, partialsTests)
|
|
||||||
}
|
|
|
@ -1,209 +0,0 @@
|
||||||
package handlebars
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/aymerick/raymond"
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Those tests come from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/spec/subexpression.js
|
|
||||||
//
|
|
||||||
var subexpressionsTests = []Test{
|
|
||||||
{
|
|
||||||
"arg-less helper",
|
|
||||||
"{{foo (bar)}}!",
|
|
||||||
map[string]interface{}{},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"foo": func(val string) string {
|
|
||||||
return val + val
|
|
||||||
},
|
|
||||||
"bar": func() string {
|
|
||||||
return "LOL"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"LOLLOL!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helper w args",
|
|
||||||
"{{blog (equal a b)}}",
|
|
||||||
map[string]interface{}{"bar": "LOL"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"blog": blogHelper,
|
|
||||||
"equal": equalHelper,
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"val is true",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mixed paths and helpers",
|
|
||||||
"{{blog baz.bat (equal a b) baz.bar}}",
|
|
||||||
map[string]interface{}{"bar": "LOL", "baz": map[string]string{"bat": "foo!", "bar": "bar!"}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"blog": func(p, p2, p3 string) string {
|
|
||||||
return "val is " + p + ", " + p2 + " and " + p3
|
|
||||||
},
|
|
||||||
"equal": equalHelper,
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"val is foo!, true and bar!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"supports much nesting",
|
|
||||||
"{{blog (equal (equal true true) true)}}",
|
|
||||||
map[string]interface{}{"bar": "LOL"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"blog": blogHelper,
|
|
||||||
"equal": equalHelper,
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"val is true",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"GH-800 : Complex subexpressions (1)",
|
|
||||||
"{{dash 'abc' (concat a b)}}",
|
|
||||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
|
||||||
nil,
|
|
||||||
"abc-ab",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"GH-800 : Complex subexpressions (2)",
|
|
||||||
"{{dash d (concat a b)}}",
|
|
||||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
|
||||||
nil,
|
|
||||||
"d-ab",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"GH-800 : Complex subexpressions (3)",
|
|
||||||
"{{dash c.c (concat a b)}}",
|
|
||||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
|
||||||
nil,
|
|
||||||
"c-ab",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"GH-800 : Complex subexpressions (4)",
|
|
||||||
"{{dash (concat a b) c.c}}",
|
|
||||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
|
||||||
nil,
|
|
||||||
"ab-c",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"GH-800 : Complex subexpressions (5)",
|
|
||||||
"{{dash (concat a e.e) c.c}}",
|
|
||||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
|
||||||
nil,
|
|
||||||
"ae-c",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
// note: test not relevant
|
|
||||||
"provides each nested helper invocation its own options hash",
|
|
||||||
"{{equal (equal true true) true}}",
|
|
||||||
map[string]interface{}{},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"equal": equalHelper,
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"with hashes",
|
|
||||||
"{{blog (equal (equal true true) true fun='yes')}}",
|
|
||||||
map[string]interface{}{"bar": "LOL"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"blog": blogHelper,
|
|
||||||
"equal": equalHelper,
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"val is true",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"as hashes",
|
|
||||||
"{{blog fun=(equal (blog fun=1) 'val is 1')}}",
|
|
||||||
map[string]interface{}{},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"blog": func(options *raymond.Options) string {
|
|
||||||
return "val is " + options.HashStr("fun")
|
|
||||||
},
|
|
||||||
"equal": equalHelper,
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"val is true",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"multiple subexpressions in a hash",
|
|
||||||
`{{input aria-label=(t "Name") placeholder=(t "Example User")}}`,
|
|
||||||
map[string]interface{}{},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"input": func(options *raymond.Options) raymond.SafeString {
|
|
||||||
return raymond.SafeString(`<input aria-label="` + options.HashStr("aria-label") + `" placeholder="` + options.HashStr("placeholder") + `" />`)
|
|
||||||
},
|
|
||||||
"t": func(param string) raymond.SafeString {
|
|
||||||
return raymond.SafeString(param)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
`<input aria-label="Name" placeholder="Example User" />`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"multiple subexpressions in a hash with context",
|
|
||||||
`{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}`,
|
|
||||||
map[string]map[string]string{"item": {"field": "Name", "placeholder": "Example User"}},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"input": func(options *raymond.Options) raymond.SafeString {
|
|
||||||
return raymond.SafeString(`<input aria-label="` + options.HashStr("aria-label") + `" placeholder="` + options.HashStr("placeholder") + `" />`)
|
|
||||||
},
|
|
||||||
"t": func(param string) raymond.SafeString {
|
|
||||||
return raymond.SafeString(param)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
`<input aria-label="Name" placeholder="Example User" />`,
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "in string params mode"
|
|
||||||
|
|
||||||
// @todo "as hashes in string params mode"
|
|
||||||
|
|
||||||
{
|
|
||||||
"subexpression functions on the context",
|
|
||||||
"{{foo (bar)}}!",
|
|
||||||
map[string]interface{}{"bar": func() string { return "LOL" }},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{
|
|
||||||
"foo": func(val string) string {
|
|
||||||
return val + val
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
"LOLLOL!",
|
|
||||||
},
|
|
||||||
|
|
||||||
// @todo "subexpressions can't just be property lookups" should raise error
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubexpressions(t *testing.T) {
|
|
||||||
launchTests(t, subexpressionsTests)
|
|
||||||
}
|
|
|
@ -1,259 +0,0 @@
|
||||||
package handlebars
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
//
|
|
||||||
// Those tests come from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/spec/whitespace-control.js
|
|
||||||
//
|
|
||||||
var whitespaceControlTests = []Test{
|
|
||||||
{
|
|
||||||
"should strip whitespace around mustache calls (1)",
|
|
||||||
" {{~foo~}} ",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar<",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around mustache calls (2)",
|
|
||||||
" {{~foo}} ",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar< ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around mustache calls (3)",
|
|
||||||
" {{foo~}} ",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
" bar<",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around mustache calls (4)",
|
|
||||||
" {{~&foo~}} ",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar<",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around mustache calls (5)",
|
|
||||||
" {{~{foo}~}} ",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar<",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around mustache calls (6)",
|
|
||||||
"1\n{{foo~}} \n\n 23\n{{bar}}4",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"1\n23\n4",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"blocks - should strip whitespace around simple block calls (1)",
|
|
||||||
" {{~#if foo~}} bar {{~/if~}} ",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"blocks - should strip whitespace around simple block calls (2)",
|
|
||||||
" {{#if foo~}} bar {{/if~}} ",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
" bar ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"blocks - should strip whitespace around simple block calls (3)",
|
|
||||||
" {{~#if foo}} bar {{~/if}} ",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
" bar ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"blocks - should strip whitespace around simple block calls (4)",
|
|
||||||
" {{#if foo}} bar {{/if}} ",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
" bar ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"blocks - should strip whitespace around simple block calls (5)",
|
|
||||||
" \n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\n ",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"blocks - should strip whitespace around simple block calls (6)",
|
|
||||||
" a\n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\na ",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
" abara ",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"should strip whitespace around inverse block calls (1)",
|
|
||||||
" {{~^if foo~}} bar {{~/if~}} ",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around inverse block calls (2)",
|
|
||||||
" {{^if foo~}} bar {{/if~}} ",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
" bar ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around inverse block calls (3)",
|
|
||||||
" {{~^if foo}} bar {{~/if}} ",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
" bar ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around inverse block calls (4)",
|
|
||||||
" {{^if foo}} bar {{/if}} ",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
" bar ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around inverse block calls (5)",
|
|
||||||
" \n\n{{~^if foo~}} \n\nbar \n\n{{~/if~}}\n\n ",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (1)",
|
|
||||||
"{{#if foo~}} bar {{~^~}} baz {{~/if}}",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (2)",
|
|
||||||
"{{#if foo~}} bar {{^~}} baz {{/if}}",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (3)",
|
|
||||||
"{{#if foo}} bar {{~^~}} baz {{~/if}}",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
" bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (4)",
|
|
||||||
"{{#if foo}} bar {{^~}} baz {{/if}}",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
" bar ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (5)",
|
|
||||||
"{{#if foo~}} bar {{~else~}} baz {{~/if}}",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (6)",
|
|
||||||
"\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (7)",
|
|
||||||
"\n\n{{~#if foo~}} \n\n{{{foo}}} \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n",
|
|
||||||
map[string]string{"foo": "bar<"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"bar<",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (8)",
|
|
||||||
"{{#if foo~}} bar {{~^~}} baz {{~/if}}",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"baz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (9)",
|
|
||||||
"{{#if foo}} bar {{~^~}} baz {{/if}}",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"baz ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (10)",
|
|
||||||
"{{#if foo~}} bar {{~^}} baz {{~/if}}",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
" baz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (11)",
|
|
||||||
"{{#if foo~}} bar {{~^}} baz {{/if}}",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
" baz ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (12)",
|
|
||||||
"{{#if foo~}} bar {{~else~}} baz {{~/if}}",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"baz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around complex block calls (13)",
|
|
||||||
"\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n",
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
"baz",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"should strip whitespace around partials (1)",
|
|
||||||
"foo {{~> dude~}} ",
|
|
||||||
nil, nil, nil,
|
|
||||||
map[string]string{"dude": "bar"},
|
|
||||||
"foobar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around partials (2)",
|
|
||||||
"foo {{> dude~}} ",
|
|
||||||
nil, nil, nil,
|
|
||||||
map[string]string{"dude": "bar"},
|
|
||||||
"foo bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around partials (3)",
|
|
||||||
"foo {{> dude}} ",
|
|
||||||
nil, nil, nil,
|
|
||||||
map[string]string{"dude": "bar"},
|
|
||||||
"foo bar ",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around partials (4)",
|
|
||||||
"foo\n {{~> dude}} ",
|
|
||||||
nil, nil, nil,
|
|
||||||
map[string]string{"dude": "bar"},
|
|
||||||
"foobar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"should strip whitespace around partials (5)",
|
|
||||||
"foo\n {{> dude}} ",
|
|
||||||
nil, nil, nil,
|
|
||||||
map[string]string{"dude": "bar"},
|
|
||||||
"foo\n bar",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"should only strip whitespace once",
|
|
||||||
" {{~foo~}} {{foo}} {{foo}} ",
|
|
||||||
map[string]string{"foo": "bar"},
|
|
||||||
nil, nil, nil,
|
|
||||||
"barbar bar ",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWhitespaceControl(t *testing.T) {
|
|
||||||
launchTests(t, whitespaceControlTests)
|
|
||||||
}
|
|
|
@ -1,371 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options represents the options argument provided to helpers and context functions.
|
|
||||||
type Options struct {
|
|
||||||
// evaluation visitor
|
|
||||||
eval *evalVisitor
|
|
||||||
|
|
||||||
// params
|
|
||||||
params []interface{}
|
|
||||||
hash map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// helpers stores all globally registered helpers
|
|
||||||
var helpers = make(map[string]reflect.Value)
|
|
||||||
|
|
||||||
// protects global helpers
|
|
||||||
var helpersMutex sync.RWMutex
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// register builtin helpers
|
|
||||||
RegisterHelper("if", ifHelper)
|
|
||||||
RegisterHelper("unless", unlessHelper)
|
|
||||||
RegisterHelper("with", withHelper)
|
|
||||||
RegisterHelper("each", eachHelper)
|
|
||||||
RegisterHelper("log", logHelper)
|
|
||||||
RegisterHelper("lookup", lookupHelper)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterHelper registers a global helper. That helper will be available to all templates.
|
|
||||||
func RegisterHelper(name string, helper interface{}) {
|
|
||||||
helpersMutex.Lock()
|
|
||||||
defer helpersMutex.Unlock()
|
|
||||||
|
|
||||||
if helpers[name] != zero {
|
|
||||||
panic(fmt.Errorf("Helper already registered: %s", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
val := reflect.ValueOf(helper)
|
|
||||||
ensureValidHelper(name, val)
|
|
||||||
|
|
||||||
helpers[name] = val
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterHelpers registers several global helpers. Those helpers will be available to all templates.
|
|
||||||
func RegisterHelpers(helpers map[string]interface{}) {
|
|
||||||
for name, helper := range helpers {
|
|
||||||
RegisterHelper(name, helper)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureValidHelper panics if given helper is not valid
|
|
||||||
func ensureValidHelper(name string, funcValue reflect.Value) {
|
|
||||||
if funcValue.Kind() != reflect.Func {
|
|
||||||
panic(fmt.Errorf("Helper must be a function: %s", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
funcType := funcValue.Type()
|
|
||||||
|
|
||||||
if funcType.NumOut() != 1 {
|
|
||||||
panic(fmt.Errorf("Helper function must return a string or a SafeString: %s", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
// @todo Check if first returned value is a string, SafeString or interface{} ?
|
|
||||||
}
|
|
||||||
|
|
||||||
// findHelper finds a globally registered helper
|
|
||||||
func findHelper(name string) reflect.Value {
|
|
||||||
helpersMutex.RLock()
|
|
||||||
defer helpersMutex.RUnlock()
|
|
||||||
|
|
||||||
return helpers[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
// newOptions instanciates a new Options
|
|
||||||
func newOptions(eval *evalVisitor, params []interface{}, hash map[string]interface{}) *Options {
|
|
||||||
return &Options{
|
|
||||||
eval: eval,
|
|
||||||
params: params,
|
|
||||||
hash: hash,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newEmptyOptions instanciates a new empty Options
|
|
||||||
func newEmptyOptions(eval *evalVisitor) *Options {
|
|
||||||
return &Options{
|
|
||||||
eval: eval,
|
|
||||||
hash: make(map[string]interface{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Context Values
|
|
||||||
//
|
|
||||||
|
|
||||||
// Value returns field value from current context.
|
|
||||||
func (options *Options) Value(name string) interface{} {
|
|
||||||
value := options.eval.evalField(options.eval.curCtx(), name, false)
|
|
||||||
if !value.IsValid() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Interface()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValueStr returns string representation of field value from current context.
|
|
||||||
func (options *Options) ValueStr(name string) string {
|
|
||||||
return Str(options.Value(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctx returns current evaluation context.
|
|
||||||
func (options *Options) Ctx() interface{} {
|
|
||||||
return options.eval.curCtx()
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Hash Arguments
|
|
||||||
//
|
|
||||||
|
|
||||||
// HashProp returns hash property.
|
|
||||||
func (options *Options) HashProp(name string) interface{} {
|
|
||||||
return options.hash[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
// HashStr returns string representation of hash property.
|
|
||||||
func (options *Options) HashStr(name string) string {
|
|
||||||
return Str(options.hash[name])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash returns entire hash.
|
|
||||||
func (options *Options) Hash() map[string]interface{} {
|
|
||||||
return options.hash
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Parameters
|
|
||||||
//
|
|
||||||
|
|
||||||
// Param returns parameter at given position.
|
|
||||||
func (options *Options) Param(pos int) interface{} {
|
|
||||||
if len(options.params) > pos {
|
|
||||||
return options.params[pos]
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParamStr returns string representation of parameter at given position.
|
|
||||||
func (options *Options) ParamStr(pos int) string {
|
|
||||||
return Str(options.Param(pos))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Params returns all parameters.
|
|
||||||
func (options *Options) Params() []interface{} {
|
|
||||||
return options.params
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Private data
|
|
||||||
//
|
|
||||||
|
|
||||||
// Data returns private data value.
|
|
||||||
func (options *Options) Data(name string) interface{} {
|
|
||||||
return options.eval.dataFrame.Get(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DataStr returns string representation of private data value.
|
|
||||||
func (options *Options) DataStr(name string) string {
|
|
||||||
return Str(options.eval.dataFrame.Get(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
// DataFrame returns current private data frame.
|
|
||||||
func (options *Options) DataFrame() *DataFrame {
|
|
||||||
return options.eval.dataFrame
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDataFrame instanciates a new data frame that is a copy of current evaluation data frame.
|
|
||||||
//
|
|
||||||
// Parent of returned data frame is set to current evaluation data frame.
|
|
||||||
func (options *Options) NewDataFrame() *DataFrame {
|
|
||||||
return options.eval.dataFrame.Copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
// newIterDataFrame instanciates a new data frame and set iteration specific vars
|
|
||||||
func (options *Options) newIterDataFrame(length int, i int, key interface{}) *DataFrame {
|
|
||||||
return options.eval.dataFrame.newIterDataFrame(length, i, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Evaluation
|
|
||||||
//
|
|
||||||
|
|
||||||
// evalBlock evaluates block with given context, private data and iteration key
|
|
||||||
func (options *Options) evalBlock(ctx interface{}, data *DataFrame, key interface{}) string {
|
|
||||||
result := ""
|
|
||||||
|
|
||||||
if block := options.eval.curBlock(); (block != nil) && (block.Program != nil) {
|
|
||||||
result = options.eval.evalProgram(block.Program, ctx, data, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fn evaluates block with current evaluation context.
|
|
||||||
func (options *Options) Fn() string {
|
|
||||||
return options.evalBlock(nil, nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FnCtxData evaluates block with given context and private data frame.
|
|
||||||
func (options *Options) FnCtxData(ctx interface{}, data *DataFrame) string {
|
|
||||||
return options.evalBlock(ctx, data, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FnWith evaluates block with given context.
|
|
||||||
func (options *Options) FnWith(ctx interface{}) string {
|
|
||||||
return options.evalBlock(ctx, nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FnData evaluates block with given private data frame.
|
|
||||||
func (options *Options) FnData(data *DataFrame) string {
|
|
||||||
return options.evalBlock(nil, data, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inverse evaluates "else block".
|
|
||||||
func (options *Options) Inverse() string {
|
|
||||||
result := ""
|
|
||||||
if block := options.eval.curBlock(); (block != nil) && (block.Inverse != nil) {
|
|
||||||
result, _ = block.Inverse.Accept(options.eval).(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eval evaluates field for given context.
|
|
||||||
func (options *Options) Eval(ctx interface{}, field string) interface{} {
|
|
||||||
if ctx == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if field == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
val := options.eval.evalField(reflect.ValueOf(ctx), field, false)
|
|
||||||
if !val.IsValid() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return val.Interface()
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Misc
|
|
||||||
//
|
|
||||||
|
|
||||||
// isIncludableZero returns true if 'includeZero' option is set and first param is the number 0
|
|
||||||
func (options *Options) isIncludableZero() bool {
|
|
||||||
b, ok := options.HashProp("includeZero").(bool)
|
|
||||||
if ok && b {
|
|
||||||
nb, ok := options.Param(0).(int)
|
|
||||||
if ok && nb == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Builtin helpers
|
|
||||||
//
|
|
||||||
|
|
||||||
// #if block helper
|
|
||||||
func ifHelper(conditional interface{}, options *Options) interface{} {
|
|
||||||
if options.isIncludableZero() || IsTrue(conditional) {
|
|
||||||
return options.Fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
return options.Inverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
// #unless block helper
|
|
||||||
func unlessHelper(conditional interface{}, options *Options) interface{} {
|
|
||||||
if options.isIncludableZero() || IsTrue(conditional) {
|
|
||||||
return options.Inverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
return options.Fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
// #with block helper
|
|
||||||
func withHelper(context interface{}, options *Options) interface{} {
|
|
||||||
if IsTrue(context) {
|
|
||||||
return options.FnWith(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
return options.Inverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
// #each block helper
|
|
||||||
func eachHelper(context interface{}, options *Options) interface{} {
|
|
||||||
if !IsTrue(context) {
|
|
||||||
return options.Inverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
result := ""
|
|
||||||
|
|
||||||
val := reflect.ValueOf(context)
|
|
||||||
switch val.Kind() {
|
|
||||||
case reflect.Array, reflect.Slice:
|
|
||||||
for i := 0; i < val.Len(); i++ {
|
|
||||||
// computes private data
|
|
||||||
data := options.newIterDataFrame(val.Len(), i, nil)
|
|
||||||
|
|
||||||
// evaluates block
|
|
||||||
result += options.evalBlock(val.Index(i).Interface(), data, i)
|
|
||||||
}
|
|
||||||
case reflect.Map:
|
|
||||||
// note: a go hash is not ordered, so result may vary, this behaviour differs from the JS implementation
|
|
||||||
keys := val.MapKeys()
|
|
||||||
for i := 0; i < len(keys); i++ {
|
|
||||||
key := keys[i].Interface()
|
|
||||||
ctx := val.MapIndex(keys[i]).Interface()
|
|
||||||
|
|
||||||
// computes private data
|
|
||||||
data := options.newIterDataFrame(len(keys), i, key)
|
|
||||||
|
|
||||||
// evaluates block
|
|
||||||
result += options.evalBlock(ctx, data, key)
|
|
||||||
}
|
|
||||||
case reflect.Struct:
|
|
||||||
var exportedFields []int
|
|
||||||
|
|
||||||
// collect exported fields only
|
|
||||||
for i := 0; i < val.NumField(); i++ {
|
|
||||||
if tField := val.Type().Field(i); tField.PkgPath == "" {
|
|
||||||
exportedFields = append(exportedFields, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, fieldIndex := range exportedFields {
|
|
||||||
key := val.Type().Field(fieldIndex).Name
|
|
||||||
ctx := val.Field(fieldIndex).Interface()
|
|
||||||
|
|
||||||
// computes private data
|
|
||||||
data := options.newIterDataFrame(len(exportedFields), i, key)
|
|
||||||
|
|
||||||
// evaluates block
|
|
||||||
result += options.evalBlock(ctx, data, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// #log helper
|
|
||||||
func logHelper(message string) interface{} {
|
|
||||||
log.Print(message)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// #lookup helper
|
|
||||||
func lookupHelper(obj interface{}, field string, options *Options) interface{} {
|
|
||||||
return Str(options.Eval(obj, field))
|
|
||||||
}
|
|
|
@ -1,165 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
const (
|
|
||||||
VERBOSE = false
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Helpers
|
|
||||||
//
|
|
||||||
|
|
||||||
func barHelper(options *Options) string { return "bar" }
|
|
||||||
|
|
||||||
func echoHelper(str string, nb int) string {
|
|
||||||
result := ""
|
|
||||||
for i := 0; i < nb; i++ {
|
|
||||||
result += str
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolHelper(b bool) string {
|
|
||||||
if b {
|
|
||||||
return "yes it is"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "absolutely not"
|
|
||||||
}
|
|
||||||
|
|
||||||
func gnakHelper(nb int) string {
|
|
||||||
result := ""
|
|
||||||
for i := 0; i < nb; i++ {
|
|
||||||
result += "GnAK!"
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Tests
|
|
||||||
//
|
|
||||||
|
|
||||||
var helperTests = []Test{
|
|
||||||
{
|
|
||||||
"simple helper",
|
|
||||||
`{{foo}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"foo": barHelper},
|
|
||||||
nil,
|
|
||||||
`bar`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helper with literal string param",
|
|
||||||
`{{echo "foo" 1}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"echo": echoHelper},
|
|
||||||
nil,
|
|
||||||
`foo`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helper with identifier param",
|
|
||||||
`{{echo foo 1}}`,
|
|
||||||
map[string]interface{}{"foo": "bar"},
|
|
||||||
nil,
|
|
||||||
map[string]interface{}{"echo": echoHelper},
|
|
||||||
nil,
|
|
||||||
`bar`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helper with literal boolean param",
|
|
||||||
`{{bool true}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"bool": boolHelper},
|
|
||||||
nil,
|
|
||||||
`yes it is`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helper with literal boolean param",
|
|
||||||
`{{bool false}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"bool": boolHelper},
|
|
||||||
nil,
|
|
||||||
`absolutely not`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helper with literal boolean param",
|
|
||||||
`{{gnak 5}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"gnak": gnakHelper},
|
|
||||||
nil,
|
|
||||||
`GnAK!GnAK!GnAK!GnAK!GnAK!`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"helper with several parameters",
|
|
||||||
`{{echo "GnAK!" 3}}`,
|
|
||||||
nil, nil,
|
|
||||||
map[string]interface{}{"echo": echoHelper},
|
|
||||||
nil,
|
|
||||||
`GnAK!GnAK!GnAK!`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if helper with true literal",
|
|
||||||
`{{#if true}}YES MAN{{/if}}`,
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
`YES MAN`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if helper with false literal",
|
|
||||||
`{{#if false}}YES MAN{{/if}}`,
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
``,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if helper with truthy identifier",
|
|
||||||
`{{#if ok}}YES MAN{{/if}}`,
|
|
||||||
map[string]interface{}{"ok": true},
|
|
||||||
nil, nil, nil,
|
|
||||||
`YES MAN`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#if helper with falsy identifier",
|
|
||||||
`{{#if ok}}YES MAN{{/if}}`,
|
|
||||||
map[string]interface{}{"ok": false},
|
|
||||||
nil, nil, nil,
|
|
||||||
``,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#unless helper with true literal",
|
|
||||||
`{{#unless true}}YES MAN{{/unless}}`,
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
``,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#unless helper with false literal",
|
|
||||||
`{{#unless false}}YES MAN{{/unless}}`,
|
|
||||||
nil, nil, nil, nil,
|
|
||||||
`YES MAN`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#unless helper with truthy identifier",
|
|
||||||
`{{#unless ok}}YES MAN{{/unless}}`,
|
|
||||||
map[string]interface{}{"ok": true},
|
|
||||||
nil, nil, nil,
|
|
||||||
``,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#unless helper with falsy identifier",
|
|
||||||
`{{#unless ok}}YES MAN{{/unless}}`,
|
|
||||||
map[string]interface{}{"ok": false},
|
|
||||||
nil, nil, nil,
|
|
||||||
`YES MAN`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Let's go
|
|
||||||
//
|
|
||||||
|
|
||||||
func TestHelper(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
launchTests(t, helperTests)
|
|
||||||
}
|
|
|
@ -1,650 +0,0 @@
|
||||||
// Package lexer provides a handlebars tokenizer.
|
|
||||||
package lexer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
// References:
|
|
||||||
// - https://github.com/wycats/handlebars.js/blob/master/src/handlebars.l
|
|
||||||
// - https://github.com/golang/go/blob/master/src/text/template/parse/lex.go
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Mustaches detection
|
|
||||||
ESCAPED_ESCAPED_OPEN_MUSTACHE = "\\\\{{"
|
|
||||||
ESCAPED_OPEN_MUSTACHE = "\\{{"
|
|
||||||
OPEN_MUSTACHE = "{{"
|
|
||||||
CLOSE_MUSTACHE = "}}"
|
|
||||||
CLOSE_STRIP_MUSTACHE = "~}}"
|
|
||||||
CLOSE_UNESCAPED_STRIP_MUSTACHE = "}~}}"
|
|
||||||
)
|
|
||||||
|
|
||||||
const eof = -1
|
|
||||||
|
|
||||||
// lexFunc represents a function that returns the next lexer function.
|
|
||||||
type lexFunc func(*Lexer) lexFunc
|
|
||||||
|
|
||||||
// Lexer is a lexical analyzer.
|
|
||||||
type Lexer struct {
|
|
||||||
input string // input to scan
|
|
||||||
name string // lexer name, used for testing purpose
|
|
||||||
tokens chan Token // channel of scanned tokens
|
|
||||||
nextFunc lexFunc // the next function to execute
|
|
||||||
|
|
||||||
pos int // current byte position in input string
|
|
||||||
line int // current line position in input string
|
|
||||||
width int // size of last rune scanned from input string
|
|
||||||
start int // start position of the token we are scanning
|
|
||||||
|
|
||||||
// the shameful contextual properties needed because `nextFunc` is not enough
|
|
||||||
closeComment *regexp.Regexp // regexp to scan close of current comment
|
|
||||||
rawBlock bool // are we parsing a raw block content ?
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
lookheadChars = `[\s` + regexp.QuoteMeta("=~}/)|") + `]`
|
|
||||||
literalLookheadChars = `[\s` + regexp.QuoteMeta("~})") + `]`
|
|
||||||
|
|
||||||
// characters not allowed in an identifier
|
|
||||||
unallowedIDChars = " \n\t!\"#%&'()*+,./;<=>@[\\]^`{|}~"
|
|
||||||
|
|
||||||
// regular expressions
|
|
||||||
rID = regexp.MustCompile(`^[^` + regexp.QuoteMeta(unallowedIDChars) + `]+`)
|
|
||||||
rDotID = regexp.MustCompile(`^\.` + lookheadChars)
|
|
||||||
rTrue = regexp.MustCompile(`^true` + literalLookheadChars)
|
|
||||||
rFalse = regexp.MustCompile(`^false` + literalLookheadChars)
|
|
||||||
rOpenRaw = regexp.MustCompile(`^\{\{\{\{`)
|
|
||||||
rCloseRaw = regexp.MustCompile(`^\}\}\}\}`)
|
|
||||||
rOpenEndRaw = regexp.MustCompile(`^\{\{\{\{/`)
|
|
||||||
rOpenEndRawLookAhead = regexp.MustCompile(`\{\{\{\{/`)
|
|
||||||
rOpenUnescaped = regexp.MustCompile(`^\{\{~?\{`)
|
|
||||||
rCloseUnescaped = regexp.MustCompile(`^\}~?\}\}`)
|
|
||||||
rOpenBlock = regexp.MustCompile(`^\{\{~?#`)
|
|
||||||
rOpenEndBlock = regexp.MustCompile(`^\{\{~?/`)
|
|
||||||
rOpenPartial = regexp.MustCompile(`^\{\{~?>`)
|
|
||||||
// {{^}} or {{else}}
|
|
||||||
rInverse = regexp.MustCompile(`^(\{\{~?\^\s*~?\}\}|\{\{~?\s*else\s*~?\}\})`)
|
|
||||||
rOpenInverse = regexp.MustCompile(`^\{\{~?\^`)
|
|
||||||
rOpenInverseChain = regexp.MustCompile(`^\{\{~?\s*else`)
|
|
||||||
// {{ or {{&
|
|
||||||
rOpen = regexp.MustCompile(`^\{\{~?&?`)
|
|
||||||
rClose = regexp.MustCompile(`^~?\}\}`)
|
|
||||||
rOpenBlockParams = regexp.MustCompile(`^as\s+\|`)
|
|
||||||
// {{!-- ... --}}
|
|
||||||
rOpenCommentDash = regexp.MustCompile(`^\{\{~?!--\s*`)
|
|
||||||
rCloseCommentDash = regexp.MustCompile(`^\s*--~?\}\}`)
|
|
||||||
// {{! ... }}
|
|
||||||
rOpenComment = regexp.MustCompile(`^\{\{~?!\s*`)
|
|
||||||
rCloseComment = regexp.MustCompile(`^\s*~?\}\}`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Scan scans given input.
|
|
||||||
//
|
|
||||||
// Tokens can then be fetched sequentially thanks to NextToken() function on returned lexer.
|
|
||||||
func Scan(input string) *Lexer {
|
|
||||||
return scanWithName(input, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// scanWithName scans given input, with a name used for testing
|
|
||||||
//
|
|
||||||
// Tokens can then be fetched sequentially thanks to NextToken() function on returned lexer.
|
|
||||||
func scanWithName(input string, name string) *Lexer {
|
|
||||||
result := &Lexer{
|
|
||||||
input: input,
|
|
||||||
name: name,
|
|
||||||
tokens: make(chan Token),
|
|
||||||
line: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
go result.run()
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect scans and collect all tokens.
|
|
||||||
//
|
|
||||||
// This should be used for debugging purpose only. You should use Scan() and lexer.NextToken() functions instead.
|
|
||||||
func Collect(input string) []Token {
|
|
||||||
var result []Token
|
|
||||||
|
|
||||||
l := Scan(input)
|
|
||||||
for {
|
|
||||||
token := l.NextToken()
|
|
||||||
result = append(result, token)
|
|
||||||
|
|
||||||
if token.Kind == TokenEOF || token.Kind == TokenError {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextToken returns the next scanned token.
|
|
||||||
func (l *Lexer) NextToken() Token {
|
|
||||||
result := <-l.tokens
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pos returns the current byte position.
|
|
||||||
func (l *Lexer) Pos() int {
|
|
||||||
return l.pos
|
|
||||||
}
|
|
||||||
|
|
||||||
// Line returns the current line number.
|
|
||||||
func (l *Lexer) Line() int {
|
|
||||||
return l.line
|
|
||||||
}
|
|
||||||
|
|
||||||
// run starts lexical analysis
|
|
||||||
func (l *Lexer) run() {
|
|
||||||
for l.nextFunc = lexContent; l.nextFunc != nil; {
|
|
||||||
l.nextFunc = l.nextFunc(l)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// next returns next character from input, or eof of there is nothing left to scan
|
|
||||||
func (l *Lexer) next() rune {
|
|
||||||
if l.pos >= len(l.input) {
|
|
||||||
l.width = 0
|
|
||||||
return eof
|
|
||||||
}
|
|
||||||
|
|
||||||
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
|
|
||||||
l.width = w
|
|
||||||
l.pos += l.width
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Lexer) produce(kind TokenKind, val string) {
|
|
||||||
l.tokens <- Token{kind, val, l.start, l.line}
|
|
||||||
|
|
||||||
// scanning a new token
|
|
||||||
l.start = l.pos
|
|
||||||
|
|
||||||
// update line number
|
|
||||||
l.line += strings.Count(val, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// emit emits a new scanned token
|
|
||||||
func (l *Lexer) emit(kind TokenKind) {
|
|
||||||
l.produce(kind, l.input[l.start:l.pos])
|
|
||||||
}
|
|
||||||
|
|
||||||
// emitContent emits scanned content
|
|
||||||
func (l *Lexer) emitContent() {
|
|
||||||
if l.pos > l.start {
|
|
||||||
l.emit(TokenContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// emitString emits a scanned string
|
|
||||||
func (l *Lexer) emitString(delimiter rune) {
|
|
||||||
str := l.input[l.start:l.pos]
|
|
||||||
|
|
||||||
// replace escaped delimiters
|
|
||||||
str = strings.Replace(str, "\\"+string(delimiter), string(delimiter), -1)
|
|
||||||
|
|
||||||
l.produce(TokenString, str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// peek returns but does not consume the next character in the input
|
|
||||||
func (l *Lexer) peek() rune {
|
|
||||||
r := l.next()
|
|
||||||
l.backup()
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// backup steps back one character
|
|
||||||
//
|
|
||||||
// WARNING: Can only be called once per call of next
|
|
||||||
func (l *Lexer) backup() {
|
|
||||||
l.pos -= l.width
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignoreskips all characters that have been scanned up to current position
|
|
||||||
func (l *Lexer) ignore() {
|
|
||||||
l.start = l.pos
|
|
||||||
}
|
|
||||||
|
|
||||||
// accept scans the next character if it is included in given string
|
|
||||||
func (l *Lexer) accept(valid string) bool {
|
|
||||||
if strings.IndexRune(valid, l.next()) >= 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
l.backup()
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// acceptRun scans all following characters that are part of given string
|
|
||||||
func (l *Lexer) acceptRun(valid string) {
|
|
||||||
for strings.IndexRune(valid, l.next()) >= 0 {
|
|
||||||
}
|
|
||||||
|
|
||||||
l.backup()
|
|
||||||
}
|
|
||||||
|
|
||||||
// errorf emits an error token
|
|
||||||
func (l *Lexer) errorf(format string, args ...interface{}) lexFunc {
|
|
||||||
l.tokens <- Token{TokenError, fmt.Sprintf(format, args...), l.start, l.line}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isString returns true if content at current scanning position starts with given string
|
|
||||||
func (l *Lexer) isString(str string) bool {
|
|
||||||
return strings.HasPrefix(l.input[l.pos:], str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// findRegexp returns the first string from current scanning position that matches given regular expression
|
|
||||||
func (l *Lexer) findRegexp(r *regexp.Regexp) string {
|
|
||||||
return r.FindString(l.input[l.pos:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// indexRegexp returns the index of the first string from current scanning position that matches given regular expression
|
|
||||||
//
|
|
||||||
// It returns -1 if not found
|
|
||||||
func (l *Lexer) indexRegexp(r *regexp.Regexp) int {
|
|
||||||
loc := r.FindStringIndex(l.input[l.pos:])
|
|
||||||
if loc == nil {
|
|
||||||
return -1
|
|
||||||
} else {
|
|
||||||
return loc[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexContent scans content (ie: not between mustaches)
|
|
||||||
func lexContent(l *Lexer) lexFunc {
|
|
||||||
var next lexFunc
|
|
||||||
|
|
||||||
if l.rawBlock {
|
|
||||||
if i := l.indexRegexp(rOpenEndRawLookAhead); i != -1 {
|
|
||||||
// {{{{/
|
|
||||||
l.rawBlock = false
|
|
||||||
l.pos += i
|
|
||||||
|
|
||||||
next = lexOpenMustache
|
|
||||||
} else {
|
|
||||||
return l.errorf("Unclosed raw block")
|
|
||||||
}
|
|
||||||
} else if l.isString(ESCAPED_ESCAPED_OPEN_MUSTACHE) {
|
|
||||||
// \\{{
|
|
||||||
|
|
||||||
// emit content with only one escaped escape
|
|
||||||
l.next()
|
|
||||||
l.emitContent()
|
|
||||||
|
|
||||||
// ignore second escaped escape
|
|
||||||
l.next()
|
|
||||||
l.ignore()
|
|
||||||
|
|
||||||
next = lexContent
|
|
||||||
} else if l.isString(ESCAPED_OPEN_MUSTACHE) {
|
|
||||||
// \{{
|
|
||||||
next = lexEscapedOpenMustache
|
|
||||||
} else if str := l.findRegexp(rOpenCommentDash); str != "" {
|
|
||||||
// {{!--
|
|
||||||
l.closeComment = rCloseCommentDash
|
|
||||||
|
|
||||||
next = lexComment
|
|
||||||
} else if str := l.findRegexp(rOpenComment); str != "" {
|
|
||||||
// {{!
|
|
||||||
l.closeComment = rCloseComment
|
|
||||||
|
|
||||||
next = lexComment
|
|
||||||
} else if l.isString(OPEN_MUSTACHE) {
|
|
||||||
// {{
|
|
||||||
next = lexOpenMustache
|
|
||||||
}
|
|
||||||
|
|
||||||
if next != nil {
|
|
||||||
// emit scanned content
|
|
||||||
l.emitContent()
|
|
||||||
|
|
||||||
// scan next token
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
// scan next rune
|
|
||||||
if l.next() == eof {
|
|
||||||
// emit scanned content
|
|
||||||
l.emitContent()
|
|
||||||
|
|
||||||
// this is over
|
|
||||||
l.emit(TokenEOF)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// continue content scanning
|
|
||||||
return lexContent
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexEscapedOpenMustache scans \{{
|
|
||||||
func lexEscapedOpenMustache(l *Lexer) lexFunc {
|
|
||||||
// ignore escape character
|
|
||||||
l.next()
|
|
||||||
l.ignore()
|
|
||||||
|
|
||||||
// scan mustaches
|
|
||||||
for l.peek() == '{' {
|
|
||||||
l.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
return lexContent
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexOpenMustache scans {{
|
|
||||||
func lexOpenMustache(l *Lexer) lexFunc {
|
|
||||||
var str string
|
|
||||||
var tok TokenKind
|
|
||||||
|
|
||||||
nextFunc := lexExpression
|
|
||||||
|
|
||||||
if str = l.findRegexp(rOpenEndRaw); str != "" {
|
|
||||||
tok = TokenOpenEndRawBlock
|
|
||||||
} else if str = l.findRegexp(rOpenRaw); str != "" {
|
|
||||||
tok = TokenOpenRawBlock
|
|
||||||
l.rawBlock = true
|
|
||||||
} else if str = l.findRegexp(rOpenUnescaped); str != "" {
|
|
||||||
tok = TokenOpenUnescaped
|
|
||||||
} else if str = l.findRegexp(rOpenBlock); str != "" {
|
|
||||||
tok = TokenOpenBlock
|
|
||||||
} else if str = l.findRegexp(rOpenEndBlock); str != "" {
|
|
||||||
tok = TokenOpenEndBlock
|
|
||||||
} else if str = l.findRegexp(rOpenPartial); str != "" {
|
|
||||||
tok = TokenOpenPartial
|
|
||||||
} else if str = l.findRegexp(rInverse); str != "" {
|
|
||||||
tok = TokenInverse
|
|
||||||
nextFunc = lexContent
|
|
||||||
} else if str = l.findRegexp(rOpenInverse); str != "" {
|
|
||||||
tok = TokenOpenInverse
|
|
||||||
} else if str = l.findRegexp(rOpenInverseChain); str != "" {
|
|
||||||
tok = TokenOpenInverseChain
|
|
||||||
} else if str = l.findRegexp(rOpen); str != "" {
|
|
||||||
tok = TokenOpen
|
|
||||||
} else {
|
|
||||||
// this is rotten
|
|
||||||
panic("Current pos MUST be an opening mustache")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.pos += len(str)
|
|
||||||
l.emit(tok)
|
|
||||||
|
|
||||||
return nextFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexCloseMustache scans }} or ~}}
|
|
||||||
func lexCloseMustache(l *Lexer) lexFunc {
|
|
||||||
var str string
|
|
||||||
var tok TokenKind
|
|
||||||
|
|
||||||
if str = l.findRegexp(rCloseRaw); str != "" {
|
|
||||||
// }}}}
|
|
||||||
tok = TokenCloseRawBlock
|
|
||||||
} else if str = l.findRegexp(rCloseUnescaped); str != "" {
|
|
||||||
// }}}
|
|
||||||
tok = TokenCloseUnescaped
|
|
||||||
} else if str = l.findRegexp(rClose); str != "" {
|
|
||||||
// }}
|
|
||||||
tok = TokenClose
|
|
||||||
} else {
|
|
||||||
// this is rotten
|
|
||||||
panic("Current pos MUST be a closing mustache")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.pos += len(str)
|
|
||||||
l.emit(tok)
|
|
||||||
|
|
||||||
return lexContent
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexExpression scans inside mustaches
|
|
||||||
func lexExpression(l *Lexer) lexFunc {
|
|
||||||
// search close mustache delimiter
|
|
||||||
if l.isString(CLOSE_MUSTACHE) || l.isString(CLOSE_STRIP_MUSTACHE) || l.isString(CLOSE_UNESCAPED_STRIP_MUSTACHE) {
|
|
||||||
return lexCloseMustache
|
|
||||||
}
|
|
||||||
|
|
||||||
// search some patterns before advancing scanning position
|
|
||||||
|
|
||||||
// "as |"
|
|
||||||
if str := l.findRegexp(rOpenBlockParams); str != "" {
|
|
||||||
l.pos += len(str)
|
|
||||||
l.emit(TokenOpenBlockParams)
|
|
||||||
return lexExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
// ..
|
|
||||||
if l.isString("..") {
|
|
||||||
l.pos += len("..")
|
|
||||||
l.emit(TokenID)
|
|
||||||
return lexExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
// .
|
|
||||||
if str := l.findRegexp(rDotID); str != "" {
|
|
||||||
l.pos += len(".")
|
|
||||||
l.emit(TokenID)
|
|
||||||
return lexExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
// true
|
|
||||||
if str := l.findRegexp(rTrue); str != "" {
|
|
||||||
l.pos += len("true")
|
|
||||||
l.emit(TokenBoolean)
|
|
||||||
return lexExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
// false
|
|
||||||
if str := l.findRegexp(rFalse); str != "" {
|
|
||||||
l.pos += len("false")
|
|
||||||
l.emit(TokenBoolean)
|
|
||||||
return lexExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
// let's scan next character
|
|
||||||
switch r := l.next(); {
|
|
||||||
case r == eof:
|
|
||||||
return l.errorf("Unclosed expression")
|
|
||||||
case isIgnorable(r):
|
|
||||||
return lexIgnorable
|
|
||||||
case r == '(':
|
|
||||||
l.emit(TokenOpenSexpr)
|
|
||||||
case r == ')':
|
|
||||||
l.emit(TokenCloseSexpr)
|
|
||||||
case r == '=':
|
|
||||||
l.emit(TokenEquals)
|
|
||||||
case r == '@':
|
|
||||||
l.emit(TokenData)
|
|
||||||
case r == '"' || r == '\'':
|
|
||||||
l.backup()
|
|
||||||
return lexString
|
|
||||||
case r == '/' || r == '.':
|
|
||||||
l.emit(TokenSep)
|
|
||||||
case r == '|':
|
|
||||||
l.emit(TokenCloseBlockParams)
|
|
||||||
case r == '+' || r == '-' || (r >= '0' && r <= '9'):
|
|
||||||
l.backup()
|
|
||||||
return lexNumber
|
|
||||||
case r == '[':
|
|
||||||
return lexPathLiteral
|
|
||||||
case strings.IndexRune(unallowedIDChars, r) < 0:
|
|
||||||
l.backup()
|
|
||||||
return lexIdentifier
|
|
||||||
default:
|
|
||||||
return l.errorf("Unexpected character in expression: '%c'", r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lexExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexComment scans {{!-- or {{!
|
|
||||||
func lexComment(l *Lexer) lexFunc {
|
|
||||||
if str := l.findRegexp(l.closeComment); str != "" {
|
|
||||||
l.pos += len(str)
|
|
||||||
l.emit(TokenComment)
|
|
||||||
|
|
||||||
return lexContent
|
|
||||||
}
|
|
||||||
|
|
||||||
if r := l.next(); r == eof {
|
|
||||||
return l.errorf("Unclosed comment")
|
|
||||||
}
|
|
||||||
|
|
||||||
return lexComment
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexIgnorable scans all following ignorable characters
|
|
||||||
func lexIgnorable(l *Lexer) lexFunc {
|
|
||||||
for isIgnorable(l.peek()) {
|
|
||||||
l.next()
|
|
||||||
}
|
|
||||||
l.ignore()
|
|
||||||
|
|
||||||
return lexExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexString scans a string
|
|
||||||
func lexString(l *Lexer) lexFunc {
|
|
||||||
// get string delimiter
|
|
||||||
delim := l.next()
|
|
||||||
var prev rune = 0
|
|
||||||
|
|
||||||
// ignore delimiter
|
|
||||||
l.ignore()
|
|
||||||
|
|
||||||
for {
|
|
||||||
r := l.next()
|
|
||||||
if r == eof || r == '\n' {
|
|
||||||
return l.errorf("Unterminated string")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r == delim) && (prev != '\\') {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
prev = r
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove end delimiter
|
|
||||||
l.backup()
|
|
||||||
|
|
||||||
// emit string
|
|
||||||
l.emitString(delim)
|
|
||||||
|
|
||||||
// skip end delimiter
|
|
||||||
l.next()
|
|
||||||
l.ignore()
|
|
||||||
|
|
||||||
return lexExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexNumber scans a number: decimal, octal, hex, float, or imaginary. This
|
|
||||||
// isn't a perfect number scanner - for instance it accepts "." and "0x0.2"
|
|
||||||
// and "089" - but when it's wrong the input is invalid and the parser (via
|
|
||||||
// strconv) will notice.
|
|
||||||
//
|
|
||||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/parse/lex.go
|
|
||||||
func lexNumber(l *Lexer) lexFunc {
|
|
||||||
if !l.scanNumber() {
|
|
||||||
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
|
||||||
}
|
|
||||||
if sign := l.peek(); sign == '+' || sign == '-' {
|
|
||||||
// Complex: 1+2i. No spaces, must end in 'i'.
|
|
||||||
if !l.scanNumber() || l.input[l.pos-1] != 'i' {
|
|
||||||
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
|
||||||
}
|
|
||||||
l.emit(TokenNumber)
|
|
||||||
} else {
|
|
||||||
l.emit(TokenNumber)
|
|
||||||
}
|
|
||||||
return lexExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
// scanNumber scans a number
|
|
||||||
//
|
|
||||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/parse/lex.go
|
|
||||||
func (l *Lexer) scanNumber() bool {
|
|
||||||
// Optional leading sign.
|
|
||||||
l.accept("+-")
|
|
||||||
|
|
||||||
// Is it hex?
|
|
||||||
digits := "0123456789"
|
|
||||||
|
|
||||||
if l.accept("0") && l.accept("xX") {
|
|
||||||
digits = "0123456789abcdefABCDEF"
|
|
||||||
}
|
|
||||||
|
|
||||||
l.acceptRun(digits)
|
|
||||||
|
|
||||||
if l.accept(".") {
|
|
||||||
l.acceptRun(digits)
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.accept("eE") {
|
|
||||||
l.accept("+-")
|
|
||||||
l.acceptRun("0123456789")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is it imaginary?
|
|
||||||
l.accept("i")
|
|
||||||
|
|
||||||
// Next thing mustn't be alphanumeric.
|
|
||||||
if isAlphaNumeric(l.peek()) {
|
|
||||||
l.next()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexIdentifier scans an ID
|
|
||||||
func lexIdentifier(l *Lexer) lexFunc {
|
|
||||||
str := l.findRegexp(rID)
|
|
||||||
if len(str) == 0 {
|
|
||||||
// this is rotten
|
|
||||||
panic("Identifier expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.pos += len(str)
|
|
||||||
l.emit(TokenID)
|
|
||||||
|
|
||||||
return lexExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexPathLiteral scans an [ID]
|
|
||||||
func lexPathLiteral(l *Lexer) lexFunc {
|
|
||||||
for {
|
|
||||||
r := l.next()
|
|
||||||
if r == eof || r == '\n' {
|
|
||||||
return l.errorf("Unterminated path literal")
|
|
||||||
}
|
|
||||||
|
|
||||||
if r == ']' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.emit(TokenID)
|
|
||||||
|
|
||||||
return lexExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
// isIgnorable returns true if given character is ignorable (ie. whitespace of line feed)
|
|
||||||
func isIgnorable(r rune) bool {
|
|
||||||
return r == ' ' || r == '\t' || r == '\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
|
|
||||||
//
|
|
||||||
// NOTE borrowed from https://github.com/golang/go/tree/master/src/text/template/parse/lex.go
|
|
||||||
func isAlphaNumeric(r rune) bool {
|
|
||||||
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
|
||||||
}
|
|
|
@ -1,541 +0,0 @@
|
||||||
package lexer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type lexTest struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
tokens []Token
|
|
||||||
}
|
|
||||||
|
|
||||||
// helpers
|
|
||||||
func tokContent(val string) Token { return Token{TokenContent, val, 0, 1} }
|
|
||||||
func tokID(val string) Token { return Token{TokenID, val, 0, 1} }
|
|
||||||
func tokSep(val string) Token { return Token{TokenSep, val, 0, 1} }
|
|
||||||
func tokString(val string) Token { return Token{TokenString, val, 0, 1} }
|
|
||||||
func tokNumber(val string) Token { return Token{TokenNumber, val, 0, 1} }
|
|
||||||
func tokInverse(val string) Token { return Token{TokenInverse, val, 0, 1} }
|
|
||||||
func tokBool(val string) Token { return Token{TokenBoolean, val, 0, 1} }
|
|
||||||
func tokError(val string) Token { return Token{TokenError, val, 0, 1} }
|
|
||||||
func tokComment(val string) Token { return Token{TokenComment, val, 0, 1} }
|
|
||||||
|
|
||||||
var tokEOF = Token{TokenEOF, "", 0, 1}
|
|
||||||
var tokEquals = Token{TokenEquals, "=", 0, 1}
|
|
||||||
var tokData = Token{TokenData, "@", 0, 1}
|
|
||||||
var tokOpen = Token{TokenOpen, "{{", 0, 1}
|
|
||||||
var tokOpenAmp = Token{TokenOpen, "{{&", 0, 1}
|
|
||||||
var tokOpenPartial = Token{TokenOpenPartial, "{{>", 0, 1}
|
|
||||||
var tokClose = Token{TokenClose, "}}", 0, 1}
|
|
||||||
var tokOpenStrip = Token{TokenOpen, "{{~", 0, 1}
|
|
||||||
var tokCloseStrip = Token{TokenClose, "~}}", 0, 1}
|
|
||||||
var tokOpenUnescaped = Token{TokenOpenUnescaped, "{{{", 0, 1}
|
|
||||||
var tokCloseUnescaped = Token{TokenCloseUnescaped, "}}}", 0, 1}
|
|
||||||
var tokOpenUnescapedStrip = Token{TokenOpenUnescaped, "{{~{", 0, 1}
|
|
||||||
var tokCloseUnescapedStrip = Token{TokenCloseUnescaped, "}~}}", 0, 1}
|
|
||||||
var tokOpenBlock = Token{TokenOpenBlock, "{{#", 0, 1}
|
|
||||||
var tokOpenEndBlock = Token{TokenOpenEndBlock, "{{/", 0, 1}
|
|
||||||
var tokOpenInverse = Token{TokenOpenInverse, "{{^", 0, 1}
|
|
||||||
var tokOpenInverseChain = Token{TokenOpenInverseChain, "{{else", 0, 1}
|
|
||||||
var tokOpenSexpr = Token{TokenOpenSexpr, "(", 0, 1}
|
|
||||||
var tokCloseSexpr = Token{TokenCloseSexpr, ")", 0, 1}
|
|
||||||
var tokOpenBlockParams = Token{TokenOpenBlockParams, "as |", 0, 1}
|
|
||||||
var tokCloseBlockParams = Token{TokenCloseBlockParams, "|", 0, 1}
|
|
||||||
var tokOpenRawBlock = Token{TokenOpenRawBlock, "{{{{", 0, 1}
|
|
||||||
var tokCloseRawBlock = Token{TokenCloseRawBlock, "}}}}", 0, 1}
|
|
||||||
var tokOpenEndRawBlock = Token{TokenOpenEndRawBlock, "{{{{/", 0, 1}
|
|
||||||
|
|
||||||
var lexTests = []lexTest{
|
|
||||||
{"empty", "", []Token{tokEOF}},
|
|
||||||
{"spaces", " \t\n", []Token{tokContent(" \t\n"), tokEOF}},
|
|
||||||
{"content", `now is the time`, []Token{tokContent(`now is the time`), tokEOF}},
|
|
||||||
|
|
||||||
{
|
|
||||||
`does not tokenizes identifier starting with true as boolean`,
|
|
||||||
`{{ foo truebar }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("truebar"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`does not tokenizes identifier starting with false as boolean`,
|
|
||||||
`{{ foo falsebar }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("falsebar"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes raw block`,
|
|
||||||
`{{{{foo}}}} {{{{/foo}}}}`,
|
|
||||||
[]Token{tokOpenRawBlock, tokID("foo"), tokCloseRawBlock, tokContent(" "), tokOpenEndRawBlock, tokID("foo"), tokCloseRawBlock, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes raw block with mustaches in content`,
|
|
||||||
`{{{{foo}}}}{{bar}}{{{{/foo}}}}`,
|
|
||||||
[]Token{tokOpenRawBlock, tokID("foo"), tokCloseRawBlock, tokContent("{{bar}}"), tokOpenEndRawBlock, tokID("foo"), tokCloseRawBlock, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes @../foo`,
|
|
||||||
`{{@../foo}}`,
|
|
||||||
[]Token{tokOpen, tokData, tokID(".."), tokSep("/"), tokID("foo"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes escaped mustaches`,
|
|
||||||
"\\{{bar}}",
|
|
||||||
[]Token{tokContent("{{bar}}"), tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes strip mustaches`,
|
|
||||||
`{{~ foo ~}}`,
|
|
||||||
[]Token{tokOpenStrip, tokID("foo"), tokCloseStrip, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes unescaped strip mustaches`,
|
|
||||||
`{{~{ foo }~}}`,
|
|
||||||
[]Token{tokOpenUnescapedStrip, tokID("foo"), tokCloseUnescapedStrip, tokEOF},
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// Next tests come from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/spec/tokenizer.js
|
|
||||||
//
|
|
||||||
{
|
|
||||||
`tokenizes a simple mustache as "OPEN ID CLOSE"`,
|
|
||||||
`{{foo}}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`supports unescaping with &`,
|
|
||||||
`{{&bar}}`,
|
|
||||||
[]Token{tokOpenAmp, tokID("bar"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`supports unescaping with {{{`,
|
|
||||||
`{{{bar}}}`,
|
|
||||||
[]Token{tokOpenUnescaped, tokID("bar"), tokCloseUnescaped, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`supports escaping delimiters`,
|
|
||||||
"{{foo}} \\{{bar}} {{baz}}",
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`supports escaping multiple delimiters`,
|
|
||||||
"{{foo}} \\{{bar}} \\{{baz}}",
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokContent("{{baz}}"), tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`supports escaping a triple stash`,
|
|
||||||
"{{foo}} \\{{{bar}}} {{baz}}",
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{{bar}}} "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`supports escaping escape character`,
|
|
||||||
"{{foo}} \\\\{{bar}} {{baz}}",
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`supports escaping multiple escape characters`,
|
|
||||||
"{{foo}} \\\\{{bar}} \\\\{{baz}}",
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" \\"), tokOpen, tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`supports escaped mustaches after escaped escape characters`,
|
|
||||||
"{{foo}} \\\\{{bar}} \\{{baz}}",
|
|
||||||
// NOTE: JS implementation returns:
|
|
||||||
// ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT'],
|
|
||||||
// WTF is the last CONTENT ?
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" "), tokContent("{{baz}}"), tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`supports escaped escape characters after escaped mustaches`,
|
|
||||||
"{{foo}} \\{{bar}} \\\\{{baz}}",
|
|
||||||
// NOTE: JS implementation returns:
|
|
||||||
// []Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokContent("\\"), tokOpen, tokID("baz"), tokClose, tokEOF},
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} \\"), tokOpen, tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`supports escaped escape character on a triple stash`,
|
|
||||||
"{{foo}} \\\\{{{bar}}} {{baz}}",
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpenUnescaped, tokID("bar"), tokCloseUnescaped, tokContent(" "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a simple path`,
|
|
||||||
`{{foo/bar}}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokSep("/"), tokID("bar"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`allows dot notation (1)`,
|
|
||||||
`{{foo.bar}}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("bar"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`allows dot notation (2)`,
|
|
||||||
`{{foo.bar.baz}}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("bar"), tokSep("."), tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`allows path literals with []`,
|
|
||||||
`{{foo.[bar]}}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("[bar]"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`allows multiple path literals on a line with []`,
|
|
||||||
`{{foo.[bar]}}{{foo.[baz]}}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("[bar]"), tokClose, tokOpen, tokID("foo"), tokSep("."), tokID("[baz]"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes {{.}} as OPEN ID CLOSE`,
|
|
||||||
`{{.}}`,
|
|
||||||
[]Token{tokOpen, tokID("."), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a path as "OPEN (ID SEP)* ID CLOSE"`,
|
|
||||||
`{{../foo/bar}}`,
|
|
||||||
[]Token{tokOpen, tokID(".."), tokSep("/"), tokID("foo"), tokSep("/"), tokID("bar"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a path with .. as a parent path`,
|
|
||||||
`{{../foo.bar}}`,
|
|
||||||
[]Token{tokOpen, tokID(".."), tokSep("/"), tokID("foo"), tokSep("."), tokID("bar"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a path with this/foo as OPEN ID SEP ID CLOSE`,
|
|
||||||
`{{this/foo}}`,
|
|
||||||
[]Token{tokOpen, tokID("this"), tokSep("/"), tokID("foo"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a simple mustache with spaces as "OPEN ID CLOSE"`,
|
|
||||||
`{{ foo }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a simple mustache with line breaks as "OPEN ID ID CLOSE"`,
|
|
||||||
"{{ foo \n bar }}",
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes raw content as "CONTENT"`,
|
|
||||||
`foo {{ bar }} baz`,
|
|
||||||
[]Token{tokContent("foo "), tokOpen, tokID("bar"), tokClose, tokContent(" baz"), tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a partial as "OPEN_PARTIAL ID CLOSE"`,
|
|
||||||
`{{> foo}}`,
|
|
||||||
[]Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"`,
|
|
||||||
`{{> foo bar }}`,
|
|
||||||
[]Token{tokOpenPartial, tokID("foo"), tokID("bar"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"`,
|
|
||||||
`{{>foo}}`,
|
|
||||||
[]Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"`,
|
|
||||||
`{{>foo }}`,
|
|
||||||
[]Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"`,
|
|
||||||
`{{>foo/bar.baz }}`,
|
|
||||||
[]Token{tokOpenPartial, tokID("foo"), tokSep("/"), tokID("bar"), tokSep("."), tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a comment as "COMMENT"`,
|
|
||||||
`foo {{! this is a comment }} bar {{ baz }}`,
|
|
||||||
[]Token{tokContent("foo "), tokComment("{{! this is a comment }}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a block comment as "COMMENT"`,
|
|
||||||
`foo {{!-- this is a {{comment}} --}} bar {{ baz }}`,
|
|
||||||
[]Token{tokContent("foo "), tokComment("{{!-- this is a {{comment}} --}}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes a block comment with whitespace as "COMMENT"`,
|
|
||||||
"foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}",
|
|
||||||
[]Token{tokContent("foo "), tokComment("{{!-- this is a\n{{comment}}\n--}}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE`,
|
|
||||||
`{{#foo}}content{{/foo}}`,
|
|
||||||
[]Token{tokOpenBlock, tokID("foo"), tokClose, tokContent("content"), tokOpenEndBlock, tokID("foo"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes inverse sections as "INVERSE"`,
|
|
||||||
`{{^}}`,
|
|
||||||
[]Token{tokInverse("{{^}}"), tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes inverse sections as "INVERSE" with alternate format`,
|
|
||||||
`{{else}}`,
|
|
||||||
[]Token{tokInverse("{{else}}"), tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes inverse sections as "INVERSE" with spaces`,
|
|
||||||
`{{ else }}`,
|
|
||||||
[]Token{tokInverse("{{ else }}"), tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"`,
|
|
||||||
`{{^foo}}`,
|
|
||||||
[]Token{tokOpenInverse, tokID("foo"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"`,
|
|
||||||
`{{^ foo }}`,
|
|
||||||
[]Token{tokOpenInverse, tokID("foo"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes mustaches with params as "OPEN ID ID ID CLOSE"`,
|
|
||||||
`{{ foo bar baz }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"`,
|
|
||||||
`{{ foo bar "baz" }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes mustaches with String params using single quotes as "OPEN ID ID STRING CLOSE"`,
|
|
||||||
`{{ foo bar 'baz' }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes String params with spaces inside as "STRING"`,
|
|
||||||
`{{ foo bar "baz bat" }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz bat"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes String params with escapes quotes as STRING`,
|
|
||||||
`{{ foo "bar\"baz" }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokString(`bar"baz`), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes String params using single quotes with escapes quotes as STRING`,
|
|
||||||
`{{ foo 'bar\'baz' }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokString(`bar'baz`), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes numbers`,
|
|
||||||
`{{ foo 1 }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokNumber("1"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes floats`,
|
|
||||||
`{{ foo 1.1 }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokNumber("1.1"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes negative numbers`,
|
|
||||||
`{{ foo -1 }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokNumber("-1"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes negative floats`,
|
|
||||||
`{{ foo -1.1 }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokNumber("-1.1"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes boolean true`,
|
|
||||||
`{{ foo true }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokBool("true"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes boolean false`,
|
|
||||||
`{{ foo false }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokBool("false"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
// SKIP: 'tokenizes undefined and null'
|
|
||||||
{
|
|
||||||
`tokenizes hash arguments (1)`,
|
|
||||||
`{{ foo bar=baz }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokEquals, tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes hash arguments (2)`,
|
|
||||||
`{{ foo bar baz=bat }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokID("bat"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes hash arguments (3)`,
|
|
||||||
`{{ foo bar baz=1 }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokNumber("1"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes hash arguments (4)`,
|
|
||||||
`{{ foo bar baz=true }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokBool("true"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes hash arguments (5)`,
|
|
||||||
`{{ foo bar baz=false }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokBool("false"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes hash arguments (6)`,
|
|
||||||
"{{ foo bar\n baz=bat }}",
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokID("bat"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes hash arguments (7)`,
|
|
||||||
`{{ foo bar baz="bat" }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokString("bat"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes hash arguments (8)`,
|
|
||||||
`{{ foo bar baz="bat" bam=wot }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokString("bat"), tokID("bam"), tokEquals, tokID("wot"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes hash arguments (9)`,
|
|
||||||
`{{foo omg bar=baz bat="bam"}}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("omg"), tokID("bar"), tokEquals, tokID("baz"), tokID("bat"), tokEquals, tokString("bam"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes special @ identifiers (1)`,
|
|
||||||
`{{ @foo }}`,
|
|
||||||
[]Token{tokOpen, tokData, tokID("foo"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes special @ identifiers (2)`,
|
|
||||||
`{{ foo @bar }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokData, tokID("bar"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes special @ identifiers (3)`,
|
|
||||||
`{{ foo bar=@baz }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokEquals, tokData, tokID("baz"), tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`does not time out in a mustache with a single } followed by EOF`,
|
|
||||||
`{{foo}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokError("Unexpected character in expression: '}'")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`does not time out in a mustache when invalid ID characters are used`,
|
|
||||||
`{{foo & }}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokError("Unexpected character in expression: '&'")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes subexpressions (1)`,
|
|
||||||
`{{foo (bar)}}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokCloseSexpr, tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes subexpressions (2)`,
|
|
||||||
`{{foo (a-x b-y)}}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("a-x"), tokID("b-y"), tokCloseSexpr, tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes nested subexpressions`,
|
|
||||||
`{{foo (bar (lol rofl)) (baz)}}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokOpenSexpr, tokID("lol"), tokID("rofl"), tokCloseSexpr, tokCloseSexpr, tokOpenSexpr, tokID("baz"), tokCloseSexpr, tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes nested subexpressions: literals`,
|
|
||||||
`{{foo (bar (lol true) false) (baz 1) (blah 'b') (blorg "c")}}`,
|
|
||||||
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokOpenSexpr, tokID("lol"), tokBool("true"), tokCloseSexpr, tokBool("false"), tokCloseSexpr, tokOpenSexpr, tokID("baz"), tokNumber("1"), tokCloseSexpr, tokOpenSexpr, tokID("blah"), tokString("b"), tokCloseSexpr, tokOpenSexpr, tokID("blorg"), tokString("c"), tokCloseSexpr, tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes block params (1)`,
|
|
||||||
`{{#foo as |bar|}}`,
|
|
||||||
[]Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokCloseBlockParams, tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes block params (2)`,
|
|
||||||
`{{#foo as |bar baz|}}`,
|
|
||||||
[]Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes block params (3)`,
|
|
||||||
`{{#foo as | bar baz |}}`,
|
|
||||||
[]Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes block params (4)`,
|
|
||||||
`{{#foo as as | bar baz |}}`,
|
|
||||||
[]Token{tokOpenBlock, tokID("foo"), tokID("as"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`tokenizes block params (5)`,
|
|
||||||
`{{else foo as |bar baz|}}`,
|
|
||||||
[]Token{tokOpenInverseChain, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func collect(t *lexTest) []Token {
|
|
||||||
var result []Token
|
|
||||||
|
|
||||||
l := scanWithName(t.input, t.name)
|
|
||||||
for {
|
|
||||||
token := l.NextToken()
|
|
||||||
result = append(result, token)
|
|
||||||
|
|
||||||
if token.Kind == TokenEOF || token.Kind == TokenError {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func equal(i1, i2 []Token, checkPos bool) bool {
|
|
||||||
if len(i1) != len(i2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for k := range i1 {
|
|
||||||
if i1[k].Kind != i2[k].Kind {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if checkPos && i1[k].Pos != i2[k].Pos {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if i1[k].Val != i2[k].Val {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLexer(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, test := range lexTests {
|
|
||||||
tokens := collect(&test)
|
|
||||||
if !equal(tokens, test.tokens, false) {
|
|
||||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\nexpected\n\t%v\ngot\n\t%+v\n", test.name, test.input, test.tokens, tokens)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @todo Test errors:
|
|
||||||
// `{{{{raw foo`
|
|
||||||
|
|
||||||
// package example
|
|
||||||
func Example() {
|
|
||||||
source := "You know {{nothing}} John Snow"
|
|
||||||
|
|
||||||
output := ""
|
|
||||||
|
|
||||||
lex := Scan(source)
|
|
||||||
for {
|
|
||||||
// consume next token
|
|
||||||
token := lex.NextToken()
|
|
||||||
|
|
||||||
output += fmt.Sprintf(" %s", token)
|
|
||||||
|
|
||||||
// stops when all tokens have been consumed, or on error
|
|
||||||
if token.Kind == TokenEOF || token.Kind == TokenError {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Print(output)
|
|
||||||
// Output: Content{"You know "} Open{"{{"} ID{"nothing"} Close{"}}"} Content{" John Snow"} EOF
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
package lexer
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
const (
|
|
||||||
TokenError TokenKind = iota
|
|
||||||
TokenEOF
|
|
||||||
|
|
||||||
// mustache delimiters
|
|
||||||
TokenOpen // OPEN
|
|
||||||
TokenClose // CLOSE
|
|
||||||
TokenOpenRawBlock // OPEN_RAW_BLOCK
|
|
||||||
TokenCloseRawBlock // CLOSE_RAW_BLOCK
|
|
||||||
TokenOpenEndRawBlock // END_RAW_BLOCK
|
|
||||||
TokenOpenUnescaped // OPEN_UNESCAPED
|
|
||||||
TokenCloseUnescaped // CLOSE_UNESCAPED
|
|
||||||
TokenOpenBlock // OPEN_BLOCK
|
|
||||||
TokenOpenEndBlock // OPEN_ENDBLOCK
|
|
||||||
TokenInverse // INVERSE
|
|
||||||
TokenOpenInverse // OPEN_INVERSE
|
|
||||||
TokenOpenInverseChain // OPEN_INVERSE_CHAIN
|
|
||||||
TokenOpenPartial // OPEN_PARTIAL
|
|
||||||
TokenComment // COMMENT
|
|
||||||
|
|
||||||
// inside mustaches
|
|
||||||
TokenOpenSexpr // OPEN_SEXPR
|
|
||||||
TokenCloseSexpr // CLOSE_SEXPR
|
|
||||||
TokenEquals // EQUALS
|
|
||||||
TokenData // DATA
|
|
||||||
TokenSep // SEP
|
|
||||||
TokenOpenBlockParams // OPEN_BLOCK_PARAMS
|
|
||||||
TokenCloseBlockParams // CLOSE_BLOCK_PARAMS
|
|
||||||
|
|
||||||
// tokens with content
|
|
||||||
TokenContent // CONTENT
|
|
||||||
TokenID // ID
|
|
||||||
TokenString // STRING
|
|
||||||
TokenNumber // NUMBER
|
|
||||||
TokenBoolean // BOOLEAN
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Option to generate token position in its string representation
|
|
||||||
DUMP_TOKEN_POS = false
|
|
||||||
|
|
||||||
// Option to generate values for all token kinds for their string representations
|
|
||||||
DUMP_ALL_TOKENS_VAL = true
|
|
||||||
)
|
|
||||||
|
|
||||||
// TokenKind represents a Token type.
|
|
||||||
type TokenKind int
|
|
||||||
|
|
||||||
// Token represents a scanned token.
|
|
||||||
type Token struct {
|
|
||||||
Kind TokenKind // Token kind
|
|
||||||
Val string // Token value
|
|
||||||
|
|
||||||
Pos int // Byte position in input string
|
|
||||||
Line int // Line number in input string
|
|
||||||
}
|
|
||||||
|
|
||||||
// tokenName permits to display token name given token type
|
|
||||||
var tokenName = map[TokenKind]string{
|
|
||||||
TokenError: "Error",
|
|
||||||
TokenEOF: "EOF",
|
|
||||||
TokenContent: "Content",
|
|
||||||
TokenComment: "Comment",
|
|
||||||
TokenOpen: "Open",
|
|
||||||
TokenClose: "Close",
|
|
||||||
TokenOpenUnescaped: "OpenUnescaped",
|
|
||||||
TokenCloseUnescaped: "CloseUnescaped",
|
|
||||||
TokenOpenBlock: "OpenBlock",
|
|
||||||
TokenOpenEndBlock: "OpenEndBlock",
|
|
||||||
TokenOpenRawBlock: "OpenRawBlock",
|
|
||||||
TokenCloseRawBlock: "CloseRawBlock",
|
|
||||||
TokenOpenEndRawBlock: "OpenEndRawBlock",
|
|
||||||
TokenOpenBlockParams: "OpenBlockParams",
|
|
||||||
TokenCloseBlockParams: "CloseBlockParams",
|
|
||||||
TokenInverse: "Inverse",
|
|
||||||
TokenOpenInverse: "OpenInverse",
|
|
||||||
TokenOpenInverseChain: "OpenInverseChain",
|
|
||||||
TokenOpenPartial: "OpenPartial",
|
|
||||||
TokenOpenSexpr: "OpenSexpr",
|
|
||||||
TokenCloseSexpr: "CloseSexpr",
|
|
||||||
TokenID: "ID",
|
|
||||||
TokenEquals: "Equals",
|
|
||||||
TokenString: "String",
|
|
||||||
TokenNumber: "Number",
|
|
||||||
TokenBoolean: "Boolean",
|
|
||||||
TokenData: "Data",
|
|
||||||
TokenSep: "Sep",
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the token kind string representation for debugging.
|
|
||||||
func (k TokenKind) String() string {
|
|
||||||
s := tokenName[k]
|
|
||||||
if s == "" {
|
|
||||||
return fmt.Sprintf("Token-%d", int(k))
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the token string representation for debugging.
|
|
||||||
func (t Token) String() string {
|
|
||||||
result := ""
|
|
||||||
|
|
||||||
if DUMP_TOKEN_POS {
|
|
||||||
result += fmt.Sprintf("%d:", t.Pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
result += fmt.Sprintf("%s", t.Kind)
|
|
||||||
|
|
||||||
if (DUMP_ALL_TOKENS_VAL || (t.Kind >= TokenContent)) && len(t.Val) > 0 {
|
|
||||||
if len(t.Val) > 100 {
|
|
||||||
result += fmt.Sprintf("{%.20q...}", t.Val)
|
|
||||||
} else {
|
|
||||||
result += fmt.Sprintf("{%q}", t.Val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
|
@ -1,234 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"path"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Note, as the JS implementation, the divergences from mustache spec:
|
|
||||||
// - we don't support alternative delimeters
|
|
||||||
// - the mustache lambda spec differs
|
|
||||||
//
|
|
||||||
|
|
||||||
type mustacheTest struct {
|
|
||||||
Name string
|
|
||||||
Desc string
|
|
||||||
Data interface{}
|
|
||||||
Template string
|
|
||||||
Expected string
|
|
||||||
Partials map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type mustacheTestFile struct {
|
|
||||||
Overview string
|
|
||||||
Tests []mustacheTest
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
rAltDelim = regexp.MustCompile(regexp.QuoteMeta("{{="))
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
musTestLambdaInterMult = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMustache(t *testing.T) {
|
|
||||||
skipFiles := map[string]bool{
|
|
||||||
// mustache lambdas differ from handlebars lambdas
|
|
||||||
"~lambdas.yml": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fileName := range mustacheTestFiles() {
|
|
||||||
if skipFiles[fileName] {
|
|
||||||
// fmt.Printf("Skipped file: %s\n", fileName)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
launchTests(t, testsFromMustacheFile(fileName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testsFromMustacheFile(fileName string) []Test {
|
|
||||||
result := []Test{}
|
|
||||||
|
|
||||||
fileData, err := ioutil.ReadFile(path.Join("mustache", "specs", fileName))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var testFile mustacheTestFile
|
|
||||||
if err := yaml.Unmarshal(fileData, &testFile); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mustacheTest := range testFile.Tests {
|
|
||||||
if mustBeSkipped(mustacheTest, fileName) {
|
|
||||||
// fmt.Printf("Skipped test: %s\n", mustacheTest.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
test := Test{
|
|
||||||
name: mustacheTest.Name,
|
|
||||||
input: mustacheTest.Template,
|
|
||||||
data: mustacheTest.Data,
|
|
||||||
partials: mustacheTest.Partials,
|
|
||||||
output: mustacheTest.Expected,
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, test)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns true if test must be skipped
|
|
||||||
func mustBeSkipped(test mustacheTest, fileName string) bool {
|
|
||||||
// handlebars does not support alternative delimiters
|
|
||||||
return haveAltDelimiter(test) ||
|
|
||||||
// the JS implementation skips those tests
|
|
||||||
fileName == "partials.yml" && (test.Name == "Failed Lookup" || test.Name == "Standalone Indentation")
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns true if test have alternative delimeter in template or in partials
|
|
||||||
func haveAltDelimiter(test mustacheTest) bool {
|
|
||||||
// check template
|
|
||||||
if rAltDelim.MatchString(test.Template) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// check partials
|
|
||||||
for _, partial := range test.Partials {
|
|
||||||
if rAltDelim.MatchString(partial) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustacheTestFiles() []string {
|
|
||||||
var result []string
|
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(path.Join("mustache", "specs"))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
fileName := file.Name()
|
|
||||||
|
|
||||||
if !file.IsDir() && strings.HasSuffix(fileName, ".yml") {
|
|
||||||
result = append(result, fileName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Following tests come fron ~lambdas.yml
|
|
||||||
//
|
|
||||||
|
|
||||||
var mustacheLambdasTests = []Test{
|
|
||||||
{
|
|
||||||
"Interpolation",
|
|
||||||
"Hello, {{lambda}}!",
|
|
||||||
map[string]interface{}{"lambda": func() string { return "world" }},
|
|
||||||
nil, nil, nil,
|
|
||||||
"Hello, world!",
|
|
||||||
},
|
|
||||||
|
|
||||||
// // SKIP: lambda return value is not parsed
|
|
||||||
// {
|
|
||||||
// "Interpolation - Expansion",
|
|
||||||
// "Hello, {{lambda}}!",
|
|
||||||
// map[string]interface{}{"lambda": func() string { return "{{planet}}" }},
|
|
||||||
// nil, nil, nil,
|
|
||||||
// "Hello, world!",
|
|
||||||
// },
|
|
||||||
|
|
||||||
// SKIP "Interpolation - Alternate Delimiters"
|
|
||||||
|
|
||||||
{
|
|
||||||
"Interpolation - Multiple Calls",
|
|
||||||
"{{lambda}} == {{{lambda}}} == {{lambda}}",
|
|
||||||
map[string]interface{}{"lambda": func() string {
|
|
||||||
musTestLambdaInterMult += 1
|
|
||||||
return Str(musTestLambdaInterMult)
|
|
||||||
}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"1 == 2 == 3",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"Escaping",
|
|
||||||
"<{{lambda}}{{{lambda}}}",
|
|
||||||
map[string]interface{}{"lambda": func() string { return ">" }},
|
|
||||||
nil, nil, nil,
|
|
||||||
"<>>",
|
|
||||||
},
|
|
||||||
|
|
||||||
// // SKIP: "Lambdas used for sections should receive the raw section string."
|
|
||||||
// {
|
|
||||||
// "Section",
|
|
||||||
// "<{{#lambda}}{{x}}{{/lambda}}>",
|
|
||||||
// map[string]interface{}{"lambda": func(param string) string {
|
|
||||||
// if param == "{{x}}" {
|
|
||||||
// return "yes"
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return "false"
|
|
||||||
// }, "x": "Error!"},
|
|
||||||
// nil, nil, nil,
|
|
||||||
// "<yes>",
|
|
||||||
// },
|
|
||||||
|
|
||||||
// // SKIP: lambda return value is not parsed
|
|
||||||
// {
|
|
||||||
// "Section - Expansion",
|
|
||||||
// "<{{#lambda}}-{{/lambda}}>",
|
|
||||||
// map[string]interface{}{"lambda": func(param string) string {
|
|
||||||
// return param + "{{planet}}" + param
|
|
||||||
// }, "planet": "Earth"},
|
|
||||||
// nil, nil, nil,
|
|
||||||
// "<-Earth->",
|
|
||||||
// },
|
|
||||||
|
|
||||||
// SKIP: "Section - Alternate Delimiters"
|
|
||||||
|
|
||||||
{
|
|
||||||
"Section - Multiple Calls",
|
|
||||||
"{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}",
|
|
||||||
map[string]interface{}{"lambda": func(options *Options) string {
|
|
||||||
return "__" + options.Fn() + "__"
|
|
||||||
}},
|
|
||||||
nil, nil, nil,
|
|
||||||
"__FILE__ != __LINE__",
|
|
||||||
},
|
|
||||||
|
|
||||||
// // SKIP: "Lambdas used for inverted sections should be considered truthy."
|
|
||||||
// {
|
|
||||||
// "Inverted Section",
|
|
||||||
// "<{{^lambda}}{{static}}{{/lambda}}>",
|
|
||||||
// map[string]interface{}{
|
|
||||||
// "lambda": func() interface{} {
|
|
||||||
// return false
|
|
||||||
// },
|
|
||||||
// "static": "static",
|
|
||||||
// },
|
|
||||||
// nil, nil, nil,
|
|
||||||
// "<>",
|
|
||||||
// },
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMustacheLambdas(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
launchTests(t, mustacheLambdasTests)
|
|
||||||
}
|
|
|
@ -1,846 +0,0 @@
|
||||||
// Package parser provides a handlebars syntax analyser. It consumes the tokens provided by the lexer to build an AST.
|
|
||||||
package parser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/aymerick/raymond/ast"
|
|
||||||
"github.com/aymerick/raymond/lexer"
|
|
||||||
)
|
|
||||||
|
|
||||||
// References:
|
|
||||||
// - https://github.com/wycats/handlebars.js/blob/master/src/handlebars.yy
|
|
||||||
// - https://github.com/golang/go/blob/master/src/text/template/parse/parse.go
|
|
||||||
|
|
||||||
// Parser is a syntax analyzer.
|
|
||||||
type parser struct {
|
|
||||||
// Lexer
|
|
||||||
lex *lexer.Lexer
|
|
||||||
|
|
||||||
// Root node
|
|
||||||
root ast.Node
|
|
||||||
|
|
||||||
// Tokens parsed but not consumed yet
|
|
||||||
tokens []*lexer.Token
|
|
||||||
|
|
||||||
// All tokens have been retreieved from lexer
|
|
||||||
lexOver bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
rOpenComment = regexp.MustCompile(`^\{\{~?!-?-?`)
|
|
||||||
rCloseComment = regexp.MustCompile(`-?-?~?\}\}$`)
|
|
||||||
rOpenAmp = regexp.MustCompile(`^\{\{~?&`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// new instanciates a new parser
|
|
||||||
func new(input string) *parser {
|
|
||||||
return &parser{
|
|
||||||
lex: lexer.Scan(input),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse analyzes given input and returns the AST root node.
|
|
||||||
func Parse(input string) (result *ast.Program, err error) {
|
|
||||||
// recover error
|
|
||||||
defer errRecover(&err)
|
|
||||||
|
|
||||||
parser := new(input)
|
|
||||||
|
|
||||||
// parse
|
|
||||||
result = parser.parseProgram()
|
|
||||||
|
|
||||||
// check last token
|
|
||||||
token := parser.shift()
|
|
||||||
if token.Kind != lexer.TokenEOF {
|
|
||||||
// Parsing ended before EOF
|
|
||||||
errToken(token, "Syntax error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// fix whitespaces
|
|
||||||
processWhitespaces(result)
|
|
||||||
|
|
||||||
// named returned values
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// errRecover recovers parsing panic
|
|
||||||
func errRecover(errp *error) {
|
|
||||||
e := recover()
|
|
||||||
if e != nil {
|
|
||||||
switch err := e.(type) {
|
|
||||||
case runtime.Error:
|
|
||||||
panic(e)
|
|
||||||
case error:
|
|
||||||
*errp = err
|
|
||||||
default:
|
|
||||||
panic(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// errPanic panics
|
|
||||||
func errPanic(err error, line int) {
|
|
||||||
panic(fmt.Errorf("Parse error on line %d:\n%s", line, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// errNode panics with given node infos
|
|
||||||
func errNode(node ast.Node, msg string) {
|
|
||||||
errPanic(fmt.Errorf("%s\nNode: %s", msg, node), node.Location().Line)
|
|
||||||
}
|
|
||||||
|
|
||||||
// errNode panics with given Token infos
|
|
||||||
func errToken(tok *lexer.Token, msg string) {
|
|
||||||
errPanic(fmt.Errorf("%s\nToken: %s", msg, tok), tok.Line)
|
|
||||||
}
|
|
||||||
|
|
||||||
// errNode panics because of an unexpected Token kind
|
|
||||||
func errExpected(expect lexer.TokenKind, tok *lexer.Token) {
|
|
||||||
errPanic(fmt.Errorf("Expecting %s, got: '%s'", expect, tok), tok.Line)
|
|
||||||
}
|
|
||||||
|
|
||||||
// program : statement*
|
|
||||||
func (p *parser) parseProgram() *ast.Program {
|
|
||||||
result := ast.NewProgram(p.lex.Pos(), p.lex.Line())
|
|
||||||
|
|
||||||
for p.isStatement() {
|
|
||||||
result.AddStatement(p.parseStatement())
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// statement : mustache | block | rawBlock | partial | content | COMMENT
|
|
||||||
func (p *parser) parseStatement() ast.Node {
|
|
||||||
var result ast.Node
|
|
||||||
|
|
||||||
tok := p.next()
|
|
||||||
|
|
||||||
switch tok.Kind {
|
|
||||||
case lexer.TokenOpen, lexer.TokenOpenUnescaped:
|
|
||||||
// mustache
|
|
||||||
result = p.parseMustache()
|
|
||||||
case lexer.TokenOpenBlock:
|
|
||||||
// block
|
|
||||||
result = p.parseBlock()
|
|
||||||
case lexer.TokenOpenInverse:
|
|
||||||
// block
|
|
||||||
result = p.parseInverse()
|
|
||||||
case lexer.TokenOpenRawBlock:
|
|
||||||
// rawBlock
|
|
||||||
result = p.parseRawBlock()
|
|
||||||
case lexer.TokenOpenPartial:
|
|
||||||
// partial
|
|
||||||
result = p.parsePartial()
|
|
||||||
case lexer.TokenContent:
|
|
||||||
// content
|
|
||||||
result = p.parseContent()
|
|
||||||
case lexer.TokenComment:
|
|
||||||
// COMMENT
|
|
||||||
result = p.parseComment()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// isStatement returns true if next token starts a statement
|
|
||||||
func (p *parser) isStatement() bool {
|
|
||||||
if !p.have(1) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch p.next().Kind {
|
|
||||||
case lexer.TokenOpen, lexer.TokenOpenUnescaped, lexer.TokenOpenBlock,
|
|
||||||
lexer.TokenOpenInverse, lexer.TokenOpenRawBlock, lexer.TokenOpenPartial,
|
|
||||||
lexer.TokenContent, lexer.TokenComment:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// content : CONTENT
|
|
||||||
func (p *parser) parseContent() *ast.ContentStatement {
|
|
||||||
// CONTENT
|
|
||||||
tok := p.shift()
|
|
||||||
if tok.Kind != lexer.TokenContent {
|
|
||||||
// @todo This check can be removed if content is optional in a raw block
|
|
||||||
errExpected(lexer.TokenContent, tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ast.NewContentStatement(tok.Pos, tok.Line, tok.Val)
|
|
||||||
}
|
|
||||||
|
|
||||||
// COMMENT
|
|
||||||
func (p *parser) parseComment() *ast.CommentStatement {
|
|
||||||
// COMMENT
|
|
||||||
tok := p.shift()
|
|
||||||
|
|
||||||
value := rOpenComment.ReplaceAllString(tok.Val, "")
|
|
||||||
value = rCloseComment.ReplaceAllString(value, "")
|
|
||||||
|
|
||||||
result := ast.NewCommentStatement(tok.Pos, tok.Line, value)
|
|
||||||
result.Strip = ast.NewStripForStr(tok.Val)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// param* hash?
|
|
||||||
func (p *parser) parseExpressionParamsHash() ([]ast.Node, *ast.Hash) {
|
|
||||||
var params []ast.Node
|
|
||||||
var hash *ast.Hash
|
|
||||||
|
|
||||||
// params*
|
|
||||||
if p.isParam() {
|
|
||||||
params = p.parseParams()
|
|
||||||
}
|
|
||||||
|
|
||||||
// hash?
|
|
||||||
if p.isHashSegment() {
|
|
||||||
hash = p.parseHash()
|
|
||||||
}
|
|
||||||
|
|
||||||
return params, hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// helperName param* hash?
|
|
||||||
func (p *parser) parseExpression(tok *lexer.Token) *ast.Expression {
|
|
||||||
result := ast.NewExpression(tok.Pos, tok.Line)
|
|
||||||
|
|
||||||
// helperName
|
|
||||||
result.Path = p.parseHelperName()
|
|
||||||
|
|
||||||
// param* hash?
|
|
||||||
result.Params, result.Hash = p.parseExpressionParamsHash()
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// rawBlock : openRawBlock content endRawBlock
|
|
||||||
// openRawBlock : OPEN_RAW_BLOCK helperName param* hash? CLOSE_RAW_BLOCK
|
|
||||||
// endRawBlock : OPEN_END_RAW_BLOCK helperName CLOSE_RAW_BLOCK
|
|
||||||
func (p *parser) parseRawBlock() *ast.BlockStatement {
|
|
||||||
// OPEN_RAW_BLOCK
|
|
||||||
tok := p.shift()
|
|
||||||
|
|
||||||
result := ast.NewBlockStatement(tok.Pos, tok.Line)
|
|
||||||
|
|
||||||
// helperName param* hash?
|
|
||||||
result.Expression = p.parseExpression(tok)
|
|
||||||
|
|
||||||
openName := result.Expression.Canonical()
|
|
||||||
|
|
||||||
// CLOSE_RAW_BLOCK
|
|
||||||
tok = p.shift()
|
|
||||||
if tok.Kind != lexer.TokenCloseRawBlock {
|
|
||||||
errExpected(lexer.TokenCloseRawBlock, tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
// content
|
|
||||||
// @todo Is content mandatory in a raw block ?
|
|
||||||
content := p.parseContent()
|
|
||||||
|
|
||||||
program := ast.NewProgram(tok.Pos, tok.Line)
|
|
||||||
program.AddStatement(content)
|
|
||||||
|
|
||||||
result.Program = program
|
|
||||||
|
|
||||||
// OPEN_END_RAW_BLOCK
|
|
||||||
tok = p.shift()
|
|
||||||
if tok.Kind != lexer.TokenOpenEndRawBlock {
|
|
||||||
// should never happen as it is caught by lexer
|
|
||||||
errExpected(lexer.TokenOpenEndRawBlock, tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helperName
|
|
||||||
endId := p.parseHelperName()
|
|
||||||
|
|
||||||
closeName, ok := ast.HelperNameStr(endId)
|
|
||||||
if !ok {
|
|
||||||
errNode(endId, "Erroneous closing expression")
|
|
||||||
}
|
|
||||||
|
|
||||||
if openName != closeName {
|
|
||||||
errNode(endId, fmt.Sprintf("%s doesn't match %s", openName, closeName))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CLOSE_RAW_BLOCK
|
|
||||||
tok = p.shift()
|
|
||||||
if tok.Kind != lexer.TokenCloseRawBlock {
|
|
||||||
errExpected(lexer.TokenCloseRawBlock, tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// block : openBlock program inverseChain? closeBlock
|
|
||||||
func (p *parser) parseBlock() *ast.BlockStatement {
|
|
||||||
// openBlock
|
|
||||||
result, blockParams := p.parseOpenBlock()
|
|
||||||
|
|
||||||
// program
|
|
||||||
program := p.parseProgram()
|
|
||||||
program.BlockParams = blockParams
|
|
||||||
result.Program = program
|
|
||||||
|
|
||||||
// inverseChain?
|
|
||||||
if p.isInverseChain() {
|
|
||||||
result.Inverse = p.parseInverseChain()
|
|
||||||
}
|
|
||||||
|
|
||||||
// closeBlock
|
|
||||||
p.parseCloseBlock(result)
|
|
||||||
|
|
||||||
setBlockInverseStrip(result)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// setBlockInverseStrip is called when parsing `block` (openBlock | openInverse) and `inverseChain`
|
|
||||||
//
|
|
||||||
// TODO: This was totally cargo culted ! CHECK THAT !
|
|
||||||
//
|
|
||||||
// cf. prepareBlock() in:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/helper.js
|
|
||||||
func setBlockInverseStrip(block *ast.BlockStatement) {
|
|
||||||
if block.Inverse == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if block.Inverse.Chained {
|
|
||||||
b, _ := block.Inverse.Body[0].(*ast.BlockStatement)
|
|
||||||
b.CloseStrip = block.CloseStrip
|
|
||||||
}
|
|
||||||
|
|
||||||
block.InverseStrip = block.Inverse.Strip
|
|
||||||
}
|
|
||||||
|
|
||||||
// block : openInverse program inverseAndProgram? closeBlock
|
|
||||||
func (p *parser) parseInverse() *ast.BlockStatement {
|
|
||||||
// openInverse
|
|
||||||
result, blockParams := p.parseOpenBlock()
|
|
||||||
|
|
||||||
// program
|
|
||||||
program := p.parseProgram()
|
|
||||||
|
|
||||||
program.BlockParams = blockParams
|
|
||||||
result.Inverse = program
|
|
||||||
|
|
||||||
// inverseAndProgram?
|
|
||||||
if p.isInverse() {
|
|
||||||
result.Program = p.parseInverseAndProgram()
|
|
||||||
}
|
|
||||||
|
|
||||||
// closeBlock
|
|
||||||
p.parseCloseBlock(result)
|
|
||||||
|
|
||||||
setBlockInverseStrip(result)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// helperName param* hash? blockParams?
|
|
||||||
func (p *parser) parseOpenBlockExpression(tok *lexer.Token) (*ast.BlockStatement, []string) {
|
|
||||||
var blockParams []string
|
|
||||||
|
|
||||||
result := ast.NewBlockStatement(tok.Pos, tok.Line)
|
|
||||||
|
|
||||||
// helperName param* hash?
|
|
||||||
result.Expression = p.parseExpression(tok)
|
|
||||||
|
|
||||||
// blockParams?
|
|
||||||
if p.isBlockParams() {
|
|
||||||
blockParams = p.parseBlockParams()
|
|
||||||
}
|
|
||||||
|
|
||||||
// named returned values
|
|
||||||
return result, blockParams
|
|
||||||
}
|
|
||||||
|
|
||||||
// inverseChain : openInverseChain program inverseChain?
|
|
||||||
// | inverseAndProgram
|
|
||||||
func (p *parser) parseInverseChain() *ast.Program {
|
|
||||||
if p.isInverse() {
|
|
||||||
// inverseAndProgram
|
|
||||||
return p.parseInverseAndProgram()
|
|
||||||
} else {
|
|
||||||
result := ast.NewProgram(p.lex.Pos(), p.lex.Line())
|
|
||||||
|
|
||||||
// openInverseChain
|
|
||||||
block, blockParams := p.parseOpenBlock()
|
|
||||||
|
|
||||||
// program
|
|
||||||
program := p.parseProgram()
|
|
||||||
|
|
||||||
program.BlockParams = blockParams
|
|
||||||
block.Program = program
|
|
||||||
|
|
||||||
// inverseChain?
|
|
||||||
if p.isInverseChain() {
|
|
||||||
block.Inverse = p.parseInverseChain()
|
|
||||||
}
|
|
||||||
|
|
||||||
setBlockInverseStrip(block)
|
|
||||||
|
|
||||||
result.Chained = true
|
|
||||||
result.AddStatement(block)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if current token starts an inverse chain
|
|
||||||
func (p *parser) isInverseChain() bool {
|
|
||||||
return p.isOpenInverseChain() || p.isInverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
// inverseAndProgram : INVERSE program
|
|
||||||
func (p *parser) parseInverseAndProgram() *ast.Program {
|
|
||||||
// INVERSE
|
|
||||||
tok := p.shift()
|
|
||||||
|
|
||||||
// program
|
|
||||||
result := p.parseProgram()
|
|
||||||
result.Strip = ast.NewStripForStr(tok.Val)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// openBlock : OPEN_BLOCK helperName param* hash? blockParams? CLOSE
|
|
||||||
// openInverse : OPEN_INVERSE helperName param* hash? blockParams? CLOSE
|
|
||||||
// openInverseChain: OPEN_INVERSE_CHAIN helperName param* hash? blockParams? CLOSE
|
|
||||||
func (p *parser) parseOpenBlock() (*ast.BlockStatement, []string) {
|
|
||||||
// OPEN_BLOCK | OPEN_INVERSE | OPEN_INVERSE_CHAIN
|
|
||||||
tok := p.shift()
|
|
||||||
|
|
||||||
// helperName param* hash? blockParams?
|
|
||||||
result, blockParams := p.parseOpenBlockExpression(tok)
|
|
||||||
|
|
||||||
// CLOSE
|
|
||||||
tokClose := p.shift()
|
|
||||||
if tokClose.Kind != lexer.TokenClose {
|
|
||||||
errExpected(lexer.TokenClose, tokClose)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.OpenStrip = ast.NewStrip(tok.Val, tokClose.Val)
|
|
||||||
|
|
||||||
// named returned values
|
|
||||||
return result, blockParams
|
|
||||||
}
|
|
||||||
|
|
||||||
// closeBlock : OPEN_ENDBLOCK helperName CLOSE
|
|
||||||
func (p *parser) parseCloseBlock(block *ast.BlockStatement) {
|
|
||||||
// OPEN_ENDBLOCK
|
|
||||||
tok := p.shift()
|
|
||||||
if tok.Kind != lexer.TokenOpenEndBlock {
|
|
||||||
errExpected(lexer.TokenOpenEndBlock, tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helperName
|
|
||||||
endId := p.parseHelperName()
|
|
||||||
|
|
||||||
closeName, ok := ast.HelperNameStr(endId)
|
|
||||||
if !ok {
|
|
||||||
errNode(endId, "Erroneous closing expression")
|
|
||||||
}
|
|
||||||
|
|
||||||
openName := block.Expression.Canonical()
|
|
||||||
if openName != closeName {
|
|
||||||
errNode(endId, fmt.Sprintf("%s doesn't match %s", openName, closeName))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CLOSE
|
|
||||||
tokClose := p.shift()
|
|
||||||
if tokClose.Kind != lexer.TokenClose {
|
|
||||||
errExpected(lexer.TokenClose, tokClose)
|
|
||||||
}
|
|
||||||
|
|
||||||
block.CloseStrip = ast.NewStrip(tok.Val, tokClose.Val)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mustache : OPEN helperName param* hash? CLOSE
|
|
||||||
// | OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED
|
|
||||||
func (p *parser) parseMustache() *ast.MustacheStatement {
|
|
||||||
// OPEN | OPEN_UNESCAPED
|
|
||||||
tok := p.shift()
|
|
||||||
|
|
||||||
closeToken := lexer.TokenClose
|
|
||||||
if tok.Kind == lexer.TokenOpenUnescaped {
|
|
||||||
closeToken = lexer.TokenCloseUnescaped
|
|
||||||
}
|
|
||||||
|
|
||||||
unescaped := false
|
|
||||||
if (tok.Kind == lexer.TokenOpenUnescaped) || (rOpenAmp.MatchString(tok.Val)) {
|
|
||||||
unescaped = true
|
|
||||||
}
|
|
||||||
|
|
||||||
result := ast.NewMustacheStatement(tok.Pos, tok.Line, unescaped)
|
|
||||||
|
|
||||||
// helperName param* hash?
|
|
||||||
result.Expression = p.parseExpression(tok)
|
|
||||||
|
|
||||||
// CLOSE | CLOSE_UNESCAPED
|
|
||||||
tokClose := p.shift()
|
|
||||||
if tokClose.Kind != closeToken {
|
|
||||||
errExpected(closeToken, tokClose)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Strip = ast.NewStrip(tok.Val, tokClose.Val)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// partial : OPEN_PARTIAL partialName param* hash? CLOSE
|
|
||||||
func (p *parser) parsePartial() *ast.PartialStatement {
|
|
||||||
// OPEN_PARTIAL
|
|
||||||
tok := p.shift()
|
|
||||||
|
|
||||||
result := ast.NewPartialStatement(tok.Pos, tok.Line)
|
|
||||||
|
|
||||||
// partialName
|
|
||||||
result.Name = p.parsePartialName()
|
|
||||||
|
|
||||||
// param* hash?
|
|
||||||
result.Params, result.Hash = p.parseExpressionParamsHash()
|
|
||||||
|
|
||||||
// CLOSE
|
|
||||||
tokClose := p.shift()
|
|
||||||
if tokClose.Kind != lexer.TokenClose {
|
|
||||||
errExpected(lexer.TokenClose, tokClose)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Strip = ast.NewStrip(tok.Val, tokClose.Val)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// helperName | sexpr
|
|
||||||
func (p *parser) parseHelperNameOrSexpr() ast.Node {
|
|
||||||
if p.isSexpr() {
|
|
||||||
// sexpr
|
|
||||||
return p.parseSexpr()
|
|
||||||
} else {
|
|
||||||
// helperName
|
|
||||||
return p.parseHelperName()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// param : helperName | sexpr
|
|
||||||
func (p *parser) parseParam() ast.Node {
|
|
||||||
return p.parseHelperNameOrSexpr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if next tokens represent a `param`
|
|
||||||
func (p *parser) isParam() bool {
|
|
||||||
return (p.isSexpr() || p.isHelperName()) && !p.isHashSegment()
|
|
||||||
}
|
|
||||||
|
|
||||||
// param*
|
|
||||||
func (p *parser) parseParams() []ast.Node {
|
|
||||||
var result []ast.Node
|
|
||||||
|
|
||||||
for p.isParam() {
|
|
||||||
result = append(result, p.parseParam())
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// sexpr : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR
|
|
||||||
func (p *parser) parseSexpr() *ast.SubExpression {
|
|
||||||
// OPEN_SEXPR
|
|
||||||
tok := p.shift()
|
|
||||||
|
|
||||||
result := ast.NewSubExpression(tok.Pos, tok.Line)
|
|
||||||
|
|
||||||
// helperName param* hash?
|
|
||||||
result.Expression = p.parseExpression(tok)
|
|
||||||
|
|
||||||
// CLOSE_SEXPR
|
|
||||||
tok = p.shift()
|
|
||||||
if tok.Kind != lexer.TokenCloseSexpr {
|
|
||||||
errExpected(lexer.TokenCloseSexpr, tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// hash : hashSegment+
|
|
||||||
func (p *parser) parseHash() *ast.Hash {
|
|
||||||
var pairs []*ast.HashPair
|
|
||||||
|
|
||||||
for p.isHashSegment() {
|
|
||||||
pairs = append(pairs, p.parseHashSegment())
|
|
||||||
}
|
|
||||||
|
|
||||||
firstLoc := pairs[0].Location()
|
|
||||||
|
|
||||||
result := ast.NewHash(firstLoc.Pos, firstLoc.Line)
|
|
||||||
result.Pairs = pairs
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns true if next tokens represents a `hashSegment`
|
|
||||||
func (p *parser) isHashSegment() bool {
|
|
||||||
return p.have(2) && (p.next().Kind == lexer.TokenID) && (p.nextAt(1).Kind == lexer.TokenEquals)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hashSegment : ID EQUALS param
|
|
||||||
func (p *parser) parseHashSegment() *ast.HashPair {
|
|
||||||
// ID
|
|
||||||
tok := p.shift()
|
|
||||||
|
|
||||||
// EQUALS
|
|
||||||
p.shift()
|
|
||||||
|
|
||||||
// param
|
|
||||||
param := p.parseParam()
|
|
||||||
|
|
||||||
result := ast.NewHashPair(tok.Pos, tok.Line)
|
|
||||||
result.Key = tok.Val
|
|
||||||
result.Val = param
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// blockParams : OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS
|
|
||||||
func (p *parser) parseBlockParams() []string {
|
|
||||||
var result []string
|
|
||||||
|
|
||||||
// OPEN_BLOCK_PARAMS
|
|
||||||
tok := p.shift()
|
|
||||||
|
|
||||||
// ID+
|
|
||||||
for p.isID() {
|
|
||||||
result = append(result, p.shift().Val)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(result) == 0 {
|
|
||||||
errExpected(lexer.TokenID, p.next())
|
|
||||||
}
|
|
||||||
|
|
||||||
// CLOSE_BLOCK_PARAMS
|
|
||||||
tok = p.shift()
|
|
||||||
if tok.Kind != lexer.TokenCloseBlockParams {
|
|
||||||
errExpected(lexer.TokenCloseBlockParams, tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// helperName : path | dataName | STRING | NUMBER | BOOLEAN | UNDEFINED | NULL
|
|
||||||
func (p *parser) parseHelperName() ast.Node {
|
|
||||||
var result ast.Node
|
|
||||||
|
|
||||||
tok := p.next()
|
|
||||||
|
|
||||||
switch tok.Kind {
|
|
||||||
case lexer.TokenBoolean:
|
|
||||||
// BOOLEAN
|
|
||||||
p.shift()
|
|
||||||
result = ast.NewBooleanLiteral(tok.Pos, tok.Line, (tok.Val == "true"), tok.Val)
|
|
||||||
case lexer.TokenNumber:
|
|
||||||
// NUMBER
|
|
||||||
p.shift()
|
|
||||||
|
|
||||||
val, isInt := parseNumber(tok)
|
|
||||||
result = ast.NewNumberLiteral(tok.Pos, tok.Line, val, isInt, tok.Val)
|
|
||||||
case lexer.TokenString:
|
|
||||||
// STRING
|
|
||||||
p.shift()
|
|
||||||
result = ast.NewStringLiteral(tok.Pos, tok.Line, tok.Val)
|
|
||||||
case lexer.TokenData:
|
|
||||||
// dataName
|
|
||||||
result = p.parseDataName()
|
|
||||||
default:
|
|
||||||
// path
|
|
||||||
result = p.parsePath(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseNumber parses a number
|
|
||||||
func parseNumber(tok *lexer.Token) (result float64, isInt bool) {
|
|
||||||
var valInt int
|
|
||||||
var err error
|
|
||||||
|
|
||||||
valInt, err = strconv.Atoi(tok.Val)
|
|
||||||
if err == nil {
|
|
||||||
isInt = true
|
|
||||||
|
|
||||||
result = float64(valInt)
|
|
||||||
} else {
|
|
||||||
isInt = false
|
|
||||||
|
|
||||||
result, err = strconv.ParseFloat(tok.Val, 64)
|
|
||||||
if err != nil {
|
|
||||||
errToken(tok, fmt.Sprintf("Failed to parse number: %s", tok.Val))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// named returned values
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if next tokens represent a `helperName`
|
|
||||||
func (p *parser) isHelperName() bool {
|
|
||||||
switch p.next().Kind {
|
|
||||||
case lexer.TokenBoolean, lexer.TokenNumber, lexer.TokenString, lexer.TokenData, lexer.TokenID:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// partialName : helperName | sexpr
|
|
||||||
func (p *parser) parsePartialName() ast.Node {
|
|
||||||
return p.parseHelperNameOrSexpr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// dataName : DATA pathSegments
|
|
||||||
func (p *parser) parseDataName() *ast.PathExpression {
|
|
||||||
// DATA
|
|
||||||
p.shift()
|
|
||||||
|
|
||||||
// pathSegments
|
|
||||||
return p.parsePath(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// path : pathSegments
|
|
||||||
// pathSegments : pathSegments SEP ID
|
|
||||||
// | ID
|
|
||||||
func (p *parser) parsePath(data bool) *ast.PathExpression {
|
|
||||||
var tok *lexer.Token
|
|
||||||
|
|
||||||
// ID
|
|
||||||
tok = p.shift()
|
|
||||||
if tok.Kind != lexer.TokenID {
|
|
||||||
errExpected(lexer.TokenID, tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := ast.NewPathExpression(tok.Pos, tok.Line, data)
|
|
||||||
result.Part(tok.Val)
|
|
||||||
|
|
||||||
for p.isPathSep() {
|
|
||||||
// SEP
|
|
||||||
tok = p.shift()
|
|
||||||
result.Sep(tok.Val)
|
|
||||||
|
|
||||||
// ID
|
|
||||||
tok = p.shift()
|
|
||||||
if tok.Kind != lexer.TokenID {
|
|
||||||
errExpected(lexer.TokenID, tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Part(tok.Val)
|
|
||||||
|
|
||||||
if len(result.Parts) > 0 {
|
|
||||||
switch tok.Val {
|
|
||||||
case "..", ".", "this":
|
|
||||||
errToken(tok, "Invalid path: "+result.Original)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures there is token to parse at given index
|
|
||||||
func (p *parser) ensure(index int) {
|
|
||||||
if p.lexOver {
|
|
||||||
// nothing more to grab
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nb := index + 1
|
|
||||||
|
|
||||||
for len(p.tokens) < nb {
|
|
||||||
// fetch next token
|
|
||||||
tok := p.lex.NextToken()
|
|
||||||
|
|
||||||
// queue it
|
|
||||||
p.tokens = append(p.tokens, &tok)
|
|
||||||
|
|
||||||
if (tok.Kind == lexer.TokenEOF) || (tok.Kind == lexer.TokenError) {
|
|
||||||
p.lexOver = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// have returns true is there are a list given number of tokens to consume left
|
|
||||||
func (p *parser) have(nb int) bool {
|
|
||||||
p.ensure(nb - 1)
|
|
||||||
|
|
||||||
return len(p.tokens) >= nb
|
|
||||||
}
|
|
||||||
|
|
||||||
// nextAt returns next token at given index, without consuming it
|
|
||||||
func (p *parser) nextAt(index int) *lexer.Token {
|
|
||||||
p.ensure(index)
|
|
||||||
|
|
||||||
return p.tokens[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
// next returns next token without consuming it
|
|
||||||
func (p *parser) next() *lexer.Token {
|
|
||||||
return p.nextAt(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// shift returns next token and remove it from the tokens buffer
|
|
||||||
//
|
|
||||||
// Panics if next token is `TokenError`
|
|
||||||
func (p *parser) shift() *lexer.Token {
|
|
||||||
var result *lexer.Token
|
|
||||||
|
|
||||||
p.ensure(0)
|
|
||||||
|
|
||||||
result, p.tokens = p.tokens[0], p.tokens[1:]
|
|
||||||
|
|
||||||
// check error token
|
|
||||||
if result.Kind == lexer.TokenError {
|
|
||||||
errToken(result, "Lexer error")
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// isToken returns true if next token is of given type
|
|
||||||
func (p *parser) isToken(kind lexer.TokenKind) bool {
|
|
||||||
return p.have(1) && p.next().Kind == kind
|
|
||||||
}
|
|
||||||
|
|
||||||
// isSexpr returns true if next token starts a sexpr
|
|
||||||
func (p *parser) isSexpr() bool {
|
|
||||||
return p.isToken(lexer.TokenOpenSexpr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isPathSep returns true if next token is a path separator
|
|
||||||
func (p *parser) isPathSep() bool {
|
|
||||||
return p.isToken(lexer.TokenSep)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isID returns true if next token is an ID
|
|
||||||
func (p *parser) isID() bool {
|
|
||||||
return p.isToken(lexer.TokenID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isBlockParams returns true if next token starts a block params
|
|
||||||
func (p *parser) isBlockParams() bool {
|
|
||||||
return p.isToken(lexer.TokenOpenBlockParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isInverse returns true if next token starts an INVERSE sequence
|
|
||||||
func (p *parser) isInverse() bool {
|
|
||||||
return p.isToken(lexer.TokenInverse)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isOpenInverseChain returns true if next token is OPEN_INVERSE_CHAIN
|
|
||||||
func (p *parser) isOpenInverseChain() bool {
|
|
||||||
return p.isToken(lexer.TokenOpenInverseChain)
|
|
||||||
}
|
|
|
@ -1,200 +0,0 @@
|
||||||
package parser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/aymerick/raymond/ast"
|
|
||||||
"github.com/aymerick/raymond/lexer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type parserTest struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
output string
|
|
||||||
}
|
|
||||||
|
|
||||||
var parserTests = []parserTest{
|
|
||||||
//
|
|
||||||
// Next tests come from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/spec/parser.js
|
|
||||||
//
|
|
||||||
{"parses simple mustaches (1)", `{{123}}`, "{{ NUMBER{123} [] }}\n"},
|
|
||||||
{"parses simple mustaches (2)", `{{"foo"}}`, "{{ \"foo\" [] }}\n"},
|
|
||||||
{"parses simple mustaches (3)", `{{false}}`, "{{ BOOLEAN{false} [] }}\n"},
|
|
||||||
{"parses simple mustaches (4)", `{{true}}`, "{{ BOOLEAN{true} [] }}\n"},
|
|
||||||
{"parses simple mustaches (5)", `{{foo}}`, "{{ PATH:foo [] }}\n"},
|
|
||||||
{"parses simple mustaches (6)", `{{foo?}}`, "{{ PATH:foo? [] }}\n"},
|
|
||||||
{"parses simple mustaches (7)", `{{foo_}}`, "{{ PATH:foo_ [] }}\n"},
|
|
||||||
{"parses simple mustaches (8)", `{{foo-}}`, "{{ PATH:foo- [] }}\n"},
|
|
||||||
{"parses simple mustaches (9)", `{{foo:}}`, "{{ PATH:foo: [] }}\n"},
|
|
||||||
|
|
||||||
{"parses simple mustaches with data", `{{@foo}}`, "{{ @PATH:foo [] }}\n"},
|
|
||||||
{"parses simple mustaches with data paths", `{{@../foo}}`, "{{ @PATH:foo [] }}\n"},
|
|
||||||
{"parses mustaches with paths", `{{foo/bar}}`, "{{ PATH:foo/bar [] }}\n"},
|
|
||||||
{"parses mustaches with this/foo", `{{this/foo}}`, "{{ PATH:foo [] }}\n"},
|
|
||||||
{"parses mustaches with - in a path", `{{foo-bar}}`, "{{ PATH:foo-bar [] }}\n"},
|
|
||||||
{"parses mustaches with parameters", `{{foo bar}}`, "{{ PATH:foo [PATH:bar] }}\n"},
|
|
||||||
{"parses mustaches with string parameters", `{{foo bar "baz" }}`, "{{ PATH:foo [PATH:bar, \"baz\"] }}\n"},
|
|
||||||
{"parses mustaches with NUMBER parameters", `{{foo 1}}`, "{{ PATH:foo [NUMBER{1}] }}\n"},
|
|
||||||
{"parses mustaches with BOOLEAN parameters (1)", `{{foo true}}`, "{{ PATH:foo [BOOLEAN{true}] }}\n"},
|
|
||||||
{"parses mustaches with BOOLEAN parameters (2)", `{{foo false}}`, "{{ PATH:foo [BOOLEAN{false}] }}\n"},
|
|
||||||
{"parses mustaches with DATA parameters", `{{foo @bar}}`, "{{ PATH:foo [@PATH:bar] }}\n"},
|
|
||||||
|
|
||||||
{"parses mustaches with hash arguments (01)", `{{foo bar=baz}}`, "{{ PATH:foo [] HASH{bar=PATH:baz} }}\n"},
|
|
||||||
{"parses mustaches with hash arguments (02)", `{{foo bar=1}}`, "{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n"},
|
|
||||||
{"parses mustaches with hash arguments (03)", `{{foo bar=true}}`, "{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n"},
|
|
||||||
{"parses mustaches with hash arguments (04)", `{{foo bar=false}}`, "{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n"},
|
|
||||||
{"parses mustaches with hash arguments (05)", `{{foo bar=@baz}}`, "{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n"},
|
|
||||||
{"parses mustaches with hash arguments (06)", `{{foo bar=baz bat=bam}}`, "{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n"},
|
|
||||||
{"parses mustaches with hash arguments (07)", `{{foo bar=baz bat="bam"}}`, "{{ PATH:foo [] HASH{bar=PATH:baz, bat=\"bam\"} }}\n"},
|
|
||||||
{"parses mustaches with hash arguments (08)", `{{foo bat='bam'}}`, "{{ PATH:foo [] HASH{bat=\"bam\"} }}\n"},
|
|
||||||
{"parses mustaches with hash arguments (09)", `{{foo omg bar=baz bat="bam"}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\"} }}\n"},
|
|
||||||
{"parses mustaches with hash arguments (10)", `{{foo omg bar=baz bat="bam" baz=1}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=NUMBER{1}} }}\n"},
|
|
||||||
{"parses mustaches with hash arguments (11)", `{{foo omg bar=baz bat="bam" baz=true}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=BOOLEAN{true}} }}\n"},
|
|
||||||
{"parses mustaches with hash arguments (12)", `{{foo omg bar=baz bat="bam" baz=false}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=BOOLEAN{false}} }}\n"},
|
|
||||||
|
|
||||||
{"parses contents followed by a mustache", `foo bar {{baz}}`, "CONTENT[ 'foo bar ' ]\n{{ PATH:baz [] }}\n"},
|
|
||||||
|
|
||||||
{"parses a partial (1)", `{{> foo }}`, "{{> PARTIAL:foo }}\n"},
|
|
||||||
{"parses a partial (2)", `{{> "foo" }}`, "{{> PARTIAL:foo }}\n"},
|
|
||||||
{"parses a partial (3)", `{{> 1 }}`, "{{> PARTIAL:1 }}\n"},
|
|
||||||
{"parses a partial with context", `{{> foo bar}}`, "{{> PARTIAL:foo PATH:bar }}\n"},
|
|
||||||
{"parses a partial with hash", `{{> foo bar=bat}}`, "{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n"},
|
|
||||||
{"parses a partial with context and hash", `{{> foo bar bat=baz}}`, "{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n"},
|
|
||||||
{"parses a partial with a complex name", `{{> shared/partial?.bar}}`, "{{> PARTIAL:shared/partial?.bar }}\n"},
|
|
||||||
|
|
||||||
{"parses a comment", `{{! this is a comment }}`, "{{! ' this is a comment ' }}\n"},
|
|
||||||
{"parses a multi-line comment", "{{!\nthis is a multi-line comment\n}}", "{{! '\nthis is a multi-line comment\n' }}\n"},
|
|
||||||
|
|
||||||
{"parses an inverse section", `{{#foo}} bar {{^}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"},
|
|
||||||
{"parses an inverse (else-style) section", `{{#foo}} bar {{else}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"},
|
|
||||||
{"parses multiple inverse sections", `{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"},
|
|
||||||
{"parses empty blocks", `{{#foo}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n"},
|
|
||||||
{"parses empty blocks with empty inverse section", `{{#foo}}{{^}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"},
|
|
||||||
{"parses empty blocks with empty inverse (else-style) section", `{{#foo}}{{else}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"},
|
|
||||||
{"parses non-empty blocks with empty inverse section", `{{#foo}} bar {{^}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"},
|
|
||||||
{"parses non-empty blocks with empty inverse (else-style) section", `{{#foo}} bar {{else}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"},
|
|
||||||
{"parses empty blocks with non-empty inverse section", `{{#foo}}{{^}} bar {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"},
|
|
||||||
{"parses empty blocks with non-empty inverse (else-style) section", `{{#foo}}{{else}} bar {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"},
|
|
||||||
{"parses a standalone inverse section", `{{^foo}}bar{{/foo}}`, "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"},
|
|
||||||
{"parses block with block params", `{{#foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"},
|
|
||||||
{"parses inverse block with block params", `{{^foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"},
|
|
||||||
{"parses chained inverse block with block params", `{{#foo}}{{else foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParser(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, test := range parserTests {
|
|
||||||
output := ""
|
|
||||||
|
|
||||||
node, err := Parse(test.input)
|
|
||||||
if err == nil {
|
|
||||||
output = ast.Print(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err != nil) || (test.output != output) {
|
|
||||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\nexpected\n\t%q\ngot\n\t%q\nerror:\n\t%s", test.name, test.input, test.output, output, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var parserErrorTests = []parserTest{
|
|
||||||
{"lexer error", `{{! unclosed comment`, "Lexer error"},
|
|
||||||
{"syntax error", `foo{{^}}`, "Syntax error"},
|
|
||||||
|
|
||||||
{"open raw block must be closed", `{{{{raw foo}} bar {{{{/raw}}}}`, "Expecting CloseRawBlock"},
|
|
||||||
{"end raw block must be closed", `{{{{raw foo}}}} bar {{{{/raw}}`, "Expecting CloseRawBlock"},
|
|
||||||
|
|
||||||
{"raw block names must match (1)", `{{{{1}}}}{{foo}}{{{{/raw}}}}`, "1 doesn't match raw"},
|
|
||||||
{"raw block names must match (2)", `{{{{raw}}}}{{foo}}{{{{/1}}}}`, "raw doesn't match 1"},
|
|
||||||
{"raw block names must match (3)", `{{{{goodbyes}}}}test{{{{/hellos}}}}`, "goodbyes doesn't match hellos"},
|
|
||||||
|
|
||||||
{"open block must be closed", `{{#foo bar}}}{{/foo}}`, "Expecting Close"},
|
|
||||||
{"end block must be closed", `{{#foo bar}}{{/foo}}}`, "Expecting Close"},
|
|
||||||
{"an open block must have a end block", `{{#foo}}test`, "Expecting OpenEndBlock"},
|
|
||||||
|
|
||||||
{"block names must match (1)", `{{#1 bar}}{{/foo}}`, "1 doesn't match foo"},
|
|
||||||
{"block names must match (2)", `{{#foo bar}}{{/1}}`, "foo doesn't match 1"},
|
|
||||||
{"block names must match (3)", `{{#foo}}test{{/bar}}`, "foo doesn't match bar"},
|
|
||||||
|
|
||||||
{"an mustache must terminate with a close mustache", `{{foo}}}`, "Expecting Close"},
|
|
||||||
{"an unescaped mustache must terminate with a close unescaped mustache", `{{{foo}}`, "Expecting CloseUnescaped"},
|
|
||||||
|
|
||||||
{"an partial must terminate with a close mustache", `{{> foo}}}`, "Expecting Close"},
|
|
||||||
{"a subexpression must terminate with a close subexpression", `{{foo (false}}`, "Expecting CloseSexpr"},
|
|
||||||
|
|
||||||
{"raises on missing hash value (1)", `{{foo bar=}}`, "Parse error on line 1"},
|
|
||||||
{"raises on missing hash value (2)", `{{foo bar=baz bim=}}`, "Parse error on line 1"},
|
|
||||||
|
|
||||||
{"block param must have at least one param", `{{#foo as ||}}content{{/foo}}`, "Expecting ID"},
|
|
||||||
{"open block params must be closed", `{{#foo as |}}content{{/foo}}`, "Expecting ID"},
|
|
||||||
|
|
||||||
{"a path must start with an ID", `{{#/}}content{{/foo}}`, "Expecting ID"},
|
|
||||||
{"a path must end with an ID", `{{foo/bar/}}`, "Expecting ID"},
|
|
||||||
|
|
||||||
//
|
|
||||||
// Next tests come from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/spec/parser.js
|
|
||||||
//
|
|
||||||
{"throws on old inverse section", `{{else foo}}bar{{/foo}}`, ""},
|
|
||||||
|
|
||||||
{"raises if there's a parser error (1)", `foo{{^}}bar`, "Parse error on line 1"},
|
|
||||||
{"raises if there's a parser error (2)", `{{foo}`, "Parse error on line 1"},
|
|
||||||
{"raises if there's a parser error (3)", `{{foo &}}`, "Parse error on line 1"},
|
|
||||||
{"raises if there's a parser error (4)", `{{#goodbyes}}{{/hellos}}`, "Parse error on line 1"},
|
|
||||||
{"raises if there's a parser error (5)", `{{#goodbyes}}{{/hellos}}`, "goodbyes doesn't match hellos"},
|
|
||||||
|
|
||||||
{"should handle invalid paths (1)", `{{foo/../bar}}`, `Invalid path: foo/..`},
|
|
||||||
{"should handle invalid paths (2)", `{{foo/./bar}}`, `Invalid path: foo/.`},
|
|
||||||
{"should handle invalid paths (3)", `{{foo/this/bar}}`, `Invalid path: foo/this`},
|
|
||||||
|
|
||||||
{"knows how to report the correct line number in errors (1)", "hello\nmy\n{{foo}", "Parse error on line 3"},
|
|
||||||
{"knows how to report the correct line number in errors (2)", "hello\n\nmy\n\n{{foo}", "Parse error on line 5"},
|
|
||||||
|
|
||||||
{"knows how to report the correct line number in errors when the first character is a newline", "\n\nhello\n\nmy\n\n{{foo}", "Parse error on line 7"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParserErrors(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, test := range parserErrorTests {
|
|
||||||
node, err := Parse(test.input)
|
|
||||||
if err == nil {
|
|
||||||
output := ast.Print(node)
|
|
||||||
tokens := lexer.Collect(test.input)
|
|
||||||
|
|
||||||
t.Errorf("Test '%s' failed - Error expected\ninput:\n\t'%s'\ngot\n\t%q\ntokens:\n\t%q", test.name, test.input, output, tokens)
|
|
||||||
} else if test.output != "" {
|
|
||||||
matched, errMatch := regexp.MatchString(regexp.QuoteMeta(test.output), fmt.Sprint(err))
|
|
||||||
if errMatch != nil {
|
|
||||||
panic("Failed to match regexp")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !matched {
|
|
||||||
t.Errorf("Test '%s' failed - Incorrect error returned\ninput:\n\t'%s'\nexpected\n\t%q\ngot\n\t%q", test.name, test.input, test.output, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// package example
|
|
||||||
func Example() {
|
|
||||||
source := "You know {{nothing}} John Snow"
|
|
||||||
|
|
||||||
// parse template
|
|
||||||
program, err := Parse(source)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// print AST
|
|
||||||
output := ast.Print(program)
|
|
||||||
|
|
||||||
fmt.Print(output)
|
|
||||||
// CONTENT[ 'You know ' ]
|
|
||||||
// {{ PATH:nothing [] }}
|
|
||||||
// CONTENT[ ' John Snow' ]
|
|
||||||
}
|
|
|
@ -1,360 +0,0 @@
|
||||||
package parser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/aymerick/raymond/ast"
|
|
||||||
)
|
|
||||||
|
|
||||||
// whitespaceVisitor walks through the AST to perform whitespace control
|
|
||||||
//
|
|
||||||
// The logic was shamelessly borrowed from:
|
|
||||||
// https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/whitespace-control.js
|
|
||||||
type whitespaceVisitor struct {
|
|
||||||
isRootSeen bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
rTrimLeft = regexp.MustCompile(`^[ \t]*\r?\n?`)
|
|
||||||
rTrimLeftMultiple = regexp.MustCompile(`^\s+`)
|
|
||||||
|
|
||||||
rTrimRight = regexp.MustCompile(`[ \t]+$`)
|
|
||||||
rTrimRightMultiple = regexp.MustCompile(`\s+$`)
|
|
||||||
|
|
||||||
rPrevWhitespace = regexp.MustCompile(`\r?\n\s*?$`)
|
|
||||||
rPrevWhitespaceStart = regexp.MustCompile(`(^|\r?\n)\s*?$`)
|
|
||||||
|
|
||||||
rNextWhitespace = regexp.MustCompile(`^\s*?\r?\n`)
|
|
||||||
rNextWhitespaceEnd = regexp.MustCompile(`^\s*?(\r?\n|$)`)
|
|
||||||
|
|
||||||
rPartialIndent = regexp.MustCompile(`([ \t]+$)`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// newWhitespaceVisitor instanciates a new whitespaceVisitor
|
|
||||||
func newWhitespaceVisitor() *whitespaceVisitor {
|
|
||||||
return &whitespaceVisitor{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// processWhitespaces performs whitespace control on given AST
|
|
||||||
//
|
|
||||||
// WARNING: It must be called only once on AST.
|
|
||||||
func processWhitespaces(node ast.Node) {
|
|
||||||
node.Accept(newWhitespaceVisitor())
|
|
||||||
}
|
|
||||||
|
|
||||||
func omitRightFirst(body []ast.Node, multiple bool) {
|
|
||||||
omitRight(body, -1, multiple)
|
|
||||||
}
|
|
||||||
|
|
||||||
func omitRight(body []ast.Node, i int, multiple bool) {
|
|
||||||
if i+1 >= len(body) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
current := body[i+1]
|
|
||||||
|
|
||||||
node, ok := current.(*ast.ContentStatement)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !multiple && node.RightStripped {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
original := node.Value
|
|
||||||
|
|
||||||
r := rTrimLeft
|
|
||||||
if multiple {
|
|
||||||
r = rTrimLeftMultiple
|
|
||||||
}
|
|
||||||
|
|
||||||
node.Value = r.ReplaceAllString(node.Value, "")
|
|
||||||
|
|
||||||
node.RightStripped = (original != node.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func omitLeftLast(body []ast.Node, multiple bool) {
|
|
||||||
omitLeft(body, len(body), multiple)
|
|
||||||
}
|
|
||||||
|
|
||||||
func omitLeft(body []ast.Node, i int, multiple bool) bool {
|
|
||||||
if i-1 < 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
current := body[i-1]
|
|
||||||
|
|
||||||
node, ok := current.(*ast.ContentStatement)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !multiple && node.LeftStripped {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
original := node.Value
|
|
||||||
|
|
||||||
r := rTrimRight
|
|
||||||
if multiple {
|
|
||||||
r = rTrimRightMultiple
|
|
||||||
}
|
|
||||||
|
|
||||||
node.Value = r.ReplaceAllString(node.Value, "")
|
|
||||||
|
|
||||||
node.LeftStripped = (original != node.Value)
|
|
||||||
|
|
||||||
return node.LeftStripped
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPrevWhitespace(body []ast.Node) bool {
|
|
||||||
return isPrevWhitespaceProgram(body, len(body), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPrevWhitespaceProgram(body []ast.Node, i int, isRoot bool) bool {
|
|
||||||
if i < 1 {
|
|
||||||
return isRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
prev := body[i-1]
|
|
||||||
|
|
||||||
if node, ok := prev.(*ast.ContentStatement); ok {
|
|
||||||
if (node.Value == "") && node.RightStripped {
|
|
||||||
// already stripped, so it may be an empty string not catched by regexp
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
r := rPrevWhitespaceStart
|
|
||||||
if (i > 1) || !isRoot {
|
|
||||||
r = rPrevWhitespace
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.MatchString(node.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNextWhitespace(body []ast.Node) bool {
|
|
||||||
return isNextWhitespaceProgram(body, -1, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNextWhitespaceProgram(body []ast.Node, i int, isRoot bool) bool {
|
|
||||||
if i+1 >= len(body) {
|
|
||||||
return isRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
next := body[i+1]
|
|
||||||
|
|
||||||
if node, ok := next.(*ast.ContentStatement); ok {
|
|
||||||
if (node.Value == "") && node.LeftStripped {
|
|
||||||
// already stripped, so it may be an empty string not catched by regexp
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
r := rNextWhitespaceEnd
|
|
||||||
if (i+2 > len(body)) || !isRoot {
|
|
||||||
r = rNextWhitespace
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.MatchString(node.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Visitor interface
|
|
||||||
//
|
|
||||||
|
|
||||||
func (v *whitespaceVisitor) VisitProgram(program *ast.Program) interface{} {
|
|
||||||
isRoot := !v.isRootSeen
|
|
||||||
v.isRootSeen = true
|
|
||||||
|
|
||||||
body := program.Body
|
|
||||||
for i, current := range body {
|
|
||||||
strip, _ := current.Accept(v).(*ast.Strip)
|
|
||||||
if strip == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_isPrevWhitespace := isPrevWhitespaceProgram(body, i, isRoot)
|
|
||||||
_isNextWhitespace := isNextWhitespaceProgram(body, i, isRoot)
|
|
||||||
|
|
||||||
openStandalone := strip.OpenStandalone && _isPrevWhitespace
|
|
||||||
closeStandalone := strip.CloseStandalone && _isNextWhitespace
|
|
||||||
inlineStandalone := strip.InlineStandalone && _isPrevWhitespace && _isNextWhitespace
|
|
||||||
|
|
||||||
if strip.Close {
|
|
||||||
omitRight(body, i, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strip.Open && (i > 0) {
|
|
||||||
omitLeft(body, i, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if inlineStandalone {
|
|
||||||
omitRight(body, i, false)
|
|
||||||
|
|
||||||
if omitLeft(body, i, false) {
|
|
||||||
// If we are on a standalone node, save the indent info for partials
|
|
||||||
if partial, ok := current.(*ast.PartialStatement); ok {
|
|
||||||
// Pull out the whitespace from the final line
|
|
||||||
if i > 0 {
|
|
||||||
if prevContent, ok := body[i-1].(*ast.ContentStatement); ok {
|
|
||||||
partial.Indent = rPartialIndent.FindString(prevContent.Original)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if b, ok := current.(*ast.BlockStatement); ok {
|
|
||||||
if openStandalone {
|
|
||||||
prog := b.Program
|
|
||||||
if prog == nil {
|
|
||||||
prog = b.Inverse
|
|
||||||
}
|
|
||||||
|
|
||||||
omitRightFirst(prog.Body, false)
|
|
||||||
|
|
||||||
// Strip out the previous content node if it's whitespace only
|
|
||||||
omitLeft(body, i, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if closeStandalone {
|
|
||||||
prog := b.Inverse
|
|
||||||
if prog == nil {
|
|
||||||
prog = b.Program
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always strip the next node
|
|
||||||
omitRight(body, i, false)
|
|
||||||
|
|
||||||
omitLeftLast(prog.Body, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *whitespaceVisitor) VisitBlock(block *ast.BlockStatement) interface{} {
|
|
||||||
if block.Program != nil {
|
|
||||||
block.Program.Accept(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if block.Inverse != nil {
|
|
||||||
block.Inverse.Accept(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
program := block.Program
|
|
||||||
inverse := block.Inverse
|
|
||||||
|
|
||||||
if program == nil {
|
|
||||||
program = inverse
|
|
||||||
inverse = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
firstInverse := inverse
|
|
||||||
lastInverse := inverse
|
|
||||||
|
|
||||||
if (inverse != nil) && inverse.Chained {
|
|
||||||
b, _ := inverse.Body[0].(*ast.BlockStatement)
|
|
||||||
firstInverse = b.Program
|
|
||||||
|
|
||||||
for lastInverse.Chained {
|
|
||||||
b, _ := lastInverse.Body[len(lastInverse.Body)-1].(*ast.BlockStatement)
|
|
||||||
lastInverse = b.Program
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeProg := firstInverse
|
|
||||||
if closeProg == nil {
|
|
||||||
closeProg = program
|
|
||||||
}
|
|
||||||
|
|
||||||
strip := &ast.Strip{
|
|
||||||
Open: (block.OpenStrip != nil) && block.OpenStrip.Open,
|
|
||||||
Close: (block.CloseStrip != nil) && block.CloseStrip.Close,
|
|
||||||
|
|
||||||
OpenStandalone: isNextWhitespace(program.Body),
|
|
||||||
CloseStandalone: isPrevWhitespace(closeProg.Body),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (block.OpenStrip != nil) && block.OpenStrip.Close {
|
|
||||||
omitRightFirst(program.Body, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if inverse != nil {
|
|
||||||
if block.InverseStrip != nil {
|
|
||||||
inverseStrip := block.InverseStrip
|
|
||||||
|
|
||||||
if inverseStrip.Open {
|
|
||||||
omitLeftLast(program.Body, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if inverseStrip.Close {
|
|
||||||
omitRightFirst(firstInverse.Body, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (block.CloseStrip != nil) && block.CloseStrip.Open {
|
|
||||||
omitLeftLast(lastInverse.Body, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find standalone else statements
|
|
||||||
if isPrevWhitespace(program.Body) && isNextWhitespace(firstInverse.Body) {
|
|
||||||
omitLeftLast(program.Body, false)
|
|
||||||
|
|
||||||
omitRightFirst(firstInverse.Body, false)
|
|
||||||
}
|
|
||||||
} else if (block.CloseStrip != nil) && block.CloseStrip.Open {
|
|
||||||
omitLeftLast(program.Body, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strip
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *whitespaceVisitor) VisitMustache(mustache *ast.MustacheStatement) interface{} {
|
|
||||||
return mustache.Strip
|
|
||||||
}
|
|
||||||
|
|
||||||
func _inlineStandalone(strip *ast.Strip) interface{} {
|
|
||||||
return &ast.Strip{
|
|
||||||
Open: strip.Open,
|
|
||||||
Close: strip.Close,
|
|
||||||
InlineStandalone: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *whitespaceVisitor) VisitPartial(node *ast.PartialStatement) interface{} {
|
|
||||||
strip := node.Strip
|
|
||||||
if strip == nil {
|
|
||||||
strip = &ast.Strip{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _inlineStandalone(strip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *whitespaceVisitor) VisitComment(node *ast.CommentStatement) interface{} {
|
|
||||||
strip := node.Strip
|
|
||||||
if strip == nil {
|
|
||||||
strip = &ast.Strip{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _inlineStandalone(strip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOOP
|
|
||||||
func (v *whitespaceVisitor) VisitContent(node *ast.ContentStatement) interface{} { return nil }
|
|
||||||
func (v *whitespaceVisitor) VisitExpression(node *ast.Expression) interface{} { return nil }
|
|
||||||
func (v *whitespaceVisitor) VisitSubExpression(node *ast.SubExpression) interface{} { return nil }
|
|
||||||
func (v *whitespaceVisitor) VisitPath(node *ast.PathExpression) interface{} { return nil }
|
|
||||||
func (v *whitespaceVisitor) VisitString(node *ast.StringLiteral) interface{} { return nil }
|
|
||||||
func (v *whitespaceVisitor) VisitBoolean(node *ast.BooleanLiteral) interface{} { return nil }
|
|
||||||
func (v *whitespaceVisitor) VisitNumber(node *ast.NumberLiteral) interface{} { return nil }
|
|
||||||
func (v *whitespaceVisitor) VisitHash(node *ast.Hash) interface{} { return nil }
|
|
||||||
func (v *whitespaceVisitor) VisitHashPair(node *ast.HashPair) interface{} { return nil }
|
|
|
@ -1,85 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// partial represents a partial template
|
|
||||||
type partial struct {
|
|
||||||
name string
|
|
||||||
source string
|
|
||||||
tpl *Template
|
|
||||||
}
|
|
||||||
|
|
||||||
// partials stores all global partials
|
|
||||||
var partials map[string]*partial
|
|
||||||
|
|
||||||
// protects global partials
|
|
||||||
var partialsMutex sync.RWMutex
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
partials = make(map[string]*partial)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newPartial instanciates a new partial
|
|
||||||
func newPartial(name string, source string, tpl *Template) *partial {
|
|
||||||
return &partial{
|
|
||||||
name: name,
|
|
||||||
source: source,
|
|
||||||
tpl: tpl,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterPartial registers a global partial. That partial will be available to all templates.
|
|
||||||
func RegisterPartial(name string, source string) {
|
|
||||||
partialsMutex.Lock()
|
|
||||||
defer partialsMutex.Unlock()
|
|
||||||
|
|
||||||
if partials[name] != nil {
|
|
||||||
panic(fmt.Errorf("Partial already registered: %s", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
partials[name] = newPartial(name, source, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterPartials registers several global partials. Those partials will be available to all templates.
|
|
||||||
func RegisterPartials(partials map[string]string) {
|
|
||||||
for name, p := range partials {
|
|
||||||
RegisterPartial(name, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterPartial registers a global partial with given parsed template. That partial will be available to all templates.
|
|
||||||
func RegisterPartialTemplate(name string, tpl *Template) {
|
|
||||||
partialsMutex.Lock()
|
|
||||||
defer partialsMutex.Unlock()
|
|
||||||
|
|
||||||
if partials[name] != nil {
|
|
||||||
panic(fmt.Errorf("Partial already registered: %s", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
partials[name] = newPartial(name, "", tpl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// findPartial finds a registered global partial
|
|
||||||
func findPartial(name string) *partial {
|
|
||||||
partialsMutex.RLock()
|
|
||||||
defer partialsMutex.RUnlock()
|
|
||||||
|
|
||||||
return partials[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
// template returns parsed partial template
|
|
||||||
func (p *partial) template() (*Template, error) {
|
|
||||||
if p.tpl == nil {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
p.tpl, err = Parse(p.source)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.tpl, nil
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
// Package raymond provides handlebars evaluation
|
|
||||||
package raymond
|
|
||||||
|
|
||||||
// Render parses a template and evaluates it with given context
|
|
||||||
//
|
|
||||||
// Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead.
|
|
||||||
func Render(source string, ctx interface{}) (string, error) {
|
|
||||||
// parse template
|
|
||||||
tpl, err := Parse(source)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// renders template
|
|
||||||
str, err := tpl.Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return str, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustRender parses a template and evaluates it with given context. It panics on error.
|
|
||||||
//
|
|
||||||
// Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead.
|
|
||||||
func MustRender(source string, ctx interface{}) string {
|
|
||||||
return MustParse(source).MustExec(ctx)
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
|
@ -1,115 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func Example() {
|
|
||||||
source := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"title": "foo",
|
|
||||||
"body": map[string]string{"content": "bar"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse template
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
// evaluate template with context
|
|
||||||
output := tpl.MustExec(ctx)
|
|
||||||
|
|
||||||
// alternatively, for one shots:
|
|
||||||
// output := MustRender(source, ctx)
|
|
||||||
|
|
||||||
fmt.Print(output)
|
|
||||||
// Output: <h1>foo</h1><p>bar</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
func Example_struct() {
|
|
||||||
source := `<div class="post">
|
|
||||||
<h1>By {{fullName author}}</h1>
|
|
||||||
<div class="body">{{body}}</div>
|
|
||||||
|
|
||||||
<h1>Comments</h1>
|
|
||||||
|
|
||||||
{{#each comments}}
|
|
||||||
<h2>By {{fullName author}}</h2>
|
|
||||||
<div class="body">{{body}}</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>`
|
|
||||||
|
|
||||||
type Person struct {
|
|
||||||
FirstName string
|
|
||||||
LastName string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Comment struct {
|
|
||||||
Author Person
|
|
||||||
Body string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Post struct {
|
|
||||||
Author Person
|
|
||||||
Body string
|
|
||||||
Comments []Comment
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := Post{
|
|
||||||
Person{"Jean", "Valjean"},
|
|
||||||
"Life is difficult",
|
|
||||||
[]Comment{
|
|
||||||
Comment{
|
|
||||||
Person{"Marcel", "Beliveau"},
|
|
||||||
"LOL!",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterHelper("fullName", func(person Person) string {
|
|
||||||
return person.FirstName + " " + person.LastName
|
|
||||||
})
|
|
||||||
|
|
||||||
output := MustRender(source, ctx)
|
|
||||||
|
|
||||||
fmt.Print(output)
|
|
||||||
// Output: <div class="post">
|
|
||||||
// <h1>By Jean Valjean</h1>
|
|
||||||
// <div class="body">Life is difficult</div>
|
|
||||||
//
|
|
||||||
// <h1>Comments</h1>
|
|
||||||
//
|
|
||||||
// <h2>By Marcel Beliveau</h2>
|
|
||||||
// <div class="body">LOL!</div>
|
|
||||||
// </div>
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleRender() {
|
|
||||||
tpl := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"title": "foo",
|
|
||||||
"body": map[string]string{"content": "bar"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// render template with context
|
|
||||||
output, err := Render(tpl, ctx)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Print(output)
|
|
||||||
// Output: <h1>foo</h1><p>bar</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleMustRender() {
|
|
||||||
tpl := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"title": "foo",
|
|
||||||
"body": map[string]string{"content": "bar"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// render template with context
|
|
||||||
output := MustRender(tpl, ctx)
|
|
||||||
|
|
||||||
fmt.Print(output)
|
|
||||||
// Output: <h1>foo</h1><p>bar</p>
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SafeString represents a string that must not be escaped.
|
|
||||||
//
|
|
||||||
// A SafeString can be returned by helpers to disable escaping.
|
|
||||||
type SafeString string
|
|
||||||
|
|
||||||
// IsSafeString returns true if argument is a SafeString
|
|
||||||
func isSafeString(value interface{}) bool {
|
|
||||||
if _, ok := value.(SafeString); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Str returns string representation of any basic type value.
|
|
||||||
func Str(value interface{}) string {
|
|
||||||
return strValue(reflect.ValueOf(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
// strValue returns string representation of a reflect.Value
|
|
||||||
func strValue(value reflect.Value) string {
|
|
||||||
result := ""
|
|
||||||
|
|
||||||
ival, ok := printableValue(value)
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Errorf("Can't print value: %q", value))
|
|
||||||
}
|
|
||||||
|
|
||||||
val := reflect.ValueOf(ival)
|
|
||||||
|
|
||||||
switch val.Kind() {
|
|
||||||
case reflect.Array, reflect.Slice:
|
|
||||||
for i := 0; i < val.Len(); i++ {
|
|
||||||
result += strValue(val.Index(i))
|
|
||||||
}
|
|
||||||
case reflect.Bool:
|
|
||||||
result = "false"
|
|
||||||
if val.Bool() {
|
|
||||||
result = "true"
|
|
||||||
}
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
||||||
result = fmt.Sprintf("%d", ival)
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
result = strconv.FormatFloat(val.Float(), 'f', -1, 64)
|
|
||||||
case reflect.Invalid:
|
|
||||||
result = ""
|
|
||||||
default:
|
|
||||||
result = fmt.Sprintf("%s", ival)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// printableValue returns the, possibly indirected, interface value inside v that
|
|
||||||
// is best for a call to formatted printer.
|
|
||||||
//
|
|
||||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
|
||||||
func printableValue(v reflect.Value) (interface{}, bool) {
|
|
||||||
if v.Kind() == reflect.Ptr {
|
|
||||||
v, _ = indirect(v) // fmt.Fprint handles nil.
|
|
||||||
}
|
|
||||||
if !v.IsValid() {
|
|
||||||
return "", true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
|
|
||||||
if v.CanAddr() && (reflect.PtrTo(v.Type()).Implements(errorType) || reflect.PtrTo(v.Type()).Implements(fmtStringerType)) {
|
|
||||||
v = v.Addr()
|
|
||||||
} else {
|
|
||||||
switch v.Kind() {
|
|
||||||
case reflect.Chan, reflect.Func:
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v.Interface(), true
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type strTest struct {
|
|
||||||
name string
|
|
||||||
input interface{}
|
|
||||||
output string
|
|
||||||
}
|
|
||||||
|
|
||||||
var strTests = []strTest{
|
|
||||||
{"String", "foo", "foo"},
|
|
||||||
{"Boolean true", true, "true"},
|
|
||||||
{"Boolean false", false, "false"},
|
|
||||||
{"Integer", 25, "25"},
|
|
||||||
{"Float", 25.75, "25.75"},
|
|
||||||
{"Nil", nil, ""},
|
|
||||||
{"[]string", []string{"foo", "bar"}, "foobar"},
|
|
||||||
{"[]interface{} (strings)", []interface{}{"foo", "bar"}, "foobar"},
|
|
||||||
{"[]Boolean", []bool{true, false}, "truefalse"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStr(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, test := range strTests {
|
|
||||||
if res := Str(test.input); res != test.output {
|
|
||||||
t.Errorf("Failed to stringify: %s\nexpected:\n\t'%s'got:\n\t%q", test.name, test.output, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleStr() {
|
|
||||||
output := Str(3) + " foos are " + Str(true) + " and " + Str(-1.25) + " bars are " + Str(false) + "\n"
|
|
||||||
output += "But you know '" + Str(nil) + "' John Snow\n"
|
|
||||||
output += "map: " + Str(map[string]string{"foo": "bar"}) + "\n"
|
|
||||||
output += "array: " + Str([]interface{}{true, 10, "foo", 5, "bar"})
|
|
||||||
|
|
||||||
fmt.Println(output)
|
|
||||||
// Output: 3 foos are true and -1.25 bars are false
|
|
||||||
// But you know '' John Snow
|
|
||||||
// map: map[foo:bar]
|
|
||||||
// array: true10foo5bar
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleSafeString() {
|
|
||||||
RegisterHelper("em", func() SafeString {
|
|
||||||
return SafeString("<em>FOO BAR</em>")
|
|
||||||
})
|
|
||||||
|
|
||||||
tpl := MustParse("{{em}}")
|
|
||||||
|
|
||||||
result := tpl.MustExec(nil)
|
|
||||||
fmt.Print(result)
|
|
||||||
// Output: <em>FOO BAR</em>
|
|
||||||
}
|
|
|
@ -1,249 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"reflect"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/aymerick/raymond/ast"
|
|
||||||
"github.com/aymerick/raymond/parser"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Template represents a handlebars template.
|
|
||||||
type Template struct {
|
|
||||||
source string
|
|
||||||
program *ast.Program
|
|
||||||
helpers map[string]reflect.Value
|
|
||||||
partials map[string]*partial
|
|
||||||
mutex sync.RWMutex // protects helpers and partials
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTemplate instanciate a new template without parsing it
|
|
||||||
func newTemplate(source string) *Template {
|
|
||||||
return &Template{
|
|
||||||
source: source,
|
|
||||||
helpers: make(map[string]reflect.Value),
|
|
||||||
partials: make(map[string]*partial),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse instanciates a template by parsing given source.
|
|
||||||
func Parse(source string) (*Template, error) {
|
|
||||||
tpl := newTemplate(source)
|
|
||||||
|
|
||||||
// parse template
|
|
||||||
if err := tpl.parse(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tpl, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustParse instanciates a template by parsing given source. It panics on error.
|
|
||||||
func MustParse(source string) *Template {
|
|
||||||
result, err := Parse(source)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseFile reads given file and returns parsed template.
|
|
||||||
func ParseFile(filePath string) (*Template, error) {
|
|
||||||
b, err := ioutil.ReadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return Parse(string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse parses the template
|
|
||||||
//
|
|
||||||
// It can be called several times, the parsing will be done only once.
|
|
||||||
func (tpl *Template) parse() error {
|
|
||||||
if tpl.program == nil {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
tpl.program, err = parser.Parse(tpl.source)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone returns a copy of that template.
|
|
||||||
func (tpl *Template) Clone() *Template {
|
|
||||||
result := newTemplate(tpl.source)
|
|
||||||
|
|
||||||
result.program = tpl.program
|
|
||||||
|
|
||||||
tpl.mutex.RLock()
|
|
||||||
defer tpl.mutex.RUnlock()
|
|
||||||
|
|
||||||
for name, helper := range tpl.helpers {
|
|
||||||
result.RegisterHelper(name, helper.Interface())
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, partial := range tpl.partials {
|
|
||||||
result.addPartial(name, partial.source, partial.tpl)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tpl *Template) findHelper(name string) reflect.Value {
|
|
||||||
tpl.mutex.RLock()
|
|
||||||
defer tpl.mutex.RUnlock()
|
|
||||||
|
|
||||||
return tpl.helpers[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterHelper registers a helper for that template.
|
|
||||||
func (tpl *Template) RegisterHelper(name string, helper interface{}) {
|
|
||||||
tpl.mutex.Lock()
|
|
||||||
defer tpl.mutex.Unlock()
|
|
||||||
|
|
||||||
if tpl.helpers[name] != zero {
|
|
||||||
panic(fmt.Sprintf("Helper %s already registered", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
val := reflect.ValueOf(helper)
|
|
||||||
ensureValidHelper(name, val)
|
|
||||||
|
|
||||||
tpl.helpers[name] = val
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterHelpers registers several helpers for that template.
|
|
||||||
func (tpl *Template) RegisterHelpers(helpers map[string]interface{}) {
|
|
||||||
for name, helper := range helpers {
|
|
||||||
tpl.RegisterHelper(name, helper)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tpl *Template) addPartial(name string, source string, template *Template) {
|
|
||||||
tpl.mutex.Lock()
|
|
||||||
defer tpl.mutex.Unlock()
|
|
||||||
|
|
||||||
if tpl.partials[name] != nil {
|
|
||||||
panic(fmt.Sprintf("Partial %s already registered", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl.partials[name] = newPartial(name, source, template)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tpl *Template) findPartial(name string) *partial {
|
|
||||||
tpl.mutex.RLock()
|
|
||||||
defer tpl.mutex.RUnlock()
|
|
||||||
|
|
||||||
return tpl.partials[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterPartial registers a partial for that template.
|
|
||||||
func (tpl *Template) RegisterPartial(name string, source string) {
|
|
||||||
tpl.addPartial(name, source, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterPartials registers several partials for that template.
|
|
||||||
func (tpl *Template) RegisterPartials(partials map[string]string) {
|
|
||||||
for name, partial := range partials {
|
|
||||||
tpl.RegisterPartial(name, partial)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterPartialFile reads given file and registers its content as a partial with given name.
|
|
||||||
func (tpl *Template) RegisterPartialFile(filePath string, name string) error {
|
|
||||||
b, err := ioutil.ReadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl.RegisterPartial(name, string(b))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterPartialFiles reads several files and registers them as partials, the filename base is used as the partial name.
|
|
||||||
func (tpl *Template) RegisterPartialFiles(filePaths ...string) error {
|
|
||||||
if len(filePaths) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, filePath := range filePaths {
|
|
||||||
name := fileBase(filePath)
|
|
||||||
|
|
||||||
if err := tpl.RegisterPartialFile(filePath, name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterPartial registers an already parsed partial for that template.
|
|
||||||
func (tpl *Template) RegisterPartialTemplate(name string, template *Template) {
|
|
||||||
tpl.addPartial(name, "", template)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec evaluates template with given context.
|
|
||||||
func (tpl *Template) Exec(ctx interface{}) (result string, err error) {
|
|
||||||
return tpl.ExecWith(ctx, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustExec evaluates template with given context. It panics on error.
|
|
||||||
func (tpl *Template) MustExec(ctx interface{}) string {
|
|
||||||
result, err := tpl.Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecWith evaluates template with given context and private data frame.
|
|
||||||
func (tpl *Template) ExecWith(ctx interface{}, privData *DataFrame) (result string, err error) {
|
|
||||||
defer errRecover(&err)
|
|
||||||
|
|
||||||
// parses template if necessary
|
|
||||||
err = tpl.parse()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup visitor
|
|
||||||
v := newEvalVisitor(tpl, ctx, privData)
|
|
||||||
|
|
||||||
// visit AST
|
|
||||||
result, _ = tpl.program.Accept(v).(string)
|
|
||||||
|
|
||||||
// named return values
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// errRecover recovers evaluation panic
|
|
||||||
func errRecover(errp *error) {
|
|
||||||
e := recover()
|
|
||||||
if e != nil {
|
|
||||||
switch err := e.(type) {
|
|
||||||
case runtime.Error:
|
|
||||||
panic(e)
|
|
||||||
case error:
|
|
||||||
*errp = err
|
|
||||||
default:
|
|
||||||
panic(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrintAST returns string representation of parsed template.
|
|
||||||
func (tpl *Template) PrintAST() string {
|
|
||||||
if err := tpl.parse(); err != nil {
|
|
||||||
return fmt.Sprintf("PARSER ERROR: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ast.Print(tpl.program)
|
|
||||||
}
|
|
|
@ -1,166 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var sourceBasic = `<div class="entry">
|
|
||||||
<h1>{{title}}</h1>
|
|
||||||
<div class="body">
|
|
||||||
{{body}}
|
|
||||||
</div>
|
|
||||||
</div>`
|
|
||||||
|
|
||||||
var basicAST = `CONTENT[ '<div class="entry">
|
|
||||||
<h1>' ]
|
|
||||||
{{ PATH:title [] }}
|
|
||||||
CONTENT[ '</h1>
|
|
||||||
<div class="body">
|
|
||||||
' ]
|
|
||||||
{{ PATH:body [] }}
|
|
||||||
CONTENT[ '
|
|
||||||
</div>
|
|
||||||
</div>' ]
|
|
||||||
`
|
|
||||||
|
|
||||||
func TestNewTemplate(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tpl := newTemplate(sourceBasic)
|
|
||||||
if tpl.source != sourceBasic {
|
|
||||||
t.Errorf("Failed to instantiate template")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tpl, err := Parse(sourceBasic)
|
|
||||||
if err != nil || (tpl.source != sourceBasic) {
|
|
||||||
t.Errorf("Failed to parse template")
|
|
||||||
}
|
|
||||||
|
|
||||||
if str := tpl.PrintAST(); str != basicAST {
|
|
||||||
t.Errorf("Template parsing incorrect: %s", str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClone(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
sourcePartial := `I am a {{wat}} partial`
|
|
||||||
sourcePartial2 := `Partial for the {{wat}}`
|
|
||||||
|
|
||||||
tpl := MustParse(sourceBasic)
|
|
||||||
tpl.RegisterPartial("p", sourcePartial)
|
|
||||||
|
|
||||||
if (len(tpl.partials) != 1) || (tpl.partials["p"] == nil) {
|
|
||||||
t.Errorf("What?")
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned := tpl.Clone()
|
|
||||||
|
|
||||||
if (len(cloned.partials) != 1) || (cloned.partials["p"] == nil) {
|
|
||||||
t.Errorf("Template partials must be cloned")
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned.RegisterPartial("p2", sourcePartial2)
|
|
||||||
|
|
||||||
if (len(cloned.partials) != 2) || (cloned.partials["p"] == nil) || (cloned.partials["p2"] == nil) {
|
|
||||||
t.Errorf("Failed to register a partial on cloned template")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (len(tpl.partials) != 1) || (tpl.partials["p"] == nil) {
|
|
||||||
t.Errorf("Modification of a cloned template MUST NOT affect original template")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleTemplate_Exec() {
|
|
||||||
source := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"title": "foo",
|
|
||||||
"body": map[string]string{"content": "bar"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse template
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
// evaluate template with context
|
|
||||||
output, err := tpl.Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Print(output)
|
|
||||||
// Output: <h1>foo</h1><p>bar</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleTemplate_MustExec() {
|
|
||||||
source := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"title": "foo",
|
|
||||||
"body": map[string]string{"content": "bar"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse template
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
// evaluate template with context
|
|
||||||
output := tpl.MustExec(ctx)
|
|
||||||
|
|
||||||
fmt.Print(output)
|
|
||||||
// Output: <h1>foo</h1><p>bar</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleTemplate_ExecWith() {
|
|
||||||
source := "<h1>{{title}}</h1><p>{{#body}}{{content}} and {{@baz.bat}}{{/body}}</p>"
|
|
||||||
|
|
||||||
ctx := map[string]interface{}{
|
|
||||||
"title": "foo",
|
|
||||||
"body": map[string]string{"content": "bar"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse template
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
// computes private data frame
|
|
||||||
frame := NewDataFrame()
|
|
||||||
frame.Set("baz", map[string]string{"bat": "unicorns"})
|
|
||||||
|
|
||||||
// evaluate template
|
|
||||||
output, err := tpl.ExecWith(ctx, frame)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Print(output)
|
|
||||||
// Output: <h1>foo</h1><p>bar and unicorns</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleTemplate_PrintAST() {
|
|
||||||
source := "<h1>{{title}}</h1><p>{{#body}}{{content}} and {{@baz.bat}}{{/body}}</p>"
|
|
||||||
|
|
||||||
// parse template
|
|
||||||
tpl := MustParse(source)
|
|
||||||
|
|
||||||
// print AST
|
|
||||||
output := tpl.PrintAST()
|
|
||||||
|
|
||||||
fmt.Print(output)
|
|
||||||
// Output: CONTENT[ '<h1>' ]
|
|
||||||
// {{ PATH:title [] }}
|
|
||||||
// CONTENT[ '</h1><p>' ]
|
|
||||||
// BLOCK:
|
|
||||||
// PATH:body []
|
|
||||||
// PROGRAM:
|
|
||||||
// {{ PATH:content []
|
|
||||||
// }}
|
|
||||||
// CONTENT[ ' and ' ]
|
|
||||||
// {{ @PATH:baz/bat []
|
|
||||||
// }}
|
|
||||||
// CONTENT[ '</p>' ]
|
|
||||||
//
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
"reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// indirect returns the item at the end of indirection, and a bool to indicate if it's nil.
|
|
||||||
// We indirect through pointers and empty interfaces (only) because
|
|
||||||
// non-empty interfaces have methods we might need.
|
|
||||||
//
|
|
||||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
|
||||||
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
|
|
||||||
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
|
|
||||||
if v.IsNil() {
|
|
||||||
return v, true
|
|
||||||
}
|
|
||||||
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsTrue returns true if obj is a truthy value.
|
|
||||||
func IsTrue(obj interface{}) bool {
|
|
||||||
thruth, ok := isTrueValue(reflect.ValueOf(obj))
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return thruth
|
|
||||||
}
|
|
||||||
|
|
||||||
// isTrueValue reports whether the value is 'true', in the sense of not the zero of its type,
|
|
||||||
// and whether the value has a meaningful truth value
|
|
||||||
//
|
|
||||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
|
||||||
func isTrueValue(val reflect.Value) (truth, ok bool) {
|
|
||||||
if !val.IsValid() {
|
|
||||||
// Something like var x interface{}, never set. It's a form of nil.
|
|
||||||
return false, true
|
|
||||||
}
|
|
||||||
switch val.Kind() {
|
|
||||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
|
||||||
truth = val.Len() > 0
|
|
||||||
case reflect.Bool:
|
|
||||||
truth = val.Bool()
|
|
||||||
case reflect.Complex64, reflect.Complex128:
|
|
||||||
truth = val.Complex() != 0
|
|
||||||
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
|
|
||||||
truth = !val.IsNil()
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
truth = val.Int() != 0
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
truth = val.Float() != 0
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
||||||
truth = val.Uint() != 0
|
|
||||||
case reflect.Struct:
|
|
||||||
truth = true // Struct values are always true.
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return truth, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero.
|
|
||||||
//
|
|
||||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
|
||||||
func canBeNil(typ reflect.Type) bool {
|
|
||||||
switch typ.Kind() {
|
|
||||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// fileBase returns base file name
|
|
||||||
//
|
|
||||||
// example: /foo/bar/baz.png => baz
|
|
||||||
func fileBase(filePath string) string {
|
|
||||||
fileName := path.Base(filePath)
|
|
||||||
fileExt := path.Ext(filePath)
|
|
||||||
|
|
||||||
return fileName[:len(fileName)-len(fileExt)]
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
package raymond
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func ExampleIsTrue() {
|
|
||||||
output := "Empty array: " + Str(IsTrue([0]string{})) + "\n"
|
|
||||||
output += "Non empty array: " + Str(IsTrue([1]string{"foo"})) + "\n"
|
|
||||||
|
|
||||||
output += "Empty slice: " + Str(IsTrue([]string{})) + "\n"
|
|
||||||
output += "Non empty slice: " + Str(IsTrue([]string{"foo"})) + "\n"
|
|
||||||
|
|
||||||
output += "Empty map: " + Str(IsTrue(map[string]string{})) + "\n"
|
|
||||||
output += "Non empty map: " + Str(IsTrue(map[string]string{"foo": "bar"})) + "\n"
|
|
||||||
|
|
||||||
output += "Empty string: " + Str(IsTrue("")) + "\n"
|
|
||||||
output += "Non empty string: " + Str(IsTrue("foo")) + "\n"
|
|
||||||
|
|
||||||
output += "true bool: " + Str(IsTrue(true)) + "\n"
|
|
||||||
output += "false bool: " + Str(IsTrue(false)) + "\n"
|
|
||||||
|
|
||||||
output += "0 integer: " + Str(IsTrue(0)) + "\n"
|
|
||||||
output += "positive integer: " + Str(IsTrue(10)) + "\n"
|
|
||||||
output += "negative integer: " + Str(IsTrue(-10)) + "\n"
|
|
||||||
|
|
||||||
output += "0 float: " + Str(IsTrue(0.0)) + "\n"
|
|
||||||
output += "positive float: " + Str(IsTrue(10.0)) + "\n"
|
|
||||||
output += "negative integer: " + Str(IsTrue(-10.0)) + "\n"
|
|
||||||
|
|
||||||
output += "struct: " + Str(IsTrue(struct{}{})) + "\n"
|
|
||||||
output += "nil: " + Str(IsTrue(nil)) + "\n"
|
|
||||||
|
|
||||||
fmt.Println(output)
|
|
||||||
// Output: Empty array: false
|
|
||||||
// Non empty array: true
|
|
||||||
// Empty slice: false
|
|
||||||
// Non empty slice: true
|
|
||||||
// Empty map: false
|
|
||||||
// Non empty map: true
|
|
||||||
// Empty string: false
|
|
||||||
// Non empty string: true
|
|
||||||
// true bool: true
|
|
||||||
// false bool: false
|
|
||||||
// 0 integer: false
|
|
||||||
// positive integer: true
|
|
||||||
// negative integer: true
|
|
||||||
// 0 float: false
|
|
||||||
// positive float: true
|
|
||||||
// negative integer: true
|
|
||||||
// struct: true
|
|
||||||
// nil: false
|
|
||||||
}
|
|
|
@ -1,342 +0,0 @@
|
||||||
package drone
|
|
||||||
|
|
||||||
//go:generate mockery -all
|
|
||||||
//go:generate mv mocks/Client.go mocks/client.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
pathSelf = "%s/api/user"
|
|
||||||
pathFeed = "%s/api/user/feed"
|
|
||||||
pathRepos = "%s/api/user/repos"
|
|
||||||
pathRepo = "%s/api/repos/%s/%s"
|
|
||||||
pathEncrypt = "%s/api/repos/%s/%s/encrypt"
|
|
||||||
pathBuilds = "%s/api/repos/%s/%s/builds"
|
|
||||||
pathBuild = "%s/api/repos/%s/%s/builds/%v"
|
|
||||||
pathJob = "%s/api/repos/%s/%s/builds/%d/%d"
|
|
||||||
pathLog = "%s/api/repos/%s/%s/logs/%d/%d"
|
|
||||||
pathKey = "%s/api/repos/%s/%s/key"
|
|
||||||
pathNodes = "%s/api/nodes"
|
|
||||||
pathNode = "%s/api/nodes/%d"
|
|
||||||
pathUsers = "%s/api/users"
|
|
||||||
pathUser = "%s/api/users/%s"
|
|
||||||
)
|
|
||||||
|
|
||||||
type client struct {
|
|
||||||
client *http.Client
|
|
||||||
base string // base url
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient returns a client at the specified url.
|
|
||||||
func NewClient(uri string) Client {
|
|
||||||
return &client{http.DefaultClient, uri}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClientToken returns a client at the specified url that
|
|
||||||
// authenticates all outbound requests with the given token.
|
|
||||||
func NewClientToken(uri, token string) Client {
|
|
||||||
config := new(oauth2.Config)
|
|
||||||
auther := config.Client(oauth2.NoContext, &oauth2.Token{AccessToken: token})
|
|
||||||
return &client{auther, uri}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetClient sets the default http client. This should be
|
|
||||||
// used in conjunction with golang.org/x/oauth2 to
|
|
||||||
// authenticate requests to the Drone server.
|
|
||||||
func (c *client) SetClient(client *http.Client) {
|
|
||||||
c.client = client
|
|
||||||
}
|
|
||||||
|
|
||||||
// Self returns the currently authenticated user.
|
|
||||||
func (c *client) Self() (*User, error) {
|
|
||||||
out := new(User)
|
|
||||||
uri := fmt.Sprintf(pathSelf, c.base)
|
|
||||||
err := c.get(uri, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// User returns a user by login.
|
|
||||||
func (c *client) User(login string) (*User, error) {
|
|
||||||
out := new(User)
|
|
||||||
uri := fmt.Sprintf(pathUser, c.base, login)
|
|
||||||
err := c.get(uri, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserList returns a list of all registered users.
|
|
||||||
func (c *client) UserList() ([]*User, error) {
|
|
||||||
out := make([]*User, 0)
|
|
||||||
uri := fmt.Sprintf(pathUsers, c.base)
|
|
||||||
err := c.get(uri, &out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserPost creates a new user account.
|
|
||||||
func (c *client) UserPost(in *User) (*User, error) {
|
|
||||||
out := new(User)
|
|
||||||
uri := fmt.Sprintf(pathUsers, c.base)
|
|
||||||
err := c.post(uri, in, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserPatch updates a user account.
|
|
||||||
func (c *client) UserPatch(in *User) (*User, error) {
|
|
||||||
out := new(User)
|
|
||||||
uri := fmt.Sprintf(pathUser, c.base, in.Login)
|
|
||||||
err := c.patch(uri, in, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserDel deletes a user account.
|
|
||||||
func (c *client) UserDel(login string) error {
|
|
||||||
uri := fmt.Sprintf(pathUser, c.base, login)
|
|
||||||
err := c.delete(uri)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserFeed returns the user's activity feed.
|
|
||||||
func (c *client) UserFeed() ([]*Activity, error) {
|
|
||||||
out := make([]*Activity, 0)
|
|
||||||
uri := fmt.Sprintf(pathFeed, c.base)
|
|
||||||
err := c.get(uri, &out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repo returns a repository by name.
|
|
||||||
func (c *client) Repo(owner string, name string) (*Repo, error) {
|
|
||||||
out := new(Repo)
|
|
||||||
uri := fmt.Sprintf(pathRepo, c.base, owner, name)
|
|
||||||
err := c.get(uri, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RepoList returns a list of all repositories to which
|
|
||||||
// the user has explicit access in the host system.
|
|
||||||
func (c *client) RepoList() ([]*Repo, error) {
|
|
||||||
out := make([]*Repo, 0)
|
|
||||||
uri := fmt.Sprintf(pathRepos, c.base)
|
|
||||||
err := c.get(uri, &out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RepoPost activates a repository.
|
|
||||||
func (c *client) RepoPost(owner string, name string) (*Repo, error) {
|
|
||||||
out := new(Repo)
|
|
||||||
uri := fmt.Sprintf(pathRepo, c.base, owner, name)
|
|
||||||
err := c.post(uri, nil, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RepoPatch updates a repository.
|
|
||||||
func (c *client) RepoPatch(in *Repo) (*Repo, error) {
|
|
||||||
out := new(Repo)
|
|
||||||
uri := fmt.Sprintf(pathRepo, c.base, in.Owner, in.Name)
|
|
||||||
err := c.patch(uri, in, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RepoDel deletes a repository.
|
|
||||||
func (c *client) RepoDel(owner, name string) error {
|
|
||||||
uri := fmt.Sprintf(pathRepo, c.base, owner, name)
|
|
||||||
err := c.delete(uri)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RepoKey returns a repository public key.
|
|
||||||
func (c *client) RepoKey(owner, name string) (*Key, error) {
|
|
||||||
out := new(Key)
|
|
||||||
uri := fmt.Sprintf(pathKey, c.base, owner, name)
|
|
||||||
rc, err := c.stream(uri, "GET", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rc.Close()
|
|
||||||
raw, _ := ioutil.ReadAll(rc)
|
|
||||||
out.Public = string(raw)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build returns a repository build by number.
|
|
||||||
func (c *client) Build(owner, name string, num int) (*Build, error) {
|
|
||||||
out := new(Build)
|
|
||||||
uri := fmt.Sprintf(pathBuild, c.base, owner, name, num)
|
|
||||||
err := c.get(uri, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build returns the latest repository build by branch.
|
|
||||||
func (c *client) BuildLast(owner, name, branch string) (*Build, error) {
|
|
||||||
out := new(Build)
|
|
||||||
uri := fmt.Sprintf(pathBuild, c.base, owner, name, "latest")
|
|
||||||
if len(branch) != 0 {
|
|
||||||
uri += "?branch=" + branch
|
|
||||||
}
|
|
||||||
err := c.get(uri, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildList returns a list of recent builds for the
|
|
||||||
// the specified repository.
|
|
||||||
func (c *client) BuildList(owner, name string) ([]*Build, error) {
|
|
||||||
out := make([]*Build, 0)
|
|
||||||
uri := fmt.Sprintf(pathBuilds, c.base, owner, name)
|
|
||||||
err := c.get(uri, &out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildStart re-starts a stopped build.
|
|
||||||
func (c *client) BuildStart(owner, name string, num int) (*Build, error) {
|
|
||||||
out := new(Build)
|
|
||||||
uri := fmt.Sprintf(pathBuild, c.base, owner, name, num)
|
|
||||||
err := c.post(uri, nil, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildStop cancels the running job.
|
|
||||||
func (c *client) BuildStop(owner, name string, num, job int) error {
|
|
||||||
uri := fmt.Sprintf(pathJob, c.base, owner, name, num, job)
|
|
||||||
err := c.delete(uri)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildFork re-starts a stopped build with a new build number,
|
|
||||||
// preserving the prior history.
|
|
||||||
func (c *client) BuildFork(owner, name string, num int) (*Build, error) {
|
|
||||||
out := new(Build)
|
|
||||||
uri := fmt.Sprintf(pathBuild+"?fork=true", c.base, owner, name, num)
|
|
||||||
err := c.post(uri, nil, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildLogs returns the build logs for the specified job.
|
|
||||||
func (c *client) BuildLogs(owner, name string, num, job int) (io.ReadCloser, error) {
|
|
||||||
uri := fmt.Sprintf(pathLog, c.base, owner, name, num, job)
|
|
||||||
return c.stream(uri, "GET", nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node returns a node by id.
|
|
||||||
func (c *client) Node(id int64) (*Node, error) {
|
|
||||||
out := new(Node)
|
|
||||||
uri := fmt.Sprintf(pathNode, c.base, id)
|
|
||||||
err := c.get(uri, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeList returns a list of all registered worker nodes.
|
|
||||||
func (c *client) NodeList() ([]*Node, error) {
|
|
||||||
out := make([]*Node, 0)
|
|
||||||
uri := fmt.Sprintf(pathNodes, c.base)
|
|
||||||
err := c.get(uri, &out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodePost registers a new worker node.
|
|
||||||
func (c *client) NodePost(in *Node) (*Node, error) {
|
|
||||||
out := new(Node)
|
|
||||||
uri := fmt.Sprintf(pathNodes, c.base)
|
|
||||||
err := c.post(uri, in, out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeDel deletes a worker node.
|
|
||||||
func (c *client) NodeDel(id int64) error {
|
|
||||||
uri := fmt.Sprintf(pathNode, c.base, id)
|
|
||||||
err := c.delete(uri)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// http request helper functions
|
|
||||||
//
|
|
||||||
|
|
||||||
// helper function for making an http GET request.
|
|
||||||
func (c *client) get(rawurl string, out interface{}) error {
|
|
||||||
return c.do(rawurl, "GET", nil, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function for making an http POST request.
|
|
||||||
func (c *client) post(rawurl string, in, out interface{}) error {
|
|
||||||
return c.do(rawurl, "POST", in, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function for making an http PUT request.
|
|
||||||
func (c *client) put(rawurl string, in, out interface{}) error {
|
|
||||||
return c.do(rawurl, "PUT", in, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function for making an http PATCH request.
|
|
||||||
func (c *client) patch(rawurl string, in, out interface{}) error {
|
|
||||||
return c.do(rawurl, "PATCH", in, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function for making an http DELETE request.
|
|
||||||
func (c *client) delete(rawurl string) error {
|
|
||||||
return c.do(rawurl, "DELETE", nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function to make an http request
|
|
||||||
func (c *client) do(rawurl, method string, in, out interface{}) error {
|
|
||||||
// executes the http request and returns the body as
|
|
||||||
// and io.ReadCloser
|
|
||||||
body, err := c.stream(rawurl, method, in, out)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer body.Close()
|
|
||||||
|
|
||||||
// if a json response is expected, parse and return
|
|
||||||
// the json response.
|
|
||||||
if out != nil {
|
|
||||||
return json.NewDecoder(body).Decode(out)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function to stream an http request
|
|
||||||
func (c *client) stream(rawurl, method string, in, out interface{}) (io.ReadCloser, error) {
|
|
||||||
uri, err := url.Parse(rawurl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we are posting or putting data, we need to
|
|
||||||
// write it to the body of the request.
|
|
||||||
var buf io.ReadWriter
|
|
||||||
if in != nil {
|
|
||||||
buf = new(bytes.Buffer)
|
|
||||||
err := json.NewEncoder(buf).Encode(in)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// creates a new http request to bitbucket.
|
|
||||||
req, err := http.NewRequest(method, uri.String(), buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if in != nil {
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode > http.StatusPartialContent {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
out, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
return nil, fmt.Errorf(string(out))
|
|
||||||
}
|
|
||||||
return resp.Body, nil
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package drone
|
|
||||||
|
|
||||||
const (
|
|
||||||
EventPush = "push"
|
|
||||||
EventPull = "pull_request"
|
|
||||||
EventTag = "tag"
|
|
||||||
EventDeploy = "deployment"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
StatusSkipped = "skipped"
|
|
||||||
StatusPending = "pending"
|
|
||||||
StatusRunning = "running"
|
|
||||||
StatusSuccess = "success"
|
|
||||||
StatusFailure = "failure"
|
|
||||||
StatusKilled = "killed"
|
|
||||||
StatusError = "error"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Freebsd_386 uint = iota
|
|
||||||
Freebsd_amd64
|
|
||||||
Freebsd_arm
|
|
||||||
Linux_386
|
|
||||||
Linux_amd64
|
|
||||||
Linux_arm
|
|
||||||
Linux_arm64
|
|
||||||
Solaris_amd64
|
|
||||||
Windows_386
|
|
||||||
Windows_amd64
|
|
||||||
)
|
|
||||||
|
|
||||||
var Archs = map[string]uint{
|
|
||||||
"freebsd_386": Freebsd_386,
|
|
||||||
"freebsd_amd64": Freebsd_amd64,
|
|
||||||
"freebsd_arm": Freebsd_arm,
|
|
||||||
"linux_386": Linux_386,
|
|
||||||
"linux_amd64": Linux_amd64,
|
|
||||||
"linux_arm": Linux_arm,
|
|
||||||
"linux_arm64": Linux_arm64,
|
|
||||||
"solaris_amd64": Solaris_amd64,
|
|
||||||
"windows_386": Windows_386,
|
|
||||||
"windows_amd64": Windows_amd64,
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
package drone
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
type Client interface {
|
|
||||||
// Self returns the currently authenticated user.
|
|
||||||
Self() (*User, error)
|
|
||||||
|
|
||||||
// User returns a user by login.
|
|
||||||
User(string) (*User, error)
|
|
||||||
|
|
||||||
// UserList returns a list of all registered users.
|
|
||||||
UserList() ([]*User, error)
|
|
||||||
|
|
||||||
// UserPost creates a new user account.
|
|
||||||
UserPost(*User) (*User, error)
|
|
||||||
|
|
||||||
// UserPatch updates a user account.
|
|
||||||
UserPatch(*User) (*User, error)
|
|
||||||
|
|
||||||
// UserDel deletes a user account.
|
|
||||||
UserDel(string) error
|
|
||||||
|
|
||||||
// UserFeed returns the user's activity feed.
|
|
||||||
UserFeed() ([]*Activity, error)
|
|
||||||
|
|
||||||
// Repo returns a repository by name.
|
|
||||||
Repo(string, string) (*Repo, error)
|
|
||||||
|
|
||||||
// RepoList returns a list of all repositories to which
|
|
||||||
// the user has explicit access in the host system.
|
|
||||||
RepoList() ([]*Repo, error)
|
|
||||||
|
|
||||||
// RepoPost activates a repository.
|
|
||||||
RepoPost(string, string) (*Repo, error)
|
|
||||||
|
|
||||||
// RepoPatch updates a repository.
|
|
||||||
RepoPatch(*Repo) (*Repo, error)
|
|
||||||
|
|
||||||
// RepoDel deletes a repository.
|
|
||||||
RepoDel(string, string) error
|
|
||||||
|
|
||||||
// RepoKey returns a repository public key.
|
|
||||||
RepoKey(string, string) (*Key, error)
|
|
||||||
|
|
||||||
// Build returns a repository build by number.
|
|
||||||
Build(string, string, int) (*Build, error)
|
|
||||||
|
|
||||||
// BuildLast returns the latest repository build by branch.
|
|
||||||
// An empty branch will result in the default branch.
|
|
||||||
BuildLast(string, string, string) (*Build, error)
|
|
||||||
|
|
||||||
// BuildList returns a list of recent builds for the
|
|
||||||
// the specified repository.
|
|
||||||
BuildList(string, string) ([]*Build, error)
|
|
||||||
|
|
||||||
// BuildStart re-starts a stopped build.
|
|
||||||
BuildStart(string, string, int) (*Build, error)
|
|
||||||
|
|
||||||
// BuildStop stops the specified running job for given build.
|
|
||||||
BuildStop(string, string, int, int) error
|
|
||||||
|
|
||||||
// BuildFork re-starts a stopped build with a new build number,
|
|
||||||
// preserving the prior history.
|
|
||||||
BuildFork(string, string, int) (*Build, error)
|
|
||||||
|
|
||||||
// BuildLogs returns the build logs for the specified job.
|
|
||||||
BuildLogs(string, string, int, int) (io.ReadCloser, error)
|
|
||||||
|
|
||||||
// Node returns a node by id.
|
|
||||||
Node(int64) (*Node, error)
|
|
||||||
|
|
||||||
// NodeList returns a list of all registered worker nodes.
|
|
||||||
NodeList() ([]*Node, error)
|
|
||||||
|
|
||||||
// NodePost registers a new worker node.
|
|
||||||
NodePost(*Node) (*Node, error)
|
|
||||||
|
|
||||||
// NodeDel deletes a worker node.
|
|
||||||
NodeDel(int64) error
|
|
||||||
}
|
|
|
@ -1,435 +0,0 @@
|
||||||
package mocks
|
|
||||||
|
|
||||||
import "github.com/drone/drone-go/drone"
|
|
||||||
import "github.com/stretchr/testify/mock"
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_m *Client) Self() (*drone.User, error) {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
var r0 *drone.User
|
|
||||||
if rf, ok := ret.Get(0).(func() *drone.User); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*drone.User)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func() error); ok {
|
|
||||||
r1 = rf()
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) User(_a0 string) (*drone.User, error) {
|
|
||||||
ret := _m.Called(_a0)
|
|
||||||
|
|
||||||
var r0 *drone.User
|
|
||||||
if rf, ok := ret.Get(0).(func(string) *drone.User); ok {
|
|
||||||
r0 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*drone.User)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
|
||||||
r1 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) UserList() ([]*drone.User, error) {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
var r0 []*drone.User
|
|
||||||
if rf, ok := ret.Get(0).(func() []*drone.User); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).([]*drone.User)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func() error); ok {
|
|
||||||
r1 = rf()
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) UserPost(_a0 *drone.User) (*drone.User, error) {
|
|
||||||
ret := _m.Called(_a0)
|
|
||||||
|
|
||||||
var r0 *drone.User
|
|
||||||
if rf, ok := ret.Get(0).(func(*drone.User) *drone.User); ok {
|
|
||||||
r0 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*drone.User)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(*drone.User) error); ok {
|
|
||||||
r1 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) UserPatch(_a0 *drone.User) (*drone.User, error) {
|
|
||||||
ret := _m.Called(_a0)
|
|
||||||
|
|
||||||
var r0 *drone.User
|
|
||||||
if rf, ok := ret.Get(0).(func(*drone.User) *drone.User); ok {
|
|
||||||
r0 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*drone.User)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(*drone.User) error); ok {
|
|
||||||
r1 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) UserDel(_a0 string) error {
|
|
||||||
ret := _m.Called(_a0)
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
|
||||||
r0 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
func (_m *Client) UserFeed() ([]*drone.Activity, error) {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
var r0 []*drone.Activity
|
|
||||||
if rf, ok := ret.Get(0).(func() []*drone.Activity); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).([]*drone.Activity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func() error); ok {
|
|
||||||
r1 = rf()
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) Repo(_a0 string, _a1 string) (*drone.Repo, error) {
|
|
||||||
ret := _m.Called(_a0, _a1)
|
|
||||||
|
|
||||||
var r0 *drone.Repo
|
|
||||||
if rf, ok := ret.Get(0).(func(string, string) *drone.Repo); ok {
|
|
||||||
r0 = rf(_a0, _a1)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*drone.Repo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
|
||||||
r1 = rf(_a0, _a1)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) RepoList() ([]*drone.Repo, error) {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
var r0 []*drone.Repo
|
|
||||||
if rf, ok := ret.Get(0).(func() []*drone.Repo); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).([]*drone.Repo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func() error); ok {
|
|
||||||
r1 = rf()
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) RepoPost(_a0 string, _a1 string) (*drone.Repo, error) {
|
|
||||||
ret := _m.Called(_a0, _a1)
|
|
||||||
|
|
||||||
var r0 *drone.Repo
|
|
||||||
if rf, ok := ret.Get(0).(func(string, string) *drone.Repo); ok {
|
|
||||||
r0 = rf(_a0, _a1)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*drone.Repo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
|
||||||
r1 = rf(_a0, _a1)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) RepoPatch(_a0 *drone.Repo) (*drone.Repo, error) {
|
|
||||||
ret := _m.Called(_a0)
|
|
||||||
|
|
||||||
var r0 *drone.Repo
|
|
||||||
if rf, ok := ret.Get(0).(func(*drone.Repo) *drone.Repo); ok {
|
|
||||||
r0 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*drone.Repo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(*drone.Repo) error); ok {
|
|
||||||
r1 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) RepoDel(_a0 string, _a1 string) error {
|
|
||||||
ret := _m.Called(_a0, _a1)
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
|
||||||
r0 = rf(_a0, _a1)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
func (_m *Client) RepoKey(_a0 string, _a1 string) (*drone.Key, error) {
|
|
||||||
ret := _m.Called(_a0, _a1)
|
|
||||||
|
|
||||||
var r0 *drone.Key
|
|
||||||
if rf, ok := ret.Get(0).(func(string, string) *drone.Key); ok {
|
|
||||||
r0 = rf(_a0, _a1)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*drone.Key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
|
||||||
r1 = rf(_a0, _a1)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) Build(_a0 string, _a1 string, _a2 int) (*drone.Build, error) {
|
|
||||||
ret := _m.Called(_a0, _a1, _a2)
|
|
||||||
|
|
||||||
var r0 *drone.Build
|
|
||||||
if rf, ok := ret.Get(0).(func(string, string, int) *drone.Build); ok {
|
|
||||||
r0 = rf(_a0, _a1, _a2)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*drone.Build)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(string, string, int) error); ok {
|
|
||||||
r1 = rf(_a0, _a1, _a2)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) BuildList(_a0 string, _a1 string) ([]*drone.Build, error) {
|
|
||||||
ret := _m.Called(_a0, _a1)
|
|
||||||
|
|
||||||
var r0 []*drone.Build
|
|
||||||
if rf, ok := ret.Get(0).(func(string, string) []*drone.Build); ok {
|
|
||||||
r0 = rf(_a0, _a1)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).([]*drone.Build)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
|
||||||
r1 = rf(_a0, _a1)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) BuildStart(_a0 string, _a1 string, _a2 int) (*drone.Build, error) {
|
|
||||||
ret := _m.Called(_a0, _a1, _a2)
|
|
||||||
|
|
||||||
var r0 *drone.Build
|
|
||||||
if rf, ok := ret.Get(0).(func(string, string, int) *drone.Build); ok {
|
|
||||||
r0 = rf(_a0, _a1, _a2)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*drone.Build)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(string, string, int) error); ok {
|
|
||||||
r1 = rf(_a0, _a1, _a2)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) BuildStop(_a0 string, _a1 string, _a2 int, _a3 int) error {
|
|
||||||
ret := _m.Called(_a0, _a1, _a2, _a3)
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(string, string, int, int) error); ok {
|
|
||||||
r0 = rf(_a0, _a1, _a2, _a3)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
func (_m *Client) BuildLogs(_a0 string, _a1 string, _a2 int, _a3 int) (io.ReadCloser, error) {
|
|
||||||
ret := _m.Called(_a0, _a1, _a2, _a3)
|
|
||||||
|
|
||||||
var r0 io.ReadCloser
|
|
||||||
if rf, ok := ret.Get(0).(func(string, string, int, int) io.ReadCloser); ok {
|
|
||||||
r0 = rf(_a0, _a1, _a2, _a3)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Get(0).(io.ReadCloser)
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
|
|
||||||
r1 = rf(_a0, _a1, _a2, _a3)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) Node(_a0 int64) (*drone.Node, error) {
|
|
||||||
ret := _m.Called(_a0)
|
|
||||||
|
|
||||||
var r0 *drone.Node
|
|
||||||
if rf, ok := ret.Get(0).(func(int64) *drone.Node); ok {
|
|
||||||
r0 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*drone.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(int64) error); ok {
|
|
||||||
r1 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) NodeList() ([]*drone.Node, error) {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
var r0 []*drone.Node
|
|
||||||
if rf, ok := ret.Get(0).(func() []*drone.Node); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).([]*drone.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func() error); ok {
|
|
||||||
r1 = rf()
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) NodePost(_a0 *drone.Node) (*drone.Node, error) {
|
|
||||||
ret := _m.Called(_a0)
|
|
||||||
|
|
||||||
var r0 *drone.Node
|
|
||||||
if rf, ok := ret.Get(0).(func(*drone.Node) *drone.Node); ok {
|
|
||||||
r0 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*drone.Node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(*drone.Node) error); ok {
|
|
||||||
r1 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
func (_m *Client) NodeDel(_a0 int64) error {
|
|
||||||
ret := _m.Called(_a0)
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(int64) error); ok {
|
|
||||||
r0 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
|
@ -1,157 +0,0 @@
|
||||||
package drone
|
|
||||||
|
|
||||||
// User represents a user account.
|
|
||||||
type User struct {
|
|
||||||
ID int64 `json:"id""`
|
|
||||||
Login string `json:"login"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
Avatar string `json:"avatar_url"`
|
|
||||||
Active bool `json:"active"`
|
|
||||||
Admin bool `json:"admin"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repo represents a version control repository.
|
|
||||||
type Repo struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Owner string `json:"owner"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
FullName string `json:"full_name"`
|
|
||||||
Avatar string `json:"avatar_url"`
|
|
||||||
Link string `json:"link_url"`
|
|
||||||
Clone string `json:"clone_url"`
|
|
||||||
Branch string `json:"default_branch"`
|
|
||||||
Timeout int64 `json:"timeout"`
|
|
||||||
IsPrivate bool `json:"private"`
|
|
||||||
IsTrusted bool `json:"trusted"`
|
|
||||||
AllowPull bool `json:"allow_pr"`
|
|
||||||
AllowPush bool `json:"allow_push"`
|
|
||||||
AllowDeploy bool `json:"allow_deploys"`
|
|
||||||
AllowTag bool `json:"allow_tags"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build represents the process of compiling and testing a changeset,
|
|
||||||
// typically triggered by the remote system (ie GitHub).
|
|
||||||
type Build struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Number int `json:"number"`
|
|
||||||
Event string `json:"event"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Enqueued int64 `json:"enqueued_at"`
|
|
||||||
Created int64 `json:"created_at"`
|
|
||||||
Started int64 `json:"started_at"`
|
|
||||||
Finished int64 `json:"finished_at"`
|
|
||||||
Commit string `json:"commit"`
|
|
||||||
Branch string `json:"branch"`
|
|
||||||
Ref string `json:"ref"`
|
|
||||||
Refspec string `json:"refspec"`
|
|
||||||
Remote string `json:"remote"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Timestamp int64 `json:"timestamp"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
Avatar string `json:"author_avatar"`
|
|
||||||
Email string `json:"author_email"`
|
|
||||||
Link string `json:"link_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Job represents a single job that is being executed as part
|
|
||||||
// of a Build.
|
|
||||||
type Job struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Number int `json:"number"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
ExitCode int `json:"exit_code"`
|
|
||||||
Enqueued int64 `json:"enqueued_at"`
|
|
||||||
Started int64 `json:"started_at"`
|
|
||||||
Finished int64 `json:"finished_at"`
|
|
||||||
|
|
||||||
Environment map[string]string `json:"environment"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activity represents a build activity. It combines the
|
|
||||||
// build details with summary Repository information.
|
|
||||||
type Activity struct {
|
|
||||||
Owner string `json:"owner"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
FullName string `json:"full_name"`
|
|
||||||
Number int `json:"number"`
|
|
||||||
Event string `json:"event"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Enqueued int64 `json:"enqueued_at"`
|
|
||||||
Created int64 `json:"created_at"`
|
|
||||||
Started int64 `json:"started_at"`
|
|
||||||
Finished int64 `json:"finished_at"`
|
|
||||||
Commit string `json:"commit"`
|
|
||||||
Branch string `json:"branch"`
|
|
||||||
Ref string `json:"ref"`
|
|
||||||
Refspec string `json:"refspec"`
|
|
||||||
Remote string `json:"remote"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Timestamp int64 `json:"timestamp"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
Avatar string `json:"author_avatar"`
|
|
||||||
Email string `json:"author_email"`
|
|
||||||
Link string `json:"link_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repo represents a local or remote Docker daemon that is
|
|
||||||
// repsonsible for running jobs.
|
|
||||||
type Node struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Addr string `json:"address"`
|
|
||||||
Arch string `json:"architecture"`
|
|
||||||
Cert string `json:"cert"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
CA string `json:"ca"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key represents an RSA public and private key assigned to a
|
|
||||||
// repository. It may be used to clone private repositories, or as
|
|
||||||
// a deployment key.
|
|
||||||
type Key struct {
|
|
||||||
Public string `json:"public"`
|
|
||||||
Private string `json:"private"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Netrc defines a default .netrc file that should be injected
|
|
||||||
// into the build environment. It will be used to authorize access
|
|
||||||
// to https resources, such as git+https clones.
|
|
||||||
type Netrc struct {
|
|
||||||
Machine string `json:"machine"`
|
|
||||||
Login string `json:"login"`
|
|
||||||
Password string `json:"user"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type System struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
Link string `json:"link_url"`
|
|
||||||
Plugins []string `json:"plugins"`
|
|
||||||
Globals []string `json:"globals"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workspace defines the build's workspace inside the
|
|
||||||
// container. This helps the plugin locate the source
|
|
||||||
// code directory.
|
|
||||||
type Workspace struct {
|
|
||||||
Root string `json:"root"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
|
|
||||||
Netrc *Netrc `json:"netrc"`
|
|
||||||
Keys *Key `json:"keys"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Payload defines the full payload send to plugins.
|
|
||||||
type Payload struct {
|
|
||||||
Yaml string `json:"config"`
|
|
||||||
YamlEnc string `json:"secret"`
|
|
||||||
Repo *Repo `json:"repo"`
|
|
||||||
Build *Build `json:"build"`
|
|
||||||
BuildLast *Build `json:"build_last"`
|
|
||||||
Job *Job `json:"job"`
|
|
||||||
Netrc *Netrc `json:"netrc"`
|
|
||||||
Keys *Key `json:"keys"`
|
|
||||||
System *System `json:"system"`
|
|
||||||
Workspace *Workspace `json:"workspace"`
|
|
||||||
Vargs interface{} `json:"vargs"`
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
package drone
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StringSlice representes a string or an array of strings.
|
|
||||||
type StringSlice struct {
|
|
||||||
parts []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *StringSlice) UnmarshalJSON(b []byte) error {
|
|
||||||
if len(b) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p := make([]string, 0, 1)
|
|
||||||
if err := json.Unmarshal(b, &p); err != nil {
|
|
||||||
var s string
|
|
||||||
if err := json.Unmarshal(b, &s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p = append(p, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
e.parts = p
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *StringSlice) Len() int {
|
|
||||||
if e == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return len(e.parts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *StringSlice) Slice() []string {
|
|
||||||
if e == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return e.parts
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringInt representes a string or an integer value.
|
|
||||||
type StringInt struct {
|
|
||||||
value string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *StringInt) UnmarshalJSON(b []byte) error {
|
|
||||||
var num int
|
|
||||||
err := json.Unmarshal(b, &num)
|
|
||||||
if err == nil {
|
|
||||||
e.value = strconv.Itoa(num)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return json.Unmarshal(b, &e.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e StringInt) String() string {
|
|
||||||
return e.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringMap representes a string or a map of strings.
|
|
||||||
// StringMap representes a string or a map of strings.
|
|
||||||
type StringMap struct {
|
|
||||||
parts map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *StringMap) UnmarshalJSON(b []byte) error {
|
|
||||||
if len(b) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p := map[string]string{}
|
|
||||||
if err := json.Unmarshal(b, &p); err != nil {
|
|
||||||
var s string
|
|
||||||
if err := json.Unmarshal(b, &s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p[""] = s
|
|
||||||
}
|
|
||||||
|
|
||||||
e.parts = p
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *StringMap) Len() int {
|
|
||||||
if e == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return len(e.parts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *StringMap) String() (str string) {
|
|
||||||
if e == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, val := range e.parts {
|
|
||||||
return val // returns the first string value
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *StringMap) Map() map[string]string {
|
|
||||||
if e == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return e.parts
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStringMap(parts map[string]string) StringMap {
|
|
||||||
return StringMap{parts}
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
package plugin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Stdin *ParamSet
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// defaults to stdin
|
|
||||||
Stdin = NewParamSet(os.Stdin)
|
|
||||||
|
|
||||||
// check for params after the double dash
|
|
||||||
// in the command string
|
|
||||||
for i, argv := range os.Args {
|
|
||||||
if argv == "--" {
|
|
||||||
arg := os.Args[i+1]
|
|
||||||
buf := bytes.NewBufferString(arg)
|
|
||||||
Stdin = NewParamSet(buf)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this init function is deprecated, but I'm keeping it
|
|
||||||
// around just in case it proves useful in the future.
|
|
||||||
func deprecated_init() {
|
|
||||||
// if piping from stdin we can just exit
|
|
||||||
// and use the default Stdin value
|
|
||||||
stat, _ := os.Stdin.Stat()
|
|
||||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for params after the double dash
|
|
||||||
// in the command string
|
|
||||||
for i, argv := range os.Args {
|
|
||||||
if argv == "--" {
|
|
||||||
arg := os.Args[i+1]
|
|
||||||
buf := bytes.NewBufferString(arg)
|
|
||||||
Stdin = NewParamSet(buf)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// else use the first variable in the list
|
|
||||||
if len(os.Args) > 1 {
|
|
||||||
buf := bytes.NewBufferString(os.Args[1])
|
|
||||||
Stdin = NewParamSet(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParamSet struct {
|
|
||||||
reader io.Reader
|
|
||||||
params map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewParamSet(reader io.Reader) *ParamSet {
|
|
||||||
var p = new(ParamSet)
|
|
||||||
p.reader = reader
|
|
||||||
p.params = map[string]interface{}{}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// Param defines a parameter with the specified name.
|
|
||||||
func (p ParamSet) Param(name string, value interface{}) {
|
|
||||||
p.params[name] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parses parameter definitions from the map.
|
|
||||||
func (p ParamSet) Parse() error {
|
|
||||||
raw := map[string]json.RawMessage{}
|
|
||||||
err := json.NewDecoder(p.reader).Decode(&raw)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, val := range p.params {
|
|
||||||
data, ok := raw[key]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := json.Unmarshal(data, val)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Unable to unarmshal %s. %s", key, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal parses the JSON payload from the command
|
|
||||||
// arguments and unmarshal into a value pointed to by v.
|
|
||||||
func (p ParamSet) Unmarshal(v interface{}) error {
|
|
||||||
return json.NewDecoder(p.reader).Decode(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Param defines a parameter with the specified name.
|
|
||||||
func Param(name string, value interface{}) {
|
|
||||||
Stdin.Param(name, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parses parameter definitions from the map.
|
|
||||||
func Parse() error {
|
|
||||||
return Stdin.Parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal parses the JSON payload from the command
|
|
||||||
// arguments and unmarshal into a value pointed to by v.
|
|
||||||
func Unmarshal(v interface{}) error {
|
|
||||||
return Stdin.Unmarshal(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal parses the JSON payload from the command
|
|
||||||
// arguments and unmarshal into a value pointed to by v.
|
|
||||||
func MustUnmarshal(v interface{}) error {
|
|
||||||
return Stdin.Unmarshal(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustParse parses parameter definitions from the map
|
|
||||||
// and panics if there is a parsing error.
|
|
||||||
func MustParse() {
|
|
||||||
err := Parse()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
package plugin
|
|
|
@ -1,124 +0,0 @@
|
||||||
package template
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/aymerick/raymond"
|
|
||||||
"github.com/drone/drone-go/drone"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
raymond.RegisterHelpers(funcs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render parses and executes a template, returning the results
|
|
||||||
// in string format.
|
|
||||||
func Render(template string, playload *drone.Payload) (string, error) {
|
|
||||||
return raymond.Render(template, normalize(playload))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderTrim parses and executes a template, returning the results
|
|
||||||
// in string format. The result is trimmed to remove left and right
|
|
||||||
// padding and newlines that may be added unintentially in the
|
|
||||||
// template markup.
|
|
||||||
func RenderTrim(template string, playload *drone.Payload) (string, error) {
|
|
||||||
out, err := Render(template, playload)
|
|
||||||
return strings.Trim(out, " \n"), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write parses and executes a template, writing the results to
|
|
||||||
// writer w.
|
|
||||||
func Write(w io.Writer, template string, playload *drone.Payload) error {
|
|
||||||
out, err := Render(template, playload)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = io.WriteString(w, out)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var funcs = map[string]interface{}{
|
|
||||||
"uppercase": strings.ToUpper,
|
|
||||||
"lowercase": strings.ToLower,
|
|
||||||
"uppercasefirst": uppercaseFirst,
|
|
||||||
"duration": toDuration,
|
|
||||||
"datetime": toDatetime,
|
|
||||||
"success": isSuccess,
|
|
||||||
"failure": isFailure,
|
|
||||||
}
|
|
||||||
|
|
||||||
// uppercaseFirst is a helper function that takes a string and capitalizes
|
|
||||||
// the first letter.
|
|
||||||
func uppercaseFirst(s string) string {
|
|
||||||
a := []rune(s)
|
|
||||||
a[0] = unicode.ToUpper(a[0])
|
|
||||||
s = string(a)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// toDuration is a helper function that calculates a duration for a start and
|
|
||||||
// and end time, and returns the duration in string format.
|
|
||||||
func toDuration(started, finished float64) string {
|
|
||||||
dur := time.Duration(int64(finished - started))
|
|
||||||
return fmt.Sprintln(dur)
|
|
||||||
}
|
|
||||||
|
|
||||||
// toDatetime is a helper function that converts a unix timestamp to a string.
|
|
||||||
func toDatetime(timestamp float64, layout, zone string) string {
|
|
||||||
if len(zone) == 0 {
|
|
||||||
return time.Unix(int64(timestamp), 0).Format(layout)
|
|
||||||
}
|
|
||||||
loc, err := time.LoadLocation(zone)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error parsing timezone, defaulting to local timezone. %s\n", err)
|
|
||||||
return time.Unix(int64(timestamp), 0).Local().Format(layout)
|
|
||||||
}
|
|
||||||
return time.Unix(int64(timestamp), 0).In(loc).Format(layout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isSuccess is a helper function that executes a block iff the status
|
|
||||||
// is success, else it executes the else block.
|
|
||||||
func isSuccess(conditional bool, options *raymond.Options) string {
|
|
||||||
if !conditional {
|
|
||||||
return options.Inverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch options.ParamStr(0) {
|
|
||||||
case "success":
|
|
||||||
return options.Fn()
|
|
||||||
default:
|
|
||||||
return options.Inverse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isFailure is a helper function that executes a block iff the status
|
|
||||||
// is a form of failure, else it executes the else block.
|
|
||||||
func isFailure(conditional bool, options *raymond.Options) string {
|
|
||||||
if !conditional {
|
|
||||||
return options.Inverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch options.ParamStr(0) {
|
|
||||||
case "failure", "error", "killed":
|
|
||||||
return options.Fn()
|
|
||||||
default:
|
|
||||||
return options.Inverse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalize takes a Go representation of the variable, marshals
|
|
||||||
// to json and then unmarshals to a map[string]interfacce{}. This
|
|
||||||
// is important because it let's us use the JSON variable names
|
|
||||||
// in our template
|
|
||||||
func normalize(in interface{}) map[string]interface{} {
|
|
||||||
data, _ := json.Marshal(in) // we own the types, so this should never fail
|
|
||||||
|
|
||||||
out := map[string]interface{}{}
|
|
||||||
json.Unmarshal(data, &out)
|
|
||||||
return out
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
package template
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/drone/drone-go/drone"
|
|
||||||
)
|
|
||||||
|
|
||||||
var tests = []struct {
|
|
||||||
Payload *drone.Payload
|
|
||||||
Input string
|
|
||||||
Output string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
&drone.Payload{Build: &drone.Build{Number: 1}},
|
|
||||||
"build #{{build.number}}",
|
|
||||||
"build #1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusSuccess}},
|
|
||||||
"{{uppercase build.status}}",
|
|
||||||
"SUCCESS",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
&drone.Payload{Build: &drone.Build{Author: "Octocat"}},
|
|
||||||
"{{lowercase build.author}}",
|
|
||||||
"octocat",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusSuccess}},
|
|
||||||
"{{uppercasefirst build.status}}",
|
|
||||||
"Success",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
&drone.Payload{Build: &drone.Build{
|
|
||||||
Started: 1448127131,
|
|
||||||
Finished: 1448127505},
|
|
||||||
},
|
|
||||||
"{{ duration build.started_at build.finished_at }}",
|
|
||||||
"374ns",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
&drone.Payload{Build: &drone.Build{Finished: 1448127505}},
|
|
||||||
`finished at {{ datetime build.finished_at "3:04PM" "UTC" }}`,
|
|
||||||
"finished at 5:38PM",
|
|
||||||
},
|
|
||||||
// verify the success if / else block works
|
|
||||||
{
|
|
||||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusSuccess}},
|
|
||||||
"{{#success build.status}}SUCCESS{{/success}}",
|
|
||||||
"SUCCESS",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusFailure}},
|
|
||||||
"{{#success build.status}}SUCCESS{{/success}}",
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusFailure}},
|
|
||||||
"{{#success build.status}}SUCCESS{{else}}NOT SUCCESS{{/success}}",
|
|
||||||
"NOT SUCCESS",
|
|
||||||
},
|
|
||||||
// verify the failure if / else block works
|
|
||||||
{
|
|
||||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusFailure}},
|
|
||||||
"{{#failure build.status}}FAILURE{{/failure}}",
|
|
||||||
"FAILURE",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusSuccess}},
|
|
||||||
"{{#failure build.status}}FAILURE{{/failure}}",
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusSuccess}},
|
|
||||||
"{{#failure build.status}}FAILURE{{else}}NOT FAILURE{{/failure}}",
|
|
||||||
"NOT FAILURE",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTemplate(t *testing.T) {
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
got, err := RenderTrim(test.Input, test.Payload)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed rendering template %q, got error %s.", test.Input, err)
|
|
||||||
}
|
|
||||||
if got != test.Output {
|
|
||||||
t.Errorf("Wanted rendered template %q, got %q", test.Output, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,447 +0,0 @@
|
||||||
// Copyright 2014 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package context defines the Context type, which carries deadlines,
|
|
||||||
// cancelation signals, and other request-scoped values across API boundaries
|
|
||||||
// and between processes.
|
|
||||||
//
|
|
||||||
// Incoming requests to a server should create a Context, and outgoing calls to
|
|
||||||
// servers should accept a Context. The chain of function calls between must
|
|
||||||
// propagate the Context, optionally replacing it with a modified copy created
|
|
||||||
// using WithDeadline, WithTimeout, WithCancel, or WithValue.
|
|
||||||
//
|
|
||||||
// Programs that use Contexts should follow these rules to keep interfaces
|
|
||||||
// consistent across packages and enable static analysis tools to check context
|
|
||||||
// propagation:
|
|
||||||
//
|
|
||||||
// Do not store Contexts inside a struct type; instead, pass a Context
|
|
||||||
// explicitly to each function that needs it. The Context should be the first
|
|
||||||
// parameter, typically named ctx:
|
|
||||||
//
|
|
||||||
// func DoSomething(ctx context.Context, arg Arg) error {
|
|
||||||
// // ... use ctx ...
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Do not pass a nil Context, even if a function permits it. Pass context.TODO
|
|
||||||
// if you are unsure about which Context to use.
|
|
||||||
//
|
|
||||||
// Use context Values only for request-scoped data that transits processes and
|
|
||||||
// APIs, not for passing optional parameters to functions.
|
|
||||||
//
|
|
||||||
// The same Context may be passed to functions running in different goroutines;
|
|
||||||
// Contexts are safe for simultaneous use by multiple goroutines.
|
|
||||||
//
|
|
||||||
// See http://blog.golang.org/context for example code for a server that uses
|
|
||||||
// Contexts.
|
|
||||||
package context // import "golang.org/x/net/context"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Context carries a deadline, a cancelation signal, and other values across
|
|
||||||
// API boundaries.
|
|
||||||
//
|
|
||||||
// Context's methods may be called by multiple goroutines simultaneously.
|
|
||||||
type Context interface {
|
|
||||||
// Deadline returns the time when work done on behalf of this context
|
|
||||||
// should be canceled. Deadline returns ok==false when no deadline is
|
|
||||||
// set. Successive calls to Deadline return the same results.
|
|
||||||
Deadline() (deadline time.Time, ok bool)
|
|
||||||
|
|
||||||
// Done returns a channel that's closed when work done on behalf of this
|
|
||||||
// context should be canceled. Done may return nil if this context can
|
|
||||||
// never be canceled. Successive calls to Done return the same value.
|
|
||||||
//
|
|
||||||
// WithCancel arranges for Done to be closed when cancel is called;
|
|
||||||
// WithDeadline arranges for Done to be closed when the deadline
|
|
||||||
// expires; WithTimeout arranges for Done to be closed when the timeout
|
|
||||||
// elapses.
|
|
||||||
//
|
|
||||||
// Done is provided for use in select statements:
|
|
||||||
//
|
|
||||||
// // Stream generates values with DoSomething and sends them to out
|
|
||||||
// // until DoSomething returns an error or ctx.Done is closed.
|
|
||||||
// func Stream(ctx context.Context, out <-chan Value) error {
|
|
||||||
// for {
|
|
||||||
// v, err := DoSomething(ctx)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// select {
|
|
||||||
// case <-ctx.Done():
|
|
||||||
// return ctx.Err()
|
|
||||||
// case out <- v:
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// See http://blog.golang.org/pipelines for more examples of how to use
|
|
||||||
// a Done channel for cancelation.
|
|
||||||
Done() <-chan struct{}
|
|
||||||
|
|
||||||
// Err returns a non-nil error value after Done is closed. Err returns
|
|
||||||
// Canceled if the context was canceled or DeadlineExceeded if the
|
|
||||||
// context's deadline passed. No other values for Err are defined.
|
|
||||||
// After Done is closed, successive calls to Err return the same value.
|
|
||||||
Err() error
|
|
||||||
|
|
||||||
// Value returns the value associated with this context for key, or nil
|
|
||||||
// if no value is associated with key. Successive calls to Value with
|
|
||||||
// the same key returns the same result.
|
|
||||||
//
|
|
||||||
// Use context values only for request-scoped data that transits
|
|
||||||
// processes and API boundaries, not for passing optional parameters to
|
|
||||||
// functions.
|
|
||||||
//
|
|
||||||
// A key identifies a specific value in a Context. Functions that wish
|
|
||||||
// to store values in Context typically allocate a key in a global
|
|
||||||
// variable then use that key as the argument to context.WithValue and
|
|
||||||
// Context.Value. A key can be any type that supports equality;
|
|
||||||
// packages should define keys as an unexported type to avoid
|
|
||||||
// collisions.
|
|
||||||
//
|
|
||||||
// Packages that define a Context key should provide type-safe accessors
|
|
||||||
// for the values stores using that key:
|
|
||||||
//
|
|
||||||
// // Package user defines a User type that's stored in Contexts.
|
|
||||||
// package user
|
|
||||||
//
|
|
||||||
// import "golang.org/x/net/context"
|
|
||||||
//
|
|
||||||
// // User is the type of value stored in the Contexts.
|
|
||||||
// type User struct {...}
|
|
||||||
//
|
|
||||||
// // key is an unexported type for keys defined in this package.
|
|
||||||
// // This prevents collisions with keys defined in other packages.
|
|
||||||
// type key int
|
|
||||||
//
|
|
||||||
// // userKey is the key for user.User values in Contexts. It is
|
|
||||||
// // unexported; clients use user.NewContext and user.FromContext
|
|
||||||
// // instead of using this key directly.
|
|
||||||
// var userKey key = 0
|
|
||||||
//
|
|
||||||
// // NewContext returns a new Context that carries value u.
|
|
||||||
// func NewContext(ctx context.Context, u *User) context.Context {
|
|
||||||
// return context.WithValue(ctx, userKey, u)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // FromContext returns the User value stored in ctx, if any.
|
|
||||||
// func FromContext(ctx context.Context) (*User, bool) {
|
|
||||||
// u, ok := ctx.Value(userKey).(*User)
|
|
||||||
// return u, ok
|
|
||||||
// }
|
|
||||||
Value(key interface{}) interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Canceled is the error returned by Context.Err when the context is canceled.
|
|
||||||
var Canceled = errors.New("context canceled")
|
|
||||||
|
|
||||||
// DeadlineExceeded is the error returned by Context.Err when the context's
|
|
||||||
// deadline passes.
|
|
||||||
var DeadlineExceeded = errors.New("context deadline exceeded")
|
|
||||||
|
|
||||||
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
|
|
||||||
// struct{}, since vars of this type must have distinct addresses.
|
|
||||||
type emptyCtx int
|
|
||||||
|
|
||||||
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*emptyCtx) Done() <-chan struct{} {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*emptyCtx) Err() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*emptyCtx) Value(key interface{}) interface{} {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *emptyCtx) String() string {
|
|
||||||
switch e {
|
|
||||||
case background:
|
|
||||||
return "context.Background"
|
|
||||||
case todo:
|
|
||||||
return "context.TODO"
|
|
||||||
}
|
|
||||||
return "unknown empty Context"
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
background = new(emptyCtx)
|
|
||||||
todo = new(emptyCtx)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Background returns a non-nil, empty Context. It is never canceled, has no
|
|
||||||
// values, and has no deadline. It is typically used by the main function,
|
|
||||||
// initialization, and tests, and as the top-level Context for incoming
|
|
||||||
// requests.
|
|
||||||
func Background() Context {
|
|
||||||
return background
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO returns a non-nil, empty Context. Code should use context.TODO when
|
|
||||||
// it's unclear which Context to use or it's is not yet available (because the
|
|
||||||
// surrounding function has not yet been extended to accept a Context
|
|
||||||
// parameter). TODO is recognized by static analysis tools that determine
|
|
||||||
// whether Contexts are propagated correctly in a program.
|
|
||||||
func TODO() Context {
|
|
||||||
return todo
|
|
||||||
}
|
|
||||||
|
|
||||||
// A CancelFunc tells an operation to abandon its work.
|
|
||||||
// A CancelFunc does not wait for the work to stop.
|
|
||||||
// After the first call, subsequent calls to a CancelFunc do nothing.
|
|
||||||
type CancelFunc func()
|
|
||||||
|
|
||||||
// WithCancel returns a copy of parent with a new Done channel. The returned
|
|
||||||
// context's Done channel is closed when the returned cancel function is called
|
|
||||||
// or when the parent context's Done channel is closed, whichever happens first.
|
|
||||||
//
|
|
||||||
// Canceling this context releases resources associated with it, so code should
|
|
||||||
// call cancel as soon as the operations running in this Context complete.
|
|
||||||
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
|
||||||
c := newCancelCtx(parent)
|
|
||||||
propagateCancel(parent, &c)
|
|
||||||
return &c, func() { c.cancel(true, Canceled) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// newCancelCtx returns an initialized cancelCtx.
|
|
||||||
func newCancelCtx(parent Context) cancelCtx {
|
|
||||||
return cancelCtx{
|
|
||||||
Context: parent,
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// propagateCancel arranges for child to be canceled when parent is.
|
|
||||||
func propagateCancel(parent Context, child canceler) {
|
|
||||||
if parent.Done() == nil {
|
|
||||||
return // parent is never canceled
|
|
||||||
}
|
|
||||||
if p, ok := parentCancelCtx(parent); ok {
|
|
||||||
p.mu.Lock()
|
|
||||||
if p.err != nil {
|
|
||||||
// parent has already been canceled
|
|
||||||
child.cancel(false, p.err)
|
|
||||||
} else {
|
|
||||||
if p.children == nil {
|
|
||||||
p.children = make(map[canceler]bool)
|
|
||||||
}
|
|
||||||
p.children[child] = true
|
|
||||||
}
|
|
||||||
p.mu.Unlock()
|
|
||||||
} else {
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-parent.Done():
|
|
||||||
child.cancel(false, parent.Err())
|
|
||||||
case <-child.Done():
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parentCancelCtx follows a chain of parent references until it finds a
|
|
||||||
// *cancelCtx. This function understands how each of the concrete types in this
|
|
||||||
// package represents its parent.
|
|
||||||
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
|
|
||||||
for {
|
|
||||||
switch c := parent.(type) {
|
|
||||||
case *cancelCtx:
|
|
||||||
return c, true
|
|
||||||
case *timerCtx:
|
|
||||||
return &c.cancelCtx, true
|
|
||||||
case *valueCtx:
|
|
||||||
parent = c.Context
|
|
||||||
default:
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeChild removes a context from its parent.
|
|
||||||
func removeChild(parent Context, child canceler) {
|
|
||||||
p, ok := parentCancelCtx(parent)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
p.mu.Lock()
|
|
||||||
if p.children != nil {
|
|
||||||
delete(p.children, child)
|
|
||||||
}
|
|
||||||
p.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// A canceler is a context type that can be canceled directly. The
|
|
||||||
// implementations are *cancelCtx and *timerCtx.
|
|
||||||
type canceler interface {
|
|
||||||
cancel(removeFromParent bool, err error)
|
|
||||||
Done() <-chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A cancelCtx can be canceled. When canceled, it also cancels any children
|
|
||||||
// that implement canceler.
|
|
||||||
type cancelCtx struct {
|
|
||||||
Context
|
|
||||||
|
|
||||||
done chan struct{} // closed by the first cancel call.
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
children map[canceler]bool // set to nil by the first cancel call
|
|
||||||
err error // set to non-nil by the first cancel call
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cancelCtx) Done() <-chan struct{} {
|
|
||||||
return c.done
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cancelCtx) Err() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
return c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cancelCtx) String() string {
|
|
||||||
return fmt.Sprintf("%v.WithCancel", c.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cancel closes c.done, cancels each of c's children, and, if
|
|
||||||
// removeFromParent is true, removes c from its parent's children.
|
|
||||||
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
|
|
||||||
if err == nil {
|
|
||||||
panic("context: internal error: missing cancel error")
|
|
||||||
}
|
|
||||||
c.mu.Lock()
|
|
||||||
if c.err != nil {
|
|
||||||
c.mu.Unlock()
|
|
||||||
return // already canceled
|
|
||||||
}
|
|
||||||
c.err = err
|
|
||||||
close(c.done)
|
|
||||||
for child := range c.children {
|
|
||||||
// NOTE: acquiring the child's lock while holding parent's lock.
|
|
||||||
child.cancel(false, err)
|
|
||||||
}
|
|
||||||
c.children = nil
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
if removeFromParent {
|
|
||||||
removeChild(c.Context, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
|
||||||
// to be no later than d. If the parent's deadline is already earlier than d,
|
|
||||||
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
|
|
||||||
// context's Done channel is closed when the deadline expires, when the returned
|
|
||||||
// cancel function is called, or when the parent context's Done channel is
|
|
||||||
// closed, whichever happens first.
|
|
||||||
//
|
|
||||||
// Canceling this context releases resources associated with it, so code should
|
|
||||||
// call cancel as soon as the operations running in this Context complete.
|
|
||||||
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
|
|
||||||
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
|
|
||||||
// The current deadline is already sooner than the new one.
|
|
||||||
return WithCancel(parent)
|
|
||||||
}
|
|
||||||
c := &timerCtx{
|
|
||||||
cancelCtx: newCancelCtx(parent),
|
|
||||||
deadline: deadline,
|
|
||||||
}
|
|
||||||
propagateCancel(parent, c)
|
|
||||||
d := deadline.Sub(time.Now())
|
|
||||||
if d <= 0 {
|
|
||||||
c.cancel(true, DeadlineExceeded) // deadline has already passed
|
|
||||||
return c, func() { c.cancel(true, Canceled) }
|
|
||||||
}
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
if c.err == nil {
|
|
||||||
c.timer = time.AfterFunc(d, func() {
|
|
||||||
c.cancel(true, DeadlineExceeded)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c, func() { c.cancel(true, Canceled) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
|
|
||||||
// implement Done and Err. It implements cancel by stopping its timer then
|
|
||||||
// delegating to cancelCtx.cancel.
|
|
||||||
type timerCtx struct {
|
|
||||||
cancelCtx
|
|
||||||
timer *time.Timer // Under cancelCtx.mu.
|
|
||||||
|
|
||||||
deadline time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
|
|
||||||
return c.deadline, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *timerCtx) String() string {
|
|
||||||
return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *timerCtx) cancel(removeFromParent bool, err error) {
|
|
||||||
c.cancelCtx.cancel(false, err)
|
|
||||||
if removeFromParent {
|
|
||||||
// Remove this timerCtx from its parent cancelCtx's children.
|
|
||||||
removeChild(c.cancelCtx.Context, c)
|
|
||||||
}
|
|
||||||
c.mu.Lock()
|
|
||||||
if c.timer != nil {
|
|
||||||
c.timer.Stop()
|
|
||||||
c.timer = nil
|
|
||||||
}
|
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
|
|
||||||
//
|
|
||||||
// Canceling this context releases resources associated with it, so code should
|
|
||||||
// call cancel as soon as the operations running in this Context complete:
|
|
||||||
//
|
|
||||||
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
|
|
||||||
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
|
||||||
// defer cancel() // releases resources if slowOperation completes before timeout elapses
|
|
||||||
// return slowOperation(ctx)
|
|
||||||
// }
|
|
||||||
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
|
|
||||||
return WithDeadline(parent, time.Now().Add(timeout))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithValue returns a copy of parent in which the value associated with key is
|
|
||||||
// val.
|
|
||||||
//
|
|
||||||
// Use context Values only for request-scoped data that transits processes and
|
|
||||||
// APIs, not for passing optional parameters to functions.
|
|
||||||
func WithValue(parent Context, key interface{}, val interface{}) Context {
|
|
||||||
return &valueCtx{parent, key, val}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A valueCtx carries a key-value pair. It implements Value for that key and
|
|
||||||
// delegates all other calls to the embedded Context.
|
|
||||||
type valueCtx struct {
|
|
||||||
Context
|
|
||||||
key, val interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *valueCtx) String() string {
|
|
||||||
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *valueCtx) Value(key interface{}) interface{} {
|
|
||||||
if c.key == key {
|
|
||||||
return c.val
|
|
||||||
}
|
|
||||||
return c.Context.Value(key)
|
|
||||||
}
|
|
|
@ -1,575 +0,0 @@
|
||||||
// Copyright 2014 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package context
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// otherContext is a Context that's not one of the types defined in context.go.
|
|
||||||
// This lets us test code paths that differ based on the underlying type of the
|
|
||||||
// Context.
|
|
||||||
type otherContext struct {
|
|
||||||
Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBackground(t *testing.T) {
|
|
||||||
c := Background()
|
|
||||||
if c == nil {
|
|
||||||
t.Fatalf("Background returned nil")
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case x := <-c.Done():
|
|
||||||
t.Errorf("<-c.Done() == %v want nothing (it should block)", x)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
if got, want := fmt.Sprint(c), "context.Background"; got != want {
|
|
||||||
t.Errorf("Background().String() = %q want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTODO(t *testing.T) {
|
|
||||||
c := TODO()
|
|
||||||
if c == nil {
|
|
||||||
t.Fatalf("TODO returned nil")
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case x := <-c.Done():
|
|
||||||
t.Errorf("<-c.Done() == %v want nothing (it should block)", x)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
if got, want := fmt.Sprint(c), "context.TODO"; got != want {
|
|
||||||
t.Errorf("TODO().String() = %q want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithCancel(t *testing.T) {
|
|
||||||
c1, cancel := WithCancel(Background())
|
|
||||||
|
|
||||||
if got, want := fmt.Sprint(c1), "context.Background.WithCancel"; got != want {
|
|
||||||
t.Errorf("c1.String() = %q want %q", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
o := otherContext{c1}
|
|
||||||
c2, _ := WithCancel(o)
|
|
||||||
contexts := []Context{c1, o, c2}
|
|
||||||
|
|
||||||
for i, c := range contexts {
|
|
||||||
if d := c.Done(); d == nil {
|
|
||||||
t.Errorf("c[%d].Done() == %v want non-nil", i, d)
|
|
||||||
}
|
|
||||||
if e := c.Err(); e != nil {
|
|
||||||
t.Errorf("c[%d].Err() == %v want nil", i, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case x := <-c.Done():
|
|
||||||
t.Errorf("<-c.Done() == %v want nothing (it should block)", x)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
time.Sleep(100 * time.Millisecond) // let cancelation propagate
|
|
||||||
|
|
||||||
for i, c := range contexts {
|
|
||||||
select {
|
|
||||||
case <-c.Done():
|
|
||||||
default:
|
|
||||||
t.Errorf("<-c[%d].Done() blocked, but shouldn't have", i)
|
|
||||||
}
|
|
||||||
if e := c.Err(); e != Canceled {
|
|
||||||
t.Errorf("c[%d].Err() == %v want %v", i, e, Canceled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParentFinishesChild(t *testing.T) {
|
|
||||||
// Context tree:
|
|
||||||
// parent -> cancelChild
|
|
||||||
// parent -> valueChild -> timerChild
|
|
||||||
parent, cancel := WithCancel(Background())
|
|
||||||
cancelChild, stop := WithCancel(parent)
|
|
||||||
defer stop()
|
|
||||||
valueChild := WithValue(parent, "key", "value")
|
|
||||||
timerChild, stop := WithTimeout(valueChild, 10000*time.Hour)
|
|
||||||
defer stop()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case x := <-parent.Done():
|
|
||||||
t.Errorf("<-parent.Done() == %v want nothing (it should block)", x)
|
|
||||||
case x := <-cancelChild.Done():
|
|
||||||
t.Errorf("<-cancelChild.Done() == %v want nothing (it should block)", x)
|
|
||||||
case x := <-timerChild.Done():
|
|
||||||
t.Errorf("<-timerChild.Done() == %v want nothing (it should block)", x)
|
|
||||||
case x := <-valueChild.Done():
|
|
||||||
t.Errorf("<-valueChild.Done() == %v want nothing (it should block)", x)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// The parent's children should contain the two cancelable children.
|
|
||||||
pc := parent.(*cancelCtx)
|
|
||||||
cc := cancelChild.(*cancelCtx)
|
|
||||||
tc := timerChild.(*timerCtx)
|
|
||||||
pc.mu.Lock()
|
|
||||||
if len(pc.children) != 2 || !pc.children[cc] || !pc.children[tc] {
|
|
||||||
t.Errorf("bad linkage: pc.children = %v, want %v and %v",
|
|
||||||
pc.children, cc, tc)
|
|
||||||
}
|
|
||||||
pc.mu.Unlock()
|
|
||||||
|
|
||||||
if p, ok := parentCancelCtx(cc.Context); !ok || p != pc {
|
|
||||||
t.Errorf("bad linkage: parentCancelCtx(cancelChild.Context) = %v, %v want %v, true", p, ok, pc)
|
|
||||||
}
|
|
||||||
if p, ok := parentCancelCtx(tc.Context); !ok || p != pc {
|
|
||||||
t.Errorf("bad linkage: parentCancelCtx(timerChild.Context) = %v, %v want %v, true", p, ok, pc)
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
pc.mu.Lock()
|
|
||||||
if len(pc.children) != 0 {
|
|
||||||
t.Errorf("pc.cancel didn't clear pc.children = %v", pc.children)
|
|
||||||
}
|
|
||||||
pc.mu.Unlock()
|
|
||||||
|
|
||||||
// parent and children should all be finished.
|
|
||||||
check := func(ctx Context, name string) {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
default:
|
|
||||||
t.Errorf("<-%s.Done() blocked, but shouldn't have", name)
|
|
||||||
}
|
|
||||||
if e := ctx.Err(); e != Canceled {
|
|
||||||
t.Errorf("%s.Err() == %v want %v", name, e, Canceled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
check(parent, "parent")
|
|
||||||
check(cancelChild, "cancelChild")
|
|
||||||
check(valueChild, "valueChild")
|
|
||||||
check(timerChild, "timerChild")
|
|
||||||
|
|
||||||
// WithCancel should return a canceled context on a canceled parent.
|
|
||||||
precanceledChild := WithValue(parent, "key", "value")
|
|
||||||
select {
|
|
||||||
case <-precanceledChild.Done():
|
|
||||||
default:
|
|
||||||
t.Errorf("<-precanceledChild.Done() blocked, but shouldn't have")
|
|
||||||
}
|
|
||||||
if e := precanceledChild.Err(); e != Canceled {
|
|
||||||
t.Errorf("precanceledChild.Err() == %v want %v", e, Canceled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChildFinishesFirst(t *testing.T) {
|
|
||||||
cancelable, stop := WithCancel(Background())
|
|
||||||
defer stop()
|
|
||||||
for _, parent := range []Context{Background(), cancelable} {
|
|
||||||
child, cancel := WithCancel(parent)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case x := <-parent.Done():
|
|
||||||
t.Errorf("<-parent.Done() == %v want nothing (it should block)", x)
|
|
||||||
case x := <-child.Done():
|
|
||||||
t.Errorf("<-child.Done() == %v want nothing (it should block)", x)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
cc := child.(*cancelCtx)
|
|
||||||
pc, pcok := parent.(*cancelCtx) // pcok == false when parent == Background()
|
|
||||||
if p, ok := parentCancelCtx(cc.Context); ok != pcok || (ok && pc != p) {
|
|
||||||
t.Errorf("bad linkage: parentCancelCtx(cc.Context) = %v, %v want %v, %v", p, ok, pc, pcok)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pcok {
|
|
||||||
pc.mu.Lock()
|
|
||||||
if len(pc.children) != 1 || !pc.children[cc] {
|
|
||||||
t.Errorf("bad linkage: pc.children = %v, cc = %v", pc.children, cc)
|
|
||||||
}
|
|
||||||
pc.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if pcok {
|
|
||||||
pc.mu.Lock()
|
|
||||||
if len(pc.children) != 0 {
|
|
||||||
t.Errorf("child's cancel didn't remove self from pc.children = %v", pc.children)
|
|
||||||
}
|
|
||||||
pc.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// child should be finished.
|
|
||||||
select {
|
|
||||||
case <-child.Done():
|
|
||||||
default:
|
|
||||||
t.Errorf("<-child.Done() blocked, but shouldn't have")
|
|
||||||
}
|
|
||||||
if e := child.Err(); e != Canceled {
|
|
||||||
t.Errorf("child.Err() == %v want %v", e, Canceled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parent should not be finished.
|
|
||||||
select {
|
|
||||||
case x := <-parent.Done():
|
|
||||||
t.Errorf("<-parent.Done() == %v want nothing (it should block)", x)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
if e := parent.Err(); e != nil {
|
|
||||||
t.Errorf("parent.Err() == %v want nil", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDeadline(c Context, wait time.Duration, t *testing.T) {
|
|
||||||
select {
|
|
||||||
case <-time.After(wait):
|
|
||||||
t.Fatalf("context should have timed out")
|
|
||||||
case <-c.Done():
|
|
||||||
}
|
|
||||||
if e := c.Err(); e != DeadlineExceeded {
|
|
||||||
t.Errorf("c.Err() == %v want %v", e, DeadlineExceeded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeadline(t *testing.T) {
|
|
||||||
c, _ := WithDeadline(Background(), time.Now().Add(100*time.Millisecond))
|
|
||||||
if got, prefix := fmt.Sprint(c), "context.Background.WithDeadline("; !strings.HasPrefix(got, prefix) {
|
|
||||||
t.Errorf("c.String() = %q want prefix %q", got, prefix)
|
|
||||||
}
|
|
||||||
testDeadline(c, 200*time.Millisecond, t)
|
|
||||||
|
|
||||||
c, _ = WithDeadline(Background(), time.Now().Add(100*time.Millisecond))
|
|
||||||
o := otherContext{c}
|
|
||||||
testDeadline(o, 200*time.Millisecond, t)
|
|
||||||
|
|
||||||
c, _ = WithDeadline(Background(), time.Now().Add(100*time.Millisecond))
|
|
||||||
o = otherContext{c}
|
|
||||||
c, _ = WithDeadline(o, time.Now().Add(300*time.Millisecond))
|
|
||||||
testDeadline(c, 200*time.Millisecond, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTimeout(t *testing.T) {
|
|
||||||
c, _ := WithTimeout(Background(), 100*time.Millisecond)
|
|
||||||
if got, prefix := fmt.Sprint(c), "context.Background.WithDeadline("; !strings.HasPrefix(got, prefix) {
|
|
||||||
t.Errorf("c.String() = %q want prefix %q", got, prefix)
|
|
||||||
}
|
|
||||||
testDeadline(c, 200*time.Millisecond, t)
|
|
||||||
|
|
||||||
c, _ = WithTimeout(Background(), 100*time.Millisecond)
|
|
||||||
o := otherContext{c}
|
|
||||||
testDeadline(o, 200*time.Millisecond, t)
|
|
||||||
|
|
||||||
c, _ = WithTimeout(Background(), 100*time.Millisecond)
|
|
||||||
o = otherContext{c}
|
|
||||||
c, _ = WithTimeout(o, 300*time.Millisecond)
|
|
||||||
testDeadline(c, 200*time.Millisecond, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCanceledTimeout(t *testing.T) {
|
|
||||||
c, _ := WithTimeout(Background(), 200*time.Millisecond)
|
|
||||||
o := otherContext{c}
|
|
||||||
c, cancel := WithTimeout(o, 400*time.Millisecond)
|
|
||||||
cancel()
|
|
||||||
time.Sleep(100 * time.Millisecond) // let cancelation propagate
|
|
||||||
select {
|
|
||||||
case <-c.Done():
|
|
||||||
default:
|
|
||||||
t.Errorf("<-c.Done() blocked, but shouldn't have")
|
|
||||||
}
|
|
||||||
if e := c.Err(); e != Canceled {
|
|
||||||
t.Errorf("c.Err() == %v want %v", e, Canceled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type key1 int
|
|
||||||
type key2 int
|
|
||||||
|
|
||||||
var k1 = key1(1)
|
|
||||||
var k2 = key2(1) // same int as k1, different type
|
|
||||||
var k3 = key2(3) // same type as k2, different int
|
|
||||||
|
|
||||||
func TestValues(t *testing.T) {
|
|
||||||
check := func(c Context, nm, v1, v2, v3 string) {
|
|
||||||
if v, ok := c.Value(k1).(string); ok == (len(v1) == 0) || v != v1 {
|
|
||||||
t.Errorf(`%s.Value(k1).(string) = %q, %t want %q, %t`, nm, v, ok, v1, len(v1) != 0)
|
|
||||||
}
|
|
||||||
if v, ok := c.Value(k2).(string); ok == (len(v2) == 0) || v != v2 {
|
|
||||||
t.Errorf(`%s.Value(k2).(string) = %q, %t want %q, %t`, nm, v, ok, v2, len(v2) != 0)
|
|
||||||
}
|
|
||||||
if v, ok := c.Value(k3).(string); ok == (len(v3) == 0) || v != v3 {
|
|
||||||
t.Errorf(`%s.Value(k3).(string) = %q, %t want %q, %t`, nm, v, ok, v3, len(v3) != 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c0 := Background()
|
|
||||||
check(c0, "c0", "", "", "")
|
|
||||||
|
|
||||||
c1 := WithValue(Background(), k1, "c1k1")
|
|
||||||
check(c1, "c1", "c1k1", "", "")
|
|
||||||
|
|
||||||
if got, want := fmt.Sprint(c1), `context.Background.WithValue(1, "c1k1")`; got != want {
|
|
||||||
t.Errorf("c.String() = %q want %q", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
c2 := WithValue(c1, k2, "c2k2")
|
|
||||||
check(c2, "c2", "c1k1", "c2k2", "")
|
|
||||||
|
|
||||||
c3 := WithValue(c2, k3, "c3k3")
|
|
||||||
check(c3, "c2", "c1k1", "c2k2", "c3k3")
|
|
||||||
|
|
||||||
c4 := WithValue(c3, k1, nil)
|
|
||||||
check(c4, "c4", "", "c2k2", "c3k3")
|
|
||||||
|
|
||||||
o0 := otherContext{Background()}
|
|
||||||
check(o0, "o0", "", "", "")
|
|
||||||
|
|
||||||
o1 := otherContext{WithValue(Background(), k1, "c1k1")}
|
|
||||||
check(o1, "o1", "c1k1", "", "")
|
|
||||||
|
|
||||||
o2 := WithValue(o1, k2, "o2k2")
|
|
||||||
check(o2, "o2", "c1k1", "o2k2", "")
|
|
||||||
|
|
||||||
o3 := otherContext{c4}
|
|
||||||
check(o3, "o3", "", "c2k2", "c3k3")
|
|
||||||
|
|
||||||
o4 := WithValue(o3, k3, nil)
|
|
||||||
check(o4, "o4", "", "c2k2", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAllocs(t *testing.T) {
|
|
||||||
bg := Background()
|
|
||||||
for _, test := range []struct {
|
|
||||||
desc string
|
|
||||||
f func()
|
|
||||||
limit float64
|
|
||||||
gccgoLimit float64
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "Background()",
|
|
||||||
f: func() { Background() },
|
|
||||||
limit: 0,
|
|
||||||
gccgoLimit: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: fmt.Sprintf("WithValue(bg, %v, nil)", k1),
|
|
||||||
f: func() {
|
|
||||||
c := WithValue(bg, k1, nil)
|
|
||||||
c.Value(k1)
|
|
||||||
},
|
|
||||||
limit: 3,
|
|
||||||
gccgoLimit: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "WithTimeout(bg, 15*time.Millisecond)",
|
|
||||||
f: func() {
|
|
||||||
c, _ := WithTimeout(bg, 15*time.Millisecond)
|
|
||||||
<-c.Done()
|
|
||||||
},
|
|
||||||
limit: 8,
|
|
||||||
gccgoLimit: 15,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "WithCancel(bg)",
|
|
||||||
f: func() {
|
|
||||||
c, cancel := WithCancel(bg)
|
|
||||||
cancel()
|
|
||||||
<-c.Done()
|
|
||||||
},
|
|
||||||
limit: 5,
|
|
||||||
gccgoLimit: 8,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "WithTimeout(bg, 100*time.Millisecond)",
|
|
||||||
f: func() {
|
|
||||||
c, cancel := WithTimeout(bg, 100*time.Millisecond)
|
|
||||||
cancel()
|
|
||||||
<-c.Done()
|
|
||||||
},
|
|
||||||
limit: 8,
|
|
||||||
gccgoLimit: 25,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
limit := test.limit
|
|
||||||
if runtime.Compiler == "gccgo" {
|
|
||||||
// gccgo does not yet do escape analysis.
|
|
||||||
// TOOD(iant): Remove this when gccgo does do escape analysis.
|
|
||||||
limit = test.gccgoLimit
|
|
||||||
}
|
|
||||||
if n := testing.AllocsPerRun(100, test.f); n > limit {
|
|
||||||
t.Errorf("%s allocs = %f want %d", test.desc, n, int(limit))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSimultaneousCancels(t *testing.T) {
|
|
||||||
root, cancel := WithCancel(Background())
|
|
||||||
m := map[Context]CancelFunc{root: cancel}
|
|
||||||
q := []Context{root}
|
|
||||||
// Create a tree of contexts.
|
|
||||||
for len(q) != 0 && len(m) < 100 {
|
|
||||||
parent := q[0]
|
|
||||||
q = q[1:]
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
ctx, cancel := WithCancel(parent)
|
|
||||||
m[ctx] = cancel
|
|
||||||
q = append(q, ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Start all the cancels in a random order.
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(len(m))
|
|
||||||
for _, cancel := range m {
|
|
||||||
go func(cancel CancelFunc) {
|
|
||||||
cancel()
|
|
||||||
wg.Done()
|
|
||||||
}(cancel)
|
|
||||||
}
|
|
||||||
// Wait on all the contexts in a random order.
|
|
||||||
for ctx := range m {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
buf := make([]byte, 10<<10)
|
|
||||||
n := runtime.Stack(buf, true)
|
|
||||||
t.Fatalf("timed out waiting for <-ctx.Done(); stacks:\n%s", buf[:n])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Wait for all the cancel functions to return.
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
buf := make([]byte, 10<<10)
|
|
||||||
n := runtime.Stack(buf, true)
|
|
||||||
t.Fatalf("timed out waiting for cancel functions; stacks:\n%s", buf[:n])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInterlockedCancels(t *testing.T) {
|
|
||||||
parent, cancelParent := WithCancel(Background())
|
|
||||||
child, cancelChild := WithCancel(parent)
|
|
||||||
go func() {
|
|
||||||
parent.Done()
|
|
||||||
cancelChild()
|
|
||||||
}()
|
|
||||||
cancelParent()
|
|
||||||
select {
|
|
||||||
case <-child.Done():
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
buf := make([]byte, 10<<10)
|
|
||||||
n := runtime.Stack(buf, true)
|
|
||||||
t.Fatalf("timed out waiting for child.Done(); stacks:\n%s", buf[:n])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLayersCancel(t *testing.T) {
|
|
||||||
testLayers(t, time.Now().UnixNano(), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLayersTimeout(t *testing.T) {
|
|
||||||
testLayers(t, time.Now().UnixNano(), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testLayers(t *testing.T, seed int64, testTimeout bool) {
|
|
||||||
rand.Seed(seed)
|
|
||||||
errorf := func(format string, a ...interface{}) {
|
|
||||||
t.Errorf(fmt.Sprintf("seed=%d: %s", seed, format), a...)
|
|
||||||
}
|
|
||||||
const (
|
|
||||||
timeout = 200 * time.Millisecond
|
|
||||||
minLayers = 30
|
|
||||||
)
|
|
||||||
type value int
|
|
||||||
var (
|
|
||||||
vals []*value
|
|
||||||
cancels []CancelFunc
|
|
||||||
numTimers int
|
|
||||||
ctx = Background()
|
|
||||||
)
|
|
||||||
for i := 0; i < minLayers || numTimers == 0 || len(cancels) == 0 || len(vals) == 0; i++ {
|
|
||||||
switch rand.Intn(3) {
|
|
||||||
case 0:
|
|
||||||
v := new(value)
|
|
||||||
ctx = WithValue(ctx, v, v)
|
|
||||||
vals = append(vals, v)
|
|
||||||
case 1:
|
|
||||||
var cancel CancelFunc
|
|
||||||
ctx, cancel = WithCancel(ctx)
|
|
||||||
cancels = append(cancels, cancel)
|
|
||||||
case 2:
|
|
||||||
var cancel CancelFunc
|
|
||||||
ctx, cancel = WithTimeout(ctx, timeout)
|
|
||||||
cancels = append(cancels, cancel)
|
|
||||||
numTimers++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkValues := func(when string) {
|
|
||||||
for _, key := range vals {
|
|
||||||
if val := ctx.Value(key).(*value); key != val {
|
|
||||||
errorf("%s: ctx.Value(%p) = %p want %p", when, key, val, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
errorf("ctx should not be canceled yet")
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
if s, prefix := fmt.Sprint(ctx), "context.Background."; !strings.HasPrefix(s, prefix) {
|
|
||||||
t.Errorf("ctx.String() = %q want prefix %q", s, prefix)
|
|
||||||
}
|
|
||||||
t.Log(ctx)
|
|
||||||
checkValues("before cancel")
|
|
||||||
if testTimeout {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case <-time.After(timeout + 100*time.Millisecond):
|
|
||||||
errorf("ctx should have timed out")
|
|
||||||
}
|
|
||||||
checkValues("after timeout")
|
|
||||||
} else {
|
|
||||||
cancel := cancels[rand.Intn(len(cancels))]
|
|
||||||
cancel()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
default:
|
|
||||||
errorf("ctx should be canceled")
|
|
||||||
}
|
|
||||||
checkValues("after cancel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCancelRemoves(t *testing.T) {
|
|
||||||
checkChildren := func(when string, ctx Context, want int) {
|
|
||||||
if got := len(ctx.(*cancelCtx).children); got != want {
|
|
||||||
t.Errorf("%s: context has %d children, want %d", when, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, _ := WithCancel(Background())
|
|
||||||
checkChildren("after creation", ctx, 0)
|
|
||||||
_, cancel := WithCancel(ctx)
|
|
||||||
checkChildren("with WithCancel child ", ctx, 1)
|
|
||||||
cancel()
|
|
||||||
checkChildren("after cancelling WithCancel child", ctx, 0)
|
|
||||||
|
|
||||||
ctx, _ = WithCancel(Background())
|
|
||||||
checkChildren("after creation", ctx, 0)
|
|
||||||
_, cancel = WithTimeout(ctx, 60*time.Minute)
|
|
||||||
checkChildren("with WithTimeout child ", ctx, 1)
|
|
||||||
cancel()
|
|
||||||
checkChildren("after cancelling WithTimeout child", ctx, 0)
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
// Copyright 2015 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.
|
|
||||||
|
|
||||||
// +build go1.5
|
|
||||||
|
|
||||||
package ctxhttp
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
func canceler(client *http.Client, req *http.Request) func() {
|
|
||||||
ch := make(chan struct{})
|
|
||||||
req.Cancel = ch
|
|
||||||
|
|
||||||
return func() {
|
|
||||||
close(ch)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
// Copyright 2015 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.
|
|
||||||
|
|
||||||
// +build !go1.5
|
|
||||||
|
|
||||||
package ctxhttp
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
type requestCanceler interface {
|
|
||||||
CancelRequest(*http.Request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func canceler(client *http.Client, req *http.Request) func() {
|
|
||||||
rc, ok := client.Transport.(requestCanceler)
|
|
||||||
if !ok {
|
|
||||||
return func() {}
|
|
||||||
}
|
|
||||||
return func() {
|
|
||||||
rc.CancelRequest(req)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
// Copyright 2015 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package ctxhttp provides helper functions for performing context-aware HTTP requests.
|
|
||||||
package ctxhttp // import "golang.org/x/net/context/ctxhttp"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Do sends an HTTP request with the provided http.Client and returns an HTTP response.
|
|
||||||
// If the client is nil, http.DefaultClient is used.
|
|
||||||
// If the context is canceled or times out, ctx.Err() will be returned.
|
|
||||||
func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
|
|
||||||
if client == nil {
|
|
||||||
client = http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request cancelation changed in Go 1.5, see cancelreq.go and cancelreq_go14.go.
|
|
||||||
cancel := canceler(client, req)
|
|
||||||
|
|
||||||
type responseAndError struct {
|
|
||||||
resp *http.Response
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
result := make(chan responseAndError, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
result <- responseAndError{resp, err}
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
cancel()
|
|
||||||
return nil, ctx.Err()
|
|
||||||
case r := <-result:
|
|
||||||
return r.resp, r.err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get issues a GET request via the Do function.
|
|
||||||
func Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return Do(ctx, client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Head issues a HEAD request via the Do function.
|
|
||||||
func Head(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
|
|
||||||
req, err := http.NewRequest("HEAD", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return Do(ctx, client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post issues a POST request via the Do function.
|
|
||||||
func Post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) {
|
|
||||||
req, err := http.NewRequest("POST", url, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", bodyType)
|
|
||||||
return Do(ctx, client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostForm issues a POST request via the Do function.
|
|
||||||
func PostForm(ctx context.Context, client *http.Client, url string, data url.Values) (*http.Response, error) {
|
|
||||||
return Post(ctx, client, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
// Copyright 2015 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ctxhttp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
requestDuration = 100 * time.Millisecond
|
|
||||||
requestBody = "ok"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNoTimeout(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := doRequest(ctx)
|
|
||||||
|
|
||||||
if resp == nil || err != nil {
|
|
||||||
t.Fatalf("error received from client: %v %v", err, resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestCancel(t *testing.T) {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
go func() {
|
|
||||||
time.Sleep(requestDuration / 2)
|
|
||||||
cancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
resp, err := doRequest(ctx)
|
|
||||||
|
|
||||||
if resp != nil || err == nil {
|
|
||||||
t.Fatalf("expected error, didn't get one. resp: %v", resp)
|
|
||||||
}
|
|
||||||
if err != ctx.Err() {
|
|
||||||
t.Fatalf("expected error from context but got: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCancelAfterRequest(t *testing.T) {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
resp, err := doRequest(ctx)
|
|
||||||
|
|
||||||
// Cancel before reading the body.
|
|
||||||
// Request.Body should still be readable after the context is canceled.
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil || string(b) != requestBody {
|
|
||||||
t.Fatalf("could not read body: %q %v", b, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func doRequest(ctx context.Context) (*http.Response, error) {
|
|
||||||
var okHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
time.Sleep(requestDuration)
|
|
||||||
w.Write([]byte(requestBody))
|
|
||||||
})
|
|
||||||
|
|
||||||
serv := httptest.NewServer(okHandler)
|
|
||||||
defer serv.Close()
|
|
||||||
|
|
||||||
return Get(ctx, nil, serv.URL)
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
// Copyright 2014 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package context_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExampleWithTimeout() {
|
|
||||||
// Pass a context with a timeout to tell a blocking function that it
|
|
||||||
// should abandon its work after the timeout elapses.
|
|
||||||
ctx, _ := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
||||||
select {
|
|
||||||
case <-time.After(200 * time.Millisecond):
|
|
||||||
fmt.Println("overslept")
|
|
||||||
case <-ctx.Done():
|
|
||||||
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
|
|
||||||
}
|
|
||||||
// Output:
|
|
||||||
// context deadline exceeded
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
# This source code refers to The Go Authors for copyright purposes.
|
|
||||||
# The master list of authors is in the main Go distribution,
|
|
||||||
# visible at http://tip.golang.org/AUTHORS.
|
|
|
@ -1,31 +0,0 @@
|
||||||
# Contributing to Go
|
|
||||||
|
|
||||||
Go is an open source project.
|
|
||||||
|
|
||||||
It is the work of hundreds of contributors. We appreciate your help!
|
|
||||||
|
|
||||||
|
|
||||||
## Filing issues
|
|
||||||
|
|
||||||
When [filing an issue](https://github.com/golang/oauth2/issues), make sure to answer these five questions:
|
|
||||||
|
|
||||||
1. What version of Go are you using (`go version`)?
|
|
||||||
2. What operating system and processor architecture are you using?
|
|
||||||
3. What did you do?
|
|
||||||
4. What did you expect to see?
|
|
||||||
5. What did you see instead?
|
|
||||||
|
|
||||||
General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker.
|
|
||||||
The gophers there will answer or ask you to file an issue if you've tripped over a bug.
|
|
||||||
|
|
||||||
## Contributing code
|
|
||||||
|
|
||||||
Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html)
|
|
||||||
before sending patches.
|
|
||||||
|
|
||||||
**We do not accept GitHub pull requests**
|
|
||||||
(we use [Gerrit](https://code.google.com/p/gerrit/) instead for code review).
|
|
||||||
|
|
||||||
Unless otherwise noted, the Go source files are distributed under
|
|
||||||
the BSD-style license found in the LICENSE file.
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
# This source code was written by the Go contributors.
|
|
||||||
# The master list of contributors is in the main Go distribution,
|
|
||||||
# visible at http://tip.golang.org/CONTRIBUTORS.
|
|
|
@ -1,27 +0,0 @@
|
||||||
Copyright (c) 2009 The oauth2 Authors. All rights reserved.
|
|
||||||
|
|
||||||
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.
|
|
|
@ -1,64 +0,0 @@
|
||||||
# OAuth2 for Go
|
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/golang/oauth2.svg?branch=master)](https://travis-ci.org/golang/oauth2)
|
|
||||||
|
|
||||||
oauth2 package contains a client implementation for OAuth 2.0 spec.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
~~~~
|
|
||||||
go get golang.org/x/oauth2
|
|
||||||
~~~~
|
|
||||||
|
|
||||||
See godoc for further documentation and examples.
|
|
||||||
|
|
||||||
* [godoc.org/golang.org/x/oauth2](http://godoc.org/golang.org/x/oauth2)
|
|
||||||
* [godoc.org/golang.org/x/oauth2/google](http://godoc.org/golang.org/x/oauth2/google)
|
|
||||||
|
|
||||||
|
|
||||||
## App Engine
|
|
||||||
|
|
||||||
In change 96e89be (March 2015) we removed the `oauth2.Context2` type in favor
|
|
||||||
of the [`context.Context`](https://golang.org/x/net/context#Context) type from
|
|
||||||
the `golang.org/x/net/context` package
|
|
||||||
|
|
||||||
This means its no longer possible to use the "Classic App Engine"
|
|
||||||
`appengine.Context` type with the `oauth2` package. (You're using
|
|
||||||
Classic App Engine if you import the package `"appengine"`.)
|
|
||||||
|
|
||||||
To work around this, you may use the new `"google.golang.org/appengine"`
|
|
||||||
package. This package has almost the same API as the `"appengine"` package,
|
|
||||||
but it can be fetched with `go get` and used on "Managed VMs" and well as
|
|
||||||
Classic App Engine.
|
|
||||||
|
|
||||||
See the [new `appengine` package's readme](https://github.com/golang/appengine#updating-a-go-app-engine-app)
|
|
||||||
for information on updating your app.
|
|
||||||
|
|
||||||
If you don't want to update your entire app to use the new App Engine packages,
|
|
||||||
you may use both sets of packages in parallel, using only the new packages
|
|
||||||
with the `oauth2` package.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/google"
|
|
||||||
newappengine "google.golang.org/appengine"
|
|
||||||
newurlfetch "google.golang.org/appengine/urlfetch"
|
|
||||||
|
|
||||||
"appengine"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var c appengine.Context = appengine.NewContext(r)
|
|
||||||
c.Infof("Logging a message with the old package")
|
|
||||||
|
|
||||||
var ctx context.Context = newappengine.NewContext(r)
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: &oauth2.Transport{
|
|
||||||
Source: google.AppEngineTokenSource(ctx, "scope"),
|
|
||||||
Base: &newurlfetch.Transport{Context: ctx},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client.Get("...")
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package bitbucket provides constants for using OAuth2 to access Bitbucket.
|
|
||||||
package bitbucket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Endpoint is Bitbucket's OAuth 2.0 endpoint.
|
|
||||||
var Endpoint = oauth2.Endpoint{
|
|
||||||
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
|
|
||||||
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build appengine
|
|
||||||
|
|
||||||
// App Engine hooks.
|
|
||||||
|
|
||||||
package oauth2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
"golang.org/x/oauth2/internal"
|
|
||||||
"google.golang.org/appengine/urlfetch"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
internal.RegisterContextClientFunc(contextClientAppEngine)
|
|
||||||
}
|
|
||||||
|
|
||||||
func contextClientAppEngine(ctx context.Context) (*http.Client, error) {
|
|
||||||
return urlfetch.Client(ctx), nil
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package clientcredentials implements the OAuth2.0 "client credentials" token flow,
|
|
||||||
// also known as the "two-legged OAuth 2.0".
|
|
||||||
//
|
|
||||||
// This should be used when the client is acting on its own behalf or when the client
|
|
||||||
// is the resource owner. It may also be used when requesting access to protected
|
|
||||||
// resources based on an authorization previously arranged with the authorization
|
|
||||||
// server.
|
|
||||||
//
|
|
||||||
// See http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.4
|
|
||||||
package clientcredentials // import "golang.org/x/oauth2/clientcredentials"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
// tokenFromInternal maps an *internal.Token struct into
|
|
||||||
// an *oauth2.Token struct.
|
|
||||||
func tokenFromInternal(t *internal.Token) *oauth2.Token {
|
|
||||||
if t == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tk := &oauth2.Token{
|
|
||||||
AccessToken: t.AccessToken,
|
|
||||||
TokenType: t.TokenType,
|
|
||||||
RefreshToken: t.RefreshToken,
|
|
||||||
Expiry: t.Expiry,
|
|
||||||
}
|
|
||||||
return tk.WithExtra(t.Raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrieveToken takes a *Config and uses that to retrieve an *internal.Token.
|
|
||||||
// This token is then mapped from *internal.Token into an *oauth2.Token which is
|
|
||||||
// returned along with an error.
|
|
||||||
func retrieveToken(ctx context.Context, c *Config, v url.Values) (*oauth2.Token, error) {
|
|
||||||
tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.TokenURL, v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tokenFromInternal(tk), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client Credentials Config describes a 2-legged OAuth2 flow, with both the
|
|
||||||
// client application information and the server's endpoint URLs.
|
|
||||||
type Config struct {
|
|
||||||
// ClientID is the application's ID.
|
|
||||||
ClientID string
|
|
||||||
|
|
||||||
// ClientSecret is the application's secret.
|
|
||||||
ClientSecret string
|
|
||||||
|
|
||||||
// TokenURL is the resource server's token endpoint
|
|
||||||
// URL. This is a constant specific to each server.
|
|
||||||
TokenURL string
|
|
||||||
|
|
||||||
// Scope specifies optional requested permissions.
|
|
||||||
Scopes []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Token uses client credentials to retreive a token.
|
|
||||||
// The HTTP client to use is derived from the context.
|
|
||||||
// If nil, http.DefaultClient is used.
|
|
||||||
func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) {
|
|
||||||
return retrieveToken(ctx, c, url.Values{
|
|
||||||
"grant_type": {"client_credentials"},
|
|
||||||
"scope": internal.CondVal(strings.Join(c.Scopes, " ")),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client returns an HTTP client using the provided token.
|
|
||||||
// The token will auto-refresh as necessary. The underlying
|
|
||||||
// HTTP transport will be obtained using the provided context.
|
|
||||||
// The returned client and its Transport should not be modified.
|
|
||||||
func (c *Config) Client(ctx context.Context) *http.Client {
|
|
||||||
return oauth2.NewClient(ctx, c.TokenSource(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenSource returns a TokenSource that returns t until t expires,
|
|
||||||
// automatically refreshing it as necessary using the provided context and the
|
|
||||||
// client ID and client secret.
|
|
||||||
//
|
|
||||||
// Most users will use Config.Client instead.
|
|
||||||
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
|
|
||||||
source := &tokenSource{
|
|
||||||
ctx: ctx,
|
|
||||||
conf: c,
|
|
||||||
}
|
|
||||||
return oauth2.ReuseTokenSource(nil, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
type tokenSource struct {
|
|
||||||
ctx context.Context
|
|
||||||
conf *Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// Token refreshes the token by using a new client credentials request.
|
|
||||||
// tokens received this way do not include a refresh token
|
|
||||||
func (c *tokenSource) Token() (*oauth2.Token, error) {
|
|
||||||
return retrieveToken(c.ctx, c.conf, url.Values{
|
|
||||||
"grant_type": {"client_credentials"},
|
|
||||||
"scope": internal.CondVal(strings.Join(c.conf.Scopes, " ")),
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package clientcredentials
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newConf(url string) *Config {
|
|
||||||
return &Config{
|
|
||||||
ClientID: "CLIENT_ID",
|
|
||||||
ClientSecret: "CLIENT_SECRET",
|
|
||||||
Scopes: []string{"scope1", "scope2"},
|
|
||||||
TokenURL: url + "/token",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockTransport struct {
|
|
||||||
rt func(req *http.Request) (resp *http.Response, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
|
||||||
return t.rt(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTokenRequest(t *testing.T) {
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.String() != "/token" {
|
|
||||||
t.Errorf("authenticate client request URL = %q; want %q", r.URL, "/token")
|
|
||||||
}
|
|
||||||
headerAuth := r.Header.Get("Authorization")
|
|
||||||
if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" {
|
|
||||||
t.Errorf("Unexpected authorization header, %v is found.", headerAuth)
|
|
||||||
}
|
|
||||||
if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want {
|
|
||||||
t.Errorf("Content-Type header = %q; want %q", got, want)
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
r.Body.Close()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed reading request body: %s.", err)
|
|
||||||
}
|
|
||||||
if string(body) != "client_id=CLIENT_ID&grant_type=client_credentials&scope=scope1+scope2" {
|
|
||||||
t.Errorf("payload = %q; want %q", string(body), "client_id=CLIENT_ID&grant_type=client_credentials&scope=scope1+scope2")
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&token_type=bearer"))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
conf := newConf(ts.URL)
|
|
||||||
tok, err := conf.Token(oauth2.NoContext)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
if !tok.Valid() {
|
|
||||||
t.Fatalf("token invalid. got: %#v", tok)
|
|
||||||
}
|
|
||||||
if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" {
|
|
||||||
t.Errorf("Access token = %q; want %q", tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c")
|
|
||||||
}
|
|
||||||
if tok.TokenType != "bearer" {
|
|
||||||
t.Errorf("token type = %q; want %q", tok.TokenType, "bearer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTokenRefreshRequest(t *testing.T) {
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.String() == "/somethingelse" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.URL.String() != "/token" {
|
|
||||||
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
|
|
||||||
}
|
|
||||||
headerContentType := r.Header.Get("Content-Type")
|
|
||||||
if headerContentType != "application/x-www-form-urlencoded" {
|
|
||||||
t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType)
|
|
||||||
}
|
|
||||||
body, _ := ioutil.ReadAll(r.Body)
|
|
||||||
if string(body) != "client_id=CLIENT_ID&grant_type=client_credentials&scope=scope1+scope2" {
|
|
||||||
t.Errorf("Unexpected refresh token payload, %v is found.", string(body))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
conf := newConf(ts.URL)
|
|
||||||
c := conf.Client(oauth2.NoContext)
|
|
||||||
c.Get(ts.URL + "/somethingelse")
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package oauth2_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExampleConfig() {
|
|
||||||
conf := &oauth2.Config{
|
|
||||||
ClientID: "YOUR_CLIENT_ID",
|
|
||||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
|
||||||
Scopes: []string{"SCOPE1", "SCOPE2"},
|
|
||||||
Endpoint: oauth2.Endpoint{
|
|
||||||
AuthURL: "https://provider.com/o/oauth2/auth",
|
|
||||||
TokenURL: "https://provider.com/o/oauth2/token",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect user to consent page to ask for permission
|
|
||||||
// for the scopes specified above.
|
|
||||||
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
|
|
||||||
fmt.Printf("Visit the URL for the auth dialog: %v", url)
|
|
||||||
|
|
||||||
// Use the authorization code that is pushed to the redirect URL.
|
|
||||||
// NewTransportWithCode will do the handshake to retrieve
|
|
||||||
// an access token and initiate a Transport that is
|
|
||||||
// authorized and authenticated by the retrieved token.
|
|
||||||
var code string
|
|
||||||
if _, err := fmt.Scan(&code); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
tok, err := conf.Exchange(oauth2.NoContext, code)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client := conf.Client(oauth2.NoContext, tok)
|
|
||||||
client.Get("...")
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package facebook provides constants for using OAuth2 to access Facebook.
|
|
||||||
package facebook // import "golang.org/x/oauth2/facebook"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Endpoint is Facebook's OAuth 2.0 endpoint.
|
|
||||||
var Endpoint = oauth2.Endpoint{
|
|
||||||
AuthURL: "https://www.facebook.com/dialog/oauth",
|
|
||||||
TokenURL: "https://graph.facebook.com/oauth/access_token",
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package github provides constants for using OAuth2 to access Github.
|
|
||||||
package github // import "golang.org/x/oauth2/github"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Endpoint is Github's OAuth 2.0 endpoint.
|
|
||||||
var Endpoint = oauth2.Endpoint{
|
|
||||||
AuthURL: "https://github.com/login/oauth/authorize",
|
|
||||||
TokenURL: "https://github.com/login/oauth/access_token",
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package google
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Set at init time by appenginevm_hook.go. If true, we are on App Engine Managed VMs.
|
|
||||||
var appengineVM bool
|
|
||||||
|
|
||||||
// Set at init time by appengine_hook.go. If nil, we're not on App Engine.
|
|
||||||
var appengineTokenFunc func(c context.Context, scopes ...string) (token string, expiry time.Time, err error)
|
|
||||||
|
|
||||||
// AppEngineTokenSource returns a token source that fetches tokens
|
|
||||||
// issued to the current App Engine application's service account.
|
|
||||||
// If you are implementing a 3-legged OAuth 2.0 flow on App Engine
|
|
||||||
// that involves user accounts, see oauth2.Config instead.
|
|
||||||
//
|
|
||||||
// The provided context must have come from appengine.NewContext.
|
|
||||||
func AppEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource {
|
|
||||||
if appengineTokenFunc == nil {
|
|
||||||
panic("google: AppEngineTokenSource can only be used on App Engine.")
|
|
||||||
}
|
|
||||||
scopes := append([]string{}, scope...)
|
|
||||||
sort.Strings(scopes)
|
|
||||||
return &appEngineTokenSource{
|
|
||||||
ctx: ctx,
|
|
||||||
scopes: scopes,
|
|
||||||
key: strings.Join(scopes, " "),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// aeTokens helps the fetched tokens to be reused until their expiration.
|
|
||||||
var (
|
|
||||||
aeTokensMu sync.Mutex
|
|
||||||
aeTokens = make(map[string]*tokenLock) // key is space-separated scopes
|
|
||||||
)
|
|
||||||
|
|
||||||
type tokenLock struct {
|
|
||||||
mu sync.Mutex // guards t; held while fetching or updating t
|
|
||||||
t *oauth2.Token
|
|
||||||
}
|
|
||||||
|
|
||||||
type appEngineTokenSource struct {
|
|
||||||
ctx context.Context
|
|
||||||
scopes []string
|
|
||||||
key string // to aeTokens map; space-separated scopes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *appEngineTokenSource) Token() (*oauth2.Token, error) {
|
|
||||||
if appengineTokenFunc == nil {
|
|
||||||
panic("google: AppEngineTokenSource can only be used on App Engine.")
|
|
||||||
}
|
|
||||||
|
|
||||||
aeTokensMu.Lock()
|
|
||||||
tok, ok := aeTokens[ts.key]
|
|
||||||
if !ok {
|
|
||||||
tok = &tokenLock{}
|
|
||||||
aeTokens[ts.key] = tok
|
|
||||||
}
|
|
||||||
aeTokensMu.Unlock()
|
|
||||||
|
|
||||||
tok.mu.Lock()
|
|
||||||
defer tok.mu.Unlock()
|
|
||||||
if tok.t.Valid() {
|
|
||||||
return tok.t, nil
|
|
||||||
}
|
|
||||||
access, exp, err := appengineTokenFunc(ts.ctx, ts.scopes...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tok.t = &oauth2.Token{
|
|
||||||
AccessToken: access,
|
|
||||||
Expiry: exp,
|
|
||||||
}
|
|
||||||
return tok.t, nil
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build appengine
|
|
||||||
|
|
||||||
package google
|
|
||||||
|
|
||||||
import "google.golang.org/appengine"
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
appengineTokenFunc = appengine.AccessToken
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build appenginevm
|
|
||||||
|
|
||||||
package google
|
|
||||||
|
|
||||||
import "google.golang.org/appengine"
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
appengineVM = true
|
|
||||||
appengineTokenFunc = appengine.AccessToken
|
|
||||||
}
|
|
|
@ -1,155 +0,0 @@
|
||||||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package google
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/jwt"
|
|
||||||
"google.golang.org/cloud/compute/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultClient returns an HTTP Client that uses the
|
|
||||||
// DefaultTokenSource to obtain authentication credentials.
|
|
||||||
//
|
|
||||||
// This client should be used when developing services
|
|
||||||
// that run on Google App Engine or Google Compute Engine
|
|
||||||
// and use "Application Default Credentials."
|
|
||||||
//
|
|
||||||
// For more details, see:
|
|
||||||
// https://developers.google.com/accounts/docs/application-default-credentials
|
|
||||||
//
|
|
||||||
func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) {
|
|
||||||
ts, err := DefaultTokenSource(ctx, scope...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return oauth2.NewClient(ctx, ts), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultTokenSource is a token source that uses
|
|
||||||
// "Application Default Credentials".
|
|
||||||
//
|
|
||||||
// It looks for credentials in the following places,
|
|
||||||
// preferring the first location found:
|
|
||||||
//
|
|
||||||
// 1. A JSON file whose path is specified by the
|
|
||||||
// GOOGLE_APPLICATION_CREDENTIALS environment variable.
|
|
||||||
// 2. A JSON file in a location known to the gcloud command-line tool.
|
|
||||||
// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json.
|
|
||||||
// On other systems, $HOME/.config/gcloud/application_default_credentials.json.
|
|
||||||
// 3. On Google App Engine it uses the appengine.AccessToken function.
|
|
||||||
// 4. On Google Compute Engine and Google App Engine Managed VMs, it fetches
|
|
||||||
// credentials from the metadata server.
|
|
||||||
// (In this final case any provided scopes are ignored.)
|
|
||||||
//
|
|
||||||
// For more details, see:
|
|
||||||
// https://developers.google.com/accounts/docs/application-default-credentials
|
|
||||||
//
|
|
||||||
func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSource, error) {
|
|
||||||
// First, try the environment variable.
|
|
||||||
const envVar = "GOOGLE_APPLICATION_CREDENTIALS"
|
|
||||||
if filename := os.Getenv(envVar); filename != "" {
|
|
||||||
ts, err := tokenSourceFromFile(ctx, filename, scope)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("google: error getting credentials using %v environment variable: %v", envVar, err)
|
|
||||||
}
|
|
||||||
return ts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second, try a well-known file.
|
|
||||||
filename := wellKnownFile()
|
|
||||||
_, err := os.Stat(filename)
|
|
||||||
if err == nil {
|
|
||||||
ts, err2 := tokenSourceFromFile(ctx, filename, scope)
|
|
||||||
if err2 == nil {
|
|
||||||
return ts, nil
|
|
||||||
}
|
|
||||||
err = err2
|
|
||||||
} else if os.IsNotExist(err) {
|
|
||||||
err = nil // ignore this error
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Third, if we're on Google App Engine use those credentials.
|
|
||||||
if appengineTokenFunc != nil && !appengineVM {
|
|
||||||
return AppEngineTokenSource(ctx, scope...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fourth, if we're on Google Compute Engine use the metadata server.
|
|
||||||
if metadata.OnGCE() {
|
|
||||||
return ComputeTokenSource(""), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// None are found; return helpful error.
|
|
||||||
const url = "https://developers.google.com/accounts/docs/application-default-credentials"
|
|
||||||
return nil, fmt.Errorf("google: could not find default credentials. See %v for more information.", url)
|
|
||||||
}
|
|
||||||
|
|
||||||
func wellKnownFile() string {
|
|
||||||
const f = "application_default_credentials.json"
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
return filepath.Join(os.Getenv("APPDATA"), "gcloud", f)
|
|
||||||
}
|
|
||||||
return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tokenSourceFromFile(ctx context.Context, filename string, scopes []string) (oauth2.TokenSource, error) {
|
|
||||||
b, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var d struct {
|
|
||||||
// Common fields
|
|
||||||
Type string
|
|
||||||
ClientID string `json:"client_id"`
|
|
||||||
|
|
||||||
// User Credential fields
|
|
||||||
ClientSecret string `json:"client_secret"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
|
|
||||||
// Service Account fields
|
|
||||||
ClientEmail string `json:"client_email"`
|
|
||||||
PrivateKeyID string `json:"private_key_id"`
|
|
||||||
PrivateKey string `json:"private_key"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(b, &d); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch d.Type {
|
|
||||||
case "authorized_user":
|
|
||||||
cfg := &oauth2.Config{
|
|
||||||
ClientID: d.ClientID,
|
|
||||||
ClientSecret: d.ClientSecret,
|
|
||||||
Scopes: append([]string{}, scopes...), // copy
|
|
||||||
Endpoint: Endpoint,
|
|
||||||
}
|
|
||||||
tok := &oauth2.Token{RefreshToken: d.RefreshToken}
|
|
||||||
return cfg.TokenSource(ctx, tok), nil
|
|
||||||
case "service_account":
|
|
||||||
cfg := &jwt.Config{
|
|
||||||
Email: d.ClientEmail,
|
|
||||||
PrivateKey: []byte(d.PrivateKey),
|
|
||||||
Scopes: append([]string{}, scopes...), // copy
|
|
||||||
TokenURL: JWTTokenURL,
|
|
||||||
}
|
|
||||||
return cfg.TokenSource(ctx), nil
|
|
||||||
case "":
|
|
||||||
return nil, errors.New("missing 'type' field in credentials")
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown credential type: %q", d.Type)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,150 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build appenginevm !appengine
|
|
||||||
|
|
||||||
package google_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/google"
|
|
||||||
"golang.org/x/oauth2/jwt"
|
|
||||||
"google.golang.org/appengine"
|
|
||||||
"google.golang.org/appengine/urlfetch"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExampleDefaultClient() {
|
|
||||||
client, err := google.DefaultClient(oauth2.NoContext,
|
|
||||||
"https://www.googleapis.com/auth/devstorage.full_control")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
client.Get("...")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Example_webServer() {
|
|
||||||
// Your credentials should be obtained from the Google
|
|
||||||
// Developer Console (https://console.developers.google.com).
|
|
||||||
conf := &oauth2.Config{
|
|
||||||
ClientID: "YOUR_CLIENT_ID",
|
|
||||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
|
||||||
RedirectURL: "YOUR_REDIRECT_URL",
|
|
||||||
Scopes: []string{
|
|
||||||
"https://www.googleapis.com/auth/bigquery",
|
|
||||||
"https://www.googleapis.com/auth/blogger",
|
|
||||||
},
|
|
||||||
Endpoint: google.Endpoint,
|
|
||||||
}
|
|
||||||
// Redirect user to Google's consent page to ask for permission
|
|
||||||
// for the scopes specified above.
|
|
||||||
url := conf.AuthCodeURL("state")
|
|
||||||
fmt.Printf("Visit the URL for the auth dialog: %v", url)
|
|
||||||
|
|
||||||
// Handle the exchange code to initiate a transport.
|
|
||||||
tok, err := conf.Exchange(oauth2.NoContext, "authorization-code")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
client := conf.Client(oauth2.NoContext, tok)
|
|
||||||
client.Get("...")
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleJWTConfigFromJSON() {
|
|
||||||
// Your credentials should be obtained from the Google
|
|
||||||
// Developer Console (https://console.developers.google.com).
|
|
||||||
// Navigate to your project, then see the "Credentials" page
|
|
||||||
// under "APIs & Auth".
|
|
||||||
// To create a service account client, click "Create new Client ID",
|
|
||||||
// select "Service Account", and click "Create Client ID". A JSON
|
|
||||||
// key file will then be downloaded to your computer.
|
|
||||||
data, err := ioutil.ReadFile("/path/to/your-project-key.json")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
conf, err := google.JWTConfigFromJSON(data, "https://www.googleapis.com/auth/bigquery")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
// Initiate an http.Client. The following GET request will be
|
|
||||||
// authorized and authenticated on the behalf of
|
|
||||||
// your service account.
|
|
||||||
client := conf.Client(oauth2.NoContext)
|
|
||||||
client.Get("...")
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleSDKConfig() {
|
|
||||||
// The credentials will be obtained from the first account that
|
|
||||||
// has been authorized with `gcloud auth login`.
|
|
||||||
conf, err := google.NewSDKConfig("")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
// Initiate an http.Client. The following GET request will be
|
|
||||||
// authorized and authenticated on the behalf of the SDK user.
|
|
||||||
client := conf.Client(oauth2.NoContext)
|
|
||||||
client.Get("...")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Example_serviceAccount() {
|
|
||||||
// Your credentials should be obtained from the Google
|
|
||||||
// Developer Console (https://console.developers.google.com).
|
|
||||||
conf := &jwt.Config{
|
|
||||||
Email: "xxx@developer.gserviceaccount.com",
|
|
||||||
// The contents of your RSA private key or your PEM file
|
|
||||||
// that contains a private key.
|
|
||||||
// If you have a p12 file instead, you
|
|
||||||
// can use `openssl` to export the private key into a pem file.
|
|
||||||
//
|
|
||||||
// $ openssl pkcs12 -in key.p12 -passin pass:notasecret -out key.pem -nodes
|
|
||||||
//
|
|
||||||
// The field only supports PEM containers with no passphrase.
|
|
||||||
// The openssl command will convert p12 keys to passphrase-less PEM containers.
|
|
||||||
PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----..."),
|
|
||||||
Scopes: []string{
|
|
||||||
"https://www.googleapis.com/auth/bigquery",
|
|
||||||
"https://www.googleapis.com/auth/blogger",
|
|
||||||
},
|
|
||||||
TokenURL: google.JWTTokenURL,
|
|
||||||
// If you would like to impersonate a user, you can
|
|
||||||
// create a transport with a subject. The following GET
|
|
||||||
// request will be made on the behalf of user@example.com.
|
|
||||||
// Optional.
|
|
||||||
Subject: "user@example.com",
|
|
||||||
}
|
|
||||||
// Initiate an http.Client, the following GET request will be
|
|
||||||
// authorized and authenticated on the behalf of user@example.com.
|
|
||||||
client := conf.Client(oauth2.NoContext)
|
|
||||||
client.Get("...")
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleAppEngineTokenSource() {
|
|
||||||
var req *http.Request // from the ServeHTTP handler
|
|
||||||
ctx := appengine.NewContext(req)
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: &oauth2.Transport{
|
|
||||||
Source: google.AppEngineTokenSource(ctx, "https://www.googleapis.com/auth/bigquery"),
|
|
||||||
Base: &urlfetch.Transport{
|
|
||||||
Context: ctx,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client.Get("...")
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleComputeTokenSource() {
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: &oauth2.Transport{
|
|
||||||
// Fetch from Google Compute Engine's metadata server to retrieve
|
|
||||||
// an access token for the provided account.
|
|
||||||
// If no account is specified, "default" is used.
|
|
||||||
Source: google.ComputeTokenSource(""),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client.Get("...")
|
|
||||||
}
|
|
|
@ -1,145 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package google provides support for making OAuth2 authorized and
|
|
||||||
// authenticated HTTP requests to Google APIs.
|
|
||||||
// It supports the Web server flow, client-side credentials, service accounts,
|
|
||||||
// Google Compute Engine service accounts, and Google App Engine service
|
|
||||||
// accounts.
|
|
||||||
//
|
|
||||||
// For more information, please read
|
|
||||||
// https://developers.google.com/accounts/docs/OAuth2
|
|
||||||
// and
|
|
||||||
// https://developers.google.com/accounts/docs/application-default-credentials.
|
|
||||||
package google // import "golang.org/x/oauth2/google"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/jwt"
|
|
||||||
"google.golang.org/cloud/compute/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Endpoint is Google's OAuth 2.0 endpoint.
|
|
||||||
var Endpoint = oauth2.Endpoint{
|
|
||||||
AuthURL: "https://accounts.google.com/o/oauth2/auth",
|
|
||||||
TokenURL: "https://accounts.google.com/o/oauth2/token",
|
|
||||||
}
|
|
||||||
|
|
||||||
// JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
|
|
||||||
const JWTTokenURL = "https://accounts.google.com/o/oauth2/token"
|
|
||||||
|
|
||||||
// ConfigFromJSON uses a Google Developers Console client_credentials.json
|
|
||||||
// file to construct a config.
|
|
||||||
// client_credentials.json can be downloadable from https://console.developers.google.com,
|
|
||||||
// under "APIs & Auth" > "Credentials". Download the Web application credentials in the
|
|
||||||
// JSON format and provide the contents of the file as jsonKey.
|
|
||||||
func ConfigFromJSON(jsonKey []byte, scope ...string) (*oauth2.Config, error) {
|
|
||||||
type cred struct {
|
|
||||||
ClientID string `json:"client_id"`
|
|
||||||
ClientSecret string `json:"client_secret"`
|
|
||||||
RedirectURIs []string `json:"redirect_uris"`
|
|
||||||
AuthURI string `json:"auth_uri"`
|
|
||||||
TokenURI string `json:"token_uri"`
|
|
||||||
}
|
|
||||||
var j struct {
|
|
||||||
Web *cred `json:"web"`
|
|
||||||
Installed *cred `json:"installed"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(jsonKey, &j); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var c *cred
|
|
||||||
switch {
|
|
||||||
case j.Web != nil:
|
|
||||||
c = j.Web
|
|
||||||
case j.Installed != nil:
|
|
||||||
c = j.Installed
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("oauth2/google: no credentials found")
|
|
||||||
}
|
|
||||||
if len(c.RedirectURIs) < 1 {
|
|
||||||
return nil, errors.New("oauth2/google: missing redirect URL in the client_credentials.json")
|
|
||||||
}
|
|
||||||
return &oauth2.Config{
|
|
||||||
ClientID: c.ClientID,
|
|
||||||
ClientSecret: c.ClientSecret,
|
|
||||||
RedirectURL: c.RedirectURIs[0],
|
|
||||||
Scopes: scope,
|
|
||||||
Endpoint: oauth2.Endpoint{
|
|
||||||
AuthURL: c.AuthURI,
|
|
||||||
TokenURL: c.TokenURI,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// JWTConfigFromJSON uses a Google Developers service account JSON key file to read
|
|
||||||
// the credentials that authorize and authenticate the requests.
|
|
||||||
// Create a service account on "Credentials" page under "APIs & Auth" for your
|
|
||||||
// project at https://console.developers.google.com to download a JSON key file.
|
|
||||||
func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
|
|
||||||
var key struct {
|
|
||||||
Email string `json:"client_email"`
|
|
||||||
PrivateKey string `json:"private_key"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(jsonKey, &key); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &jwt.Config{
|
|
||||||
Email: key.Email,
|
|
||||||
PrivateKey: []byte(key.PrivateKey),
|
|
||||||
Scopes: scope,
|
|
||||||
TokenURL: JWTTokenURL,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ComputeTokenSource returns a token source that fetches access tokens
|
|
||||||
// from Google Compute Engine (GCE)'s metadata server. It's only valid to use
|
|
||||||
// this token source if your program is running on a GCE instance.
|
|
||||||
// If no account is specified, "default" is used.
|
|
||||||
// Further information about retrieving access tokens from the GCE metadata
|
|
||||||
// server can be found at https://cloud.google.com/compute/docs/authentication.
|
|
||||||
func ComputeTokenSource(account string) oauth2.TokenSource {
|
|
||||||
return oauth2.ReuseTokenSource(nil, computeSource{account: account})
|
|
||||||
}
|
|
||||||
|
|
||||||
type computeSource struct {
|
|
||||||
account string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cs computeSource) Token() (*oauth2.Token, error) {
|
|
||||||
if !metadata.OnGCE() {
|
|
||||||
return nil, errors.New("oauth2/google: can't get a token from the metadata service; not running on GCE")
|
|
||||||
}
|
|
||||||
acct := cs.account
|
|
||||||
if acct == "" {
|
|
||||||
acct = "default"
|
|
||||||
}
|
|
||||||
tokenJSON, err := metadata.Get("instance/service-accounts/" + acct + "/token")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var res struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
ExpiresInSec int `json:"expires_in"`
|
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
}
|
|
||||||
err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err)
|
|
||||||
}
|
|
||||||
if res.ExpiresInSec == 0 || res.AccessToken == "" {
|
|
||||||
return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata")
|
|
||||||
}
|
|
||||||
return &oauth2.Token{
|
|
||||||
AccessToken: res.AccessToken,
|
|
||||||
TokenType: res.TokenType,
|
|
||||||
Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second),
|
|
||||||
}, nil
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package google
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var webJSONKey = []byte(`
|
|
||||||
{
|
|
||||||
"web": {
|
|
||||||
"auth_uri": "https://google.com/o/oauth2/auth",
|
|
||||||
"client_secret": "3Oknc4jS_wA2r9i",
|
|
||||||
"token_uri": "https://google.com/o/oauth2/token",
|
|
||||||
"client_email": "222-nprqovg5k43uum874cs9osjt2koe97g8@developer.gserviceaccount.com",
|
|
||||||
"redirect_uris": ["https://www.example.com/oauth2callback"],
|
|
||||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/222-nprqovg5k43uum874cs9osjt2koe97g8@developer.gserviceaccount.com",
|
|
||||||
"client_id": "222-nprqovg5k43uum874cs9osjt2koe97g8.apps.googleusercontent.com",
|
|
||||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
||||||
"javascript_origins": ["https://www.example.com"]
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
var installedJSONKey = []byte(`{
|
|
||||||
"installed": {
|
|
||||||
"client_id": "222-installed.apps.googleusercontent.com",
|
|
||||||
"redirect_uris": ["https://www.example.com/oauth2callback"]
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
func TestConfigFromJSON(t *testing.T) {
|
|
||||||
conf, err := ConfigFromJSON(webJSONKey, "scope1", "scope2")
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
if got, want := conf.ClientID, "222-nprqovg5k43uum874cs9osjt2koe97g8.apps.googleusercontent.com"; got != want {
|
|
||||||
t.Errorf("ClientID = %q; want %q", got, want)
|
|
||||||
}
|
|
||||||
if got, want := conf.ClientSecret, "3Oknc4jS_wA2r9i"; got != want {
|
|
||||||
t.Errorf("ClientSecret = %q; want %q", got, want)
|
|
||||||
}
|
|
||||||
if got, want := conf.RedirectURL, "https://www.example.com/oauth2callback"; got != want {
|
|
||||||
t.Errorf("RedictURL = %q; want %q", got, want)
|
|
||||||
}
|
|
||||||
if got, want := strings.Join(conf.Scopes, ","), "scope1,scope2"; got != want {
|
|
||||||
t.Errorf("Scopes = %q; want %q", got, want)
|
|
||||||
}
|
|
||||||
if got, want := conf.Endpoint.AuthURL, "https://google.com/o/oauth2/auth"; got != want {
|
|
||||||
t.Errorf("AuthURL = %q; want %q", got, want)
|
|
||||||
}
|
|
||||||
if got, want := conf.Endpoint.TokenURL, "https://google.com/o/oauth2/token"; got != want {
|
|
||||||
t.Errorf("TokenURL = %q; want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigFromJSON_Installed(t *testing.T) {
|
|
||||||
conf, err := ConfigFromJSON(installedJSONKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
if got, want := conf.ClientID, "222-installed.apps.googleusercontent.com"; got != want {
|
|
||||||
t.Errorf("ClientID = %q; want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package google
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rsa"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/internal"
|
|
||||||
"golang.org/x/oauth2/jws"
|
|
||||||
)
|
|
||||||
|
|
||||||
// JWTAccessTokenSourceFromJSON uses a Google Developers service account JSON
|
|
||||||
// key file to read the credentials that authorize and authenticate the
|
|
||||||
// requests, and returns a TokenSource that does not use any OAuth2 flow but
|
|
||||||
// instead creates a JWT and sends that as the access token.
|
|
||||||
// The audience is typically a URL that specifies the scope of the credentials.
|
|
||||||
//
|
|
||||||
// Note that this is not a standard OAuth flow, but rather an
|
|
||||||
// optimization supported by a few Google services.
|
|
||||||
// Unless you know otherwise, you should use JWTConfigFromJSON instead.
|
|
||||||
func JWTAccessTokenSourceFromJSON(jsonKey []byte, audience string) (oauth2.TokenSource, error) {
|
|
||||||
cfg, err := JWTConfigFromJSON(jsonKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("google: could not parse JSON key: %v", err)
|
|
||||||
}
|
|
||||||
pk, err := internal.ParseKey(cfg.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("google: could not parse key: %v", err)
|
|
||||||
}
|
|
||||||
ts := &jwtAccessTokenSource{
|
|
||||||
email: cfg.Email,
|
|
||||||
audience: audience,
|
|
||||||
pk: pk,
|
|
||||||
}
|
|
||||||
tok, err := ts.Token()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return oauth2.ReuseTokenSource(tok, ts), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type jwtAccessTokenSource struct {
|
|
||||||
email, audience string
|
|
||||||
pk *rsa.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *jwtAccessTokenSource) Token() (*oauth2.Token, error) {
|
|
||||||
iat := time.Now()
|
|
||||||
exp := iat.Add(time.Hour)
|
|
||||||
cs := &jws.ClaimSet{
|
|
||||||
Iss: ts.email,
|
|
||||||
Sub: ts.email,
|
|
||||||
Aud: ts.audience,
|
|
||||||
Iat: iat.Unix(),
|
|
||||||
Exp: exp.Unix(),
|
|
||||||
}
|
|
||||||
hdr := &jws.Header{
|
|
||||||
Algorithm: "RS256",
|
|
||||||
Typ: "JWT",
|
|
||||||
}
|
|
||||||
msg, err := jws.Encode(hdr, cs, ts.pk)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("google: could not encode JWT: %v", err)
|
|
||||||
}
|
|
||||||
return &oauth2.Token{AccessToken: msg, TokenType: "Bearer", Expiry: exp}, nil
|
|
||||||
}
|
|
|
@ -1,168 +0,0 @@
|
||||||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package google
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
type sdkCredentials struct {
|
|
||||||
Data []struct {
|
|
||||||
Credential struct {
|
|
||||||
ClientID string `json:"client_id"`
|
|
||||||
ClientSecret string `json:"client_secret"`
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
TokenExpiry *time.Time `json:"token_expiry"`
|
|
||||||
} `json:"credential"`
|
|
||||||
Key struct {
|
|
||||||
Account string `json:"account"`
|
|
||||||
Scope string `json:"scope"`
|
|
||||||
} `json:"key"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// An SDKConfig provides access to tokens from an account already
|
|
||||||
// authorized via the Google Cloud SDK.
|
|
||||||
type SDKConfig struct {
|
|
||||||
conf oauth2.Config
|
|
||||||
initialToken *oauth2.Token
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSDKConfig creates an SDKConfig for the given Google Cloud SDK
|
|
||||||
// account. If account is empty, the account currently active in
|
|
||||||
// Google Cloud SDK properties is used.
|
|
||||||
// Google Cloud SDK credentials must be created by running `gcloud auth`
|
|
||||||
// before using this function.
|
|
||||||
// The Google Cloud SDK is available at https://cloud.google.com/sdk/.
|
|
||||||
func NewSDKConfig(account string) (*SDKConfig, error) {
|
|
||||||
configPath, err := sdkConfigPath()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err)
|
|
||||||
}
|
|
||||||
credentialsPath := filepath.Join(configPath, "credentials")
|
|
||||||
f, err := os.Open(credentialsPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
var c sdkCredentials
|
|
||||||
if err := json.NewDecoder(f).Decode(&c); err != nil {
|
|
||||||
return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err)
|
|
||||||
}
|
|
||||||
if len(c.Data) == 0 {
|
|
||||||
return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath)
|
|
||||||
}
|
|
||||||
if account == "" {
|
|
||||||
propertiesPath := filepath.Join(configPath, "properties")
|
|
||||||
f, err := os.Open(propertiesPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
ini, err := internal.ParseINI(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err)
|
|
||||||
}
|
|
||||||
core, ok := ini["core"]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini)
|
|
||||||
}
|
|
||||||
active, ok := core["account"]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core)
|
|
||||||
}
|
|
||||||
account = active
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, d := range c.Data {
|
|
||||||
if account == "" || d.Key.Account == account {
|
|
||||||
if d.Credential.AccessToken == "" && d.Credential.RefreshToken == "" {
|
|
||||||
return nil, fmt.Errorf("oauth2/google: no token available for account %q", account)
|
|
||||||
}
|
|
||||||
var expiry time.Time
|
|
||||||
if d.Credential.TokenExpiry != nil {
|
|
||||||
expiry = *d.Credential.TokenExpiry
|
|
||||||
}
|
|
||||||
return &SDKConfig{
|
|
||||||
conf: oauth2.Config{
|
|
||||||
ClientID: d.Credential.ClientID,
|
|
||||||
ClientSecret: d.Credential.ClientSecret,
|
|
||||||
Scopes: strings.Split(d.Key.Scope, " "),
|
|
||||||
Endpoint: Endpoint,
|
|
||||||
RedirectURL: "oob",
|
|
||||||
},
|
|
||||||
initialToken: &oauth2.Token{
|
|
||||||
AccessToken: d.Credential.AccessToken,
|
|
||||||
RefreshToken: d.Credential.RefreshToken,
|
|
||||||
Expiry: expiry,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client returns an HTTP client using Google Cloud SDK credentials to
|
|
||||||
// authorize requests. The token will auto-refresh as necessary. The
|
|
||||||
// underlying http.RoundTripper will be obtained using the provided
|
|
||||||
// context. The returned client and its Transport should not be
|
|
||||||
// modified.
|
|
||||||
func (c *SDKConfig) Client(ctx context.Context) *http.Client {
|
|
||||||
return &http.Client{
|
|
||||||
Transport: &oauth2.Transport{
|
|
||||||
Source: c.TokenSource(ctx),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenSource returns an oauth2.TokenSource that retrieve tokens from
|
|
||||||
// Google Cloud SDK credentials using the provided context.
|
|
||||||
// It will returns the current access token stored in the credentials,
|
|
||||||
// and refresh it when it expires, but it won't update the credentials
|
|
||||||
// with the new access token.
|
|
||||||
func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource {
|
|
||||||
return c.conf.TokenSource(ctx, c.initialToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scopes are the OAuth 2.0 scopes the current account is authorized for.
|
|
||||||
func (c *SDKConfig) Scopes() []string {
|
|
||||||
return c.conf.Scopes
|
|
||||||
}
|
|
||||||
|
|
||||||
// sdkConfigPath tries to guess where the gcloud config is located.
|
|
||||||
// It can be overridden during tests.
|
|
||||||
var sdkConfigPath = func() (string, error) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil
|
|
||||||
}
|
|
||||||
homeDir := guessUnixHomeDir()
|
|
||||||
if homeDir == "" {
|
|
||||||
return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty")
|
|
||||||
}
|
|
||||||
return filepath.Join(homeDir, ".config", "gcloud"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func guessUnixHomeDir() string {
|
|
||||||
usr, err := user.Current()
|
|
||||||
if err == nil {
|
|
||||||
return usr.HomeDir
|
|
||||||
}
|
|
||||||
return os.Getenv("HOME")
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package google
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestSDKConfig(t *testing.T) {
|
|
||||||
sdkConfigPath = func() (string, error) {
|
|
||||||
return "testdata/gcloud", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
account string
|
|
||||||
accessToken string
|
|
||||||
err bool
|
|
||||||
}{
|
|
||||||
{"", "bar_access_token", false},
|
|
||||||
{"foo@example.com", "foo_access_token", false},
|
|
||||||
{"bar@example.com", "bar_access_token", false},
|
|
||||||
{"baz@serviceaccount.example.com", "", true},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
c, err := NewSDKConfig(tt.account)
|
|
||||||
if got, want := err != nil, tt.err; got != want {
|
|
||||||
if !tt.err {
|
|
||||||
t.Errorf("expected no error, got error: %v", tt.err, err)
|
|
||||||
} else {
|
|
||||||
t.Errorf("expected error, got none")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tok := c.initialToken
|
|
||||||
if tok == nil {
|
|
||||||
t.Errorf("expected token %q, got: nil", tt.accessToken)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if tok.AccessToken != tt.accessToken {
|
|
||||||
t.Errorf("expected token %q, got: %q", tt.accessToken, tok.AccessToken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package internal contains support packages for oauth2 package.
|
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseKey converts the binary contents of a private key file
|
|
||||||
// to an *rsa.PrivateKey. It detects whether the private key is in a
|
|
||||||
// PEM container or not. If so, it extracts the the private key
|
|
||||||
// from PEM container before conversion. It only supports PEM
|
|
||||||
// containers with no passphrase.
|
|
||||||
func ParseKey(key []byte) (*rsa.PrivateKey, error) {
|
|
||||||
block, _ := pem.Decode(key)
|
|
||||||
if block != nil {
|
|
||||||
key = block.Bytes
|
|
||||||
}
|
|
||||||
parsedKey, err := x509.ParsePKCS8PrivateKey(key)
|
|
||||||
if err != nil {
|
|
||||||
parsedKey, err = x509.ParsePKCS1PrivateKey(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("private key should be a PEM or plain PKSC1 or PKCS8; parse error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parsed, ok := parsedKey.(*rsa.PrivateKey)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("private key is invalid")
|
|
||||||
}
|
|
||||||
return parsed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseINI(ini io.Reader) (map[string]map[string]string, error) {
|
|
||||||
result := map[string]map[string]string{
|
|
||||||
"": map[string]string{}, // root section
|
|
||||||
}
|
|
||||||
scanner := bufio.NewScanner(ini)
|
|
||||||
currentSection := ""
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
if strings.HasPrefix(line, ";") {
|
|
||||||
// comment.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
|
||||||
currentSection = strings.TrimSpace(line[1 : len(line)-1])
|
|
||||||
result[currentSection] = map[string]string{}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(line, "=", 2)
|
|
||||||
if len(parts) == 2 && parts[0] != "" {
|
|
||||||
result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("error scanning ini: %v", err)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CondVal(v string) []string {
|
|
||||||
if v == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []string{v}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package internal contains support packages for oauth2 package.
|
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseINI(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
ini string
|
|
||||||
want map[string]map[string]string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
`root = toor
|
|
||||||
[foo]
|
|
||||||
bar = hop
|
|
||||||
ini = nin
|
|
||||||
`,
|
|
||||||
map[string]map[string]string{
|
|
||||||
"": map[string]string{"root": "toor"},
|
|
||||||
"foo": map[string]string{"bar": "hop", "ini": "nin"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`[empty]
|
|
||||||
[section]
|
|
||||||
empty=
|
|
||||||
`,
|
|
||||||
map[string]map[string]string{
|
|
||||||
"": map[string]string{},
|
|
||||||
"empty": map[string]string{},
|
|
||||||
"section": map[string]string{"empty": ""},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`ignore
|
|
||||||
[invalid
|
|
||||||
=stuff
|
|
||||||
;comment=true
|
|
||||||
`,
|
|
||||||
map[string]map[string]string{
|
|
||||||
"": map[string]string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
result, err := ParseINI(strings.NewReader(tt.ini))
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("ParseINI(%q) error %v, want: no error", tt.ini, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(result, tt.want) {
|
|
||||||
t.Errorf("ParseINI(%q) = %#v, want: %#v", tt.ini, result, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,214 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package internal contains support packages for oauth2 package.
|
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Token represents the crendentials used to authorize
|
|
||||||
// the requests to access protected resources on the OAuth 2.0
|
|
||||||
// provider's backend.
|
|
||||||
//
|
|
||||||
// This type is a mirror of oauth2.Token and exists to break
|
|
||||||
// an otherwise-circular dependency. Other internal packages
|
|
||||||
// should convert this Token into an oauth2.Token before use.
|
|
||||||
type Token struct {
|
|
||||||
// AccessToken is the token that authorizes and authenticates
|
|
||||||
// the requests.
|
|
||||||
AccessToken string
|
|
||||||
|
|
||||||
// TokenType is the type of token.
|
|
||||||
// The Type method returns either this or "Bearer", the default.
|
|
||||||
TokenType string
|
|
||||||
|
|
||||||
// RefreshToken is a token that's used by the application
|
|
||||||
// (as opposed to the user) to refresh the access token
|
|
||||||
// if it expires.
|
|
||||||
RefreshToken string
|
|
||||||
|
|
||||||
// Expiry is the optional expiration time of the access token.
|
|
||||||
//
|
|
||||||
// If zero, TokenSource implementations will reuse the same
|
|
||||||
// token forever and RefreshToken or equivalent
|
|
||||||
// mechanisms for that TokenSource will not be used.
|
|
||||||
Expiry time.Time
|
|
||||||
|
|
||||||
// Raw optionally contains extra metadata from the server
|
|
||||||
// when updating a token.
|
|
||||||
Raw interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tokenJSON is the struct representing the HTTP response from OAuth2
|
|
||||||
// providers returning a token in JSON form.
|
|
||||||
type tokenJSON struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
|
|
||||||
Expires expirationTime `json:"expires"` // broken Facebook spelling of expires_in
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *tokenJSON) expiry() (t time.Time) {
|
|
||||||
if v := e.ExpiresIn; v != 0 {
|
|
||||||
return time.Now().Add(time.Duration(v) * time.Second)
|
|
||||||
}
|
|
||||||
if v := e.Expires; v != 0 {
|
|
||||||
return time.Now().Add(time.Duration(v) * time.Second)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type expirationTime int32
|
|
||||||
|
|
||||||
func (e *expirationTime) UnmarshalJSON(b []byte) error {
|
|
||||||
var n json.Number
|
|
||||||
err := json.Unmarshal(b, &n)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
i, err := n.Int64()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*e = expirationTime(i)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var brokenAuthHeaderProviders = []string{
|
|
||||||
"https://accounts.google.com/",
|
|
||||||
"https://www.googleapis.com/",
|
|
||||||
"https://api.instagram.com/",
|
|
||||||
"https://www.douban.com/",
|
|
||||||
"https://api.dropbox.com/",
|
|
||||||
"https://api.soundcloud.com/",
|
|
||||||
"https://www.linkedin.com/",
|
|
||||||
"https://api.twitch.tv/",
|
|
||||||
"https://oauth.vk.com/",
|
|
||||||
"https://api.odnoklassniki.ru/",
|
|
||||||
"https://connect.stripe.com/",
|
|
||||||
"https://api.pushbullet.com/",
|
|
||||||
"https://oauth.sandbox.trainingpeaks.com/",
|
|
||||||
"https://oauth.trainingpeaks.com/",
|
|
||||||
"https://www.strava.com/oauth/",
|
|
||||||
"https://app.box.com/",
|
|
||||||
"https://test-sandbox.auth.corp.google.com",
|
|
||||||
"https://user.gini.net/",
|
|
||||||
"https://api.netatmo.net/",
|
|
||||||
"https://slack.com/",
|
|
||||||
}
|
|
||||||
|
|
||||||
// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
|
|
||||||
// implements the OAuth2 spec correctly
|
|
||||||
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
|
|
||||||
// In summary:
|
|
||||||
// - Reddit only accepts client secret in the Authorization header
|
|
||||||
// - Dropbox accepts either it in URL param or Auth header, but not both.
|
|
||||||
// - Google only accepts URL param (not spec compliant?), not Auth header
|
|
||||||
// - Stripe only accepts client secret in Auth header with Bearer method, not Basic
|
|
||||||
func providerAuthHeaderWorks(tokenURL string) bool {
|
|
||||||
for _, s := range brokenAuthHeaderProviders {
|
|
||||||
if strings.HasPrefix(tokenURL, s) {
|
|
||||||
// Some sites fail to implement the OAuth2 spec fully.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assume the provider implements the spec properly
|
|
||||||
// otherwise. We can add more exceptions as they're
|
|
||||||
// discovered. We will _not_ be adding configurable hooks
|
|
||||||
// to this package to let users select server bugs.
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func RetrieveToken(ctx context.Context, ClientID, ClientSecret, TokenURL string, v url.Values) (*Token, error) {
|
|
||||||
hc, err := ContextClient(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v.Set("client_id", ClientID)
|
|
||||||
bustedAuth := !providerAuthHeaderWorks(TokenURL)
|
|
||||||
if bustedAuth && ClientSecret != "" {
|
|
||||||
v.Set("client_secret", ClientSecret)
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest("POST", TokenURL, strings.NewReader(v.Encode()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
if !bustedAuth {
|
|
||||||
req.SetBasicAuth(ClientID, ClientSecret)
|
|
||||||
}
|
|
||||||
r, err := hc.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
|
||||||
}
|
|
||||||
if code := r.StatusCode; code < 200 || code > 299 {
|
|
||||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
var token *Token
|
|
||||||
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
|
||||||
switch content {
|
|
||||||
case "application/x-www-form-urlencoded", "text/plain":
|
|
||||||
vals, err := url.ParseQuery(string(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
token = &Token{
|
|
||||||
AccessToken: vals.Get("access_token"),
|
|
||||||
TokenType: vals.Get("token_type"),
|
|
||||||
RefreshToken: vals.Get("refresh_token"),
|
|
||||||
Raw: vals,
|
|
||||||
}
|
|
||||||
e := vals.Get("expires_in")
|
|
||||||
if e == "" {
|
|
||||||
// TODO(jbd): Facebook's OAuth2 implementation is broken and
|
|
||||||
// returns expires_in field in expires. Remove the fallback to expires,
|
|
||||||
// when Facebook fixes their implementation.
|
|
||||||
e = vals.Get("expires")
|
|
||||||
}
|
|
||||||
expires, _ := strconv.Atoi(e)
|
|
||||||
if expires != 0 {
|
|
||||||
token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
var tj tokenJSON
|
|
||||||
if err = json.Unmarshal(body, &tj); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
token = &Token{
|
|
||||||
AccessToken: tj.AccessToken,
|
|
||||||
TokenType: tj.TokenType,
|
|
||||||
RefreshToken: tj.RefreshToken,
|
|
||||||
Expiry: tj.expiry(),
|
|
||||||
Raw: make(map[string]interface{}),
|
|
||||||
}
|
|
||||||
json.Unmarshal(body, &token.Raw) // no error checks for optional fields
|
|
||||||
}
|
|
||||||
// Don't overwrite `RefreshToken` with an empty value
|
|
||||||
// if this was a token refreshing request.
|
|
||||||
if token.RefreshToken == "" {
|
|
||||||
token.RefreshToken = v.Get("refresh_token")
|
|
||||||
}
|
|
||||||
return token, nil
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package internal contains support packages for oauth2 package.
|
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_providerAuthHeaderWorks(t *testing.T) {
|
|
||||||
for _, p := range brokenAuthHeaderProviders {
|
|
||||||
if providerAuthHeaderWorks(p) {
|
|
||||||
t.Errorf("URL: %s not found in list", p)
|
|
||||||
}
|
|
||||||
p := fmt.Sprintf("%ssomesuffix", p)
|
|
||||||
if providerAuthHeaderWorks(p) {
|
|
||||||
t.Errorf("URL: %s not found in list", p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p := "https://api.not-in-the-list-example.com/"
|
|
||||||
if !providerAuthHeaderWorks(p) {
|
|
||||||
t.Errorf("URL: %s found in list", p)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package internal contains support packages for oauth2 package.
|
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTTPClient is the context key to use with golang.org/x/net/context's
|
|
||||||
// WithValue function to associate an *http.Client value with a context.
|
|
||||||
var HTTPClient ContextKey
|
|
||||||
|
|
||||||
// ContextKey is just an empty struct. It exists so HTTPClient can be
|
|
||||||
// an immutable public variable with a unique type. It's immutable
|
|
||||||
// because nobody else can create a ContextKey, being unexported.
|
|
||||||
type ContextKey struct{}
|
|
||||||
|
|
||||||
// ContextClientFunc is a func which tries to return an *http.Client
|
|
||||||
// given a Context value. If it returns an error, the search stops
|
|
||||||
// with that error. If it returns (nil, nil), the search continues
|
|
||||||
// down the list of registered funcs.
|
|
||||||
type ContextClientFunc func(context.Context) (*http.Client, error)
|
|
||||||
|
|
||||||
var contextClientFuncs []ContextClientFunc
|
|
||||||
|
|
||||||
func RegisterContextClientFunc(fn ContextClientFunc) {
|
|
||||||
contextClientFuncs = append(contextClientFuncs, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ContextClient(ctx context.Context) (*http.Client, error) {
|
|
||||||
for _, fn := range contextClientFuncs {
|
|
||||||
c, err := fn(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if c != nil {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hc, ok := ctx.Value(HTTPClient).(*http.Client); ok {
|
|
||||||
return hc, nil
|
|
||||||
}
|
|
||||||
return http.DefaultClient, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ContextTransport(ctx context.Context) http.RoundTripper {
|
|
||||||
hc, err := ContextClient(ctx)
|
|
||||||
// This is a rare error case (somebody using nil on App Engine).
|
|
||||||
if err != nil {
|
|
||||||
return ErrorTransport{err}
|
|
||||||
}
|
|
||||||
return hc.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorTransport returns the specified error on RoundTrip.
|
|
||||||
// This RoundTripper should be used in rare error cases where
|
|
||||||
// error handling can be postponed to response handling time.
|
|
||||||
type ErrorTransport struct{ Err error }
|
|
||||||
|
|
||||||
func (t ErrorTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
|
||||||
return nil, t.Err
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package jws provides encoding and decoding utilities for
|
|
||||||
// signed JWS messages.
|
|
||||||
package jws // import "golang.org/x/oauth2/jws"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ClaimSet contains information about the JWT signature including the
|
|
||||||
// permissions being requested (scopes), the target of the token, the issuer,
|
|
||||||
// the time the token was issued, and the lifetime of the token.
|
|
||||||
type ClaimSet struct {
|
|
||||||
Iss string `json:"iss"` // email address of the client_id of the application making the access token request
|
|
||||||
Scope string `json:"scope,omitempty"` // space-delimited list of the permissions the application requests
|
|
||||||
Aud string `json:"aud"` // descriptor of the intended target of the assertion (Optional).
|
|
||||||
Exp int64 `json:"exp"` // the expiration time of the assertion (seconds since Unix epoch)
|
|
||||||
Iat int64 `json:"iat"` // the time the assertion was issued (seconds since Unix epoch)
|
|
||||||
Typ string `json:"typ,omitempty"` // token type (Optional).
|
|
||||||
|
|
||||||
// Email for which the application is requesting delegated access (Optional).
|
|
||||||
Sub string `json:"sub,omitempty"`
|
|
||||||
|
|
||||||
// The old name of Sub. Client keeps setting Prn to be
|
|
||||||
// complaint with legacy OAuth 2.0 providers. (Optional)
|
|
||||||
Prn string `json:"prn,omitempty"`
|
|
||||||
|
|
||||||
// See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3
|
|
||||||
// This array is marshalled using custom code (see (c *ClaimSet) encode()).
|
|
||||||
PrivateClaims map[string]interface{} `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClaimSet) encode() (string, error) {
|
|
||||||
// Reverting time back for machines whose time is not perfectly in sync.
|
|
||||||
// If client machine's time is in the future according
|
|
||||||
// to Google servers, an access token will not be issued.
|
|
||||||
now := time.Now().Add(-10 * time.Second)
|
|
||||||
if c.Iat == 0 {
|
|
||||||
c.Iat = now.Unix()
|
|
||||||
}
|
|
||||||
if c.Exp == 0 {
|
|
||||||
c.Exp = now.Add(time.Hour).Unix()
|
|
||||||
}
|
|
||||||
if c.Exp < c.Iat {
|
|
||||||
return "", fmt.Errorf("jws: invalid Exp = %v; must be later than Iat = %v", c.Exp, c.Iat)
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := json.Marshal(c)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.PrivateClaims) == 0 {
|
|
||||||
return base64Encode(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal private claim set and then append it to b.
|
|
||||||
prv, err := json.Marshal(c.PrivateClaims)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("jws: invalid map of private claims %v", c.PrivateClaims)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Concatenate public and private claim JSON objects.
|
|
||||||
if !bytes.HasSuffix(b, []byte{'}'}) {
|
|
||||||
return "", fmt.Errorf("jws: invalid JSON %s", b)
|
|
||||||
}
|
|
||||||
if !bytes.HasPrefix(prv, []byte{'{'}) {
|
|
||||||
return "", fmt.Errorf("jws: invalid JSON %s", prv)
|
|
||||||
}
|
|
||||||
b[len(b)-1] = ',' // Replace closing curly brace with a comma.
|
|
||||||
b = append(b, prv[1:]...) // Append private claims.
|
|
||||||
return base64Encode(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header represents the header for the signed JWS payloads.
|
|
||||||
type Header struct {
|
|
||||||
// The algorithm used for signature.
|
|
||||||
Algorithm string `json:"alg"`
|
|
||||||
|
|
||||||
// Represents the token type.
|
|
||||||
Typ string `json:"typ"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Header) encode() (string, error) {
|
|
||||||
b, err := json.Marshal(h)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64Encode(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode decodes a claim set from a JWS payload.
|
|
||||||
func Decode(payload string) (*ClaimSet, error) {
|
|
||||||
// decode returned id token to get expiry
|
|
||||||
s := strings.Split(payload, ".")
|
|
||||||
if len(s) < 2 {
|
|
||||||
// TODO(jbd): Provide more context about the error.
|
|
||||||
return nil, errors.New("jws: invalid token received")
|
|
||||||
}
|
|
||||||
decoded, err := base64Decode(s[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c := &ClaimSet{}
|
|
||||||
err = json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c)
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode encodes a signed JWS with provided header and claim set.
|
|
||||||
func Encode(header *Header, c *ClaimSet, signature *rsa.PrivateKey) (string, error) {
|
|
||||||
head, err := header.encode()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
cs, err := c.encode()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
ss := fmt.Sprintf("%s.%s", head, cs)
|
|
||||||
h := sha256.New()
|
|
||||||
h.Write([]byte(ss))
|
|
||||||
b, err := rsa.SignPKCS1v15(rand.Reader, signature, crypto.SHA256, h.Sum(nil))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
sig := base64Encode(b)
|
|
||||||
return fmt.Sprintf("%s.%s", ss, sig), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// base64Encode returns and Base64url encoded version of the input string with any
|
|
||||||
// trailing "=" stripped.
|
|
||||||
func base64Encode(b []byte) string {
|
|
||||||
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
|
|
||||||
}
|
|
||||||
|
|
||||||
// base64Decode decodes the Base64url encoded string
|
|
||||||
func base64Decode(s string) ([]byte, error) {
|
|
||||||
// add back missing padding
|
|
||||||
switch len(s) % 4 {
|
|
||||||
case 2:
|
|
||||||
s += "=="
|
|
||||||
case 3:
|
|
||||||
s += "="
|
|
||||||
}
|
|
||||||
return base64.URLEncoding.DecodeString(s)
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package jwt_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExampleJWTConfig() {
|
|
||||||
conf := &jwt.Config{
|
|
||||||
Email: "xxx@developer.com",
|
|
||||||
// The contents of your RSA private key or your PEM file
|
|
||||||
// that contains a private key.
|
|
||||||
// If you have a p12 file instead, you
|
|
||||||
// can use `openssl` to export the private key into a pem file.
|
|
||||||
//
|
|
||||||
// $ openssl pkcs12 -in key.p12 -out key.pem -nodes
|
|
||||||
//
|
|
||||||
// It only supports PEM containers with no passphrase.
|
|
||||||
PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----..."),
|
|
||||||
Subject: "user@example.com",
|
|
||||||
TokenURL: "https://provider.com/o/oauth2/token",
|
|
||||||
}
|
|
||||||
// Initiate an http.Client, the following GET request will be
|
|
||||||
// authorized and authenticated on the behalf of user@example.com.
|
|
||||||
client := conf.Client(oauth2.NoContext)
|
|
||||||
client.Get("...")
|
|
||||||
}
|
|
|
@ -1,153 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package jwt implements the OAuth 2.0 JSON Web Token flow, commonly
|
|
||||||
// known as "two-legged OAuth 2.0".
|
|
||||||
//
|
|
||||||
// See: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12
|
|
||||||
package jwt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/internal"
|
|
||||||
"golang.org/x/oauth2/jws"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
|
||||||
defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config is the configuration for using JWT to fetch tokens,
|
|
||||||
// commonly known as "two-legged OAuth 2.0".
|
|
||||||
type Config struct {
|
|
||||||
// Email is the OAuth client identifier used when communicating with
|
|
||||||
// the configured OAuth provider.
|
|
||||||
Email string
|
|
||||||
|
|
||||||
// PrivateKey contains the contents of an RSA private key or the
|
|
||||||
// contents of a PEM file that contains a private key. The provided
|
|
||||||
// private key is used to sign JWT payloads.
|
|
||||||
// PEM containers with a passphrase are not supported.
|
|
||||||
// Use the following command to convert a PKCS 12 file into a PEM.
|
|
||||||
//
|
|
||||||
// $ openssl pkcs12 -in key.p12 -out key.pem -nodes
|
|
||||||
//
|
|
||||||
PrivateKey []byte
|
|
||||||
|
|
||||||
// Subject is the optional user to impersonate.
|
|
||||||
Subject string
|
|
||||||
|
|
||||||
// Scopes optionally specifies a list of requested permission scopes.
|
|
||||||
Scopes []string
|
|
||||||
|
|
||||||
// TokenURL is the endpoint required to complete the 2-legged JWT flow.
|
|
||||||
TokenURL string
|
|
||||||
|
|
||||||
// Expires optionally specifies how long the token is valid for.
|
|
||||||
Expires time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenSource returns a JWT TokenSource using the configuration
|
|
||||||
// in c and the HTTP client from the provided context.
|
|
||||||
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
|
|
||||||
return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client returns an HTTP client wrapping the context's
|
|
||||||
// HTTP transport and adding Authorization headers with tokens
|
|
||||||
// obtained from c.
|
|
||||||
//
|
|
||||||
// The returned client and its Transport should not be modified.
|
|
||||||
func (c *Config) Client(ctx context.Context) *http.Client {
|
|
||||||
return oauth2.NewClient(ctx, c.TokenSource(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
// jwtSource is a source that always does a signed JWT request for a token.
|
|
||||||
// It should typically be wrapped with a reuseTokenSource.
|
|
||||||
type jwtSource struct {
|
|
||||||
ctx context.Context
|
|
||||||
conf *Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (js jwtSource) Token() (*oauth2.Token, error) {
|
|
||||||
pk, err := internal.ParseKey(js.conf.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hc := oauth2.NewClient(js.ctx, nil)
|
|
||||||
claimSet := &jws.ClaimSet{
|
|
||||||
Iss: js.conf.Email,
|
|
||||||
Scope: strings.Join(js.conf.Scopes, " "),
|
|
||||||
Aud: js.conf.TokenURL,
|
|
||||||
}
|
|
||||||
if subject := js.conf.Subject; subject != "" {
|
|
||||||
claimSet.Sub = subject
|
|
||||||
// prn is the old name of sub. Keep setting it
|
|
||||||
// to be compatible with legacy OAuth 2.0 providers.
|
|
||||||
claimSet.Prn = subject
|
|
||||||
}
|
|
||||||
if t := js.conf.Expires; t > 0 {
|
|
||||||
claimSet.Exp = time.Now().Add(t).Unix()
|
|
||||||
}
|
|
||||||
payload, err := jws.Encode(defaultHeader, claimSet, pk)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("grant_type", defaultGrantType)
|
|
||||||
v.Set("assertion", payload)
|
|
||||||
resp, err := hc.PostForm(js.conf.TokenURL, v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
|
||||||
}
|
|
||||||
if c := resp.StatusCode; c < 200 || c > 299 {
|
|
||||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", resp.Status, body)
|
|
||||||
}
|
|
||||||
// tokenRes is the JSON response body.
|
|
||||||
var tokenRes struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
IDToken string `json:"id_token"`
|
|
||||||
ExpiresIn int64 `json:"expires_in"` // relative seconds from now
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(body, &tokenRes); err != nil {
|
|
||||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
|
||||||
}
|
|
||||||
token := &oauth2.Token{
|
|
||||||
AccessToken: tokenRes.AccessToken,
|
|
||||||
TokenType: tokenRes.TokenType,
|
|
||||||
}
|
|
||||||
raw := make(map[string]interface{})
|
|
||||||
json.Unmarshal(body, &raw) // no error checks for optional fields
|
|
||||||
token = token.WithExtra(raw)
|
|
||||||
|
|
||||||
if secs := tokenRes.ExpiresIn; secs > 0 {
|
|
||||||
token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
|
|
||||||
}
|
|
||||||
if v := tokenRes.IDToken; v != "" {
|
|
||||||
// decode returned id token to get expiry
|
|
||||||
claimSet, err := jws.Decode(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("oauth2: error decoding JWT token: %v", err)
|
|
||||||
}
|
|
||||||
token.Expiry = time.Unix(claimSet.Exp, 0)
|
|
||||||
}
|
|
||||||
return token, nil
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package jwt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var dummyPrivateKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEpAIBAAKCAQEAx4fm7dngEmOULNmAs1IGZ9Apfzh+BkaQ1dzkmbUgpcoghucE
|
|
||||||
DZRnAGd2aPyB6skGMXUytWQvNYav0WTR00wFtX1ohWTfv68HGXJ8QXCpyoSKSSFY
|
|
||||||
fuP9X36wBSkSX9J5DVgiuzD5VBdzUISSmapjKm+DcbRALjz6OUIPEWi1Tjl6p5RK
|
|
||||||
1w41qdbmt7E5/kGhKLDuT7+M83g4VWhgIvaAXtnhklDAggilPPa8ZJ1IFe31lNlr
|
|
||||||
k4DRk38nc6sEutdf3RL7QoH7FBusI7uXV03DC6dwN1kP4GE7bjJhcRb/7jYt7CQ9
|
|
||||||
/E9Exz3c0yAp0yrTg0Fwh+qxfH9dKwN52S7SBwIDAQABAoIBAQCaCs26K07WY5Jt
|
|
||||||
3a2Cw3y2gPrIgTCqX6hJs7O5ByEhXZ8nBwsWANBUe4vrGaajQHdLj5OKfsIDrOvn
|
|
||||||
2NI1MqflqeAbu/kR32q3tq8/Rl+PPiwUsW3E6Pcf1orGMSNCXxeducF2iySySzh3
|
|
||||||
nSIhCG5uwJDWI7a4+9KiieFgK1pt/Iv30q1SQS8IEntTfXYwANQrfKUVMmVF9aIK
|
|
||||||
6/WZE2yd5+q3wVVIJ6jsmTzoDCX6QQkkJICIYwCkglmVy5AeTckOVwcXL0jqw5Kf
|
|
||||||
5/soZJQwLEyBoQq7Kbpa26QHq+CJONetPP8Ssy8MJJXBT+u/bSseMb3Zsr5cr43e
|
|
||||||
DJOhwsThAoGBAPY6rPKl2NT/K7XfRCGm1sbWjUQyDShscwuWJ5+kD0yudnT/ZEJ1
|
|
||||||
M3+KS/iOOAoHDdEDi9crRvMl0UfNa8MAcDKHflzxg2jg/QI+fTBjPP5GOX0lkZ9g
|
|
||||||
z6VePoVoQw2gpPFVNPPTxKfk27tEzbaffvOLGBEih0Kb7HTINkW8rIlzAoGBAM9y
|
|
||||||
1yr+jvfS1cGFtNU+Gotoihw2eMKtIqR03Yn3n0PK1nVCDKqwdUqCypz4+ml6cxRK
|
|
||||||
J8+Pfdh7D+ZJd4LEG6Y4QRDLuv5OA700tUoSHxMSNn3q9As4+T3MUyYxWKvTeu3U
|
|
||||||
f2NWP9ePU0lV8ttk7YlpVRaPQmc1qwooBA/z/8AdAoGAW9x0HWqmRICWTBnpjyxx
|
|
||||||
QGlW9rQ9mHEtUotIaRSJ6K/F3cxSGUEkX1a3FRnp6kPLcckC6NlqdNgNBd6rb2rA
|
|
||||||
cPl/uSkZP42Als+9YMoFPU/xrrDPbUhu72EDrj3Bllnyb168jKLa4VBOccUvggxr
|
|
||||||
Dm08I1hgYgdN5huzs7y6GeUCgYEAj+AZJSOJ6o1aXS6rfV3mMRve9bQ9yt8jcKXw
|
|
||||||
5HhOCEmMtaSKfnOF1Ziih34Sxsb7O2428DiX0mV/YHtBnPsAJidL0SdLWIapBzeg
|
|
||||||
KHArByIRkwE6IvJvwpGMdaex1PIGhx5i/3VZL9qiq/ElT05PhIb+UXgoWMabCp84
|
|
||||||
OgxDK20CgYAeaFo8BdQ7FmVX2+EEejF+8xSge6WVLtkaon8bqcn6P0O8lLypoOhd
|
|
||||||
mJAYH8WU+UAy9pecUnDZj14LAGNVmYcse8HFX71MoshnvCTFEPVo4rZxIAGwMpeJ
|
|
||||||
5jgQ3slYLpqrGlcbLgUXBUgzEO684Wk/UV9DFPlHALVqCfXQ9dpJPg==
|
|
||||||
-----END RSA PRIVATE KEY-----`)
|
|
||||||
|
|
||||||
func TestJWTFetch_JSONResponse(t *testing.T) {
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write([]byte(`{
|
|
||||||
"access_token": "90d64460d14870c08c81352a05dedd3465940a7c",
|
|
||||||
"scope": "user",
|
|
||||||
"token_type": "bearer",
|
|
||||||
"expires_in": 3600
|
|
||||||
}`))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
conf := &Config{
|
|
||||||
Email: "aaa@xxx.com",
|
|
||||||
PrivateKey: dummyPrivateKey,
|
|
||||||
TokenURL: ts.URL,
|
|
||||||
}
|
|
||||||
tok, err := conf.TokenSource(oauth2.NoContext).Token()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !tok.Valid() {
|
|
||||||
t.Errorf("Token invalid")
|
|
||||||
}
|
|
||||||
if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" {
|
|
||||||
t.Errorf("Unexpected access token, %#v", tok.AccessToken)
|
|
||||||
}
|
|
||||||
if tok.TokenType != "bearer" {
|
|
||||||
t.Errorf("Unexpected token type, %#v", tok.TokenType)
|
|
||||||
}
|
|
||||||
if tok.Expiry.IsZero() {
|
|
||||||
t.Errorf("Unexpected token expiry, %#v", tok.Expiry)
|
|
||||||
}
|
|
||||||
scope := tok.Extra("scope")
|
|
||||||
if scope != "user" {
|
|
||||||
t.Errorf("Unexpected value for scope: %v", scope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJWTFetch_BadResponse(t *testing.T) {
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write([]byte(`{"scope": "user", "token_type": "bearer"}`))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
conf := &Config{
|
|
||||||
Email: "aaa@xxx.com",
|
|
||||||
PrivateKey: dummyPrivateKey,
|
|
||||||
TokenURL: ts.URL,
|
|
||||||
}
|
|
||||||
tok, err := conf.TokenSource(oauth2.NoContext).Token()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if tok == nil {
|
|
||||||
t.Fatalf("token is nil")
|
|
||||||
}
|
|
||||||
if tok.Valid() {
|
|
||||||
t.Errorf("token is valid. want invalid.")
|
|
||||||
}
|
|
||||||
if tok.AccessToken != "" {
|
|
||||||
t.Errorf("Unexpected non-empty access token %q.", tok.AccessToken)
|
|
||||||
}
|
|
||||||
if want := "bearer"; tok.TokenType != want {
|
|
||||||
t.Errorf("TokenType = %q; want %q", tok.TokenType, want)
|
|
||||||
}
|
|
||||||
scope := tok.Extra("scope")
|
|
||||||
if want := "user"; scope != want {
|
|
||||||
t.Errorf("token scope = %q; want %q", scope, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJWTFetch_BadResponseType(t *testing.T) {
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write([]byte(`{"access_token":123, "scope": "user", "token_type": "bearer"}`))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
conf := &Config{
|
|
||||||
Email: "aaa@xxx.com",
|
|
||||||
PrivateKey: dummyPrivateKey,
|
|
||||||
TokenURL: ts.URL,
|
|
||||||
}
|
|
||||||
tok, err := conf.TokenSource(oauth2.NoContext).Token()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("got a token; expected error")
|
|
||||||
if tok.AccessToken != "" {
|
|
||||||
t.Errorf("Unexpected access token, %#v.", tok.AccessToken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package linkedin provides constants for using OAuth2 to access LinkedIn.
|
|
||||||
package linkedin // import "golang.org/x/oauth2/linkedin"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Endpoint is LinkedIn's OAuth 2.0 endpoint.
|
|
||||||
var Endpoint = oauth2.Endpoint{
|
|
||||||
AuthURL: "https://www.linkedin.com/uas/oauth2/authorization",
|
|
||||||
TokenURL: "https://www.linkedin.com/uas/oauth2/accessToken",
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue