Implemented inital version

This commit is contained in:
Thomas Boerger
2018-01-09 09:59:42 +01:00
parent 5407453cb7
commit 2df8580cb0
131 changed files with 28318 additions and 2 deletions
+62
View File
@@ -0,0 +1,62 @@
version: '{build}'
image: 'Visual Studio 2017'
platform: x64
clone_folder: 'c:\go\src\github.com\drone-plugins\drone-manifest'
max_jobs: 1
environment:
DOCKER_USERNAME:
secure: '4YzzahbEiMZQJpOCOd1LAw=='
DOCKER_PASSWORD:
secure: 'VqO/G3Zfslu6zSLdwHKO+Q=='
install:
- ps: |
docker version
go version
build_script:
- ps: |
if ( $env:APPVEYOR_REPO_TAG -eq 'false' ) {
go build -ldflags "-X main.build=$env:APPVEYOR_BUILD_VERSION" -a -o drone-manifest.exe
} else {
$version = $env:APPVEYOR_REPO_TAG_NAME.substring(1)
go build -ldflags "-X main.version=$version -X main.build=$env:APPVEYOR_BUILD_VERSION" -a -o drone-manifest.exe
}
docker pull microsoft/nanoserver:10.0.14393.1593
docker build -f Dockerfile.windows -t plugins/manifest:windows .
test_script:
- ps: |
docker run --rm plugins/manifest:windows --version
deploy_script:
- ps: |
$ErrorActionPreference = 'Stop';
if ( $env:APPVEYOR_PULL_REQUEST_NUMBER ) {
Write-Host Nothing to deploy.
} else {
docker login --username $env:DOCKER_USERNAME --password $env:DOCKER_PASSWORD
if ( $env:APPVEYOR_REPO_TAG -eq 'true' ) {
$major,$minor,$patch = $env:APPVEYOR_REPO_TAG_NAME.substring(1).split('.')
docker push plugins/manifest:windows
docker tag plugins/manifest:windows plugins/manifest:$major.$minor.$patch-windows
docker push plugins/manifest:$major.$minor.$patch-windows
docker tag plugins/manifest:windows plugins/manifest:$major.$minor-windows
docker push plugins/manifest:$major.$minor-windows
docker tag plugins/manifest:windows plugins/manifest:$major-windows
docker push plugins/manifest:$major-windows
} else {
if ( $env:APPVEYOR_REPO_BRANCH -eq 'master' ) {
docker push plugins/manifest:windows
}
}
}
+2
View File
@@ -0,0 +1,2 @@
*
!release/
+144
View File
@@ -0,0 +1,144 @@
workspace:
base: /go
path: src/github.com/drone-plugins/drone-manifest
pipeline:
test:
image: golang:1.9
pull: true
commands:
- go vet
- |
for PKG in $(go list ./... | grep -v /vendor/); do
go test -cover -coverprofile $GOPATH/src/$PKG/coverage.out $PKG
done
build_linux_amd64:
image: golang:1.9
pull: true
group: build
environment:
- GOOS=linux
- GOARCH=amd64
- CGO_ENABLED=0
commands:
- |
if test "${DRONE_TAG}" = ""; then
go build -v -ldflags "-X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/amd64/drone-manifest
else
go build -v -ldflags "-X main.version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/amd64/drone-manifest
fi
build_linux_i386:
image: golang:1.9
pull: true
group: build
environment:
- GOOS=linux
- GOARCH=386
- CGO_ENABLED=0
commands:
- |
if test "${DRONE_TAG}" = ""; then
go build -v -ldflags "-X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/i386/drone-manifest
else
go build -v -ldflags "-X main.version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/i386/drone-manifest
fi
build_linux_arm:
image: golang:1.9
pull: true
group: build
environment:
- GOOS=linux
- GOARCH=arm
- CGO_ENABLED=0
- GOARM=7
commands:
- |
if test "${DRONE_TAG}" = ""; then
go build -v -ldflags "-X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/arm/drone-manifest
else
go build -v -ldflags "-X main.version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/arm/drone-manifest
fi
build_linux_arm64:
image: golang:1.9
pull: true
group: build
environment:
- GOOS=linux
- GOARCH=arm64
- CGO_ENABLED=0
commands:
- |
if test "${DRONE_TAG}" = ""; then
go build -v -ldflags "-X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/arm64/drone-manifest
else
go build -v -ldflags "-X main.version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/arm64/drone-manifest
fi
publish_linux_amd64:
image: plugins/docker:17.05
pull: true
secrets: [ docker_username, docker_password ]
group: docker
repo: plugins/manifest
auto_tag: true
auto_tag_suffix: amd64
dockerfile: Dockerfile
when:
event: [ push, tag ]
publish_linux_i386:
image: plugins/docker:17.05
pull: true
secrets: [ docker_username, docker_password ]
group: docker
repo: plugins/manifest
auto_tag: true
auto_tag_suffix: i386
dockerfile: Dockerfile.i386
when:
event: [ push, tag ]
publish_linux_arm:
image: plugins/docker:17.05
pull: true
secrets: [ docker_username, docker_password ]
group: docker
repo: plugins/manifest
auto_tag: true
auto_tag_suffix: arm
dockerfile: Dockerfile.arm
when:
event: [ push, tag ]
publish_linux_arm64:
image: plugins/docker:17.05
pull: true
secrets: [ docker_username, docker_password ]
group: docker
repo: plugins/manifest
auto_tag: true
auto_tag_suffix: arm64
dockerfile: Dockerfile.arm64
when:
event: [ push, tag ]
manifests:
image: plugins/manifest:1
pull: true
secrets: [ docker_username, docker_password ]
spec: manifest.tmpl
auto_tag: true
ignore_missing: true
when:
event: [ push, tag ]
microbadger:
image: plugins/webhook:1
pull: true
secrets: [ webhook_url ]
when:
status: [ success ]
View File
View File
+28
View File
@@ -0,0 +1,28 @@
# 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
release/
coverage.out
drone-manifest
+20
View File
@@ -0,0 +1,20 @@
FROM alpine:3.6 as base
ENV MANIFEST_VERSION 0.7.0
ENV MANIFEST_URL https://github.com/estesp/manifest-tool/releases/download/v${MANIFEST_VERSION}/manifest-tool-linux-amd64
RUN apk add --no-cache curl && \
curl -sSLo /bin/manifest-tool ${MANIFEST_URL} && \
chmod +x /bin/manifest-tool
FROM plugins/base:multiarch
LABEL maintainer="Drone.IO Community <drone-dev@googlegroups.com>" \
org.label-schema.name="Drone Manifest" \
org.label-schema.vendor="Drone.IO Community" \
org.label-schema.schema-version="1.0"
COPY --from=base /bin/manifest-tool /bin/
ADD release/linux/amd64/drone-manifest /bin/
ENTRYPOINT ["/bin/drone-manifest"]
+18
View File
@@ -0,0 +1,18 @@
FROM alpine:3.6 as base
ENV MANIFEST_VERSION 0.7.0
ENV MANIFEST_URL https://github.com/estesp/manifest-tool/releases/download/v${MANIFEST_VERSION}/manifest-tool-linux-armv7
RUN apk add --no-cache curl && \
curl -sSLo /bin/manifest-tool ${MANIFEST_URL} && \
chmod +x /bin/manifest-tool
FROM plugins/base:multiarch
LABEL maintainer="Drone.IO Community <drone-dev@googlegroups.com>" \
org.label-schema.name="Drone Manifest" \
org.label-schema.vendor="Drone.IO Community" \
org.label-schema.schema-version="1.0"
ADD release/linux/arm/drone-manifest /bin/
ENTRYPOINT ["/bin/drone-manifest"]
+18
View File
@@ -0,0 +1,18 @@
FROM alpine:3.6 as base
ENV MANIFEST_VERSION 0.7.0
ENV MANIFEST_URL https://github.com/estesp/manifest-tool/releases/download/v${MANIFEST_VERSION}/manifest-tool-linux-arm64
RUN apk add --no-cache curl && \
curl -sSLo /bin/manifest-tool ${MANIFEST_URL} && \
chmod +x /bin/manifest-tool
FROM plugins/base:multiarch
LABEL maintainer="Drone.IO Community <drone-dev@googlegroups.com>" \
org.label-schema.name="Drone Manifest" \
org.label-schema.vendor="Drone.IO Community" \
org.label-schema.schema-version="1.0"
ADD release/linux/arm64/drone-manifest /bin/
ENTRYPOINT ["/bin/drone-manifest"]
+18
View File
@@ -0,0 +1,18 @@
FROM alpine:3.6 as base
ENV MANIFEST_VERSION 0.7.0
ENV MANIFEST_URL https://github.com/estesp/manifest-tool/releases/download/v${MANIFEST_VERSION}/manifest-tool-linux-386
RUN apk add --no-cache curl && \
curl -sSLo /bin/manifest-tool ${MANIFEST_URL} && \
chmod +x /bin/manifest-tool
FROM plugins/base:multiarch
LABEL maintainer="Drone.IO Community <drone-dev@googlegroups.com>" \
org.label-schema.name="Drone Manifest" \
org.label-schema.vendor="Drone.IO Community" \
org.label-schema.schema-version="1.0"
ADD release/linux/i386/drone-manifest /bin/
ENTRYPOINT ["/bin/drone-manifest"]
+16
View File
@@ -0,0 +1,16 @@
# escape=`
FROM microsoft/nanoserver:10.0.14393.1593
ENV MANIFEST_VERSION 0.7.0
ENV MANIFEST_URL https://github.com/estesp/manifest-tool/releases/download/v${MANIFEST_VERSION}/manifest-tool-windows-amd64.exe
RUN powershell -Command $ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue'; `
Invoke-WebRequest -Uri $env:MANIFEST_URL -OutFile "C:\Windows\System32\manifest-tool.exe"
LABEL maintainer="Drone.IO Community <drone-dev@googlegroups.com>" `
org.label-schema.name="Drone Manifest" `
org.label-schema.vendor="Drone.IO Community" `
org.label-schema.schema-version="1.0"
ADD drone-manifest.exe /drone-manifest.exe
ENTRYPOINT [ "\\drone-manifest.exe" ]
Generated
+33
View File
@@ -0,0 +1,33 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/aymerick/raymond"
packages = [".","ast","lexer","parser"]
revision = "a2232af10b53ef1ae5a767f5178db3a6c1dab655"
version = "v2.0.1"
[[projects]]
name = "github.com/coreos/go-semver"
packages = ["semver"]
revision = "8ab6407b697782a06568d4b7f1db25550ec2e4c6"
version = "v0.2.0"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
branch = "master"
name = "github.com/urfave/cli"
packages = ["."]
revision = "75104e932ac2ddb944a6ea19d9f9f26316ff1145"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "a89f1c3cf068c93fd8bf83840a119df2915c502193ca71a2b5ae4b6a6cfa4d70"
solver-name = "gps-cdcl"
solver-version = 1
+15
View File
@@ -0,0 +1,15 @@
[[constraint]]
name = "github.com/aymerick/raymond"
version = "2.0.1"
[[constraint]]
name = "github.com/coreos/go-semver"
version = "0.2.0"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
[[constraint]]
branch = "master"
name = "github.com/urfave/cli"
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+37 -2
View File
@@ -1,2 +1,37 @@
# drone-manifest-tool
Drone plugin to push Docker manifests
# drone-manifest
[![Build Status](http://beta.drone.io/api/badges/drone-plugins/drone-manifest/status.svg)](http://beta.drone.io/drone-plugins/drone-manifest)
[![Join the discussion at https://discourse.drone.io](https://img.shields.io/badge/discourse-forum-orange.svg)](https://discourse.drone.io)
[![Drone questions at https://stackoverflow.com](https://img.shields.io/badge/drone-stackoverflow-orange.svg)](https://stackoverflow.com/questions/tagged/drone.io)
[![Go Doc](https://godoc.org/github.com/drone-plugins/drone-manifest?status.svg)](http://godoc.org/github.com/drone-plugins/drone-manifest)
[![Go Report](https://goreportcard.com/badge/github.com/drone-plugins/drone-manifest)](https://goreportcard.com/report/github.com/drone-plugins/drone-manifest)
[![](https://images.microbadger.com/badges/image/plugins/manifest.svg)](https://microbadger.com/images/plugins/manifest "Get your own image badge on microbadger.com")
Drone plugin to push Docker manifest to a registry for multi-architecture mappings. For the usage information and a listing of the available options please take a look at [the docs](http://plugins.drone.io/drone-plugins/drone-manifest/).
## Build
Build the binary with the following commands:
```
go build
```
## Docker
Build the Docker image with the following commands:
```
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a -tags netgo -o release/linux/amd64/drone-manifest
docker build --rm -t plugins/manifest .
```
### Usage
```
docker run --rm \
-e PLUGIN_PLATFORMS=linux/amd64,linux/arm,linux/arm64 \
-e PLUGIN_TEMPLATE=organization/project-ARCH:1.0.0 \
-e PLUGIN_TARGET=organization/project:1.0.0 \
plugins/manifest
```
+102
View File
@@ -0,0 +1,102 @@
package command
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"strings"
"github.com/pkg/errors"
)
type Command struct {
username string
password string
spec string
platforms []string
target string
template string
path string
ignoreMissing bool
}
func New(opts ...Option) *Command {
c := &Command{}
for _, opt := range opts {
opt(c)
}
return c
}
func (c *Command) Exec() error {
args := []string{}
if c.username != "" {
args = append(args, fmt.Sprintf("--username=%s", c.username))
}
if c.password != "" {
args = append(args, fmt.Sprintf("--password=%s", c.password))
}
args = append(args, "push")
if c.spec != "" {
tmpfile, err := ioutil.TempFile(c.path, "manifest-")
if err != nil {
return errors.Wrap(err, "failed to create tempfile")
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write([]byte(c.spec)); err != nil {
return errors.Wrap(err, "failed to write tempfile")
}
if err := tmpfile.Close(); err != nil {
return errors.Wrap(err, "failed to close temp file")
}
args = append(args, "from-spec")
args = append(args, tmpfile.Name())
} else {
args = append(args, "from-args")
if len(c.platforms) != 0 {
args = append(args, fmt.Sprintf("--platforms=%s", strings.Join(c.platforms, ",")))
}
if c.target != "" {
args = append(args, fmt.Sprintf("--target=%s", c.target))
}
if c.template != "" {
args = append(args, fmt.Sprintf("--template=%s", c.template))
}
}
if c.ignoreMissing {
args = append(args, "--ignore-missing")
}
cmd := exec.Command(
"manifest-tool",
args...,
)
buf := bytes.NewBufferString("")
cmd.Stdout = io.MultiWriter(os.Stdout, buf)
cmd.Stderr = io.MultiWriter(os.Stderr, buf)
if c.path != "" {
cmd.Dir = c.path
}
return cmd.Run()
}
+51
View File
@@ -0,0 +1,51 @@
package command
type Option func(*Command)
func WithUsername(val string) Option {
return func(t *Command) {
t.username = val
}
}
func WithPassword(val string) Option {
return func(t *Command) {
t.password = val
}
}
func WithSpec(val string) Option {
return func(t *Command) {
t.spec = val
}
}
func WithPlatforms(val []string) Option {
return func(t *Command) {
t.platforms = val
}
}
func WithTarget(val string) Option {
return func(t *Command) {
t.target = val
}
}
func WithTemplate(val string) Option {
return func(t *Command) {
t.template = val
}
}
func WithPath(val string) Option {
return func(t *Command) {
t.path = val
}
}
func IgnoreMissing() Option {
return func(t *Command) {
t.ignoreMissing = true
}
}
+210
View File
@@ -0,0 +1,210 @@
package main
import (
"fmt"
"log"
"os"
"github.com/drone-plugins/drone-manifest/tagging"
"github.com/urfave/cli"
)
var (
version = "0.0.0"
build = "0"
)
func main() {
app := cli.NewApp()
app.Name = "manifest plugin"
app.Usage = "manifest plugin"
app.Version = fmt.Sprintf("%s+%s", version, build)
app.Action = run
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "username",
Usage: "username for registry",
EnvVar: "PLUGIN_USERNAME,MANIFEST_USERNAME,DOCKER_USERNAME",
},
cli.StringFlag{
Name: "password",
Usage: "password for registry",
EnvVar: "PLUGIN_PASSWORD,MANIFEST_PASSWORD,DOCKER_PASSWORD",
},
cli.StringSliceFlag{
Name: "platforms",
Usage: "platforms for manifests",
EnvVar: "PLUGIN_PLATFORMS",
},
cli.StringFlag{
Name: "target",
Usage: "target for manifests",
EnvVar: "PLUGIN_TARGET",
},
cli.StringFlag{
Name: "template",
Usage: "template for manifests",
EnvVar: "PLUGIN_TEMPLATE",
},
cli.StringFlag{
Name: "spec",
Usage: "path to manifest spec",
EnvVar: "PLUGIN_SPEC",
},
cli.BoolFlag{
Name: "ignore-missing",
Usage: "ignore missing images",
EnvVar: "PLUGIN_IGNORE_MISSING",
},
cli.StringSliceFlag{
Name: "tags",
Usage: "list of additional tags",
Value: &cli.StringSlice{},
EnvVar: "PLUGIN_TAG,PLUGIN_TAGS",
FilePath: ".tags",
},
cli.BoolFlag{
Name: "tags.auto",
Usage: "automatically build tags",
EnvVar: "PLUGIN_DEFAULT_TAGS,PLUGIN_AUTO_TAG",
},
cli.StringFlag{
Name: "path",
Usage: "git clone path",
EnvVar: "DRONE_WORKSPACE",
},
cli.StringFlag{
Name: "repo.owner",
Usage: "repository owner",
EnvVar: "DRONE_REPO_OWNER",
},
cli.StringFlag{
Name: "repo.name",
Usage: "repository name",
EnvVar: "DRONE_REPO_NAME",
},
cli.StringFlag{
Name: "repo.branch",
Usage: "repository default branch",
EnvVar: "DRONE_REPO_BRANCH",
},
cli.StringFlag{
Name: "commit.sha",
Usage: "git commit sha",
EnvVar: "DRONE_COMMIT_SHA",
Value: "00000000",
},
cli.StringFlag{
Name: "commit.ref",
Usage: "git commit ref",
EnvVar: "DRONE_COMMIT_REF",
},
cli.StringFlag{
Name: "commit.branch",
Value: "master",
Usage: "git commit branch",
EnvVar: "DRONE_COMMIT_BRANCH",
},
cli.StringFlag{
Name: "commit.pull",
Usage: "git pull request",
EnvVar: "DRONE_PULL_REQUEST",
},
cli.StringFlag{
Name: "build.event",
Value: "push",
Usage: "build event",
EnvVar: "DRONE_BUILD_EVENT",
},
cli.IntFlag{
Name: "build.number",
Usage: "build number",
EnvVar: "DRONE_BUILD_NUMBER",
},
cli.StringFlag{
Name: "build.status",
Usage: "build status",
Value: "success",
EnvVar: "DRONE_BUILD_STATUS",
},
cli.StringFlag{
Name: "build.link",
Usage: "build link",
EnvVar: "DRONE_BUILD_LINK",
},
cli.Int64Flag{
Name: "build.started",
Usage: "build started",
EnvVar: "DRONE_BUILD_STARTED",
},
cli.Int64Flag{
Name: "build.created",
Usage: "build created",
EnvVar: "DRONE_BUILD_CREATED",
},
cli.StringFlag{
Name: "build.tag",
Usage: "build tag",
EnvVar: "DRONE_TAG",
},
cli.Int64Flag{
Name: "job.started",
Usage: "job started",
EnvVar: "DRONE_JOB_STARTED",
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func run(c *cli.Context) error {
plugin := Plugin{
Repo: Repo{
Owner: c.String("repo.owner"),
Name: c.String("repo.name"),
Branch: c.String("repo.branch"),
},
Build: Build{
Path: c.String("path"),
Tag: c.String("build.tag"),
Number: c.Int("build.number"),
Event: c.String("build.event"),
Status: c.String("build.status"),
Commit: c.String("commit.sha"),
Ref: c.String("commit.ref"),
Branch: c.String("commit.branch"),
Pull: c.String("commit.pull"),
Started: c.Int64("build.started"),
Created: c.Int64("build.created"),
Tags: c.StringSlice("tags"),
},
Job: Job{
Started: c.Int64("job.started"),
},
Auto: Auto{
Tags: []string{"1.0", "1"},
},
Config: Config{
Username: c.String("username"),
Password: c.String("password"),
Platforms: c.StringSlice("platforms"),
Target: c.String("target"),
Template: c.String("template"),
Spec: c.String("spec"),
IgnoreMissing: c.Bool("ignore-missing"),
},
}
if c.Bool("tags.auto") {
if tagging.UseDefaultTag(c.String("commit.ref"), c.String("repo.branch")) {
plugin.Build.Tags = tagging.DefaultTags(c.String("commit.ref"))
} else {
log.Printf("skipping automated tags for %s", c.String("commit.ref"))
return nil
}
}
return plugin.Exec()
}
+33
View File
@@ -0,0 +1,33 @@
image: plugins/manifest:{{#if build.tag}}{{trimPrefix build.tag "v"}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
-
image: plugins/manifest:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}amd64
platform:
architecture: amd64
os: linux
-
image: plugins/manifest:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}i386
platform:
architecture: 386
os: linux
-
image: plugins/manifest:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}arm64
platform:
architecture: arm64
os: linux
-
image: plugins/manifest:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}arm
platform:
architecture: arm
os: linux
-
image: plugins/manifest:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}windows
platform:
architecture: amd64
os: windows
+127
View File
@@ -0,0 +1,127 @@
package main
import (
"errors"
"log"
"strings"
"github.com/drone-plugins/drone-manifest/command"
)
type (
Repo struct {
Owner string
Name string
Branch string
}
Build struct {
Path string
Tag string
Event string
Number int
Commit string
Ref string
Branch string
Author string
Pull string
Message string
DeployTo string
Status string
Link string
Started int64
Created int64
Tags []string
}
Job struct {
Started int64
}
Auto struct {
Tags []string
}
Config struct {
Username string
Password string
Platforms []string
Target string
Template string
Spec string
IgnoreMissing bool
}
Plugin struct {
Repo Repo
Build Build
Job Job
Auto Auto
Config Config
}
)
func (p *Plugin) Exec() error {
opts := make([]command.Option, 0)
if p.Config.Username == "" {
return errors.New("you must provide a username")
} else {
opts = append(opts, command.WithUsername(p.Config.Username))
}
if p.Config.Password == "" {
return errors.New("you must provide a password")
} else {
opts = append(opts, command.WithPassword(p.Config.Password))
}
if p.Config.Spec != "" {
spec, err := RenderTrim(p.Config.Spec, p)
if err != nil {
return err
}
opts = append(opts, command.WithSpec(spec))
log.Printf(
"pushing by spec",
)
} else {
if len(p.Config.Platforms) == 0 {
return errors.New("you must provide platforms")
} else {
opts = append(opts, command.WithPlatforms(p.Config.Platforms))
}
if p.Config.Target == "" {
return errors.New("you must provide a target")
} else {
opts = append(opts, command.WithTarget(p.Config.Target))
}
if p.Config.Template == "" {
return errors.New("you must provide a template")
} else {
opts = append(opts, command.WithTemplate(p.Config.Template))
}
log.Printf(
"pushing %s to %s for %s",
p.Config.Template,
p.Config.Target,
strings.Join(p.Config.Platforms, ", "),
)
}
if p.Config.IgnoreMissing {
opts = append(opts, command.IgnoreMissing())
}
if p.Build.Path != "" {
opts = append(opts, command.WithPath(p.Build.Path))
}
return command.New(opts...).Exec()
}
+68
View File
@@ -0,0 +1,68 @@
package tagging
import (
"fmt"
"strings"
"github.com/coreos/go-semver/semver"
)
// DefaultTags returns a set of default suggested tags.
func DefaultTags(ref string) []string {
if !strings.HasPrefix(ref, "refs/tags/") {
return []string{"latest"}
}
v := stripTagPrefix(ref)
version, err := semver.NewVersion(v)
if err != nil {
return []string{"latest"}
}
if version.PreRelease != "" || version.Metadata != "" {
return []string{
version.String(),
}
}
if version.Major == 0 {
return []string{
fmt.Sprintf("%d.%d", version.Major, version.Minor),
fmt.Sprintf("%d.%d.%d", version.Major, version.Minor, version.Patch),
}
}
return []string{
fmt.Sprint(version.Major),
fmt.Sprintf("%d.%d", version.Major, version.Minor),
fmt.Sprintf("%d.%d.%d", version.Major, version.Minor, version.Patch),
}
}
// UseDefaultTag to restrict latest tag for default branch.
func UseDefaultTag(ref, defaultBranch string) bool {
if strings.HasPrefix(ref, "refs/tags/") {
return true
}
if stripHeadPrefix(ref) == defaultBranch {
return true
}
return false
}
// stripHeadPrefix just strips the ref heads prefix.
func stripHeadPrefix(ref string) string {
return strings.TrimPrefix(ref, "refs/heads/")
}
// stripTagPrefix just strips the ref tags prefix.
func stripTagPrefix(ref string) string {
ref = strings.TrimPrefix(ref, "refs/tags/")
ref = strings.TrimPrefix(ref, "v")
return ref
}
+120
View File
@@ -0,0 +1,120 @@
package tagging
import (
"reflect"
"testing"
)
func TestDefaultTags(t *testing.T) {
var tests = []struct {
Before string
After []string
}{
// valid combinations
{"", []string{"latest"}},
{"refs/heads/master", []string{"latest"}},
{"refs/tags/0.9.0", []string{"0.9", "0.9.0"}},
{"refs/tags/1.0.0", []string{"1", "1.0", "1.0.0"}},
{"refs/tags/v1.0.0", []string{"1", "1.0", "1.0.0"}},
{"refs/tags/v1.0.0-alpha.1", []string{"1.0.0-alpha.1"}},
// malformed or errors
{"refs/tags/x1.0.0", []string{"latest"}},
{"v1.0.0", []string{"latest"}},
}
for _, test := range tests {
got, want := DefaultTags(test.Before), test.After
if !reflect.DeepEqual(got, want) {
t.Errorf("Got tag %v, want %v", got, want)
}
}
}
func TestUseDefaultTag(t *testing.T) {
type args struct {
ref string
defaultBranch string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "latest tag for default branch",
args: args{
ref: "refs/heads/master",
defaultBranch: "master",
},
want: true,
},
{
name: "build from tags",
args: args{
ref: "refs/tags/v1.0.0",
defaultBranch: "master",
},
want: true,
},
{
name: "skip build for not default branch",
args: args{
ref: "refs/heads/develop",
defaultBranch: "master",
},
want: false,
},
}
for _, tt := range tests {
if got := UseDefaultTag(tt.args.ref, tt.args.defaultBranch); got != tt.want {
t.Errorf("%q. UseDefaultTag() = %v, want %v", tt.name, got, tt.want)
}
}
}
func Test_stripHeadPrefix(t *testing.T) {
type args struct {
ref string
}
tests := []struct {
args args
want string
}{
{
args: args{
ref: "refs/heads/master",
},
want: "master",
},
}
for _, tt := range tests {
if got := stripHeadPrefix(tt.args.ref); got != tt.want {
t.Errorf("stripHeadPrefix() = %v, want %v", got, tt.want)
}
}
}
func Test_stripTagPrefix(t *testing.T) {
var tests = []struct {
Before string
After string
}{
{"refs/tags/1.0.0", "1.0.0"},
{"refs/tags/v1.0.0", "1.0.0"},
{"v1.0.0", "1.0.0"},
}
for _, test := range tests {
got, want := stripTagPrefix(test.Before), test.After
if got != want {
t.Errorf("Got tag %s, want %s", got, want)
}
}
}
+141
View File
@@ -0,0 +1,141 @@
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/aymerick/raymond"
)
func init() {
raymond.RegisterHelpers(funcs)
}
// Render parses and executes a template, returning the results in string format.
func Render(template string, payload interface{}) (s string, err error) {
u, err := url.Parse(template)
if err == nil {
switch u.Scheme {
case "http", "https":
res, err := http.Get(template)
if err != nil {
return s, err
}
defer res.Body.Close()
out, err := ioutil.ReadAll(res.Body)
if err != nil {
return s, err
}
template = string(out)
default:
out, err := ioutil.ReadFile(u.Path)
if err != nil {
return s, err
}
template = string(out)
}
}
return raymond.Render(template, payload)
}
// 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 interface{}) (string, error) {
out, err := Render(template, playload)
return strings.Trim(out, " \n"), err
}
var funcs = map[string]interface{}{
"uppercase": strings.ToUpper,
"lowercase": strings.ToLower,
"trimPrefix": strings.TrimPrefix,
"trimSuffix": strings.TrimSuffix,
"quote": strconv.Quote,
"join": strings.Join,
"uppercasefirst": uppercaseFirst,
"duration": toDuration,
"datetime": toDatetime,
"success": isSuccess,
"failure": isFailure,
"truncate": truncate,
"urlencode": urlencode,
"since": since,
}
func uppercaseFirst(s string) string {
a := []rune(s)
a[0] = unicode.ToUpper(a[0])
s = string(a)
return s
}
func toDuration(started, finished float64) string {
return fmt.Sprintln(time.Duration(finished-started) * time.Second)
}
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 {
return time.Unix(int64(timestamp), 0).Local().Format(layout)
}
return time.Unix(int64(timestamp), 0).In(loc).Format(layout)
}
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()
}
}
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()
}
}
func truncate(s string, len int) string {
if utf8.RuneCountInString(s) <= len {
return s
}
runes := []rune(s)
return string(runes[:len])
}
func urlencode(options *raymond.Options) string {
return url.QueryEscape(options.Fn())
}
func since(start int64) string {
// NOTE: not using `time.Since()` because the fractional second component
// will give us something like "40m12.917523438s" vs "40m12s". We lose
// some precision, but the format is much more readable.
now := time.Unix(time.Now().Unix(), 0)
return fmt.Sprintln(now.Sub(time.Unix(start, 0)))
}
+3
View File
@@ -0,0 +1,3 @@
[submodule "mustache"]
path = mustache
url = git://github.com/mustache/spec.git
+6
View File
@@ -0,0 +1,6 @@
---
language: go
go:
- 1.3
- tip
+46
View File
@@ -0,0 +1,46 @@
# 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
+33
View File
@@ -0,0 +1,33 @@
# Raymond Changelog
### Raymond 2.0.1 _(June 01, 2016)_
- [BUGFIX] Removes data races [#3](https://github.com/aymerick/raymond/issues/3) - Thanks [@markbates](https://github.com/markbates)
### Raymond 2.0.0 _(May 01, 2016)_
- [BUGFIX] Fixes passing of context in helper options [#2](https://github.com/aymerick/raymond/issues/2) - Thanks [@GhostRussia](https://github.com/GhostRussia)
- [BREAKING] Renames and unexports constants:
- `handlebars.DUMP_TPL`
- `lexer.ESCAPED_ESCAPED_OPEN_MUSTACHE`
- `lexer.ESCAPED_OPEN_MUSTACHE`
- `lexer.OPEN_MUSTACHE`
- `lexer.CLOSE_MUSTACHE`
- `lexer.CLOSE_STRIP_MUSTACHE`
- `lexer.CLOSE_UNESCAPED_STRIP_MUSTACHE`
- `lexer.DUMP_TOKEN_POS`
- `lexer.DUMP_ALL_TOKENS_VAL`
### 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.
+22
View File
@@ -0,0 +1,22 @@
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.
+1382
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
2.0.1
+785
View File
@@ -0,0 +1,785 @@
// 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 (
// NodeProgram is the program node
NodeProgram NodeType = iota
// NodeMustache is the mustache statement node
NodeMustache
// NodeBlock is the block statement node
NodeBlock
// NodePartial is the partial statement node
NodePartial
// NodeContent is the content statement node
NodeContent
// NodeComment is the comment statement node
NodeComment
// NodeExpression is the expression node
NodeExpression
// NodeSubExpression is the subexpression node
NodeSubExpression
// NodePath is the expression path node
NodePath
// NodeBoolean is the literal boolean node
NodeBoolean
// NodeNumber is the literal number node
NodeNumber
// NodeString is the literal string node
NodeString
// NodeHash is the hash node
NodeHash
// NodeHashPair is the hash pair node
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++
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"
}
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)
}
return node.Value
}
//
// Hash
//
// Hash represents a hash node.
type Hash struct {
NodeType
Loc
Pairs []*HashPair
}
// NewHash 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{[%d", 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)
}
+279
View File
@@ -0,0 +1,279 @@
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
}
+167
View File
@@ -0,0 +1,167 @@
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)
}
}
}
}
}
+316
View File
@@ -0,0 +1,316 @@
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)
}
}
+95
View File
@@ -0,0 +1,95 @@
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
}
+65
View File
@@ -0,0 +1,65 @@
package raymond
import (
"bytes"
"strings"
)
//
// That whole file is borrowed from https://github.com/golang/go/tree/master/src/html/escape.go
//
// With changes:
// &#39 => &apos;
// &#34 => &quot;
//
// 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 = "&amp;"
case '\'':
esc = "&apos;"
case '<':
esc = "&lt;"
case '>':
esc = "&gt;"
case '"':
esc = "&quot;"
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()
}
+20
View File
@@ -0,0 +1,20 @@
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 &lt;em&gt;cool&lt;/em&gt; website</a>
}
+984
View File
@@ -0,0 +1,984 @@
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)
}
+215
View File
@@ -0,0 +1,215 @@
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)
}
}
+100
View File
@@ -0,0 +1,100 @@
package handlebars
import (
"fmt"
"io/ioutil"
"path"
"strconv"
"testing"
"github.com/aymerick/raymond"
)
// cf. https://github.com/aymerick/go-fuzz-tests/raymond
const dumpTpl = false
var dumpTplNb = 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 dumpTpl {
filename := strconv.Itoa(dumpTplNb)
if err := ioutil.WriteFile(path.Join(".", "dump_tpl", filename), []byte(test.input), 0644); err != nil {
panic(err)
}
dumpTplNb++
}
// 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())
}
}
}
}
}
}
+650
View File
@@ -0,0 +1,650 @@
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,
"&amp;&quot;&apos;`\\&lt;&gt;",
},
{
"escaping expressions (9)",
"{{awesome}}",
map[string]string{"awesome": "Escaped, <b> looks like: &lt;b&gt;"},
nil, nil, nil,
"Escaped, &lt;b&gt; looks like: &amp;lt;b&amp;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")
}},
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)
}
}
}
+208
View File
@@ -0,0 +1,208 @@
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)
}
+341
View File
@@ -0,0 +1,341 @@
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{"&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!", "2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. 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)
}
+300
View File
@@ -0,0 +1,300 @@
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)
}
+2
View File
@@ -0,0 +1,2 @@
// Package handlebars contains all the tests that come from handlebars.js project.
package handlebars
+665
View File
@@ -0,0 +1,665 @@
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&apos;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")
}
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")
}
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()
}
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()
}
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()
}
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)
}
+182
View File
@@ -0,0 +1,182 @@
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)
}
+209
View File
@@ -0,0 +1,209 @@
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)
}
+259
View File
@@ -0,0 +1,259 @@
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&lt;",
},
{
"should strip whitespace around mustache calls (2)",
" {{~foo}} ",
map[string]string{"foo": "bar<"},
nil, nil, nil,
"bar&lt; ",
},
{
"should strip whitespace around mustache calls (3)",
" {{foo~}} ",
map[string]string{"foo": "bar<"},
nil, nil, nil,
" bar&lt;",
},
{
"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)
}
+371
View File
@@ -0,0 +1,371 @@
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().Interface()
}
//
// 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]
}
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))
}
+193
View File
@@ -0,0 +1,193 @@
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)
}
//
// Fixes: https://github.com/aymerick/raymond/issues/2
//
type Author struct {
FirstName string
LastName string
}
func TestHelperCtx(t *testing.T) {
RegisterHelper("template", func(name string, options *Options) SafeString {
context := options.Ctx()
template := name + " - {{ firstName }} {{ lastName }}"
result, _ := Render(template, context)
return SafeString(result)
})
template := `By {{ template "namefile" }}`
context := Author{"Alan", "Johnson"}
result, _ := Render(template, context)
if result != "By namefile - Alan Johnson" {
t.Errorf("Failed to render template in helper: %q", result)
}
}
+639
View File
@@ -0,0 +1,639 @@
// 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
escapedEscapedOpenMustache = "\\\\{{"
escapedOpenMustache = "\\{{"
openMustache = "{{"
closeMustache = "}}"
closeStripMustache = "~}}"
closeUnescapedStripMustache = "}~}}"
)
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
}
// 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
}
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(escapedEscapedOpenMustache) {
// \\{{
// 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(escapedOpenMustache) {
// \{{
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(openMustache) {
// {{
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(closeMustache) || l.isString(closeStripMustache) || l.isString(closeUnescapedStripMustache) {
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
// 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)
}
+541
View File
@@ -0,0 +1,541 @@
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
}
+183
View File
@@ -0,0 +1,183 @@
package lexer
import "fmt"
const (
// TokenError represents an error
TokenError TokenKind = iota
// TokenEOF represents an End Of File
TokenEOF
//
// Mustache delimiters
//
// TokenOpen is the OPEN token
TokenOpen
// TokenClose is the CLOSE token
TokenClose
// TokenOpenRawBlock is the OPEN_RAW_BLOCK token
TokenOpenRawBlock
// TokenCloseRawBlock is the CLOSE_RAW_BLOCK token
TokenCloseRawBlock
// TokenOpenEndRawBlock is the END_RAW_BLOCK token
TokenOpenEndRawBlock
// TokenOpenUnescaped is the OPEN_UNESCAPED token
TokenOpenUnescaped
// TokenCloseUnescaped is the CLOSE_UNESCAPED token
TokenCloseUnescaped
// TokenOpenBlock is the OPEN_BLOCK token
TokenOpenBlock
// TokenOpenEndBlock is the OPEN_ENDBLOCK token
TokenOpenEndBlock
// TokenInverse is the INVERSE token
TokenInverse
// TokenOpenInverse is the OPEN_INVERSE token
TokenOpenInverse
// TokenOpenInverseChain is the OPEN_INVERSE_CHAIN token
TokenOpenInverseChain
// TokenOpenPartial is the OPEN_PARTIAL token
TokenOpenPartial
// TokenComment is the COMMENT token
TokenComment
//
// Inside mustaches
//
// TokenOpenSexpr is the OPEN_SEXPR token
TokenOpenSexpr
// TokenCloseSexpr is the CLOSE_SEXPR token
TokenCloseSexpr
// TokenEquals is the EQUALS token
TokenEquals
// TokenData is the DATA token
TokenData
// TokenSep is the SEP token
TokenSep
// TokenOpenBlockParams is the OPEN_BLOCK_PARAMS token
TokenOpenBlockParams
// TokenCloseBlockParams is the CLOSE_BLOCK_PARAMS token
TokenCloseBlockParams
//
// Tokens with content
//
// TokenContent is the CONTENT token
TokenContent
// TokenID is the ID token
TokenID
// TokenString is the STRING token
TokenString
// TokenNumber is the NUMBER token
TokenNumber
// TokenBoolean is the BOOLEAN token
TokenBoolean
)
const (
// Option to generate token position in its string representation
dumpTokenPos = false
// Option to generate values for all token kinds for their string representations
dumpAllTokensVal = 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 dumpTokenPos {
result += fmt.Sprintf("%d:", t.Pos)
}
result += fmt.Sprintf("%s", t.Kind)
if (dumpAllTokensVal || (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
}
+234
View File
@@ -0,0 +1,234 @@
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++
return Str(musTestLambdaInterMult)
}},
nil, nil, nil,
"1 == 2 == 3",
},
{
"Escaping",
"<{{lambda}}{{{lambda}}}",
map[string]interface{}{"lambda": func() string { return ">" }},
nil, nil, nil,
"<&gt;>",
},
// // 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)
}
+846
View File
@@ -0,0 +1,846 @@
// 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.next().Pos, p.next().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()
}
result := ast.NewProgram(p.next().Pos, p.next().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()
}
// 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)
}
+200
View File
@@ -0,0 +1,200 @@
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' ]
}
+360
View File
@@ -0,0 +1,360 @@
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 }
+85
View File
@@ -0,0 +1,85 @@
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)
}
}
// RegisterPartialTemplate 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
}
+28
View File
@@ -0,0 +1,28 @@
// 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)
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+115
View File
@@ -0,0 +1,115 @@
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>
}
+84
View File
@@ -0,0 +1,84 @@
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
}
+59
View File
@@ -0,0 +1,59 @@
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>
}
+248
View File
@@ -0,0 +1,248 @@
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
}
// RegisterPartialTemplate 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)
}
+166
View File
@@ -0,0 +1,166 @@
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>' ]
//
}
+85
View File
@@ -0,0 +1,85 @@
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)]
}
+51
View File
@@ -0,0 +1,51 @@
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
}
+8
View File
@@ -0,0 +1,8 @@
language: go
sudo: false
go:
- 1.4
- 1.5
- 1.6
- tip
script: cd semver && go test
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+28
View File
@@ -0,0 +1,28 @@
# go-semver - Semantic Versioning Library
[![Build Status](https://travis-ci.org/coreos/go-semver.svg?branch=master)](https://travis-ci.org/coreos/go-semver)
[![GoDoc](https://godoc.org/github.com/coreos/go-semver/semver?status.svg)](https://godoc.org/github.com/coreos/go-semver/semver)
go-semver is a [semantic versioning][semver] library for Go. It lets you parse
and compare two semantic version strings.
[semver]: http://semver.org/
## Usage
```go
vA := semver.New("1.2.3")
vB := semver.New("3.2.1")
fmt.Printf("%s < %s == %t\n", vA, vB, vA.LessThan(*vB))
```
## Example Application
```
$ go run example.go 1.2.3 3.2.1
1.2.3 < 3.2.1 == true
$ go run example.go 5.2.3 3.2.1
5.2.3 < 3.2.1 == false
```
+20
View File
@@ -0,0 +1,20 @@
package main
import (
"fmt"
"github.com/coreos/go-semver/semver"
"os"
)
func main() {
vA, err := semver.NewVersion(os.Args[1])
if err != nil {
fmt.Println(err.Error())
}
vB, err := semver.NewVersion(os.Args[2])
if err != nil {
fmt.Println(err.Error())
}
fmt.Printf("%s < %s == %t\n", vA, vB, vA.LessThan(*vB))
}
+268
View File
@@ -0,0 +1,268 @@
// Copyright 2013-2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Semantic Versions http://semver.org
package semver
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
)
type Version struct {
Major int64
Minor int64
Patch int64
PreRelease PreRelease
Metadata string
}
type PreRelease string
func splitOff(input *string, delim string) (val string) {
parts := strings.SplitN(*input, delim, 2)
if len(parts) == 2 {
*input = parts[0]
val = parts[1]
}
return val
}
func New(version string) *Version {
return Must(NewVersion(version))
}
func NewVersion(version string) (*Version, error) {
v := Version{}
if err := v.Set(version); err != nil {
return nil, err
}
return &v, nil
}
// Must is a helper for wrapping NewVersion and will panic if err is not nil.
func Must(v *Version, err error) *Version {
if err != nil {
panic(err)
}
return v
}
// Set parses and updates v from the given version string. Implements flag.Value
func (v *Version) Set(version string) error {
metadata := splitOff(&version, "+")
preRelease := PreRelease(splitOff(&version, "-"))
dotParts := strings.SplitN(version, ".", 3)
if len(dotParts) != 3 {
return fmt.Errorf("%s is not in dotted-tri format", version)
}
parsed := make([]int64, 3, 3)
for i, v := range dotParts[:3] {
val, err := strconv.ParseInt(v, 10, 64)
parsed[i] = val
if err != nil {
return err
}
}
v.Metadata = metadata
v.PreRelease = preRelease
v.Major = parsed[0]
v.Minor = parsed[1]
v.Patch = parsed[2]
return nil
}
func (v Version) String() string {
var buffer bytes.Buffer
fmt.Fprintf(&buffer, "%d.%d.%d", v.Major, v.Minor, v.Patch)
if v.PreRelease != "" {
fmt.Fprintf(&buffer, "-%s", v.PreRelease)
}
if v.Metadata != "" {
fmt.Fprintf(&buffer, "+%s", v.Metadata)
}
return buffer.String()
}
func (v *Version) UnmarshalYAML(unmarshal func(interface{}) error) error {
var data string
if err := unmarshal(&data); err != nil {
return err
}
return v.Set(data)
}
func (v Version) MarshalJSON() ([]byte, error) {
return []byte(`"` + v.String() + `"`), nil
}
func (v *Version) UnmarshalJSON(data []byte) error {
l := len(data)
if l == 0 || string(data) == `""` {
return nil
}
if l < 2 || data[0] != '"' || data[l-1] != '"' {
return errors.New("invalid semver string")
}
return v.Set(string(data[1 : l-1]))
}
// Compare tests if v is less than, equal to, or greater than versionB,
// returning -1, 0, or +1 respectively.
func (v Version) Compare(versionB Version) int {
if cmp := recursiveCompare(v.Slice(), versionB.Slice()); cmp != 0 {
return cmp
}
return preReleaseCompare(v, versionB)
}
// Equal tests if v is equal to versionB.
func (v Version) Equal(versionB Version) bool {
return v.Compare(versionB) == 0
}
// LessThan tests if v is less than versionB.
func (v Version) LessThan(versionB Version) bool {
return v.Compare(versionB) < 0
}
// Slice converts the comparable parts of the semver into a slice of integers.
func (v Version) Slice() []int64 {
return []int64{v.Major, v.Minor, v.Patch}
}
func (p PreRelease) Slice() []string {
preRelease := string(p)
return strings.Split(preRelease, ".")
}
func preReleaseCompare(versionA Version, versionB Version) int {
a := versionA.PreRelease
b := versionB.PreRelease
/* Handle the case where if two versions are otherwise equal it is the
* one without a PreRelease that is greater */
if len(a) == 0 && (len(b) > 0) {
return 1
} else if len(b) == 0 && (len(a) > 0) {
return -1
}
// If there is a prerelease, check and compare each part.
return recursivePreReleaseCompare(a.Slice(), b.Slice())
}
func recursiveCompare(versionA []int64, versionB []int64) int {
if len(versionA) == 0 {
return 0
}
a := versionA[0]
b := versionB[0]
if a > b {
return 1
} else if a < b {
return -1
}
return recursiveCompare(versionA[1:], versionB[1:])
}
func recursivePreReleaseCompare(versionA []string, versionB []string) int {
// A larger set of pre-release fields has a higher precedence than a smaller set,
// if all of the preceding identifiers are equal.
if len(versionA) == 0 {
if len(versionB) > 0 {
return -1
}
return 0
} else if len(versionB) == 0 {
// We're longer than versionB so return 1.
return 1
}
a := versionA[0]
b := versionB[0]
aInt := false
bInt := false
aI, err := strconv.Atoi(versionA[0])
if err == nil {
aInt = true
}
bI, err := strconv.Atoi(versionB[0])
if err == nil {
bInt = true
}
// Handle Integer Comparison
if aInt && bInt {
if aI > bI {
return 1
} else if aI < bI {
return -1
}
}
// Handle String Comparison
if a > b {
return 1
} else if a < b {
return -1
}
return recursivePreReleaseCompare(versionA[1:], versionB[1:])
}
// BumpMajor increments the Major field by 1 and resets all other fields to their default values
func (v *Version) BumpMajor() {
v.Major += 1
v.Minor = 0
v.Patch = 0
v.PreRelease = PreRelease("")
v.Metadata = ""
}
// BumpMinor increments the Minor field by 1 and resets all other fields to their default values
func (v *Version) BumpMinor() {
v.Minor += 1
v.Patch = 0
v.PreRelease = PreRelease("")
v.Metadata = ""
}
// BumpPatch increments the Patch field by 1 and resets all other fields to their default values
func (v *Version) BumpPatch() {
v.Patch += 1
v.PreRelease = PreRelease("")
v.Metadata = ""
}
+370
View File
@@ -0,0 +1,370 @@
// Copyright 2013-2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package semver
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"math/rand"
"reflect"
"testing"
"time"
"gopkg.in/yaml.v2"
)
type fixture struct {
GreaterVersion string
LesserVersion string
}
var fixtures = []fixture{
fixture{"0.0.0", "0.0.0-foo"},
fixture{"0.0.1", "0.0.0"},
fixture{"1.0.0", "0.9.9"},
fixture{"0.10.0", "0.9.0"},
fixture{"0.99.0", "0.10.0"},
fixture{"2.0.0", "1.2.3"},
fixture{"0.0.0", "0.0.0-foo"},
fixture{"0.0.1", "0.0.0"},
fixture{"1.0.0", "0.9.9"},
fixture{"0.10.0", "0.9.0"},
fixture{"0.99.0", "0.10.0"},
fixture{"2.0.0", "1.2.3"},
fixture{"0.0.0", "0.0.0-foo"},
fixture{"0.0.1", "0.0.0"},
fixture{"1.0.0", "0.9.9"},
fixture{"0.10.0", "0.9.0"},
fixture{"0.99.0", "0.10.0"},
fixture{"2.0.0", "1.2.3"},
fixture{"1.2.3", "1.2.3-asdf"},
fixture{"1.2.3", "1.2.3-4"},
fixture{"1.2.3", "1.2.3-4-foo"},
fixture{"1.2.3-5-foo", "1.2.3-5"},
fixture{"1.2.3-5", "1.2.3-4"},
fixture{"1.2.3-5-foo", "1.2.3-5-Foo"},
fixture{"3.0.0", "2.7.2+asdf"},
fixture{"3.0.0+foobar", "2.7.2"},
fixture{"1.2.3-a.10", "1.2.3-a.5"},
fixture{"1.2.3-a.b", "1.2.3-a.5"},
fixture{"1.2.3-a.b", "1.2.3-a"},
fixture{"1.2.3-a.b.c.10.d.5", "1.2.3-a.b.c.5.d.100"},
fixture{"1.0.0", "1.0.0-rc.1"},
fixture{"1.0.0-rc.2", "1.0.0-rc.1"},
fixture{"1.0.0-rc.1", "1.0.0-beta.11"},
fixture{"1.0.0-beta.11", "1.0.0-beta.2"},
fixture{"1.0.0-beta.2", "1.0.0-beta"},
fixture{"1.0.0-beta", "1.0.0-alpha.beta"},
fixture{"1.0.0-alpha.beta", "1.0.0-alpha.1"},
fixture{"1.0.0-alpha.1", "1.0.0-alpha"},
}
func TestCompare(t *testing.T) {
for _, v := range fixtures {
gt, err := NewVersion(v.GreaterVersion)
if err != nil {
t.Error(err)
}
lt, err := NewVersion(v.LesserVersion)
if err != nil {
t.Error(err)
}
if gt.LessThan(*lt) {
t.Errorf("%s should not be less than %s", gt, lt)
}
if gt.Equal(*lt) {
t.Errorf("%s should not be equal to %s", gt, lt)
}
if gt.Compare(*lt) <= 0 {
t.Errorf("%s should be greater than %s", gt, lt)
}
if !lt.LessThan(*gt) {
t.Errorf("%s should be less than %s", lt, gt)
}
if !lt.Equal(*lt) {
t.Errorf("%s should be equal to %s", lt, lt)
}
if lt.Compare(*gt) > 0 {
t.Errorf("%s should not be greater than %s", lt, gt)
}
}
}
func testString(t *testing.T, orig string, version *Version) {
if orig != version.String() {
t.Errorf("%s != %s", orig, version)
}
}
func TestString(t *testing.T) {
for _, v := range fixtures {
gt, err := NewVersion(v.GreaterVersion)
if err != nil {
t.Error(err)
}
testString(t, v.GreaterVersion, gt)
lt, err := NewVersion(v.LesserVersion)
if err != nil {
t.Error(err)
}
testString(t, v.LesserVersion, lt)
}
}
func shuffleStringSlice(src []string) []string {
dest := make([]string, len(src))
rand.Seed(time.Now().Unix())
perm := rand.Perm(len(src))
for i, v := range perm {
dest[v] = src[i]
}
return dest
}
func TestSort(t *testing.T) {
sortedVersions := []string{"1.0.0", "1.0.2", "1.2.0", "3.1.1"}
unsortedVersions := shuffleStringSlice(sortedVersions)
semvers := []*Version{}
for _, v := range unsortedVersions {
sv, err := NewVersion(v)
if err != nil {
t.Fatal(err)
}
semvers = append(semvers, sv)
}
Sort(semvers)
for idx, sv := range semvers {
if sv.String() != sortedVersions[idx] {
t.Fatalf("incorrect sort at index %v", idx)
}
}
}
func TestBumpMajor(t *testing.T) {
version, _ := NewVersion("1.0.0")
version.BumpMajor()
if version.Major != 2 {
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
}
version, _ = NewVersion("1.5.2")
version.BumpMajor()
if version.Minor != 0 && version.Patch != 0 {
t.Fatalf("bumping major on 1.5.2 resulted in %v", version)
}
version, _ = NewVersion("1.0.0+build.1-alpha.1")
version.BumpMajor()
if version.PreRelease != "" && version.PreRelease != "" {
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
}
}
func TestBumpMinor(t *testing.T) {
version, _ := NewVersion("1.0.0")
version.BumpMinor()
if version.Major != 1 {
t.Fatalf("bumping minor on 1.0.0 resulted in %v", version)
}
if version.Minor != 1 {
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
}
version, _ = NewVersion("1.0.0+build.1-alpha.1")
version.BumpMinor()
if version.PreRelease != "" && version.PreRelease != "" {
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
}
}
func TestBumpPatch(t *testing.T) {
version, _ := NewVersion("1.0.0")
version.BumpPatch()
if version.Major != 1 {
t.Fatalf("bumping minor on 1.0.0 resulted in %v", version)
}
if version.Minor != 0 {
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
}
if version.Patch != 1 {
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
}
version, _ = NewVersion("1.0.0+build.1-alpha.1")
version.BumpPatch()
if version.PreRelease != "" && version.PreRelease != "" {
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
}
}
func TestMust(t *testing.T) {
tests := []struct {
versionStr string
version *Version
recov interface{}
}{
{
versionStr: "1.0.0",
version: &Version{Major: 1},
},
{
versionStr: "version number",
recov: errors.New("version number is not in dotted-tri format"),
},
}
for _, tt := range tests {
func() {
defer func() {
recov := recover()
if !reflect.DeepEqual(tt.recov, recov) {
t.Fatalf("incorrect panic for %q: want %v, got %v", tt.versionStr, tt.recov, recov)
}
}()
version := Must(NewVersion(tt.versionStr))
if !reflect.DeepEqual(tt.version, version) {
t.Fatalf("incorrect version for %q: want %+v, got %+v", tt.versionStr, tt.version, version)
}
}()
}
}
type fixtureJSON struct {
GreaterVersion *Version
LesserVersion *Version
}
func TestJSON(t *testing.T) {
fj := make([]fixtureJSON, len(fixtures))
for i, v := range fixtures {
var err error
fj[i].GreaterVersion, err = NewVersion(v.GreaterVersion)
if err != nil {
t.Fatal(err)
}
fj[i].LesserVersion, err = NewVersion(v.LesserVersion)
if err != nil {
t.Fatal(err)
}
}
fromStrings, err := json.Marshal(fixtures)
if err != nil {
t.Fatal(err)
}
fromVersions, err := json.Marshal(fj)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(fromStrings, fromVersions) {
t.Errorf("Expected: %s", fromStrings)
t.Errorf("Unexpected: %s", fromVersions)
}
fromJson := make([]fixtureJSON, 0, len(fj))
err = json.Unmarshal(fromStrings, &fromJson)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(fromJson, fj) {
t.Error("Expected: ", fj)
t.Error("Unexpected: ", fromJson)
}
}
func TestYAML(t *testing.T) {
document, err := yaml.Marshal(fixtures)
if err != nil {
t.Fatal(err)
}
expected := make([]fixtureJSON, len(fixtures))
for i, v := range fixtures {
var err error
expected[i].GreaterVersion, err = NewVersion(v.GreaterVersion)
if err != nil {
t.Fatal(err)
}
expected[i].LesserVersion, err = NewVersion(v.LesserVersion)
if err != nil {
t.Fatal(err)
}
}
fromYAML := make([]fixtureJSON, 0, len(fixtures))
err = yaml.Unmarshal(document, &fromYAML)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(fromYAML, expected) {
t.Error("Expected: ", expected)
t.Error("Unexpected: ", fromYAML)
}
}
func TestBadInput(t *testing.T) {
bad := []string{
"1.2",
"1.2.3x",
"0x1.3.4",
"-1.2.3",
"1.2.3.4",
}
for _, b := range bad {
if _, err := NewVersion(b); err == nil {
t.Error("Improperly accepted value: ", b)
}
}
}
func TestFlag(t *testing.T) {
v := Version{}
f := flag.NewFlagSet("version", flag.ContinueOnError)
f.Var(&v, "version", "set version")
if err := f.Set("version", "1.2.3"); err != nil {
t.Fatal(err)
}
if v.String() != "1.2.3" {
t.Errorf("Set wrong value %q", v)
}
}
func ExampleVersion_LessThan() {
vA := New("1.2.3")
vB := New("3.2.1")
fmt.Printf("%s < %s == %t\n", vA, vB, vA.LessThan(*vB))
// Output:
// 1.2.3 < 3.2.1 == true
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2013-2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package semver
import (
"sort"
)
type Versions []*Version
func (s Versions) Len() int {
return len(s)
}
func (s Versions) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s Versions) Less(i, j int) bool {
return s[i].LessThan(*s[j])
}
// Sort sorts the given slice of Version
func Sort(versions []*Version) {
sort.Sort(Versions(versions))
}
+24
View File
@@ -0,0 +1,24 @@
# 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
+11
View File
@@ -0,0 +1,11 @@
language: go
go_import_path: github.com/pkg/errors
go:
- 1.4.3
- 1.5.4
- 1.6.2
- 1.7.1
- tip
script:
- go test -v ./...
+23
View File
@@ -0,0 +1,23 @@
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
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.
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 HOLDER 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.
+52
View File
@@ -0,0 +1,52 @@
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors)
Package errors provides simple error handling primitives.
`go get github.com/pkg/errors`
The traditional error handling idiom in Go is roughly akin to
```go
if err != nil {
return err
}
```
which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
## Adding context to an error
The errors.Wrap function returns a new error that adds context to the original error. For example
```go
_, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrap(err, "read failed")
}
```
## Retrieving the cause of an error
Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
```go
type causer interface {
Cause() error
}
```
`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
```go
switch err := errors.Cause(err).(type) {
case *MyError:
// handle specifically
default:
// unknown error
}
```
[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).
## Contributing
We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high.
Before proposing a change, please discuss your change by raising an issue.
## Licence
BSD-2-Clause
+32
View File
@@ -0,0 +1,32 @@
version: build-{build}.{branch}
clone_folder: C:\gopath\src\github.com\pkg\errors
shallow_clone: true # for startup speed
environment:
GOPATH: C:\gopath
platform:
- x64
# http://www.appveyor.com/docs/installed-software
install:
# some helpful output for debugging builds
- go version
- go env
# pre-installed MinGW at C:\MinGW is 32bit only
# but MSYS2 at C:\msys64 has mingw64
- set PATH=C:\msys64\mingw64\bin;%PATH%
- gcc --version
- g++ --version
build_script:
- go install -v ./...
test_script:
- set PATH=C:\gopath\bin;%PATH%
- go test -v ./...
#artifacts:
# - path: '%GOPATH%\bin\*.exe'
deploy: off
+59
View File
@@ -0,0 +1,59 @@
// +build go1.7
package errors
import (
"fmt"
"testing"
stderrors "errors"
)
func noErrors(at, depth int) error {
if at >= depth {
return stderrors.New("no error")
}
return noErrors(at+1, depth)
}
func yesErrors(at, depth int) error {
if at >= depth {
return New("ye error")
}
return yesErrors(at+1, depth)
}
func BenchmarkErrors(b *testing.B) {
var toperr error
type run struct {
stack int
std bool
}
runs := []run{
{10, false},
{10, true},
{100, false},
{100, true},
{1000, false},
{1000, true},
}
for _, r := range runs {
part := "pkg/errors"
if r.std {
part = "errors"
}
name := fmt.Sprintf("%s-stack-%d", part, r.stack)
b.Run(name, func(b *testing.B) {
var err error
f := yesErrors
if r.std {
f = noErrors
}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
err = f(0, r.stack)
}
b.StopTimer()
toperr = err
})
}
}
+269
View File
@@ -0,0 +1,269 @@
// Package errors provides simple error handling primitives.
//
// The traditional error handling idiom in Go is roughly akin to
//
// if err != nil {
// return err
// }
//
// which applied recursively up the call stack results in error reports
// without context or debugging information. The errors package allows
// programmers to add context to the failure path in their code in a way
// that does not destroy the original value of the error.
//
// Adding context to an error
//
// The errors.Wrap function returns a new error that adds context to the
// original error by recording a stack trace at the point Wrap is called,
// and the supplied message. For example
//
// _, err := ioutil.ReadAll(r)
// if err != nil {
// return errors.Wrap(err, "read failed")
// }
//
// If additional control is required the errors.WithStack and errors.WithMessage
// functions destructure errors.Wrap into its component operations of annotating
// an error with a stack trace and an a message, respectively.
//
// Retrieving the cause of an error
//
// Using errors.Wrap constructs a stack of errors, adding context to the
// preceding error. Depending on the nature of the error it may be necessary
// to reverse the operation of errors.Wrap to retrieve the original error
// for inspection. Any error value which implements this interface
//
// type causer interface {
// Cause() error
// }
//
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
// the topmost error which does not implement causer, which is assumed to be
// the original cause. For example:
//
// switch err := errors.Cause(err).(type) {
// case *MyError:
// // handle specifically
// default:
// // unknown error
// }
//
// causer interface is not exported by this package, but is considered a part
// of stable public API.
//
// Formatted printing of errors
//
// All error values returned from this package implement fmt.Formatter and can
// be formatted by the fmt package. The following verbs are supported
//
// %s print the error. If the error has a Cause it will be
// printed recursively
// %v see %s
// %+v extended format. Each Frame of the error's StackTrace will
// be printed in detail.
//
// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface.
//
// type stackTracer interface {
// StackTrace() errors.StackTrace
// }
//
// Where errors.StackTrace is defined as
//
// type StackTrace []Frame
//
// The Frame type represents a call site in the stack trace. Frame supports
// the fmt.Formatter interface that can be used for printing information about
// the stack trace of this error. For example:
//
// if err, ok := err.(stackTracer); ok {
// for _, f := range err.StackTrace() {
// fmt.Printf("%+s:%d", f)
// }
// }
//
// stackTracer interface is not exported by this package, but is considered a part
// of stable public API.
//
// See the documentation for Frame.Format for more details.
package errors
import (
"fmt"
"io"
)
// New returns an error with the supplied message.
// New also records the stack trace at the point it was called.
func New(message string) error {
return &fundamental{
msg: message,
stack: callers(),
}
}
// Errorf formats according to a format specifier and returns the string
// as a value that satisfies error.
// Errorf also records the stack trace at the point it was called.
func Errorf(format string, args ...interface{}) error {
return &fundamental{
msg: fmt.Sprintf(format, args...),
stack: callers(),
}
}
// fundamental is an error that has a message and a stack, but no caller.
type fundamental struct {
msg string
*stack
}
func (f *fundamental) Error() string { return f.msg }
func (f *fundamental) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
io.WriteString(s, f.msg)
f.stack.Format(s, verb)
return
}
fallthrough
case 's':
io.WriteString(s, f.msg)
case 'q':
fmt.Fprintf(s, "%q", f.msg)
}
}
// WithStack annotates err with a stack trace at the point WithStack was called.
// If err is nil, WithStack returns nil.
func WithStack(err error) error {
if err == nil {
return nil
}
return &withStack{
err,
callers(),
}
}
type withStack struct {
error
*stack
}
func (w *withStack) Cause() error { return w.error }
func (w *withStack) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
fmt.Fprintf(s, "%+v", w.Cause())
w.stack.Format(s, verb)
return
}
fallthrough
case 's':
io.WriteString(s, w.Error())
case 'q':
fmt.Fprintf(s, "%q", w.Error())
}
}
// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: message,
}
return &withStack{
err,
callers(),
}
}
// Wrapf returns an error annotating err with a stack trace
// at the point Wrapf is call, and the format specifier.
// If err is nil, Wrapf returns nil.
func Wrapf(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: fmt.Sprintf(format, args...),
}
return &withStack{
err,
callers(),
}
}
// WithMessage annotates err with a new message.
// If err is nil, WithMessage returns nil.
func WithMessage(err error, message string) error {
if err == nil {
return nil
}
return &withMessage{
cause: err,
msg: message,
}
}
type withMessage struct {
cause error
msg string
}
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
func (w *withMessage) Cause() error { return w.cause }
func (w *withMessage) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
fmt.Fprintf(s, "%+v\n", w.Cause())
io.WriteString(s, w.msg)
return
}
fallthrough
case 's', 'q':
io.WriteString(s, w.Error())
}
}
// Cause returns the underlying cause of the error, if possible.
// An error value has a cause if it implements the following
// interface:
//
// type causer interface {
// Cause() error
// }
//
// If the error does not implement Cause, the original error will
// be returned. If the error is nil, nil will be returned without further
// investigation.
func Cause(err error) error {
type causer interface {
Cause() error
}
for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
return err
}
+226
View File
@@ -0,0 +1,226 @@
package errors
import (
"errors"
"fmt"
"io"
"reflect"
"testing"
)
func TestNew(t *testing.T) {
tests := []struct {
err string
want error
}{
{"", fmt.Errorf("")},
{"foo", fmt.Errorf("foo")},
{"foo", New("foo")},
{"string with format specifiers: %v", errors.New("string with format specifiers: %v")},
}
for _, tt := range tests {
got := New(tt.err)
if got.Error() != tt.want.Error() {
t.Errorf("New.Error(): got: %q, want %q", got, tt.want)
}
}
}
func TestWrapNil(t *testing.T) {
got := Wrap(nil, "no error")
if got != nil {
t.Errorf("Wrap(nil, \"no error\"): got %#v, expected nil", got)
}
}
func TestWrap(t *testing.T) {
tests := []struct {
err error
message string
want string
}{
{io.EOF, "read error", "read error: EOF"},
{Wrap(io.EOF, "read error"), "client error", "client error: read error: EOF"},
}
for _, tt := range tests {
got := Wrap(tt.err, tt.message).Error()
if got != tt.want {
t.Errorf("Wrap(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want)
}
}
}
type nilError struct{}
func (nilError) Error() string { return "nil error" }
func TestCause(t *testing.T) {
x := New("error")
tests := []struct {
err error
want error
}{{
// nil error is nil
err: nil,
want: nil,
}, {
// explicit nil error is nil
err: (error)(nil),
want: nil,
}, {
// typed nil is nil
err: (*nilError)(nil),
want: (*nilError)(nil),
}, {
// uncaused error is unaffected
err: io.EOF,
want: io.EOF,
}, {
// caused error returns cause
err: Wrap(io.EOF, "ignored"),
want: io.EOF,
}, {
err: x, // return from errors.New
want: x,
}, {
WithMessage(nil, "whoops"),
nil,
}, {
WithMessage(io.EOF, "whoops"),
io.EOF,
}, {
WithStack(nil),
nil,
}, {
WithStack(io.EOF),
io.EOF,
}}
for i, tt := range tests {
got := Cause(tt.err)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("test %d: got %#v, want %#v", i+1, got, tt.want)
}
}
}
func TestWrapfNil(t *testing.T) {
got := Wrapf(nil, "no error")
if got != nil {
t.Errorf("Wrapf(nil, \"no error\"): got %#v, expected nil", got)
}
}
func TestWrapf(t *testing.T) {
tests := []struct {
err error
message string
want string
}{
{io.EOF, "read error", "read error: EOF"},
{Wrapf(io.EOF, "read error without format specifiers"), "client error", "client error: read error without format specifiers: EOF"},
{Wrapf(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"},
}
for _, tt := range tests {
got := Wrapf(tt.err, tt.message).Error()
if got != tt.want {
t.Errorf("Wrapf(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want)
}
}
}
func TestErrorf(t *testing.T) {
tests := []struct {
err error
want string
}{
{Errorf("read error without format specifiers"), "read error without format specifiers"},
{Errorf("read error with %d format specifier", 1), "read error with 1 format specifier"},
}
for _, tt := range tests {
got := tt.err.Error()
if got != tt.want {
t.Errorf("Errorf(%v): got: %q, want %q", tt.err, got, tt.want)
}
}
}
func TestWithStackNil(t *testing.T) {
got := WithStack(nil)
if got != nil {
t.Errorf("WithStack(nil): got %#v, expected nil", got)
}
}
func TestWithStack(t *testing.T) {
tests := []struct {
err error
want string
}{
{io.EOF, "EOF"},
{WithStack(io.EOF), "EOF"},
}
for _, tt := range tests {
got := WithStack(tt.err).Error()
if got != tt.want {
t.Errorf("WithStack(%v): got: %v, want %v", tt.err, got, tt.want)
}
}
}
func TestWithMessageNil(t *testing.T) {
got := WithMessage(nil, "no error")
if got != nil {
t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got)
}
}
func TestWithMessage(t *testing.T) {
tests := []struct {
err error
message string
want string
}{
{io.EOF, "read error", "read error: EOF"},
{WithMessage(io.EOF, "read error"), "client error", "client error: read error: EOF"},
}
for _, tt := range tests {
got := WithMessage(tt.err, tt.message).Error()
if got != tt.want {
t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want)
}
}
}
// errors.New, etc values are not expected to be compared by value
// but the change in errors#27 made them incomparable. Assert that
// various kinds of errors have a functional equality operator, even
// if the result of that equality is always false.
func TestErrorEquality(t *testing.T) {
vals := []error{
nil,
io.EOF,
errors.New("EOF"),
New("EOF"),
Errorf("EOF"),
Wrap(io.EOF, "EOF"),
Wrapf(io.EOF, "EOF%d", 2),
WithMessage(nil, "whoops"),
WithMessage(io.EOF, "whoops"),
WithStack(io.EOF),
WithStack(nil),
}
for i := range vals {
for j := range vals {
_ = vals[i] == vals[j] // mustn't panic
}
}
}
+205
View File
@@ -0,0 +1,205 @@
package errors_test
import (
"fmt"
"github.com/pkg/errors"
)
func ExampleNew() {
err := errors.New("whoops")
fmt.Println(err)
// Output: whoops
}
func ExampleNew_printf() {
err := errors.New("whoops")
fmt.Printf("%+v", err)
// Example output:
// whoops
// github.com/pkg/errors_test.ExampleNew_printf
// /home/dfc/src/github.com/pkg/errors/example_test.go:17
// testing.runExample
// /home/dfc/go/src/testing/example.go:114
// testing.RunExamples
// /home/dfc/go/src/testing/example.go:38
// testing.(*M).Run
// /home/dfc/go/src/testing/testing.go:744
// main.main
// /github.com/pkg/errors/_test/_testmain.go:106
// runtime.main
// /home/dfc/go/src/runtime/proc.go:183
// runtime.goexit
// /home/dfc/go/src/runtime/asm_amd64.s:2059
}
func ExampleWithMessage() {
cause := errors.New("whoops")
err := errors.WithMessage(cause, "oh noes")
fmt.Println(err)
// Output: oh noes: whoops
}
func ExampleWithStack() {
cause := errors.New("whoops")
err := errors.WithStack(cause)
fmt.Println(err)
// Output: whoops
}
func ExampleWithStack_printf() {
cause := errors.New("whoops")
err := errors.WithStack(cause)
fmt.Printf("%+v", err)
// Example Output:
// whoops
// github.com/pkg/errors_test.ExampleWithStack_printf
// /home/fabstu/go/src/github.com/pkg/errors/example_test.go:55
// testing.runExample
// /usr/lib/go/src/testing/example.go:114
// testing.RunExamples
// /usr/lib/go/src/testing/example.go:38
// testing.(*M).Run
// /usr/lib/go/src/testing/testing.go:744
// main.main
// github.com/pkg/errors/_test/_testmain.go:106
// runtime.main
// /usr/lib/go/src/runtime/proc.go:183
// runtime.goexit
// /usr/lib/go/src/runtime/asm_amd64.s:2086
// github.com/pkg/errors_test.ExampleWithStack_printf
// /home/fabstu/go/src/github.com/pkg/errors/example_test.go:56
// testing.runExample
// /usr/lib/go/src/testing/example.go:114
// testing.RunExamples
// /usr/lib/go/src/testing/example.go:38
// testing.(*M).Run
// /usr/lib/go/src/testing/testing.go:744
// main.main
// github.com/pkg/errors/_test/_testmain.go:106
// runtime.main
// /usr/lib/go/src/runtime/proc.go:183
// runtime.goexit
// /usr/lib/go/src/runtime/asm_amd64.s:2086
}
func ExampleWrap() {
cause := errors.New("whoops")
err := errors.Wrap(cause, "oh noes")
fmt.Println(err)
// Output: oh noes: whoops
}
func fn() error {
e1 := errors.New("error")
e2 := errors.Wrap(e1, "inner")
e3 := errors.Wrap(e2, "middle")
return errors.Wrap(e3, "outer")
}
func ExampleCause() {
err := fn()
fmt.Println(err)
fmt.Println(errors.Cause(err))
// Output: outer: middle: inner: error
// error
}
func ExampleWrap_extended() {
err := fn()
fmt.Printf("%+v\n", err)
// Example output:
// error
// github.com/pkg/errors_test.fn
// /home/dfc/src/github.com/pkg/errors/example_test.go:47
// github.com/pkg/errors_test.ExampleCause_printf
// /home/dfc/src/github.com/pkg/errors/example_test.go:63
// testing.runExample
// /home/dfc/go/src/testing/example.go:114
// testing.RunExamples
// /home/dfc/go/src/testing/example.go:38
// testing.(*M).Run
// /home/dfc/go/src/testing/testing.go:744
// main.main
// /github.com/pkg/errors/_test/_testmain.go:104
// runtime.main
// /home/dfc/go/src/runtime/proc.go:183
// runtime.goexit
// /home/dfc/go/src/runtime/asm_amd64.s:2059
// github.com/pkg/errors_test.fn
// /home/dfc/src/github.com/pkg/errors/example_test.go:48: inner
// github.com/pkg/errors_test.fn
// /home/dfc/src/github.com/pkg/errors/example_test.go:49: middle
// github.com/pkg/errors_test.fn
// /home/dfc/src/github.com/pkg/errors/example_test.go:50: outer
}
func ExampleWrapf() {
cause := errors.New("whoops")
err := errors.Wrapf(cause, "oh noes #%d", 2)
fmt.Println(err)
// Output: oh noes #2: whoops
}
func ExampleErrorf_extended() {
err := errors.Errorf("whoops: %s", "foo")
fmt.Printf("%+v", err)
// Example output:
// whoops: foo
// github.com/pkg/errors_test.ExampleErrorf
// /home/dfc/src/github.com/pkg/errors/example_test.go:101
// testing.runExample
// /home/dfc/go/src/testing/example.go:114
// testing.RunExamples
// /home/dfc/go/src/testing/example.go:38
// testing.(*M).Run
// /home/dfc/go/src/testing/testing.go:744
// main.main
// /github.com/pkg/errors/_test/_testmain.go:102
// runtime.main
// /home/dfc/go/src/runtime/proc.go:183
// runtime.goexit
// /home/dfc/go/src/runtime/asm_amd64.s:2059
}
func Example_stackTrace() {
type stackTracer interface {
StackTrace() errors.StackTrace
}
err, ok := errors.Cause(fn()).(stackTracer)
if !ok {
panic("oops, err does not implement stackTracer")
}
st := err.StackTrace()
fmt.Printf("%+v", st[0:2]) // top two frames
// Example output:
// github.com/pkg/errors_test.fn
// /home/dfc/src/github.com/pkg/errors/example_test.go:47
// github.com/pkg/errors_test.Example_stackTrace
// /home/dfc/src/github.com/pkg/errors/example_test.go:127
}
func ExampleCause_printf() {
err := errors.Wrap(func() error {
return func() error {
return errors.Errorf("hello %s", fmt.Sprintf("world"))
}()
}(), "failed")
fmt.Printf("%v", err)
// Output: failed: hello world
}
+535
View File
@@ -0,0 +1,535 @@
package errors
import (
"errors"
"fmt"
"io"
"regexp"
"strings"
"testing"
)
func TestFormatNew(t *testing.T) {
tests := []struct {
error
format string
want string
}{{
New("error"),
"%s",
"error",
}, {
New("error"),
"%v",
"error",
}, {
New("error"),
"%+v",
"error\n" +
"github.com/pkg/errors.TestFormatNew\n" +
"\t.+/github.com/pkg/errors/format_test.go:26",
}, {
New("error"),
"%q",
`"error"`,
}}
for i, tt := range tests {
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
}
}
func TestFormatErrorf(t *testing.T) {
tests := []struct {
error
format string
want string
}{{
Errorf("%s", "error"),
"%s",
"error",
}, {
Errorf("%s", "error"),
"%v",
"error",
}, {
Errorf("%s", "error"),
"%+v",
"error\n" +
"github.com/pkg/errors.TestFormatErrorf\n" +
"\t.+/github.com/pkg/errors/format_test.go:56",
}}
for i, tt := range tests {
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
}
}
func TestFormatWrap(t *testing.T) {
tests := []struct {
error
format string
want string
}{{
Wrap(New("error"), "error2"),
"%s",
"error2: error",
}, {
Wrap(New("error"), "error2"),
"%v",
"error2: error",
}, {
Wrap(New("error"), "error2"),
"%+v",
"error\n" +
"github.com/pkg/errors.TestFormatWrap\n" +
"\t.+/github.com/pkg/errors/format_test.go:82",
}, {
Wrap(io.EOF, "error"),
"%s",
"error: EOF",
}, {
Wrap(io.EOF, "error"),
"%v",
"error: EOF",
}, {
Wrap(io.EOF, "error"),
"%+v",
"EOF\n" +
"error\n" +
"github.com/pkg/errors.TestFormatWrap\n" +
"\t.+/github.com/pkg/errors/format_test.go:96",
}, {
Wrap(Wrap(io.EOF, "error1"), "error2"),
"%+v",
"EOF\n" +
"error1\n" +
"github.com/pkg/errors.TestFormatWrap\n" +
"\t.+/github.com/pkg/errors/format_test.go:103\n",
}, {
Wrap(New("error with space"), "context"),
"%q",
`"context: error with space"`,
}}
for i, tt := range tests {
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
}
}
func TestFormatWrapf(t *testing.T) {
tests := []struct {
error
format string
want string
}{{
Wrapf(io.EOF, "error%d", 2),
"%s",
"error2: EOF",
}, {
Wrapf(io.EOF, "error%d", 2),
"%v",
"error2: EOF",
}, {
Wrapf(io.EOF, "error%d", 2),
"%+v",
"EOF\n" +
"error2\n" +
"github.com/pkg/errors.TestFormatWrapf\n" +
"\t.+/github.com/pkg/errors/format_test.go:134",
}, {
Wrapf(New("error"), "error%d", 2),
"%s",
"error2: error",
}, {
Wrapf(New("error"), "error%d", 2),
"%v",
"error2: error",
}, {
Wrapf(New("error"), "error%d", 2),
"%+v",
"error\n" +
"github.com/pkg/errors.TestFormatWrapf\n" +
"\t.+/github.com/pkg/errors/format_test.go:149",
}}
for i, tt := range tests {
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
}
}
func TestFormatWithStack(t *testing.T) {
tests := []struct {
error
format string
want []string
}{{
WithStack(io.EOF),
"%s",
[]string{"EOF"},
}, {
WithStack(io.EOF),
"%v",
[]string{"EOF"},
}, {
WithStack(io.EOF),
"%+v",
[]string{"EOF",
"github.com/pkg/errors.TestFormatWithStack\n" +
"\t.+/github.com/pkg/errors/format_test.go:175"},
}, {
WithStack(New("error")),
"%s",
[]string{"error"},
}, {
WithStack(New("error")),
"%v",
[]string{"error"},
}, {
WithStack(New("error")),
"%+v",
[]string{"error",
"github.com/pkg/errors.TestFormatWithStack\n" +
"\t.+/github.com/pkg/errors/format_test.go:189",
"github.com/pkg/errors.TestFormatWithStack\n" +
"\t.+/github.com/pkg/errors/format_test.go:189"},
}, {
WithStack(WithStack(io.EOF)),
"%+v",
[]string{"EOF",
"github.com/pkg/errors.TestFormatWithStack\n" +
"\t.+/github.com/pkg/errors/format_test.go:197",
"github.com/pkg/errors.TestFormatWithStack\n" +
"\t.+/github.com/pkg/errors/format_test.go:197"},
}, {
WithStack(WithStack(Wrapf(io.EOF, "message"))),
"%+v",
[]string{"EOF",
"message",
"github.com/pkg/errors.TestFormatWithStack\n" +
"\t.+/github.com/pkg/errors/format_test.go:205",
"github.com/pkg/errors.TestFormatWithStack\n" +
"\t.+/github.com/pkg/errors/format_test.go:205",
"github.com/pkg/errors.TestFormatWithStack\n" +
"\t.+/github.com/pkg/errors/format_test.go:205"},
}, {
WithStack(Errorf("error%d", 1)),
"%+v",
[]string{"error1",
"github.com/pkg/errors.TestFormatWithStack\n" +
"\t.+/github.com/pkg/errors/format_test.go:216",
"github.com/pkg/errors.TestFormatWithStack\n" +
"\t.+/github.com/pkg/errors/format_test.go:216"},
}}
for i, tt := range tests {
testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true)
}
}
func TestFormatWithMessage(t *testing.T) {
tests := []struct {
error
format string
want []string
}{{
WithMessage(New("error"), "error2"),
"%s",
[]string{"error2: error"},
}, {
WithMessage(New("error"), "error2"),
"%v",
[]string{"error2: error"},
}, {
WithMessage(New("error"), "error2"),
"%+v",
[]string{
"error",
"github.com/pkg/errors.TestFormatWithMessage\n" +
"\t.+/github.com/pkg/errors/format_test.go:244",
"error2"},
}, {
WithMessage(io.EOF, "addition1"),
"%s",
[]string{"addition1: EOF"},
}, {
WithMessage(io.EOF, "addition1"),
"%v",
[]string{"addition1: EOF"},
}, {
WithMessage(io.EOF, "addition1"),
"%+v",
[]string{"EOF", "addition1"},
}, {
WithMessage(WithMessage(io.EOF, "addition1"), "addition2"),
"%v",
[]string{"addition2: addition1: EOF"},
}, {
WithMessage(WithMessage(io.EOF, "addition1"), "addition2"),
"%+v",
[]string{"EOF", "addition1", "addition2"},
}, {
Wrap(WithMessage(io.EOF, "error1"), "error2"),
"%+v",
[]string{"EOF", "error1", "error2",
"github.com/pkg/errors.TestFormatWithMessage\n" +
"\t.+/github.com/pkg/errors/format_test.go:272"},
}, {
WithMessage(Errorf("error%d", 1), "error2"),
"%+v",
[]string{"error1",
"github.com/pkg/errors.TestFormatWithMessage\n" +
"\t.+/github.com/pkg/errors/format_test.go:278",
"error2"},
}, {
WithMessage(WithStack(io.EOF), "error"),
"%+v",
[]string{
"EOF",
"github.com/pkg/errors.TestFormatWithMessage\n" +
"\t.+/github.com/pkg/errors/format_test.go:285",
"error"},
}, {
WithMessage(Wrap(WithStack(io.EOF), "inside-error"), "outside-error"),
"%+v",
[]string{
"EOF",
"github.com/pkg/errors.TestFormatWithMessage\n" +
"\t.+/github.com/pkg/errors/format_test.go:293",
"inside-error",
"github.com/pkg/errors.TestFormatWithMessage\n" +
"\t.+/github.com/pkg/errors/format_test.go:293",
"outside-error"},
}}
for i, tt := range tests {
testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true)
}
}
func TestFormatGeneric(t *testing.T) {
starts := []struct {
err error
want []string
}{
{New("new-error"), []string{
"new-error",
"github.com/pkg/errors.TestFormatGeneric\n" +
"\t.+/github.com/pkg/errors/format_test.go:315"},
}, {Errorf("errorf-error"), []string{
"errorf-error",
"github.com/pkg/errors.TestFormatGeneric\n" +
"\t.+/github.com/pkg/errors/format_test.go:319"},
}, {errors.New("errors-new-error"), []string{
"errors-new-error"},
},
}
wrappers := []wrapper{
{
func(err error) error { return WithMessage(err, "with-message") },
[]string{"with-message"},
}, {
func(err error) error { return WithStack(err) },
[]string{
"github.com/pkg/errors.(func·002|TestFormatGeneric.func2)\n\t" +
".+/github.com/pkg/errors/format_test.go:333",
},
}, {
func(err error) error { return Wrap(err, "wrap-error") },
[]string{
"wrap-error",
"github.com/pkg/errors.(func·003|TestFormatGeneric.func3)\n\t" +
".+/github.com/pkg/errors/format_test.go:339",
},
}, {
func(err error) error { return Wrapf(err, "wrapf-error%d", 1) },
[]string{
"wrapf-error1",
"github.com/pkg/errors.(func·004|TestFormatGeneric.func4)\n\t" +
".+/github.com/pkg/errors/format_test.go:346",
},
},
}
for s := range starts {
err := starts[s].err
want := starts[s].want
testFormatCompleteCompare(t, s, err, "%+v", want, false)
testGenericRecursive(t, err, want, wrappers, 3)
}
}
func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) {
got := fmt.Sprintf(format, arg)
gotLines := strings.SplitN(got, "\n", -1)
wantLines := strings.SplitN(want, "\n", -1)
if len(wantLines) > len(gotLines) {
t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want)
return
}
for i, w := range wantLines {
match, err := regexp.MatchString(w, gotLines[i])
if err != nil {
t.Fatal(err)
}
if !match {
t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want)
}
}
}
var stackLineR = regexp.MustCompile(`\.`)
// parseBlocks parses input into a slice, where:
// - incase entry contains a newline, its a stacktrace
// - incase entry contains no newline, its a solo line.
//
// Detecting stack boundaries only works incase the WithStack-calls are
// to be found on the same line, thats why it is optionally here.
//
// Example use:
//
// for _, e := range blocks {
// if strings.ContainsAny(e, "\n") {
// // Match as stack
// } else {
// // Match as line
// }
// }
//
func parseBlocks(input string, detectStackboundaries bool) ([]string, error) {
var blocks []string
stack := ""
wasStack := false
lines := map[string]bool{} // already found lines
for _, l := range strings.Split(input, "\n") {
isStackLine := stackLineR.MatchString(l)
switch {
case !isStackLine && wasStack:
blocks = append(blocks, stack, l)
stack = ""
lines = map[string]bool{}
case isStackLine:
if wasStack {
// Detecting two stacks after another, possible cause lines match in
// our tests due to WithStack(WithStack(io.EOF)) on same line.
if detectStackboundaries {
if lines[l] {
if len(stack) == 0 {
return nil, errors.New("len of block must not be zero here")
}
blocks = append(blocks, stack)
stack = l
lines = map[string]bool{l: true}
continue
}
}
stack = stack + "\n" + l
} else {
stack = l
}
lines[l] = true
case !isStackLine && !wasStack:
blocks = append(blocks, l)
default:
return nil, errors.New("must not happen")
}
wasStack = isStackLine
}
// Use up stack
if stack != "" {
blocks = append(blocks, stack)
}
return blocks, nil
}
func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) {
gotStr := fmt.Sprintf(format, arg)
got, err := parseBlocks(gotStr, detectStackBoundaries)
if err != nil {
t.Fatal(err)
}
if len(got) != len(want) {
t.Fatalf("test %d: fmt.Sprintf(%s, err) -> wrong number of blocks: got(%d) want(%d)\n got: %s\nwant: %s\ngotStr: %q",
n+1, format, len(got), len(want), prettyBlocks(got), prettyBlocks(want), gotStr)
}
for i := range got {
if strings.ContainsAny(want[i], "\n") {
// Match as stack
match, err := regexp.MatchString(want[i], got[i])
if err != nil {
t.Fatal(err)
}
if !match {
t.Fatalf("test %d: block %d: fmt.Sprintf(%q, err):\ngot:\n%q\nwant:\n%q\nall-got:\n%s\nall-want:\n%s\n",
n+1, i+1, format, got[i], want[i], prettyBlocks(got), prettyBlocks(want))
}
} else {
// Match as message
if got[i] != want[i] {
t.Fatalf("test %d: fmt.Sprintf(%s, err) at block %d got != want:\n got: %q\nwant: %q", n+1, format, i+1, got[i], want[i])
}
}
}
}
type wrapper struct {
wrap func(err error) error
want []string
}
func prettyBlocks(blocks []string, prefix ...string) string {
var out []string
for _, b := range blocks {
out = append(out, fmt.Sprintf("%v", b))
}
return " " + strings.Join(out, "\n ")
}
func testGenericRecursive(t *testing.T, beforeErr error, beforeWant []string, list []wrapper, maxDepth int) {
if len(beforeWant) == 0 {
panic("beforeWant must not be empty")
}
for _, w := range list {
if len(w.want) == 0 {
panic("want must not be empty")
}
err := w.wrap(beforeErr)
// Copy required cause append(beforeWant, ..) modified beforeWant subtly.
beforeCopy := make([]string, len(beforeWant))
copy(beforeCopy, beforeWant)
beforeWant := beforeCopy
last := len(beforeWant) - 1
var want []string
// Merge two stacks behind each other.
if strings.ContainsAny(beforeWant[last], "\n") && strings.ContainsAny(w.want[0], "\n") {
want = append(beforeWant[:last], append([]string{beforeWant[last] + "((?s).*)" + w.want[0]}, w.want[1:]...)...)
} else {
want = append(beforeWant, w.want...)
}
testFormatCompleteCompare(t, maxDepth, err, "%+v", want, false)
if maxDepth > 0 {
testGenericRecursive(t, err, want, list, maxDepth-1)
}
}
}
+178
View File
@@ -0,0 +1,178 @@
package errors
import (
"fmt"
"io"
"path"
"runtime"
"strings"
)
// Frame represents a program counter inside a stack frame.
type Frame uintptr
// pc returns the program counter for this frame;
// multiple frames may have the same PC value.
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
// file returns the full path to the file that contains the
// function for this Frame's pc.
func (f Frame) file() string {
fn := runtime.FuncForPC(f.pc())
if fn == nil {
return "unknown"
}
file, _ := fn.FileLine(f.pc())
return file
}
// line returns the line number of source code of the
// function for this Frame's pc.
func (f Frame) line() int {
fn := runtime.FuncForPC(f.pc())
if fn == nil {
return 0
}
_, line := fn.FileLine(f.pc())
return line
}
// Format formats the frame according to the fmt.Formatter interface.
//
// %s source file
// %d source line
// %n function name
// %v equivalent to %s:%d
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
// %+s path of source file relative to the compile time GOPATH
// %+v equivalent to %+s:%d
func (f Frame) Format(s fmt.State, verb rune) {
switch verb {
case 's':
switch {
case s.Flag('+'):
pc := f.pc()
fn := runtime.FuncForPC(pc)
if fn == nil {
io.WriteString(s, "unknown")
} else {
file, _ := fn.FileLine(pc)
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
}
default:
io.WriteString(s, path.Base(f.file()))
}
case 'd':
fmt.Fprintf(s, "%d", f.line())
case 'n':
name := runtime.FuncForPC(f.pc()).Name()
io.WriteString(s, funcname(name))
case 'v':
f.Format(s, 's')
io.WriteString(s, ":")
f.Format(s, 'd')
}
}
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
type StackTrace []Frame
func (st StackTrace) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
switch {
case s.Flag('+'):
for _, f := range st {
fmt.Fprintf(s, "\n%+v", f)
}
case s.Flag('#'):
fmt.Fprintf(s, "%#v", []Frame(st))
default:
fmt.Fprintf(s, "%v", []Frame(st))
}
case 's':
fmt.Fprintf(s, "%s", []Frame(st))
}
}
// stack represents a stack of program counters.
type stack []uintptr
func (s *stack) Format(st fmt.State, verb rune) {
switch verb {
case 'v':
switch {
case st.Flag('+'):
for _, pc := range *s {
f := Frame(pc)
fmt.Fprintf(st, "\n%+v", f)
}
}
}
}
func (s *stack) StackTrace() StackTrace {
f := make([]Frame, len(*s))
for i := 0; i < len(f); i++ {
f[i] = Frame((*s)[i])
}
return f
}
func callers() *stack {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
var st stack = pcs[0:n]
return &st
}
// funcname removes the path prefix component of a function's name reported by func.Name().
func funcname(name string) string {
i := strings.LastIndex(name, "/")
name = name[i+1:]
i = strings.Index(name, ".")
return name[i+1:]
}
func trimGOPATH(name, file string) string {
// Here we want to get the source file path relative to the compile time
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
// GOPATH at runtime, but we can infer the number of path segments in the
// GOPATH. We note that fn.Name() returns the function name qualified by
// the import path, which does not include the GOPATH. Thus we can trim
// segments from the beginning of the file path until the number of path
// separators remaining is one more than the number of path separators in
// the function name. For example, given:
//
// GOPATH /home/user
// file /home/user/src/pkg/sub/file.go
// fn.Name() pkg/sub.Type.Method
//
// We want to produce:
//
// pkg/sub/file.go
//
// From this we can easily see that fn.Name() has one less path separator
// than our desired output. We count separators from the end of the file
// path until it finds two more than in the function name and then move
// one character forward to preserve the initial path segment without a
// leading separator.
const sep = "/"
goal := strings.Count(name, sep) + 2
i := len(file)
for n := 0; n < goal; n++ {
i = strings.LastIndex(file[:i], sep)
if i == -1 {
// not enough separators found, set i so that the slice expression
// below leaves file unmodified
i = -len(sep)
break
}
}
// get back to 0 or trim the leading separator
file = file[i+len(sep):]
return file
}
+292
View File
@@ -0,0 +1,292 @@
package errors
import (
"fmt"
"runtime"
"testing"
)
var initpc, _, _, _ = runtime.Caller(0)
func TestFrameLine(t *testing.T) {
var tests = []struct {
Frame
want int
}{{
Frame(initpc),
9,
}, {
func() Frame {
var pc, _, _, _ = runtime.Caller(0)
return Frame(pc)
}(),
20,
}, {
func() Frame {
var pc, _, _, _ = runtime.Caller(1)
return Frame(pc)
}(),
28,
}, {
Frame(0), // invalid PC
0,
}}
for _, tt := range tests {
got := tt.Frame.line()
want := tt.want
if want != got {
t.Errorf("Frame(%v): want: %v, got: %v", uintptr(tt.Frame), want, got)
}
}
}
type X struct{}
func (x X) val() Frame {
var pc, _, _, _ = runtime.Caller(0)
return Frame(pc)
}
func (x *X) ptr() Frame {
var pc, _, _, _ = runtime.Caller(0)
return Frame(pc)
}
func TestFrameFormat(t *testing.T) {
var tests = []struct {
Frame
format string
want string
}{{
Frame(initpc),
"%s",
"stack_test.go",
}, {
Frame(initpc),
"%+s",
"github.com/pkg/errors.init\n" +
"\t.+/github.com/pkg/errors/stack_test.go",
}, {
Frame(0),
"%s",
"unknown",
}, {
Frame(0),
"%+s",
"unknown",
}, {
Frame(initpc),
"%d",
"9",
}, {
Frame(0),
"%d",
"0",
}, {
Frame(initpc),
"%n",
"init",
}, {
func() Frame {
var x X
return x.ptr()
}(),
"%n",
`\(\*X\).ptr`,
}, {
func() Frame {
var x X
return x.val()
}(),
"%n",
"X.val",
}, {
Frame(0),
"%n",
"",
}, {
Frame(initpc),
"%v",
"stack_test.go:9",
}, {
Frame(initpc),
"%+v",
"github.com/pkg/errors.init\n" +
"\t.+/github.com/pkg/errors/stack_test.go:9",
}, {
Frame(0),
"%v",
"unknown:0",
}}
for i, tt := range tests {
testFormatRegexp(t, i, tt.Frame, tt.format, tt.want)
}
}
func TestFuncname(t *testing.T) {
tests := []struct {
name, want string
}{
{"", ""},
{"runtime.main", "main"},
{"github.com/pkg/errors.funcname", "funcname"},
{"funcname", "funcname"},
{"io.copyBuffer", "copyBuffer"},
{"main.(*R).Write", "(*R).Write"},
}
for _, tt := range tests {
got := funcname(tt.name)
want := tt.want
if got != want {
t.Errorf("funcname(%q): want: %q, got %q", tt.name, want, got)
}
}
}
func TestTrimGOPATH(t *testing.T) {
var tests = []struct {
Frame
want string
}{{
Frame(initpc),
"github.com/pkg/errors/stack_test.go",
}}
for i, tt := range tests {
pc := tt.Frame.pc()
fn := runtime.FuncForPC(pc)
file, _ := fn.FileLine(pc)
got := trimGOPATH(fn.Name(), file)
testFormatRegexp(t, i, got, "%s", tt.want)
}
}
func TestStackTrace(t *testing.T) {
tests := []struct {
err error
want []string
}{{
New("ooh"), []string{
"github.com/pkg/errors.TestStackTrace\n" +
"\t.+/github.com/pkg/errors/stack_test.go:172",
},
}, {
Wrap(New("ooh"), "ahh"), []string{
"github.com/pkg/errors.TestStackTrace\n" +
"\t.+/github.com/pkg/errors/stack_test.go:177", // this is the stack of Wrap, not New
},
}, {
Cause(Wrap(New("ooh"), "ahh")), []string{
"github.com/pkg/errors.TestStackTrace\n" +
"\t.+/github.com/pkg/errors/stack_test.go:182", // this is the stack of New
},
}, {
func() error { return New("ooh") }(), []string{
`github.com/pkg/errors.(func·009|TestStackTrace.func1)` +
"\n\t.+/github.com/pkg/errors/stack_test.go:187", // this is the stack of New
"github.com/pkg/errors.TestStackTrace\n" +
"\t.+/github.com/pkg/errors/stack_test.go:187", // this is the stack of New's caller
},
}, {
Cause(func() error {
return func() error {
return Errorf("hello %s", fmt.Sprintf("world"))
}()
}()), []string{
`github.com/pkg/errors.(func·010|TestStackTrace.func2.1)` +
"\n\t.+/github.com/pkg/errors/stack_test.go:196", // this is the stack of Errorf
`github.com/pkg/errors.(func·011|TestStackTrace.func2)` +
"\n\t.+/github.com/pkg/errors/stack_test.go:197", // this is the stack of Errorf's caller
"github.com/pkg/errors.TestStackTrace\n" +
"\t.+/github.com/pkg/errors/stack_test.go:198", // this is the stack of Errorf's caller's caller
},
}}
for i, tt := range tests {
x, ok := tt.err.(interface {
StackTrace() StackTrace
})
if !ok {
t.Errorf("expected %#v to implement StackTrace() StackTrace", tt.err)
continue
}
st := x.StackTrace()
for j, want := range tt.want {
testFormatRegexp(t, i, st[j], "%+v", want)
}
}
}
func stackTrace() StackTrace {
const depth = 8
var pcs [depth]uintptr
n := runtime.Callers(1, pcs[:])
var st stack = pcs[0:n]
return st.StackTrace()
}
func TestStackTraceFormat(t *testing.T) {
tests := []struct {
StackTrace
format string
want string
}{{
nil,
"%s",
`\[\]`,
}, {
nil,
"%v",
`\[\]`,
}, {
nil,
"%+v",
"",
}, {
nil,
"%#v",
`\[\]errors.Frame\(nil\)`,
}, {
make(StackTrace, 0),
"%s",
`\[\]`,
}, {
make(StackTrace, 0),
"%v",
`\[\]`,
}, {
make(StackTrace, 0),
"%+v",
"",
}, {
make(StackTrace, 0),
"%#v",
`\[\]errors.Frame{}`,
}, {
stackTrace()[:2],
"%s",
`\[stack_test.go stack_test.go\]`,
}, {
stackTrace()[:2],
"%v",
`\[stack_test.go:225 stack_test.go:272\]`,
}, {
stackTrace()[:2],
"%+v",
"\n" +
"github.com/pkg/errors.stackTrace\n" +
"\t.+/github.com/pkg/errors/stack_test.go:225\n" +
"github.com/pkg/errors.TestStackTraceFormat\n" +
"\t.+/github.com/pkg/errors/stack_test.go:276",
}, {
stackTrace()[:2],
"%#v",
`\[\]errors.Frame{stack_test.go:225, stack_test.go:284}`,
}}
for i, tt := range tests {
testFormatRegexp(t, i, tt.StackTrace, tt.format, tt.want)
}
}
+2
View File
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 120
+2
View File
@@ -0,0 +1,2 @@
*.coverprofile
node_modules/
+27
View File
@@ -0,0 +1,27 @@
language: go
sudo: false
dist: trusty
osx_image: xcode8.3
go: 1.8.x
os:
- linux
- osx
cache:
directories:
- node_modules
before_script:
- go get github.com/urfave/gfmrun/... || true
- go get golang.org/x/tools/cmd/goimports
- if [ ! -f node_modules/.bin/markdown-toc ] ; then
npm install markdown-toc ;
fi
script:
- ./runtests gen
- ./runtests vet
- ./runtests test
- ./runtests gfmrun
- ./runtests toc
+435
View File
@@ -0,0 +1,435 @@
# Change Log
**ATTN**: This project uses [semantic versioning](http://semver.org/).
## [Unreleased]
## 1.20.0 - 2017-08-10
### Fixed
* `HandleExitCoder` is now correctly iterates over all errors in
a `MultiError`. The exit code is the exit code of the last error or `1` if
there are no `ExitCoder`s in the `MultiError`.
* Fixed YAML file loading on Windows (previously would fail validate the file path)
* Subcommand `Usage`, `Description`, `ArgsUsage`, `OnUsageError` correctly
propogated
* `ErrWriter` is now passed downwards through command structure to avoid the
need to redefine it
* Pass `Command` context into `OnUsageError` rather than parent context so that
all fields are avaiable
* Errors occuring in `Before` funcs are no longer double printed
* Use `UsageText` in the help templates for commands and subcommands if
defined; otherwise build the usage as before (was previously ignoring this
field)
* `IsSet` and `GlobalIsSet` now correctly return whether a flag is set if
a program calls `Set` or `GlobalSet` directly after flag parsing (would
previously only return `true` if the flag was set during parsing)
### Changed
* No longer exit the program on command/subcommand error if the error raised is
not an `OsExiter`. This exiting behavior was introduced in 1.19.0, but was
determined to be a regression in functionality. See [the
PR](https://github.com/urfave/cli/pull/595) for discussion.
### Added
* `CommandsByName` type was added to make it easy to sort `Command`s by name,
alphabetically
* `altsrc` now handles loading of string and int arrays from TOML
* Support for definition of custom help templates for `App` via
`CustomAppHelpTemplate`
* Support for arbitrary key/value fields on `App` to be used with
`CustomAppHelpTemplate` via `ExtraInfo`
* `HelpFlag`, `VersionFlag`, and `BashCompletionFlag` changed to explictly be
`cli.Flag`s allowing for the use of custom flags satisfying the `cli.Flag`
interface to be used.
## [1.19.1] - 2016-11-21
### Fixed
- Fixes regression introduced in 1.19.0 where using an `ActionFunc` as
the `Action` for a command would cause it to error rather than calling the
function. Should not have a affected declarative cases using `func(c
*cli.Context) err)`.
- Shell completion now handles the case where the user specifies
`--generate-bash-completion` immediately after a flag that takes an argument.
Previously it call the application with `--generate-bash-completion` as the
flag value.
## [1.19.0] - 2016-11-19
### Added
- `FlagsByName` was added to make it easy to sort flags (e.g. `sort.Sort(cli.FlagsByName(app.Flags))`)
- A `Description` field was added to `App` for a more detailed description of
the application (similar to the existing `Description` field on `Command`)
- Flag type code generation via `go generate`
- Write to stderr and exit 1 if action returns non-nil error
- Added support for TOML to the `altsrc` loader
- `SkipArgReorder` was added to allow users to skip the argument reordering.
This is useful if you want to consider all "flags" after an argument as
arguments rather than flags (the default behavior of the stdlib `flag`
library). This is backported functionality from the [removal of the flag
reordering](https://github.com/urfave/cli/pull/398) in the unreleased version
2
- For formatted errors (those implementing `ErrorFormatter`), the errors will
be formatted during output. Compatible with `pkg/errors`.
### Changed
- Raise minimum tested/supported Go version to 1.2+
### Fixed
- Consider empty environment variables as set (previously environment variables
with the equivalent of `""` would be skipped rather than their value used).
- Return an error if the value in a given environment variable cannot be parsed
as the flag type. Previously these errors were silently swallowed.
- Print full error when an invalid flag is specified (which includes the invalid flag)
- `App.Writer` defaults to `stdout` when `nil`
- If no action is specified on a command or app, the help is now printed instead of `panic`ing
- `App.Metadata` is initialized automatically now (previously was `nil` unless initialized)
- Correctly show help message if `-h` is provided to a subcommand
- `context.(Global)IsSet` now respects environment variables. Previously it
would return `false` if a flag was specified in the environment rather than
as an argument
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
- `altsrc`s import paths were updated to use `gopkg.in/urfave/cli.v1`. This
fixes issues that occurred when `gopkg.in/urfave/cli.v1` was imported as well
as `altsrc` where Go would complain that the types didn't match
## [1.18.1] - 2016-08-28
### Fixed
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user (backported)
## [1.18.0] - 2016-06-27
### Added
- `./runtests` test runner with coverage tracking by default
- testing on OS X
- testing on Windows
- `UintFlag`, `Uint64Flag`, and `Int64Flag` types and supporting code
### Changed
- Use spaces for alignment in help/usage output instead of tabs, making the
output alignment consistent regardless of tab width
### Fixed
- Printing of command aliases in help text
- Printing of visible flags for both struct and struct pointer flags
- Display the `help` subcommand when using `CommandCategories`
- No longer swallows `panic`s that occur within the `Action`s themselves when
detecting the signature of the `Action` field
## [1.17.1] - 2016-08-28
### Fixed
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
## [1.17.0] - 2016-05-09
### Added
- Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc`
- `context.GlobalBoolT` was added as an analogue to `context.GlobalBool`
- Support for hiding commands by setting `Hidden: true` -- this will hide the
commands in help output
### Changed
- `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer
quoted in help text output.
- All flag types now include `(default: {value})` strings following usage when a
default value can be (reasonably) detected.
- `IntSliceFlag` and `StringSliceFlag` usage strings are now more consistent
with non-slice flag types
- Apps now exit with a code of 3 if an unknown subcommand is specified
(previously they printed "No help topic for...", but still exited 0. This
makes it easier to script around apps built using `cli` since they can trust
that a 0 exit code indicated a successful execution.
- cleanups based on [Go Report Card
feedback](https://goreportcard.com/report/github.com/urfave/cli)
## [1.16.1] - 2016-08-28
### Fixed
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
## [1.16.0] - 2016-05-02
### Added
- `Hidden` field on all flag struct types to omit from generated help text
### Changed
- `BashCompletionFlag` (`--enable-bash-completion`) is now omitted from
generated help text via the `Hidden` field
### Fixed
- handling of error values in `HandleAction` and `HandleExitCoder`
## [1.15.0] - 2016-04-30
### Added
- This file!
- Support for placeholders in flag usage strings
- `App.Metadata` map for arbitrary data/state management
- `Set` and `GlobalSet` methods on `*cli.Context` for altering values after
parsing.
- Support for nested lookup of dot-delimited keys in structures loaded from
YAML.
### Changed
- The `App.Action` and `Command.Action` now prefer a return signature of
`func(*cli.Context) error`, as defined by `cli.ActionFunc`. If a non-nil
`error` is returned, there may be two outcomes:
- If the error fulfills `cli.ExitCoder`, then `os.Exit` will be called
automatically
- Else the error is bubbled up and returned from `App.Run`
- Specifying an `Action` with the legacy return signature of
`func(*cli.Context)` will produce a deprecation message to stderr
- Specifying an `Action` that is not a `func` type will produce a non-zero exit
from `App.Run`
- Specifying an `Action` func that has an invalid (input) signature will
produce a non-zero exit from `App.Run`
### Deprecated
- <a name="deprecated-cli-app-runandexitonerror"></a>
`cli.App.RunAndExitOnError`, which should now be done by returning an error
that fulfills `cli.ExitCoder` to `cli.App.Run`.
- <a name="deprecated-cli-app-action-signature"></a> the legacy signature for
`cli.App.Action` of `func(*cli.Context)`, which should now have a return
signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`.
### Fixed
- Added missing `*cli.Context.GlobalFloat64` method
## [1.14.0] - 2016-04-03 (backfilled 2016-04-25)
### Added
- Codebeat badge
- Support for categorization via `CategorizedHelp` and `Categories` on app.
### Changed
- Use `filepath.Base` instead of `path.Base` in `Name` and `HelpName`.
### Fixed
- Ensure version is not shown in help text when `HideVersion` set.
## [1.13.0] - 2016-03-06 (backfilled 2016-04-25)
### Added
- YAML file input support.
- `NArg` method on context.
## [1.12.0] - 2016-02-17 (backfilled 2016-04-25)
### Added
- Custom usage error handling.
- Custom text support in `USAGE` section of help output.
- Improved help messages for empty strings.
- AppVeyor CI configuration.
### Changed
- Removed `panic` from default help printer func.
- De-duping and optimizations.
### Fixed
- Correctly handle `Before`/`After` at command level when no subcommands.
- Case of literal `-` argument causing flag reordering.
- Environment variable hints on Windows.
- Docs updates.
## [1.11.1] - 2015-12-21 (backfilled 2016-04-25)
### Changed
- Use `path.Base` in `Name` and `HelpName`
- Export `GetName` on flag types.
### Fixed
- Flag parsing when skipping is enabled.
- Test output cleanup.
- Move completion check to account for empty input case.
## [1.11.0] - 2015-11-15 (backfilled 2016-04-25)
### Added
- Destination scan support for flags.
- Testing against `tip` in Travis CI config.
### Changed
- Go version in Travis CI config.
### Fixed
- Removed redundant tests.
- Use correct example naming in tests.
## [1.10.2] - 2015-10-29 (backfilled 2016-04-25)
### Fixed
- Remove unused var in bash completion.
## [1.10.1] - 2015-10-21 (backfilled 2016-04-25)
### Added
- Coverage and reference logos in README.
### Fixed
- Use specified values in help and version parsing.
- Only display app version and help message once.
## [1.10.0] - 2015-10-06 (backfilled 2016-04-25)
### Added
- More tests for existing functionality.
- `ArgsUsage` at app and command level for help text flexibility.
### Fixed
- Honor `HideHelp` and `HideVersion` in `App.Run`.
- Remove juvenile word from README.
## [1.9.0] - 2015-09-08 (backfilled 2016-04-25)
### Added
- `FullName` on command with accompanying help output update.
- Set default `$PROG` in bash completion.
### Changed
- Docs formatting.
### Fixed
- Removed self-referential imports in tests.
## [1.8.0] - 2015-06-30 (backfilled 2016-04-25)
### Added
- Support for `Copyright` at app level.
- `Parent` func at context level to walk up context lineage.
### Fixed
- Global flag processing at top level.
## [1.7.1] - 2015-06-11 (backfilled 2016-04-25)
### Added
- Aggregate errors from `Before`/`After` funcs.
- Doc comments on flag structs.
- Include non-global flags when checking version and help.
- Travis CI config updates.
### Fixed
- Ensure slice type flags have non-nil values.
- Collect global flags from the full command hierarchy.
- Docs prose.
## [1.7.0] - 2015-05-03 (backfilled 2016-04-25)
### Changed
- `HelpPrinter` signature includes output writer.
### Fixed
- Specify go 1.1+ in docs.
- Set `Writer` when running command as app.
## [1.6.0] - 2015-03-23 (backfilled 2016-04-25)
### Added
- Multiple author support.
- `NumFlags` at context level.
- `Aliases` at command level.
### Deprecated
- `ShortName` at command level.
### Fixed
- Subcommand help output.
- Backward compatible support for deprecated `Author` and `Email` fields.
- Docs regarding `Names`/`Aliases`.
## [1.5.0] - 2015-02-20 (backfilled 2016-04-25)
### Added
- `After` hook func support at app and command level.
### Fixed
- Use parsed context when running command as subcommand.
- Docs prose.
## [1.4.1] - 2015-01-09 (backfilled 2016-04-25)
### Added
- Support for hiding `-h / --help` flags, but not `help` subcommand.
- Stop flag parsing after `--`.
### Fixed
- Help text for generic flags to specify single value.
- Use double quotes in output for defaults.
- Use `ParseInt` instead of `ParseUint` for int environment var values.
- Use `0` as base when parsing int environment var values.
## [1.4.0] - 2014-12-12 (backfilled 2016-04-25)
### Added
- Support for environment variable lookup "cascade".
- Support for `Stdout` on app for output redirection.
### Fixed
- Print command help instead of app help in `ShowCommandHelp`.
## [1.3.1] - 2014-11-13 (backfilled 2016-04-25)
### Added
- Docs and example code updates.
### Changed
- Default `-v / --version` flag made optional.
## [1.3.0] - 2014-08-10 (backfilled 2016-04-25)
### Added
- `FlagNames` at context level.
- Exposed `VersionPrinter` var for more control over version output.
- Zsh completion hook.
- `AUTHOR` section in default app help template.
- Contribution guidelines.
- `DurationFlag` type.
## [1.2.0] - 2014-08-02
### Added
- Support for environment variable defaults on flags plus tests.
## [1.1.0] - 2014-07-15
### Added
- Bash completion.
- Optional hiding of built-in help command.
- Optional skipping of flag parsing at command level.
- `Author`, `Email`, and `Compiled` metadata on app.
- `Before` hook func support at app and command level.
- `CommandNotFound` func support at app level.
- Command reference available on context.
- `GenericFlag` type.
- `Float64Flag` type.
- `BoolTFlag` type.
- `IsSet` flag helper on context.
- More flag lookup funcs at context level.
- More tests &amp; docs.
### Changed
- Help template updates to account for presence/absence of flags.
- Separated subcommand help template.
- Exposed `HelpPrinter` var for more control over help output.
## [1.0.0] - 2013-11-01
### Added
- `help` flag in default app flag set and each command flag set.
- Custom handling of argument parsing errors.
- Command lookup by name at app level.
- `StringSliceFlag` type and supporting `StringSlice` type.
- `IntSliceFlag` type and supporting `IntSlice` type.
- Slice type flag lookups by name at context level.
- Export of app and command help functions.
- More tests &amp; docs.
## 0.1.0 - 2013-07-22
### Added
- Initial implementation.
[Unreleased]: https://github.com/urfave/cli/compare/v1.18.0...HEAD
[1.18.0]: https://github.com/urfave/cli/compare/v1.17.0...v1.18.0
[1.17.0]: https://github.com/urfave/cli/compare/v1.16.0...v1.17.0
[1.16.0]: https://github.com/urfave/cli/compare/v1.15.0...v1.16.0
[1.15.0]: https://github.com/urfave/cli/compare/v1.14.0...v1.15.0
[1.14.0]: https://github.com/urfave/cli/compare/v1.13.0...v1.14.0
[1.13.0]: https://github.com/urfave/cli/compare/v1.12.0...v1.13.0
[1.12.0]: https://github.com/urfave/cli/compare/v1.11.1...v1.12.0
[1.11.1]: https://github.com/urfave/cli/compare/v1.11.0...v1.11.1
[1.11.0]: https://github.com/urfave/cli/compare/v1.10.2...v1.11.0
[1.10.2]: https://github.com/urfave/cli/compare/v1.10.1...v1.10.2
[1.10.1]: https://github.com/urfave/cli/compare/v1.10.0...v1.10.1
[1.10.0]: https://github.com/urfave/cli/compare/v1.9.0...v1.10.0
[1.9.0]: https://github.com/urfave/cli/compare/v1.8.0...v1.9.0
[1.8.0]: https://github.com/urfave/cli/compare/v1.7.1...v1.8.0
[1.7.1]: https://github.com/urfave/cli/compare/v1.7.0...v1.7.1
[1.7.0]: https://github.com/urfave/cli/compare/v1.6.0...v1.7.0
[1.6.0]: https://github.com/urfave/cli/compare/v1.5.0...v1.6.0
[1.5.0]: https://github.com/urfave/cli/compare/v1.4.1...v1.5.0
[1.4.1]: https://github.com/urfave/cli/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/urfave/cli/compare/v1.3.1...v1.4.0
[1.3.1]: https://github.com/urfave/cli/compare/v1.3.0...v1.3.1
[1.3.0]: https://github.com/urfave/cli/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/urfave/cli/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/urfave/cli/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/urfave/cli/compare/v0.1.0...v1.0.0
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Jeremy Saenz & Contributors
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.
+1448
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
package altsrc
//go:generate python ../generate-flag-types altsrc -i ../flag-types.json -o flag_generated.go
+261
View File
@@ -0,0 +1,261 @@
package altsrc
import (
"fmt"
"strconv"
"strings"
"syscall"
"gopkg.in/urfave/cli.v1"
)
// FlagInputSourceExtension is an extension interface of cli.Flag that
// allows a value to be set on the existing parsed flags.
type FlagInputSourceExtension interface {
cli.Flag
ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error
}
// ApplyInputSourceValues iterates over all provided flags and
// executes ApplyInputSourceValue on flags implementing the
// FlagInputSourceExtension interface to initialize these flags
// to an alternate input source.
func ApplyInputSourceValues(context *cli.Context, inputSourceContext InputSourceContext, flags []cli.Flag) error {
for _, f := range flags {
inputSourceExtendedFlag, isType := f.(FlagInputSourceExtension)
if isType {
err := inputSourceExtendedFlag.ApplyInputSourceValue(context, inputSourceContext)
if err != nil {
return err
}
}
}
return nil
}
// InitInputSource is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new
// input source based on the func provided. If there is no error it will then apply the new input source to any flags
// that are supported by the input source
func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceContext, error)) cli.BeforeFunc {
return func(context *cli.Context) error {
inputSource, err := createInputSource()
if err != nil {
return fmt.Errorf("Unable to create input source: inner error: \n'%v'", err.Error())
}
return ApplyInputSourceValues(context, inputSource, flags)
}
}
// InitInputSourceWithContext is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new
// input source based on the func provided with potentially using existing cli.Context values to initialize itself. If there is
// no error it will then apply the new input source to any flags that are supported by the input source
func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(context *cli.Context) (InputSourceContext, error)) cli.BeforeFunc {
return func(context *cli.Context) error {
inputSource, err := createInputSource(context)
if err != nil {
return fmt.Errorf("Unable to create input source with context: inner error: \n'%v'", err.Error())
}
return ApplyInputSourceValues(context, inputSource, flags)
}
}
// ApplyInputSourceValue applies a generic value to the flagSet if required
func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) {
value, err := isc.Generic(f.GenericFlag.Name)
if err != nil {
return err
}
if value != nil {
eachName(f.Name, func(name string) {
f.set.Set(f.Name, value.String())
})
}
}
}
return nil
}
// ApplyInputSourceValue applies a StringSlice value to the flagSet if required
func (f *StringSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) {
value, err := isc.StringSlice(f.StringSliceFlag.Name)
if err != nil {
return err
}
if value != nil {
var sliceValue cli.StringSlice = value
eachName(f.Name, func(name string) {
underlyingFlag := f.set.Lookup(f.Name)
if underlyingFlag != nil {
underlyingFlag.Value = &sliceValue
}
})
}
}
}
return nil
}
// ApplyInputSourceValue applies a IntSlice value if required
func (f *IntSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) {
value, err := isc.IntSlice(f.IntSliceFlag.Name)
if err != nil {
return err
}
if value != nil {
var sliceValue cli.IntSlice = value
eachName(f.Name, func(name string) {
underlyingFlag := f.set.Lookup(f.Name)
if underlyingFlag != nil {
underlyingFlag.Value = &sliceValue
}
})
}
}
}
return nil
}
// ApplyInputSourceValue applies a Bool value to the flagSet if required
func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) {
value, err := isc.Bool(f.BoolFlag.Name)
if err != nil {
return err
}
if value {
eachName(f.Name, func(name string) {
f.set.Set(f.Name, strconv.FormatBool(value))
})
}
}
}
return nil
}
// ApplyInputSourceValue applies a BoolT value to the flagSet if required
func (f *BoolTFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) {
value, err := isc.BoolT(f.BoolTFlag.Name)
if err != nil {
return err
}
if !value {
eachName(f.Name, func(name string) {
f.set.Set(f.Name, strconv.FormatBool(value))
})
}
}
}
return nil
}
// ApplyInputSourceValue applies a String value to the flagSet if required
func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) {
value, err := isc.String(f.StringFlag.Name)
if err != nil {
return err
}
if value != "" {
eachName(f.Name, func(name string) {
f.set.Set(f.Name, value)
})
}
}
}
return nil
}
// ApplyInputSourceValue applies a int value to the flagSet if required
func (f *IntFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) {
value, err := isc.Int(f.IntFlag.Name)
if err != nil {
return err
}
if value > 0 {
eachName(f.Name, func(name string) {
f.set.Set(f.Name, strconv.FormatInt(int64(value), 10))
})
}
}
}
return nil
}
// ApplyInputSourceValue applies a Duration value to the flagSet if required
func (f *DurationFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) {
value, err := isc.Duration(f.DurationFlag.Name)
if err != nil {
return err
}
if value > 0 {
eachName(f.Name, func(name string) {
f.set.Set(f.Name, value.String())
})
}
}
}
return nil
}
// ApplyInputSourceValue applies a Float64 value to the flagSet if required
func (f *Float64Flag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
if f.set != nil {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) {
value, err := isc.Float64(f.Float64Flag.Name)
if err != nil {
return err
}
if value > 0 {
floatStr := float64ToString(value)
eachName(f.Name, func(name string) {
f.set.Set(f.Name, floatStr)
})
}
}
}
return nil
}
func isEnvVarSet(envVars string) bool {
for _, envVar := range strings.Split(envVars, ",") {
envVar = strings.TrimSpace(envVar)
if _, ok := syscall.Getenv(envVar); ok {
// TODO: Can't use this for bools as
// set means that it was true or false based on
// Bool flag type, should work for other types
return true
}
}
return false
}
func float64ToString(f float64) string {
return fmt.Sprintf("%v", f)
}
func eachName(longName string, fn func(string)) {
parts := strings.Split(longName, ",")
for _, name := range parts {
name = strings.Trim(name, " ")
fn(name)
}
}
+347
View File
@@ -0,0 +1,347 @@
package altsrc
import (
"flag"
"gopkg.in/urfave/cli.v1"
)
// WARNING: This file is generated!
// BoolFlag is the flag type that wraps cli.BoolFlag to allow
// for other values to be specified
type BoolFlag struct {
cli.BoolFlag
set *flag.FlagSet
}
// NewBoolFlag creates a new BoolFlag
func NewBoolFlag(fl cli.BoolFlag) *BoolFlag {
return &BoolFlag{BoolFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped BoolFlag.Apply
func (f *BoolFlag) Apply(set *flag.FlagSet) {
f.set = set
f.BoolFlag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped BoolFlag.ApplyWithError
func (f *BoolFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.BoolFlag.ApplyWithError(set)
}
// BoolTFlag is the flag type that wraps cli.BoolTFlag to allow
// for other values to be specified
type BoolTFlag struct {
cli.BoolTFlag
set *flag.FlagSet
}
// NewBoolTFlag creates a new BoolTFlag
func NewBoolTFlag(fl cli.BoolTFlag) *BoolTFlag {
return &BoolTFlag{BoolTFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped BoolTFlag.Apply
func (f *BoolTFlag) Apply(set *flag.FlagSet) {
f.set = set
f.BoolTFlag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped BoolTFlag.ApplyWithError
func (f *BoolTFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.BoolTFlag.ApplyWithError(set)
}
// DurationFlag is the flag type that wraps cli.DurationFlag to allow
// for other values to be specified
type DurationFlag struct {
cli.DurationFlag
set *flag.FlagSet
}
// NewDurationFlag creates a new DurationFlag
func NewDurationFlag(fl cli.DurationFlag) *DurationFlag {
return &DurationFlag{DurationFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped DurationFlag.Apply
func (f *DurationFlag) Apply(set *flag.FlagSet) {
f.set = set
f.DurationFlag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped DurationFlag.ApplyWithError
func (f *DurationFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.DurationFlag.ApplyWithError(set)
}
// Float64Flag is the flag type that wraps cli.Float64Flag to allow
// for other values to be specified
type Float64Flag struct {
cli.Float64Flag
set *flag.FlagSet
}
// NewFloat64Flag creates a new Float64Flag
func NewFloat64Flag(fl cli.Float64Flag) *Float64Flag {
return &Float64Flag{Float64Flag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped Float64Flag.Apply
func (f *Float64Flag) Apply(set *flag.FlagSet) {
f.set = set
f.Float64Flag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped Float64Flag.ApplyWithError
func (f *Float64Flag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.Float64Flag.ApplyWithError(set)
}
// GenericFlag is the flag type that wraps cli.GenericFlag to allow
// for other values to be specified
type GenericFlag struct {
cli.GenericFlag
set *flag.FlagSet
}
// NewGenericFlag creates a new GenericFlag
func NewGenericFlag(fl cli.GenericFlag) *GenericFlag {
return &GenericFlag{GenericFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped GenericFlag.Apply
func (f *GenericFlag) Apply(set *flag.FlagSet) {
f.set = set
f.GenericFlag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped GenericFlag.ApplyWithError
func (f *GenericFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.GenericFlag.ApplyWithError(set)
}
// Int64Flag is the flag type that wraps cli.Int64Flag to allow
// for other values to be specified
type Int64Flag struct {
cli.Int64Flag
set *flag.FlagSet
}
// NewInt64Flag creates a new Int64Flag
func NewInt64Flag(fl cli.Int64Flag) *Int64Flag {
return &Int64Flag{Int64Flag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped Int64Flag.Apply
func (f *Int64Flag) Apply(set *flag.FlagSet) {
f.set = set
f.Int64Flag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped Int64Flag.ApplyWithError
func (f *Int64Flag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.Int64Flag.ApplyWithError(set)
}
// IntFlag is the flag type that wraps cli.IntFlag to allow
// for other values to be specified
type IntFlag struct {
cli.IntFlag
set *flag.FlagSet
}
// NewIntFlag creates a new IntFlag
func NewIntFlag(fl cli.IntFlag) *IntFlag {
return &IntFlag{IntFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped IntFlag.Apply
func (f *IntFlag) Apply(set *flag.FlagSet) {
f.set = set
f.IntFlag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped IntFlag.ApplyWithError
func (f *IntFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.IntFlag.ApplyWithError(set)
}
// IntSliceFlag is the flag type that wraps cli.IntSliceFlag to allow
// for other values to be specified
type IntSliceFlag struct {
cli.IntSliceFlag
set *flag.FlagSet
}
// NewIntSliceFlag creates a new IntSliceFlag
func NewIntSliceFlag(fl cli.IntSliceFlag) *IntSliceFlag {
return &IntSliceFlag{IntSliceFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped IntSliceFlag.Apply
func (f *IntSliceFlag) Apply(set *flag.FlagSet) {
f.set = set
f.IntSliceFlag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped IntSliceFlag.ApplyWithError
func (f *IntSliceFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.IntSliceFlag.ApplyWithError(set)
}
// Int64SliceFlag is the flag type that wraps cli.Int64SliceFlag to allow
// for other values to be specified
type Int64SliceFlag struct {
cli.Int64SliceFlag
set *flag.FlagSet
}
// NewInt64SliceFlag creates a new Int64SliceFlag
func NewInt64SliceFlag(fl cli.Int64SliceFlag) *Int64SliceFlag {
return &Int64SliceFlag{Int64SliceFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped Int64SliceFlag.Apply
func (f *Int64SliceFlag) Apply(set *flag.FlagSet) {
f.set = set
f.Int64SliceFlag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped Int64SliceFlag.ApplyWithError
func (f *Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.Int64SliceFlag.ApplyWithError(set)
}
// StringFlag is the flag type that wraps cli.StringFlag to allow
// for other values to be specified
type StringFlag struct {
cli.StringFlag
set *flag.FlagSet
}
// NewStringFlag creates a new StringFlag
func NewStringFlag(fl cli.StringFlag) *StringFlag {
return &StringFlag{StringFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped StringFlag.Apply
func (f *StringFlag) Apply(set *flag.FlagSet) {
f.set = set
f.StringFlag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped StringFlag.ApplyWithError
func (f *StringFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.StringFlag.ApplyWithError(set)
}
// StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow
// for other values to be specified
type StringSliceFlag struct {
cli.StringSliceFlag
set *flag.FlagSet
}
// NewStringSliceFlag creates a new StringSliceFlag
func NewStringSliceFlag(fl cli.StringSliceFlag) *StringSliceFlag {
return &StringSliceFlag{StringSliceFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped StringSliceFlag.Apply
func (f *StringSliceFlag) Apply(set *flag.FlagSet) {
f.set = set
f.StringSliceFlag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped StringSliceFlag.ApplyWithError
func (f *StringSliceFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.StringSliceFlag.ApplyWithError(set)
}
// Uint64Flag is the flag type that wraps cli.Uint64Flag to allow
// for other values to be specified
type Uint64Flag struct {
cli.Uint64Flag
set *flag.FlagSet
}
// NewUint64Flag creates a new Uint64Flag
func NewUint64Flag(fl cli.Uint64Flag) *Uint64Flag {
return &Uint64Flag{Uint64Flag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped Uint64Flag.Apply
func (f *Uint64Flag) Apply(set *flag.FlagSet) {
f.set = set
f.Uint64Flag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped Uint64Flag.ApplyWithError
func (f *Uint64Flag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.Uint64Flag.ApplyWithError(set)
}
// UintFlag is the flag type that wraps cli.UintFlag to allow
// for other values to be specified
type UintFlag struct {
cli.UintFlag
set *flag.FlagSet
}
// NewUintFlag creates a new UintFlag
func NewUintFlag(fl cli.UintFlag) *UintFlag {
return &UintFlag{UintFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped UintFlag.Apply
func (f *UintFlag) Apply(set *flag.FlagSet) {
f.set = set
f.UintFlag.Apply(set)
}
// ApplyWithError saves the flagSet for later usage calls, then calls the
// wrapped UintFlag.ApplyWithError
func (f *UintFlag) ApplyWithError(set *flag.FlagSet) error {
f.set = set
return f.UintFlag.ApplyWithError(set)
}
+336
View File
@@ -0,0 +1,336 @@
package altsrc
import (
"flag"
"fmt"
"os"
"strings"
"testing"
"time"
"gopkg.in/urfave/cli.v1"
)
type testApplyInputSource struct {
Flag FlagInputSourceExtension
FlagName string
FlagSetName string
Expected string
ContextValueString string
ContextValue flag.Value
EnvVarValue string
EnvVarName string
MapValue interface{}
}
func TestGenericApplyInputSourceValue(t *testing.T) {
v := &Parser{"abc", "def"}
c := runTest(t, testApplyInputSource{
Flag: NewGenericFlag(cli.GenericFlag{Name: "test", Value: &Parser{}}),
FlagName: "test",
MapValue: v,
})
expect(t, v, c.Generic("test"))
}
func TestGenericApplyInputSourceMethodContextSet(t *testing.T) {
p := &Parser{"abc", "def"}
c := runTest(t, testApplyInputSource{
Flag: NewGenericFlag(cli.GenericFlag{Name: "test", Value: &Parser{}}),
FlagName: "test",
MapValue: &Parser{"efg", "hig"},
ContextValueString: p.String(),
})
expect(t, p, c.Generic("test"))
}
func TestGenericApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewGenericFlag(cli.GenericFlag{Name: "test", Value: &Parser{}, EnvVar: "TEST"}),
FlagName: "test",
MapValue: &Parser{"efg", "hij"},
EnvVarName: "TEST",
EnvVarValue: "abc,def",
})
expect(t, &Parser{"abc", "def"}, c.Generic("test"))
}
func TestStringSliceApplyInputSourceValue(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test"}),
FlagName: "test",
MapValue: []interface{}{"hello", "world"},
})
expect(t, c.StringSlice("test"), []string{"hello", "world"})
}
func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test"}),
FlagName: "test",
MapValue: []interface{}{"hello", "world"},
ContextValueString: "ohno",
})
expect(t, c.StringSlice("test"), []string{"ohno"})
}
func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test", EnvVar: "TEST"}),
FlagName: "test",
MapValue: []interface{}{"hello", "world"},
EnvVarName: "TEST",
EnvVarValue: "oh,no",
})
expect(t, c.StringSlice("test"), []string{"oh", "no"})
}
func TestIntSliceApplyInputSourceValue(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test"}),
FlagName: "test",
MapValue: []interface{}{1, 2},
})
expect(t, c.IntSlice("test"), []int{1, 2})
}
func TestIntSliceApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test"}),
FlagName: "test",
MapValue: []interface{}{1, 2},
ContextValueString: "3",
})
expect(t, c.IntSlice("test"), []int{3})
}
func TestIntSliceApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test", EnvVar: "TEST"}),
FlagName: "test",
MapValue: []interface{}{1, 2},
EnvVarName: "TEST",
EnvVarValue: "3,4",
})
expect(t, c.IntSlice("test"), []int{3, 4})
}
func TestBoolApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewBoolFlag(cli.BoolFlag{Name: "test"}),
FlagName: "test",
MapValue: true,
})
expect(t, true, c.Bool("test"))
}
func TestBoolApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewBoolFlag(cli.BoolFlag{Name: "test"}),
FlagName: "test",
MapValue: false,
ContextValueString: "true",
})
expect(t, true, c.Bool("test"))
}
func TestBoolApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewBoolFlag(cli.BoolFlag{Name: "test", EnvVar: "TEST"}),
FlagName: "test",
MapValue: false,
EnvVarName: "TEST",
EnvVarValue: "true",
})
expect(t, true, c.Bool("test"))
}
func TestBoolTApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewBoolTFlag(cli.BoolTFlag{Name: "test"}),
FlagName: "test",
MapValue: false,
})
expect(t, false, c.BoolT("test"))
}
func TestBoolTApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewBoolTFlag(cli.BoolTFlag{Name: "test"}),
FlagName: "test",
MapValue: true,
ContextValueString: "false",
})
expect(t, false, c.BoolT("test"))
}
func TestBoolTApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewBoolTFlag(cli.BoolTFlag{Name: "test", EnvVar: "TEST"}),
FlagName: "test",
MapValue: true,
EnvVarName: "TEST",
EnvVarValue: "false",
})
expect(t, false, c.BoolT("test"))
}
func TestStringApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewStringFlag(cli.StringFlag{Name: "test"}),
FlagName: "test",
MapValue: "hello",
})
expect(t, "hello", c.String("test"))
}
func TestStringApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewStringFlag(cli.StringFlag{Name: "test"}),
FlagName: "test",
MapValue: "hello",
ContextValueString: "goodbye",
})
expect(t, "goodbye", c.String("test"))
}
func TestStringApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewStringFlag(cli.StringFlag{Name: "test", EnvVar: "TEST"}),
FlagName: "test",
MapValue: "hello",
EnvVarName: "TEST",
EnvVarValue: "goodbye",
})
expect(t, "goodbye", c.String("test"))
}
func TestIntApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewIntFlag(cli.IntFlag{Name: "test"}),
FlagName: "test",
MapValue: 15,
})
expect(t, 15, c.Int("test"))
}
func TestIntApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewIntFlag(cli.IntFlag{Name: "test"}),
FlagName: "test",
MapValue: 15,
ContextValueString: "7",
})
expect(t, 7, c.Int("test"))
}
func TestIntApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewIntFlag(cli.IntFlag{Name: "test", EnvVar: "TEST"}),
FlagName: "test",
MapValue: 15,
EnvVarName: "TEST",
EnvVarValue: "12",
})
expect(t, 12, c.Int("test"))
}
func TestDurationApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewDurationFlag(cli.DurationFlag{Name: "test"}),
FlagName: "test",
MapValue: time.Duration(30 * time.Second),
})
expect(t, time.Duration(30*time.Second), c.Duration("test"))
}
func TestDurationApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewDurationFlag(cli.DurationFlag{Name: "test"}),
FlagName: "test",
MapValue: time.Duration(30 * time.Second),
ContextValueString: time.Duration(15 * time.Second).String(),
})
expect(t, time.Duration(15*time.Second), c.Duration("test"))
}
func TestDurationApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewDurationFlag(cli.DurationFlag{Name: "test", EnvVar: "TEST"}),
FlagName: "test",
MapValue: time.Duration(30 * time.Second),
EnvVarName: "TEST",
EnvVarValue: time.Duration(15 * time.Second).String(),
})
expect(t, time.Duration(15*time.Second), c.Duration("test"))
}
func TestFloat64ApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewFloat64Flag(cli.Float64Flag{Name: "test"}),
FlagName: "test",
MapValue: 1.3,
})
expect(t, 1.3, c.Float64("test"))
}
func TestFloat64ApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewFloat64Flag(cli.Float64Flag{Name: "test"}),
FlagName: "test",
MapValue: 1.3,
ContextValueString: fmt.Sprintf("%v", 1.4),
})
expect(t, 1.4, c.Float64("test"))
}
func TestFloat64ApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewFloat64Flag(cli.Float64Flag{Name: "test", EnvVar: "TEST"}),
FlagName: "test",
MapValue: 1.3,
EnvVarName: "TEST",
EnvVarValue: fmt.Sprintf("%v", 1.4),
})
expect(t, 1.4, c.Float64("test"))
}
func runTest(t *testing.T, test testApplyInputSource) *cli.Context {
inputSource := &MapInputSource{valueMap: map[interface{}]interface{}{test.FlagName: test.MapValue}}
set := flag.NewFlagSet(test.FlagSetName, flag.ContinueOnError)
c := cli.NewContext(nil, set, nil)
if test.EnvVarName != "" && test.EnvVarValue != "" {
os.Setenv(test.EnvVarName, test.EnvVarValue)
defer os.Setenv(test.EnvVarName, "")
}
test.Flag.Apply(set)
if test.ContextValue != nil {
flag := set.Lookup(test.FlagName)
flag.Value = test.ContextValue
}
if test.ContextValueString != "" {
set.Set(test.FlagName, test.ContextValueString)
}
test.Flag.ApplyInputSourceValue(c, inputSource)
return c
}
type Parser [2]string
func (p *Parser) Set(value string) error {
parts := strings.Split(value, ",")
if len(parts) != 2 {
return fmt.Errorf("invalid format")
}
(*p)[0] = parts[0]
(*p)[1] = parts[1]
return nil
}
func (p *Parser) String() string {
return fmt.Sprintf("%s,%s", p[0], p[1])
}
+18
View File
@@ -0,0 +1,18 @@
package altsrc
import (
"reflect"
"testing"
)
func expect(t *testing.T, a interface{}, b interface{}) {
if !reflect.DeepEqual(b, a) {
t.Errorf("Expected %#v (type %v) - Got %#v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
}
}
func refute(t *testing.T, a interface{}, b interface{}) {
if a == b {
t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
}
}
+21
View File
@@ -0,0 +1,21 @@
package altsrc
import (
"time"
"gopkg.in/urfave/cli.v1"
)
// InputSourceContext is an interface used to allow
// other input sources to be implemented as needed.
type InputSourceContext interface {
Int(name string) (int, error)
Duration(name string) (time.Duration, error)
Float64(name string) (float64, error)
String(name string) (string, error)
StringSlice(name string) ([]string, error)
IntSlice(name string) ([]int, error)
Generic(name string) (cli.Generic, error)
Bool(name string) (bool, error)
BoolT(name string) (bool, error)
}
+262
View File
@@ -0,0 +1,262 @@
package altsrc
import (
"fmt"
"reflect"
"strings"
"time"
"gopkg.in/urfave/cli.v1"
)
// MapInputSource implements InputSourceContext to return
// data from the map that is loaded.
type MapInputSource struct {
valueMap map[interface{}]interface{}
}
// nestedVal checks if the name has '.' delimiters.
// If so, it tries to traverse the tree by the '.' delimited sections to find
// a nested value for the key.
func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool) {
if sections := strings.Split(name, "."); len(sections) > 1 {
node := tree
for _, section := range sections[:len(sections)-1] {
child, ok := node[section]
if !ok {
return nil, false
}
ctype, ok := child.(map[interface{}]interface{})
if !ok {
return nil, false
}
node = ctype
}
if val, ok := node[sections[len(sections)-1]]; ok {
return val, true
}
}
return nil, false
}
// Int returns an int from the map if it exists otherwise returns 0
func (fsm *MapInputSource) Int(name string) (int, error) {
otherGenericValue, exists := fsm.valueMap[name]
if exists {
otherValue, isType := otherGenericValue.(int)
if !isType {
return 0, incorrectTypeForFlagError(name, "int", otherGenericValue)
}
return otherValue, nil
}
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
if exists {
otherValue, isType := nestedGenericValue.(int)
if !isType {
return 0, incorrectTypeForFlagError(name, "int", nestedGenericValue)
}
return otherValue, nil
}
return 0, nil
}
// Duration returns a duration from the map if it exists otherwise returns 0
func (fsm *MapInputSource) Duration(name string) (time.Duration, error) {
otherGenericValue, exists := fsm.valueMap[name]
if exists {
otherValue, isType := otherGenericValue.(time.Duration)
if !isType {
return 0, incorrectTypeForFlagError(name, "duration", otherGenericValue)
}
return otherValue, nil
}
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
if exists {
otherValue, isType := nestedGenericValue.(time.Duration)
if !isType {
return 0, incorrectTypeForFlagError(name, "duration", nestedGenericValue)
}
return otherValue, nil
}
return 0, nil
}
// Float64 returns an float64 from the map if it exists otherwise returns 0
func (fsm *MapInputSource) Float64(name string) (float64, error) {
otherGenericValue, exists := fsm.valueMap[name]
if exists {
otherValue, isType := otherGenericValue.(float64)
if !isType {
return 0, incorrectTypeForFlagError(name, "float64", otherGenericValue)
}
return otherValue, nil
}
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
if exists {
otherValue, isType := nestedGenericValue.(float64)
if !isType {
return 0, incorrectTypeForFlagError(name, "float64", nestedGenericValue)
}
return otherValue, nil
}
return 0, nil
}
// String returns a string from the map if it exists otherwise returns an empty string
func (fsm *MapInputSource) String(name string) (string, error) {
otherGenericValue, exists := fsm.valueMap[name]
if exists {
otherValue, isType := otherGenericValue.(string)
if !isType {
return "", incorrectTypeForFlagError(name, "string", otherGenericValue)
}
return otherValue, nil
}
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
if exists {
otherValue, isType := nestedGenericValue.(string)
if !isType {
return "", incorrectTypeForFlagError(name, "string", nestedGenericValue)
}
return otherValue, nil
}
return "", nil
}
// StringSlice returns an []string from the map if it exists otherwise returns nil
func (fsm *MapInputSource) StringSlice(name string) ([]string, error) {
otherGenericValue, exists := fsm.valueMap[name]
if !exists {
otherGenericValue, exists = nestedVal(name, fsm.valueMap)
if !exists {
return nil, nil
}
}
otherValue, isType := otherGenericValue.([]interface{})
if !isType {
return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue)
}
var stringSlice = make([]string, 0, len(otherValue))
for i, v := range otherValue {
stringValue, isType := v.(string)
if !isType {
return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "string", v)
}
stringSlice = append(stringSlice, stringValue)
}
return stringSlice, nil
}
// IntSlice returns an []int from the map if it exists otherwise returns nil
func (fsm *MapInputSource) IntSlice(name string) ([]int, error) {
otherGenericValue, exists := fsm.valueMap[name]
if !exists {
otherGenericValue, exists = nestedVal(name, fsm.valueMap)
if !exists {
return nil, nil
}
}
otherValue, isType := otherGenericValue.([]interface{})
if !isType {
return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue)
}
var intSlice = make([]int, 0, len(otherValue))
for i, v := range otherValue {
intValue, isType := v.(int)
if !isType {
return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int", v)
}
intSlice = append(intSlice, intValue)
}
return intSlice, nil
}
// Generic returns an cli.Generic from the map if it exists otherwise returns nil
func (fsm *MapInputSource) Generic(name string) (cli.Generic, error) {
otherGenericValue, exists := fsm.valueMap[name]
if exists {
otherValue, isType := otherGenericValue.(cli.Generic)
if !isType {
return nil, incorrectTypeForFlagError(name, "cli.Generic", otherGenericValue)
}
return otherValue, nil
}
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
if exists {
otherValue, isType := nestedGenericValue.(cli.Generic)
if !isType {
return nil, incorrectTypeForFlagError(name, "cli.Generic", nestedGenericValue)
}
return otherValue, nil
}
return nil, nil
}
// Bool returns an bool from the map otherwise returns false
func (fsm *MapInputSource) Bool(name string) (bool, error) {
otherGenericValue, exists := fsm.valueMap[name]
if exists {
otherValue, isType := otherGenericValue.(bool)
if !isType {
return false, incorrectTypeForFlagError(name, "bool", otherGenericValue)
}
return otherValue, nil
}
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
if exists {
otherValue, isType := nestedGenericValue.(bool)
if !isType {
return false, incorrectTypeForFlagError(name, "bool", nestedGenericValue)
}
return otherValue, nil
}
return false, nil
}
// BoolT returns an bool from the map otherwise returns true
func (fsm *MapInputSource) BoolT(name string) (bool, error) {
otherGenericValue, exists := fsm.valueMap[name]
if exists {
otherValue, isType := otherGenericValue.(bool)
if !isType {
return true, incorrectTypeForFlagError(name, "bool", otherGenericValue)
}
return otherValue, nil
}
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
if exists {
otherValue, isType := nestedGenericValue.(bool)
if !isType {
return true, incorrectTypeForFlagError(name, "bool", nestedGenericValue)
}
return otherValue, nil
}
return true, nil
}
func incorrectTypeForFlagError(name, expectedTypeName string, value interface{}) error {
valueType := reflect.TypeOf(value)
valueTypeName := ""
if valueType != nil {
valueTypeName = valueType.Name()
}
return fmt.Errorf("Mismatched type for flag '%s'. Expected '%s' but actual is '%s'", name, expectedTypeName, valueTypeName)
}

Some files were not shown because too many files have changed in this diff Show More