Deleted vendor folder
This commit is contained in:
parent
14ea821a37
commit
ec28f1dfc6
|
@ -1,46 +0,0 @@
|
|||
# Benchmarks
|
||||
|
||||
Hardware: MacBookPro11,1 - Intel Core i5 - 2,6 GHz - 8 Go RAM
|
||||
|
||||
With:
|
||||
|
||||
- handlebars.js #8cba84df119c317fcebc49fb285518542ca9c2d0
|
||||
- raymond #7bbaaf50ed03c96b56687d7fa6c6e04e02375a98
|
||||
|
||||
|
||||
## handlebars.js (ops/ms)
|
||||
|
||||
arguments 198 ±4 (5)
|
||||
array-each 568 ±23 (5)
|
||||
array-mustache 522 ±18 (4)
|
||||
complex 71 ±7 (3)
|
||||
data 67 ±2 (3)
|
||||
depth-1 47 ±2 (3)
|
||||
depth-2 14 ±1 (2)
|
||||
object-mustache 1099 ±47 (5)
|
||||
object 907 ±58 (4)
|
||||
partial-recursion 46 ±3 (4)
|
||||
partial 68 ±3 (3)
|
||||
paths 1650 ±50 (3)
|
||||
string 2552 ±157 (3)
|
||||
subexpression 141 ±2 (4)
|
||||
variables 2671 ±83 (4)
|
||||
|
||||
|
||||
## raymond
|
||||
|
||||
BenchmarkArguments 200000 6642 ns/op 151 ops/ms
|
||||
BenchmarkArrayEach 100000 19584 ns/op 51 ops/ms
|
||||
BenchmarkArrayMustache 100000 17305 ns/op 58 ops/ms
|
||||
BenchmarkComplex 30000 50270 ns/op 20 ops/ms
|
||||
BenchmarkData 50000 25551 ns/op 39 ops/ms
|
||||
BenchmarkDepth1 100000 20162 ns/op 50 ops/ms
|
||||
BenchmarkDepth2 30000 47782 ns/op 21 ops/ms
|
||||
BenchmarkObjectMustache 200000 7668 ns/op 130 ops/ms
|
||||
BenchmarkObject 200000 8843 ns/op 113 ops/ms
|
||||
BenchmarkPartialRecursion 50000 23139 ns/op 43 ops/ms
|
||||
BenchmarkPartial 50000 31015 ns/op 32 ops/ms
|
||||
BenchmarkPath 200000 8997 ns/op 111 ops/ms
|
||||
BenchmarkString 1000000 1879 ns/op 532 ops/ms
|
||||
BenchmarkSubExpression 300000 4935 ns/op 203 ops/ms
|
||||
BenchmarkVariables 200000 6478 ns/op 154 ops/ms
|
|
@ -1,13 +0,0 @@
|
|||
# Raymond Changelog
|
||||
|
||||
### Raymond 1.1.0 _(June 15, 2015)_
|
||||
|
||||
- Permits templates references with lowercase versions of struct fields.
|
||||
- Adds `ParseFile()` function.
|
||||
- Adds `RegisterPartialFile()`, `RegisterPartialFiles()` and `Clone()` methods on `Template`.
|
||||
- Helpers can now be struct methods.
|
||||
- Ensures safe concurrent access to helpers and partials.
|
||||
|
||||
### Raymond 1.0.0 _(June 09, 2015)_
|
||||
|
||||
- This is the first release. Raymond supports almost all handlebars features. See https://github.com/aymerick/raymond#limitations for a list of differences with the javascript implementation.
|
|
@ -1,22 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Aymerick JEHANNE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -1 +0,0 @@
|
|||
1.1.0
|
|
@ -1,767 +0,0 @@
|
|||
// Package ast provides structures to represent a handlebars Abstract Syntax Tree, and a Visitor interface to visit that tree.
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// References:
|
||||
// - https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/ast.js
|
||||
// - https://github.com/wycats/handlebars.js/blob/master/docs/compiler-api.md
|
||||
// - https://github.com/golang/go/blob/master/src/text/template/parse/node.go
|
||||
|
||||
// Node is an element in the AST.
|
||||
type Node interface {
|
||||
// node type
|
||||
Type() NodeType
|
||||
|
||||
// location of node in original input string
|
||||
Location() Loc
|
||||
|
||||
// string representation, used for debugging
|
||||
String() string
|
||||
|
||||
// accepts visitor
|
||||
Accept(Visitor) interface{}
|
||||
}
|
||||
|
||||
// Visitor is the interface to visit an AST.
|
||||
type Visitor interface {
|
||||
VisitProgram(*Program) interface{}
|
||||
|
||||
// statements
|
||||
VisitMustache(*MustacheStatement) interface{}
|
||||
VisitBlock(*BlockStatement) interface{}
|
||||
VisitPartial(*PartialStatement) interface{}
|
||||
VisitContent(*ContentStatement) interface{}
|
||||
VisitComment(*CommentStatement) interface{}
|
||||
|
||||
// expressions
|
||||
VisitExpression(*Expression) interface{}
|
||||
VisitSubExpression(*SubExpression) interface{}
|
||||
VisitPath(*PathExpression) interface{}
|
||||
|
||||
// literals
|
||||
VisitString(*StringLiteral) interface{}
|
||||
VisitBoolean(*BooleanLiteral) interface{}
|
||||
VisitNumber(*NumberLiteral) interface{}
|
||||
|
||||
// miscellaneous
|
||||
VisitHash(*Hash) interface{}
|
||||
VisitHashPair(*HashPair) interface{}
|
||||
}
|
||||
|
||||
// NodeType represents an AST Node type.
|
||||
type NodeType int
|
||||
|
||||
// Type returns itself, and permits struct includers to satisfy that part of Node interface.
|
||||
func (t NodeType) Type() NodeType {
|
||||
return t
|
||||
}
|
||||
|
||||
const (
|
||||
// program
|
||||
NodeProgram NodeType = iota
|
||||
|
||||
// statements
|
||||
NodeMustache
|
||||
NodeBlock
|
||||
NodePartial
|
||||
NodeContent
|
||||
NodeComment
|
||||
|
||||
// expressions
|
||||
NodeExpression
|
||||
NodeSubExpression
|
||||
NodePath
|
||||
|
||||
// literals
|
||||
NodeBoolean
|
||||
NodeNumber
|
||||
NodeString
|
||||
|
||||
// miscellaneous
|
||||
NodeHash
|
||||
NodeHashPair
|
||||
)
|
||||
|
||||
// Loc represents the position of a parsed node in source file.
|
||||
type Loc struct {
|
||||
Pos int // Byte position
|
||||
Line int // Line number
|
||||
}
|
||||
|
||||
// Location returns itself, and permits struct includers to satisfy that part of Node interface.
|
||||
func (l Loc) Location() Loc {
|
||||
return l
|
||||
}
|
||||
|
||||
// Strip describes node whitespace management.
|
||||
type Strip struct {
|
||||
Open bool
|
||||
Close bool
|
||||
|
||||
OpenStandalone bool
|
||||
CloseStandalone bool
|
||||
InlineStandalone bool
|
||||
}
|
||||
|
||||
// NewStrip instanciates a Strip for given open and close mustaches.
|
||||
func NewStrip(openStr, closeStr string) *Strip {
|
||||
return &Strip{
|
||||
Open: (len(openStr) > 2) && openStr[2] == '~',
|
||||
Close: (len(closeStr) > 2) && closeStr[len(closeStr)-3] == '~',
|
||||
}
|
||||
}
|
||||
|
||||
// NewStripForStr instanciates a Strip for given tag.
|
||||
func NewStripForStr(str string) *Strip {
|
||||
return &Strip{
|
||||
Open: (len(str) > 2) && str[2] == '~',
|
||||
Close: (len(str) > 2) && str[len(str)-3] == '~',
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (s *Strip) String() string {
|
||||
return fmt.Sprintf("Open: %t, Close: %t, OpenStandalone: %t, CloseStandalone: %t, InlineStandalone: %t", s.Open, s.Close, s.OpenStandalone, s.CloseStandalone, s.InlineStandalone)
|
||||
}
|
||||
|
||||
//
|
||||
// Program
|
||||
//
|
||||
|
||||
// Program represents a program node.
|
||||
type Program struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Body []Node // [ Statement ... ]
|
||||
BlockParams []string
|
||||
Chained bool
|
||||
|
||||
// whitespace management
|
||||
Strip *Strip
|
||||
}
|
||||
|
||||
// NewProgram instanciates a new program node.
|
||||
func NewProgram(pos int, line int) *Program {
|
||||
return &Program{
|
||||
NodeType: NodeProgram,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *Program) String() string {
|
||||
return fmt.Sprintf("Program{Pos: %d}", node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *Program) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitProgram(node)
|
||||
}
|
||||
|
||||
// AddStatement adds given statement to program.
|
||||
func (node *Program) AddStatement(statement Node) {
|
||||
node.Body = append(node.Body, statement)
|
||||
}
|
||||
|
||||
//
|
||||
// Mustache Statement
|
||||
//
|
||||
|
||||
// MustacheStatement represents a mustache node.
|
||||
type MustacheStatement struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Unescaped bool
|
||||
Expression *Expression
|
||||
|
||||
// whitespace management
|
||||
Strip *Strip
|
||||
}
|
||||
|
||||
// NewMustacheStatement instanciates a new mustache node.
|
||||
func NewMustacheStatement(pos int, line int, unescaped bool) *MustacheStatement {
|
||||
return &MustacheStatement{
|
||||
NodeType: NodeMustache,
|
||||
Loc: Loc{pos, line},
|
||||
Unescaped: unescaped,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *MustacheStatement) String() string {
|
||||
return fmt.Sprintf("Mustache{Pos: %d}", node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *MustacheStatement) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitMustache(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Block Statement
|
||||
//
|
||||
|
||||
// BlockStatement represents a block node.
|
||||
type BlockStatement struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Expression *Expression
|
||||
|
||||
Program *Program
|
||||
Inverse *Program
|
||||
|
||||
// whitespace management
|
||||
OpenStrip *Strip
|
||||
InverseStrip *Strip
|
||||
CloseStrip *Strip
|
||||
}
|
||||
|
||||
// NewBlockStatement instanciates a new block node.
|
||||
func NewBlockStatement(pos int, line int) *BlockStatement {
|
||||
return &BlockStatement{
|
||||
NodeType: NodeBlock,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *BlockStatement) String() string {
|
||||
return fmt.Sprintf("Block{Pos: %d}", node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *BlockStatement) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitBlock(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Partial Statement
|
||||
//
|
||||
|
||||
// PartialStatement represents a partial node.
|
||||
type PartialStatement struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Name Node // PathExpression | SubExpression
|
||||
Params []Node // [ Expression ... ]
|
||||
Hash *Hash
|
||||
|
||||
// whitespace management
|
||||
Strip *Strip
|
||||
Indent string
|
||||
}
|
||||
|
||||
// NewPartialStatement instanciates a new partial node.
|
||||
func NewPartialStatement(pos int, line int) *PartialStatement {
|
||||
return &PartialStatement{
|
||||
NodeType: NodePartial,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *PartialStatement) String() string {
|
||||
return fmt.Sprintf("Partial{Name:%s, Pos:%d}", node.Name, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *PartialStatement) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitPartial(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Content Statement
|
||||
//
|
||||
|
||||
// ContentStatement represents a content node.
|
||||
type ContentStatement struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Value string
|
||||
Original string
|
||||
|
||||
// whitespace management
|
||||
RightStripped bool
|
||||
LeftStripped bool
|
||||
}
|
||||
|
||||
// NewContentStatement instanciates a new content node.
|
||||
func NewContentStatement(pos int, line int, val string) *ContentStatement {
|
||||
return &ContentStatement{
|
||||
NodeType: NodeContent,
|
||||
Loc: Loc{pos, line},
|
||||
|
||||
Value: val,
|
||||
Original: val,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *ContentStatement) String() string {
|
||||
return fmt.Sprintf("Content{Value:'%s', Pos:%d}", node.Value, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *ContentStatement) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitContent(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Comment Statement
|
||||
//
|
||||
|
||||
// CommentStatement represents a comment node.
|
||||
type CommentStatement struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Value string
|
||||
|
||||
// whitespace management
|
||||
Strip *Strip
|
||||
}
|
||||
|
||||
// NewCommentStatement instanciates a new comment node.
|
||||
func NewCommentStatement(pos int, line int, val string) *CommentStatement {
|
||||
return &CommentStatement{
|
||||
NodeType: NodeComment,
|
||||
Loc: Loc{pos, line},
|
||||
|
||||
Value: val,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *CommentStatement) String() string {
|
||||
return fmt.Sprintf("Comment{Value:'%s', Pos:%d}", node.Value, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *CommentStatement) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitComment(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Expression
|
||||
//
|
||||
|
||||
// Expression represents an expression node.
|
||||
type Expression struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Path Node // PathExpression | StringLiteral | BooleanLiteral | NumberLiteral
|
||||
Params []Node // [ Expression ... ]
|
||||
Hash *Hash
|
||||
}
|
||||
|
||||
// NewExpression instanciates a new expression node.
|
||||
func NewExpression(pos int, line int) *Expression {
|
||||
return &Expression{
|
||||
NodeType: NodeExpression,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *Expression) String() string {
|
||||
return fmt.Sprintf("Expr{Path:%s, Pos:%d}", node.Path, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *Expression) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitExpression(node)
|
||||
}
|
||||
|
||||
// HelperName returns helper name, or an empty string if this expression can't be a helper.
|
||||
func (node *Expression) HelperName() string {
|
||||
path, ok := node.Path.(*PathExpression)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
if path.Data || (len(path.Parts) != 1) || (path.Depth > 0) || path.Scoped {
|
||||
return ""
|
||||
}
|
||||
|
||||
return path.Parts[0]
|
||||
}
|
||||
|
||||
// FieldPath returns path expression representing a field path, or nil if this is not a field path.
|
||||
func (node *Expression) FieldPath() *PathExpression {
|
||||
path, ok := node.Path.(*PathExpression)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// LiteralStr returns the string representation of literal value, with a boolean set to false if this is not a literal.
|
||||
func (node *Expression) LiteralStr() (string, bool) {
|
||||
return LiteralStr(node.Path)
|
||||
}
|
||||
|
||||
// Canonical returns the canonical form of expression node as a string.
|
||||
func (node *Expression) Canonical() string {
|
||||
if str, ok := HelperNameStr(node.Path); ok {
|
||||
return str
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// HelperNameStr returns the string representation of a helper name, with a boolean set to false if this is not a valid helper name.
|
||||
//
|
||||
// helperName : path | dataName | STRING | NUMBER | BOOLEAN | UNDEFINED | NULL
|
||||
func HelperNameStr(node Node) (string, bool) {
|
||||
// PathExpression
|
||||
if str, ok := PathExpressionStr(node); ok {
|
||||
return str, ok
|
||||
}
|
||||
|
||||
// Literal
|
||||
if str, ok := LiteralStr(node); ok {
|
||||
return str, ok
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// PathExpressionStr returns the string representation of path expression value, with a boolean set to false if this is not a path expression.
|
||||
func PathExpressionStr(node Node) (string, bool) {
|
||||
if path, ok := node.(*PathExpression); ok {
|
||||
result := path.Original
|
||||
|
||||
// "[foo bar]"" => "foo bar"
|
||||
if (len(result) >= 2) && (result[0] == '[') && (result[len(result)-1] == ']') {
|
||||
result = result[1 : len(result)-1]
|
||||
}
|
||||
|
||||
return result, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// LiteralStr returns the string representation of literal value, with a boolean set to false if this is not a literal.
|
||||
func LiteralStr(node Node) (string, bool) {
|
||||
if lit, ok := node.(*StringLiteral); ok {
|
||||
return lit.Value, true
|
||||
}
|
||||
|
||||
if lit, ok := node.(*BooleanLiteral); ok {
|
||||
return lit.Canonical(), true
|
||||
}
|
||||
|
||||
if lit, ok := node.(*NumberLiteral); ok {
|
||||
return lit.Canonical(), true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
//
|
||||
// SubExpression
|
||||
//
|
||||
|
||||
// SubExpression represents a subexpression node.
|
||||
type SubExpression struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Expression *Expression
|
||||
}
|
||||
|
||||
// NewSubExpression instanciates a new subexpression node.
|
||||
func NewSubExpression(pos int, line int) *SubExpression {
|
||||
return &SubExpression{
|
||||
NodeType: NodeSubExpression,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *SubExpression) String() string {
|
||||
return fmt.Sprintf("Sexp{Path:%s, Pos:%d}", node.Expression.Path, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *SubExpression) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitSubExpression(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Path Expression
|
||||
//
|
||||
|
||||
// PathExpression represents a path expression node.
|
||||
type PathExpression struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Original string
|
||||
Depth int
|
||||
Parts []string
|
||||
Data bool
|
||||
Scoped bool
|
||||
}
|
||||
|
||||
// NewPathExpression instanciates a new path expression node.
|
||||
func NewPathExpression(pos int, line int, data bool) *PathExpression {
|
||||
result := &PathExpression{
|
||||
NodeType: NodePath,
|
||||
Loc: Loc{pos, line},
|
||||
|
||||
Data: data,
|
||||
}
|
||||
|
||||
if data {
|
||||
result.Original = "@"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *PathExpression) String() string {
|
||||
return fmt.Sprintf("Path{Original:'%s', Pos:%d}", node.Original, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *PathExpression) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitPath(node)
|
||||
}
|
||||
|
||||
// Part adds path part.
|
||||
func (node *PathExpression) Part(part string) {
|
||||
node.Original += part
|
||||
|
||||
switch part {
|
||||
case "..":
|
||||
node.Depth += 1
|
||||
node.Scoped = true
|
||||
case ".", "this":
|
||||
node.Scoped = true
|
||||
default:
|
||||
node.Parts = append(node.Parts, part)
|
||||
}
|
||||
}
|
||||
|
||||
// Sep adds path separator.
|
||||
func (node *PathExpression) Sep(separator string) {
|
||||
node.Original += separator
|
||||
}
|
||||
|
||||
// IsDataRoot returns true if path expression is @root.
|
||||
func (node *PathExpression) IsDataRoot() bool {
|
||||
return node.Data && (node.Parts[0] == "root")
|
||||
}
|
||||
|
||||
//
|
||||
// String Literal
|
||||
//
|
||||
|
||||
// StringLiteral represents a string node.
|
||||
type StringLiteral struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Value string
|
||||
}
|
||||
|
||||
// NewStringLiteral instanciates a new string node.
|
||||
func NewStringLiteral(pos int, line int, val string) *StringLiteral {
|
||||
return &StringLiteral{
|
||||
NodeType: NodeString,
|
||||
Loc: Loc{pos, line},
|
||||
|
||||
Value: val,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *StringLiteral) String() string {
|
||||
return fmt.Sprintf("String{Value:'%s', Pos:%d}", node.Value, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *StringLiteral) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitString(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Boolean Literal
|
||||
//
|
||||
|
||||
// BooleanLiteral represents a boolean node.
|
||||
type BooleanLiteral struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Value bool
|
||||
Original string
|
||||
}
|
||||
|
||||
// NewBooleanLiteral instanciates a new boolean node.
|
||||
func NewBooleanLiteral(pos int, line int, val bool, original string) *BooleanLiteral {
|
||||
return &BooleanLiteral{
|
||||
NodeType: NodeBoolean,
|
||||
Loc: Loc{pos, line},
|
||||
|
||||
Value: val,
|
||||
Original: original,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *BooleanLiteral) String() string {
|
||||
return fmt.Sprintf("Boolean{Value:%s, Pos:%d}", node.Canonical(), node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *BooleanLiteral) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitBoolean(node)
|
||||
}
|
||||
|
||||
// Canonical returns the canonical form of boolean node as a string (ie. "true" | "false").
|
||||
func (node *BooleanLiteral) Canonical() string {
|
||||
if node.Value {
|
||||
return "true"
|
||||
} else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Number Literal
|
||||
//
|
||||
|
||||
// NumberLiteral represents a number node.
|
||||
type NumberLiteral struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Value float64
|
||||
IsInt bool
|
||||
Original string
|
||||
}
|
||||
|
||||
// NewNumberLiteral instanciates a new number node.
|
||||
func NewNumberLiteral(pos int, line int, val float64, isInt bool, original string) *NumberLiteral {
|
||||
return &NumberLiteral{
|
||||
NodeType: NodeNumber,
|
||||
Loc: Loc{pos, line},
|
||||
|
||||
Value: val,
|
||||
IsInt: isInt,
|
||||
Original: original,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *NumberLiteral) String() string {
|
||||
return fmt.Sprintf("Number{Value:%s, Pos:%d}", node.Canonical(), node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *NumberLiteral) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitNumber(node)
|
||||
}
|
||||
|
||||
// Canonical returns the canonical form of number node as a string (eg: "12", "-1.51").
|
||||
func (node *NumberLiteral) Canonical() string {
|
||||
prec := -1
|
||||
if node.IsInt {
|
||||
prec = 0
|
||||
}
|
||||
return strconv.FormatFloat(node.Value, 'f', prec, 64)
|
||||
}
|
||||
|
||||
// Number returns an integer or a float.
|
||||
func (node *NumberLiteral) Number() interface{} {
|
||||
if node.IsInt {
|
||||
return int(node.Value)
|
||||
} else {
|
||||
return node.Value
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Hash
|
||||
//
|
||||
|
||||
// Hash represents a hash node.
|
||||
type Hash struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Pairs []*HashPair
|
||||
}
|
||||
|
||||
// NewNumberLiteral instanciates a new hash node.
|
||||
func NewHash(pos int, line int) *Hash {
|
||||
return &Hash{
|
||||
NodeType: NodeHash,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *Hash) String() string {
|
||||
result := fmt.Sprintf("Hash{[", node.Loc.Pos)
|
||||
|
||||
for i, p := range node.Pairs {
|
||||
if i > 0 {
|
||||
result += ", "
|
||||
}
|
||||
result += p.String()
|
||||
}
|
||||
|
||||
return result + fmt.Sprintf("], Pos:%d}", node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *Hash) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitHash(node)
|
||||
}
|
||||
|
||||
//
|
||||
// HashPair
|
||||
//
|
||||
|
||||
// HashPair represents a hash pair node.
|
||||
type HashPair struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Key string
|
||||
Val Node // Expression
|
||||
}
|
||||
|
||||
// NewHashPair instanciates a new hash pair node.
|
||||
func NewHashPair(pos int, line int) *HashPair {
|
||||
return &HashPair{
|
||||
NodeType: NodeHashPair,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *HashPair) String() string {
|
||||
return node.Key + "=" + node.Val.String()
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *HashPair) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitHashPair(node)
|
||||
}
|
|
@ -1,279 +0,0 @@
|
|||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// printVisitor implements the Visitor interface to print a AST.
|
||||
type printVisitor struct {
|
||||
buf string
|
||||
depth int
|
||||
|
||||
original bool
|
||||
inBlock bool
|
||||
}
|
||||
|
||||
func newPrintVisitor() *printVisitor {
|
||||
return &printVisitor{}
|
||||
}
|
||||
|
||||
// Print returns a string representation of given AST, that can be used for debugging purpose.
|
||||
func Print(node Node) string {
|
||||
visitor := newPrintVisitor()
|
||||
node.Accept(visitor)
|
||||
return visitor.output()
|
||||
}
|
||||
|
||||
func (v *printVisitor) output() string {
|
||||
return v.buf
|
||||
}
|
||||
|
||||
func (v *printVisitor) indent() {
|
||||
for i := 0; i < v.depth; {
|
||||
v.buf += " "
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (v *printVisitor) str(val string) {
|
||||
v.buf += val
|
||||
}
|
||||
|
||||
func (v *printVisitor) nl() {
|
||||
v.str("\n")
|
||||
}
|
||||
|
||||
func (v *printVisitor) line(val string) {
|
||||
v.indent()
|
||||
v.str(val)
|
||||
v.nl()
|
||||
}
|
||||
|
||||
//
|
||||
// Visitor interface
|
||||
//
|
||||
|
||||
// Statements
|
||||
|
||||
// VisitProgram implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitProgram(node *Program) interface{} {
|
||||
if len(node.BlockParams) > 0 {
|
||||
v.line("BLOCK PARAMS: [ " + strings.Join(node.BlockParams, " ") + " ]")
|
||||
}
|
||||
|
||||
for _, n := range node.Body {
|
||||
n.Accept(v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitMustache implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitMustache(node *MustacheStatement) interface{} {
|
||||
v.indent()
|
||||
v.str("{{ ")
|
||||
|
||||
node.Expression.Accept(v)
|
||||
|
||||
v.str(" }}")
|
||||
v.nl()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitBlock implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitBlock(node *BlockStatement) interface{} {
|
||||
v.inBlock = true
|
||||
|
||||
v.line("BLOCK:")
|
||||
v.depth++
|
||||
|
||||
node.Expression.Accept(v)
|
||||
|
||||
if node.Program != nil {
|
||||
v.line("PROGRAM:")
|
||||
v.depth++
|
||||
node.Program.Accept(v)
|
||||
v.depth--
|
||||
}
|
||||
|
||||
if node.Inverse != nil {
|
||||
// if node.Program != nil {
|
||||
// v.depth++
|
||||
// }
|
||||
|
||||
v.line("{{^}}")
|
||||
v.depth++
|
||||
node.Inverse.Accept(v)
|
||||
v.depth--
|
||||
|
||||
// if node.Program != nil {
|
||||
// v.depth--
|
||||
// }
|
||||
}
|
||||
|
||||
v.inBlock = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitPartial implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitPartial(node *PartialStatement) interface{} {
|
||||
v.indent()
|
||||
v.str("{{> PARTIAL:")
|
||||
|
||||
v.original = true
|
||||
node.Name.Accept(v)
|
||||
v.original = false
|
||||
|
||||
if len(node.Params) > 0 {
|
||||
v.str(" ")
|
||||
node.Params[0].Accept(v)
|
||||
}
|
||||
|
||||
// hash
|
||||
if node.Hash != nil {
|
||||
v.str(" ")
|
||||
node.Hash.Accept(v)
|
||||
}
|
||||
|
||||
v.str(" }}")
|
||||
v.nl()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitContent implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitContent(node *ContentStatement) interface{} {
|
||||
v.line("CONTENT[ '" + node.Value + "' ]")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitComment implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitComment(node *CommentStatement) interface{} {
|
||||
v.line("{{! '" + node.Value + "' }}")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Expressions
|
||||
|
||||
// VisitExpression implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitExpression(node *Expression) interface{} {
|
||||
if v.inBlock {
|
||||
v.indent()
|
||||
}
|
||||
|
||||
// path
|
||||
node.Path.Accept(v)
|
||||
|
||||
// params
|
||||
v.str(" [")
|
||||
for i, n := range node.Params {
|
||||
if i > 0 {
|
||||
v.str(", ")
|
||||
}
|
||||
n.Accept(v)
|
||||
}
|
||||
v.str("]")
|
||||
|
||||
// hash
|
||||
if node.Hash != nil {
|
||||
v.str(" ")
|
||||
node.Hash.Accept(v)
|
||||
}
|
||||
|
||||
if v.inBlock {
|
||||
v.nl()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitSubExpression implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitSubExpression(node *SubExpression) interface{} {
|
||||
node.Expression.Accept(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitPath implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitPath(node *PathExpression) interface{} {
|
||||
if v.original {
|
||||
v.str(node.Original)
|
||||
} else {
|
||||
path := strings.Join(node.Parts, "/")
|
||||
|
||||
result := ""
|
||||
if node.Data {
|
||||
result += "@"
|
||||
}
|
||||
|
||||
v.str(result + "PATH:" + path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Literals
|
||||
|
||||
// VisitString implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitString(node *StringLiteral) interface{} {
|
||||
if v.original {
|
||||
v.str(node.Value)
|
||||
} else {
|
||||
v.str("\"" + node.Value + "\"")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitBoolean implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitBoolean(node *BooleanLiteral) interface{} {
|
||||
if v.original {
|
||||
v.str(node.Original)
|
||||
} else {
|
||||
v.str(fmt.Sprintf("BOOLEAN{%s}", node.Canonical()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitNumber implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitNumber(node *NumberLiteral) interface{} {
|
||||
if v.original {
|
||||
v.str(node.Original)
|
||||
} else {
|
||||
v.str(fmt.Sprintf("NUMBER{%s}", node.Canonical()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Miscellaneous
|
||||
|
||||
// VisitHash implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitHash(node *Hash) interface{} {
|
||||
v.str("HASH{")
|
||||
|
||||
for i, p := range node.Pairs {
|
||||
if i > 0 {
|
||||
v.str(", ")
|
||||
}
|
||||
p.Accept(v)
|
||||
}
|
||||
|
||||
v.str("}")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitHashPair implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitHashPair(node *HashPair) interface{} {
|
||||
v.str(node.Key + "=")
|
||||
node.Val.Accept(v)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Test struct {
|
||||
name string
|
||||
input string
|
||||
data interface{}
|
||||
privData map[string]interface{}
|
||||
helpers map[string]interface{}
|
||||
partials map[string]string
|
||||
output interface{}
|
||||
}
|
||||
|
||||
func launchTests(t *testing.T, tests []Test) {
|
||||
// NOTE: TestMustache() makes Parallel testing fail
|
||||
// t.Parallel()
|
||||
|
||||
for _, test := range tests {
|
||||
var err error
|
||||
var tpl *Template
|
||||
|
||||
// parse template
|
||||
tpl, err = Parse(test.input)
|
||||
if err != nil {
|
||||
t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err)
|
||||
} else {
|
||||
if len(test.helpers) > 0 {
|
||||
// register helpers
|
||||
tpl.RegisterHelpers(test.helpers)
|
||||
}
|
||||
|
||||
if len(test.partials) > 0 {
|
||||
// register partials
|
||||
tpl.RegisterPartials(test.partials)
|
||||
}
|
||||
|
||||
// setup private data frame
|
||||
var privData *DataFrame
|
||||
if test.privData != nil {
|
||||
privData = NewDataFrame()
|
||||
for k, v := range test.privData {
|
||||
privData.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// render template
|
||||
output, err := tpl.ExecWith(test.data, privData)
|
||||
if err != nil {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\nerror:\n\t%s\nAST:\n\t%s", test.name, test.input, Str(test.data), err, tpl.PrintAST())
|
||||
} else {
|
||||
// check output
|
||||
var expectedArr []string
|
||||
expectedArr, ok := test.output.([]string)
|
||||
if ok {
|
||||
match := false
|
||||
for _, expectedStr := range expectedArr {
|
||||
if expectedStr == output {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !match {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, Str(test.data), Str(test.partials), expectedArr, output, tpl.PrintAST())
|
||||
}
|
||||
} else {
|
||||
expectedStr, ok := test.output.(string)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("Erroneous test output description: %q", test.output))
|
||||
}
|
||||
|
||||
if expectedStr != output {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, Str(test.data), Str(test.partials), expectedStr, output, tpl.PrintAST())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func launchErrorTests(t *testing.T, tests []Test) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range tests {
|
||||
var err error
|
||||
var tpl *Template
|
||||
|
||||
// parse template
|
||||
tpl, err = Parse(test.input)
|
||||
if err != nil {
|
||||
t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err)
|
||||
} else {
|
||||
if len(test.helpers) > 0 {
|
||||
// register helpers
|
||||
tpl.RegisterHelpers(test.helpers)
|
||||
}
|
||||
|
||||
if len(test.partials) > 0 {
|
||||
// register partials
|
||||
tpl.RegisterPartials(test.partials)
|
||||
}
|
||||
|
||||
// setup private data frame
|
||||
var privData *DataFrame
|
||||
if test.privData != nil {
|
||||
privData := NewDataFrame()
|
||||
for k, v := range test.privData {
|
||||
privData.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// render template
|
||||
output, err := tpl.ExecWith(test.data, privData)
|
||||
if err == nil {
|
||||
t.Errorf("Test '%s' failed - Error expected\ninput:\n\t'%s'\ngot\n\t%q\nAST:\n%q", test.name, test.input, output, tpl.PrintAST())
|
||||
} else {
|
||||
var errMatch error
|
||||
match := false
|
||||
|
||||
// check output
|
||||
var expectedArr []string
|
||||
expectedArr, ok := test.output.([]string)
|
||||
if ok {
|
||||
if len(expectedArr) > 0 {
|
||||
for _, expectedStr := range expectedArr {
|
||||
match, errMatch = regexp.MatchString(regexp.QuoteMeta(expectedStr), fmt.Sprint(err))
|
||||
if errMatch != nil {
|
||||
panic("Failed to match regexp")
|
||||
}
|
||||
|
||||
if match {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// nothing to test
|
||||
match = true
|
||||
}
|
||||
} else {
|
||||
expectedStr, ok := test.output.(string)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("Erroneous test output description: %q", test.output))
|
||||
}
|
||||
|
||||
if expectedStr != "" {
|
||||
match, errMatch = regexp.MatchString(regexp.QuoteMeta(expectedStr), fmt.Sprint(err))
|
||||
if errMatch != nil {
|
||||
panic("Failed to match regexp")
|
||||
}
|
||||
} else {
|
||||
// nothing to test
|
||||
match = true
|
||||
}
|
||||
}
|
||||
|
||||
if !match {
|
||||
t.Errorf("Test '%s' failed - Incorrect error returned\ninput:\n\t'%s'\ndata:\n\t%s\nexpected\n\t%q\ngot\n\t%q", test.name, test.input, Str(test.data), test.output, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,316 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import "testing"
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/bench/
|
||||
//
|
||||
// Note that handlebars.js does NOT benchmark template compilation, it only benchmarks evaluation.
|
||||
//
|
||||
|
||||
func BenchmarkArguments(b *testing.B) {
|
||||
source := `{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}`
|
||||
|
||||
ctx := map[string]bool{
|
||||
"bar": true,
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
tpl.RegisterHelper("foo", func(a, b, c, d interface{}) string { return "" })
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkArrayEach(b *testing.B) {
|
||||
source := `{{#each names}}{{name}}{{/each}}`
|
||||
|
||||
ctx := map[string][]map[string]string{
|
||||
"names": {
|
||||
{"name": "Moe"},
|
||||
{"name": "Larry"},
|
||||
{"name": "Curly"},
|
||||
{"name": "Shemp"},
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkArrayMustache(b *testing.B) {
|
||||
source := `{{#names}}{{name}}{{/names}}`
|
||||
|
||||
ctx := map[string][]map[string]string{
|
||||
"names": {
|
||||
{"name": "Moe"},
|
||||
{"name": "Larry"},
|
||||
{"name": "Curly"},
|
||||
{"name": "Shemp"},
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkComplex(b *testing.B) {
|
||||
source := `<h1>{{header}}</h1>
|
||||
{{#if items}}
|
||||
<ul>
|
||||
{{#each items}}
|
||||
{{#if current}}
|
||||
<li><strong>{{name}}</strong></li>
|
||||
{{^}}
|
||||
<li><a href="{{url}}">{{name}}</a></li>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{^}}
|
||||
<p>The list is empty.</p>
|
||||
{{/if}}
|
||||
`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"header": func() string { return "Colors" },
|
||||
"hasItems": true,
|
||||
"items": []map[string]interface{}{
|
||||
{"name": "red", "current": true, "url": "#Red"},
|
||||
{"name": "green", "current": false, "url": "#Green"},
|
||||
{"name": "blue", "current": false, "url": "#Blue"},
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkData(b *testing.B) {
|
||||
source := `{{#each names}}{{@index}}{{name}}{{/each}}`
|
||||
|
||||
ctx := map[string][]map[string]string{
|
||||
"names": {
|
||||
{"name": "Moe"},
|
||||
{"name": "Larry"},
|
||||
{"name": "Curly"},
|
||||
{"name": "Shemp"},
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDepth1(b *testing.B) {
|
||||
source := `{{#each names}}{{../foo}}{{/each}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"names": []map[string]string{
|
||||
{"name": "Moe"},
|
||||
{"name": "Larry"},
|
||||
{"name": "Curly"},
|
||||
{"name": "Shemp"},
|
||||
},
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDepth2(b *testing.B) {
|
||||
source := `{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"names": []map[string]interface{}{
|
||||
{"bat": "foo", "name": []string{"Moe"}},
|
||||
{"bat": "foo", "name": []string{"Larry"}},
|
||||
{"bat": "foo", "name": []string{"Curly"}},
|
||||
{"bat": "foo", "name": []string{"Shemp"}},
|
||||
},
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkObjectMustache(b *testing.B) {
|
||||
source := `{{#person}}{{name}}{{age}}{{/person}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"person": map[string]interface{}{
|
||||
"name": "Larry",
|
||||
"age": 45,
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkObject(b *testing.B) {
|
||||
source := `{{#with person}}{{name}}{{age}}{{/with}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"person": map[string]interface{}{
|
||||
"name": "Larry",
|
||||
"age": 45,
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPartialRecursion(b *testing.B) {
|
||||
source := `{{name}}{{#each kids}}{{>recursion}}{{/each}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"name": 1,
|
||||
"kids": []map[string]interface{}{
|
||||
{
|
||||
"name": "1.1",
|
||||
"kids": []map[string]interface{}{
|
||||
{
|
||||
"name": "1.1.1",
|
||||
"kids": []map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
partial := MustParse(`{{name}}{{#each kids}}{{>recursion}}{{/each}}`)
|
||||
tpl.RegisterPartialTemplate("recursion", partial)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPartial(b *testing.B) {
|
||||
source := `{{#each peeps}}{{>variables}}{{/each}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"peeps": []map[string]interface{}{
|
||||
{"name": "Moe", "count": 15},
|
||||
{"name": "Moe", "count": 5},
|
||||
{"name": "Curly", "count": 1},
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
partial := MustParse(`Hello {{name}}! You have {{count}} new messages.`)
|
||||
tpl.RegisterPartialTemplate("variables", partial)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPath(b *testing.B) {
|
||||
source := `{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"person": map[string]interface{}{
|
||||
"name": map[string]interface{}{
|
||||
"bar": map[string]string{
|
||||
"baz": "Larry",
|
||||
},
|
||||
},
|
||||
"age": 45,
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkString(b *testing.B) {
|
||||
source := `Hello world`
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSubExpression(b *testing.B) {
|
||||
source := `{{echo (header)}}`
|
||||
|
||||
ctx := map[string]interface{}{}
|
||||
|
||||
tpl := MustParse(source)
|
||||
tpl.RegisterHelpers(map[string]interface{}{
|
||||
"echo": func(v string) string { return "foo " + v },
|
||||
"header": func() string { return "Colors" },
|
||||
})
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVariables(b *testing.B) {
|
||||
source := `Hello {{name}}! You have {{count}} new messages.`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"name": "Mick",
|
||||
"count": 30,
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import "reflect"
|
||||
|
||||
// DataFrame represents a private data frame.
|
||||
//
|
||||
// Cf. private variables documentation at: http://handlebarsjs.com/block_helpers.html
|
||||
type DataFrame struct {
|
||||
parent *DataFrame
|
||||
data map[string]interface{}
|
||||
}
|
||||
|
||||
// NewDataFrame instanciates a new private data frame.
|
||||
func NewDataFrame() *DataFrame {
|
||||
return &DataFrame{
|
||||
data: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Copy instanciates a new private data frame with receiver as parent.
|
||||
func (p *DataFrame) Copy() *DataFrame {
|
||||
result := NewDataFrame()
|
||||
|
||||
for k, v := range p.data {
|
||||
result.data[k] = v
|
||||
}
|
||||
|
||||
result.parent = p
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// newIterDataFrame instanciates a new private data frame with receiver as parent and with iteration data set (@index, @key, @first, @last)
|
||||
func (p *DataFrame) newIterDataFrame(length int, i int, key interface{}) *DataFrame {
|
||||
result := p.Copy()
|
||||
|
||||
result.Set("index", i)
|
||||
result.Set("key", key)
|
||||
result.Set("first", i == 0)
|
||||
result.Set("last", i == length-1)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Set sets a data value.
|
||||
func (p *DataFrame) Set(key string, val interface{}) {
|
||||
p.data[key] = val
|
||||
}
|
||||
|
||||
// Get gets a data value.
|
||||
func (p *DataFrame) Get(key string) interface{} {
|
||||
return p.find([]string{key})
|
||||
}
|
||||
|
||||
// find gets a deep data value
|
||||
//
|
||||
// @todo This is NOT consistent with the way we resolve data in template (cf. `evalDataPathExpression()`) ! FIX THAT !
|
||||
func (p *DataFrame) find(parts []string) interface{} {
|
||||
data := p.data
|
||||
|
||||
for i, part := range parts {
|
||||
val := data[part]
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if i == len(parts)-1 {
|
||||
// found
|
||||
return val
|
||||
}
|
||||
|
||||
valValue := reflect.ValueOf(val)
|
||||
if valValue.Kind() != reflect.Map {
|
||||
// not found
|
||||
return nil
|
||||
}
|
||||
|
||||
// continue
|
||||
data = mapStringInterface(valValue)
|
||||
}
|
||||
|
||||
// not found
|
||||
return nil
|
||||
}
|
||||
|
||||
// mapStringInterface converts any `map` to `map[string]interface{}`
|
||||
func mapStringInterface(value reflect.Value) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for _, key := range value.MapKeys() {
|
||||
result[strValue(key)] = value.MapIndex(key).Interface()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//
|
||||
// That whole file is borrowed from https://github.com/golang/go/tree/master/src/html/escape.go
|
||||
//
|
||||
// With changes:
|
||||
// ' => '
|
||||
// " => "
|
||||
//
|
||||
// To stay in sync with JS implementation, and make mustache tests pass.
|
||||
//
|
||||
|
||||
type writer interface {
|
||||
WriteString(string) (int, error)
|
||||
}
|
||||
|
||||
const escapedChars = `&'<>"`
|
||||
|
||||
func escape(w writer, s string) error {
|
||||
i := strings.IndexAny(s, escapedChars)
|
||||
for i != -1 {
|
||||
if _, err := w.WriteString(s[:i]); err != nil {
|
||||
return err
|
||||
}
|
||||
var esc string
|
||||
switch s[i] {
|
||||
case '&':
|
||||
esc = "&"
|
||||
case '\'':
|
||||
esc = "'"
|
||||
case '<':
|
||||
esc = "<"
|
||||
case '>':
|
||||
esc = ">"
|
||||
case '"':
|
||||
esc = """
|
||||
default:
|
||||
panic("unrecognized escape character")
|
||||
}
|
||||
s = s[i+1:]
|
||||
if _, err := w.WriteString(esc); err != nil {
|
||||
return err
|
||||
}
|
||||
i = strings.IndexAny(s, escapedChars)
|
||||
}
|
||||
_, err := w.WriteString(s)
|
||||
return err
|
||||
}
|
||||
|
||||
// Escape escapes special HTML characters.
|
||||
//
|
||||
// It can be used by helpers that return a SafeString and that need to escape some content by themselves.
|
||||
func Escape(s string) string {
|
||||
if strings.IndexAny(s, escapedChars) == -1 {
|
||||
return s
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
escape(&buf, s)
|
||||
return buf.String()
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import "fmt"
|
||||
|
||||
func ExampleEscape() {
|
||||
tpl := MustParse("{{link url text}}")
|
||||
|
||||
tpl.RegisterHelper("link", func(url string, text string) SafeString {
|
||||
return SafeString("<a href='" + Escape(url) + "'>" + Escape(text) + "</a>")
|
||||
})
|
||||
|
||||
ctx := map[string]string{
|
||||
"url": "http://www.aymerick.com/",
|
||||
"text": "This is a <em>cool</em> website",
|
||||
}
|
||||
|
||||
result := tpl.MustExec(ctx)
|
||||
fmt.Print(result)
|
||||
// Output: <a href='http://www.aymerick.com/'>This is a <em>cool</em> website</a>
|
||||
}
|
|
@ -1,984 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aymerick/raymond/ast"
|
||||
)
|
||||
|
||||
var (
|
||||
// @note borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
||||
|
||||
zero reflect.Value
|
||||
)
|
||||
|
||||
// evalVisitor evaluates a handlebars template with context
|
||||
type evalVisitor struct {
|
||||
tpl *Template
|
||||
|
||||
// contexts stack
|
||||
ctx []reflect.Value
|
||||
|
||||
// current data frame (chained with parent)
|
||||
dataFrame *DataFrame
|
||||
|
||||
// block parameters stack
|
||||
blockParams []map[string]interface{}
|
||||
|
||||
// block statements stack
|
||||
blocks []*ast.BlockStatement
|
||||
|
||||
// expressions stack
|
||||
exprs []*ast.Expression
|
||||
|
||||
// memoize expressions that were function calls
|
||||
exprFunc map[*ast.Expression]bool
|
||||
|
||||
// used for info on panic
|
||||
curNode ast.Node
|
||||
}
|
||||
|
||||
// NewEvalVisitor instanciate a new evaluation visitor with given context and initial private data frame
|
||||
//
|
||||
// If privData is nil, then a default data frame is created
|
||||
func newEvalVisitor(tpl *Template, ctx interface{}, privData *DataFrame) *evalVisitor {
|
||||
frame := privData
|
||||
if frame == nil {
|
||||
frame = NewDataFrame()
|
||||
}
|
||||
|
||||
return &evalVisitor{
|
||||
tpl: tpl,
|
||||
ctx: []reflect.Value{reflect.ValueOf(ctx)},
|
||||
dataFrame: frame,
|
||||
exprFunc: make(map[*ast.Expression]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// at sets current node
|
||||
func (v *evalVisitor) at(node ast.Node) {
|
||||
v.curNode = node
|
||||
}
|
||||
|
||||
//
|
||||
// Contexts stack
|
||||
//
|
||||
|
||||
// pushCtx pushes new context to the stack
|
||||
func (v *evalVisitor) pushCtx(ctx reflect.Value) {
|
||||
v.ctx = append(v.ctx, ctx)
|
||||
}
|
||||
|
||||
// popCtx pops last context from stack
|
||||
func (v *evalVisitor) popCtx() reflect.Value {
|
||||
if len(v.ctx) == 0 {
|
||||
return zero
|
||||
}
|
||||
|
||||
var result reflect.Value
|
||||
result, v.ctx = v.ctx[len(v.ctx)-1], v.ctx[:len(v.ctx)-1]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// rootCtx returns root context
|
||||
func (v *evalVisitor) rootCtx() reflect.Value {
|
||||
return v.ctx[0]
|
||||
}
|
||||
|
||||
// curCtx returns current context
|
||||
func (v *evalVisitor) curCtx() reflect.Value {
|
||||
return v.ancestorCtx(0)
|
||||
}
|
||||
|
||||
// ancestorCtx returns ancestor context
|
||||
func (v *evalVisitor) ancestorCtx(depth int) reflect.Value {
|
||||
index := len(v.ctx) - 1 - depth
|
||||
if index < 0 {
|
||||
return zero
|
||||
}
|
||||
|
||||
return v.ctx[index]
|
||||
}
|
||||
|
||||
//
|
||||
// Private data frame
|
||||
//
|
||||
|
||||
// setDataFrame sets new data frame
|
||||
func (v *evalVisitor) setDataFrame(frame *DataFrame) {
|
||||
v.dataFrame = frame
|
||||
}
|
||||
|
||||
// popDataFrame sets back parent data frame
|
||||
func (v *evalVisitor) popDataFrame() {
|
||||
v.dataFrame = v.dataFrame.parent
|
||||
}
|
||||
|
||||
//
|
||||
// Block Parameters stack
|
||||
//
|
||||
|
||||
// pushBlockParams pushes new block params to the stack
|
||||
func (v *evalVisitor) pushBlockParams(params map[string]interface{}) {
|
||||
v.blockParams = append(v.blockParams, params)
|
||||
}
|
||||
|
||||
// popBlockParams pops last block params from stack
|
||||
func (v *evalVisitor) popBlockParams() map[string]interface{} {
|
||||
var result map[string]interface{}
|
||||
|
||||
if len(v.blockParams) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
result, v.blockParams = v.blockParams[len(v.blockParams)-1], v.blockParams[:len(v.blockParams)-1]
|
||||
return result
|
||||
}
|
||||
|
||||
// blockParam iterates on stack to find given block parameter, and returns its value or nil if not founc
|
||||
func (v *evalVisitor) blockParam(name string) interface{} {
|
||||
for i := len(v.blockParams) - 1; i >= 0; i-- {
|
||||
for k, v := range v.blockParams[i] {
|
||||
if name == k {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Blocks stack
|
||||
//
|
||||
|
||||
// pushBlock pushes new block statement to stack
|
||||
func (v *evalVisitor) pushBlock(block *ast.BlockStatement) {
|
||||
v.blocks = append(v.blocks, block)
|
||||
}
|
||||
|
||||
// popBlock pops last block statement from stack
|
||||
func (v *evalVisitor) popBlock() *ast.BlockStatement {
|
||||
if len(v.blocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result *ast.BlockStatement
|
||||
result, v.blocks = v.blocks[len(v.blocks)-1], v.blocks[:len(v.blocks)-1]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// curBlock returns current block statement
|
||||
func (v *evalVisitor) curBlock() *ast.BlockStatement {
|
||||
if len(v.blocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.blocks[len(v.blocks)-1]
|
||||
}
|
||||
|
||||
//
|
||||
// Expressions stack
|
||||
//
|
||||
|
||||
// pushExpr pushes new expression to stack
|
||||
func (v *evalVisitor) pushExpr(expression *ast.Expression) {
|
||||
v.exprs = append(v.exprs, expression)
|
||||
}
|
||||
|
||||
// popExpr pops last expression from stack
|
||||
func (v *evalVisitor) popExpr() *ast.Expression {
|
||||
if len(v.exprs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result *ast.Expression
|
||||
result, v.exprs = v.exprs[len(v.exprs)-1], v.exprs[:len(v.exprs)-1]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// curExpr returns current expression
|
||||
func (v *evalVisitor) curExpr() *ast.Expression {
|
||||
if len(v.exprs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.exprs[len(v.exprs)-1]
|
||||
}
|
||||
|
||||
//
|
||||
// Error functions
|
||||
//
|
||||
|
||||
// errPanic panics
|
||||
func (v *evalVisitor) errPanic(err error) {
|
||||
panic(fmt.Errorf("Evaluation error: %s\nCurrent node:\n\t%s", err, v.curNode))
|
||||
}
|
||||
|
||||
// errorf panics with a custom message
|
||||
func (v *evalVisitor) errorf(format string, args ...interface{}) {
|
||||
v.errPanic(fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
//
|
||||
// Evaluation
|
||||
//
|
||||
|
||||
// evalProgram eEvaluates program with given context and returns string result
|
||||
func (v *evalVisitor) evalProgram(program *ast.Program, ctx interface{}, data *DataFrame, key interface{}) string {
|
||||
blockParams := make(map[string]interface{})
|
||||
|
||||
// compute block params
|
||||
if len(program.BlockParams) > 0 {
|
||||
blockParams[program.BlockParams[0]] = ctx
|
||||
}
|
||||
|
||||
if (len(program.BlockParams) > 1) && (key != nil) {
|
||||
blockParams[program.BlockParams[1]] = key
|
||||
}
|
||||
|
||||
// push contexts
|
||||
if len(blockParams) > 0 {
|
||||
v.pushBlockParams(blockParams)
|
||||
}
|
||||
|
||||
ctxVal := reflect.ValueOf(ctx)
|
||||
if ctxVal.IsValid() {
|
||||
v.pushCtx(ctxVal)
|
||||
}
|
||||
|
||||
if data != nil {
|
||||
v.setDataFrame(data)
|
||||
}
|
||||
|
||||
// evaluate program
|
||||
result, _ := program.Accept(v).(string)
|
||||
|
||||
// pop contexts
|
||||
if data != nil {
|
||||
v.popDataFrame()
|
||||
}
|
||||
|
||||
if ctxVal.IsValid() {
|
||||
v.popCtx()
|
||||
}
|
||||
|
||||
if len(blockParams) > 0 {
|
||||
v.popBlockParams()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// evalPath evaluates all path parts with given context
|
||||
func (v *evalVisitor) evalPath(ctx reflect.Value, parts []string, exprRoot bool) (reflect.Value, bool) {
|
||||
partResolved := false
|
||||
|
||||
for i := 0; i < len(parts); i++ {
|
||||
part := parts[i]
|
||||
|
||||
// "[foo bar]"" => "foo bar"
|
||||
if (len(part) >= 2) && (part[0] == '[') && (part[len(part)-1] == ']') {
|
||||
part = part[1 : len(part)-1]
|
||||
}
|
||||
|
||||
ctx = v.evalField(ctx, part, exprRoot)
|
||||
if !ctx.IsValid() {
|
||||
break
|
||||
}
|
||||
|
||||
// we resolved at least one part of path
|
||||
partResolved = true
|
||||
}
|
||||
|
||||
return ctx, partResolved
|
||||
}
|
||||
|
||||
// evalField evaluates field with given context
|
||||
func (v *evalVisitor) evalField(ctx reflect.Value, fieldName string, exprRoot bool) reflect.Value {
|
||||
result := zero
|
||||
|
||||
ctx, _ = indirect(ctx)
|
||||
if !ctx.IsValid() {
|
||||
return result
|
||||
}
|
||||
|
||||
// check if this is a method call
|
||||
result, isMeth := v.evalMethod(ctx, fieldName, exprRoot)
|
||||
if !isMeth {
|
||||
switch ctx.Kind() {
|
||||
case reflect.Struct:
|
||||
// example: firstName => FirstName
|
||||
expFieldName := strings.Title(fieldName)
|
||||
|
||||
// check if struct have this field and that it is exported
|
||||
if tField, ok := ctx.Type().FieldByName(expFieldName); ok && (tField.PkgPath == "") {
|
||||
// struct field
|
||||
result = ctx.FieldByIndex(tField.Index)
|
||||
}
|
||||
case reflect.Map:
|
||||
nameVal := reflect.ValueOf(fieldName)
|
||||
if nameVal.Type().AssignableTo(ctx.Type().Key()) {
|
||||
// map key
|
||||
result = ctx.MapIndex(nameVal)
|
||||
}
|
||||
case reflect.Array, reflect.Slice:
|
||||
if i, err := strconv.Atoi(fieldName); (err == nil) && (i < ctx.Len()) {
|
||||
result = ctx.Index(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if result is a function
|
||||
result, _ = indirect(result)
|
||||
if result.Kind() == reflect.Func {
|
||||
result = v.evalFieldFunc(fieldName, result, exprRoot)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// evalFieldFunc tries to evaluate given method name, and a boolean to indicate if this was a method call
|
||||
func (v *evalVisitor) evalMethod(ctx reflect.Value, name string, exprRoot bool) (reflect.Value, bool) {
|
||||
if ctx.Kind() != reflect.Interface && ctx.CanAddr() {
|
||||
ctx = ctx.Addr()
|
||||
}
|
||||
|
||||
method := ctx.MethodByName(name)
|
||||
if !method.IsValid() {
|
||||
// example: subject() => Subject()
|
||||
method = ctx.MethodByName(strings.Title(name))
|
||||
}
|
||||
|
||||
if !method.IsValid() {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
return v.evalFieldFunc(name, method, exprRoot), true
|
||||
}
|
||||
|
||||
// evalFieldFunc evaluates given function
|
||||
func (v *evalVisitor) evalFieldFunc(name string, funcVal reflect.Value, exprRoot bool) reflect.Value {
|
||||
ensureValidHelper(name, funcVal)
|
||||
|
||||
var options *Options
|
||||
if exprRoot {
|
||||
// create function arg with all params/hash
|
||||
expr := v.curExpr()
|
||||
options = v.helperOptions(expr)
|
||||
|
||||
// ok, that expression was a function call
|
||||
v.exprFunc[expr] = true
|
||||
} else {
|
||||
// we are not at root of expression, so we are a parameter... and we don't like
|
||||
// infinite loops caused by trying to parse ourself forever
|
||||
options = newEmptyOptions(v)
|
||||
}
|
||||
|
||||
return v.callFunc(name, funcVal, options)
|
||||
}
|
||||
|
||||
// findBlockParam returns node's block parameter
|
||||
func (v *evalVisitor) findBlockParam(node *ast.PathExpression) (string, interface{}) {
|
||||
if len(node.Parts) > 0 {
|
||||
name := node.Parts[0]
|
||||
if value := v.blockParam(name); value != nil {
|
||||
return name, value
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// evalPathExpression evaluates a path expression
|
||||
func (v *evalVisitor) evalPathExpression(node *ast.PathExpression, exprRoot bool) interface{} {
|
||||
var result interface{}
|
||||
|
||||
if name, value := v.findBlockParam(node); value != nil {
|
||||
// block parameter value
|
||||
|
||||
// We push a new context so we can evaluate the path expression (note: this may be a bad idea).
|
||||
//
|
||||
// Example:
|
||||
// {{#foo as |bar|}}
|
||||
// {{bar.baz}}
|
||||
// {{/foo}}
|
||||
//
|
||||
// With data:
|
||||
// {"foo": {"baz": "bat"}}
|
||||
newCtx := map[string]interface{}{name: value}
|
||||
|
||||
v.pushCtx(reflect.ValueOf(newCtx))
|
||||
result = v.evalCtxPathExpression(node, exprRoot)
|
||||
v.popCtx()
|
||||
} else {
|
||||
ctxTried := false
|
||||
|
||||
if node.IsDataRoot() {
|
||||
// context path
|
||||
result = v.evalCtxPathExpression(node, exprRoot)
|
||||
|
||||
ctxTried = true
|
||||
}
|
||||
|
||||
if (result == nil) && node.Data {
|
||||
// if it is @root, then we tried to evaluate with root context but nothing was found
|
||||
// so let's try with private data
|
||||
|
||||
// private data
|
||||
result = v.evalDataPathExpression(node, exprRoot)
|
||||
}
|
||||
|
||||
if (result == nil) && !ctxTried {
|
||||
// context path
|
||||
result = v.evalCtxPathExpression(node, exprRoot)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// evalDataPathExpression evaluates a private data path expression
|
||||
func (v *evalVisitor) evalDataPathExpression(node *ast.PathExpression, exprRoot bool) interface{} {
|
||||
// find data frame
|
||||
frame := v.dataFrame
|
||||
for i := node.Depth; i > 0; i-- {
|
||||
if frame.parent == nil {
|
||||
return nil
|
||||
}
|
||||
frame = frame.parent
|
||||
}
|
||||
|
||||
// resolve data
|
||||
// @note Can be changed to v.evalCtx() as context can't be an array
|
||||
result, _ := v.evalCtxPath(reflect.ValueOf(frame.data), node.Parts, exprRoot)
|
||||
return result
|
||||
}
|
||||
|
||||
// evalCtxPathExpression evaluates a context path expression
|
||||
func (v *evalVisitor) evalCtxPathExpression(node *ast.PathExpression, exprRoot bool) interface{} {
|
||||
v.at(node)
|
||||
|
||||
if node.IsDataRoot() {
|
||||
// `@root` - remove the first part
|
||||
parts := node.Parts[1:len(node.Parts)]
|
||||
|
||||
result, _ := v.evalCtxPath(v.rootCtx(), parts, exprRoot)
|
||||
return result
|
||||
}
|
||||
|
||||
return v.evalDepthPath(node.Depth, node.Parts, exprRoot)
|
||||
}
|
||||
|
||||
// evalDepthPath iterates on contexts, starting at given depth, until there is one that resolve given path parts
|
||||
func (v *evalVisitor) evalDepthPath(depth int, parts []string, exprRoot bool) interface{} {
|
||||
var result interface{}
|
||||
partResolved := false
|
||||
|
||||
ctx := v.ancestorCtx(depth)
|
||||
|
||||
for (result == nil) && ctx.IsValid() && (depth <= len(v.ctx) && !partResolved) {
|
||||
// try with context
|
||||
result, partResolved = v.evalCtxPath(ctx, parts, exprRoot)
|
||||
|
||||
// As soon as we find the first part of a path, we must not try to resolve with parent context if result is finally `nil`
|
||||
// Reference: "Dotted Names - Context Precedence" mustache test
|
||||
if !partResolved && (result == nil) {
|
||||
// try with previous context
|
||||
depth++
|
||||
ctx = v.ancestorCtx(depth)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// evalCtxPath evaluates path with given context
|
||||
func (v *evalVisitor) evalCtxPath(ctx reflect.Value, parts []string, exprRoot bool) (interface{}, bool) {
|
||||
var result interface{}
|
||||
partResolved := false
|
||||
|
||||
switch ctx.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
// Array context
|
||||
var results []interface{}
|
||||
|
||||
for i := 0; i < ctx.Len(); i++ {
|
||||
value, _ := v.evalPath(ctx.Index(i), parts, exprRoot)
|
||||
if value.IsValid() {
|
||||
results = append(results, value.Interface())
|
||||
}
|
||||
}
|
||||
|
||||
result = results
|
||||
default:
|
||||
// NOT array context
|
||||
var value reflect.Value
|
||||
|
||||
value, partResolved = v.evalPath(ctx, parts, exprRoot)
|
||||
if value.IsValid() {
|
||||
result = value.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return result, partResolved
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
// isHelperCall returns true if given expression is a helper call
|
||||
func (v *evalVisitor) isHelperCall(node *ast.Expression) bool {
|
||||
if helperName := node.HelperName(); helperName != "" {
|
||||
return v.findHelper(helperName) != zero
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// findHelper finds given helper
|
||||
func (v *evalVisitor) findHelper(name string) reflect.Value {
|
||||
// check template helpers
|
||||
if h := v.tpl.findHelper(name); h != zero {
|
||||
return h
|
||||
}
|
||||
|
||||
// check global helpers
|
||||
return findHelper(name)
|
||||
}
|
||||
|
||||
// callFunc calls function with given options
|
||||
func (v *evalVisitor) callFunc(name string, funcVal reflect.Value, options *Options) reflect.Value {
|
||||
params := options.Params()
|
||||
|
||||
funcType := funcVal.Type()
|
||||
|
||||
// @todo Is there a better way to do that ?
|
||||
strType := reflect.TypeOf("")
|
||||
boolType := reflect.TypeOf(true)
|
||||
|
||||
// check parameters number
|
||||
addOptions := false
|
||||
numIn := funcType.NumIn()
|
||||
|
||||
if numIn == len(params)+1 {
|
||||
lastArgType := funcType.In(numIn - 1)
|
||||
if reflect.TypeOf(options).AssignableTo(lastArgType) {
|
||||
addOptions = true
|
||||
}
|
||||
}
|
||||
|
||||
if !addOptions && (len(params) != numIn) {
|
||||
v.errorf("Helper '%s' called with wrong number of arguments, needed %d but got %d", name, numIn, len(params))
|
||||
}
|
||||
|
||||
// check and collect arguments
|
||||
args := make([]reflect.Value, numIn)
|
||||
for i, param := range params {
|
||||
arg := reflect.ValueOf(param)
|
||||
argType := funcType.In(i)
|
||||
|
||||
if !arg.IsValid() {
|
||||
if canBeNil(argType) {
|
||||
arg = reflect.Zero(argType)
|
||||
} else if argType.Kind() == reflect.String {
|
||||
arg = reflect.ValueOf("")
|
||||
} else {
|
||||
// @todo Maybe we can panic on that
|
||||
return reflect.Zero(strType)
|
||||
}
|
||||
}
|
||||
|
||||
if !arg.Type().AssignableTo(argType) {
|
||||
if strType.AssignableTo(argType) {
|
||||
// convert parameter to string
|
||||
arg = reflect.ValueOf(strValue(arg))
|
||||
} else if boolType.AssignableTo(argType) {
|
||||
// convert parameter to bool
|
||||
val, _ := isTrueValue(arg)
|
||||
arg = reflect.ValueOf(val)
|
||||
} else {
|
||||
v.errorf("Helper %s called with argument %d with type %s but it should be %s", name, i, arg.Type(), argType)
|
||||
}
|
||||
}
|
||||
|
||||
args[i] = arg
|
||||
}
|
||||
|
||||
if addOptions {
|
||||
args[numIn-1] = reflect.ValueOf(options)
|
||||
}
|
||||
|
||||
result := funcVal.Call(args)
|
||||
|
||||
return result[0]
|
||||
}
|
||||
|
||||
// callHelper invoqs helper function for given expression node
|
||||
func (v *evalVisitor) callHelper(name string, helper reflect.Value, node *ast.Expression) interface{} {
|
||||
result := v.callFunc(name, helper, v.helperOptions(node))
|
||||
if !result.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// @todo We maybe want to ensure here that helper returned a string or a SafeString
|
||||
return result.Interface()
|
||||
}
|
||||
|
||||
// helperOptions computes helper options argument from an expression
|
||||
func (v *evalVisitor) helperOptions(node *ast.Expression) *Options {
|
||||
var params []interface{}
|
||||
var hash map[string]interface{}
|
||||
|
||||
for _, paramNode := range node.Params {
|
||||
param := paramNode.Accept(v)
|
||||
params = append(params, param)
|
||||
}
|
||||
|
||||
if node.Hash != nil {
|
||||
hash, _ = node.Hash.Accept(v).(map[string]interface{})
|
||||
}
|
||||
|
||||
return newOptions(v, params, hash)
|
||||
}
|
||||
|
||||
//
|
||||
// Partials
|
||||
//
|
||||
|
||||
// findPartial finds given partial
|
||||
func (v *evalVisitor) findPartial(name string) *partial {
|
||||
// check template partials
|
||||
if p := v.tpl.findPartial(name); p != nil {
|
||||
return p
|
||||
}
|
||||
|
||||
// check global partials
|
||||
return findPartial(name)
|
||||
}
|
||||
|
||||
// partialContext computes partial context
|
||||
func (v *evalVisitor) partialContext(node *ast.PartialStatement) reflect.Value {
|
||||
if nb := len(node.Params); nb > 1 {
|
||||
v.errorf("Unsupported number of partial arguments: %d", nb)
|
||||
}
|
||||
|
||||
if (len(node.Params) > 0) && (node.Hash != nil) {
|
||||
v.errorf("Passing both context and named parameters to a partial is not allowed")
|
||||
}
|
||||
|
||||
if len(node.Params) == 1 {
|
||||
return reflect.ValueOf(node.Params[0].Accept(v))
|
||||
}
|
||||
|
||||
if node.Hash != nil {
|
||||
hash, _ := node.Hash.Accept(v).(map[string]interface{})
|
||||
return reflect.ValueOf(hash)
|
||||
}
|
||||
|
||||
return zero
|
||||
}
|
||||
|
||||
// evalPartial evaluates a partial
|
||||
func (v *evalVisitor) evalPartial(p *partial, node *ast.PartialStatement) string {
|
||||
// get partial template
|
||||
partialTpl, err := p.template()
|
||||
if err != nil {
|
||||
v.errPanic(err)
|
||||
}
|
||||
|
||||
// push partial context
|
||||
ctx := v.partialContext(node)
|
||||
if ctx.IsValid() {
|
||||
v.pushCtx(ctx)
|
||||
}
|
||||
|
||||
// evaluate partial template
|
||||
result, _ := partialTpl.program.Accept(v).(string)
|
||||
|
||||
// ident partial
|
||||
result = indentLines(result, node.Indent)
|
||||
|
||||
if ctx.IsValid() {
|
||||
v.popCtx()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// indentLines indents all lines of given string
|
||||
func indentLines(str string, indent string) string {
|
||||
if indent == "" {
|
||||
return str
|
||||
}
|
||||
|
||||
var indented []string
|
||||
|
||||
lines := strings.Split(str, "\n")
|
||||
for i, line := range lines {
|
||||
if (i == (len(lines) - 1)) && (line == "") {
|
||||
// input string ends with a new line
|
||||
indented = append(indented, line)
|
||||
} else {
|
||||
indented = append(indented, indent+line)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(indented, "\n")
|
||||
}
|
||||
|
||||
//
|
||||
// Functions
|
||||
//
|
||||
|
||||
// wasFuncCall returns true if given expression was a function call
|
||||
func (v *evalVisitor) wasFuncCall(node *ast.Expression) bool {
|
||||
// check if expression was tagged as a function call
|
||||
return v.exprFunc[node]
|
||||
}
|
||||
|
||||
//
|
||||
// Visitor interface
|
||||
//
|
||||
|
||||
// Statements
|
||||
|
||||
// VisitProgram implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitProgram(node *ast.Program) interface{} {
|
||||
v.at(node)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
for _, n := range node.Body {
|
||||
if str := Str(n.Accept(v)); str != "" {
|
||||
if _, err := buf.Write([]byte(str)); err != nil {
|
||||
v.errPanic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// VisitMustache implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitMustache(node *ast.MustacheStatement) interface{} {
|
||||
v.at(node)
|
||||
|
||||
// evaluate expression
|
||||
expr := node.Expression.Accept(v)
|
||||
|
||||
// check if this is a safe string
|
||||
isSafe := isSafeString(expr)
|
||||
|
||||
// get string value
|
||||
str := Str(expr)
|
||||
if !isSafe && !node.Unescaped {
|
||||
// escape html
|
||||
str = Escape(str)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// VisitBlock implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitBlock(node *ast.BlockStatement) interface{} {
|
||||
v.at(node)
|
||||
|
||||
v.pushBlock(node)
|
||||
|
||||
var result interface{}
|
||||
|
||||
// evaluate expression
|
||||
expr := node.Expression.Accept(v)
|
||||
|
||||
if v.isHelperCall(node.Expression) || v.wasFuncCall(node.Expression) {
|
||||
// it is the responsability of the helper/function to evaluate block
|
||||
result = expr
|
||||
} else {
|
||||
val := reflect.ValueOf(expr)
|
||||
|
||||
truth, _ := isTrueValue(val)
|
||||
if truth {
|
||||
if node.Program != nil {
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
concat := ""
|
||||
|
||||
// Array context
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
// Computes new private data frame
|
||||
frame := v.dataFrame.newIterDataFrame(val.Len(), i, nil)
|
||||
|
||||
// Evaluate program
|
||||
concat += v.evalProgram(node.Program, val.Index(i).Interface(), frame, i)
|
||||
}
|
||||
|
||||
result = concat
|
||||
default:
|
||||
// NOT array
|
||||
result = v.evalProgram(node.Program, expr, nil, nil)
|
||||
}
|
||||
}
|
||||
} else if node.Inverse != nil {
|
||||
result, _ = node.Inverse.Accept(v).(string)
|
||||
}
|
||||
}
|
||||
|
||||
v.popBlock()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// VisitPartial implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitPartial(node *ast.PartialStatement) interface{} {
|
||||
v.at(node)
|
||||
|
||||
// partialName: helperName | sexpr
|
||||
name, ok := ast.HelperNameStr(node.Name)
|
||||
if !ok {
|
||||
if subExpr, ok := node.Name.(*ast.SubExpression); ok {
|
||||
name, _ = subExpr.Accept(v).(string)
|
||||
}
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
v.errorf("Unexpected partial name: %q", node.Name)
|
||||
}
|
||||
|
||||
partial := v.findPartial(name)
|
||||
if partial == nil {
|
||||
v.errorf("Partial not found: %s", name)
|
||||
}
|
||||
|
||||
return v.evalPartial(partial, node)
|
||||
}
|
||||
|
||||
// VisitContent implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitContent(node *ast.ContentStatement) interface{} {
|
||||
v.at(node)
|
||||
|
||||
// write content as is
|
||||
return node.Value
|
||||
}
|
||||
|
||||
// VisitComment implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitComment(node *ast.CommentStatement) interface{} {
|
||||
v.at(node)
|
||||
|
||||
// ignore comments
|
||||
return ""
|
||||
}
|
||||
|
||||
// Expressions
|
||||
|
||||
// VisitExpression implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitExpression(node *ast.Expression) interface{} {
|
||||
v.at(node)
|
||||
|
||||
var result interface{}
|
||||
done := false
|
||||
|
||||
v.pushExpr(node)
|
||||
|
||||
// helper call
|
||||
if helperName := node.HelperName(); helperName != "" {
|
||||
if helper := v.findHelper(helperName); helper != zero {
|
||||
result = v.callHelper(helperName, helper, node)
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
if !done {
|
||||
// literal
|
||||
if literal, ok := node.LiteralStr(); ok {
|
||||
if val := v.evalField(v.curCtx(), literal, true); val.IsValid() {
|
||||
result = val.Interface()
|
||||
done = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !done {
|
||||
// field path
|
||||
if path := node.FieldPath(); path != nil {
|
||||
// @todo Find a cleaner way ! Don't break the pattern !
|
||||
// this is an exception to visitor pattern, because we need to pass the info
|
||||
// that this path is at root of current expression
|
||||
if val := v.evalPathExpression(path, true); val != nil {
|
||||
result = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v.popExpr()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// VisitSubExpression implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitSubExpression(node *ast.SubExpression) interface{} {
|
||||
v.at(node)
|
||||
|
||||
return node.Expression.Accept(v)
|
||||
}
|
||||
|
||||
// VisitPath implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitPath(node *ast.PathExpression) interface{} {
|
||||
return v.evalPathExpression(node, false)
|
||||
}
|
||||
|
||||
// Literals
|
||||
|
||||
// VisitString implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitString(node *ast.StringLiteral) interface{} {
|
||||
v.at(node)
|
||||
|
||||
return node.Value
|
||||
}
|
||||
|
||||
// VisitBoolean implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitBoolean(node *ast.BooleanLiteral) interface{} {
|
||||
v.at(node)
|
||||
|
||||
return node.Value
|
||||
}
|
||||
|
||||
// VisitNumber implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitNumber(node *ast.NumberLiteral) interface{} {
|
||||
v.at(node)
|
||||
|
||||
return node.Number()
|
||||
}
|
||||
|
||||
// Miscellaneous
|
||||
|
||||
// VisitHash implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitHash(node *ast.Hash) interface{} {
|
||||
v.at(node)
|
||||
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for _, pair := range node.Pairs {
|
||||
if value := pair.Accept(v); value != nil {
|
||||
result[pair.Key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// VisitHashPair implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitHashPair(node *ast.HashPair) interface{} {
|
||||
v.at(node)
|
||||
|
||||
return node.Val.Accept(v)
|
||||
}
|
|
@ -1,215 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import "testing"
|
||||
|
||||
var evalTests = []Test{
|
||||
{
|
||||
"only content",
|
||||
"this is content",
|
||||
nil, nil, nil, nil,
|
||||
"this is content",
|
||||
},
|
||||
{
|
||||
"checks path in parent contexts",
|
||||
"{{#a}}{{one}}{{#b}}{{one}}{{two}}{{one}}{{/b}}{{/a}}",
|
||||
map[string]interface{}{"a": map[string]int{"one": 1}, "b": map[string]int{"two": 2}},
|
||||
nil, nil, nil,
|
||||
"1121",
|
||||
},
|
||||
{
|
||||
"block params",
|
||||
"{{#foo as |bar|}}{{bar}}{{/foo}}{{bar}}",
|
||||
map[string]string{"foo": "baz", "bar": "bat"},
|
||||
nil, nil, nil,
|
||||
"bazbat",
|
||||
},
|
||||
{
|
||||
"block params on array",
|
||||
"{{#foo as |bar i|}}{{i}}.{{bar}} {{/foo}}",
|
||||
map[string][]string{"foo": {"baz", "bar", "bat"}},
|
||||
nil, nil, nil,
|
||||
"0.baz 1.bar 2.bat ",
|
||||
},
|
||||
{
|
||||
"nested block params",
|
||||
"{{#foos as |foo iFoo|}}{{#wats as |wat iWat|}}{{iFoo}}.{{iWat}}.{{foo}}-{{wat}} {{/wats}}{{/foos}}",
|
||||
map[string][]string{"foos": {"baz", "bar"}, "wats": {"the", "phoque"}},
|
||||
nil, nil, nil,
|
||||
"0.0.baz-the 0.1.baz-phoque 1.0.bar-the 1.1.bar-phoque ",
|
||||
},
|
||||
{
|
||||
"block params with path reference",
|
||||
"{{#foo as |bar|}}{{bar.baz}}{{/foo}}",
|
||||
map[string]map[string]string{"foo": {"baz": "bat"}},
|
||||
nil, nil, nil,
|
||||
"bat",
|
||||
},
|
||||
{
|
||||
"falsy block evaluation",
|
||||
"{{#foo}}bar{{/foo}} baz",
|
||||
map[string]interface{}{"foo": false},
|
||||
nil, nil, nil,
|
||||
" baz",
|
||||
},
|
||||
{
|
||||
"block helper returns a SafeString",
|
||||
"{{title}} - {{#bold}}{{body}}{{/bold}}",
|
||||
map[string]string{
|
||||
"title": "My new blog post",
|
||||
"body": "I have so many things to say!",
|
||||
},
|
||||
nil,
|
||||
map[string]interface{}{"bold": func(options *Options) SafeString {
|
||||
return SafeString(`<div class="mybold">` + options.Fn() + "</div>")
|
||||
}},
|
||||
nil,
|
||||
`My new blog post - <div class="mybold">I have so many things to say!</div>`,
|
||||
},
|
||||
{
|
||||
"chained blocks",
|
||||
"{{#if a}}A{{else if b}}B{{else}}C{{/if}}",
|
||||
map[string]interface{}{"b": false},
|
||||
nil, nil, nil,
|
||||
"C",
|
||||
},
|
||||
|
||||
// @todo Test with a "../../path" (depth 2 path) while context is only depth 1
|
||||
}
|
||||
|
||||
func TestEval(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
launchTests(t, evalTests)
|
||||
}
|
||||
|
||||
var evalErrors = []Test{
|
||||
{
|
||||
"functions with wrong number of arguments",
|
||||
`{{foo "bar"}}`,
|
||||
map[string]interface{}{"foo": func(a string, b string) string { return "foo" }},
|
||||
nil, nil, nil,
|
||||
"Helper 'foo' called with wrong number of arguments, needed 2 but got 1",
|
||||
},
|
||||
{
|
||||
"functions with wrong number of returned values (1)",
|
||||
"{{foo}}",
|
||||
map[string]interface{}{"foo": func() {}},
|
||||
nil, nil, nil,
|
||||
"Helper function must return a string or a SafeString",
|
||||
},
|
||||
{
|
||||
"functions with wrong number of returned values (2)",
|
||||
"{{foo}}",
|
||||
map[string]interface{}{"foo": func() (string, bool, string) { return "foo", true, "bar" }},
|
||||
nil, nil, nil,
|
||||
"Helper function must return a string or a SafeString",
|
||||
},
|
||||
}
|
||||
|
||||
func TestEvalErrors(t *testing.T) {
|
||||
launchErrorTests(t, evalErrors)
|
||||
}
|
||||
|
||||
func TestEvalStruct(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
source := `<div class="post">
|
||||
<h1>By {{author.FirstName}} {{Author.lastName}}</h1>
|
||||
<div class="body">{{Body}}</div>
|
||||
|
||||
<h1>Comments</h1>
|
||||
|
||||
{{#each comments}}
|
||||
<h2>By {{Author.FirstName}} {{author.LastName}}</h2>
|
||||
<div class="body">{{body}}</div>
|
||||
{{/each}}
|
||||
</div>`
|
||||
|
||||
expected := `<div class="post">
|
||||
<h1>By Jean Valjean</h1>
|
||||
<div class="body">Life is difficult</div>
|
||||
|
||||
<h1>Comments</h1>
|
||||
|
||||
<h2>By Marcel Beliveau</h2>
|
||||
<div class="body">LOL!</div>
|
||||
</div>`
|
||||
|
||||
type Person struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
Author Person
|
||||
Body string
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
Author Person
|
||||
Body string
|
||||
Comments []Comment
|
||||
}
|
||||
|
||||
ctx := Post{
|
||||
Person{"Jean", "Valjean"},
|
||||
"Life is difficult",
|
||||
[]Comment{
|
||||
Comment{
|
||||
Person{"Marcel", "Beliveau"},
|
||||
"LOL!",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
output := MustRender(source, ctx)
|
||||
if output != expected {
|
||||
t.Errorf("Failed to evaluate with struct context")
|
||||
}
|
||||
}
|
||||
|
||||
type TestFoo struct {
|
||||
}
|
||||
|
||||
func (t *TestFoo) Subject() string {
|
||||
return "foo"
|
||||
}
|
||||
|
||||
func TestEvalMethod(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
source := `Subject is {{subject}}! YES I SAID {{Subject}}!`
|
||||
expected := `Subject is foo! YES I SAID foo!`
|
||||
|
||||
ctx := &TestFoo{}
|
||||
|
||||
output := MustRender(source, ctx)
|
||||
if output != expected {
|
||||
t.Errorf("Failed to evaluate struct method: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
type TestBar struct {
|
||||
}
|
||||
|
||||
func (t *TestBar) Subject() interface{} {
|
||||
return testBar
|
||||
}
|
||||
|
||||
func testBar() string {
|
||||
return "bar"
|
||||
}
|
||||
|
||||
func TestEvalMethodReturningFunc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
source := `Subject is {{subject}}! YES I SAID {{Subject}}!`
|
||||
expected := `Subject is bar! YES I SAID bar!`
|
||||
|
||||
ctx := &TestBar{}
|
||||
|
||||
output := MustRender(source, ctx)
|
||||
if output != expected {
|
||||
t.Errorf("Failed to evaluate struct method: %s", output)
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
package handlebars
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
)
|
||||
|
||||
// cf. https://github.com/aymerick/go-fuzz-tests/raymond
|
||||
const DUMP_TPL = false
|
||||
|
||||
var dump_tpl_nb = 0
|
||||
|
||||
type Test struct {
|
||||
name string
|
||||
input string
|
||||
data interface{}
|
||||
privData map[string]interface{}
|
||||
helpers map[string]interface{}
|
||||
partials map[string]string
|
||||
output interface{}
|
||||
}
|
||||
|
||||
func launchTests(t *testing.T, tests []Test) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range tests {
|
||||
var err error
|
||||
var tpl *raymond.Template
|
||||
|
||||
if DUMP_TPL {
|
||||
filename := strconv.Itoa(dump_tpl_nb)
|
||||
if err := ioutil.WriteFile(path.Join(".", "dump_tpl", filename), []byte(test.input), 0644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
dump_tpl_nb += 1
|
||||
}
|
||||
|
||||
// parse template
|
||||
tpl, err = raymond.Parse(test.input)
|
||||
if err != nil {
|
||||
t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err)
|
||||
} else {
|
||||
if len(test.helpers) > 0 {
|
||||
// register helpers
|
||||
tpl.RegisterHelpers(test.helpers)
|
||||
}
|
||||
|
||||
if len(test.partials) > 0 {
|
||||
// register partials
|
||||
tpl.RegisterPartials(test.partials)
|
||||
}
|
||||
|
||||
// setup private data frame
|
||||
var privData *raymond.DataFrame
|
||||
if test.privData != nil {
|
||||
privData = raymond.NewDataFrame()
|
||||
for k, v := range test.privData {
|
||||
privData.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// render template
|
||||
output, err := tpl.ExecWith(test.data, privData)
|
||||
if err != nil {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\nerror:\n\t%s\nAST:\n\t%s", test.name, test.input, raymond.Str(test.data), err, tpl.PrintAST())
|
||||
} else {
|
||||
// check output
|
||||
var expectedArr []string
|
||||
expectedArr, ok := test.output.([]string)
|
||||
if ok {
|
||||
match := false
|
||||
for _, expectedStr := range expectedArr {
|
||||
if expectedStr == output {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !match {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, raymond.Str(test.data), raymond.Str(test.partials), expectedArr, output, tpl.PrintAST())
|
||||
}
|
||||
} else {
|
||||
expectedStr, ok := test.output.(string)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("Erroneous test output description: %q", test.output))
|
||||
}
|
||||
|
||||
if expectedStr != output {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, raymond.Str(test.data), raymond.Str(test.partials), expectedStr, output, tpl.PrintAST())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,651 +0,0 @@
|
|||
package handlebars
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
)
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/basic.js
|
||||
//
|
||||
var basicTests = []Test{
|
||||
{
|
||||
"most basic",
|
||||
"{{foo}}",
|
||||
map[string]string{"foo": "foo"},
|
||||
nil, nil, nil,
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
"escaping (1)",
|
||||
"\\{{foo}}",
|
||||
map[string]string{"foo": "food"},
|
||||
nil, nil, nil,
|
||||
"{{foo}}",
|
||||
},
|
||||
{
|
||||
"escaping (2)",
|
||||
"content \\{{foo}}",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"content {{foo}}",
|
||||
},
|
||||
{
|
||||
"escaping (3)",
|
||||
"\\\\{{foo}}",
|
||||
map[string]string{"foo": "food"},
|
||||
nil, nil, nil,
|
||||
"\\food",
|
||||
},
|
||||
{
|
||||
"escaping (4)",
|
||||
"content \\\\{{foo}}",
|
||||
map[string]string{"foo": "food"},
|
||||
nil, nil, nil,
|
||||
"content \\food",
|
||||
},
|
||||
{
|
||||
"escaping (5)",
|
||||
"\\\\ {{foo}}",
|
||||
map[string]string{"foo": "food"},
|
||||
nil, nil, nil,
|
||||
"\\\\ food",
|
||||
},
|
||||
{
|
||||
"compiling with a basic context",
|
||||
"Goodbye\n{{cruel}}\n{{world}}!",
|
||||
map[string]string{"cruel": "cruel", "world": "world"},
|
||||
nil, nil, nil,
|
||||
"Goodbye\ncruel\nworld!",
|
||||
},
|
||||
{
|
||||
"compiling with an undefined context (1)",
|
||||
"Goodbye\n{{cruel}}\n{{world.bar}}!",
|
||||
nil, nil, nil, nil,
|
||||
"Goodbye\n\n!",
|
||||
},
|
||||
{
|
||||
"compiling with an undefined context (2)",
|
||||
"{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}",
|
||||
nil, nil, nil, nil,
|
||||
"Goodbye",
|
||||
},
|
||||
{
|
||||
"comments (1)",
|
||||
"{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!",
|
||||
map[string]string{"cruel": "cruel", "world": "world"},
|
||||
nil, nil, nil,
|
||||
"Goodbye\ncruel\nworld!",
|
||||
},
|
||||
{
|
||||
"comments (2)",
|
||||
" {{~! comment ~}} blah",
|
||||
nil, nil, nil, nil,
|
||||
"blah",
|
||||
},
|
||||
{
|
||||
"comments (3)",
|
||||
" {{~!-- long-comment --~}} blah",
|
||||
nil, nil, nil, nil,
|
||||
"blah",
|
||||
},
|
||||
{
|
||||
"comments (4)",
|
||||
" {{! comment ~}} blah",
|
||||
nil, nil, nil, nil,
|
||||
" blah",
|
||||
},
|
||||
{
|
||||
"comments (5)",
|
||||
" {{!-- long-comment --~}} blah",
|
||||
nil, nil, nil, nil,
|
||||
" blah",
|
||||
},
|
||||
{
|
||||
"comments (6)",
|
||||
" {{~! comment}} blah",
|
||||
nil, nil, nil, nil,
|
||||
" blah",
|
||||
},
|
||||
{
|
||||
"comments (7)",
|
||||
" {{~!-- long-comment --}} blah",
|
||||
nil, nil, nil, nil,
|
||||
" blah",
|
||||
},
|
||||
{
|
||||
"boolean (1)",
|
||||
"{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": true, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"boolean (2)",
|
||||
"{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": false, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"zeros (1)",
|
||||
"num1: {{num1}}, num2: {{num2}}",
|
||||
map[string]interface{}{"num1": 42, "num2": 0},
|
||||
nil, nil, nil,
|
||||
"num1: 42, num2: 0",
|
||||
},
|
||||
{
|
||||
"zeros (2)",
|
||||
"num: {{.}}",
|
||||
0,
|
||||
nil, nil, nil,
|
||||
"num: 0",
|
||||
},
|
||||
{
|
||||
"zeros (3)",
|
||||
"num: {{num1/num2}}",
|
||||
map[string]map[string]interface{}{"num1": {"num2": 0}},
|
||||
nil, nil, nil,
|
||||
"num: 0",
|
||||
},
|
||||
{
|
||||
"false (1)",
|
||||
"val1: {{val1}}, val2: {{val2}}",
|
||||
map[string]interface{}{"val1": false, "val2": false},
|
||||
nil, nil, nil,
|
||||
"val1: false, val2: false",
|
||||
},
|
||||
{
|
||||
"false (2)",
|
||||
"val: {{.}}",
|
||||
false,
|
||||
nil, nil, nil,
|
||||
"val: false",
|
||||
},
|
||||
{
|
||||
"false (3)",
|
||||
"val: {{val1/val2}}",
|
||||
map[string]map[string]interface{}{"val1": {"val2": false}},
|
||||
nil, nil, nil,
|
||||
"val: false",
|
||||
},
|
||||
{
|
||||
"false (4)",
|
||||
"val1: {{{val1}}}, val2: {{{val2}}}",
|
||||
map[string]interface{}{"val1": false, "val2": false},
|
||||
nil, nil, nil,
|
||||
"val1: false, val2: false",
|
||||
},
|
||||
{
|
||||
"false (5)",
|
||||
"val: {{{val1/val2}}}",
|
||||
map[string]map[string]interface{}{"val1": {"val2": false}},
|
||||
nil, nil, nil,
|
||||
"val: false",
|
||||
},
|
||||
{
|
||||
"newlines (1)",
|
||||
"Alan's\nTest",
|
||||
nil, nil, nil, nil,
|
||||
"Alan's\nTest",
|
||||
},
|
||||
{
|
||||
"newlines (2)",
|
||||
"Alan's\rTest",
|
||||
nil, nil, nil, nil,
|
||||
"Alan's\rTest",
|
||||
},
|
||||
{
|
||||
"escaping text (1)",
|
||||
"Awesome's",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"Awesome's",
|
||||
},
|
||||
{
|
||||
"escaping text (2)",
|
||||
"Awesome\\",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"Awesome\\",
|
||||
},
|
||||
{
|
||||
"escaping text (3)",
|
||||
"Awesome\\\\ foo",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"Awesome\\\\ foo",
|
||||
},
|
||||
{
|
||||
"escaping text (4)",
|
||||
"Awesome {{foo}}",
|
||||
map[string]string{"foo": "\\"},
|
||||
nil, nil, nil,
|
||||
"Awesome \\",
|
||||
},
|
||||
{
|
||||
"escaping text (5)",
|
||||
" ' ' ",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
" ' ' ",
|
||||
},
|
||||
{
|
||||
"escaping expressions (6)",
|
||||
"{{{awesome}}}",
|
||||
map[string]string{"awesome": "&'\\<>"},
|
||||
nil, nil, nil,
|
||||
"&'\\<>",
|
||||
},
|
||||
{
|
||||
"escaping expressions (7)",
|
||||
"{{&awesome}}",
|
||||
map[string]string{"awesome": "&'\\<>"},
|
||||
nil, nil, nil,
|
||||
"&'\\<>",
|
||||
},
|
||||
{
|
||||
"escaping expressions (8)",
|
||||
"{{awesome}}",
|
||||
map[string]string{"awesome": "&\"'`\\<>"},
|
||||
nil, nil, nil,
|
||||
"&"'`\\<>",
|
||||
},
|
||||
{
|
||||
"escaping expressions (9)",
|
||||
"{{awesome}}",
|
||||
map[string]string{"awesome": "Escaped, <b> looks like: <b>"},
|
||||
nil, nil, nil,
|
||||
"Escaped, <b> looks like: &lt;b&gt;",
|
||||
},
|
||||
{
|
||||
"functions returning safestrings shouldn't be escaped",
|
||||
"{{awesome}}",
|
||||
map[string]interface{}{"awesome": func() raymond.SafeString { return raymond.SafeString("&'\\<>") }},
|
||||
nil, nil, nil,
|
||||
"&'\\<>",
|
||||
},
|
||||
{
|
||||
"functions (1)",
|
||||
"{{awesome}}",
|
||||
map[string]interface{}{"awesome": func() string { return "Awesome" }},
|
||||
nil, nil, nil,
|
||||
"Awesome",
|
||||
},
|
||||
{
|
||||
"functions (2)",
|
||||
"{{awesome}}",
|
||||
map[string]interface{}{"awesome": func(options *raymond.Options) string {
|
||||
return options.ValueStr("more")
|
||||
}, "more": "More awesome"},
|
||||
nil, nil, nil,
|
||||
"More awesome",
|
||||
},
|
||||
{
|
||||
"functions with context argument",
|
||||
"{{awesome frank}}",
|
||||
map[string]interface{}{"awesome": func(context string) string {
|
||||
return context
|
||||
}, "frank": "Frank"},
|
||||
nil, nil, nil,
|
||||
"Frank",
|
||||
},
|
||||
{
|
||||
"pathed functions with context argument",
|
||||
"{{bar.awesome frank}}",
|
||||
map[string]interface{}{"bar": map[string]interface{}{"awesome": func(context string) string {
|
||||
return context
|
||||
}}, "frank": "Frank"},
|
||||
nil, nil, nil,
|
||||
"Frank",
|
||||
},
|
||||
{
|
||||
"depthed functions with context argument",
|
||||
"{{#with frank}}{{../awesome .}}{{/with}}",
|
||||
map[string]interface{}{"awesome": func(context string) string {
|
||||
return context
|
||||
}, "frank": "Frank"},
|
||||
nil, nil, nil,
|
||||
"Frank",
|
||||
},
|
||||
{
|
||||
"block functions with context argument",
|
||||
"{{#awesome 1}}inner {{.}}{{/awesome}}",
|
||||
map[string]interface{}{"awesome": func(context interface{}, options *raymond.Options) string {
|
||||
return options.FnWith(context)
|
||||
}},
|
||||
nil, nil, nil,
|
||||
"inner 1",
|
||||
},
|
||||
{
|
||||
"depthed block functions with context argument",
|
||||
"{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}",
|
||||
map[string]interface{}{
|
||||
"awesome": func(context interface{}, options *raymond.Options) string {
|
||||
return options.FnWith(context)
|
||||
},
|
||||
"value": true,
|
||||
},
|
||||
nil, nil, nil,
|
||||
"inner 1",
|
||||
},
|
||||
{
|
||||
"block functions without context argument",
|
||||
"{{#awesome}}inner{{/awesome}}",
|
||||
map[string]interface{}{
|
||||
"awesome": func(options *raymond.Options) string {
|
||||
return options.Fn()
|
||||
},
|
||||
},
|
||||
nil, nil, nil,
|
||||
"inner",
|
||||
},
|
||||
// // @note I don't even understand why this test passes with the JS implementation... it should be
|
||||
// // the responsability of the function to evaluate the block
|
||||
// {
|
||||
// "pathed block functions without context argument",
|
||||
// "{{#foo.awesome}}inner{{/foo.awesome}}",
|
||||
// map[string]map[string]interface{}{
|
||||
// "foo": {
|
||||
// "awesome": func(options *raymond.Options) interface{} {
|
||||
// return options.Ctx()
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// nil, nil, nil,
|
||||
// "inner",
|
||||
// },
|
||||
// // @note I don't even understand why this test passes with the JS implementation... it should be
|
||||
// // the responsability of the function to evaluate the block
|
||||
// {
|
||||
// "depthed block functions without context argument",
|
||||
// "{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}",
|
||||
// map[string]interface{}{
|
||||
// "value": true,
|
||||
// "awesome": func(options *raymond.Options) interface{} {
|
||||
// return options.Ctx()
|
||||
// },
|
||||
// },
|
||||
// nil, nil, nil,
|
||||
// "inner",
|
||||
// },
|
||||
{
|
||||
"paths with hyphens (1)",
|
||||
"{{foo-bar}}",
|
||||
map[string]string{"foo-bar": "baz"},
|
||||
nil, nil, nil,
|
||||
"baz",
|
||||
},
|
||||
{
|
||||
"paths with hyphens (2)",
|
||||
"{{foo.foo-bar}}",
|
||||
map[string]map[string]string{"foo": {"foo-bar": "baz"}},
|
||||
nil, nil, nil,
|
||||
"baz",
|
||||
},
|
||||
{
|
||||
"paths with hyphens (3)",
|
||||
"{{foo/foo-bar}}",
|
||||
map[string]map[string]string{"foo": {"foo-bar": "baz"}},
|
||||
nil, nil, nil,
|
||||
"baz",
|
||||
},
|
||||
{
|
||||
"nested paths",
|
||||
"Goodbye {{alan/expression}} world!",
|
||||
map[string]map[string]string{"alan": {"expression": "beautiful"}},
|
||||
nil, nil, nil,
|
||||
"Goodbye beautiful world!",
|
||||
},
|
||||
{
|
||||
"nested paths with empty string value",
|
||||
"Goodbye {{alan/expression}} world!",
|
||||
map[string]map[string]string{"alan": {"expression": ""}},
|
||||
nil, nil, nil,
|
||||
"Goodbye world!",
|
||||
},
|
||||
{
|
||||
"literal paths (1)",
|
||||
"Goodbye {{[@alan]/expression}} world!",
|
||||
map[string]map[string]string{"@alan": {"expression": "beautiful"}},
|
||||
nil, nil, nil,
|
||||
"Goodbye beautiful world!",
|
||||
},
|
||||
{
|
||||
"literal paths (2)",
|
||||
"Goodbye {{[foo bar]/expression}} world!",
|
||||
map[string]map[string]string{"foo bar": {"expression": "beautiful"}},
|
||||
nil, nil, nil,
|
||||
"Goodbye beautiful world!",
|
||||
},
|
||||
{
|
||||
"literal references",
|
||||
"Goodbye {{[foo bar]}} world!",
|
||||
map[string]string{"foo bar": "beautiful"},
|
||||
nil, nil, nil,
|
||||
"Goodbye beautiful world!",
|
||||
},
|
||||
// @note MMm ok, well... no... I don't see the purpose of that test
|
||||
{
|
||||
"that current context path ({{.}}) doesn't hit helpers",
|
||||
"test: {{.}}",
|
||||
nil, nil,
|
||||
map[string]interface{}{"helper": func() string {
|
||||
panic("fail")
|
||||
return ""
|
||||
}},
|
||||
nil,
|
||||
"test: ",
|
||||
},
|
||||
{
|
||||
"complex but empty paths (1)",
|
||||
"{{person/name}}",
|
||||
map[string]map[string]interface{}{"person": {"name": nil}},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"complex but empty paths (2)",
|
||||
"{{person/name}}",
|
||||
map[string]map[string]string{"person": {}},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"this keyword in paths (1)",
|
||||
"{{#goodbyes}}{{this}}{{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": []string{"goodbye", "Goodbye", "GOODBYE"}},
|
||||
nil, nil, nil,
|
||||
"goodbyeGoodbyeGOODBYE",
|
||||
},
|
||||
{
|
||||
"this keyword in paths (2)",
|
||||
"{{#hellos}}{{this/text}}{{/hellos}}",
|
||||
map[string]interface{}{"hellos": []interface{}{
|
||||
map[string]string{"text": "hello"},
|
||||
map[string]string{"text": "Hello"},
|
||||
map[string]string{"text": "HELLO"},
|
||||
}},
|
||||
nil, nil, nil,
|
||||
"helloHelloHELLO",
|
||||
},
|
||||
{
|
||||
"this keyword nested inside path' (1)",
|
||||
"{{[this]}}",
|
||||
map[string]string{"this": "bar"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"this keyword nested inside path' (2)",
|
||||
"{{text/[this]}}",
|
||||
map[string]map[string]string{"text": {"this": "bar"}},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"this keyword in helpers (1)",
|
||||
"{{#goodbyes}}{{foo this}}{{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": []string{"goodbye", "Goodbye", "GOODBYE"}},
|
||||
nil,
|
||||
map[string]interface{}{"foo": barSuffixHelper},
|
||||
nil,
|
||||
"bar goodbyebar Goodbyebar GOODBYE",
|
||||
},
|
||||
{
|
||||
"this keyword in helpers (2)",
|
||||
"{{#hellos}}{{foo this/text}}{{/hellos}}",
|
||||
map[string]interface{}{"hellos": []map[string]string{{"text": "hello"}, {"text": "Hello"}, {"text": "HELLO"}}},
|
||||
nil,
|
||||
map[string]interface{}{"foo": barSuffixHelper},
|
||||
nil,
|
||||
"bar hellobar Hellobar HELLO",
|
||||
},
|
||||
{
|
||||
"this keyword nested inside helpers param (1)",
|
||||
"{{foo [this]}}",
|
||||
map[string]interface{}{"this": "bar"},
|
||||
nil,
|
||||
map[string]interface{}{"foo": echoHelper},
|
||||
nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"this keyword nested inside helpers param (2)",
|
||||
"{{foo text/[this]}}",
|
||||
map[string]map[string]string{"text": {"this": "bar"}},
|
||||
nil,
|
||||
map[string]interface{}{"foo": echoHelper},
|
||||
nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"pass string literals (1)",
|
||||
`{{"foo"}}`,
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"pass string literals (2)",
|
||||
`{{"foo"}}`,
|
||||
map[string]string{"foo": "bar"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"pass string literals (3)",
|
||||
`{{#"foo"}}{{.}}{{/"foo"}}`,
|
||||
map[string]interface{}{"foo": []string{"bar", "baz"}},
|
||||
nil, nil, nil,
|
||||
"barbaz",
|
||||
},
|
||||
{
|
||||
"pass number literals (1)",
|
||||
"{{12}}",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"pass number literals (2)",
|
||||
"{{12}}",
|
||||
map[string]string{"12": "bar"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"pass number literals (3)",
|
||||
"{{12.34}}",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"pass number literals (4)",
|
||||
"{{12.34}}",
|
||||
map[string]string{"12.34": "bar"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"pass number literals (5)",
|
||||
"{{12.34 1}}",
|
||||
map[string]interface{}{"12.34": func(context string) string {
|
||||
return "bar" + context
|
||||
}},
|
||||
nil, nil, nil,
|
||||
"bar1",
|
||||
},
|
||||
{
|
||||
"pass boolean literals (1)",
|
||||
"{{true}}",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"pass boolean literals (2)",
|
||||
"{{true}}",
|
||||
map[string]string{"": "foo"},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"pass boolean literals (3)",
|
||||
"{{false}}",
|
||||
map[string]string{"false": "foo"},
|
||||
nil, nil, nil,
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
"should handle literals in subexpression",
|
||||
"{{foo (false)}}",
|
||||
map[string]interface{}{"false": func() string { return "bar" }},
|
||||
nil,
|
||||
map[string]interface{}{"foo": func(context string) string {
|
||||
return context
|
||||
}},
|
||||
nil,
|
||||
"bar",
|
||||
},
|
||||
}
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
launchTests(t, basicTests)
|
||||
}
|
||||
|
||||
func TestBasicErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
|
||||
inputs := []string{
|
||||
// this keyword nested inside path
|
||||
"{{#hellos}}{{text/this/foo}}{{/hellos}}",
|
||||
// this keyword nested inside helpers param
|
||||
"{{#hellos}}{{foo text/this/foo}}{{/hellos}}",
|
||||
}
|
||||
|
||||
expectedError := regexp.QuoteMeta("Invalid path: text/this")
|
||||
|
||||
for _, input := range inputs {
|
||||
_, err = raymond.Parse(input)
|
||||
if err == nil {
|
||||
t.Errorf("Test failed - Error expected")
|
||||
}
|
||||
|
||||
match, errMatch := regexp.MatchString(expectedError, fmt.Sprint(err))
|
||||
if errMatch != nil {
|
||||
panic("Failed to match regexp")
|
||||
}
|
||||
|
||||
if !match {
|
||||
t.Errorf("Test failed - Expected error:\n\t%s\n\nGot:\n\t%s", expectedError, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,208 +0,0 @@
|
|||
package handlebars
|
||||
|
||||
import "testing"
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/blocks.js
|
||||
//
|
||||
var blocksTests = []Test{
|
||||
{
|
||||
"array (1) - Arrays iterate over the contents when not empty",
|
||||
"{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"array (2) - Arrays ignore the contents when empty",
|
||||
"{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"array without data",
|
||||
"{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE",
|
||||
},
|
||||
{
|
||||
"array with @index - The @index variable is used",
|
||||
"{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"empty block (1) - Arrays iterate over the contents when not empty",
|
||||
"{{#goodbyes}}{{/goodbyes}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"empty block (1) - Arrays ignore the contents when empty",
|
||||
"{{#goodbyes}}{{/goodbyes}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"block with complex lookup - Templates can access variables in contexts up the stack with relative path syntax",
|
||||
"{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "name": "Alan"},
|
||||
nil, nil, nil,
|
||||
"goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ",
|
||||
},
|
||||
{
|
||||
"multiple blocks with complex lookup",
|
||||
"{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "name": "Alan"},
|
||||
nil, nil, nil,
|
||||
"AlanAlanAlanAlanAlanAlan",
|
||||
},
|
||||
|
||||
// @todo "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}" should throw error
|
||||
|
||||
{
|
||||
"block with deep nested complex lookup",
|
||||
"{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}",
|
||||
map[string]interface{}{"omg": "OMG!", "outer": []map[string]interface{}{{"sibling": "sad", "inner": []map[string]string{{"text": "goodbye"}}}}},
|
||||
nil, nil, nil,
|
||||
"Goodbye cruel sad OMG!",
|
||||
},
|
||||
{
|
||||
"inverted sections with unset value - Inverted section rendered when value isn't set.",
|
||||
"{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}",
|
||||
map[string]interface{}{},
|
||||
nil, nil, nil,
|
||||
"Right On!",
|
||||
},
|
||||
{
|
||||
"inverted sections with false value - Inverted section rendered when value is false.",
|
||||
"{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": false},
|
||||
nil, nil, nil,
|
||||
"Right On!",
|
||||
},
|
||||
{
|
||||
"inverted section with empty set - Inverted section rendered when value is empty set.",
|
||||
"{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": []interface{}{}},
|
||||
nil, nil, nil,
|
||||
"Right On!",
|
||||
},
|
||||
{
|
||||
"block inverted sections",
|
||||
"{{#people}}{{name}}{{^}}{{none}}{{/people}}",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people",
|
||||
},
|
||||
{
|
||||
"chained inverted sections (1)",
|
||||
"{{#people}}{{name}}{{else if none}}{{none}}{{/people}}",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people",
|
||||
},
|
||||
{
|
||||
"chained inverted sections (2)",
|
||||
"{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people",
|
||||
},
|
||||
{
|
||||
"chained inverted sections (3)",
|
||||
"{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people",
|
||||
},
|
||||
|
||||
// @todo "{{#people}}{{name}}{{else if none}}{{none}}{{/if}}" should throw error
|
||||
|
||||
{
|
||||
"block inverted sections with empty arrays",
|
||||
"{{#people}}{{name}}{{^}}{{none}}{{/people}}",
|
||||
map[string]interface{}{"none": "No people", "people": map[string]interface{}{}},
|
||||
nil, nil, nil,
|
||||
"No people",
|
||||
},
|
||||
{
|
||||
"block standalone else sections (1)",
|
||||
"{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people\n",
|
||||
},
|
||||
{
|
||||
"block standalone else sections (2)",
|
||||
"{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people\n",
|
||||
},
|
||||
{
|
||||
"block standalone else sections (3)",
|
||||
"{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people\n",
|
||||
},
|
||||
{
|
||||
"block standalone chained else sections (1)",
|
||||
"{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people\n",
|
||||
},
|
||||
{
|
||||
"block standalone chained else sections (2)",
|
||||
"{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people\n",
|
||||
},
|
||||
{
|
||||
"should handle nesting",
|
||||
"{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.",
|
||||
map[string]interface{}{"data": []int{1, 3, 5}},
|
||||
nil, nil, nil,
|
||||
"1\n3\n5\nOK.",
|
||||
},
|
||||
// // @todo compat mode
|
||||
// {
|
||||
// "block with deep recursive lookup lookup",
|
||||
// "{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}",
|
||||
// map[string]interface{}{"omg": "OMG!", "outer": []map[string]interface{}{{"inner": []map[string]string{{"text": "goodbye"}}}}},
|
||||
// nil,
|
||||
// nil,
|
||||
// nil,
|
||||
// "Goodbye cruel OMG!",
|
||||
// },
|
||||
// // @todo compat mode
|
||||
// {
|
||||
// "block with deep recursive pathed lookup",
|
||||
// "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}",
|
||||
// map[string]interface{}{"omg": map[string]string{"yes": "OMG!"}, "outer": []map[string]interface{}{{"inner": []map[string]string{{"yes": "no", "text": "goodbye"}}}}},
|
||||
// nil,
|
||||
// nil,
|
||||
// nil,
|
||||
// "Goodbye cruel OMG!",
|
||||
// },
|
||||
{
|
||||
"block with missed recursive lookup",
|
||||
"{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}",
|
||||
map[string]interface{}{"omg": map[string]string{"no": "OMG!"}, "outer": []map[string]interface{}{{"inner": []map[string]string{{"yes": "no", "text": "goodbye"}}}}},
|
||||
nil, nil, nil,
|
||||
"Goodbye cruel ",
|
||||
},
|
||||
}
|
||||
|
||||
func TestBlocks(t *testing.T) {
|
||||
launchTests(t, blocksTests)
|
||||
}
|
|
@ -1,341 +0,0 @@
|
|||
package handlebars
|
||||
|
||||
import "testing"
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/builtin.js
|
||||
//
|
||||
var builtinsTests = []Test{
|
||||
{
|
||||
"#if - if with boolean argument shows the contents when true",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": true, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with string argument shows the contents",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": "dummy", "world": "world"},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with boolean argument does not show the contents when false",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": false, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with undefined does not show the contents",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with non-empty array shows the contents",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": []string{"foo"}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with empty array does not show the contents",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": []string{}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with zero does not show the contents",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": 0, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with zero and includeZero option shows the contents",
|
||||
"{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": 0, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with function shows the contents when function returns true",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{
|
||||
"goodbye": func() bool { return true },
|
||||
"world": "world",
|
||||
},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with function shows the contents when function returns string",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{
|
||||
"goodbye": func() string { return "world" },
|
||||
"world": "world",
|
||||
},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with function does not show the contents when returns false",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{
|
||||
"goodbye": func() bool { return false },
|
||||
"world": "world",
|
||||
},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with function does not show the contents when returns undefined",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{
|
||||
"goodbye": func() interface{} { return nil },
|
||||
"world": "world",
|
||||
},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#with",
|
||||
"{{#with person}}{{first}} {{last}}{{/with}}",
|
||||
map[string]interface{}{"person": map[string]string{"first": "Alan", "last": "Johnson"}},
|
||||
nil, nil, nil,
|
||||
"Alan Johnson",
|
||||
},
|
||||
{
|
||||
"#with - with with function argument",
|
||||
"{{#with person}}{{first}} {{last}}{{/with}}",
|
||||
map[string]interface{}{
|
||||
"person": func() map[string]string { return map[string]string{"first": "Alan", "last": "Johnson"} },
|
||||
}, nil, nil, nil,
|
||||
"Alan Johnson",
|
||||
},
|
||||
{
|
||||
"#with - with with else",
|
||||
"{{#with person}}Person is present{{else}}Person is not present{{/with}}",
|
||||
map[string]interface{}{},
|
||||
nil, nil, nil,
|
||||
"Person is not present",
|
||||
},
|
||||
|
||||
{
|
||||
"#each - each with array argument iterates over the contents when not empty",
|
||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - each with array argument ignores the contents when empty",
|
||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - each without data (1)",
|
||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - each without data (2)",
|
||||
"{{#each .}}{{.}}{{/each}}",
|
||||
map[string]interface{}{"goodbyes": "cruel", "world": "world"},
|
||||
nil, nil, nil,
|
||||
// note: a go hash is not ordered, so result may vary, this behaviour differs from the JS implementation
|
||||
[]string{"cruelworld", "worldcruel"},
|
||||
},
|
||||
{
|
||||
"#each - each without context",
|
||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
||||
nil, nil, nil, nil,
|
||||
"cruel !",
|
||||
},
|
||||
|
||||
// NOTE: we test with a map instead of an object
|
||||
{
|
||||
"#each - each with an object and @key (map)",
|
||||
"{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": map[interface{}]map[string]string{"<b>#1</b>": {"text": "goodbye"}, 2: {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
[]string{"<b>#1</b>. goodbye! 2. GOODBYE! cruel world!", "2. GOODBYE! <b>#1</b>. goodbye! cruel world!"},
|
||||
},
|
||||
// NOTE: An additional test with a struct, but without an html stuff for the key, because it is impossible
|
||||
{
|
||||
"#each - each with an object and @key (struct)",
|
||||
"{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{
|
||||
"goodbyes": struct {
|
||||
Foo map[string]string
|
||||
Bar map[string]int
|
||||
}{map[string]string{"text": "baz"}, map[string]int{"text": 10}},
|
||||
"world": "world",
|
||||
},
|
||||
nil, nil, nil,
|
||||
[]string{"Foo. baz! Bar. 10! cruel world!", "Bar. 10! Foo. baz! cruel world!"},
|
||||
},
|
||||
{
|
||||
"#each - each with @index",
|
||||
"{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - each with nested @index",
|
||||
"{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - each with block params",
|
||||
"{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!",
|
||||
},
|
||||
// @note: That test differs from JS impl because maps and structs are not ordered in go
|
||||
{
|
||||
"#each - each object with @index",
|
||||
"{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": map[string]map[string]string{"a": {"text": "goodbye"}, "b": {"text": "Goodbye"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
[]string{"0. goodbye! 1. Goodbye! cruel world!", "0. Goodbye! 1. goodbye! cruel world!"},
|
||||
},
|
||||
{
|
||||
"#each - each with nested @first",
|
||||
"{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!",
|
||||
},
|
||||
// @note: That test differs from JS impl because maps and structs are not ordered in go
|
||||
{
|
||||
"#each - each object with @first",
|
||||
"{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": map[string]map[string]string{"foo": {"text": "goodbye"}, "bar": {"text": "Goodbye"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
[]string{"goodbye! cruel world!", "Goodbye! cruel world!"},
|
||||
},
|
||||
{
|
||||
"#each - each with @last",
|
||||
"{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"GOODBYE! cruel world!",
|
||||
},
|
||||
// @note: That test differs from JS impl because maps and structs are not ordered in go
|
||||
{
|
||||
"#each - each object with @last",
|
||||
"{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": map[string]map[string]string{"foo": {"text": "goodbye"}, "bar": {"text": "Goodbye"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
[]string{"goodbye! cruel world!", "Goodbye! cruel world!"},
|
||||
},
|
||||
{
|
||||
"#each - each with nested @last",
|
||||
"{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!",
|
||||
},
|
||||
|
||||
{
|
||||
"#each - each with function argument (1)",
|
||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": func() []map[string]string {
|
||||
return []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}
|
||||
}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - each with function argument (2)",
|
||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - data passed to helpers",
|
||||
"{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}",
|
||||
map[string][]string{"letters": {"a", "b", "c"}},
|
||||
map[string]interface{}{"exclaim": "!"},
|
||||
map[string]interface{}{"detectDataInsideEach": detectDataHelper},
|
||||
nil,
|
||||
"a!b!c!",
|
||||
},
|
||||
|
||||
// @todo "each on implicit context" should throw error
|
||||
|
||||
// SKIP: #log - "should call logger at default level"
|
||||
// SKIP: #log - "should call logger at data level"
|
||||
// SKIP: #log - "should output to info"
|
||||
// SKIP: #log - "should log at data level"
|
||||
// SKIP: #log - "should handle missing logger"
|
||||
|
||||
// @note Test added
|
||||
// @todo Check log output
|
||||
{
|
||||
"#log",
|
||||
"{{log blah}}",
|
||||
map[string]string{"blah": "whee"},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
|
||||
// @note Test added
|
||||
{
|
||||
"#lookup - should lookup array element",
|
||||
"{{#each goodbyes}}{{lookup ../data @index}}{{/each}}",
|
||||
map[string]interface{}{"goodbyes": []int{0, 1}, "data": []string{"foo", "bar"}},
|
||||
nil, nil, nil,
|
||||
"foobar",
|
||||
},
|
||||
{
|
||||
"#lookup - should lookup map element",
|
||||
"{{#each goodbyes}}{{lookup ../data .}}{{/each}}",
|
||||
map[string]interface{}{"goodbyes": []string{"foo", "bar"}, "data": map[string]string{"foo": "baz", "bar": "bat"}},
|
||||
nil, nil, nil,
|
||||
"bazbat",
|
||||
},
|
||||
{
|
||||
"#lookup - should lookup struct field",
|
||||
"{{#each goodbyes}}{{lookup ../data .}}{{/each}}",
|
||||
map[string]interface{}{"goodbyes": []string{"Foo", "Bar"}, "data": struct {
|
||||
Foo string
|
||||
Bar string
|
||||
}{"baz", "bat"}},
|
||||
nil, nil, nil,
|
||||
"bazbat",
|
||||
},
|
||||
{
|
||||
"#lookup - should lookup arbitrary content",
|
||||
"{{#each goodbyes}}{{lookup ../data .}}{{/each}}",
|
||||
map[string]interface{}{"goodbyes": []int{0, 1}, "data": []string{"foo", "bar"}},
|
||||
nil, nil, nil,
|
||||
"foobar",
|
||||
},
|
||||
{
|
||||
"#lookup - should not fail on undefined value",
|
||||
"{{#each goodbyes}}{{lookup ../bar .}}{{/each}}",
|
||||
map[string]interface{}{"goodbyes": []int{0, 1}, "data": []string{"foo", "bar"}},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
func TestBuiltins(t *testing.T) {
|
||||
launchTests(t, builtinsTests)
|
||||
}
|
|
@ -1,300 +0,0 @@
|
|||
package handlebars
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
)
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/data.js
|
||||
//
|
||||
var dataTests = []Test{
|
||||
{
|
||||
"passing in data to a compiled function that expects data - works with helpers",
|
||||
"{{hello}}",
|
||||
map[string]string{"noun": "cat"},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{"hello": func(options *raymond.Options) string {
|
||||
return options.DataStr("adjective") + " " + options.ValueStr("noun")
|
||||
}},
|
||||
nil,
|
||||
"happy cat",
|
||||
},
|
||||
{
|
||||
"data can be looked up via @foo",
|
||||
"{{@hello}}",
|
||||
nil,
|
||||
map[string]interface{}{"hello": "hello"},
|
||||
nil, nil,
|
||||
"hello",
|
||||
},
|
||||
{
|
||||
"deep @foo triggers automatic top-level data",
|
||||
`{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}`,
|
||||
map[string]bool{"foo": true},
|
||||
map[string]interface{}{"hello": "hello"},
|
||||
map[string]interface{}{"let": func(options *raymond.Options) string {
|
||||
frame := options.NewDataFrame()
|
||||
|
||||
for k, v := range options.Hash() {
|
||||
frame.Set(k, v)
|
||||
}
|
||||
|
||||
return options.FnData(frame)
|
||||
}},
|
||||
nil,
|
||||
"Hello world",
|
||||
},
|
||||
{
|
||||
"parameter data can be looked up via @foo",
|
||||
`{{hello @world}}`,
|
||||
nil,
|
||||
map[string]interface{}{"world": "world"},
|
||||
map[string]interface{}{"hello": func(context string) string {
|
||||
return "Hello " + context
|
||||
}},
|
||||
nil,
|
||||
"Hello world",
|
||||
},
|
||||
{
|
||||
"hash values can be looked up via @foo",
|
||||
`{{hello noun=@world}}`,
|
||||
nil,
|
||||
map[string]interface{}{"world": "world"},
|
||||
map[string]interface{}{"hello": func(options *raymond.Options) string {
|
||||
return "Hello " + options.HashStr("noun")
|
||||
}},
|
||||
nil,
|
||||
"Hello world",
|
||||
},
|
||||
{
|
||||
"nested parameter data can be looked up via @foo.bar",
|
||||
`{{hello @world.bar}}`,
|
||||
nil,
|
||||
map[string]interface{}{"world": map[string]string{"bar": "world"}},
|
||||
map[string]interface{}{"hello": func(context string) string {
|
||||
return "Hello " + context
|
||||
}},
|
||||
nil,
|
||||
"Hello world",
|
||||
},
|
||||
{
|
||||
"nested parameter data does not fail with @world.bar",
|
||||
`{{hello @world.bar}}`,
|
||||
nil,
|
||||
map[string]interface{}{"foo": map[string]string{"bar": "world"}},
|
||||
map[string]interface{}{"hello": func(context string) string {
|
||||
return "Hello " + context
|
||||
}},
|
||||
nil,
|
||||
// @todo Test differs with JS implementation: we don't output `undefined`
|
||||
"Hello ",
|
||||
},
|
||||
|
||||
// @todo "parameter data throws when using complex scope references",
|
||||
|
||||
{
|
||||
"data can be functions",
|
||||
`{{@hello}}`,
|
||||
nil,
|
||||
map[string]interface{}{"hello": func() string { return "hello" }},
|
||||
nil, nil,
|
||||
"hello",
|
||||
},
|
||||
{
|
||||
"data can be functions with params",
|
||||
`{{@hello "hello"}}`,
|
||||
nil,
|
||||
map[string]interface{}{"hello": func(context string) string { return context }},
|
||||
nil, nil,
|
||||
"hello",
|
||||
},
|
||||
|
||||
{
|
||||
"data is inherited downstream",
|
||||
`{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}`,
|
||||
map[string]map[string]string{"bar": {"baz": "hello world"}},
|
||||
nil,
|
||||
map[string]interface{}{"let": func(options *raymond.Options) string {
|
||||
frame := options.NewDataFrame()
|
||||
|
||||
for k, v := range options.Hash() {
|
||||
frame.Set(k, v)
|
||||
}
|
||||
|
||||
return options.FnData(frame)
|
||||
}},
|
||||
nil,
|
||||
"2hello world1",
|
||||
},
|
||||
{
|
||||
"passing in data to a compiled function that expects data - works with helpers in partials",
|
||||
`{{>myPartial}}`,
|
||||
map[string]string{"noun": "cat"},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{"hello": func(options *raymond.Options) string {
|
||||
return options.DataStr("adjective") + " " + options.ValueStr("noun")
|
||||
}},
|
||||
map[string]string{
|
||||
"myPartial": "{{hello}}",
|
||||
},
|
||||
"happy cat",
|
||||
},
|
||||
{
|
||||
"passing in data to a compiled function that expects data - works with helpers and parameters",
|
||||
`{{hello world}}`,
|
||||
map[string]interface{}{"exclaim": true, "world": "world"},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{"hello": func(context string, options *raymond.Options) string {
|
||||
str := "error"
|
||||
if b, ok := options.Value("exclaim").(bool); ok {
|
||||
if b {
|
||||
str = "!"
|
||||
} else {
|
||||
str = ""
|
||||
}
|
||||
}
|
||||
|
||||
return options.DataStr("adjective") + " " + context + str
|
||||
}},
|
||||
nil,
|
||||
"happy world!",
|
||||
},
|
||||
{
|
||||
"passing in data to a compiled function that expects data - works with block helpers",
|
||||
`{{#hello}}{{world}}{{/hello}}`,
|
||||
map[string]bool{"exclaim": true},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{
|
||||
"hello": func(options *raymond.Options) string {
|
||||
return options.Fn()
|
||||
},
|
||||
"world": func(options *raymond.Options) string {
|
||||
str := "error"
|
||||
if b, ok := options.Value("exclaim").(bool); ok {
|
||||
if b {
|
||||
str = "!"
|
||||
} else {
|
||||
str = ""
|
||||
}
|
||||
}
|
||||
|
||||
return options.DataStr("adjective") + " world" + str
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"happy world!",
|
||||
},
|
||||
{
|
||||
"passing in data to a compiled function that expects data - works with block helpers that use ..",
|
||||
`{{#hello}}{{world ../zomg}}{{/hello}}`,
|
||||
map[string]interface{}{"exclaim": true, "zomg": "world"},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{
|
||||
"hello": func(options *raymond.Options) string {
|
||||
return options.FnWith(map[string]string{"exclaim": "?"})
|
||||
},
|
||||
"world": func(context string, options *raymond.Options) string {
|
||||
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"happy world?",
|
||||
},
|
||||
{
|
||||
"passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..",
|
||||
`{{#hello}}{{world ../zomg}}{{/hello}}`,
|
||||
map[string]interface{}{"exclaim": true, "zomg": "world"},
|
||||
map[string]interface{}{"adjective": "happy", "accessData": "#win"},
|
||||
map[string]interface{}{
|
||||
"hello": func(options *raymond.Options) string {
|
||||
return options.DataStr("accessData") + " " + options.FnWith(map[string]string{"exclaim": "?"})
|
||||
},
|
||||
"world": func(context string, options *raymond.Options) string {
|
||||
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"#win happy world?",
|
||||
},
|
||||
{
|
||||
"you can override inherited data when invoking a helper",
|
||||
`{{#hello}}{{world zomg}}{{/hello}}`,
|
||||
map[string]interface{}{"exclaim": true, "zomg": "planet"},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{
|
||||
"hello": func(options *raymond.Options) string {
|
||||
ctx := map[string]string{"exclaim": "?", "zomg": "world"}
|
||||
data := options.NewDataFrame()
|
||||
data.Set("adjective", "sad")
|
||||
|
||||
return options.FnCtxData(ctx, data)
|
||||
},
|
||||
"world": func(context string, options *raymond.Options) string {
|
||||
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"sad world?",
|
||||
},
|
||||
{
|
||||
"you can override inherited data when invoking a helper with depth",
|
||||
`{{#hello}}{{world ../zomg}}{{/hello}}`,
|
||||
map[string]interface{}{"exclaim": true, "zomg": "world"},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{
|
||||
"hello": func(options *raymond.Options) string {
|
||||
ctx := map[string]string{"exclaim": "?"}
|
||||
data := options.NewDataFrame()
|
||||
data.Set("adjective", "sad")
|
||||
|
||||
return options.FnCtxData(ctx, data)
|
||||
},
|
||||
"world": func(context string, options *raymond.Options) string {
|
||||
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"sad world?",
|
||||
},
|
||||
{
|
||||
"@root - the root context can be looked up via @root",
|
||||
`{{@root.foo}}`,
|
||||
map[string]interface{}{"foo": "hello"},
|
||||
nil, nil, nil,
|
||||
"hello",
|
||||
},
|
||||
{
|
||||
"@root - passed root values take priority",
|
||||
`{{@root.foo}}`,
|
||||
nil,
|
||||
map[string]interface{}{"root": map[string]string{"foo": "hello"}},
|
||||
nil, nil,
|
||||
"hello",
|
||||
},
|
||||
{
|
||||
"nesting - the root context can be looked up via @root",
|
||||
`{{#helper}}{{#helper}}{{@./depth}} {{@../depth}} {{@../../depth}}{{/helper}}{{/helper}}`,
|
||||
map[string]interface{}{"foo": "hello"},
|
||||
map[string]interface{}{"depth": 0},
|
||||
map[string]interface{}{
|
||||
"helper": func(options *raymond.Options) string {
|
||||
data := options.NewDataFrame()
|
||||
|
||||
if depth, ok := options.Data("depth").(int); ok {
|
||||
data.Set("depth", depth+1)
|
||||
}
|
||||
|
||||
return options.FnData(data)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"2 1 0",
|
||||
},
|
||||
}
|
||||
|
||||
func TestData(t *testing.T) {
|
||||
launchTests(t, dataTests)
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
// Package handlebars contains all the tests that come from handlebars.js project.
|
||||
package handlebars
|
|
@ -1,670 +0,0 @@
|
|||
package handlebars
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
)
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
func barSuffixHelper(context string) string {
|
||||
return "bar " + context
|
||||
}
|
||||
|
||||
func echoHelper(str string) string {
|
||||
return str
|
||||
}
|
||||
|
||||
func echoNbHelper(str string, nb int) string {
|
||||
result := ""
|
||||
for i := 0; i < nb; i++ {
|
||||
result += str
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func linkHelper(prefix string, options *raymond.Options) string {
|
||||
return fmt.Sprintf(`<a href="%s/%s">%s</a>`, prefix, options.ValueStr("url"), options.ValueStr("text"))
|
||||
}
|
||||
|
||||
func rawHelper(options *raymond.Options) string {
|
||||
return options.Fn()
|
||||
}
|
||||
|
||||
func rawThreeHelper(a, b, c string, options *raymond.Options) string {
|
||||
return options.Fn() + a + b + c
|
||||
}
|
||||
|
||||
func formHelper(options *raymond.Options) string {
|
||||
return "<form>" + options.Fn() + "</form>"
|
||||
}
|
||||
|
||||
func formCtxHelper(context interface{}, options *raymond.Options) string {
|
||||
return "<form>" + options.FnWith(context) + "</form>"
|
||||
}
|
||||
|
||||
func listHelper(context interface{}, options *raymond.Options) string {
|
||||
val := reflect.ValueOf(context)
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
if val.Len() > 0 {
|
||||
result := "<ul>"
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
result += "<li>"
|
||||
result += options.FnWith(val.Index(i).Interface())
|
||||
result += "</li>"
|
||||
}
|
||||
result += "</ul>"
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return "<p>" + options.Inverse() + "</p>"
|
||||
}
|
||||
|
||||
func blogHelper(val string) string {
|
||||
return "val is " + val
|
||||
}
|
||||
|
||||
func equalHelper(a, b string) string {
|
||||
return raymond.Str(a == b)
|
||||
}
|
||||
|
||||
func dashHelper(a, b string) string {
|
||||
return a + "-" + b
|
||||
}
|
||||
|
||||
func concatHelper(a, b string) string {
|
||||
return a + b
|
||||
}
|
||||
|
||||
func detectDataHelper(options *raymond.Options) string {
|
||||
if val, ok := options.DataFrame().Get("exclaim").(string); ok {
|
||||
return val
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/helper.js
|
||||
//
|
||||
var helpersTests = []Test{
|
||||
{
|
||||
"helper with complex lookup",
|
||||
"{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}",
|
||||
map[string]interface{}{"prefix": "/root", "goodbyes": []map[string]string{{"text": "Goodbye", "url": "goodbye"}}},
|
||||
nil,
|
||||
map[string]interface{}{"link": linkHelper},
|
||||
nil,
|
||||
`<a href="/root/goodbye">Goodbye</a>`,
|
||||
},
|
||||
{
|
||||
"helper for raw block gets raw content",
|
||||
"{{{{raw}}}} {{test}} {{{{/raw}}}}",
|
||||
map[string]interface{}{"test": "hello"},
|
||||
nil,
|
||||
map[string]interface{}{"raw": rawHelper},
|
||||
nil,
|
||||
" {{test}} ",
|
||||
},
|
||||
{
|
||||
"helper for raw block gets parameters",
|
||||
"{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}",
|
||||
map[string]interface{}{"test": "hello"},
|
||||
nil,
|
||||
map[string]interface{}{"raw": rawThreeHelper},
|
||||
nil,
|
||||
" {{test}} 123",
|
||||
},
|
||||
{
|
||||
"helper block with complex lookup expression",
|
||||
"{{#goodbyes}}{{../name}}{{/goodbyes}}",
|
||||
map[string]interface{}{"name": "Alan"},
|
||||
nil,
|
||||
map[string]interface{}{"goodbyes": func(options *raymond.Options) string {
|
||||
out := ""
|
||||
for _, str := range []string{"Goodbye", "goodbye", "GOODBYE"} {
|
||||
out += str + " " + options.FnWith(str) + "! "
|
||||
}
|
||||
return out
|
||||
}},
|
||||
nil,
|
||||
"Goodbye Alan! goodbye Alan! GOODBYE Alan! ",
|
||||
},
|
||||
{
|
||||
"helper with complex lookup and nested template",
|
||||
"{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}",
|
||||
map[string]interface{}{"prefix": "/root", "goodbyes": []map[string]string{{"text": "Goodbye", "url": "goodbye"}}},
|
||||
nil,
|
||||
map[string]interface{}{"link": linkHelper},
|
||||
nil,
|
||||
`<a href="/root/goodbye">Goodbye</a>`,
|
||||
},
|
||||
{
|
||||
// note: The JS implementation returns undefined, we return empty string
|
||||
"helper returning undefined value (1)",
|
||||
" {{nothere}}",
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
map[string]interface{}{"nothere": func() string {
|
||||
return ""
|
||||
}},
|
||||
nil,
|
||||
" ",
|
||||
},
|
||||
{
|
||||
// note: The JS implementation returns undefined, we return empty string
|
||||
"helper returning undefined value (2)",
|
||||
" {{#nothere}}{{/nothere}}",
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
map[string]interface{}{"nothere": func() string {
|
||||
return ""
|
||||
}},
|
||||
nil,
|
||||
" ",
|
||||
},
|
||||
{
|
||||
"block helper",
|
||||
"{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!",
|
||||
map[string]interface{}{"world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{"goodbyes": func(options *raymond.Options) string {
|
||||
return options.FnWith(map[string]string{"text": "GOODBYE"})
|
||||
}},
|
||||
nil,
|
||||
"GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"block helper staying in the same context",
|
||||
"{{#form}}<p>{{name}}</p>{{/form}}",
|
||||
map[string]interface{}{"name": "Yehuda"},
|
||||
nil,
|
||||
map[string]interface{}{"form": formHelper},
|
||||
nil,
|
||||
"<form><p>Yehuda</p></form>",
|
||||
},
|
||||
{
|
||||
"block helper should have context in this",
|
||||
"<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>",
|
||||
map[string]interface{}{"people": []map[string]interface{}{{"name": "Alan", "id": 1}, {"name": "Yehuda", "id": 2}}},
|
||||
nil,
|
||||
map[string]interface{}{"link": func(options *raymond.Options) string {
|
||||
return fmt.Sprintf("<a href=\"/people/%s\">%s</a>", options.ValueStr("id"), options.Fn())
|
||||
}},
|
||||
nil,
|
||||
`<ul><li><a href="/people/1">Alan</a></li><li><a href="/people/2">Yehuda</a></li></ul>`,
|
||||
},
|
||||
{
|
||||
"block helper for undefined value",
|
||||
"{{#empty}}shouldn't render{{/empty}}",
|
||||
nil, nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"block helper passing a new context",
|
||||
"{{#form yehuda}}<p>{{name}}</p>{{/form}}",
|
||||
map[string]map[string]string{"yehuda": {"name": "Yehuda"}},
|
||||
nil,
|
||||
map[string]interface{}{"form": formCtxHelper},
|
||||
nil,
|
||||
"<form><p>Yehuda</p></form>",
|
||||
},
|
||||
{
|
||||
"block helper passing a complex path context",
|
||||
"{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}",
|
||||
map[string]map[string]interface{}{"yehuda": {"name": "Yehuda", "cat": map[string]string{"name": "Harold"}}},
|
||||
nil,
|
||||
map[string]interface{}{"form": formCtxHelper},
|
||||
nil,
|
||||
"<form><p>Harold</p></form>",
|
||||
},
|
||||
{
|
||||
"nested block helpers",
|
||||
"{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}",
|
||||
map[string]map[string]string{"yehuda": {"name": "Yehuda"}},
|
||||
nil,
|
||||
map[string]interface{}{"link": func(options *raymond.Options) string {
|
||||
return fmt.Sprintf("<a href=\"%s\">%s</a>", options.ValueStr("name"), options.Fn())
|
||||
}, "form": formCtxHelper},
|
||||
nil,
|
||||
`<form><p>Yehuda</p><a href="Yehuda">Hello</a></form>`,
|
||||
},
|
||||
{
|
||||
"block helper inverted sections (1) - an inverse wrapper is passed in as a new context",
|
||||
"{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}",
|
||||
map[string][]map[string]string{"people": {{"name": "Alan"}, {"name": "Yehuda"}}},
|
||||
nil,
|
||||
map[string]interface{}{"list": listHelper},
|
||||
nil,
|
||||
`<ul><li>Alan</li><li>Yehuda</li></ul>`,
|
||||
},
|
||||
{
|
||||
"block helper inverted sections (2) - an inverse wrapper can be optionally called",
|
||||
"{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}",
|
||||
map[string][]map[string]string{"people": {}},
|
||||
nil,
|
||||
map[string]interface{}{"list": listHelper},
|
||||
nil,
|
||||
`<p><em>Nobody's here</em></p>`,
|
||||
},
|
||||
{
|
||||
"block helper inverted sections (3) - the context of an inverse is the parent of the block",
|
||||
"{{#list people}}Hello{{^}}{{message}}{{/list}}",
|
||||
map[string]interface{}{"people": []interface{}{}, "message": "Nobody's here"},
|
||||
nil,
|
||||
map[string]interface{}{"list": listHelper},
|
||||
nil,
|
||||
`<p>Nobody's here</p>`,
|
||||
},
|
||||
|
||||
{
|
||||
"pathed lambdas with parameters (1)",
|
||||
"{{./helper 1}}",
|
||||
map[string]interface{}{
|
||||
"helper": func(param int) string { return "winning" },
|
||||
"hash": map[string]interface{}{
|
||||
"helper": func(param int) string { return "winning" },
|
||||
}},
|
||||
nil,
|
||||
map[string]interface{}{"./helper": func(param int) string { return "fail" }},
|
||||
nil,
|
||||
"winning",
|
||||
},
|
||||
{
|
||||
"pathed lambdas with parameters (2)",
|
||||
"{{hash/helper 1}}",
|
||||
map[string]interface{}{
|
||||
"helper": func(param int) string { return "winning" },
|
||||
"hash": map[string]interface{}{
|
||||
"helper": func(param int) string { return "winning" },
|
||||
}},
|
||||
nil,
|
||||
map[string]interface{}{"./helper": func(param int) string { return "fail" }},
|
||||
nil,
|
||||
"winning",
|
||||
},
|
||||
|
||||
{
|
||||
"helpers hash - providing a helpers hash (1)",
|
||||
"Goodbye {{cruel}} {{world}}!",
|
||||
map[string]interface{}{"cruel": "cruel"},
|
||||
nil,
|
||||
map[string]interface{}{"world": func() string { return "world" }},
|
||||
nil,
|
||||
"Goodbye cruel world!",
|
||||
},
|
||||
{
|
||||
"helpers hash - providing a helpers hash (2)",
|
||||
"Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!",
|
||||
map[string]interface{}{"iter": []map[string]string{{"cruel": "cruel"}}},
|
||||
nil,
|
||||
map[string]interface{}{"world": func() string { return "world" }},
|
||||
nil,
|
||||
"Goodbye cruel world!",
|
||||
},
|
||||
{
|
||||
"helpers hash - in cases of conflict, helpers win (1)",
|
||||
"{{{lookup}}}",
|
||||
map[string]interface{}{"lookup": "Explicit"},
|
||||
nil,
|
||||
map[string]interface{}{"lookup": func() string { return "helpers" }},
|
||||
nil,
|
||||
"helpers",
|
||||
},
|
||||
{
|
||||
"helpers hash - in cases of conflict, helpers win (2)",
|
||||
"{{lookup}}",
|
||||
map[string]interface{}{"lookup": "Explicit"},
|
||||
nil,
|
||||
map[string]interface{}{"lookup": func() string { return "helpers" }},
|
||||
nil,
|
||||
"helpers",
|
||||
},
|
||||
{
|
||||
"helpers hash - the helpers hash is available is nested contexts",
|
||||
"{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}",
|
||||
map[string]interface{}{"outer": map[string]interface{}{"inner": map[string]interface{}{"unused": []string{}}}},
|
||||
nil,
|
||||
map[string]interface{}{"helper": func() string { return "helper" }},
|
||||
nil,
|
||||
"helper",
|
||||
},
|
||||
|
||||
// @todo "helpers hash - the helper hash should augment the global hash"
|
||||
|
||||
// @todo "registration"
|
||||
|
||||
{
|
||||
"decimal number literals work",
|
||||
"Message: {{hello -1.2 1.2}}",
|
||||
nil, nil,
|
||||
map[string]interface{}{"hello": func(times, times2 interface{}) string {
|
||||
ts, t2s := "NaN", "NaN"
|
||||
|
||||
if v, ok := times.(float64); ok {
|
||||
ts = raymond.Str(v)
|
||||
}
|
||||
|
||||
if v, ok := times2.(float64); ok {
|
||||
t2s = raymond.Str(v)
|
||||
}
|
||||
|
||||
return "Hello " + ts + " " + t2s + " times"
|
||||
}},
|
||||
nil,
|
||||
"Message: Hello -1.2 1.2 times",
|
||||
},
|
||||
{
|
||||
"negative number literals work",
|
||||
"Message: {{hello -12}}",
|
||||
nil, nil,
|
||||
map[string]interface{}{"hello": func(times interface{}) string {
|
||||
ts := "NaN"
|
||||
|
||||
if v, ok := times.(int); ok {
|
||||
ts = raymond.Str(v)
|
||||
}
|
||||
|
||||
return "Hello " + ts + " times"
|
||||
}},
|
||||
nil,
|
||||
"Message: Hello -12 times",
|
||||
},
|
||||
|
||||
{
|
||||
"String literal parameters - simple literals work",
|
||||
`Message: {{hello "world" 12 true false}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"hello": func(p, t, b, b2 interface{}) string {
|
||||
times, bool1, bool2 := "NaN", "NaB", "NaB"
|
||||
|
||||
param, ok := p.(string)
|
||||
if !ok {
|
||||
param = "NaN"
|
||||
}
|
||||
|
||||
if v, ok := t.(int); ok {
|
||||
times = raymond.Str(v)
|
||||
}
|
||||
|
||||
if v, ok := b.(bool); ok {
|
||||
bool1 = raymond.Str(v)
|
||||
}
|
||||
|
||||
if v, ok := b2.(bool); ok {
|
||||
bool2 = raymond.Str(v)
|
||||
}
|
||||
|
||||
return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2
|
||||
}},
|
||||
nil,
|
||||
"Message: Hello world 12 times: true false",
|
||||
},
|
||||
|
||||
// @todo "using a quote in the middle of a parameter raises an error"
|
||||
|
||||
{
|
||||
"String literal parameters - escaping a String is possible",
|
||||
"Message: {{{hello \"\\\"world\\\"\"}}}",
|
||||
nil, nil,
|
||||
map[string]interface{}{"hello": func(param string) string {
|
||||
return "Hello " + param
|
||||
}},
|
||||
nil,
|
||||
`Message: Hello "world"`,
|
||||
},
|
||||
{
|
||||
"String literal parameters - it works with ' marks",
|
||||
"Message: {{{hello \"Alan's world\"}}}",
|
||||
nil, nil,
|
||||
map[string]interface{}{"hello": func(param string) string {
|
||||
return "Hello " + param
|
||||
}},
|
||||
nil,
|
||||
`Message: Hello Alan's world`,
|
||||
},
|
||||
|
||||
{
|
||||
"multiple parameters - simple multi-params work",
|
||||
"Message: {{goodbye cruel world}}",
|
||||
map[string]string{"cruel": "cruel", "world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{"goodbye": func(cruel, world string) string {
|
||||
return "Goodbye " + cruel + " " + world
|
||||
}},
|
||||
nil,
|
||||
"Message: Goodbye cruel world",
|
||||
},
|
||||
{
|
||||
"multiple parameters - block multi-params work",
|
||||
"Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}",
|
||||
map[string]string{"cruel": "cruel", "world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{"goodbye": func(cruel, world string, options *raymond.Options) string {
|
||||
return options.FnWith(map[string]interface{}{"greeting": "Goodbye", "adj": cruel, "noun": world})
|
||||
}},
|
||||
nil,
|
||||
"Message: Goodbye cruel world",
|
||||
},
|
||||
|
||||
{
|
||||
"hash - helpers can take an optional hash",
|
||||
`{{goodbye cruel="CRUEL" world="WORLD" times=12}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.HashStr("world") + " " + options.HashStr("times") + " TIMES"
|
||||
}},
|
||||
nil,
|
||||
"GOODBYE CRUEL WORLD 12 TIMES",
|
||||
},
|
||||
{
|
||||
"hash - helpers can take an optional hash with booleans (1)",
|
||||
`{{goodbye cruel="CRUEL" world="WORLD" print=true}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
p, ok := options.HashProp("print").(bool)
|
||||
if ok {
|
||||
if p {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.HashStr("world")
|
||||
} else {
|
||||
return "NOT PRINTING"
|
||||
}
|
||||
}
|
||||
|
||||
return "THIS SHOULD NOT HAPPEN"
|
||||
}},
|
||||
nil,
|
||||
"GOODBYE CRUEL WORLD",
|
||||
},
|
||||
{
|
||||
"hash - helpers can take an optional hash with booleans (2)",
|
||||
`{{goodbye cruel="CRUEL" world="WORLD" print=false}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
p, ok := options.HashProp("print").(bool)
|
||||
if ok {
|
||||
if p {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.HashStr("world")
|
||||
} else {
|
||||
return "NOT PRINTING"
|
||||
}
|
||||
}
|
||||
|
||||
return "THIS SHOULD NOT HAPPEN"
|
||||
}},
|
||||
nil,
|
||||
"NOT PRINTING",
|
||||
},
|
||||
{
|
||||
"block helpers can take an optional hash",
|
||||
`{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn() + " " + options.HashStr("times") + " TIMES"
|
||||
}},
|
||||
nil,
|
||||
"GOODBYE CRUEL world 12 TIMES",
|
||||
},
|
||||
{
|
||||
"block helpers can take an optional hash with single quoted stings",
|
||||
`{{#goodbye cruel='CRUEL' times=12}}world{{/goodbye}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn() + " " + options.HashStr("times") + " TIMES"
|
||||
}},
|
||||
nil,
|
||||
"GOODBYE CRUEL world 12 TIMES",
|
||||
},
|
||||
{
|
||||
"block helpers can take an optional hash with booleans (1)",
|
||||
`{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
p, ok := options.HashProp("print").(bool)
|
||||
if ok {
|
||||
if p {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn()
|
||||
} else {
|
||||
return "NOT PRINTING"
|
||||
}
|
||||
}
|
||||
|
||||
return "THIS SHOULD NOT HAPPEN"
|
||||
}},
|
||||
nil,
|
||||
"GOODBYE CRUEL world",
|
||||
},
|
||||
{
|
||||
"block helpers can take an optional hash with booleans (1)",
|
||||
`{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
p, ok := options.HashProp("print").(bool)
|
||||
if ok {
|
||||
if p {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn()
|
||||
} else {
|
||||
return "NOT PRINTING"
|
||||
}
|
||||
}
|
||||
|
||||
return "THIS SHOULD NOT HAPPEN"
|
||||
}},
|
||||
nil,
|
||||
"NOT PRINTING",
|
||||
},
|
||||
|
||||
// @todo "helperMissing - if a context is not found, helperMissing is used" throw error
|
||||
|
||||
// @todo "helperMissing - if a context is not found, custom helperMissing is used"
|
||||
|
||||
// @todo "helperMissing - if a value is not found, custom helperMissing is used"
|
||||
|
||||
{
|
||||
"block helpers can take an optional hash with booleans (1)",
|
||||
`{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
p, ok := options.HashProp("print").(bool)
|
||||
if ok {
|
||||
if p {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn()
|
||||
} else {
|
||||
return "NOT PRINTING"
|
||||
}
|
||||
}
|
||||
|
||||
return "THIS SHOULD NOT HAPPEN"
|
||||
}},
|
||||
nil,
|
||||
"NOT PRINTING",
|
||||
},
|
||||
|
||||
// @todo "knownHelpers/knownHelpersOnly" tests
|
||||
|
||||
// @todo "blockHelperMissing" tests
|
||||
|
||||
// @todo "name field" tests
|
||||
|
||||
{
|
||||
"name conflicts - helpers take precedence over same-named context properties",
|
||||
`{{goodbye}} {{cruel world}}`,
|
||||
map[string]string{"goodbye": "goodbye", "world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"goodbye": func(options *raymond.Options) string {
|
||||
return strings.ToUpper(options.ValueStr("goodbye"))
|
||||
},
|
||||
"cruel": func(world string) string {
|
||||
return "cruel " + strings.ToUpper(world)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"GOODBYE cruel WORLD",
|
||||
},
|
||||
{
|
||||
"name conflicts - helpers take precedence over same-named context properties",
|
||||
`{{#goodbye}} {{cruel world}}{{/goodbye}}`,
|
||||
map[string]string{"goodbye": "goodbye", "world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"goodbye": func(options *raymond.Options) string {
|
||||
return strings.ToUpper(options.ValueStr("goodbye")) + options.Fn()
|
||||
},
|
||||
"cruel": func(world string) string {
|
||||
return "cruel " + strings.ToUpper(world)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"GOODBYE cruel WORLD",
|
||||
},
|
||||
{
|
||||
"name conflicts - Scoped names take precedence over helpers",
|
||||
`{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}`,
|
||||
map[string]string{"goodbye": "goodbye", "world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"goodbye": func(options *raymond.Options) string {
|
||||
return strings.ToUpper(options.ValueStr("goodbye"))
|
||||
},
|
||||
"cruel": func(world string) string {
|
||||
return "cruel " + strings.ToUpper(world)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"goodbye cruel WORLD cruel GOODBYE",
|
||||
},
|
||||
{
|
||||
"name conflicts - Scoped names take precedence over block helpers",
|
||||
`{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}`,
|
||||
map[string]string{"goodbye": "goodbye", "world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"goodbye": func(options *raymond.Options) string {
|
||||
return strings.ToUpper(options.ValueStr("goodbye")) + options.Fn()
|
||||
},
|
||||
"cruel": func(world string) string {
|
||||
return "cruel " + strings.ToUpper(world)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"GOODBYE cruel WORLD goodbye",
|
||||
},
|
||||
|
||||
// @todo "block params" tests
|
||||
}
|
||||
|
||||
func TestHelpers(t *testing.T) {
|
||||
launchTests(t, helpersTests)
|
||||
}
|
|
@ -1,182 +0,0 @@
|
|||
package handlebars
|
||||
|
||||
import "testing"
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/partials.js
|
||||
//
|
||||
var partialsTests = []Test{
|
||||
{
|
||||
"basic partials",
|
||||
"Dudes: {{#dudes}}{{> dude}}{{/dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{name}} ({{url}}) "},
|
||||
"Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
||||
},
|
||||
{
|
||||
"dynamic partials",
|
||||
"Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil,
|
||||
map[string]interface{}{"partial": func() string {
|
||||
return "dude"
|
||||
}},
|
||||
map[string]string{"dude": "{{name}} ({{url}}) "},
|
||||
"Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
||||
},
|
||||
|
||||
// @todo "failing dynamic partials"
|
||||
|
||||
{
|
||||
"partials with context",
|
||||
"Dudes: {{>dude dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{#this}}{{name}} ({{url}}) {{/this}}"},
|
||||
"Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
||||
},
|
||||
{
|
||||
"partials with undefined context",
|
||||
"Dudes: {{>dude dudes}}",
|
||||
map[string]interface{}{},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{foo}} Empty"},
|
||||
"Dudes: Empty",
|
||||
},
|
||||
|
||||
// @todo "partials with duplicate parameters"
|
||||
|
||||
{
|
||||
"partials with parameters",
|
||||
"Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}",
|
||||
map[string]interface{}{"foo": "bar", "dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{others.foo}}{{name}} ({{url}}) "},
|
||||
"Dudes: barYehuda (http://yehuda) barAlan (http://alan) ",
|
||||
},
|
||||
{
|
||||
"partial in a partial",
|
||||
"Dudes: {{#dudes}}{{>dude}}{{/dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{name}} {{> url}} ", "url": `<a href="{{url}}">{{url}}</a>`},
|
||||
`Dudes: Yehuda <a href="http://yehuda">http://yehuda</a> Alan <a href="http://alan">http://alan</a> `,
|
||||
},
|
||||
|
||||
// @todo "rendering undefined partial throws an exception"
|
||||
|
||||
// @todo "registering undefined partial throws an exception"
|
||||
|
||||
// SKIP: "rendering template partial in vm mode throws an exception"
|
||||
// SKIP: "rendering function partial in vm mode"
|
||||
|
||||
{
|
||||
"GH-14: a partial preceding a selector",
|
||||
"Dudes: {{>dude}} {{anotherDude}}",
|
||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{name}}"},
|
||||
"Dudes: Jeepers Creepers",
|
||||
},
|
||||
{
|
||||
"Partials with slash paths",
|
||||
"Dudes: {{> shared/dude}}",
|
||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
nil, nil,
|
||||
map[string]string{"shared/dude": "{{name}}"},
|
||||
"Dudes: Jeepers",
|
||||
},
|
||||
{
|
||||
"Partials with slash and point paths",
|
||||
"Dudes: {{> shared/dude.thing}}",
|
||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
nil, nil,
|
||||
map[string]string{"shared/dude.thing": "{{name}}"},
|
||||
"Dudes: Jeepers",
|
||||
},
|
||||
|
||||
// @todo "Global Partials"
|
||||
|
||||
// @todo "Multiple partial registration"
|
||||
|
||||
{
|
||||
"Partials with integer path",
|
||||
"Dudes: {{> 404}}",
|
||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
nil, nil,
|
||||
map[string]string{"404": "{{name}}"}, // @note Difference with JS test: partial name is a string
|
||||
"Dudes: Jeepers",
|
||||
},
|
||||
// @note This is not supported by our implementation. But really... who cares ?
|
||||
// {
|
||||
// "Partials with complex path",
|
||||
// "Dudes: {{> 404/asdf?.bar}}",
|
||||
// map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
// nil, nil,
|
||||
// map[string]string{"404/asdf?.bar": "{{name}}"},
|
||||
// "Dudes: Jeepers",
|
||||
// },
|
||||
{
|
||||
"Partials with escaped",
|
||||
"Dudes: {{> [+404/asdf?.bar]}}",
|
||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
nil, nil,
|
||||
map[string]string{"+404/asdf?.bar": "{{name}}"},
|
||||
"Dudes: Jeepers",
|
||||
},
|
||||
{
|
||||
"Partials with string",
|
||||
"Dudes: {{> '+404/asdf?.bar'}}",
|
||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
nil, nil,
|
||||
map[string]string{"+404/asdf?.bar": "{{name}}"},
|
||||
"Dudes: Jeepers",
|
||||
},
|
||||
{
|
||||
"should handle empty partial",
|
||||
"Dudes: {{#dudes}}{{> dude}}{{/dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": ""},
|
||||
"Dudes: ",
|
||||
},
|
||||
|
||||
// @todo "throw on missing partial"
|
||||
|
||||
// SKIP: "should pass compiler flags"
|
||||
|
||||
{
|
||||
"standalone partials (1) - indented partials",
|
||||
"Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{name}}\n"},
|
||||
"Dudes:\n Yehuda\n Alan\n",
|
||||
},
|
||||
{
|
||||
"standalone partials (2) - nested indented partials",
|
||||
"Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{name}}\n {{> url}}", "url": "{{url}}!\n"},
|
||||
"Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n",
|
||||
},
|
||||
|
||||
// // @todo preventIndent option
|
||||
// {
|
||||
// "standalone partials (3) - prevent nested indented partials",
|
||||
// "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}",
|
||||
// map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
// nil, nil,
|
||||
// map[string]string{"dude": "{{name}}\n {{> url}}", "url": "{{url}}!\n"},
|
||||
// "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n",
|
||||
// },
|
||||
|
||||
// @todo "compat mode"
|
||||
}
|
||||
|
||||
func TestPartials(t *testing.T) {
|
||||
launchTests(t, partialsTests)
|
||||
}
|
|
@ -1,209 +0,0 @@
|
|||
package handlebars
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
)
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/subexpression.js
|
||||
//
|
||||
var subexpressionsTests = []Test{
|
||||
{
|
||||
"arg-less helper",
|
||||
"{{foo (bar)}}!",
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"foo": func(val string) string {
|
||||
return val + val
|
||||
},
|
||||
"bar": func() string {
|
||||
return "LOL"
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"LOLLOL!",
|
||||
},
|
||||
{
|
||||
"helper w args",
|
||||
"{{blog (equal a b)}}",
|
||||
map[string]interface{}{"bar": "LOL"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"blog": blogHelper,
|
||||
"equal": equalHelper,
|
||||
},
|
||||
nil,
|
||||
"val is true",
|
||||
},
|
||||
{
|
||||
"mixed paths and helpers",
|
||||
"{{blog baz.bat (equal a b) baz.bar}}",
|
||||
map[string]interface{}{"bar": "LOL", "baz": map[string]string{"bat": "foo!", "bar": "bar!"}},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"blog": func(p, p2, p3 string) string {
|
||||
return "val is " + p + ", " + p2 + " and " + p3
|
||||
},
|
||||
"equal": equalHelper,
|
||||
},
|
||||
nil,
|
||||
"val is foo!, true and bar!",
|
||||
},
|
||||
{
|
||||
"supports much nesting",
|
||||
"{{blog (equal (equal true true) true)}}",
|
||||
map[string]interface{}{"bar": "LOL"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"blog": blogHelper,
|
||||
"equal": equalHelper,
|
||||
},
|
||||
nil,
|
||||
"val is true",
|
||||
},
|
||||
|
||||
{
|
||||
"GH-800 : Complex subexpressions (1)",
|
||||
"{{dash 'abc' (concat a b)}}",
|
||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
||||
nil,
|
||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
||||
nil,
|
||||
"abc-ab",
|
||||
},
|
||||
{
|
||||
"GH-800 : Complex subexpressions (2)",
|
||||
"{{dash d (concat a b)}}",
|
||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
||||
nil,
|
||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
||||
nil,
|
||||
"d-ab",
|
||||
},
|
||||
{
|
||||
"GH-800 : Complex subexpressions (3)",
|
||||
"{{dash c.c (concat a b)}}",
|
||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
||||
nil,
|
||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
||||
nil,
|
||||
"c-ab",
|
||||
},
|
||||
{
|
||||
"GH-800 : Complex subexpressions (4)",
|
||||
"{{dash (concat a b) c.c}}",
|
||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
||||
nil,
|
||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
||||
nil,
|
||||
"ab-c",
|
||||
},
|
||||
{
|
||||
"GH-800 : Complex subexpressions (5)",
|
||||
"{{dash (concat a e.e) c.c}}",
|
||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
||||
nil,
|
||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
||||
nil,
|
||||
"ae-c",
|
||||
},
|
||||
|
||||
{
|
||||
// note: test not relevant
|
||||
"provides each nested helper invocation its own options hash",
|
||||
"{{equal (equal true true) true}}",
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"equal": equalHelper,
|
||||
},
|
||||
nil,
|
||||
"true",
|
||||
},
|
||||
{
|
||||
"with hashes",
|
||||
"{{blog (equal (equal true true) true fun='yes')}}",
|
||||
map[string]interface{}{"bar": "LOL"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"blog": blogHelper,
|
||||
"equal": equalHelper,
|
||||
},
|
||||
nil,
|
||||
"val is true",
|
||||
},
|
||||
{
|
||||
"as hashes",
|
||||
"{{blog fun=(equal (blog fun=1) 'val is 1')}}",
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"blog": func(options *raymond.Options) string {
|
||||
return "val is " + options.HashStr("fun")
|
||||
},
|
||||
"equal": equalHelper,
|
||||
},
|
||||
nil,
|
||||
"val is true",
|
||||
},
|
||||
{
|
||||
"multiple subexpressions in a hash",
|
||||
`{{input aria-label=(t "Name") placeholder=(t "Example User")}}`,
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"input": func(options *raymond.Options) raymond.SafeString {
|
||||
return raymond.SafeString(`<input aria-label="` + options.HashStr("aria-label") + `" placeholder="` + options.HashStr("placeholder") + `" />`)
|
||||
},
|
||||
"t": func(param string) raymond.SafeString {
|
||||
return raymond.SafeString(param)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
`<input aria-label="Name" placeholder="Example User" />`,
|
||||
},
|
||||
{
|
||||
"multiple subexpressions in a hash with context",
|
||||
`{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}`,
|
||||
map[string]map[string]string{"item": {"field": "Name", "placeholder": "Example User"}},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"input": func(options *raymond.Options) raymond.SafeString {
|
||||
return raymond.SafeString(`<input aria-label="` + options.HashStr("aria-label") + `" placeholder="` + options.HashStr("placeholder") + `" />`)
|
||||
},
|
||||
"t": func(param string) raymond.SafeString {
|
||||
return raymond.SafeString(param)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
`<input aria-label="Name" placeholder="Example User" />`,
|
||||
},
|
||||
|
||||
// @todo "in string params mode"
|
||||
|
||||
// @todo "as hashes in string params mode"
|
||||
|
||||
{
|
||||
"subexpression functions on the context",
|
||||
"{{foo (bar)}}!",
|
||||
map[string]interface{}{"bar": func() string { return "LOL" }},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"foo": func(val string) string {
|
||||
return val + val
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"LOLLOL!",
|
||||
},
|
||||
|
||||
// @todo "subexpressions can't just be property lookups" should raise error
|
||||
}
|
||||
|
||||
func TestSubexpressions(t *testing.T) {
|
||||
launchTests(t, subexpressionsTests)
|
||||
}
|
|
@ -1,259 +0,0 @@
|
|||
package handlebars
|
||||
|
||||
import "testing"
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/whitespace-control.js
|
||||
//
|
||||
var whitespaceControlTests = []Test{
|
||||
{
|
||||
"should strip whitespace around mustache calls (1)",
|
||||
" {{~foo~}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar<",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around mustache calls (2)",
|
||||
" {{~foo}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar< ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around mustache calls (3)",
|
||||
" {{foo~}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" bar<",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around mustache calls (4)",
|
||||
" {{~&foo~}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar<",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around mustache calls (5)",
|
||||
" {{~{foo}~}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar<",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around mustache calls (6)",
|
||||
"1\n{{foo~}} \n\n 23\n{{bar}}4",
|
||||
nil, nil, nil, nil,
|
||||
"1\n23\n4",
|
||||
},
|
||||
|
||||
{
|
||||
"blocks - should strip whitespace around simple block calls (1)",
|
||||
" {{~#if foo~}} bar {{~/if~}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"blocks - should strip whitespace around simple block calls (2)",
|
||||
" {{#if foo~}} bar {{/if~}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"blocks - should strip whitespace around simple block calls (3)",
|
||||
" {{~#if foo}} bar {{~/if}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"blocks - should strip whitespace around simple block calls (4)",
|
||||
" {{#if foo}} bar {{/if}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"blocks - should strip whitespace around simple block calls (5)",
|
||||
" \n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\n ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"blocks - should strip whitespace around simple block calls (6)",
|
||||
" a\n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\na ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" abara ",
|
||||
},
|
||||
|
||||
{
|
||||
"should strip whitespace around inverse block calls (1)",
|
||||
" {{~^if foo~}} bar {{~/if~}} ",
|
||||
nil, nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around inverse block calls (2)",
|
||||
" {{^if foo~}} bar {{/if~}} ",
|
||||
nil, nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around inverse block calls (3)",
|
||||
" {{~^if foo}} bar {{~/if}} ",
|
||||
nil, nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around inverse block calls (4)",
|
||||
" {{^if foo}} bar {{/if}} ",
|
||||
nil, nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around inverse block calls (5)",
|
||||
" \n\n{{~^if foo~}} \n\nbar \n\n{{~/if~}}\n\n ",
|
||||
nil, nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
|
||||
{
|
||||
"should strip whitespace around complex block calls (1)",
|
||||
"{{#if foo~}} bar {{~^~}} baz {{~/if}}",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (2)",
|
||||
"{{#if foo~}} bar {{^~}} baz {{/if}}",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (3)",
|
||||
"{{#if foo}} bar {{~^~}} baz {{~/if}}",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" bar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (4)",
|
||||
"{{#if foo}} bar {{^~}} baz {{/if}}",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (5)",
|
||||
"{{#if foo~}} bar {{~else~}} baz {{~/if}}",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (6)",
|
||||
"\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (7)",
|
||||
"\n\n{{~#if foo~}} \n\n{{{foo}}} \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar<",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (8)",
|
||||
"{{#if foo~}} bar {{~^~}} baz {{~/if}}",
|
||||
nil, nil, nil, nil,
|
||||
"baz",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (9)",
|
||||
"{{#if foo}} bar {{~^~}} baz {{/if}}",
|
||||
nil, nil, nil, nil,
|
||||
"baz ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (10)",
|
||||
"{{#if foo~}} bar {{~^}} baz {{~/if}}",
|
||||
nil, nil, nil, nil,
|
||||
" baz",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (11)",
|
||||
"{{#if foo~}} bar {{~^}} baz {{/if}}",
|
||||
nil, nil, nil, nil,
|
||||
" baz ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (12)",
|
||||
"{{#if foo~}} bar {{~else~}} baz {{~/if}}",
|
||||
nil, nil, nil, nil,
|
||||
"baz",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (13)",
|
||||
"\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n",
|
||||
nil, nil, nil, nil,
|
||||
"baz",
|
||||
},
|
||||
|
||||
{
|
||||
"should strip whitespace around partials (1)",
|
||||
"foo {{~> dude~}} ",
|
||||
nil, nil, nil,
|
||||
map[string]string{"dude": "bar"},
|
||||
"foobar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around partials (2)",
|
||||
"foo {{> dude~}} ",
|
||||
nil, nil, nil,
|
||||
map[string]string{"dude": "bar"},
|
||||
"foo bar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around partials (3)",
|
||||
"foo {{> dude}} ",
|
||||
nil, nil, nil,
|
||||
map[string]string{"dude": "bar"},
|
||||
"foo bar ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around partials (4)",
|
||||
"foo\n {{~> dude}} ",
|
||||
nil, nil, nil,
|
||||
map[string]string{"dude": "bar"},
|
||||
"foobar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around partials (5)",
|
||||
"foo\n {{> dude}} ",
|
||||
nil, nil, nil,
|
||||
map[string]string{"dude": "bar"},
|
||||
"foo\n bar",
|
||||
},
|
||||
|
||||
{
|
||||
"should only strip whitespace once",
|
||||
" {{~foo~}} {{foo}} {{foo}} ",
|
||||
map[string]string{"foo": "bar"},
|
||||
nil, nil, nil,
|
||||
"barbar bar ",
|
||||
},
|
||||
}
|
||||
|
||||
func TestWhitespaceControl(t *testing.T) {
|
||||
launchTests(t, whitespaceControlTests)
|
||||
}
|
|
@ -1,371 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Options represents the options argument provided to helpers and context functions.
|
||||
type Options struct {
|
||||
// evaluation visitor
|
||||
eval *evalVisitor
|
||||
|
||||
// params
|
||||
params []interface{}
|
||||
hash map[string]interface{}
|
||||
}
|
||||
|
||||
// helpers stores all globally registered helpers
|
||||
var helpers = make(map[string]reflect.Value)
|
||||
|
||||
// protects global helpers
|
||||
var helpersMutex sync.RWMutex
|
||||
|
||||
func init() {
|
||||
// register builtin helpers
|
||||
RegisterHelper("if", ifHelper)
|
||||
RegisterHelper("unless", unlessHelper)
|
||||
RegisterHelper("with", withHelper)
|
||||
RegisterHelper("each", eachHelper)
|
||||
RegisterHelper("log", logHelper)
|
||||
RegisterHelper("lookup", lookupHelper)
|
||||
}
|
||||
|
||||
// RegisterHelper registers a global helper. That helper will be available to all templates.
|
||||
func RegisterHelper(name string, helper interface{}) {
|
||||
helpersMutex.Lock()
|
||||
defer helpersMutex.Unlock()
|
||||
|
||||
if helpers[name] != zero {
|
||||
panic(fmt.Errorf("Helper already registered: %s", name))
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(helper)
|
||||
ensureValidHelper(name, val)
|
||||
|
||||
helpers[name] = val
|
||||
}
|
||||
|
||||
// RegisterHelpers registers several global helpers. Those helpers will be available to all templates.
|
||||
func RegisterHelpers(helpers map[string]interface{}) {
|
||||
for name, helper := range helpers {
|
||||
RegisterHelper(name, helper)
|
||||
}
|
||||
}
|
||||
|
||||
// ensureValidHelper panics if given helper is not valid
|
||||
func ensureValidHelper(name string, funcValue reflect.Value) {
|
||||
if funcValue.Kind() != reflect.Func {
|
||||
panic(fmt.Errorf("Helper must be a function: %s", name))
|
||||
}
|
||||
|
||||
funcType := funcValue.Type()
|
||||
|
||||
if funcType.NumOut() != 1 {
|
||||
panic(fmt.Errorf("Helper function must return a string or a SafeString: %s", name))
|
||||
}
|
||||
|
||||
// @todo Check if first returned value is a string, SafeString or interface{} ?
|
||||
}
|
||||
|
||||
// findHelper finds a globally registered helper
|
||||
func findHelper(name string) reflect.Value {
|
||||
helpersMutex.RLock()
|
||||
defer helpersMutex.RUnlock()
|
||||
|
||||
return helpers[name]
|
||||
}
|
||||
|
||||
// newOptions instanciates a new Options
|
||||
func newOptions(eval *evalVisitor, params []interface{}, hash map[string]interface{}) *Options {
|
||||
return &Options{
|
||||
eval: eval,
|
||||
params: params,
|
||||
hash: hash,
|
||||
}
|
||||
}
|
||||
|
||||
// newEmptyOptions instanciates a new empty Options
|
||||
func newEmptyOptions(eval *evalVisitor) *Options {
|
||||
return &Options{
|
||||
eval: eval,
|
||||
hash: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Context Values
|
||||
//
|
||||
|
||||
// Value returns field value from current context.
|
||||
func (options *Options) Value(name string) interface{} {
|
||||
value := options.eval.evalField(options.eval.curCtx(), name, false)
|
||||
if !value.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return value.Interface()
|
||||
}
|
||||
|
||||
// ValueStr returns string representation of field value from current context.
|
||||
func (options *Options) ValueStr(name string) string {
|
||||
return Str(options.Value(name))
|
||||
}
|
||||
|
||||
// Ctx returns current evaluation context.
|
||||
func (options *Options) Ctx() interface{} {
|
||||
return options.eval.curCtx()
|
||||
}
|
||||
|
||||
//
|
||||
// Hash Arguments
|
||||
//
|
||||
|
||||
// HashProp returns hash property.
|
||||
func (options *Options) HashProp(name string) interface{} {
|
||||
return options.hash[name]
|
||||
}
|
||||
|
||||
// HashStr returns string representation of hash property.
|
||||
func (options *Options) HashStr(name string) string {
|
||||
return Str(options.hash[name])
|
||||
}
|
||||
|
||||
// Hash returns entire hash.
|
||||
func (options *Options) Hash() map[string]interface{} {
|
||||
return options.hash
|
||||
}
|
||||
|
||||
//
|
||||
// Parameters
|
||||
//
|
||||
|
||||
// Param returns parameter at given position.
|
||||
func (options *Options) Param(pos int) interface{} {
|
||||
if len(options.params) > pos {
|
||||
return options.params[pos]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ParamStr returns string representation of parameter at given position.
|
||||
func (options *Options) ParamStr(pos int) string {
|
||||
return Str(options.Param(pos))
|
||||
}
|
||||
|
||||
// Params returns all parameters.
|
||||
func (options *Options) Params() []interface{} {
|
||||
return options.params
|
||||
}
|
||||
|
||||
//
|
||||
// Private data
|
||||
//
|
||||
|
||||
// Data returns private data value.
|
||||
func (options *Options) Data(name string) interface{} {
|
||||
return options.eval.dataFrame.Get(name)
|
||||
}
|
||||
|
||||
// DataStr returns string representation of private data value.
|
||||
func (options *Options) DataStr(name string) string {
|
||||
return Str(options.eval.dataFrame.Get(name))
|
||||
}
|
||||
|
||||
// DataFrame returns current private data frame.
|
||||
func (options *Options) DataFrame() *DataFrame {
|
||||
return options.eval.dataFrame
|
||||
}
|
||||
|
||||
// NewDataFrame instanciates a new data frame that is a copy of current evaluation data frame.
|
||||
//
|
||||
// Parent of returned data frame is set to current evaluation data frame.
|
||||
func (options *Options) NewDataFrame() *DataFrame {
|
||||
return options.eval.dataFrame.Copy()
|
||||
}
|
||||
|
||||
// newIterDataFrame instanciates a new data frame and set iteration specific vars
|
||||
func (options *Options) newIterDataFrame(length int, i int, key interface{}) *DataFrame {
|
||||
return options.eval.dataFrame.newIterDataFrame(length, i, key)
|
||||
}
|
||||
|
||||
//
|
||||
// Evaluation
|
||||
//
|
||||
|
||||
// evalBlock evaluates block with given context, private data and iteration key
|
||||
func (options *Options) evalBlock(ctx interface{}, data *DataFrame, key interface{}) string {
|
||||
result := ""
|
||||
|
||||
if block := options.eval.curBlock(); (block != nil) && (block.Program != nil) {
|
||||
result = options.eval.evalProgram(block.Program, ctx, data, key)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Fn evaluates block with current evaluation context.
|
||||
func (options *Options) Fn() string {
|
||||
return options.evalBlock(nil, nil, nil)
|
||||
}
|
||||
|
||||
// FnCtxData evaluates block with given context and private data frame.
|
||||
func (options *Options) FnCtxData(ctx interface{}, data *DataFrame) string {
|
||||
return options.evalBlock(ctx, data, nil)
|
||||
}
|
||||
|
||||
// FnWith evaluates block with given context.
|
||||
func (options *Options) FnWith(ctx interface{}) string {
|
||||
return options.evalBlock(ctx, nil, nil)
|
||||
}
|
||||
|
||||
// FnData evaluates block with given private data frame.
|
||||
func (options *Options) FnData(data *DataFrame) string {
|
||||
return options.evalBlock(nil, data, nil)
|
||||
}
|
||||
|
||||
// Inverse evaluates "else block".
|
||||
func (options *Options) Inverse() string {
|
||||
result := ""
|
||||
if block := options.eval.curBlock(); (block != nil) && (block.Inverse != nil) {
|
||||
result, _ = block.Inverse.Accept(options.eval).(string)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Eval evaluates field for given context.
|
||||
func (options *Options) Eval(ctx interface{}, field string) interface{} {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if field == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
val := options.eval.evalField(reflect.ValueOf(ctx), field, false)
|
||||
if !val.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return val.Interface()
|
||||
}
|
||||
|
||||
//
|
||||
// Misc
|
||||
//
|
||||
|
||||
// isIncludableZero returns true if 'includeZero' option is set and first param is the number 0
|
||||
func (options *Options) isIncludableZero() bool {
|
||||
b, ok := options.HashProp("includeZero").(bool)
|
||||
if ok && b {
|
||||
nb, ok := options.Param(0).(int)
|
||||
if ok && nb == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
//
|
||||
// Builtin helpers
|
||||
//
|
||||
|
||||
// #if block helper
|
||||
func ifHelper(conditional interface{}, options *Options) interface{} {
|
||||
if options.isIncludableZero() || IsTrue(conditional) {
|
||||
return options.Fn()
|
||||
}
|
||||
|
||||
return options.Inverse()
|
||||
}
|
||||
|
||||
// #unless block helper
|
||||
func unlessHelper(conditional interface{}, options *Options) interface{} {
|
||||
if options.isIncludableZero() || IsTrue(conditional) {
|
||||
return options.Inverse()
|
||||
}
|
||||
|
||||
return options.Fn()
|
||||
}
|
||||
|
||||
// #with block helper
|
||||
func withHelper(context interface{}, options *Options) interface{} {
|
||||
if IsTrue(context) {
|
||||
return options.FnWith(context)
|
||||
}
|
||||
|
||||
return options.Inverse()
|
||||
}
|
||||
|
||||
// #each block helper
|
||||
func eachHelper(context interface{}, options *Options) interface{} {
|
||||
if !IsTrue(context) {
|
||||
return options.Inverse()
|
||||
}
|
||||
|
||||
result := ""
|
||||
|
||||
val := reflect.ValueOf(context)
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
// computes private data
|
||||
data := options.newIterDataFrame(val.Len(), i, nil)
|
||||
|
||||
// evaluates block
|
||||
result += options.evalBlock(val.Index(i).Interface(), data, i)
|
||||
}
|
||||
case reflect.Map:
|
||||
// note: a go hash is not ordered, so result may vary, this behaviour differs from the JS implementation
|
||||
keys := val.MapKeys()
|
||||
for i := 0; i < len(keys); i++ {
|
||||
key := keys[i].Interface()
|
||||
ctx := val.MapIndex(keys[i]).Interface()
|
||||
|
||||
// computes private data
|
||||
data := options.newIterDataFrame(len(keys), i, key)
|
||||
|
||||
// evaluates block
|
||||
result += options.evalBlock(ctx, data, key)
|
||||
}
|
||||
case reflect.Struct:
|
||||
var exportedFields []int
|
||||
|
||||
// collect exported fields only
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
if tField := val.Type().Field(i); tField.PkgPath == "" {
|
||||
exportedFields = append(exportedFields, i)
|
||||
}
|
||||
}
|
||||
|
||||
for i, fieldIndex := range exportedFields {
|
||||
key := val.Type().Field(fieldIndex).Name
|
||||
ctx := val.Field(fieldIndex).Interface()
|
||||
|
||||
// computes private data
|
||||
data := options.newIterDataFrame(len(exportedFields), i, key)
|
||||
|
||||
// evaluates block
|
||||
result += options.evalBlock(ctx, data, key)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// #log helper
|
||||
func logHelper(message string) interface{} {
|
||||
log.Print(message)
|
||||
return ""
|
||||
}
|
||||
|
||||
// #lookup helper
|
||||
func lookupHelper(obj interface{}, field string, options *Options) interface{} {
|
||||
return Str(options.Eval(obj, field))
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import "testing"
|
||||
|
||||
const (
|
||||
VERBOSE = false
|
||||
)
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
func barHelper(options *Options) string { return "bar" }
|
||||
|
||||
func echoHelper(str string, nb int) string {
|
||||
result := ""
|
||||
for i := 0; i < nb; i++ {
|
||||
result += str
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func boolHelper(b bool) string {
|
||||
if b {
|
||||
return "yes it is"
|
||||
}
|
||||
|
||||
return "absolutely not"
|
||||
}
|
||||
|
||||
func gnakHelper(nb int) string {
|
||||
result := ""
|
||||
for i := 0; i < nb; i++ {
|
||||
result += "GnAK!"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
//
|
||||
// Tests
|
||||
//
|
||||
|
||||
var helperTests = []Test{
|
||||
{
|
||||
"simple helper",
|
||||
`{{foo}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"foo": barHelper},
|
||||
nil,
|
||||
`bar`,
|
||||
},
|
||||
{
|
||||
"helper with literal string param",
|
||||
`{{echo "foo" 1}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"echo": echoHelper},
|
||||
nil,
|
||||
`foo`,
|
||||
},
|
||||
{
|
||||
"helper with identifier param",
|
||||
`{{echo foo 1}}`,
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
nil,
|
||||
map[string]interface{}{"echo": echoHelper},
|
||||
nil,
|
||||
`bar`,
|
||||
},
|
||||
{
|
||||
"helper with literal boolean param",
|
||||
`{{bool true}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"bool": boolHelper},
|
||||
nil,
|
||||
`yes it is`,
|
||||
},
|
||||
{
|
||||
"helper with literal boolean param",
|
||||
`{{bool false}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"bool": boolHelper},
|
||||
nil,
|
||||
`absolutely not`,
|
||||
},
|
||||
{
|
||||
"helper with literal boolean param",
|
||||
`{{gnak 5}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"gnak": gnakHelper},
|
||||
nil,
|
||||
`GnAK!GnAK!GnAK!GnAK!GnAK!`,
|
||||
},
|
||||
{
|
||||
"helper with several parameters",
|
||||
`{{echo "GnAK!" 3}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"echo": echoHelper},
|
||||
nil,
|
||||
`GnAK!GnAK!GnAK!`,
|
||||
},
|
||||
{
|
||||
"#if helper with true literal",
|
||||
`{{#if true}}YES MAN{{/if}}`,
|
||||
nil, nil, nil, nil,
|
||||
`YES MAN`,
|
||||
},
|
||||
{
|
||||
"#if helper with false literal",
|
||||
`{{#if false}}YES MAN{{/if}}`,
|
||||
nil, nil, nil, nil,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"#if helper with truthy identifier",
|
||||
`{{#if ok}}YES MAN{{/if}}`,
|
||||
map[string]interface{}{"ok": true},
|
||||
nil, nil, nil,
|
||||
`YES MAN`,
|
||||
},
|
||||
{
|
||||
"#if helper with falsy identifier",
|
||||
`{{#if ok}}YES MAN{{/if}}`,
|
||||
map[string]interface{}{"ok": false},
|
||||
nil, nil, nil,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"#unless helper with true literal",
|
||||
`{{#unless true}}YES MAN{{/unless}}`,
|
||||
nil, nil, nil, nil,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"#unless helper with false literal",
|
||||
`{{#unless false}}YES MAN{{/unless}}`,
|
||||
nil, nil, nil, nil,
|
||||
`YES MAN`,
|
||||
},
|
||||
{
|
||||
"#unless helper with truthy identifier",
|
||||
`{{#unless ok}}YES MAN{{/unless}}`,
|
||||
map[string]interface{}{"ok": true},
|
||||
nil, nil, nil,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"#unless helper with falsy identifier",
|
||||
`{{#unless ok}}YES MAN{{/unless}}`,
|
||||
map[string]interface{}{"ok": false},
|
||||
nil, nil, nil,
|
||||
`YES MAN`,
|
||||
},
|
||||
}
|
||||
|
||||
//
|
||||
// Let's go
|
||||
//
|
||||
|
||||
func TestHelper(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
launchTests(t, helperTests)
|
||||
}
|
|
@ -1,650 +0,0 @@
|
|||
// Package lexer provides a handlebars tokenizer.
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// References:
|
||||
// - https://github.com/wycats/handlebars.js/blob/master/src/handlebars.l
|
||||
// - https://github.com/golang/go/blob/master/src/text/template/parse/lex.go
|
||||
|
||||
const (
|
||||
// Mustaches detection
|
||||
ESCAPED_ESCAPED_OPEN_MUSTACHE = "\\\\{{"
|
||||
ESCAPED_OPEN_MUSTACHE = "\\{{"
|
||||
OPEN_MUSTACHE = "{{"
|
||||
CLOSE_MUSTACHE = "}}"
|
||||
CLOSE_STRIP_MUSTACHE = "~}}"
|
||||
CLOSE_UNESCAPED_STRIP_MUSTACHE = "}~}}"
|
||||
)
|
||||
|
||||
const eof = -1
|
||||
|
||||
// lexFunc represents a function that returns the next lexer function.
|
||||
type lexFunc func(*Lexer) lexFunc
|
||||
|
||||
// Lexer is a lexical analyzer.
|
||||
type Lexer struct {
|
||||
input string // input to scan
|
||||
name string // lexer name, used for testing purpose
|
||||
tokens chan Token // channel of scanned tokens
|
||||
nextFunc lexFunc // the next function to execute
|
||||
|
||||
pos int // current byte position in input string
|
||||
line int // current line position in input string
|
||||
width int // size of last rune scanned from input string
|
||||
start int // start position of the token we are scanning
|
||||
|
||||
// the shameful contextual properties needed because `nextFunc` is not enough
|
||||
closeComment *regexp.Regexp // regexp to scan close of current comment
|
||||
rawBlock bool // are we parsing a raw block content ?
|
||||
}
|
||||
|
||||
var (
|
||||
lookheadChars = `[\s` + regexp.QuoteMeta("=~}/)|") + `]`
|
||||
literalLookheadChars = `[\s` + regexp.QuoteMeta("~})") + `]`
|
||||
|
||||
// characters not allowed in an identifier
|
||||
unallowedIDChars = " \n\t!\"#%&'()*+,./;<=>@[\\]^`{|}~"
|
||||
|
||||
// regular expressions
|
||||
rID = regexp.MustCompile(`^[^` + regexp.QuoteMeta(unallowedIDChars) + `]+`)
|
||||
rDotID = regexp.MustCompile(`^\.` + lookheadChars)
|
||||
rTrue = regexp.MustCompile(`^true` + literalLookheadChars)
|
||||
rFalse = regexp.MustCompile(`^false` + literalLookheadChars)
|
||||
rOpenRaw = regexp.MustCompile(`^\{\{\{\{`)
|
||||
rCloseRaw = regexp.MustCompile(`^\}\}\}\}`)
|
||||
rOpenEndRaw = regexp.MustCompile(`^\{\{\{\{/`)
|
||||
rOpenEndRawLookAhead = regexp.MustCompile(`\{\{\{\{/`)
|
||||
rOpenUnescaped = regexp.MustCompile(`^\{\{~?\{`)
|
||||
rCloseUnescaped = regexp.MustCompile(`^\}~?\}\}`)
|
||||
rOpenBlock = regexp.MustCompile(`^\{\{~?#`)
|
||||
rOpenEndBlock = regexp.MustCompile(`^\{\{~?/`)
|
||||
rOpenPartial = regexp.MustCompile(`^\{\{~?>`)
|
||||
// {{^}} or {{else}}
|
||||
rInverse = regexp.MustCompile(`^(\{\{~?\^\s*~?\}\}|\{\{~?\s*else\s*~?\}\})`)
|
||||
rOpenInverse = regexp.MustCompile(`^\{\{~?\^`)
|
||||
rOpenInverseChain = regexp.MustCompile(`^\{\{~?\s*else`)
|
||||
// {{ or {{&
|
||||
rOpen = regexp.MustCompile(`^\{\{~?&?`)
|
||||
rClose = regexp.MustCompile(`^~?\}\}`)
|
||||
rOpenBlockParams = regexp.MustCompile(`^as\s+\|`)
|
||||
// {{!-- ... --}}
|
||||
rOpenCommentDash = regexp.MustCompile(`^\{\{~?!--\s*`)
|
||||
rCloseCommentDash = regexp.MustCompile(`^\s*--~?\}\}`)
|
||||
// {{! ... }}
|
||||
rOpenComment = regexp.MustCompile(`^\{\{~?!\s*`)
|
||||
rCloseComment = regexp.MustCompile(`^\s*~?\}\}`)
|
||||
)
|
||||
|
||||
// Scan scans given input.
|
||||
//
|
||||
// Tokens can then be fetched sequentially thanks to NextToken() function on returned lexer.
|
||||
func Scan(input string) *Lexer {
|
||||
return scanWithName(input, "")
|
||||
}
|
||||
|
||||
// scanWithName scans given input, with a name used for testing
|
||||
//
|
||||
// Tokens can then be fetched sequentially thanks to NextToken() function on returned lexer.
|
||||
func scanWithName(input string, name string) *Lexer {
|
||||
result := &Lexer{
|
||||
input: input,
|
||||
name: name,
|
||||
tokens: make(chan Token),
|
||||
line: 1,
|
||||
}
|
||||
|
||||
go result.run()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Collect scans and collect all tokens.
|
||||
//
|
||||
// This should be used for debugging purpose only. You should use Scan() and lexer.NextToken() functions instead.
|
||||
func Collect(input string) []Token {
|
||||
var result []Token
|
||||
|
||||
l := Scan(input)
|
||||
for {
|
||||
token := l.NextToken()
|
||||
result = append(result, token)
|
||||
|
||||
if token.Kind == TokenEOF || token.Kind == TokenError {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// NextToken returns the next scanned token.
|
||||
func (l *Lexer) NextToken() Token {
|
||||
result := <-l.tokens
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Pos returns the current byte position.
|
||||
func (l *Lexer) Pos() int {
|
||||
return l.pos
|
||||
}
|
||||
|
||||
// Line returns the current line number.
|
||||
func (l *Lexer) Line() int {
|
||||
return l.line
|
||||
}
|
||||
|
||||
// run starts lexical analysis
|
||||
func (l *Lexer) run() {
|
||||
for l.nextFunc = lexContent; l.nextFunc != nil; {
|
||||
l.nextFunc = l.nextFunc(l)
|
||||
}
|
||||
}
|
||||
|
||||
// next returns next character from input, or eof of there is nothing left to scan
|
||||
func (l *Lexer) next() rune {
|
||||
if l.pos >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
|
||||
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
|
||||
l.width = w
|
||||
l.pos += l.width
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *Lexer) produce(kind TokenKind, val string) {
|
||||
l.tokens <- Token{kind, val, l.start, l.line}
|
||||
|
||||
// scanning a new token
|
||||
l.start = l.pos
|
||||
|
||||
// update line number
|
||||
l.line += strings.Count(val, "\n")
|
||||
}
|
||||
|
||||
// emit emits a new scanned token
|
||||
func (l *Lexer) emit(kind TokenKind) {
|
||||
l.produce(kind, l.input[l.start:l.pos])
|
||||
}
|
||||
|
||||
// emitContent emits scanned content
|
||||
func (l *Lexer) emitContent() {
|
||||
if l.pos > l.start {
|
||||
l.emit(TokenContent)
|
||||
}
|
||||
}
|
||||
|
||||
// emitString emits a scanned string
|
||||
func (l *Lexer) emitString(delimiter rune) {
|
||||
str := l.input[l.start:l.pos]
|
||||
|
||||
// replace escaped delimiters
|
||||
str = strings.Replace(str, "\\"+string(delimiter), string(delimiter), -1)
|
||||
|
||||
l.produce(TokenString, str)
|
||||
}
|
||||
|
||||
// peek returns but does not consume the next character in the input
|
||||
func (l *Lexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
// backup steps back one character
|
||||
//
|
||||
// WARNING: Can only be called once per call of next
|
||||
func (l *Lexer) backup() {
|
||||
l.pos -= l.width
|
||||
}
|
||||
|
||||
// ignoreskips all characters that have been scanned up to current position
|
||||
func (l *Lexer) ignore() {
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
// accept scans the next character if it is included in given string
|
||||
func (l *Lexer) accept(valid string) bool {
|
||||
if strings.IndexRune(valid, l.next()) >= 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
l.backup()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// acceptRun scans all following characters that are part of given string
|
||||
func (l *Lexer) acceptRun(valid string) {
|
||||
for strings.IndexRune(valid, l.next()) >= 0 {
|
||||
}
|
||||
|
||||
l.backup()
|
||||
}
|
||||
|
||||
// errorf emits an error token
|
||||
func (l *Lexer) errorf(format string, args ...interface{}) lexFunc {
|
||||
l.tokens <- Token{TokenError, fmt.Sprintf(format, args...), l.start, l.line}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isString returns true if content at current scanning position starts with given string
|
||||
func (l *Lexer) isString(str string) bool {
|
||||
return strings.HasPrefix(l.input[l.pos:], str)
|
||||
}
|
||||
|
||||
// findRegexp returns the first string from current scanning position that matches given regular expression
|
||||
func (l *Lexer) findRegexp(r *regexp.Regexp) string {
|
||||
return r.FindString(l.input[l.pos:])
|
||||
}
|
||||
|
||||
// indexRegexp returns the index of the first string from current scanning position that matches given regular expression
|
||||
//
|
||||
// It returns -1 if not found
|
||||
func (l *Lexer) indexRegexp(r *regexp.Regexp) int {
|
||||
loc := r.FindStringIndex(l.input[l.pos:])
|
||||
if loc == nil {
|
||||
return -1
|
||||
} else {
|
||||
return loc[0]
|
||||
}
|
||||
}
|
||||
|
||||
// lexContent scans content (ie: not between mustaches)
|
||||
func lexContent(l *Lexer) lexFunc {
|
||||
var next lexFunc
|
||||
|
||||
if l.rawBlock {
|
||||
if i := l.indexRegexp(rOpenEndRawLookAhead); i != -1 {
|
||||
// {{{{/
|
||||
l.rawBlock = false
|
||||
l.pos += i
|
||||
|
||||
next = lexOpenMustache
|
||||
} else {
|
||||
return l.errorf("Unclosed raw block")
|
||||
}
|
||||
} else if l.isString(ESCAPED_ESCAPED_OPEN_MUSTACHE) {
|
||||
// \\{{
|
||||
|
||||
// emit content with only one escaped escape
|
||||
l.next()
|
||||
l.emitContent()
|
||||
|
||||
// ignore second escaped escape
|
||||
l.next()
|
||||
l.ignore()
|
||||
|
||||
next = lexContent
|
||||
} else if l.isString(ESCAPED_OPEN_MUSTACHE) {
|
||||
// \{{
|
||||
next = lexEscapedOpenMustache
|
||||
} else if str := l.findRegexp(rOpenCommentDash); str != "" {
|
||||
// {{!--
|
||||
l.closeComment = rCloseCommentDash
|
||||
|
||||
next = lexComment
|
||||
} else if str := l.findRegexp(rOpenComment); str != "" {
|
||||
// {{!
|
||||
l.closeComment = rCloseComment
|
||||
|
||||
next = lexComment
|
||||
} else if l.isString(OPEN_MUSTACHE) {
|
||||
// {{
|
||||
next = lexOpenMustache
|
||||
}
|
||||
|
||||
if next != nil {
|
||||
// emit scanned content
|
||||
l.emitContent()
|
||||
|
||||
// scan next token
|
||||
return next
|
||||
}
|
||||
|
||||
// scan next rune
|
||||
if l.next() == eof {
|
||||
// emit scanned content
|
||||
l.emitContent()
|
||||
|
||||
// this is over
|
||||
l.emit(TokenEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
// continue content scanning
|
||||
return lexContent
|
||||
}
|
||||
|
||||
// lexEscapedOpenMustache scans \{{
|
||||
func lexEscapedOpenMustache(l *Lexer) lexFunc {
|
||||
// ignore escape character
|
||||
l.next()
|
||||
l.ignore()
|
||||
|
||||
// scan mustaches
|
||||
for l.peek() == '{' {
|
||||
l.next()
|
||||
}
|
||||
|
||||
return lexContent
|
||||
}
|
||||
|
||||
// lexOpenMustache scans {{
|
||||
func lexOpenMustache(l *Lexer) lexFunc {
|
||||
var str string
|
||||
var tok TokenKind
|
||||
|
||||
nextFunc := lexExpression
|
||||
|
||||
if str = l.findRegexp(rOpenEndRaw); str != "" {
|
||||
tok = TokenOpenEndRawBlock
|
||||
} else if str = l.findRegexp(rOpenRaw); str != "" {
|
||||
tok = TokenOpenRawBlock
|
||||
l.rawBlock = true
|
||||
} else if str = l.findRegexp(rOpenUnescaped); str != "" {
|
||||
tok = TokenOpenUnescaped
|
||||
} else if str = l.findRegexp(rOpenBlock); str != "" {
|
||||
tok = TokenOpenBlock
|
||||
} else if str = l.findRegexp(rOpenEndBlock); str != "" {
|
||||
tok = TokenOpenEndBlock
|
||||
} else if str = l.findRegexp(rOpenPartial); str != "" {
|
||||
tok = TokenOpenPartial
|
||||
} else if str = l.findRegexp(rInverse); str != "" {
|
||||
tok = TokenInverse
|
||||
nextFunc = lexContent
|
||||
} else if str = l.findRegexp(rOpenInverse); str != "" {
|
||||
tok = TokenOpenInverse
|
||||
} else if str = l.findRegexp(rOpenInverseChain); str != "" {
|
||||
tok = TokenOpenInverseChain
|
||||
} else if str = l.findRegexp(rOpen); str != "" {
|
||||
tok = TokenOpen
|
||||
} else {
|
||||
// this is rotten
|
||||
panic("Current pos MUST be an opening mustache")
|
||||
}
|
||||
|
||||
l.pos += len(str)
|
||||
l.emit(tok)
|
||||
|
||||
return nextFunc
|
||||
}
|
||||
|
||||
// lexCloseMustache scans }} or ~}}
|
||||
func lexCloseMustache(l *Lexer) lexFunc {
|
||||
var str string
|
||||
var tok TokenKind
|
||||
|
||||
if str = l.findRegexp(rCloseRaw); str != "" {
|
||||
// }}}}
|
||||
tok = TokenCloseRawBlock
|
||||
} else if str = l.findRegexp(rCloseUnescaped); str != "" {
|
||||
// }}}
|
||||
tok = TokenCloseUnescaped
|
||||
} else if str = l.findRegexp(rClose); str != "" {
|
||||
// }}
|
||||
tok = TokenClose
|
||||
} else {
|
||||
// this is rotten
|
||||
panic("Current pos MUST be a closing mustache")
|
||||
}
|
||||
|
||||
l.pos += len(str)
|
||||
l.emit(tok)
|
||||
|
||||
return lexContent
|
||||
}
|
||||
|
||||
// lexExpression scans inside mustaches
|
||||
func lexExpression(l *Lexer) lexFunc {
|
||||
// search close mustache delimiter
|
||||
if l.isString(CLOSE_MUSTACHE) || l.isString(CLOSE_STRIP_MUSTACHE) || l.isString(CLOSE_UNESCAPED_STRIP_MUSTACHE) {
|
||||
return lexCloseMustache
|
||||
}
|
||||
|
||||
// search some patterns before advancing scanning position
|
||||
|
||||
// "as |"
|
||||
if str := l.findRegexp(rOpenBlockParams); str != "" {
|
||||
l.pos += len(str)
|
||||
l.emit(TokenOpenBlockParams)
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// ..
|
||||
if l.isString("..") {
|
||||
l.pos += len("..")
|
||||
l.emit(TokenID)
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// .
|
||||
if str := l.findRegexp(rDotID); str != "" {
|
||||
l.pos += len(".")
|
||||
l.emit(TokenID)
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// true
|
||||
if str := l.findRegexp(rTrue); str != "" {
|
||||
l.pos += len("true")
|
||||
l.emit(TokenBoolean)
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// false
|
||||
if str := l.findRegexp(rFalse); str != "" {
|
||||
l.pos += len("false")
|
||||
l.emit(TokenBoolean)
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// let's scan next character
|
||||
switch r := l.next(); {
|
||||
case r == eof:
|
||||
return l.errorf("Unclosed expression")
|
||||
case isIgnorable(r):
|
||||
return lexIgnorable
|
||||
case r == '(':
|
||||
l.emit(TokenOpenSexpr)
|
||||
case r == ')':
|
||||
l.emit(TokenCloseSexpr)
|
||||
case r == '=':
|
||||
l.emit(TokenEquals)
|
||||
case r == '@':
|
||||
l.emit(TokenData)
|
||||
case r == '"' || r == '\'':
|
||||
l.backup()
|
||||
return lexString
|
||||
case r == '/' || r == '.':
|
||||
l.emit(TokenSep)
|
||||
case r == '|':
|
||||
l.emit(TokenCloseBlockParams)
|
||||
case r == '+' || r == '-' || (r >= '0' && r <= '9'):
|
||||
l.backup()
|
||||
return lexNumber
|
||||
case r == '[':
|
||||
return lexPathLiteral
|
||||
case strings.IndexRune(unallowedIDChars, r) < 0:
|
||||
l.backup()
|
||||
return lexIdentifier
|
||||
default:
|
||||
return l.errorf("Unexpected character in expression: '%c'", r)
|
||||
}
|
||||
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// lexComment scans {{!-- or {{!
|
||||
func lexComment(l *Lexer) lexFunc {
|
||||
if str := l.findRegexp(l.closeComment); str != "" {
|
||||
l.pos += len(str)
|
||||
l.emit(TokenComment)
|
||||
|
||||
return lexContent
|
||||
}
|
||||
|
||||
if r := l.next(); r == eof {
|
||||
return l.errorf("Unclosed comment")
|
||||
}
|
||||
|
||||
return lexComment
|
||||
}
|
||||
|
||||
// lexIgnorable scans all following ignorable characters
|
||||
func lexIgnorable(l *Lexer) lexFunc {
|
||||
for isIgnorable(l.peek()) {
|
||||
l.next()
|
||||
}
|
||||
l.ignore()
|
||||
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// lexString scans a string
|
||||
func lexString(l *Lexer) lexFunc {
|
||||
// get string delimiter
|
||||
delim := l.next()
|
||||
var prev rune = 0
|
||||
|
||||
// ignore delimiter
|
||||
l.ignore()
|
||||
|
||||
for {
|
||||
r := l.next()
|
||||
if r == eof || r == '\n' {
|
||||
return l.errorf("Unterminated string")
|
||||
}
|
||||
|
||||
if (r == delim) && (prev != '\\') {
|
||||
break
|
||||
}
|
||||
|
||||
prev = r
|
||||
}
|
||||
|
||||
// remove end delimiter
|
||||
l.backup()
|
||||
|
||||
// emit string
|
||||
l.emitString(delim)
|
||||
|
||||
// skip end delimiter
|
||||
l.next()
|
||||
l.ignore()
|
||||
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// lexNumber scans a number: decimal, octal, hex, float, or imaginary. This
|
||||
// isn't a perfect number scanner - for instance it accepts "." and "0x0.2"
|
||||
// and "089" - but when it's wrong the input is invalid and the parser (via
|
||||
// strconv) will notice.
|
||||
//
|
||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/parse/lex.go
|
||||
func lexNumber(l *Lexer) lexFunc {
|
||||
if !l.scanNumber() {
|
||||
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
||||
}
|
||||
if sign := l.peek(); sign == '+' || sign == '-' {
|
||||
// Complex: 1+2i. No spaces, must end in 'i'.
|
||||
if !l.scanNumber() || l.input[l.pos-1] != 'i' {
|
||||
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
||||
}
|
||||
l.emit(TokenNumber)
|
||||
} else {
|
||||
l.emit(TokenNumber)
|
||||
}
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// scanNumber scans a number
|
||||
//
|
||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/parse/lex.go
|
||||
func (l *Lexer) scanNumber() bool {
|
||||
// Optional leading sign.
|
||||
l.accept("+-")
|
||||
|
||||
// Is it hex?
|
||||
digits := "0123456789"
|
||||
|
||||
if l.accept("0") && l.accept("xX") {
|
||||
digits = "0123456789abcdefABCDEF"
|
||||
}
|
||||
|
||||
l.acceptRun(digits)
|
||||
|
||||
if l.accept(".") {
|
||||
l.acceptRun(digits)
|
||||
}
|
||||
|
||||
if l.accept("eE") {
|
||||
l.accept("+-")
|
||||
l.acceptRun("0123456789")
|
||||
}
|
||||
|
||||
// Is it imaginary?
|
||||
l.accept("i")
|
||||
|
||||
// Next thing mustn't be alphanumeric.
|
||||
if isAlphaNumeric(l.peek()) {
|
||||
l.next()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// lexIdentifier scans an ID
|
||||
func lexIdentifier(l *Lexer) lexFunc {
|
||||
str := l.findRegexp(rID)
|
||||
if len(str) == 0 {
|
||||
// this is rotten
|
||||
panic("Identifier expected")
|
||||
}
|
||||
|
||||
l.pos += len(str)
|
||||
l.emit(TokenID)
|
||||
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// lexPathLiteral scans an [ID]
|
||||
func lexPathLiteral(l *Lexer) lexFunc {
|
||||
for {
|
||||
r := l.next()
|
||||
if r == eof || r == '\n' {
|
||||
return l.errorf("Unterminated path literal")
|
||||
}
|
||||
|
||||
if r == ']' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
l.emit(TokenID)
|
||||
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// isIgnorable returns true if given character is ignorable (ie. whitespace of line feed)
|
||||
func isIgnorable(r rune) bool {
|
||||
return r == ' ' || r == '\t' || r == '\n'
|
||||
}
|
||||
|
||||
// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
|
||||
//
|
||||
// NOTE borrowed from https://github.com/golang/go/tree/master/src/text/template/parse/lex.go
|
||||
func isAlphaNumeric(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
}
|
|
@ -1,541 +0,0 @@
|
|||
package lexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type lexTest struct {
|
||||
name string
|
||||
input string
|
||||
tokens []Token
|
||||
}
|
||||
|
||||
// helpers
|
||||
func tokContent(val string) Token { return Token{TokenContent, val, 0, 1} }
|
||||
func tokID(val string) Token { return Token{TokenID, val, 0, 1} }
|
||||
func tokSep(val string) Token { return Token{TokenSep, val, 0, 1} }
|
||||
func tokString(val string) Token { return Token{TokenString, val, 0, 1} }
|
||||
func tokNumber(val string) Token { return Token{TokenNumber, val, 0, 1} }
|
||||
func tokInverse(val string) Token { return Token{TokenInverse, val, 0, 1} }
|
||||
func tokBool(val string) Token { return Token{TokenBoolean, val, 0, 1} }
|
||||
func tokError(val string) Token { return Token{TokenError, val, 0, 1} }
|
||||
func tokComment(val string) Token { return Token{TokenComment, val, 0, 1} }
|
||||
|
||||
var tokEOF = Token{TokenEOF, "", 0, 1}
|
||||
var tokEquals = Token{TokenEquals, "=", 0, 1}
|
||||
var tokData = Token{TokenData, "@", 0, 1}
|
||||
var tokOpen = Token{TokenOpen, "{{", 0, 1}
|
||||
var tokOpenAmp = Token{TokenOpen, "{{&", 0, 1}
|
||||
var tokOpenPartial = Token{TokenOpenPartial, "{{>", 0, 1}
|
||||
var tokClose = Token{TokenClose, "}}", 0, 1}
|
||||
var tokOpenStrip = Token{TokenOpen, "{{~", 0, 1}
|
||||
var tokCloseStrip = Token{TokenClose, "~}}", 0, 1}
|
||||
var tokOpenUnescaped = Token{TokenOpenUnescaped, "{{{", 0, 1}
|
||||
var tokCloseUnescaped = Token{TokenCloseUnescaped, "}}}", 0, 1}
|
||||
var tokOpenUnescapedStrip = Token{TokenOpenUnescaped, "{{~{", 0, 1}
|
||||
var tokCloseUnescapedStrip = Token{TokenCloseUnescaped, "}~}}", 0, 1}
|
||||
var tokOpenBlock = Token{TokenOpenBlock, "{{#", 0, 1}
|
||||
var tokOpenEndBlock = Token{TokenOpenEndBlock, "{{/", 0, 1}
|
||||
var tokOpenInverse = Token{TokenOpenInverse, "{{^", 0, 1}
|
||||
var tokOpenInverseChain = Token{TokenOpenInverseChain, "{{else", 0, 1}
|
||||
var tokOpenSexpr = Token{TokenOpenSexpr, "(", 0, 1}
|
||||
var tokCloseSexpr = Token{TokenCloseSexpr, ")", 0, 1}
|
||||
var tokOpenBlockParams = Token{TokenOpenBlockParams, "as |", 0, 1}
|
||||
var tokCloseBlockParams = Token{TokenCloseBlockParams, "|", 0, 1}
|
||||
var tokOpenRawBlock = Token{TokenOpenRawBlock, "{{{{", 0, 1}
|
||||
var tokCloseRawBlock = Token{TokenCloseRawBlock, "}}}}", 0, 1}
|
||||
var tokOpenEndRawBlock = Token{TokenOpenEndRawBlock, "{{{{/", 0, 1}
|
||||
|
||||
var lexTests = []lexTest{
|
||||
{"empty", "", []Token{tokEOF}},
|
||||
{"spaces", " \t\n", []Token{tokContent(" \t\n"), tokEOF}},
|
||||
{"content", `now is the time`, []Token{tokContent(`now is the time`), tokEOF}},
|
||||
|
||||
{
|
||||
`does not tokenizes identifier starting with true as boolean`,
|
||||
`{{ foo truebar }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("truebar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`does not tokenizes identifier starting with false as boolean`,
|
||||
`{{ foo falsebar }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("falsebar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes raw block`,
|
||||
`{{{{foo}}}} {{{{/foo}}}}`,
|
||||
[]Token{tokOpenRawBlock, tokID("foo"), tokCloseRawBlock, tokContent(" "), tokOpenEndRawBlock, tokID("foo"), tokCloseRawBlock, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes raw block with mustaches in content`,
|
||||
`{{{{foo}}}}{{bar}}{{{{/foo}}}}`,
|
||||
[]Token{tokOpenRawBlock, tokID("foo"), tokCloseRawBlock, tokContent("{{bar}}"), tokOpenEndRawBlock, tokID("foo"), tokCloseRawBlock, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes @../foo`,
|
||||
`{{@../foo}}`,
|
||||
[]Token{tokOpen, tokData, tokID(".."), tokSep("/"), tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes escaped mustaches`,
|
||||
"\\{{bar}}",
|
||||
[]Token{tokContent("{{bar}}"), tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes strip mustaches`,
|
||||
`{{~ foo ~}}`,
|
||||
[]Token{tokOpenStrip, tokID("foo"), tokCloseStrip, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes unescaped strip mustaches`,
|
||||
`{{~{ foo }~}}`,
|
||||
[]Token{tokOpenUnescapedStrip, tokID("foo"), tokCloseUnescapedStrip, tokEOF},
|
||||
},
|
||||
|
||||
//
|
||||
// Next tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/tokenizer.js
|
||||
//
|
||||
{
|
||||
`tokenizes a simple mustache as "OPEN ID CLOSE"`,
|
||||
`{{foo}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports unescaping with &`,
|
||||
`{{&bar}}`,
|
||||
[]Token{tokOpenAmp, tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports unescaping with {{{`,
|
||||
`{{{bar}}}`,
|
||||
[]Token{tokOpenUnescaped, tokID("bar"), tokCloseUnescaped, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaping delimiters`,
|
||||
"{{foo}} \\{{bar}} {{baz}}",
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaping multiple delimiters`,
|
||||
"{{foo}} \\{{bar}} \\{{baz}}",
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokContent("{{baz}}"), tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaping a triple stash`,
|
||||
"{{foo}} \\{{{bar}}} {{baz}}",
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{{bar}}} "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaping escape character`,
|
||||
"{{foo}} \\\\{{bar}} {{baz}}",
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaping multiple escape characters`,
|
||||
"{{foo}} \\\\{{bar}} \\\\{{baz}}",
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" \\"), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaped mustaches after escaped escape characters`,
|
||||
"{{foo}} \\\\{{bar}} \\{{baz}}",
|
||||
// NOTE: JS implementation returns:
|
||||
// ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT'],
|
||||
// WTF is the last CONTENT ?
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" "), tokContent("{{baz}}"), tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaped escape characters after escaped mustaches`,
|
||||
"{{foo}} \\{{bar}} \\\\{{baz}}",
|
||||
// NOTE: JS implementation returns:
|
||||
// []Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokContent("\\"), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} \\"), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaped escape character on a triple stash`,
|
||||
"{{foo}} \\\\{{{bar}}} {{baz}}",
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpenUnescaped, tokID("bar"), tokCloseUnescaped, tokContent(" "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a simple path`,
|
||||
`{{foo/bar}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokSep("/"), tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`allows dot notation (1)`,
|
||||
`{{foo.bar}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`allows dot notation (2)`,
|
||||
`{{foo.bar.baz}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("bar"), tokSep("."), tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`allows path literals with []`,
|
||||
`{{foo.[bar]}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("[bar]"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`allows multiple path literals on a line with []`,
|
||||
`{{foo.[bar]}}{{foo.[baz]}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("[bar]"), tokClose, tokOpen, tokID("foo"), tokSep("."), tokID("[baz]"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes {{.}} as OPEN ID CLOSE`,
|
||||
`{{.}}`,
|
||||
[]Token{tokOpen, tokID("."), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a path as "OPEN (ID SEP)* ID CLOSE"`,
|
||||
`{{../foo/bar}}`,
|
||||
[]Token{tokOpen, tokID(".."), tokSep("/"), tokID("foo"), tokSep("/"), tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a path with .. as a parent path`,
|
||||
`{{../foo.bar}}`,
|
||||
[]Token{tokOpen, tokID(".."), tokSep("/"), tokID("foo"), tokSep("."), tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a path with this/foo as OPEN ID SEP ID CLOSE`,
|
||||
`{{this/foo}}`,
|
||||
[]Token{tokOpen, tokID("this"), tokSep("/"), tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a simple mustache with spaces as "OPEN ID CLOSE"`,
|
||||
`{{ foo }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a simple mustache with line breaks as "OPEN ID ID CLOSE"`,
|
||||
"{{ foo \n bar }}",
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes raw content as "CONTENT"`,
|
||||
`foo {{ bar }} baz`,
|
||||
[]Token{tokContent("foo "), tokOpen, tokID("bar"), tokClose, tokContent(" baz"), tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a partial as "OPEN_PARTIAL ID CLOSE"`,
|
||||
`{{> foo}}`,
|
||||
[]Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"`,
|
||||
`{{> foo bar }}`,
|
||||
[]Token{tokOpenPartial, tokID("foo"), tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"`,
|
||||
`{{>foo}}`,
|
||||
[]Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"`,
|
||||
`{{>foo }}`,
|
||||
[]Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"`,
|
||||
`{{>foo/bar.baz }}`,
|
||||
[]Token{tokOpenPartial, tokID("foo"), tokSep("/"), tokID("bar"), tokSep("."), tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a comment as "COMMENT"`,
|
||||
`foo {{! this is a comment }} bar {{ baz }}`,
|
||||
[]Token{tokContent("foo "), tokComment("{{! this is a comment }}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a block comment as "COMMENT"`,
|
||||
`foo {{!-- this is a {{comment}} --}} bar {{ baz }}`,
|
||||
[]Token{tokContent("foo "), tokComment("{{!-- this is a {{comment}} --}}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a block comment with whitespace as "COMMENT"`,
|
||||
"foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}",
|
||||
[]Token{tokContent("foo "), tokComment("{{!-- this is a\n{{comment}}\n--}}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE`,
|
||||
`{{#foo}}content{{/foo}}`,
|
||||
[]Token{tokOpenBlock, tokID("foo"), tokClose, tokContent("content"), tokOpenEndBlock, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes inverse sections as "INVERSE"`,
|
||||
`{{^}}`,
|
||||
[]Token{tokInverse("{{^}}"), tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes inverse sections as "INVERSE" with alternate format`,
|
||||
`{{else}}`,
|
||||
[]Token{tokInverse("{{else}}"), tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes inverse sections as "INVERSE" with spaces`,
|
||||
`{{ else }}`,
|
||||
[]Token{tokInverse("{{ else }}"), tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"`,
|
||||
`{{^foo}}`,
|
||||
[]Token{tokOpenInverse, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"`,
|
||||
`{{^ foo }}`,
|
||||
[]Token{tokOpenInverse, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes mustaches with params as "OPEN ID ID ID CLOSE"`,
|
||||
`{{ foo bar baz }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"`,
|
||||
`{{ foo bar "baz" }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes mustaches with String params using single quotes as "OPEN ID ID STRING CLOSE"`,
|
||||
`{{ foo bar 'baz' }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes String params with spaces inside as "STRING"`,
|
||||
`{{ foo bar "baz bat" }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz bat"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes String params with escapes quotes as STRING`,
|
||||
`{{ foo "bar\"baz" }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokString(`bar"baz`), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes String params using single quotes with escapes quotes as STRING`,
|
||||
`{{ foo 'bar\'baz' }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokString(`bar'baz`), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes numbers`,
|
||||
`{{ foo 1 }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokNumber("1"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes floats`,
|
||||
`{{ foo 1.1 }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokNumber("1.1"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes negative numbers`,
|
||||
`{{ foo -1 }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokNumber("-1"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes negative floats`,
|
||||
`{{ foo -1.1 }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokNumber("-1.1"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes boolean true`,
|
||||
`{{ foo true }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokBool("true"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes boolean false`,
|
||||
`{{ foo false }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokBool("false"), tokClose, tokEOF},
|
||||
},
|
||||
// SKIP: 'tokenizes undefined and null'
|
||||
{
|
||||
`tokenizes hash arguments (1)`,
|
||||
`{{ foo bar=baz }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokEquals, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (2)`,
|
||||
`{{ foo bar baz=bat }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokID("bat"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (3)`,
|
||||
`{{ foo bar baz=1 }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokNumber("1"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (4)`,
|
||||
`{{ foo bar baz=true }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokBool("true"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (5)`,
|
||||
`{{ foo bar baz=false }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokBool("false"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (6)`,
|
||||
"{{ foo bar\n baz=bat }}",
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokID("bat"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (7)`,
|
||||
`{{ foo bar baz="bat" }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokString("bat"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (8)`,
|
||||
`{{ foo bar baz="bat" bam=wot }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokString("bat"), tokID("bam"), tokEquals, tokID("wot"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (9)`,
|
||||
`{{foo omg bar=baz bat="bam"}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("omg"), tokID("bar"), tokEquals, tokID("baz"), tokID("bat"), tokEquals, tokString("bam"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes special @ identifiers (1)`,
|
||||
`{{ @foo }}`,
|
||||
[]Token{tokOpen, tokData, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes special @ identifiers (2)`,
|
||||
`{{ foo @bar }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokData, tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes special @ identifiers (3)`,
|
||||
`{{ foo bar=@baz }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokEquals, tokData, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`does not time out in a mustache with a single } followed by EOF`,
|
||||
`{{foo}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokError("Unexpected character in expression: '}'")},
|
||||
},
|
||||
{
|
||||
`does not time out in a mustache when invalid ID characters are used`,
|
||||
`{{foo & }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokError("Unexpected character in expression: '&'")},
|
||||
},
|
||||
{
|
||||
`tokenizes subexpressions (1)`,
|
||||
`{{foo (bar)}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokCloseSexpr, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes subexpressions (2)`,
|
||||
`{{foo (a-x b-y)}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("a-x"), tokID("b-y"), tokCloseSexpr, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes nested subexpressions`,
|
||||
`{{foo (bar (lol rofl)) (baz)}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokOpenSexpr, tokID("lol"), tokID("rofl"), tokCloseSexpr, tokCloseSexpr, tokOpenSexpr, tokID("baz"), tokCloseSexpr, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes nested subexpressions: literals`,
|
||||
`{{foo (bar (lol true) false) (baz 1) (blah 'b') (blorg "c")}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokOpenSexpr, tokID("lol"), tokBool("true"), tokCloseSexpr, tokBool("false"), tokCloseSexpr, tokOpenSexpr, tokID("baz"), tokNumber("1"), tokCloseSexpr, tokOpenSexpr, tokID("blah"), tokString("b"), tokCloseSexpr, tokOpenSexpr, tokID("blorg"), tokString("c"), tokCloseSexpr, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes block params (1)`,
|
||||
`{{#foo as |bar|}}`,
|
||||
[]Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokCloseBlockParams, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes block params (2)`,
|
||||
`{{#foo as |bar baz|}}`,
|
||||
[]Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes block params (3)`,
|
||||
`{{#foo as | bar baz |}}`,
|
||||
[]Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes block params (4)`,
|
||||
`{{#foo as as | bar baz |}}`,
|
||||
[]Token{tokOpenBlock, tokID("foo"), tokID("as"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes block params (5)`,
|
||||
`{{else foo as |bar baz|}}`,
|
||||
[]Token{tokOpenInverseChain, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
|
||||
},
|
||||
}
|
||||
|
||||
func collect(t *lexTest) []Token {
|
||||
var result []Token
|
||||
|
||||
l := scanWithName(t.input, t.name)
|
||||
for {
|
||||
token := l.NextToken()
|
||||
result = append(result, token)
|
||||
|
||||
if token.Kind == TokenEOF || token.Kind == TokenError {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func equal(i1, i2 []Token, checkPos bool) bool {
|
||||
if len(i1) != len(i2) {
|
||||
return false
|
||||
}
|
||||
|
||||
for k := range i1 {
|
||||
if i1[k].Kind != i2[k].Kind {
|
||||
return false
|
||||
}
|
||||
|
||||
if checkPos && i1[k].Pos != i2[k].Pos {
|
||||
return false
|
||||
}
|
||||
|
||||
if i1[k].Val != i2[k].Val {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func TestLexer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range lexTests {
|
||||
tokens := collect(&test)
|
||||
if !equal(tokens, test.tokens, false) {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\nexpected\n\t%v\ngot\n\t%+v\n", test.name, test.input, test.tokens, tokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @todo Test errors:
|
||||
// `{{{{raw foo`
|
||||
|
||||
// package example
|
||||
func Example() {
|
||||
source := "You know {{nothing}} John Snow"
|
||||
|
||||
output := ""
|
||||
|
||||
lex := Scan(source)
|
||||
for {
|
||||
// consume next token
|
||||
token := lex.NextToken()
|
||||
|
||||
output += fmt.Sprintf(" %s", token)
|
||||
|
||||
// stops when all tokens have been consumed, or on error
|
||||
if token.Kind == TokenEOF || token.Kind == TokenError {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: Content{"You know "} Open{"{{"} ID{"nothing"} Close{"}}"} Content{" John Snow"} EOF
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
package lexer
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
TokenError TokenKind = iota
|
||||
TokenEOF
|
||||
|
||||
// mustache delimiters
|
||||
TokenOpen // OPEN
|
||||
TokenClose // CLOSE
|
||||
TokenOpenRawBlock // OPEN_RAW_BLOCK
|
||||
TokenCloseRawBlock // CLOSE_RAW_BLOCK
|
||||
TokenOpenEndRawBlock // END_RAW_BLOCK
|
||||
TokenOpenUnescaped // OPEN_UNESCAPED
|
||||
TokenCloseUnescaped // CLOSE_UNESCAPED
|
||||
TokenOpenBlock // OPEN_BLOCK
|
||||
TokenOpenEndBlock // OPEN_ENDBLOCK
|
||||
TokenInverse // INVERSE
|
||||
TokenOpenInverse // OPEN_INVERSE
|
||||
TokenOpenInverseChain // OPEN_INVERSE_CHAIN
|
||||
TokenOpenPartial // OPEN_PARTIAL
|
||||
TokenComment // COMMENT
|
||||
|
||||
// inside mustaches
|
||||
TokenOpenSexpr // OPEN_SEXPR
|
||||
TokenCloseSexpr // CLOSE_SEXPR
|
||||
TokenEquals // EQUALS
|
||||
TokenData // DATA
|
||||
TokenSep // SEP
|
||||
TokenOpenBlockParams // OPEN_BLOCK_PARAMS
|
||||
TokenCloseBlockParams // CLOSE_BLOCK_PARAMS
|
||||
|
||||
// tokens with content
|
||||
TokenContent // CONTENT
|
||||
TokenID // ID
|
||||
TokenString // STRING
|
||||
TokenNumber // NUMBER
|
||||
TokenBoolean // BOOLEAN
|
||||
)
|
||||
|
||||
const (
|
||||
// Option to generate token position in its string representation
|
||||
DUMP_TOKEN_POS = false
|
||||
|
||||
// Option to generate values for all token kinds for their string representations
|
||||
DUMP_ALL_TOKENS_VAL = true
|
||||
)
|
||||
|
||||
// TokenKind represents a Token type.
|
||||
type TokenKind int
|
||||
|
||||
// Token represents a scanned token.
|
||||
type Token struct {
|
||||
Kind TokenKind // Token kind
|
||||
Val string // Token value
|
||||
|
||||
Pos int // Byte position in input string
|
||||
Line int // Line number in input string
|
||||
}
|
||||
|
||||
// tokenName permits to display token name given token type
|
||||
var tokenName = map[TokenKind]string{
|
||||
TokenError: "Error",
|
||||
TokenEOF: "EOF",
|
||||
TokenContent: "Content",
|
||||
TokenComment: "Comment",
|
||||
TokenOpen: "Open",
|
||||
TokenClose: "Close",
|
||||
TokenOpenUnescaped: "OpenUnescaped",
|
||||
TokenCloseUnescaped: "CloseUnescaped",
|
||||
TokenOpenBlock: "OpenBlock",
|
||||
TokenOpenEndBlock: "OpenEndBlock",
|
||||
TokenOpenRawBlock: "OpenRawBlock",
|
||||
TokenCloseRawBlock: "CloseRawBlock",
|
||||
TokenOpenEndRawBlock: "OpenEndRawBlock",
|
||||
TokenOpenBlockParams: "OpenBlockParams",
|
||||
TokenCloseBlockParams: "CloseBlockParams",
|
||||
TokenInverse: "Inverse",
|
||||
TokenOpenInverse: "OpenInverse",
|
||||
TokenOpenInverseChain: "OpenInverseChain",
|
||||
TokenOpenPartial: "OpenPartial",
|
||||
TokenOpenSexpr: "OpenSexpr",
|
||||
TokenCloseSexpr: "CloseSexpr",
|
||||
TokenID: "ID",
|
||||
TokenEquals: "Equals",
|
||||
TokenString: "String",
|
||||
TokenNumber: "Number",
|
||||
TokenBoolean: "Boolean",
|
||||
TokenData: "Data",
|
||||
TokenSep: "Sep",
|
||||
}
|
||||
|
||||
// String returns the token kind string representation for debugging.
|
||||
func (k TokenKind) String() string {
|
||||
s := tokenName[k]
|
||||
if s == "" {
|
||||
return fmt.Sprintf("Token-%d", int(k))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// String returns the token string representation for debugging.
|
||||
func (t Token) String() string {
|
||||
result := ""
|
||||
|
||||
if DUMP_TOKEN_POS {
|
||||
result += fmt.Sprintf("%d:", t.Pos)
|
||||
}
|
||||
|
||||
result += fmt.Sprintf("%s", t.Kind)
|
||||
|
||||
if (DUMP_ALL_TOKENS_VAL || (t.Kind >= TokenContent)) && len(t.Val) > 0 {
|
||||
if len(t.Val) > 100 {
|
||||
result += fmt.Sprintf("{%.20q...}", t.Val)
|
||||
} else {
|
||||
result += fmt.Sprintf("{%q}", t.Val)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -1,234 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
//
|
||||
// Note, as the JS implementation, the divergences from mustache spec:
|
||||
// - we don't support alternative delimeters
|
||||
// - the mustache lambda spec differs
|
||||
//
|
||||
|
||||
type mustacheTest struct {
|
||||
Name string
|
||||
Desc string
|
||||
Data interface{}
|
||||
Template string
|
||||
Expected string
|
||||
Partials map[string]string
|
||||
}
|
||||
|
||||
type mustacheTestFile struct {
|
||||
Overview string
|
||||
Tests []mustacheTest
|
||||
}
|
||||
|
||||
var (
|
||||
rAltDelim = regexp.MustCompile(regexp.QuoteMeta("{{="))
|
||||
)
|
||||
|
||||
var (
|
||||
musTestLambdaInterMult = 0
|
||||
)
|
||||
|
||||
func TestMustache(t *testing.T) {
|
||||
skipFiles := map[string]bool{
|
||||
// mustache lambdas differ from handlebars lambdas
|
||||
"~lambdas.yml": true,
|
||||
}
|
||||
|
||||
for _, fileName := range mustacheTestFiles() {
|
||||
if skipFiles[fileName] {
|
||||
// fmt.Printf("Skipped file: %s\n", fileName)
|
||||
continue
|
||||
}
|
||||
|
||||
launchTests(t, testsFromMustacheFile(fileName))
|
||||
}
|
||||
}
|
||||
|
||||
func testsFromMustacheFile(fileName string) []Test {
|
||||
result := []Test{}
|
||||
|
||||
fileData, err := ioutil.ReadFile(path.Join("mustache", "specs", fileName))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var testFile mustacheTestFile
|
||||
if err := yaml.Unmarshal(fileData, &testFile); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, mustacheTest := range testFile.Tests {
|
||||
if mustBeSkipped(mustacheTest, fileName) {
|
||||
// fmt.Printf("Skipped test: %s\n", mustacheTest.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
test := Test{
|
||||
name: mustacheTest.Name,
|
||||
input: mustacheTest.Template,
|
||||
data: mustacheTest.Data,
|
||||
partials: mustacheTest.Partials,
|
||||
output: mustacheTest.Expected,
|
||||
}
|
||||
|
||||
result = append(result, test)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// returns true if test must be skipped
|
||||
func mustBeSkipped(test mustacheTest, fileName string) bool {
|
||||
// handlebars does not support alternative delimiters
|
||||
return haveAltDelimiter(test) ||
|
||||
// the JS implementation skips those tests
|
||||
fileName == "partials.yml" && (test.Name == "Failed Lookup" || test.Name == "Standalone Indentation")
|
||||
}
|
||||
|
||||
// returns true if test have alternative delimeter in template or in partials
|
||||
func haveAltDelimiter(test mustacheTest) bool {
|
||||
// check template
|
||||
if rAltDelim.MatchString(test.Template) {
|
||||
return true
|
||||
}
|
||||
|
||||
// check partials
|
||||
for _, partial := range test.Partials {
|
||||
if rAltDelim.MatchString(partial) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func mustacheTestFiles() []string {
|
||||
var result []string
|
||||
|
||||
files, err := ioutil.ReadDir(path.Join("mustache", "specs"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
fileName := file.Name()
|
||||
|
||||
if !file.IsDir() && strings.HasSuffix(fileName, ".yml") {
|
||||
result = append(result, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
//
|
||||
// Following tests come fron ~lambdas.yml
|
||||
//
|
||||
|
||||
var mustacheLambdasTests = []Test{
|
||||
{
|
||||
"Interpolation",
|
||||
"Hello, {{lambda}}!",
|
||||
map[string]interface{}{"lambda": func() string { return "world" }},
|
||||
nil, nil, nil,
|
||||
"Hello, world!",
|
||||
},
|
||||
|
||||
// // SKIP: lambda return value is not parsed
|
||||
// {
|
||||
// "Interpolation - Expansion",
|
||||
// "Hello, {{lambda}}!",
|
||||
// map[string]interface{}{"lambda": func() string { return "{{planet}}" }},
|
||||
// nil, nil, nil,
|
||||
// "Hello, world!",
|
||||
// },
|
||||
|
||||
// SKIP "Interpolation - Alternate Delimiters"
|
||||
|
||||
{
|
||||
"Interpolation - Multiple Calls",
|
||||
"{{lambda}} == {{{lambda}}} == {{lambda}}",
|
||||
map[string]interface{}{"lambda": func() string {
|
||||
musTestLambdaInterMult += 1
|
||||
return Str(musTestLambdaInterMult)
|
||||
}},
|
||||
nil, nil, nil,
|
||||
"1 == 2 == 3",
|
||||
},
|
||||
|
||||
{
|
||||
"Escaping",
|
||||
"<{{lambda}}{{{lambda}}}",
|
||||
map[string]interface{}{"lambda": func() string { return ">" }},
|
||||
nil, nil, nil,
|
||||
"<>>",
|
||||
},
|
||||
|
||||
// // SKIP: "Lambdas used for sections should receive the raw section string."
|
||||
// {
|
||||
// "Section",
|
||||
// "<{{#lambda}}{{x}}{{/lambda}}>",
|
||||
// map[string]interface{}{"lambda": func(param string) string {
|
||||
// if param == "{{x}}" {
|
||||
// return "yes"
|
||||
// }
|
||||
|
||||
// return "false"
|
||||
// }, "x": "Error!"},
|
||||
// nil, nil, nil,
|
||||
// "<yes>",
|
||||
// },
|
||||
|
||||
// // SKIP: lambda return value is not parsed
|
||||
// {
|
||||
// "Section - Expansion",
|
||||
// "<{{#lambda}}-{{/lambda}}>",
|
||||
// map[string]interface{}{"lambda": func(param string) string {
|
||||
// return param + "{{planet}}" + param
|
||||
// }, "planet": "Earth"},
|
||||
// nil, nil, nil,
|
||||
// "<-Earth->",
|
||||
// },
|
||||
|
||||
// SKIP: "Section - Alternate Delimiters"
|
||||
|
||||
{
|
||||
"Section - Multiple Calls",
|
||||
"{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}",
|
||||
map[string]interface{}{"lambda": func(options *Options) string {
|
||||
return "__" + options.Fn() + "__"
|
||||
}},
|
||||
nil, nil, nil,
|
||||
"__FILE__ != __LINE__",
|
||||
},
|
||||
|
||||
// // SKIP: "Lambdas used for inverted sections should be considered truthy."
|
||||
// {
|
||||
// "Inverted Section",
|
||||
// "<{{^lambda}}{{static}}{{/lambda}}>",
|
||||
// map[string]interface{}{
|
||||
// "lambda": func() interface{} {
|
||||
// return false
|
||||
// },
|
||||
// "static": "static",
|
||||
// },
|
||||
// nil, nil, nil,
|
||||
// "<>",
|
||||
// },
|
||||
}
|
||||
|
||||
func TestMustacheLambdas(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
launchTests(t, mustacheLambdasTests)
|
||||
}
|
|
@ -1,846 +0,0 @@
|
|||
// Package parser provides a handlebars syntax analyser. It consumes the tokens provided by the lexer to build an AST.
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/aymerick/raymond/ast"
|
||||
"github.com/aymerick/raymond/lexer"
|
||||
)
|
||||
|
||||
// References:
|
||||
// - https://github.com/wycats/handlebars.js/blob/master/src/handlebars.yy
|
||||
// - https://github.com/golang/go/blob/master/src/text/template/parse/parse.go
|
||||
|
||||
// Parser is a syntax analyzer.
|
||||
type parser struct {
|
||||
// Lexer
|
||||
lex *lexer.Lexer
|
||||
|
||||
// Root node
|
||||
root ast.Node
|
||||
|
||||
// Tokens parsed but not consumed yet
|
||||
tokens []*lexer.Token
|
||||
|
||||
// All tokens have been retreieved from lexer
|
||||
lexOver bool
|
||||
}
|
||||
|
||||
var (
|
||||
rOpenComment = regexp.MustCompile(`^\{\{~?!-?-?`)
|
||||
rCloseComment = regexp.MustCompile(`-?-?~?\}\}$`)
|
||||
rOpenAmp = regexp.MustCompile(`^\{\{~?&`)
|
||||
)
|
||||
|
||||
// new instanciates a new parser
|
||||
func new(input string) *parser {
|
||||
return &parser{
|
||||
lex: lexer.Scan(input),
|
||||
}
|
||||
}
|
||||
|
||||
// Parse analyzes given input and returns the AST root node.
|
||||
func Parse(input string) (result *ast.Program, err error) {
|
||||
// recover error
|
||||
defer errRecover(&err)
|
||||
|
||||
parser := new(input)
|
||||
|
||||
// parse
|
||||
result = parser.parseProgram()
|
||||
|
||||
// check last token
|
||||
token := parser.shift()
|
||||
if token.Kind != lexer.TokenEOF {
|
||||
// Parsing ended before EOF
|
||||
errToken(token, "Syntax error")
|
||||
}
|
||||
|
||||
// fix whitespaces
|
||||
processWhitespaces(result)
|
||||
|
||||
// named returned values
|
||||
return
|
||||
}
|
||||
|
||||
// errRecover recovers parsing panic
|
||||
func errRecover(errp *error) {
|
||||
e := recover()
|
||||
if e != nil {
|
||||
switch err := e.(type) {
|
||||
case runtime.Error:
|
||||
panic(e)
|
||||
case error:
|
||||
*errp = err
|
||||
default:
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errPanic panics
|
||||
func errPanic(err error, line int) {
|
||||
panic(fmt.Errorf("Parse error on line %d:\n%s", line, err))
|
||||
}
|
||||
|
||||
// errNode panics with given node infos
|
||||
func errNode(node ast.Node, msg string) {
|
||||
errPanic(fmt.Errorf("%s\nNode: %s", msg, node), node.Location().Line)
|
||||
}
|
||||
|
||||
// errNode panics with given Token infos
|
||||
func errToken(tok *lexer.Token, msg string) {
|
||||
errPanic(fmt.Errorf("%s\nToken: %s", msg, tok), tok.Line)
|
||||
}
|
||||
|
||||
// errNode panics because of an unexpected Token kind
|
||||
func errExpected(expect lexer.TokenKind, tok *lexer.Token) {
|
||||
errPanic(fmt.Errorf("Expecting %s, got: '%s'", expect, tok), tok.Line)
|
||||
}
|
||||
|
||||
// program : statement*
|
||||
func (p *parser) parseProgram() *ast.Program {
|
||||
result := ast.NewProgram(p.lex.Pos(), p.lex.Line())
|
||||
|
||||
for p.isStatement() {
|
||||
result.AddStatement(p.parseStatement())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// statement : mustache | block | rawBlock | partial | content | COMMENT
|
||||
func (p *parser) parseStatement() ast.Node {
|
||||
var result ast.Node
|
||||
|
||||
tok := p.next()
|
||||
|
||||
switch tok.Kind {
|
||||
case lexer.TokenOpen, lexer.TokenOpenUnescaped:
|
||||
// mustache
|
||||
result = p.parseMustache()
|
||||
case lexer.TokenOpenBlock:
|
||||
// block
|
||||
result = p.parseBlock()
|
||||
case lexer.TokenOpenInverse:
|
||||
// block
|
||||
result = p.parseInverse()
|
||||
case lexer.TokenOpenRawBlock:
|
||||
// rawBlock
|
||||
result = p.parseRawBlock()
|
||||
case lexer.TokenOpenPartial:
|
||||
// partial
|
||||
result = p.parsePartial()
|
||||
case lexer.TokenContent:
|
||||
// content
|
||||
result = p.parseContent()
|
||||
case lexer.TokenComment:
|
||||
// COMMENT
|
||||
result = p.parseComment()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// isStatement returns true if next token starts a statement
|
||||
func (p *parser) isStatement() bool {
|
||||
if !p.have(1) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch p.next().Kind {
|
||||
case lexer.TokenOpen, lexer.TokenOpenUnescaped, lexer.TokenOpenBlock,
|
||||
lexer.TokenOpenInverse, lexer.TokenOpenRawBlock, lexer.TokenOpenPartial,
|
||||
lexer.TokenContent, lexer.TokenComment:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// content : CONTENT
|
||||
func (p *parser) parseContent() *ast.ContentStatement {
|
||||
// CONTENT
|
||||
tok := p.shift()
|
||||
if tok.Kind != lexer.TokenContent {
|
||||
// @todo This check can be removed if content is optional in a raw block
|
||||
errExpected(lexer.TokenContent, tok)
|
||||
}
|
||||
|
||||
return ast.NewContentStatement(tok.Pos, tok.Line, tok.Val)
|
||||
}
|
||||
|
||||
// COMMENT
|
||||
func (p *parser) parseComment() *ast.CommentStatement {
|
||||
// COMMENT
|
||||
tok := p.shift()
|
||||
|
||||
value := rOpenComment.ReplaceAllString(tok.Val, "")
|
||||
value = rCloseComment.ReplaceAllString(value, "")
|
||||
|
||||
result := ast.NewCommentStatement(tok.Pos, tok.Line, value)
|
||||
result.Strip = ast.NewStripForStr(tok.Val)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// param* hash?
|
||||
func (p *parser) parseExpressionParamsHash() ([]ast.Node, *ast.Hash) {
|
||||
var params []ast.Node
|
||||
var hash *ast.Hash
|
||||
|
||||
// params*
|
||||
if p.isParam() {
|
||||
params = p.parseParams()
|
||||
}
|
||||
|
||||
// hash?
|
||||
if p.isHashSegment() {
|
||||
hash = p.parseHash()
|
||||
}
|
||||
|
||||
return params, hash
|
||||
}
|
||||
|
||||
// helperName param* hash?
|
||||
func (p *parser) parseExpression(tok *lexer.Token) *ast.Expression {
|
||||
result := ast.NewExpression(tok.Pos, tok.Line)
|
||||
|
||||
// helperName
|
||||
result.Path = p.parseHelperName()
|
||||
|
||||
// param* hash?
|
||||
result.Params, result.Hash = p.parseExpressionParamsHash()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// rawBlock : openRawBlock content endRawBlock
|
||||
// openRawBlock : OPEN_RAW_BLOCK helperName param* hash? CLOSE_RAW_BLOCK
|
||||
// endRawBlock : OPEN_END_RAW_BLOCK helperName CLOSE_RAW_BLOCK
|
||||
func (p *parser) parseRawBlock() *ast.BlockStatement {
|
||||
// OPEN_RAW_BLOCK
|
||||
tok := p.shift()
|
||||
|
||||
result := ast.NewBlockStatement(tok.Pos, tok.Line)
|
||||
|
||||
// helperName param* hash?
|
||||
result.Expression = p.parseExpression(tok)
|
||||
|
||||
openName := result.Expression.Canonical()
|
||||
|
||||
// CLOSE_RAW_BLOCK
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenCloseRawBlock {
|
||||
errExpected(lexer.TokenCloseRawBlock, tok)
|
||||
}
|
||||
|
||||
// content
|
||||
// @todo Is content mandatory in a raw block ?
|
||||
content := p.parseContent()
|
||||
|
||||
program := ast.NewProgram(tok.Pos, tok.Line)
|
||||
program.AddStatement(content)
|
||||
|
||||
result.Program = program
|
||||
|
||||
// OPEN_END_RAW_BLOCK
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenOpenEndRawBlock {
|
||||
// should never happen as it is caught by lexer
|
||||
errExpected(lexer.TokenOpenEndRawBlock, tok)
|
||||
}
|
||||
|
||||
// helperName
|
||||
endId := p.parseHelperName()
|
||||
|
||||
closeName, ok := ast.HelperNameStr(endId)
|
||||
if !ok {
|
||||
errNode(endId, "Erroneous closing expression")
|
||||
}
|
||||
|
||||
if openName != closeName {
|
||||
errNode(endId, fmt.Sprintf("%s doesn't match %s", openName, closeName))
|
||||
}
|
||||
|
||||
// CLOSE_RAW_BLOCK
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenCloseRawBlock {
|
||||
errExpected(lexer.TokenCloseRawBlock, tok)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// block : openBlock program inverseChain? closeBlock
|
||||
func (p *parser) parseBlock() *ast.BlockStatement {
|
||||
// openBlock
|
||||
result, blockParams := p.parseOpenBlock()
|
||||
|
||||
// program
|
||||
program := p.parseProgram()
|
||||
program.BlockParams = blockParams
|
||||
result.Program = program
|
||||
|
||||
// inverseChain?
|
||||
if p.isInverseChain() {
|
||||
result.Inverse = p.parseInverseChain()
|
||||
}
|
||||
|
||||
// closeBlock
|
||||
p.parseCloseBlock(result)
|
||||
|
||||
setBlockInverseStrip(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// setBlockInverseStrip is called when parsing `block` (openBlock | openInverse) and `inverseChain`
|
||||
//
|
||||
// TODO: This was totally cargo culted ! CHECK THAT !
|
||||
//
|
||||
// cf. prepareBlock() in:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/helper.js
|
||||
func setBlockInverseStrip(block *ast.BlockStatement) {
|
||||
if block.Inverse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if block.Inverse.Chained {
|
||||
b, _ := block.Inverse.Body[0].(*ast.BlockStatement)
|
||||
b.CloseStrip = block.CloseStrip
|
||||
}
|
||||
|
||||
block.InverseStrip = block.Inverse.Strip
|
||||
}
|
||||
|
||||
// block : openInverse program inverseAndProgram? closeBlock
|
||||
func (p *parser) parseInverse() *ast.BlockStatement {
|
||||
// openInverse
|
||||
result, blockParams := p.parseOpenBlock()
|
||||
|
||||
// program
|
||||
program := p.parseProgram()
|
||||
|
||||
program.BlockParams = blockParams
|
||||
result.Inverse = program
|
||||
|
||||
// inverseAndProgram?
|
||||
if p.isInverse() {
|
||||
result.Program = p.parseInverseAndProgram()
|
||||
}
|
||||
|
||||
// closeBlock
|
||||
p.parseCloseBlock(result)
|
||||
|
||||
setBlockInverseStrip(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// helperName param* hash? blockParams?
|
||||
func (p *parser) parseOpenBlockExpression(tok *lexer.Token) (*ast.BlockStatement, []string) {
|
||||
var blockParams []string
|
||||
|
||||
result := ast.NewBlockStatement(tok.Pos, tok.Line)
|
||||
|
||||
// helperName param* hash?
|
||||
result.Expression = p.parseExpression(tok)
|
||||
|
||||
// blockParams?
|
||||
if p.isBlockParams() {
|
||||
blockParams = p.parseBlockParams()
|
||||
}
|
||||
|
||||
// named returned values
|
||||
return result, blockParams
|
||||
}
|
||||
|
||||
// inverseChain : openInverseChain program inverseChain?
|
||||
// | inverseAndProgram
|
||||
func (p *parser) parseInverseChain() *ast.Program {
|
||||
if p.isInverse() {
|
||||
// inverseAndProgram
|
||||
return p.parseInverseAndProgram()
|
||||
} else {
|
||||
result := ast.NewProgram(p.lex.Pos(), p.lex.Line())
|
||||
|
||||
// openInverseChain
|
||||
block, blockParams := p.parseOpenBlock()
|
||||
|
||||
// program
|
||||
program := p.parseProgram()
|
||||
|
||||
program.BlockParams = blockParams
|
||||
block.Program = program
|
||||
|
||||
// inverseChain?
|
||||
if p.isInverseChain() {
|
||||
block.Inverse = p.parseInverseChain()
|
||||
}
|
||||
|
||||
setBlockInverseStrip(block)
|
||||
|
||||
result.Chained = true
|
||||
result.AddStatement(block)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if current token starts an inverse chain
|
||||
func (p *parser) isInverseChain() bool {
|
||||
return p.isOpenInverseChain() || p.isInverse()
|
||||
}
|
||||
|
||||
// inverseAndProgram : INVERSE program
|
||||
func (p *parser) parseInverseAndProgram() *ast.Program {
|
||||
// INVERSE
|
||||
tok := p.shift()
|
||||
|
||||
// program
|
||||
result := p.parseProgram()
|
||||
result.Strip = ast.NewStripForStr(tok.Val)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// openBlock : OPEN_BLOCK helperName param* hash? blockParams? CLOSE
|
||||
// openInverse : OPEN_INVERSE helperName param* hash? blockParams? CLOSE
|
||||
// openInverseChain: OPEN_INVERSE_CHAIN helperName param* hash? blockParams? CLOSE
|
||||
func (p *parser) parseOpenBlock() (*ast.BlockStatement, []string) {
|
||||
// OPEN_BLOCK | OPEN_INVERSE | OPEN_INVERSE_CHAIN
|
||||
tok := p.shift()
|
||||
|
||||
// helperName param* hash? blockParams?
|
||||
result, blockParams := p.parseOpenBlockExpression(tok)
|
||||
|
||||
// CLOSE
|
||||
tokClose := p.shift()
|
||||
if tokClose.Kind != lexer.TokenClose {
|
||||
errExpected(lexer.TokenClose, tokClose)
|
||||
}
|
||||
|
||||
result.OpenStrip = ast.NewStrip(tok.Val, tokClose.Val)
|
||||
|
||||
// named returned values
|
||||
return result, blockParams
|
||||
}
|
||||
|
||||
// closeBlock : OPEN_ENDBLOCK helperName CLOSE
|
||||
func (p *parser) parseCloseBlock(block *ast.BlockStatement) {
|
||||
// OPEN_ENDBLOCK
|
||||
tok := p.shift()
|
||||
if tok.Kind != lexer.TokenOpenEndBlock {
|
||||
errExpected(lexer.TokenOpenEndBlock, tok)
|
||||
}
|
||||
|
||||
// helperName
|
||||
endId := p.parseHelperName()
|
||||
|
||||
closeName, ok := ast.HelperNameStr(endId)
|
||||
if !ok {
|
||||
errNode(endId, "Erroneous closing expression")
|
||||
}
|
||||
|
||||
openName := block.Expression.Canonical()
|
||||
if openName != closeName {
|
||||
errNode(endId, fmt.Sprintf("%s doesn't match %s", openName, closeName))
|
||||
}
|
||||
|
||||
// CLOSE
|
||||
tokClose := p.shift()
|
||||
if tokClose.Kind != lexer.TokenClose {
|
||||
errExpected(lexer.TokenClose, tokClose)
|
||||
}
|
||||
|
||||
block.CloseStrip = ast.NewStrip(tok.Val, tokClose.Val)
|
||||
}
|
||||
|
||||
// mustache : OPEN helperName param* hash? CLOSE
|
||||
// | OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED
|
||||
func (p *parser) parseMustache() *ast.MustacheStatement {
|
||||
// OPEN | OPEN_UNESCAPED
|
||||
tok := p.shift()
|
||||
|
||||
closeToken := lexer.TokenClose
|
||||
if tok.Kind == lexer.TokenOpenUnescaped {
|
||||
closeToken = lexer.TokenCloseUnescaped
|
||||
}
|
||||
|
||||
unescaped := false
|
||||
if (tok.Kind == lexer.TokenOpenUnescaped) || (rOpenAmp.MatchString(tok.Val)) {
|
||||
unescaped = true
|
||||
}
|
||||
|
||||
result := ast.NewMustacheStatement(tok.Pos, tok.Line, unescaped)
|
||||
|
||||
// helperName param* hash?
|
||||
result.Expression = p.parseExpression(tok)
|
||||
|
||||
// CLOSE | CLOSE_UNESCAPED
|
||||
tokClose := p.shift()
|
||||
if tokClose.Kind != closeToken {
|
||||
errExpected(closeToken, tokClose)
|
||||
}
|
||||
|
||||
result.Strip = ast.NewStrip(tok.Val, tokClose.Val)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// partial : OPEN_PARTIAL partialName param* hash? CLOSE
|
||||
func (p *parser) parsePartial() *ast.PartialStatement {
|
||||
// OPEN_PARTIAL
|
||||
tok := p.shift()
|
||||
|
||||
result := ast.NewPartialStatement(tok.Pos, tok.Line)
|
||||
|
||||
// partialName
|
||||
result.Name = p.parsePartialName()
|
||||
|
||||
// param* hash?
|
||||
result.Params, result.Hash = p.parseExpressionParamsHash()
|
||||
|
||||
// CLOSE
|
||||
tokClose := p.shift()
|
||||
if tokClose.Kind != lexer.TokenClose {
|
||||
errExpected(lexer.TokenClose, tokClose)
|
||||
}
|
||||
|
||||
result.Strip = ast.NewStrip(tok.Val, tokClose.Val)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// helperName | sexpr
|
||||
func (p *parser) parseHelperNameOrSexpr() ast.Node {
|
||||
if p.isSexpr() {
|
||||
// sexpr
|
||||
return p.parseSexpr()
|
||||
} else {
|
||||
// helperName
|
||||
return p.parseHelperName()
|
||||
}
|
||||
}
|
||||
|
||||
// param : helperName | sexpr
|
||||
func (p *parser) parseParam() ast.Node {
|
||||
return p.parseHelperNameOrSexpr()
|
||||
}
|
||||
|
||||
// Returns true if next tokens represent a `param`
|
||||
func (p *parser) isParam() bool {
|
||||
return (p.isSexpr() || p.isHelperName()) && !p.isHashSegment()
|
||||
}
|
||||
|
||||
// param*
|
||||
func (p *parser) parseParams() []ast.Node {
|
||||
var result []ast.Node
|
||||
|
||||
for p.isParam() {
|
||||
result = append(result, p.parseParam())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// sexpr : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR
|
||||
func (p *parser) parseSexpr() *ast.SubExpression {
|
||||
// OPEN_SEXPR
|
||||
tok := p.shift()
|
||||
|
||||
result := ast.NewSubExpression(tok.Pos, tok.Line)
|
||||
|
||||
// helperName param* hash?
|
||||
result.Expression = p.parseExpression(tok)
|
||||
|
||||
// CLOSE_SEXPR
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenCloseSexpr {
|
||||
errExpected(lexer.TokenCloseSexpr, tok)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// hash : hashSegment+
|
||||
func (p *parser) parseHash() *ast.Hash {
|
||||
var pairs []*ast.HashPair
|
||||
|
||||
for p.isHashSegment() {
|
||||
pairs = append(pairs, p.parseHashSegment())
|
||||
}
|
||||
|
||||
firstLoc := pairs[0].Location()
|
||||
|
||||
result := ast.NewHash(firstLoc.Pos, firstLoc.Line)
|
||||
result.Pairs = pairs
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// returns true if next tokens represents a `hashSegment`
|
||||
func (p *parser) isHashSegment() bool {
|
||||
return p.have(2) && (p.next().Kind == lexer.TokenID) && (p.nextAt(1).Kind == lexer.TokenEquals)
|
||||
}
|
||||
|
||||
// hashSegment : ID EQUALS param
|
||||
func (p *parser) parseHashSegment() *ast.HashPair {
|
||||
// ID
|
||||
tok := p.shift()
|
||||
|
||||
// EQUALS
|
||||
p.shift()
|
||||
|
||||
// param
|
||||
param := p.parseParam()
|
||||
|
||||
result := ast.NewHashPair(tok.Pos, tok.Line)
|
||||
result.Key = tok.Val
|
||||
result.Val = param
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// blockParams : OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS
|
||||
func (p *parser) parseBlockParams() []string {
|
||||
var result []string
|
||||
|
||||
// OPEN_BLOCK_PARAMS
|
||||
tok := p.shift()
|
||||
|
||||
// ID+
|
||||
for p.isID() {
|
||||
result = append(result, p.shift().Val)
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
errExpected(lexer.TokenID, p.next())
|
||||
}
|
||||
|
||||
// CLOSE_BLOCK_PARAMS
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenCloseBlockParams {
|
||||
errExpected(lexer.TokenCloseBlockParams, tok)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// helperName : path | dataName | STRING | NUMBER | BOOLEAN | UNDEFINED | NULL
|
||||
func (p *parser) parseHelperName() ast.Node {
|
||||
var result ast.Node
|
||||
|
||||
tok := p.next()
|
||||
|
||||
switch tok.Kind {
|
||||
case lexer.TokenBoolean:
|
||||
// BOOLEAN
|
||||
p.shift()
|
||||
result = ast.NewBooleanLiteral(tok.Pos, tok.Line, (tok.Val == "true"), tok.Val)
|
||||
case lexer.TokenNumber:
|
||||
// NUMBER
|
||||
p.shift()
|
||||
|
||||
val, isInt := parseNumber(tok)
|
||||
result = ast.NewNumberLiteral(tok.Pos, tok.Line, val, isInt, tok.Val)
|
||||
case lexer.TokenString:
|
||||
// STRING
|
||||
p.shift()
|
||||
result = ast.NewStringLiteral(tok.Pos, tok.Line, tok.Val)
|
||||
case lexer.TokenData:
|
||||
// dataName
|
||||
result = p.parseDataName()
|
||||
default:
|
||||
// path
|
||||
result = p.parsePath(false)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// parseNumber parses a number
|
||||
func parseNumber(tok *lexer.Token) (result float64, isInt bool) {
|
||||
var valInt int
|
||||
var err error
|
||||
|
||||
valInt, err = strconv.Atoi(tok.Val)
|
||||
if err == nil {
|
||||
isInt = true
|
||||
|
||||
result = float64(valInt)
|
||||
} else {
|
||||
isInt = false
|
||||
|
||||
result, err = strconv.ParseFloat(tok.Val, 64)
|
||||
if err != nil {
|
||||
errToken(tok, fmt.Sprintf("Failed to parse number: %s", tok.Val))
|
||||
}
|
||||
}
|
||||
|
||||
// named returned values
|
||||
return
|
||||
}
|
||||
|
||||
// Returns true if next tokens represent a `helperName`
|
||||
func (p *parser) isHelperName() bool {
|
||||
switch p.next().Kind {
|
||||
case lexer.TokenBoolean, lexer.TokenNumber, lexer.TokenString, lexer.TokenData, lexer.TokenID:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// partialName : helperName | sexpr
|
||||
func (p *parser) parsePartialName() ast.Node {
|
||||
return p.parseHelperNameOrSexpr()
|
||||
}
|
||||
|
||||
// dataName : DATA pathSegments
|
||||
func (p *parser) parseDataName() *ast.PathExpression {
|
||||
// DATA
|
||||
p.shift()
|
||||
|
||||
// pathSegments
|
||||
return p.parsePath(true)
|
||||
}
|
||||
|
||||
// path : pathSegments
|
||||
// pathSegments : pathSegments SEP ID
|
||||
// | ID
|
||||
func (p *parser) parsePath(data bool) *ast.PathExpression {
|
||||
var tok *lexer.Token
|
||||
|
||||
// ID
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenID {
|
||||
errExpected(lexer.TokenID, tok)
|
||||
}
|
||||
|
||||
result := ast.NewPathExpression(tok.Pos, tok.Line, data)
|
||||
result.Part(tok.Val)
|
||||
|
||||
for p.isPathSep() {
|
||||
// SEP
|
||||
tok = p.shift()
|
||||
result.Sep(tok.Val)
|
||||
|
||||
// ID
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenID {
|
||||
errExpected(lexer.TokenID, tok)
|
||||
}
|
||||
|
||||
result.Part(tok.Val)
|
||||
|
||||
if len(result.Parts) > 0 {
|
||||
switch tok.Val {
|
||||
case "..", ".", "this":
|
||||
errToken(tok, "Invalid path: "+result.Original)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Ensures there is token to parse at given index
|
||||
func (p *parser) ensure(index int) {
|
||||
if p.lexOver {
|
||||
// nothing more to grab
|
||||
return
|
||||
}
|
||||
|
||||
nb := index + 1
|
||||
|
||||
for len(p.tokens) < nb {
|
||||
// fetch next token
|
||||
tok := p.lex.NextToken()
|
||||
|
||||
// queue it
|
||||
p.tokens = append(p.tokens, &tok)
|
||||
|
||||
if (tok.Kind == lexer.TokenEOF) || (tok.Kind == lexer.TokenError) {
|
||||
p.lexOver = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// have returns true is there are a list given number of tokens to consume left
|
||||
func (p *parser) have(nb int) bool {
|
||||
p.ensure(nb - 1)
|
||||
|
||||
return len(p.tokens) >= nb
|
||||
}
|
||||
|
||||
// nextAt returns next token at given index, without consuming it
|
||||
func (p *parser) nextAt(index int) *lexer.Token {
|
||||
p.ensure(index)
|
||||
|
||||
return p.tokens[index]
|
||||
}
|
||||
|
||||
// next returns next token without consuming it
|
||||
func (p *parser) next() *lexer.Token {
|
||||
return p.nextAt(0)
|
||||
}
|
||||
|
||||
// shift returns next token and remove it from the tokens buffer
|
||||
//
|
||||
// Panics if next token is `TokenError`
|
||||
func (p *parser) shift() *lexer.Token {
|
||||
var result *lexer.Token
|
||||
|
||||
p.ensure(0)
|
||||
|
||||
result, p.tokens = p.tokens[0], p.tokens[1:]
|
||||
|
||||
// check error token
|
||||
if result.Kind == lexer.TokenError {
|
||||
errToken(result, "Lexer error")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// isToken returns true if next token is of given type
|
||||
func (p *parser) isToken(kind lexer.TokenKind) bool {
|
||||
return p.have(1) && p.next().Kind == kind
|
||||
}
|
||||
|
||||
// isSexpr returns true if next token starts a sexpr
|
||||
func (p *parser) isSexpr() bool {
|
||||
return p.isToken(lexer.TokenOpenSexpr)
|
||||
}
|
||||
|
||||
// isPathSep returns true if next token is a path separator
|
||||
func (p *parser) isPathSep() bool {
|
||||
return p.isToken(lexer.TokenSep)
|
||||
}
|
||||
|
||||
// isID returns true if next token is an ID
|
||||
func (p *parser) isID() bool {
|
||||
return p.isToken(lexer.TokenID)
|
||||
}
|
||||
|
||||
// isBlockParams returns true if next token starts a block params
|
||||
func (p *parser) isBlockParams() bool {
|
||||
return p.isToken(lexer.TokenOpenBlockParams)
|
||||
}
|
||||
|
||||
// isInverse returns true if next token starts an INVERSE sequence
|
||||
func (p *parser) isInverse() bool {
|
||||
return p.isToken(lexer.TokenInverse)
|
||||
}
|
||||
|
||||
// isOpenInverseChain returns true if next token is OPEN_INVERSE_CHAIN
|
||||
func (p *parser) isOpenInverseChain() bool {
|
||||
return p.isToken(lexer.TokenOpenInverseChain)
|
||||
}
|
|
@ -1,200 +0,0 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/aymerick/raymond/ast"
|
||||
"github.com/aymerick/raymond/lexer"
|
||||
)
|
||||
|
||||
type parserTest struct {
|
||||
name string
|
||||
input string
|
||||
output string
|
||||
}
|
||||
|
||||
var parserTests = []parserTest{
|
||||
//
|
||||
// Next tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/parser.js
|
||||
//
|
||||
{"parses simple mustaches (1)", `{{123}}`, "{{ NUMBER{123} [] }}\n"},
|
||||
{"parses simple mustaches (2)", `{{"foo"}}`, "{{ \"foo\" [] }}\n"},
|
||||
{"parses simple mustaches (3)", `{{false}}`, "{{ BOOLEAN{false} [] }}\n"},
|
||||
{"parses simple mustaches (4)", `{{true}}`, "{{ BOOLEAN{true} [] }}\n"},
|
||||
{"parses simple mustaches (5)", `{{foo}}`, "{{ PATH:foo [] }}\n"},
|
||||
{"parses simple mustaches (6)", `{{foo?}}`, "{{ PATH:foo? [] }}\n"},
|
||||
{"parses simple mustaches (7)", `{{foo_}}`, "{{ PATH:foo_ [] }}\n"},
|
||||
{"parses simple mustaches (8)", `{{foo-}}`, "{{ PATH:foo- [] }}\n"},
|
||||
{"parses simple mustaches (9)", `{{foo:}}`, "{{ PATH:foo: [] }}\n"},
|
||||
|
||||
{"parses simple mustaches with data", `{{@foo}}`, "{{ @PATH:foo [] }}\n"},
|
||||
{"parses simple mustaches with data paths", `{{@../foo}}`, "{{ @PATH:foo [] }}\n"},
|
||||
{"parses mustaches with paths", `{{foo/bar}}`, "{{ PATH:foo/bar [] }}\n"},
|
||||
{"parses mustaches with this/foo", `{{this/foo}}`, "{{ PATH:foo [] }}\n"},
|
||||
{"parses mustaches with - in a path", `{{foo-bar}}`, "{{ PATH:foo-bar [] }}\n"},
|
||||
{"parses mustaches with parameters", `{{foo bar}}`, "{{ PATH:foo [PATH:bar] }}\n"},
|
||||
{"parses mustaches with string parameters", `{{foo bar "baz" }}`, "{{ PATH:foo [PATH:bar, \"baz\"] }}\n"},
|
||||
{"parses mustaches with NUMBER parameters", `{{foo 1}}`, "{{ PATH:foo [NUMBER{1}] }}\n"},
|
||||
{"parses mustaches with BOOLEAN parameters (1)", `{{foo true}}`, "{{ PATH:foo [BOOLEAN{true}] }}\n"},
|
||||
{"parses mustaches with BOOLEAN parameters (2)", `{{foo false}}`, "{{ PATH:foo [BOOLEAN{false}] }}\n"},
|
||||
{"parses mustaches with DATA parameters", `{{foo @bar}}`, "{{ PATH:foo [@PATH:bar] }}\n"},
|
||||
|
||||
{"parses mustaches with hash arguments (01)", `{{foo bar=baz}}`, "{{ PATH:foo [] HASH{bar=PATH:baz} }}\n"},
|
||||
{"parses mustaches with hash arguments (02)", `{{foo bar=1}}`, "{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n"},
|
||||
{"parses mustaches with hash arguments (03)", `{{foo bar=true}}`, "{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n"},
|
||||
{"parses mustaches with hash arguments (04)", `{{foo bar=false}}`, "{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n"},
|
||||
{"parses mustaches with hash arguments (05)", `{{foo bar=@baz}}`, "{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n"},
|
||||
{"parses mustaches with hash arguments (06)", `{{foo bar=baz bat=bam}}`, "{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n"},
|
||||
{"parses mustaches with hash arguments (07)", `{{foo bar=baz bat="bam"}}`, "{{ PATH:foo [] HASH{bar=PATH:baz, bat=\"bam\"} }}\n"},
|
||||
{"parses mustaches with hash arguments (08)", `{{foo bat='bam'}}`, "{{ PATH:foo [] HASH{bat=\"bam\"} }}\n"},
|
||||
{"parses mustaches with hash arguments (09)", `{{foo omg bar=baz bat="bam"}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\"} }}\n"},
|
||||
{"parses mustaches with hash arguments (10)", `{{foo omg bar=baz bat="bam" baz=1}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=NUMBER{1}} }}\n"},
|
||||
{"parses mustaches with hash arguments (11)", `{{foo omg bar=baz bat="bam" baz=true}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=BOOLEAN{true}} }}\n"},
|
||||
{"parses mustaches with hash arguments (12)", `{{foo omg bar=baz bat="bam" baz=false}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=BOOLEAN{false}} }}\n"},
|
||||
|
||||
{"parses contents followed by a mustache", `foo bar {{baz}}`, "CONTENT[ 'foo bar ' ]\n{{ PATH:baz [] }}\n"},
|
||||
|
||||
{"parses a partial (1)", `{{> foo }}`, "{{> PARTIAL:foo }}\n"},
|
||||
{"parses a partial (2)", `{{> "foo" }}`, "{{> PARTIAL:foo }}\n"},
|
||||
{"parses a partial (3)", `{{> 1 }}`, "{{> PARTIAL:1 }}\n"},
|
||||
{"parses a partial with context", `{{> foo bar}}`, "{{> PARTIAL:foo PATH:bar }}\n"},
|
||||
{"parses a partial with hash", `{{> foo bar=bat}}`, "{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n"},
|
||||
{"parses a partial with context and hash", `{{> foo bar bat=baz}}`, "{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n"},
|
||||
{"parses a partial with a complex name", `{{> shared/partial?.bar}}`, "{{> PARTIAL:shared/partial?.bar }}\n"},
|
||||
|
||||
{"parses a comment", `{{! this is a comment }}`, "{{! ' this is a comment ' }}\n"},
|
||||
{"parses a multi-line comment", "{{!\nthis is a multi-line comment\n}}", "{{! '\nthis is a multi-line comment\n' }}\n"},
|
||||
|
||||
{"parses an inverse section", `{{#foo}} bar {{^}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"},
|
||||
{"parses an inverse (else-style) section", `{{#foo}} bar {{else}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"},
|
||||
{"parses multiple inverse sections", `{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"},
|
||||
{"parses empty blocks", `{{#foo}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n"},
|
||||
{"parses empty blocks with empty inverse section", `{{#foo}}{{^}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"},
|
||||
{"parses empty blocks with empty inverse (else-style) section", `{{#foo}}{{else}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"},
|
||||
{"parses non-empty blocks with empty inverse section", `{{#foo}} bar {{^}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"},
|
||||
{"parses non-empty blocks with empty inverse (else-style) section", `{{#foo}} bar {{else}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"},
|
||||
{"parses empty blocks with non-empty inverse section", `{{#foo}}{{^}} bar {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"},
|
||||
{"parses empty blocks with non-empty inverse (else-style) section", `{{#foo}}{{else}} bar {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"},
|
||||
{"parses a standalone inverse section", `{{^foo}}bar{{/foo}}`, "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"},
|
||||
{"parses block with block params", `{{#foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"},
|
||||
{"parses inverse block with block params", `{{^foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"},
|
||||
{"parses chained inverse block with block params", `{{#foo}}{{else foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"},
|
||||
}
|
||||
|
||||
func TestParser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range parserTests {
|
||||
output := ""
|
||||
|
||||
node, err := Parse(test.input)
|
||||
if err == nil {
|
||||
output = ast.Print(node)
|
||||
}
|
||||
|
||||
if (err != nil) || (test.output != output) {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\nexpected\n\t%q\ngot\n\t%q\nerror:\n\t%s", test.name, test.input, test.output, output, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var parserErrorTests = []parserTest{
|
||||
{"lexer error", `{{! unclosed comment`, "Lexer error"},
|
||||
{"syntax error", `foo{{^}}`, "Syntax error"},
|
||||
|
||||
{"open raw block must be closed", `{{{{raw foo}} bar {{{{/raw}}}}`, "Expecting CloseRawBlock"},
|
||||
{"end raw block must be closed", `{{{{raw foo}}}} bar {{{{/raw}}`, "Expecting CloseRawBlock"},
|
||||
|
||||
{"raw block names must match (1)", `{{{{1}}}}{{foo}}{{{{/raw}}}}`, "1 doesn't match raw"},
|
||||
{"raw block names must match (2)", `{{{{raw}}}}{{foo}}{{{{/1}}}}`, "raw doesn't match 1"},
|
||||
{"raw block names must match (3)", `{{{{goodbyes}}}}test{{{{/hellos}}}}`, "goodbyes doesn't match hellos"},
|
||||
|
||||
{"open block must be closed", `{{#foo bar}}}{{/foo}}`, "Expecting Close"},
|
||||
{"end block must be closed", `{{#foo bar}}{{/foo}}}`, "Expecting Close"},
|
||||
{"an open block must have a end block", `{{#foo}}test`, "Expecting OpenEndBlock"},
|
||||
|
||||
{"block names must match (1)", `{{#1 bar}}{{/foo}}`, "1 doesn't match foo"},
|
||||
{"block names must match (2)", `{{#foo bar}}{{/1}}`, "foo doesn't match 1"},
|
||||
{"block names must match (3)", `{{#foo}}test{{/bar}}`, "foo doesn't match bar"},
|
||||
|
||||
{"an mustache must terminate with a close mustache", `{{foo}}}`, "Expecting Close"},
|
||||
{"an unescaped mustache must terminate with a close unescaped mustache", `{{{foo}}`, "Expecting CloseUnescaped"},
|
||||
|
||||
{"an partial must terminate with a close mustache", `{{> foo}}}`, "Expecting Close"},
|
||||
{"a subexpression must terminate with a close subexpression", `{{foo (false}}`, "Expecting CloseSexpr"},
|
||||
|
||||
{"raises on missing hash value (1)", `{{foo bar=}}`, "Parse error on line 1"},
|
||||
{"raises on missing hash value (2)", `{{foo bar=baz bim=}}`, "Parse error on line 1"},
|
||||
|
||||
{"block param must have at least one param", `{{#foo as ||}}content{{/foo}}`, "Expecting ID"},
|
||||
{"open block params must be closed", `{{#foo as |}}content{{/foo}}`, "Expecting ID"},
|
||||
|
||||
{"a path must start with an ID", `{{#/}}content{{/foo}}`, "Expecting ID"},
|
||||
{"a path must end with an ID", `{{foo/bar/}}`, "Expecting ID"},
|
||||
|
||||
//
|
||||
// Next tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/parser.js
|
||||
//
|
||||
{"throws on old inverse section", `{{else foo}}bar{{/foo}}`, ""},
|
||||
|
||||
{"raises if there's a parser error (1)", `foo{{^}}bar`, "Parse error on line 1"},
|
||||
{"raises if there's a parser error (2)", `{{foo}`, "Parse error on line 1"},
|
||||
{"raises if there's a parser error (3)", `{{foo &}}`, "Parse error on line 1"},
|
||||
{"raises if there's a parser error (4)", `{{#goodbyes}}{{/hellos}}`, "Parse error on line 1"},
|
||||
{"raises if there's a parser error (5)", `{{#goodbyes}}{{/hellos}}`, "goodbyes doesn't match hellos"},
|
||||
|
||||
{"should handle invalid paths (1)", `{{foo/../bar}}`, `Invalid path: foo/..`},
|
||||
{"should handle invalid paths (2)", `{{foo/./bar}}`, `Invalid path: foo/.`},
|
||||
{"should handle invalid paths (3)", `{{foo/this/bar}}`, `Invalid path: foo/this`},
|
||||
|
||||
{"knows how to report the correct line number in errors (1)", "hello\nmy\n{{foo}", "Parse error on line 3"},
|
||||
{"knows how to report the correct line number in errors (2)", "hello\n\nmy\n\n{{foo}", "Parse error on line 5"},
|
||||
|
||||
{"knows how to report the correct line number in errors when the first character is a newline", "\n\nhello\n\nmy\n\n{{foo}", "Parse error on line 7"},
|
||||
}
|
||||
|
||||
func TestParserErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range parserErrorTests {
|
||||
node, err := Parse(test.input)
|
||||
if err == nil {
|
||||
output := ast.Print(node)
|
||||
tokens := lexer.Collect(test.input)
|
||||
|
||||
t.Errorf("Test '%s' failed - Error expected\ninput:\n\t'%s'\ngot\n\t%q\ntokens:\n\t%q", test.name, test.input, output, tokens)
|
||||
} else if test.output != "" {
|
||||
matched, errMatch := regexp.MatchString(regexp.QuoteMeta(test.output), fmt.Sprint(err))
|
||||
if errMatch != nil {
|
||||
panic("Failed to match regexp")
|
||||
}
|
||||
|
||||
if !matched {
|
||||
t.Errorf("Test '%s' failed - Incorrect error returned\ninput:\n\t'%s'\nexpected\n\t%q\ngot\n\t%q", test.name, test.input, test.output, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// package example
|
||||
func Example() {
|
||||
source := "You know {{nothing}} John Snow"
|
||||
|
||||
// parse template
|
||||
program, err := Parse(source)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// print AST
|
||||
output := ast.Print(program)
|
||||
|
||||
fmt.Print(output)
|
||||
// CONTENT[ 'You know ' ]
|
||||
// {{ PATH:nothing [] }}
|
||||
// CONTENT[ ' John Snow' ]
|
||||
}
|
|
@ -1,360 +0,0 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/aymerick/raymond/ast"
|
||||
)
|
||||
|
||||
// whitespaceVisitor walks through the AST to perform whitespace control
|
||||
//
|
||||
// The logic was shamelessly borrowed from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/whitespace-control.js
|
||||
type whitespaceVisitor struct {
|
||||
isRootSeen bool
|
||||
}
|
||||
|
||||
var (
|
||||
rTrimLeft = regexp.MustCompile(`^[ \t]*\r?\n?`)
|
||||
rTrimLeftMultiple = regexp.MustCompile(`^\s+`)
|
||||
|
||||
rTrimRight = regexp.MustCompile(`[ \t]+$`)
|
||||
rTrimRightMultiple = regexp.MustCompile(`\s+$`)
|
||||
|
||||
rPrevWhitespace = regexp.MustCompile(`\r?\n\s*?$`)
|
||||
rPrevWhitespaceStart = regexp.MustCompile(`(^|\r?\n)\s*?$`)
|
||||
|
||||
rNextWhitespace = regexp.MustCompile(`^\s*?\r?\n`)
|
||||
rNextWhitespaceEnd = regexp.MustCompile(`^\s*?(\r?\n|$)`)
|
||||
|
||||
rPartialIndent = regexp.MustCompile(`([ \t]+$)`)
|
||||
)
|
||||
|
||||
// newWhitespaceVisitor instanciates a new whitespaceVisitor
|
||||
func newWhitespaceVisitor() *whitespaceVisitor {
|
||||
return &whitespaceVisitor{}
|
||||
}
|
||||
|
||||
// processWhitespaces performs whitespace control on given AST
|
||||
//
|
||||
// WARNING: It must be called only once on AST.
|
||||
func processWhitespaces(node ast.Node) {
|
||||
node.Accept(newWhitespaceVisitor())
|
||||
}
|
||||
|
||||
func omitRightFirst(body []ast.Node, multiple bool) {
|
||||
omitRight(body, -1, multiple)
|
||||
}
|
||||
|
||||
func omitRight(body []ast.Node, i int, multiple bool) {
|
||||
if i+1 >= len(body) {
|
||||
return
|
||||
}
|
||||
|
||||
current := body[i+1]
|
||||
|
||||
node, ok := current.(*ast.ContentStatement)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if !multiple && node.RightStripped {
|
||||
return
|
||||
}
|
||||
|
||||
original := node.Value
|
||||
|
||||
r := rTrimLeft
|
||||
if multiple {
|
||||
r = rTrimLeftMultiple
|
||||
}
|
||||
|
||||
node.Value = r.ReplaceAllString(node.Value, "")
|
||||
|
||||
node.RightStripped = (original != node.Value)
|
||||
}
|
||||
|
||||
func omitLeftLast(body []ast.Node, multiple bool) {
|
||||
omitLeft(body, len(body), multiple)
|
||||
}
|
||||
|
||||
func omitLeft(body []ast.Node, i int, multiple bool) bool {
|
||||
if i-1 < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
current := body[i-1]
|
||||
|
||||
node, ok := current.(*ast.ContentStatement)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if !multiple && node.LeftStripped {
|
||||
return false
|
||||
}
|
||||
|
||||
original := node.Value
|
||||
|
||||
r := rTrimRight
|
||||
if multiple {
|
||||
r = rTrimRightMultiple
|
||||
}
|
||||
|
||||
node.Value = r.ReplaceAllString(node.Value, "")
|
||||
|
||||
node.LeftStripped = (original != node.Value)
|
||||
|
||||
return node.LeftStripped
|
||||
}
|
||||
|
||||
func isPrevWhitespace(body []ast.Node) bool {
|
||||
return isPrevWhitespaceProgram(body, len(body), false)
|
||||
}
|
||||
|
||||
func isPrevWhitespaceProgram(body []ast.Node, i int, isRoot bool) bool {
|
||||
if i < 1 {
|
||||
return isRoot
|
||||
}
|
||||
|
||||
prev := body[i-1]
|
||||
|
||||
if node, ok := prev.(*ast.ContentStatement); ok {
|
||||
if (node.Value == "") && node.RightStripped {
|
||||
// already stripped, so it may be an empty string not catched by regexp
|
||||
return true
|
||||
}
|
||||
|
||||
r := rPrevWhitespaceStart
|
||||
if (i > 1) || !isRoot {
|
||||
r = rPrevWhitespace
|
||||
}
|
||||
|
||||
return r.MatchString(node.Value)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isNextWhitespace(body []ast.Node) bool {
|
||||
return isNextWhitespaceProgram(body, -1, false)
|
||||
}
|
||||
|
||||
func isNextWhitespaceProgram(body []ast.Node, i int, isRoot bool) bool {
|
||||
if i+1 >= len(body) {
|
||||
return isRoot
|
||||
}
|
||||
|
||||
next := body[i+1]
|
||||
|
||||
if node, ok := next.(*ast.ContentStatement); ok {
|
||||
if (node.Value == "") && node.LeftStripped {
|
||||
// already stripped, so it may be an empty string not catched by regexp
|
||||
return true
|
||||
}
|
||||
|
||||
r := rNextWhitespaceEnd
|
||||
if (i+2 > len(body)) || !isRoot {
|
||||
r = rNextWhitespace
|
||||
}
|
||||
|
||||
return r.MatchString(node.Value)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
//
|
||||
// Visitor interface
|
||||
//
|
||||
|
||||
func (v *whitespaceVisitor) VisitProgram(program *ast.Program) interface{} {
|
||||
isRoot := !v.isRootSeen
|
||||
v.isRootSeen = true
|
||||
|
||||
body := program.Body
|
||||
for i, current := range body {
|
||||
strip, _ := current.Accept(v).(*ast.Strip)
|
||||
if strip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
_isPrevWhitespace := isPrevWhitespaceProgram(body, i, isRoot)
|
||||
_isNextWhitespace := isNextWhitespaceProgram(body, i, isRoot)
|
||||
|
||||
openStandalone := strip.OpenStandalone && _isPrevWhitespace
|
||||
closeStandalone := strip.CloseStandalone && _isNextWhitespace
|
||||
inlineStandalone := strip.InlineStandalone && _isPrevWhitespace && _isNextWhitespace
|
||||
|
||||
if strip.Close {
|
||||
omitRight(body, i, true)
|
||||
}
|
||||
|
||||
if strip.Open && (i > 0) {
|
||||
omitLeft(body, i, true)
|
||||
}
|
||||
|
||||
if inlineStandalone {
|
||||
omitRight(body, i, false)
|
||||
|
||||
if omitLeft(body, i, false) {
|
||||
// If we are on a standalone node, save the indent info for partials
|
||||
if partial, ok := current.(*ast.PartialStatement); ok {
|
||||
// Pull out the whitespace from the final line
|
||||
if i > 0 {
|
||||
if prevContent, ok := body[i-1].(*ast.ContentStatement); ok {
|
||||
partial.Indent = rPartialIndent.FindString(prevContent.Original)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b, ok := current.(*ast.BlockStatement); ok {
|
||||
if openStandalone {
|
||||
prog := b.Program
|
||||
if prog == nil {
|
||||
prog = b.Inverse
|
||||
}
|
||||
|
||||
omitRightFirst(prog.Body, false)
|
||||
|
||||
// Strip out the previous content node if it's whitespace only
|
||||
omitLeft(body, i, false)
|
||||
}
|
||||
|
||||
if closeStandalone {
|
||||
prog := b.Inverse
|
||||
if prog == nil {
|
||||
prog = b.Program
|
||||
}
|
||||
|
||||
// Always strip the next node
|
||||
omitRight(body, i, false)
|
||||
|
||||
omitLeftLast(prog.Body, false)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *whitespaceVisitor) VisitBlock(block *ast.BlockStatement) interface{} {
|
||||
if block.Program != nil {
|
||||
block.Program.Accept(v)
|
||||
}
|
||||
|
||||
if block.Inverse != nil {
|
||||
block.Inverse.Accept(v)
|
||||
}
|
||||
|
||||
program := block.Program
|
||||
inverse := block.Inverse
|
||||
|
||||
if program == nil {
|
||||
program = inverse
|
||||
inverse = nil
|
||||
}
|
||||
|
||||
firstInverse := inverse
|
||||
lastInverse := inverse
|
||||
|
||||
if (inverse != nil) && inverse.Chained {
|
||||
b, _ := inverse.Body[0].(*ast.BlockStatement)
|
||||
firstInverse = b.Program
|
||||
|
||||
for lastInverse.Chained {
|
||||
b, _ := lastInverse.Body[len(lastInverse.Body)-1].(*ast.BlockStatement)
|
||||
lastInverse = b.Program
|
||||
}
|
||||
}
|
||||
|
||||
closeProg := firstInverse
|
||||
if closeProg == nil {
|
||||
closeProg = program
|
||||
}
|
||||
|
||||
strip := &ast.Strip{
|
||||
Open: (block.OpenStrip != nil) && block.OpenStrip.Open,
|
||||
Close: (block.CloseStrip != nil) && block.CloseStrip.Close,
|
||||
|
||||
OpenStandalone: isNextWhitespace(program.Body),
|
||||
CloseStandalone: isPrevWhitespace(closeProg.Body),
|
||||
}
|
||||
|
||||
if (block.OpenStrip != nil) && block.OpenStrip.Close {
|
||||
omitRightFirst(program.Body, true)
|
||||
}
|
||||
|
||||
if inverse != nil {
|
||||
if block.InverseStrip != nil {
|
||||
inverseStrip := block.InverseStrip
|
||||
|
||||
if inverseStrip.Open {
|
||||
omitLeftLast(program.Body, true)
|
||||
}
|
||||
|
||||
if inverseStrip.Close {
|
||||
omitRightFirst(firstInverse.Body, true)
|
||||
}
|
||||
}
|
||||
|
||||
if (block.CloseStrip != nil) && block.CloseStrip.Open {
|
||||
omitLeftLast(lastInverse.Body, true)
|
||||
}
|
||||
|
||||
// Find standalone else statements
|
||||
if isPrevWhitespace(program.Body) && isNextWhitespace(firstInverse.Body) {
|
||||
omitLeftLast(program.Body, false)
|
||||
|
||||
omitRightFirst(firstInverse.Body, false)
|
||||
}
|
||||
} else if (block.CloseStrip != nil) && block.CloseStrip.Open {
|
||||
omitLeftLast(program.Body, true)
|
||||
}
|
||||
|
||||
return strip
|
||||
}
|
||||
|
||||
func (v *whitespaceVisitor) VisitMustache(mustache *ast.MustacheStatement) interface{} {
|
||||
return mustache.Strip
|
||||
}
|
||||
|
||||
func _inlineStandalone(strip *ast.Strip) interface{} {
|
||||
return &ast.Strip{
|
||||
Open: strip.Open,
|
||||
Close: strip.Close,
|
||||
InlineStandalone: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *whitespaceVisitor) VisitPartial(node *ast.PartialStatement) interface{} {
|
||||
strip := node.Strip
|
||||
if strip == nil {
|
||||
strip = &ast.Strip{}
|
||||
}
|
||||
|
||||
return _inlineStandalone(strip)
|
||||
}
|
||||
|
||||
func (v *whitespaceVisitor) VisitComment(node *ast.CommentStatement) interface{} {
|
||||
strip := node.Strip
|
||||
if strip == nil {
|
||||
strip = &ast.Strip{}
|
||||
}
|
||||
|
||||
return _inlineStandalone(strip)
|
||||
}
|
||||
|
||||
// NOOP
|
||||
func (v *whitespaceVisitor) VisitContent(node *ast.ContentStatement) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitExpression(node *ast.Expression) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitSubExpression(node *ast.SubExpression) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitPath(node *ast.PathExpression) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitString(node *ast.StringLiteral) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitBoolean(node *ast.BooleanLiteral) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitNumber(node *ast.NumberLiteral) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitHash(node *ast.Hash) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitHashPair(node *ast.HashPair) interface{} { return nil }
|
|
@ -1,85 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// partial represents a partial template
|
||||
type partial struct {
|
||||
name string
|
||||
source string
|
||||
tpl *Template
|
||||
}
|
||||
|
||||
// partials stores all global partials
|
||||
var partials map[string]*partial
|
||||
|
||||
// protects global partials
|
||||
var partialsMutex sync.RWMutex
|
||||
|
||||
func init() {
|
||||
partials = make(map[string]*partial)
|
||||
}
|
||||
|
||||
// newPartial instanciates a new partial
|
||||
func newPartial(name string, source string, tpl *Template) *partial {
|
||||
return &partial{
|
||||
name: name,
|
||||
source: source,
|
||||
tpl: tpl,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPartial registers a global partial. That partial will be available to all templates.
|
||||
func RegisterPartial(name string, source string) {
|
||||
partialsMutex.Lock()
|
||||
defer partialsMutex.Unlock()
|
||||
|
||||
if partials[name] != nil {
|
||||
panic(fmt.Errorf("Partial already registered: %s", name))
|
||||
}
|
||||
|
||||
partials[name] = newPartial(name, source, nil)
|
||||
}
|
||||
|
||||
// RegisterPartials registers several global partials. Those partials will be available to all templates.
|
||||
func RegisterPartials(partials map[string]string) {
|
||||
for name, p := range partials {
|
||||
RegisterPartial(name, p)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPartial registers a global partial with given parsed template. That partial will be available to all templates.
|
||||
func RegisterPartialTemplate(name string, tpl *Template) {
|
||||
partialsMutex.Lock()
|
||||
defer partialsMutex.Unlock()
|
||||
|
||||
if partials[name] != nil {
|
||||
panic(fmt.Errorf("Partial already registered: %s", name))
|
||||
}
|
||||
|
||||
partials[name] = newPartial(name, "", tpl)
|
||||
}
|
||||
|
||||
// findPartial finds a registered global partial
|
||||
func findPartial(name string) *partial {
|
||||
partialsMutex.RLock()
|
||||
defer partialsMutex.RUnlock()
|
||||
|
||||
return partials[name]
|
||||
}
|
||||
|
||||
// template returns parsed partial template
|
||||
func (p *partial) template() (*Template, error) {
|
||||
if p.tpl == nil {
|
||||
var err error
|
||||
|
||||
p.tpl, err = Parse(p.source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return p.tpl, nil
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
// Package raymond provides handlebars evaluation
|
||||
package raymond
|
||||
|
||||
// Render parses a template and evaluates it with given context
|
||||
//
|
||||
// Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead.
|
||||
func Render(source string, ctx interface{}) (string, error) {
|
||||
// parse template
|
||||
tpl, err := Parse(source)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// renders template
|
||||
str, err := tpl.Exec(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// MustRender parses a template and evaluates it with given context. It panics on error.
|
||||
//
|
||||
// Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead.
|
||||
func MustRender(source string, ctx interface{}) string {
|
||||
return MustParse(source).MustExec(ctx)
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
|
@ -1,115 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Example() {
|
||||
source := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"title": "foo",
|
||||
"body": map[string]string{"content": "bar"},
|
||||
}
|
||||
|
||||
// parse template
|
||||
tpl := MustParse(source)
|
||||
|
||||
// evaluate template with context
|
||||
output := tpl.MustExec(ctx)
|
||||
|
||||
// alternatively, for one shots:
|
||||
// output := MustRender(source, ctx)
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <h1>foo</h1><p>bar</p>
|
||||
}
|
||||
|
||||
func Example_struct() {
|
||||
source := `<div class="post">
|
||||
<h1>By {{fullName author}}</h1>
|
||||
<div class="body">{{body}}</div>
|
||||
|
||||
<h1>Comments</h1>
|
||||
|
||||
{{#each comments}}
|
||||
<h2>By {{fullName author}}</h2>
|
||||
<div class="body">{{body}}</div>
|
||||
{{/each}}
|
||||
</div>`
|
||||
|
||||
type Person struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
Author Person
|
||||
Body string
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
Author Person
|
||||
Body string
|
||||
Comments []Comment
|
||||
}
|
||||
|
||||
ctx := Post{
|
||||
Person{"Jean", "Valjean"},
|
||||
"Life is difficult",
|
||||
[]Comment{
|
||||
Comment{
|
||||
Person{"Marcel", "Beliveau"},
|
||||
"LOL!",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
RegisterHelper("fullName", func(person Person) string {
|
||||
return person.FirstName + " " + person.LastName
|
||||
})
|
||||
|
||||
output := MustRender(source, ctx)
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <div class="post">
|
||||
// <h1>By Jean Valjean</h1>
|
||||
// <div class="body">Life is difficult</div>
|
||||
//
|
||||
// <h1>Comments</h1>
|
||||
//
|
||||
// <h2>By Marcel Beliveau</h2>
|
||||
// <div class="body">LOL!</div>
|
||||
// </div>
|
||||
}
|
||||
|
||||
func ExampleRender() {
|
||||
tpl := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"title": "foo",
|
||||
"body": map[string]string{"content": "bar"},
|
||||
}
|
||||
|
||||
// render template with context
|
||||
output, err := Render(tpl, ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <h1>foo</h1><p>bar</p>
|
||||
}
|
||||
|
||||
func ExampleMustRender() {
|
||||
tpl := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"title": "foo",
|
||||
"body": map[string]string{"content": "bar"},
|
||||
}
|
||||
|
||||
// render template with context
|
||||
output := MustRender(tpl, ctx)
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <h1>foo</h1><p>bar</p>
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SafeString represents a string that must not be escaped.
|
||||
//
|
||||
// A SafeString can be returned by helpers to disable escaping.
|
||||
type SafeString string
|
||||
|
||||
// IsSafeString returns true if argument is a SafeString
|
||||
func isSafeString(value interface{}) bool {
|
||||
if _, ok := value.(SafeString); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Str returns string representation of any basic type value.
|
||||
func Str(value interface{}) string {
|
||||
return strValue(reflect.ValueOf(value))
|
||||
}
|
||||
|
||||
// strValue returns string representation of a reflect.Value
|
||||
func strValue(value reflect.Value) string {
|
||||
result := ""
|
||||
|
||||
ival, ok := printableValue(value)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("Can't print value: %q", value))
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(ival)
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
result += strValue(val.Index(i))
|
||||
}
|
||||
case reflect.Bool:
|
||||
result = "false"
|
||||
if val.Bool() {
|
||||
result = "true"
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
result = fmt.Sprintf("%d", ival)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
result = strconv.FormatFloat(val.Float(), 'f', -1, 64)
|
||||
case reflect.Invalid:
|
||||
result = ""
|
||||
default:
|
||||
result = fmt.Sprintf("%s", ival)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// printableValue returns the, possibly indirected, interface value inside v that
|
||||
// is best for a call to formatted printer.
|
||||
//
|
||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
||||
func printableValue(v reflect.Value) (interface{}, bool) {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v, _ = indirect(v) // fmt.Fprint handles nil.
|
||||
}
|
||||
if !v.IsValid() {
|
||||
return "", true
|
||||
}
|
||||
|
||||
if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
|
||||
if v.CanAddr() && (reflect.PtrTo(v.Type()).Implements(errorType) || reflect.PtrTo(v.Type()).Implements(fmtStringerType)) {
|
||||
v = v.Addr()
|
||||
} else {
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return v.Interface(), true
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type strTest struct {
|
||||
name string
|
||||
input interface{}
|
||||
output string
|
||||
}
|
||||
|
||||
var strTests = []strTest{
|
||||
{"String", "foo", "foo"},
|
||||
{"Boolean true", true, "true"},
|
||||
{"Boolean false", false, "false"},
|
||||
{"Integer", 25, "25"},
|
||||
{"Float", 25.75, "25.75"},
|
||||
{"Nil", nil, ""},
|
||||
{"[]string", []string{"foo", "bar"}, "foobar"},
|
||||
{"[]interface{} (strings)", []interface{}{"foo", "bar"}, "foobar"},
|
||||
{"[]Boolean", []bool{true, false}, "truefalse"},
|
||||
}
|
||||
|
||||
func TestStr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range strTests {
|
||||
if res := Str(test.input); res != test.output {
|
||||
t.Errorf("Failed to stringify: %s\nexpected:\n\t'%s'got:\n\t%q", test.name, test.output, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleStr() {
|
||||
output := Str(3) + " foos are " + Str(true) + " and " + Str(-1.25) + " bars are " + Str(false) + "\n"
|
||||
output += "But you know '" + Str(nil) + "' John Snow\n"
|
||||
output += "map: " + Str(map[string]string{"foo": "bar"}) + "\n"
|
||||
output += "array: " + Str([]interface{}{true, 10, "foo", 5, "bar"})
|
||||
|
||||
fmt.Println(output)
|
||||
// Output: 3 foos are true and -1.25 bars are false
|
||||
// But you know '' John Snow
|
||||
// map: map[foo:bar]
|
||||
// array: true10foo5bar
|
||||
}
|
||||
|
||||
func ExampleSafeString() {
|
||||
RegisterHelper("em", func() SafeString {
|
||||
return SafeString("<em>FOO BAR</em>")
|
||||
})
|
||||
|
||||
tpl := MustParse("{{em}}")
|
||||
|
||||
result := tpl.MustExec(nil)
|
||||
fmt.Print(result)
|
||||
// Output: <em>FOO BAR</em>
|
||||
}
|
|
@ -1,249 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/aymerick/raymond/ast"
|
||||
"github.com/aymerick/raymond/parser"
|
||||
)
|
||||
|
||||
// Template represents a handlebars template.
|
||||
type Template struct {
|
||||
source string
|
||||
program *ast.Program
|
||||
helpers map[string]reflect.Value
|
||||
partials map[string]*partial
|
||||
mutex sync.RWMutex // protects helpers and partials
|
||||
|
||||
}
|
||||
|
||||
// newTemplate instanciate a new template without parsing it
|
||||
func newTemplate(source string) *Template {
|
||||
return &Template{
|
||||
source: source,
|
||||
helpers: make(map[string]reflect.Value),
|
||||
partials: make(map[string]*partial),
|
||||
}
|
||||
}
|
||||
|
||||
// Parse instanciates a template by parsing given source.
|
||||
func Parse(source string) (*Template, error) {
|
||||
tpl := newTemplate(source)
|
||||
|
||||
// parse template
|
||||
if err := tpl.parse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tpl, nil
|
||||
}
|
||||
|
||||
// MustParse instanciates a template by parsing given source. It panics on error.
|
||||
func MustParse(source string) *Template {
|
||||
result, err := Parse(source)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ParseFile reads given file and returns parsed template.
|
||||
func ParseFile(filePath string) (*Template, error) {
|
||||
b, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return Parse(string(b))
|
||||
}
|
||||
|
||||
// parse parses the template
|
||||
//
|
||||
// It can be called several times, the parsing will be done only once.
|
||||
func (tpl *Template) parse() error {
|
||||
if tpl.program == nil {
|
||||
var err error
|
||||
|
||||
tpl.program, err = parser.Parse(tpl.source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone returns a copy of that template.
|
||||
func (tpl *Template) Clone() *Template {
|
||||
result := newTemplate(tpl.source)
|
||||
|
||||
result.program = tpl.program
|
||||
|
||||
tpl.mutex.RLock()
|
||||
defer tpl.mutex.RUnlock()
|
||||
|
||||
for name, helper := range tpl.helpers {
|
||||
result.RegisterHelper(name, helper.Interface())
|
||||
}
|
||||
|
||||
for name, partial := range tpl.partials {
|
||||
result.addPartial(name, partial.source, partial.tpl)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (tpl *Template) findHelper(name string) reflect.Value {
|
||||
tpl.mutex.RLock()
|
||||
defer tpl.mutex.RUnlock()
|
||||
|
||||
return tpl.helpers[name]
|
||||
}
|
||||
|
||||
// RegisterHelper registers a helper for that template.
|
||||
func (tpl *Template) RegisterHelper(name string, helper interface{}) {
|
||||
tpl.mutex.Lock()
|
||||
defer tpl.mutex.Unlock()
|
||||
|
||||
if tpl.helpers[name] != zero {
|
||||
panic(fmt.Sprintf("Helper %s already registered", name))
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(helper)
|
||||
ensureValidHelper(name, val)
|
||||
|
||||
tpl.helpers[name] = val
|
||||
}
|
||||
|
||||
// RegisterHelpers registers several helpers for that template.
|
||||
func (tpl *Template) RegisterHelpers(helpers map[string]interface{}) {
|
||||
for name, helper := range helpers {
|
||||
tpl.RegisterHelper(name, helper)
|
||||
}
|
||||
}
|
||||
|
||||
func (tpl *Template) addPartial(name string, source string, template *Template) {
|
||||
tpl.mutex.Lock()
|
||||
defer tpl.mutex.Unlock()
|
||||
|
||||
if tpl.partials[name] != nil {
|
||||
panic(fmt.Sprintf("Partial %s already registered", name))
|
||||
}
|
||||
|
||||
tpl.partials[name] = newPartial(name, source, template)
|
||||
}
|
||||
|
||||
func (tpl *Template) findPartial(name string) *partial {
|
||||
tpl.mutex.RLock()
|
||||
defer tpl.mutex.RUnlock()
|
||||
|
||||
return tpl.partials[name]
|
||||
}
|
||||
|
||||
// RegisterPartial registers a partial for that template.
|
||||
func (tpl *Template) RegisterPartial(name string, source string) {
|
||||
tpl.addPartial(name, source, nil)
|
||||
}
|
||||
|
||||
// RegisterPartials registers several partials for that template.
|
||||
func (tpl *Template) RegisterPartials(partials map[string]string) {
|
||||
for name, partial := range partials {
|
||||
tpl.RegisterPartial(name, partial)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPartialFile reads given file and registers its content as a partial with given name.
|
||||
func (tpl *Template) RegisterPartialFile(filePath string, name string) error {
|
||||
b, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tpl.RegisterPartial(name, string(b))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterPartialFiles reads several files and registers them as partials, the filename base is used as the partial name.
|
||||
func (tpl *Template) RegisterPartialFiles(filePaths ...string) error {
|
||||
if len(filePaths) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, filePath := range filePaths {
|
||||
name := fileBase(filePath)
|
||||
|
||||
if err := tpl.RegisterPartialFile(filePath, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterPartial registers an already parsed partial for that template.
|
||||
func (tpl *Template) RegisterPartialTemplate(name string, template *Template) {
|
||||
tpl.addPartial(name, "", template)
|
||||
}
|
||||
|
||||
// Exec evaluates template with given context.
|
||||
func (tpl *Template) Exec(ctx interface{}) (result string, err error) {
|
||||
return tpl.ExecWith(ctx, nil)
|
||||
}
|
||||
|
||||
// MustExec evaluates template with given context. It panics on error.
|
||||
func (tpl *Template) MustExec(ctx interface{}) string {
|
||||
result, err := tpl.Exec(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ExecWith evaluates template with given context and private data frame.
|
||||
func (tpl *Template) ExecWith(ctx interface{}, privData *DataFrame) (result string, err error) {
|
||||
defer errRecover(&err)
|
||||
|
||||
// parses template if necessary
|
||||
err = tpl.parse()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// setup visitor
|
||||
v := newEvalVisitor(tpl, ctx, privData)
|
||||
|
||||
// visit AST
|
||||
result, _ = tpl.program.Accept(v).(string)
|
||||
|
||||
// named return values
|
||||
return
|
||||
}
|
||||
|
||||
// errRecover recovers evaluation panic
|
||||
func errRecover(errp *error) {
|
||||
e := recover()
|
||||
if e != nil {
|
||||
switch err := e.(type) {
|
||||
case runtime.Error:
|
||||
panic(e)
|
||||
case error:
|
||||
*errp = err
|
||||
default:
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintAST returns string representation of parsed template.
|
||||
func (tpl *Template) PrintAST() string {
|
||||
if err := tpl.parse(); err != nil {
|
||||
return fmt.Sprintf("PARSER ERROR: %s", err)
|
||||
}
|
||||
|
||||
return ast.Print(tpl.program)
|
||||
}
|
|
@ -1,166 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var sourceBasic = `<div class="entry">
|
||||
<h1>{{title}}</h1>
|
||||
<div class="body">
|
||||
{{body}}
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
var basicAST = `CONTENT[ '<div class="entry">
|
||||
<h1>' ]
|
||||
{{ PATH:title [] }}
|
||||
CONTENT[ '</h1>
|
||||
<div class="body">
|
||||
' ]
|
||||
{{ PATH:body [] }}
|
||||
CONTENT[ '
|
||||
</div>
|
||||
</div>' ]
|
||||
`
|
||||
|
||||
func TestNewTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tpl := newTemplate(sourceBasic)
|
||||
if tpl.source != sourceBasic {
|
||||
t.Errorf("Failed to instantiate template")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tpl, err := Parse(sourceBasic)
|
||||
if err != nil || (tpl.source != sourceBasic) {
|
||||
t.Errorf("Failed to parse template")
|
||||
}
|
||||
|
||||
if str := tpl.PrintAST(); str != basicAST {
|
||||
t.Errorf("Template parsing incorrect: %s", str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sourcePartial := `I am a {{wat}} partial`
|
||||
sourcePartial2 := `Partial for the {{wat}}`
|
||||
|
||||
tpl := MustParse(sourceBasic)
|
||||
tpl.RegisterPartial("p", sourcePartial)
|
||||
|
||||
if (len(tpl.partials) != 1) || (tpl.partials["p"] == nil) {
|
||||
t.Errorf("What?")
|
||||
}
|
||||
|
||||
cloned := tpl.Clone()
|
||||
|
||||
if (len(cloned.partials) != 1) || (cloned.partials["p"] == nil) {
|
||||
t.Errorf("Template partials must be cloned")
|
||||
}
|
||||
|
||||
cloned.RegisterPartial("p2", sourcePartial2)
|
||||
|
||||
if (len(cloned.partials) != 2) || (cloned.partials["p"] == nil) || (cloned.partials["p2"] == nil) {
|
||||
t.Errorf("Failed to register a partial on cloned template")
|
||||
}
|
||||
|
||||
if (len(tpl.partials) != 1) || (tpl.partials["p"] == nil) {
|
||||
t.Errorf("Modification of a cloned template MUST NOT affect original template")
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleTemplate_Exec() {
|
||||
source := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"title": "foo",
|
||||
"body": map[string]string{"content": "bar"},
|
||||
}
|
||||
|
||||
// parse template
|
||||
tpl := MustParse(source)
|
||||
|
||||
// evaluate template with context
|
||||
output, err := tpl.Exec(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <h1>foo</h1><p>bar</p>
|
||||
}
|
||||
|
||||
func ExampleTemplate_MustExec() {
|
||||
source := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"title": "foo",
|
||||
"body": map[string]string{"content": "bar"},
|
||||
}
|
||||
|
||||
// parse template
|
||||
tpl := MustParse(source)
|
||||
|
||||
// evaluate template with context
|
||||
output := tpl.MustExec(ctx)
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <h1>foo</h1><p>bar</p>
|
||||
}
|
||||
|
||||
func ExampleTemplate_ExecWith() {
|
||||
source := "<h1>{{title}}</h1><p>{{#body}}{{content}} and {{@baz.bat}}{{/body}}</p>"
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"title": "foo",
|
||||
"body": map[string]string{"content": "bar"},
|
||||
}
|
||||
|
||||
// parse template
|
||||
tpl := MustParse(source)
|
||||
|
||||
// computes private data frame
|
||||
frame := NewDataFrame()
|
||||
frame.Set("baz", map[string]string{"bat": "unicorns"})
|
||||
|
||||
// evaluate template
|
||||
output, err := tpl.ExecWith(ctx, frame)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <h1>foo</h1><p>bar and unicorns</p>
|
||||
}
|
||||
|
||||
func ExampleTemplate_PrintAST() {
|
||||
source := "<h1>{{title}}</h1><p>{{#body}}{{content}} and {{@baz.bat}}{{/body}}</p>"
|
||||
|
||||
// parse template
|
||||
tpl := MustParse(source)
|
||||
|
||||
// print AST
|
||||
output := tpl.PrintAST()
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: CONTENT[ '<h1>' ]
|
||||
// {{ PATH:title [] }}
|
||||
// CONTENT[ '</h1><p>' ]
|
||||
// BLOCK:
|
||||
// PATH:body []
|
||||
// PROGRAM:
|
||||
// {{ PATH:content []
|
||||
// }}
|
||||
// CONTENT[ ' and ' ]
|
||||
// {{ @PATH:baz/bat []
|
||||
// }}
|
||||
// CONTENT[ '</p>' ]
|
||||
//
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import (
|
||||
"path"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// indirect returns the item at the end of indirection, and a bool to indicate if it's nil.
|
||||
// We indirect through pointers and empty interfaces (only) because
|
||||
// non-empty interfaces have methods we might need.
|
||||
//
|
||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
||||
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
|
||||
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
|
||||
if v.IsNil() {
|
||||
return v, true
|
||||
}
|
||||
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return v, false
|
||||
}
|
||||
|
||||
// IsTrue returns true if obj is a truthy value.
|
||||
func IsTrue(obj interface{}) bool {
|
||||
thruth, ok := isTrueValue(reflect.ValueOf(obj))
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return thruth
|
||||
}
|
||||
|
||||
// isTrueValue reports whether the value is 'true', in the sense of not the zero of its type,
|
||||
// and whether the value has a meaningful truth value
|
||||
//
|
||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
||||
func isTrueValue(val reflect.Value) (truth, ok bool) {
|
||||
if !val.IsValid() {
|
||||
// Something like var x interface{}, never set. It's a form of nil.
|
||||
return false, true
|
||||
}
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||
truth = val.Len() > 0
|
||||
case reflect.Bool:
|
||||
truth = val.Bool()
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
truth = val.Complex() != 0
|
||||
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
|
||||
truth = !val.IsNil()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
truth = val.Int() != 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
truth = val.Float() != 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
truth = val.Uint() != 0
|
||||
case reflect.Struct:
|
||||
truth = true // Struct values are always true.
|
||||
default:
|
||||
return
|
||||
}
|
||||
return truth, true
|
||||
}
|
||||
|
||||
// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero.
|
||||
//
|
||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
||||
func canBeNil(typ reflect.Type) bool {
|
||||
switch typ.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// fileBase returns base file name
|
||||
//
|
||||
// example: /foo/bar/baz.png => baz
|
||||
func fileBase(filePath string) string {
|
||||
fileName := path.Base(filePath)
|
||||
fileExt := path.Ext(filePath)
|
||||
|
||||
return fileName[:len(fileName)-len(fileExt)]
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package raymond
|
||||
|
||||
import "fmt"
|
||||
|
||||
func ExampleIsTrue() {
|
||||
output := "Empty array: " + Str(IsTrue([0]string{})) + "\n"
|
||||
output += "Non empty array: " + Str(IsTrue([1]string{"foo"})) + "\n"
|
||||
|
||||
output += "Empty slice: " + Str(IsTrue([]string{})) + "\n"
|
||||
output += "Non empty slice: " + Str(IsTrue([]string{"foo"})) + "\n"
|
||||
|
||||
output += "Empty map: " + Str(IsTrue(map[string]string{})) + "\n"
|
||||
output += "Non empty map: " + Str(IsTrue(map[string]string{"foo": "bar"})) + "\n"
|
||||
|
||||
output += "Empty string: " + Str(IsTrue("")) + "\n"
|
||||
output += "Non empty string: " + Str(IsTrue("foo")) + "\n"
|
||||
|
||||
output += "true bool: " + Str(IsTrue(true)) + "\n"
|
||||
output += "false bool: " + Str(IsTrue(false)) + "\n"
|
||||
|
||||
output += "0 integer: " + Str(IsTrue(0)) + "\n"
|
||||
output += "positive integer: " + Str(IsTrue(10)) + "\n"
|
||||
output += "negative integer: " + Str(IsTrue(-10)) + "\n"
|
||||
|
||||
output += "0 float: " + Str(IsTrue(0.0)) + "\n"
|
||||
output += "positive float: " + Str(IsTrue(10.0)) + "\n"
|
||||
output += "negative integer: " + Str(IsTrue(-10.0)) + "\n"
|
||||
|
||||
output += "struct: " + Str(IsTrue(struct{}{})) + "\n"
|
||||
output += "nil: " + Str(IsTrue(nil)) + "\n"
|
||||
|
||||
fmt.Println(output)
|
||||
// Output: Empty array: false
|
||||
// Non empty array: true
|
||||
// Empty slice: false
|
||||
// Non empty slice: true
|
||||
// Empty map: false
|
||||
// Non empty map: true
|
||||
// Empty string: false
|
||||
// Non empty string: true
|
||||
// true bool: true
|
||||
// false bool: false
|
||||
// 0 integer: false
|
||||
// positive integer: true
|
||||
// negative integer: true
|
||||
// 0 float: false
|
||||
// positive float: true
|
||||
// negative integer: true
|
||||
// struct: true
|
||||
// nil: false
|
||||
}
|
|
@ -1,342 +0,0 @@
|
|||
package drone
|
||||
|
||||
//go:generate mockery -all
|
||||
//go:generate mv mocks/Client.go mocks/client.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
pathSelf = "%s/api/user"
|
||||
pathFeed = "%s/api/user/feed"
|
||||
pathRepos = "%s/api/user/repos"
|
||||
pathRepo = "%s/api/repos/%s/%s"
|
||||
pathEncrypt = "%s/api/repos/%s/%s/encrypt"
|
||||
pathBuilds = "%s/api/repos/%s/%s/builds"
|
||||
pathBuild = "%s/api/repos/%s/%s/builds/%v"
|
||||
pathJob = "%s/api/repos/%s/%s/builds/%d/%d"
|
||||
pathLog = "%s/api/repos/%s/%s/logs/%d/%d"
|
||||
pathKey = "%s/api/repos/%s/%s/key"
|
||||
pathNodes = "%s/api/nodes"
|
||||
pathNode = "%s/api/nodes/%d"
|
||||
pathUsers = "%s/api/users"
|
||||
pathUser = "%s/api/users/%s"
|
||||
)
|
||||
|
||||
type client struct {
|
||||
client *http.Client
|
||||
base string // base url
|
||||
}
|
||||
|
||||
// NewClient returns a client at the specified url.
|
||||
func NewClient(uri string) Client {
|
||||
return &client{http.DefaultClient, uri}
|
||||
}
|
||||
|
||||
// NewClientToken returns a client at the specified url that
|
||||
// authenticates all outbound requests with the given token.
|
||||
func NewClientToken(uri, token string) Client {
|
||||
config := new(oauth2.Config)
|
||||
auther := config.Client(oauth2.NoContext, &oauth2.Token{AccessToken: token})
|
||||
return &client{auther, uri}
|
||||
}
|
||||
|
||||
// SetClient sets the default http client. This should be
|
||||
// used in conjunction with golang.org/x/oauth2 to
|
||||
// authenticate requests to the Drone server.
|
||||
func (c *client) SetClient(client *http.Client) {
|
||||
c.client = client
|
||||
}
|
||||
|
||||
// Self returns the currently authenticated user.
|
||||
func (c *client) Self() (*User, error) {
|
||||
out := new(User)
|
||||
uri := fmt.Sprintf(pathSelf, c.base)
|
||||
err := c.get(uri, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// User returns a user by login.
|
||||
func (c *client) User(login string) (*User, error) {
|
||||
out := new(User)
|
||||
uri := fmt.Sprintf(pathUser, c.base, login)
|
||||
err := c.get(uri, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// UserList returns a list of all registered users.
|
||||
func (c *client) UserList() ([]*User, error) {
|
||||
out := make([]*User, 0)
|
||||
uri := fmt.Sprintf(pathUsers, c.base)
|
||||
err := c.get(uri, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// UserPost creates a new user account.
|
||||
func (c *client) UserPost(in *User) (*User, error) {
|
||||
out := new(User)
|
||||
uri := fmt.Sprintf(pathUsers, c.base)
|
||||
err := c.post(uri, in, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// UserPatch updates a user account.
|
||||
func (c *client) UserPatch(in *User) (*User, error) {
|
||||
out := new(User)
|
||||
uri := fmt.Sprintf(pathUser, c.base, in.Login)
|
||||
err := c.patch(uri, in, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// UserDel deletes a user account.
|
||||
func (c *client) UserDel(login string) error {
|
||||
uri := fmt.Sprintf(pathUser, c.base, login)
|
||||
err := c.delete(uri)
|
||||
return err
|
||||
}
|
||||
|
||||
// UserFeed returns the user's activity feed.
|
||||
func (c *client) UserFeed() ([]*Activity, error) {
|
||||
out := make([]*Activity, 0)
|
||||
uri := fmt.Sprintf(pathFeed, c.base)
|
||||
err := c.get(uri, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Repo returns a repository by name.
|
||||
func (c *client) Repo(owner string, name string) (*Repo, error) {
|
||||
out := new(Repo)
|
||||
uri := fmt.Sprintf(pathRepo, c.base, owner, name)
|
||||
err := c.get(uri, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// RepoList returns a list of all repositories to which
|
||||
// the user has explicit access in the host system.
|
||||
func (c *client) RepoList() ([]*Repo, error) {
|
||||
out := make([]*Repo, 0)
|
||||
uri := fmt.Sprintf(pathRepos, c.base)
|
||||
err := c.get(uri, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// RepoPost activates a repository.
|
||||
func (c *client) RepoPost(owner string, name string) (*Repo, error) {
|
||||
out := new(Repo)
|
||||
uri := fmt.Sprintf(pathRepo, c.base, owner, name)
|
||||
err := c.post(uri, nil, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// RepoPatch updates a repository.
|
||||
func (c *client) RepoPatch(in *Repo) (*Repo, error) {
|
||||
out := new(Repo)
|
||||
uri := fmt.Sprintf(pathRepo, c.base, in.Owner, in.Name)
|
||||
err := c.patch(uri, in, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// RepoDel deletes a repository.
|
||||
func (c *client) RepoDel(owner, name string) error {
|
||||
uri := fmt.Sprintf(pathRepo, c.base, owner, name)
|
||||
err := c.delete(uri)
|
||||
return err
|
||||
}
|
||||
|
||||
// RepoKey returns a repository public key.
|
||||
func (c *client) RepoKey(owner, name string) (*Key, error) {
|
||||
out := new(Key)
|
||||
uri := fmt.Sprintf(pathKey, c.base, owner, name)
|
||||
rc, err := c.stream(uri, "GET", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rc.Close()
|
||||
raw, _ := ioutil.ReadAll(rc)
|
||||
out.Public = string(raw)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Build returns a repository build by number.
|
||||
func (c *client) Build(owner, name string, num int) (*Build, error) {
|
||||
out := new(Build)
|
||||
uri := fmt.Sprintf(pathBuild, c.base, owner, name, num)
|
||||
err := c.get(uri, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Build returns the latest repository build by branch.
|
||||
func (c *client) BuildLast(owner, name, branch string) (*Build, error) {
|
||||
out := new(Build)
|
||||
uri := fmt.Sprintf(pathBuild, c.base, owner, name, "latest")
|
||||
if len(branch) != 0 {
|
||||
uri += "?branch=" + branch
|
||||
}
|
||||
err := c.get(uri, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// BuildList returns a list of recent builds for the
|
||||
// the specified repository.
|
||||
func (c *client) BuildList(owner, name string) ([]*Build, error) {
|
||||
out := make([]*Build, 0)
|
||||
uri := fmt.Sprintf(pathBuilds, c.base, owner, name)
|
||||
err := c.get(uri, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// BuildStart re-starts a stopped build.
|
||||
func (c *client) BuildStart(owner, name string, num int) (*Build, error) {
|
||||
out := new(Build)
|
||||
uri := fmt.Sprintf(pathBuild, c.base, owner, name, num)
|
||||
err := c.post(uri, nil, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// BuildStop cancels the running job.
|
||||
func (c *client) BuildStop(owner, name string, num, job int) error {
|
||||
uri := fmt.Sprintf(pathJob, c.base, owner, name, num, job)
|
||||
err := c.delete(uri)
|
||||
return err
|
||||
}
|
||||
|
||||
// BuildFork re-starts a stopped build with a new build number,
|
||||
// preserving the prior history.
|
||||
func (c *client) BuildFork(owner, name string, num int) (*Build, error) {
|
||||
out := new(Build)
|
||||
uri := fmt.Sprintf(pathBuild+"?fork=true", c.base, owner, name, num)
|
||||
err := c.post(uri, nil, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// BuildLogs returns the build logs for the specified job.
|
||||
func (c *client) BuildLogs(owner, name string, num, job int) (io.ReadCloser, error) {
|
||||
uri := fmt.Sprintf(pathLog, c.base, owner, name, num, job)
|
||||
return c.stream(uri, "GET", nil, nil)
|
||||
}
|
||||
|
||||
// Node returns a node by id.
|
||||
func (c *client) Node(id int64) (*Node, error) {
|
||||
out := new(Node)
|
||||
uri := fmt.Sprintf(pathNode, c.base, id)
|
||||
err := c.get(uri, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// NodeList returns a list of all registered worker nodes.
|
||||
func (c *client) NodeList() ([]*Node, error) {
|
||||
out := make([]*Node, 0)
|
||||
uri := fmt.Sprintf(pathNodes, c.base)
|
||||
err := c.get(uri, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// NodePost registers a new worker node.
|
||||
func (c *client) NodePost(in *Node) (*Node, error) {
|
||||
out := new(Node)
|
||||
uri := fmt.Sprintf(pathNodes, c.base)
|
||||
err := c.post(uri, in, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// NodeDel deletes a worker node.
|
||||
func (c *client) NodeDel(id int64) error {
|
||||
uri := fmt.Sprintf(pathNode, c.base, id)
|
||||
err := c.delete(uri)
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
// http request helper functions
|
||||
//
|
||||
|
||||
// helper function for making an http GET request.
|
||||
func (c *client) get(rawurl string, out interface{}) error {
|
||||
return c.do(rawurl, "GET", nil, out)
|
||||
}
|
||||
|
||||
// helper function for making an http POST request.
|
||||
func (c *client) post(rawurl string, in, out interface{}) error {
|
||||
return c.do(rawurl, "POST", in, out)
|
||||
}
|
||||
|
||||
// helper function for making an http PUT request.
|
||||
func (c *client) put(rawurl string, in, out interface{}) error {
|
||||
return c.do(rawurl, "PUT", in, out)
|
||||
}
|
||||
|
||||
// helper function for making an http PATCH request.
|
||||
func (c *client) patch(rawurl string, in, out interface{}) error {
|
||||
return c.do(rawurl, "PATCH", in, out)
|
||||
}
|
||||
|
||||
// helper function for making an http DELETE request.
|
||||
func (c *client) delete(rawurl string) error {
|
||||
return c.do(rawurl, "DELETE", nil, nil)
|
||||
}
|
||||
|
||||
// helper function to make an http request
|
||||
func (c *client) do(rawurl, method string, in, out interface{}) error {
|
||||
// executes the http request and returns the body as
|
||||
// and io.ReadCloser
|
||||
body, err := c.stream(rawurl, method, in, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
// if a json response is expected, parse and return
|
||||
// the json response.
|
||||
if out != nil {
|
||||
return json.NewDecoder(body).Decode(out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// helper function to stream an http request
|
||||
func (c *client) stream(rawurl, method string, in, out interface{}) (io.ReadCloser, error) {
|
||||
uri, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if we are posting or putting data, we need to
|
||||
// write it to the body of the request.
|
||||
var buf io.ReadWriter
|
||||
if in != nil {
|
||||
buf = new(bytes.Buffer)
|
||||
err := json.NewEncoder(buf).Encode(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// creates a new http request to bitbucket.
|
||||
req, err := http.NewRequest(method, uri.String(), buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode > http.StatusPartialContent {
|
||||
defer resp.Body.Close()
|
||||
out, _ := ioutil.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf(string(out))
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package drone
|
||||
|
||||
const (
|
||||
EventPush = "push"
|
||||
EventPull = "pull_request"
|
||||
EventTag = "tag"
|
||||
EventDeploy = "deployment"
|
||||
)
|
||||
|
||||
const (
|
||||
StatusSkipped = "skipped"
|
||||
StatusPending = "pending"
|
||||
StatusRunning = "running"
|
||||
StatusSuccess = "success"
|
||||
StatusFailure = "failure"
|
||||
StatusKilled = "killed"
|
||||
StatusError = "error"
|
||||
)
|
||||
|
||||
const (
|
||||
Freebsd_386 uint = iota
|
||||
Freebsd_amd64
|
||||
Freebsd_arm
|
||||
Linux_386
|
||||
Linux_amd64
|
||||
Linux_arm
|
||||
Linux_arm64
|
||||
Solaris_amd64
|
||||
Windows_386
|
||||
Windows_amd64
|
||||
)
|
||||
|
||||
var Archs = map[string]uint{
|
||||
"freebsd_386": Freebsd_386,
|
||||
"freebsd_amd64": Freebsd_amd64,
|
||||
"freebsd_arm": Freebsd_arm,
|
||||
"linux_386": Linux_386,
|
||||
"linux_amd64": Linux_amd64,
|
||||
"linux_arm": Linux_arm,
|
||||
"linux_arm64": Linux_arm64,
|
||||
"solaris_amd64": Solaris_amd64,
|
||||
"windows_386": Windows_386,
|
||||
"windows_amd64": Windows_amd64,
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
package drone
|
||||
|
||||
import "io"
|
||||
|
||||
type Client interface {
|
||||
// Self returns the currently authenticated user.
|
||||
Self() (*User, error)
|
||||
|
||||
// User returns a user by login.
|
||||
User(string) (*User, error)
|
||||
|
||||
// UserList returns a list of all registered users.
|
||||
UserList() ([]*User, error)
|
||||
|
||||
// UserPost creates a new user account.
|
||||
UserPost(*User) (*User, error)
|
||||
|
||||
// UserPatch updates a user account.
|
||||
UserPatch(*User) (*User, error)
|
||||
|
||||
// UserDel deletes a user account.
|
||||
UserDel(string) error
|
||||
|
||||
// UserFeed returns the user's activity feed.
|
||||
UserFeed() ([]*Activity, error)
|
||||
|
||||
// Repo returns a repository by name.
|
||||
Repo(string, string) (*Repo, error)
|
||||
|
||||
// RepoList returns a list of all repositories to which
|
||||
// the user has explicit access in the host system.
|
||||
RepoList() ([]*Repo, error)
|
||||
|
||||
// RepoPost activates a repository.
|
||||
RepoPost(string, string) (*Repo, error)
|
||||
|
||||
// RepoPatch updates a repository.
|
||||
RepoPatch(*Repo) (*Repo, error)
|
||||
|
||||
// RepoDel deletes a repository.
|
||||
RepoDel(string, string) error
|
||||
|
||||
// RepoKey returns a repository public key.
|
||||
RepoKey(string, string) (*Key, error)
|
||||
|
||||
// Build returns a repository build by number.
|
||||
Build(string, string, int) (*Build, error)
|
||||
|
||||
// BuildLast returns the latest repository build by branch.
|
||||
// An empty branch will result in the default branch.
|
||||
BuildLast(string, string, string) (*Build, error)
|
||||
|
||||
// BuildList returns a list of recent builds for the
|
||||
// the specified repository.
|
||||
BuildList(string, string) ([]*Build, error)
|
||||
|
||||
// BuildStart re-starts a stopped build.
|
||||
BuildStart(string, string, int) (*Build, error)
|
||||
|
||||
// BuildStop stops the specified running job for given build.
|
||||
BuildStop(string, string, int, int) error
|
||||
|
||||
// BuildFork re-starts a stopped build with a new build number,
|
||||
// preserving the prior history.
|
||||
BuildFork(string, string, int) (*Build, error)
|
||||
|
||||
// BuildLogs returns the build logs for the specified job.
|
||||
BuildLogs(string, string, int, int) (io.ReadCloser, error)
|
||||
|
||||
// Node returns a node by id.
|
||||
Node(int64) (*Node, error)
|
||||
|
||||
// NodeList returns a list of all registered worker nodes.
|
||||
NodeList() ([]*Node, error)
|
||||
|
||||
// NodePost registers a new worker node.
|
||||
NodePost(*Node) (*Node, error)
|
||||
|
||||
// NodeDel deletes a worker node.
|
||||
NodeDel(int64) error
|
||||
}
|
|
@ -1,435 +0,0 @@
|
|||
package mocks
|
||||
|
||||
import "github.com/drone/drone-go/drone"
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
import "io"
|
||||
|
||||
type Client struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (_m *Client) Self() (*drone.User, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *drone.User
|
||||
if rf, ok := ret.Get(0).(func() *drone.User); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*drone.User)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) User(_a0 string) (*drone.User, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *drone.User
|
||||
if rf, ok := ret.Get(0).(func(string) *drone.User); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*drone.User)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) UserList() ([]*drone.User, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*drone.User
|
||||
if rf, ok := ret.Get(0).(func() []*drone.User); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*drone.User)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) UserPost(_a0 *drone.User) (*drone.User, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *drone.User
|
||||
if rf, ok := ret.Get(0).(func(*drone.User) *drone.User); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*drone.User)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*drone.User) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) UserPatch(_a0 *drone.User) (*drone.User, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *drone.User
|
||||
if rf, ok := ret.Get(0).(func(*drone.User) *drone.User); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*drone.User)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*drone.User) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) UserDel(_a0 string) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
func (_m *Client) UserFeed() ([]*drone.Activity, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*drone.Activity
|
||||
if rf, ok := ret.Get(0).(func() []*drone.Activity); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*drone.Activity)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) Repo(_a0 string, _a1 string) (*drone.Repo, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
var r0 *drone.Repo
|
||||
if rf, ok := ret.Get(0).(func(string, string) *drone.Repo); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*drone.Repo)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) RepoList() ([]*drone.Repo, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*drone.Repo
|
||||
if rf, ok := ret.Get(0).(func() []*drone.Repo); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*drone.Repo)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) RepoPost(_a0 string, _a1 string) (*drone.Repo, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
var r0 *drone.Repo
|
||||
if rf, ok := ret.Get(0).(func(string, string) *drone.Repo); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*drone.Repo)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) RepoPatch(_a0 *drone.Repo) (*drone.Repo, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *drone.Repo
|
||||
if rf, ok := ret.Get(0).(func(*drone.Repo) *drone.Repo); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*drone.Repo)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*drone.Repo) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) RepoDel(_a0 string, _a1 string) error {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
func (_m *Client) RepoKey(_a0 string, _a1 string) (*drone.Key, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
var r0 *drone.Key
|
||||
if rf, ok := ret.Get(0).(func(string, string) *drone.Key); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*drone.Key)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) Build(_a0 string, _a1 string, _a2 int) (*drone.Build, error) {
|
||||
ret := _m.Called(_a0, _a1, _a2)
|
||||
|
||||
var r0 *drone.Build
|
||||
if rf, ok := ret.Get(0).(func(string, string, int) *drone.Build); ok {
|
||||
r0 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*drone.Build)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string, int) error); ok {
|
||||
r1 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) BuildList(_a0 string, _a1 string) ([]*drone.Build, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
var r0 []*drone.Build
|
||||
if rf, ok := ret.Get(0).(func(string, string) []*drone.Build); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*drone.Build)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) BuildStart(_a0 string, _a1 string, _a2 int) (*drone.Build, error) {
|
||||
ret := _m.Called(_a0, _a1, _a2)
|
||||
|
||||
var r0 *drone.Build
|
||||
if rf, ok := ret.Get(0).(func(string, string, int) *drone.Build); ok {
|
||||
r0 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*drone.Build)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string, int) error); ok {
|
||||
r1 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) BuildStop(_a0 string, _a1 string, _a2 int, _a3 int) error {
|
||||
ret := _m.Called(_a0, _a1, _a2, _a3)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, int, int) error); ok {
|
||||
r0 = rf(_a0, _a1, _a2, _a3)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
func (_m *Client) BuildLogs(_a0 string, _a1 string, _a2 int, _a3 int) (io.ReadCloser, error) {
|
||||
ret := _m.Called(_a0, _a1, _a2, _a3)
|
||||
|
||||
var r0 io.ReadCloser
|
||||
if rf, ok := ret.Get(0).(func(string, string, int, int) io.ReadCloser); ok {
|
||||
r0 = rf(_a0, _a1, _a2, _a3)
|
||||
} else {
|
||||
r0 = ret.Get(0).(io.ReadCloser)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
|
||||
r1 = rf(_a0, _a1, _a2, _a3)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) Node(_a0 int64) (*drone.Node, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *drone.Node
|
||||
if rf, ok := ret.Get(0).(func(int64) *drone.Node); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*drone.Node)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(int64) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) NodeList() ([]*drone.Node, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*drone.Node
|
||||
if rf, ok := ret.Get(0).(func() []*drone.Node); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*drone.Node)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) NodePost(_a0 *drone.Node) (*drone.Node, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *drone.Node
|
||||
if rf, ok := ret.Get(0).(func(*drone.Node) *drone.Node); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*drone.Node)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*drone.Node) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
func (_m *Client) NodeDel(_a0 int64) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(int64) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
|
@ -1,157 +0,0 @@
|
|||
package drone
|
||||
|
||||
// User represents a user account.
|
||||
type User struct {
|
||||
ID int64 `json:"id""`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar_url"`
|
||||
Active bool `json:"active"`
|
||||
Admin bool `json:"admin"`
|
||||
}
|
||||
|
||||
// Repo represents a version control repository.
|
||||
type Repo struct {
|
||||
ID int64 `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Avatar string `json:"avatar_url"`
|
||||
Link string `json:"link_url"`
|
||||
Clone string `json:"clone_url"`
|
||||
Branch string `json:"default_branch"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
IsPrivate bool `json:"private"`
|
||||
IsTrusted bool `json:"trusted"`
|
||||
AllowPull bool `json:"allow_pr"`
|
||||
AllowPush bool `json:"allow_push"`
|
||||
AllowDeploy bool `json:"allow_deploys"`
|
||||
AllowTag bool `json:"allow_tags"`
|
||||
}
|
||||
|
||||
// Build represents the process of compiling and testing a changeset,
|
||||
// typically triggered by the remote system (ie GitHub).
|
||||
type Build struct {
|
||||
ID int64 `json:"id"`
|
||||
Number int `json:"number"`
|
||||
Event string `json:"event"`
|
||||
Status string `json:"status"`
|
||||
Enqueued int64 `json:"enqueued_at"`
|
||||
Created int64 `json:"created_at"`
|
||||
Started int64 `json:"started_at"`
|
||||
Finished int64 `json:"finished_at"`
|
||||
Commit string `json:"commit"`
|
||||
Branch string `json:"branch"`
|
||||
Ref string `json:"ref"`
|
||||
Refspec string `json:"refspec"`
|
||||
Remote string `json:"remote"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Author string `json:"author"`
|
||||
Avatar string `json:"author_avatar"`
|
||||
Email string `json:"author_email"`
|
||||
Link string `json:"link_url"`
|
||||
}
|
||||
|
||||
// Job represents a single job that is being executed as part
|
||||
// of a Build.
|
||||
type Job struct {
|
||||
ID int64 `json:"id"`
|
||||
Number int `json:"number"`
|
||||
Status string `json:"status"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Enqueued int64 `json:"enqueued_at"`
|
||||
Started int64 `json:"started_at"`
|
||||
Finished int64 `json:"finished_at"`
|
||||
|
||||
Environment map[string]string `json:"environment"`
|
||||
}
|
||||
|
||||
// Activity represents a build activity. It combines the
|
||||
// build details with summary Repository information.
|
||||
type Activity struct {
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Number int `json:"number"`
|
||||
Event string `json:"event"`
|
||||
Status string `json:"status"`
|
||||
Enqueued int64 `json:"enqueued_at"`
|
||||
Created int64 `json:"created_at"`
|
||||
Started int64 `json:"started_at"`
|
||||
Finished int64 `json:"finished_at"`
|
||||
Commit string `json:"commit"`
|
||||
Branch string `json:"branch"`
|
||||
Ref string `json:"ref"`
|
||||
Refspec string `json:"refspec"`
|
||||
Remote string `json:"remote"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Author string `json:"author"`
|
||||
Avatar string `json:"author_avatar"`
|
||||
Email string `json:"author_email"`
|
||||
Link string `json:"link_url"`
|
||||
}
|
||||
|
||||
// Repo represents a local or remote Docker daemon that is
|
||||
// repsonsible for running jobs.
|
||||
type Node struct {
|
||||
ID int64 `json:"id"`
|
||||
Addr string `json:"address"`
|
||||
Arch string `json:"architecture"`
|
||||
Cert string `json:"cert"`
|
||||
Key string `json:"key"`
|
||||
CA string `json:"ca"`
|
||||
}
|
||||
|
||||
// Key represents an RSA public and private key assigned to a
|
||||
// repository. It may be used to clone private repositories, or as
|
||||
// a deployment key.
|
||||
type Key struct {
|
||||
Public string `json:"public"`
|
||||
Private string `json:"private"`
|
||||
}
|
||||
|
||||
// Netrc defines a default .netrc file that should be injected
|
||||
// into the build environment. It will be used to authorize access
|
||||
// to https resources, such as git+https clones.
|
||||
type Netrc struct {
|
||||
Machine string `json:"machine"`
|
||||
Login string `json:"login"`
|
||||
Password string `json:"user"`
|
||||
}
|
||||
|
||||
type System struct {
|
||||
Version string `json:"version"`
|
||||
Link string `json:"link_url"`
|
||||
Plugins []string `json:"plugins"`
|
||||
Globals []string `json:"globals"`
|
||||
}
|
||||
|
||||
// Workspace defines the build's workspace inside the
|
||||
// container. This helps the plugin locate the source
|
||||
// code directory.
|
||||
type Workspace struct {
|
||||
Root string `json:"root"`
|
||||
Path string `json:"path"`
|
||||
|
||||
Netrc *Netrc `json:"netrc"`
|
||||
Keys *Key `json:"keys"`
|
||||
}
|
||||
|
||||
// Payload defines the full payload send to plugins.
|
||||
type Payload struct {
|
||||
Yaml string `json:"config"`
|
||||
YamlEnc string `json:"secret"`
|
||||
Repo *Repo `json:"repo"`
|
||||
Build *Build `json:"build"`
|
||||
BuildLast *Build `json:"build_last"`
|
||||
Job *Job `json:"job"`
|
||||
Netrc *Netrc `json:"netrc"`
|
||||
Keys *Key `json:"keys"`
|
||||
System *System `json:"system"`
|
||||
Workspace *Workspace `json:"workspace"`
|
||||
Vargs interface{} `json:"vargs"`
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
package drone
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// StringSlice representes a string or an array of strings.
|
||||
type StringSlice struct {
|
||||
parts []string
|
||||
}
|
||||
|
||||
func (e *StringSlice) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
p := make([]string, 0, 1)
|
||||
if err := json.Unmarshal(b, &p); err != nil {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
p = append(p, s)
|
||||
}
|
||||
|
||||
e.parts = p
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *StringSlice) Len() int {
|
||||
if e == nil {
|
||||
return 0
|
||||
}
|
||||
return len(e.parts)
|
||||
}
|
||||
|
||||
func (e *StringSlice) Slice() []string {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.parts
|
||||
}
|
||||
|
||||
// StringInt representes a string or an integer value.
|
||||
type StringInt struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func (e *StringInt) UnmarshalJSON(b []byte) error {
|
||||
var num int
|
||||
err := json.Unmarshal(b, &num)
|
||||
if err == nil {
|
||||
e.value = strconv.Itoa(num)
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(b, &e.value)
|
||||
}
|
||||
|
||||
func (e StringInt) String() string {
|
||||
return e.value
|
||||
}
|
||||
|
||||
// StringMap representes a string or a map of strings.
|
||||
// StringMap representes a string or a map of strings.
|
||||
type StringMap struct {
|
||||
parts map[string]string
|
||||
}
|
||||
|
||||
func (e *StringMap) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
p := map[string]string{}
|
||||
if err := json.Unmarshal(b, &p); err != nil {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
p[""] = s
|
||||
}
|
||||
|
||||
e.parts = p
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *StringMap) Len() int {
|
||||
if e == nil {
|
||||
return 0
|
||||
}
|
||||
return len(e.parts)
|
||||
}
|
||||
|
||||
func (e *StringMap) String() (str string) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
for _, val := range e.parts {
|
||||
return val // returns the first string value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (e *StringMap) Map() map[string]string {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.parts
|
||||
}
|
||||
|
||||
func NewStringMap(parts map[string]string) StringMap {
|
||||
return StringMap{parts}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
var Stdin *ParamSet
|
||||
|
||||
func init() {
|
||||
// defaults to stdin
|
||||
Stdin = NewParamSet(os.Stdin)
|
||||
|
||||
// check for params after the double dash
|
||||
// in the command string
|
||||
for i, argv := range os.Args {
|
||||
if argv == "--" {
|
||||
arg := os.Args[i+1]
|
||||
buf := bytes.NewBufferString(arg)
|
||||
Stdin = NewParamSet(buf)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this init function is deprecated, but I'm keeping it
|
||||
// around just in case it proves useful in the future.
|
||||
func deprecated_init() {
|
||||
// if piping from stdin we can just exit
|
||||
// and use the default Stdin value
|
||||
stat, _ := os.Stdin.Stat()
|
||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// check for params after the double dash
|
||||
// in the command string
|
||||
for i, argv := range os.Args {
|
||||
if argv == "--" {
|
||||
arg := os.Args[i+1]
|
||||
buf := bytes.NewBufferString(arg)
|
||||
Stdin = NewParamSet(buf)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// else use the first variable in the list
|
||||
if len(os.Args) > 1 {
|
||||
buf := bytes.NewBufferString(os.Args[1])
|
||||
Stdin = NewParamSet(buf)
|
||||
}
|
||||
}
|
||||
|
||||
type ParamSet struct {
|
||||
reader io.Reader
|
||||
params map[string]interface{}
|
||||
}
|
||||
|
||||
func NewParamSet(reader io.Reader) *ParamSet {
|
||||
var p = new(ParamSet)
|
||||
p.reader = reader
|
||||
p.params = map[string]interface{}{}
|
||||
return p
|
||||
}
|
||||
|
||||
// Param defines a parameter with the specified name.
|
||||
func (p ParamSet) Param(name string, value interface{}) {
|
||||
p.params[name] = value
|
||||
}
|
||||
|
||||
// Parse parses parameter definitions from the map.
|
||||
func (p ParamSet) Parse() error {
|
||||
raw := map[string]json.RawMessage{}
|
||||
err := json.NewDecoder(p.reader).Decode(&raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for key, val := range p.params {
|
||||
data, ok := raw[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
err := json.Unmarshal(data, val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to unarmshal %s. %s", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unmarshal parses the JSON payload from the command
|
||||
// arguments and unmarshal into a value pointed to by v.
|
||||
func (p ParamSet) Unmarshal(v interface{}) error {
|
||||
return json.NewDecoder(p.reader).Decode(v)
|
||||
}
|
||||
|
||||
// Param defines a parameter with the specified name.
|
||||
func Param(name string, value interface{}) {
|
||||
Stdin.Param(name, value)
|
||||
}
|
||||
|
||||
// Parse parses parameter definitions from the map.
|
||||
func Parse() error {
|
||||
return Stdin.Parse()
|
||||
}
|
||||
|
||||
// Unmarshal parses the JSON payload from the command
|
||||
// arguments and unmarshal into a value pointed to by v.
|
||||
func Unmarshal(v interface{}) error {
|
||||
return Stdin.Unmarshal(v)
|
||||
}
|
||||
|
||||
// Unmarshal parses the JSON payload from the command
|
||||
// arguments and unmarshal into a value pointed to by v.
|
||||
func MustUnmarshal(v interface{}) error {
|
||||
return Stdin.Unmarshal(v)
|
||||
}
|
||||
|
||||
// MustParse parses parameter definitions from the map
|
||||
// and panics if there is a parsing error.
|
||||
func MustParse() {
|
||||
err := Parse()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
package plugin
|
|
@ -1,124 +0,0 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
"github.com/drone/drone-go/drone"
|
||||
)
|
||||
|
||||
func init() {
|
||||
raymond.RegisterHelpers(funcs)
|
||||
}
|
||||
|
||||
// Render parses and executes a template, returning the results
|
||||
// in string format.
|
||||
func Render(template string, playload *drone.Payload) (string, error) {
|
||||
return raymond.Render(template, normalize(playload))
|
||||
}
|
||||
|
||||
// RenderTrim parses and executes a template, returning the results
|
||||
// in string format. The result is trimmed to remove left and right
|
||||
// padding and newlines that may be added unintentially in the
|
||||
// template markup.
|
||||
func RenderTrim(template string, playload *drone.Payload) (string, error) {
|
||||
out, err := Render(template, playload)
|
||||
return strings.Trim(out, " \n"), err
|
||||
}
|
||||
|
||||
// Write parses and executes a template, writing the results to
|
||||
// writer w.
|
||||
func Write(w io.Writer, template string, playload *drone.Payload) error {
|
||||
out, err := Render(template, playload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.WriteString(w, out)
|
||||
return err
|
||||
}
|
||||
|
||||
var funcs = map[string]interface{}{
|
||||
"uppercase": strings.ToUpper,
|
||||
"lowercase": strings.ToLower,
|
||||
"uppercasefirst": uppercaseFirst,
|
||||
"duration": toDuration,
|
||||
"datetime": toDatetime,
|
||||
"success": isSuccess,
|
||||
"failure": isFailure,
|
||||
}
|
||||
|
||||
// uppercaseFirst is a helper function that takes a string and capitalizes
|
||||
// the first letter.
|
||||
func uppercaseFirst(s string) string {
|
||||
a := []rune(s)
|
||||
a[0] = unicode.ToUpper(a[0])
|
||||
s = string(a)
|
||||
return s
|
||||
}
|
||||
|
||||
// toDuration is a helper function that calculates a duration for a start and
|
||||
// and end time, and returns the duration in string format.
|
||||
func toDuration(started, finished float64) string {
|
||||
dur := time.Duration(int64(finished - started))
|
||||
return fmt.Sprintln(dur)
|
||||
}
|
||||
|
||||
// toDatetime is a helper function that converts a unix timestamp to a string.
|
||||
func toDatetime(timestamp float64, layout, zone string) string {
|
||||
if len(zone) == 0 {
|
||||
return time.Unix(int64(timestamp), 0).Format(layout)
|
||||
}
|
||||
loc, err := time.LoadLocation(zone)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing timezone, defaulting to local timezone. %s\n", err)
|
||||
return time.Unix(int64(timestamp), 0).Local().Format(layout)
|
||||
}
|
||||
return time.Unix(int64(timestamp), 0).In(loc).Format(layout)
|
||||
}
|
||||
|
||||
// isSuccess is a helper function that executes a block iff the status
|
||||
// is success, else it executes the else block.
|
||||
func isSuccess(conditional bool, options *raymond.Options) string {
|
||||
if !conditional {
|
||||
return options.Inverse()
|
||||
}
|
||||
|
||||
switch options.ParamStr(0) {
|
||||
case "success":
|
||||
return options.Fn()
|
||||
default:
|
||||
return options.Inverse()
|
||||
}
|
||||
}
|
||||
|
||||
// isFailure is a helper function that executes a block iff the status
|
||||
// is a form of failure, else it executes the else block.
|
||||
func isFailure(conditional bool, options *raymond.Options) string {
|
||||
if !conditional {
|
||||
return options.Inverse()
|
||||
}
|
||||
|
||||
switch options.ParamStr(0) {
|
||||
case "failure", "error", "killed":
|
||||
return options.Fn()
|
||||
default:
|
||||
return options.Inverse()
|
||||
}
|
||||
}
|
||||
|
||||
// normalize takes a Go representation of the variable, marshals
|
||||
// to json and then unmarshals to a map[string]interfacce{}. This
|
||||
// is important because it let's us use the JSON variable names
|
||||
// in our template
|
||||
func normalize(in interface{}) map[string]interface{} {
|
||||
data, _ := json.Marshal(in) // we own the types, so this should never fail
|
||||
|
||||
out := map[string]interface{}{}
|
||||
json.Unmarshal(data, &out)
|
||||
return out
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
)
|
||||
|
||||
var tests = []struct {
|
||||
Payload *drone.Payload
|
||||
Input string
|
||||
Output string
|
||||
}{
|
||||
{
|
||||
&drone.Payload{Build: &drone.Build{Number: 1}},
|
||||
"build #{{build.number}}",
|
||||
"build #1",
|
||||
},
|
||||
{
|
||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusSuccess}},
|
||||
"{{uppercase build.status}}",
|
||||
"SUCCESS",
|
||||
},
|
||||
{
|
||||
&drone.Payload{Build: &drone.Build{Author: "Octocat"}},
|
||||
"{{lowercase build.author}}",
|
||||
"octocat",
|
||||
},
|
||||
{
|
||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusSuccess}},
|
||||
"{{uppercasefirst build.status}}",
|
||||
"Success",
|
||||
},
|
||||
{
|
||||
&drone.Payload{Build: &drone.Build{
|
||||
Started: 1448127131,
|
||||
Finished: 1448127505},
|
||||
},
|
||||
"{{ duration build.started_at build.finished_at }}",
|
||||
"374ns",
|
||||
},
|
||||
{
|
||||
&drone.Payload{Build: &drone.Build{Finished: 1448127505}},
|
||||
`finished at {{ datetime build.finished_at "3:04PM" "UTC" }}`,
|
||||
"finished at 5:38PM",
|
||||
},
|
||||
// verify the success if / else block works
|
||||
{
|
||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusSuccess}},
|
||||
"{{#success build.status}}SUCCESS{{/success}}",
|
||||
"SUCCESS",
|
||||
},
|
||||
{
|
||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusFailure}},
|
||||
"{{#success build.status}}SUCCESS{{/success}}",
|
||||
"",
|
||||
},
|
||||
{
|
||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusFailure}},
|
||||
"{{#success build.status}}SUCCESS{{else}}NOT SUCCESS{{/success}}",
|
||||
"NOT SUCCESS",
|
||||
},
|
||||
// verify the failure if / else block works
|
||||
{
|
||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusFailure}},
|
||||
"{{#failure build.status}}FAILURE{{/failure}}",
|
||||
"FAILURE",
|
||||
},
|
||||
{
|
||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusSuccess}},
|
||||
"{{#failure build.status}}FAILURE{{/failure}}",
|
||||
"",
|
||||
},
|
||||
{
|
||||
&drone.Payload{Build: &drone.Build{Status: drone.StatusSuccess}},
|
||||
"{{#failure build.status}}FAILURE{{else}}NOT FAILURE{{/failure}}",
|
||||
"NOT FAILURE",
|
||||
},
|
||||
}
|
||||
|
||||
func TestTemplate(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
got, err := RenderTrim(test.Input, test.Payload)
|
||||
if err != nil {
|
||||
t.Errorf("Failed rendering template %q, got error %s.", test.Input, err)
|
||||
}
|
||||
if got != test.Output {
|
||||
t.Errorf("Wanted rendered template %q, got %q", test.Output, got)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,447 +0,0 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package context defines the Context type, which carries deadlines,
|
||||
// cancelation signals, and other request-scoped values across API boundaries
|
||||
// and between processes.
|
||||
//
|
||||
// Incoming requests to a server should create a Context, and outgoing calls to
|
||||
// servers should accept a Context. The chain of function calls between must
|
||||
// propagate the Context, optionally replacing it with a modified copy created
|
||||
// using WithDeadline, WithTimeout, WithCancel, or WithValue.
|
||||
//
|
||||
// Programs that use Contexts should follow these rules to keep interfaces
|
||||
// consistent across packages and enable static analysis tools to check context
|
||||
// propagation:
|
||||
//
|
||||
// Do not store Contexts inside a struct type; instead, pass a Context
|
||||
// explicitly to each function that needs it. The Context should be the first
|
||||
// parameter, typically named ctx:
|
||||
//
|
||||
// func DoSomething(ctx context.Context, arg Arg) error {
|
||||
// // ... use ctx ...
|
||||
// }
|
||||
//
|
||||
// Do not pass a nil Context, even if a function permits it. Pass context.TODO
|
||||
// if you are unsure about which Context to use.
|
||||
//
|
||||
// Use context Values only for request-scoped data that transits processes and
|
||||
// APIs, not for passing optional parameters to functions.
|
||||
//
|
||||
// The same Context may be passed to functions running in different goroutines;
|
||||
// Contexts are safe for simultaneous use by multiple goroutines.
|
||||
//
|
||||
// See http://blog.golang.org/context for example code for a server that uses
|
||||
// Contexts.
|
||||
package context // import "golang.org/x/net/context"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Context carries a deadline, a cancelation signal, and other values across
|
||||
// API boundaries.
|
||||
//
|
||||
// Context's methods may be called by multiple goroutines simultaneously.
|
||||
type Context interface {
|
||||
// Deadline returns the time when work done on behalf of this context
|
||||
// should be canceled. Deadline returns ok==false when no deadline is
|
||||
// set. Successive calls to Deadline return the same results.
|
||||
Deadline() (deadline time.Time, ok bool)
|
||||
|
||||
// Done returns a channel that's closed when work done on behalf of this
|
||||
// context should be canceled. Done may return nil if this context can
|
||||
// never be canceled. Successive calls to Done return the same value.
|
||||
//
|
||||
// WithCancel arranges for Done to be closed when cancel is called;
|
||||
// WithDeadline arranges for Done to be closed when the deadline
|
||||
// expires; WithTimeout arranges for Done to be closed when the timeout
|
||||
// elapses.
|
||||
//
|
||||
// Done is provided for use in select statements:
|
||||
//
|
||||
// // Stream generates values with DoSomething and sends them to out
|
||||
// // until DoSomething returns an error or ctx.Done is closed.
|
||||
// func Stream(ctx context.Context, out <-chan Value) error {
|
||||
// for {
|
||||
// v, err := DoSomething(ctx)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// select {
|
||||
// case <-ctx.Done():
|
||||
// return ctx.Err()
|
||||
// case out <- v:
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// See http://blog.golang.org/pipelines for more examples of how to use
|
||||
// a Done channel for cancelation.
|
||||
Done() <-chan struct{}
|
||||
|
||||
// Err returns a non-nil error value after Done is closed. Err returns
|
||||
// Canceled if the context was canceled or DeadlineExceeded if the
|
||||
// context's deadline passed. No other values for Err are defined.
|
||||
// After Done is closed, successive calls to Err return the same value.
|
||||
Err() error
|
||||
|
||||
// Value returns the value associated with this context for key, or nil
|
||||
// if no value is associated with key. Successive calls to Value with
|
||||
// the same key returns the same result.
|
||||
//
|
||||
// Use context values only for request-scoped data that transits
|
||||
// processes and API boundaries, not for passing optional parameters to
|
||||
// functions.
|
||||
//
|
||||
// A key identifies a specific value in a Context. Functions that wish
|
||||
// to store values in Context typically allocate a key in a global
|
||||
// variable then use that key as the argument to context.WithValue and
|
||||
// Context.Value. A key can be any type that supports equality;
|
||||
// packages should define keys as an unexported type to avoid
|
||||
// collisions.
|
||||
//
|
||||
// Packages that define a Context key should provide type-safe accessors
|
||||
// for the values stores using that key:
|
||||
//
|
||||
// // Package user defines a User type that's stored in Contexts.
|
||||
// package user
|
||||
//
|
||||
// import "golang.org/x/net/context"
|
||||
//
|
||||
// // User is the type of value stored in the Contexts.
|
||||
// type User struct {...}
|
||||
//
|
||||
// // key is an unexported type for keys defined in this package.
|
||||
// // This prevents collisions with keys defined in other packages.
|
||||
// type key int
|
||||
//
|
||||
// // userKey is the key for user.User values in Contexts. It is
|
||||
// // unexported; clients use user.NewContext and user.FromContext
|
||||
// // instead of using this key directly.
|
||||
// var userKey key = 0
|
||||
//
|
||||
// // NewContext returns a new Context that carries value u.
|
||||
// func NewContext(ctx context.Context, u *User) context.Context {
|
||||
// return context.WithValue(ctx, userKey, u)
|
||||
// }
|
||||
//
|
||||
// // FromContext returns the User value stored in ctx, if any.
|
||||
// func FromContext(ctx context.Context) (*User, bool) {
|
||||
// u, ok := ctx.Value(userKey).(*User)
|
||||
// return u, ok
|
||||
// }
|
||||
Value(key interface{}) interface{}
|
||||
}
|
||||
|
||||
// Canceled is the error returned by Context.Err when the context is canceled.
|
||||
var Canceled = errors.New("context canceled")
|
||||
|
||||
// DeadlineExceeded is the error returned by Context.Err when the context's
|
||||
// deadline passes.
|
||||
var DeadlineExceeded = errors.New("context deadline exceeded")
|
||||
|
||||
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
|
||||
// struct{}, since vars of this type must have distinct addresses.
|
||||
type emptyCtx int
|
||||
|
||||
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (*emptyCtx) Done() <-chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*emptyCtx) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*emptyCtx) Value(key interface{}) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *emptyCtx) String() string {
|
||||
switch e {
|
||||
case background:
|
||||
return "context.Background"
|
||||
case todo:
|
||||
return "context.TODO"
|
||||
}
|
||||
return "unknown empty Context"
|
||||
}
|
||||
|
||||
var (
|
||||
background = new(emptyCtx)
|
||||
todo = new(emptyCtx)
|
||||
)
|
||||
|
||||
// Background returns a non-nil, empty Context. It is never canceled, has no
|
||||
// values, and has no deadline. It is typically used by the main function,
|
||||
// initialization, and tests, and as the top-level Context for incoming
|
||||
// requests.
|
||||
func Background() Context {
|
||||
return background
|
||||
}
|
||||
|
||||
// TODO returns a non-nil, empty Context. Code should use context.TODO when
|
||||
// it's unclear which Context to use or it's is not yet available (because the
|
||||
// surrounding function has not yet been extended to accept a Context
|
||||
// parameter). TODO is recognized by static analysis tools that determine
|
||||
// whether Contexts are propagated correctly in a program.
|
||||
func TODO() Context {
|
||||
return todo
|
||||
}
|
||||
|
||||
// A CancelFunc tells an operation to abandon its work.
|
||||
// A CancelFunc does not wait for the work to stop.
|
||||
// After the first call, subsequent calls to a CancelFunc do nothing.
|
||||
type CancelFunc func()
|
||||
|
||||
// WithCancel returns a copy of parent with a new Done channel. The returned
|
||||
// context's Done channel is closed when the returned cancel function is called
|
||||
// or when the parent context's Done channel is closed, whichever happens first.
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete.
|
||||
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
||||
c := newCancelCtx(parent)
|
||||
propagateCancel(parent, &c)
|
||||
return &c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
|
||||
// newCancelCtx returns an initialized cancelCtx.
|
||||
func newCancelCtx(parent Context) cancelCtx {
|
||||
return cancelCtx{
|
||||
Context: parent,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// propagateCancel arranges for child to be canceled when parent is.
|
||||
func propagateCancel(parent Context, child canceler) {
|
||||
if parent.Done() == nil {
|
||||
return // parent is never canceled
|
||||
}
|
||||
if p, ok := parentCancelCtx(parent); ok {
|
||||
p.mu.Lock()
|
||||
if p.err != nil {
|
||||
// parent has already been canceled
|
||||
child.cancel(false, p.err)
|
||||
} else {
|
||||
if p.children == nil {
|
||||
p.children = make(map[canceler]bool)
|
||||
}
|
||||
p.children[child] = true
|
||||
}
|
||||
p.mu.Unlock()
|
||||
} else {
|
||||
go func() {
|
||||
select {
|
||||
case <-parent.Done():
|
||||
child.cancel(false, parent.Err())
|
||||
case <-child.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// parentCancelCtx follows a chain of parent references until it finds a
|
||||
// *cancelCtx. This function understands how each of the concrete types in this
|
||||
// package represents its parent.
|
||||
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
|
||||
for {
|
||||
switch c := parent.(type) {
|
||||
case *cancelCtx:
|
||||
return c, true
|
||||
case *timerCtx:
|
||||
return &c.cancelCtx, true
|
||||
case *valueCtx:
|
||||
parent = c.Context
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// removeChild removes a context from its parent.
|
||||
func removeChild(parent Context, child canceler) {
|
||||
p, ok := parentCancelCtx(parent)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.mu.Lock()
|
||||
if p.children != nil {
|
||||
delete(p.children, child)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// A canceler is a context type that can be canceled directly. The
|
||||
// implementations are *cancelCtx and *timerCtx.
|
||||
type canceler interface {
|
||||
cancel(removeFromParent bool, err error)
|
||||
Done() <-chan struct{}
|
||||
}
|
||||
|
||||
// A cancelCtx can be canceled. When canceled, it also cancels any children
|
||||
// that implement canceler.
|
||||
type cancelCtx struct {
|
||||
Context
|
||||
|
||||
done chan struct{} // closed by the first cancel call.
|
||||
|
||||
mu sync.Mutex
|
||||
children map[canceler]bool // set to nil by the first cancel call
|
||||
err error // set to non-nil by the first cancel call
|
||||
}
|
||||
|
||||
func (c *cancelCtx) Done() <-chan struct{} {
|
||||
return c.done
|
||||
}
|
||||
|
||||
func (c *cancelCtx) Err() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.err
|
||||
}
|
||||
|
||||
func (c *cancelCtx) String() string {
|
||||
return fmt.Sprintf("%v.WithCancel", c.Context)
|
||||
}
|
||||
|
||||
// cancel closes c.done, cancels each of c's children, and, if
|
||||
// removeFromParent is true, removes c from its parent's children.
|
||||
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
|
||||
if err == nil {
|
||||
panic("context: internal error: missing cancel error")
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.err != nil {
|
||||
c.mu.Unlock()
|
||||
return // already canceled
|
||||
}
|
||||
c.err = err
|
||||
close(c.done)
|
||||
for child := range c.children {
|
||||
// NOTE: acquiring the child's lock while holding parent's lock.
|
||||
child.cancel(false, err)
|
||||
}
|
||||
c.children = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
if removeFromParent {
|
||||
removeChild(c.Context, c)
|
||||
}
|
||||
}
|
||||
|
||||
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
||||
// to be no later than d. If the parent's deadline is already earlier than d,
|
||||
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
|
||||
// context's Done channel is closed when the deadline expires, when the returned
|
||||
// cancel function is called, or when the parent context's Done channel is
|
||||
// closed, whichever happens first.
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete.
|
||||
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
|
||||
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
|
||||
// The current deadline is already sooner than the new one.
|
||||
return WithCancel(parent)
|
||||
}
|
||||
c := &timerCtx{
|
||||
cancelCtx: newCancelCtx(parent),
|
||||
deadline: deadline,
|
||||
}
|
||||
propagateCancel(parent, c)
|
||||
d := deadline.Sub(time.Now())
|
||||
if d <= 0 {
|
||||
c.cancel(true, DeadlineExceeded) // deadline has already passed
|
||||
return c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.err == nil {
|
||||
c.timer = time.AfterFunc(d, func() {
|
||||
c.cancel(true, DeadlineExceeded)
|
||||
})
|
||||
}
|
||||
return c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
|
||||
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
|
||||
// implement Done and Err. It implements cancel by stopping its timer then
|
||||
// delegating to cancelCtx.cancel.
|
||||
type timerCtx struct {
|
||||
cancelCtx
|
||||
timer *time.Timer // Under cancelCtx.mu.
|
||||
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
|
||||
return c.deadline, true
|
||||
}
|
||||
|
||||
func (c *timerCtx) String() string {
|
||||
return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
|
||||
}
|
||||
|
||||
func (c *timerCtx) cancel(removeFromParent bool, err error) {
|
||||
c.cancelCtx.cancel(false, err)
|
||||
if removeFromParent {
|
||||
// Remove this timerCtx from its parent cancelCtx's children.
|
||||
removeChild(c.cancelCtx.Context, c)
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.timer != nil {
|
||||
c.timer.Stop()
|
||||
c.timer = nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete:
|
||||
//
|
||||
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
|
||||
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
// defer cancel() // releases resources if slowOperation completes before timeout elapses
|
||||
// return slowOperation(ctx)
|
||||
// }
|
||||
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
|
||||
return WithDeadline(parent, time.Now().Add(timeout))
|
||||
}
|
||||
|
||||
// WithValue returns a copy of parent in which the value associated with key is
|
||||
// val.
|
||||
//
|
||||
// Use context Values only for request-scoped data that transits processes and
|
||||
// APIs, not for passing optional parameters to functions.
|
||||
func WithValue(parent Context, key interface{}, val interface{}) Context {
|
||||
return &valueCtx{parent, key, val}
|
||||
}
|
||||
|
||||
// A valueCtx carries a key-value pair. It implements Value for that key and
|
||||
// delegates all other calls to the embedded Context.
|
||||
type valueCtx struct {
|
||||
Context
|
||||
key, val interface{}
|
||||
}
|
||||
|
||||
func (c *valueCtx) String() string {
|
||||
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
|
||||
}
|
||||
|
||||
func (c *valueCtx) Value(key interface{}) interface{} {
|
||||
if c.key == key {
|
||||
return c.val
|
||||
}
|
||||
return c.Context.Value(key)
|
||||
}
|
|
@ -1,575 +0,0 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// otherContext is a Context that's not one of the types defined in context.go.
|
||||
// This lets us test code paths that differ based on the underlying type of the
|
||||
// Context.
|
||||
type otherContext struct {
|
||||
Context
|
||||
}
|
||||
|
||||
func TestBackground(t *testing.T) {
|
||||
c := Background()
|
||||
if c == nil {
|
||||
t.Fatalf("Background returned nil")
|
||||
}
|
||||
select {
|
||||
case x := <-c.Done():
|
||||
t.Errorf("<-c.Done() == %v want nothing (it should block)", x)
|
||||
default:
|
||||
}
|
||||
if got, want := fmt.Sprint(c), "context.Background"; got != want {
|
||||
t.Errorf("Background().String() = %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTODO(t *testing.T) {
|
||||
c := TODO()
|
||||
if c == nil {
|
||||
t.Fatalf("TODO returned nil")
|
||||
}
|
||||
select {
|
||||
case x := <-c.Done():
|
||||
t.Errorf("<-c.Done() == %v want nothing (it should block)", x)
|
||||
default:
|
||||
}
|
||||
if got, want := fmt.Sprint(c), "context.TODO"; got != want {
|
||||
t.Errorf("TODO().String() = %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithCancel(t *testing.T) {
|
||||
c1, cancel := WithCancel(Background())
|
||||
|
||||
if got, want := fmt.Sprint(c1), "context.Background.WithCancel"; got != want {
|
||||
t.Errorf("c1.String() = %q want %q", got, want)
|
||||
}
|
||||
|
||||
o := otherContext{c1}
|
||||
c2, _ := WithCancel(o)
|
||||
contexts := []Context{c1, o, c2}
|
||||
|
||||
for i, c := range contexts {
|
||||
if d := c.Done(); d == nil {
|
||||
t.Errorf("c[%d].Done() == %v want non-nil", i, d)
|
||||
}
|
||||
if e := c.Err(); e != nil {
|
||||
t.Errorf("c[%d].Err() == %v want nil", i, e)
|
||||
}
|
||||
|
||||
select {
|
||||
case x := <-c.Done():
|
||||
t.Errorf("<-c.Done() == %v want nothing (it should block)", x)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
cancel()
|
||||
time.Sleep(100 * time.Millisecond) // let cancelation propagate
|
||||
|
||||
for i, c := range contexts {
|
||||
select {
|
||||
case <-c.Done():
|
||||
default:
|
||||
t.Errorf("<-c[%d].Done() blocked, but shouldn't have", i)
|
||||
}
|
||||
if e := c.Err(); e != Canceled {
|
||||
t.Errorf("c[%d].Err() == %v want %v", i, e, Canceled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParentFinishesChild(t *testing.T) {
|
||||
// Context tree:
|
||||
// parent -> cancelChild
|
||||
// parent -> valueChild -> timerChild
|
||||
parent, cancel := WithCancel(Background())
|
||||
cancelChild, stop := WithCancel(parent)
|
||||
defer stop()
|
||||
valueChild := WithValue(parent, "key", "value")
|
||||
timerChild, stop := WithTimeout(valueChild, 10000*time.Hour)
|
||||
defer stop()
|
||||
|
||||
select {
|
||||
case x := <-parent.Done():
|
||||
t.Errorf("<-parent.Done() == %v want nothing (it should block)", x)
|
||||
case x := <-cancelChild.Done():
|
||||
t.Errorf("<-cancelChild.Done() == %v want nothing (it should block)", x)
|
||||
case x := <-timerChild.Done():
|
||||
t.Errorf("<-timerChild.Done() == %v want nothing (it should block)", x)
|
||||
case x := <-valueChild.Done():
|
||||
t.Errorf("<-valueChild.Done() == %v want nothing (it should block)", x)
|
||||
default:
|
||||
}
|
||||
|
||||
// The parent's children should contain the two cancelable children.
|
||||
pc := parent.(*cancelCtx)
|
||||
cc := cancelChild.(*cancelCtx)
|
||||
tc := timerChild.(*timerCtx)
|
||||
pc.mu.Lock()
|
||||
if len(pc.children) != 2 || !pc.children[cc] || !pc.children[tc] {
|
||||
t.Errorf("bad linkage: pc.children = %v, want %v and %v",
|
||||
pc.children, cc, tc)
|
||||
}
|
||||
pc.mu.Unlock()
|
||||
|
||||
if p, ok := parentCancelCtx(cc.Context); !ok || p != pc {
|
||||
t.Errorf("bad linkage: parentCancelCtx(cancelChild.Context) = %v, %v want %v, true", p, ok, pc)
|
||||
}
|
||||
if p, ok := parentCancelCtx(tc.Context); !ok || p != pc {
|
||||
t.Errorf("bad linkage: parentCancelCtx(timerChild.Context) = %v, %v want %v, true", p, ok, pc)
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
pc.mu.Lock()
|
||||
if len(pc.children) != 0 {
|
||||
t.Errorf("pc.cancel didn't clear pc.children = %v", pc.children)
|
||||
}
|
||||
pc.mu.Unlock()
|
||||
|
||||
// parent and children should all be finished.
|
||||
check := func(ctx Context, name string) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
default:
|
||||
t.Errorf("<-%s.Done() blocked, but shouldn't have", name)
|
||||
}
|
||||
if e := ctx.Err(); e != Canceled {
|
||||
t.Errorf("%s.Err() == %v want %v", name, e, Canceled)
|
||||
}
|
||||
}
|
||||
check(parent, "parent")
|
||||
check(cancelChild, "cancelChild")
|
||||
check(valueChild, "valueChild")
|
||||
check(timerChild, "timerChild")
|
||||
|
||||
// WithCancel should return a canceled context on a canceled parent.
|
||||
precanceledChild := WithValue(parent, "key", "value")
|
||||
select {
|
||||
case <-precanceledChild.Done():
|
||||
default:
|
||||
t.Errorf("<-precanceledChild.Done() blocked, but shouldn't have")
|
||||
}
|
||||
if e := precanceledChild.Err(); e != Canceled {
|
||||
t.Errorf("precanceledChild.Err() == %v want %v", e, Canceled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChildFinishesFirst(t *testing.T) {
|
||||
cancelable, stop := WithCancel(Background())
|
||||
defer stop()
|
||||
for _, parent := range []Context{Background(), cancelable} {
|
||||
child, cancel := WithCancel(parent)
|
||||
|
||||
select {
|
||||
case x := <-parent.Done():
|
||||
t.Errorf("<-parent.Done() == %v want nothing (it should block)", x)
|
||||
case x := <-child.Done():
|
||||
t.Errorf("<-child.Done() == %v want nothing (it should block)", x)
|
||||
default:
|
||||
}
|
||||
|
||||
cc := child.(*cancelCtx)
|
||||
pc, pcok := parent.(*cancelCtx) // pcok == false when parent == Background()
|
||||
if p, ok := parentCancelCtx(cc.Context); ok != pcok || (ok && pc != p) {
|
||||
t.Errorf("bad linkage: parentCancelCtx(cc.Context) = %v, %v want %v, %v", p, ok, pc, pcok)
|
||||
}
|
||||
|
||||
if pcok {
|
||||
pc.mu.Lock()
|
||||
if len(pc.children) != 1 || !pc.children[cc] {
|
||||
t.Errorf("bad linkage: pc.children = %v, cc = %v", pc.children, cc)
|
||||
}
|
||||
pc.mu.Unlock()
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
if pcok {
|
||||
pc.mu.Lock()
|
||||
if len(pc.children) != 0 {
|
||||
t.Errorf("child's cancel didn't remove self from pc.children = %v", pc.children)
|
||||
}
|
||||
pc.mu.Unlock()
|
||||
}
|
||||
|
||||
// child should be finished.
|
||||
select {
|
||||
case <-child.Done():
|
||||
default:
|
||||
t.Errorf("<-child.Done() blocked, but shouldn't have")
|
||||
}
|
||||
if e := child.Err(); e != Canceled {
|
||||
t.Errorf("child.Err() == %v want %v", e, Canceled)
|
||||
}
|
||||
|
||||
// parent should not be finished.
|
||||
select {
|
||||
case x := <-parent.Done():
|
||||
t.Errorf("<-parent.Done() == %v want nothing (it should block)", x)
|
||||
default:
|
||||
}
|
||||
if e := parent.Err(); e != nil {
|
||||
t.Errorf("parent.Err() == %v want nil", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDeadline(c Context, wait time.Duration, t *testing.T) {
|
||||
select {
|
||||
case <-time.After(wait):
|
||||
t.Fatalf("context should have timed out")
|
||||
case <-c.Done():
|
||||
}
|
||||
if e := c.Err(); e != DeadlineExceeded {
|
||||
t.Errorf("c.Err() == %v want %v", e, DeadlineExceeded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeadline(t *testing.T) {
|
||||
c, _ := WithDeadline(Background(), time.Now().Add(100*time.Millisecond))
|
||||
if got, prefix := fmt.Sprint(c), "context.Background.WithDeadline("; !strings.HasPrefix(got, prefix) {
|
||||
t.Errorf("c.String() = %q want prefix %q", got, prefix)
|
||||
}
|
||||
testDeadline(c, 200*time.Millisecond, t)
|
||||
|
||||
c, _ = WithDeadline(Background(), time.Now().Add(100*time.Millisecond))
|
||||
o := otherContext{c}
|
||||
testDeadline(o, 200*time.Millisecond, t)
|
||||
|
||||
c, _ = WithDeadline(Background(), time.Now().Add(100*time.Millisecond))
|
||||
o = otherContext{c}
|
||||
c, _ = WithDeadline(o, time.Now().Add(300*time.Millisecond))
|
||||
testDeadline(c, 200*time.Millisecond, t)
|
||||
}
|
||||
|
||||
func TestTimeout(t *testing.T) {
|
||||
c, _ := WithTimeout(Background(), 100*time.Millisecond)
|
||||
if got, prefix := fmt.Sprint(c), "context.Background.WithDeadline("; !strings.HasPrefix(got, prefix) {
|
||||
t.Errorf("c.String() = %q want prefix %q", got, prefix)
|
||||
}
|
||||
testDeadline(c, 200*time.Millisecond, t)
|
||||
|
||||
c, _ = WithTimeout(Background(), 100*time.Millisecond)
|
||||
o := otherContext{c}
|
||||
testDeadline(o, 200*time.Millisecond, t)
|
||||
|
||||
c, _ = WithTimeout(Background(), 100*time.Millisecond)
|
||||
o = otherContext{c}
|
||||
c, _ = WithTimeout(o, 300*time.Millisecond)
|
||||
testDeadline(c, 200*time.Millisecond, t)
|
||||
}
|
||||
|
||||
func TestCanceledTimeout(t *testing.T) {
|
||||
c, _ := WithTimeout(Background(), 200*time.Millisecond)
|
||||
o := otherContext{c}
|
||||
c, cancel := WithTimeout(o, 400*time.Millisecond)
|
||||
cancel()
|
||||
time.Sleep(100 * time.Millisecond) // let cancelation propagate
|
||||
select {
|
||||
case <-c.Done():
|
||||
default:
|
||||
t.Errorf("<-c.Done() blocked, but shouldn't have")
|
||||
}
|
||||
if e := c.Err(); e != Canceled {
|
||||
t.Errorf("c.Err() == %v want %v", e, Canceled)
|
||||
}
|
||||
}
|
||||
|
||||
type key1 int
|
||||
type key2 int
|
||||
|
||||
var k1 = key1(1)
|
||||
var k2 = key2(1) // same int as k1, different type
|
||||
var k3 = key2(3) // same type as k2, different int
|
||||
|
||||
func TestValues(t *testing.T) {
|
||||
check := func(c Context, nm, v1, v2, v3 string) {
|
||||
if v, ok := c.Value(k1).(string); ok == (len(v1) == 0) || v != v1 {
|
||||
t.Errorf(`%s.Value(k1).(string) = %q, %t want %q, %t`, nm, v, ok, v1, len(v1) != 0)
|
||||
}
|
||||
if v, ok := c.Value(k2).(string); ok == (len(v2) == 0) || v != v2 {
|
||||
t.Errorf(`%s.Value(k2).(string) = %q, %t want %q, %t`, nm, v, ok, v2, len(v2) != 0)
|
||||
}
|
||||
if v, ok := c.Value(k3).(string); ok == (len(v3) == 0) || v != v3 {
|
||||
t.Errorf(`%s.Value(k3).(string) = %q, %t want %q, %t`, nm, v, ok, v3, len(v3) != 0)
|
||||
}
|
||||
}
|
||||
|
||||
c0 := Background()
|
||||
check(c0, "c0", "", "", "")
|
||||
|
||||
c1 := WithValue(Background(), k1, "c1k1")
|
||||
check(c1, "c1", "c1k1", "", "")
|
||||
|
||||
if got, want := fmt.Sprint(c1), `context.Background.WithValue(1, "c1k1")`; got != want {
|
||||
t.Errorf("c.String() = %q want %q", got, want)
|
||||
}
|
||||
|
||||
c2 := WithValue(c1, k2, "c2k2")
|
||||
check(c2, "c2", "c1k1", "c2k2", "")
|
||||
|
||||
c3 := WithValue(c2, k3, "c3k3")
|
||||
check(c3, "c2", "c1k1", "c2k2", "c3k3")
|
||||
|
||||
c4 := WithValue(c3, k1, nil)
|
||||
check(c4, "c4", "", "c2k2", "c3k3")
|
||||
|
||||
o0 := otherContext{Background()}
|
||||
check(o0, "o0", "", "", "")
|
||||
|
||||
o1 := otherContext{WithValue(Background(), k1, "c1k1")}
|
||||
check(o1, "o1", "c1k1", "", "")
|
||||
|
||||
o2 := WithValue(o1, k2, "o2k2")
|
||||
check(o2, "o2", "c1k1", "o2k2", "")
|
||||
|
||||
o3 := otherContext{c4}
|
||||
check(o3, "o3", "", "c2k2", "c3k3")
|
||||
|
||||
o4 := WithValue(o3, k3, nil)
|
||||
check(o4, "o4", "", "c2k2", "")
|
||||
}
|
||||
|
||||
func TestAllocs(t *testing.T) {
|
||||
bg := Background()
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
f func()
|
||||
limit float64
|
||||
gccgoLimit float64
|
||||
}{
|
||||
{
|
||||
desc: "Background()",
|
||||
f: func() { Background() },
|
||||
limit: 0,
|
||||
gccgoLimit: 0,
|
||||
},
|
||||
{
|
||||
desc: fmt.Sprintf("WithValue(bg, %v, nil)", k1),
|
||||
f: func() {
|
||||
c := WithValue(bg, k1, nil)
|
||||
c.Value(k1)
|
||||
},
|
||||
limit: 3,
|
||||
gccgoLimit: 3,
|
||||
},
|
||||
{
|
||||
desc: "WithTimeout(bg, 15*time.Millisecond)",
|
||||
f: func() {
|
||||
c, _ := WithTimeout(bg, 15*time.Millisecond)
|
||||
<-c.Done()
|
||||
},
|
||||
limit: 8,
|
||||
gccgoLimit: 15,
|
||||
},
|
||||
{
|
||||
desc: "WithCancel(bg)",
|
||||
f: func() {
|
||||
c, cancel := WithCancel(bg)
|
||||
cancel()
|
||||
<-c.Done()
|
||||
},
|
||||
limit: 5,
|
||||
gccgoLimit: 8,
|
||||
},
|
||||
{
|
||||
desc: "WithTimeout(bg, 100*time.Millisecond)",
|
||||
f: func() {
|
||||
c, cancel := WithTimeout(bg, 100*time.Millisecond)
|
||||
cancel()
|
||||
<-c.Done()
|
||||
},
|
||||
limit: 8,
|
||||
gccgoLimit: 25,
|
||||
},
|
||||
} {
|
||||
limit := test.limit
|
||||
if runtime.Compiler == "gccgo" {
|
||||
// gccgo does not yet do escape analysis.
|
||||
// TOOD(iant): Remove this when gccgo does do escape analysis.
|
||||
limit = test.gccgoLimit
|
||||
}
|
||||
if n := testing.AllocsPerRun(100, test.f); n > limit {
|
||||
t.Errorf("%s allocs = %f want %d", test.desc, n, int(limit))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimultaneousCancels(t *testing.T) {
|
||||
root, cancel := WithCancel(Background())
|
||||
m := map[Context]CancelFunc{root: cancel}
|
||||
q := []Context{root}
|
||||
// Create a tree of contexts.
|
||||
for len(q) != 0 && len(m) < 100 {
|
||||
parent := q[0]
|
||||
q = q[1:]
|
||||
for i := 0; i < 4; i++ {
|
||||
ctx, cancel := WithCancel(parent)
|
||||
m[ctx] = cancel
|
||||
q = append(q, ctx)
|
||||
}
|
||||
}
|
||||
// Start all the cancels in a random order.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(m))
|
||||
for _, cancel := range m {
|
||||
go func(cancel CancelFunc) {
|
||||
cancel()
|
||||
wg.Done()
|
||||
}(cancel)
|
||||
}
|
||||
// Wait on all the contexts in a random order.
|
||||
for ctx := range m {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(1 * time.Second):
|
||||
buf := make([]byte, 10<<10)
|
||||
n := runtime.Stack(buf, true)
|
||||
t.Fatalf("timed out waiting for <-ctx.Done(); stacks:\n%s", buf[:n])
|
||||
}
|
||||
}
|
||||
// Wait for all the cancel functions to return.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(1 * time.Second):
|
||||
buf := make([]byte, 10<<10)
|
||||
n := runtime.Stack(buf, true)
|
||||
t.Fatalf("timed out waiting for cancel functions; stacks:\n%s", buf[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterlockedCancels(t *testing.T) {
|
||||
parent, cancelParent := WithCancel(Background())
|
||||
child, cancelChild := WithCancel(parent)
|
||||
go func() {
|
||||
parent.Done()
|
||||
cancelChild()
|
||||
}()
|
||||
cancelParent()
|
||||
select {
|
||||
case <-child.Done():
|
||||
case <-time.After(1 * time.Second):
|
||||
buf := make([]byte, 10<<10)
|
||||
n := runtime.Stack(buf, true)
|
||||
t.Fatalf("timed out waiting for child.Done(); stacks:\n%s", buf[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLayersCancel(t *testing.T) {
|
||||
testLayers(t, time.Now().UnixNano(), false)
|
||||
}
|
||||
|
||||
func TestLayersTimeout(t *testing.T) {
|
||||
testLayers(t, time.Now().UnixNano(), true)
|
||||
}
|
||||
|
||||
func testLayers(t *testing.T, seed int64, testTimeout bool) {
|
||||
rand.Seed(seed)
|
||||
errorf := func(format string, a ...interface{}) {
|
||||
t.Errorf(fmt.Sprintf("seed=%d: %s", seed, format), a...)
|
||||
}
|
||||
const (
|
||||
timeout = 200 * time.Millisecond
|
||||
minLayers = 30
|
||||
)
|
||||
type value int
|
||||
var (
|
||||
vals []*value
|
||||
cancels []CancelFunc
|
||||
numTimers int
|
||||
ctx = Background()
|
||||
)
|
||||
for i := 0; i < minLayers || numTimers == 0 || len(cancels) == 0 || len(vals) == 0; i++ {
|
||||
switch rand.Intn(3) {
|
||||
case 0:
|
||||
v := new(value)
|
||||
ctx = WithValue(ctx, v, v)
|
||||
vals = append(vals, v)
|
||||
case 1:
|
||||
var cancel CancelFunc
|
||||
ctx, cancel = WithCancel(ctx)
|
||||
cancels = append(cancels, cancel)
|
||||
case 2:
|
||||
var cancel CancelFunc
|
||||
ctx, cancel = WithTimeout(ctx, timeout)
|
||||
cancels = append(cancels, cancel)
|
||||
numTimers++
|
||||
}
|
||||
}
|
||||
checkValues := func(when string) {
|
||||
for _, key := range vals {
|
||||
if val := ctx.Value(key).(*value); key != val {
|
||||
errorf("%s: ctx.Value(%p) = %p want %p", when, key, val, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
errorf("ctx should not be canceled yet")
|
||||
default:
|
||||
}
|
||||
if s, prefix := fmt.Sprint(ctx), "context.Background."; !strings.HasPrefix(s, prefix) {
|
||||
t.Errorf("ctx.String() = %q want prefix %q", s, prefix)
|
||||
}
|
||||
t.Log(ctx)
|
||||
checkValues("before cancel")
|
||||
if testTimeout {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(timeout + 100*time.Millisecond):
|
||||
errorf("ctx should have timed out")
|
||||
}
|
||||
checkValues("after timeout")
|
||||
} else {
|
||||
cancel := cancels[rand.Intn(len(cancels))]
|
||||
cancel()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
default:
|
||||
errorf("ctx should be canceled")
|
||||
}
|
||||
checkValues("after cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelRemoves(t *testing.T) {
|
||||
checkChildren := func(when string, ctx Context, want int) {
|
||||
if got := len(ctx.(*cancelCtx).children); got != want {
|
||||
t.Errorf("%s: context has %d children, want %d", when, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
ctx, _ := WithCancel(Background())
|
||||
checkChildren("after creation", ctx, 0)
|
||||
_, cancel := WithCancel(ctx)
|
||||
checkChildren("with WithCancel child ", ctx, 1)
|
||||
cancel()
|
||||
checkChildren("after cancelling WithCancel child", ctx, 0)
|
||||
|
||||
ctx, _ = WithCancel(Background())
|
||||
checkChildren("after creation", ctx, 0)
|
||||
_, cancel = WithTimeout(ctx, 60*time.Minute)
|
||||
checkChildren("with WithTimeout child ", ctx, 1)
|
||||
cancel()
|
||||
checkChildren("after cancelling WithTimeout child", ctx, 0)
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.5
|
||||
|
||||
package ctxhttp
|
||||
|
||||
import "net/http"
|
||||
|
||||
func canceler(client *http.Client, req *http.Request) func() {
|
||||
ch := make(chan struct{})
|
||||
req.Cancel = ch
|
||||
|
||||
return func() {
|
||||
close(ch)
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.5
|
||||
|
||||
package ctxhttp
|
||||
|
||||
import "net/http"
|
||||
|
||||
type requestCanceler interface {
|
||||
CancelRequest(*http.Request)
|
||||
}
|
||||
|
||||
func canceler(client *http.Client, req *http.Request) func() {
|
||||
rc, ok := client.Transport.(requestCanceler)
|
||||
if !ok {
|
||||
return func() {}
|
||||
}
|
||||
return func() {
|
||||
rc.CancelRequest(req)
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package ctxhttp provides helper functions for performing context-aware HTTP requests.
|
||||
package ctxhttp // import "golang.org/x/net/context/ctxhttp"
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Do sends an HTTP request with the provided http.Client and returns an HTTP response.
|
||||
// If the client is nil, http.DefaultClient is used.
|
||||
// If the context is canceled or times out, ctx.Err() will be returned.
|
||||
func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
|
||||
// Request cancelation changed in Go 1.5, see cancelreq.go and cancelreq_go14.go.
|
||||
cancel := canceler(client, req)
|
||||
|
||||
type responseAndError struct {
|
||||
resp *http.Response
|
||||
err error
|
||||
}
|
||||
result := make(chan responseAndError, 1)
|
||||
|
||||
go func() {
|
||||
resp, err := client.Do(req)
|
||||
result <- responseAndError{resp, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
return nil, ctx.Err()
|
||||
case r := <-result:
|
||||
return r.resp, r.err
|
||||
}
|
||||
}
|
||||
|
||||
// Get issues a GET request via the Do function.
|
||||
func Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Do(ctx, client, req)
|
||||
}
|
||||
|
||||
// Head issues a HEAD request via the Do function.
|
||||
func Head(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("HEAD", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Do(ctx, client, req)
|
||||
}
|
||||
|
||||
// Post issues a POST request via the Do function.
|
||||
func Post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", bodyType)
|
||||
return Do(ctx, client, req)
|
||||
}
|
||||
|
||||
// PostForm issues a POST request via the Do function.
|
||||
func PostForm(ctx context.Context, client *http.Client, url string, data url.Values) (*http.Response, error) {
|
||||
return Post(ctx, client, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ctxhttp
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const (
|
||||
requestDuration = 100 * time.Millisecond
|
||||
requestBody = "ok"
|
||||
)
|
||||
|
||||
func TestNoTimeout(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
resp, err := doRequest(ctx)
|
||||
|
||||
if resp == nil || err != nil {
|
||||
t.Fatalf("error received from client: %v %v", err, resp)
|
||||
}
|
||||
}
|
||||
func TestCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(requestDuration / 2)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
resp, err := doRequest(ctx)
|
||||
|
||||
if resp != nil || err == nil {
|
||||
t.Fatalf("expected error, didn't get one. resp: %v", resp)
|
||||
}
|
||||
if err != ctx.Err() {
|
||||
t.Fatalf("expected error from context but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelAfterRequest(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
resp, err := doRequest(ctx)
|
||||
|
||||
// Cancel before reading the body.
|
||||
// Request.Body should still be readable after the context is canceled.
|
||||
cancel()
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil || string(b) != requestBody {
|
||||
t.Fatalf("could not read body: %q %v", b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func doRequest(ctx context.Context) (*http.Response, error) {
|
||||
var okHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(requestDuration)
|
||||
w.Write([]byte(requestBody))
|
||||
})
|
||||
|
||||
serv := httptest.NewServer(okHandler)
|
||||
defer serv.Close()
|
||||
|
||||
return Get(ctx, nil, serv.URL)
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package context_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func ExampleWithTimeout() {
|
||||
// Pass a context with a timeout to tell a blocking function that it
|
||||
// should abandon its work after the timeout elapses.
|
||||
ctx, _ := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
select {
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
fmt.Println("overslept")
|
||||
case <-ctx.Done():
|
||||
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
|
||||
}
|
||||
// Output:
|
||||
// context deadline exceeded
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
# This source code refers to The Go Authors for copyright purposes.
|
||||
# The master list of authors is in the main Go distribution,
|
||||
# visible at http://tip.golang.org/AUTHORS.
|
|
@ -1,31 +0,0 @@
|
|||
# Contributing to Go
|
||||
|
||||
Go is an open source project.
|
||||
|
||||
It is the work of hundreds of contributors. We appreciate your help!
|
||||
|
||||
|
||||
## Filing issues
|
||||
|
||||
When [filing an issue](https://github.com/golang/oauth2/issues), make sure to answer these five questions:
|
||||
|
||||
1. What version of Go are you using (`go version`)?
|
||||
2. What operating system and processor architecture are you using?
|
||||
3. What did you do?
|
||||
4. What did you expect to see?
|
||||
5. What did you see instead?
|
||||
|
||||
General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker.
|
||||
The gophers there will answer or ask you to file an issue if you've tripped over a bug.
|
||||
|
||||
## Contributing code
|
||||
|
||||
Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html)
|
||||
before sending patches.
|
||||
|
||||
**We do not accept GitHub pull requests**
|
||||
(we use [Gerrit](https://code.google.com/p/gerrit/) instead for code review).
|
||||
|
||||
Unless otherwise noted, the Go source files are distributed under
|
||||
the BSD-style license found in the LICENSE file.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# This source code was written by the Go contributors.
|
||||
# The master list of contributors is in the main Go distribution,
|
||||
# visible at http://tip.golang.org/CONTRIBUTORS.
|
|
@ -1,27 +0,0 @@
|
|||
Copyright (c) 2009 The oauth2 Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -1,64 +0,0 @@
|
|||
# OAuth2 for Go
|
||||
|
||||
[![Build Status](https://travis-ci.org/golang/oauth2.svg?branch=master)](https://travis-ci.org/golang/oauth2)
|
||||
|
||||
oauth2 package contains a client implementation for OAuth 2.0 spec.
|
||||
|
||||
## Installation
|
||||
|
||||
~~~~
|
||||
go get golang.org/x/oauth2
|
||||
~~~~
|
||||
|
||||
See godoc for further documentation and examples.
|
||||
|
||||
* [godoc.org/golang.org/x/oauth2](http://godoc.org/golang.org/x/oauth2)
|
||||
* [godoc.org/golang.org/x/oauth2/google](http://godoc.org/golang.org/x/oauth2/google)
|
||||
|
||||
|
||||
## App Engine
|
||||
|
||||
In change 96e89be (March 2015) we removed the `oauth2.Context2` type in favor
|
||||
of the [`context.Context`](https://golang.org/x/net/context#Context) type from
|
||||
the `golang.org/x/net/context` package
|
||||
|
||||
This means its no longer possible to use the "Classic App Engine"
|
||||
`appengine.Context` type with the `oauth2` package. (You're using
|
||||
Classic App Engine if you import the package `"appengine"`.)
|
||||
|
||||
To work around this, you may use the new `"google.golang.org/appengine"`
|
||||
package. This package has almost the same API as the `"appengine"` package,
|
||||
but it can be fetched with `go get` and used on "Managed VMs" and well as
|
||||
Classic App Engine.
|
||||
|
||||
See the [new `appengine` package's readme](https://github.com/golang/appengine#updating-a-go-app-engine-app)
|
||||
for information on updating your app.
|
||||
|
||||
If you don't want to update your entire app to use the new App Engine packages,
|
||||
you may use both sets of packages in parallel, using only the new packages
|
||||
with the `oauth2` package.
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
newappengine "google.golang.org/appengine"
|
||||
newurlfetch "google.golang.org/appengine/urlfetch"
|
||||
|
||||
"appengine"
|
||||
)
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
var c appengine.Context = appengine.NewContext(r)
|
||||
c.Infof("Logging a message with the old package")
|
||||
|
||||
var ctx context.Context = newappengine.NewContext(r)
|
||||
client := &http.Client{
|
||||
Transport: &oauth2.Transport{
|
||||
Source: google.AppEngineTokenSource(ctx, "scope"),
|
||||
Base: &newurlfetch.Transport{Context: ctx},
|
||||
},
|
||||
}
|
||||
client.Get("...")
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package bitbucket provides constants for using OAuth2 to access Bitbucket.
|
||||
package bitbucket
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Endpoint is Bitbucket's OAuth 2.0 endpoint.
|
||||
var Endpoint = oauth2.Endpoint{
|
||||
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
|
||||
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build appengine
|
||||
|
||||
// App Engine hooks.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2/internal"
|
||||
"google.golang.org/appengine/urlfetch"
|
||||
)
|
||||
|
||||
func init() {
|
||||
internal.RegisterContextClientFunc(contextClientAppEngine)
|
||||
}
|
||||
|
||||
func contextClientAppEngine(ctx context.Context) (*http.Client, error) {
|
||||
return urlfetch.Client(ctx), nil
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package clientcredentials implements the OAuth2.0 "client credentials" token flow,
|
||||
// also known as the "two-legged OAuth 2.0".
|
||||
//
|
||||
// This should be used when the client is acting on its own behalf or when the client
|
||||
// is the resource owner. It may also be used when requesting access to protected
|
||||
// resources based on an authorization previously arranged with the authorization
|
||||
// server.
|
||||
//
|
||||
// See http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.4
|
||||
package clientcredentials // import "golang.org/x/oauth2/clientcredentials"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/internal"
|
||||
)
|
||||
|
||||
// tokenFromInternal maps an *internal.Token struct into
|
||||
// an *oauth2.Token struct.
|
||||
func tokenFromInternal(t *internal.Token) *oauth2.Token {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
tk := &oauth2.Token{
|
||||
AccessToken: t.AccessToken,
|
||||
TokenType: t.TokenType,
|
||||
RefreshToken: t.RefreshToken,
|
||||
Expiry: t.Expiry,
|
||||
}
|
||||
return tk.WithExtra(t.Raw)
|
||||
}
|
||||
|
||||
// retrieveToken takes a *Config and uses that to retrieve an *internal.Token.
|
||||
// This token is then mapped from *internal.Token into an *oauth2.Token which is
|
||||
// returned along with an error.
|
||||
func retrieveToken(ctx context.Context, c *Config, v url.Values) (*oauth2.Token, error) {
|
||||
tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.TokenURL, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tokenFromInternal(tk), nil
|
||||
}
|
||||
|
||||
// Client Credentials Config describes a 2-legged OAuth2 flow, with both the
|
||||
// client application information and the server's endpoint URLs.
|
||||
type Config struct {
|
||||
// ClientID is the application's ID.
|
||||
ClientID string
|
||||
|
||||
// ClientSecret is the application's secret.
|
||||
ClientSecret string
|
||||
|
||||
// TokenURL is the resource server's token endpoint
|
||||
// URL. This is a constant specific to each server.
|
||||
TokenURL string
|
||||
|
||||
// Scope specifies optional requested permissions.
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
// Token uses client credentials to retreive a token.
|
||||
// The HTTP client to use is derived from the context.
|
||||
// If nil, http.DefaultClient is used.
|
||||
func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) {
|
||||
return retrieveToken(ctx, c, url.Values{
|
||||
"grant_type": {"client_credentials"},
|
||||
"scope": internal.CondVal(strings.Join(c.Scopes, " ")),
|
||||
})
|
||||
}
|
||||
|
||||
// Client returns an HTTP client using the provided token.
|
||||
// The token will auto-refresh as necessary. The underlying
|
||||
// HTTP transport will be obtained using the provided context.
|
||||
// The returned client and its Transport should not be modified.
|
||||
func (c *Config) Client(ctx context.Context) *http.Client {
|
||||
return oauth2.NewClient(ctx, c.TokenSource(ctx))
|
||||
}
|
||||
|
||||
// TokenSource returns a TokenSource that returns t until t expires,
|
||||
// automatically refreshing it as necessary using the provided context and the
|
||||
// client ID and client secret.
|
||||
//
|
||||
// Most users will use Config.Client instead.
|
||||
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
|
||||
source := &tokenSource{
|
||||
ctx: ctx,
|
||||
conf: c,
|
||||
}
|
||||
return oauth2.ReuseTokenSource(nil, source)
|
||||
}
|
||||
|
||||
type tokenSource struct {
|
||||
ctx context.Context
|
||||
conf *Config
|
||||
}
|
||||
|
||||
// Token refreshes the token by using a new client credentials request.
|
||||
// tokens received this way do not include a refresh token
|
||||
func (c *tokenSource) Token() (*oauth2.Token, error) {
|
||||
return retrieveToken(c.ctx, c.conf, url.Values{
|
||||
"grant_type": {"client_credentials"},
|
||||
"scope": internal.CondVal(strings.Join(c.conf.Scopes, " ")),
|
||||
})
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package clientcredentials
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func newConf(url string) *Config {
|
||||
return &Config{
|
||||
ClientID: "CLIENT_ID",
|
||||
ClientSecret: "CLIENT_SECRET",
|
||||
Scopes: []string{"scope1", "scope2"},
|
||||
TokenURL: url + "/token",
|
||||
}
|
||||
}
|
||||
|
||||
type mockTransport struct {
|
||||
rt func(req *http.Request) (resp *http.Response, err error)
|
||||
}
|
||||
|
||||
func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
return t.rt(req)
|
||||
}
|
||||
|
||||
func TestTokenRequest(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() != "/token" {
|
||||
t.Errorf("authenticate client request URL = %q; want %q", r.URL, "/token")
|
||||
}
|
||||
headerAuth := r.Header.Get("Authorization")
|
||||
if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" {
|
||||
t.Errorf("Unexpected authorization header, %v is found.", headerAuth)
|
||||
}
|
||||
if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want {
|
||||
t.Errorf("Content-Type header = %q; want %q", got, want)
|
||||
}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
r.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("failed reading request body: %s.", err)
|
||||
}
|
||||
if string(body) != "client_id=CLIENT_ID&grant_type=client_credentials&scope=scope1+scope2" {
|
||||
t.Errorf("payload = %q; want %q", string(body), "client_id=CLIENT_ID&grant_type=client_credentials&scope=scope1+scope2")
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&token_type=bearer"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
tok, err := conf.Token(oauth2.NoContext)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !tok.Valid() {
|
||||
t.Fatalf("token invalid. got: %#v", tok)
|
||||
}
|
||||
if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" {
|
||||
t.Errorf("Access token = %q; want %q", tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c")
|
||||
}
|
||||
if tok.TokenType != "bearer" {
|
||||
t.Errorf("token type = %q; want %q", tok.TokenType, "bearer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenRefreshRequest(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() == "/somethingelse" {
|
||||
return
|
||||
}
|
||||
if r.URL.String() != "/token" {
|
||||
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
|
||||
}
|
||||
headerContentType := r.Header.Get("Content-Type")
|
||||
if headerContentType != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType)
|
||||
}
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
if string(body) != "client_id=CLIENT_ID&grant_type=client_credentials&scope=scope1+scope2" {
|
||||
t.Errorf("Unexpected refresh token payload, %v is found.", string(body))
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
c := conf.Client(oauth2.NoContext)
|
||||
c.Get(ts.URL + "/somethingelse")
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func ExampleConfig() {
|
||||
conf := &oauth2.Config{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
Scopes: []string{"SCOPE1", "SCOPE2"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://provider.com/o/oauth2/auth",
|
||||
TokenURL: "https://provider.com/o/oauth2/token",
|
||||
},
|
||||
}
|
||||
|
||||
// Redirect user to consent page to ask for permission
|
||||
// for the scopes specified above.
|
||||
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
|
||||
fmt.Printf("Visit the URL for the auth dialog: %v", url)
|
||||
|
||||
// Use the authorization code that is pushed to the redirect URL.
|
||||
// NewTransportWithCode will do the handshake to retrieve
|
||||
// an access token and initiate a Transport that is
|
||||
// authorized and authenticated by the retrieved token.
|
||||
var code string
|
||||
if _, err := fmt.Scan(&code); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tok, err := conf.Exchange(oauth2.NoContext, code)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
client := conf.Client(oauth2.NoContext, tok)
|
||||
client.Get("...")
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package facebook provides constants for using OAuth2 to access Facebook.
|
||||
package facebook // import "golang.org/x/oauth2/facebook"
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Endpoint is Facebook's OAuth 2.0 endpoint.
|
||||
var Endpoint = oauth2.Endpoint{
|
||||
AuthURL: "https://www.facebook.com/dialog/oauth",
|
||||
TokenURL: "https://graph.facebook.com/oauth/access_token",
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package github provides constants for using OAuth2 to access Github.
|
||||
package github // import "golang.org/x/oauth2/github"
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Endpoint is Github's OAuth 2.0 endpoint.
|
||||
var Endpoint = oauth2.Endpoint{
|
||||
AuthURL: "https://github.com/login/oauth/authorize",
|
||||
TokenURL: "https://github.com/login/oauth/access_token",
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package google
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Set at init time by appenginevm_hook.go. If true, we are on App Engine Managed VMs.
|
||||
var appengineVM bool
|
||||
|
||||
// Set at init time by appengine_hook.go. If nil, we're not on App Engine.
|
||||
var appengineTokenFunc func(c context.Context, scopes ...string) (token string, expiry time.Time, err error)
|
||||
|
||||
// AppEngineTokenSource returns a token source that fetches tokens
|
||||
// issued to the current App Engine application's service account.
|
||||
// If you are implementing a 3-legged OAuth 2.0 flow on App Engine
|
||||
// that involves user accounts, see oauth2.Config instead.
|
||||
//
|
||||
// The provided context must have come from appengine.NewContext.
|
||||
func AppEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource {
|
||||
if appengineTokenFunc == nil {
|
||||
panic("google: AppEngineTokenSource can only be used on App Engine.")
|
||||
}
|
||||
scopes := append([]string{}, scope...)
|
||||
sort.Strings(scopes)
|
||||
return &appEngineTokenSource{
|
||||
ctx: ctx,
|
||||
scopes: scopes,
|
||||
key: strings.Join(scopes, " "),
|
||||
}
|
||||
}
|
||||
|
||||
// aeTokens helps the fetched tokens to be reused until their expiration.
|
||||
var (
|
||||
aeTokensMu sync.Mutex
|
||||
aeTokens = make(map[string]*tokenLock) // key is space-separated scopes
|
||||
)
|
||||
|
||||
type tokenLock struct {
|
||||
mu sync.Mutex // guards t; held while fetching or updating t
|
||||
t *oauth2.Token
|
||||
}
|
||||
|
||||
type appEngineTokenSource struct {
|
||||
ctx context.Context
|
||||
scopes []string
|
||||
key string // to aeTokens map; space-separated scopes
|
||||
}
|
||||
|
||||
func (ts *appEngineTokenSource) Token() (*oauth2.Token, error) {
|
||||
if appengineTokenFunc == nil {
|
||||
panic("google: AppEngineTokenSource can only be used on App Engine.")
|
||||
}
|
||||
|
||||
aeTokensMu.Lock()
|
||||
tok, ok := aeTokens[ts.key]
|
||||
if !ok {
|
||||
tok = &tokenLock{}
|
||||
aeTokens[ts.key] = tok
|
||||
}
|
||||
aeTokensMu.Unlock()
|
||||
|
||||
tok.mu.Lock()
|
||||
defer tok.mu.Unlock()
|
||||
if tok.t.Valid() {
|
||||
return tok.t, nil
|
||||
}
|
||||
access, exp, err := appengineTokenFunc(ts.ctx, ts.scopes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tok.t = &oauth2.Token{
|
||||
AccessToken: access,
|
||||
Expiry: exp,
|
||||
}
|
||||
return tok.t, nil
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build appengine
|
||||
|
||||
package google
|
||||
|
||||
import "google.golang.org/appengine"
|
||||
|
||||
func init() {
|
||||
appengineTokenFunc = appengine.AccessToken
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build appenginevm
|
||||
|
||||
package google
|
||||
|
||||
import "google.golang.org/appengine"
|
||||
|
||||
func init() {
|
||||
appengineVM = true
|
||||
appengineTokenFunc = appengine.AccessToken
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package google
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/jwt"
|
||||
"google.golang.org/cloud/compute/metadata"
|
||||
)
|
||||
|
||||
// DefaultClient returns an HTTP Client that uses the
|
||||
// DefaultTokenSource to obtain authentication credentials.
|
||||
//
|
||||
// This client should be used when developing services
|
||||
// that run on Google App Engine or Google Compute Engine
|
||||
// and use "Application Default Credentials."
|
||||
//
|
||||
// For more details, see:
|
||||
// https://developers.google.com/accounts/docs/application-default-credentials
|
||||
//
|
||||
func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) {
|
||||
ts, err := DefaultTokenSource(ctx, scope...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return oauth2.NewClient(ctx, ts), nil
|
||||
}
|
||||
|
||||
// DefaultTokenSource is a token source that uses
|
||||
// "Application Default Credentials".
|
||||
//
|
||||
// It looks for credentials in the following places,
|
||||
// preferring the first location found:
|
||||
//
|
||||
// 1. A JSON file whose path is specified by the
|
||||
// GOOGLE_APPLICATION_CREDENTIALS environment variable.
|
||||
// 2. A JSON file in a location known to the gcloud command-line tool.
|
||||
// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json.
|
||||
// On other systems, $HOME/.config/gcloud/application_default_credentials.json.
|
||||
// 3. On Google App Engine it uses the appengine.AccessToken function.
|
||||
// 4. On Google Compute Engine and Google App Engine Managed VMs, it fetches
|
||||
// credentials from the metadata server.
|
||||
// (In this final case any provided scopes are ignored.)
|
||||
//
|
||||
// For more details, see:
|
||||
// https://developers.google.com/accounts/docs/application-default-credentials
|
||||
//
|
||||
func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSource, error) {
|
||||
// First, try the environment variable.
|
||||
const envVar = "GOOGLE_APPLICATION_CREDENTIALS"
|
||||
if filename := os.Getenv(envVar); filename != "" {
|
||||
ts, err := tokenSourceFromFile(ctx, filename, scope)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("google: error getting credentials using %v environment variable: %v", envVar, err)
|
||||
}
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
// Second, try a well-known file.
|
||||
filename := wellKnownFile()
|
||||
_, err := os.Stat(filename)
|
||||
if err == nil {
|
||||
ts, err2 := tokenSourceFromFile(ctx, filename, scope)
|
||||
if err2 == nil {
|
||||
return ts, nil
|
||||
}
|
||||
err = err2
|
||||
} else if os.IsNotExist(err) {
|
||||
err = nil // ignore this error
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err)
|
||||
}
|
||||
|
||||
// Third, if we're on Google App Engine use those credentials.
|
||||
if appengineTokenFunc != nil && !appengineVM {
|
||||
return AppEngineTokenSource(ctx, scope...), nil
|
||||
}
|
||||
|
||||
// Fourth, if we're on Google Compute Engine use the metadata server.
|
||||
if metadata.OnGCE() {
|
||||
return ComputeTokenSource(""), nil
|
||||
}
|
||||
|
||||
// None are found; return helpful error.
|
||||
const url = "https://developers.google.com/accounts/docs/application-default-credentials"
|
||||
return nil, fmt.Errorf("google: could not find default credentials. See %v for more information.", url)
|
||||
}
|
||||
|
||||
func wellKnownFile() string {
|
||||
const f = "application_default_credentials.json"
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(os.Getenv("APPDATA"), "gcloud", f)
|
||||
}
|
||||
return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f)
|
||||
}
|
||||
|
||||
func tokenSourceFromFile(ctx context.Context, filename string, scopes []string) (oauth2.TokenSource, error) {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var d struct {
|
||||
// Common fields
|
||||
Type string
|
||||
ClientID string `json:"client_id"`
|
||||
|
||||
// User Credential fields
|
||||
ClientSecret string `json:"client_secret"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
|
||||
// Service Account fields
|
||||
ClientEmail string `json:"client_email"`
|
||||
PrivateKeyID string `json:"private_key_id"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch d.Type {
|
||||
case "authorized_user":
|
||||
cfg := &oauth2.Config{
|
||||
ClientID: d.ClientID,
|
||||
ClientSecret: d.ClientSecret,
|
||||
Scopes: append([]string{}, scopes...), // copy
|
||||
Endpoint: Endpoint,
|
||||
}
|
||||
tok := &oauth2.Token{RefreshToken: d.RefreshToken}
|
||||
return cfg.TokenSource(ctx, tok), nil
|
||||
case "service_account":
|
||||
cfg := &jwt.Config{
|
||||
Email: d.ClientEmail,
|
||||
PrivateKey: []byte(d.PrivateKey),
|
||||
Scopes: append([]string{}, scopes...), // copy
|
||||
TokenURL: JWTTokenURL,
|
||||
}
|
||||
return cfg.TokenSource(ctx), nil
|
||||
case "":
|
||||
return nil, errors.New("missing 'type' field in credentials")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown credential type: %q", d.Type)
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build appenginevm !appengine
|
||||
|
||||
package google_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"golang.org/x/oauth2/jwt"
|
||||
"google.golang.org/appengine"
|
||||
"google.golang.org/appengine/urlfetch"
|
||||
)
|
||||
|
||||
func ExampleDefaultClient() {
|
||||
client, err := google.DefaultClient(oauth2.NoContext,
|
||||
"https://www.googleapis.com/auth/devstorage.full_control")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
client.Get("...")
|
||||
}
|
||||
|
||||
func Example_webServer() {
|
||||
// Your credentials should be obtained from the Google
|
||||
// Developer Console (https://console.developers.google.com).
|
||||
conf := &oauth2.Config{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
RedirectURL: "YOUR_REDIRECT_URL",
|
||||
Scopes: []string{
|
||||
"https://www.googleapis.com/auth/bigquery",
|
||||
"https://www.googleapis.com/auth/blogger",
|
||||
},
|
||||
Endpoint: google.Endpoint,
|
||||
}
|
||||
// Redirect user to Google's consent page to ask for permission
|
||||
// for the scopes specified above.
|
||||
url := conf.AuthCodeURL("state")
|
||||
fmt.Printf("Visit the URL for the auth dialog: %v", url)
|
||||
|
||||
// Handle the exchange code to initiate a transport.
|
||||
tok, err := conf.Exchange(oauth2.NoContext, "authorization-code")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
client := conf.Client(oauth2.NoContext, tok)
|
||||
client.Get("...")
|
||||
}
|
||||
|
||||
func ExampleJWTConfigFromJSON() {
|
||||
// Your credentials should be obtained from the Google
|
||||
// Developer Console (https://console.developers.google.com).
|
||||
// Navigate to your project, then see the "Credentials" page
|
||||
// under "APIs & Auth".
|
||||
// To create a service account client, click "Create new Client ID",
|
||||
// select "Service Account", and click "Create Client ID". A JSON
|
||||
// key file will then be downloaded to your computer.
|
||||
data, err := ioutil.ReadFile("/path/to/your-project-key.json")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
conf, err := google.JWTConfigFromJSON(data, "https://www.googleapis.com/auth/bigquery")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Initiate an http.Client. The following GET request will be
|
||||
// authorized and authenticated on the behalf of
|
||||
// your service account.
|
||||
client := conf.Client(oauth2.NoContext)
|
||||
client.Get("...")
|
||||
}
|
||||
|
||||
func ExampleSDKConfig() {
|
||||
// The credentials will be obtained from the first account that
|
||||
// has been authorized with `gcloud auth login`.
|
||||
conf, err := google.NewSDKConfig("")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Initiate an http.Client. The following GET request will be
|
||||
// authorized and authenticated on the behalf of the SDK user.
|
||||
client := conf.Client(oauth2.NoContext)
|
||||
client.Get("...")
|
||||
}
|
||||
|
||||
func Example_serviceAccount() {
|
||||
// Your credentials should be obtained from the Google
|
||||
// Developer Console (https://console.developers.google.com).
|
||||
conf := &jwt.Config{
|
||||
Email: "xxx@developer.gserviceaccount.com",
|
||||
// The contents of your RSA private key or your PEM file
|
||||
// that contains a private key.
|
||||
// If you have a p12 file instead, you
|
||||
// can use `openssl` to export the private key into a pem file.
|
||||
//
|
||||
// $ openssl pkcs12 -in key.p12 -passin pass:notasecret -out key.pem -nodes
|
||||
//
|
||||
// The field only supports PEM containers with no passphrase.
|
||||
// The openssl command will convert p12 keys to passphrase-less PEM containers.
|
||||
PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----..."),
|
||||
Scopes: []string{
|
||||
"https://www.googleapis.com/auth/bigquery",
|
||||
"https://www.googleapis.com/auth/blogger",
|
||||
},
|
||||
TokenURL: google.JWTTokenURL,
|
||||
// If you would like to impersonate a user, you can
|
||||
// create a transport with a subject. The following GET
|
||||
// request will be made on the behalf of user@example.com.
|
||||
// Optional.
|
||||
Subject: "user@example.com",
|
||||
}
|
||||
// Initiate an http.Client, the following GET request will be
|
||||
// authorized and authenticated on the behalf of user@example.com.
|
||||
client := conf.Client(oauth2.NoContext)
|
||||
client.Get("...")
|
||||
}
|
||||
|
||||
func ExampleAppEngineTokenSource() {
|
||||
var req *http.Request // from the ServeHTTP handler
|
||||
ctx := appengine.NewContext(req)
|
||||
client := &http.Client{
|
||||
Transport: &oauth2.Transport{
|
||||
Source: google.AppEngineTokenSource(ctx, "https://www.googleapis.com/auth/bigquery"),
|
||||
Base: &urlfetch.Transport{
|
||||
Context: ctx,
|
||||
},
|
||||
},
|
||||
}
|
||||
client.Get("...")
|
||||
}
|
||||
|
||||
func ExampleComputeTokenSource() {
|
||||
client := &http.Client{
|
||||
Transport: &oauth2.Transport{
|
||||
// Fetch from Google Compute Engine's metadata server to retrieve
|
||||
// an access token for the provided account.
|
||||
// If no account is specified, "default" is used.
|
||||
Source: google.ComputeTokenSource(""),
|
||||
},
|
||||
}
|
||||
client.Get("...")
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package google provides support for making OAuth2 authorized and
|
||||
// authenticated HTTP requests to Google APIs.
|
||||
// It supports the Web server flow, client-side credentials, service accounts,
|
||||
// Google Compute Engine service accounts, and Google App Engine service
|
||||
// accounts.
|
||||
//
|
||||
// For more information, please read
|
||||
// https://developers.google.com/accounts/docs/OAuth2
|
||||
// and
|
||||
// https://developers.google.com/accounts/docs/application-default-credentials.
|
||||
package google // import "golang.org/x/oauth2/google"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/jwt"
|
||||
"google.golang.org/cloud/compute/metadata"
|
||||
)
|
||||
|
||||
// Endpoint is Google's OAuth 2.0 endpoint.
|
||||
var Endpoint = oauth2.Endpoint{
|
||||
AuthURL: "https://accounts.google.com/o/oauth2/auth",
|
||||
TokenURL: "https://accounts.google.com/o/oauth2/token",
|
||||
}
|
||||
|
||||
// JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
|
||||
const JWTTokenURL = "https://accounts.google.com/o/oauth2/token"
|
||||
|
||||
// ConfigFromJSON uses a Google Developers Console client_credentials.json
|
||||
// file to construct a config.
|
||||
// client_credentials.json can be downloadable from https://console.developers.google.com,
|
||||
// under "APIs & Auth" > "Credentials". Download the Web application credentials in the
|
||||
// JSON format and provide the contents of the file as jsonKey.
|
||||
func ConfigFromJSON(jsonKey []byte, scope ...string) (*oauth2.Config, error) {
|
||||
type cred struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
RedirectURIs []string `json:"redirect_uris"`
|
||||
AuthURI string `json:"auth_uri"`
|
||||
TokenURI string `json:"token_uri"`
|
||||
}
|
||||
var j struct {
|
||||
Web *cred `json:"web"`
|
||||
Installed *cred `json:"installed"`
|
||||
}
|
||||
if err := json.Unmarshal(jsonKey, &j); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c *cred
|
||||
switch {
|
||||
case j.Web != nil:
|
||||
c = j.Web
|
||||
case j.Installed != nil:
|
||||
c = j.Installed
|
||||
default:
|
||||
return nil, fmt.Errorf("oauth2/google: no credentials found")
|
||||
}
|
||||
if len(c.RedirectURIs) < 1 {
|
||||
return nil, errors.New("oauth2/google: missing redirect URL in the client_credentials.json")
|
||||
}
|
||||
return &oauth2.Config{
|
||||
ClientID: c.ClientID,
|
||||
ClientSecret: c.ClientSecret,
|
||||
RedirectURL: c.RedirectURIs[0],
|
||||
Scopes: scope,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: c.AuthURI,
|
||||
TokenURL: c.TokenURI,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// JWTConfigFromJSON uses a Google Developers service account JSON key file to read
|
||||
// the credentials that authorize and authenticate the requests.
|
||||
// Create a service account on "Credentials" page under "APIs & Auth" for your
|
||||
// project at https://console.developers.google.com to download a JSON key file.
|
||||
func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
|
||||
var key struct {
|
||||
Email string `json:"client_email"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
}
|
||||
if err := json.Unmarshal(jsonKey, &key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &jwt.Config{
|
||||
Email: key.Email,
|
||||
PrivateKey: []byte(key.PrivateKey),
|
||||
Scopes: scope,
|
||||
TokenURL: JWTTokenURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ComputeTokenSource returns a token source that fetches access tokens
|
||||
// from Google Compute Engine (GCE)'s metadata server. It's only valid to use
|
||||
// this token source if your program is running on a GCE instance.
|
||||
// If no account is specified, "default" is used.
|
||||
// Further information about retrieving access tokens from the GCE metadata
|
||||
// server can be found at https://cloud.google.com/compute/docs/authentication.
|
||||
func ComputeTokenSource(account string) oauth2.TokenSource {
|
||||
return oauth2.ReuseTokenSource(nil, computeSource{account: account})
|
||||
}
|
||||
|
||||
type computeSource struct {
|
||||
account string
|
||||
}
|
||||
|
||||
func (cs computeSource) Token() (*oauth2.Token, error) {
|
||||
if !metadata.OnGCE() {
|
||||
return nil, errors.New("oauth2/google: can't get a token from the metadata service; not running on GCE")
|
||||
}
|
||||
acct := cs.account
|
||||
if acct == "" {
|
||||
acct = "default"
|
||||
}
|
||||
tokenJSON, err := metadata.Get("instance/service-accounts/" + acct + "/token")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var res struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresInSec int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err)
|
||||
}
|
||||
if res.ExpiresInSec == 0 || res.AccessToken == "" {
|
||||
return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata")
|
||||
}
|
||||
return &oauth2.Token{
|
||||
AccessToken: res.AccessToken,
|
||||
TokenType: res.TokenType,
|
||||
Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second),
|
||||
}, nil
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package google
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var webJSONKey = []byte(`
|
||||
{
|
||||
"web": {
|
||||
"auth_uri": "https://google.com/o/oauth2/auth",
|
||||
"client_secret": "3Oknc4jS_wA2r9i",
|
||||
"token_uri": "https://google.com/o/oauth2/token",
|
||||
"client_email": "222-nprqovg5k43uum874cs9osjt2koe97g8@developer.gserviceaccount.com",
|
||||
"redirect_uris": ["https://www.example.com/oauth2callback"],
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/222-nprqovg5k43uum874cs9osjt2koe97g8@developer.gserviceaccount.com",
|
||||
"client_id": "222-nprqovg5k43uum874cs9osjt2koe97g8.apps.googleusercontent.com",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"javascript_origins": ["https://www.example.com"]
|
||||
}
|
||||
}`)
|
||||
|
||||
var installedJSONKey = []byte(`{
|
||||
"installed": {
|
||||
"client_id": "222-installed.apps.googleusercontent.com",
|
||||
"redirect_uris": ["https://www.example.com/oauth2callback"]
|
||||
}
|
||||
}`)
|
||||
|
||||
func TestConfigFromJSON(t *testing.T) {
|
||||
conf, err := ConfigFromJSON(webJSONKey, "scope1", "scope2")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if got, want := conf.ClientID, "222-nprqovg5k43uum874cs9osjt2koe97g8.apps.googleusercontent.com"; got != want {
|
||||
t.Errorf("ClientID = %q; want %q", got, want)
|
||||
}
|
||||
if got, want := conf.ClientSecret, "3Oknc4jS_wA2r9i"; got != want {
|
||||
t.Errorf("ClientSecret = %q; want %q", got, want)
|
||||
}
|
||||
if got, want := conf.RedirectURL, "https://www.example.com/oauth2callback"; got != want {
|
||||
t.Errorf("RedictURL = %q; want %q", got, want)
|
||||
}
|
||||
if got, want := strings.Join(conf.Scopes, ","), "scope1,scope2"; got != want {
|
||||
t.Errorf("Scopes = %q; want %q", got, want)
|
||||
}
|
||||
if got, want := conf.Endpoint.AuthURL, "https://google.com/o/oauth2/auth"; got != want {
|
||||
t.Errorf("AuthURL = %q; want %q", got, want)
|
||||
}
|
||||
if got, want := conf.Endpoint.TokenURL, "https://google.com/o/oauth2/token"; got != want {
|
||||
t.Errorf("TokenURL = %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigFromJSON_Installed(t *testing.T) {
|
||||
conf, err := ConfigFromJSON(installedJSONKey)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if got, want := conf.ClientID, "222-installed.apps.googleusercontent.com"; got != want {
|
||||
t.Errorf("ClientID = %q; want %q", got, want)
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package google
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/internal"
|
||||
"golang.org/x/oauth2/jws"
|
||||
)
|
||||
|
||||
// JWTAccessTokenSourceFromJSON uses a Google Developers service account JSON
|
||||
// key file to read the credentials that authorize and authenticate the
|
||||
// requests, and returns a TokenSource that does not use any OAuth2 flow but
|
||||
// instead creates a JWT and sends that as the access token.
|
||||
// The audience is typically a URL that specifies the scope of the credentials.
|
||||
//
|
||||
// Note that this is not a standard OAuth flow, but rather an
|
||||
// optimization supported by a few Google services.
|
||||
// Unless you know otherwise, you should use JWTConfigFromJSON instead.
|
||||
func JWTAccessTokenSourceFromJSON(jsonKey []byte, audience string) (oauth2.TokenSource, error) {
|
||||
cfg, err := JWTConfigFromJSON(jsonKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("google: could not parse JSON key: %v", err)
|
||||
}
|
||||
pk, err := internal.ParseKey(cfg.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("google: could not parse key: %v", err)
|
||||
}
|
||||
ts := &jwtAccessTokenSource{
|
||||
email: cfg.Email,
|
||||
audience: audience,
|
||||
pk: pk,
|
||||
}
|
||||
tok, err := ts.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return oauth2.ReuseTokenSource(tok, ts), nil
|
||||
}
|
||||
|
||||
type jwtAccessTokenSource struct {
|
||||
email, audience string
|
||||
pk *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func (ts *jwtAccessTokenSource) Token() (*oauth2.Token, error) {
|
||||
iat := time.Now()
|
||||
exp := iat.Add(time.Hour)
|
||||
cs := &jws.ClaimSet{
|
||||
Iss: ts.email,
|
||||
Sub: ts.email,
|
||||
Aud: ts.audience,
|
||||
Iat: iat.Unix(),
|
||||
Exp: exp.Unix(),
|
||||
}
|
||||
hdr := &jws.Header{
|
||||
Algorithm: "RS256",
|
||||
Typ: "JWT",
|
||||
}
|
||||
msg, err := jws.Encode(hdr, cs, ts.pk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("google: could not encode JWT: %v", err)
|
||||
}
|
||||
return &oauth2.Token{AccessToken: msg, TokenType: "Bearer", Expiry: exp}, nil
|
||||
}
|
|
@ -1,168 +0,0 @@
|
|||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package google
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/internal"
|
||||
)
|
||||
|
||||
type sdkCredentials struct {
|
||||
Data []struct {
|
||||
Credential struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
TokenExpiry *time.Time `json:"token_expiry"`
|
||||
} `json:"credential"`
|
||||
Key struct {
|
||||
Account string `json:"account"`
|
||||
Scope string `json:"scope"`
|
||||
} `json:"key"`
|
||||
}
|
||||
}
|
||||
|
||||
// An SDKConfig provides access to tokens from an account already
|
||||
// authorized via the Google Cloud SDK.
|
||||
type SDKConfig struct {
|
||||
conf oauth2.Config
|
||||
initialToken *oauth2.Token
|
||||
}
|
||||
|
||||
// NewSDKConfig creates an SDKConfig for the given Google Cloud SDK
|
||||
// account. If account is empty, the account currently active in
|
||||
// Google Cloud SDK properties is used.
|
||||
// Google Cloud SDK credentials must be created by running `gcloud auth`
|
||||
// before using this function.
|
||||
// The Google Cloud SDK is available at https://cloud.google.com/sdk/.
|
||||
func NewSDKConfig(account string) (*SDKConfig, error) {
|
||||
configPath, err := sdkConfigPath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err)
|
||||
}
|
||||
credentialsPath := filepath.Join(configPath, "credentials")
|
||||
f, err := os.Open(credentialsPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var c sdkCredentials
|
||||
if err := json.NewDecoder(f).Decode(&c); err != nil {
|
||||
return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err)
|
||||
}
|
||||
if len(c.Data) == 0 {
|
||||
return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath)
|
||||
}
|
||||
if account == "" {
|
||||
propertiesPath := filepath.Join(configPath, "properties")
|
||||
f, err := os.Open(propertiesPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
ini, err := internal.ParseINI(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err)
|
||||
}
|
||||
core, ok := ini["core"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini)
|
||||
}
|
||||
active, ok := core["account"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core)
|
||||
}
|
||||
account = active
|
||||
}
|
||||
|
||||
for _, d := range c.Data {
|
||||
if account == "" || d.Key.Account == account {
|
||||
if d.Credential.AccessToken == "" && d.Credential.RefreshToken == "" {
|
||||
return nil, fmt.Errorf("oauth2/google: no token available for account %q", account)
|
||||
}
|
||||
var expiry time.Time
|
||||
if d.Credential.TokenExpiry != nil {
|
||||
expiry = *d.Credential.TokenExpiry
|
||||
}
|
||||
return &SDKConfig{
|
||||
conf: oauth2.Config{
|
||||
ClientID: d.Credential.ClientID,
|
||||
ClientSecret: d.Credential.ClientSecret,
|
||||
Scopes: strings.Split(d.Key.Scope, " "),
|
||||
Endpoint: Endpoint,
|
||||
RedirectURL: "oob",
|
||||
},
|
||||
initialToken: &oauth2.Token{
|
||||
AccessToken: d.Credential.AccessToken,
|
||||
RefreshToken: d.Credential.RefreshToken,
|
||||
Expiry: expiry,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account)
|
||||
}
|
||||
|
||||
// Client returns an HTTP client using Google Cloud SDK credentials to
|
||||
// authorize requests. The token will auto-refresh as necessary. The
|
||||
// underlying http.RoundTripper will be obtained using the provided
|
||||
// context. The returned client and its Transport should not be
|
||||
// modified.
|
||||
func (c *SDKConfig) Client(ctx context.Context) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: &oauth2.Transport{
|
||||
Source: c.TokenSource(ctx),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TokenSource returns an oauth2.TokenSource that retrieve tokens from
|
||||
// Google Cloud SDK credentials using the provided context.
|
||||
// It will returns the current access token stored in the credentials,
|
||||
// and refresh it when it expires, but it won't update the credentials
|
||||
// with the new access token.
|
||||
func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource {
|
||||
return c.conf.TokenSource(ctx, c.initialToken)
|
||||
}
|
||||
|
||||
// Scopes are the OAuth 2.0 scopes the current account is authorized for.
|
||||
func (c *SDKConfig) Scopes() []string {
|
||||
return c.conf.Scopes
|
||||
}
|
||||
|
||||
// sdkConfigPath tries to guess where the gcloud config is located.
|
||||
// It can be overridden during tests.
|
||||
var sdkConfigPath = func() (string, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil
|
||||
}
|
||||
homeDir := guessUnixHomeDir()
|
||||
if homeDir == "" {
|
||||
return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty")
|
||||
}
|
||||
return filepath.Join(homeDir, ".config", "gcloud"), nil
|
||||
}
|
||||
|
||||
func guessUnixHomeDir() string {
|
||||
usr, err := user.Current()
|
||||
if err == nil {
|
||||
return usr.HomeDir
|
||||
}
|
||||
return os.Getenv("HOME")
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package google
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSDKConfig(t *testing.T) {
|
||||
sdkConfigPath = func() (string, error) {
|
||||
return "testdata/gcloud", nil
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
account string
|
||||
accessToken string
|
||||
err bool
|
||||
}{
|
||||
{"", "bar_access_token", false},
|
||||
{"foo@example.com", "foo_access_token", false},
|
||||
{"bar@example.com", "bar_access_token", false},
|
||||
{"baz@serviceaccount.example.com", "", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
c, err := NewSDKConfig(tt.account)
|
||||
if got, want := err != nil, tt.err; got != want {
|
||||
if !tt.err {
|
||||
t.Errorf("expected no error, got error: %v", tt.err, err)
|
||||
} else {
|
||||
t.Errorf("expected error, got none")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
tok := c.initialToken
|
||||
if tok == nil {
|
||||
t.Errorf("expected token %q, got: nil", tt.accessToken)
|
||||
continue
|
||||
}
|
||||
if tok.AccessToken != tt.accessToken {
|
||||
t.Errorf("expected token %q, got: %q", tt.accessToken, tok.AccessToken)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal contains support packages for oauth2 package.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseKey converts the binary contents of a private key file
|
||||
// to an *rsa.PrivateKey. It detects whether the private key is in a
|
||||
// PEM container or not. If so, it extracts the the private key
|
||||
// from PEM container before conversion. It only supports PEM
|
||||
// containers with no passphrase.
|
||||
func ParseKey(key []byte) (*rsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode(key)
|
||||
if block != nil {
|
||||
key = block.Bytes
|
||||
}
|
||||
parsedKey, err := x509.ParsePKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
parsedKey, err = x509.ParsePKCS1PrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("private key should be a PEM or plain PKSC1 or PKCS8; parse error: %v", err)
|
||||
}
|
||||
}
|
||||
parsed, ok := parsedKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("private key is invalid")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func ParseINI(ini io.Reader) (map[string]map[string]string, error) {
|
||||
result := map[string]map[string]string{
|
||||
"": map[string]string{}, // root section
|
||||
}
|
||||
scanner := bufio.NewScanner(ini)
|
||||
currentSection := ""
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if strings.HasPrefix(line, ";") {
|
||||
// comment.
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
currentSection = strings.TrimSpace(line[1 : len(line)-1])
|
||||
result[currentSection] = map[string]string{}
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 && parts[0] != "" {
|
||||
result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error scanning ini: %v", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func CondVal(v string) []string {
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{v}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal contains support packages for oauth2 package.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseINI(t *testing.T) {
|
||||
tests := []struct {
|
||||
ini string
|
||||
want map[string]map[string]string
|
||||
}{
|
||||
{
|
||||
`root = toor
|
||||
[foo]
|
||||
bar = hop
|
||||
ini = nin
|
||||
`,
|
||||
map[string]map[string]string{
|
||||
"": map[string]string{"root": "toor"},
|
||||
"foo": map[string]string{"bar": "hop", "ini": "nin"},
|
||||
},
|
||||
},
|
||||
{
|
||||
`[empty]
|
||||
[section]
|
||||
empty=
|
||||
`,
|
||||
map[string]map[string]string{
|
||||
"": map[string]string{},
|
||||
"empty": map[string]string{},
|
||||
"section": map[string]string{"empty": ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
`ignore
|
||||
[invalid
|
||||
=stuff
|
||||
;comment=true
|
||||
`,
|
||||
map[string]map[string]string{
|
||||
"": map[string]string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
result, err := ParseINI(strings.NewReader(tt.ini))
|
||||
if err != nil {
|
||||
t.Errorf("ParseINI(%q) error %v, want: no error", tt.ini, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(result, tt.want) {
|
||||
t.Errorf("ParseINI(%q) = %#v, want: %#v", tt.ini, result, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal contains support packages for oauth2 package.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Token represents the crendentials used to authorize
|
||||
// the requests to access protected resources on the OAuth 2.0
|
||||
// provider's backend.
|
||||
//
|
||||
// This type is a mirror of oauth2.Token and exists to break
|
||||
// an otherwise-circular dependency. Other internal packages
|
||||
// should convert this Token into an oauth2.Token before use.
|
||||
type Token struct {
|
||||
// AccessToken is the token that authorizes and authenticates
|
||||
// the requests.
|
||||
AccessToken string
|
||||
|
||||
// TokenType is the type of token.
|
||||
// The Type method returns either this or "Bearer", the default.
|
||||
TokenType string
|
||||
|
||||
// RefreshToken is a token that's used by the application
|
||||
// (as opposed to the user) to refresh the access token
|
||||
// if it expires.
|
||||
RefreshToken string
|
||||
|
||||
// Expiry is the optional expiration time of the access token.
|
||||
//
|
||||
// If zero, TokenSource implementations will reuse the same
|
||||
// token forever and RefreshToken or equivalent
|
||||
// mechanisms for that TokenSource will not be used.
|
||||
Expiry time.Time
|
||||
|
||||
// Raw optionally contains extra metadata from the server
|
||||
// when updating a token.
|
||||
Raw interface{}
|
||||
}
|
||||
|
||||
// tokenJSON is the struct representing the HTTP response from OAuth2
|
||||
// providers returning a token in JSON form.
|
||||
type tokenJSON struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
|
||||
Expires expirationTime `json:"expires"` // broken Facebook spelling of expires_in
|
||||
}
|
||||
|
||||
func (e *tokenJSON) expiry() (t time.Time) {
|
||||
if v := e.ExpiresIn; v != 0 {
|
||||
return time.Now().Add(time.Duration(v) * time.Second)
|
||||
}
|
||||
if v := e.Expires; v != 0 {
|
||||
return time.Now().Add(time.Duration(v) * time.Second)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type expirationTime int32
|
||||
|
||||
func (e *expirationTime) UnmarshalJSON(b []byte) error {
|
||||
var n json.Number
|
||||
err := json.Unmarshal(b, &n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i, err := n.Int64()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*e = expirationTime(i)
|
||||
return nil
|
||||
}
|
||||
|
||||
var brokenAuthHeaderProviders = []string{
|
||||
"https://accounts.google.com/",
|
||||
"https://www.googleapis.com/",
|
||||
"https://api.instagram.com/",
|
||||
"https://www.douban.com/",
|
||||
"https://api.dropbox.com/",
|
||||
"https://api.soundcloud.com/",
|
||||
"https://www.linkedin.com/",
|
||||
"https://api.twitch.tv/",
|
||||
"https://oauth.vk.com/",
|
||||
"https://api.odnoklassniki.ru/",
|
||||
"https://connect.stripe.com/",
|
||||
"https://api.pushbullet.com/",
|
||||
"https://oauth.sandbox.trainingpeaks.com/",
|
||||
"https://oauth.trainingpeaks.com/",
|
||||
"https://www.strava.com/oauth/",
|
||||
"https://app.box.com/",
|
||||
"https://test-sandbox.auth.corp.google.com",
|
||||
"https://user.gini.net/",
|
||||
"https://api.netatmo.net/",
|
||||
"https://slack.com/",
|
||||
}
|
||||
|
||||
// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
|
||||
// implements the OAuth2 spec correctly
|
||||
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
|
||||
// In summary:
|
||||
// - Reddit only accepts client secret in the Authorization header
|
||||
// - Dropbox accepts either it in URL param or Auth header, but not both.
|
||||
// - Google only accepts URL param (not spec compliant?), not Auth header
|
||||
// - Stripe only accepts client secret in Auth header with Bearer method, not Basic
|
||||
func providerAuthHeaderWorks(tokenURL string) bool {
|
||||
for _, s := range brokenAuthHeaderProviders {
|
||||
if strings.HasPrefix(tokenURL, s) {
|
||||
// Some sites fail to implement the OAuth2 spec fully.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Assume the provider implements the spec properly
|
||||
// otherwise. We can add more exceptions as they're
|
||||
// discovered. We will _not_ be adding configurable hooks
|
||||
// to this package to let users select server bugs.
|
||||
return true
|
||||
}
|
||||
|
||||
func RetrieveToken(ctx context.Context, ClientID, ClientSecret, TokenURL string, v url.Values) (*Token, error) {
|
||||
hc, err := ContextClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.Set("client_id", ClientID)
|
||||
bustedAuth := !providerAuthHeaderWorks(TokenURL)
|
||||
if bustedAuth && ClientSecret != "" {
|
||||
v.Set("client_secret", ClientSecret)
|
||||
}
|
||||
req, err := http.NewRequest("POST", TokenURL, strings.NewReader(v.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if !bustedAuth {
|
||||
req.SetBasicAuth(ClientID, ClientSecret)
|
||||
}
|
||||
r, err := hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||
}
|
||||
if code := r.StatusCode; code < 200 || code > 299 {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body)
|
||||
}
|
||||
|
||||
var token *Token
|
||||
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
switch content {
|
||||
case "application/x-www-form-urlencoded", "text/plain":
|
||||
vals, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token = &Token{
|
||||
AccessToken: vals.Get("access_token"),
|
||||
TokenType: vals.Get("token_type"),
|
||||
RefreshToken: vals.Get("refresh_token"),
|
||||
Raw: vals,
|
||||
}
|
||||
e := vals.Get("expires_in")
|
||||
if e == "" {
|
||||
// TODO(jbd): Facebook's OAuth2 implementation is broken and
|
||||
// returns expires_in field in expires. Remove the fallback to expires,
|
||||
// when Facebook fixes their implementation.
|
||||
e = vals.Get("expires")
|
||||
}
|
||||
expires, _ := strconv.Atoi(e)
|
||||
if expires != 0 {
|
||||
token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
|
||||
}
|
||||
default:
|
||||
var tj tokenJSON
|
||||
if err = json.Unmarshal(body, &tj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token = &Token{
|
||||
AccessToken: tj.AccessToken,
|
||||
TokenType: tj.TokenType,
|
||||
RefreshToken: tj.RefreshToken,
|
||||
Expiry: tj.expiry(),
|
||||
Raw: make(map[string]interface{}),
|
||||
}
|
||||
json.Unmarshal(body, &token.Raw) // no error checks for optional fields
|
||||
}
|
||||
// Don't overwrite `RefreshToken` with an empty value
|
||||
// if this was a token refreshing request.
|
||||
if token.RefreshToken == "" {
|
||||
token.RefreshToken = v.Get("refresh_token")
|
||||
}
|
||||
return token, nil
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal contains support packages for oauth2 package.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_providerAuthHeaderWorks(t *testing.T) {
|
||||
for _, p := range brokenAuthHeaderProviders {
|
||||
if providerAuthHeaderWorks(p) {
|
||||
t.Errorf("URL: %s not found in list", p)
|
||||
}
|
||||
p := fmt.Sprintf("%ssomesuffix", p)
|
||||
if providerAuthHeaderWorks(p) {
|
||||
t.Errorf("URL: %s not found in list", p)
|
||||
}
|
||||
}
|
||||
p := "https://api.not-in-the-list-example.com/"
|
||||
if !providerAuthHeaderWorks(p) {
|
||||
t.Errorf("URL: %s found in list", p)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal contains support packages for oauth2 package.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// HTTPClient is the context key to use with golang.org/x/net/context's
|
||||
// WithValue function to associate an *http.Client value with a context.
|
||||
var HTTPClient ContextKey
|
||||
|
||||
// ContextKey is just an empty struct. It exists so HTTPClient can be
|
||||
// an immutable public variable with a unique type. It's immutable
|
||||
// because nobody else can create a ContextKey, being unexported.
|
||||
type ContextKey struct{}
|
||||
|
||||
// ContextClientFunc is a func which tries to return an *http.Client
|
||||
// given a Context value. If it returns an error, the search stops
|
||||
// with that error. If it returns (nil, nil), the search continues
|
||||
// down the list of registered funcs.
|
||||
type ContextClientFunc func(context.Context) (*http.Client, error)
|
||||
|
||||
var contextClientFuncs []ContextClientFunc
|
||||
|
||||
func RegisterContextClientFunc(fn ContextClientFunc) {
|
||||
contextClientFuncs = append(contextClientFuncs, fn)
|
||||
}
|
||||
|
||||
func ContextClient(ctx context.Context) (*http.Client, error) {
|
||||
for _, fn := range contextClientFuncs {
|
||||
c, err := fn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c != nil {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
if hc, ok := ctx.Value(HTTPClient).(*http.Client); ok {
|
||||
return hc, nil
|
||||
}
|
||||
return http.DefaultClient, nil
|
||||
}
|
||||
|
||||
func ContextTransport(ctx context.Context) http.RoundTripper {
|
||||
hc, err := ContextClient(ctx)
|
||||
// This is a rare error case (somebody using nil on App Engine).
|
||||
if err != nil {
|
||||
return ErrorTransport{err}
|
||||
}
|
||||
return hc.Transport
|
||||
}
|
||||
|
||||
// ErrorTransport returns the specified error on RoundTrip.
|
||||
// This RoundTripper should be used in rare error cases where
|
||||
// error handling can be postponed to response handling time.
|
||||
type ErrorTransport struct{ Err error }
|
||||
|
||||
func (t ErrorTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
return nil, t.Err
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package jws provides encoding and decoding utilities for
|
||||
// signed JWS messages.
|
||||
package jws // import "golang.org/x/oauth2/jws"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ClaimSet contains information about the JWT signature including the
|
||||
// permissions being requested (scopes), the target of the token, the issuer,
|
||||
// the time the token was issued, and the lifetime of the token.
|
||||
type ClaimSet struct {
|
||||
Iss string `json:"iss"` // email address of the client_id of the application making the access token request
|
||||
Scope string `json:"scope,omitempty"` // space-delimited list of the permissions the application requests
|
||||
Aud string `json:"aud"` // descriptor of the intended target of the assertion (Optional).
|
||||
Exp int64 `json:"exp"` // the expiration time of the assertion (seconds since Unix epoch)
|
||||
Iat int64 `json:"iat"` // the time the assertion was issued (seconds since Unix epoch)
|
||||
Typ string `json:"typ,omitempty"` // token type (Optional).
|
||||
|
||||
// Email for which the application is requesting delegated access (Optional).
|
||||
Sub string `json:"sub,omitempty"`
|
||||
|
||||
// The old name of Sub. Client keeps setting Prn to be
|
||||
// complaint with legacy OAuth 2.0 providers. (Optional)
|
||||
Prn string `json:"prn,omitempty"`
|
||||
|
||||
// See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3
|
||||
// This array is marshalled using custom code (see (c *ClaimSet) encode()).
|
||||
PrivateClaims map[string]interface{} `json:"-"`
|
||||
}
|
||||
|
||||
func (c *ClaimSet) encode() (string, error) {
|
||||
// Reverting time back for machines whose time is not perfectly in sync.
|
||||
// If client machine's time is in the future according
|
||||
// to Google servers, an access token will not be issued.
|
||||
now := time.Now().Add(-10 * time.Second)
|
||||
if c.Iat == 0 {
|
||||
c.Iat = now.Unix()
|
||||
}
|
||||
if c.Exp == 0 {
|
||||
c.Exp = now.Add(time.Hour).Unix()
|
||||
}
|
||||
if c.Exp < c.Iat {
|
||||
return "", fmt.Errorf("jws: invalid Exp = %v; must be later than Iat = %v", c.Exp, c.Iat)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(c.PrivateClaims) == 0 {
|
||||
return base64Encode(b), nil
|
||||
}
|
||||
|
||||
// Marshal private claim set and then append it to b.
|
||||
prv, err := json.Marshal(c.PrivateClaims)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("jws: invalid map of private claims %v", c.PrivateClaims)
|
||||
}
|
||||
|
||||
// Concatenate public and private claim JSON objects.
|
||||
if !bytes.HasSuffix(b, []byte{'}'}) {
|
||||
return "", fmt.Errorf("jws: invalid JSON %s", b)
|
||||
}
|
||||
if !bytes.HasPrefix(prv, []byte{'{'}) {
|
||||
return "", fmt.Errorf("jws: invalid JSON %s", prv)
|
||||
}
|
||||
b[len(b)-1] = ',' // Replace closing curly brace with a comma.
|
||||
b = append(b, prv[1:]...) // Append private claims.
|
||||
return base64Encode(b), nil
|
||||
}
|
||||
|
||||
// Header represents the header for the signed JWS payloads.
|
||||
type Header struct {
|
||||
// The algorithm used for signature.
|
||||
Algorithm string `json:"alg"`
|
||||
|
||||
// Represents the token type.
|
||||
Typ string `json:"typ"`
|
||||
}
|
||||
|
||||
func (h *Header) encode() (string, error) {
|
||||
b, err := json.Marshal(h)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64Encode(b), nil
|
||||
}
|
||||
|
||||
// Decode decodes a claim set from a JWS payload.
|
||||
func Decode(payload string) (*ClaimSet, error) {
|
||||
// decode returned id token to get expiry
|
||||
s := strings.Split(payload, ".")
|
||||
if len(s) < 2 {
|
||||
// TODO(jbd): Provide more context about the error.
|
||||
return nil, errors.New("jws: invalid token received")
|
||||
}
|
||||
decoded, err := base64Decode(s[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &ClaimSet{}
|
||||
err = json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c)
|
||||
return c, err
|
||||
}
|
||||
|
||||
// Encode encodes a signed JWS with provided header and claim set.
|
||||
func Encode(header *Header, c *ClaimSet, signature *rsa.PrivateKey) (string, error) {
|
||||
head, err := header.encode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cs, err := c.encode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ss := fmt.Sprintf("%s.%s", head, cs)
|
||||
h := sha256.New()
|
||||
h.Write([]byte(ss))
|
||||
b, err := rsa.SignPKCS1v15(rand.Reader, signature, crypto.SHA256, h.Sum(nil))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sig := base64Encode(b)
|
||||
return fmt.Sprintf("%s.%s", ss, sig), nil
|
||||
}
|
||||
|
||||
// base64Encode returns and Base64url encoded version of the input string with any
|
||||
// trailing "=" stripped.
|
||||
func base64Encode(b []byte) string {
|
||||
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
|
||||
}
|
||||
|
||||
// base64Decode decodes the Base64url encoded string
|
||||
func base64Decode(s string) ([]byte, error) {
|
||||
// add back missing padding
|
||||
switch len(s) % 4 {
|
||||
case 2:
|
||||
s += "=="
|
||||
case 3:
|
||||
s += "="
|
||||
}
|
||||
return base64.URLEncoding.DecodeString(s)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jwt_test
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/jwt"
|
||||
)
|
||||
|
||||
func ExampleJWTConfig() {
|
||||
conf := &jwt.Config{
|
||||
Email: "xxx@developer.com",
|
||||
// The contents of your RSA private key or your PEM file
|
||||
// that contains a private key.
|
||||
// If you have a p12 file instead, you
|
||||
// can use `openssl` to export the private key into a pem file.
|
||||
//
|
||||
// $ openssl pkcs12 -in key.p12 -out key.pem -nodes
|
||||
//
|
||||
// It only supports PEM containers with no passphrase.
|
||||
PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----..."),
|
||||
Subject: "user@example.com",
|
||||
TokenURL: "https://provider.com/o/oauth2/token",
|
||||
}
|
||||
// Initiate an http.Client, the following GET request will be
|
||||
// authorized and authenticated on the behalf of user@example.com.
|
||||
client := conf.Client(oauth2.NoContext)
|
||||
client.Get("...")
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package jwt implements the OAuth 2.0 JSON Web Token flow, commonly
|
||||
// known as "two-legged OAuth 2.0".
|
||||
//
|
||||
// See: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/internal"
|
||||
"golang.org/x/oauth2/jws"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
||||
defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"}
|
||||
)
|
||||
|
||||
// Config is the configuration for using JWT to fetch tokens,
|
||||
// commonly known as "two-legged OAuth 2.0".
|
||||
type Config struct {
|
||||
// Email is the OAuth client identifier used when communicating with
|
||||
// the configured OAuth provider.
|
||||
Email string
|
||||
|
||||
// PrivateKey contains the contents of an RSA private key or the
|
||||
// contents of a PEM file that contains a private key. The provided
|
||||
// private key is used to sign JWT payloads.
|
||||
// PEM containers with a passphrase are not supported.
|
||||
// Use the following command to convert a PKCS 12 file into a PEM.
|
||||
//
|
||||
// $ openssl pkcs12 -in key.p12 -out key.pem -nodes
|
||||
//
|
||||
PrivateKey []byte
|
||||
|
||||
// Subject is the optional user to impersonate.
|
||||
Subject string
|
||||
|
||||
// Scopes optionally specifies a list of requested permission scopes.
|
||||
Scopes []string
|
||||
|
||||
// TokenURL is the endpoint required to complete the 2-legged JWT flow.
|
||||
TokenURL string
|
||||
|
||||
// Expires optionally specifies how long the token is valid for.
|
||||
Expires time.Duration
|
||||
}
|
||||
|
||||
// TokenSource returns a JWT TokenSource using the configuration
|
||||
// in c and the HTTP client from the provided context.
|
||||
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
|
||||
return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c})
|
||||
}
|
||||
|
||||
// Client returns an HTTP client wrapping the context's
|
||||
// HTTP transport and adding Authorization headers with tokens
|
||||
// obtained from c.
|
||||
//
|
||||
// The returned client and its Transport should not be modified.
|
||||
func (c *Config) Client(ctx context.Context) *http.Client {
|
||||
return oauth2.NewClient(ctx, c.TokenSource(ctx))
|
||||
}
|
||||
|
||||
// jwtSource is a source that always does a signed JWT request for a token.
|
||||
// It should typically be wrapped with a reuseTokenSource.
|
||||
type jwtSource struct {
|
||||
ctx context.Context
|
||||
conf *Config
|
||||
}
|
||||
|
||||
func (js jwtSource) Token() (*oauth2.Token, error) {
|
||||
pk, err := internal.ParseKey(js.conf.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hc := oauth2.NewClient(js.ctx, nil)
|
||||
claimSet := &jws.ClaimSet{
|
||||
Iss: js.conf.Email,
|
||||
Scope: strings.Join(js.conf.Scopes, " "),
|
||||
Aud: js.conf.TokenURL,
|
||||
}
|
||||
if subject := js.conf.Subject; subject != "" {
|
||||
claimSet.Sub = subject
|
||||
// prn is the old name of sub. Keep setting it
|
||||
// to be compatible with legacy OAuth 2.0 providers.
|
||||
claimSet.Prn = subject
|
||||
}
|
||||
if t := js.conf.Expires; t > 0 {
|
||||
claimSet.Exp = time.Now().Add(t).Unix()
|
||||
}
|
||||
payload, err := jws.Encode(defaultHeader, claimSet, pk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := url.Values{}
|
||||
v.Set("grant_type", defaultGrantType)
|
||||
v.Set("assertion", payload)
|
||||
resp, err := hc.PostForm(js.conf.TokenURL, v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||
}
|
||||
if c := resp.StatusCode; c < 200 || c > 299 {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", resp.Status, body)
|
||||
}
|
||||
// tokenRes is the JSON response body.
|
||||
var tokenRes struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
IDToken string `json:"id_token"`
|
||||
ExpiresIn int64 `json:"expires_in"` // relative seconds from now
|
||||
}
|
||||
if err := json.Unmarshal(body, &tokenRes); err != nil {
|
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||
}
|
||||
token := &oauth2.Token{
|
||||
AccessToken: tokenRes.AccessToken,
|
||||
TokenType: tokenRes.TokenType,
|
||||
}
|
||||
raw := make(map[string]interface{})
|
||||
json.Unmarshal(body, &raw) // no error checks for optional fields
|
||||
token = token.WithExtra(raw)
|
||||
|
||||
if secs := tokenRes.ExpiresIn; secs > 0 {
|
||||
token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
|
||||
}
|
||||
if v := tokenRes.IDToken; v != "" {
|
||||
// decode returned id token to get expiry
|
||||
claimSet, err := jws.Decode(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oauth2: error decoding JWT token: %v", err)
|
||||
}
|
||||
token.Expiry = time.Unix(claimSet.Exp, 0)
|
||||
}
|
||||
return token, nil
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var dummyPrivateKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAx4fm7dngEmOULNmAs1IGZ9Apfzh+BkaQ1dzkmbUgpcoghucE
|
||||
DZRnAGd2aPyB6skGMXUytWQvNYav0WTR00wFtX1ohWTfv68HGXJ8QXCpyoSKSSFY
|
||||
fuP9X36wBSkSX9J5DVgiuzD5VBdzUISSmapjKm+DcbRALjz6OUIPEWi1Tjl6p5RK
|
||||
1w41qdbmt7E5/kGhKLDuT7+M83g4VWhgIvaAXtnhklDAggilPPa8ZJ1IFe31lNlr
|
||||
k4DRk38nc6sEutdf3RL7QoH7FBusI7uXV03DC6dwN1kP4GE7bjJhcRb/7jYt7CQ9
|
||||
/E9Exz3c0yAp0yrTg0Fwh+qxfH9dKwN52S7SBwIDAQABAoIBAQCaCs26K07WY5Jt
|
||||
3a2Cw3y2gPrIgTCqX6hJs7O5ByEhXZ8nBwsWANBUe4vrGaajQHdLj5OKfsIDrOvn
|
||||
2NI1MqflqeAbu/kR32q3tq8/Rl+PPiwUsW3E6Pcf1orGMSNCXxeducF2iySySzh3
|
||||
nSIhCG5uwJDWI7a4+9KiieFgK1pt/Iv30q1SQS8IEntTfXYwANQrfKUVMmVF9aIK
|
||||
6/WZE2yd5+q3wVVIJ6jsmTzoDCX6QQkkJICIYwCkglmVy5AeTckOVwcXL0jqw5Kf
|
||||
5/soZJQwLEyBoQq7Kbpa26QHq+CJONetPP8Ssy8MJJXBT+u/bSseMb3Zsr5cr43e
|
||||
DJOhwsThAoGBAPY6rPKl2NT/K7XfRCGm1sbWjUQyDShscwuWJ5+kD0yudnT/ZEJ1
|
||||
M3+KS/iOOAoHDdEDi9crRvMl0UfNa8MAcDKHflzxg2jg/QI+fTBjPP5GOX0lkZ9g
|
||||
z6VePoVoQw2gpPFVNPPTxKfk27tEzbaffvOLGBEih0Kb7HTINkW8rIlzAoGBAM9y
|
||||
1yr+jvfS1cGFtNU+Gotoihw2eMKtIqR03Yn3n0PK1nVCDKqwdUqCypz4+ml6cxRK
|
||||
J8+Pfdh7D+ZJd4LEG6Y4QRDLuv5OA700tUoSHxMSNn3q9As4+T3MUyYxWKvTeu3U
|
||||
f2NWP9ePU0lV8ttk7YlpVRaPQmc1qwooBA/z/8AdAoGAW9x0HWqmRICWTBnpjyxx
|
||||
QGlW9rQ9mHEtUotIaRSJ6K/F3cxSGUEkX1a3FRnp6kPLcckC6NlqdNgNBd6rb2rA
|
||||
cPl/uSkZP42Als+9YMoFPU/xrrDPbUhu72EDrj3Bllnyb168jKLa4VBOccUvggxr
|
||||
Dm08I1hgYgdN5huzs7y6GeUCgYEAj+AZJSOJ6o1aXS6rfV3mMRve9bQ9yt8jcKXw
|
||||
5HhOCEmMtaSKfnOF1Ziih34Sxsb7O2428DiX0mV/YHtBnPsAJidL0SdLWIapBzeg
|
||||
KHArByIRkwE6IvJvwpGMdaex1PIGhx5i/3VZL9qiq/ElT05PhIb+UXgoWMabCp84
|
||||
OgxDK20CgYAeaFo8BdQ7FmVX2+EEejF+8xSge6WVLtkaon8bqcn6P0O8lLypoOhd
|
||||
mJAYH8WU+UAy9pecUnDZj14LAGNVmYcse8HFX71MoshnvCTFEPVo4rZxIAGwMpeJ
|
||||
5jgQ3slYLpqrGlcbLgUXBUgzEO684Wk/UV9DFPlHALVqCfXQ9dpJPg==
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
func TestJWTFetch_JSONResponse(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{
|
||||
"access_token": "90d64460d14870c08c81352a05dedd3465940a7c",
|
||||
"scope": "user",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 3600
|
||||
}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
conf := &Config{
|
||||
Email: "aaa@xxx.com",
|
||||
PrivateKey: dummyPrivateKey,
|
||||
TokenURL: ts.URL,
|
||||
}
|
||||
tok, err := conf.TokenSource(oauth2.NoContext).Token()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !tok.Valid() {
|
||||
t.Errorf("Token invalid")
|
||||
}
|
||||
if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" {
|
||||
t.Errorf("Unexpected access token, %#v", tok.AccessToken)
|
||||
}
|
||||
if tok.TokenType != "bearer" {
|
||||
t.Errorf("Unexpected token type, %#v", tok.TokenType)
|
||||
}
|
||||
if tok.Expiry.IsZero() {
|
||||
t.Errorf("Unexpected token expiry, %#v", tok.Expiry)
|
||||
}
|
||||
scope := tok.Extra("scope")
|
||||
if scope != "user" {
|
||||
t.Errorf("Unexpected value for scope: %v", scope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTFetch_BadResponse(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"scope": "user", "token_type": "bearer"}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
conf := &Config{
|
||||
Email: "aaa@xxx.com",
|
||||
PrivateKey: dummyPrivateKey,
|
||||
TokenURL: ts.URL,
|
||||
}
|
||||
tok, err := conf.TokenSource(oauth2.NoContext).Token()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tok == nil {
|
||||
t.Fatalf("token is nil")
|
||||
}
|
||||
if tok.Valid() {
|
||||
t.Errorf("token is valid. want invalid.")
|
||||
}
|
||||
if tok.AccessToken != "" {
|
||||
t.Errorf("Unexpected non-empty access token %q.", tok.AccessToken)
|
||||
}
|
||||
if want := "bearer"; tok.TokenType != want {
|
||||
t.Errorf("TokenType = %q; want %q", tok.TokenType, want)
|
||||
}
|
||||
scope := tok.Extra("scope")
|
||||
if want := "user"; scope != want {
|
||||
t.Errorf("token scope = %q; want %q", scope, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTFetch_BadResponseType(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"access_token":123, "scope": "user", "token_type": "bearer"}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := &Config{
|
||||
Email: "aaa@xxx.com",
|
||||
PrivateKey: dummyPrivateKey,
|
||||
TokenURL: ts.URL,
|
||||
}
|
||||
tok, err := conf.TokenSource(oauth2.NoContext).Token()
|
||||
if err == nil {
|
||||
t.Error("got a token; expected error")
|
||||
if tok.AccessToken != "" {
|
||||
t.Errorf("Unexpected access token, %#v.", tok.AccessToken)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package linkedin provides constants for using OAuth2 to access LinkedIn.
|
||||
package linkedin // import "golang.org/x/oauth2/linkedin"
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Endpoint is LinkedIn's OAuth 2.0 endpoint.
|
||||
var Endpoint = oauth2.Endpoint{
|
||||
AuthURL: "https://www.linkedin.com/uas/oauth2/authorization",
|
||||
TokenURL: "https://www.linkedin.com/uas/oauth2/accessToken",
|
||||
}
|
|
@ -1,325 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package oauth2 provides support for making
|
||||
// OAuth2 authorized and authenticated HTTP requests.
|
||||
// It can additionally grant authorization with Bearer JWT.
|
||||
package oauth2 // import "golang.org/x/oauth2"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2/internal"
|
||||
)
|
||||
|
||||
// NoContext is the default context you should supply if not using
|
||||
// your own context.Context (see https://golang.org/x/net/context).
|
||||
var NoContext = context.TODO()
|
||||
|
||||
// Config describes a typical 3-legged OAuth2 flow, with both the
|
||||
// client application information and the server's endpoint URLs.
|
||||
type Config struct {
|
||||
// ClientID is the application's ID.
|
||||
ClientID string
|
||||
|
||||
// ClientSecret is the application's secret.
|
||||
ClientSecret string
|
||||
|
||||
// Endpoint contains the resource server's token endpoint
|
||||
// URLs. These are constants specific to each server and are
|
||||
// often available via site-specific packages, such as
|
||||
// google.Endpoint or github.Endpoint.
|
||||
Endpoint Endpoint
|
||||
|
||||
// RedirectURL is the URL to redirect users going through
|
||||
// the OAuth flow, after the resource owner's URLs.
|
||||
RedirectURL string
|
||||
|
||||
// Scope specifies optional requested permissions.
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
// A TokenSource is anything that can return a token.
|
||||
type TokenSource interface {
|
||||
// Token returns a token or an error.
|
||||
// Token must be safe for concurrent use by multiple goroutines.
|
||||
// The returned Token must not be modified.
|
||||
Token() (*Token, error)
|
||||
}
|
||||
|
||||
// Endpoint contains the OAuth 2.0 provider's authorization and token
|
||||
// endpoint URLs.
|
||||
type Endpoint struct {
|
||||
AuthURL string
|
||||
TokenURL string
|
||||
}
|
||||
|
||||
var (
|
||||
// AccessTypeOnline and AccessTypeOffline are options passed
|
||||
// to the Options.AuthCodeURL method. They modify the
|
||||
// "access_type" field that gets sent in the URL returned by
|
||||
// AuthCodeURL.
|
||||
//
|
||||
// Online is the default if neither is specified. If your
|
||||
// application needs to refresh access tokens when the user
|
||||
// is not present at the browser, then use offline. This will
|
||||
// result in your application obtaining a refresh token the
|
||||
// first time your application exchanges an authorization
|
||||
// code for a user.
|
||||
AccessTypeOnline AuthCodeOption = SetAuthURLParam("access_type", "online")
|
||||
AccessTypeOffline AuthCodeOption = SetAuthURLParam("access_type", "offline")
|
||||
|
||||
// ApprovalForce forces the users to view the consent dialog
|
||||
// and confirm the permissions request at the URL returned
|
||||
// from AuthCodeURL, even if they've already done so.
|
||||
ApprovalForce AuthCodeOption = SetAuthURLParam("approval_prompt", "force")
|
||||
)
|
||||
|
||||
// An AuthCodeOption is passed to Config.AuthCodeURL.
|
||||
type AuthCodeOption interface {
|
||||
setValue(url.Values)
|
||||
}
|
||||
|
||||
type setParam struct{ k, v string }
|
||||
|
||||
func (p setParam) setValue(m url.Values) { m.Set(p.k, p.v) }
|
||||
|
||||
// SetAuthURLParam builds an AuthCodeOption which passes key/value parameters
|
||||
// to a provider's authorization endpoint.
|
||||
func SetAuthURLParam(key, value string) AuthCodeOption {
|
||||
return setParam{key, value}
|
||||
}
|
||||
|
||||
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
|
||||
// that asks for permissions for the required scopes explicitly.
|
||||
//
|
||||
// State is a token to protect the user from CSRF attacks. You must
|
||||
// always provide a non-zero string and validate that it matches the
|
||||
// the state query parameter on your redirect callback.
|
||||
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
||||
//
|
||||
// Opts may include AccessTypeOnline or AccessTypeOffline, as well
|
||||
// as ApprovalForce.
|
||||
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(c.Endpoint.AuthURL)
|
||||
v := url.Values{
|
||||
"response_type": {"code"},
|
||||
"client_id": {c.ClientID},
|
||||
"redirect_uri": internal.CondVal(c.RedirectURL),
|
||||
"scope": internal.CondVal(strings.Join(c.Scopes, " ")),
|
||||
"state": internal.CondVal(state),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.setValue(v)
|
||||
}
|
||||
if strings.Contains(c.Endpoint.AuthURL, "?") {
|
||||
buf.WriteByte('&')
|
||||
} else {
|
||||
buf.WriteByte('?')
|
||||
}
|
||||
buf.WriteString(v.Encode())
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// PasswordCredentialsToken converts a resource owner username and password
|
||||
// pair into a token.
|
||||
//
|
||||
// Per the RFC, this grant type should only be used "when there is a high
|
||||
// degree of trust between the resource owner and the client (e.g., the client
|
||||
// is part of the device operating system or a highly privileged application),
|
||||
// and when other authorization grant types are not available."
|
||||
// See https://tools.ietf.org/html/rfc6749#section-4.3 for more info.
|
||||
//
|
||||
// The HTTP client to use is derived from the context.
|
||||
// If nil, http.DefaultClient is used.
|
||||
func (c *Config) PasswordCredentialsToken(ctx context.Context, username, password string) (*Token, error) {
|
||||
return retrieveToken(ctx, c, url.Values{
|
||||
"grant_type": {"password"},
|
||||
"username": {username},
|
||||
"password": {password},
|
||||
"scope": internal.CondVal(strings.Join(c.Scopes, " ")),
|
||||
})
|
||||
}
|
||||
|
||||
// Exchange converts an authorization code into a token.
|
||||
//
|
||||
// It is used after a resource provider redirects the user back
|
||||
// to the Redirect URI (the URL obtained from AuthCodeURL).
|
||||
//
|
||||
// The HTTP client to use is derived from the context.
|
||||
// If a client is not provided via the context, http.DefaultClient is used.
|
||||
//
|
||||
// The code will be in the *http.Request.FormValue("code"). Before
|
||||
// calling Exchange, be sure to validate FormValue("state").
|
||||
func (c *Config) Exchange(ctx context.Context, code string) (*Token, error) {
|
||||
return retrieveToken(ctx, c, url.Values{
|
||||
"grant_type": {"authorization_code"},
|
||||
"code": {code},
|
||||
"redirect_uri": internal.CondVal(c.RedirectURL),
|
||||
"scope": internal.CondVal(strings.Join(c.Scopes, " ")),
|
||||
})
|
||||
}
|
||||
|
||||
// Client returns an HTTP client using the provided token.
|
||||
// The token will auto-refresh as necessary. The underlying
|
||||
// HTTP transport will be obtained using the provided context.
|
||||
// The returned client and its Transport should not be modified.
|
||||
func (c *Config) Client(ctx context.Context, t *Token) *http.Client {
|
||||
return NewClient(ctx, c.TokenSource(ctx, t))
|
||||
}
|
||||
|
||||
// TokenSource returns a TokenSource that returns t until t expires,
|
||||
// automatically refreshing it as necessary using the provided context.
|
||||
//
|
||||
// Most users will use Config.Client instead.
|
||||
func (c *Config) TokenSource(ctx context.Context, t *Token) TokenSource {
|
||||
tkr := &tokenRefresher{
|
||||
ctx: ctx,
|
||||
conf: c,
|
||||
}
|
||||
if t != nil {
|
||||
tkr.refreshToken = t.RefreshToken
|
||||
}
|
||||
return &reuseTokenSource{
|
||||
t: t,
|
||||
new: tkr,
|
||||
}
|
||||
}
|
||||
|
||||
// tokenRefresher is a TokenSource that makes "grant_type"=="refresh_token"
|
||||
// HTTP requests to renew a token using a RefreshToken.
|
||||
type tokenRefresher struct {
|
||||
ctx context.Context // used to get HTTP requests
|
||||
conf *Config
|
||||
refreshToken string
|
||||
}
|
||||
|
||||
// WARNING: Token is not safe for concurrent access, as it
|
||||
// updates the tokenRefresher's refreshToken field.
|
||||
// Within this package, it is used by reuseTokenSource which
|
||||
// synchronizes calls to this method with its own mutex.
|
||||
func (tf *tokenRefresher) Token() (*Token, error) {
|
||||
if tf.refreshToken == "" {
|
||||
return nil, errors.New("oauth2: token expired and refresh token is not set")
|
||||
}
|
||||
|
||||
tk, err := retrieveToken(tf.ctx, tf.conf, url.Values{
|
||||
"grant_type": {"refresh_token"},
|
||||
"refresh_token": {tf.refreshToken},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tf.refreshToken != tk.RefreshToken {
|
||||
tf.refreshToken = tk.RefreshToken
|
||||
}
|
||||
return tk, err
|
||||
}
|
||||
|
||||
// reuseTokenSource is a TokenSource that holds a single token in memory
|
||||
// and validates its expiry before each call to retrieve it with
|
||||
// Token. If it's expired, it will be auto-refreshed using the
|
||||
// new TokenSource.
|
||||
type reuseTokenSource struct {
|
||||
new TokenSource // called when t is expired.
|
||||
|
||||
mu sync.Mutex // guards t
|
||||
t *Token
|
||||
}
|
||||
|
||||
// Token returns the current token if it's still valid, else will
|
||||
// refresh the current token (using r.Context for HTTP client
|
||||
// information) and return the new one.
|
||||
func (s *reuseTokenSource) Token() (*Token, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.t.Valid() {
|
||||
return s.t, nil
|
||||
}
|
||||
t, err := s.new.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.t = t
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// StaticTokenSource returns a TokenSource that always returns the same token.
|
||||
// Because the provided token t is never refreshed, StaticTokenSource is only
|
||||
// useful for tokens that never expire.
|
||||
func StaticTokenSource(t *Token) TokenSource {
|
||||
return staticTokenSource{t}
|
||||
}
|
||||
|
||||
// staticTokenSource is a TokenSource that always returns the same Token.
|
||||
type staticTokenSource struct {
|
||||
t *Token
|
||||
}
|
||||
|
||||
func (s staticTokenSource) Token() (*Token, error) {
|
||||
return s.t, nil
|
||||
}
|
||||
|
||||
// HTTPClient is the context key to use with golang.org/x/net/context's
|
||||
// WithValue function to associate an *http.Client value with a context.
|
||||
var HTTPClient internal.ContextKey
|
||||
|
||||
// NewClient creates an *http.Client from a Context and TokenSource.
|
||||
// The returned client is not valid beyond the lifetime of the context.
|
||||
//
|
||||
// As a special case, if src is nil, a non-OAuth2 client is returned
|
||||
// using the provided context. This exists to support related OAuth2
|
||||
// packages.
|
||||
func NewClient(ctx context.Context, src TokenSource) *http.Client {
|
||||
if src == nil {
|
||||
c, err := internal.ContextClient(ctx)
|
||||
if err != nil {
|
||||
return &http.Client{Transport: internal.ErrorTransport{err}}
|
||||
}
|
||||
return c
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: &Transport{
|
||||
Base: internal.ContextTransport(ctx),
|
||||
Source: ReuseTokenSource(nil, src),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ReuseTokenSource returns a TokenSource which repeatedly returns the
|
||||
// same token as long as it's valid, starting with t.
|
||||
// When its cached token is invalid, a new token is obtained from src.
|
||||
//
|
||||
// ReuseTokenSource is typically used to reuse tokens from a cache
|
||||
// (such as a file on disk) between runs of a program, rather than
|
||||
// obtaining new tokens unnecessarily.
|
||||
//
|
||||
// The initial token t may be nil, in which case the TokenSource is
|
||||
// wrapped in a caching version if it isn't one already. This also
|
||||
// means it's always safe to wrap ReuseTokenSource around any other
|
||||
// TokenSource without adverse effects.
|
||||
func ReuseTokenSource(t *Token, src TokenSource) TokenSource {
|
||||
// Don't wrap a reuseTokenSource in itself. That would work,
|
||||
// but cause an unnecessary number of mutex operations.
|
||||
// Just build the equivalent one.
|
||||
if rt, ok := src.(*reuseTokenSource); ok {
|
||||
if t == nil {
|
||||
// Just use it directly.
|
||||
return rt
|
||||
}
|
||||
src = rt.new
|
||||
}
|
||||
return &reuseTokenSource{
|
||||
t: t,
|
||||
new: src,
|
||||
}
|
||||
}
|
|
@ -1,471 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type mockTransport struct {
|
||||
rt func(req *http.Request) (resp *http.Response, err error)
|
||||
}
|
||||
|
||||
func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
return t.rt(req)
|
||||
}
|
||||
|
||||
type mockCache struct {
|
||||
token *Token
|
||||
readErr error
|
||||
}
|
||||
|
||||
func (c *mockCache) ReadToken() (*Token, error) {
|
||||
return c.token, c.readErr
|
||||
}
|
||||
|
||||
func (c *mockCache) WriteToken(*Token) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func newConf(url string) *Config {
|
||||
return &Config{
|
||||
ClientID: "CLIENT_ID",
|
||||
ClientSecret: "CLIENT_SECRET",
|
||||
RedirectURL: "REDIRECT_URL",
|
||||
Scopes: []string{"scope1", "scope2"},
|
||||
Endpoint: Endpoint{
|
||||
AuthURL: url + "/auth",
|
||||
TokenURL: url + "/token",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthCodeURL(t *testing.T) {
|
||||
conf := newConf("server")
|
||||
url := conf.AuthCodeURL("foo", AccessTypeOffline, ApprovalForce)
|
||||
if url != "server/auth?access_type=offline&approval_prompt=force&client_id=CLIENT_ID&redirect_uri=REDIRECT_URL&response_type=code&scope=scope1+scope2&state=foo" {
|
||||
t.Errorf("Auth code URL doesn't match the expected, found: %v", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthCodeURL_CustomParam(t *testing.T) {
|
||||
conf := newConf("server")
|
||||
param := SetAuthURLParam("foo", "bar")
|
||||
url := conf.AuthCodeURL("baz", param)
|
||||
if url != "server/auth?client_id=CLIENT_ID&foo=bar&redirect_uri=REDIRECT_URL&response_type=code&scope=scope1+scope2&state=baz" {
|
||||
t.Errorf("Auth code URL doesn't match the expected, found: %v", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthCodeURL_Optional(t *testing.T) {
|
||||
conf := &Config{
|
||||
ClientID: "CLIENT_ID",
|
||||
Endpoint: Endpoint{
|
||||
AuthURL: "/auth-url",
|
||||
TokenURL: "/token-url",
|
||||
},
|
||||
}
|
||||
url := conf.AuthCodeURL("")
|
||||
if url != "/auth-url?client_id=CLIENT_ID&response_type=code" {
|
||||
t.Fatalf("Auth code URL doesn't match the expected, found: %v", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeRequest(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() != "/token" {
|
||||
t.Errorf("Unexpected exchange request URL, %v is found.", r.URL)
|
||||
}
|
||||
headerAuth := r.Header.Get("Authorization")
|
||||
if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" {
|
||||
t.Errorf("Unexpected authorization header, %v is found.", headerAuth)
|
||||
}
|
||||
headerContentType := r.Header.Get("Content-Type")
|
||||
if headerContentType != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType)
|
||||
}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Failed reading request body: %s.", err)
|
||||
}
|
||||
if string(body) != "client_id=CLIENT_ID&code=exchange-code&grant_type=authorization_code&redirect_uri=REDIRECT_URL&scope=scope1+scope2" {
|
||||
t.Errorf("Unexpected exchange payload, %v is found.", string(body))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&scope=user&token_type=bearer"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
tok, err := conf.Exchange(NoContext, "exchange-code")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !tok.Valid() {
|
||||
t.Fatalf("Token invalid. Got: %#v", tok)
|
||||
}
|
||||
if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" {
|
||||
t.Errorf("Unexpected access token, %#v.", tok.AccessToken)
|
||||
}
|
||||
if tok.TokenType != "bearer" {
|
||||
t.Errorf("Unexpected token type, %#v.", tok.TokenType)
|
||||
}
|
||||
scope := tok.Extra("scope")
|
||||
if scope != "user" {
|
||||
t.Errorf("Unexpected value for scope: %v", scope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeRequest_JSONResponse(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() != "/token" {
|
||||
t.Errorf("Unexpected exchange request URL, %v is found.", r.URL)
|
||||
}
|
||||
headerAuth := r.Header.Get("Authorization")
|
||||
if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" {
|
||||
t.Errorf("Unexpected authorization header, %v is found.", headerAuth)
|
||||
}
|
||||
headerContentType := r.Header.Get("Content-Type")
|
||||
if headerContentType != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType)
|
||||
}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Failed reading request body: %s.", err)
|
||||
}
|
||||
if string(body) != "client_id=CLIENT_ID&code=exchange-code&grant_type=authorization_code&redirect_uri=REDIRECT_URL&scope=scope1+scope2" {
|
||||
t.Errorf("Unexpected exchange payload, %v is found.", string(body))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"access_token": "90d64460d14870c08c81352a05dedd3465940a7c", "scope": "user", "token_type": "bearer", "expires_in": 86400}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
tok, err := conf.Exchange(NoContext, "exchange-code")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !tok.Valid() {
|
||||
t.Fatalf("Token invalid. Got: %#v", tok)
|
||||
}
|
||||
if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" {
|
||||
t.Errorf("Unexpected access token, %#v.", tok.AccessToken)
|
||||
}
|
||||
if tok.TokenType != "bearer" {
|
||||
t.Errorf("Unexpected token type, %#v.", tok.TokenType)
|
||||
}
|
||||
scope := tok.Extra("scope")
|
||||
if scope != "user" {
|
||||
t.Errorf("Unexpected value for scope: %v", scope)
|
||||
}
|
||||
expiresIn := tok.Extra("expires_in")
|
||||
if expiresIn != float64(86400) {
|
||||
t.Errorf("Unexpected non-numeric value for expires_in: %v", expiresIn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtraValueRetrieval(t *testing.T) {
|
||||
values := url.Values{}
|
||||
|
||||
kvmap := map[string]string{
|
||||
"scope": "user", "token_type": "bearer", "expires_in": "86400.92",
|
||||
"server_time": "1443571905.5606415", "referer_ip": "10.0.0.1",
|
||||
"etag": "\"afZYj912P4alikMz_P11982\"", "request_id": "86400",
|
||||
"untrimmed": " untrimmed ",
|
||||
}
|
||||
|
||||
for key, value := range kvmap {
|
||||
values.Set(key, value)
|
||||
}
|
||||
|
||||
tok := Token{
|
||||
raw: values,
|
||||
}
|
||||
|
||||
scope := tok.Extra("scope")
|
||||
if scope != "user" {
|
||||
t.Errorf("Unexpected scope %v wanted \"user\"", scope)
|
||||
}
|
||||
serverTime := tok.Extra("server_time")
|
||||
if serverTime != 1443571905.5606415 {
|
||||
t.Errorf("Unexpected non-float64 value for server_time: %v", serverTime)
|
||||
}
|
||||
refererIp := tok.Extra("referer_ip")
|
||||
if refererIp != "10.0.0.1" {
|
||||
t.Errorf("Unexpected non-string value for referer_ip: %v", refererIp)
|
||||
}
|
||||
expires_in := tok.Extra("expires_in")
|
||||
if expires_in != 86400.92 {
|
||||
t.Errorf("Unexpected value for expires_in, wanted 86400 got %v", expires_in)
|
||||
}
|
||||
requestId := tok.Extra("request_id")
|
||||
if requestId != int64(86400) {
|
||||
t.Errorf("Unexpected non-int64 value for request_id: %v", requestId)
|
||||
}
|
||||
untrimmed := tok.Extra("untrimmed")
|
||||
if untrimmed != " untrimmed " {
|
||||
t.Errorf("Unexpected value for untrimmed, got %q expected \" untrimmed \"", untrimmed)
|
||||
}
|
||||
}
|
||||
|
||||
const day = 24 * time.Hour
|
||||
|
||||
func TestExchangeRequest_JSONResponse_Expiry(t *testing.T) {
|
||||
seconds := int32(day.Seconds())
|
||||
jsonNumberType := reflect.TypeOf(json.Number("0"))
|
||||
for _, c := range []struct {
|
||||
expires string
|
||||
expect error
|
||||
}{
|
||||
{fmt.Sprintf(`"expires_in": %d`, seconds), nil},
|
||||
{fmt.Sprintf(`"expires_in": "%d"`, seconds), nil}, // PayPal case
|
||||
{fmt.Sprintf(`"expires": %d`, seconds), nil}, // Facebook case
|
||||
{`"expires": false`, &json.UnmarshalTypeError{Value: "bool", Type: jsonNumberType}}, // wrong type
|
||||
{`"expires": {}`, &json.UnmarshalTypeError{Value: "object", Type: jsonNumberType}}, // wrong type
|
||||
{`"expires": "zzz"`, &strconv.NumError{Func: "ParseInt", Num: "zzz", Err: strconv.ErrSyntax}}, // wrong value
|
||||
} {
|
||||
testExchangeRequest_JSONResponse_expiry(t, c.expires, c.expect)
|
||||
}
|
||||
}
|
||||
|
||||
func testExchangeRequest_JSONResponse_expiry(t *testing.T, exp string, expect error) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(fmt.Sprintf(`{"access_token": "90d", "scope": "user", "token_type": "bearer", %s}`, exp)))
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
t1 := time.Now().Add(day)
|
||||
tok, err := conf.Exchange(NoContext, "exchange-code")
|
||||
t2 := time.Now().Add(day)
|
||||
// Do a fmt.Sprint comparison so either side can be
|
||||
// nil. fmt.Sprint just stringifies them to "<nil>", and no
|
||||
// non-nil expected error ever stringifies as "<nil>", so this
|
||||
// isn't terribly disgusting. We do this because Go 1.4 and
|
||||
// Go 1.5 return a different deep value for
|
||||
// json.UnmarshalTypeError. In Go 1.5, the
|
||||
// json.UnmarshalTypeError contains a new field with a new
|
||||
// non-zero value. Rather than ignore it here with reflect or
|
||||
// add new files and +build tags, just look at the strings.
|
||||
if fmt.Sprint(err) != fmt.Sprint(expect) {
|
||||
t.Errorf("Error = %v; want %v", err, expect)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !tok.Valid() {
|
||||
t.Fatalf("Token invalid. Got: %#v", tok)
|
||||
}
|
||||
expiry := tok.Expiry
|
||||
if expiry.Before(t1) || expiry.After(t2) {
|
||||
t.Errorf("Unexpected value for Expiry: %v (shold be between %v and %v)", expiry, t1, t2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeRequest_BadResponse(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"scope": "user", "token_type": "bearer"}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
tok, err := conf.Exchange(NoContext, "code")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tok.AccessToken != "" {
|
||||
t.Errorf("Unexpected access token, %#v.", tok.AccessToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeRequest_BadResponseType(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"access_token":123, "scope": "user", "token_type": "bearer"}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
_, err := conf.Exchange(NoContext, "exchange-code")
|
||||
if err == nil {
|
||||
t.Error("expected error from invalid access_token type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeRequest_NonBasicAuth(t *testing.T) {
|
||||
tr := &mockTransport{
|
||||
rt: func(r *http.Request) (w *http.Response, err error) {
|
||||
headerAuth := r.Header.Get("Authorization")
|
||||
if headerAuth != "" {
|
||||
t.Errorf("Unexpected authorization header, %v is found.", headerAuth)
|
||||
}
|
||||
return nil, errors.New("no response")
|
||||
},
|
||||
}
|
||||
c := &http.Client{Transport: tr}
|
||||
conf := &Config{
|
||||
ClientID: "CLIENT_ID",
|
||||
Endpoint: Endpoint{
|
||||
AuthURL: "https://accounts.google.com/auth",
|
||||
TokenURL: "https://accounts.google.com/token",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), HTTPClient, c)
|
||||
conf.Exchange(ctx, "code")
|
||||
}
|
||||
|
||||
func TestPasswordCredentialsTokenRequest(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
expected := "/token"
|
||||
if r.URL.String() != expected {
|
||||
t.Errorf("URL = %q; want %q", r.URL, expected)
|
||||
}
|
||||
headerAuth := r.Header.Get("Authorization")
|
||||
expected = "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ="
|
||||
if headerAuth != expected {
|
||||
t.Errorf("Authorization header = %q; want %q", headerAuth, expected)
|
||||
}
|
||||
headerContentType := r.Header.Get("Content-Type")
|
||||
expected = "application/x-www-form-urlencoded"
|
||||
if headerContentType != expected {
|
||||
t.Errorf("Content-Type header = %q; want %q", headerContentType, expected)
|
||||
}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Failed reading request body: %s.", err)
|
||||
}
|
||||
expected = "client_id=CLIENT_ID&grant_type=password&password=password1&scope=scope1+scope2&username=user1"
|
||||
if string(body) != expected {
|
||||
t.Errorf("res.Body = %q; want %q", string(body), expected)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&scope=user&token_type=bearer"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
tok, err := conf.PasswordCredentialsToken(NoContext, "user1", "password1")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !tok.Valid() {
|
||||
t.Fatalf("Token invalid. Got: %#v", tok)
|
||||
}
|
||||
expected := "90d64460d14870c08c81352a05dedd3465940a7c"
|
||||
if tok.AccessToken != expected {
|
||||
t.Errorf("AccessToken = %q; want %q", tok.AccessToken, expected)
|
||||
}
|
||||
expected = "bearer"
|
||||
if tok.TokenType != expected {
|
||||
t.Errorf("TokenType = %q; want %q", tok.TokenType, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenRefreshRequest(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() == "/somethingelse" {
|
||||
return
|
||||
}
|
||||
if r.URL.String() != "/token" {
|
||||
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
|
||||
}
|
||||
headerContentType := r.Header.Get("Content-Type")
|
||||
if headerContentType != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType)
|
||||
}
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
if string(body) != "client_id=CLIENT_ID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN" {
|
||||
t.Errorf("Unexpected refresh token payload, %v is found.", string(body))
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
c := conf.Client(NoContext, &Token{RefreshToken: "REFRESH_TOKEN"})
|
||||
c.Get(ts.URL + "/somethingelse")
|
||||
}
|
||||
|
||||
func TestFetchWithNoRefreshToken(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() == "/somethingelse" {
|
||||
return
|
||||
}
|
||||
if r.URL.String() != "/token" {
|
||||
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
|
||||
}
|
||||
headerContentType := r.Header.Get("Content-Type")
|
||||
if headerContentType != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType)
|
||||
}
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
if string(body) != "client_id=CLIENT_ID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN" {
|
||||
t.Errorf("Unexpected refresh token payload, %v is found.", string(body))
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
c := conf.Client(NoContext, nil)
|
||||
_, err := c.Get(ts.URL + "/somethingelse")
|
||||
if err == nil {
|
||||
t.Errorf("Fetch should return an error if no refresh token is set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshToken_RefreshTokenReplacement(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"access_token":"ACCESS TOKEN", "scope": "user", "token_type": "bearer", "refresh_token": "NEW REFRESH TOKEN"}`))
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
tkr := tokenRefresher{
|
||||
conf: conf,
|
||||
ctx: NoContext,
|
||||
refreshToken: "OLD REFRESH TOKEN",
|
||||
}
|
||||
tk, err := tkr.Token()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected refreshToken error returned: %v", err)
|
||||
return
|
||||
}
|
||||
if tk.RefreshToken != tkr.refreshToken {
|
||||
t.Errorf("tokenRefresher.refresh_token = %s; want %s", tkr.refreshToken, tk.RefreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigClientWithToken(t *testing.T) {
|
||||
tok := &Token{
|
||||
AccessToken: "abc123",
|
||||
}
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got, want := r.Header.Get("Authorization"), fmt.Sprintf("Bearer %s", tok.AccessToken); got != want {
|
||||
t.Errorf("Authorization header = %q; want %q", got, want)
|
||||
}
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
conf := newConf(ts.URL)
|
||||
|
||||
c := conf.Client(NoContext, tok)
|
||||
req, err := http.NewRequest("GET", ts.URL, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = c.Do(req)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package odnoklassniki provides constants for using OAuth2 to access Odnoklassniki.
|
||||
package odnoklassniki // import "golang.org/x/oauth2/odnoklassniki"
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Endpoint is Odnoklassniki's OAuth 2.0 endpoint.
|
||||
var Endpoint = oauth2.Endpoint{
|
||||
AuthURL: "https://www.odnoklassniki.ru/oauth/authorize",
|
||||
TokenURL: "https://api.odnoklassniki.ru/oauth/token.do",
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package paypal provides constants for using OAuth2 to access PayPal.
|
||||
package paypal // import "golang.org/x/oauth2/paypal"
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Endpoint is PayPal's OAuth 2.0 endpoint in live (production) environment.
|
||||
var Endpoint = oauth2.Endpoint{
|
||||
AuthURL: "https://www.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize",
|
||||
TokenURL: "https://api.paypal.com/v1/identity/openidconnect/tokenservice",
|
||||
}
|
||||
|
||||
// SandboxEndpoint is PayPal's OAuth 2.0 endpoint in sandbox (testing) environment.
|
||||
var SandboxEndpoint = oauth2.Endpoint{
|
||||
AuthURL: "https://www.sandbox.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize",
|
||||
TokenURL: "https://api.sandbox.paypal.com/v1/identity/openidconnect/tokenservice",
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2/internal"
|
||||
)
|
||||
|
||||
// expiryDelta determines how earlier a token should be considered
|
||||
// expired than its actual expiration time. It is used to avoid late
|
||||
// expirations due to client-server time mismatches.
|
||||
const expiryDelta = 10 * time.Second
|
||||
|
||||
// Token represents the crendentials used to authorize
|
||||
// the requests to access protected resources on the OAuth 2.0
|
||||
// provider's backend.
|
||||
//
|
||||
// Most users of this package should not access fields of Token
|
||||
// directly. They're exported mostly for use by related packages
|
||||
// implementing derivative OAuth2 flows.
|
||||
type Token struct {
|
||||
// AccessToken is the token that authorizes and authenticates
|
||||
// the requests.
|
||||
AccessToken string `json:"access_token"`
|
||||
|
||||
// TokenType is the type of token.
|
||||
// The Type method returns either this or "Bearer", the default.
|
||||
TokenType string `json:"token_type,omitempty"`
|
||||
|
||||
// RefreshToken is a token that's used by the application
|
||||
// (as opposed to the user) to refresh the access token
|
||||
// if it expires.
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
|
||||
// Expiry is the optional expiration time of the access token.
|
||||
//
|
||||
// If zero, TokenSource implementations will reuse the same
|
||||
// token forever and RefreshToken or equivalent
|
||||
// mechanisms for that TokenSource will not be used.
|
||||
Expiry time.Time `json:"expiry,omitempty"`
|
||||
|
||||
// raw optionally contains extra metadata from the server
|
||||
// when updating a token.
|
||||
raw interface{}
|
||||
}
|
||||
|
||||
// Type returns t.TokenType if non-empty, else "Bearer".
|
||||
func (t *Token) Type() string {
|
||||
if strings.EqualFold(t.TokenType, "bearer") {
|
||||
return "Bearer"
|
||||
}
|
||||
if strings.EqualFold(t.TokenType, "mac") {
|
||||
return "MAC"
|
||||
}
|
||||
if strings.EqualFold(t.TokenType, "basic") {
|
||||
return "Basic"
|
||||
}
|
||||
if t.TokenType != "" {
|
||||
return t.TokenType
|
||||
}
|
||||
return "Bearer"
|
||||
}
|
||||
|
||||
// SetAuthHeader sets the Authorization header to r using the access
|
||||
// token in t.
|
||||
//
|
||||
// This method is unnecessary when using Transport or an HTTP Client
|
||||
// returned by this package.
|
||||
func (t *Token) SetAuthHeader(r *http.Request) {
|
||||
r.Header.Set("Authorization", t.Type()+" "+t.AccessToken)
|
||||
}
|
||||
|
||||
// WithExtra returns a new Token that's a clone of t, but using the
|
||||
// provided raw extra map. This is only intended for use by packages
|
||||
// implementing derivative OAuth2 flows.
|
||||
func (t *Token) WithExtra(extra interface{}) *Token {
|
||||
t2 := new(Token)
|
||||
*t2 = *t
|
||||
t2.raw = extra
|
||||
return t2
|
||||
}
|
||||
|
||||
// Extra returns an extra field.
|
||||
// Extra fields are key-value pairs returned by the server as a
|
||||
// part of the token retrieval response.
|
||||
func (t *Token) Extra(key string) interface{} {
|
||||
if raw, ok := t.raw.(map[string]interface{}); ok {
|
||||
return raw[key]
|
||||
}
|
||||
|
||||
vals, ok := t.raw.(url.Values)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := vals.Get(key)
|
||||
switch s := strings.TrimSpace(v); strings.Count(s, ".") {
|
||||
case 0: // Contains no "."; try to parse as int
|
||||
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||
return i
|
||||
}
|
||||
case 1: // Contains a single "."; try to parse as float
|
||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// expired reports whether the token is expired.
|
||||
// t must be non-nil.
|
||||
func (t *Token) expired() bool {
|
||||
if t.Expiry.IsZero() {
|
||||
return false
|
||||
}
|
||||
return t.Expiry.Add(-expiryDelta).Before(time.Now())
|
||||
}
|
||||
|
||||
// Valid reports whether t is non-nil, has an AccessToken, and is not expired.
|
||||
func (t *Token) Valid() bool {
|
||||
return t != nil && t.AccessToken != "" && !t.expired()
|
||||
}
|
||||
|
||||
// tokenFromInternal maps an *internal.Token struct into
|
||||
// a *Token struct.
|
||||
func tokenFromInternal(t *internal.Token) *Token {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return &Token{
|
||||
AccessToken: t.AccessToken,
|
||||
TokenType: t.TokenType,
|
||||
RefreshToken: t.RefreshToken,
|
||||
Expiry: t.Expiry,
|
||||
raw: t.Raw,
|
||||
}
|
||||
}
|
||||
|
||||
// retrieveToken takes a *Config and uses that to retrieve an *internal.Token.
|
||||
// This token is then mapped from *internal.Token into an *oauth2.Token which is returned along
|
||||
// with an error..
|
||||
func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) {
|
||||
tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tokenFromInternal(tk), nil
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTokenExtra(t *testing.T) {
|
||||
type testCase struct {
|
||||
key string
|
||||
val interface{}
|
||||
want interface{}
|
||||
}
|
||||
const key = "extra-key"
|
||||
cases := []testCase{
|
||||
{key: key, val: "abc", want: "abc"},
|
||||
{key: key, val: 123, want: 123},
|
||||
{key: key, val: "", want: ""},
|
||||
{key: "other-key", val: "def", want: nil},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
extra := make(map[string]interface{})
|
||||
extra[tc.key] = tc.val
|
||||
tok := &Token{raw: extra}
|
||||
if got, want := tok.Extra(key), tc.want; got != want {
|
||||
t.Errorf("Extra(%q) = %q; want %q", key, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenExpiry(t *testing.T) {
|
||||
now := time.Now()
|
||||
cases := []struct {
|
||||
name string
|
||||
tok *Token
|
||||
want bool
|
||||
}{
|
||||
{name: "12 seconds", tok: &Token{Expiry: now.Add(12 * time.Second)}, want: false},
|
||||
{name: "10 seconds", tok: &Token{Expiry: now.Add(expiryDelta)}, want: true},
|
||||
{name: "-1 hour", tok: &Token{Expiry: now.Add(-1 * time.Hour)}, want: true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got, want := tc.tok.expired(), tc.want; got != want {
|
||||
t.Errorf("expired (%q) = %v; want %v", tc.name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenTypeMethod(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
tok *Token
|
||||
want string
|
||||
}{
|
||||
{name: "bearer-mixed_case", tok: &Token{TokenType: "beAREr"}, want: "Bearer"},
|
||||
{name: "default-bearer", tok: &Token{}, want: "Bearer"},
|
||||
{name: "basic", tok: &Token{TokenType: "basic"}, want: "Basic"},
|
||||
{name: "basic-capitalized", tok: &Token{TokenType: "Basic"}, want: "Basic"},
|
||||
{name: "mac", tok: &Token{TokenType: "mac"}, want: "MAC"},
|
||||
{name: "mac-caps", tok: &Token{TokenType: "MAC"}, want: "MAC"},
|
||||
{name: "mac-mixed_case", tok: &Token{TokenType: "mAc"}, want: "MAC"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got, want := tc.tok.Type(), tc.want; got != want {
|
||||
t.Errorf("TokenType(%q) = %v; want %v", tc.name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Transport is an http.RoundTripper that makes OAuth 2.0 HTTP requests,
|
||||
// wrapping a base RoundTripper and adding an Authorization header
|
||||
// with a token from the supplied Sources.
|
||||
//
|
||||
// Transport is a low-level mechanism. Most code will use the
|
||||
// higher-level Config.Client method instead.
|
||||
type Transport struct {
|
||||
// Source supplies the token to add to outgoing requests'
|
||||
// Authorization headers.
|
||||
Source TokenSource
|
||||
|
||||
// Base is the base RoundTripper used to make HTTP requests.
|
||||
// If nil, http.DefaultTransport is used.
|
||||
Base http.RoundTripper
|
||||
|
||||
mu sync.Mutex // guards modReq
|
||||
modReq map[*http.Request]*http.Request // original -> modified
|
||||
}
|
||||
|
||||
// RoundTrip authorizes and authenticates the request with an
|
||||
// access token. If no token exists or token is expired,
|
||||
// tries to refresh/fetch a new token.
|
||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if t.Source == nil {
|
||||
return nil, errors.New("oauth2: Transport's Source is nil")
|
||||
}
|
||||
token, err := t.Source.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req2 := cloneRequest(req) // per RoundTripper contract
|
||||
token.SetAuthHeader(req2)
|
||||
t.setModReq(req, req2)
|
||||
res, err := t.base().RoundTrip(req2)
|
||||
if err != nil {
|
||||
t.setModReq(req, nil)
|
||||
return nil, err
|
||||
}
|
||||
res.Body = &onEOFReader{
|
||||
rc: res.Body,
|
||||
fn: func() { t.setModReq(req, nil) },
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// CancelRequest cancels an in-flight request by closing its connection.
|
||||
func (t *Transport) CancelRequest(req *http.Request) {
|
||||
type canceler interface {
|
||||
CancelRequest(*http.Request)
|
||||
}
|
||||
if cr, ok := t.base().(canceler); ok {
|
||||
t.mu.Lock()
|
||||
modReq := t.modReq[req]
|
||||
delete(t.modReq, req)
|
||||
t.mu.Unlock()
|
||||
cr.CancelRequest(modReq)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) base() http.RoundTripper {
|
||||
if t.Base != nil {
|
||||
return t.Base
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
func (t *Transport) setModReq(orig, mod *http.Request) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.modReq == nil {
|
||||
t.modReq = make(map[*http.Request]*http.Request)
|
||||
}
|
||||
if mod == nil {
|
||||
delete(t.modReq, orig)
|
||||
} else {
|
||||
t.modReq[orig] = mod
|
||||
}
|
||||
}
|
||||
|
||||
// cloneRequest returns a clone of the provided *http.Request.
|
||||
// The clone is a shallow copy of the struct and its Header map.
|
||||
func cloneRequest(r *http.Request) *http.Request {
|
||||
// shallow copy of the struct
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
// deep copy of the Header
|
||||
r2.Header = make(http.Header, len(r.Header))
|
||||
for k, s := range r.Header {
|
||||
r2.Header[k] = append([]string(nil), s...)
|
||||
}
|
||||
return r2
|
||||
}
|
||||
|
||||
type onEOFReader struct {
|
||||
rc io.ReadCloser
|
||||
fn func()
|
||||
}
|
||||
|
||||
func (r *onEOFReader) Read(p []byte) (n int, err error) {
|
||||
n, err = r.rc.Read(p)
|
||||
if err == io.EOF {
|
||||
r.runFunc()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *onEOFReader) Close() error {
|
||||
err := r.rc.Close()
|
||||
r.runFunc()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *onEOFReader) runFunc() {
|
||||
if fn := r.fn; fn != nil {
|
||||
fn()
|
||||
r.fn = nil
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
package oauth2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type tokenSource struct{ token *Token }
|
||||
|
||||
func (t *tokenSource) Token() (*Token, error) {
|
||||
return t.token, nil
|
||||
}
|
||||
|
||||
func TestTransportTokenSource(t *testing.T) {
|
||||
ts := &tokenSource{
|
||||
token: &Token{
|
||||
AccessToken: "abc",
|
||||
},
|
||||
}
|
||||
tr := &Transport{
|
||||
Source: ts,
|
||||
}
|
||||
server := newMockServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "Bearer abc" {
|
||||
t.Errorf("Transport doesn't set the Authorization header from the fetched token")
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
client := http.Client{Transport: tr}
|
||||
client.Get(server.URL)
|
||||
}
|
||||
|
||||
// Test for case-sensitive token types, per https://github.com/golang/oauth2/issues/113
|
||||
func TestTransportTokenSourceTypes(t *testing.T) {
|
||||
const val = "abc"
|
||||
tests := []struct {
|
||||
key string
|
||||
val string
|
||||
want string
|
||||
}{
|
||||
{key: "bearer", val: val, want: "Bearer abc"},
|
||||
{key: "mac", val: val, want: "MAC abc"},
|
||||
{key: "basic", val: val, want: "Basic abc"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
ts := &tokenSource{
|
||||
token: &Token{
|
||||
AccessToken: tc.val,
|
||||
TokenType: tc.key,
|
||||
},
|
||||
}
|
||||
tr := &Transport{
|
||||
Source: ts,
|
||||
}
|
||||
server := newMockServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got, want := r.Header.Get("Authorization"), tc.want; got != want {
|
||||
t.Errorf("Authorization header (%q) = %q; want %q", val, got, want)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
client := http.Client{Transport: tr}
|
||||
client.Get(server.URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenValidNoAccessToken(t *testing.T) {
|
||||
token := &Token{}
|
||||
if token.Valid() {
|
||||
t.Errorf("Token should not be valid with no access token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiredWithExpiry(t *testing.T) {
|
||||
token := &Token{
|
||||
Expiry: time.Now().Add(-5 * time.Hour),
|
||||
}
|
||||
if token.Valid() {
|
||||
t.Errorf("Token should not be valid if it expired in the past")
|
||||
}
|
||||
}
|
||||
|
||||
func newMockServer(handler func(w http.ResponseWriter, r *http.Request)) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(handler))
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue