0
0
mirror of https://github.com/thegeeklab/wp-matrix.git synced 2024-11-09 18:10:39 +00:00

Merge pull request #8 from drone-plugins/restructure

Updated to current build process
This commit is contained in:
Thomas Boerger 2018-03-16 15:01:25 +01:00 committed by GitHub
commit d6bf193ae0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 34 additions and 25728 deletions

View File

@ -1,11 +1,12 @@
version: '{build}'
image: 'Visual Studio 2017'
platform: x64
platform: 'x64'
clone_folder: 'c:\go\src\github.com\drone-plugins\drone-matrix'
clone_folder: 'c:\gopath\src\github.com\drone-plugins\drone-matrix'
max_jobs: 1
environment:
GOPATH: c:\gopath
DOCKER_USERNAME:
secure: '4YzzahbEiMZQJpOCOd1LAw=='
DOCKER_PASSWORD:
@ -15,14 +16,19 @@ install:
- ps: |
docker version
go version
- ps: |
$env:Path = "c:\gopath\bin;$env:Path"
build_script:
- ps: |
go get -u github.com/golang/dep/cmd/dep
dep ensure
if ( $env:APPVEYOR_REPO_TAG -eq 'false' ) {
go build -ldflags "-X main.build=$env:APPVEYOR_BUILD_VERSION" -a -o drone-matrix.exe
go build -ldflags "-X main.build=$env:APPVEYOR_BUILD_VERSION" -a -o release/drone-matrix.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-matrix.exe
go build -ldflags "-X main.version=$version -X main.build=$env:APPVEYOR_BUILD_VERSION" -a -o release/drone-matrix.exe
}
docker pull microsoft/nanoserver:10.0.14393.1593

View File

@ -3,18 +3,22 @@ workspace:
path: src/github.com/drone-plugins/drone-matrix
pipeline:
test:
image: golang:1.9
deps:
image: golang:1.10
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
- go get -u github.com/golang/dep/cmd/dep
- dep ensure
test:
image: golang:1.10
pull: true
commands:
- go vet ./...
- go test -cover ./...
build_linux_amd64:
image: golang:1.9
image: golang:1.10
pull: true
group: build
environment:
@ -30,7 +34,7 @@ pipeline:
fi
build_linux_i386:
image: golang:1.9
image: golang:1.10
pull: true
group: build
environment:
@ -46,7 +50,7 @@ pipeline:
fi
build_linux_arm64:
image: golang:1.9
image: golang:1.10
pull: true
group: build
environment:
@ -62,7 +66,7 @@ pipeline:
fi
build_linux_arm:
image: golang:1.9
image: golang:1.10
pull: true
group: build
environment:
@ -79,7 +83,7 @@ pipeline:
fi
publish_linux_amd64:
image: plugins/docker:17.05
image: plugins/docker:17.12
pull: true
secrets: [ docker_username, docker_password ]
group: docker
@ -91,7 +95,7 @@ pipeline:
event: [ push, tag ]
publish_linux_i386:
image: plugins/docker:17.05
image: plugins/docker:17.12
pull: true
secrets: [ docker_username, docker_password ]
group: docker
@ -103,7 +107,7 @@ pipeline:
event: [ push, tag ]
publish_linux_arm64:
image: plugins/docker:17.05
image: plugins/docker:17.12
pull: true
secrets: [ docker_username, docker_password ]
group: docker
@ -115,7 +119,7 @@ pipeline:
event: [ push, tag ]
publish_linux_arm:
image: plugins/docker:17.05
image: plugins/docker:17.12
pull: true
secrets: [ docker_username, docker_password ]
group: docker

2
.gitignore vendored
View File

@ -24,5 +24,7 @@ _testmain.go
*.prof
release/
vendor/
coverage.out
drone-matrix

View File

@ -6,5 +6,7 @@ LABEL maintainer="Drone.IO Community <drone-dev@googlegroups.com>" `
org.label-schema.vendor="Drone.IO Community" `
org.label-schema.schema-version="1.0"
ADD drone-matrix.exe c:\drone-matrix.exe
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
ADD release\drone-matrix.exe c:\drone-matrix.exe
ENTRYPOINT [ "c:\\drone-matrix.exe" ]

View File

@ -1,26 +1,3 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
name = "github.com/aymerick/raymond"
version = "2.0.1"

View File

@ -1,3 +0,0 @@
[submodule "mustache"]
path = mustache
url = git://github.com/mustache/spec.git

View File

@ -1,6 +0,0 @@
---
language: go
go:
- 1.3
- tip

View File

@ -1,46 +0,0 @@
# Benchmarks
Hardware: MacBookPro11,1 - Intel Core i5 - 2,6 GHz - 8 Go RAM
With:
- handlebars.js #8cba84df119c317fcebc49fb285518542ca9c2d0
- raymond #7bbaaf50ed03c96b56687d7fa6c6e04e02375a98
## handlebars.js (ops/ms)
arguments 198 ±4 (5)
array-each 568 ±23 (5)
array-mustache 522 ±18 (4)
complex 71 ±7 (3)
data 67 ±2 (3)
depth-1 47 ±2 (3)
depth-2 14 ±1 (2)
object-mustache 1099 ±47 (5)
object 907 ±58 (4)
partial-recursion 46 ±3 (4)
partial 68 ±3 (3)
paths 1650 ±50 (3)
string 2552 ±157 (3)
subexpression 141 ±2 (4)
variables 2671 ±83 (4)
## raymond
BenchmarkArguments 200000 6642 ns/op 151 ops/ms
BenchmarkArrayEach 100000 19584 ns/op 51 ops/ms
BenchmarkArrayMustache 100000 17305 ns/op 58 ops/ms
BenchmarkComplex 30000 50270 ns/op 20 ops/ms
BenchmarkData 50000 25551 ns/op 39 ops/ms
BenchmarkDepth1 100000 20162 ns/op 50 ops/ms
BenchmarkDepth2 30000 47782 ns/op 21 ops/ms
BenchmarkObjectMustache 200000 7668 ns/op 130 ops/ms
BenchmarkObject 200000 8843 ns/op 113 ops/ms
BenchmarkPartialRecursion 50000 23139 ns/op 43 ops/ms
BenchmarkPartial 50000 31015 ns/op 32 ops/ms
BenchmarkPath 200000 8997 ns/op 111 ops/ms
BenchmarkString 1000000 1879 ns/op 532 ops/ms
BenchmarkSubExpression 300000 4935 ns/op 203 ops/ms
BenchmarkVariables 200000 6478 ns/op 154 ops/ms

View File

@ -1,33 +0,0 @@
# 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.

View File

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Aymerick JEHANNE
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
2.0.1

View File

@ -1,785 +0,0 @@
// Package ast provides structures to represent a handlebars Abstract Syntax Tree, and a Visitor interface to visit that tree.
package ast
import (
"fmt"
"strconv"
)
// References:
// - https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/ast.js
// - https://github.com/wycats/handlebars.js/blob/master/docs/compiler-api.md
// - https://github.com/golang/go/blob/master/src/text/template/parse/node.go
// Node is an element in the AST.
type Node interface {
// node type
Type() NodeType
// location of node in original input string
Location() Loc
// string representation, used for debugging
String() string
// accepts visitor
Accept(Visitor) interface{}
}
// Visitor is the interface to visit an AST.
type Visitor interface {
VisitProgram(*Program) interface{}
// statements
VisitMustache(*MustacheStatement) interface{}
VisitBlock(*BlockStatement) interface{}
VisitPartial(*PartialStatement) interface{}
VisitContent(*ContentStatement) interface{}
VisitComment(*CommentStatement) interface{}
// expressions
VisitExpression(*Expression) interface{}
VisitSubExpression(*SubExpression) interface{}
VisitPath(*PathExpression) interface{}
// literals
VisitString(*StringLiteral) interface{}
VisitBoolean(*BooleanLiteral) interface{}
VisitNumber(*NumberLiteral) interface{}
// miscellaneous
VisitHash(*Hash) interface{}
VisitHashPair(*HashPair) interface{}
}
// NodeType represents an AST Node type.
type NodeType int
// Type returns itself, and permits struct includers to satisfy that part of Node interface.
func (t NodeType) Type() NodeType {
return t
}
const (
// 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)
}

View File

@ -1,279 +0,0 @@
package ast
import (
"fmt"
"strings"
)
// printVisitor implements the Visitor interface to print a AST.
type printVisitor struct {
buf string
depth int
original bool
inBlock bool
}
func newPrintVisitor() *printVisitor {
return &printVisitor{}
}
// Print returns a string representation of given AST, that can be used for debugging purpose.
func Print(node Node) string {
visitor := newPrintVisitor()
node.Accept(visitor)
return visitor.output()
}
func (v *printVisitor) output() string {
return v.buf
}
func (v *printVisitor) indent() {
for i := 0; i < v.depth; {
v.buf += " "
i++
}
}
func (v *printVisitor) str(val string) {
v.buf += val
}
func (v *printVisitor) nl() {
v.str("\n")
}
func (v *printVisitor) line(val string) {
v.indent()
v.str(val)
v.nl()
}
//
// Visitor interface
//
// Statements
// VisitProgram implements corresponding Visitor interface method
func (v *printVisitor) VisitProgram(node *Program) interface{} {
if len(node.BlockParams) > 0 {
v.line("BLOCK PARAMS: [ " + strings.Join(node.BlockParams, " ") + " ]")
}
for _, n := range node.Body {
n.Accept(v)
}
return nil
}
// VisitMustache implements corresponding Visitor interface method
func (v *printVisitor) VisitMustache(node *MustacheStatement) interface{} {
v.indent()
v.str("{{ ")
node.Expression.Accept(v)
v.str(" }}")
v.nl()
return nil
}
// VisitBlock implements corresponding Visitor interface method
func (v *printVisitor) VisitBlock(node *BlockStatement) interface{} {
v.inBlock = true
v.line("BLOCK:")
v.depth++
node.Expression.Accept(v)
if node.Program != nil {
v.line("PROGRAM:")
v.depth++
node.Program.Accept(v)
v.depth--
}
if node.Inverse != nil {
// if node.Program != nil {
// v.depth++
// }
v.line("{{^}}")
v.depth++
node.Inverse.Accept(v)
v.depth--
// if node.Program != nil {
// v.depth--
// }
}
v.inBlock = false
return nil
}
// VisitPartial implements corresponding Visitor interface method
func (v *printVisitor) VisitPartial(node *PartialStatement) interface{} {
v.indent()
v.str("{{> PARTIAL:")
v.original = true
node.Name.Accept(v)
v.original = false
if len(node.Params) > 0 {
v.str(" ")
node.Params[0].Accept(v)
}
// hash
if node.Hash != nil {
v.str(" ")
node.Hash.Accept(v)
}
v.str(" }}")
v.nl()
return nil
}
// VisitContent implements corresponding Visitor interface method
func (v *printVisitor) VisitContent(node *ContentStatement) interface{} {
v.line("CONTENT[ '" + node.Value + "' ]")
return nil
}
// VisitComment implements corresponding Visitor interface method
func (v *printVisitor) VisitComment(node *CommentStatement) interface{} {
v.line("{{! '" + node.Value + "' }}")
return nil
}
// Expressions
// VisitExpression implements corresponding Visitor interface method
func (v *printVisitor) VisitExpression(node *Expression) interface{} {
if v.inBlock {
v.indent()
}
// path
node.Path.Accept(v)
// params
v.str(" [")
for i, n := range node.Params {
if i > 0 {
v.str(", ")
}
n.Accept(v)
}
v.str("]")
// hash
if node.Hash != nil {
v.str(" ")
node.Hash.Accept(v)
}
if v.inBlock {
v.nl()
}
return nil
}
// VisitSubExpression implements corresponding Visitor interface method
func (v *printVisitor) VisitSubExpression(node *SubExpression) interface{} {
node.Expression.Accept(v)
return nil
}
// VisitPath implements corresponding Visitor interface method
func (v *printVisitor) VisitPath(node *PathExpression) interface{} {
if v.original {
v.str(node.Original)
} else {
path := strings.Join(node.Parts, "/")
result := ""
if node.Data {
result += "@"
}
v.str(result + "PATH:" + path)
}
return nil
}
// Literals
// VisitString implements corresponding Visitor interface method
func (v *printVisitor) VisitString(node *StringLiteral) interface{} {
if v.original {
v.str(node.Value)
} else {
v.str("\"" + node.Value + "\"")
}
return nil
}
// VisitBoolean implements corresponding Visitor interface method
func (v *printVisitor) VisitBoolean(node *BooleanLiteral) interface{} {
if v.original {
v.str(node.Original)
} else {
v.str(fmt.Sprintf("BOOLEAN{%s}", node.Canonical()))
}
return nil
}
// VisitNumber implements corresponding Visitor interface method
func (v *printVisitor) VisitNumber(node *NumberLiteral) interface{} {
if v.original {
v.str(node.Original)
} else {
v.str(fmt.Sprintf("NUMBER{%s}", node.Canonical()))
}
return nil
}
// Miscellaneous
// VisitHash implements corresponding Visitor interface method
func (v *printVisitor) VisitHash(node *Hash) interface{} {
v.str("HASH{")
for i, p := range node.Pairs {
if i > 0 {
v.str(", ")
}
p.Accept(v)
}
v.str("}")
return nil
}
// VisitHashPair implements corresponding Visitor interface method
func (v *printVisitor) VisitHashPair(node *HashPair) interface{} {
v.str(node.Key + "=")
node.Val.Accept(v)
return nil
}

View File

@ -1,167 +0,0 @@
package raymond
import (
"fmt"
"regexp"
"testing"
)
type Test struct {
name string
input string
data interface{}
privData map[string]interface{}
helpers map[string]interface{}
partials map[string]string
output interface{}
}
func launchTests(t *testing.T, tests []Test) {
// NOTE: TestMustache() makes Parallel testing fail
// t.Parallel()
for _, test := range tests {
var err error
var tpl *Template
// parse template
tpl, err = Parse(test.input)
if err != nil {
t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err)
} else {
if len(test.helpers) > 0 {
// register helpers
tpl.RegisterHelpers(test.helpers)
}
if len(test.partials) > 0 {
// register partials
tpl.RegisterPartials(test.partials)
}
// setup private data frame
var privData *DataFrame
if test.privData != nil {
privData = NewDataFrame()
for k, v := range test.privData {
privData.Set(k, v)
}
}
// render template
output, err := tpl.ExecWith(test.data, privData)
if err != nil {
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\nerror:\n\t%s\nAST:\n\t%s", test.name, test.input, Str(test.data), err, tpl.PrintAST())
} else {
// check output
var expectedArr []string
expectedArr, ok := test.output.([]string)
if ok {
match := false
for _, expectedStr := range expectedArr {
if expectedStr == output {
match = true
break
}
}
if !match {
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, Str(test.data), Str(test.partials), expectedArr, output, tpl.PrintAST())
}
} else {
expectedStr, ok := test.output.(string)
if !ok {
panic(fmt.Errorf("Erroneous test output description: %q", test.output))
}
if expectedStr != output {
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, Str(test.data), Str(test.partials), expectedStr, output, tpl.PrintAST())
}
}
}
}
}
}
func launchErrorTests(t *testing.T, tests []Test) {
t.Parallel()
for _, test := range tests {
var err error
var tpl *Template
// parse template
tpl, err = Parse(test.input)
if err != nil {
t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err)
} else {
if len(test.helpers) > 0 {
// register helpers
tpl.RegisterHelpers(test.helpers)
}
if len(test.partials) > 0 {
// register partials
tpl.RegisterPartials(test.partials)
}
// setup private data frame
var privData *DataFrame
if test.privData != nil {
privData := NewDataFrame()
for k, v := range test.privData {
privData.Set(k, v)
}
}
// render template
output, err := tpl.ExecWith(test.data, privData)
if err == nil {
t.Errorf("Test '%s' failed - Error expected\ninput:\n\t'%s'\ngot\n\t%q\nAST:\n%q", test.name, test.input, output, tpl.PrintAST())
} else {
var errMatch error
match := false
// check output
var expectedArr []string
expectedArr, ok := test.output.([]string)
if ok {
if len(expectedArr) > 0 {
for _, expectedStr := range expectedArr {
match, errMatch = regexp.MatchString(regexp.QuoteMeta(expectedStr), fmt.Sprint(err))
if errMatch != nil {
panic("Failed to match regexp")
}
if match {
break
}
}
} else {
// nothing to test
match = true
}
} else {
expectedStr, ok := test.output.(string)
if !ok {
panic(fmt.Errorf("Erroneous test output description: %q", test.output))
}
if expectedStr != "" {
match, errMatch = regexp.MatchString(regexp.QuoteMeta(expectedStr), fmt.Sprint(err))
if errMatch != nil {
panic("Failed to match regexp")
}
} else {
// nothing to test
match = true
}
}
if !match {
t.Errorf("Test '%s' failed - Incorrect error returned\ninput:\n\t'%s'\ndata:\n\t%s\nexpected\n\t%q\ngot\n\t%q", test.name, test.input, Str(test.data), test.output, err)
}
}
}
}
}

View File

@ -1,316 +0,0 @@
package raymond
import "testing"
//
// Those tests come from:
// https://github.com/wycats/handlebars.js/blob/master/bench/
//
// Note that handlebars.js does NOT benchmark template compilation, it only benchmarks evaluation.
//
func BenchmarkArguments(b *testing.B) {
source := `{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}`
ctx := map[string]bool{
"bar": true,
}
tpl := MustParse(source)
tpl.RegisterHelper("foo", func(a, b, c, d interface{}) string { return "" })
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkArrayEach(b *testing.B) {
source := `{{#each names}}{{name}}{{/each}}`
ctx := map[string][]map[string]string{
"names": {
{"name": "Moe"},
{"name": "Larry"},
{"name": "Curly"},
{"name": "Shemp"},
},
}
tpl := MustParse(source)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkArrayMustache(b *testing.B) {
source := `{{#names}}{{name}}{{/names}}`
ctx := map[string][]map[string]string{
"names": {
{"name": "Moe"},
{"name": "Larry"},
{"name": "Curly"},
{"name": "Shemp"},
},
}
tpl := MustParse(source)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkComplex(b *testing.B) {
source := `<h1>{{header}}</h1>
{{#if items}}
<ul>
{{#each items}}
{{#if current}}
<li><strong>{{name}}</strong></li>
{{^}}
<li><a href="{{url}}">{{name}}</a></li>
{{/if}}
{{/each}}
</ul>
{{^}}
<p>The list is empty.</p>
{{/if}}
`
ctx := map[string]interface{}{
"header": func() string { return "Colors" },
"hasItems": true,
"items": []map[string]interface{}{
{"name": "red", "current": true, "url": "#Red"},
{"name": "green", "current": false, "url": "#Green"},
{"name": "blue", "current": false, "url": "#Blue"},
},
}
tpl := MustParse(source)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkData(b *testing.B) {
source := `{{#each names}}{{@index}}{{name}}{{/each}}`
ctx := map[string][]map[string]string{
"names": {
{"name": "Moe"},
{"name": "Larry"},
{"name": "Curly"},
{"name": "Shemp"},
},
}
tpl := MustParse(source)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkDepth1(b *testing.B) {
source := `{{#each names}}{{../foo}}{{/each}}`
ctx := map[string]interface{}{
"names": []map[string]string{
{"name": "Moe"},
{"name": "Larry"},
{"name": "Curly"},
{"name": "Shemp"},
},
"foo": "bar",
}
tpl := MustParse(source)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkDepth2(b *testing.B) {
source := `{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}`
ctx := map[string]interface{}{
"names": []map[string]interface{}{
{"bat": "foo", "name": []string{"Moe"}},
{"bat": "foo", "name": []string{"Larry"}},
{"bat": "foo", "name": []string{"Curly"}},
{"bat": "foo", "name": []string{"Shemp"}},
},
"foo": "bar",
}
tpl := MustParse(source)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkObjectMustache(b *testing.B) {
source := `{{#person}}{{name}}{{age}}{{/person}}`
ctx := map[string]interface{}{
"person": map[string]interface{}{
"name": "Larry",
"age": 45,
},
}
tpl := MustParse(source)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkObject(b *testing.B) {
source := `{{#with person}}{{name}}{{age}}{{/with}}`
ctx := map[string]interface{}{
"person": map[string]interface{}{
"name": "Larry",
"age": 45,
},
}
tpl := MustParse(source)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkPartialRecursion(b *testing.B) {
source := `{{name}}{{#each kids}}{{>recursion}}{{/each}}`
ctx := map[string]interface{}{
"name": 1,
"kids": []map[string]interface{}{
{
"name": "1.1",
"kids": []map[string]interface{}{
{
"name": "1.1.1",
"kids": []map[string]interface{}{},
},
},
},
},
}
tpl := MustParse(source)
partial := MustParse(`{{name}}{{#each kids}}{{>recursion}}{{/each}}`)
tpl.RegisterPartialTemplate("recursion", partial)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkPartial(b *testing.B) {
source := `{{#each peeps}}{{>variables}}{{/each}}`
ctx := map[string]interface{}{
"peeps": []map[string]interface{}{
{"name": "Moe", "count": 15},
{"name": "Moe", "count": 5},
{"name": "Curly", "count": 1},
},
}
tpl := MustParse(source)
partial := MustParse(`Hello {{name}}! You have {{count}} new messages.`)
tpl.RegisterPartialTemplate("variables", partial)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkPath(b *testing.B) {
source := `{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}`
ctx := map[string]interface{}{
"person": map[string]interface{}{
"name": map[string]interface{}{
"bar": map[string]string{
"baz": "Larry",
},
},
"age": 45,
},
}
tpl := MustParse(source)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkString(b *testing.B) {
source := `Hello world`
tpl := MustParse(source)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(nil)
}
}
func BenchmarkSubExpression(b *testing.B) {
source := `{{echo (header)}}`
ctx := map[string]interface{}{}
tpl := MustParse(source)
tpl.RegisterHelpers(map[string]interface{}{
"echo": func(v string) string { return "foo " + v },
"header": func() string { return "Colors" },
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}
func BenchmarkVariables(b *testing.B) {
source := `Hello {{name}}! You have {{count}} new messages.`
ctx := map[string]interface{}{
"name": "Mick",
"count": 30,
}
tpl := MustParse(source)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.MustExec(ctx)
}
}

View File

@ -1,95 +0,0 @@
package raymond
import "reflect"
// DataFrame represents a private data frame.
//
// Cf. private variables documentation at: http://handlebarsjs.com/block_helpers.html
type DataFrame struct {
parent *DataFrame
data map[string]interface{}
}
// NewDataFrame instanciates a new private data frame.
func NewDataFrame() *DataFrame {
return &DataFrame{
data: make(map[string]interface{}),
}
}
// Copy instanciates a new private data frame with receiver as parent.
func (p *DataFrame) Copy() *DataFrame {
result := NewDataFrame()
for k, v := range p.data {
result.data[k] = v
}
result.parent = p
return result
}
// newIterDataFrame instanciates a new private data frame with receiver as parent and with iteration data set (@index, @key, @first, @last)
func (p *DataFrame) newIterDataFrame(length int, i int, key interface{}) *DataFrame {
result := p.Copy()
result.Set("index", i)
result.Set("key", key)
result.Set("first", i == 0)
result.Set("last", i == length-1)
return result
}
// Set sets a data value.
func (p *DataFrame) Set(key string, val interface{}) {
p.data[key] = val
}
// Get gets a data value.
func (p *DataFrame) Get(key string) interface{} {
return p.find([]string{key})
}
// find gets a deep data value
//
// @todo This is NOT consistent with the way we resolve data in template (cf. `evalDataPathExpression()`) ! FIX THAT !
func (p *DataFrame) find(parts []string) interface{} {
data := p.data
for i, part := range parts {
val := data[part]
if val == nil {
return nil
}
if i == len(parts)-1 {
// found
return val
}
valValue := reflect.ValueOf(val)
if valValue.Kind() != reflect.Map {
// not found
return nil
}
// continue
data = mapStringInterface(valValue)
}
// not found
return nil
}
// mapStringInterface converts any `map` to `map[string]interface{}`
func mapStringInterface(value reflect.Value) map[string]interface{} {
result := make(map[string]interface{})
for _, key := range value.MapKeys() {
result[strValue(key)] = value.MapIndex(key).Interface()
}
return result
}

View File

@ -1,65 +0,0 @@
package raymond
import (
"bytes"
"strings"
)
//
// That whole file is borrowed from https://github.com/golang/go/tree/master/src/html/escape.go
//
// With changes:
// &#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()
}

View File

@ -1,20 +0,0 @@
package raymond
import "fmt"
func ExampleEscape() {
tpl := MustParse("{{link url text}}")
tpl.RegisterHelper("link", func(url string, text string) SafeString {
return SafeString("<a href='" + Escape(url) + "'>" + Escape(text) + "</a>")
})
ctx := map[string]string{
"url": "http://www.aymerick.com/",
"text": "This is a <em>cool</em> website",
}
result := tpl.MustExec(ctx)
fmt.Print(result)
// Output: <a href='http://www.aymerick.com/'>This is a &lt;em&gt;cool&lt;/em&gt; website</a>
}

View File

@ -1,984 +0,0 @@
package raymond
import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/aymerick/raymond/ast"
)
var (
// @note borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
errorType = reflect.TypeOf((*error)(nil)).Elem()
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
zero reflect.Value
)
// evalVisitor evaluates a handlebars template with context
type evalVisitor struct {
tpl *Template
// contexts stack
ctx []reflect.Value
// current data frame (chained with parent)
dataFrame *DataFrame
// block parameters stack
blockParams []map[string]interface{}
// block statements stack
blocks []*ast.BlockStatement
// expressions stack
exprs []*ast.Expression
// memoize expressions that were function calls
exprFunc map[*ast.Expression]bool
// used for info on panic
curNode ast.Node
}
// NewEvalVisitor instanciate a new evaluation visitor with given context and initial private data frame
//
// If privData is nil, then a default data frame is created
func newEvalVisitor(tpl *Template, ctx interface{}, privData *DataFrame) *evalVisitor {
frame := privData
if frame == nil {
frame = NewDataFrame()
}
return &evalVisitor{
tpl: tpl,
ctx: []reflect.Value{reflect.ValueOf(ctx)},
dataFrame: frame,
exprFunc: make(map[*ast.Expression]bool),
}
}
// at sets current node
func (v *evalVisitor) at(node ast.Node) {
v.curNode = node
}
//
// Contexts stack
//
// pushCtx pushes new context to the stack
func (v *evalVisitor) pushCtx(ctx reflect.Value) {
v.ctx = append(v.ctx, ctx)
}
// popCtx pops last context from stack
func (v *evalVisitor) popCtx() reflect.Value {
if len(v.ctx) == 0 {
return zero
}
var result reflect.Value
result, v.ctx = v.ctx[len(v.ctx)-1], v.ctx[:len(v.ctx)-1]
return result
}
// rootCtx returns root context
func (v *evalVisitor) rootCtx() reflect.Value {
return v.ctx[0]
}
// curCtx returns current context
func (v *evalVisitor) curCtx() reflect.Value {
return v.ancestorCtx(0)
}
// ancestorCtx returns ancestor context
func (v *evalVisitor) ancestorCtx(depth int) reflect.Value {
index := len(v.ctx) - 1 - depth
if index < 0 {
return zero
}
return v.ctx[index]
}
//
// Private data frame
//
// setDataFrame sets new data frame
func (v *evalVisitor) setDataFrame(frame *DataFrame) {
v.dataFrame = frame
}
// popDataFrame sets back parent data frame
func (v *evalVisitor) popDataFrame() {
v.dataFrame = v.dataFrame.parent
}
//
// Block Parameters stack
//
// pushBlockParams pushes new block params to the stack
func (v *evalVisitor) pushBlockParams(params map[string]interface{}) {
v.blockParams = append(v.blockParams, params)
}
// popBlockParams pops last block params from stack
func (v *evalVisitor) popBlockParams() map[string]interface{} {
var result map[string]interface{}
if len(v.blockParams) == 0 {
return result
}
result, v.blockParams = v.blockParams[len(v.blockParams)-1], v.blockParams[:len(v.blockParams)-1]
return result
}
// blockParam iterates on stack to find given block parameter, and returns its value or nil if not founc
func (v *evalVisitor) blockParam(name string) interface{} {
for i := len(v.blockParams) - 1; i >= 0; i-- {
for k, v := range v.blockParams[i] {
if name == k {
return v
}
}
}
return nil
}
//
// Blocks stack
//
// pushBlock pushes new block statement to stack
func (v *evalVisitor) pushBlock(block *ast.BlockStatement) {
v.blocks = append(v.blocks, block)
}
// popBlock pops last block statement from stack
func (v *evalVisitor) popBlock() *ast.BlockStatement {
if len(v.blocks) == 0 {
return nil
}
var result *ast.BlockStatement
result, v.blocks = v.blocks[len(v.blocks)-1], v.blocks[:len(v.blocks)-1]
return result
}
// curBlock returns current block statement
func (v *evalVisitor) curBlock() *ast.BlockStatement {
if len(v.blocks) == 0 {
return nil
}
return v.blocks[len(v.blocks)-1]
}
//
// Expressions stack
//
// pushExpr pushes new expression to stack
func (v *evalVisitor) pushExpr(expression *ast.Expression) {
v.exprs = append(v.exprs, expression)
}
// popExpr pops last expression from stack
func (v *evalVisitor) popExpr() *ast.Expression {
if len(v.exprs) == 0 {
return nil
}
var result *ast.Expression
result, v.exprs = v.exprs[len(v.exprs)-1], v.exprs[:len(v.exprs)-1]
return result
}
// curExpr returns current expression
func (v *evalVisitor) curExpr() *ast.Expression {
if len(v.exprs) == 0 {
return nil
}
return v.exprs[len(v.exprs)-1]
}
//
// Error functions
//
// errPanic panics
func (v *evalVisitor) errPanic(err error) {
panic(fmt.Errorf("Evaluation error: %s\nCurrent node:\n\t%s", err, v.curNode))
}
// errorf panics with a custom message
func (v *evalVisitor) errorf(format string, args ...interface{}) {
v.errPanic(fmt.Errorf(format, args...))
}
//
// Evaluation
//
// evalProgram eEvaluates program with given context and returns string result
func (v *evalVisitor) evalProgram(program *ast.Program, ctx interface{}, data *DataFrame, key interface{}) string {
blockParams := make(map[string]interface{})
// compute block params
if len(program.BlockParams) > 0 {
blockParams[program.BlockParams[0]] = ctx
}
if (len(program.BlockParams) > 1) && (key != nil) {
blockParams[program.BlockParams[1]] = key
}
// push contexts
if len(blockParams) > 0 {
v.pushBlockParams(blockParams)
}
ctxVal := reflect.ValueOf(ctx)
if ctxVal.IsValid() {
v.pushCtx(ctxVal)
}
if data != nil {
v.setDataFrame(data)
}
// evaluate program
result, _ := program.Accept(v).(string)
// pop contexts
if data != nil {
v.popDataFrame()
}
if ctxVal.IsValid() {
v.popCtx()
}
if len(blockParams) > 0 {
v.popBlockParams()
}
return result
}
// evalPath evaluates all path parts with given context
func (v *evalVisitor) evalPath(ctx reflect.Value, parts []string, exprRoot bool) (reflect.Value, bool) {
partResolved := false
for i := 0; i < len(parts); i++ {
part := parts[i]
// "[foo bar]"" => "foo bar"
if (len(part) >= 2) && (part[0] == '[') && (part[len(part)-1] == ']') {
part = part[1 : len(part)-1]
}
ctx = v.evalField(ctx, part, exprRoot)
if !ctx.IsValid() {
break
}
// we resolved at least one part of path
partResolved = true
}
return ctx, partResolved
}
// evalField evaluates field with given context
func (v *evalVisitor) evalField(ctx reflect.Value, fieldName string, exprRoot bool) reflect.Value {
result := zero
ctx, _ = indirect(ctx)
if !ctx.IsValid() {
return result
}
// check if this is a method call
result, isMeth := v.evalMethod(ctx, fieldName, exprRoot)
if !isMeth {
switch ctx.Kind() {
case reflect.Struct:
// example: firstName => FirstName
expFieldName := strings.Title(fieldName)
// check if struct have this field and that it is exported
if tField, ok := ctx.Type().FieldByName(expFieldName); ok && (tField.PkgPath == "") {
// struct field
result = ctx.FieldByIndex(tField.Index)
}
case reflect.Map:
nameVal := reflect.ValueOf(fieldName)
if nameVal.Type().AssignableTo(ctx.Type().Key()) {
// map key
result = ctx.MapIndex(nameVal)
}
case reflect.Array, reflect.Slice:
if i, err := strconv.Atoi(fieldName); (err == nil) && (i < ctx.Len()) {
result = ctx.Index(i)
}
}
}
// check if result is a function
result, _ = indirect(result)
if result.Kind() == reflect.Func {
result = v.evalFieldFunc(fieldName, result, exprRoot)
}
return result
}
// evalFieldFunc tries to evaluate given method name, and a boolean to indicate if this was a method call
func (v *evalVisitor) evalMethod(ctx reflect.Value, name string, exprRoot bool) (reflect.Value, bool) {
if ctx.Kind() != reflect.Interface && ctx.CanAddr() {
ctx = ctx.Addr()
}
method := ctx.MethodByName(name)
if !method.IsValid() {
// example: subject() => Subject()
method = ctx.MethodByName(strings.Title(name))
}
if !method.IsValid() {
return zero, false
}
return v.evalFieldFunc(name, method, exprRoot), true
}
// evalFieldFunc evaluates given function
func (v *evalVisitor) evalFieldFunc(name string, funcVal reflect.Value, exprRoot bool) reflect.Value {
ensureValidHelper(name, funcVal)
var options *Options
if exprRoot {
// create function arg with all params/hash
expr := v.curExpr()
options = v.helperOptions(expr)
// ok, that expression was a function call
v.exprFunc[expr] = true
} else {
// we are not at root of expression, so we are a parameter... and we don't like
// infinite loops caused by trying to parse ourself forever
options = newEmptyOptions(v)
}
return v.callFunc(name, funcVal, options)
}
// findBlockParam returns node's block parameter
func (v *evalVisitor) findBlockParam(node *ast.PathExpression) (string, interface{}) {
if len(node.Parts) > 0 {
name := node.Parts[0]
if value := v.blockParam(name); value != nil {
return name, value
}
}
return "", nil
}
// evalPathExpression evaluates a path expression
func (v *evalVisitor) evalPathExpression(node *ast.PathExpression, exprRoot bool) interface{} {
var result interface{}
if name, value := v.findBlockParam(node); value != nil {
// block parameter value
// We push a new context so we can evaluate the path expression (note: this may be a bad idea).
//
// Example:
// {{#foo as |bar|}}
// {{bar.baz}}
// {{/foo}}
//
// With data:
// {"foo": {"baz": "bat"}}
newCtx := map[string]interface{}{name: value}
v.pushCtx(reflect.ValueOf(newCtx))
result = v.evalCtxPathExpression(node, exprRoot)
v.popCtx()
} else {
ctxTried := false
if node.IsDataRoot() {
// context path
result = v.evalCtxPathExpression(node, exprRoot)
ctxTried = true
}
if (result == nil) && node.Data {
// if it is @root, then we tried to evaluate with root context but nothing was found
// so let's try with private data
// private data
result = v.evalDataPathExpression(node, exprRoot)
}
if (result == nil) && !ctxTried {
// context path
result = v.evalCtxPathExpression(node, exprRoot)
}
}
return result
}
// evalDataPathExpression evaluates a private data path expression
func (v *evalVisitor) evalDataPathExpression(node *ast.PathExpression, exprRoot bool) interface{} {
// find data frame
frame := v.dataFrame
for i := node.Depth; i > 0; i-- {
if frame.parent == nil {
return nil
}
frame = frame.parent
}
// resolve data
// @note Can be changed to v.evalCtx() as context can't be an array
result, _ := v.evalCtxPath(reflect.ValueOf(frame.data), node.Parts, exprRoot)
return result
}
// evalCtxPathExpression evaluates a context path expression
func (v *evalVisitor) evalCtxPathExpression(node *ast.PathExpression, exprRoot bool) interface{} {
v.at(node)
if node.IsDataRoot() {
// `@root` - remove the first part
parts := node.Parts[1:len(node.Parts)]
result, _ := v.evalCtxPath(v.rootCtx(), parts, exprRoot)
return result
}
return v.evalDepthPath(node.Depth, node.Parts, exprRoot)
}
// evalDepthPath iterates on contexts, starting at given depth, until there is one that resolve given path parts
func (v *evalVisitor) evalDepthPath(depth int, parts []string, exprRoot bool) interface{} {
var result interface{}
partResolved := false
ctx := v.ancestorCtx(depth)
for (result == nil) && ctx.IsValid() && (depth <= len(v.ctx) && !partResolved) {
// try with context
result, partResolved = v.evalCtxPath(ctx, parts, exprRoot)
// As soon as we find the first part of a path, we must not try to resolve with parent context if result is finally `nil`
// Reference: "Dotted Names - Context Precedence" mustache test
if !partResolved && (result == nil) {
// try with previous context
depth++
ctx = v.ancestorCtx(depth)
}
}
return result
}
// evalCtxPath evaluates path with given context
func (v *evalVisitor) evalCtxPath(ctx reflect.Value, parts []string, exprRoot bool) (interface{}, bool) {
var result interface{}
partResolved := false
switch ctx.Kind() {
case reflect.Array, reflect.Slice:
// Array context
var results []interface{}
for i := 0; i < ctx.Len(); i++ {
value, _ := v.evalPath(ctx.Index(i), parts, exprRoot)
if value.IsValid() {
results = append(results, value.Interface())
}
}
result = results
default:
// NOT array context
var value reflect.Value
value, partResolved = v.evalPath(ctx, parts, exprRoot)
if value.IsValid() {
result = value.Interface()
}
}
return result, partResolved
}
//
// Helpers
//
// isHelperCall returns true if given expression is a helper call
func (v *evalVisitor) isHelperCall(node *ast.Expression) bool {
if helperName := node.HelperName(); helperName != "" {
return v.findHelper(helperName) != zero
}
return false
}
// findHelper finds given helper
func (v *evalVisitor) findHelper(name string) reflect.Value {
// check template helpers
if h := v.tpl.findHelper(name); h != zero {
return h
}
// check global helpers
return findHelper(name)
}
// callFunc calls function with given options
func (v *evalVisitor) callFunc(name string, funcVal reflect.Value, options *Options) reflect.Value {
params := options.Params()
funcType := funcVal.Type()
// @todo Is there a better way to do that ?
strType := reflect.TypeOf("")
boolType := reflect.TypeOf(true)
// check parameters number
addOptions := false
numIn := funcType.NumIn()
if numIn == len(params)+1 {
lastArgType := funcType.In(numIn - 1)
if reflect.TypeOf(options).AssignableTo(lastArgType) {
addOptions = true
}
}
if !addOptions && (len(params) != numIn) {
v.errorf("Helper '%s' called with wrong number of arguments, needed %d but got %d", name, numIn, len(params))
}
// check and collect arguments
args := make([]reflect.Value, numIn)
for i, param := range params {
arg := reflect.ValueOf(param)
argType := funcType.In(i)
if !arg.IsValid() {
if canBeNil(argType) {
arg = reflect.Zero(argType)
} else if argType.Kind() == reflect.String {
arg = reflect.ValueOf("")
} else {
// @todo Maybe we can panic on that
return reflect.Zero(strType)
}
}
if !arg.Type().AssignableTo(argType) {
if strType.AssignableTo(argType) {
// convert parameter to string
arg = reflect.ValueOf(strValue(arg))
} else if boolType.AssignableTo(argType) {
// convert parameter to bool
val, _ := isTrueValue(arg)
arg = reflect.ValueOf(val)
} else {
v.errorf("Helper %s called with argument %d with type %s but it should be %s", name, i, arg.Type(), argType)
}
}
args[i] = arg
}
if addOptions {
args[numIn-1] = reflect.ValueOf(options)
}
result := funcVal.Call(args)
return result[0]
}
// callHelper invoqs helper function for given expression node
func (v *evalVisitor) callHelper(name string, helper reflect.Value, node *ast.Expression) interface{} {
result := v.callFunc(name, helper, v.helperOptions(node))
if !result.IsValid() {
return nil
}
// @todo We maybe want to ensure here that helper returned a string or a SafeString
return result.Interface()
}
// helperOptions computes helper options argument from an expression
func (v *evalVisitor) helperOptions(node *ast.Expression) *Options {
var params []interface{}
var hash map[string]interface{}
for _, paramNode := range node.Params {
param := paramNode.Accept(v)
params = append(params, param)
}
if node.Hash != nil {
hash, _ = node.Hash.Accept(v).(map[string]interface{})
}
return newOptions(v, params, hash)
}
//
// Partials
//
// findPartial finds given partial
func (v *evalVisitor) findPartial(name string) *partial {
// check template partials
if p := v.tpl.findPartial(name); p != nil {
return p
}
// check global partials
return findPartial(name)
}
// partialContext computes partial context
func (v *evalVisitor) partialContext(node *ast.PartialStatement) reflect.Value {
if nb := len(node.Params); nb > 1 {
v.errorf("Unsupported number of partial arguments: %d", nb)
}
if (len(node.Params) > 0) && (node.Hash != nil) {
v.errorf("Passing both context and named parameters to a partial is not allowed")
}
if len(node.Params) == 1 {
return reflect.ValueOf(node.Params[0].Accept(v))
}
if node.Hash != nil {
hash, _ := node.Hash.Accept(v).(map[string]interface{})
return reflect.ValueOf(hash)
}
return zero
}
// evalPartial evaluates a partial
func (v *evalVisitor) evalPartial(p *partial, node *ast.PartialStatement) string {
// get partial template
partialTpl, err := p.template()
if err != nil {
v.errPanic(err)
}
// push partial context
ctx := v.partialContext(node)
if ctx.IsValid() {
v.pushCtx(ctx)
}
// evaluate partial template
result, _ := partialTpl.program.Accept(v).(string)
// ident partial
result = indentLines(result, node.Indent)
if ctx.IsValid() {
v.popCtx()
}
return result
}
// indentLines indents all lines of given string
func indentLines(str string, indent string) string {
if indent == "" {
return str
}
var indented []string
lines := strings.Split(str, "\n")
for i, line := range lines {
if (i == (len(lines) - 1)) && (line == "") {
// input string ends with a new line
indented = append(indented, line)
} else {
indented = append(indented, indent+line)
}
}
return strings.Join(indented, "\n")
}
//
// Functions
//
// wasFuncCall returns true if given expression was a function call
func (v *evalVisitor) wasFuncCall(node *ast.Expression) bool {
// check if expression was tagged as a function call
return v.exprFunc[node]
}
//
// Visitor interface
//
// Statements
// VisitProgram implements corresponding Visitor interface method
func (v *evalVisitor) VisitProgram(node *ast.Program) interface{} {
v.at(node)
buf := new(bytes.Buffer)
for _, n := range node.Body {
if str := Str(n.Accept(v)); str != "" {
if _, err := buf.Write([]byte(str)); err != nil {
v.errPanic(err)
}
}
}
return buf.String()
}
// VisitMustache implements corresponding Visitor interface method
func (v *evalVisitor) VisitMustache(node *ast.MustacheStatement) interface{} {
v.at(node)
// evaluate expression
expr := node.Expression.Accept(v)
// check if this is a safe string
isSafe := isSafeString(expr)
// get string value
str := Str(expr)
if !isSafe && !node.Unescaped {
// escape html
str = Escape(str)
}
return str
}
// VisitBlock implements corresponding Visitor interface method
func (v *evalVisitor) VisitBlock(node *ast.BlockStatement) interface{} {
v.at(node)
v.pushBlock(node)
var result interface{}
// evaluate expression
expr := node.Expression.Accept(v)
if v.isHelperCall(node.Expression) || v.wasFuncCall(node.Expression) {
// it is the responsability of the helper/function to evaluate block
result = expr
} else {
val := reflect.ValueOf(expr)
truth, _ := isTrueValue(val)
if truth {
if node.Program != nil {
switch val.Kind() {
case reflect.Array, reflect.Slice:
concat := ""
// Array context
for i := 0; i < val.Len(); i++ {
// Computes new private data frame
frame := v.dataFrame.newIterDataFrame(val.Len(), i, nil)
// Evaluate program
concat += v.evalProgram(node.Program, val.Index(i).Interface(), frame, i)
}
result = concat
default:
// NOT array
result = v.evalProgram(node.Program, expr, nil, nil)
}
}
} else if node.Inverse != nil {
result, _ = node.Inverse.Accept(v).(string)
}
}
v.popBlock()
return result
}
// VisitPartial implements corresponding Visitor interface method
func (v *evalVisitor) VisitPartial(node *ast.PartialStatement) interface{} {
v.at(node)
// partialName: helperName | sexpr
name, ok := ast.HelperNameStr(node.Name)
if !ok {
if subExpr, ok := node.Name.(*ast.SubExpression); ok {
name, _ = subExpr.Accept(v).(string)
}
}
if name == "" {
v.errorf("Unexpected partial name: %q", node.Name)
}
partial := v.findPartial(name)
if partial == nil {
v.errorf("Partial not found: %s", name)
}
return v.evalPartial(partial, node)
}
// VisitContent implements corresponding Visitor interface method
func (v *evalVisitor) VisitContent(node *ast.ContentStatement) interface{} {
v.at(node)
// write content as is
return node.Value
}
// VisitComment implements corresponding Visitor interface method
func (v *evalVisitor) VisitComment(node *ast.CommentStatement) interface{} {
v.at(node)
// ignore comments
return ""
}
// Expressions
// VisitExpression implements corresponding Visitor interface method
func (v *evalVisitor) VisitExpression(node *ast.Expression) interface{} {
v.at(node)
var result interface{}
done := false
v.pushExpr(node)
// helper call
if helperName := node.HelperName(); helperName != "" {
if helper := v.findHelper(helperName); helper != zero {
result = v.callHelper(helperName, helper, node)
done = true
}
}
if !done {
// literal
if literal, ok := node.LiteralStr(); ok {
if val := v.evalField(v.curCtx(), literal, true); val.IsValid() {
result = val.Interface()
done = true
}
}
}
if !done {
// field path
if path := node.FieldPath(); path != nil {
// @todo Find a cleaner way ! Don't break the pattern !
// this is an exception to visitor pattern, because we need to pass the info
// that this path is at root of current expression
if val := v.evalPathExpression(path, true); val != nil {
result = val
}
}
}
v.popExpr()
return result
}
// VisitSubExpression implements corresponding Visitor interface method
func (v *evalVisitor) VisitSubExpression(node *ast.SubExpression) interface{} {
v.at(node)
return node.Expression.Accept(v)
}
// VisitPath implements corresponding Visitor interface method
func (v *evalVisitor) VisitPath(node *ast.PathExpression) interface{} {
return v.evalPathExpression(node, false)
}
// Literals
// VisitString implements corresponding Visitor interface method
func (v *evalVisitor) VisitString(node *ast.StringLiteral) interface{} {
v.at(node)
return node.Value
}
// VisitBoolean implements corresponding Visitor interface method
func (v *evalVisitor) VisitBoolean(node *ast.BooleanLiteral) interface{} {
v.at(node)
return node.Value
}
// VisitNumber implements corresponding Visitor interface method
func (v *evalVisitor) VisitNumber(node *ast.NumberLiteral) interface{} {
v.at(node)
return node.Number()
}
// Miscellaneous
// VisitHash implements corresponding Visitor interface method
func (v *evalVisitor) VisitHash(node *ast.Hash) interface{} {
v.at(node)
result := make(map[string]interface{})
for _, pair := range node.Pairs {
if value := pair.Accept(v); value != nil {
result[pair.Key] = value
}
}
return result
}
// VisitHashPair implements corresponding Visitor interface method
func (v *evalVisitor) VisitHashPair(node *ast.HashPair) interface{} {
v.at(node)
return node.Val.Accept(v)
}

View File

@ -1,215 +0,0 @@
package raymond
import "testing"
var evalTests = []Test{
{
"only content",
"this is content",
nil, nil, nil, nil,
"this is content",
},
{
"checks path in parent contexts",
"{{#a}}{{one}}{{#b}}{{one}}{{two}}{{one}}{{/b}}{{/a}}",
map[string]interface{}{"a": map[string]int{"one": 1}, "b": map[string]int{"two": 2}},
nil, nil, nil,
"1121",
},
{
"block params",
"{{#foo as |bar|}}{{bar}}{{/foo}}{{bar}}",
map[string]string{"foo": "baz", "bar": "bat"},
nil, nil, nil,
"bazbat",
},
{
"block params on array",
"{{#foo as |bar i|}}{{i}}.{{bar}} {{/foo}}",
map[string][]string{"foo": {"baz", "bar", "bat"}},
nil, nil, nil,
"0.baz 1.bar 2.bat ",
},
{
"nested block params",
"{{#foos as |foo iFoo|}}{{#wats as |wat iWat|}}{{iFoo}}.{{iWat}}.{{foo}}-{{wat}} {{/wats}}{{/foos}}",
map[string][]string{"foos": {"baz", "bar"}, "wats": {"the", "phoque"}},
nil, nil, nil,
"0.0.baz-the 0.1.baz-phoque 1.0.bar-the 1.1.bar-phoque ",
},
{
"block params with path reference",
"{{#foo as |bar|}}{{bar.baz}}{{/foo}}",
map[string]map[string]string{"foo": {"baz": "bat"}},
nil, nil, nil,
"bat",
},
{
"falsy block evaluation",
"{{#foo}}bar{{/foo}} baz",
map[string]interface{}{"foo": false},
nil, nil, nil,
" baz",
},
{
"block helper returns a SafeString",
"{{title}} - {{#bold}}{{body}}{{/bold}}",
map[string]string{
"title": "My new blog post",
"body": "I have so many things to say!",
},
nil,
map[string]interface{}{"bold": func(options *Options) SafeString {
return SafeString(`<div class="mybold">` + options.Fn() + "</div>")
}},
nil,
`My new blog post - <div class="mybold">I have so many things to say!</div>`,
},
{
"chained blocks",
"{{#if a}}A{{else if b}}B{{else}}C{{/if}}",
map[string]interface{}{"b": false},
nil, nil, nil,
"C",
},
// @todo Test with a "../../path" (depth 2 path) while context is only depth 1
}
func TestEval(t *testing.T) {
t.Parallel()
launchTests(t, evalTests)
}
var evalErrors = []Test{
{
"functions with wrong number of arguments",
`{{foo "bar"}}`,
map[string]interface{}{"foo": func(a string, b string) string { return "foo" }},
nil, nil, nil,
"Helper 'foo' called with wrong number of arguments, needed 2 but got 1",
},
{
"functions with wrong number of returned values (1)",
"{{foo}}",
map[string]interface{}{"foo": func() {}},
nil, nil, nil,
"Helper function must return a string or a SafeString",
},
{
"functions with wrong number of returned values (2)",
"{{foo}}",
map[string]interface{}{"foo": func() (string, bool, string) { return "foo", true, "bar" }},
nil, nil, nil,
"Helper function must return a string or a SafeString",
},
}
func TestEvalErrors(t *testing.T) {
launchErrorTests(t, evalErrors)
}
func TestEvalStruct(t *testing.T) {
t.Parallel()
source := `<div class="post">
<h1>By {{author.FirstName}} {{Author.lastName}}</h1>
<div class="body">{{Body}}</div>
<h1>Comments</h1>
{{#each comments}}
<h2>By {{Author.FirstName}} {{author.LastName}}</h2>
<div class="body">{{body}}</div>
{{/each}}
</div>`
expected := `<div class="post">
<h1>By Jean Valjean</h1>
<div class="body">Life is difficult</div>
<h1>Comments</h1>
<h2>By Marcel Beliveau</h2>
<div class="body">LOL!</div>
</div>`
type Person struct {
FirstName string
LastName string
}
type Comment struct {
Author Person
Body string
}
type Post struct {
Author Person
Body string
Comments []Comment
}
ctx := Post{
Person{"Jean", "Valjean"},
"Life is difficult",
[]Comment{
Comment{
Person{"Marcel", "Beliveau"},
"LOL!",
},
},
}
output := MustRender(source, ctx)
if output != expected {
t.Errorf("Failed to evaluate with struct context")
}
}
type TestFoo struct {
}
func (t *TestFoo) Subject() string {
return "foo"
}
func TestEvalMethod(t *testing.T) {
t.Parallel()
source := `Subject is {{subject}}! YES I SAID {{Subject}}!`
expected := `Subject is foo! YES I SAID foo!`
ctx := &TestFoo{}
output := MustRender(source, ctx)
if output != expected {
t.Errorf("Failed to evaluate struct method: %s", output)
}
}
type TestBar struct {
}
func (t *TestBar) Subject() interface{} {
return testBar
}
func testBar() string {
return "bar"
}
func TestEvalMethodReturningFunc(t *testing.T) {
t.Parallel()
source := `Subject is {{subject}}! YES I SAID {{Subject}}!`
expected := `Subject is bar! YES I SAID bar!`
ctx := &TestBar{}
output := MustRender(source, ctx)
if output != expected {
t.Errorf("Failed to evaluate struct method: %s", output)
}
}

View File

@ -1,100 +0,0 @@
package handlebars
import (
"fmt"
"io/ioutil"
"path"
"strconv"
"testing"
"github.com/aymerick/raymond"
)
// cf. https://github.com/aymerick/go-fuzz-tests/raymond
const 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())
}
}
}
}
}
}

View File

@ -1,650 +0,0 @@
package handlebars
import (
"fmt"
"regexp"
"testing"
"github.com/aymerick/raymond"
)
//
// Those tests come from:
// https://github.com/wycats/handlebars.js/blob/master/spec/basic.js
//
var basicTests = []Test{
{
"most basic",
"{{foo}}",
map[string]string{"foo": "foo"},
nil, nil, nil,
"foo",
},
{
"escaping (1)",
"\\{{foo}}",
map[string]string{"foo": "food"},
nil, nil, nil,
"{{foo}}",
},
{
"escaping (2)",
"content \\{{foo}}",
map[string]string{},
nil, nil, nil,
"content {{foo}}",
},
{
"escaping (3)",
"\\\\{{foo}}",
map[string]string{"foo": "food"},
nil, nil, nil,
"\\food",
},
{
"escaping (4)",
"content \\\\{{foo}}",
map[string]string{"foo": "food"},
nil, nil, nil,
"content \\food",
},
{
"escaping (5)",
"\\\\ {{foo}}",
map[string]string{"foo": "food"},
nil, nil, nil,
"\\\\ food",
},
{
"compiling with a basic context",
"Goodbye\n{{cruel}}\n{{world}}!",
map[string]string{"cruel": "cruel", "world": "world"},
nil, nil, nil,
"Goodbye\ncruel\nworld!",
},
{
"compiling with an undefined context (1)",
"Goodbye\n{{cruel}}\n{{world.bar}}!",
nil, nil, nil, nil,
"Goodbye\n\n!",
},
{
"compiling with an undefined context (2)",
"{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}",
nil, nil, nil, nil,
"Goodbye",
},
{
"comments (1)",
"{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!",
map[string]string{"cruel": "cruel", "world": "world"},
nil, nil, nil,
"Goodbye\ncruel\nworld!",
},
{
"comments (2)",
" {{~! comment ~}} blah",
nil, nil, nil, nil,
"blah",
},
{
"comments (3)",
" {{~!-- long-comment --~}} blah",
nil, nil, nil, nil,
"blah",
},
{
"comments (4)",
" {{! comment ~}} blah",
nil, nil, nil, nil,
" blah",
},
{
"comments (5)",
" {{!-- long-comment --~}} blah",
nil, nil, nil, nil,
" blah",
},
{
"comments (6)",
" {{~! comment}} blah",
nil, nil, nil, nil,
" blah",
},
{
"comments (7)",
" {{~!-- long-comment --}} blah",
nil, nil, nil, nil,
" blah",
},
{
"boolean (1)",
"{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!",
map[string]interface{}{"goodbye": true, "world": "world"},
nil, nil, nil,
"GOODBYE cruel world!",
},
{
"boolean (2)",
"{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!",
map[string]interface{}{"goodbye": false, "world": "world"},
nil, nil, nil,
"cruel world!",
},
{
"zeros (1)",
"num1: {{num1}}, num2: {{num2}}",
map[string]interface{}{"num1": 42, "num2": 0},
nil, nil, nil,
"num1: 42, num2: 0",
},
{
"zeros (2)",
"num: {{.}}",
0,
nil, nil, nil,
"num: 0",
},
{
"zeros (3)",
"num: {{num1/num2}}",
map[string]map[string]interface{}{"num1": {"num2": 0}},
nil, nil, nil,
"num: 0",
},
{
"false (1)",
"val1: {{val1}}, val2: {{val2}}",
map[string]interface{}{"val1": false, "val2": false},
nil, nil, nil,
"val1: false, val2: false",
},
{
"false (2)",
"val: {{.}}",
false,
nil, nil, nil,
"val: false",
},
{
"false (3)",
"val: {{val1/val2}}",
map[string]map[string]interface{}{"val1": {"val2": false}},
nil, nil, nil,
"val: false",
},
{
"false (4)",
"val1: {{{val1}}}, val2: {{{val2}}}",
map[string]interface{}{"val1": false, "val2": false},
nil, nil, nil,
"val1: false, val2: false",
},
{
"false (5)",
"val: {{{val1/val2}}}",
map[string]map[string]interface{}{"val1": {"val2": false}},
nil, nil, nil,
"val: false",
},
{
"newlines (1)",
"Alan's\nTest",
nil, nil, nil, nil,
"Alan's\nTest",
},
{
"newlines (2)",
"Alan's\rTest",
nil, nil, nil, nil,
"Alan's\rTest",
},
{
"escaping text (1)",
"Awesome's",
map[string]string{},
nil, nil, nil,
"Awesome's",
},
{
"escaping text (2)",
"Awesome\\",
map[string]string{},
nil, nil, nil,
"Awesome\\",
},
{
"escaping text (3)",
"Awesome\\\\ foo",
map[string]string{},
nil, nil, nil,
"Awesome\\\\ foo",
},
{
"escaping text (4)",
"Awesome {{foo}}",
map[string]string{"foo": "\\"},
nil, nil, nil,
"Awesome \\",
},
{
"escaping text (5)",
" ' ' ",
map[string]string{},
nil, nil, nil,
" ' ' ",
},
{
"escaping expressions (6)",
"{{{awesome}}}",
map[string]string{"awesome": "&'\\<>"},
nil, nil, nil,
"&'\\<>",
},
{
"escaping expressions (7)",
"{{&awesome}}",
map[string]string{"awesome": "&'\\<>"},
nil, nil, nil,
"&'\\<>",
},
{
"escaping expressions (8)",
"{{awesome}}",
map[string]string{"awesome": "&\"'`\\<>"},
nil, nil, nil,
"&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)
}
}
}

View File

@ -1,208 +0,0 @@
package handlebars
import "testing"
//
// Those tests come from:
// https://github.com/wycats/handlebars.js/blob/master/spec/blocks.js
//
var blocksTests = []Test{
{
"array (1) - Arrays iterate over the contents when not empty",
"{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!",
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
nil, nil, nil,
"goodbye! Goodbye! GOODBYE! cruel world!",
},
{
"array (2) - Arrays ignore the contents when empty",
"{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!",
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
nil, nil, nil,
"cruel world!",
},
{
"array without data",
"{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}",
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
nil, nil, nil,
"goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE",
},
{
"array with @index - The @index variable is used",
"{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!",
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
nil, nil, nil,
"0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!",
},
{
"empty block (1) - Arrays iterate over the contents when not empty",
"{{#goodbyes}}{{/goodbyes}}cruel {{world}}!",
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
nil, nil, nil,
"cruel world!",
},
{
"empty block (1) - Arrays ignore the contents when empty",
"{{#goodbyes}}{{/goodbyes}}cruel {{world}}!",
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
nil, nil, nil,
"cruel world!",
},
{
"block with complex lookup - Templates can access variables in contexts up the stack with relative path syntax",
"{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}",
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "name": "Alan"},
nil, nil, nil,
"goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ",
},
{
"multiple blocks with complex lookup",
"{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}",
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "name": "Alan"},
nil, nil, nil,
"AlanAlanAlanAlanAlanAlan",
},
// @todo "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}" should throw error
{
"block with deep nested complex lookup",
"{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}",
map[string]interface{}{"omg": "OMG!", "outer": []map[string]interface{}{{"sibling": "sad", "inner": []map[string]string{{"text": "goodbye"}}}}},
nil, nil, nil,
"Goodbye cruel sad OMG!",
},
{
"inverted sections with unset value - Inverted section rendered when value isn't set.",
"{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}",
map[string]interface{}{},
nil, nil, nil,
"Right On!",
},
{
"inverted sections with false value - Inverted section rendered when value is false.",
"{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}",
map[string]interface{}{"goodbyes": false},
nil, nil, nil,
"Right On!",
},
{
"inverted section with empty set - Inverted section rendered when value is empty set.",
"{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}",
map[string]interface{}{"goodbyes": []interface{}{}},
nil, nil, nil,
"Right On!",
},
{
"block inverted sections",
"{{#people}}{{name}}{{^}}{{none}}{{/people}}",
map[string]interface{}{"none": "No people"},
nil, nil, nil,
"No people",
},
{
"chained inverted sections (1)",
"{{#people}}{{name}}{{else if none}}{{none}}{{/people}}",
map[string]interface{}{"none": "No people"},
nil, nil, nil,
"No people",
},
{
"chained inverted sections (2)",
"{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}",
map[string]interface{}{"none": "No people"},
nil, nil, nil,
"No people",
},
{
"chained inverted sections (3)",
"{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}",
map[string]interface{}{"none": "No people"},
nil, nil, nil,
"No people",
},
// @todo "{{#people}}{{name}}{{else if none}}{{none}}{{/if}}" should throw error
{
"block inverted sections with empty arrays",
"{{#people}}{{name}}{{^}}{{none}}{{/people}}",
map[string]interface{}{"none": "No people", "people": map[string]interface{}{}},
nil, nil, nil,
"No people",
},
{
"block standalone else sections (1)",
"{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n",
map[string]interface{}{"none": "No people"},
nil, nil, nil,
"No people\n",
},
{
"block standalone else sections (2)",
"{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n",
map[string]interface{}{"none": "No people"},
nil, nil, nil,
"No people\n",
},
{
"block standalone else sections (3)",
"{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n",
map[string]interface{}{"none": "No people"},
nil, nil, nil,
"No people\n",
},
{
"block standalone chained else sections (1)",
"{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n",
map[string]interface{}{"none": "No people"},
nil, nil, nil,
"No people\n",
},
{
"block standalone chained else sections (2)",
"{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n",
map[string]interface{}{"none": "No people"},
nil, nil, nil,
"No people\n",
},
{
"should handle nesting",
"{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.",
map[string]interface{}{"data": []int{1, 3, 5}},
nil, nil, nil,
"1\n3\n5\nOK.",
},
// // @todo compat mode
// {
// "block with deep recursive lookup lookup",
// "{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}",
// map[string]interface{}{"omg": "OMG!", "outer": []map[string]interface{}{{"inner": []map[string]string{{"text": "goodbye"}}}}},
// nil,
// nil,
// nil,
// "Goodbye cruel OMG!",
// },
// // @todo compat mode
// {
// "block with deep recursive pathed lookup",
// "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}",
// map[string]interface{}{"omg": map[string]string{"yes": "OMG!"}, "outer": []map[string]interface{}{{"inner": []map[string]string{{"yes": "no", "text": "goodbye"}}}}},
// nil,
// nil,
// nil,
// "Goodbye cruel OMG!",
// },
{
"block with missed recursive lookup",
"{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}",
map[string]interface{}{"omg": map[string]string{"no": "OMG!"}, "outer": []map[string]interface{}{{"inner": []map[string]string{{"yes": "no", "text": "goodbye"}}}}},
nil, nil, nil,
"Goodbye cruel ",
},
}
func TestBlocks(t *testing.T) {
launchTests(t, blocksTests)
}

View File

@ -1,341 +0,0 @@
package handlebars
import "testing"
//
// Those tests come from:
// https://github.com/wycats/handlebars.js/blob/master/spec/builtin.js
//
var builtinsTests = []Test{
{
"#if - if with boolean argument shows the contents when true",
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
map[string]interface{}{"goodbye": true, "world": "world"},
nil, nil, nil,
"GOODBYE cruel world!",
},
{
"#if - if with string argument shows the contents",
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
map[string]interface{}{"goodbye": "dummy", "world": "world"},
nil, nil, nil,
"GOODBYE cruel world!",
},
{
"#if - if with boolean argument does not show the contents when false",
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
map[string]interface{}{"goodbye": false, "world": "world"},
nil, nil, nil,
"cruel world!",
},
{
"#if - if with undefined does not show the contents",
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
map[string]interface{}{"world": "world"},
nil, nil, nil,
"cruel world!",
},
{
"#if - if with non-empty array shows the contents",
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
map[string]interface{}{"goodbye": []string{"foo"}, "world": "world"},
nil, nil, nil,
"GOODBYE cruel world!",
},
{
"#if - if with empty array does not show the contents",
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
map[string]interface{}{"goodbye": []string{}, "world": "world"},
nil, nil, nil,
"cruel world!",
},
{
"#if - if with zero does not show the contents",
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
map[string]interface{}{"goodbye": 0, "world": "world"},
nil, nil, nil,
"cruel world!",
},
{
"#if - if with zero and includeZero option shows the contents",
"{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!",
map[string]interface{}{"goodbye": 0, "world": "world"},
nil, nil, nil,
"GOODBYE cruel world!",
},
{
"#if - if with function shows the contents when function returns true",
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
map[string]interface{}{
"goodbye": func() bool { return true },
"world": "world",
},
nil, nil, nil,
"GOODBYE cruel world!",
},
{
"#if - if with function shows the contents when function returns string",
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
map[string]interface{}{
"goodbye": func() string { return "world" },
"world": "world",
},
nil, nil, nil,
"GOODBYE cruel world!",
},
{
"#if - if with function does not show the contents when returns false",
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
map[string]interface{}{
"goodbye": func() bool { return false },
"world": "world",
},
nil, nil, nil,
"cruel world!",
},
{
"#if - if with function does not show the contents when returns undefined",
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
map[string]interface{}{
"goodbye": func() interface{} { return nil },
"world": "world",
},
nil, nil, nil,
"cruel world!",
},
{
"#with",
"{{#with person}}{{first}} {{last}}{{/with}}",
map[string]interface{}{"person": map[string]string{"first": "Alan", "last": "Johnson"}},
nil, nil, nil,
"Alan Johnson",
},
{
"#with - with with function argument",
"{{#with person}}{{first}} {{last}}{{/with}}",
map[string]interface{}{
"person": func() map[string]string { return map[string]string{"first": "Alan", "last": "Johnson"} },
}, nil, nil, nil,
"Alan Johnson",
},
{
"#with - with with else",
"{{#with person}}Person is present{{else}}Person is not present{{/with}}",
map[string]interface{}{},
nil, nil, nil,
"Person is not present",
},
{
"#each - each with array argument iterates over the contents when not empty",
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
nil, nil, nil,
"goodbye! Goodbye! GOODBYE! cruel world!",
},
{
"#each - each with array argument ignores the contents when empty",
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
nil, nil, nil,
"cruel world!",
},
{
"#each - each without data (1)",
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
nil, nil, nil,
"goodbye! Goodbye! GOODBYE! cruel world!",
},
{
"#each - each without data (2)",
"{{#each .}}{{.}}{{/each}}",
map[string]interface{}{"goodbyes": "cruel", "world": "world"},
nil, nil, nil,
// note: a go hash is not ordered, so result may vary, this behaviour differs from the JS implementation
[]string{"cruelworld", "worldcruel"},
},
{
"#each - each without context",
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
nil, nil, nil, nil,
"cruel !",
},
// NOTE: we test with a map instead of an object
{
"#each - each with an object and @key (map)",
"{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!",
map[string]interface{}{"goodbyes": map[interface{}]map[string]string{"<b>#1</b>": {"text": "goodbye"}, 2: {"text": "GOODBYE"}}, "world": "world"},
nil, nil, nil,
[]string{"&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)
}

View File

@ -1,300 +0,0 @@
package handlebars
import (
"testing"
"github.com/aymerick/raymond"
)
//
// Those tests come from:
// https://github.com/wycats/handlebars.js/blob/master/spec/data.js
//
var dataTests = []Test{
{
"passing in data to a compiled function that expects data - works with helpers",
"{{hello}}",
map[string]string{"noun": "cat"},
map[string]interface{}{"adjective": "happy"},
map[string]interface{}{"hello": func(options *raymond.Options) string {
return options.DataStr("adjective") + " " + options.ValueStr("noun")
}},
nil,
"happy cat",
},
{
"data can be looked up via @foo",
"{{@hello}}",
nil,
map[string]interface{}{"hello": "hello"},
nil, nil,
"hello",
},
{
"deep @foo triggers automatic top-level data",
`{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}`,
map[string]bool{"foo": true},
map[string]interface{}{"hello": "hello"},
map[string]interface{}{"let": func(options *raymond.Options) string {
frame := options.NewDataFrame()
for k, v := range options.Hash() {
frame.Set(k, v)
}
return options.FnData(frame)
}},
nil,
"Hello world",
},
{
"parameter data can be looked up via @foo",
`{{hello @world}}`,
nil,
map[string]interface{}{"world": "world"},
map[string]interface{}{"hello": func(context string) string {
return "Hello " + context
}},
nil,
"Hello world",
},
{
"hash values can be looked up via @foo",
`{{hello noun=@world}}`,
nil,
map[string]interface{}{"world": "world"},
map[string]interface{}{"hello": func(options *raymond.Options) string {
return "Hello " + options.HashStr("noun")
}},
nil,
"Hello world",
},
{
"nested parameter data can be looked up via @foo.bar",
`{{hello @world.bar}}`,
nil,
map[string]interface{}{"world": map[string]string{"bar": "world"}},
map[string]interface{}{"hello": func(context string) string {
return "Hello " + context
}},
nil,
"Hello world",
},
{
"nested parameter data does not fail with @world.bar",
`{{hello @world.bar}}`,
nil,
map[string]interface{}{"foo": map[string]string{"bar": "world"}},
map[string]interface{}{"hello": func(context string) string {
return "Hello " + context
}},
nil,
// @todo Test differs with JS implementation: we don't output `undefined`
"Hello ",
},
// @todo "parameter data throws when using complex scope references",
{
"data can be functions",
`{{@hello}}`,
nil,
map[string]interface{}{"hello": func() string { return "hello" }},
nil, nil,
"hello",
},
{
"data can be functions with params",
`{{@hello "hello"}}`,
nil,
map[string]interface{}{"hello": func(context string) string { return context }},
nil, nil,
"hello",
},
{
"data is inherited downstream",
`{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}`,
map[string]map[string]string{"bar": {"baz": "hello world"}},
nil,
map[string]interface{}{"let": func(options *raymond.Options) string {
frame := options.NewDataFrame()
for k, v := range options.Hash() {
frame.Set(k, v)
}
return options.FnData(frame)
}},
nil,
"2hello world1",
},
{
"passing in data to a compiled function that expects data - works with helpers in partials",
`{{>myPartial}}`,
map[string]string{"noun": "cat"},
map[string]interface{}{"adjective": "happy"},
map[string]interface{}{"hello": func(options *raymond.Options) string {
return options.DataStr("adjective") + " " + options.ValueStr("noun")
}},
map[string]string{
"myPartial": "{{hello}}",
},
"happy cat",
},
{
"passing in data to a compiled function that expects data - works with helpers and parameters",
`{{hello world}}`,
map[string]interface{}{"exclaim": true, "world": "world"},
map[string]interface{}{"adjective": "happy"},
map[string]interface{}{"hello": func(context string, options *raymond.Options) string {
str := "error"
if b, ok := options.Value("exclaim").(bool); ok {
if b {
str = "!"
} else {
str = ""
}
}
return options.DataStr("adjective") + " " + context + str
}},
nil,
"happy world!",
},
{
"passing in data to a compiled function that expects data - works with block helpers",
`{{#hello}}{{world}}{{/hello}}`,
map[string]bool{"exclaim": true},
map[string]interface{}{"adjective": "happy"},
map[string]interface{}{
"hello": func(options *raymond.Options) string {
return options.Fn()
},
"world": func(options *raymond.Options) string {
str := "error"
if b, ok := options.Value("exclaim").(bool); ok {
if b {
str = "!"
} else {
str = ""
}
}
return options.DataStr("adjective") + " world" + str
},
},
nil,
"happy world!",
},
{
"passing in data to a compiled function that expects data - works with block helpers that use ..",
`{{#hello}}{{world ../zomg}}{{/hello}}`,
map[string]interface{}{"exclaim": true, "zomg": "world"},
map[string]interface{}{"adjective": "happy"},
map[string]interface{}{
"hello": func(options *raymond.Options) string {
return options.FnWith(map[string]string{"exclaim": "?"})
},
"world": func(context string, options *raymond.Options) string {
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
},
},
nil,
"happy world?",
},
{
"passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..",
`{{#hello}}{{world ../zomg}}{{/hello}}`,
map[string]interface{}{"exclaim": true, "zomg": "world"},
map[string]interface{}{"adjective": "happy", "accessData": "#win"},
map[string]interface{}{
"hello": func(options *raymond.Options) string {
return options.DataStr("accessData") + " " + options.FnWith(map[string]string{"exclaim": "?"})
},
"world": func(context string, options *raymond.Options) string {
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
},
},
nil,
"#win happy world?",
},
{
"you can override inherited data when invoking a helper",
`{{#hello}}{{world zomg}}{{/hello}}`,
map[string]interface{}{"exclaim": true, "zomg": "planet"},
map[string]interface{}{"adjective": "happy"},
map[string]interface{}{
"hello": func(options *raymond.Options) string {
ctx := map[string]string{"exclaim": "?", "zomg": "world"}
data := options.NewDataFrame()
data.Set("adjective", "sad")
return options.FnCtxData(ctx, data)
},
"world": func(context string, options *raymond.Options) string {
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
},
},
nil,
"sad world?",
},
{
"you can override inherited data when invoking a helper with depth",
`{{#hello}}{{world ../zomg}}{{/hello}}`,
map[string]interface{}{"exclaim": true, "zomg": "world"},
map[string]interface{}{"adjective": "happy"},
map[string]interface{}{
"hello": func(options *raymond.Options) string {
ctx := map[string]string{"exclaim": "?"}
data := options.NewDataFrame()
data.Set("adjective", "sad")
return options.FnCtxData(ctx, data)
},
"world": func(context string, options *raymond.Options) string {
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
},
},
nil,
"sad world?",
},
{
"@root - the root context can be looked up via @root",
`{{@root.foo}}`,
map[string]interface{}{"foo": "hello"},
nil, nil, nil,
"hello",
},
{
"@root - passed root values take priority",
`{{@root.foo}}`,
nil,
map[string]interface{}{"root": map[string]string{"foo": "hello"}},
nil, nil,
"hello",
},
{
"nesting - the root context can be looked up via @root",
`{{#helper}}{{#helper}}{{@./depth}} {{@../depth}} {{@../../depth}}{{/helper}}{{/helper}}`,
map[string]interface{}{"foo": "hello"},
map[string]interface{}{"depth": 0},
map[string]interface{}{
"helper": func(options *raymond.Options) string {
data := options.NewDataFrame()
if depth, ok := options.Data("depth").(int); ok {
data.Set("depth", depth+1)
}
return options.FnData(data)
},
},
nil,
"2 1 0",
},
}
func TestData(t *testing.T) {
launchTests(t, dataTests)
}

View File

@ -1,2 +0,0 @@
// Package handlebars contains all the tests that come from handlebars.js project.
package handlebars

View File

@ -1,665 +0,0 @@
package handlebars
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/aymerick/raymond"
)
//
// Helpers
//
func barSuffixHelper(context string) string {
return "bar " + context
}
func echoHelper(str string) string {
return str
}
func echoNbHelper(str string, nb int) string {
result := ""
for i := 0; i < nb; i++ {
result += str
}
return result
}
func linkHelper(prefix string, options *raymond.Options) string {
return fmt.Sprintf(`<a href="%s/%s">%s</a>`, prefix, options.ValueStr("url"), options.ValueStr("text"))
}
func rawHelper(options *raymond.Options) string {
return options.Fn()
}
func rawThreeHelper(a, b, c string, options *raymond.Options) string {
return options.Fn() + a + b + c
}
func formHelper(options *raymond.Options) string {
return "<form>" + options.Fn() + "</form>"
}
func formCtxHelper(context interface{}, options *raymond.Options) string {
return "<form>" + options.FnWith(context) + "</form>"
}
func listHelper(context interface{}, options *raymond.Options) string {
val := reflect.ValueOf(context)
switch val.Kind() {
case reflect.Array, reflect.Slice:
if val.Len() > 0 {
result := "<ul>"
for i := 0; i < val.Len(); i++ {
result += "<li>"
result += options.FnWith(val.Index(i).Interface())
result += "</li>"
}
result += "</ul>"
return result
}
}
return "<p>" + options.Inverse() + "</p>"
}
func blogHelper(val string) string {
return "val is " + val
}
func equalHelper(a, b string) string {
return raymond.Str(a == b)
}
func dashHelper(a, b string) string {
return a + "-" + b
}
func concatHelper(a, b string) string {
return a + b
}
func detectDataHelper(options *raymond.Options) string {
if val, ok := options.DataFrame().Get("exclaim").(string); ok {
return val
}
return ""
}
//
// Those tests come from:
// https://github.com/wycats/handlebars.js/blob/master/spec/helper.js
//
var helpersTests = []Test{
{
"helper with complex lookup",
"{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}",
map[string]interface{}{"prefix": "/root", "goodbyes": []map[string]string{{"text": "Goodbye", "url": "goodbye"}}},
nil,
map[string]interface{}{"link": linkHelper},
nil,
`<a href="/root/goodbye">Goodbye</a>`,
},
{
"helper for raw block gets raw content",
"{{{{raw}}}} {{test}} {{{{/raw}}}}",
map[string]interface{}{"test": "hello"},
nil,
map[string]interface{}{"raw": rawHelper},
nil,
" {{test}} ",
},
{
"helper for raw block gets parameters",
"{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}",
map[string]interface{}{"test": "hello"},
nil,
map[string]interface{}{"raw": rawThreeHelper},
nil,
" {{test}} 123",
},
{
"helper block with complex lookup expression",
"{{#goodbyes}}{{../name}}{{/goodbyes}}",
map[string]interface{}{"name": "Alan"},
nil,
map[string]interface{}{"goodbyes": func(options *raymond.Options) string {
out := ""
for _, str := range []string{"Goodbye", "goodbye", "GOODBYE"} {
out += str + " " + options.FnWith(str) + "! "
}
return out
}},
nil,
"Goodbye Alan! goodbye Alan! GOODBYE Alan! ",
},
{
"helper with complex lookup and nested template",
"{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}",
map[string]interface{}{"prefix": "/root", "goodbyes": []map[string]string{{"text": "Goodbye", "url": "goodbye"}}},
nil,
map[string]interface{}{"link": linkHelper},
nil,
`<a href="/root/goodbye">Goodbye</a>`,
},
{
// note: The JS implementation returns undefined, we return empty string
"helper returning undefined value (1)",
" {{nothere}}",
map[string]interface{}{},
nil,
map[string]interface{}{"nothere": func() string {
return ""
}},
nil,
" ",
},
{
// note: The JS implementation returns undefined, we return empty string
"helper returning undefined value (2)",
" {{#nothere}}{{/nothere}}",
map[string]interface{}{},
nil,
map[string]interface{}{"nothere": func() string {
return ""
}},
nil,
" ",
},
{
"block helper",
"{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!",
map[string]interface{}{"world": "world"},
nil,
map[string]interface{}{"goodbyes": func(options *raymond.Options) string {
return options.FnWith(map[string]string{"text": "GOODBYE"})
}},
nil,
"GOODBYE! cruel world!",
},
{
"block helper staying in the same context",
"{{#form}}<p>{{name}}</p>{{/form}}",
map[string]interface{}{"name": "Yehuda"},
nil,
map[string]interface{}{"form": formHelper},
nil,
"<form><p>Yehuda</p></form>",
},
{
"block helper should have context in this",
"<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>",
map[string]interface{}{"people": []map[string]interface{}{{"name": "Alan", "id": 1}, {"name": "Yehuda", "id": 2}}},
nil,
map[string]interface{}{"link": func(options *raymond.Options) string {
return fmt.Sprintf("<a href=\"/people/%s\">%s</a>", options.ValueStr("id"), options.Fn())
}},
nil,
`<ul><li><a href="/people/1">Alan</a></li><li><a href="/people/2">Yehuda</a></li></ul>`,
},
{
"block helper for undefined value",
"{{#empty}}shouldn't render{{/empty}}",
nil, nil, nil, nil,
"",
},
{
"block helper passing a new context",
"{{#form yehuda}}<p>{{name}}</p>{{/form}}",
map[string]map[string]string{"yehuda": {"name": "Yehuda"}},
nil,
map[string]interface{}{"form": formCtxHelper},
nil,
"<form><p>Yehuda</p></form>",
},
{
"block helper passing a complex path context",
"{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}",
map[string]map[string]interface{}{"yehuda": {"name": "Yehuda", "cat": map[string]string{"name": "Harold"}}},
nil,
map[string]interface{}{"form": formCtxHelper},
nil,
"<form><p>Harold</p></form>",
},
{
"nested block helpers",
"{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}",
map[string]map[string]string{"yehuda": {"name": "Yehuda"}},
nil,
map[string]interface{}{"link": func(options *raymond.Options) string {
return fmt.Sprintf("<a href=\"%s\">%s</a>", options.ValueStr("name"), options.Fn())
}, "form": formCtxHelper},
nil,
`<form><p>Yehuda</p><a href="Yehuda">Hello</a></form>`,
},
{
"block helper inverted sections (1) - an inverse wrapper is passed in as a new context",
"{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}",
map[string][]map[string]string{"people": {{"name": "Alan"}, {"name": "Yehuda"}}},
nil,
map[string]interface{}{"list": listHelper},
nil,
`<ul><li>Alan</li><li>Yehuda</li></ul>`,
},
{
"block helper inverted sections (2) - an inverse wrapper can be optionally called",
"{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}",
map[string][]map[string]string{"people": {}},
nil,
map[string]interface{}{"list": listHelper},
nil,
`<p><em>Nobody's here</em></p>`,
},
{
"block helper inverted sections (3) - the context of an inverse is the parent of the block",
"{{#list people}}Hello{{^}}{{message}}{{/list}}",
map[string]interface{}{"people": []interface{}{}, "message": "Nobody's here"},
nil,
map[string]interface{}{"list": listHelper},
nil,
`<p>Nobody&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)
}

View File

@ -1,182 +0,0 @@
package handlebars
import "testing"
//
// Those tests come from:
// https://github.com/wycats/handlebars.js/blob/master/spec/partials.js
//
var partialsTests = []Test{
{
"basic partials",
"Dudes: {{#dudes}}{{> dude}}{{/dudes}}",
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
nil, nil,
map[string]string{"dude": "{{name}} ({{url}}) "},
"Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
},
{
"dynamic partials",
"Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}",
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
nil,
map[string]interface{}{"partial": func() string {
return "dude"
}},
map[string]string{"dude": "{{name}} ({{url}}) "},
"Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
},
// @todo "failing dynamic partials"
{
"partials with context",
"Dudes: {{>dude dudes}}",
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
nil, nil,
map[string]string{"dude": "{{#this}}{{name}} ({{url}}) {{/this}}"},
"Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
},
{
"partials with undefined context",
"Dudes: {{>dude dudes}}",
map[string]interface{}{},
nil, nil,
map[string]string{"dude": "{{foo}} Empty"},
"Dudes: Empty",
},
// @todo "partials with duplicate parameters"
{
"partials with parameters",
"Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}",
map[string]interface{}{"foo": "bar", "dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
nil, nil,
map[string]string{"dude": "{{others.foo}}{{name}} ({{url}}) "},
"Dudes: barYehuda (http://yehuda) barAlan (http://alan) ",
},
{
"partial in a partial",
"Dudes: {{#dudes}}{{>dude}}{{/dudes}}",
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
nil, nil,
map[string]string{"dude": "{{name}} {{> url}} ", "url": `<a href="{{url}}">{{url}}</a>`},
`Dudes: Yehuda <a href="http://yehuda">http://yehuda</a> Alan <a href="http://alan">http://alan</a> `,
},
// @todo "rendering undefined partial throws an exception"
// @todo "registering undefined partial throws an exception"
// SKIP: "rendering template partial in vm mode throws an exception"
// SKIP: "rendering function partial in vm mode"
{
"GH-14: a partial preceding a selector",
"Dudes: {{>dude}} {{anotherDude}}",
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
nil, nil,
map[string]string{"dude": "{{name}}"},
"Dudes: Jeepers Creepers",
},
{
"Partials with slash paths",
"Dudes: {{> shared/dude}}",
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
nil, nil,
map[string]string{"shared/dude": "{{name}}"},
"Dudes: Jeepers",
},
{
"Partials with slash and point paths",
"Dudes: {{> shared/dude.thing}}",
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
nil, nil,
map[string]string{"shared/dude.thing": "{{name}}"},
"Dudes: Jeepers",
},
// @todo "Global Partials"
// @todo "Multiple partial registration"
{
"Partials with integer path",
"Dudes: {{> 404}}",
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
nil, nil,
map[string]string{"404": "{{name}}"}, // @note Difference with JS test: partial name is a string
"Dudes: Jeepers",
},
// @note This is not supported by our implementation. But really... who cares ?
// {
// "Partials with complex path",
// "Dudes: {{> 404/asdf?.bar}}",
// map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
// nil, nil,
// map[string]string{"404/asdf?.bar": "{{name}}"},
// "Dudes: Jeepers",
// },
{
"Partials with escaped",
"Dudes: {{> [+404/asdf?.bar]}}",
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
nil, nil,
map[string]string{"+404/asdf?.bar": "{{name}}"},
"Dudes: Jeepers",
},
{
"Partials with string",
"Dudes: {{> '+404/asdf?.bar'}}",
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
nil, nil,
map[string]string{"+404/asdf?.bar": "{{name}}"},
"Dudes: Jeepers",
},
{
"should handle empty partial",
"Dudes: {{#dudes}}{{> dude}}{{/dudes}}",
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
nil, nil,
map[string]string{"dude": ""},
"Dudes: ",
},
// @todo "throw on missing partial"
// SKIP: "should pass compiler flags"
{
"standalone partials (1) - indented partials",
"Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}",
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
nil, nil,
map[string]string{"dude": "{{name}}\n"},
"Dudes:\n Yehuda\n Alan\n",
},
{
"standalone partials (2) - nested indented partials",
"Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}",
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
nil, nil,
map[string]string{"dude": "{{name}}\n {{> url}}", "url": "{{url}}!\n"},
"Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n",
},
// // @todo preventIndent option
// {
// "standalone partials (3) - prevent nested indented partials",
// "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}",
// map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
// nil, nil,
// map[string]string{"dude": "{{name}}\n {{> url}}", "url": "{{url}}!\n"},
// "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n",
// },
// @todo "compat mode"
}
func TestPartials(t *testing.T) {
launchTests(t, partialsTests)
}

View File

@ -1,209 +0,0 @@
package handlebars
import (
"testing"
"github.com/aymerick/raymond"
)
//
// Those tests come from:
// https://github.com/wycats/handlebars.js/blob/master/spec/subexpression.js
//
var subexpressionsTests = []Test{
{
"arg-less helper",
"{{foo (bar)}}!",
map[string]interface{}{},
nil,
map[string]interface{}{
"foo": func(val string) string {
return val + val
},
"bar": func() string {
return "LOL"
},
},
nil,
"LOLLOL!",
},
{
"helper w args",
"{{blog (equal a b)}}",
map[string]interface{}{"bar": "LOL"},
nil,
map[string]interface{}{
"blog": blogHelper,
"equal": equalHelper,
},
nil,
"val is true",
},
{
"mixed paths and helpers",
"{{blog baz.bat (equal a b) baz.bar}}",
map[string]interface{}{"bar": "LOL", "baz": map[string]string{"bat": "foo!", "bar": "bar!"}},
nil,
map[string]interface{}{
"blog": func(p, p2, p3 string) string {
return "val is " + p + ", " + p2 + " and " + p3
},
"equal": equalHelper,
},
nil,
"val is foo!, true and bar!",
},
{
"supports much nesting",
"{{blog (equal (equal true true) true)}}",
map[string]interface{}{"bar": "LOL"},
nil,
map[string]interface{}{
"blog": blogHelper,
"equal": equalHelper,
},
nil,
"val is true",
},
{
"GH-800 : Complex subexpressions (1)",
"{{dash 'abc' (concat a b)}}",
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
nil,
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
nil,
"abc-ab",
},
{
"GH-800 : Complex subexpressions (2)",
"{{dash d (concat a b)}}",
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
nil,
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
nil,
"d-ab",
},
{
"GH-800 : Complex subexpressions (3)",
"{{dash c.c (concat a b)}}",
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
nil,
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
nil,
"c-ab",
},
{
"GH-800 : Complex subexpressions (4)",
"{{dash (concat a b) c.c}}",
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
nil,
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
nil,
"ab-c",
},
{
"GH-800 : Complex subexpressions (5)",
"{{dash (concat a e.e) c.c}}",
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
nil,
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
nil,
"ae-c",
},
{
// note: test not relevant
"provides each nested helper invocation its own options hash",
"{{equal (equal true true) true}}",
map[string]interface{}{},
nil,
map[string]interface{}{
"equal": equalHelper,
},
nil,
"true",
},
{
"with hashes",
"{{blog (equal (equal true true) true fun='yes')}}",
map[string]interface{}{"bar": "LOL"},
nil,
map[string]interface{}{
"blog": blogHelper,
"equal": equalHelper,
},
nil,
"val is true",
},
{
"as hashes",
"{{blog fun=(equal (blog fun=1) 'val is 1')}}",
map[string]interface{}{},
nil,
map[string]interface{}{
"blog": func(options *raymond.Options) string {
return "val is " + options.HashStr("fun")
},
"equal": equalHelper,
},
nil,
"val is true",
},
{
"multiple subexpressions in a hash",
`{{input aria-label=(t "Name") placeholder=(t "Example User")}}`,
map[string]interface{}{},
nil,
map[string]interface{}{
"input": func(options *raymond.Options) raymond.SafeString {
return raymond.SafeString(`<input aria-label="` + options.HashStr("aria-label") + `" placeholder="` + options.HashStr("placeholder") + `" />`)
},
"t": func(param string) raymond.SafeString {
return raymond.SafeString(param)
},
},
nil,
`<input aria-label="Name" placeholder="Example User" />`,
},
{
"multiple subexpressions in a hash with context",
`{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}`,
map[string]map[string]string{"item": {"field": "Name", "placeholder": "Example User"}},
nil,
map[string]interface{}{
"input": func(options *raymond.Options) raymond.SafeString {
return raymond.SafeString(`<input aria-label="` + options.HashStr("aria-label") + `" placeholder="` + options.HashStr("placeholder") + `" />`)
},
"t": func(param string) raymond.SafeString {
return raymond.SafeString(param)
},
},
nil,
`<input aria-label="Name" placeholder="Example User" />`,
},
// @todo "in string params mode"
// @todo "as hashes in string params mode"
{
"subexpression functions on the context",
"{{foo (bar)}}!",
map[string]interface{}{"bar": func() string { return "LOL" }},
nil,
map[string]interface{}{
"foo": func(val string) string {
return val + val
},
},
nil,
"LOLLOL!",
},
// @todo "subexpressions can't just be property lookups" should raise error
}
func TestSubexpressions(t *testing.T) {
launchTests(t, subexpressionsTests)
}

View File

@ -1,259 +0,0 @@
package handlebars
import "testing"
//
// Those tests come from:
// https://github.com/wycats/handlebars.js/blob/master/spec/whitespace-control.js
//
var whitespaceControlTests = []Test{
{
"should strip whitespace around mustache calls (1)",
" {{~foo~}} ",
map[string]string{"foo": "bar<"},
nil, nil, nil,
"bar&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)
}

View File

@ -1,371 +0,0 @@
package raymond
import (
"fmt"
"log"
"reflect"
"sync"
)
// Options represents the options argument provided to helpers and context functions.
type Options struct {
// evaluation visitor
eval *evalVisitor
// params
params []interface{}
hash map[string]interface{}
}
// helpers stores all globally registered helpers
var helpers = make(map[string]reflect.Value)
// protects global helpers
var helpersMutex sync.RWMutex
func init() {
// register builtin helpers
RegisterHelper("if", ifHelper)
RegisterHelper("unless", unlessHelper)
RegisterHelper("with", withHelper)
RegisterHelper("each", eachHelper)
RegisterHelper("log", logHelper)
RegisterHelper("lookup", lookupHelper)
}
// RegisterHelper registers a global helper. That helper will be available to all templates.
func RegisterHelper(name string, helper interface{}) {
helpersMutex.Lock()
defer helpersMutex.Unlock()
if helpers[name] != zero {
panic(fmt.Errorf("Helper already registered: %s", name))
}
val := reflect.ValueOf(helper)
ensureValidHelper(name, val)
helpers[name] = val
}
// RegisterHelpers registers several global helpers. Those helpers will be available to all templates.
func RegisterHelpers(helpers map[string]interface{}) {
for name, helper := range helpers {
RegisterHelper(name, helper)
}
}
// ensureValidHelper panics if given helper is not valid
func ensureValidHelper(name string, funcValue reflect.Value) {
if funcValue.Kind() != reflect.Func {
panic(fmt.Errorf("Helper must be a function: %s", name))
}
funcType := funcValue.Type()
if funcType.NumOut() != 1 {
panic(fmt.Errorf("Helper function must return a string or a SafeString: %s", name))
}
// @todo Check if first returned value is a string, SafeString or interface{} ?
}
// findHelper finds a globally registered helper
func findHelper(name string) reflect.Value {
helpersMutex.RLock()
defer helpersMutex.RUnlock()
return helpers[name]
}
// newOptions instanciates a new Options
func newOptions(eval *evalVisitor, params []interface{}, hash map[string]interface{}) *Options {
return &Options{
eval: eval,
params: params,
hash: hash,
}
}
// newEmptyOptions instanciates a new empty Options
func newEmptyOptions(eval *evalVisitor) *Options {
return &Options{
eval: eval,
hash: make(map[string]interface{}),
}
}
//
// Context Values
//
// Value returns field value from current context.
func (options *Options) Value(name string) interface{} {
value := options.eval.evalField(options.eval.curCtx(), name, false)
if !value.IsValid() {
return nil
}
return value.Interface()
}
// ValueStr returns string representation of field value from current context.
func (options *Options) ValueStr(name string) string {
return Str(options.Value(name))
}
// Ctx returns current evaluation context.
func (options *Options) Ctx() interface{} {
return options.eval.curCtx().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))
}

View File

@ -1,193 +0,0 @@
package raymond
import "testing"
const (
VERBOSE = false
)
//
// Helpers
//
func barHelper(options *Options) string { return "bar" }
func echoHelper(str string, nb int) string {
result := ""
for i := 0; i < nb; i++ {
result += str
}
return result
}
func boolHelper(b bool) string {
if b {
return "yes it is"
}
return "absolutely not"
}
func gnakHelper(nb int) string {
result := ""
for i := 0; i < nb; i++ {
result += "GnAK!"
}
return result
}
//
// Tests
//
var helperTests = []Test{
{
"simple helper",
`{{foo}}`,
nil, nil,
map[string]interface{}{"foo": barHelper},
nil,
`bar`,
},
{
"helper with literal string param",
`{{echo "foo" 1}}`,
nil, nil,
map[string]interface{}{"echo": echoHelper},
nil,
`foo`,
},
{
"helper with identifier param",
`{{echo foo 1}}`,
map[string]interface{}{"foo": "bar"},
nil,
map[string]interface{}{"echo": echoHelper},
nil,
`bar`,
},
{
"helper with literal boolean param",
`{{bool true}}`,
nil, nil,
map[string]interface{}{"bool": boolHelper},
nil,
`yes it is`,
},
{
"helper with literal boolean param",
`{{bool false}}`,
nil, nil,
map[string]interface{}{"bool": boolHelper},
nil,
`absolutely not`,
},
{
"helper with literal boolean param",
`{{gnak 5}}`,
nil, nil,
map[string]interface{}{"gnak": gnakHelper},
nil,
`GnAK!GnAK!GnAK!GnAK!GnAK!`,
},
{
"helper with several parameters",
`{{echo "GnAK!" 3}}`,
nil, nil,
map[string]interface{}{"echo": echoHelper},
nil,
`GnAK!GnAK!GnAK!`,
},
{
"#if helper with true literal",
`{{#if true}}YES MAN{{/if}}`,
nil, nil, nil, nil,
`YES MAN`,
},
{
"#if helper with false literal",
`{{#if false}}YES MAN{{/if}}`,
nil, nil, nil, nil,
``,
},
{
"#if helper with truthy identifier",
`{{#if ok}}YES MAN{{/if}}`,
map[string]interface{}{"ok": true},
nil, nil, nil,
`YES MAN`,
},
{
"#if helper with falsy identifier",
`{{#if ok}}YES MAN{{/if}}`,
map[string]interface{}{"ok": false},
nil, nil, nil,
``,
},
{
"#unless helper with true literal",
`{{#unless true}}YES MAN{{/unless}}`,
nil, nil, nil, nil,
``,
},
{
"#unless helper with false literal",
`{{#unless false}}YES MAN{{/unless}}`,
nil, nil, nil, nil,
`YES MAN`,
},
{
"#unless helper with truthy identifier",
`{{#unless ok}}YES MAN{{/unless}}`,
map[string]interface{}{"ok": true},
nil, nil, nil,
``,
},
{
"#unless helper with falsy identifier",
`{{#unless ok}}YES MAN{{/unless}}`,
map[string]interface{}{"ok": false},
nil, nil, nil,
`YES MAN`,
},
}
//
// Let's go
//
func TestHelper(t *testing.T) {
t.Parallel()
launchTests(t, helperTests)
}
//
// 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)
}
}

View File

@ -1,639 +0,0 @@
// Package lexer provides a handlebars tokenizer.
package lexer
import (
"fmt"
"regexp"
"strings"
"unicode"
"unicode/utf8"
)
// References:
// - https://github.com/wycats/handlebars.js/blob/master/src/handlebars.l
// - https://github.com/golang/go/blob/master/src/text/template/parse/lex.go
const (
// Mustaches detection
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)
}

View File

@ -1,541 +0,0 @@
package lexer
import (
"fmt"
"testing"
)
type lexTest struct {
name string
input string
tokens []Token
}
// helpers
func tokContent(val string) Token { return Token{TokenContent, val, 0, 1} }
func tokID(val string) Token { return Token{TokenID, val, 0, 1} }
func tokSep(val string) Token { return Token{TokenSep, val, 0, 1} }
func tokString(val string) Token { return Token{TokenString, val, 0, 1} }
func tokNumber(val string) Token { return Token{TokenNumber, val, 0, 1} }
func tokInverse(val string) Token { return Token{TokenInverse, val, 0, 1} }
func tokBool(val string) Token { return Token{TokenBoolean, val, 0, 1} }
func tokError(val string) Token { return Token{TokenError, val, 0, 1} }
func tokComment(val string) Token { return Token{TokenComment, val, 0, 1} }
var tokEOF = Token{TokenEOF, "", 0, 1}
var tokEquals = Token{TokenEquals, "=", 0, 1}
var tokData = Token{TokenData, "@", 0, 1}
var tokOpen = Token{TokenOpen, "{{", 0, 1}
var tokOpenAmp = Token{TokenOpen, "{{&", 0, 1}
var tokOpenPartial = Token{TokenOpenPartial, "{{>", 0, 1}
var tokClose = Token{TokenClose, "}}", 0, 1}
var tokOpenStrip = Token{TokenOpen, "{{~", 0, 1}
var tokCloseStrip = Token{TokenClose, "~}}", 0, 1}
var tokOpenUnescaped = Token{TokenOpenUnescaped, "{{{", 0, 1}
var tokCloseUnescaped = Token{TokenCloseUnescaped, "}}}", 0, 1}
var tokOpenUnescapedStrip = Token{TokenOpenUnescaped, "{{~{", 0, 1}
var tokCloseUnescapedStrip = Token{TokenCloseUnescaped, "}~}}", 0, 1}
var tokOpenBlock = Token{TokenOpenBlock, "{{#", 0, 1}
var tokOpenEndBlock = Token{TokenOpenEndBlock, "{{/", 0, 1}
var tokOpenInverse = Token{TokenOpenInverse, "{{^", 0, 1}
var tokOpenInverseChain = Token{TokenOpenInverseChain, "{{else", 0, 1}
var tokOpenSexpr = Token{TokenOpenSexpr, "(", 0, 1}
var tokCloseSexpr = Token{TokenCloseSexpr, ")", 0, 1}
var tokOpenBlockParams = Token{TokenOpenBlockParams, "as |", 0, 1}
var tokCloseBlockParams = Token{TokenCloseBlockParams, "|", 0, 1}
var tokOpenRawBlock = Token{TokenOpenRawBlock, "{{{{", 0, 1}
var tokCloseRawBlock = Token{TokenCloseRawBlock, "}}}}", 0, 1}
var tokOpenEndRawBlock = Token{TokenOpenEndRawBlock, "{{{{/", 0, 1}
var lexTests = []lexTest{
{"empty", "", []Token{tokEOF}},
{"spaces", " \t\n", []Token{tokContent(" \t\n"), tokEOF}},
{"content", `now is the time`, []Token{tokContent(`now is the time`), tokEOF}},
{
`does not tokenizes identifier starting with true as boolean`,
`{{ foo truebar }}`,
[]Token{tokOpen, tokID("foo"), tokID("truebar"), tokClose, tokEOF},
},
{
`does not tokenizes identifier starting with false as boolean`,
`{{ foo falsebar }}`,
[]Token{tokOpen, tokID("foo"), tokID("falsebar"), tokClose, tokEOF},
},
{
`tokenizes raw block`,
`{{{{foo}}}} {{{{/foo}}}}`,
[]Token{tokOpenRawBlock, tokID("foo"), tokCloseRawBlock, tokContent(" "), tokOpenEndRawBlock, tokID("foo"), tokCloseRawBlock, tokEOF},
},
{
`tokenizes raw block with mustaches in content`,
`{{{{foo}}}}{{bar}}{{{{/foo}}}}`,
[]Token{tokOpenRawBlock, tokID("foo"), tokCloseRawBlock, tokContent("{{bar}}"), tokOpenEndRawBlock, tokID("foo"), tokCloseRawBlock, tokEOF},
},
{
`tokenizes @../foo`,
`{{@../foo}}`,
[]Token{tokOpen, tokData, tokID(".."), tokSep("/"), tokID("foo"), tokClose, tokEOF},
},
{
`tokenizes escaped mustaches`,
"\\{{bar}}",
[]Token{tokContent("{{bar}}"), tokEOF},
},
{
`tokenizes strip mustaches`,
`{{~ foo ~}}`,
[]Token{tokOpenStrip, tokID("foo"), tokCloseStrip, tokEOF},
},
{
`tokenizes unescaped strip mustaches`,
`{{~{ foo }~}}`,
[]Token{tokOpenUnescapedStrip, tokID("foo"), tokCloseUnescapedStrip, tokEOF},
},
//
// Next tests come from:
// https://github.com/wycats/handlebars.js/blob/master/spec/tokenizer.js
//
{
`tokenizes a simple mustache as "OPEN ID CLOSE"`,
`{{foo}}`,
[]Token{tokOpen, tokID("foo"), tokClose, tokEOF},
},
{
`supports unescaping with &`,
`{{&bar}}`,
[]Token{tokOpenAmp, tokID("bar"), tokClose, tokEOF},
},
{
`supports unescaping with {{{`,
`{{{bar}}}`,
[]Token{tokOpenUnescaped, tokID("bar"), tokCloseUnescaped, tokEOF},
},
{
`supports escaping delimiters`,
"{{foo}} \\{{bar}} {{baz}}",
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokOpen, tokID("baz"), tokClose, tokEOF},
},
{
`supports escaping multiple delimiters`,
"{{foo}} \\{{bar}} \\{{baz}}",
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokContent("{{baz}}"), tokEOF},
},
{
`supports escaping a triple stash`,
"{{foo}} \\{{{bar}}} {{baz}}",
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{{bar}}} "), tokOpen, tokID("baz"), tokClose, tokEOF},
},
{
`supports escaping escape character`,
"{{foo}} \\\\{{bar}} {{baz}}",
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" "), tokOpen, tokID("baz"), tokClose, tokEOF},
},
{
`supports escaping multiple escape characters`,
"{{foo}} \\\\{{bar}} \\\\{{baz}}",
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" \\"), tokOpen, tokID("baz"), tokClose, tokEOF},
},
{
`supports escaped mustaches after escaped escape characters`,
"{{foo}} \\\\{{bar}} \\{{baz}}",
// NOTE: JS implementation returns:
// ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT'],
// WTF is the last CONTENT ?
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" "), tokContent("{{baz}}"), tokEOF},
},
{
`supports escaped escape characters after escaped mustaches`,
"{{foo}} \\{{bar}} \\\\{{baz}}",
// NOTE: JS implementation returns:
// []Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokContent("\\"), tokOpen, tokID("baz"), tokClose, tokEOF},
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} \\"), tokOpen, tokID("baz"), tokClose, tokEOF},
},
{
`supports escaped escape character on a triple stash`,
"{{foo}} \\\\{{{bar}}} {{baz}}",
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpenUnescaped, tokID("bar"), tokCloseUnescaped, tokContent(" "), tokOpen, tokID("baz"), tokClose, tokEOF},
},
{
`tokenizes a simple path`,
`{{foo/bar}}`,
[]Token{tokOpen, tokID("foo"), tokSep("/"), tokID("bar"), tokClose, tokEOF},
},
{
`allows dot notation (1)`,
`{{foo.bar}}`,
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("bar"), tokClose, tokEOF},
},
{
`allows dot notation (2)`,
`{{foo.bar.baz}}`,
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("bar"), tokSep("."), tokID("baz"), tokClose, tokEOF},
},
{
`allows path literals with []`,
`{{foo.[bar]}}`,
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("[bar]"), tokClose, tokEOF},
},
{
`allows multiple path literals on a line with []`,
`{{foo.[bar]}}{{foo.[baz]}}`,
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("[bar]"), tokClose, tokOpen, tokID("foo"), tokSep("."), tokID("[baz]"), tokClose, tokEOF},
},
{
`tokenizes {{.}} as OPEN ID CLOSE`,
`{{.}}`,
[]Token{tokOpen, tokID("."), tokClose, tokEOF},
},
{
`tokenizes a path as "OPEN (ID SEP)* ID CLOSE"`,
`{{../foo/bar}}`,
[]Token{tokOpen, tokID(".."), tokSep("/"), tokID("foo"), tokSep("/"), tokID("bar"), tokClose, tokEOF},
},
{
`tokenizes a path with .. as a parent path`,
`{{../foo.bar}}`,
[]Token{tokOpen, tokID(".."), tokSep("/"), tokID("foo"), tokSep("."), tokID("bar"), tokClose, tokEOF},
},
{
`tokenizes a path with this/foo as OPEN ID SEP ID CLOSE`,
`{{this/foo}}`,
[]Token{tokOpen, tokID("this"), tokSep("/"), tokID("foo"), tokClose, tokEOF},
},
{
`tokenizes a simple mustache with spaces as "OPEN ID CLOSE"`,
`{{ foo }}`,
[]Token{tokOpen, tokID("foo"), tokClose, tokEOF},
},
{
`tokenizes a simple mustache with line breaks as "OPEN ID ID CLOSE"`,
"{{ foo \n bar }}",
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokClose, tokEOF},
},
{
`tokenizes raw content as "CONTENT"`,
`foo {{ bar }} baz`,
[]Token{tokContent("foo "), tokOpen, tokID("bar"), tokClose, tokContent(" baz"), tokEOF},
},
{
`tokenizes a partial as "OPEN_PARTIAL ID CLOSE"`,
`{{> foo}}`,
[]Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF},
},
{
`tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"`,
`{{> foo bar }}`,
[]Token{tokOpenPartial, tokID("foo"), tokID("bar"), tokClose, tokEOF},
},
{
`tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"`,
`{{>foo}}`,
[]Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF},
},
{
`tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"`,
`{{>foo }}`,
[]Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF},
},
{
`tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"`,
`{{>foo/bar.baz }}`,
[]Token{tokOpenPartial, tokID("foo"), tokSep("/"), tokID("bar"), tokSep("."), tokID("baz"), tokClose, tokEOF},
},
{
`tokenizes a comment as "COMMENT"`,
`foo {{! this is a comment }} bar {{ baz }}`,
[]Token{tokContent("foo "), tokComment("{{! this is a comment }}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF},
},
{
`tokenizes a block comment as "COMMENT"`,
`foo {{!-- this is a {{comment}} --}} bar {{ baz }}`,
[]Token{tokContent("foo "), tokComment("{{!-- this is a {{comment}} --}}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF},
},
{
`tokenizes a block comment with whitespace as "COMMENT"`,
"foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}",
[]Token{tokContent("foo "), tokComment("{{!-- this is a\n{{comment}}\n--}}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF},
},
{
`tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE`,
`{{#foo}}content{{/foo}}`,
[]Token{tokOpenBlock, tokID("foo"), tokClose, tokContent("content"), tokOpenEndBlock, tokID("foo"), tokClose, tokEOF},
},
{
`tokenizes inverse sections as "INVERSE"`,
`{{^}}`,
[]Token{tokInverse("{{^}}"), tokEOF},
},
{
`tokenizes inverse sections as "INVERSE" with alternate format`,
`{{else}}`,
[]Token{tokInverse("{{else}}"), tokEOF},
},
{
`tokenizes inverse sections as "INVERSE" with spaces`,
`{{ else }}`,
[]Token{tokInverse("{{ else }}"), tokEOF},
},
{
`tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"`,
`{{^foo}}`,
[]Token{tokOpenInverse, tokID("foo"), tokClose, tokEOF},
},
{
`tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"`,
`{{^ foo }}`,
[]Token{tokOpenInverse, tokID("foo"), tokClose, tokEOF},
},
{
`tokenizes mustaches with params as "OPEN ID ID ID CLOSE"`,
`{{ foo bar baz }}`,
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokClose, tokEOF},
},
{
`tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"`,
`{{ foo bar "baz" }}`,
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz"), tokClose, tokEOF},
},
{
`tokenizes mustaches with String params using single quotes as "OPEN ID ID STRING CLOSE"`,
`{{ foo bar 'baz' }}`,
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz"), tokClose, tokEOF},
},
{
`tokenizes String params with spaces inside as "STRING"`,
`{{ foo bar "baz bat" }}`,
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz bat"), tokClose, tokEOF},
},
{
`tokenizes String params with escapes quotes as STRING`,
`{{ foo "bar\"baz" }}`,
[]Token{tokOpen, tokID("foo"), tokString(`bar"baz`), tokClose, tokEOF},
},
{
`tokenizes String params using single quotes with escapes quotes as STRING`,
`{{ foo 'bar\'baz' }}`,
[]Token{tokOpen, tokID("foo"), tokString(`bar'baz`), tokClose, tokEOF},
},
{
`tokenizes numbers`,
`{{ foo 1 }}`,
[]Token{tokOpen, tokID("foo"), tokNumber("1"), tokClose, tokEOF},
},
{
`tokenizes floats`,
`{{ foo 1.1 }}`,
[]Token{tokOpen, tokID("foo"), tokNumber("1.1"), tokClose, tokEOF},
},
{
`tokenizes negative numbers`,
`{{ foo -1 }}`,
[]Token{tokOpen, tokID("foo"), tokNumber("-1"), tokClose, tokEOF},
},
{
`tokenizes negative floats`,
`{{ foo -1.1 }}`,
[]Token{tokOpen, tokID("foo"), tokNumber("-1.1"), tokClose, tokEOF},
},
{
`tokenizes boolean true`,
`{{ foo true }}`,
[]Token{tokOpen, tokID("foo"), tokBool("true"), tokClose, tokEOF},
},
{
`tokenizes boolean false`,
`{{ foo false }}`,
[]Token{tokOpen, tokID("foo"), tokBool("false"), tokClose, tokEOF},
},
// SKIP: 'tokenizes undefined and null'
{
`tokenizes hash arguments (1)`,
`{{ foo bar=baz }}`,
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokEquals, tokID("baz"), tokClose, tokEOF},
},
{
`tokenizes hash arguments (2)`,
`{{ foo bar baz=bat }}`,
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokID("bat"), tokClose, tokEOF},
},
{
`tokenizes hash arguments (3)`,
`{{ foo bar baz=1 }}`,
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokNumber("1"), tokClose, tokEOF},
},
{
`tokenizes hash arguments (4)`,
`{{ foo bar baz=true }}`,
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokBool("true"), tokClose, tokEOF},
},
{
`tokenizes hash arguments (5)`,
`{{ foo bar baz=false }}`,
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokBool("false"), tokClose, tokEOF},
},
{
`tokenizes hash arguments (6)`,
"{{ foo bar\n baz=bat }}",
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokID("bat"), tokClose, tokEOF},
},
{
`tokenizes hash arguments (7)`,
`{{ foo bar baz="bat" }}`,
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokString("bat"), tokClose, tokEOF},
},
{
`tokenizes hash arguments (8)`,
`{{ foo bar baz="bat" bam=wot }}`,
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokString("bat"), tokID("bam"), tokEquals, tokID("wot"), tokClose, tokEOF},
},
{
`tokenizes hash arguments (9)`,
`{{foo omg bar=baz bat="bam"}}`,
[]Token{tokOpen, tokID("foo"), tokID("omg"), tokID("bar"), tokEquals, tokID("baz"), tokID("bat"), tokEquals, tokString("bam"), tokClose, tokEOF},
},
{
`tokenizes special @ identifiers (1)`,
`{{ @foo }}`,
[]Token{tokOpen, tokData, tokID("foo"), tokClose, tokEOF},
},
{
`tokenizes special @ identifiers (2)`,
`{{ foo @bar }}`,
[]Token{tokOpen, tokID("foo"), tokData, tokID("bar"), tokClose, tokEOF},
},
{
`tokenizes special @ identifiers (3)`,
`{{ foo bar=@baz }}`,
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokEquals, tokData, tokID("baz"), tokClose, tokEOF},
},
{
`does not time out in a mustache with a single } followed by EOF`,
`{{foo}`,
[]Token{tokOpen, tokID("foo"), tokError("Unexpected character in expression: '}'")},
},
{
`does not time out in a mustache when invalid ID characters are used`,
`{{foo & }}`,
[]Token{tokOpen, tokID("foo"), tokError("Unexpected character in expression: '&'")},
},
{
`tokenizes subexpressions (1)`,
`{{foo (bar)}}`,
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokCloseSexpr, tokClose, tokEOF},
},
{
`tokenizes subexpressions (2)`,
`{{foo (a-x b-y)}}`,
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("a-x"), tokID("b-y"), tokCloseSexpr, tokClose, tokEOF},
},
{
`tokenizes nested subexpressions`,
`{{foo (bar (lol rofl)) (baz)}}`,
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokOpenSexpr, tokID("lol"), tokID("rofl"), tokCloseSexpr, tokCloseSexpr, tokOpenSexpr, tokID("baz"), tokCloseSexpr, tokClose, tokEOF},
},
{
`tokenizes nested subexpressions: literals`,
`{{foo (bar (lol true) false) (baz 1) (blah 'b') (blorg "c")}}`,
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokOpenSexpr, tokID("lol"), tokBool("true"), tokCloseSexpr, tokBool("false"), tokCloseSexpr, tokOpenSexpr, tokID("baz"), tokNumber("1"), tokCloseSexpr, tokOpenSexpr, tokID("blah"), tokString("b"), tokCloseSexpr, tokOpenSexpr, tokID("blorg"), tokString("c"), tokCloseSexpr, tokClose, tokEOF},
},
{
`tokenizes block params (1)`,
`{{#foo as |bar|}}`,
[]Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokCloseBlockParams, tokClose, tokEOF},
},
{
`tokenizes block params (2)`,
`{{#foo as |bar baz|}}`,
[]Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
},
{
`tokenizes block params (3)`,
`{{#foo as | bar baz |}}`,
[]Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
},
{
`tokenizes block params (4)`,
`{{#foo as as | bar baz |}}`,
[]Token{tokOpenBlock, tokID("foo"), tokID("as"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
},
{
`tokenizes block params (5)`,
`{{else foo as |bar baz|}}`,
[]Token{tokOpenInverseChain, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
},
}
func collect(t *lexTest) []Token {
var result []Token
l := scanWithName(t.input, t.name)
for {
token := l.NextToken()
result = append(result, token)
if token.Kind == TokenEOF || token.Kind == TokenError {
break
}
}
return result
}
func equal(i1, i2 []Token, checkPos bool) bool {
if len(i1) != len(i2) {
return false
}
for k := range i1 {
if i1[k].Kind != i2[k].Kind {
return false
}
if checkPos && i1[k].Pos != i2[k].Pos {
return false
}
if i1[k].Val != i2[k].Val {
return false
}
}
return true
}
func TestLexer(t *testing.T) {
t.Parallel()
for _, test := range lexTests {
tokens := collect(&test)
if !equal(tokens, test.tokens, false) {
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\nexpected\n\t%v\ngot\n\t%+v\n", test.name, test.input, test.tokens, tokens)
}
}
}
// @todo Test errors:
// `{{{{raw foo`
// package example
func Example() {
source := "You know {{nothing}} John Snow"
output := ""
lex := Scan(source)
for {
// consume next token
token := lex.NextToken()
output += fmt.Sprintf(" %s", token)
// stops when all tokens have been consumed, or on error
if token.Kind == TokenEOF || token.Kind == TokenError {
break
}
}
fmt.Print(output)
// Output: Content{"You know "} Open{"{{"} ID{"nothing"} Close{"}}"} Content{" John Snow"} EOF
}

View File

@ -1,183 +0,0 @@
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
}

View File

@ -1,234 +0,0 @@
package raymond
import (
"io/ioutil"
"path"
"regexp"
"strings"
"testing"
"gopkg.in/yaml.v2"
)
//
// Note, as the JS implementation, the divergences from mustache spec:
// - we don't support alternative delimeters
// - the mustache lambda spec differs
//
type mustacheTest struct {
Name string
Desc string
Data interface{}
Template string
Expected string
Partials map[string]string
}
type mustacheTestFile struct {
Overview string
Tests []mustacheTest
}
var (
rAltDelim = regexp.MustCompile(regexp.QuoteMeta("{{="))
)
var (
musTestLambdaInterMult = 0
)
func TestMustache(t *testing.T) {
skipFiles := map[string]bool{
// mustache lambdas differ from handlebars lambdas
"~lambdas.yml": true,
}
for _, fileName := range mustacheTestFiles() {
if skipFiles[fileName] {
// fmt.Printf("Skipped file: %s\n", fileName)
continue
}
launchTests(t, testsFromMustacheFile(fileName))
}
}
func testsFromMustacheFile(fileName string) []Test {
result := []Test{}
fileData, err := ioutil.ReadFile(path.Join("mustache", "specs", fileName))
if err != nil {
panic(err)
}
var testFile mustacheTestFile
if err := yaml.Unmarshal(fileData, &testFile); err != nil {
panic(err)
}
for _, mustacheTest := range testFile.Tests {
if mustBeSkipped(mustacheTest, fileName) {
// fmt.Printf("Skipped test: %s\n", mustacheTest.Name)
continue
}
test := Test{
name: mustacheTest.Name,
input: mustacheTest.Template,
data: mustacheTest.Data,
partials: mustacheTest.Partials,
output: mustacheTest.Expected,
}
result = append(result, test)
}
return result
}
// returns true if test must be skipped
func mustBeSkipped(test mustacheTest, fileName string) bool {
// handlebars does not support alternative delimiters
return haveAltDelimiter(test) ||
// the JS implementation skips those tests
fileName == "partials.yml" && (test.Name == "Failed Lookup" || test.Name == "Standalone Indentation")
}
// returns true if test have alternative delimeter in template or in partials
func haveAltDelimiter(test mustacheTest) bool {
// check template
if rAltDelim.MatchString(test.Template) {
return true
}
// check partials
for _, partial := range test.Partials {
if rAltDelim.MatchString(partial) {
return true
}
}
return false
}
func mustacheTestFiles() []string {
var result []string
files, err := ioutil.ReadDir(path.Join("mustache", "specs"))
if err != nil {
panic(err)
}
for _, file := range files {
fileName := file.Name()
if !file.IsDir() && strings.HasSuffix(fileName, ".yml") {
result = append(result, fileName)
}
}
return result
}
//
// Following tests come fron ~lambdas.yml
//
var mustacheLambdasTests = []Test{
{
"Interpolation",
"Hello, {{lambda}}!",
map[string]interface{}{"lambda": func() string { return "world" }},
nil, nil, nil,
"Hello, world!",
},
// // SKIP: lambda return value is not parsed
// {
// "Interpolation - Expansion",
// "Hello, {{lambda}}!",
// map[string]interface{}{"lambda": func() string { return "{{planet}}" }},
// nil, nil, nil,
// "Hello, world!",
// },
// SKIP "Interpolation - Alternate Delimiters"
{
"Interpolation - Multiple Calls",
"{{lambda}} == {{{lambda}}} == {{lambda}}",
map[string]interface{}{"lambda": func() string {
musTestLambdaInterMult++
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)
}

View File

@ -1,846 +0,0 @@
// Package parser provides a handlebars syntax analyser. It consumes the tokens provided by the lexer to build an AST.
package parser
import (
"fmt"
"regexp"
"runtime"
"strconv"
"github.com/aymerick/raymond/ast"
"github.com/aymerick/raymond/lexer"
)
// References:
// - https://github.com/wycats/handlebars.js/blob/master/src/handlebars.yy
// - https://github.com/golang/go/blob/master/src/text/template/parse/parse.go
// parser is a syntax analyzer.
type parser struct {
// Lexer
lex *lexer.Lexer
// Root node
root ast.Node
// Tokens parsed but not consumed yet
tokens []*lexer.Token
// All tokens have been retreieved from lexer
lexOver bool
}
var (
rOpenComment = regexp.MustCompile(`^\{\{~?!-?-?`)
rCloseComment = regexp.MustCompile(`-?-?~?\}\}$`)
rOpenAmp = regexp.MustCompile(`^\{\{~?&`)
)
// new instanciates a new parser
func new(input string) *parser {
return &parser{
lex: lexer.Scan(input),
}
}
// Parse analyzes given input and returns the AST root node.
func Parse(input string) (result *ast.Program, err error) {
// recover error
defer errRecover(&err)
parser := new(input)
// parse
result = parser.parseProgram()
// check last token
token := parser.shift()
if token.Kind != lexer.TokenEOF {
// Parsing ended before EOF
errToken(token, "Syntax error")
}
// fix whitespaces
processWhitespaces(result)
// named returned values
return
}
// errRecover recovers parsing panic
func errRecover(errp *error) {
e := recover()
if e != nil {
switch err := e.(type) {
case runtime.Error:
panic(e)
case error:
*errp = err
default:
panic(e)
}
}
}
// errPanic panics
func errPanic(err error, line int) {
panic(fmt.Errorf("Parse error on line %d:\n%s", line, err))
}
// errNode panics with given node infos
func errNode(node ast.Node, msg string) {
errPanic(fmt.Errorf("%s\nNode: %s", msg, node), node.Location().Line)
}
// errNode panics with given Token infos
func errToken(tok *lexer.Token, msg string) {
errPanic(fmt.Errorf("%s\nToken: %s", msg, tok), tok.Line)
}
// errNode panics because of an unexpected Token kind
func errExpected(expect lexer.TokenKind, tok *lexer.Token) {
errPanic(fmt.Errorf("Expecting %s, got: '%s'", expect, tok), tok.Line)
}
// program : statement*
func (p *parser) parseProgram() *ast.Program {
result := ast.NewProgram(p.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)
}

View File

@ -1,200 +0,0 @@
package parser
import (
"fmt"
"regexp"
"testing"
"github.com/aymerick/raymond/ast"
"github.com/aymerick/raymond/lexer"
)
type parserTest struct {
name string
input string
output string
}
var parserTests = []parserTest{
//
// Next tests come from:
// https://github.com/wycats/handlebars.js/blob/master/spec/parser.js
//
{"parses simple mustaches (1)", `{{123}}`, "{{ NUMBER{123} [] }}\n"},
{"parses simple mustaches (2)", `{{"foo"}}`, "{{ \"foo\" [] }}\n"},
{"parses simple mustaches (3)", `{{false}}`, "{{ BOOLEAN{false} [] }}\n"},
{"parses simple mustaches (4)", `{{true}}`, "{{ BOOLEAN{true} [] }}\n"},
{"parses simple mustaches (5)", `{{foo}}`, "{{ PATH:foo [] }}\n"},
{"parses simple mustaches (6)", `{{foo?}}`, "{{ PATH:foo? [] }}\n"},
{"parses simple mustaches (7)", `{{foo_}}`, "{{ PATH:foo_ [] }}\n"},
{"parses simple mustaches (8)", `{{foo-}}`, "{{ PATH:foo- [] }}\n"},
{"parses simple mustaches (9)", `{{foo:}}`, "{{ PATH:foo: [] }}\n"},
{"parses simple mustaches with data", `{{@foo}}`, "{{ @PATH:foo [] }}\n"},
{"parses simple mustaches with data paths", `{{@../foo}}`, "{{ @PATH:foo [] }}\n"},
{"parses mustaches with paths", `{{foo/bar}}`, "{{ PATH:foo/bar [] }}\n"},
{"parses mustaches with this/foo", `{{this/foo}}`, "{{ PATH:foo [] }}\n"},
{"parses mustaches with - in a path", `{{foo-bar}}`, "{{ PATH:foo-bar [] }}\n"},
{"parses mustaches with parameters", `{{foo bar}}`, "{{ PATH:foo [PATH:bar] }}\n"},
{"parses mustaches with string parameters", `{{foo bar "baz" }}`, "{{ PATH:foo [PATH:bar, \"baz\"] }}\n"},
{"parses mustaches with NUMBER parameters", `{{foo 1}}`, "{{ PATH:foo [NUMBER{1}] }}\n"},
{"parses mustaches with BOOLEAN parameters (1)", `{{foo true}}`, "{{ PATH:foo [BOOLEAN{true}] }}\n"},
{"parses mustaches with BOOLEAN parameters (2)", `{{foo false}}`, "{{ PATH:foo [BOOLEAN{false}] }}\n"},
{"parses mustaches with DATA parameters", `{{foo @bar}}`, "{{ PATH:foo [@PATH:bar] }}\n"},
{"parses mustaches with hash arguments (01)", `{{foo bar=baz}}`, "{{ PATH:foo [] HASH{bar=PATH:baz} }}\n"},
{"parses mustaches with hash arguments (02)", `{{foo bar=1}}`, "{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n"},
{"parses mustaches with hash arguments (03)", `{{foo bar=true}}`, "{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n"},
{"parses mustaches with hash arguments (04)", `{{foo bar=false}}`, "{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n"},
{"parses mustaches with hash arguments (05)", `{{foo bar=@baz}}`, "{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n"},
{"parses mustaches with hash arguments (06)", `{{foo bar=baz bat=bam}}`, "{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n"},
{"parses mustaches with hash arguments (07)", `{{foo bar=baz bat="bam"}}`, "{{ PATH:foo [] HASH{bar=PATH:baz, bat=\"bam\"} }}\n"},
{"parses mustaches with hash arguments (08)", `{{foo bat='bam'}}`, "{{ PATH:foo [] HASH{bat=\"bam\"} }}\n"},
{"parses mustaches with hash arguments (09)", `{{foo omg bar=baz bat="bam"}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\"} }}\n"},
{"parses mustaches with hash arguments (10)", `{{foo omg bar=baz bat="bam" baz=1}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=NUMBER{1}} }}\n"},
{"parses mustaches with hash arguments (11)", `{{foo omg bar=baz bat="bam" baz=true}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=BOOLEAN{true}} }}\n"},
{"parses mustaches with hash arguments (12)", `{{foo omg bar=baz bat="bam" baz=false}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=BOOLEAN{false}} }}\n"},
{"parses contents followed by a mustache", `foo bar {{baz}}`, "CONTENT[ 'foo bar ' ]\n{{ PATH:baz [] }}\n"},
{"parses a partial (1)", `{{> foo }}`, "{{> PARTIAL:foo }}\n"},
{"parses a partial (2)", `{{> "foo" }}`, "{{> PARTIAL:foo }}\n"},
{"parses a partial (3)", `{{> 1 }}`, "{{> PARTIAL:1 }}\n"},
{"parses a partial with context", `{{> foo bar}}`, "{{> PARTIAL:foo PATH:bar }}\n"},
{"parses a partial with hash", `{{> foo bar=bat}}`, "{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n"},
{"parses a partial with context and hash", `{{> foo bar bat=baz}}`, "{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n"},
{"parses a partial with a complex name", `{{> shared/partial?.bar}}`, "{{> PARTIAL:shared/partial?.bar }}\n"},
{"parses a comment", `{{! this is a comment }}`, "{{! ' this is a comment ' }}\n"},
{"parses a multi-line comment", "{{!\nthis is a multi-line comment\n}}", "{{! '\nthis is a multi-line comment\n' }}\n"},
{"parses an inverse section", `{{#foo}} bar {{^}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"},
{"parses an inverse (else-style) section", `{{#foo}} bar {{else}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"},
{"parses multiple inverse sections", `{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"},
{"parses empty blocks", `{{#foo}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n"},
{"parses empty blocks with empty inverse section", `{{#foo}}{{^}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"},
{"parses empty blocks with empty inverse (else-style) section", `{{#foo}}{{else}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"},
{"parses non-empty blocks with empty inverse section", `{{#foo}} bar {{^}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"},
{"parses non-empty blocks with empty inverse (else-style) section", `{{#foo}} bar {{else}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"},
{"parses empty blocks with non-empty inverse section", `{{#foo}}{{^}} bar {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"},
{"parses empty blocks with non-empty inverse (else-style) section", `{{#foo}}{{else}} bar {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"},
{"parses a standalone inverse section", `{{^foo}}bar{{/foo}}`, "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"},
{"parses block with block params", `{{#foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"},
{"parses inverse block with block params", `{{^foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"},
{"parses chained inverse block with block params", `{{#foo}}{{else foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"},
}
func TestParser(t *testing.T) {
t.Parallel()
for _, test := range parserTests {
output := ""
node, err := Parse(test.input)
if err == nil {
output = ast.Print(node)
}
if (err != nil) || (test.output != output) {
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\nexpected\n\t%q\ngot\n\t%q\nerror:\n\t%s", test.name, test.input, test.output, output, err)
}
}
}
var parserErrorTests = []parserTest{
{"lexer error", `{{! unclosed comment`, "Lexer error"},
{"syntax error", `foo{{^}}`, "Syntax error"},
{"open raw block must be closed", `{{{{raw foo}} bar {{{{/raw}}}}`, "Expecting CloseRawBlock"},
{"end raw block must be closed", `{{{{raw foo}}}} bar {{{{/raw}}`, "Expecting CloseRawBlock"},
{"raw block names must match (1)", `{{{{1}}}}{{foo}}{{{{/raw}}}}`, "1 doesn't match raw"},
{"raw block names must match (2)", `{{{{raw}}}}{{foo}}{{{{/1}}}}`, "raw doesn't match 1"},
{"raw block names must match (3)", `{{{{goodbyes}}}}test{{{{/hellos}}}}`, "goodbyes doesn't match hellos"},
{"open block must be closed", `{{#foo bar}}}{{/foo}}`, "Expecting Close"},
{"end block must be closed", `{{#foo bar}}{{/foo}}}`, "Expecting Close"},
{"an open block must have a end block", `{{#foo}}test`, "Expecting OpenEndBlock"},
{"block names must match (1)", `{{#1 bar}}{{/foo}}`, "1 doesn't match foo"},
{"block names must match (2)", `{{#foo bar}}{{/1}}`, "foo doesn't match 1"},
{"block names must match (3)", `{{#foo}}test{{/bar}}`, "foo doesn't match bar"},
{"an mustache must terminate with a close mustache", `{{foo}}}`, "Expecting Close"},
{"an unescaped mustache must terminate with a close unescaped mustache", `{{{foo}}`, "Expecting CloseUnescaped"},
{"an partial must terminate with a close mustache", `{{> foo}}}`, "Expecting Close"},
{"a subexpression must terminate with a close subexpression", `{{foo (false}}`, "Expecting CloseSexpr"},
{"raises on missing hash value (1)", `{{foo bar=}}`, "Parse error on line 1"},
{"raises on missing hash value (2)", `{{foo bar=baz bim=}}`, "Parse error on line 1"},
{"block param must have at least one param", `{{#foo as ||}}content{{/foo}}`, "Expecting ID"},
{"open block params must be closed", `{{#foo as |}}content{{/foo}}`, "Expecting ID"},
{"a path must start with an ID", `{{#/}}content{{/foo}}`, "Expecting ID"},
{"a path must end with an ID", `{{foo/bar/}}`, "Expecting ID"},
//
// Next tests come from:
// https://github.com/wycats/handlebars.js/blob/master/spec/parser.js
//
{"throws on old inverse section", `{{else foo}}bar{{/foo}}`, ""},
{"raises if there's a parser error (1)", `foo{{^}}bar`, "Parse error on line 1"},
{"raises if there's a parser error (2)", `{{foo}`, "Parse error on line 1"},
{"raises if there's a parser error (3)", `{{foo &}}`, "Parse error on line 1"},
{"raises if there's a parser error (4)", `{{#goodbyes}}{{/hellos}}`, "Parse error on line 1"},
{"raises if there's a parser error (5)", `{{#goodbyes}}{{/hellos}}`, "goodbyes doesn't match hellos"},
{"should handle invalid paths (1)", `{{foo/../bar}}`, `Invalid path: foo/..`},
{"should handle invalid paths (2)", `{{foo/./bar}}`, `Invalid path: foo/.`},
{"should handle invalid paths (3)", `{{foo/this/bar}}`, `Invalid path: foo/this`},
{"knows how to report the correct line number in errors (1)", "hello\nmy\n{{foo}", "Parse error on line 3"},
{"knows how to report the correct line number in errors (2)", "hello\n\nmy\n\n{{foo}", "Parse error on line 5"},
{"knows how to report the correct line number in errors when the first character is a newline", "\n\nhello\n\nmy\n\n{{foo}", "Parse error on line 7"},
}
func TestParserErrors(t *testing.T) {
t.Parallel()
for _, test := range parserErrorTests {
node, err := Parse(test.input)
if err == nil {
output := ast.Print(node)
tokens := lexer.Collect(test.input)
t.Errorf("Test '%s' failed - Error expected\ninput:\n\t'%s'\ngot\n\t%q\ntokens:\n\t%q", test.name, test.input, output, tokens)
} else if test.output != "" {
matched, errMatch := regexp.MatchString(regexp.QuoteMeta(test.output), fmt.Sprint(err))
if errMatch != nil {
panic("Failed to match regexp")
}
if !matched {
t.Errorf("Test '%s' failed - Incorrect error returned\ninput:\n\t'%s'\nexpected\n\t%q\ngot\n\t%q", test.name, test.input, test.output, err)
}
}
}
}
// package example
func Example() {
source := "You know {{nothing}} John Snow"
// parse template
program, err := Parse(source)
if err != nil {
panic(err)
}
// print AST
output := ast.Print(program)
fmt.Print(output)
// CONTENT[ 'You know ' ]
// {{ PATH:nothing [] }}
// CONTENT[ ' John Snow' ]
}

View File

@ -1,360 +0,0 @@
package parser
import (
"regexp"
"github.com/aymerick/raymond/ast"
)
// whitespaceVisitor walks through the AST to perform whitespace control
//
// The logic was shamelessly borrowed from:
// https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/whitespace-control.js
type whitespaceVisitor struct {
isRootSeen bool
}
var (
rTrimLeft = regexp.MustCompile(`^[ \t]*\r?\n?`)
rTrimLeftMultiple = regexp.MustCompile(`^\s+`)
rTrimRight = regexp.MustCompile(`[ \t]+$`)
rTrimRightMultiple = regexp.MustCompile(`\s+$`)
rPrevWhitespace = regexp.MustCompile(`\r?\n\s*?$`)
rPrevWhitespaceStart = regexp.MustCompile(`(^|\r?\n)\s*?$`)
rNextWhitespace = regexp.MustCompile(`^\s*?\r?\n`)
rNextWhitespaceEnd = regexp.MustCompile(`^\s*?(\r?\n|$)`)
rPartialIndent = regexp.MustCompile(`([ \t]+$)`)
)
// newWhitespaceVisitor instanciates a new whitespaceVisitor
func newWhitespaceVisitor() *whitespaceVisitor {
return &whitespaceVisitor{}
}
// processWhitespaces performs whitespace control on given AST
//
// WARNING: It must be called only once on AST.
func processWhitespaces(node ast.Node) {
node.Accept(newWhitespaceVisitor())
}
func omitRightFirst(body []ast.Node, multiple bool) {
omitRight(body, -1, multiple)
}
func omitRight(body []ast.Node, i int, multiple bool) {
if i+1 >= len(body) {
return
}
current := body[i+1]
node, ok := current.(*ast.ContentStatement)
if !ok {
return
}
if !multiple && node.RightStripped {
return
}
original := node.Value
r := rTrimLeft
if multiple {
r = rTrimLeftMultiple
}
node.Value = r.ReplaceAllString(node.Value, "")
node.RightStripped = (original != node.Value)
}
func omitLeftLast(body []ast.Node, multiple bool) {
omitLeft(body, len(body), multiple)
}
func omitLeft(body []ast.Node, i int, multiple bool) bool {
if i-1 < 0 {
return false
}
current := body[i-1]
node, ok := current.(*ast.ContentStatement)
if !ok {
return false
}
if !multiple && node.LeftStripped {
return false
}
original := node.Value
r := rTrimRight
if multiple {
r = rTrimRightMultiple
}
node.Value = r.ReplaceAllString(node.Value, "")
node.LeftStripped = (original != node.Value)
return node.LeftStripped
}
func isPrevWhitespace(body []ast.Node) bool {
return isPrevWhitespaceProgram(body, len(body), false)
}
func isPrevWhitespaceProgram(body []ast.Node, i int, isRoot bool) bool {
if i < 1 {
return isRoot
}
prev := body[i-1]
if node, ok := prev.(*ast.ContentStatement); ok {
if (node.Value == "") && node.RightStripped {
// already stripped, so it may be an empty string not catched by regexp
return true
}
r := rPrevWhitespaceStart
if (i > 1) || !isRoot {
r = rPrevWhitespace
}
return r.MatchString(node.Value)
}
return false
}
func isNextWhitespace(body []ast.Node) bool {
return isNextWhitespaceProgram(body, -1, false)
}
func isNextWhitespaceProgram(body []ast.Node, i int, isRoot bool) bool {
if i+1 >= len(body) {
return isRoot
}
next := body[i+1]
if node, ok := next.(*ast.ContentStatement); ok {
if (node.Value == "") && node.LeftStripped {
// already stripped, so it may be an empty string not catched by regexp
return true
}
r := rNextWhitespaceEnd
if (i+2 > len(body)) || !isRoot {
r = rNextWhitespace
}
return r.MatchString(node.Value)
}
return false
}
//
// Visitor interface
//
func (v *whitespaceVisitor) VisitProgram(program *ast.Program) interface{} {
isRoot := !v.isRootSeen
v.isRootSeen = true
body := program.Body
for i, current := range body {
strip, _ := current.Accept(v).(*ast.Strip)
if strip == nil {
continue
}
_isPrevWhitespace := isPrevWhitespaceProgram(body, i, isRoot)
_isNextWhitespace := isNextWhitespaceProgram(body, i, isRoot)
openStandalone := strip.OpenStandalone && _isPrevWhitespace
closeStandalone := strip.CloseStandalone && _isNextWhitespace
inlineStandalone := strip.InlineStandalone && _isPrevWhitespace && _isNextWhitespace
if strip.Close {
omitRight(body, i, true)
}
if strip.Open && (i > 0) {
omitLeft(body, i, true)
}
if inlineStandalone {
omitRight(body, i, false)
if omitLeft(body, i, false) {
// If we are on a standalone node, save the indent info for partials
if partial, ok := current.(*ast.PartialStatement); ok {
// Pull out the whitespace from the final line
if i > 0 {
if prevContent, ok := body[i-1].(*ast.ContentStatement); ok {
partial.Indent = rPartialIndent.FindString(prevContent.Original)
}
}
}
}
}
if b, ok := current.(*ast.BlockStatement); ok {
if openStandalone {
prog := b.Program
if prog == nil {
prog = b.Inverse
}
omitRightFirst(prog.Body, false)
// Strip out the previous content node if it's whitespace only
omitLeft(body, i, false)
}
if closeStandalone {
prog := b.Inverse
if prog == nil {
prog = b.Program
}
// Always strip the next node
omitRight(body, i, false)
omitLeftLast(prog.Body, false)
}
}
}
return nil
}
func (v *whitespaceVisitor) VisitBlock(block *ast.BlockStatement) interface{} {
if block.Program != nil {
block.Program.Accept(v)
}
if block.Inverse != nil {
block.Inverse.Accept(v)
}
program := block.Program
inverse := block.Inverse
if program == nil {
program = inverse
inverse = nil
}
firstInverse := inverse
lastInverse := inverse
if (inverse != nil) && inverse.Chained {
b, _ := inverse.Body[0].(*ast.BlockStatement)
firstInverse = b.Program
for lastInverse.Chained {
b, _ := lastInverse.Body[len(lastInverse.Body)-1].(*ast.BlockStatement)
lastInverse = b.Program
}
}
closeProg := firstInverse
if closeProg == nil {
closeProg = program
}
strip := &ast.Strip{
Open: (block.OpenStrip != nil) && block.OpenStrip.Open,
Close: (block.CloseStrip != nil) && block.CloseStrip.Close,
OpenStandalone: isNextWhitespace(program.Body),
CloseStandalone: isPrevWhitespace(closeProg.Body),
}
if (block.OpenStrip != nil) && block.OpenStrip.Close {
omitRightFirst(program.Body, true)
}
if inverse != nil {
if block.InverseStrip != nil {
inverseStrip := block.InverseStrip
if inverseStrip.Open {
omitLeftLast(program.Body, true)
}
if inverseStrip.Close {
omitRightFirst(firstInverse.Body, true)
}
}
if (block.CloseStrip != nil) && block.CloseStrip.Open {
omitLeftLast(lastInverse.Body, true)
}
// Find standalone else statements
if isPrevWhitespace(program.Body) && isNextWhitespace(firstInverse.Body) {
omitLeftLast(program.Body, false)
omitRightFirst(firstInverse.Body, false)
}
} else if (block.CloseStrip != nil) && block.CloseStrip.Open {
omitLeftLast(program.Body, true)
}
return strip
}
func (v *whitespaceVisitor) VisitMustache(mustache *ast.MustacheStatement) interface{} {
return mustache.Strip
}
func _inlineStandalone(strip *ast.Strip) interface{} {
return &ast.Strip{
Open: strip.Open,
Close: strip.Close,
InlineStandalone: true,
}
}
func (v *whitespaceVisitor) VisitPartial(node *ast.PartialStatement) interface{} {
strip := node.Strip
if strip == nil {
strip = &ast.Strip{}
}
return _inlineStandalone(strip)
}
func (v *whitespaceVisitor) VisitComment(node *ast.CommentStatement) interface{} {
strip := node.Strip
if strip == nil {
strip = &ast.Strip{}
}
return _inlineStandalone(strip)
}
// NOOP
func (v *whitespaceVisitor) VisitContent(node *ast.ContentStatement) interface{} { return nil }
func (v *whitespaceVisitor) VisitExpression(node *ast.Expression) interface{} { return nil }
func (v *whitespaceVisitor) VisitSubExpression(node *ast.SubExpression) interface{} { return nil }
func (v *whitespaceVisitor) VisitPath(node *ast.PathExpression) interface{} { return nil }
func (v *whitespaceVisitor) VisitString(node *ast.StringLiteral) interface{} { return nil }
func (v *whitespaceVisitor) VisitBoolean(node *ast.BooleanLiteral) interface{} { return nil }
func (v *whitespaceVisitor) VisitNumber(node *ast.NumberLiteral) interface{} { return nil }
func (v *whitespaceVisitor) VisitHash(node *ast.Hash) interface{} { return nil }
func (v *whitespaceVisitor) VisitHashPair(node *ast.HashPair) interface{} { return nil }

View File

@ -1,85 +0,0 @@
package raymond
import (
"fmt"
"sync"
)
// partial represents a partial template
type partial struct {
name string
source string
tpl *Template
}
// partials stores all global partials
var partials map[string]*partial
// protects global partials
var partialsMutex sync.RWMutex
func init() {
partials = make(map[string]*partial)
}
// newPartial instanciates a new partial
func newPartial(name string, source string, tpl *Template) *partial {
return &partial{
name: name,
source: source,
tpl: tpl,
}
}
// RegisterPartial registers a global partial. That partial will be available to all templates.
func RegisterPartial(name string, source string) {
partialsMutex.Lock()
defer partialsMutex.Unlock()
if partials[name] != nil {
panic(fmt.Errorf("Partial already registered: %s", name))
}
partials[name] = newPartial(name, source, nil)
}
// RegisterPartials registers several global partials. Those partials will be available to all templates.
func RegisterPartials(partials map[string]string) {
for name, p := range partials {
RegisterPartial(name, p)
}
}
// 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
}

View File

@ -1,28 +0,0 @@
// Package raymond provides handlebars evaluation
package raymond
// Render parses a template and evaluates it with given context
//
// Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead.
func Render(source string, ctx interface{}) (string, error) {
// parse template
tpl, err := Parse(source)
if err != nil {
return "", err
}
// renders template
str, err := tpl.Exec(ctx)
if err != nil {
return "", err
}
return str, nil
}
// MustRender parses a template and evaluates it with given context. It panics on error.
//
// Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead.
func MustRender(source string, ctx interface{}) string {
return MustParse(source).MustExec(ctx)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,115 +0,0 @@
package raymond
import "fmt"
func Example() {
source := "<h1>{{title}}</h1><p>{{body.content}}</p>"
ctx := map[string]interface{}{
"title": "foo",
"body": map[string]string{"content": "bar"},
}
// parse template
tpl := MustParse(source)
// evaluate template with context
output := tpl.MustExec(ctx)
// alternatively, for one shots:
// output := MustRender(source, ctx)
fmt.Print(output)
// Output: <h1>foo</h1><p>bar</p>
}
func Example_struct() {
source := `<div class="post">
<h1>By {{fullName author}}</h1>
<div class="body">{{body}}</div>
<h1>Comments</h1>
{{#each comments}}
<h2>By {{fullName author}}</h2>
<div class="body">{{body}}</div>
{{/each}}
</div>`
type Person struct {
FirstName string
LastName string
}
type Comment struct {
Author Person
Body string
}
type Post struct {
Author Person
Body string
Comments []Comment
}
ctx := Post{
Person{"Jean", "Valjean"},
"Life is difficult",
[]Comment{
Comment{
Person{"Marcel", "Beliveau"},
"LOL!",
},
},
}
RegisterHelper("fullName", func(person Person) string {
return person.FirstName + " " + person.LastName
})
output := MustRender(source, ctx)
fmt.Print(output)
// Output: <div class="post">
// <h1>By Jean Valjean</h1>
// <div class="body">Life is difficult</div>
//
// <h1>Comments</h1>
//
// <h2>By Marcel Beliveau</h2>
// <div class="body">LOL!</div>
// </div>
}
func ExampleRender() {
tpl := "<h1>{{title}}</h1><p>{{body.content}}</p>"
ctx := map[string]interface{}{
"title": "foo",
"body": map[string]string{"content": "bar"},
}
// render template with context
output, err := Render(tpl, ctx)
if err != nil {
panic(err)
}
fmt.Print(output)
// Output: <h1>foo</h1><p>bar</p>
}
func ExampleMustRender() {
tpl := "<h1>{{title}}</h1><p>{{body.content}}</p>"
ctx := map[string]interface{}{
"title": "foo",
"body": map[string]string{"content": "bar"},
}
// render template with context
output := MustRender(tpl, ctx)
fmt.Print(output)
// Output: <h1>foo</h1><p>bar</p>
}

View File

@ -1,84 +0,0 @@
package raymond
import (
"fmt"
"reflect"
"strconv"
)
// SafeString represents a string that must not be escaped.
//
// A SafeString can be returned by helpers to disable escaping.
type SafeString string
// isSafeString returns true if argument is a SafeString
func isSafeString(value interface{}) bool {
if _, ok := value.(SafeString); ok {
return true
}
return false
}
// Str returns string representation of any basic type value.
func Str(value interface{}) string {
return strValue(reflect.ValueOf(value))
}
// strValue returns string representation of a reflect.Value
func strValue(value reflect.Value) string {
result := ""
ival, ok := printableValue(value)
if !ok {
panic(fmt.Errorf("Can't print value: %q", value))
}
val := reflect.ValueOf(ival)
switch val.Kind() {
case reflect.Array, reflect.Slice:
for i := 0; i < val.Len(); i++ {
result += strValue(val.Index(i))
}
case reflect.Bool:
result = "false"
if val.Bool() {
result = "true"
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
result = fmt.Sprintf("%d", ival)
case reflect.Float32, reflect.Float64:
result = strconv.FormatFloat(val.Float(), 'f', -1, 64)
case reflect.Invalid:
result = ""
default:
result = fmt.Sprintf("%s", ival)
}
return result
}
// printableValue returns the, possibly indirected, interface value inside v that
// is best for a call to formatted printer.
//
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
func printableValue(v reflect.Value) (interface{}, bool) {
if v.Kind() == reflect.Ptr {
v, _ = indirect(v) // fmt.Fprint handles nil.
}
if !v.IsValid() {
return "", true
}
if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
if v.CanAddr() && (reflect.PtrTo(v.Type()).Implements(errorType) || reflect.PtrTo(v.Type()).Implements(fmtStringerType)) {
v = v.Addr()
} else {
switch v.Kind() {
case reflect.Chan, reflect.Func:
return nil, false
}
}
}
return v.Interface(), true
}

View File

@ -1,59 +0,0 @@
package raymond
import (
"fmt"
"testing"
)
type strTest struct {
name string
input interface{}
output string
}
var strTests = []strTest{
{"String", "foo", "foo"},
{"Boolean true", true, "true"},
{"Boolean false", false, "false"},
{"Integer", 25, "25"},
{"Float", 25.75, "25.75"},
{"Nil", nil, ""},
{"[]string", []string{"foo", "bar"}, "foobar"},
{"[]interface{} (strings)", []interface{}{"foo", "bar"}, "foobar"},
{"[]Boolean", []bool{true, false}, "truefalse"},
}
func TestStr(t *testing.T) {
t.Parallel()
for _, test := range strTests {
if res := Str(test.input); res != test.output {
t.Errorf("Failed to stringify: %s\nexpected:\n\t'%s'got:\n\t%q", test.name, test.output, res)
}
}
}
func ExampleStr() {
output := Str(3) + " foos are " + Str(true) + " and " + Str(-1.25) + " bars are " + Str(false) + "\n"
output += "But you know '" + Str(nil) + "' John Snow\n"
output += "map: " + Str(map[string]string{"foo": "bar"}) + "\n"
output += "array: " + Str([]interface{}{true, 10, "foo", 5, "bar"})
fmt.Println(output)
// Output: 3 foos are true and -1.25 bars are false
// But you know '' John Snow
// map: map[foo:bar]
// array: true10foo5bar
}
func ExampleSafeString() {
RegisterHelper("em", func() SafeString {
return SafeString("<em>FOO BAR</em>")
})
tpl := MustParse("{{em}}")
result := tpl.MustExec(nil)
fmt.Print(result)
// Output: <em>FOO BAR</em>
}

View File

@ -1,248 +0,0 @@
package raymond
import (
"fmt"
"io/ioutil"
"reflect"
"runtime"
"sync"
"github.com/aymerick/raymond/ast"
"github.com/aymerick/raymond/parser"
)
// Template represents a handlebars template.
type Template struct {
source string
program *ast.Program
helpers map[string]reflect.Value
partials map[string]*partial
mutex sync.RWMutex // protects helpers and partials
}
// newTemplate instanciate a new template without parsing it
func newTemplate(source string) *Template {
return &Template{
source: source,
helpers: make(map[string]reflect.Value),
partials: make(map[string]*partial),
}
}
// Parse instanciates a template by parsing given source.
func Parse(source string) (*Template, error) {
tpl := newTemplate(source)
// parse template
if err := tpl.parse(); err != nil {
return nil, err
}
return tpl, nil
}
// MustParse instanciates a template by parsing given source. It panics on error.
func MustParse(source string) *Template {
result, err := Parse(source)
if err != nil {
panic(err)
}
return result
}
// ParseFile reads given file and returns parsed template.
func ParseFile(filePath string) (*Template, error) {
b, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, err
}
return Parse(string(b))
}
// parse parses the template
//
// It can be called several times, the parsing will be done only once.
func (tpl *Template) parse() error {
if tpl.program == nil {
var err error
tpl.program, err = parser.Parse(tpl.source)
if err != nil {
return err
}
}
return nil
}
// Clone returns a copy of that template.
func (tpl *Template) Clone() *Template {
result := newTemplate(tpl.source)
result.program = tpl.program
tpl.mutex.RLock()
defer tpl.mutex.RUnlock()
for name, helper := range tpl.helpers {
result.RegisterHelper(name, helper.Interface())
}
for name, partial := range tpl.partials {
result.addPartial(name, partial.source, partial.tpl)
}
return result
}
func (tpl *Template) findHelper(name string) reflect.Value {
tpl.mutex.RLock()
defer tpl.mutex.RUnlock()
return tpl.helpers[name]
}
// RegisterHelper registers a helper for that template.
func (tpl *Template) RegisterHelper(name string, helper interface{}) {
tpl.mutex.Lock()
defer tpl.mutex.Unlock()
if tpl.helpers[name] != zero {
panic(fmt.Sprintf("Helper %s already registered", name))
}
val := reflect.ValueOf(helper)
ensureValidHelper(name, val)
tpl.helpers[name] = val
}
// RegisterHelpers registers several helpers for that template.
func (tpl *Template) RegisterHelpers(helpers map[string]interface{}) {
for name, helper := range helpers {
tpl.RegisterHelper(name, helper)
}
}
func (tpl *Template) addPartial(name string, source string, template *Template) {
tpl.mutex.Lock()
defer tpl.mutex.Unlock()
if tpl.partials[name] != nil {
panic(fmt.Sprintf("Partial %s already registered", name))
}
tpl.partials[name] = newPartial(name, source, template)
}
func (tpl *Template) findPartial(name string) *partial {
tpl.mutex.RLock()
defer tpl.mutex.RUnlock()
return tpl.partials[name]
}
// RegisterPartial registers a partial for that template.
func (tpl *Template) RegisterPartial(name string, source string) {
tpl.addPartial(name, source, nil)
}
// RegisterPartials registers several partials for that template.
func (tpl *Template) RegisterPartials(partials map[string]string) {
for name, partial := range partials {
tpl.RegisterPartial(name, partial)
}
}
// RegisterPartialFile reads given file and registers its content as a partial with given name.
func (tpl *Template) RegisterPartialFile(filePath string, name string) error {
b, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
tpl.RegisterPartial(name, string(b))
return nil
}
// RegisterPartialFiles reads several files and registers them as partials, the filename base is used as the partial name.
func (tpl *Template) RegisterPartialFiles(filePaths ...string) error {
if len(filePaths) == 0 {
return nil
}
for _, filePath := range filePaths {
name := fileBase(filePath)
if err := tpl.RegisterPartialFile(filePath, name); err != nil {
return err
}
}
return nil
}
// 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)
}

View File

@ -1,166 +0,0 @@
package raymond
import (
"fmt"
"testing"
)
var sourceBasic = `<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
</div>`
var basicAST = `CONTENT[ '<div class="entry">
<h1>' ]
{{ PATH:title [] }}
CONTENT[ '</h1>
<div class="body">
' ]
{{ PATH:body [] }}
CONTENT[ '
</div>
</div>' ]
`
func TestNewTemplate(t *testing.T) {
t.Parallel()
tpl := newTemplate(sourceBasic)
if tpl.source != sourceBasic {
t.Errorf("Failed to instantiate template")
}
}
func TestParse(t *testing.T) {
t.Parallel()
tpl, err := Parse(sourceBasic)
if err != nil || (tpl.source != sourceBasic) {
t.Errorf("Failed to parse template")
}
if str := tpl.PrintAST(); str != basicAST {
t.Errorf("Template parsing incorrect: %s", str)
}
}
func TestClone(t *testing.T) {
t.Parallel()
sourcePartial := `I am a {{wat}} partial`
sourcePartial2 := `Partial for the {{wat}}`
tpl := MustParse(sourceBasic)
tpl.RegisterPartial("p", sourcePartial)
if (len(tpl.partials) != 1) || (tpl.partials["p"] == nil) {
t.Errorf("What?")
}
cloned := tpl.Clone()
if (len(cloned.partials) != 1) || (cloned.partials["p"] == nil) {
t.Errorf("Template partials must be cloned")
}
cloned.RegisterPartial("p2", sourcePartial2)
if (len(cloned.partials) != 2) || (cloned.partials["p"] == nil) || (cloned.partials["p2"] == nil) {
t.Errorf("Failed to register a partial on cloned template")
}
if (len(tpl.partials) != 1) || (tpl.partials["p"] == nil) {
t.Errorf("Modification of a cloned template MUST NOT affect original template")
}
}
func ExampleTemplate_Exec() {
source := "<h1>{{title}}</h1><p>{{body.content}}</p>"
ctx := map[string]interface{}{
"title": "foo",
"body": map[string]string{"content": "bar"},
}
// parse template
tpl := MustParse(source)
// evaluate template with context
output, err := tpl.Exec(ctx)
if err != nil {
panic(err)
}
fmt.Print(output)
// Output: <h1>foo</h1><p>bar</p>
}
func ExampleTemplate_MustExec() {
source := "<h1>{{title}}</h1><p>{{body.content}}</p>"
ctx := map[string]interface{}{
"title": "foo",
"body": map[string]string{"content": "bar"},
}
// parse template
tpl := MustParse(source)
// evaluate template with context
output := tpl.MustExec(ctx)
fmt.Print(output)
// Output: <h1>foo</h1><p>bar</p>
}
func ExampleTemplate_ExecWith() {
source := "<h1>{{title}}</h1><p>{{#body}}{{content}} and {{@baz.bat}}{{/body}}</p>"
ctx := map[string]interface{}{
"title": "foo",
"body": map[string]string{"content": "bar"},
}
// parse template
tpl := MustParse(source)
// computes private data frame
frame := NewDataFrame()
frame.Set("baz", map[string]string{"bat": "unicorns"})
// evaluate template
output, err := tpl.ExecWith(ctx, frame)
if err != nil {
panic(err)
}
fmt.Print(output)
// Output: <h1>foo</h1><p>bar and unicorns</p>
}
func ExampleTemplate_PrintAST() {
source := "<h1>{{title}}</h1><p>{{#body}}{{content}} and {{@baz.bat}}{{/body}}</p>"
// parse template
tpl := MustParse(source)
// print AST
output := tpl.PrintAST()
fmt.Print(output)
// Output: CONTENT[ '<h1>' ]
// {{ PATH:title [] }}
// CONTENT[ '</h1><p>' ]
// BLOCK:
// PATH:body []
// PROGRAM:
// {{ PATH:content []
// }}
// CONTENT[ ' and ' ]
// {{ @PATH:baz/bat []
// }}
// CONTENT[ '</p>' ]
//
}

View File

@ -1,85 +0,0 @@
package raymond
import (
"path"
"reflect"
)
// indirect returns the item at the end of indirection, and a bool to indicate if it's nil.
// We indirect through pointers and empty interfaces (only) because
// non-empty interfaces have methods we might need.
//
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
if v.IsNil() {
return v, true
}
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
break
}
}
return v, false
}
// IsTrue returns true if obj is a truthy value.
func IsTrue(obj interface{}) bool {
thruth, ok := isTrueValue(reflect.ValueOf(obj))
if !ok {
return false
}
return thruth
}
// isTrueValue reports whether the value is 'true', in the sense of not the zero of its type,
// and whether the value has a meaningful truth value
//
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
func isTrueValue(val reflect.Value) (truth, ok bool) {
if !val.IsValid() {
// Something like var x interface{}, never set. It's a form of nil.
return false, true
}
switch val.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
truth = val.Len() > 0
case reflect.Bool:
truth = val.Bool()
case reflect.Complex64, reflect.Complex128:
truth = val.Complex() != 0
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
truth = !val.IsNil()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
truth = val.Int() != 0
case reflect.Float32, reflect.Float64:
truth = val.Float() != 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
truth = val.Uint() != 0
case reflect.Struct:
truth = true // Struct values are always true.
default:
return
}
return truth, true
}
// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero.
//
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
func canBeNil(typ reflect.Type) bool {
switch typ.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return true
}
return false
}
// fileBase returns base file name
//
// example: /foo/bar/baz.png => baz
func fileBase(filePath string) string {
fileName := path.Base(filePath)
fileExt := path.Ext(filePath)
return fileName[:len(fileName)-len(fileExt)]
}

View File

@ -1,51 +0,0 @@
package raymond
import "fmt"
func ExampleIsTrue() {
output := "Empty array: " + Str(IsTrue([0]string{})) + "\n"
output += "Non empty array: " + Str(IsTrue([1]string{"foo"})) + "\n"
output += "Empty slice: " + Str(IsTrue([]string{})) + "\n"
output += "Non empty slice: " + Str(IsTrue([]string{"foo"})) + "\n"
output += "Empty map: " + Str(IsTrue(map[string]string{})) + "\n"
output += "Non empty map: " + Str(IsTrue(map[string]string{"foo": "bar"})) + "\n"
output += "Empty string: " + Str(IsTrue("")) + "\n"
output += "Non empty string: " + Str(IsTrue("foo")) + "\n"
output += "true bool: " + Str(IsTrue(true)) + "\n"
output += "false bool: " + Str(IsTrue(false)) + "\n"
output += "0 integer: " + Str(IsTrue(0)) + "\n"
output += "positive integer: " + Str(IsTrue(10)) + "\n"
output += "negative integer: " + Str(IsTrue(-10)) + "\n"
output += "0 float: " + Str(IsTrue(0.0)) + "\n"
output += "positive float: " + Str(IsTrue(10.0)) + "\n"
output += "negative integer: " + Str(IsTrue(-10.0)) + "\n"
output += "struct: " + Str(IsTrue(struct{}{})) + "\n"
output += "nil: " + Str(IsTrue(nil)) + "\n"
fmt.Println(output)
// Output: Empty array: false
// Non empty array: true
// Empty slice: false
// Non empty slice: true
// Empty map: false
// Non empty map: true
// Empty string: false
// Non empty string: true
// true bool: true
// false bool: false
// 0 integer: false
// positive integer: true
// negative integer: true
// 0 float: false
// positive float: true
// negative integer: true
// struct: true
// nil: false
}

View File

@ -1,24 +0,0 @@
# 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

View File

@ -1,9 +0,0 @@
language: go
go:
- 1.8
install:
- go get github.com/golang/lint/golint
- go get github.com/fzipp/gocyclo
- go get github.com/client9/misspell/...
- go get github.com/gordonklaus/ineffassign
script: ./hooks/pre-commit

View File

@ -1,201 +0,0 @@
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.

View File

@ -1,6 +0,0 @@
# gomatrix
[![GoDoc](https://godoc.org/github.com/matrix-org/gomatrix?status.svg)](https://godoc.org/github.com/matrix-org/gomatrix)
A Golang Matrix client.
**THIS IS UNDER ACTIVE DEVELOPMENT: BREAKING CHANGES ARE FREQUENT.**

View File

@ -1,703 +0,0 @@
// Package gomatrix implements the Matrix Client-Server API.
//
// Specification can be found at http://matrix.org/docs/spec/client_server/r0.2.0.html
package gomatrix
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"path"
"strconv"
"sync"
"time"
)
// Client represents a Matrix client.
type Client struct {
HomeserverURL *url.URL // The base homeserver URL
Prefix string // The API prefix eg '/_matrix/client/r0'
UserID string // The user ID of the client. Used for forming HTTP paths which use the client's user ID.
AccessToken string // The access_token for the client.
Client *http.Client // The underlying HTTP client which will be used to make HTTP requests.
Syncer Syncer // The thing which can process /sync responses
Store Storer // The thing which can store rooms/tokens/ids
// The ?user_id= query parameter for application services. This must be set *prior* to calling a method. If this is empty,
// no user_id parameter will be sent.
// See http://matrix.org/docs/spec/application_service/unstable.html#identity-assertion
AppServiceUserID string
syncingMutex sync.Mutex // protects syncingID
syncingID uint32 // Identifies the current Sync. Only one Sync can be active at any given time.
}
// HTTPError An HTTP Error response, which may wrap an underlying native Go Error.
type HTTPError struct {
WrappedError error
Message string
Code int
}
func (e HTTPError) Error() string {
var wrappedErrMsg string
if e.WrappedError != nil {
wrappedErrMsg = e.WrappedError.Error()
}
return fmt.Sprintf("msg=%s code=%d wrapped=%s", e.Message, e.Code, wrappedErrMsg)
}
// BuildURL builds a URL with the Client's homserver/prefix/access_token set already.
func (cli *Client) BuildURL(urlPath ...string) string {
ps := []string{cli.Prefix}
for _, p := range urlPath {
ps = append(ps, p)
}
return cli.BuildBaseURL(ps...)
}
// BuildBaseURL builds a URL with the Client's homeserver/access_token set already. You must
// supply the prefix in the path.
func (cli *Client) BuildBaseURL(urlPath ...string) string {
// copy the URL. Purposefully ignore error as the input is from a valid URL already
hsURL, _ := url.Parse(cli.HomeserverURL.String())
parts := []string{hsURL.Path}
parts = append(parts, urlPath...)
hsURL.Path = path.Join(parts...)
query := hsURL.Query()
if cli.AccessToken != "" {
query.Set("access_token", cli.AccessToken)
}
if cli.AppServiceUserID != "" {
query.Set("user_id", cli.AppServiceUserID)
}
hsURL.RawQuery = query.Encode()
return hsURL.String()
}
// BuildURLWithQuery builds a URL with query parameters in addition to the Client's homeserver/prefix/access_token set already.
func (cli *Client) BuildURLWithQuery(urlPath []string, urlQuery map[string]string) string {
u, _ := url.Parse(cli.BuildURL(urlPath...))
q := u.Query()
for k, v := range urlQuery {
q.Set(k, v)
}
u.RawQuery = q.Encode()
return u.String()
}
// SetCredentials sets the user ID and access token on this client instance.
func (cli *Client) SetCredentials(userID, accessToken string) {
cli.AccessToken = accessToken
cli.UserID = userID
}
// ClearCredentials removes the user ID and access token on this client instance.
func (cli *Client) ClearCredentials() {
cli.AccessToken = ""
cli.UserID = ""
}
// Sync starts syncing with the provided Homeserver. If Sync() is called twice then the first sync will be stopped and the
// error will be nil.
//
// This function will block until a fatal /sync error occurs, so it should almost always be started as a new goroutine.
// Fatal sync errors can be caused by:
// - The failure to create a filter.
// - Client.Syncer.OnFailedSync returning an error in response to a failed sync.
// - Client.Syncer.ProcessResponse returning an error.
// If you wish to continue retrying in spite of these fatal errors, call Sync() again.
func (cli *Client) Sync() error {
// Mark the client as syncing.
// We will keep syncing until the syncing state changes. Either because
// Sync is called or StopSync is called.
syncingID := cli.incrementSyncingID()
nextBatch := cli.Store.LoadNextBatch(cli.UserID)
filterID := cli.Store.LoadFilterID(cli.UserID)
if filterID == "" {
filterJSON := cli.Syncer.GetFilterJSON(cli.UserID)
resFilter, err := cli.CreateFilter(filterJSON)
if err != nil {
return err
}
filterID = resFilter.FilterID
cli.Store.SaveFilterID(cli.UserID, filterID)
}
for {
resSync, err := cli.SyncRequest(30000, nextBatch, filterID, false, "")
if err != nil {
duration, err2 := cli.Syncer.OnFailedSync(resSync, err)
if err2 != nil {
return err2
}
time.Sleep(duration)
continue
}
// Check that the syncing state hasn't changed
// Either because we've stopped syncing or another sync has been started.
// We discard the response from our sync.
if cli.getSyncingID() != syncingID {
return nil
}
// Save the token now *before* processing it. This means it's possible
// to not process some events, but it means that we won't get constantly stuck processing
// a malformed/buggy event which keeps making us panic.
cli.Store.SaveNextBatch(cli.UserID, resSync.NextBatch)
if err = cli.Syncer.ProcessResponse(resSync, nextBatch); err != nil {
return err
}
nextBatch = resSync.NextBatch
}
}
func (cli *Client) incrementSyncingID() uint32 {
cli.syncingMutex.Lock()
defer cli.syncingMutex.Unlock()
cli.syncingID++
return cli.syncingID
}
func (cli *Client) getSyncingID() uint32 {
cli.syncingMutex.Lock()
defer cli.syncingMutex.Unlock()
return cli.syncingID
}
// StopSync stops the ongoing sync started by Sync.
func (cli *Client) StopSync() {
// Advance the syncing state so that any running Syncs will terminate.
cli.incrementSyncingID()
}
// MakeRequest makes a JSON HTTP request to the given URL.
// If "resBody" is not nil, the response body will be json.Unmarshalled into it.
//
// Returns the HTTP body as bytes on 2xx with a nil error. Returns an error if the response is not 2xx along
// with the HTTP body bytes if it got that far. This error is an HTTPError which includes the returned
// HTTP status code and possibly a RespError as the WrappedError, if the HTTP body could be decoded as a RespError.
func (cli *Client) MakeRequest(method string, httpURL string, reqBody interface{}, resBody interface{}) ([]byte, error) {
var req *http.Request
var err error
if reqBody != nil {
var jsonStr []byte
jsonStr, err = json.Marshal(reqBody)
if err != nil {
return nil, err
}
req, err = http.NewRequest(method, httpURL, bytes.NewBuffer(jsonStr))
} else {
req, err = http.NewRequest(method, httpURL, nil)
}
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
res, err := cli.Client.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
contents, err := ioutil.ReadAll(res.Body)
if res.StatusCode/100 != 2 { // not 2xx
var wrap error
var respErr RespError
if _ = json.Unmarshal(contents, &respErr); respErr.ErrCode != "" {
wrap = respErr
}
// If we failed to decode as RespError, don't just drop the HTTP body, include it in the
// HTTP error instead (e.g proxy errors which return HTML).
msg := "Failed to " + method + " JSON to " + req.URL.Path
if wrap == nil {
msg = msg + ": " + string(contents)
}
return contents, HTTPError{
Code: res.StatusCode,
Message: msg,
WrappedError: wrap,
}
}
if err != nil {
return nil, err
}
if resBody != nil {
if err = json.Unmarshal(contents, &resBody); err != nil {
return nil, err
}
}
return contents, nil
}
// CreateFilter makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter
func (cli *Client) CreateFilter(filter json.RawMessage) (resp *RespCreateFilter, err error) {
urlPath := cli.BuildURL("user", cli.UserID, "filter")
_, err = cli.MakeRequest("POST", urlPath, &filter, &resp)
return
}
// SyncRequest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync
func (cli *Client) SyncRequest(timeout int, since, filterID string, fullState bool, setPresence string) (resp *RespSync, err error) {
query := map[string]string{
"timeout": strconv.Itoa(timeout),
}
if since != "" {
query["since"] = since
}
if filterID != "" {
query["filter"] = filterID
}
if setPresence != "" {
query["set_presence"] = setPresence
}
if fullState {
query["full_state"] = "true"
}
urlPath := cli.BuildURLWithQuery([]string{"sync"}, query)
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
func (cli *Client) register(u string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) {
var bodyBytes []byte
bodyBytes, err = cli.MakeRequest("POST", u, req, nil)
if err != nil {
httpErr, ok := err.(HTTPError)
if !ok { // network error
return
}
if httpErr.Code == 401 {
// body should be RespUserInteractive, if it isn't, fail with the error
err = json.Unmarshal(bodyBytes, &uiaResp)
return
}
return
}
// body should be RespRegister
err = json.Unmarshal(bodyBytes, &resp)
return
}
// Register makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
//
// Registers with kind=user. For kind=guest, see RegisterGuest.
func (cli *Client) Register(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) {
u := cli.BuildURL("register")
return cli.register(u, req)
}
// RegisterGuest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
// with kind=guest.
//
// For kind=user, see Register.
func (cli *Client) RegisterGuest(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) {
query := map[string]string{
"kind": "guest",
}
u := cli.BuildURLWithQuery([]string{"register"}, query)
return cli.register(u, req)
}
// RegisterDummy performs m.login.dummy registration according to https://matrix.org/docs/spec/client_server/r0.2.0.html#dummy-auth
//
// Only a username and password need to be provided on the ReqRegister struct. Most local/developer homeservers will allow registration
// this way. If the homeserver does not, an error is returned.
//
// This does not set credentials on the client instance. See SetCredentials() instead.
//
// res, err := cli.RegisterDummy(&gomatrix.ReqRegister{
// Username: "alice",
// Password: "wonderland",
// })
// if err != nil {
// panic(err)
// }
// token := res.AccessToken
func (cli *Client) RegisterDummy(req *ReqRegister) (*RespRegister, error) {
res, uia, err := cli.Register(req)
if err != nil && uia == nil {
return nil, err
}
if uia != nil && uia.HasSingleStageFlow("m.login.dummy") {
req.Auth = struct {
Type string `json:"type"`
Session string `json:"session,omitempty"`
}{"m.login.dummy", uia.Session}
res, _, err = cli.Register(req)
if err != nil {
return nil, err
}
}
if res == nil {
return nil, fmt.Errorf("registration failed: does this server support m.login.dummy?")
}
return res, nil
}
// Login a user to the homeserver according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
// This does not set credentials on this client instance. See SetCredentials() instead.
func (cli *Client) Login(req *ReqLogin) (resp *RespLogin, err error) {
urlPath := cli.BuildURL("login")
_, err = cli.MakeRequest("POST", urlPath, req, &resp)
return
}
// Logout the current user. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout
// This does not clear the credentials from the client instance. See ClearCredentials() instead.
func (cli *Client) Logout() (resp *RespLogout, err error) {
urlPath := cli.BuildURL("logout")
_, err = cli.MakeRequest("POST", urlPath, nil, &resp)
return
}
// Versions returns the list of supported Matrix versions on this homeserver. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-versions
func (cli *Client) Versions() (resp *RespVersions, err error) {
urlPath := cli.BuildBaseURL("_matrix", "client", "versions")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// JoinRoom joins the client to a room ID or alias. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-join-roomidoralias
//
// If serverName is specified, this will be added as a query param to instruct the homeserver to join via that server. If content is specified, it will
// be JSON encoded and used as the request body.
func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{}) (resp *RespJoinRoom, err error) {
var urlPath string
if serverName != "" {
urlPath = cli.BuildURLWithQuery([]string{"join", roomIDorAlias}, map[string]string{
"server_name": serverName,
})
} else {
urlPath = cli.BuildURL("join", roomIDorAlias)
}
_, err = cli.MakeRequest("POST", urlPath, content, &resp)
return
}
// GetDisplayName returns the display name of the user from the specified MXID. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
func (cli *Client) GetDisplayName(mxid string) (resp *RespUserDisplayName, err error) {
urlPath := cli.BuildURL("profile", mxid, "displayname")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// GetOwnDisplayName returns the user's display name. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
func (cli *Client) GetOwnDisplayName() (resp *RespUserDisplayName, err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// SetDisplayName sets the user's profile display name. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-displayname
func (cli *Client) SetDisplayName(displayName string) (err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
s := struct {
DisplayName string `json:"displayname"`
}{displayName}
_, err = cli.MakeRequest("PUT", urlPath, &s, nil)
return
}
// GetAvatarURL gets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-avatar-url
func (cli *Client) GetAvatarURL() (url string, err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url")
s := struct {
AvatarURL string `json:"avatar_url"`
}{}
_, err = cli.MakeRequest("GET", urlPath, nil, &s)
if err != nil {
return "", err
}
return s.AvatarURL, nil
}
// SetAvatarURL sets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-avatar-url
func (cli *Client) SetAvatarURL(url string) (err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url")
s := struct {
AvatarURL string `json:"avatar_url"`
}{url}
_, err = cli.MakeRequest("PUT", urlPath, &s, nil)
if err != nil {
return err
}
return nil
}
// SendMessageEvent sends a message event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
func (cli *Client) SendMessageEvent(roomID string, eventType string, contentJSON interface{}) (resp *RespSendEvent, err error) {
txnID := txnID()
urlPath := cli.BuildURL("rooms", roomID, "send", eventType, txnID)
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
return
}
// SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
func (cli *Client) SendStateEvent(roomID, eventType, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) {
urlPath := cli.BuildURL("rooms", roomID, "state", eventType, stateKey)
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
return
}
// SendText sends an m.room.message event into the given room with a msgtype of m.text
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-text
func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
TextMessage{"m.text", text})
}
// SendImage sends an m.room.message event into the given room with a msgtype of m.image
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
ImageMessage{
MsgType: "m.image",
Body: body,
URL: url,
})
}
// SendVideo sends an m.room.message event into the given room with a msgtype of m.video
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
func (cli *Client) SendVideo(roomID, body, url string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
VideoMessage{
MsgType: "m.video",
Body: body,
URL: url,
})
}
// SendNotice sends an m.room.message event into the given room with a msgtype of m.notice
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-notice
func (cli *Client) SendNotice(roomID, text string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
TextMessage{"m.notice", text})
}
// RedactEvent redacts the given event. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
func (cli *Client) RedactEvent(roomID, eventID string, req *ReqRedact) (resp *RespSendEvent, err error) {
txnID := txnID()
urlPath := cli.BuildURL("rooms", roomID, "redact", eventID, txnID)
_, err = cli.MakeRequest("PUT", urlPath, req, &resp)
return
}
// CreateRoom creates a new Matrix room. See https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
// resp, err := cli.CreateRoom(&gomatrix.ReqCreateRoom{
// Preset: "public_chat",
// })
// fmt.Println("Room:", resp.RoomID)
func (cli *Client) CreateRoom(req *ReqCreateRoom) (resp *RespCreateRoom, err error) {
urlPath := cli.BuildURL("createRoom")
_, err = cli.MakeRequest("POST", urlPath, req, &resp)
return
}
// LeaveRoom leaves the given room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-leave
func (cli *Client) LeaveRoom(roomID string) (resp *RespLeaveRoom, err error) {
u := cli.BuildURL("rooms", roomID, "leave")
_, err = cli.MakeRequest("POST", u, struct{}{}, &resp)
return
}
// ForgetRoom forgets a room entirely. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-forget
func (cli *Client) ForgetRoom(roomID string) (resp *RespForgetRoom, err error) {
u := cli.BuildURL("rooms", roomID, "forget")
_, err = cli.MakeRequest("POST", u, struct{}{}, &resp)
return
}
// InviteUser invites a user to a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite
func (cli *Client) InviteUser(roomID string, req *ReqInviteUser) (resp *RespInviteUser, err error) {
u := cli.BuildURL("rooms", roomID, "invite")
_, err = cli.MakeRequest("POST", u, struct{}{}, &resp)
return
}
// InviteUserByThirdParty invites a third-party identifier to a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#invite-by-third-party-id-endpoint
func (cli *Client) InviteUserByThirdParty(roomID string, req *ReqInvite3PID) (resp *RespInviteUser, err error) {
u := cli.BuildURL("rooms", roomID, "invite")
_, err = cli.MakeRequest("POST", u, req, &resp)
return
}
// KickUser kicks a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick
func (cli *Client) KickUser(roomID string, req *ReqKickUser) (resp *RespKickUser, err error) {
u := cli.BuildURL("rooms", roomID, "kick")
_, err = cli.MakeRequest("POST", u, req, &resp)
return
}
// BanUser bans a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban
func (cli *Client) BanUser(roomID string, req *ReqBanUser) (resp *RespBanUser, err error) {
u := cli.BuildURL("rooms", roomID, "ban")
_, err = cli.MakeRequest("POST", u, req, &resp)
return
}
// UnbanUser unbans a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
func (cli *Client) UnbanUser(roomID string, req *ReqUnbanUser) (resp *RespUnbanUser, err error) {
u := cli.BuildURL("rooms", roomID, "unban")
_, err = cli.MakeRequest("POST", u, req, &resp)
return
}
// UserTyping sets the typing status of the user. See https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
func (cli *Client) UserTyping(roomID string, typing bool, timeout int64) (resp *RespTyping, err error) {
req := ReqTyping{Typing: typing, Timeout: timeout}
u := cli.BuildURL("rooms", roomID, "typing", cli.UserID)
_, err = cli.MakeRequest("PUT", u, req, &resp)
return
}
// StateEvent gets a single state event in a room. It will attempt to JSON unmarshal into the given "outContent" struct with
// the HTTP response body, or return an error.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey
func (cli *Client) StateEvent(roomID, eventType, stateKey string, outContent interface{}) (err error) {
u := cli.BuildURL("rooms", roomID, "state", eventType, stateKey)
_, err = cli.MakeRequest("GET", u, nil, outContent)
return
}
// UploadLink uploads an HTTP URL and then returns an MXC URI.
func (cli *Client) UploadLink(link string) (*RespMediaUpload, error) {
res, err := cli.Client.Get(link)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
return cli.UploadToContentRepo(res.Body, res.Header.Get("Content-Type"), res.ContentLength)
}
// UploadToContentRepo uploads the given bytes to the content repository and returns an MXC URI.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-media-r0-upload
func (cli *Client) UploadToContentRepo(content io.Reader, contentType string, contentLength int64) (*RespMediaUpload, error) {
req, err := http.NewRequest("POST", cli.BuildBaseURL("_matrix/media/r0/upload"), content)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
req.ContentLength = contentLength
res, err := cli.Client.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
contents, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, HTTPError{
Message: "Upload request failed - Failed to read response body: " + err.Error(),
Code: res.StatusCode,
}
}
return nil, HTTPError{
Message: "Upload request failed: " + string(contents),
Code: res.StatusCode,
}
}
var m RespMediaUpload
if err := json.NewDecoder(res.Body).Decode(&m); err != nil {
return nil, err
}
return &m, nil
}
// JoinedMembers returns a map of joined room members. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680
//
// In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes.
// This API is primarily designed for application services which may want to efficiently look up joined members in a room.
func (cli *Client) JoinedMembers(roomID string) (resp *RespJoinedMembers, err error) {
u := cli.BuildURL("rooms", roomID, "joined_members")
_, err = cli.MakeRequest("GET", u, nil, &resp)
return
}
// JoinedRooms returns a list of rooms which the client is joined to. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680
//
// In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes.
// This API is primarily designed for application services which may want to efficiently look up joined rooms.
func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) {
u := cli.BuildURL("joined_rooms")
_, err = cli.MakeRequest("GET", u, nil, &resp)
return
}
// Messages returns a list of message and state events for a room. It uses
// pagination query parameters to paginate history in the room.
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
func (cli *Client) Messages(roomID, from, to string, dir rune, limit int) (resp *RespMessages, err error) {
query := map[string]string{
"from": from,
"dir": string(dir),
}
if to != "" {
query["to"] = to
}
if limit != 0 {
query["limit"] = strconv.Itoa(limit)
}
urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "messages"}, query)
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// TurnServer returns turn server details and credentials for the client to use when initiating calls.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-voip-turnserver
func (cli *Client) TurnServer() (resp *RespTurnServer, err error) {
urlPath := cli.BuildURL("voip", "turnServer")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
func txnID() string {
return "go" + strconv.FormatInt(time.Now().UnixNano(), 10)
}
// NewClient creates a new Matrix Client ready for syncing
func NewClient(homeserverURL, userID, accessToken string) (*Client, error) {
hsURL, err := url.Parse(homeserverURL)
if err != nil {
return nil, err
}
// By default, use an in-memory store which will never save filter ids / next batch tokens to disk.
// The client will work with this storer: it just won't remember across restarts.
// In practice, a database backend should be used.
store := NewInMemoryStore()
cli := Client{
AccessToken: accessToken,
HomeserverURL: hsURL,
UserID: userID,
Prefix: "/_matrix/client/r0",
Syncer: NewDefaultSyncer(userID, store),
Store: store,
}
// By default, use the default HTTP client.
cli.Client = http.DefaultClient
return &cli, nil
}

View File

@ -1,119 +0,0 @@
package gomatrix
import (
"fmt"
"net/http"
)
func Example_sync() {
cli, _ := NewClient("https://matrix.org", "@example:matrix.org", "MDAefhiuwehfuiwe")
cli.Store.SaveFilterID("@example:matrix.org", "2") // Optional: if you know it already
cli.Store.SaveNextBatch("@example:matrix.org", "111_222_333_444") // Optional: if you know it already
syncer := cli.Syncer.(*DefaultSyncer)
syncer.OnEventType("m.room.message", func(ev *Event) {
fmt.Println("Message: ", ev)
})
// Blocking version
if err := cli.Sync(); err != nil {
fmt.Println("Sync() returned ", err)
}
// Non-blocking version
go func() {
for {
if err := cli.Sync(); err != nil {
fmt.Println("Sync() returned ", err)
}
// Optional: Wait a period of time before trying to sync again.
}
}()
}
func Example_customInterfaces() {
// Custom interfaces must be set prior to calling functions on the client.
cli, _ := NewClient("https://matrix.org", "@example:matrix.org", "MDAefhiuwehfuiwe")
// anything which implements the Storer interface
customStore := NewInMemoryStore()
cli.Store = customStore
// anything which implements the Syncer interface
customSyncer := NewDefaultSyncer("@example:matrix.org", customStore)
cli.Syncer = customSyncer
// any http.Client
cli.Client = http.DefaultClient
// Once you call a function, you can't safely change the interfaces.
cli.SendText("!foo:bar", "Down the rabbit hole")
}
func ExampleClient_BuildURLWithQuery() {
cli, _ := NewClient("https://matrix.org", "@example:matrix.org", "abcdef123456")
out := cli.BuildURLWithQuery([]string{"sync"}, map[string]string{
"filter_id": "5",
})
fmt.Println(out)
// Output: https://matrix.org/_matrix/client/r0/sync?access_token=abcdef123456&filter_id=5
}
func ExampleClient_BuildURL() {
userID := "@example:matrix.org"
cli, _ := NewClient("https://matrix.org", userID, "abcdef123456")
out := cli.BuildURL("user", userID, "filter")
fmt.Println(out)
// Output: https://matrix.org/_matrix/client/r0/user/@example:matrix.org/filter?access_token=abcdef123456
}
func ExampleClient_BuildBaseURL() {
userID := "@example:matrix.org"
cli, _ := NewClient("https://matrix.org", userID, "abcdef123456")
out := cli.BuildBaseURL("_matrix", "client", "r0", "directory", "room", "#matrix:matrix.org")
fmt.Println(out)
// Output: https://matrix.org/_matrix/client/r0/directory/room/%23matrix:matrix.org?access_token=abcdef123456
}
// Retrieve the content of a m.room.name state event.
func ExampleClient_StateEvent() {
content := struct {
Name string `json:"name"`
}{}
cli, _ := NewClient("https://matrix.org", "@example:matrix.org", "abcdef123456")
if err := cli.StateEvent("!foo:bar", "m.room.name", "", &content); err != nil {
panic(err)
}
}
// Join a room by ID.
func ExampleClient_JoinRoom_id() {
cli, _ := NewClient("http://localhost:8008", "@example:localhost", "abcdef123456")
if _, err := cli.JoinRoom("!uOILRrqxnsYgQdUzar:localhost", "", nil); err != nil {
panic(err)
}
}
// Join a room by alias.
func ExampleClient_JoinRoom_alias() {
cli, _ := NewClient("http://localhost:8008", "@example:localhost", "abcdef123456")
if resp, err := cli.JoinRoom("#test:localhost", "", nil); err != nil {
panic(err)
} else {
// Use room ID for something.
_ = resp.RoomID
}
}
// Login to a local homeserver and set the user ID and access token on success.
func ExampleClient_Login() {
cli, _ := NewClient("http://localhost:8008", "", "")
resp, err := cli.Login(&ReqLogin{
Type: "m.login.password",
User: "alice",
Password: "wonderland",
})
if err != nil {
panic(err)
}
cli.SetCredentials(resp.UserID, resp.AccessToken)
}

View File

@ -1,105 +0,0 @@
package gomatrix
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"testing"
)
func TestClient_LeaveRoom(t *testing.T) {
cli := mockClient(func(req *http.Request) (*http.Response, error) {
if req.Method == "POST" && req.URL.Path == "/_matrix/client/r0/rooms/!foo:bar/leave" {
return &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(`{}`)),
}, nil
}
return nil, fmt.Errorf("unhandled URL: %s", req.URL.Path)
})
if _, err := cli.LeaveRoom("!foo:bar"); err != nil {
t.Fatalf("LeaveRoom: error, got %s", err.Error())
}
}
func TestClient_GetAvatarUrl(t *testing.T) {
cli := mockClient(func(req *http.Request) (*http.Response, error) {
if req.Method == "GET" && req.URL.Path == "/_matrix/client/r0/profile/@user:test.gomatrix.org/avatar_url" {
return &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(`{"avatar_url":"mxc://matrix.org/iJaUjkshgdfsdkjfn"}`)),
}, nil
}
return nil, fmt.Errorf("unhandled URL: %s", req.URL.Path)
})
if response, err := cli.GetAvatarURL(); err != nil {
t.Fatalf("GetAvatarURL: Got error: %s", err.Error())
} else if response == "" {
t.Fatal("GetAvatarURL: Got empty response")
} else if response != "mxc://matrix.org/iJaUjkshgdfsdkjfn" {
t.Fatalf("Unexpected response URL: %s", response)
}
}
func TestClient_SetAvatarUrl(t *testing.T) {
cli := mockClient(func(req *http.Request) (*http.Response, error) {
if req.Method == "PUT" && req.URL.Path == "/_matrix/client/r0/profile/@user:test.gomatrix.org/avatar_url" {
return &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(`{}`)),
}, nil
}
return nil, fmt.Errorf("unhandled URL: %s", req.URL.Path)
})
if err := cli.SetAvatarURL("https://foo.com/bar.png"); err != nil {
t.Fatalf("GetAvatarURL: Got error: %s", err.Error())
}
}
func TestClient_StateEvent(t *testing.T) {
cli := mockClient(func(req *http.Request) (*http.Response, error) {
if req.Method == "GET" && req.URL.Path == "/_matrix/client/r0/rooms/!foo:bar/state/m.room.name" {
return &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(`{"name":"Room Name Goes Here"}`)),
}, nil
}
return nil, fmt.Errorf("unhandled URL: %s", req.URL.Path)
})
content := struct {
Name string `json:"name"`
}{}
if err := cli.StateEvent("!foo:bar", "m.room.name", "", &content); err != nil {
t.Fatalf("StateEvent: error, got %s", err.Error())
}
if content.Name != "Room Name Goes Here" {
t.Fatalf("StateEvent: got %s, want %s", content.Name, "Room Name Goes Here")
}
}
func mockClient(fn func(*http.Request) (*http.Response, error)) *Client {
mrt := MockRoundTripper{
RT: fn,
}
cli, _ := NewClient("https://test.gomatrix.org", "@user:test.gomatrix.org", "abcdef")
cli.Client = &http.Client{
Transport: mrt,
}
return cli
}
type MockRoundTripper struct {
RT func(*http.Request) (*http.Response, error)
}
func (t MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return t.RT(req)
}

View File

@ -1,101 +0,0 @@
package gomatrix
import (
"html"
"regexp"
)
// Event represents a single Matrix event.
type Event struct {
StateKey *string `json:"state_key,omitempty"` // The state key for the event. Only present on State Events.
Sender string `json:"sender"` // The user ID of the sender of the event
Type string `json:"type"` // The event type
Timestamp int64 `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server
ID string `json:"event_id"` // The unique ID of this event
RoomID string `json:"room_id"` // The room the event was sent to. May be nil (e.g. for presence)
Content map[string]interface{} `json:"content"` // The JSON content of the event.
}
// Body returns the value of the "body" key in the event content if it is
// present and is a string.
func (event *Event) Body() (body string, ok bool) {
value, exists := event.Content["body"]
if !exists {
return
}
body, ok = value.(string)
return
}
// MessageType returns the value of the "msgtype" key in the event content if
// it is present and is a string.
func (event *Event) MessageType() (msgtype string, ok bool) {
value, exists := event.Content["msgtype"]
if !exists {
return
}
msgtype, ok = value.(string)
return
}
// TextMessage is the contents of a Matrix formated message event.
type TextMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
}
// ImageInfo contains info about an image - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
type ImageInfo struct {
Height uint `json:"h,omitempty"`
Width uint `json:"w,omitempty"`
Mimetype string `json:"mimetype,omitempty"`
Size uint `json:"size,omitempty"`
}
// VideoInfo contains info about a video - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
type VideoInfo struct {
Mimetype string `json:"mimetype,omitempty"`
ThumbnailInfo ImageInfo `json:"thumbnail_info"`
ThumbnailURL string `json:"thumbnail_url,omitempty"`
Height uint `json:"h,omitempty"`
Width uint `json:"w,omitempty"`
Duration uint `json:"duration,omitempty"`
Size uint `json:"size,omitempty"`
}
// VideoMessage is an m.video - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
type VideoMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
URL string `json:"url"`
Info VideoInfo `json:"info"`
}
// ImageMessage is an m.image event
type ImageMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
URL string `json:"url"`
Info ImageInfo `json:"info"`
}
// An HTMLMessage is the contents of a Matrix HTML formated message event.
type HTMLMessage struct {
Body string `json:"body"`
MsgType string `json:"msgtype"`
Format string `json:"format"`
FormattedBody string `json:"formatted_body"`
}
var htmlRegex = regexp.MustCompile("<[^<]+?>")
// GetHTMLMessage returns an HTMLMessage with the body set to a stripped version of the provided HTML, in addition
// to the provided HTML.
func GetHTMLMessage(msgtype, htmlText string) HTMLMessage {
return HTMLMessage{
Body: html.UnescapeString(htmlRegex.ReplaceAllLiteralString(htmlText, "")),
MsgType: msgtype,
Format: "org.matrix.custom.html",
FormattedBody: htmlText,
}
}

View File

@ -1,43 +0,0 @@
// Copyright 2017 Jan Christian Grünhage
//
// 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 gomatrix
//Filter is used by clients to specify how the server should filter responses to e.g. sync requests
//Specified by: https://matrix.org/docs/spec/client_server/r0.2.0.html#filtering
type Filter struct {
AccountData FilterPart `json:"account_data,omitempty"`
EventFields []string `json:"event_fields,omitempty"`
EventFormat string `json:"event_format,omitempty"`
Presence FilterPart `json:"presence,omitempty"`
Room struct {
AccountData FilterPart `json:"account_data,omitempty"`
Ephemeral FilterPart `json:"ephemeral,omitempty"`
IncludeLeave bool `json:"include_leave,omitempty"`
NotRooms []string `json:"not_rooms,omitempty"`
Rooms []string `json:"rooms,omitempty"`
State FilterPart `json:"state,omitempty"`
Timeline FilterPart `json:"timeline,omitempty"`
} `json:"room,omitempty"`
}
type FilterPart struct {
NotRooms []string `json:"not_rooms,omitempty"`
Rooms []string `json:"rooms,omitempty"`
Limit *int `json:"limit,omitempty"`
NotSenders []string `json:"not_senders,omitempty"`
NotTypes []string `json:"not_types,omitempty"`
Senders []string `json:"senders,omitempty"`
Types []string `json:"types,omitempty"`
}

View File

@ -1,5 +0,0 @@
#! /bin/bash
DOT_GIT="$(dirname $0)/../.git"
ln -s "../../hooks/pre-commit" "$DOT_GIT/hooks/pre-commit"

View File

@ -1,26 +0,0 @@
#! /bin/bash
set -eu
golint
misspell --error .
# gofmt doesn't exit with an error code if the files don't match the expected
# format. So we have to run it and see if it outputs anything.
if gofmt -l -s . 2>&1 | read
then
echo "Error: not all code had been formatted with gofmt."
echo "Fixing the following files"
gofmt -s -w -l .
echo
echo "Please add them to the commit"
git status --short
exit 1
fi
ineffassign .
go fmt
go tool vet --all --shadow .
gocyclo -over 12 .
go test -timeout 5s -test.v

View File

@ -1,78 +0,0 @@
package gomatrix
// ReqRegister is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
type ReqRegister struct {
Username string `json:"username,omitempty"`
BindEmail bool `json:"bind_email,omitempty"`
Password string `json:"password,omitempty"`
DeviceID string `json:"device_id,omitempty"`
InitialDeviceDisplayName string `json:"initial_device_display_name"`
Auth interface{} `json:"auth,omitempty"`
}
// ReqLogin is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
type ReqLogin struct {
Type string `json:"type"`
Password string `json:"password,omitempty"`
Medium string `json:"medium,omitempty"`
User string `json:"user,omitempty"`
Address string `json:"address,omitempty"`
Token string `json:"token,omitempty"`
DeviceID string `json:"device_id,omitempty"`
InitialDeviceDisplayName string `json:"initial_device_display_name,omitempty"`
}
// ReqCreateRoom is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
type ReqCreateRoom struct {
Visibility string `json:"visibility,omitempty"`
RoomAliasName string `json:"room_alias_name,omitempty"`
Name string `json:"name,omitempty"`
Topic string `json:"topic,omitempty"`
Invite []string `json:"invite,omitempty"`
Invite3PID []ReqInvite3PID `json:"invite_3pid,omitempty"`
CreationContent map[string]interface{} `json:"creation_content,omitempty"`
InitialState []Event `json:"initial_state,omitempty"`
Preset string `json:"preset,omitempty"`
IsDirect bool `json:"is_direct,omitempty"`
}
// ReqRedact is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
type ReqRedact struct {
Reason string `json:"reason,omitempty"`
}
// ReqInvite3PID is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#id57
// It is also a JSON object used in https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
type ReqInvite3PID struct {
IDServer string `json:"id_server"`
Medium string `json:"medium"`
Address string `json:"address"`
}
// ReqInviteUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite
type ReqInviteUser struct {
UserID string `json:"user_id"`
}
// ReqKickUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick
type ReqKickUser struct {
Reason string `json:"reason,omitempty"`
UserID string `json:"user_id"`
}
// ReqBanUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban
type ReqBanUser struct {
Reason string `json:"reason,omitempty"`
UserID string `json:"user_id"`
}
// ReqUnbanUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
type ReqUnbanUser struct {
UserID string `json:"user_id"`
}
// ReqTyping is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
type ReqTyping struct {
Typing bool `json:"typing"`
Timeout int64 `json:"timeout"`
}

View File

@ -1,176 +0,0 @@
package gomatrix
// RespError is the standard JSON error response from Homeservers. It also implements the Golang "error" interface.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#api-standards
type RespError struct {
ErrCode string `json:"errcode"`
Err string `json:"error"`
}
// Error returns the errcode and error message.
func (e RespError) Error() string {
return e.ErrCode + ": " + e.Err
}
// RespCreateFilter is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter
type RespCreateFilter struct {
FilterID string `json:"filter_id"`
}
// RespVersions is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-versions
type RespVersions struct {
Versions []string `json:"versions"`
}
// RespJoinRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-join
type RespJoinRoom struct {
RoomID string `json:"room_id"`
}
// RespLeaveRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-leave
type RespLeaveRoom struct{}
// RespForgetRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-forget
type RespForgetRoom struct{}
// RespInviteUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite
type RespInviteUser struct{}
// RespKickUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick
type RespKickUser struct{}
// RespBanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban
type RespBanUser struct{}
// RespUnbanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
type RespUnbanUser struct{}
// RespTyping is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
type RespTyping struct{}
// RespJoinedRooms is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680
type RespJoinedRooms struct {
JoinedRooms []string `json:"joined_rooms"`
}
// RespJoinedMembers is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680
type RespJoinedMembers struct {
Joined map[string]struct {
DisplayName *string `json:"display_name"`
AvatarURL *string `json:"avatar_url"`
} `json:"joined"`
}
// RespMessages is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
type RespMessages struct {
Start string `json:"start"`
Chunk []Event `json:"chunk"`
End string `json:"end"`
}
// RespSendEvent is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
type RespSendEvent struct {
EventID string `json:"event_id"`
}
// RespMediaUpload is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-media-r0-upload
type RespMediaUpload struct {
ContentURI string `json:"content_uri"`
}
// RespUserInteractive is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#user-interactive-authentication-api
type RespUserInteractive struct {
Flows []struct {
Stages []string `json:"stages"`
} `json:"flows"`
Params map[string]interface{} `json:"params"`
Session string `json:"string"`
Completed []string `json:"completed"`
ErrCode string `json:"errcode"`
Error string `json:"error"`
}
// HasSingleStageFlow returns true if there exists at least 1 Flow with a single stage of stageName.
func (r RespUserInteractive) HasSingleStageFlow(stageName string) bool {
for _, f := range r.Flows {
if len(f.Stages) == 1 && f.Stages[0] == stageName {
return true
}
}
return false
}
// RespUserDisplayName is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
type RespUserDisplayName struct {
DisplayName string `json:"displayname"`
}
// RespRegister is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
type RespRegister struct {
AccessToken string `json:"access_token"`
DeviceID string `json:"device_id"`
HomeServer string `json:"home_server"`
RefreshToken string `json:"refresh_token"`
UserID string `json:"user_id"`
}
// RespLogin is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
type RespLogin struct {
AccessToken string `json:"access_token"`
DeviceID string `json:"device_id"`
HomeServer string `json:"home_server"`
UserID string `json:"user_id"`
}
// RespLogout is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout
type RespLogout struct{}
// RespCreateRoom is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
type RespCreateRoom struct {
RoomID string `json:"room_id"`
}
// RespSync is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync
type RespSync struct {
NextBatch string `json:"next_batch"`
AccountData struct {
Events []Event `json:"events"`
} `json:"account_data"`
Presence struct {
Events []Event `json:"events"`
} `json:"presence"`
Rooms struct {
Leave map[string]struct {
State struct {
Events []Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
} `json:"leave"`
Join map[string]struct {
State struct {
Events []Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
} `json:"join"`
Invite map[string]struct {
State struct {
Events []Event
} `json:"invite_state"`
} `json:"invite"`
} `json:"rooms"`
}
type RespTurnServer struct {
Username string `json:"username"`
Password string `json:"password"`
TTL int `json:"ttl"`
URIs []string `json:"uris"`
}

View File

@ -1,50 +0,0 @@
package gomatrix
// Room represents a single Matrix room.
type Room struct {
ID string
State map[string]map[string]*Event
}
// UpdateState updates the room's current state with the given Event. This will clobber events based
// on the type/state_key combination.
func (room Room) UpdateState(event *Event) {
_, exists := room.State[event.Type]
if !exists {
room.State[event.Type] = make(map[string]*Event)
}
room.State[event.Type][*event.StateKey] = event
}
// GetStateEvent returns the state event for the given type/state_key combo, or nil.
func (room Room) GetStateEvent(eventType string, stateKey string) *Event {
stateEventMap, _ := room.State[eventType]
event, _ := stateEventMap[stateKey]
return event
}
// GetMembershipState returns the membership state of the given user ID in this room. If there is
// no entry for this member, 'leave' is returned for consistency with left users.
func (room Room) GetMembershipState(userID string) string {
state := "leave"
event := room.GetStateEvent("m.room.member", userID)
if event != nil {
membershipState, found := event.Content["membership"]
if found {
mState, isString := membershipState.(string)
if isString {
state = mState
}
}
}
return state
}
// NewRoom creates a new Room with the given ID
func NewRoom(roomID string) *Room {
// Init the State map and return a pointer to the Room
return &Room{
ID: roomID,
State: make(map[string]map[string]*Event),
}
}

View File

@ -1,65 +0,0 @@
package gomatrix
// Storer is an interface which must be satisfied to store client data.
//
// You can either write a struct which persists this data to disk, or you can use the
// provided "InMemoryStore" which just keeps data around in-memory which is lost on
// restarts.
type Storer interface {
SaveFilterID(userID, filterID string)
LoadFilterID(userID string) string
SaveNextBatch(userID, nextBatchToken string)
LoadNextBatch(userID string) string
SaveRoom(room *Room)
LoadRoom(roomID string) *Room
}
// InMemoryStore implements the Storer interface.
//
// Everything is persisted in-memory as maps. It is not safe to load/save filter IDs
// or next batch tokens on any goroutine other than the syncing goroutine: the one
// which called Client.Sync().
type InMemoryStore struct {
Filters map[string]string
NextBatch map[string]string
Rooms map[string]*Room
}
// SaveFilterID to memory.
func (s *InMemoryStore) SaveFilterID(userID, filterID string) {
s.Filters[userID] = filterID
}
// LoadFilterID from memory.
func (s *InMemoryStore) LoadFilterID(userID string) string {
return s.Filters[userID]
}
// SaveNextBatch to memory.
func (s *InMemoryStore) SaveNextBatch(userID, nextBatchToken string) {
s.NextBatch[userID] = nextBatchToken
}
// LoadNextBatch from memory.
func (s *InMemoryStore) LoadNextBatch(userID string) string {
return s.NextBatch[userID]
}
// SaveRoom to memory.
func (s *InMemoryStore) SaveRoom(room *Room) {
s.Rooms[room.ID] = room
}
// LoadRoom from memory.
func (s *InMemoryStore) LoadRoom(roomID string) *Room {
return s.Rooms[roomID]
}
// NewInMemoryStore constructs a new InMemoryStore.
func NewInMemoryStore() *InMemoryStore {
return &InMemoryStore{
Filters: make(map[string]string),
NextBatch: make(map[string]string),
Rooms: make(map[string]*Room),
}
}

View File

@ -1,164 +0,0 @@
package gomatrix
import (
"encoding/json"
"fmt"
"runtime/debug"
"time"
)
// Syncer represents an interface that must be satisfied in order to do /sync requests on a client.
type Syncer interface {
// Process the /sync response. The since parameter is the since= value that was used to produce the response.
// This is useful for detecting the very first sync (since=""). If an error is return, Syncing will be stopped
// permanently.
ProcessResponse(resp *RespSync, since string) error
// OnFailedSync returns either the time to wait before retrying or an error to stop syncing permanently.
OnFailedSync(res *RespSync, err error) (time.Duration, error)
// GetFilterJSON for the given user ID. NOT the filter ID.
GetFilterJSON(userID string) json.RawMessage
}
// DefaultSyncer is the default syncing implementation. You can either write your own syncer, or selectively
// replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer
// pattern to notify callers about incoming events. See DefaultSyncer.OnEventType for more information.
type DefaultSyncer struct {
UserID string
Store Storer
listeners map[string][]OnEventListener // event type to listeners array
}
// OnEventListener can be used with DefaultSyncer.OnEventType to be informed of incoming events.
type OnEventListener func(*Event)
// NewDefaultSyncer returns an instantiated DefaultSyncer
func NewDefaultSyncer(userID string, store Storer) *DefaultSyncer {
return &DefaultSyncer{
UserID: userID,
Store: store,
listeners: make(map[string][]OnEventListener),
}
}
// ProcessResponse processes the /sync response in a way suitable for bots. "Suitable for bots" means a stream of
// unrepeating events. Returns a fatal error if a listener panics.
func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error) {
if !s.shouldProcessResponse(res, since) {
return
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("ProcessResponse panicked! userID=%s since=%s panic=%s\n%s", s.UserID, since, r, debug.Stack())
}
}()
for roomID, roomData := range res.Rooms.Join {
room := s.getOrCreateRoom(roomID)
for _, event := range roomData.State.Events {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
for _, event := range roomData.Timeline.Events {
event.RoomID = roomID
s.notifyListeners(&event)
}
}
for roomID, roomData := range res.Rooms.Invite {
room := s.getOrCreateRoom(roomID)
for _, event := range roomData.State.Events {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
}
for roomID, roomData := range res.Rooms.Leave {
room := s.getOrCreateRoom(roomID)
for _, event := range roomData.Timeline.Events {
if event.StateKey != nil {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
}
}
return
}
// OnEventType allows callers to be notified when there are new events for the given event type.
// There are no duplicate checks.
func (s *DefaultSyncer) OnEventType(eventType string, callback OnEventListener) {
_, exists := s.listeners[eventType]
if !exists {
s.listeners[eventType] = []OnEventListener{}
}
s.listeners[eventType] = append(s.listeners[eventType], callback)
}
// shouldProcessResponse returns true if the response should be processed. May modify the response to remove
// stuff that shouldn't be processed.
func (s *DefaultSyncer) shouldProcessResponse(resp *RespSync, since string) bool {
if since == "" {
return false
}
// This is a horrible hack because /sync will return the most recent messages for a room
// as soon as you /join it. We do NOT want to process those events in that particular room
// because they may have already been processed (if you toggle the bot in/out of the room).
//
// Work around this by inspecting each room's timeline and seeing if an m.room.member event for us
// exists and is "join" and then discard processing that room entirely if so.
// TODO: We probably want to process messages from after the last join event in the timeline.
for roomID, roomData := range resp.Rooms.Join {
for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- {
e := roomData.Timeline.Events[i]
if e.Type == "m.room.member" && e.StateKey != nil && *e.StateKey == s.UserID {
m := e.Content["membership"]
mship, ok := m.(string)
if !ok {
continue
}
if mship == "join" {
_, ok := resp.Rooms.Join[roomID]
if !ok {
continue
}
delete(resp.Rooms.Join, roomID) // don't re-process messages
delete(resp.Rooms.Invite, roomID) // don't re-process invites
break
}
}
}
}
return true
}
// getOrCreateRoom must only be called by the Sync() goroutine which calls ProcessResponse()
func (s *DefaultSyncer) getOrCreateRoom(roomID string) *Room {
room := s.Store.LoadRoom(roomID)
if room == nil { // create a new Room
room = NewRoom(roomID)
s.Store.SaveRoom(room)
}
return room
}
func (s *DefaultSyncer) notifyListeners(event *Event) {
listeners, exists := s.listeners[event.Type]
if !exists {
return
}
for _, fn := range listeners {
fn(event)
}
}
// OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error.
func (s *DefaultSyncer) OnFailedSync(res *RespSync, err error) (time.Duration, error) {
return 10 * time.Second, nil
}
// GetFilterJSON returns a filter with a timeline limit of 50.
func (s *DefaultSyncer) GetFilterJSON(userID string) json.RawMessage {
return json.RawMessage(`{"room":{"timeline":{"limit":50}}}`)
}

View File

@ -1,130 +0,0 @@
package gomatrix
import (
"bytes"
"encoding/hex"
"fmt"
"strings"
)
const lowerhex = "0123456789abcdef"
// encode the given byte using quoted-printable encoding (e.g "=2f")
// and writes it to the buffer
// See https://golang.org/src/mime/quotedprintable/writer.go
func encode(buf *bytes.Buffer, b byte) {
buf.WriteByte('=')
buf.WriteByte(lowerhex[b>>4])
buf.WriteByte(lowerhex[b&0x0f])
}
// escape the given alpha character and writes it to the buffer
func escape(buf *bytes.Buffer, b byte) {
buf.WriteByte('_')
if b == '_' {
buf.WriteByte('_') // another _
} else {
buf.WriteByte(b + 0x20) // ASCII shift A-Z to a-z
}
}
func shouldEncode(b byte) bool {
return b != '-' && b != '.' && b != '_' && !(b >= '0' && b <= '9') && !(b >= 'a' && b <= 'z') && !(b >= 'A' && b <= 'Z')
}
func shouldEscape(b byte) bool {
return (b >= 'A' && b <= 'Z') || b == '_'
}
func isValidByte(b byte) bool {
return isValidEscapedChar(b) || (b >= '0' && b <= '9') || b == '.' || b == '=' || b == '-'
}
func isValidEscapedChar(b byte) bool {
return b == '_' || (b >= 'a' && b <= 'z')
}
// EncodeUserLocalpart encodes the given string into Matrix-compliant user ID localpart form.
// See http://matrix.org/docs/spec/intro.html#mapping-from-other-character-sets
//
// This returns a string with only the characters "a-z0-9._=-". The uppercase range A-Z
// are encoded using leading underscores ("_"). Characters outside the aforementioned ranges
// (including literal underscores ("_") and equals ("=")) are encoded as UTF8 code points (NOT NCRs)
// and converted to lower-case hex with a leading "=". For example:
// Alph@Bet_50up => _alph=40_bet=5f50up
func EncodeUserLocalpart(str string) string {
strBytes := []byte(str)
var outputBuffer bytes.Buffer
for _, b := range strBytes {
if shouldEncode(b) {
encode(&outputBuffer, b)
} else if shouldEscape(b) {
escape(&outputBuffer, b)
} else {
outputBuffer.WriteByte(b)
}
}
return outputBuffer.String()
}
// DecodeUserLocalpart decodes the given string back into the original input string.
// Returns an error if the given string is not a valid user ID localpart encoding.
// See http://matrix.org/docs/spec/intro.html#mapping-from-other-character-sets
//
// This decodes quoted-printable bytes back into UTF8, and unescapes casing. For
// example:
// _alph=40_bet=5f50up => Alph@Bet_50up
// Returns an error if the input string contains characters outside the
// range "a-z0-9._=-", has an invalid quote-printable byte (e.g. not hex), or has
// an invalid _ escaped byte (e.g. "_5").
func DecodeUserLocalpart(str string) (string, error) {
strBytes := []byte(str)
var outputBuffer bytes.Buffer
for i := 0; i < len(strBytes); i++ {
b := strBytes[i]
if !isValidByte(b) {
return "", fmt.Errorf("Byte pos %d: Invalid byte", i)
}
if b == '_' { // next byte is a-z and should be upper-case or is another _ and should be a literal _
if i+1 >= len(strBytes) {
return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding but ran out of string", i)
}
if !isValidEscapedChar(strBytes[i+1]) { // invalid escaping
return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding", i)
}
if strBytes[i+1] == '_' {
outputBuffer.WriteByte('_')
} else {
outputBuffer.WriteByte(strBytes[i+1] - 0x20) // ASCII shift a-z to A-Z
}
i++ // skip next byte since we just handled it
} else if b == '=' { // next 2 bytes are hex and should be buffered ready to be read as utf8
if i+2 >= len(strBytes) {
return "", fmt.Errorf("Byte pos: %d: expected quote-printable encoding but ran out of string", i)
}
dst := make([]byte, 1)
_, err := hex.Decode(dst, strBytes[i+1:i+3])
if err != nil {
return "", err
}
outputBuffer.WriteByte(dst[0])
i += 2 // skip next 2 bytes since we just handled it
} else { // pass through
outputBuffer.WriteByte(b)
}
}
return outputBuffer.String(), nil
}
// ExtractUserLocalpart extracts the localpart portion of a user ID.
// See http://matrix.org/docs/spec/intro.html#user-identifiers
func ExtractUserLocalpart(userID string) (string, error) {
if len(userID) == 0 || userID[0] != '@' {
return "", fmt.Errorf("%s is not a valid user id", userID)
}
return strings.TrimPrefix(
strings.SplitN(userID, ":", 2)[0], // @foo:bar:8448 => [ "@foo", "bar:8448" ]
"@", // remove "@" prefix
), nil
}

View File

@ -1,27 +0,0 @@
package gomatrix
import "fmt"
func ExampleEncodeUserLocalpart() {
localpart := EncodeUserLocalpart("Alph@Bet_50up")
fmt.Println(localpart)
// Output: _alph=40_bet__50up
}
func ExampleDecodeUserLocalpart() {
localpart, err := DecodeUserLocalpart("_alph=40_bet__50up")
if err != nil {
panic(err)
}
fmt.Println(localpart)
// Output: Alph@Bet_50up
}
func ExampleExtractUserLocalpart() {
localpart, err := ExtractUserLocalpart("@alice:matrix.org")
if err != nil {
panic(err)
}
fmt.Println(localpart)
// Output: alice
}

View File

@ -1,86 +0,0 @@
package gomatrix
import (
"testing"
)
var useridtests = []struct {
Input string
Output string
}{
{"Alph@Bet_50up", "_alph=40_bet__50up"}, // The doc example
{"abcdef", "abcdef"}, // no-op
{"i_like_pie_", "i__like__pie__"}, // double underscore escaping
{"ABCDEF", "_a_b_c_d_e_f"}, // all-caps
{"!£", "=21=c2=a3"}, // punctuation and outside ascii range (U+00A3 => c2 a3)
{"___", "______"}, // literal underscores
{"hello-world.", "hello-world."}, // allowed punctuation
{"5+5=10", "5=2b5=3d10"}, // equals sign
{"東方Project", "=e6=9d=b1=e6=96=b9_project"}, // CJK mixed
{" foo bar", "=09foo=20bar"}, // whitespace (tab and space)
}
func TestEncodeUserLocalpart(t *testing.T) {
for _, u := range useridtests {
out := EncodeUserLocalpart(u.Input)
if out != u.Output {
t.Fatalf("TestEncodeUserLocalpart(%s) => Got: %s Expected: %s", u.Input, out, u.Output)
}
}
}
func TestDecodeUserLocalpart(t *testing.T) {
for _, u := range useridtests {
in, _ := DecodeUserLocalpart(u.Output)
if in != u.Input {
t.Fatalf("TestDecodeUserLocalpart(%s) => Got: %s Expected: %s", u.Output, in, u.Input)
}
}
}
var errtests = []struct {
Input string
}{
{"foo@bar"}, // invalid character @
{"foo_5bar"}, // invalid character after _
{"foo_._-bar"}, // multiple invalid characters after _
{"foo=2Hbar"}, // invalid hex after =
{"foo=2hbar"}, // invalid hex after = (lower-case)
{"foo=======2fbar"}, // multiple invalid hex after =
{"foo=2"}, // end of string after =
{"foo_"}, // end of string after _
}
func TestDecodeUserLocalpartErrors(t *testing.T) {
for _, u := range errtests {
out, err := DecodeUserLocalpart(u.Input)
if out != "" {
t.Fatalf("TestDecodeUserLocalpartErrors(%s) => Got: %s Expected: empty string", u.Input, out)
}
if err == nil {
t.Fatalf("TestDecodeUserLocalpartErrors(%s) => Got: nil error Expected: error", u.Input)
}
}
}
var localparttests = []struct {
Input string
ExpectOutput string
}{
{"@foo:bar", "foo"},
{"@foo:bar:8448", "foo"},
{"@foo.bar:baz.quuz", "foo.bar"},
}
func TestExtractUserLocalpart(t *testing.T) {
for _, u := range localparttests {
out, err := ExtractUserLocalpart(u.Input)
if err != nil {
t.Errorf("TestExtractUserLocalpart(%s) => Error: %s", u.Input, err)
continue
}
if out != u.ExpectOutput {
t.Errorf("TestExtractUserLocalpart(%s) => Got: %s, Want %s", u.Input, out, u.ExpectOutput)
}
}
}

View File

@ -1,2 +0,0 @@
[flake8]
max-line-length = 120

View File

@ -1,2 +0,0 @@
*.coverprofile
node_modules/

View File

@ -1,27 +0,0 @@
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

View File

@ -1,435 +0,0 @@
# 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
vendor/github.com/urfave/cli/LICENSE generated vendored
View File

@ -1,21 +0,0 @@
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.

1381
vendor/github.com/urfave/cli/README.md generated vendored

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
package altsrc
//go:generate python ../generate-flag-types altsrc -i ../flag-types.json -o flag_generated.go

View File

@ -1,261 +0,0 @@
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)
}
}

View File

@ -1,347 +0,0 @@
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)
}

View File

@ -1,336 +0,0 @@
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])
}

View File

@ -1,18 +0,0 @@
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))
}
}

View File

@ -1,21 +0,0 @@
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)
}

View File

@ -1,262 +0,0 @@
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] {
if child, ok := node[section]; !ok {
return nil, false
} else {
if ctype, ok := child.(map[interface{}]interface{}); !ok {
return nil, false
} else {
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)
}

View File

@ -1,310 +0,0 @@
// Disabling building of toml support in cases where golang is 1.0 or 1.1
// as the encoding library is not implemented or supported.
// +build go1.2
package altsrc
import (
"flag"
"io/ioutil"
"os"
"testing"
"gopkg.in/urfave/cli.v1"
)
func TestCommandTomFileTest(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("test = 15"), 0666)
defer os.Remove("current.toml")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 15)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "test"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileTestGlobalEnvVarWins(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("test = 15"), 0666)
defer os.Remove("current.toml")
os.Setenv("THE_TEST", "10")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 10)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "test", EnvVar: "THE_TEST"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileTestGlobalEnvVarWinsNested(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666)
defer os.Remove("current.toml")
os.Setenv("THE_TEST", "10")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 10)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "top.test", EnvVar: "THE_TEST"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileTestSpecifiedFlagWins(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("test = 15"), 0666)
defer os.Remove("current.toml")
test := []string{"test-cmd", "--load", "current.toml", "--test", "7"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 7)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "test"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileTestSpecifiedFlagWinsNested(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte(`[top]
test = 15`), 0666)
defer os.Remove("current.toml")
test := []string{"test-cmd", "--load", "current.toml", "--top.test", "7"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 7)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "top.test"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileTestDefaultValueFileWins(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("test = 15"), 0666)
defer os.Remove("current.toml")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 15)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "test", Value: 7}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileTestDefaultValueFileWinsNested(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666)
defer os.Remove("current.toml")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 15)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "top.test", Value: 7}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWins(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("test = 15"), 0666)
defer os.Remove("current.toml")
os.Setenv("THE_TEST", "11")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 11)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "test", Value: 7, EnvVar: "THE_TEST"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWinsNested(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666)
defer os.Remove("current.toml")
os.Setenv("THE_TEST", "11")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 11)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "top.test", Value: 7, EnvVar: "THE_TEST"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}

View File

@ -1,113 +0,0 @@
// Disabling building of toml support in cases where golang is 1.0 or 1.1
// as the encoding library is not implemented or supported.
// +build go1.2
package altsrc
import (
"fmt"
"reflect"
"github.com/BurntSushi/toml"
"gopkg.in/urfave/cli.v1"
)
type tomlMap struct {
Map map[interface{}]interface{}
}
func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) {
ret = make(map[interface{}]interface{})
m := i.(map[string]interface{})
for key, val := range m {
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.Bool:
ret[key] = val.(bool)
case reflect.String:
ret[key] = val.(string)
case reflect.Int:
ret[key] = int(val.(int))
case reflect.Int8:
ret[key] = int(val.(int8))
case reflect.Int16:
ret[key] = int(val.(int16))
case reflect.Int32:
ret[key] = int(val.(int32))
case reflect.Int64:
ret[key] = int(val.(int64))
case reflect.Uint:
ret[key] = int(val.(uint))
case reflect.Uint8:
ret[key] = int(val.(uint8))
case reflect.Uint16:
ret[key] = int(val.(uint16))
case reflect.Uint32:
ret[key] = int(val.(uint32))
case reflect.Uint64:
ret[key] = int(val.(uint64))
case reflect.Float32:
ret[key] = float64(val.(float32))
case reflect.Float64:
ret[key] = float64(val.(float64))
case reflect.Map:
if tmp, err := unmarshalMap(val); err == nil {
ret[key] = tmp
} else {
return nil, err
}
case reflect.Array, reflect.Slice:
ret[key] = val.([]interface{})
default:
return nil, fmt.Errorf("Unsupported: type = %#v", v.Kind())
}
}
return ret, nil
}
func (self *tomlMap) UnmarshalTOML(i interface{}) error {
if tmp, err := unmarshalMap(i); err == nil {
self.Map = tmp
} else {
return err
}
return nil
}
type tomlSourceContext struct {
FilePath string
}
// NewTomlSourceFromFile creates a new TOML InputSourceContext from a filepath.
func NewTomlSourceFromFile(file string) (InputSourceContext, error) {
tsc := &tomlSourceContext{FilePath: file}
var results tomlMap = tomlMap{}
if err := readCommandToml(tsc.FilePath, &results); err != nil {
return nil, fmt.Errorf("Unable to load TOML file '%s': inner error: \n'%v'", tsc.FilePath, err.Error())
}
return &MapInputSource{valueMap: results.Map}, nil
}
// NewTomlSourceFromFlagFunc creates a new TOML InputSourceContext from a provided flag name and source context.
func NewTomlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) {
return func(context *cli.Context) (InputSourceContext, error) {
filePath := context.String(flagFileName)
return NewTomlSourceFromFile(filePath)
}
}
func readCommandToml(filePath string, container interface{}) (err error) {
b, err := loadDataFrom(filePath)
if err != nil {
return err
}
err = toml.Unmarshal(b, container)
if err != nil {
return err
}
err = nil
return
}

View File

@ -1,313 +0,0 @@
// Disabling building of yaml support in cases where golang is 1.0 or 1.1
// as the encoding library is not implemented or supported.
// +build go1.2
package altsrc
import (
"flag"
"io/ioutil"
"os"
"testing"
"gopkg.in/urfave/cli.v1"
)
func TestCommandYamlFileTest(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666)
defer os.Remove("current.yaml")
test := []string{"test-cmd", "--load", "current.yaml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 15)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "test"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandYamlFileTestGlobalEnvVarWins(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666)
defer os.Remove("current.yaml")
os.Setenv("THE_TEST", "10")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", "current.yaml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 10)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "test", EnvVar: "THE_TEST"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandYamlFileTestGlobalEnvVarWinsNested(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.yaml", []byte(`top:
test: 15`), 0666)
defer os.Remove("current.yaml")
os.Setenv("THE_TEST", "10")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", "current.yaml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 10)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "top.test", EnvVar: "THE_TEST"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666)
defer os.Remove("current.yaml")
test := []string{"test-cmd", "--load", "current.yaml", "--test", "7"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 7)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "test"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandYamlFileTestSpecifiedFlagWinsNested(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.yaml", []byte(`top:
test: 15`), 0666)
defer os.Remove("current.yaml")
test := []string{"test-cmd", "--load", "current.yaml", "--top.test", "7"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 7)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "top.test"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666)
defer os.Remove("current.yaml")
test := []string{"test-cmd", "--load", "current.yaml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 15)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "test", Value: 7}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandYamlFileTestDefaultValueFileWinsNested(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.yaml", []byte(`top:
test: 15`), 0666)
defer os.Remove("current.yaml")
test := []string{"test-cmd", "--load", "current.yaml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 15)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "top.test", Value: 7}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWins(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666)
defer os.Remove("current.yaml")
os.Setenv("THE_TEST", "11")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", "current.yaml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 11)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "test", Value: 7, EnvVar: "THE_TEST"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWinsNested(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.yaml", []byte(`top:
test: 15`), 0666)
defer os.Remove("current.yaml")
os.Setenv("THE_TEST", "11")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", "current.yaml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 11)
return nil
},
Flags: []cli.Flag{
NewIntFlag(cli.IntFlag{Name: "top.test", Value: 7, EnvVar: "THE_TEST"}),
cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}

View File

@ -1,92 +0,0 @@
// Disabling building of yaml support in cases where golang is 1.0 or 1.1
// as the encoding library is not implemented or supported.
// +build go1.2
package altsrc
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"runtime"
"strings"
"gopkg.in/urfave/cli.v1"
"gopkg.in/yaml.v2"
)
type yamlSourceContext struct {
FilePath string
}
// NewYamlSourceFromFile creates a new Yaml InputSourceContext from a filepath.
func NewYamlSourceFromFile(file string) (InputSourceContext, error) {
ysc := &yamlSourceContext{FilePath: file}
var results map[interface{}]interface{}
err := readCommandYaml(ysc.FilePath, &results)
if err != nil {
return nil, fmt.Errorf("Unable to load Yaml file '%s': inner error: \n'%v'", ysc.FilePath, err.Error())
}
return &MapInputSource{valueMap: results}, nil
}
// NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a provided flag name and source context.
func NewYamlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) {
return func(context *cli.Context) (InputSourceContext, error) {
filePath := context.String(flagFileName)
return NewYamlSourceFromFile(filePath)
}
}
func readCommandYaml(filePath string, container interface{}) (err error) {
b, err := loadDataFrom(filePath)
if err != nil {
return err
}
err = yaml.Unmarshal(b, container)
if err != nil {
return err
}
err = nil
return
}
func loadDataFrom(filePath string) ([]byte, error) {
u, err := url.Parse(filePath)
if err != nil {
return nil, err
}
if u.Host != "" { // i have a host, now do i support the scheme?
switch u.Scheme {
case "http", "https":
res, err := http.Get(filePath)
if err != nil {
return nil, err
}
return ioutil.ReadAll(res.Body)
default:
return nil, fmt.Errorf("scheme of %s is unsupported", filePath)
}
} else if u.Path != "" { // i dont have a host, but I have a path. I am a local file.
if _, notFoundFileErr := os.Stat(filePath); notFoundFileErr != nil {
return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath)
}
return ioutil.ReadFile(filePath)
} else if runtime.GOOS == "windows" && strings.Contains(u.String(), "\\") {
// on Windows systems u.Path is always empty, so we need to check the string directly.
if _, notFoundFileErr := os.Stat(filePath); notFoundFileErr != nil {
return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath)
}
return ioutil.ReadFile(filePath)
} else {
return nil, fmt.Errorf("unable to determine how to load from path %s", filePath)
}
}

497
vendor/github.com/urfave/cli/app.go generated vendored
View File

@ -1,497 +0,0 @@
package cli
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"time"
)
var (
changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md"
appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL)
runAndExitOnErrorDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-runandexitonerror", changeLogURL)
contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you."
errInvalidActionType = NewExitError("ERROR invalid Action type. "+
fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+
fmt.Sprintf("See %s", appActionDeprecationURL), 2)
)
// App is the main structure of a cli application. It is recommended that
// an app be created with the cli.NewApp() function
type App struct {
// The name of the program. Defaults to path.Base(os.Args[0])
Name string
// Full name of command for help, defaults to Name
HelpName string
// Description of the program.
Usage string
// Text to override the USAGE section of help
UsageText string
// Description of the program argument format.
ArgsUsage string
// Version of the program
Version string
// Description of the program
Description string
// List of commands to execute
Commands []Command
// List of flags to parse
Flags []Flag
// Boolean to enable bash completion commands
EnableBashCompletion bool
// Boolean to hide built-in help command
HideHelp bool
// Boolean to hide built-in version flag and the VERSION section of help
HideVersion bool
// Populate on app startup, only gettable through method Categories()
categories CommandCategories
// An action to execute when the bash-completion flag is set
BashComplete BashCompleteFunc
// An action to execute before any subcommands are run, but after the context is ready
// If a non-nil error is returned, no subcommands are run
Before BeforeFunc
// An action to execute after any subcommands are run, but after the subcommand has finished
// It is run even if Action() panics
After AfterFunc
// The action to execute when no subcommands are specified
// Expects a `cli.ActionFunc` but will accept the *deprecated* signature of `func(*cli.Context) {}`
// *Note*: support for the deprecated `Action` signature will be removed in a future version
Action interface{}
// Execute this function if the proper command cannot be found
CommandNotFound CommandNotFoundFunc
// Execute this function if an usage error occurs
OnUsageError OnUsageErrorFunc
// Compilation date
Compiled time.Time
// List of all authors who contributed
Authors []Author
// Copyright of the binary if any
Copyright string
// Name of Author (Note: Use App.Authors, this is deprecated)
Author string
// Email of Author (Note: Use App.Authors, this is deprecated)
Email string
// Writer writer to write output to
Writer io.Writer
// ErrWriter writes error output
ErrWriter io.Writer
// Other custom info
Metadata map[string]interface{}
// Carries a function which returns app specific info.
ExtraInfo func() map[string]string
// CustomAppHelpTemplate the text template for app help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
CustomAppHelpTemplate string
didSetup bool
}
// Tries to find out when this binary was compiled.
// Returns the current time if it fails to find it.
func compileTime() time.Time {
info, err := os.Stat(os.Args[0])
if err != nil {
return time.Now()
}
return info.ModTime()
}
// NewApp creates a new cli Application with some reasonable defaults for Name,
// Usage, Version and Action.
func NewApp() *App {
return &App{
Name: filepath.Base(os.Args[0]),
HelpName: filepath.Base(os.Args[0]),
Usage: "A new cli application",
UsageText: "",
Version: "0.0.0",
BashComplete: DefaultAppComplete,
Action: helpCommand.Action,
Compiled: compileTime(),
Writer: os.Stdout,
}
}
// Setup runs initialization code to ensure all data structures are ready for
// `Run` or inspection prior to `Run`. It is internally called by `Run`, but
// will return early if setup has already happened.
func (a *App) Setup() {
if a.didSetup {
return
}
a.didSetup = true
if a.Author != "" || a.Email != "" {
a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email})
}
newCmds := []Command{}
for _, c := range a.Commands {
if c.HelpName == "" {
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
}
newCmds = append(newCmds, c)
}
a.Commands = newCmds
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
if (HelpFlag != BoolFlag{}) {
a.appendFlag(HelpFlag)
}
}
if !a.HideVersion {
a.appendFlag(VersionFlag)
}
a.categories = CommandCategories{}
for _, command := range a.Commands {
a.categories = a.categories.AddCommand(command.Category, command)
}
sort.Sort(a.categories)
if a.Metadata == nil {
a.Metadata = make(map[string]interface{})
}
if a.Writer == nil {
a.Writer = os.Stdout
}
}
// Run is the entry point to the cli app. Parses the arguments slice and routes
// to the proper flag/args combination
func (a *App) Run(arguments []string) (err error) {
a.Setup()
// handle the completion flag separately from the flagset since
// completion could be attempted after a flag, but before its value was put
// on the command line. this causes the flagset to interpret the completion
// flag name as the value of the flag before it which is undesirable
// note that we can only do this because the shell autocomplete function
// always appends the completion flag at the end of the command
shellComplete, arguments := checkShellCompleteFlag(a, arguments)
// parse flags
set, err := flagSet(a.Name, a.Flags)
if err != nil {
return err
}
set.SetOutput(ioutil.Discard)
err = set.Parse(arguments[1:])
nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, nil)
if nerr != nil {
fmt.Fprintln(a.Writer, nerr)
ShowAppHelp(context)
return nerr
}
context.shellComplete = shellComplete
if checkCompletions(context) {
return nil
}
if err != nil {
if a.OnUsageError != nil {
err := a.OnUsageError(context, err, false)
HandleExitCoder(err)
return err
}
fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
ShowAppHelp(context)
return err
}
if !a.HideHelp && checkHelp(context) {
ShowAppHelp(context)
return nil
}
if !a.HideVersion && checkVersion(context) {
ShowVersion(context)
return nil
}
if a.After != nil {
defer func() {
if afterErr := a.After(context); afterErr != nil {
if err != nil {
err = NewMultiError(err, afterErr)
} else {
err = afterErr
}
}
}()
}
if a.Before != nil {
beforeErr := a.Before(context)
if beforeErr != nil {
ShowAppHelp(context)
HandleExitCoder(beforeErr)
err = beforeErr
return err
}
}
args := context.Args()
if args.Present() {
name := args.First()
c := a.Command(name)
if c != nil {
return c.Run(context)
}
}
if a.Action == nil {
a.Action = helpCommand.Action
}
// Run default Action
err = HandleAction(a.Action, context)
HandleExitCoder(err)
return err
}
// RunAndExitOnError calls .Run() and exits non-zero if an error was returned
//
// Deprecated: instead you should return an error that fulfills cli.ExitCoder
// to cli.App.Run. This will cause the application to exit with the given eror
// code in the cli.ExitCoder
func (a *App) RunAndExitOnError() {
if err := a.Run(os.Args); err != nil {
fmt.Fprintln(a.errWriter(), err)
OsExiter(1)
}
}
// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to
// generate command-specific flags
func (a *App) RunAsSubcommand(ctx *Context) (err error) {
// append help to commands
if len(a.Commands) > 0 {
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
if (HelpFlag != BoolFlag{}) {
a.appendFlag(HelpFlag)
}
}
}
newCmds := []Command{}
for _, c := range a.Commands {
if c.HelpName == "" {
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
}
newCmds = append(newCmds, c)
}
a.Commands = newCmds
// parse flags
set, err := flagSet(a.Name, a.Flags)
if err != nil {
return err
}
set.SetOutput(ioutil.Discard)
err = set.Parse(ctx.Args().Tail())
nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, ctx)
if nerr != nil {
fmt.Fprintln(a.Writer, nerr)
fmt.Fprintln(a.Writer)
if len(a.Commands) > 0 {
ShowSubcommandHelp(context)
} else {
ShowCommandHelp(ctx, context.Args().First())
}
return nerr
}
if checkCompletions(context) {
return nil
}
if err != nil {
if a.OnUsageError != nil {
err = a.OnUsageError(context, err, true)
HandleExitCoder(err)
return err
}
fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
ShowSubcommandHelp(context)
return err
}
if len(a.Commands) > 0 {
if checkSubcommandHelp(context) {
return nil
}
} else {
if checkCommandHelp(ctx, context.Args().First()) {
return nil
}
}
if a.After != nil {
defer func() {
afterErr := a.After(context)
if afterErr != nil {
HandleExitCoder(err)
if err != nil {
err = NewMultiError(err, afterErr)
} else {
err = afterErr
}
}
}()
}
if a.Before != nil {
beforeErr := a.Before(context)
if beforeErr != nil {
HandleExitCoder(beforeErr)
err = beforeErr
return err
}
}
args := context.Args()
if args.Present() {
name := args.First()
c := a.Command(name)
if c != nil {
return c.Run(context)
}
}
// Run default Action
err = HandleAction(a.Action, context)
HandleExitCoder(err)
return err
}
// Command returns the named command on App. Returns nil if the command does not exist
func (a *App) Command(name string) *Command {
for _, c := range a.Commands {
if c.HasName(name) {
return &c
}
}
return nil
}
// Categories returns a slice containing all the categories with the commands they contain
func (a *App) Categories() CommandCategories {
return a.categories
}
// VisibleCategories returns a slice of categories and commands that are
// Hidden=false
func (a *App) VisibleCategories() []*CommandCategory {
ret := []*CommandCategory{}
for _, category := range a.categories {
if visible := func() *CommandCategory {
for _, command := range category.Commands {
if !command.Hidden {
return category
}
}
return nil
}(); visible != nil {
ret = append(ret, visible)
}
}
return ret
}
// VisibleCommands returns a slice of the Commands with Hidden=false
func (a *App) VisibleCommands() []Command {
ret := []Command{}
for _, command := range a.Commands {
if !command.Hidden {
ret = append(ret, command)
}
}
return ret
}
// VisibleFlags returns a slice of the Flags with Hidden=false
func (a *App) VisibleFlags() []Flag {
return visibleFlags(a.Flags)
}
func (a *App) hasFlag(flag Flag) bool {
for _, f := range a.Flags {
if flag == f {
return true
}
}
return false
}
func (a *App) errWriter() io.Writer {
// When the app ErrWriter is nil use the package level one.
if a.ErrWriter == nil {
return ErrWriter
}
return a.ErrWriter
}
func (a *App) appendFlag(flag Flag) {
if !a.hasFlag(flag) {
a.Flags = append(a.Flags, flag)
}
}
// Author represents someone who has contributed to a cli project.
type Author struct {
Name string // The Authors name
Email string // The Authors email
}
// String makes Author comply to the Stringer interface, to allow an easy print in the templating process
func (a Author) String() string {
e := ""
if a.Email != "" {
e = " <" + a.Email + ">"
}
return fmt.Sprintf("%v%v", a.Name, e)
}
// HandleAction attempts to figure out which Action signature was used. If
// it's an ActionFunc or a func with the legacy signature for Action, the func
// is run!
func HandleAction(action interface{}, context *Context) (err error) {
if a, ok := action.(ActionFunc); ok {
return a(context)
} else if a, ok := action.(func(*Context) error); ok {
return a(context)
} else if a, ok := action.(func(*Context)); ok { // deprecated function signature
a(context)
return nil
} else {
return errInvalidActionType
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +0,0 @@
version: "{build}"
os: Windows Server 2016
image: Visual Studio 2017
clone_folder: c:\gopath\src\github.com\urfave\cli
environment:
GOPATH: C:\gopath
GOVERSION: 1.8.x
PYTHON: C:\Python36-x64
PYTHON_VERSION: 3.6.x
PYTHON_ARCH: 64
install:
- set PATH=%GOPATH%\bin;C:\go\bin;%PATH%
- go version
- go env
- go get github.com/urfave/gfmrun/...
- go get -v -t ./...
build_script:
- python runtests vet
- python runtests test
- python runtests gfmrun

View File

@ -1,16 +0,0 @@
#! /bin/bash
: ${PROG:=$(basename ${BASH_SOURCE})}
_cli_bash_autocomplete() {
local cur opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
complete -F _cli_bash_autocomplete $PROG
unset PROG

View File

@ -1,5 +0,0 @@
autoload -U compinit && compinit
autoload -U bashcompinit && bashcompinit
script_dir=$(dirname $0)
source ${script_dir}/bash_autocomplete

View File

@ -1,44 +0,0 @@
package cli
// CommandCategories is a slice of *CommandCategory.
type CommandCategories []*CommandCategory
// CommandCategory is a category containing commands.
type CommandCategory struct {
Name string
Commands Commands
}
func (c CommandCategories) Less(i, j int) bool {
return c[i].Name < c[j].Name
}
func (c CommandCategories) Len() int {
return len(c)
}
func (c CommandCategories) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
// AddCommand adds a command to a category.
func (c CommandCategories) AddCommand(category string, command Command) CommandCategories {
for _, commandCategory := range c {
if commandCategory.Name == category {
commandCategory.Commands = append(commandCategory.Commands, command)
return c
}
}
return append(c, &CommandCategory{Name: category, Commands: []Command{command}})
}
// VisibleCommands returns a slice of the Commands with Hidden=false
func (c *CommandCategory) VisibleCommands() []Command {
ret := []Command{}
for _, command := range c.Commands {
if !command.Hidden {
ret = append(ret, command)
}
}
return ret
}

22
vendor/github.com/urfave/cli/cli.go generated vendored
View File

@ -1,22 +0,0 @@
// Package cli provides a minimal framework for creating and organizing command line
// Go applications. cli is designed to be easy to understand and write, the most simple
// cli application can be written as follows:
// func main() {
// cli.NewApp().Run(os.Args)
// }
//
// Of course this application does not do much, so let's make this an actual application:
// func main() {
// app := cli.NewApp()
// app.Name = "greet"
// app.Usage = "say a greeting"
// app.Action = func(c *cli.Context) error {
// println("Greetings")
// return nil
// }
//
// app.Run(os.Args)
// }
package cli
//go:generate python ./generate-flag-types cli -i flag-types.json -o flag_generated.go

View File

@ -1,304 +0,0 @@
package cli
import (
"fmt"
"io/ioutil"
"sort"
"strings"
)
// Command is a subcommand for a cli.App.
type Command struct {
// The name of the command
Name string
// short name of the command. Typically one character (deprecated, use `Aliases`)
ShortName string
// A list of aliases for the command
Aliases []string
// A short description of the usage of this command
Usage string
// Custom text to show on USAGE section of help
UsageText string
// A longer explanation of how the command works
Description string
// A short description of the arguments of this command
ArgsUsage string
// The category the command is part of
Category string
// The function to call when checking for bash command completions
BashComplete BashCompleteFunc
// An action to execute before any sub-subcommands are run, but after the context is ready
// If a non-nil error is returned, no sub-subcommands are run
Before BeforeFunc
// An action to execute after any subcommands are run, but after the subcommand has finished
// It is run even if Action() panics
After AfterFunc
// The function to call when this command is invoked
Action interface{}
// TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind
// of deprecation period has passed, maybe?
// Execute this function if a usage error occurs.
OnUsageError OnUsageErrorFunc
// List of child commands
Subcommands Commands
// List of flags to parse
Flags []Flag
// Treat all flags as normal arguments if true
SkipFlagParsing bool
// Skip argument reordering which attempts to move flags before arguments,
// but only works if all flags appear after all arguments. This behavior was
// removed n version 2 since it only works under specific conditions so we
// backport here by exposing it as an option for compatibility.
SkipArgReorder bool
// Boolean to hide built-in help command
HideHelp bool
// Boolean to hide this command from help or completion
Hidden bool
// Full name of command for help, defaults to full command name, including parent commands.
HelpName string
commandNamePath []string
// CustomHelpTemplate the text template for the command help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
CustomHelpTemplate string
}
type CommandsByName []Command
func (c CommandsByName) Len() int {
return len(c)
}
func (c CommandsByName) Less(i, j int) bool {
return c[i].Name < c[j].Name
}
func (c CommandsByName) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
// FullName returns the full name of the command.
// For subcommands this ensures that parent commands are part of the command path
func (c Command) FullName() string {
if c.commandNamePath == nil {
return c.Name
}
return strings.Join(c.commandNamePath, " ")
}
// Commands is a slice of Command
type Commands []Command
// Run invokes the command given the context, parses ctx.Args() to generate command-specific flags
func (c Command) Run(ctx *Context) (err error) {
if len(c.Subcommands) > 0 {
return c.startApp(ctx)
}
if !c.HideHelp && (HelpFlag != BoolFlag{}) {
// append help to flags
c.Flags = append(
c.Flags,
HelpFlag,
)
}
set, err := flagSet(c.Name, c.Flags)
if err != nil {
return err
}
set.SetOutput(ioutil.Discard)
if c.SkipFlagParsing {
err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...))
} else if !c.SkipArgReorder {
firstFlagIndex := -1
terminatorIndex := -1
for index, arg := range ctx.Args() {
if arg == "--" {
terminatorIndex = index
break
} else if arg == "-" {
// Do nothing. A dash alone is not really a flag.
continue
} else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 {
firstFlagIndex = index
}
}
if firstFlagIndex > -1 {
args := ctx.Args()
regularArgs := make([]string, len(args[1:firstFlagIndex]))
copy(regularArgs, args[1:firstFlagIndex])
var flagArgs []string
if terminatorIndex > -1 {
flagArgs = args[firstFlagIndex:terminatorIndex]
regularArgs = append(regularArgs, args[terminatorIndex:]...)
} else {
flagArgs = args[firstFlagIndex:]
}
err = set.Parse(append(flagArgs, regularArgs...))
} else {
err = set.Parse(ctx.Args().Tail())
}
} else {
err = set.Parse(ctx.Args().Tail())
}
nerr := normalizeFlags(c.Flags, set)
if nerr != nil {
fmt.Fprintln(ctx.App.Writer, nerr)
fmt.Fprintln(ctx.App.Writer)
ShowCommandHelp(ctx, c.Name)
return nerr
}
context := NewContext(ctx.App, set, ctx)
context.Command = c
if checkCommandCompletions(context, c.Name) {
return nil
}
if err != nil {
if c.OnUsageError != nil {
err := c.OnUsageError(context, err, false)
HandleExitCoder(err)
return err
}
fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error())
fmt.Fprintln(context.App.Writer)
ShowCommandHelp(context, c.Name)
return err
}
if checkCommandHelp(context, c.Name) {
return nil
}
if c.After != nil {
defer func() {
afterErr := c.After(context)
if afterErr != nil {
HandleExitCoder(err)
if err != nil {
err = NewMultiError(err, afterErr)
} else {
err = afterErr
}
}
}()
}
if c.Before != nil {
err = c.Before(context)
if err != nil {
ShowCommandHelp(context, c.Name)
HandleExitCoder(err)
return err
}
}
if c.Action == nil {
c.Action = helpSubcommand.Action
}
err = HandleAction(c.Action, context)
if err != nil {
HandleExitCoder(err)
}
return err
}
// Names returns the names including short names and aliases.
func (c Command) Names() []string {
names := []string{c.Name}
if c.ShortName != "" {
names = append(names, c.ShortName)
}
return append(names, c.Aliases...)
}
// HasName returns true if Command.Name or Command.ShortName matches given name
func (c Command) HasName(name string) bool {
for _, n := range c.Names() {
if n == name {
return true
}
}
return false
}
func (c Command) startApp(ctx *Context) error {
app := NewApp()
app.Metadata = ctx.App.Metadata
// set the name and usage
app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name)
if c.HelpName == "" {
app.HelpName = c.HelpName
} else {
app.HelpName = app.Name
}
app.Usage = c.Usage
app.Description = c.Description
app.ArgsUsage = c.ArgsUsage
// set CommandNotFound
app.CommandNotFound = ctx.App.CommandNotFound
app.CustomAppHelpTemplate = c.CustomHelpTemplate
// set the flags and commands
app.Commands = c.Subcommands
app.Flags = c.Flags
app.HideHelp = c.HideHelp
app.Version = ctx.App.Version
app.HideVersion = ctx.App.HideVersion
app.Compiled = ctx.App.Compiled
app.Author = ctx.App.Author
app.Email = ctx.App.Email
app.Writer = ctx.App.Writer
app.ErrWriter = ctx.App.ErrWriter
app.categories = CommandCategories{}
for _, command := range c.Subcommands {
app.categories = app.categories.AddCommand(command.Category, command)
}
sort.Sort(app.categories)
// bash completion
app.EnableBashCompletion = ctx.App.EnableBashCompletion
if c.BashComplete != nil {
app.BashComplete = c.BashComplete
}
// set the actions
app.Before = c.Before
app.After = c.After
if c.Action != nil {
app.Action = c.Action
} else {
app.Action = helpSubcommand.Action
}
app.OnUsageError = c.OnUsageError
for index, cc := range app.Commands {
app.Commands[index].commandNamePath = []string{c.Name, cc.Name}
}
return app.RunAsSubcommand(ctx)
}
// VisibleFlags returns a slice of the Flags with Hidden=false
func (c Command) VisibleFlags() []Flag {
return visibleFlags(c.Flags)
}

View File

@ -1,240 +0,0 @@
package cli
import (
"errors"
"flag"
"fmt"
"io/ioutil"
"strings"
"testing"
)
func TestCommandFlagParsing(t *testing.T) {
cases := []struct {
testArgs []string
skipFlagParsing bool
skipArgReorder bool
expectedErr error
}{
// Test normal "not ignoring flags" flow
{[]string{"test-cmd", "blah", "blah", "-break"}, false, false, errors.New("flag provided but not defined: -break")},
// Test no arg reorder
{[]string{"test-cmd", "blah", "blah", "-break"}, false, true, nil},
{[]string{"test-cmd", "blah", "blah"}, true, false, nil}, // Test SkipFlagParsing without any args that look like flags
{[]string{"test-cmd", "blah", "-break"}, true, false, nil}, // Test SkipFlagParsing with random flag arg
{[]string{"test-cmd", "blah", "-help"}, true, false, nil}, // Test SkipFlagParsing with "special" help flag arg
}
for _, c := range cases {
app := NewApp()
app.Writer = ioutil.Discard
set := flag.NewFlagSet("test", 0)
set.Parse(c.testArgs)
context := NewContext(app, set, nil)
command := Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(_ *Context) error { return nil },
SkipFlagParsing: c.skipFlagParsing,
SkipArgReorder: c.skipArgReorder,
}
err := command.Run(context)
expect(t, err, c.expectedErr)
expect(t, []string(context.Args()), c.testArgs)
}
}
func TestCommand_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) {
app := NewApp()
app.Commands = []Command{
{
Name: "bar",
Before: func(c *Context) error {
return fmt.Errorf("before error")
},
After: func(c *Context) error {
return fmt.Errorf("after error")
},
},
}
err := app.Run([]string{"foo", "bar"})
if err == nil {
t.Fatalf("expected to receive error from Run, got none")
}
if !strings.Contains(err.Error(), "before error") {
t.Errorf("expected text of error from Before method, but got none in \"%v\"", err)
}
if !strings.Contains(err.Error(), "after error") {
t.Errorf("expected text of error from After method, but got none in \"%v\"", err)
}
}
func TestCommand_Run_BeforeSavesMetadata(t *testing.T) {
var receivedMsgFromAction string
var receivedMsgFromAfter string
app := NewApp()
app.Commands = []Command{
{
Name: "bar",
Before: func(c *Context) error {
c.App.Metadata["msg"] = "hello world"
return nil
},
Action: func(c *Context) error {
msg, ok := c.App.Metadata["msg"]
if !ok {
return errors.New("msg not found")
}
receivedMsgFromAction = msg.(string)
return nil
},
After: func(c *Context) error {
msg, ok := c.App.Metadata["msg"]
if !ok {
return errors.New("msg not found")
}
receivedMsgFromAfter = msg.(string)
return nil
},
},
}
err := app.Run([]string{"foo", "bar"})
if err != nil {
t.Fatalf("expected no error from Run, got %s", err)
}
expectedMsg := "hello world"
if receivedMsgFromAction != expectedMsg {
t.Fatalf("expected msg from Action to match. Given: %q\nExpected: %q",
receivedMsgFromAction, expectedMsg)
}
if receivedMsgFromAfter != expectedMsg {
t.Fatalf("expected msg from After to match. Given: %q\nExpected: %q",
receivedMsgFromAction, expectedMsg)
}
}
func TestCommand_OnUsageError_hasCommandContext(t *testing.T) {
app := NewApp()
app.Commands = []Command{
{
Name: "bar",
Flags: []Flag{
IntFlag{Name: "flag"},
},
OnUsageError: func(c *Context, err error, _ bool) error {
return fmt.Errorf("intercepted in %s: %s", c.Command.Name, err.Error())
},
},
}
err := app.Run([]string{"foo", "bar", "--flag=wrong"})
if err == nil {
t.Fatalf("expected to receive error from Run, got none")
}
if !strings.HasPrefix(err.Error(), "intercepted in bar") {
t.Errorf("Expect an intercepted error, but got \"%v\"", err)
}
}
func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) {
app := NewApp()
app.Commands = []Command{
{
Name: "bar",
Flags: []Flag{
IntFlag{Name: "flag"},
},
OnUsageError: func(c *Context, err error, _ bool) error {
if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") {
t.Errorf("Expect an invalid value error, but got \"%v\"", err)
}
return errors.New("intercepted: " + err.Error())
},
},
}
err := app.Run([]string{"foo", "bar", "--flag=wrong"})
if err == nil {
t.Fatalf("expected to receive error from Run, got none")
}
if !strings.HasPrefix(err.Error(), "intercepted: invalid value") {
t.Errorf("Expect an intercepted error, but got \"%v\"", err)
}
}
func TestCommand_OnUsageError_WithSubcommand(t *testing.T) {
app := NewApp()
app.Commands = []Command{
{
Name: "bar",
Subcommands: []Command{
{
Name: "baz",
},
},
Flags: []Flag{
IntFlag{Name: "flag"},
},
OnUsageError: func(c *Context, err error, _ bool) error {
if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") {
t.Errorf("Expect an invalid value error, but got \"%v\"", err)
}
return errors.New("intercepted: " + err.Error())
},
},
}
err := app.Run([]string{"foo", "bar", "--flag=wrong"})
if err == nil {
t.Fatalf("expected to receive error from Run, got none")
}
if !strings.HasPrefix(err.Error(), "intercepted: invalid value") {
t.Errorf("Expect an intercepted error, but got \"%v\"", err)
}
}
func TestCommand_Run_SubcommandsCanUseErrWriter(t *testing.T) {
app := NewApp()
app.ErrWriter = ioutil.Discard
app.Commands = []Command{
{
Name: "bar",
Usage: "this is for testing",
Subcommands: []Command{
{
Name: "baz",
Usage: "this is for testing",
Action: func(c *Context) error {
if c.App.ErrWriter != ioutil.Discard {
return fmt.Errorf("ErrWriter not passed")
}
return nil
},
},
},
},
}
err := app.Run([]string{"foo", "bar", "baz"})
if err != nil {
t.Fatal(err)
}
}

View File

@ -1,278 +0,0 @@
package cli
import (
"errors"
"flag"
"reflect"
"strings"
"syscall"
)
// Context is a type that is passed through to
// each Handler action in a cli application. Context
// can be used to retrieve context-specific Args and
// parsed command-line options.
type Context struct {
App *App
Command Command
shellComplete bool
flagSet *flag.FlagSet
setFlags map[string]bool
parentContext *Context
}
// NewContext creates a new context. For use in when invoking an App or Command action.
func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context {
c := &Context{App: app, flagSet: set, parentContext: parentCtx}
if parentCtx != nil {
c.shellComplete = parentCtx.shellComplete
}
return c
}
// NumFlags returns the number of flags set
func (c *Context) NumFlags() int {
return c.flagSet.NFlag()
}
// Set sets a context flag to a value.
func (c *Context) Set(name, value string) error {
c.setFlags = nil
return c.flagSet.Set(name, value)
}
// GlobalSet sets a context flag to a value on the global flagset
func (c *Context) GlobalSet(name, value string) error {
globalContext(c).setFlags = nil
return globalContext(c).flagSet.Set(name, value)
}
// IsSet determines if the flag was actually set
func (c *Context) IsSet(name string) bool {
if c.setFlags == nil {
c.setFlags = make(map[string]bool)
c.flagSet.Visit(func(f *flag.Flag) {
c.setFlags[f.Name] = true
})
c.flagSet.VisitAll(func(f *flag.Flag) {
if _, ok := c.setFlags[f.Name]; ok {
return
}
c.setFlags[f.Name] = false
})
// XXX hack to support IsSet for flags with EnvVar
//
// There isn't an easy way to do this with the current implementation since
// whether a flag was set via an environment variable is very difficult to
// determine here. Instead, we intend to introduce a backwards incompatible
// change in version 2 to add `IsSet` to the Flag interface to push the
// responsibility closer to where the information required to determine
// whether a flag is set by non-standard means such as environment
// variables is avaliable.
//
// See https://github.com/urfave/cli/issues/294 for additional discussion
flags := c.Command.Flags
if c.Command.Name == "" { // cannot == Command{} since it contains slice types
if c.App != nil {
flags = c.App.Flags
}
}
for _, f := range flags {
eachName(f.GetName(), func(name string) {
if isSet, ok := c.setFlags[name]; isSet || !ok {
return
}
val := reflect.ValueOf(f)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
envVarValue := val.FieldByName("EnvVar")
if !envVarValue.IsValid() {
return
}
eachName(envVarValue.String(), func(envVar string) {
envVar = strings.TrimSpace(envVar)
if _, ok := syscall.Getenv(envVar); ok {
c.setFlags[name] = true
return
}
})
})
}
}
return c.setFlags[name]
}
// GlobalIsSet determines if the global flag was actually set
func (c *Context) GlobalIsSet(name string) bool {
ctx := c
if ctx.parentContext != nil {
ctx = ctx.parentContext
}
for ; ctx != nil; ctx = ctx.parentContext {
if ctx.IsSet(name) {
return true
}
}
return false
}
// FlagNames returns a slice of flag names used in this context.
func (c *Context) FlagNames() (names []string) {
for _, flag := range c.Command.Flags {
name := strings.Split(flag.GetName(), ",")[0]
if name == "help" {
continue
}
names = append(names, name)
}
return
}
// GlobalFlagNames returns a slice of global flag names used by the app.
func (c *Context) GlobalFlagNames() (names []string) {
for _, flag := range c.App.Flags {
name := strings.Split(flag.GetName(), ",")[0]
if name == "help" || name == "version" {
continue
}
names = append(names, name)
}
return
}
// Parent returns the parent context, if any
func (c *Context) Parent() *Context {
return c.parentContext
}
// value returns the value of the flag coressponding to `name`
func (c *Context) value(name string) interface{} {
return c.flagSet.Lookup(name).Value.(flag.Getter).Get()
}
// Args contains apps console arguments
type Args []string
// Args returns the command line arguments associated with the context.
func (c *Context) Args() Args {
args := Args(c.flagSet.Args())
return args
}
// NArg returns the number of the command line arguments.
func (c *Context) NArg() int {
return len(c.Args())
}
// Get returns the nth argument, or else a blank string
func (a Args) Get(n int) string {
if len(a) > n {
return a[n]
}
return ""
}
// First returns the first argument, or else a blank string
func (a Args) First() string {
return a.Get(0)
}
// Tail returns the rest of the arguments (not the first one)
// or else an empty string slice
func (a Args) Tail() []string {
if len(a) >= 2 {
return []string(a)[1:]
}
return []string{}
}
// Present checks if there are any arguments present
func (a Args) Present() bool {
return len(a) != 0
}
// Swap swaps arguments at the given indexes
func (a Args) Swap(from, to int) error {
if from >= len(a) || to >= len(a) {
return errors.New("index out of range")
}
a[from], a[to] = a[to], a[from]
return nil
}
func globalContext(ctx *Context) *Context {
if ctx == nil {
return nil
}
for {
if ctx.parentContext == nil {
return ctx
}
ctx = ctx.parentContext
}
}
func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet {
if ctx.parentContext != nil {
ctx = ctx.parentContext
}
for ; ctx != nil; ctx = ctx.parentContext {
if f := ctx.flagSet.Lookup(name); f != nil {
return ctx.flagSet
}
}
return nil
}
func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
switch ff.Value.(type) {
case *StringSlice:
default:
set.Set(name, ff.Value.String())
}
}
func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
visited := make(map[string]bool)
set.Visit(func(f *flag.Flag) {
visited[f.Name] = true
})
for _, f := range flags {
parts := strings.Split(f.GetName(), ",")
if len(parts) == 1 {
continue
}
var ff *flag.Flag
for _, name := range parts {
name = strings.Trim(name, " ")
if visited[name] {
if ff != nil {
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
}
ff = set.Lookup(name)
}
}
if ff == nil {
continue
}
for _, name := range parts {
name = strings.Trim(name, " ")
if !visited[name] {
copyFlag(name, ff, set)
}
}
}
return nil
}

View File

@ -1,403 +0,0 @@
package cli
import (
"flag"
"os"
"testing"
"time"
)
func TestNewContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int("myflag", 12, "doc")
set.Int64("myflagInt64", int64(12), "doc")
set.Uint("myflagUint", uint(93), "doc")
set.Uint64("myflagUint64", uint64(93), "doc")
set.Float64("myflag64", float64(17), "doc")
globalSet := flag.NewFlagSet("test", 0)
globalSet.Int("myflag", 42, "doc")
globalSet.Int64("myflagInt64", int64(42), "doc")
globalSet.Uint("myflagUint", uint(33), "doc")
globalSet.Uint64("myflagUint64", uint64(33), "doc")
globalSet.Float64("myflag64", float64(47), "doc")
globalCtx := NewContext(nil, globalSet, nil)
command := Command{Name: "mycommand"}
c := NewContext(nil, set, globalCtx)
c.Command = command
expect(t, c.Int("myflag"), 12)
expect(t, c.Int64("myflagInt64"), int64(12))
expect(t, c.Uint("myflagUint"), uint(93))
expect(t, c.Uint64("myflagUint64"), uint64(93))
expect(t, c.Float64("myflag64"), float64(17))
expect(t, c.GlobalInt("myflag"), 42)
expect(t, c.GlobalInt64("myflagInt64"), int64(42))
expect(t, c.GlobalUint("myflagUint"), uint(33))
expect(t, c.GlobalUint64("myflagUint64"), uint64(33))
expect(t, c.GlobalFloat64("myflag64"), float64(47))
expect(t, c.Command.Name, "mycommand")
}
func TestContext_Int(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int("myflag", 12, "doc")
c := NewContext(nil, set, nil)
expect(t, c.Int("myflag"), 12)
}
func TestContext_Int64(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int64("myflagInt64", 12, "doc")
c := NewContext(nil, set, nil)
expect(t, c.Int64("myflagInt64"), int64(12))
}
func TestContext_Uint(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Uint("myflagUint", uint(13), "doc")
c := NewContext(nil, set, nil)
expect(t, c.Uint("myflagUint"), uint(13))
}
func TestContext_Uint64(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Uint64("myflagUint64", uint64(9), "doc")
c := NewContext(nil, set, nil)
expect(t, c.Uint64("myflagUint64"), uint64(9))
}
func TestContext_GlobalInt(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int("myflag", 12, "doc")
c := NewContext(nil, set, nil)
expect(t, c.GlobalInt("myflag"), 12)
expect(t, c.GlobalInt("nope"), 0)
}
func TestContext_GlobalInt64(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int64("myflagInt64", 12, "doc")
c := NewContext(nil, set, nil)
expect(t, c.GlobalInt64("myflagInt64"), int64(12))
expect(t, c.GlobalInt64("nope"), int64(0))
}
func TestContext_Float64(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Float64("myflag", float64(17), "doc")
c := NewContext(nil, set, nil)
expect(t, c.Float64("myflag"), float64(17))
}
func TestContext_GlobalFloat64(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Float64("myflag", float64(17), "doc")
c := NewContext(nil, set, nil)
expect(t, c.GlobalFloat64("myflag"), float64(17))
expect(t, c.GlobalFloat64("nope"), float64(0))
}
func TestContext_Duration(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Duration("myflag", time.Duration(12*time.Second), "doc")
c := NewContext(nil, set, nil)
expect(t, c.Duration("myflag"), time.Duration(12*time.Second))
}
func TestContext_String(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.String("myflag", "hello world", "doc")
c := NewContext(nil, set, nil)
expect(t, c.String("myflag"), "hello world")
}
func TestContext_Bool(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Bool("myflag", false, "doc")
c := NewContext(nil, set, nil)
expect(t, c.Bool("myflag"), false)
}
func TestContext_BoolT(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Bool("myflag", true, "doc")
c := NewContext(nil, set, nil)
expect(t, c.BoolT("myflag"), true)
}
func TestContext_GlobalBool(t *testing.T) {
set := flag.NewFlagSet("test", 0)
globalSet := flag.NewFlagSet("test-global", 0)
globalSet.Bool("myflag", false, "doc")
globalCtx := NewContext(nil, globalSet, nil)
c := NewContext(nil, set, globalCtx)
expect(t, c.GlobalBool("myflag"), false)
expect(t, c.GlobalBool("nope"), false)
}
func TestContext_GlobalBoolT(t *testing.T) {
set := flag.NewFlagSet("test", 0)
globalSet := flag.NewFlagSet("test-global", 0)
globalSet.Bool("myflag", true, "doc")
globalCtx := NewContext(nil, globalSet, nil)
c := NewContext(nil, set, globalCtx)
expect(t, c.GlobalBoolT("myflag"), true)
expect(t, c.GlobalBoolT("nope"), false)
}
func TestContext_Args(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Bool("myflag", false, "doc")
c := NewContext(nil, set, nil)
set.Parse([]string{"--myflag", "bat", "baz"})
expect(t, len(c.Args()), 2)
expect(t, c.Bool("myflag"), true)
}
func TestContext_NArg(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Bool("myflag", false, "doc")
c := NewContext(nil, set, nil)
set.Parse([]string{"--myflag", "bat", "baz"})
expect(t, c.NArg(), 2)
}
func TestContext_IsSet(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Bool("myflag", false, "doc")
set.String("otherflag", "hello world", "doc")
globalSet := flag.NewFlagSet("test", 0)
globalSet.Bool("myflagGlobal", true, "doc")
globalCtx := NewContext(nil, globalSet, nil)
c := NewContext(nil, set, globalCtx)
set.Parse([]string{"--myflag", "bat", "baz"})
globalSet.Parse([]string{"--myflagGlobal", "bat", "baz"})
expect(t, c.IsSet("myflag"), true)
expect(t, c.IsSet("otherflag"), false)
expect(t, c.IsSet("bogusflag"), false)
expect(t, c.IsSet("myflagGlobal"), false)
}
// XXX Corresponds to hack in context.IsSet for flags with EnvVar field
// Should be moved to `flag_test` in v2
func TestContext_IsSet_fromEnv(t *testing.T) {
var (
timeoutIsSet, tIsSet bool
noEnvVarIsSet, nIsSet bool
passwordIsSet, pIsSet bool
unparsableIsSet, uIsSet bool
)
clearenv()
os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
os.Setenv("APP_PASSWORD", "")
a := App{
Flags: []Flag{
Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"},
StringFlag{Name: "password, p", EnvVar: "APP_PASSWORD"},
Float64Flag{Name: "unparsable, u", EnvVar: "APP_UNPARSABLE"},
Float64Flag{Name: "no-env-var, n"},
},
Action: func(ctx *Context) error {
timeoutIsSet = ctx.IsSet("timeout")
tIsSet = ctx.IsSet("t")
passwordIsSet = ctx.IsSet("password")
pIsSet = ctx.IsSet("p")
unparsableIsSet = ctx.IsSet("unparsable")
uIsSet = ctx.IsSet("u")
noEnvVarIsSet = ctx.IsSet("no-env-var")
nIsSet = ctx.IsSet("n")
return nil
},
}
a.Run([]string{"run"})
expect(t, timeoutIsSet, true)
expect(t, tIsSet, true)
expect(t, passwordIsSet, true)
expect(t, pIsSet, true)
expect(t, noEnvVarIsSet, false)
expect(t, nIsSet, false)
os.Setenv("APP_UNPARSABLE", "foobar")
a.Run([]string{"run"})
expect(t, unparsableIsSet, false)
expect(t, uIsSet, false)
}
func TestContext_GlobalIsSet(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Bool("myflag", false, "doc")
set.String("otherflag", "hello world", "doc")
globalSet := flag.NewFlagSet("test", 0)
globalSet.Bool("myflagGlobal", true, "doc")
globalSet.Bool("myflagGlobalUnset", true, "doc")
globalCtx := NewContext(nil, globalSet, nil)
c := NewContext(nil, set, globalCtx)
set.Parse([]string{"--myflag", "bat", "baz"})
globalSet.Parse([]string{"--myflagGlobal", "bat", "baz"})
expect(t, c.GlobalIsSet("myflag"), false)
expect(t, c.GlobalIsSet("otherflag"), false)
expect(t, c.GlobalIsSet("bogusflag"), false)
expect(t, c.GlobalIsSet("myflagGlobal"), true)
expect(t, c.GlobalIsSet("myflagGlobalUnset"), false)
expect(t, c.GlobalIsSet("bogusGlobal"), false)
}
// XXX Corresponds to hack in context.IsSet for flags with EnvVar field
// Should be moved to `flag_test` in v2
func TestContext_GlobalIsSet_fromEnv(t *testing.T) {
var (
timeoutIsSet, tIsSet bool
noEnvVarIsSet, nIsSet bool
passwordIsSet, pIsSet bool
unparsableIsSet, uIsSet bool
)
clearenv()
os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
os.Setenv("APP_PASSWORD", "")
a := App{
Flags: []Flag{
Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"},
StringFlag{Name: "password, p", EnvVar: "APP_PASSWORD"},
Float64Flag{Name: "no-env-var, n"},
Float64Flag{Name: "unparsable, u", EnvVar: "APP_UNPARSABLE"},
},
Commands: []Command{
{
Name: "hello",
Action: func(ctx *Context) error {
timeoutIsSet = ctx.GlobalIsSet("timeout")
tIsSet = ctx.GlobalIsSet("t")
passwordIsSet = ctx.GlobalIsSet("password")
pIsSet = ctx.GlobalIsSet("p")
unparsableIsSet = ctx.GlobalIsSet("unparsable")
uIsSet = ctx.GlobalIsSet("u")
noEnvVarIsSet = ctx.GlobalIsSet("no-env-var")
nIsSet = ctx.GlobalIsSet("n")
return nil
},
},
},
}
if err := a.Run([]string{"run", "hello"}); err != nil {
t.Logf("error running Run(): %+v", err)
}
expect(t, timeoutIsSet, true)
expect(t, tIsSet, true)
expect(t, passwordIsSet, true)
expect(t, pIsSet, true)
expect(t, noEnvVarIsSet, false)
expect(t, nIsSet, false)
os.Setenv("APP_UNPARSABLE", "foobar")
if err := a.Run([]string{"run"}); err != nil {
t.Logf("error running Run(): %+v", err)
}
expect(t, unparsableIsSet, false)
expect(t, uIsSet, false)
}
func TestContext_NumFlags(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Bool("myflag", false, "doc")
set.String("otherflag", "hello world", "doc")
globalSet := flag.NewFlagSet("test", 0)
globalSet.Bool("myflagGlobal", true, "doc")
globalCtx := NewContext(nil, globalSet, nil)
c := NewContext(nil, set, globalCtx)
set.Parse([]string{"--myflag", "--otherflag=foo"})
globalSet.Parse([]string{"--myflagGlobal"})
expect(t, c.NumFlags(), 2)
}
func TestContext_GlobalFlag(t *testing.T) {
var globalFlag string
var globalFlagSet bool
app := NewApp()
app.Flags = []Flag{
StringFlag{Name: "global, g", Usage: "global"},
}
app.Action = func(c *Context) error {
globalFlag = c.GlobalString("global")
globalFlagSet = c.GlobalIsSet("global")
return nil
}
app.Run([]string{"command", "-g", "foo"})
expect(t, globalFlag, "foo")
expect(t, globalFlagSet, true)
}
func TestContext_GlobalFlagsInSubcommands(t *testing.T) {
subcommandRun := false
parentFlag := false
app := NewApp()
app.Flags = []Flag{
BoolFlag{Name: "debug, d", Usage: "Enable debugging"},
}
app.Commands = []Command{
{
Name: "foo",
Flags: []Flag{
BoolFlag{Name: "parent, p", Usage: "Parent flag"},
},
Subcommands: []Command{
{
Name: "bar",
Action: func(c *Context) error {
if c.GlobalBool("debug") {
subcommandRun = true
}
if c.GlobalBool("parent") {
parentFlag = true
}
return nil
},
},
},
},
}
app.Run([]string{"command", "-d", "foo", "-p", "bar"})
expect(t, subcommandRun, true)
expect(t, parentFlag, true)
}
func TestContext_Set(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int("int", 5, "an int")
c := NewContext(nil, set, nil)
expect(t, c.IsSet("int"), false)
c.Set("int", "1")
expect(t, c.Int("int"), 1)
expect(t, c.IsSet("int"), true)
}
func TestContext_GlobalSet(t *testing.T) {
gSet := flag.NewFlagSet("test", 0)
gSet.Int("int", 5, "an int")
set := flag.NewFlagSet("sub", 0)
set.Int("int", 3, "an int")
pc := NewContext(nil, gSet, nil)
c := NewContext(nil, set, pc)
c.Set("int", "1")
expect(t, c.Int("int"), 1)
expect(t, c.GlobalInt("int"), 5)
expect(t, c.GlobalIsSet("int"), false)
c.GlobalSet("int", "1")
expect(t, c.Int("int"), 1)
expect(t, c.GlobalInt("int"), 1)
expect(t, c.GlobalIsSet("int"), true)
}

View File

@ -1,115 +0,0 @@
package cli
import (
"fmt"
"io"
"os"
"strings"
)
// OsExiter is the function used when the app exits. If not set defaults to os.Exit.
var OsExiter = os.Exit
// ErrWriter is used to write errors to the user. This can be anything
// implementing the io.Writer interface and defaults to os.Stderr.
var ErrWriter io.Writer = os.Stderr
// MultiError is an error that wraps multiple errors.
type MultiError struct {
Errors []error
}
// NewMultiError creates a new MultiError. Pass in one or more errors.
func NewMultiError(err ...error) MultiError {
return MultiError{Errors: err}
}
// Error implements the error interface.
func (m MultiError) Error() string {
errs := make([]string, len(m.Errors))
for i, err := range m.Errors {
errs[i] = err.Error()
}
return strings.Join(errs, "\n")
}
type ErrorFormatter interface {
Format(s fmt.State, verb rune)
}
// ExitCoder is the interface checked by `App` and `Command` for a custom exit
// code
type ExitCoder interface {
error
ExitCode() int
}
// ExitError fulfills both the builtin `error` interface and `ExitCoder`
type ExitError struct {
exitCode int
message interface{}
}
// NewExitError makes a new *ExitError
func NewExitError(message interface{}, exitCode int) *ExitError {
return &ExitError{
exitCode: exitCode,
message: message,
}
}
// Error returns the string message, fulfilling the interface required by
// `error`
func (ee *ExitError) Error() string {
return fmt.Sprintf("%v", ee.message)
}
// ExitCode returns the exit code, fulfilling the interface required by
// `ExitCoder`
func (ee *ExitError) ExitCode() int {
return ee.exitCode
}
// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if
// so prints the error to stderr (if it is non-empty) and calls OsExiter with the
// given exit code. If the given error is a MultiError, then this func is
// called on all members of the Errors slice and calls OsExiter with the last exit code.
func HandleExitCoder(err error) {
if err == nil {
return
}
if exitErr, ok := err.(ExitCoder); ok {
if err.Error() != "" {
if _, ok := exitErr.(ErrorFormatter); ok {
fmt.Fprintf(ErrWriter, "%+v\n", err)
} else {
fmt.Fprintln(ErrWriter, err)
}
}
OsExiter(exitErr.ExitCode())
return
}
if multiErr, ok := err.(MultiError); ok {
code := handleMultiError(multiErr)
OsExiter(code)
return
}
}
func handleMultiError(multiErr MultiError) int {
code := 1
for _, merr := range multiErr.Errors {
if multiErr2, ok := merr.(MultiError); ok {
code = handleMultiError(multiErr2)
} else {
fmt.Fprintln(ErrWriter, merr)
if exitErr, ok := merr.(ExitCoder); ok {
code = exitErr.ExitCode()
}
}
}
return code
}

View File

@ -1,122 +0,0 @@
package cli
import (
"bytes"
"errors"
"fmt"
"testing"
)
func TestHandleExitCoder_nil(t *testing.T) {
exitCode := 0
called := false
OsExiter = func(rc int) {
if !called {
exitCode = rc
called = true
}
}
defer func() { OsExiter = fakeOsExiter }()
HandleExitCoder(nil)
expect(t, exitCode, 0)
expect(t, called, false)
}
func TestHandleExitCoder_ExitCoder(t *testing.T) {
exitCode := 0
called := false
OsExiter = func(rc int) {
if !called {
exitCode = rc
called = true
}
}
defer func() { OsExiter = fakeOsExiter }()
HandleExitCoder(NewExitError("galactic perimeter breach", 9))
expect(t, exitCode, 9)
expect(t, called, true)
}
func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) {
exitCode := 0
called := false
OsExiter = func(rc int) {
if !called {
exitCode = rc
called = true
}
}
defer func() { OsExiter = fakeOsExiter }()
exitErr := NewExitError("galactic perimeter breach", 9)
exitErr2 := NewExitError("last ExitCoder", 11)
err := NewMultiError(errors.New("wowsa"), errors.New("egad"), exitErr, exitErr2)
HandleExitCoder(err)
expect(t, exitCode, 11)
expect(t, called, true)
}
// make a stub to not import pkg/errors
type ErrorWithFormat struct {
error
}
func NewErrorWithFormat(m string) *ErrorWithFormat {
return &ErrorWithFormat{error: errors.New(m)}
}
func (f *ErrorWithFormat) Format(s fmt.State, verb rune) {
fmt.Fprintf(s, "This the format: %v", f.error)
}
func TestHandleExitCoder_ErrorWithFormat(t *testing.T) {
called := false
OsExiter = func(rc int) {
if !called {
called = true
}
}
ErrWriter = &bytes.Buffer{}
defer func() {
OsExiter = fakeOsExiter
ErrWriter = fakeErrWriter
}()
err := NewExitError(NewErrorWithFormat("I am formatted"), 1)
HandleExitCoder(err)
expect(t, called, true)
expect(t, ErrWriter.(*bytes.Buffer).String(), "This the format: I am formatted\n")
}
func TestHandleExitCoder_MultiErrorWithFormat(t *testing.T) {
called := false
OsExiter = func(rc int) {
if !called {
called = true
}
}
ErrWriter = &bytes.Buffer{}
defer func() { OsExiter = fakeOsExiter }()
err := NewMultiError(NewErrorWithFormat("err1"), NewErrorWithFormat("err2"))
HandleExitCoder(err)
expect(t, called, true)
expect(t, ErrWriter.(*bytes.Buffer).String(), "This the format: err1\nThis the format: err2\n")
}

View File

@ -1,93 +0,0 @@
[
{
"name": "Bool",
"type": "bool",
"value": false,
"context_default": "false",
"parser": "strconv.ParseBool(f.Value.String())"
},
{
"name": "BoolT",
"type": "bool",
"value": false,
"doctail": " that is true by default",
"context_default": "false",
"parser": "strconv.ParseBool(f.Value.String())"
},
{
"name": "Duration",
"type": "time.Duration",
"doctail": " (see https://golang.org/pkg/time/#ParseDuration)",
"context_default": "0",
"parser": "time.ParseDuration(f.Value.String())"
},
{
"name": "Float64",
"type": "float64",
"context_default": "0",
"parser": "strconv.ParseFloat(f.Value.String(), 64)"
},
{
"name": "Generic",
"type": "Generic",
"dest": false,
"context_default": "nil",
"context_type": "interface{}"
},
{
"name": "Int64",
"type": "int64",
"context_default": "0",
"parser": "strconv.ParseInt(f.Value.String(), 0, 64)"
},
{
"name": "Int",
"type": "int",
"context_default": "0",
"parser": "strconv.ParseInt(f.Value.String(), 0, 64)",
"parser_cast": "int(parsed)"
},
{
"name": "IntSlice",
"type": "*IntSlice",
"dest": false,
"context_default": "nil",
"context_type": "[]int",
"parser": "(f.Value.(*IntSlice)).Value(), error(nil)"
},
{
"name": "Int64Slice",
"type": "*Int64Slice",
"dest": false,
"context_default": "nil",
"context_type": "[]int64",
"parser": "(f.Value.(*Int64Slice)).Value(), error(nil)"
},
{
"name": "String",
"type": "string",
"context_default": "\"\"",
"parser": "f.Value.String(), error(nil)"
},
{
"name": "StringSlice",
"type": "*StringSlice",
"dest": false,
"context_default": "nil",
"context_type": "[]string",
"parser": "(f.Value.(*StringSlice)).Value(), error(nil)"
},
{
"name": "Uint64",
"type": "uint64",
"context_default": "0",
"parser": "strconv.ParseUint(f.Value.String(), 0, 64)"
},
{
"name": "Uint",
"type": "uint",
"context_default": "0",
"parser": "strconv.ParseUint(f.Value.String(), 0, 64)",
"parser_cast": "uint(parsed)"
}
]

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