Merge remote-tracking branch 'origin/master' into master-wq
Former-commit-id: 00b07c6e6c94fd8d3f8dee9e791146e62b5b280c
This commit is contained in:
commit
f5e737696b
|
|
@ -13,9 +13,6 @@ PCM
|
|||
*.out
|
||||
coverage.txt
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
vendor/
|
||||
|
||||
# buf for protobuf
|
||||
buf.lock
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
squirrel.test
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
- 1.13.x
|
||||
|
||||
services:
|
||||
- mysql
|
||||
- postgresql
|
||||
|
||||
# Setting sudo access to false will let Travis CI use containers rather than
|
||||
# VMs to run the tests. For more details see:
|
||||
# - http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
# - http://docs.travis-ci.com/user/workers/standard-infrastructure/
|
||||
sudo: false
|
||||
|
||||
before_script:
|
||||
- mysql -e 'CREATE DATABASE squirrel;'
|
||||
- psql -c 'CREATE DATABASE squirrel;' -U postgres
|
||||
|
||||
script:
|
||||
- go test
|
||||
- cd integration
|
||||
- go test -args -driver sqlite3
|
||||
- go test -args -driver mysql -dataSource travis@/squirrel
|
||||
- go test -args -driver postgres -dataSource 'postgres://postgres@localhost/squirrel?sslmode=disable'
|
||||
|
||||
notifications:
|
||||
irc: "irc.freenode.net#masterminds"
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
MIT License
|
||||
|
||||
Squirrel: The Masterminds
|
||||
Copyright (c) 2014-2015, Lann Martin. Copyright (C) 2015-2016, Google. Copyright (C) 2015, Matt Farina and Matt Butcher.
|
||||
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
[](https://masterminds.github.io/stability/maintenance.html)
|
||||
### Squirrel is "complete".
|
||||
Bug fixes will still be merged (slowly). Bug reports are welcome, but I will not necessarily respond to them. If another fork (or substantially similar project) actively improves on what Squirrel does, let me know and I may link to it here.
|
||||
|
||||
|
||||
# Squirrel - fluent SQL generator for Go
|
||||
|
||||
```go
|
||||
import "github.com/Masterminds/squirrel"
|
||||
```
|
||||
|
||||
|
||||
[](https://godoc.org/github.com/Masterminds/squirrel)
|
||||
[](https://travis-ci.org/Masterminds/squirrel)
|
||||
|
||||
**Squirrel is not an ORM.** For an application of Squirrel, check out
|
||||
[structable, a table-struct mapper](https://github.com/Masterminds/structable)
|
||||
|
||||
|
||||
Squirrel helps you build SQL queries from composable parts:
|
||||
|
||||
```go
|
||||
import sq "github.com/Masterminds/squirrel"
|
||||
|
||||
users := sq.Select("*").From("users").Join("emails USING (email_id)")
|
||||
|
||||
active := users.Where(sq.Eq{"deleted_at": nil})
|
||||
|
||||
sql, args, err := active.ToSql()
|
||||
|
||||
sql == "SELECT * FROM users JOIN emails USING (email_id) WHERE deleted_at IS NULL"
|
||||
```
|
||||
|
||||
```go
|
||||
sql, args, err := sq.
|
||||
Insert("users").Columns("name", "age").
|
||||
Values("moe", 13).Values("larry", sq.Expr("? + 5", 12)).
|
||||
ToSql()
|
||||
|
||||
sql == "INSERT INTO users (name,age) VALUES (?,?),(?,? + 5)"
|
||||
```
|
||||
|
||||
Squirrel can also execute queries directly:
|
||||
|
||||
```go
|
||||
stooges := users.Where(sq.Eq{"username": []string{"moe", "larry", "curly", "shemp"}})
|
||||
three_stooges := stooges.Limit(3)
|
||||
rows, err := three_stooges.RunWith(db).Query()
|
||||
|
||||
// Behaves like:
|
||||
rows, err := db.Query("SELECT * FROM users WHERE username IN (?,?,?,?) LIMIT 3",
|
||||
"moe", "larry", "curly", "shemp")
|
||||
```
|
||||
|
||||
Squirrel makes conditional query building a breeze:
|
||||
|
||||
```go
|
||||
if len(q) > 0 {
|
||||
users = users.Where("name LIKE ?", fmt.Sprint("%", q, "%"))
|
||||
}
|
||||
```
|
||||
|
||||
Squirrel wants to make your life easier:
|
||||
|
||||
```go
|
||||
// StmtCache caches Prepared Stmts for you
|
||||
dbCache := sq.NewStmtCache(db)
|
||||
|
||||
// StatementBuilder keeps your syntax neat
|
||||
mydb := sq.StatementBuilder.RunWith(dbCache)
|
||||
select_users := mydb.Select("*").From("users")
|
||||
```
|
||||
|
||||
Squirrel loves PostgreSQL:
|
||||
|
||||
```go
|
||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||
|
||||
// You use question marks for placeholders...
|
||||
sql, _, _ := psql.Select("*").From("elephants").Where("name IN (?,?)", "Dumbo", "Verna").ToSql()
|
||||
|
||||
/// ...squirrel replaces them using PlaceholderFormat.
|
||||
sql == "SELECT * FROM elephants WHERE name IN ($1,$2)"
|
||||
|
||||
|
||||
/// You can retrieve id ...
|
||||
query := sq.Insert("nodes").
|
||||
Columns("uuid", "type", "data").
|
||||
Values(node.Uuid, node.Type, node.Data).
|
||||
Suffix("RETURNING \"id\"").
|
||||
RunWith(m.db).
|
||||
PlaceholderFormat(sq.Dollar)
|
||||
|
||||
query.QueryRow().Scan(&node.id)
|
||||
```
|
||||
|
||||
You can escape question marks by inserting two question marks:
|
||||
|
||||
```sql
|
||||
SELECT * FROM nodes WHERE meta->'format' ??| array[?,?]
|
||||
```
|
||||
|
||||
will generate with the Dollar Placeholder:
|
||||
|
||||
```sql
|
||||
SELECT * FROM nodes WHERE meta->'format' ?| array[$1,$2]
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
* **How can I build an IN query on composite keys / tuples, e.g. `WHERE (col1, col2) IN ((1,2),(3,4))`? ([#104](https://github.com/Masterminds/squirrel/issues/104))**
|
||||
|
||||
Squirrel does not explicitly support tuples, but you can get the same effect with e.g.:
|
||||
|
||||
```go
|
||||
sq.Or{
|
||||
sq.Eq{"col1": 1, "col2": 2},
|
||||
sq.Eq{"col1": 3, "col2": 4}}
|
||||
```
|
||||
|
||||
```sql
|
||||
WHERE (col1 = 1 AND col2 = 2) OR (col1 = 3 AND col2 = 4)
|
||||
```
|
||||
|
||||
(which should produce the same query plan as the tuple version)
|
||||
|
||||
* **Why doesn't `Eq{"mynumber": []uint8{1,2,3}}` turn into an `IN` query? ([#114](https://github.com/Masterminds/squirrel/issues/114))**
|
||||
|
||||
Values of type `[]byte` are handled specially by `database/sql`. In Go, [`byte` is just an alias of `uint8`](https://golang.org/pkg/builtin/#byte), so there is no way to distinguish `[]uint8` from `[]byte`.
|
||||
|
||||
* **Some features are poorly documented!**
|
||||
|
||||
This isn't a frequent complaints section!
|
||||
|
||||
* **Some features are poorly documented?**
|
||||
|
||||
Yes. The tests should be considered a part of the documentation; take a look at those for ideas on how to express more complex queries.
|
||||
|
||||
## License
|
||||
|
||||
Squirrel is released under the
|
||||
[MIT License](http://www.opensource.org/licenses/MIT).
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package squirrel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
|
||||
"github.com/lann/builder"
|
||||
)
|
||||
|
||||
func init() {
|
||||
builder.Register(CaseBuilder{}, caseData{})
|
||||
}
|
||||
|
||||
// sqlizerBuffer is a helper that allows to write many Sqlizers one by one
|
||||
// without constant checks for errors that may come from Sqlizer
|
||||
type sqlizerBuffer struct {
|
||||
bytes.Buffer
|
||||
args []interface{}
|
||||
err error
|
||||
}
|
||||
|
||||
// WriteSql converts Sqlizer to SQL strings and writes it to buffer
|
||||
func (b *sqlizerBuffer) WriteSql(item Sqlizer) {
|
||||
if b.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var str string
|
||||
var args []interface{}
|
||||
str, args, b.err = nestedToSql(item)
|
||||
|
||||
if b.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.WriteString(str)
|
||||
b.WriteByte(' ')
|
||||
b.args = append(b.args, args...)
|
||||
}
|
||||
|
||||
func (b *sqlizerBuffer) ToSql() (string, []interface{}, error) {
|
||||
return b.String(), b.args, b.err
|
||||
}
|
||||
|
||||
// whenPart is a helper structure to describe SQLs "WHEN ... THEN ..." expression
|
||||
type whenPart struct {
|
||||
when Sqlizer
|
||||
then Sqlizer
|
||||
}
|
||||
|
||||
func newWhenPart(when interface{}, then interface{}) whenPart {
|
||||
return whenPart{newPart(when), newPart(then)}
|
||||
}
|
||||
|
||||
// caseData holds all the data required to build a CASE SQL construct
|
||||
type caseData struct {
|
||||
What Sqlizer
|
||||
WhenParts []whenPart
|
||||
Else Sqlizer
|
||||
}
|
||||
|
||||
// ToSql implements Sqlizer
|
||||
func (d *caseData) ToSql() (sqlStr string, args []interface{}, err error) {
|
||||
if len(d.WhenParts) == 0 {
|
||||
err = errors.New("case expression must contain at lease one WHEN clause")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sql := sqlizerBuffer{}
|
||||
|
||||
sql.WriteString("CASE ")
|
||||
if d.What != nil {
|
||||
sql.WriteSql(d.What)
|
||||
}
|
||||
|
||||
for _, p := range d.WhenParts {
|
||||
sql.WriteString("WHEN ")
|
||||
sql.WriteSql(p.when)
|
||||
sql.WriteString("THEN ")
|
||||
sql.WriteSql(p.then)
|
||||
}
|
||||
|
||||
if d.Else != nil {
|
||||
sql.WriteString("ELSE ")
|
||||
sql.WriteSql(d.Else)
|
||||
}
|
||||
|
||||
sql.WriteString("END")
|
||||
|
||||
return sql.ToSql()
|
||||
}
|
||||
|
||||
// CaseBuilder builds SQL CASE construct which could be used as parts of queries.
|
||||
type CaseBuilder builder.Builder
|
||||
|
||||
// ToSql builds the query into a SQL string and bound args.
|
||||
func (b CaseBuilder) ToSql() (string, []interface{}, error) {
|
||||
data := builder.GetStruct(b).(caseData)
|
||||
return data.ToSql()
|
||||
}
|
||||
|
||||
// MustSql builds the query into a SQL string and bound args.
|
||||
// It panics if there are any errors.
|
||||
func (b CaseBuilder) MustSql() (string, []interface{}) {
|
||||
sql, args, err := b.ToSql()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sql, args
|
||||
}
|
||||
|
||||
// what sets optional value for CASE construct "CASE [value] ..."
|
||||
func (b CaseBuilder) what(expr interface{}) CaseBuilder {
|
||||
return builder.Set(b, "What", newPart(expr)).(CaseBuilder)
|
||||
}
|
||||
|
||||
// When adds "WHEN ... THEN ..." part to CASE construct
|
||||
func (b CaseBuilder) When(when interface{}, then interface{}) CaseBuilder {
|
||||
// TODO: performance hint: replace slice of WhenPart with just slice of parts
|
||||
// where even indices of the slice belong to "when"s and odd indices belong to "then"s
|
||||
return builder.Append(b, "WhenParts", newWhenPart(when, then)).(CaseBuilder)
|
||||
}
|
||||
|
||||
// What sets optional "ELSE ..." part for CASE construct
|
||||
func (b CaseBuilder) Else(expr interface{}) CaseBuilder {
|
||||
return builder.Set(b, "Else", newPart(expr)).(CaseBuilder)
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
package squirrel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lann/builder"
|
||||
)
|
||||
|
||||
type deleteData struct {
|
||||
PlaceholderFormat PlaceholderFormat
|
||||
RunWith BaseRunner
|
||||
Prefixes []Sqlizer
|
||||
From string
|
||||
WhereParts []Sqlizer
|
||||
OrderBys []string
|
||||
Limit string
|
||||
Offset string
|
||||
Suffixes []Sqlizer
|
||||
}
|
||||
|
||||
func (d *deleteData) Exec() (sql.Result, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
return ExecWith(d.RunWith, d)
|
||||
}
|
||||
|
||||
func (d *deleteData) ToSql() (sqlStr string, args []interface{}, err error) {
|
||||
if len(d.From) == 0 {
|
||||
err = fmt.Errorf("delete statements must specify a From table")
|
||||
return
|
||||
}
|
||||
|
||||
sql := &bytes.Buffer{}
|
||||
|
||||
if len(d.Prefixes) > 0 {
|
||||
args, err = appendToSql(d.Prefixes, sql, " ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sql.WriteString(" ")
|
||||
}
|
||||
|
||||
sql.WriteString("DELETE FROM ")
|
||||
sql.WriteString(d.From)
|
||||
|
||||
if len(d.WhereParts) > 0 {
|
||||
sql.WriteString(" WHERE ")
|
||||
args, err = appendToSql(d.WhereParts, sql, " AND ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.OrderBys) > 0 {
|
||||
sql.WriteString(" ORDER BY ")
|
||||
sql.WriteString(strings.Join(d.OrderBys, ", "))
|
||||
}
|
||||
|
||||
if len(d.Limit) > 0 {
|
||||
sql.WriteString(" LIMIT ")
|
||||
sql.WriteString(d.Limit)
|
||||
}
|
||||
|
||||
if len(d.Offset) > 0 {
|
||||
sql.WriteString(" OFFSET ")
|
||||
sql.WriteString(d.Offset)
|
||||
}
|
||||
|
||||
if len(d.Suffixes) > 0 {
|
||||
sql.WriteString(" ")
|
||||
args, err = appendToSql(d.Suffixes, sql, " ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sqlStr, err = d.PlaceholderFormat.ReplacePlaceholders(sql.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Builder
|
||||
|
||||
// DeleteBuilder builds SQL DELETE statements.
|
||||
type DeleteBuilder builder.Builder
|
||||
|
||||
func init() {
|
||||
builder.Register(DeleteBuilder{}, deleteData{})
|
||||
}
|
||||
|
||||
// Format methods
|
||||
|
||||
// PlaceholderFormat sets PlaceholderFormat (e.g. Question or Dollar) for the
|
||||
// query.
|
||||
func (b DeleteBuilder) PlaceholderFormat(f PlaceholderFormat) DeleteBuilder {
|
||||
return builder.Set(b, "PlaceholderFormat", f).(DeleteBuilder)
|
||||
}
|
||||
|
||||
// Runner methods
|
||||
|
||||
// RunWith sets a Runner (like database/sql.DB) to be used with e.g. Exec.
|
||||
func (b DeleteBuilder) RunWith(runner BaseRunner) DeleteBuilder {
|
||||
return setRunWith(b, runner).(DeleteBuilder)
|
||||
}
|
||||
|
||||
// Exec builds and Execs the query with the Runner set by RunWith.
|
||||
func (b DeleteBuilder) Exec() (sql.Result, error) {
|
||||
data := builder.GetStruct(b).(deleteData)
|
||||
return data.Exec()
|
||||
}
|
||||
|
||||
// SQL methods
|
||||
|
||||
// ToSql builds the query into a SQL string and bound args.
|
||||
func (b DeleteBuilder) ToSql() (string, []interface{}, error) {
|
||||
data := builder.GetStruct(b).(deleteData)
|
||||
return data.ToSql()
|
||||
}
|
||||
|
||||
// MustSql builds the query into a SQL string and bound args.
|
||||
// It panics if there are any errors.
|
||||
func (b DeleteBuilder) MustSql() (string, []interface{}) {
|
||||
sql, args, err := b.ToSql()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sql, args
|
||||
}
|
||||
|
||||
// Prefix adds an expression to the beginning of the query
|
||||
func (b DeleteBuilder) Prefix(sql string, args ...interface{}) DeleteBuilder {
|
||||
return b.PrefixExpr(Expr(sql, args...))
|
||||
}
|
||||
|
||||
// PrefixExpr adds an expression to the very beginning of the query
|
||||
func (b DeleteBuilder) PrefixExpr(expr Sqlizer) DeleteBuilder {
|
||||
return builder.Append(b, "Prefixes", expr).(DeleteBuilder)
|
||||
}
|
||||
|
||||
// From sets the table to be deleted from.
|
||||
func (b DeleteBuilder) From(from string) DeleteBuilder {
|
||||
return builder.Set(b, "From", from).(DeleteBuilder)
|
||||
}
|
||||
|
||||
// Where adds WHERE expressions to the query.
|
||||
//
|
||||
// See SelectBuilder.Where for more information.
|
||||
func (b DeleteBuilder) Where(pred interface{}, args ...interface{}) DeleteBuilder {
|
||||
return builder.Append(b, "WhereParts", newWherePart(pred, args...)).(DeleteBuilder)
|
||||
}
|
||||
|
||||
// OrderBy adds ORDER BY expressions to the query.
|
||||
func (b DeleteBuilder) OrderBy(orderBys ...string) DeleteBuilder {
|
||||
return builder.Extend(b, "OrderBys", orderBys).(DeleteBuilder)
|
||||
}
|
||||
|
||||
// Limit sets a LIMIT clause on the query.
|
||||
func (b DeleteBuilder) Limit(limit uint64) DeleteBuilder {
|
||||
return builder.Set(b, "Limit", fmt.Sprintf("%d", limit)).(DeleteBuilder)
|
||||
}
|
||||
|
||||
// Offset sets a OFFSET clause on the query.
|
||||
func (b DeleteBuilder) Offset(offset uint64) DeleteBuilder {
|
||||
return builder.Set(b, "Offset", fmt.Sprintf("%d", offset)).(DeleteBuilder)
|
||||
}
|
||||
|
||||
// Suffix adds an expression to the end of the query
|
||||
func (b DeleteBuilder) Suffix(sql string, args ...interface{}) DeleteBuilder {
|
||||
return b.SuffixExpr(Expr(sql, args...))
|
||||
}
|
||||
|
||||
// SuffixExpr adds an expression to the end of the query
|
||||
func (b DeleteBuilder) SuffixExpr(expr Sqlizer) DeleteBuilder {
|
||||
return builder.Append(b, "Suffixes", expr).(DeleteBuilder)
|
||||
}
|
||||
|
||||
func (b DeleteBuilder) Query() (*sql.Rows, error) {
|
||||
data := builder.GetStruct(b).(deleteData)
|
||||
return data.Query()
|
||||
}
|
||||
|
||||
func (d *deleteData) Query() (*sql.Rows, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
return QueryWith(d.RunWith, d)
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// +build go1.8
|
||||
|
||||
package squirrel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/lann/builder"
|
||||
)
|
||||
|
||||
func (d *deleteData) ExecContext(ctx context.Context) (sql.Result, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
ctxRunner, ok := d.RunWith.(ExecerContext)
|
||||
if !ok {
|
||||
return nil, NoContextSupport
|
||||
}
|
||||
return ExecContextWith(ctx, ctxRunner, d)
|
||||
}
|
||||
|
||||
func (d *deleteData) QueryContext(ctx context.Context) (*sql.Rows, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
ctxRunner, ok := d.RunWith.(QueryerContext)
|
||||
if !ok {
|
||||
return nil, NoContextSupport
|
||||
}
|
||||
return QueryContextWith(ctx, ctxRunner, d)
|
||||
}
|
||||
|
||||
func (d *deleteData) QueryRowContext(ctx context.Context) RowScanner {
|
||||
if d.RunWith == nil {
|
||||
return &Row{err: RunnerNotSet}
|
||||
}
|
||||
queryRower, ok := d.RunWith.(QueryRowerContext)
|
||||
if !ok {
|
||||
if _, ok := d.RunWith.(QueryerContext); !ok {
|
||||
return &Row{err: RunnerNotQueryRunner}
|
||||
}
|
||||
return &Row{err: NoContextSupport}
|
||||
}
|
||||
return QueryRowContextWith(ctx, queryRower, d)
|
||||
}
|
||||
|
||||
// ExecContext builds and ExecContexts the query with the Runner set by RunWith.
|
||||
func (b DeleteBuilder) ExecContext(ctx context.Context) (sql.Result, error) {
|
||||
data := builder.GetStruct(b).(deleteData)
|
||||
return data.ExecContext(ctx)
|
||||
}
|
||||
|
||||
// QueryContext builds and QueryContexts the query with the Runner set by RunWith.
|
||||
func (b DeleteBuilder) QueryContext(ctx context.Context) (*sql.Rows, error) {
|
||||
data := builder.GetStruct(b).(deleteData)
|
||||
return data.QueryContext(ctx)
|
||||
}
|
||||
|
||||
// QueryRowContext builds and QueryRowContexts the query with the Runner set by RunWith.
|
||||
func (b DeleteBuilder) QueryRowContext(ctx context.Context) RowScanner {
|
||||
data := builder.GetStruct(b).(deleteData)
|
||||
return data.QueryRowContext(ctx)
|
||||
}
|
||||
|
||||
// ScanContext is a shortcut for QueryRowContext().Scan.
|
||||
func (b DeleteBuilder) ScanContext(ctx context.Context, dest ...interface{}) error {
|
||||
return b.QueryRowContext(ctx).Scan(dest...)
|
||||
}
|
||||
|
|
@ -0,0 +1,419 @@
|
|||
package squirrel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// Portable true/false literals.
|
||||
sqlTrue = "(1=1)"
|
||||
sqlFalse = "(1=0)"
|
||||
)
|
||||
|
||||
type expr struct {
|
||||
sql string
|
||||
args []interface{}
|
||||
}
|
||||
|
||||
// Expr builds an expression from a SQL fragment and arguments.
|
||||
//
|
||||
// Ex:
|
||||
// Expr("FROM_UNIXTIME(?)", t)
|
||||
func Expr(sql string, args ...interface{}) Sqlizer {
|
||||
return expr{sql: sql, args: args}
|
||||
}
|
||||
|
||||
func (e expr) ToSql() (sql string, args []interface{}, err error) {
|
||||
simple := true
|
||||
for _, arg := range e.args {
|
||||
if _, ok := arg.(Sqlizer); ok {
|
||||
simple = false
|
||||
}
|
||||
}
|
||||
if simple {
|
||||
return e.sql, e.args, nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
ap := e.args
|
||||
sp := e.sql
|
||||
|
||||
var isql string
|
||||
var iargs []interface{}
|
||||
|
||||
for err == nil && len(ap) > 0 && len(sp) > 0 {
|
||||
i := strings.Index(sp, "?")
|
||||
if i < 0 {
|
||||
// no more placeholders
|
||||
break
|
||||
}
|
||||
if len(sp) > i+1 && sp[i+1:i+2] == "?" {
|
||||
// escaped "??"; append it and step past
|
||||
buf.WriteString(sp[:i+2])
|
||||
sp = sp[i+2:]
|
||||
continue
|
||||
}
|
||||
|
||||
if as, ok := ap[0].(Sqlizer); ok {
|
||||
// sqlizer argument; expand it and append the result
|
||||
isql, iargs, err = as.ToSql()
|
||||
buf.WriteString(sp[:i])
|
||||
buf.WriteString(isql)
|
||||
args = append(args, iargs...)
|
||||
} else {
|
||||
// normal argument; append it and the placeholder
|
||||
buf.WriteString(sp[:i+1])
|
||||
args = append(args, ap[0])
|
||||
}
|
||||
|
||||
// step past the argument and placeholder
|
||||
ap = ap[1:]
|
||||
sp = sp[i+1:]
|
||||
}
|
||||
|
||||
// append the remaining sql and arguments
|
||||
buf.WriteString(sp)
|
||||
return buf.String(), append(args, ap...), err
|
||||
}
|
||||
|
||||
type concatExpr []interface{}
|
||||
|
||||
func (ce concatExpr) ToSql() (sql string, args []interface{}, err error) {
|
||||
for _, part := range ce {
|
||||
switch p := part.(type) {
|
||||
case string:
|
||||
sql += p
|
||||
case Sqlizer:
|
||||
pSql, pArgs, err := p.ToSql()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
sql += pSql
|
||||
args = append(args, pArgs...)
|
||||
default:
|
||||
return "", nil, fmt.Errorf("%#v is not a string or Sqlizer", part)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ConcatExpr builds an expression by concatenating strings and other expressions.
|
||||
//
|
||||
// Ex:
|
||||
// name_expr := Expr("CONCAT(?, ' ', ?)", firstName, lastName)
|
||||
// ConcatExpr("COALESCE(full_name,", name_expr, ")")
|
||||
func ConcatExpr(parts ...interface{}) concatExpr {
|
||||
return concatExpr(parts)
|
||||
}
|
||||
|
||||
// aliasExpr helps to alias part of SQL query generated with underlying "expr"
|
||||
type aliasExpr struct {
|
||||
expr Sqlizer
|
||||
alias string
|
||||
}
|
||||
|
||||
// Alias allows to define alias for column in SelectBuilder. Useful when column is
|
||||
// defined as complex expression like IF or CASE
|
||||
// Ex:
|
||||
// .Column(Alias(caseStmt, "case_column"))
|
||||
func Alias(expr Sqlizer, alias string) aliasExpr {
|
||||
return aliasExpr{expr, alias}
|
||||
}
|
||||
|
||||
func (e aliasExpr) ToSql() (sql string, args []interface{}, err error) {
|
||||
sql, args, err = e.expr.ToSql()
|
||||
if err == nil {
|
||||
sql = fmt.Sprintf("(%s) AS %s", sql, e.alias)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Eq is syntactic sugar for use with Where/Having/Set methods.
|
||||
type Eq map[string]interface{}
|
||||
|
||||
func (eq Eq) toSQL(useNotOpr bool) (sql string, args []interface{}, err error) {
|
||||
if len(eq) == 0 {
|
||||
// Empty Sql{} evaluates to true.
|
||||
sql = sqlTrue
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
exprs []string
|
||||
equalOpr = "="
|
||||
inOpr = "IN"
|
||||
nullOpr = "IS"
|
||||
inEmptyExpr = sqlFalse
|
||||
)
|
||||
|
||||
if useNotOpr {
|
||||
equalOpr = "<>"
|
||||
inOpr = "NOT IN"
|
||||
nullOpr = "IS NOT"
|
||||
inEmptyExpr = sqlTrue
|
||||
}
|
||||
|
||||
sortedKeys := getSortedKeys(eq)
|
||||
for _, key := range sortedKeys {
|
||||
var expr string
|
||||
val := eq[key]
|
||||
|
||||
switch v := val.(type) {
|
||||
case driver.Valuer:
|
||||
if val, err = v.Value(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r := reflect.ValueOf(val)
|
||||
if r.Kind() == reflect.Ptr {
|
||||
if r.IsNil() {
|
||||
val = nil
|
||||
} else {
|
||||
val = r.Elem().Interface()
|
||||
}
|
||||
}
|
||||
|
||||
if val == nil {
|
||||
expr = fmt.Sprintf("%s %s NULL", key, nullOpr)
|
||||
} else {
|
||||
if isListType(val) {
|
||||
valVal := reflect.ValueOf(val)
|
||||
if valVal.Len() == 0 {
|
||||
expr = inEmptyExpr
|
||||
if args == nil {
|
||||
args = []interface{}{}
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < valVal.Len(); i++ {
|
||||
args = append(args, valVal.Index(i).Interface())
|
||||
}
|
||||
expr = fmt.Sprintf("%s %s (%s)", key, inOpr, Placeholders(valVal.Len()))
|
||||
}
|
||||
} else {
|
||||
expr = fmt.Sprintf("%s %s ?", key, equalOpr)
|
||||
args = append(args, val)
|
||||
}
|
||||
}
|
||||
exprs = append(exprs, expr)
|
||||
}
|
||||
sql = strings.Join(exprs, " AND ")
|
||||
return
|
||||
}
|
||||
|
||||
func (eq Eq) ToSql() (sql string, args []interface{}, err error) {
|
||||
return eq.toSQL(false)
|
||||
}
|
||||
|
||||
// NotEq is syntactic sugar for use with Where/Having/Set methods.
|
||||
// Ex:
|
||||
// .Where(NotEq{"id": 1}) == "id <> 1"
|
||||
type NotEq Eq
|
||||
|
||||
func (neq NotEq) ToSql() (sql string, args []interface{}, err error) {
|
||||
return Eq(neq).toSQL(true)
|
||||
}
|
||||
|
||||
// Like is syntactic sugar for use with LIKE conditions.
|
||||
// Ex:
|
||||
// .Where(Like{"name": "%irrel"})
|
||||
type Like map[string]interface{}
|
||||
|
||||
func (lk Like) toSql(opr string) (sql string, args []interface{}, err error) {
|
||||
var exprs []string
|
||||
for key, val := range lk {
|
||||
expr := ""
|
||||
|
||||
switch v := val.(type) {
|
||||
case driver.Valuer:
|
||||
if val, err = v.Value(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if val == nil {
|
||||
err = fmt.Errorf("cannot use null with like operators")
|
||||
return
|
||||
} else {
|
||||
if isListType(val) {
|
||||
err = fmt.Errorf("cannot use array or slice with like operators")
|
||||
return
|
||||
} else {
|
||||
expr = fmt.Sprintf("%s %s ?", key, opr)
|
||||
args = append(args, val)
|
||||
}
|
||||
}
|
||||
exprs = append(exprs, expr)
|
||||
}
|
||||
sql = strings.Join(exprs, " AND ")
|
||||
return
|
||||
}
|
||||
|
||||
func (lk Like) ToSql() (sql string, args []interface{}, err error) {
|
||||
return lk.toSql("LIKE")
|
||||
}
|
||||
|
||||
// NotLike is syntactic sugar for use with LIKE conditions.
|
||||
// Ex:
|
||||
// .Where(NotLike{"name": "%irrel"})
|
||||
type NotLike Like
|
||||
|
||||
func (nlk NotLike) ToSql() (sql string, args []interface{}, err error) {
|
||||
return Like(nlk).toSql("NOT LIKE")
|
||||
}
|
||||
|
||||
// ILike is syntactic sugar for use with ILIKE conditions.
|
||||
// Ex:
|
||||
// .Where(ILike{"name": "sq%"})
|
||||
type ILike Like
|
||||
|
||||
func (ilk ILike) ToSql() (sql string, args []interface{}, err error) {
|
||||
return Like(ilk).toSql("ILIKE")
|
||||
}
|
||||
|
||||
// NotILike is syntactic sugar for use with ILIKE conditions.
|
||||
// Ex:
|
||||
// .Where(NotILike{"name": "sq%"})
|
||||
type NotILike Like
|
||||
|
||||
func (nilk NotILike) ToSql() (sql string, args []interface{}, err error) {
|
||||
return Like(nilk).toSql("NOT ILIKE")
|
||||
}
|
||||
|
||||
// Lt is syntactic sugar for use with Where/Having/Set methods.
|
||||
// Ex:
|
||||
// .Where(Lt{"id": 1})
|
||||
type Lt map[string]interface{}
|
||||
|
||||
func (lt Lt) toSql(opposite, orEq bool) (sql string, args []interface{}, err error) {
|
||||
var (
|
||||
exprs []string
|
||||
opr = "<"
|
||||
)
|
||||
|
||||
if opposite {
|
||||
opr = ">"
|
||||
}
|
||||
|
||||
if orEq {
|
||||
opr = fmt.Sprintf("%s%s", opr, "=")
|
||||
}
|
||||
|
||||
sortedKeys := getSortedKeys(lt)
|
||||
for _, key := range sortedKeys {
|
||||
var expr string
|
||||
val := lt[key]
|
||||
|
||||
switch v := val.(type) {
|
||||
case driver.Valuer:
|
||||
if val, err = v.Value(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if val == nil {
|
||||
err = fmt.Errorf("cannot use null with less than or greater than operators")
|
||||
return
|
||||
}
|
||||
if isListType(val) {
|
||||
err = fmt.Errorf("cannot use array or slice with less than or greater than operators")
|
||||
return
|
||||
}
|
||||
expr = fmt.Sprintf("%s %s ?", key, opr)
|
||||
args = append(args, val)
|
||||
|
||||
exprs = append(exprs, expr)
|
||||
}
|
||||
sql = strings.Join(exprs, " AND ")
|
||||
return
|
||||
}
|
||||
|
||||
func (lt Lt) ToSql() (sql string, args []interface{}, err error) {
|
||||
return lt.toSql(false, false)
|
||||
}
|
||||
|
||||
// LtOrEq is syntactic sugar for use with Where/Having/Set methods.
|
||||
// Ex:
|
||||
// .Where(LtOrEq{"id": 1}) == "id <= 1"
|
||||
type LtOrEq Lt
|
||||
|
||||
func (ltOrEq LtOrEq) ToSql() (sql string, args []interface{}, err error) {
|
||||
return Lt(ltOrEq).toSql(false, true)
|
||||
}
|
||||
|
||||
// Gt is syntactic sugar for use with Where/Having/Set methods.
|
||||
// Ex:
|
||||
// .Where(Gt{"id": 1}) == "id > 1"
|
||||
type Gt Lt
|
||||
|
||||
func (gt Gt) ToSql() (sql string, args []interface{}, err error) {
|
||||
return Lt(gt).toSql(true, false)
|
||||
}
|
||||
|
||||
// GtOrEq is syntactic sugar for use with Where/Having/Set methods.
|
||||
// Ex:
|
||||
// .Where(GtOrEq{"id": 1}) == "id >= 1"
|
||||
type GtOrEq Lt
|
||||
|
||||
func (gtOrEq GtOrEq) ToSql() (sql string, args []interface{}, err error) {
|
||||
return Lt(gtOrEq).toSql(true, true)
|
||||
}
|
||||
|
||||
type conj []Sqlizer
|
||||
|
||||
func (c conj) join(sep, defaultExpr string) (sql string, args []interface{}, err error) {
|
||||
if len(c) == 0 {
|
||||
return defaultExpr, []interface{}{}, nil
|
||||
}
|
||||
var sqlParts []string
|
||||
for _, sqlizer := range c {
|
||||
partSQL, partArgs, err := nestedToSql(sqlizer)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if partSQL != "" {
|
||||
sqlParts = append(sqlParts, partSQL)
|
||||
args = append(args, partArgs...)
|
||||
}
|
||||
}
|
||||
if len(sqlParts) > 0 {
|
||||
sql = fmt.Sprintf("(%s)", strings.Join(sqlParts, sep))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// And conjunction Sqlizers
|
||||
type And conj
|
||||
|
||||
func (a And) ToSql() (string, []interface{}, error) {
|
||||
return conj(a).join(" AND ", sqlTrue)
|
||||
}
|
||||
|
||||
// Or conjunction Sqlizers
|
||||
type Or conj
|
||||
|
||||
func (o Or) ToSql() (string, []interface{}, error) {
|
||||
return conj(o).join(" OR ", sqlFalse)
|
||||
}
|
||||
|
||||
func getSortedKeys(exp map[string]interface{}) []string {
|
||||
sortedKeys := make([]string, 0, len(exp))
|
||||
for k := range exp {
|
||||
sortedKeys = append(sortedKeys, k)
|
||||
}
|
||||
sort.Strings(sortedKeys)
|
||||
return sortedKeys
|
||||
}
|
||||
|
||||
func isListType(val interface{}) bool {
|
||||
if driver.IsValue(val) {
|
||||
return false
|
||||
}
|
||||
valVal := reflect.ValueOf(val)
|
||||
return valVal.Kind() == reflect.Array || valVal.Kind() == reflect.Slice
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
package squirrel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/lann/builder"
|
||||
)
|
||||
|
||||
type insertData struct {
|
||||
PlaceholderFormat PlaceholderFormat
|
||||
RunWith BaseRunner
|
||||
Prefixes []Sqlizer
|
||||
StatementKeyword string
|
||||
Options []string
|
||||
Into string
|
||||
Columns []string
|
||||
Values [][]interface{}
|
||||
Suffixes []Sqlizer
|
||||
Select *SelectBuilder
|
||||
}
|
||||
|
||||
func (d *insertData) Exec() (sql.Result, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
return ExecWith(d.RunWith, d)
|
||||
}
|
||||
|
||||
func (d *insertData) Query() (*sql.Rows, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
return QueryWith(d.RunWith, d)
|
||||
}
|
||||
|
||||
func (d *insertData) QueryRow() RowScanner {
|
||||
if d.RunWith == nil {
|
||||
return &Row{err: RunnerNotSet}
|
||||
}
|
||||
queryRower, ok := d.RunWith.(QueryRower)
|
||||
if !ok {
|
||||
return &Row{err: RunnerNotQueryRunner}
|
||||
}
|
||||
return QueryRowWith(queryRower, d)
|
||||
}
|
||||
|
||||
func (d *insertData) ToSql() (sqlStr string, args []interface{}, err error) {
|
||||
if len(d.Into) == 0 {
|
||||
err = errors.New("insert statements must specify a table")
|
||||
return
|
||||
}
|
||||
if len(d.Values) == 0 && d.Select == nil {
|
||||
err = errors.New("insert statements must have at least one set of values or select clause")
|
||||
return
|
||||
}
|
||||
|
||||
sql := &bytes.Buffer{}
|
||||
|
||||
if len(d.Prefixes) > 0 {
|
||||
args, err = appendToSql(d.Prefixes, sql, " ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sql.WriteString(" ")
|
||||
}
|
||||
|
||||
if d.StatementKeyword == "" {
|
||||
sql.WriteString("INSERT ")
|
||||
} else {
|
||||
sql.WriteString(d.StatementKeyword)
|
||||
sql.WriteString(" ")
|
||||
}
|
||||
|
||||
if len(d.Options) > 0 {
|
||||
sql.WriteString(strings.Join(d.Options, " "))
|
||||
sql.WriteString(" ")
|
||||
}
|
||||
|
||||
sql.WriteString("INTO ")
|
||||
sql.WriteString(d.Into)
|
||||
sql.WriteString(" ")
|
||||
|
||||
if len(d.Columns) > 0 {
|
||||
sql.WriteString("(")
|
||||
sql.WriteString(strings.Join(d.Columns, ","))
|
||||
sql.WriteString(") ")
|
||||
}
|
||||
|
||||
if d.Select != nil {
|
||||
args, err = d.appendSelectToSQL(sql, args)
|
||||
} else {
|
||||
args, err = d.appendValuesToSQL(sql, args)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(d.Suffixes) > 0 {
|
||||
sql.WriteString(" ")
|
||||
args, err = appendToSql(d.Suffixes, sql, " ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sqlStr, err = d.PlaceholderFormat.ReplacePlaceholders(sql.String())
|
||||
return
|
||||
}
|
||||
|
||||
func (d *insertData) appendValuesToSQL(w io.Writer, args []interface{}) ([]interface{}, error) {
|
||||
if len(d.Values) == 0 {
|
||||
return args, errors.New("values for insert statements are not set")
|
||||
}
|
||||
|
||||
io.WriteString(w, "VALUES ")
|
||||
|
||||
valuesStrings := make([]string, len(d.Values))
|
||||
for r, row := range d.Values {
|
||||
valueStrings := make([]string, len(row))
|
||||
for v, val := range row {
|
||||
if vs, ok := val.(Sqlizer); ok {
|
||||
vsql, vargs, err := vs.ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valueStrings[v] = vsql
|
||||
args = append(args, vargs...)
|
||||
} else {
|
||||
valueStrings[v] = "?"
|
||||
args = append(args, val)
|
||||
}
|
||||
}
|
||||
valuesStrings[r] = fmt.Sprintf("(%s)", strings.Join(valueStrings, ","))
|
||||
}
|
||||
|
||||
io.WriteString(w, strings.Join(valuesStrings, ","))
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (d *insertData) appendSelectToSQL(w io.Writer, args []interface{}) ([]interface{}, error) {
|
||||
if d.Select == nil {
|
||||
return args, errors.New("select clause for insert statements are not set")
|
||||
}
|
||||
|
||||
selectClause, sArgs, err := d.Select.ToSql()
|
||||
if err != nil {
|
||||
return args, err
|
||||
}
|
||||
|
||||
io.WriteString(w, selectClause)
|
||||
args = append(args, sArgs...)
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// Builder
|
||||
|
||||
// InsertBuilder builds SQL INSERT statements.
|
||||
type InsertBuilder builder.Builder
|
||||
|
||||
func init() {
|
||||
builder.Register(InsertBuilder{}, insertData{})
|
||||
}
|
||||
|
||||
// Format methods
|
||||
|
||||
// PlaceholderFormat sets PlaceholderFormat (e.g. Question or Dollar) for the
|
||||
// query.
|
||||
func (b InsertBuilder) PlaceholderFormat(f PlaceholderFormat) InsertBuilder {
|
||||
return builder.Set(b, "PlaceholderFormat", f).(InsertBuilder)
|
||||
}
|
||||
|
||||
// Runner methods
|
||||
|
||||
// RunWith sets a Runner (like database/sql.DB) to be used with e.g. Exec.
|
||||
func (b InsertBuilder) RunWith(runner BaseRunner) InsertBuilder {
|
||||
return setRunWith(b, runner).(InsertBuilder)
|
||||
}
|
||||
|
||||
// Exec builds and Execs the query with the Runner set by RunWith.
|
||||
func (b InsertBuilder) Exec() (sql.Result, error) {
|
||||
data := builder.GetStruct(b).(insertData)
|
||||
return data.Exec()
|
||||
}
|
||||
|
||||
// Query builds and Querys the query with the Runner set by RunWith.
|
||||
func (b InsertBuilder) Query() (*sql.Rows, error) {
|
||||
data := builder.GetStruct(b).(insertData)
|
||||
return data.Query()
|
||||
}
|
||||
|
||||
// QueryRow builds and QueryRows the query with the Runner set by RunWith.
|
||||
func (b InsertBuilder) QueryRow() RowScanner {
|
||||
data := builder.GetStruct(b).(insertData)
|
||||
return data.QueryRow()
|
||||
}
|
||||
|
||||
// Scan is a shortcut for QueryRow().Scan.
|
||||
func (b InsertBuilder) Scan(dest ...interface{}) error {
|
||||
return b.QueryRow().Scan(dest...)
|
||||
}
|
||||
|
||||
// SQL methods
|
||||
|
||||
// ToSql builds the query into a SQL string and bound args.
|
||||
func (b InsertBuilder) ToSql() (string, []interface{}, error) {
|
||||
data := builder.GetStruct(b).(insertData)
|
||||
return data.ToSql()
|
||||
}
|
||||
|
||||
// MustSql builds the query into a SQL string and bound args.
|
||||
// It panics if there are any errors.
|
||||
func (b InsertBuilder) MustSql() (string, []interface{}) {
|
||||
sql, args, err := b.ToSql()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sql, args
|
||||
}
|
||||
|
||||
// Prefix adds an expression to the beginning of the query
|
||||
func (b InsertBuilder) Prefix(sql string, args ...interface{}) InsertBuilder {
|
||||
return b.PrefixExpr(Expr(sql, args...))
|
||||
}
|
||||
|
||||
// PrefixExpr adds an expression to the very beginning of the query
|
||||
func (b InsertBuilder) PrefixExpr(expr Sqlizer) InsertBuilder {
|
||||
return builder.Append(b, "Prefixes", expr).(InsertBuilder)
|
||||
}
|
||||
|
||||
// Options adds keyword options before the INTO clause of the query.
|
||||
func (b InsertBuilder) Options(options ...string) InsertBuilder {
|
||||
return builder.Extend(b, "Options", options).(InsertBuilder)
|
||||
}
|
||||
|
||||
// Into sets the INTO clause of the query.
|
||||
func (b InsertBuilder) Into(from string) InsertBuilder {
|
||||
return builder.Set(b, "Into", from).(InsertBuilder)
|
||||
}
|
||||
|
||||
// Columns adds insert columns to the query.
|
||||
func (b InsertBuilder) Columns(columns ...string) InsertBuilder {
|
||||
return builder.Extend(b, "Columns", columns).(InsertBuilder)
|
||||
}
|
||||
|
||||
// Values adds a single row's values to the query.
|
||||
func (b InsertBuilder) Values(values ...interface{}) InsertBuilder {
|
||||
return builder.Append(b, "Values", values).(InsertBuilder)
|
||||
}
|
||||
|
||||
// Suffix adds an expression to the end of the query
|
||||
func (b InsertBuilder) Suffix(sql string, args ...interface{}) InsertBuilder {
|
||||
return b.SuffixExpr(Expr(sql, args...))
|
||||
}
|
||||
|
||||
// SuffixExpr adds an expression to the end of the query
|
||||
func (b InsertBuilder) SuffixExpr(expr Sqlizer) InsertBuilder {
|
||||
return builder.Append(b, "Suffixes", expr).(InsertBuilder)
|
||||
}
|
||||
|
||||
// SetMap set columns and values for insert builder from a map of column name and value
|
||||
// note that it will reset all previous columns and values was set if any
|
||||
func (b InsertBuilder) SetMap(clauses map[string]interface{}) InsertBuilder {
|
||||
// Keep the columns in a consistent order by sorting the column key string.
|
||||
cols := make([]string, 0, len(clauses))
|
||||
for col := range clauses {
|
||||
cols = append(cols, col)
|
||||
}
|
||||
sort.Strings(cols)
|
||||
|
||||
vals := make([]interface{}, 0, len(clauses))
|
||||
for _, col := range cols {
|
||||
vals = append(vals, clauses[col])
|
||||
}
|
||||
|
||||
b = builder.Set(b, "Columns", cols).(InsertBuilder)
|
||||
b = builder.Set(b, "Values", [][]interface{}{vals}).(InsertBuilder)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Select set Select clause for insert query
|
||||
// If Values and Select are used, then Select has higher priority
|
||||
func (b InsertBuilder) Select(sb SelectBuilder) InsertBuilder {
|
||||
return builder.Set(b, "Select", &sb).(InsertBuilder)
|
||||
}
|
||||
|
||||
func (b InsertBuilder) statementKeyword(keyword string) InsertBuilder {
|
||||
return builder.Set(b, "StatementKeyword", keyword).(InsertBuilder)
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// +build go1.8
|
||||
|
||||
package squirrel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/lann/builder"
|
||||
)
|
||||
|
||||
func (d *insertData) ExecContext(ctx context.Context) (sql.Result, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
ctxRunner, ok := d.RunWith.(ExecerContext)
|
||||
if !ok {
|
||||
return nil, NoContextSupport
|
||||
}
|
||||
return ExecContextWith(ctx, ctxRunner, d)
|
||||
}
|
||||
|
||||
func (d *insertData) QueryContext(ctx context.Context) (*sql.Rows, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
ctxRunner, ok := d.RunWith.(QueryerContext)
|
||||
if !ok {
|
||||
return nil, NoContextSupport
|
||||
}
|
||||
return QueryContextWith(ctx, ctxRunner, d)
|
||||
}
|
||||
|
||||
func (d *insertData) QueryRowContext(ctx context.Context) RowScanner {
|
||||
if d.RunWith == nil {
|
||||
return &Row{err: RunnerNotSet}
|
||||
}
|
||||
queryRower, ok := d.RunWith.(QueryRowerContext)
|
||||
if !ok {
|
||||
if _, ok := d.RunWith.(QueryerContext); !ok {
|
||||
return &Row{err: RunnerNotQueryRunner}
|
||||
}
|
||||
return &Row{err: NoContextSupport}
|
||||
}
|
||||
return QueryRowContextWith(ctx, queryRower, d)
|
||||
}
|
||||
|
||||
// ExecContext builds and ExecContexts the query with the Runner set by RunWith.
|
||||
func (b InsertBuilder) ExecContext(ctx context.Context) (sql.Result, error) {
|
||||
data := builder.GetStruct(b).(insertData)
|
||||
return data.ExecContext(ctx)
|
||||
}
|
||||
|
||||
// QueryContext builds and QueryContexts the query with the Runner set by RunWith.
|
||||
func (b InsertBuilder) QueryContext(ctx context.Context) (*sql.Rows, error) {
|
||||
data := builder.GetStruct(b).(insertData)
|
||||
return data.QueryContext(ctx)
|
||||
}
|
||||
|
||||
// QueryRowContext builds and QueryRowContexts the query with the Runner set by RunWith.
|
||||
func (b InsertBuilder) QueryRowContext(ctx context.Context) RowScanner {
|
||||
data := builder.GetStruct(b).(insertData)
|
||||
return data.QueryRowContext(ctx)
|
||||
}
|
||||
|
||||
// ScanContext is a shortcut for QueryRowContext().Scan.
|
||||
func (b InsertBuilder) ScanContext(ctx context.Context, dest ...interface{}) error {
|
||||
return b.QueryRowContext(ctx).Scan(dest...)
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package squirrel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type part struct {
|
||||
pred interface{}
|
||||
args []interface{}
|
||||
}
|
||||
|
||||
func newPart(pred interface{}, args ...interface{}) Sqlizer {
|
||||
return &part{pred, args}
|
||||
}
|
||||
|
||||
func (p part) ToSql() (sql string, args []interface{}, err error) {
|
||||
switch pred := p.pred.(type) {
|
||||
case nil:
|
||||
// no-op
|
||||
case Sqlizer:
|
||||
sql, args, err = nestedToSql(pred)
|
||||
case string:
|
||||
sql = pred
|
||||
args = p.args
|
||||
default:
|
||||
err = fmt.Errorf("expected string or Sqlizer, not %T", pred)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func nestedToSql(s Sqlizer) (string, []interface{}, error) {
|
||||
if raw, ok := s.(rawSqlizer); ok {
|
||||
return raw.toSqlRaw()
|
||||
} else {
|
||||
return s.ToSql()
|
||||
}
|
||||
}
|
||||
|
||||
func appendToSql(parts []Sqlizer, w io.Writer, sep string, args []interface{}) ([]interface{}, error) {
|
||||
for i, p := range parts {
|
||||
partSql, partArgs, err := nestedToSql(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(partSql) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
_, err := io.WriteString(w, sep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = io.WriteString(w, partSql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, partArgs...)
|
||||
}
|
||||
return args, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package squirrel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PlaceholderFormat is the interface that wraps the ReplacePlaceholders method.
|
||||
//
|
||||
// ReplacePlaceholders takes a SQL statement and replaces each question mark
|
||||
// placeholder with a (possibly different) SQL placeholder.
|
||||
type PlaceholderFormat interface {
|
||||
ReplacePlaceholders(sql string) (string, error)
|
||||
}
|
||||
|
||||
type placeholderDebugger interface {
|
||||
debugPlaceholder() string
|
||||
}
|
||||
|
||||
var (
|
||||
// Question is a PlaceholderFormat instance that leaves placeholders as
|
||||
// question marks.
|
||||
Question = questionFormat{}
|
||||
|
||||
// Dollar is a PlaceholderFormat instance that replaces placeholders with
|
||||
// dollar-prefixed positional placeholders (e.g. $1, $2, $3).
|
||||
Dollar = dollarFormat{}
|
||||
|
||||
// Colon is a PlaceholderFormat instance that replaces placeholders with
|
||||
// colon-prefixed positional placeholders (e.g. :1, :2, :3).
|
||||
Colon = colonFormat{}
|
||||
|
||||
// AtP is a PlaceholderFormat instance that replaces placeholders with
|
||||
// "@p"-prefixed positional placeholders (e.g. @p1, @p2, @p3).
|
||||
AtP = atpFormat{}
|
||||
)
|
||||
|
||||
type questionFormat struct{}
|
||||
|
||||
func (questionFormat) ReplacePlaceholders(sql string) (string, error) {
|
||||
return sql, nil
|
||||
}
|
||||
|
||||
func (questionFormat) debugPlaceholder() string {
|
||||
return "?"
|
||||
}
|
||||
|
||||
type dollarFormat struct{}
|
||||
|
||||
func (dollarFormat) ReplacePlaceholders(sql string) (string, error) {
|
||||
return replacePositionalPlaceholders(sql, "$")
|
||||
}
|
||||
|
||||
func (dollarFormat) debugPlaceholder() string {
|
||||
return "$"
|
||||
}
|
||||
|
||||
type colonFormat struct{}
|
||||
|
||||
func (colonFormat) ReplacePlaceholders(sql string) (string, error) {
|
||||
return replacePositionalPlaceholders(sql, ":")
|
||||
}
|
||||
|
||||
func (colonFormat) debugPlaceholder() string {
|
||||
return ":"
|
||||
}
|
||||
|
||||
type atpFormat struct{}
|
||||
|
||||
func (atpFormat) ReplacePlaceholders(sql string) (string, error) {
|
||||
return replacePositionalPlaceholders(sql, "@p")
|
||||
}
|
||||
|
||||
func (atpFormat) debugPlaceholder() string {
|
||||
return "@p"
|
||||
}
|
||||
|
||||
// Placeholders returns a string with count ? placeholders joined with commas.
|
||||
func Placeholders(count int) string {
|
||||
if count < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.Repeat(",?", count)[1:]
|
||||
}
|
||||
|
||||
func replacePositionalPlaceholders(sql, prefix string) (string, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
i := 0
|
||||
for {
|
||||
p := strings.Index(sql, "?")
|
||||
if p == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
if len(sql[p:]) > 1 && sql[p:p+2] == "??" { // escape ?? => ?
|
||||
buf.WriteString(sql[:p])
|
||||
buf.WriteString("?")
|
||||
if len(sql[p:]) == 1 {
|
||||
break
|
||||
}
|
||||
sql = sql[p+2:]
|
||||
} else {
|
||||
i++
|
||||
buf.WriteString(sql[:p])
|
||||
fmt.Fprintf(buf, "%s%d", prefix, i)
|
||||
sql = sql[p+1:]
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString(sql)
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package squirrel
|
||||
|
||||
// RowScanner is the interface that wraps the Scan method.
|
||||
//
|
||||
// Scan behaves like database/sql.Row.Scan.
|
||||
type RowScanner interface {
|
||||
Scan(...interface{}) error
|
||||
}
|
||||
|
||||
// Row wraps database/sql.Row to let squirrel return new errors on Scan.
|
||||
type Row struct {
|
||||
RowScanner
|
||||
err error
|
||||
}
|
||||
|
||||
// Scan returns Row.err or calls RowScanner.Scan.
|
||||
func (r *Row) Scan(dest ...interface{}) error {
|
||||
if r.err != nil {
|
||||
return r.err
|
||||
}
|
||||
return r.RowScanner.Scan(dest...)
|
||||
}
|
||||
|
|
@ -0,0 +1,403 @@
|
|||
package squirrel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lann/builder"
|
||||
)
|
||||
|
||||
type selectData struct {
|
||||
PlaceholderFormat PlaceholderFormat
|
||||
RunWith BaseRunner
|
||||
Prefixes []Sqlizer
|
||||
Options []string
|
||||
Columns []Sqlizer
|
||||
From Sqlizer
|
||||
Joins []Sqlizer
|
||||
WhereParts []Sqlizer
|
||||
GroupBys []string
|
||||
HavingParts []Sqlizer
|
||||
OrderByParts []Sqlizer
|
||||
Limit string
|
||||
Offset string
|
||||
Suffixes []Sqlizer
|
||||
}
|
||||
|
||||
func (d *selectData) Exec() (sql.Result, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
return ExecWith(d.RunWith, d)
|
||||
}
|
||||
|
||||
func (d *selectData) Query() (*sql.Rows, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
return QueryWith(d.RunWith, d)
|
||||
}
|
||||
|
||||
func (d *selectData) QueryRow() RowScanner {
|
||||
if d.RunWith == nil {
|
||||
return &Row{err: RunnerNotSet}
|
||||
}
|
||||
queryRower, ok := d.RunWith.(QueryRower)
|
||||
if !ok {
|
||||
return &Row{err: RunnerNotQueryRunner}
|
||||
}
|
||||
return QueryRowWith(queryRower, d)
|
||||
}
|
||||
|
||||
func (d *selectData) ToSql() (sqlStr string, args []interface{}, err error) {
|
||||
sqlStr, args, err = d.toSqlRaw()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sqlStr, err = d.PlaceholderFormat.ReplacePlaceholders(sqlStr)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *selectData) toSqlRaw() (sqlStr string, args []interface{}, err error) {
|
||||
if len(d.Columns) == 0 {
|
||||
err = fmt.Errorf("select statements must have at least one result column")
|
||||
return
|
||||
}
|
||||
|
||||
sql := &bytes.Buffer{}
|
||||
|
||||
if len(d.Prefixes) > 0 {
|
||||
args, err = appendToSql(d.Prefixes, sql, " ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sql.WriteString(" ")
|
||||
}
|
||||
|
||||
sql.WriteString("SELECT ")
|
||||
|
||||
if len(d.Options) > 0 {
|
||||
sql.WriteString(strings.Join(d.Options, " "))
|
||||
sql.WriteString(" ")
|
||||
}
|
||||
|
||||
if len(d.Columns) > 0 {
|
||||
args, err = appendToSql(d.Columns, sql, ", ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if d.From != nil {
|
||||
sql.WriteString(" FROM ")
|
||||
args, err = appendToSql([]Sqlizer{d.From}, sql, "", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.Joins) > 0 {
|
||||
sql.WriteString(" ")
|
||||
args, err = appendToSql(d.Joins, sql, " ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.WhereParts) > 0 {
|
||||
sql.WriteString(" WHERE ")
|
||||
args, err = appendToSql(d.WhereParts, sql, " AND ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.GroupBys) > 0 {
|
||||
sql.WriteString(" GROUP BY ")
|
||||
sql.WriteString(strings.Join(d.GroupBys, ", "))
|
||||
}
|
||||
|
||||
if len(d.HavingParts) > 0 {
|
||||
sql.WriteString(" HAVING ")
|
||||
args, err = appendToSql(d.HavingParts, sql, " AND ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.OrderByParts) > 0 {
|
||||
sql.WriteString(" ORDER BY ")
|
||||
args, err = appendToSql(d.OrderByParts, sql, ", ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.Limit) > 0 {
|
||||
sql.WriteString(" LIMIT ")
|
||||
sql.WriteString(d.Limit)
|
||||
}
|
||||
|
||||
if len(d.Offset) > 0 {
|
||||
sql.WriteString(" OFFSET ")
|
||||
sql.WriteString(d.Offset)
|
||||
}
|
||||
|
||||
if len(d.Suffixes) > 0 {
|
||||
sql.WriteString(" ")
|
||||
|
||||
args, err = appendToSql(d.Suffixes, sql, " ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sqlStr = sql.String()
|
||||
return
|
||||
}
|
||||
|
||||
// Builder
|
||||
|
||||
// SelectBuilder builds SQL SELECT statements.
|
||||
type SelectBuilder builder.Builder
|
||||
|
||||
func init() {
|
||||
builder.Register(SelectBuilder{}, selectData{})
|
||||
}
|
||||
|
||||
// Format methods
|
||||
|
||||
// PlaceholderFormat sets PlaceholderFormat (e.g. Question or Dollar) for the
|
||||
// query.
|
||||
func (b SelectBuilder) PlaceholderFormat(f PlaceholderFormat) SelectBuilder {
|
||||
return builder.Set(b, "PlaceholderFormat", f).(SelectBuilder)
|
||||
}
|
||||
|
||||
// Runner methods
|
||||
|
||||
// RunWith sets a Runner (like database/sql.DB) to be used with e.g. Exec.
|
||||
// For most cases runner will be a database connection.
|
||||
//
|
||||
// Internally we use this to mock out the database connection for testing.
|
||||
func (b SelectBuilder) RunWith(runner BaseRunner) SelectBuilder {
|
||||
return setRunWith(b, runner).(SelectBuilder)
|
||||
}
|
||||
|
||||
// Exec builds and Execs the query with the Runner set by RunWith.
|
||||
func (b SelectBuilder) Exec() (sql.Result, error) {
|
||||
data := builder.GetStruct(b).(selectData)
|
||||
return data.Exec()
|
||||
}
|
||||
|
||||
// Query builds and Querys the query with the Runner set by RunWith.
|
||||
func (b SelectBuilder) Query() (*sql.Rows, error) {
|
||||
data := builder.GetStruct(b).(selectData)
|
||||
return data.Query()
|
||||
}
|
||||
|
||||
// QueryRow builds and QueryRows the query with the Runner set by RunWith.
|
||||
func (b SelectBuilder) QueryRow() RowScanner {
|
||||
data := builder.GetStruct(b).(selectData)
|
||||
return data.QueryRow()
|
||||
}
|
||||
|
||||
// Scan is a shortcut for QueryRow().Scan.
|
||||
func (b SelectBuilder) Scan(dest ...interface{}) error {
|
||||
return b.QueryRow().Scan(dest...)
|
||||
}
|
||||
|
||||
// SQL methods
|
||||
|
||||
// ToSql builds the query into a SQL string and bound args.
|
||||
func (b SelectBuilder) ToSql() (string, []interface{}, error) {
|
||||
data := builder.GetStruct(b).(selectData)
|
||||
return data.ToSql()
|
||||
}
|
||||
|
||||
func (b SelectBuilder) toSqlRaw() (string, []interface{}, error) {
|
||||
data := builder.GetStruct(b).(selectData)
|
||||
return data.toSqlRaw()
|
||||
}
|
||||
|
||||
// MustSql builds the query into a SQL string and bound args.
|
||||
// It panics if there are any errors.
|
||||
func (b SelectBuilder) MustSql() (string, []interface{}) {
|
||||
sql, args, err := b.ToSql()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sql, args
|
||||
}
|
||||
|
||||
// Prefix adds an expression to the beginning of the query
|
||||
func (b SelectBuilder) Prefix(sql string, args ...interface{}) SelectBuilder {
|
||||
return b.PrefixExpr(Expr(sql, args...))
|
||||
}
|
||||
|
||||
// PrefixExpr adds an expression to the very beginning of the query
|
||||
func (b SelectBuilder) PrefixExpr(expr Sqlizer) SelectBuilder {
|
||||
return builder.Append(b, "Prefixes", expr).(SelectBuilder)
|
||||
}
|
||||
|
||||
// Distinct adds a DISTINCT clause to the query.
|
||||
func (b SelectBuilder) Distinct() SelectBuilder {
|
||||
return b.Options("DISTINCT")
|
||||
}
|
||||
|
||||
// Options adds select option to the query
|
||||
func (b SelectBuilder) Options(options ...string) SelectBuilder {
|
||||
return builder.Extend(b, "Options", options).(SelectBuilder)
|
||||
}
|
||||
|
||||
// Columns adds result columns to the query.
|
||||
func (b SelectBuilder) Columns(columns ...string) SelectBuilder {
|
||||
parts := make([]interface{}, 0, len(columns))
|
||||
for _, str := range columns {
|
||||
parts = append(parts, newPart(str))
|
||||
}
|
||||
return builder.Extend(b, "Columns", parts).(SelectBuilder)
|
||||
}
|
||||
|
||||
// RemoveColumns remove all columns from query.
|
||||
// Must add a new column with Column or Columns methods, otherwise
|
||||
// return a error.
|
||||
func (b SelectBuilder) RemoveColumns() SelectBuilder {
|
||||
return builder.Delete(b, "Columns").(SelectBuilder)
|
||||
}
|
||||
|
||||
// Column adds a result column to the query.
|
||||
// Unlike Columns, Column accepts args which will be bound to placeholders in
|
||||
// the columns string, for example:
|
||||
// Column("IF(col IN ("+squirrel.Placeholders(3)+"), 1, 0) as col", 1, 2, 3)
|
||||
func (b SelectBuilder) Column(column interface{}, args ...interface{}) SelectBuilder {
|
||||
return builder.Append(b, "Columns", newPart(column, args...)).(SelectBuilder)
|
||||
}
|
||||
|
||||
// From sets the FROM clause of the query.
|
||||
func (b SelectBuilder) From(from string) SelectBuilder {
|
||||
return builder.Set(b, "From", newPart(from)).(SelectBuilder)
|
||||
}
|
||||
|
||||
// FromSelect sets a subquery into the FROM clause of the query.
|
||||
func (b SelectBuilder) FromSelect(from SelectBuilder, alias string) SelectBuilder {
|
||||
// Prevent misnumbered parameters in nested selects (#183).
|
||||
from = from.PlaceholderFormat(Question)
|
||||
return builder.Set(b, "From", Alias(from, alias)).(SelectBuilder)
|
||||
}
|
||||
|
||||
// JoinClause adds a join clause to the query.
|
||||
func (b SelectBuilder) JoinClause(pred interface{}, args ...interface{}) SelectBuilder {
|
||||
return builder.Append(b, "Joins", newPart(pred, args...)).(SelectBuilder)
|
||||
}
|
||||
|
||||
// Join adds a JOIN clause to the query.
|
||||
func (b SelectBuilder) Join(join string, rest ...interface{}) SelectBuilder {
|
||||
return b.JoinClause("JOIN "+join, rest...)
|
||||
}
|
||||
|
||||
// LeftJoin adds a LEFT JOIN clause to the query.
|
||||
func (b SelectBuilder) LeftJoin(join string, rest ...interface{}) SelectBuilder {
|
||||
return b.JoinClause("LEFT JOIN "+join, rest...)
|
||||
}
|
||||
|
||||
// RightJoin adds a RIGHT JOIN clause to the query.
|
||||
func (b SelectBuilder) RightJoin(join string, rest ...interface{}) SelectBuilder {
|
||||
return b.JoinClause("RIGHT JOIN "+join, rest...)
|
||||
}
|
||||
|
||||
// InnerJoin adds a INNER JOIN clause to the query.
|
||||
func (b SelectBuilder) InnerJoin(join string, rest ...interface{}) SelectBuilder {
|
||||
return b.JoinClause("INNER JOIN "+join, rest...)
|
||||
}
|
||||
|
||||
// CrossJoin adds a CROSS JOIN clause to the query.
|
||||
func (b SelectBuilder) CrossJoin(join string, rest ...interface{}) SelectBuilder {
|
||||
return b.JoinClause("CROSS JOIN "+join, rest...)
|
||||
}
|
||||
|
||||
// Where adds an expression to the WHERE clause of the query.
|
||||
//
|
||||
// Expressions are ANDed together in the generated SQL.
|
||||
//
|
||||
// Where accepts several types for its pred argument:
|
||||
//
|
||||
// nil OR "" - ignored.
|
||||
//
|
||||
// string - SQL expression.
|
||||
// If the expression has SQL placeholders then a set of arguments must be passed
|
||||
// as well, one for each placeholder.
|
||||
//
|
||||
// map[string]interface{} OR Eq - map of SQL expressions to values. Each key is
|
||||
// transformed into an expression like "<key> = ?", with the corresponding value
|
||||
// bound to the placeholder. If the value is nil, the expression will be "<key>
|
||||
// IS NULL". If the value is an array or slice, the expression will be "<key> IN
|
||||
// (?,?,...)", with one placeholder for each item in the value. These expressions
|
||||
// are ANDed together.
|
||||
//
|
||||
// Where will panic if pred isn't any of the above types.
|
||||
func (b SelectBuilder) Where(pred interface{}, args ...interface{}) SelectBuilder {
|
||||
if pred == nil || pred == "" {
|
||||
return b
|
||||
}
|
||||
return builder.Append(b, "WhereParts", newWherePart(pred, args...)).(SelectBuilder)
|
||||
}
|
||||
|
||||
// GroupBy adds GROUP BY expressions to the query.
|
||||
func (b SelectBuilder) GroupBy(groupBys ...string) SelectBuilder {
|
||||
return builder.Extend(b, "GroupBys", groupBys).(SelectBuilder)
|
||||
}
|
||||
|
||||
// Having adds an expression to the HAVING clause of the query.
|
||||
//
|
||||
// See Where.
|
||||
func (b SelectBuilder) Having(pred interface{}, rest ...interface{}) SelectBuilder {
|
||||
return builder.Append(b, "HavingParts", newWherePart(pred, rest...)).(SelectBuilder)
|
||||
}
|
||||
|
||||
// OrderByClause adds ORDER BY clause to the query.
|
||||
func (b SelectBuilder) OrderByClause(pred interface{}, args ...interface{}) SelectBuilder {
|
||||
return builder.Append(b, "OrderByParts", newPart(pred, args...)).(SelectBuilder)
|
||||
}
|
||||
|
||||
// OrderBy adds ORDER BY expressions to the query.
|
||||
func (b SelectBuilder) OrderBy(orderBys ...string) SelectBuilder {
|
||||
for _, orderBy := range orderBys {
|
||||
b = b.OrderByClause(orderBy)
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Limit sets a LIMIT clause on the query.
|
||||
func (b SelectBuilder) Limit(limit uint64) SelectBuilder {
|
||||
return builder.Set(b, "Limit", fmt.Sprintf("%d", limit)).(SelectBuilder)
|
||||
}
|
||||
|
||||
// Limit ALL allows to access all records with limit
|
||||
func (b SelectBuilder) RemoveLimit() SelectBuilder {
|
||||
return builder.Delete(b, "Limit").(SelectBuilder)
|
||||
}
|
||||
|
||||
// Offset sets a OFFSET clause on the query.
|
||||
func (b SelectBuilder) Offset(offset uint64) SelectBuilder {
|
||||
return builder.Set(b, "Offset", fmt.Sprintf("%d", offset)).(SelectBuilder)
|
||||
}
|
||||
|
||||
// RemoveOffset removes OFFSET clause.
|
||||
func (b SelectBuilder) RemoveOffset() SelectBuilder {
|
||||
return builder.Delete(b, "Offset").(SelectBuilder)
|
||||
}
|
||||
|
||||
// Suffix adds an expression to the end of the query
|
||||
func (b SelectBuilder) Suffix(sql string, args ...interface{}) SelectBuilder {
|
||||
return b.SuffixExpr(Expr(sql, args...))
|
||||
}
|
||||
|
||||
// SuffixExpr adds an expression to the end of the query
|
||||
func (b SelectBuilder) SuffixExpr(expr Sqlizer) SelectBuilder {
|
||||
return builder.Append(b, "Suffixes", expr).(SelectBuilder)
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// +build go1.8
|
||||
|
||||
package squirrel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/lann/builder"
|
||||
)
|
||||
|
||||
func (d *selectData) ExecContext(ctx context.Context) (sql.Result, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
ctxRunner, ok := d.RunWith.(ExecerContext)
|
||||
if !ok {
|
||||
return nil, NoContextSupport
|
||||
}
|
||||
return ExecContextWith(ctx, ctxRunner, d)
|
||||
}
|
||||
|
||||
func (d *selectData) QueryContext(ctx context.Context) (*sql.Rows, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
ctxRunner, ok := d.RunWith.(QueryerContext)
|
||||
if !ok {
|
||||
return nil, NoContextSupport
|
||||
}
|
||||
return QueryContextWith(ctx, ctxRunner, d)
|
||||
}
|
||||
|
||||
func (d *selectData) QueryRowContext(ctx context.Context) RowScanner {
|
||||
if d.RunWith == nil {
|
||||
return &Row{err: RunnerNotSet}
|
||||
}
|
||||
queryRower, ok := d.RunWith.(QueryRowerContext)
|
||||
if !ok {
|
||||
if _, ok := d.RunWith.(QueryerContext); !ok {
|
||||
return &Row{err: RunnerNotQueryRunner}
|
||||
}
|
||||
return &Row{err: NoContextSupport}
|
||||
}
|
||||
return QueryRowContextWith(ctx, queryRower, d)
|
||||
}
|
||||
|
||||
// ExecContext builds and ExecContexts the query with the Runner set by RunWith.
|
||||
func (b SelectBuilder) ExecContext(ctx context.Context) (sql.Result, error) {
|
||||
data := builder.GetStruct(b).(selectData)
|
||||
return data.ExecContext(ctx)
|
||||
}
|
||||
|
||||
// QueryContext builds and QueryContexts the query with the Runner set by RunWith.
|
||||
func (b SelectBuilder) QueryContext(ctx context.Context) (*sql.Rows, error) {
|
||||
data := builder.GetStruct(b).(selectData)
|
||||
return data.QueryContext(ctx)
|
||||
}
|
||||
|
||||
// QueryRowContext builds and QueryRowContexts the query with the Runner set by RunWith.
|
||||
func (b SelectBuilder) QueryRowContext(ctx context.Context) RowScanner {
|
||||
data := builder.GetStruct(b).(selectData)
|
||||
return data.QueryRowContext(ctx)
|
||||
}
|
||||
|
||||
// ScanContext is a shortcut for QueryRowContext().Scan.
|
||||
func (b SelectBuilder) ScanContext(ctx context.Context, dest ...interface{}) error {
|
||||
return b.QueryRowContext(ctx).Scan(dest...)
|
||||
}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
// Package squirrel provides a fluent SQL generator.
|
||||
//
|
||||
// See https://github.com/Masterminds/squirrel for examples.
|
||||
package squirrel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lann/builder"
|
||||
)
|
||||
|
||||
// Sqlizer is the interface that wraps the ToSql method.
|
||||
//
|
||||
// ToSql returns a SQL representation of the Sqlizer, along with a slice of args
|
||||
// as passed to e.g. database/sql.Exec. It can also return an error.
|
||||
type Sqlizer interface {
|
||||
ToSql() (string, []interface{}, error)
|
||||
}
|
||||
|
||||
// rawSqlizer is expected to do what Sqlizer does, but without finalizing placeholders.
|
||||
// This is useful for nested queries.
|
||||
type rawSqlizer interface {
|
||||
toSqlRaw() (string, []interface{}, error)
|
||||
}
|
||||
|
||||
// Execer is the interface that wraps the Exec method.
|
||||
//
|
||||
// Exec executes the given query as implemented by database/sql.Exec.
|
||||
type Execer interface {
|
||||
Exec(query string, args ...interface{}) (sql.Result, error)
|
||||
}
|
||||
|
||||
// Queryer is the interface that wraps the Query method.
|
||||
//
|
||||
// Query executes the given query as implemented by database/sql.Query.
|
||||
type Queryer interface {
|
||||
Query(query string, args ...interface{}) (*sql.Rows, error)
|
||||
}
|
||||
|
||||
// QueryRower is the interface that wraps the QueryRow method.
|
||||
//
|
||||
// QueryRow executes the given query as implemented by database/sql.QueryRow.
|
||||
type QueryRower interface {
|
||||
QueryRow(query string, args ...interface{}) RowScanner
|
||||
}
|
||||
|
||||
// BaseRunner groups the Execer and Queryer interfaces.
|
||||
type BaseRunner interface {
|
||||
Execer
|
||||
Queryer
|
||||
}
|
||||
|
||||
// Runner groups the Execer, Queryer, and QueryRower interfaces.
|
||||
type Runner interface {
|
||||
Execer
|
||||
Queryer
|
||||
QueryRower
|
||||
}
|
||||
|
||||
// WrapStdSql wraps a type implementing the standard SQL interface with methods that
|
||||
// squirrel expects.
|
||||
func WrapStdSql(stdSql StdSql) Runner {
|
||||
return &stdsqlRunner{stdSql}
|
||||
}
|
||||
|
||||
// StdSql encompasses the standard methods of the *sql.DB type, and other types that
|
||||
// wrap these methods.
|
||||
type StdSql interface {
|
||||
Query(string, ...interface{}) (*sql.Rows, error)
|
||||
QueryRow(string, ...interface{}) *sql.Row
|
||||
Exec(string, ...interface{}) (sql.Result, error)
|
||||
}
|
||||
|
||||
type stdsqlRunner struct {
|
||||
StdSql
|
||||
}
|
||||
|
||||
func (r *stdsqlRunner) QueryRow(query string, args ...interface{}) RowScanner {
|
||||
return r.StdSql.QueryRow(query, args...)
|
||||
}
|
||||
|
||||
func setRunWith(b interface{}, runner BaseRunner) interface{} {
|
||||
switch r := runner.(type) {
|
||||
case StdSqlCtx:
|
||||
runner = WrapStdSqlCtx(r)
|
||||
case StdSql:
|
||||
runner = WrapStdSql(r)
|
||||
}
|
||||
return builder.Set(b, "RunWith", runner)
|
||||
}
|
||||
|
||||
// RunnerNotSet is returned by methods that need a Runner if it isn't set.
|
||||
var RunnerNotSet = fmt.Errorf("cannot run; no Runner set (RunWith)")
|
||||
|
||||
// RunnerNotQueryRunner is returned by QueryRow if the RunWith value doesn't implement QueryRower.
|
||||
var RunnerNotQueryRunner = fmt.Errorf("cannot QueryRow; Runner is not a QueryRower")
|
||||
|
||||
// ExecWith Execs the SQL returned by s with db.
|
||||
func ExecWith(db Execer, s Sqlizer) (res sql.Result, err error) {
|
||||
query, args, err := s.ToSql()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return db.Exec(query, args...)
|
||||
}
|
||||
|
||||
// QueryWith Querys the SQL returned by s with db.
|
||||
func QueryWith(db Queryer, s Sqlizer) (rows *sql.Rows, err error) {
|
||||
query, args, err := s.ToSql()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return db.Query(query, args...)
|
||||
}
|
||||
|
||||
// QueryRowWith QueryRows the SQL returned by s with db.
|
||||
func QueryRowWith(db QueryRower, s Sqlizer) RowScanner {
|
||||
query, args, err := s.ToSql()
|
||||
return &Row{RowScanner: db.QueryRow(query, args...), err: err}
|
||||
}
|
||||
|
||||
// DebugSqlizer calls ToSql on s and shows the approximate SQL to be executed
|
||||
//
|
||||
// If ToSql returns an error, the result of this method will look like:
|
||||
// "[ToSql error: %s]" or "[DebugSqlizer error: %s]"
|
||||
//
|
||||
// IMPORTANT: As its name suggests, this function should only be used for
|
||||
// debugging. While the string result *might* be valid SQL, this function does
|
||||
// not try very hard to ensure it. Additionally, executing the output of this
|
||||
// function with any untrusted user input is certainly insecure.
|
||||
func DebugSqlizer(s Sqlizer) string {
|
||||
sql, args, err := s.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[ToSql error: %s]", err)
|
||||
}
|
||||
|
||||
var placeholder string
|
||||
downCast, ok := s.(placeholderDebugger)
|
||||
if !ok {
|
||||
placeholder = "?"
|
||||
} else {
|
||||
placeholder = downCast.debugPlaceholder()
|
||||
}
|
||||
// TODO: dedupe this with placeholder.go
|
||||
buf := &bytes.Buffer{}
|
||||
i := 0
|
||||
for {
|
||||
p := strings.Index(sql, placeholder)
|
||||
if p == -1 {
|
||||
break
|
||||
}
|
||||
if len(sql[p:]) > 1 && sql[p:p+2] == "??" { // escape ?? => ?
|
||||
buf.WriteString(sql[:p])
|
||||
buf.WriteString("?")
|
||||
if len(sql[p:]) == 1 {
|
||||
break
|
||||
}
|
||||
sql = sql[p+2:]
|
||||
} else {
|
||||
if i+1 > len(args) {
|
||||
return fmt.Sprintf(
|
||||
"[DebugSqlizer error: too many placeholders in %#v for %d args]",
|
||||
sql, len(args))
|
||||
}
|
||||
buf.WriteString(sql[:p])
|
||||
fmt.Fprintf(buf, "'%v'", args[i])
|
||||
// advance our sql string "cursor" beyond the arg we placed
|
||||
sql = sql[p+1:]
|
||||
i++
|
||||
}
|
||||
}
|
||||
if i < len(args) {
|
||||
return fmt.Sprintf(
|
||||
"[DebugSqlizer error: not enough placeholders in %#v for %d args]",
|
||||
sql, len(args))
|
||||
}
|
||||
// "append" any remaning sql that won't need interpolating
|
||||
buf.WriteString(sql)
|
||||
return buf.String()
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
// +build go1.8
|
||||
|
||||
package squirrel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// NoContextSupport is returned if a db doesn't support Context.
|
||||
var NoContextSupport = errors.New("DB does not support Context")
|
||||
|
||||
// ExecerContext is the interface that wraps the ExecContext method.
|
||||
//
|
||||
// Exec executes the given query as implemented by database/sql.ExecContext.
|
||||
type ExecerContext interface {
|
||||
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
|
||||
}
|
||||
|
||||
// QueryerContext is the interface that wraps the QueryContext method.
|
||||
//
|
||||
// QueryContext executes the given query as implemented by database/sql.QueryContext.
|
||||
type QueryerContext interface {
|
||||
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
|
||||
}
|
||||
|
||||
// QueryRowerContext is the interface that wraps the QueryRowContext method.
|
||||
//
|
||||
// QueryRowContext executes the given query as implemented by database/sql.QueryRowContext.
|
||||
type QueryRowerContext interface {
|
||||
QueryRowContext(ctx context.Context, query string, args ...interface{}) RowScanner
|
||||
}
|
||||
|
||||
// RunnerContext groups the Runner interface, along with the Context versions of each of
|
||||
// its methods
|
||||
type RunnerContext interface {
|
||||
Runner
|
||||
QueryerContext
|
||||
QueryRowerContext
|
||||
ExecerContext
|
||||
}
|
||||
|
||||
// WrapStdSqlCtx wraps a type implementing the standard SQL interface plus the context
|
||||
// versions of the methods with methods that squirrel expects.
|
||||
func WrapStdSqlCtx(stdSqlCtx StdSqlCtx) RunnerContext {
|
||||
return &stdsqlCtxRunner{stdSqlCtx}
|
||||
}
|
||||
|
||||
// StdSqlCtx encompasses the standard methods of the *sql.DB type, along with the Context
|
||||
// versions of those methods, and other types that wrap these methods.
|
||||
type StdSqlCtx interface {
|
||||
StdSql
|
||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||
}
|
||||
|
||||
type stdsqlCtxRunner struct {
|
||||
StdSqlCtx
|
||||
}
|
||||
|
||||
func (r *stdsqlCtxRunner) QueryRow(query string, args ...interface{}) RowScanner {
|
||||
return r.StdSqlCtx.QueryRow(query, args...)
|
||||
}
|
||||
|
||||
func (r *stdsqlCtxRunner) QueryRowContext(ctx context.Context, query string, args ...interface{}) RowScanner {
|
||||
return r.StdSqlCtx.QueryRowContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
// ExecContextWith ExecContexts the SQL returned by s with db.
|
||||
func ExecContextWith(ctx context.Context, db ExecerContext, s Sqlizer) (res sql.Result, err error) {
|
||||
query, args, err := s.ToSql()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return db.ExecContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
// QueryContextWith QueryContexts the SQL returned by s with db.
|
||||
func QueryContextWith(ctx context.Context, db QueryerContext, s Sqlizer) (rows *sql.Rows, err error) {
|
||||
query, args, err := s.ToSql()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return db.QueryContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
// QueryRowContextWith QueryRowContexts the SQL returned by s with db.
|
||||
func QueryRowContextWith(ctx context.Context, db QueryRowerContext, s Sqlizer) RowScanner {
|
||||
query, args, err := s.ToSql()
|
||||
return &Row{RowScanner: db.QueryRowContext(ctx, query, args...), err: err}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package squirrel
|
||||
|
||||
import "github.com/lann/builder"
|
||||
|
||||
// StatementBuilderType is the type of StatementBuilder.
|
||||
type StatementBuilderType builder.Builder
|
||||
|
||||
// Select returns a SelectBuilder for this StatementBuilderType.
|
||||
func (b StatementBuilderType) Select(columns ...string) SelectBuilder {
|
||||
return SelectBuilder(b).Columns(columns...)
|
||||
}
|
||||
|
||||
// Insert returns a InsertBuilder for this StatementBuilderType.
|
||||
func (b StatementBuilderType) Insert(into string) InsertBuilder {
|
||||
return InsertBuilder(b).Into(into)
|
||||
}
|
||||
|
||||
// Replace returns a InsertBuilder for this StatementBuilderType with the
|
||||
// statement keyword set to "REPLACE".
|
||||
func (b StatementBuilderType) Replace(into string) InsertBuilder {
|
||||
return InsertBuilder(b).statementKeyword("REPLACE").Into(into)
|
||||
}
|
||||
|
||||
// Update returns a UpdateBuilder for this StatementBuilderType.
|
||||
func (b StatementBuilderType) Update(table string) UpdateBuilder {
|
||||
return UpdateBuilder(b).Table(table)
|
||||
}
|
||||
|
||||
// Delete returns a DeleteBuilder for this StatementBuilderType.
|
||||
func (b StatementBuilderType) Delete(from string) DeleteBuilder {
|
||||
return DeleteBuilder(b).From(from)
|
||||
}
|
||||
|
||||
// PlaceholderFormat sets the PlaceholderFormat field for any child builders.
|
||||
func (b StatementBuilderType) PlaceholderFormat(f PlaceholderFormat) StatementBuilderType {
|
||||
return builder.Set(b, "PlaceholderFormat", f).(StatementBuilderType)
|
||||
}
|
||||
|
||||
// RunWith sets the RunWith field for any child builders.
|
||||
func (b StatementBuilderType) RunWith(runner BaseRunner) StatementBuilderType {
|
||||
return setRunWith(b, runner).(StatementBuilderType)
|
||||
}
|
||||
|
||||
// Where adds WHERE expressions to the query.
|
||||
//
|
||||
// See SelectBuilder.Where for more information.
|
||||
func (b StatementBuilderType) Where(pred interface{}, args ...interface{}) StatementBuilderType {
|
||||
return builder.Append(b, "WhereParts", newWherePart(pred, args...)).(StatementBuilderType)
|
||||
}
|
||||
|
||||
// StatementBuilder is a parent builder for other builders, e.g. SelectBuilder.
|
||||
var StatementBuilder = StatementBuilderType(builder.EmptyBuilder).PlaceholderFormat(Question)
|
||||
|
||||
// Select returns a new SelectBuilder, optionally setting some result columns.
|
||||
//
|
||||
// See SelectBuilder.Columns.
|
||||
func Select(columns ...string) SelectBuilder {
|
||||
return StatementBuilder.Select(columns...)
|
||||
}
|
||||
|
||||
// Insert returns a new InsertBuilder with the given table name.
|
||||
//
|
||||
// See InsertBuilder.Into.
|
||||
func Insert(into string) InsertBuilder {
|
||||
return StatementBuilder.Insert(into)
|
||||
}
|
||||
|
||||
// Replace returns a new InsertBuilder with the statement keyword set to
|
||||
// "REPLACE" and with the given table name.
|
||||
//
|
||||
// See InsertBuilder.Into.
|
||||
func Replace(into string) InsertBuilder {
|
||||
return StatementBuilder.Replace(into)
|
||||
}
|
||||
|
||||
// Update returns a new UpdateBuilder with the given table name.
|
||||
//
|
||||
// See UpdateBuilder.Table.
|
||||
func Update(table string) UpdateBuilder {
|
||||
return StatementBuilder.Update(table)
|
||||
}
|
||||
|
||||
// Delete returns a new DeleteBuilder with the given table name.
|
||||
//
|
||||
// See DeleteBuilder.Table.
|
||||
func Delete(from string) DeleteBuilder {
|
||||
return StatementBuilder.Delete(from)
|
||||
}
|
||||
|
||||
// Case returns a new CaseBuilder
|
||||
// "what" represents case value
|
||||
func Case(what ...interface{}) CaseBuilder {
|
||||
b := CaseBuilder(builder.EmptyBuilder)
|
||||
|
||||
switch len(what) {
|
||||
case 0:
|
||||
case 1:
|
||||
b = b.what(what[0])
|
||||
default:
|
||||
b = b.what(newPart(what[0], what[1:]...))
|
||||
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package squirrel
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Prepareer is the interface that wraps the Prepare method.
|
||||
//
|
||||
// Prepare executes the given query as implemented by database/sql.Prepare.
|
||||
type Preparer interface {
|
||||
Prepare(query string) (*sql.Stmt, error)
|
||||
}
|
||||
|
||||
// DBProxy groups the Execer, Queryer, QueryRower, and Preparer interfaces.
|
||||
type DBProxy interface {
|
||||
Execer
|
||||
Queryer
|
||||
QueryRower
|
||||
Preparer
|
||||
}
|
||||
|
||||
// NOTE: NewStmtCache is defined in stmtcacher_ctx.go (Go >= 1.8) or stmtcacher_noctx.go (Go < 1.8).
|
||||
|
||||
// StmtCache wraps and delegates down to a Preparer type
|
||||
//
|
||||
// It also automatically prepares all statements sent to the underlying Preparer calls
|
||||
// for Exec, Query and QueryRow and caches the returns *sql.Stmt using the provided
|
||||
// query as the key. So that it can be automatically re-used.
|
||||
type StmtCache struct {
|
||||
prep Preparer
|
||||
cache map[string]*sql.Stmt
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Prepare delegates down to the underlying Preparer and caches the result
|
||||
// using the provided query as a key
|
||||
func (sc *StmtCache) Prepare(query string) (*sql.Stmt, error) {
|
||||
sc.mu.Lock()
|
||||
defer sc.mu.Unlock()
|
||||
|
||||
stmt, ok := sc.cache[query]
|
||||
if ok {
|
||||
return stmt, nil
|
||||
}
|
||||
stmt, err := sc.prep.Prepare(query)
|
||||
if err == nil {
|
||||
sc.cache[query] = stmt
|
||||
}
|
||||
return stmt, err
|
||||
}
|
||||
|
||||
// Exec delegates down to the underlying Preparer using a prepared statement
|
||||
func (sc *StmtCache) Exec(query string, args ...interface{}) (res sql.Result, err error) {
|
||||
stmt, err := sc.Prepare(query)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return stmt.Exec(args...)
|
||||
}
|
||||
|
||||
// Query delegates down to the underlying Preparer using a prepared statement
|
||||
func (sc *StmtCache) Query(query string, args ...interface{}) (rows *sql.Rows, err error) {
|
||||
stmt, err := sc.Prepare(query)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return stmt.Query(args...)
|
||||
}
|
||||
|
||||
// QueryRow delegates down to the underlying Preparer using a prepared statement
|
||||
func (sc *StmtCache) QueryRow(query string, args ...interface{}) RowScanner {
|
||||
stmt, err := sc.Prepare(query)
|
||||
if err != nil {
|
||||
return &Row{err: err}
|
||||
}
|
||||
return stmt.QueryRow(args...)
|
||||
}
|
||||
|
||||
// Clear removes and closes all the currently cached prepared statements
|
||||
func (sc *StmtCache) Clear() (err error) {
|
||||
sc.mu.Lock()
|
||||
defer sc.mu.Unlock()
|
||||
|
||||
for key, stmt := range sc.cache {
|
||||
delete(sc.cache, key)
|
||||
|
||||
if stmt == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if cerr := stmt.Close(); cerr != nil {
|
||||
err = cerr
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("one or more Stmt.Close failed; last error: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type DBProxyBeginner interface {
|
||||
DBProxy
|
||||
Begin() (*sql.Tx, error)
|
||||
}
|
||||
|
||||
type stmtCacheProxy struct {
|
||||
DBProxy
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewStmtCacheProxy(db *sql.DB) DBProxyBeginner {
|
||||
return &stmtCacheProxy{DBProxy: NewStmtCache(db), db: db}
|
||||
}
|
||||
|
||||
func (sp *stmtCacheProxy) Begin() (*sql.Tx, error) {
|
||||
return sp.db.Begin()
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
// +build go1.8
|
||||
|
||||
package squirrel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// PrepareerContext is the interface that wraps the Prepare and PrepareContext methods.
|
||||
//
|
||||
// Prepare executes the given query as implemented by database/sql.Prepare.
|
||||
// PrepareContext executes the given query as implemented by database/sql.PrepareContext.
|
||||
type PreparerContext interface {
|
||||
Preparer
|
||||
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
|
||||
}
|
||||
|
||||
// DBProxyContext groups the Execer, Queryer, QueryRower and PreparerContext interfaces.
|
||||
type DBProxyContext interface {
|
||||
Execer
|
||||
Queryer
|
||||
QueryRower
|
||||
PreparerContext
|
||||
}
|
||||
|
||||
// NewStmtCache returns a *StmtCache wrapping a PreparerContext that caches Prepared Stmts.
|
||||
//
|
||||
// Stmts are cached based on the string value of their queries.
|
||||
func NewStmtCache(prep PreparerContext) *StmtCache {
|
||||
return &StmtCache{prep: prep, cache: make(map[string]*sql.Stmt)}
|
||||
}
|
||||
|
||||
// NewStmtCacher is deprecated
|
||||
//
|
||||
// Use NewStmtCache instead
|
||||
func NewStmtCacher(prep PreparerContext) DBProxyContext {
|
||||
return NewStmtCache(prep)
|
||||
}
|
||||
|
||||
// PrepareContext delegates down to the underlying PreparerContext and caches the result
|
||||
// using the provided query as a key
|
||||
func (sc *StmtCache) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
|
||||
ctxPrep, ok := sc.prep.(PreparerContext)
|
||||
if !ok {
|
||||
return nil, NoContextSupport
|
||||
}
|
||||
sc.mu.Lock()
|
||||
defer sc.mu.Unlock()
|
||||
stmt, ok := sc.cache[query]
|
||||
if ok {
|
||||
return stmt, nil
|
||||
}
|
||||
stmt, err := ctxPrep.PrepareContext(ctx, query)
|
||||
if err == nil {
|
||||
sc.cache[query] = stmt
|
||||
}
|
||||
return stmt, err
|
||||
}
|
||||
|
||||
// ExecContext delegates down to the underlying PreparerContext using a prepared statement
|
||||
func (sc *StmtCache) ExecContext(ctx context.Context, query string, args ...interface{}) (res sql.Result, err error) {
|
||||
stmt, err := sc.PrepareContext(ctx, query)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return stmt.ExecContext(ctx, args...)
|
||||
}
|
||||
|
||||
// QueryContext delegates down to the underlying PreparerContext using a prepared statement
|
||||
func (sc *StmtCache) QueryContext(ctx context.Context, query string, args ...interface{}) (rows *sql.Rows, err error) {
|
||||
stmt, err := sc.PrepareContext(ctx, query)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return stmt.QueryContext(ctx, args...)
|
||||
}
|
||||
|
||||
// QueryRowContext delegates down to the underlying PreparerContext using a prepared statement
|
||||
func (sc *StmtCache) QueryRowContext(ctx context.Context, query string, args ...interface{}) RowScanner {
|
||||
stmt, err := sc.PrepareContext(ctx, query)
|
||||
if err != nil {
|
||||
return &Row{err: err}
|
||||
}
|
||||
return stmt.QueryRowContext(ctx, args...)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// +build !go1.8
|
||||
|
||||
package squirrel
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// NewStmtCacher returns a DBProxy wrapping prep that caches Prepared Stmts.
|
||||
//
|
||||
// Stmts are cached based on the string value of their queries.
|
||||
func NewStmtCache(prep Preparer) *StmtCache {
|
||||
return &StmtCacher{prep: prep, cache: make(map[string]*sql.Stmt)}
|
||||
}
|
||||
|
||||
// NewStmtCacher is deprecated
|
||||
//
|
||||
// Use NewStmtCache instead
|
||||
func NewStmtCacher(prep Preparer) DBProxy {
|
||||
return NewStmtCache(prep)
|
||||
}
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
package squirrel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/lann/builder"
|
||||
)
|
||||
|
||||
type updateData struct {
|
||||
PlaceholderFormat PlaceholderFormat
|
||||
RunWith BaseRunner
|
||||
Prefixes []Sqlizer
|
||||
Table string
|
||||
SetClauses []setClause
|
||||
From Sqlizer
|
||||
WhereParts []Sqlizer
|
||||
OrderBys []string
|
||||
Limit string
|
||||
Offset string
|
||||
Suffixes []Sqlizer
|
||||
}
|
||||
|
||||
type setClause struct {
|
||||
column string
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (d *updateData) Exec() (sql.Result, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
return ExecWith(d.RunWith, d)
|
||||
}
|
||||
|
||||
func (d *updateData) Query() (*sql.Rows, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
return QueryWith(d.RunWith, d)
|
||||
}
|
||||
|
||||
func (d *updateData) QueryRow() RowScanner {
|
||||
if d.RunWith == nil {
|
||||
return &Row{err: RunnerNotSet}
|
||||
}
|
||||
queryRower, ok := d.RunWith.(QueryRower)
|
||||
if !ok {
|
||||
return &Row{err: RunnerNotQueryRunner}
|
||||
}
|
||||
return QueryRowWith(queryRower, d)
|
||||
}
|
||||
|
||||
func (d *updateData) ToSql() (sqlStr string, args []interface{}, err error) {
|
||||
if len(d.Table) == 0 {
|
||||
err = fmt.Errorf("update statements must specify a table")
|
||||
return
|
||||
}
|
||||
if len(d.SetClauses) == 0 {
|
||||
err = fmt.Errorf("update statements must have at least one Set clause")
|
||||
return
|
||||
}
|
||||
|
||||
sql := &bytes.Buffer{}
|
||||
|
||||
if len(d.Prefixes) > 0 {
|
||||
args, err = appendToSql(d.Prefixes, sql, " ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sql.WriteString(" ")
|
||||
}
|
||||
|
||||
sql.WriteString("UPDATE ")
|
||||
sql.WriteString(d.Table)
|
||||
|
||||
sql.WriteString(" SET ")
|
||||
setSqls := make([]string, len(d.SetClauses))
|
||||
for i, setClause := range d.SetClauses {
|
||||
var valSql string
|
||||
if vs, ok := setClause.value.(Sqlizer); ok {
|
||||
vsql, vargs, err := vs.ToSql()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if _, ok := vs.(SelectBuilder); ok {
|
||||
valSql = fmt.Sprintf("(%s)", vsql)
|
||||
} else {
|
||||
valSql = vsql
|
||||
}
|
||||
args = append(args, vargs...)
|
||||
} else {
|
||||
valSql = "?"
|
||||
args = append(args, setClause.value)
|
||||
}
|
||||
setSqls[i] = fmt.Sprintf("%s = %s", setClause.column, valSql)
|
||||
}
|
||||
sql.WriteString(strings.Join(setSqls, ", "))
|
||||
|
||||
if d.From != nil {
|
||||
sql.WriteString(" FROM ")
|
||||
args, err = appendToSql([]Sqlizer{d.From}, sql, "", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.WhereParts) > 0 {
|
||||
sql.WriteString(" WHERE ")
|
||||
args, err = appendToSql(d.WhereParts, sql, " AND ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.OrderBys) > 0 {
|
||||
sql.WriteString(" ORDER BY ")
|
||||
sql.WriteString(strings.Join(d.OrderBys, ", "))
|
||||
}
|
||||
|
||||
if len(d.Limit) > 0 {
|
||||
sql.WriteString(" LIMIT ")
|
||||
sql.WriteString(d.Limit)
|
||||
}
|
||||
|
||||
if len(d.Offset) > 0 {
|
||||
sql.WriteString(" OFFSET ")
|
||||
sql.WriteString(d.Offset)
|
||||
}
|
||||
|
||||
if len(d.Suffixes) > 0 {
|
||||
sql.WriteString(" ")
|
||||
args, err = appendToSql(d.Suffixes, sql, " ", args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sqlStr, err = d.PlaceholderFormat.ReplacePlaceholders(sql.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Builder
|
||||
|
||||
// UpdateBuilder builds SQL UPDATE statements.
|
||||
type UpdateBuilder builder.Builder
|
||||
|
||||
func init() {
|
||||
builder.Register(UpdateBuilder{}, updateData{})
|
||||
}
|
||||
|
||||
// Format methods
|
||||
|
||||
// PlaceholderFormat sets PlaceholderFormat (e.g. Question or Dollar) for the
|
||||
// query.
|
||||
func (b UpdateBuilder) PlaceholderFormat(f PlaceholderFormat) UpdateBuilder {
|
||||
return builder.Set(b, "PlaceholderFormat", f).(UpdateBuilder)
|
||||
}
|
||||
|
||||
// Runner methods
|
||||
|
||||
// RunWith sets a Runner (like database/sql.DB) to be used with e.g. Exec.
|
||||
func (b UpdateBuilder) RunWith(runner BaseRunner) UpdateBuilder {
|
||||
return setRunWith(b, runner).(UpdateBuilder)
|
||||
}
|
||||
|
||||
// Exec builds and Execs the query with the Runner set by RunWith.
|
||||
func (b UpdateBuilder) Exec() (sql.Result, error) {
|
||||
data := builder.GetStruct(b).(updateData)
|
||||
return data.Exec()
|
||||
}
|
||||
|
||||
func (b UpdateBuilder) Query() (*sql.Rows, error) {
|
||||
data := builder.GetStruct(b).(updateData)
|
||||
return data.Query()
|
||||
}
|
||||
|
||||
func (b UpdateBuilder) QueryRow() RowScanner {
|
||||
data := builder.GetStruct(b).(updateData)
|
||||
return data.QueryRow()
|
||||
}
|
||||
|
||||
func (b UpdateBuilder) Scan(dest ...interface{}) error {
|
||||
return b.QueryRow().Scan(dest...)
|
||||
}
|
||||
|
||||
// SQL methods
|
||||
|
||||
// ToSql builds the query into a SQL string and bound args.
|
||||
func (b UpdateBuilder) ToSql() (string, []interface{}, error) {
|
||||
data := builder.GetStruct(b).(updateData)
|
||||
return data.ToSql()
|
||||
}
|
||||
|
||||
// MustSql builds the query into a SQL string and bound args.
|
||||
// It panics if there are any errors.
|
||||
func (b UpdateBuilder) MustSql() (string, []interface{}) {
|
||||
sql, args, err := b.ToSql()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sql, args
|
||||
}
|
||||
|
||||
// Prefix adds an expression to the beginning of the query
|
||||
func (b UpdateBuilder) Prefix(sql string, args ...interface{}) UpdateBuilder {
|
||||
return b.PrefixExpr(Expr(sql, args...))
|
||||
}
|
||||
|
||||
// PrefixExpr adds an expression to the very beginning of the query
|
||||
func (b UpdateBuilder) PrefixExpr(expr Sqlizer) UpdateBuilder {
|
||||
return builder.Append(b, "Prefixes", expr).(UpdateBuilder)
|
||||
}
|
||||
|
||||
// Table sets the table to be updated.
|
||||
func (b UpdateBuilder) Table(table string) UpdateBuilder {
|
||||
return builder.Set(b, "Table", table).(UpdateBuilder)
|
||||
}
|
||||
|
||||
// Set adds SET clauses to the query.
|
||||
func (b UpdateBuilder) Set(column string, value interface{}) UpdateBuilder {
|
||||
return builder.Append(b, "SetClauses", setClause{column: column, value: value}).(UpdateBuilder)
|
||||
}
|
||||
|
||||
// SetMap is a convenience method which calls .Set for each key/value pair in clauses.
|
||||
func (b UpdateBuilder) SetMap(clauses map[string]interface{}) UpdateBuilder {
|
||||
keys := make([]string, len(clauses))
|
||||
i := 0
|
||||
for key := range clauses {
|
||||
keys[i] = key
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
val, _ := clauses[key]
|
||||
b = b.Set(key, val)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// From adds FROM clause to the query
|
||||
// FROM is valid construct in postgresql only.
|
||||
func (b UpdateBuilder) From(from string) UpdateBuilder {
|
||||
return builder.Set(b, "From", newPart(from)).(UpdateBuilder)
|
||||
}
|
||||
|
||||
// FromSelect sets a subquery into the FROM clause of the query.
|
||||
func (b UpdateBuilder) FromSelect(from SelectBuilder, alias string) UpdateBuilder {
|
||||
// Prevent misnumbered parameters in nested selects (#183).
|
||||
from = from.PlaceholderFormat(Question)
|
||||
return builder.Set(b, "From", Alias(from, alias)).(UpdateBuilder)
|
||||
}
|
||||
|
||||
// Where adds WHERE expressions to the query.
|
||||
//
|
||||
// See SelectBuilder.Where for more information.
|
||||
func (b UpdateBuilder) Where(pred interface{}, args ...interface{}) UpdateBuilder {
|
||||
return builder.Append(b, "WhereParts", newWherePart(pred, args...)).(UpdateBuilder)
|
||||
}
|
||||
|
||||
// OrderBy adds ORDER BY expressions to the query.
|
||||
func (b UpdateBuilder) OrderBy(orderBys ...string) UpdateBuilder {
|
||||
return builder.Extend(b, "OrderBys", orderBys).(UpdateBuilder)
|
||||
}
|
||||
|
||||
// Limit sets a LIMIT clause on the query.
|
||||
func (b UpdateBuilder) Limit(limit uint64) UpdateBuilder {
|
||||
return builder.Set(b, "Limit", fmt.Sprintf("%d", limit)).(UpdateBuilder)
|
||||
}
|
||||
|
||||
// Offset sets a OFFSET clause on the query.
|
||||
func (b UpdateBuilder) Offset(offset uint64) UpdateBuilder {
|
||||
return builder.Set(b, "Offset", fmt.Sprintf("%d", offset)).(UpdateBuilder)
|
||||
}
|
||||
|
||||
// Suffix adds an expression to the end of the query
|
||||
func (b UpdateBuilder) Suffix(sql string, args ...interface{}) UpdateBuilder {
|
||||
return b.SuffixExpr(Expr(sql, args...))
|
||||
}
|
||||
|
||||
// SuffixExpr adds an expression to the end of the query
|
||||
func (b UpdateBuilder) SuffixExpr(expr Sqlizer) UpdateBuilder {
|
||||
return builder.Append(b, "Suffixes", expr).(UpdateBuilder)
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// +build go1.8
|
||||
|
||||
package squirrel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/lann/builder"
|
||||
)
|
||||
|
||||
func (d *updateData) ExecContext(ctx context.Context) (sql.Result, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
ctxRunner, ok := d.RunWith.(ExecerContext)
|
||||
if !ok {
|
||||
return nil, NoContextSupport
|
||||
}
|
||||
return ExecContextWith(ctx, ctxRunner, d)
|
||||
}
|
||||
|
||||
func (d *updateData) QueryContext(ctx context.Context) (*sql.Rows, error) {
|
||||
if d.RunWith == nil {
|
||||
return nil, RunnerNotSet
|
||||
}
|
||||
ctxRunner, ok := d.RunWith.(QueryerContext)
|
||||
if !ok {
|
||||
return nil, NoContextSupport
|
||||
}
|
||||
return QueryContextWith(ctx, ctxRunner, d)
|
||||
}
|
||||
|
||||
func (d *updateData) QueryRowContext(ctx context.Context) RowScanner {
|
||||
if d.RunWith == nil {
|
||||
return &Row{err: RunnerNotSet}
|
||||
}
|
||||
queryRower, ok := d.RunWith.(QueryRowerContext)
|
||||
if !ok {
|
||||
if _, ok := d.RunWith.(QueryerContext); !ok {
|
||||
return &Row{err: RunnerNotQueryRunner}
|
||||
}
|
||||
return &Row{err: NoContextSupport}
|
||||
}
|
||||
return QueryRowContextWith(ctx, queryRower, d)
|
||||
}
|
||||
|
||||
// ExecContext builds and ExecContexts the query with the Runner set by RunWith.
|
||||
func (b UpdateBuilder) ExecContext(ctx context.Context) (sql.Result, error) {
|
||||
data := builder.GetStruct(b).(updateData)
|
||||
return data.ExecContext(ctx)
|
||||
}
|
||||
|
||||
// QueryContext builds and QueryContexts the query with the Runner set by RunWith.
|
||||
func (b UpdateBuilder) QueryContext(ctx context.Context) (*sql.Rows, error) {
|
||||
data := builder.GetStruct(b).(updateData)
|
||||
return data.QueryContext(ctx)
|
||||
}
|
||||
|
||||
// QueryRowContext builds and QueryRowContexts the query with the Runner set by RunWith.
|
||||
func (b UpdateBuilder) QueryRowContext(ctx context.Context) RowScanner {
|
||||
data := builder.GetStruct(b).(updateData)
|
||||
return data.QueryRowContext(ctx)
|
||||
}
|
||||
|
||||
// ScanContext is a shortcut for QueryRowContext().Scan.
|
||||
func (b UpdateBuilder) ScanContext(ctx context.Context, dest ...interface{}) error {
|
||||
return b.QueryRowContext(ctx).Scan(dest...)
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package squirrel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type wherePart part
|
||||
|
||||
func newWherePart(pred interface{}, args ...interface{}) Sqlizer {
|
||||
return &wherePart{pred: pred, args: args}
|
||||
}
|
||||
|
||||
func (p wherePart) ToSql() (sql string, args []interface{}, err error) {
|
||||
switch pred := p.pred.(type) {
|
||||
case nil:
|
||||
// no-op
|
||||
case rawSqlizer:
|
||||
return pred.toSqlRaw()
|
||||
case Sqlizer:
|
||||
return pred.ToSql()
|
||||
case map[string]interface{}:
|
||||
return Eq(pred).ToSql()
|
||||
case string:
|
||||
sql = pred
|
||||
args = p.args
|
||||
default:
|
||||
err = fmt.Errorf("expected string-keyed map or string, not %T", pred)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
sudo: false
|
||||
language: go
|
||||
install: go get -t -v ./...
|
||||
go:
|
||||
- 1.2.x
|
||||
- 1.3.x
|
||||
- 1.4.x
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (C) 2014 Alec Thomas
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,709 @@
|
|||
# CONTRIBUTIONS ONLY
|
||||
|
||||
**What does this mean?** I do not have time to fix issues myself. The only way fixes or new features will be added is by people submitting PRs. If you are interested in taking over maintenance and have a history of contributions to Kingpin, please let me know.
|
||||
|
||||
**Current status.** Kingpin is largely feature stable. There hasn't been a need to add new features in a while, but there are some bugs that should be fixed.
|
||||
|
||||
**Why?** I no longer use Kingpin personally (I now use [kong](https://github.com/alecthomas/kong)). Rather than leave the project in a limbo of people filing issues and wondering why they're not being worked on, I believe this notice will more clearly set expectations.
|
||||
|
||||
# Kingpin - A Go (golang) command line and flag parser
|
||||
[](http://godoc.org/github.com/alecthomas/kingpin) [](https://github.com/alecthomas/kingpin/actions/workflows/ci.yml)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- MarkdownTOC -->
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Features](#features)
|
||||
- [User-visible changes between v1 and v2](#user-visible-changes-between-v1-and-v2)
|
||||
- [Flags can be used at any point after their definition.](#flags-can-be-used-at-any-point-after-their-definition)
|
||||
- [Short flags can be combined with their parameters](#short-flags-can-be-combined-with-their-parameters)
|
||||
- [API changes between v1 and v2](#api-changes-between-v1-and-v2)
|
||||
- [Versions](#versions)
|
||||
- [V2 is the current stable version](#v2-is-the-current-stable-version)
|
||||
- [V1 is the OLD stable version](#v1-is-the-old-stable-version)
|
||||
- [Change History](#change-history)
|
||||
- [Examples](#examples)
|
||||
- [Simple Example](#simple-example)
|
||||
- [Complex Example](#complex-example)
|
||||
- [Reference Documentation](#reference-documentation)
|
||||
- [Displaying errors and usage information](#displaying-errors-and-usage-information)
|
||||
- [Sub-commands](#sub-commands)
|
||||
- [Custom Parsers](#custom-parsers)
|
||||
- [Repeatable flags](#repeatable-flags)
|
||||
- [Boolean Values](#boolean-values)
|
||||
- [Default Values](#default-values)
|
||||
- [Place-holders in Help](#place-holders-in-help)
|
||||
- [Consuming all remaining arguments](#consuming-all-remaining-arguments)
|
||||
- [Bash/ZSH Shell Completion](#bashzsh-shell-completion)
|
||||
- [Supporting -h for help](#supporting--h-for-help)
|
||||
- [Custom help](#custom-help)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
## Overview
|
||||
|
||||
Kingpin is a [fluent-style](http://en.wikipedia.org/wiki/Fluent_interface),
|
||||
type-safe command-line parser. It supports flags, nested commands, and
|
||||
positional arguments.
|
||||
|
||||
Install it with:
|
||||
|
||||
$ go get github.com/alecthomas/kingpin/v2
|
||||
|
||||
It looks like this:
|
||||
|
||||
```go
|
||||
var (
|
||||
verbose = kingpin.Flag("verbose", "Verbose mode.").Short('v').Bool()
|
||||
name = kingpin.Arg("name", "Name of user.").Required().String()
|
||||
)
|
||||
|
||||
func main() {
|
||||
kingpin.Parse()
|
||||
fmt.Printf("%v, %s\n", *verbose, *name)
|
||||
}
|
||||
```
|
||||
|
||||
More [examples](https://github.com/alecthomas/kingpin/tree/master/_examples) are available.
|
||||
|
||||
Second to parsing, providing the user with useful help is probably the most
|
||||
important thing a command-line parser does. Kingpin tries to provide detailed
|
||||
contextual help if `--help` is encountered at any point in the command line
|
||||
(excluding after `--`).
|
||||
|
||||
## Features
|
||||
|
||||
- Help output that isn't as ugly as sin.
|
||||
- Fully [customisable help](#custom-help), via Go templates.
|
||||
- Parsed, type-safe flags (`kingpin.Flag("f", "help").Int()`)
|
||||
- Parsed, type-safe positional arguments (`kingpin.Arg("a", "help").Int()`).
|
||||
- Parsed, type-safe, arbitrarily deep commands (`kingpin.Command("c", "help")`).
|
||||
- Support for required flags and required positional arguments (`kingpin.Flag("f", "").Required().Int()`).
|
||||
- Support for arbitrarily nested default commands (`command.Default()`).
|
||||
- Callbacks per command, flag and argument (`kingpin.Command("c", "").Action(myAction)`).
|
||||
- POSIX-style short flag combining (`-a -b` -> `-ab`).
|
||||
- Short-flag+parameter combining (`-a parm` -> `-aparm`).
|
||||
- Read command-line from files (`@<file>`).
|
||||
- Automatically generate man pages (`--help-man`).
|
||||
|
||||
## User-visible changes between v1 and v2
|
||||
|
||||
### Flags can be used at any point after their definition.
|
||||
|
||||
Flags can be specified at any point after their definition, not just
|
||||
*immediately after their associated command*. From the chat example below, the
|
||||
following used to be required:
|
||||
|
||||
```
|
||||
$ chat --server=chat.server.com:8080 post --image=~/Downloads/owls.jpg pics
|
||||
```
|
||||
|
||||
But the following will now work:
|
||||
|
||||
```
|
||||
$ chat post --server=chat.server.com:8080 --image=~/Downloads/owls.jpg pics
|
||||
```
|
||||
|
||||
### Short flags can be combined with their parameters
|
||||
|
||||
Previously, if a short flag was used, any argument to that flag would have to
|
||||
be separated by a space. That is no longer the case.
|
||||
|
||||
## API changes between v1 and v2
|
||||
|
||||
- `ParseWithFileExpansion()` is gone. The new parser directly supports expanding `@<file>`.
|
||||
- Added `FatalUsage()` and `FatalUsageContext()` for displaying an error + usage and terminating.
|
||||
- `Dispatch()` renamed to `Action()`.
|
||||
- Added `ParseContext()` for parsing a command line into its intermediate context form without executing.
|
||||
- Added `Terminate()` function to override the termination function.
|
||||
- Added `UsageForContextWithTemplate()` for printing usage via a custom template.
|
||||
- Added `UsageTemplate()` for overriding the default template to use. Two templates are included:
|
||||
1. `DefaultUsageTemplate` - default template.
|
||||
2. `CompactUsageTemplate` - compact command template for larger applications.
|
||||
|
||||
## Versions
|
||||
|
||||
The current stable version is [github.com/alecthomas/kingpin/v2](https://github.com/alecthomas/kingpin/v2). The previous version, [gopkg.in/alecthomas/kingpin.v1](https://gopkg.in/alecthomas/kingpin.v1), is deprecated and in maintenance mode.
|
||||
|
||||
### [V2](https://github.com/alecthomas/kingpin/v2) is the current stable version
|
||||
|
||||
Installation:
|
||||
|
||||
```sh
|
||||
$ go get github.com/alecthomas/kingpin/v2
|
||||
```
|
||||
|
||||
### [V1](https://gopkg.in/alecthomas/kingpin.v1) is the OLD stable version
|
||||
|
||||
Installation:
|
||||
|
||||
```sh
|
||||
$ go get gopkg.in/alecthomas/kingpin.v1
|
||||
```
|
||||
|
||||
## Change History
|
||||
|
||||
- *2015-09-19* -- Stable v2.1.0 release.
|
||||
- Added `command.Default()` to specify a default command to use if no other
|
||||
command matches. This allows for convenient user shortcuts.
|
||||
- Exposed `HelpFlag` and `VersionFlag` for further customisation.
|
||||
- `Action()` and `PreAction()` added and both now support an arbitrary
|
||||
number of callbacks.
|
||||
- `kingpin.SeparateOptionalFlagsUsageTemplate`.
|
||||
- `--help-long` and `--help-man` (hidden by default) flags.
|
||||
- Flags are "interspersed" by default, but can be disabled with `app.Interspersed(false)`.
|
||||
- Added flags for all simple builtin types (int8, uint16, etc.) and slice variants.
|
||||
- Use `app.Writer(os.Writer)` to specify the default writer for all output functions.
|
||||
- Dropped `os.Writer` prefix from all printf-like functions.
|
||||
|
||||
- *2015-05-22* -- Stable v2.0.0 release.
|
||||
- Initial stable release of v2.0.0.
|
||||
- Fully supports interspersed flags, commands and arguments.
|
||||
- Flags can be present at any point after their logical definition.
|
||||
- Application.Parse() terminates if commands are present and a command is not parsed.
|
||||
- Dispatch() -> Action().
|
||||
- Actions are dispatched after all values are populated.
|
||||
- Override termination function (defaults to os.Exit).
|
||||
- Override output stream (defaults to os.Stderr).
|
||||
- Templatised usage help, with default and compact templates.
|
||||
- Make error/usage functions more consistent.
|
||||
- Support argument expansion from files by default (with @<file>).
|
||||
- Fully public data model is available via .Model().
|
||||
- Parser has been completely refactored.
|
||||
- Parsing and execution has been split into distinct stages.
|
||||
- Use `go generate` to generate repeated flags.
|
||||
- Support combined short-flag+argument: -fARG.
|
||||
|
||||
- *2015-01-23* -- Stable v1.3.4 release.
|
||||
- Support "--" for separating flags from positional arguments.
|
||||
- Support loading flags from files (ParseWithFileExpansion()). Use @FILE as an argument.
|
||||
- Add post-app and post-cmd validation hooks. This allows arbitrary validation to be added.
|
||||
- A bunch of improvements to help usage and formatting.
|
||||
- Support arbitrarily nested sub-commands.
|
||||
|
||||
- *2014-07-08* -- Stable v1.2.0 release.
|
||||
- Pass any value through to `Strings()` when final argument.
|
||||
Allows for values that look like flags to be processed.
|
||||
- Allow `--help` to be used with commands.
|
||||
- Support `Hidden()` flags.
|
||||
- Parser for [units.Base2Bytes](https://github.com/alecthomas/units)
|
||||
type. Allows for flags like `--ram=512MB` or `--ram=1GB`.
|
||||
- Add an `Enum()` value, allowing only one of a set of values
|
||||
to be selected. eg. `Flag(...).Enum("debug", "info", "warning")`.
|
||||
|
||||
- *2014-06-27* -- Stable v1.1.0 release.
|
||||
- Bug fixes.
|
||||
- Always return an error (rather than panicing) when misconfigured.
|
||||
- `OpenFile(flag, perm)` value type added, for finer control over opening files.
|
||||
- Significantly improved usage formatting.
|
||||
|
||||
- *2014-06-19* -- Stable v1.0.0 release.
|
||||
- Support [cumulative positional](#consuming-all-remaining-arguments) arguments.
|
||||
- Return error rather than panic when there are fatal errors not caught by
|
||||
the type system. eg. when a default value is invalid.
|
||||
- Use gokpg.in.
|
||||
|
||||
- *2014-06-10* -- Place-holder streamlining.
|
||||
- Renamed `MetaVar` to `PlaceHolder`.
|
||||
- Removed `MetaVarFromDefault`. Kingpin now uses [heuristics](#place-holders-in-help)
|
||||
to determine what to display.
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple Example
|
||||
|
||||
Kingpin can be used for simple flag+arg applications like so:
|
||||
|
||||
```
|
||||
$ ping --help
|
||||
usage: ping [<flags>] <ip> [<count>]
|
||||
|
||||
Flags:
|
||||
--debug Enable debug mode.
|
||||
--help Show help.
|
||||
-t, --timeout=5s Timeout waiting for ping.
|
||||
|
||||
Args:
|
||||
<ip> IP address to ping.
|
||||
[<count>] Number of packets to send
|
||||
$ ping 1.2.3.4 5
|
||||
Would ping: 1.2.3.4 with timeout 5s and count 5
|
||||
```
|
||||
|
||||
From the following source:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
debug = kingpin.Flag("debug", "Enable debug mode.").Bool()
|
||||
timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").Default("5s").Envar("PING_TIMEOUT").Short('t').Duration()
|
||||
ip = kingpin.Arg("ip", "IP address to ping.").Required().IP()
|
||||
count = kingpin.Arg("count", "Number of packets to send").Int()
|
||||
)
|
||||
|
||||
func main() {
|
||||
kingpin.Version("0.0.1")
|
||||
kingpin.Parse()
|
||||
fmt.Printf("Would ping: %s with timeout %s and count %d\n", *ip, *timeout, *count)
|
||||
}
|
||||
```
|
||||
|
||||
#### Reading arguments from a file
|
||||
Kingpin supports reading arguments from a file.
|
||||
Create a file with the corresponding arguments:
|
||||
```
|
||||
echo -t=5\n > args
|
||||
```
|
||||
And now supply it:
|
||||
```
|
||||
$ ping @args
|
||||
```
|
||||
|
||||
### Complex Example
|
||||
|
||||
Kingpin can also produce complex command-line applications with global flags,
|
||||
subcommands, and per-subcommand flags, like this:
|
||||
|
||||
```
|
||||
$ chat --help
|
||||
usage: chat [<flags>] <command> [<flags>] [<args> ...]
|
||||
|
||||
A command-line chat application.
|
||||
|
||||
Flags:
|
||||
--help Show help.
|
||||
--debug Enable debug mode.
|
||||
--server=127.0.0.1 Server address.
|
||||
|
||||
Commands:
|
||||
help [<command>]
|
||||
Show help for a command.
|
||||
|
||||
register <nick> <name>
|
||||
Register a new user.
|
||||
|
||||
post [<flags>] <channel> [<text>]
|
||||
Post a message to a channel.
|
||||
|
||||
$ chat help post
|
||||
usage: chat [<flags>] post [<flags>] <channel> [<text>]
|
||||
|
||||
Post a message to a channel.
|
||||
|
||||
Flags:
|
||||
--image=IMAGE Image to post.
|
||||
|
||||
Args:
|
||||
<channel> Channel to post to.
|
||||
[<text>] Text to post.
|
||||
|
||||
$ chat post --image=~/Downloads/owls.jpg pics
|
||||
...
|
||||
```
|
||||
|
||||
From this code:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
app = kingpin.New("chat", "A command-line chat application.")
|
||||
debug = app.Flag("debug", "Enable debug mode.").Bool()
|
||||
serverIP = app.Flag("server", "Server address.").Default("127.0.0.1").IP()
|
||||
|
||||
register = app.Command("register", "Register a new user.")
|
||||
registerNick = register.Arg("nick", "Nickname for user.").Required().String()
|
||||
registerName = register.Arg("name", "Name of user.").Required().String()
|
||||
|
||||
post = app.Command("post", "Post a message to a channel.")
|
||||
postImage = post.Flag("image", "Image to post.").File()
|
||||
postChannel = post.Arg("channel", "Channel to post to.").Required().String()
|
||||
postText = post.Arg("text", "Text to post.").Strings()
|
||||
)
|
||||
|
||||
func main() {
|
||||
switch kingpin.MustParse(app.Parse(os.Args[1:])) {
|
||||
// Register user
|
||||
case register.FullCommand():
|
||||
println(*registerNick)
|
||||
|
||||
// Post message
|
||||
case post.FullCommand():
|
||||
if *postImage != nil {
|
||||
}
|
||||
text := strings.Join(*postText, " ")
|
||||
println("Post:", text)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
### Displaying errors and usage information
|
||||
|
||||
Kingpin exports a set of functions to provide consistent errors and usage
|
||||
information to the user.
|
||||
|
||||
Error messages look something like this:
|
||||
|
||||
<app>: error: <message>
|
||||
|
||||
The functions on `Application` are:
|
||||
|
||||
Function | Purpose
|
||||
---------|--------------
|
||||
`Errorf(format, args)` | Display a printf formatted error to the user.
|
||||
`Fatalf(format, args)` | As with Errorf, but also call the termination handler.
|
||||
`FatalUsage(format, args)` | As with Fatalf, but also print contextual usage information.
|
||||
`FatalUsageContext(context, format, args)` | As with Fatalf, but also print contextual usage information from a `ParseContext`.
|
||||
`FatalIfError(err, format, args)` | Conditionally print an error prefixed with format+args, then call the termination handler
|
||||
|
||||
There are equivalent global functions in the kingpin namespace for the default
|
||||
`kingpin.CommandLine` instance.
|
||||
|
||||
### Sub-commands
|
||||
|
||||
Kingpin supports nested sub-commands, with separate flag and positional
|
||||
arguments per sub-command. Note that positional arguments may only occur after
|
||||
sub-commands.
|
||||
|
||||
For example:
|
||||
|
||||
```go
|
||||
var (
|
||||
deleteCommand = kingpin.Command("delete", "Delete an object.")
|
||||
deleteUserCommand = deleteCommand.Command("user", "Delete a user.")
|
||||
deleteUserUIDFlag = deleteUserCommand.Flag("uid", "Delete user by UID rather than username.")
|
||||
deleteUserUsername = deleteUserCommand.Arg("username", "Username to delete.")
|
||||
deletePostCommand = deleteCommand.Command("post", "Delete a post.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
switch kingpin.Parse() {
|
||||
case deleteUserCommand.FullCommand():
|
||||
case deletePostCommand.FullCommand():
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Parsers
|
||||
|
||||
Kingpin supports both flag and positional argument parsers for converting to
|
||||
Go types. For example, some included parsers are `Int()`, `Float()`,
|
||||
`Duration()` and `ExistingFile()` (see [parsers.go](./parsers.go) for a complete list of included parsers).
|
||||
|
||||
Parsers conform to Go's [`flag.Value`](http://godoc.org/flag#Value)
|
||||
interface, so any existing implementations will work.
|
||||
|
||||
For example, a parser for accumulating HTTP header values might look like this:
|
||||
|
||||
```go
|
||||
type HTTPHeaderValue http.Header
|
||||
|
||||
func (h *HTTPHeaderValue) Set(value string) error {
|
||||
parts := strings.SplitN(value, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("expected HEADER:VALUE got '%s'", value)
|
||||
}
|
||||
(*http.Header)(h).Add(parts[0], parts[1])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HTTPHeaderValue) String() string {
|
||||
return ""
|
||||
}
|
||||
```
|
||||
|
||||
As a convenience, I would recommend something like this:
|
||||
|
||||
```go
|
||||
func HTTPHeader(s Settings) (target *http.Header) {
|
||||
target = &http.Header{}
|
||||
s.SetValue((*HTTPHeaderValue)(target))
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
You would use it like so:
|
||||
|
||||
```go
|
||||
headers = HTTPHeader(kingpin.Flag("header", "Add a HTTP header to the request.").Short('H'))
|
||||
```
|
||||
|
||||
### Repeatable flags
|
||||
|
||||
Depending on the `Value` they hold, some flags may be repeated. The
|
||||
`IsCumulative() bool` function on `Value` tells if it's safe to call `Set()`
|
||||
multiple times or if an error should be raised if several values are passed.
|
||||
|
||||
The built-in `Value`s returning slices and maps, as well as `Counter` are
|
||||
examples of `Value`s that make a flag repeatable.
|
||||
|
||||
### Boolean values
|
||||
|
||||
Boolean values are uniquely managed by Kingpin. Each boolean flag will have a negative complement:
|
||||
`--<name>` and `--no-<name>`.
|
||||
|
||||
### Default Values
|
||||
|
||||
The default value is the zero value for a type. This can be overridden with
|
||||
the `Default(value...)` function on flags and arguments. This function accepts
|
||||
one or several strings, which are parsed by the value itself, so they *must*
|
||||
be compliant with the format expected.
|
||||
|
||||
### Place-holders in Help
|
||||
|
||||
The place-holder value for a flag is the value used in the help to describe
|
||||
the value of a non-boolean flag.
|
||||
|
||||
The value provided to PlaceHolder() is used if provided, then the value
|
||||
provided by Default() if provided, then finally the capitalised flag name is
|
||||
used.
|
||||
|
||||
Here are some examples of flags with various permutations:
|
||||
|
||||
--name=NAME // Flag(...).String()
|
||||
--name="Harry" // Flag(...).Default("Harry").String()
|
||||
--name=FULL-NAME // Flag(...).PlaceHolder("FULL-NAME").Default("Harry").String()
|
||||
|
||||
### Consuming all remaining arguments
|
||||
|
||||
A common command-line idiom is to use all remaining arguments for some
|
||||
purpose. eg. The following command accepts an arbitrary number of
|
||||
IP addresses as positional arguments:
|
||||
|
||||
./cmd ping 10.1.1.1 192.168.1.1
|
||||
|
||||
Such arguments are similar to [repeatable flags](#repeatable-flags), but for
|
||||
arguments. Therefore they use the same `IsCumulative() bool` function on the
|
||||
underlying `Value`, so the built-in `Value`s for which the `Set()` function
|
||||
can be called several times will consume multiple arguments.
|
||||
|
||||
To implement the above example with a custom `Value`, we might do something
|
||||
like this:
|
||||
|
||||
```go
|
||||
type ipList []net.IP
|
||||
|
||||
func (i *ipList) Set(value string) error {
|
||||
if ip := net.ParseIP(value); ip == nil {
|
||||
return fmt.Errorf("'%s' is not an IP address", value)
|
||||
} else {
|
||||
*i = append(*i, ip)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ipList) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (i *ipList) IsCumulative() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func IPList(s Settings) (target *[]net.IP) {
|
||||
target = new([]net.IP)
|
||||
s.SetValue((*ipList)(target))
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
And use it like so:
|
||||
|
||||
```go
|
||||
ips := IPList(kingpin.Arg("ips", "IP addresses to ping."))
|
||||
```
|
||||
|
||||
### Bash/ZSH Shell Completion
|
||||
|
||||
By default, all flags and commands/subcommands generate completions
|
||||
internally.
|
||||
|
||||
Out of the box, CLI tools using kingpin should be able to take advantage
|
||||
of completion hinting for flags and commands. By specifying
|
||||
`--completion-bash` as the first argument, your CLI tool will show
|
||||
possible subcommands. By ending your argv with `--`, hints for flags
|
||||
will be shown.
|
||||
|
||||
To allow your end users to take advantage you must package a
|
||||
`/etc/bash_completion.d` script with your distribution (or the equivalent
|
||||
for your target platform/shell). An alternative is to instruct your end
|
||||
user to source a script from their `bash_profile` (or equivalent).
|
||||
|
||||
Fortunately Kingpin makes it easy to generate or source a script for use
|
||||
with end users shells. `./yourtool --completion-script-bash` and
|
||||
`./yourtool --completion-script-zsh` will generate these scripts for you.
|
||||
|
||||
**Installation by Package**
|
||||
|
||||
For the best user experience, you should bundle your pre-created
|
||||
completion script with your CLI tool and install it inside
|
||||
`/etc/bash_completion.d` (or equivalent). A good suggestion is to add
|
||||
this as an automated step to your build pipeline, in the implementation
|
||||
is improved for bug fixed.
|
||||
|
||||
**Installation by `bash_profile`**
|
||||
|
||||
Alternatively, instruct your users to add an additional statement to
|
||||
their `bash_profile` (or equivalent):
|
||||
|
||||
```
|
||||
eval "$(your-cli-tool --completion-script-bash)"
|
||||
```
|
||||
|
||||
Or for ZSH
|
||||
|
||||
```
|
||||
eval "$(your-cli-tool --completion-script-zsh)"
|
||||
```
|
||||
|
||||
#### Additional API
|
||||
To provide more flexibility, a completion option API has been
|
||||
exposed for flags to allow user defined completion options, to extend
|
||||
completions further than just EnumVar/Enum.
|
||||
|
||||
|
||||
**Provide Static Options**
|
||||
|
||||
When using an `Enum` or `EnumVar`, users are limited to only the options
|
||||
given. Maybe we wish to hint possible options to the user, but also
|
||||
allow them to provide their own custom option. `HintOptions` gives
|
||||
this functionality to flags.
|
||||
|
||||
```
|
||||
app := kingpin.New("completion", "My application with bash completion.")
|
||||
app.Flag("port", "Provide a port to connect to").
|
||||
Required().
|
||||
HintOptions("80", "443", "8080").
|
||||
IntVar(&c.port)
|
||||
```
|
||||
|
||||
**Provide Dynamic Options**
|
||||
Consider the case that you needed to read a local database or a file to
|
||||
provide suggestions. You can dynamically generate the options
|
||||
|
||||
```
|
||||
func listHosts() []string {
|
||||
// Provide a dynamic list of hosts from a hosts file or otherwise
|
||||
// for bash completion. In this example we simply return static slice.
|
||||
|
||||
// You could use this functionality to reach into a hosts file to provide
|
||||
// completion for a list of known hosts.
|
||||
return []string{"sshhost.example", "webhost.example", "ftphost.example"}
|
||||
}
|
||||
|
||||
app := kingpin.New("completion", "My application with bash completion.")
|
||||
app.Flag("flag-1", "").HintAction(listHosts).String()
|
||||
```
|
||||
|
||||
**EnumVar/Enum**
|
||||
When using `Enum` or `EnumVar`, any provided options will be automatically
|
||||
used for bash autocompletion. However, if you wish to provide a subset or
|
||||
different options, you can use `HintOptions` or `HintAction` which will override
|
||||
the default completion options for `Enum`/`EnumVar`.
|
||||
|
||||
|
||||
**Examples**
|
||||
You can see an in depth example of the completion API within
|
||||
`examples/completion/main.go`
|
||||
|
||||
|
||||
### Supporting -h for help
|
||||
|
||||
`kingpin.CommandLine.HelpFlag.Short('h')`
|
||||
|
||||
Short help is also available when creating a more complicated app:
|
||||
|
||||
```go
|
||||
var (
|
||||
app = kingpin.New("chat", "A command-line chat application.")
|
||||
// ...
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.HelpFlag.Short('h')
|
||||
switch kingpin.MustParse(app.Parse(os.Args[1:])) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom help
|
||||
|
||||
Kingpin v2 supports templatised help using the text/template library (actually, [a fork](https://github.com/alecthomas/template)).
|
||||
|
||||
You can specify the template to use with the [Application.UsageTemplate()](http://godoc.org/github.com/alecthomas/kingpin/v2#Application.UsageTemplate) function.
|
||||
|
||||
There are four included templates: `kingpin.DefaultUsageTemplate` is the default,
|
||||
`kingpin.CompactUsageTemplate` provides a more compact representation for more complex command-line structures,
|
||||
`kingpin.SeparateOptionalFlagsUsageTemplate` looks like the default template, but splits required
|
||||
and optional command flags into separate lists, and `kingpin.ManPageTemplate` is used to generate man pages.
|
||||
|
||||
See the above templates for examples of usage, and the the function [UsageForContextWithTemplate()](https://github.com/alecthomas/kingpin/blob/master/usage.go#L198) method for details on the context.
|
||||
|
||||
#### Default help template
|
||||
|
||||
```
|
||||
$ go run ./examples/curl/curl.go --help
|
||||
usage: curl [<flags>] <command> [<args> ...]
|
||||
|
||||
An example implementation of curl.
|
||||
|
||||
Flags:
|
||||
--help Show help.
|
||||
-t, --timeout=5s Set connection timeout.
|
||||
-H, --headers=HEADER=VALUE
|
||||
Add HTTP headers to the request.
|
||||
|
||||
Commands:
|
||||
help [<command>...]
|
||||
Show help.
|
||||
|
||||
get url <url>
|
||||
Retrieve a URL.
|
||||
|
||||
get file <file>
|
||||
Retrieve a file.
|
||||
|
||||
post [<flags>] <url>
|
||||
POST a resource.
|
||||
```
|
||||
|
||||
#### Compact help template
|
||||
|
||||
```
|
||||
$ go run ./examples/curl/curl.go --help
|
||||
usage: curl [<flags>] <command> [<args> ...]
|
||||
|
||||
An example implementation of curl.
|
||||
|
||||
Flags:
|
||||
--help Show help.
|
||||
-t, --timeout=5s Set connection timeout.
|
||||
-H, --headers=HEADER=VALUE
|
||||
Add HTTP headers to the request.
|
||||
|
||||
Commands:
|
||||
help [<command>...]
|
||||
get [<flags>]
|
||||
url <url>
|
||||
file <file>
|
||||
post [<flags>] <url>
|
||||
```
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package kingpin
|
||||
|
||||
// Action callback executed at various stages after all values are populated.
|
||||
// The application, commands, arguments and flags all have corresponding
|
||||
// actions.
|
||||
type Action func(*ParseContext) error
|
||||
|
||||
type actionMixin struct {
|
||||
actions []Action
|
||||
preActions []Action
|
||||
}
|
||||
|
||||
type actionApplier interface {
|
||||
applyActions(*ParseContext) error
|
||||
applyPreActions(*ParseContext) error
|
||||
}
|
||||
|
||||
func (a *actionMixin) addAction(action Action) {
|
||||
a.actions = append(a.actions, action)
|
||||
}
|
||||
|
||||
func (a *actionMixin) addPreAction(action Action) {
|
||||
a.preActions = append(a.preActions, action)
|
||||
}
|
||||
|
||||
func (a *actionMixin) applyActions(context *ParseContext) error {
|
||||
for _, action := range a.actions {
|
||||
if err := action(context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *actionMixin) applyPreActions(context *ParseContext) error {
|
||||
for _, preAction := range a.preActions {
|
||||
if err := preAction(context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,703 @@
|
|||
package kingpin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCommandNotSpecified = fmt.Errorf("command not specified")
|
||||
)
|
||||
|
||||
var (
|
||||
envarTransformRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]+`)
|
||||
)
|
||||
|
||||
type ApplicationValidator func(*Application) error
|
||||
|
||||
// An Application contains the definitions of flags, arguments and commands
|
||||
// for an application.
|
||||
type Application struct {
|
||||
cmdMixin
|
||||
initialized bool
|
||||
|
||||
Name string
|
||||
Help string
|
||||
|
||||
author string
|
||||
version string
|
||||
errorWriter io.Writer // Destination for errors.
|
||||
usageWriter io.Writer // Destination for usage
|
||||
usageTemplate string
|
||||
usageFuncs template.FuncMap
|
||||
validator ApplicationValidator
|
||||
terminate func(status int) // See Terminate()
|
||||
noInterspersed bool // can flags be interspersed with args (or must they come first)
|
||||
defaultEnvars bool
|
||||
completion bool
|
||||
|
||||
// Help flag. Exposed for user customisation.
|
||||
HelpFlag *FlagClause
|
||||
// Help command. Exposed for user customisation. May be nil.
|
||||
HelpCommand *CmdClause
|
||||
// Version flag. Exposed for user customisation. May be nil.
|
||||
VersionFlag *FlagClause
|
||||
}
|
||||
|
||||
// New creates a new Kingpin application instance.
|
||||
func New(name, help string) *Application {
|
||||
a := &Application{
|
||||
Name: name,
|
||||
Help: help,
|
||||
errorWriter: os.Stderr, // Left for backwards compatibility purposes.
|
||||
usageWriter: os.Stderr,
|
||||
usageTemplate: DefaultUsageTemplate,
|
||||
terminate: os.Exit,
|
||||
}
|
||||
a.flagGroup = newFlagGroup()
|
||||
a.argGroup = newArgGroup()
|
||||
a.cmdGroup = newCmdGroup(a)
|
||||
a.HelpFlag = a.Flag("help", "Show context-sensitive help (also try --help-long and --help-man).")
|
||||
a.HelpFlag.Bool()
|
||||
a.Flag("help-long", "Generate long help.").Hidden().PreAction(a.generateLongHelp).Bool()
|
||||
a.Flag("help-man", "Generate a man page.").Hidden().PreAction(a.generateManPage).Bool()
|
||||
a.Flag("completion-bash", "Output possible completions for the given args.").Hidden().BoolVar(&a.completion)
|
||||
a.Flag("completion-script-bash", "Generate completion script for bash.").Hidden().PreAction(a.generateBashCompletionScript).Bool()
|
||||
a.Flag("completion-script-zsh", "Generate completion script for ZSH.").Hidden().PreAction(a.generateZSHCompletionScript).Bool()
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Application) generateLongHelp(c *ParseContext) error {
|
||||
a.Writer(os.Stdout)
|
||||
if err := a.UsageForContextWithTemplate(c, 2, LongHelpTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
a.terminate(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) generateManPage(c *ParseContext) error {
|
||||
a.Writer(os.Stdout)
|
||||
if err := a.UsageForContextWithTemplate(c, 2, ManPageTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
a.terminate(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) generateBashCompletionScript(c *ParseContext) error {
|
||||
a.Writer(os.Stdout)
|
||||
if err := a.UsageForContextWithTemplate(c, 2, BashCompletionTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
a.terminate(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) generateZSHCompletionScript(c *ParseContext) error {
|
||||
a.Writer(os.Stdout)
|
||||
if err := a.UsageForContextWithTemplate(c, 2, ZshCompletionTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
a.terminate(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultEnvars configures all flags (that do not already have an associated
|
||||
// envar) to use a default environment variable in the form "<app>_<flag>".
|
||||
//
|
||||
// For example, if the application is named "foo" and a flag is named "bar-
|
||||
// waz" the environment variable: "FOO_BAR_WAZ".
|
||||
func (a *Application) DefaultEnvars() *Application {
|
||||
a.defaultEnvars = true
|
||||
return a
|
||||
}
|
||||
|
||||
// Terminate specifies the termination handler. Defaults to os.Exit(status).
|
||||
// If nil is passed, a no-op function will be used.
|
||||
func (a *Application) Terminate(terminate func(int)) *Application {
|
||||
if terminate == nil {
|
||||
terminate = func(int) {}
|
||||
}
|
||||
a.terminate = terminate
|
||||
return a
|
||||
}
|
||||
|
||||
// Writer specifies the writer to use for usage and errors. Defaults to os.Stderr.
|
||||
// DEPRECATED: See ErrorWriter and UsageWriter.
|
||||
func (a *Application) Writer(w io.Writer) *Application {
|
||||
a.errorWriter = w
|
||||
a.usageWriter = w
|
||||
return a
|
||||
}
|
||||
|
||||
// ErrorWriter sets the io.Writer to use for errors.
|
||||
func (a *Application) ErrorWriter(w io.Writer) *Application {
|
||||
a.errorWriter = w
|
||||
return a
|
||||
}
|
||||
|
||||
// UsageWriter sets the io.Writer to use for errors.
|
||||
func (a *Application) UsageWriter(w io.Writer) *Application {
|
||||
a.usageWriter = w
|
||||
return a
|
||||
}
|
||||
|
||||
// UsageTemplate specifies the text template to use when displaying usage
|
||||
// information. The default is UsageTemplate.
|
||||
func (a *Application) UsageTemplate(template string) *Application {
|
||||
a.usageTemplate = template
|
||||
return a
|
||||
}
|
||||
|
||||
// UsageFuncs adds extra functions that can be used in the usage template.
|
||||
func (a *Application) UsageFuncs(funcs template.FuncMap) *Application {
|
||||
a.usageFuncs = funcs
|
||||
return a
|
||||
}
|
||||
|
||||
// Validate sets a validation function to run when parsing.
|
||||
func (a *Application) Validate(validator ApplicationValidator) *Application {
|
||||
a.validator = validator
|
||||
return a
|
||||
}
|
||||
|
||||
// ParseContext parses the given command line and returns the fully populated
|
||||
// ParseContext.
|
||||
func (a *Application) ParseContext(args []string) (*ParseContext, error) {
|
||||
return a.parseContext(false, args)
|
||||
}
|
||||
|
||||
func (a *Application) parseContext(ignoreDefault bool, args []string) (*ParseContext, error) {
|
||||
if err := a.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
context := tokenize(args, ignoreDefault)
|
||||
err := parse(context, a)
|
||||
return context, err
|
||||
}
|
||||
|
||||
// Parse parses command-line arguments. It returns the selected command and an
|
||||
// error. The selected command will be a space separated subcommand, if
|
||||
// subcommands have been configured.
|
||||
//
|
||||
// This will populate all flag and argument values, call all callbacks, and so
|
||||
// on.
|
||||
func (a *Application) Parse(args []string) (command string, err error) {
|
||||
|
||||
context, parseErr := a.ParseContext(args)
|
||||
selected := []string{}
|
||||
var setValuesErr error
|
||||
|
||||
if context == nil {
|
||||
// Since we do not throw error immediately, there could be a case
|
||||
// where a context returns nil. Protect against that.
|
||||
return "", parseErr
|
||||
}
|
||||
|
||||
if err = a.setDefaults(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
selected, setValuesErr = a.setValues(context)
|
||||
|
||||
if err = a.applyPreActions(context, !a.completion); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if a.completion {
|
||||
a.generateBashCompletion(context)
|
||||
a.terminate(0)
|
||||
} else {
|
||||
if parseErr != nil {
|
||||
return "", parseErr
|
||||
}
|
||||
|
||||
a.maybeHelp(context)
|
||||
if !context.EOL() {
|
||||
return "", fmt.Errorf("unexpected argument '%s'", context.Peek())
|
||||
}
|
||||
|
||||
if setValuesErr != nil {
|
||||
return "", setValuesErr
|
||||
}
|
||||
|
||||
command, err = a.execute(context, selected)
|
||||
if err == ErrCommandNotSpecified {
|
||||
a.writeUsage(context, nil)
|
||||
}
|
||||
}
|
||||
return command, err
|
||||
}
|
||||
|
||||
func (a *Application) writeUsage(context *ParseContext, err error) {
|
||||
if err != nil {
|
||||
a.Errorf("%s", err)
|
||||
}
|
||||
if err := a.UsageForContext(context); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err != nil {
|
||||
a.terminate(1)
|
||||
} else {
|
||||
a.terminate(0)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) maybeHelp(context *ParseContext) {
|
||||
for _, element := range context.Elements {
|
||||
if flag, ok := element.Clause.(*FlagClause); ok && flag == a.HelpFlag {
|
||||
// Re-parse the command-line ignoring defaults, so that help works correctly.
|
||||
context, _ = a.parseContext(true, context.rawArgs)
|
||||
a.writeUsage(context, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Version adds a --version flag for displaying the application version.
|
||||
func (a *Application) Version(version string) *Application {
|
||||
a.version = version
|
||||
a.VersionFlag = a.Flag("version", "Show application version.").PreAction(func(*ParseContext) error {
|
||||
fmt.Fprintln(a.usageWriter, version)
|
||||
a.terminate(0)
|
||||
return nil
|
||||
})
|
||||
a.VersionFlag.Bool()
|
||||
return a
|
||||
}
|
||||
|
||||
// Author sets the author output by some help templates.
|
||||
func (a *Application) Author(author string) *Application {
|
||||
a.author = author
|
||||
return a
|
||||
}
|
||||
|
||||
// Action callback to call when all values are populated and parsing is
|
||||
// complete, but before any command, flag or argument actions.
|
||||
//
|
||||
// All Action() callbacks are called in the order they are encountered on the
|
||||
// command line.
|
||||
func (a *Application) Action(action Action) *Application {
|
||||
a.addAction(action)
|
||||
return a
|
||||
}
|
||||
|
||||
// Action called after parsing completes but before validation and execution.
|
||||
func (a *Application) PreAction(action Action) *Application {
|
||||
a.addPreAction(action)
|
||||
return a
|
||||
}
|
||||
|
||||
// Command adds a new top-level command.
|
||||
func (a *Application) Command(name, help string) *CmdClause {
|
||||
return a.addCommand(name, help)
|
||||
}
|
||||
|
||||
// Interspersed control if flags can be interspersed with positional arguments
|
||||
//
|
||||
// true (the default) means that they can, false means that all the flags must appear before the first positional arguments.
|
||||
func (a *Application) Interspersed(interspersed bool) *Application {
|
||||
a.noInterspersed = !interspersed
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Application) defaultEnvarPrefix() string {
|
||||
if a.defaultEnvars {
|
||||
return a.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *Application) init() error {
|
||||
if a.initialized {
|
||||
return nil
|
||||
}
|
||||
if a.cmdGroup.have() && a.argGroup.have() {
|
||||
return fmt.Errorf("can't mix top-level Arg()s with Command()s")
|
||||
}
|
||||
|
||||
// If we have subcommands, add a help command at the top-level.
|
||||
if a.cmdGroup.have() {
|
||||
var command []string
|
||||
a.HelpCommand = a.Command("help", "Show help.").PreAction(func(context *ParseContext) error {
|
||||
a.Usage(command)
|
||||
a.terminate(0)
|
||||
return nil
|
||||
})
|
||||
a.HelpCommand.Arg("command", "Show help on command.").StringsVar(&command)
|
||||
// Make help first command.
|
||||
l := len(a.commandOrder)
|
||||
a.commandOrder = append(a.commandOrder[l-1:l], a.commandOrder[:l-1]...)
|
||||
}
|
||||
|
||||
if err := a.flagGroup.init(a.defaultEnvarPrefix()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.cmdGroup.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.argGroup.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cmd := range a.commands {
|
||||
if err := cmd.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
flagGroups := []*flagGroup{a.flagGroup}
|
||||
for _, cmd := range a.commandOrder {
|
||||
if err := checkDuplicateFlags(cmd, flagGroups); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
a.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recursively check commands for duplicate flags.
|
||||
func checkDuplicateFlags(current *CmdClause, flagGroups []*flagGroup) error {
|
||||
// Check for duplicates.
|
||||
for _, flags := range flagGroups {
|
||||
for _, flag := range current.flagOrder {
|
||||
if flag.shorthand != 0 {
|
||||
if _, ok := flags.short[string(flag.shorthand)]; ok {
|
||||
return fmt.Errorf("duplicate short flag -%c", flag.shorthand)
|
||||
}
|
||||
}
|
||||
if _, ok := flags.long[flag.name]; ok {
|
||||
return fmt.Errorf("duplicate long flag --%s", flag.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
flagGroups = append(flagGroups, current.flagGroup)
|
||||
// Check subcommands.
|
||||
for _, subcmd := range current.commandOrder {
|
||||
if err := checkDuplicateFlags(subcmd, flagGroups); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) execute(context *ParseContext, selected []string) (string, error) {
|
||||
var err error
|
||||
|
||||
if err = a.validateRequired(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = a.applyValidators(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = a.applyActions(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
command := strings.Join(selected, " ")
|
||||
if command == "" && a.cmdGroup.have() {
|
||||
return "", ErrCommandNotSpecified
|
||||
}
|
||||
return command, err
|
||||
}
|
||||
|
||||
func (a *Application) setDefaults(context *ParseContext) error {
|
||||
flagElements := map[string]*ParseElement{}
|
||||
for _, element := range context.Elements {
|
||||
if flag, ok := element.Clause.(*FlagClause); ok {
|
||||
if flag.name == "help" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if flag.name == "version" {
|
||||
return nil
|
||||
}
|
||||
flagElements[flag.name] = element
|
||||
}
|
||||
}
|
||||
|
||||
argElements := map[string]*ParseElement{}
|
||||
for _, element := range context.Elements {
|
||||
if arg, ok := element.Clause.(*ArgClause); ok {
|
||||
argElements[arg.name] = element
|
||||
}
|
||||
}
|
||||
|
||||
// Check required flags and set defaults.
|
||||
for _, flag := range context.flags.long {
|
||||
if flagElements[flag.name] == nil {
|
||||
if err := flag.setDefault(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range context.arguments.args {
|
||||
if argElements[arg.name] == nil {
|
||||
if err := arg.setDefault(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) validateRequired(context *ParseContext) error {
|
||||
flagElements := map[string]*ParseElement{}
|
||||
for _, element := range context.Elements {
|
||||
if flag, ok := element.Clause.(*FlagClause); ok {
|
||||
flagElements[flag.name] = element
|
||||
}
|
||||
}
|
||||
|
||||
argElements := map[string]*ParseElement{}
|
||||
for _, element := range context.Elements {
|
||||
if arg, ok := element.Clause.(*ArgClause); ok {
|
||||
argElements[arg.name] = element
|
||||
}
|
||||
}
|
||||
|
||||
// Check required flags and set defaults.
|
||||
var missingFlags []string
|
||||
for _, flag := range context.flags.long {
|
||||
if flagElements[flag.name] == nil {
|
||||
// Check required flags were provided.
|
||||
if flag.needsValue() {
|
||||
missingFlags = append(missingFlags, fmt.Sprintf("'--%s'", flag.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(missingFlags) != 0 {
|
||||
return fmt.Errorf("required flag(s) %s not provided", strings.Join(missingFlags, ", "))
|
||||
}
|
||||
|
||||
for _, arg := range context.arguments.args {
|
||||
if argElements[arg.name] == nil {
|
||||
if arg.needsValue() {
|
||||
return fmt.Errorf("required argument '%s' not provided", arg.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) setValues(context *ParseContext) (selected []string, err error) {
|
||||
// Set all arg and flag values.
|
||||
var (
|
||||
lastCmd *CmdClause
|
||||
flagSet = map[string]struct{}{}
|
||||
)
|
||||
for _, element := range context.Elements {
|
||||
switch clause := element.Clause.(type) {
|
||||
case *FlagClause:
|
||||
if _, ok := flagSet[clause.name]; ok {
|
||||
if v, ok := clause.value.(repeatableFlag); !ok || !v.IsCumulative() {
|
||||
return nil, fmt.Errorf("flag '%s' cannot be repeated", clause.name)
|
||||
}
|
||||
}
|
||||
if err = clause.value.Set(*element.Value); err != nil {
|
||||
return
|
||||
}
|
||||
flagSet[clause.name] = struct{}{}
|
||||
|
||||
case *ArgClause:
|
||||
if err = clause.value.Set(*element.Value); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case *CmdClause:
|
||||
selected = append(selected, clause.name)
|
||||
lastCmd = clause
|
||||
}
|
||||
}
|
||||
|
||||
if lastCmd != nil && len(lastCmd.commands) > 0 {
|
||||
return nil, fmt.Errorf("must select a subcommand of '%s'", lastCmd.FullCommand())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Application) applyValidators(context *ParseContext) (err error) {
|
||||
// Call command validation functions.
|
||||
for _, element := range context.Elements {
|
||||
if cmd, ok := element.Clause.(*CmdClause); ok && cmd.validator != nil {
|
||||
if err = cmd.validator(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if a.validator != nil {
|
||||
err = a.validator(a)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *Application) applyPreActions(context *ParseContext, dispatch bool) error {
|
||||
if err := a.actionMixin.applyPreActions(context); err != nil {
|
||||
return err
|
||||
}
|
||||
// Dispatch to actions.
|
||||
if dispatch {
|
||||
for _, element := range context.Elements {
|
||||
if applier, ok := element.Clause.(actionApplier); ok {
|
||||
if err := applier.applyPreActions(context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) applyActions(context *ParseContext) error {
|
||||
if err := a.actionMixin.applyActions(context); err != nil {
|
||||
return err
|
||||
}
|
||||
// Dispatch to actions.
|
||||
for _, element := range context.Elements {
|
||||
if applier, ok := element.Clause.(actionApplier); ok {
|
||||
if err := applier.applyActions(context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Errorf prints an error message to w in the format "<appname>: error: <message>".
|
||||
func (a *Application) Errorf(format string, args ...interface{}) {
|
||||
fmt.Fprintf(a.errorWriter, a.Name+": error: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
// Fatalf writes a formatted error to w then terminates with exit status 1.
|
||||
func (a *Application) Fatalf(format string, args ...interface{}) {
|
||||
a.Errorf(format, args...)
|
||||
a.terminate(1)
|
||||
}
|
||||
|
||||
// FatalUsage prints an error message followed by usage information, then
|
||||
// exits with a non-zero status.
|
||||
func (a *Application) FatalUsage(format string, args ...interface{}) {
|
||||
a.Errorf(format, args...)
|
||||
// Force usage to go to error output.
|
||||
a.usageWriter = a.errorWriter
|
||||
a.Usage([]string{})
|
||||
a.terminate(1)
|
||||
}
|
||||
|
||||
// FatalUsageContext writes a printf formatted error message to w, then usage
|
||||
// information for the given ParseContext, before exiting.
|
||||
func (a *Application) FatalUsageContext(context *ParseContext, format string, args ...interface{}) {
|
||||
a.Errorf(format, args...)
|
||||
if err := a.UsageForContext(context); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
a.terminate(1)
|
||||
}
|
||||
|
||||
// FatalIfError prints an error and exits if err is not nil. The error is printed
|
||||
// with the given formatted string, if any.
|
||||
func (a *Application) FatalIfError(err error, format string, args ...interface{}) {
|
||||
if err != nil {
|
||||
prefix := ""
|
||||
if format != "" {
|
||||
prefix = fmt.Sprintf(format, args...) + ": "
|
||||
}
|
||||
a.Errorf(prefix+"%s", err)
|
||||
a.terminate(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) completionOptions(context *ParseContext) []string {
|
||||
args := context.rawArgs
|
||||
|
||||
var (
|
||||
currArg string
|
||||
prevArg string
|
||||
target cmdMixin
|
||||
)
|
||||
|
||||
numArgs := len(args)
|
||||
if numArgs > 1 {
|
||||
args = args[1:]
|
||||
currArg = args[len(args)-1]
|
||||
}
|
||||
if numArgs > 2 {
|
||||
prevArg = args[len(args)-2]
|
||||
}
|
||||
|
||||
target = a.cmdMixin
|
||||
if context.SelectedCommand != nil {
|
||||
// A subcommand was in use. We will use it as the target
|
||||
target = context.SelectedCommand.cmdMixin
|
||||
}
|
||||
|
||||
if (currArg != "" && strings.HasPrefix(currArg, "--")) || strings.HasPrefix(prevArg, "--") {
|
||||
if context.argsOnly {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Perform completion for A flag. The last/current argument started with "-"
|
||||
var (
|
||||
flagName string // The name of a flag if given (could be half complete)
|
||||
flagValue string // The value assigned to a flag (if given) (could be half complete)
|
||||
)
|
||||
|
||||
if strings.HasPrefix(prevArg, "--") && !strings.HasPrefix(currArg, "--") {
|
||||
// Matches: ./myApp --flag value
|
||||
// Wont Match: ./myApp --flag --
|
||||
flagName = prevArg[2:] // Strip the "--"
|
||||
flagValue = currArg
|
||||
} else if strings.HasPrefix(currArg, "--") {
|
||||
// Matches: ./myApp --flag --
|
||||
// Matches: ./myApp --flag somevalue --
|
||||
// Matches: ./myApp --
|
||||
flagName = currArg[2:] // Strip the "--"
|
||||
}
|
||||
|
||||
options, flagMatched, valueMatched := target.FlagCompletion(flagName, flagValue)
|
||||
if valueMatched {
|
||||
// Value Matched. Show cmdCompletions
|
||||
return target.CmdCompletion(context)
|
||||
}
|
||||
|
||||
// Add top level flags if we're not at the top level and no match was found.
|
||||
if context.SelectedCommand != nil && !flagMatched {
|
||||
topOptions, topFlagMatched, topValueMatched := a.FlagCompletion(flagName, flagValue)
|
||||
if topValueMatched {
|
||||
// Value Matched. Back to cmdCompletions
|
||||
return target.CmdCompletion(context)
|
||||
}
|
||||
|
||||
if topFlagMatched {
|
||||
// Top level had a flag which matched the input. Return it's options.
|
||||
options = topOptions
|
||||
} else {
|
||||
// Add top level flags
|
||||
options = append(options, topOptions...)
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// Perform completion for sub commands and arguments.
|
||||
return target.CmdCompletion(context)
|
||||
}
|
||||
|
||||
func (a *Application) generateBashCompletion(context *ParseContext) {
|
||||
options := a.completionOptions(context)
|
||||
fmt.Printf("%s", strings.Join(options, "\n"))
|
||||
}
|
||||
|
||||
func envarTransform(name string) string {
|
||||
return strings.ToUpper(envarTransformRegexp.ReplaceAllString(name, "_"))
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
package kingpin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type argGroup struct {
|
||||
args []*ArgClause
|
||||
}
|
||||
|
||||
func newArgGroup() *argGroup {
|
||||
return &argGroup{}
|
||||
}
|
||||
|
||||
func (a *argGroup) have() bool {
|
||||
return len(a.args) > 0
|
||||
}
|
||||
|
||||
// GetArg gets an argument definition.
|
||||
//
|
||||
// This allows existing arguments to be modified after definition but before parsing. Useful for
|
||||
// modular applications.
|
||||
func (a *argGroup) GetArg(name string) *ArgClause {
|
||||
for _, arg := range a.args {
|
||||
if arg.name == name {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *argGroup) Arg(name, help string) *ArgClause {
|
||||
arg := newArg(name, help)
|
||||
a.args = append(a.args, arg)
|
||||
return arg
|
||||
}
|
||||
|
||||
func (a *argGroup) init() error {
|
||||
required := 0
|
||||
seen := map[string]struct{}{}
|
||||
previousArgMustBeLast := false
|
||||
for i, arg := range a.args {
|
||||
if previousArgMustBeLast {
|
||||
return fmt.Errorf("Args() can't be followed by another argument '%s'", arg.name)
|
||||
}
|
||||
if arg.consumesRemainder() {
|
||||
previousArgMustBeLast = true
|
||||
}
|
||||
if _, ok := seen[arg.name]; ok {
|
||||
return fmt.Errorf("duplicate argument '%s'", arg.name)
|
||||
}
|
||||
seen[arg.name] = struct{}{}
|
||||
if arg.required && required != i {
|
||||
return fmt.Errorf("required arguments found after non-required")
|
||||
}
|
||||
if arg.required {
|
||||
required++
|
||||
}
|
||||
if err := arg.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ArgClause struct {
|
||||
actionMixin
|
||||
parserMixin
|
||||
completionsMixin
|
||||
envarMixin
|
||||
name string
|
||||
help string
|
||||
defaultValues []string
|
||||
placeholder string
|
||||
hidden bool
|
||||
required bool
|
||||
}
|
||||
|
||||
func newArg(name, help string) *ArgClause {
|
||||
a := &ArgClause{
|
||||
name: name,
|
||||
help: help,
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *ArgClause) setDefault() error {
|
||||
if a.HasEnvarValue() {
|
||||
if v, ok := a.value.(remainderArg); !ok || !v.IsCumulative() {
|
||||
// Use the value as-is
|
||||
return a.value.Set(a.GetEnvarValue())
|
||||
}
|
||||
for _, value := range a.GetSplitEnvarValue() {
|
||||
if err := a.value.Set(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a.defaultValues) > 0 {
|
||||
for _, defaultValue := range a.defaultValues {
|
||||
if err := a.value.Set(defaultValue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ArgClause) needsValue() bool {
|
||||
haveDefault := len(a.defaultValues) > 0
|
||||
return a.required && !(haveDefault || a.HasEnvarValue())
|
||||
}
|
||||
|
||||
func (a *ArgClause) consumesRemainder() bool {
|
||||
if r, ok := a.value.(remainderArg); ok {
|
||||
return r.IsCumulative()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Hidden hides the argument from usage but still allows it to be used.
|
||||
func (a *ArgClause) Hidden() *ArgClause {
|
||||
a.hidden = true
|
||||
return a
|
||||
}
|
||||
|
||||
// PlaceHolder sets the place-holder string used for arg values in the help. The
|
||||
// default behaviour is to use the arg name between < > brackets.
|
||||
func (a *ArgClause) PlaceHolder(value string) *ArgClause {
|
||||
a.placeholder = value
|
||||
return a
|
||||
}
|
||||
|
||||
// Required arguments must be input by the user. They can not have a Default() value provided.
|
||||
func (a *ArgClause) Required() *ArgClause {
|
||||
a.required = true
|
||||
return a
|
||||
}
|
||||
|
||||
// Default values for this argument. They *must* be parseable by the value of the argument.
|
||||
func (a *ArgClause) Default(values ...string) *ArgClause {
|
||||
a.defaultValues = values
|
||||
return a
|
||||
}
|
||||
|
||||
// Envar overrides the default value(s) for a flag from an environment variable,
|
||||
// if it is set. Several default values can be provided by using new lines to
|
||||
// separate them.
|
||||
func (a *ArgClause) Envar(name string) *ArgClause {
|
||||
a.envar = name
|
||||
a.noEnvar = false
|
||||
return a
|
||||
}
|
||||
|
||||
// NoEnvar forces environment variable defaults to be disabled for this flag.
|
||||
// Most useful in conjunction with app.DefaultEnvars().
|
||||
func (a *ArgClause) NoEnvar() *ArgClause {
|
||||
a.envar = ""
|
||||
a.noEnvar = true
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *ArgClause) Action(action Action) *ArgClause {
|
||||
a.addAction(action)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *ArgClause) PreAction(action Action) *ArgClause {
|
||||
a.addPreAction(action)
|
||||
return a
|
||||
}
|
||||
|
||||
// HintAction registers a HintAction (function) for the arg to provide completions
|
||||
func (a *ArgClause) HintAction(action HintAction) *ArgClause {
|
||||
a.addHintAction(action)
|
||||
return a
|
||||
}
|
||||
|
||||
// HintOptions registers any number of options for the flag to provide completions
|
||||
func (a *ArgClause) HintOptions(options ...string) *ArgClause {
|
||||
a.addHintAction(func() []string {
|
||||
return options
|
||||
})
|
||||
return a
|
||||
}
|
||||
|
||||
// Help sets the help message.
|
||||
func (a *ArgClause) Help(help string) *ArgClause {
|
||||
a.help = help
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *ArgClause) init() error {
|
||||
if a.required && len(a.defaultValues) > 0 {
|
||||
return fmt.Errorf("required argument '%s' with unusable default value", a.name)
|
||||
}
|
||||
if a.value == nil {
|
||||
return fmt.Errorf("no parser defined for arg '%s'", a.name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
package kingpin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type cmdMixin struct {
|
||||
*flagGroup
|
||||
*argGroup
|
||||
*cmdGroup
|
||||
actionMixin
|
||||
}
|
||||
|
||||
// CmdCompletion returns completion options for arguments, if that's where
|
||||
// parsing left off, or commands if there aren't any unsatisfied args.
|
||||
func (c *cmdMixin) CmdCompletion(context *ParseContext) []string {
|
||||
var options []string
|
||||
|
||||
// Count args already satisfied - we won't complete those, and add any
|
||||
// default commands' alternatives, since they weren't listed explicitly
|
||||
// and the user may want to explicitly list something else.
|
||||
argsSatisfied := 0
|
||||
allSatisfied := false
|
||||
ElementLoop:
|
||||
for _, el := range context.Elements {
|
||||
switch clause := el.Clause.(type) {
|
||||
case *ArgClause:
|
||||
// Each new element should reset the previous state
|
||||
allSatisfied = false
|
||||
options = nil
|
||||
|
||||
if el.Value != nil && *el.Value != "" {
|
||||
// Get the list of valid options for the last argument
|
||||
validOptions := c.argGroup.args[argsSatisfied].resolveCompletions()
|
||||
if len(validOptions) == 0 {
|
||||
// If there are no options for this argument,
|
||||
// mark is as allSatisfied as we can't suggest anything
|
||||
if !clause.consumesRemainder() {
|
||||
argsSatisfied++
|
||||
allSatisfied = true
|
||||
}
|
||||
continue ElementLoop
|
||||
}
|
||||
|
||||
for _, opt := range validOptions {
|
||||
if opt == *el.Value {
|
||||
// We have an exact match
|
||||
// We don't need to suggest any option
|
||||
if !clause.consumesRemainder() {
|
||||
argsSatisfied++
|
||||
}
|
||||
continue ElementLoop
|
||||
}
|
||||
if strings.HasPrefix(opt, *el.Value) {
|
||||
// If the option match the partially entered argument, add it to the list
|
||||
options = append(options, opt)
|
||||
}
|
||||
}
|
||||
// Avoid further completion as we have done everything we could
|
||||
if !clause.consumesRemainder() {
|
||||
argsSatisfied++
|
||||
allSatisfied = true
|
||||
}
|
||||
}
|
||||
case *CmdClause:
|
||||
options = append(options, clause.completionAlts...)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if argsSatisfied < len(c.argGroup.args) && !allSatisfied {
|
||||
// Since not all args have been satisfied, show options for the current one
|
||||
options = append(options, c.argGroup.args[argsSatisfied].resolveCompletions()...)
|
||||
} else {
|
||||
// If all args are satisfied, then go back to completing commands
|
||||
for _, cmd := range c.cmdGroup.commandOrder {
|
||||
if !cmd.hidden {
|
||||
options = append(options, cmd.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func (c *cmdMixin) FlagCompletion(flagName string, flagValue string) (choices []string, flagMatch bool, optionMatch bool) {
|
||||
// Check if flagName matches a known flag.
|
||||
// If it does, show the options for the flag
|
||||
// Otherwise, show all flags
|
||||
|
||||
options := []string{}
|
||||
|
||||
for _, flag := range c.flagGroup.flagOrder {
|
||||
// Loop through each flag and determine if a match exists
|
||||
if flag.name == flagName {
|
||||
// User typed entire flag. Need to look for flag options.
|
||||
options = flag.resolveCompletions()
|
||||
if len(options) == 0 {
|
||||
// No Options to Choose From, Assume Match.
|
||||
return options, true, true
|
||||
}
|
||||
|
||||
// Loop options to find if the user specified value matches
|
||||
isPrefix := false
|
||||
matched := false
|
||||
|
||||
for _, opt := range options {
|
||||
if flagValue == opt {
|
||||
matched = true
|
||||
} else if strings.HasPrefix(opt, flagValue) {
|
||||
isPrefix = true
|
||||
}
|
||||
}
|
||||
|
||||
// Matched Flag Directly
|
||||
// Flag Value Not Prefixed, and Matched Directly
|
||||
return options, true, !isPrefix && matched
|
||||
}
|
||||
|
||||
if !flag.hidden {
|
||||
options = append(options, "--"+flag.name)
|
||||
}
|
||||
}
|
||||
// No Flag directly matched.
|
||||
return options, false, false
|
||||
|
||||
}
|
||||
|
||||
type cmdGroup struct {
|
||||
app *Application
|
||||
parent *CmdClause
|
||||
commands map[string]*CmdClause
|
||||
commandOrder []*CmdClause
|
||||
}
|
||||
|
||||
func (c *cmdGroup) defaultSubcommand() *CmdClause {
|
||||
for _, cmd := range c.commandOrder {
|
||||
if cmd.isDefault {
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cmdGroup) cmdNames() []string {
|
||||
names := make([]string, 0, len(c.commandOrder))
|
||||
for _, cmd := range c.commandOrder {
|
||||
names = append(names, cmd.name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// GetArg gets a command definition.
|
||||
//
|
||||
// This allows existing commands to be modified after definition but before parsing. Useful for
|
||||
// modular applications.
|
||||
func (c *cmdGroup) GetCommand(name string) *CmdClause {
|
||||
return c.commands[name]
|
||||
}
|
||||
|
||||
func newCmdGroup(app *Application) *cmdGroup {
|
||||
return &cmdGroup{
|
||||
app: app,
|
||||
commands: make(map[string]*CmdClause),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cmdGroup) flattenedCommands() (out []*CmdClause) {
|
||||
for _, cmd := range c.commandOrder {
|
||||
if len(cmd.commands) == 0 {
|
||||
out = append(out, cmd)
|
||||
}
|
||||
out = append(out, cmd.flattenedCommands()...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cmdGroup) addCommand(name, help string) *CmdClause {
|
||||
cmd := newCommand(c.app, name, help)
|
||||
c.commands[name] = cmd
|
||||
c.commandOrder = append(c.commandOrder, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *cmdGroup) init() error {
|
||||
seen := map[string]bool{}
|
||||
if c.defaultSubcommand() != nil && !c.have() {
|
||||
return fmt.Errorf("default subcommand %q provided but no subcommands defined", c.defaultSubcommand().name)
|
||||
}
|
||||
defaults := []string{}
|
||||
for _, cmd := range c.commandOrder {
|
||||
if cmd.isDefault {
|
||||
defaults = append(defaults, cmd.name)
|
||||
}
|
||||
if seen[cmd.name] {
|
||||
return fmt.Errorf("duplicate command %q", cmd.name)
|
||||
}
|
||||
seen[cmd.name] = true
|
||||
for _, alias := range cmd.aliases {
|
||||
if seen[alias] {
|
||||
return fmt.Errorf("alias duplicates existing command %q", alias)
|
||||
}
|
||||
c.commands[alias] = cmd
|
||||
}
|
||||
if err := cmd.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(defaults) > 1 {
|
||||
return fmt.Errorf("more than one default subcommand exists: %s", strings.Join(defaults, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cmdGroup) have() bool {
|
||||
return len(c.commands) > 0
|
||||
}
|
||||
|
||||
type CmdClauseValidator func(*CmdClause) error
|
||||
|
||||
// A CmdClause is a single top-level command. It encapsulates a set of flags
|
||||
// and either subcommands or positional arguments.
|
||||
type CmdClause struct {
|
||||
cmdMixin
|
||||
app *Application
|
||||
name string
|
||||
aliases []string
|
||||
help string
|
||||
helpLong string
|
||||
isDefault bool
|
||||
validator CmdClauseValidator
|
||||
hidden bool
|
||||
completionAlts []string
|
||||
}
|
||||
|
||||
func newCommand(app *Application, name, help string) *CmdClause {
|
||||
c := &CmdClause{
|
||||
app: app,
|
||||
name: name,
|
||||
help: help,
|
||||
}
|
||||
c.flagGroup = newFlagGroup()
|
||||
c.argGroup = newArgGroup()
|
||||
c.cmdGroup = newCmdGroup(app)
|
||||
return c
|
||||
}
|
||||
|
||||
// Add an Alias for this command.
|
||||
func (c *CmdClause) Alias(name string) *CmdClause {
|
||||
c.aliases = append(c.aliases, name)
|
||||
return c
|
||||
}
|
||||
|
||||
// Validate sets a validation function to run when parsing.
|
||||
func (c *CmdClause) Validate(validator CmdClauseValidator) *CmdClause {
|
||||
c.validator = validator
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *CmdClause) FullCommand() string {
|
||||
out := []string{c.name}
|
||||
for p := c.parent; p != nil; p = p.parent {
|
||||
out = append([]string{p.name}, out...)
|
||||
}
|
||||
return strings.Join(out, " ")
|
||||
}
|
||||
|
||||
// Command adds a new sub-command.
|
||||
func (c *CmdClause) Command(name, help string) *CmdClause {
|
||||
cmd := c.addCommand(name, help)
|
||||
cmd.parent = c
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Default makes this command the default if commands don't match.
|
||||
func (c *CmdClause) Default() *CmdClause {
|
||||
c.isDefault = true
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *CmdClause) Action(action Action) *CmdClause {
|
||||
c.addAction(action)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *CmdClause) PreAction(action Action) *CmdClause {
|
||||
c.addPreAction(action)
|
||||
return c
|
||||
}
|
||||
|
||||
// Help sets the help message.
|
||||
func (c *CmdClause) Help(help string) *CmdClause {
|
||||
c.help = help
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *CmdClause) init() error {
|
||||
if err := c.flagGroup.init(c.app.defaultEnvarPrefix()); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.argGroup.have() && c.cmdGroup.have() {
|
||||
return fmt.Errorf("can't mix Arg()s with Command()s")
|
||||
}
|
||||
if err := c.argGroup.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.cmdGroup.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CmdClause) Hidden() *CmdClause {
|
||||
c.hidden = true
|
||||
return c
|
||||
}
|
||||
|
||||
// HelpLong adds a long help text, which can be used in usage templates.
|
||||
// For example, to use a longer help text in the command-specific help
|
||||
// than in the apps root help.
|
||||
func (c *CmdClause) HelpLong(help string) *CmdClause {
|
||||
c.helpLong = help
|
||||
return c
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package kingpin
|
||||
|
||||
// HintAction is a function type who is expected to return a slice of possible
|
||||
// command line arguments.
|
||||
type HintAction func() []string
|
||||
type completionsMixin struct {
|
||||
hintActions []HintAction
|
||||
builtinHintActions []HintAction
|
||||
}
|
||||
|
||||
func (a *completionsMixin) addHintAction(action HintAction) {
|
||||
a.hintActions = append(a.hintActions, action)
|
||||
}
|
||||
|
||||
// Allow adding of HintActions which are added internally, ie, EnumVar
|
||||
func (a *completionsMixin) addHintActionBuiltin(action HintAction) {
|
||||
a.builtinHintActions = append(a.builtinHintActions, action)
|
||||
}
|
||||
|
||||
func (a *completionsMixin) resolveCompletions() []string {
|
||||
var hints []string
|
||||
|
||||
options := a.builtinHintActions
|
||||
if len(a.hintActions) > 0 {
|
||||
// User specified their own hintActions. Use those instead.
|
||||
options = a.hintActions
|
||||
}
|
||||
|
||||
for _, hintAction := range options {
|
||||
hints = append(hints, hintAction()...)
|
||||
}
|
||||
return hints
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// Package kingpin provides command line interfaces like this:
|
||||
//
|
||||
// $ chat
|
||||
// usage: chat [<flags>] <command> [<flags>] [<args> ...]
|
||||
//
|
||||
// Flags:
|
||||
// --debug enable debug mode
|
||||
// --help Show help.
|
||||
// --server=127.0.0.1 server address
|
||||
//
|
||||
// Commands:
|
||||
// help <command>
|
||||
// Show help for a command.
|
||||
//
|
||||
// post [<flags>] <channel>
|
||||
// Post a message to a channel.
|
||||
//
|
||||
// register <nick> <name>
|
||||
// Register a new user.
|
||||
//
|
||||
// $ chat help post
|
||||
// usage: chat [<flags>] post [<flags>] <channel> [<text>]
|
||||
//
|
||||
// Post a message to a channel.
|
||||
//
|
||||
// Flags:
|
||||
// --image=IMAGE image to post
|
||||
//
|
||||
// Args:
|
||||
// <channel> channel to post to
|
||||
// [<text>] text to post
|
||||
// $ chat post --image=~/Downloads/owls.jpg pics
|
||||
//
|
||||
// From code like this:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import "github.com/alecthomas/kingpin/v2"
|
||||
//
|
||||
// var (
|
||||
// debug = kingpin.Flag("debug", "enable debug mode").Default("false").Bool()
|
||||
// serverIP = kingpin.Flag("server", "server address").Default("127.0.0.1").IP()
|
||||
//
|
||||
// register = kingpin.Command("register", "Register a new user.")
|
||||
// registerNick = register.Arg("nick", "nickname for user").Required().String()
|
||||
// registerName = register.Arg("name", "name of user").Required().String()
|
||||
//
|
||||
// post = kingpin.Command("post", "Post a message to a channel.")
|
||||
// postImage = post.Flag("image", "image to post").ExistingFile()
|
||||
// postChannel = post.Arg("channel", "channel to post to").Required().String()
|
||||
// postText = post.Arg("text", "text to post").String()
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
// switch kingpin.Parse() {
|
||||
// // Register user
|
||||
// case "register":
|
||||
// println(*registerNick)
|
||||
//
|
||||
// // Post message
|
||||
// case "post":
|
||||
// if *postImage != nil {
|
||||
// }
|
||||
// if *postText != "" {
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
package kingpin
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package kingpin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
envVarValuesSeparator = "\r?\n"
|
||||
envVarValuesTrimmer = regexp.MustCompile(envVarValuesSeparator + "$")
|
||||
envVarValuesSplitter = regexp.MustCompile(envVarValuesSeparator)
|
||||
)
|
||||
|
||||
type envarMixin struct {
|
||||
envar string
|
||||
noEnvar bool
|
||||
}
|
||||
|
||||
func (e *envarMixin) HasEnvarValue() bool {
|
||||
return e.GetEnvarValue() != ""
|
||||
}
|
||||
|
||||
func (e *envarMixin) GetEnvarValue() string {
|
||||
if e.noEnvar || e.envar == "" {
|
||||
return ""
|
||||
}
|
||||
return os.Getenv(e.envar)
|
||||
}
|
||||
|
||||
func (e *envarMixin) GetSplitEnvarValue() []string {
|
||||
envarValue := e.GetEnvarValue()
|
||||
if envarValue == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Split by new line to extract multiple values, if any.
|
||||
trimmed := envVarValuesTrimmer.ReplaceAllString(envarValue, "")
|
||||
|
||||
return envVarValuesSplitter.Split(trimmed, -1)
|
||||
}
|
||||
|
|
@ -0,0 +1,332 @@
|
|||
package kingpin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type flagGroup struct {
|
||||
short map[string]*FlagClause
|
||||
long map[string]*FlagClause
|
||||
flagOrder []*FlagClause
|
||||
}
|
||||
|
||||
func newFlagGroup() *flagGroup {
|
||||
return &flagGroup{
|
||||
short: map[string]*FlagClause{},
|
||||
long: map[string]*FlagClause{},
|
||||
}
|
||||
}
|
||||
|
||||
// GetFlag gets a flag definition.
|
||||
//
|
||||
// This allows existing flags to be modified after definition but before parsing. Useful for
|
||||
// modular applications.
|
||||
func (f *flagGroup) GetFlag(name string) *FlagClause {
|
||||
return f.long[name]
|
||||
}
|
||||
|
||||
// Flag defines a new flag with the given long name and help.
|
||||
func (f *flagGroup) Flag(name, help string) *FlagClause {
|
||||
flag := newFlag(name, help)
|
||||
f.long[name] = flag
|
||||
f.flagOrder = append(f.flagOrder, flag)
|
||||
return flag
|
||||
}
|
||||
|
||||
func (f *flagGroup) init(defaultEnvarPrefix string) error {
|
||||
if err := f.checkDuplicates(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, flag := range f.long {
|
||||
if defaultEnvarPrefix != "" && !flag.noEnvar && flag.envar == "" {
|
||||
flag.envar = envarTransform(defaultEnvarPrefix + "_" + flag.name)
|
||||
}
|
||||
if err := flag.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
if flag.shorthand != 0 {
|
||||
f.short[string(flag.shorthand)] = flag
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *flagGroup) checkDuplicates() error {
|
||||
seenShort := map[rune]bool{}
|
||||
seenLong := map[string]bool{}
|
||||
for _, flag := range f.flagOrder {
|
||||
if flag.shorthand != 0 {
|
||||
if _, ok := seenShort[flag.shorthand]; ok {
|
||||
return fmt.Errorf("duplicate short flag -%c", flag.shorthand)
|
||||
}
|
||||
seenShort[flag.shorthand] = true
|
||||
}
|
||||
if _, ok := seenLong[flag.name]; ok {
|
||||
return fmt.Errorf("duplicate long flag --%s", flag.name)
|
||||
}
|
||||
seenLong[flag.name] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *flagGroup) parse(context *ParseContext) (*FlagClause, error) {
|
||||
var token *Token
|
||||
|
||||
loop:
|
||||
for {
|
||||
token = context.Peek()
|
||||
switch token.Type {
|
||||
case TokenEOL:
|
||||
break loop
|
||||
|
||||
case TokenLong, TokenShort:
|
||||
flagToken := token
|
||||
defaultValue := ""
|
||||
var flag *FlagClause
|
||||
var ok bool
|
||||
invert := false
|
||||
|
||||
name := token.Value
|
||||
if token.Type == TokenLong {
|
||||
flag, ok = f.long[name]
|
||||
if !ok {
|
||||
if strings.HasPrefix(name, "no-") {
|
||||
name = name[3:]
|
||||
invert = true
|
||||
}
|
||||
flag, ok = f.long[name]
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown long flag '%s'", flagToken)
|
||||
}
|
||||
} else {
|
||||
flag, ok = f.short[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown short flag '%s'", flagToken)
|
||||
}
|
||||
}
|
||||
|
||||
context.Next()
|
||||
|
||||
flag.isSetByUser()
|
||||
|
||||
fb, ok := flag.value.(boolFlag)
|
||||
if ok && fb.IsBoolFlag() {
|
||||
if invert {
|
||||
defaultValue = "false"
|
||||
} else {
|
||||
defaultValue = "true"
|
||||
}
|
||||
} else {
|
||||
if invert {
|
||||
context.Push(token)
|
||||
return nil, fmt.Errorf("unknown long flag '%s'", flagToken)
|
||||
}
|
||||
token = context.Peek()
|
||||
if token.Type != TokenArg {
|
||||
context.Push(token)
|
||||
return nil, fmt.Errorf("expected argument for flag '%s'", flagToken)
|
||||
}
|
||||
context.Next()
|
||||
defaultValue = token.Value
|
||||
}
|
||||
|
||||
context.matchedFlag(flag, defaultValue)
|
||||
return flag, nil
|
||||
|
||||
default:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FlagClause is a fluid interface used to build flags.
|
||||
type FlagClause struct {
|
||||
parserMixin
|
||||
actionMixin
|
||||
completionsMixin
|
||||
envarMixin
|
||||
name string
|
||||
shorthand rune
|
||||
help string
|
||||
defaultValues []string
|
||||
placeholder string
|
||||
hidden bool
|
||||
setByUser *bool
|
||||
}
|
||||
|
||||
func newFlag(name, help string) *FlagClause {
|
||||
f := &FlagClause{
|
||||
name: name,
|
||||
help: help,
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *FlagClause) setDefault() error {
|
||||
if f.HasEnvarValue() {
|
||||
if v, ok := f.value.(repeatableFlag); !ok || !v.IsCumulative() {
|
||||
// Use the value as-is
|
||||
return f.value.Set(f.GetEnvarValue())
|
||||
} else {
|
||||
for _, value := range f.GetSplitEnvarValue() {
|
||||
if err := f.value.Set(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.defaultValues) > 0 {
|
||||
for _, defaultValue := range f.defaultValues {
|
||||
if err := f.value.Set(defaultValue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FlagClause) isSetByUser() {
|
||||
if f.setByUser != nil {
|
||||
*f.setByUser = true
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FlagClause) needsValue() bool {
|
||||
haveDefault := len(f.defaultValues) > 0
|
||||
return f.required && !(haveDefault || f.HasEnvarValue())
|
||||
}
|
||||
|
||||
func (f *FlagClause) init() error {
|
||||
if f.required && len(f.defaultValues) > 0 {
|
||||
return fmt.Errorf("required flag '--%s' with default value that will never be used", f.name)
|
||||
}
|
||||
if f.value == nil {
|
||||
return fmt.Errorf("no type defined for --%s (eg. .String())", f.name)
|
||||
}
|
||||
if v, ok := f.value.(repeatableFlag); (!ok || !v.IsCumulative()) && len(f.defaultValues) > 1 {
|
||||
return fmt.Errorf("invalid default for '--%s', expecting single value", f.name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dispatch to the given function after the flag is parsed and validated.
|
||||
func (f *FlagClause) Action(action Action) *FlagClause {
|
||||
f.addAction(action)
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *FlagClause) PreAction(action Action) *FlagClause {
|
||||
f.addPreAction(action)
|
||||
return f
|
||||
}
|
||||
|
||||
// HintAction registers a HintAction (function) for the flag to provide completions
|
||||
func (a *FlagClause) HintAction(action HintAction) *FlagClause {
|
||||
a.addHintAction(action)
|
||||
return a
|
||||
}
|
||||
|
||||
// HintOptions registers any number of options for the flag to provide completions
|
||||
func (a *FlagClause) HintOptions(options ...string) *FlagClause {
|
||||
a.addHintAction(func() []string {
|
||||
return options
|
||||
})
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *FlagClause) EnumVar(target *string, options ...string) {
|
||||
a.parserMixin.EnumVar(target, options...)
|
||||
a.addHintActionBuiltin(func() []string {
|
||||
return options
|
||||
})
|
||||
}
|
||||
|
||||
func (a *FlagClause) Enum(options ...string) (target *string) {
|
||||
a.addHintActionBuiltin(func() []string {
|
||||
return options
|
||||
})
|
||||
return a.parserMixin.Enum(options...)
|
||||
}
|
||||
|
||||
// IsSetByUser let to know if the flag was set by the user
|
||||
func (f *FlagClause) IsSetByUser(setByUser *bool) *FlagClause {
|
||||
if setByUser != nil {
|
||||
*setByUser = false
|
||||
}
|
||||
f.setByUser = setByUser
|
||||
return f
|
||||
}
|
||||
|
||||
// Default values for this flag. They *must* be parseable by the value of the flag.
|
||||
func (f *FlagClause) Default(values ...string) *FlagClause {
|
||||
f.defaultValues = values
|
||||
return f
|
||||
}
|
||||
|
||||
// DEPRECATED: Use Envar(name) instead.
|
||||
func (f *FlagClause) OverrideDefaultFromEnvar(envar string) *FlagClause {
|
||||
return f.Envar(envar)
|
||||
}
|
||||
|
||||
// Envar overrides the default value(s) for a flag from an environment variable,
|
||||
// if it is set. Several default values can be provided by using new lines to
|
||||
// separate them.
|
||||
func (f *FlagClause) Envar(name string) *FlagClause {
|
||||
f.envar = name
|
||||
f.noEnvar = false
|
||||
return f
|
||||
}
|
||||
|
||||
// NoEnvar forces environment variable defaults to be disabled for this flag.
|
||||
// Most useful in conjunction with app.DefaultEnvars().
|
||||
func (f *FlagClause) NoEnvar() *FlagClause {
|
||||
f.envar = ""
|
||||
f.noEnvar = true
|
||||
return f
|
||||
}
|
||||
|
||||
// PlaceHolder sets the place-holder string used for flag values in the help. The
|
||||
// default behaviour is to use the value provided by Default() if provided,
|
||||
// then fall back on the capitalized flag name.
|
||||
func (f *FlagClause) PlaceHolder(placeholder string) *FlagClause {
|
||||
f.placeholder = placeholder
|
||||
return f
|
||||
}
|
||||
|
||||
// Hidden hides a flag from usage but still allows it to be used.
|
||||
func (f *FlagClause) Hidden() *FlagClause {
|
||||
f.hidden = true
|
||||
return f
|
||||
}
|
||||
|
||||
// Required makes the flag required. You can not provide a Default() value to a Required() flag.
|
||||
func (f *FlagClause) Required() *FlagClause {
|
||||
f.required = true
|
||||
return f
|
||||
}
|
||||
|
||||
// Short sets the short flag name.
|
||||
func (f *FlagClause) Short(name rune) *FlagClause {
|
||||
f.shorthand = name
|
||||
return f
|
||||
}
|
||||
|
||||
// Help sets the help message.
|
||||
func (f *FlagClause) Help(help string) *FlagClause {
|
||||
f.help = help
|
||||
return f
|
||||
}
|
||||
|
||||
// Bool makes this flag a boolean flag.
|
||||
func (f *FlagClause) Bool() (target *bool) {
|
||||
target = new(bool)
|
||||
f.SetValue(newBoolValue(target))
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package kingpin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
// CommandLine is the default Kingpin parser.
|
||||
CommandLine = New(filepath.Base(os.Args[0]), "")
|
||||
// Global help flag. Exposed for user customisation.
|
||||
HelpFlag = CommandLine.HelpFlag
|
||||
// Top-level help command. Exposed for user customisation. May be nil.
|
||||
HelpCommand = CommandLine.HelpCommand
|
||||
// Global version flag. Exposed for user customisation. May be nil.
|
||||
VersionFlag = CommandLine.VersionFlag
|
||||
// Whether to file expansion with '@' is enabled.
|
||||
EnableFileExpansion = true
|
||||
)
|
||||
|
||||
// Command adds a new command to the default parser.
|
||||
func Command(name, help string) *CmdClause {
|
||||
return CommandLine.Command(name, help)
|
||||
}
|
||||
|
||||
// Flag adds a new flag to the default parser.
|
||||
func Flag(name, help string) *FlagClause {
|
||||
return CommandLine.Flag(name, help)
|
||||
}
|
||||
|
||||
// Arg adds a new argument to the top-level of the default parser.
|
||||
func Arg(name, help string) *ArgClause {
|
||||
return CommandLine.Arg(name, help)
|
||||
}
|
||||
|
||||
// Parse and return the selected command. Will call the termination handler if
|
||||
// an error is encountered.
|
||||
func Parse() string {
|
||||
selected := MustParse(CommandLine.Parse(os.Args[1:]))
|
||||
if selected == "" && CommandLine.cmdGroup.have() {
|
||||
Usage()
|
||||
CommandLine.terminate(0)
|
||||
}
|
||||
return selected
|
||||
}
|
||||
|
||||
// Errorf prints an error message to stderr.
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
CommandLine.Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Fatalf prints an error message to stderr and exits.
|
||||
func Fatalf(format string, args ...interface{}) {
|
||||
CommandLine.Fatalf(format, args...)
|
||||
}
|
||||
|
||||
// FatalIfError prints an error and exits if err is not nil. The error is printed
|
||||
// with the given prefix.
|
||||
func FatalIfError(err error, format string, args ...interface{}) {
|
||||
CommandLine.FatalIfError(err, format, args...)
|
||||
}
|
||||
|
||||
// FatalUsage prints an error message followed by usage information, then
|
||||
// exits with a non-zero status.
|
||||
func FatalUsage(format string, args ...interface{}) {
|
||||
CommandLine.FatalUsage(format, args...)
|
||||
}
|
||||
|
||||
// FatalUsageContext writes a printf formatted error message to stderr, then
|
||||
// usage information for the given ParseContext, before exiting.
|
||||
func FatalUsageContext(context *ParseContext, format string, args ...interface{}) {
|
||||
CommandLine.FatalUsageContext(context, format, args...)
|
||||
}
|
||||
|
||||
// Usage prints usage to stderr.
|
||||
func Usage() {
|
||||
CommandLine.Usage(os.Args[1:])
|
||||
}
|
||||
|
||||
// Set global usage template to use (defaults to DefaultUsageTemplate).
|
||||
func UsageTemplate(template string) *Application {
|
||||
return CommandLine.UsageTemplate(template)
|
||||
}
|
||||
|
||||
// MustParse can be used with app.Parse(args) to exit with an error if parsing fails.
|
||||
func MustParse(command string, err error) string {
|
||||
if err != nil {
|
||||
Fatalf("%s, try --help", err)
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// Version adds a flag for displaying the application version number.
|
||||
func Version(version string) *Application {
|
||||
return CommandLine.Version(version)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// +build appengine !linux,!freebsd,!darwin,!dragonfly,!netbsd,!openbsd
|
||||
|
||||
package kingpin
|
||||
|
||||
import "io"
|
||||
|
||||
func guessWidth(w io.Writer) int {
|
||||
return 80
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// +build !appengine,linux freebsd darwin dragonfly netbsd openbsd
|
||||
|
||||
package kingpin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func guessWidth(w io.Writer) int {
|
||||
// check if COLUMNS env is set to comply with
|
||||
// http://pubs.opengroup.org/onlinepubs/009604499/basedefs/xbd_chap08.html
|
||||
colsStr := os.Getenv("COLUMNS")
|
||||
if colsStr != "" {
|
||||
if cols, err := strconv.Atoi(colsStr); err == nil {
|
||||
return cols
|
||||
}
|
||||
}
|
||||
|
||||
if t, ok := w.(*os.File); ok {
|
||||
fd := t.Fd()
|
||||
var dimensions [4]uint16
|
||||
|
||||
if _, _, err := syscall.Syscall6(
|
||||
syscall.SYS_IOCTL,
|
||||
uintptr(fd),
|
||||
uintptr(syscall.TIOCGWINSZ),
|
||||
uintptr(unsafe.Pointer(&dimensions)),
|
||||
0, 0, 0,
|
||||
); err == 0 {
|
||||
return int(dimensions[1])
|
||||
}
|
||||
}
|
||||
return 80
|
||||
}
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
package kingpin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Data model for Kingpin command-line structure.
|
||||
|
||||
var (
|
||||
ignoreInCount = map[string]bool{
|
||||
"help": true,
|
||||
"help-long": true,
|
||||
"help-man": true,
|
||||
"completion-bash": true,
|
||||
"completion-script-bash": true,
|
||||
"completion-script-zsh": true,
|
||||
}
|
||||
)
|
||||
|
||||
type FlagGroupModel struct {
|
||||
Flags []*FlagModel
|
||||
}
|
||||
|
||||
func (f *FlagGroupModel) FlagSummary() string {
|
||||
out := []string{}
|
||||
count := 0
|
||||
|
||||
for _, flag := range f.Flags {
|
||||
|
||||
if !ignoreInCount[flag.Name] {
|
||||
count++
|
||||
}
|
||||
|
||||
if flag.Required {
|
||||
if flag.IsBoolFlag() {
|
||||
out = append(out, fmt.Sprintf("--[no-]%s", flag.Name))
|
||||
} else {
|
||||
out = append(out, fmt.Sprintf("--%s=%s", flag.Name, flag.FormatPlaceHolder()))
|
||||
}
|
||||
}
|
||||
}
|
||||
if count != len(out) {
|
||||
out = append(out, "[<flags>]")
|
||||
}
|
||||
return strings.Join(out, " ")
|
||||
}
|
||||
|
||||
type FlagModel struct {
|
||||
Name string
|
||||
Help string
|
||||
Short rune
|
||||
Default []string
|
||||
Envar string
|
||||
PlaceHolder string
|
||||
Required bool
|
||||
Hidden bool
|
||||
Value Value
|
||||
}
|
||||
|
||||
func (f *FlagModel) String() string {
|
||||
if f.Value == nil {
|
||||
return ""
|
||||
}
|
||||
return f.Value.String()
|
||||
}
|
||||
|
||||
func (f *FlagModel) IsBoolFlag() bool {
|
||||
if fl, ok := f.Value.(boolFlag); ok {
|
||||
return fl.IsBoolFlag()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *FlagModel) FormatPlaceHolder() string {
|
||||
if f.PlaceHolder != "" {
|
||||
return f.PlaceHolder
|
||||
}
|
||||
if len(f.Default) > 0 {
|
||||
ellipsis := ""
|
||||
if len(f.Default) > 1 {
|
||||
ellipsis = "..."
|
||||
}
|
||||
if _, ok := f.Value.(*stringValue); ok {
|
||||
return strconv.Quote(f.Default[0]) + ellipsis
|
||||
}
|
||||
return f.Default[0] + ellipsis
|
||||
}
|
||||
return strings.ToUpper(f.Name)
|
||||
}
|
||||
|
||||
func (f *FlagModel) HelpWithEnvar() string {
|
||||
if f.Envar == "" {
|
||||
return f.Help
|
||||
}
|
||||
return fmt.Sprintf("%s ($%s)", f.Help, f.Envar)
|
||||
}
|
||||
|
||||
type ArgGroupModel struct {
|
||||
Args []*ArgModel
|
||||
}
|
||||
|
||||
func (a *ArgGroupModel) ArgSummary() string {
|
||||
depth := 0
|
||||
out := []string{}
|
||||
for _, arg := range a.Args {
|
||||
var h string
|
||||
if arg.PlaceHolder != "" {
|
||||
h = arg.PlaceHolder
|
||||
} else {
|
||||
h = "<" + arg.Name + ">"
|
||||
}
|
||||
if !arg.Required {
|
||||
h = "[" + h
|
||||
depth++
|
||||
}
|
||||
out = append(out, h)
|
||||
}
|
||||
out[len(out)-1] = out[len(out)-1] + strings.Repeat("]", depth)
|
||||
return strings.Join(out, " ")
|
||||
}
|
||||
|
||||
func (a *ArgModel) HelpWithEnvar() string {
|
||||
if a.Envar == "" {
|
||||
return a.Help
|
||||
}
|
||||
return fmt.Sprintf("%s ($%s)", a.Help, a.Envar)
|
||||
}
|
||||
|
||||
type ArgModel struct {
|
||||
Name string
|
||||
Help string
|
||||
Default []string
|
||||
Envar string
|
||||
PlaceHolder string
|
||||
Required bool
|
||||
Hidden bool
|
||||
Value Value
|
||||
}
|
||||
|
||||
func (a *ArgModel) String() string {
|
||||
if a.Value == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return a.Value.String()
|
||||
}
|
||||
|
||||
type CmdGroupModel struct {
|
||||
Commands []*CmdModel
|
||||
}
|
||||
|
||||
func (c *CmdGroupModel) FlattenedCommands() (out []*CmdModel) {
|
||||
for _, cmd := range c.Commands {
|
||||
if len(cmd.Commands) == 0 {
|
||||
out = append(out, cmd)
|
||||
}
|
||||
out = append(out, cmd.FlattenedCommands()...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type CmdModel struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Help string
|
||||
HelpLong string
|
||||
FullCommand string
|
||||
Depth int
|
||||
Hidden bool
|
||||
Default bool
|
||||
*FlagGroupModel
|
||||
*ArgGroupModel
|
||||
*CmdGroupModel
|
||||
}
|
||||
|
||||
func (c *CmdModel) String() string {
|
||||
return c.FullCommand
|
||||
}
|
||||
|
||||
type ApplicationModel struct {
|
||||
Name string
|
||||
Help string
|
||||
Version string
|
||||
Author string
|
||||
*ArgGroupModel
|
||||
*CmdGroupModel
|
||||
*FlagGroupModel
|
||||
}
|
||||
|
||||
func (a *Application) Model() *ApplicationModel {
|
||||
return &ApplicationModel{
|
||||
Name: a.Name,
|
||||
Help: a.Help,
|
||||
Version: a.version,
|
||||
Author: a.author,
|
||||
FlagGroupModel: a.flagGroup.Model(),
|
||||
ArgGroupModel: a.argGroup.Model(),
|
||||
CmdGroupModel: a.cmdGroup.Model(),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *argGroup) Model() *ArgGroupModel {
|
||||
m := &ArgGroupModel{}
|
||||
for _, arg := range a.args {
|
||||
m.Args = append(m.Args, arg.Model())
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (a *ArgClause) Model() *ArgModel {
|
||||
return &ArgModel{
|
||||
Name: a.name,
|
||||
Help: a.help,
|
||||
Default: a.defaultValues,
|
||||
Envar: a.envar,
|
||||
PlaceHolder: a.placeholder,
|
||||
Required: a.required,
|
||||
Hidden: a.hidden,
|
||||
Value: a.value,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *flagGroup) Model() *FlagGroupModel {
|
||||
m := &FlagGroupModel{}
|
||||
for _, fl := range f.flagOrder {
|
||||
m.Flags = append(m.Flags, fl.Model())
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (f *FlagClause) Model() *FlagModel {
|
||||
return &FlagModel{
|
||||
Name: f.name,
|
||||
Help: f.help,
|
||||
Short: rune(f.shorthand),
|
||||
Default: f.defaultValues,
|
||||
Envar: f.envar,
|
||||
PlaceHolder: f.placeholder,
|
||||
Required: f.required,
|
||||
Hidden: f.hidden,
|
||||
Value: f.value,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cmdGroup) Model() *CmdGroupModel {
|
||||
m := &CmdGroupModel{}
|
||||
for _, cm := range c.commandOrder {
|
||||
m.Commands = append(m.Commands, cm.Model())
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (c *CmdClause) Model() *CmdModel {
|
||||
depth := 0
|
||||
for i := c; i != nil; i = i.parent {
|
||||
depth++
|
||||
}
|
||||
return &CmdModel{
|
||||
Name: c.name,
|
||||
Aliases: c.aliases,
|
||||
Help: c.help,
|
||||
HelpLong: c.helpLong,
|
||||
Depth: depth,
|
||||
Hidden: c.hidden,
|
||||
Default: c.isDefault,
|
||||
FullCommand: c.FullCommand(),
|
||||
FlagGroupModel: c.flagGroup.Model(),
|
||||
ArgGroupModel: c.argGroup.Model(),
|
||||
CmdGroupModel: c.cmdGroup.Model(),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,396 @@
|
|||
package kingpin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type TokenType int
|
||||
|
||||
// Token types.
|
||||
const (
|
||||
TokenShort TokenType = iota
|
||||
TokenLong
|
||||
TokenArg
|
||||
TokenError
|
||||
TokenEOL
|
||||
)
|
||||
|
||||
func (t TokenType) String() string {
|
||||
switch t {
|
||||
case TokenShort:
|
||||
return "short flag"
|
||||
case TokenLong:
|
||||
return "long flag"
|
||||
case TokenArg:
|
||||
return "argument"
|
||||
case TokenError:
|
||||
return "error"
|
||||
case TokenEOL:
|
||||
return "<EOL>"
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
var (
|
||||
TokenEOLMarker = Token{-1, TokenEOL, ""}
|
||||
)
|
||||
|
||||
type Token struct {
|
||||
Index int
|
||||
Type TokenType
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t *Token) Equal(o *Token) bool {
|
||||
return t.Index == o.Index
|
||||
}
|
||||
|
||||
func (t *Token) IsFlag() bool {
|
||||
return t.Type == TokenShort || t.Type == TokenLong
|
||||
}
|
||||
|
||||
func (t *Token) IsEOF() bool {
|
||||
return t.Type == TokenEOL
|
||||
}
|
||||
|
||||
func (t *Token) String() string {
|
||||
switch t.Type {
|
||||
case TokenShort:
|
||||
return "-" + t.Value
|
||||
case TokenLong:
|
||||
return "--" + t.Value
|
||||
case TokenArg:
|
||||
return t.Value
|
||||
case TokenError:
|
||||
return "error: " + t.Value
|
||||
case TokenEOL:
|
||||
return "<EOL>"
|
||||
default:
|
||||
panic("unhandled type")
|
||||
}
|
||||
}
|
||||
|
||||
// A union of possible elements in a parse stack.
|
||||
type ParseElement struct {
|
||||
// Clause is either *CmdClause, *ArgClause or *FlagClause.
|
||||
Clause interface{}
|
||||
// Value is corresponding value for an ArgClause or FlagClause (if any).
|
||||
Value *string
|
||||
}
|
||||
|
||||
// ParseContext holds the current context of the parser. When passed to
|
||||
// Action() callbacks Elements will be fully populated with *FlagClause,
|
||||
// *ArgClause and *CmdClause values and their corresponding arguments (if
|
||||
// any).
|
||||
type ParseContext struct {
|
||||
SelectedCommand *CmdClause
|
||||
ignoreDefault bool
|
||||
argsOnly bool
|
||||
peek []*Token
|
||||
argi int // Index of current command-line arg we're processing.
|
||||
args []string
|
||||
rawArgs []string
|
||||
flags *flagGroup
|
||||
arguments *argGroup
|
||||
argumenti int // Cursor into arguments
|
||||
// Flags, arguments and commands encountered and collected during parse.
|
||||
Elements []*ParseElement
|
||||
}
|
||||
|
||||
func (p *ParseContext) nextArg() *ArgClause {
|
||||
if p.argumenti >= len(p.arguments.args) {
|
||||
return nil
|
||||
}
|
||||
arg := p.arguments.args[p.argumenti]
|
||||
if !arg.consumesRemainder() {
|
||||
p.argumenti++
|
||||
}
|
||||
return arg
|
||||
}
|
||||
|
||||
func (p *ParseContext) next() {
|
||||
p.argi++
|
||||
p.args = p.args[1:]
|
||||
}
|
||||
|
||||
// HasTrailingArgs returns true if there are unparsed command-line arguments.
|
||||
// This can occur if the parser can not match remaining arguments.
|
||||
func (p *ParseContext) HasTrailingArgs() bool {
|
||||
return len(p.args) > 0
|
||||
}
|
||||
|
||||
func tokenize(args []string, ignoreDefault bool) *ParseContext {
|
||||
return &ParseContext{
|
||||
ignoreDefault: ignoreDefault,
|
||||
args: args,
|
||||
rawArgs: args,
|
||||
flags: newFlagGroup(),
|
||||
arguments: newArgGroup(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParseContext) mergeFlags(flags *flagGroup) {
|
||||
for _, flag := range flags.flagOrder {
|
||||
if flag.shorthand != 0 {
|
||||
p.flags.short[string(flag.shorthand)] = flag
|
||||
}
|
||||
p.flags.long[flag.name] = flag
|
||||
p.flags.flagOrder = append(p.flags.flagOrder, flag)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParseContext) mergeArgs(args *argGroup) {
|
||||
p.arguments.args = append(p.arguments.args, args.args...)
|
||||
}
|
||||
|
||||
func (p *ParseContext) EOL() bool {
|
||||
return p.Peek().Type == TokenEOL
|
||||
}
|
||||
|
||||
func (p *ParseContext) Error() bool {
|
||||
return p.Peek().Type == TokenError
|
||||
}
|
||||
|
||||
// Next token in the parse context.
|
||||
func (p *ParseContext) Next() *Token {
|
||||
if len(p.peek) > 0 {
|
||||
return p.pop()
|
||||
}
|
||||
|
||||
// End of tokens.
|
||||
if len(p.args) == 0 {
|
||||
return &Token{Index: p.argi, Type: TokenEOL}
|
||||
}
|
||||
|
||||
if p.argi > 0 && p.argi <= len(p.rawArgs) && p.rawArgs[p.argi-1] == "--" {
|
||||
// If the previous argument was a --, from now on only arguments are parsed.
|
||||
p.argsOnly = true
|
||||
}
|
||||
arg := p.args[0]
|
||||
p.next()
|
||||
|
||||
if p.argsOnly {
|
||||
return &Token{p.argi, TokenArg, arg}
|
||||
}
|
||||
|
||||
if arg == "--" {
|
||||
return p.Next()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(arg, "--") {
|
||||
parts := strings.SplitN(arg[2:], "=", 2)
|
||||
token := &Token{p.argi, TokenLong, parts[0]}
|
||||
if len(parts) == 2 {
|
||||
p.Push(&Token{p.argi, TokenArg, parts[1]})
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
if len(arg) == 1 {
|
||||
return &Token{Index: p.argi, Type: TokenArg}
|
||||
}
|
||||
shortRune, size := utf8.DecodeRuneInString(arg[1:])
|
||||
short := string(shortRune)
|
||||
flag, ok := p.flags.short[short]
|
||||
// Not a known short flag, we'll just return it anyway.
|
||||
if !ok {
|
||||
} else if fb, ok := flag.value.(boolFlag); ok && fb.IsBoolFlag() {
|
||||
// Bool short flag.
|
||||
} else {
|
||||
// Short flag with combined argument: -fARG
|
||||
token := &Token{p.argi, TokenShort, short}
|
||||
if len(arg) > size+1 {
|
||||
p.Push(&Token{p.argi, TokenArg, arg[size+1:]})
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
if len(arg) > size+1 {
|
||||
p.args = append([]string{"-" + arg[size+1:]}, p.args...)
|
||||
}
|
||||
return &Token{p.argi, TokenShort, short}
|
||||
} else if EnableFileExpansion && strings.HasPrefix(arg, "@") {
|
||||
expanded, err := ExpandArgsFromFile(arg[1:])
|
||||
if err != nil {
|
||||
return &Token{p.argi, TokenError, err.Error()}
|
||||
}
|
||||
if len(p.args) == 0 {
|
||||
p.args = expanded
|
||||
} else {
|
||||
p.args = append(expanded, p.args...)
|
||||
}
|
||||
return p.Next()
|
||||
}
|
||||
|
||||
return &Token{p.argi, TokenArg, arg}
|
||||
}
|
||||
|
||||
func (p *ParseContext) Peek() *Token {
|
||||
if len(p.peek) == 0 {
|
||||
return p.Push(p.Next())
|
||||
}
|
||||
return p.peek[len(p.peek)-1]
|
||||
}
|
||||
|
||||
func (p *ParseContext) Push(token *Token) *Token {
|
||||
p.peek = append(p.peek, token)
|
||||
return token
|
||||
}
|
||||
|
||||
func (p *ParseContext) pop() *Token {
|
||||
end := len(p.peek) - 1
|
||||
token := p.peek[end]
|
||||
p.peek = p.peek[0:end]
|
||||
return token
|
||||
}
|
||||
|
||||
func (p *ParseContext) String() string {
|
||||
return p.SelectedCommand.FullCommand()
|
||||
}
|
||||
|
||||
func (p *ParseContext) matchedFlag(flag *FlagClause, value string) {
|
||||
p.Elements = append(p.Elements, &ParseElement{Clause: flag, Value: &value})
|
||||
}
|
||||
|
||||
func (p *ParseContext) matchedArg(arg *ArgClause, value string) {
|
||||
p.Elements = append(p.Elements, &ParseElement{Clause: arg, Value: &value})
|
||||
}
|
||||
|
||||
func (p *ParseContext) matchedCmd(cmd *CmdClause) {
|
||||
p.Elements = append(p.Elements, &ParseElement{Clause: cmd})
|
||||
p.mergeFlags(cmd.flagGroup)
|
||||
p.mergeArgs(cmd.argGroup)
|
||||
p.SelectedCommand = cmd
|
||||
}
|
||||
|
||||
// Expand arguments from a file. Lines starting with # will be treated as comments.
|
||||
func ExpandArgsFromFile(filename string) (out []string, err error) {
|
||||
if filename == "" {
|
||||
return nil, fmt.Errorf("expected @ file to expand arguments from")
|
||||
}
|
||||
r, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open arguments file %q: %s", filename, err)
|
||||
}
|
||||
defer r.Close()
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, line)
|
||||
}
|
||||
err = scanner.Err()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read arguments from %q: %s", filename, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parse(context *ParseContext, app *Application) (err error) {
|
||||
context.mergeFlags(app.flagGroup)
|
||||
context.mergeArgs(app.argGroup)
|
||||
|
||||
cmds := app.cmdGroup
|
||||
ignoreDefault := context.ignoreDefault
|
||||
|
||||
loop:
|
||||
for !context.EOL() && !context.Error() {
|
||||
token := context.Peek()
|
||||
|
||||
switch token.Type {
|
||||
case TokenLong, TokenShort:
|
||||
if flag, err := context.flags.parse(context); err != nil {
|
||||
if !ignoreDefault {
|
||||
if cmd := cmds.defaultSubcommand(); cmd != nil {
|
||||
cmd.completionAlts = cmds.cmdNames()
|
||||
context.matchedCmd(cmd)
|
||||
cmds = cmd.cmdGroup
|
||||
break
|
||||
}
|
||||
}
|
||||
return err
|
||||
} else if flag == HelpFlag {
|
||||
ignoreDefault = true
|
||||
}
|
||||
|
||||
case TokenArg:
|
||||
if cmds.have() {
|
||||
selectedDefault := false
|
||||
cmd, ok := cmds.commands[token.String()]
|
||||
if !ok {
|
||||
if !ignoreDefault {
|
||||
if cmd = cmds.defaultSubcommand(); cmd != nil {
|
||||
cmd.completionAlts = cmds.cmdNames()
|
||||
selectedDefault = true
|
||||
}
|
||||
}
|
||||
if cmd == nil {
|
||||
return fmt.Errorf("expected command but got %q", token)
|
||||
}
|
||||
}
|
||||
if cmd == HelpCommand {
|
||||
ignoreDefault = true
|
||||
}
|
||||
cmd.completionAlts = nil
|
||||
context.matchedCmd(cmd)
|
||||
cmds = cmd.cmdGroup
|
||||
if !selectedDefault {
|
||||
context.Next()
|
||||
}
|
||||
} else if context.arguments.have() {
|
||||
if app.noInterspersed {
|
||||
// no more flags
|
||||
context.argsOnly = true
|
||||
}
|
||||
arg := context.nextArg()
|
||||
if arg == nil {
|
||||
break loop
|
||||
}
|
||||
context.matchedArg(arg, token.String())
|
||||
context.Next()
|
||||
} else {
|
||||
break loop
|
||||
}
|
||||
|
||||
case TokenEOL:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
// Move to innermost default command.
|
||||
for !ignoreDefault {
|
||||
if cmd := cmds.defaultSubcommand(); cmd != nil {
|
||||
cmd.completionAlts = cmds.cmdNames()
|
||||
context.matchedCmd(cmd)
|
||||
cmds = cmd.cmdGroup
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if context.Error() {
|
||||
return fmt.Errorf("%s", context.Peek().Value)
|
||||
}
|
||||
|
||||
if !context.EOL() {
|
||||
return fmt.Errorf("unexpected %s", context.Peek())
|
||||
}
|
||||
|
||||
// Set defaults for all remaining args.
|
||||
for arg := context.nextArg(); arg != nil && !arg.consumesRemainder(); arg = context.nextArg() {
|
||||
for _, defaultValue := range arg.defaultValues {
|
||||
if err := arg.value.Set(defaultValue); err != nil {
|
||||
return fmt.Errorf("invalid default value '%s' for argument '%s'", defaultValue, arg.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
package kingpin
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/units"
|
||||
)
|
||||
|
||||
type Settings interface {
|
||||
SetValue(value Value)
|
||||
}
|
||||
|
||||
type parserMixin struct {
|
||||
value Value
|
||||
required bool
|
||||
}
|
||||
|
||||
func (p *parserMixin) SetText(text Text) {
|
||||
p.value = &wrapText{text}
|
||||
}
|
||||
|
||||
func (p *parserMixin) SetValue(value Value) {
|
||||
p.value = value
|
||||
}
|
||||
|
||||
// StringMap provides key=value parsing into a map.
|
||||
func (p *parserMixin) StringMap() (target *map[string]string) {
|
||||
target = &(map[string]string{})
|
||||
p.StringMapVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
// Duration sets the parser to a time.Duration parser.
|
||||
func (p *parserMixin) Duration() (target *time.Duration) {
|
||||
target = new(time.Duration)
|
||||
p.DurationVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
// Bytes parses numeric byte units. eg. 1.5KB
|
||||
func (p *parserMixin) Bytes() (target *units.Base2Bytes) {
|
||||
target = new(units.Base2Bytes)
|
||||
p.BytesVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
// IP sets the parser to a net.IP parser.
|
||||
func (p *parserMixin) IP() (target *net.IP) {
|
||||
target = new(net.IP)
|
||||
p.IPVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
// TCP (host:port) address.
|
||||
func (p *parserMixin) TCP() (target **net.TCPAddr) {
|
||||
target = new(*net.TCPAddr)
|
||||
p.TCPVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
// TCPVar (host:port) address.
|
||||
func (p *parserMixin) TCPVar(target **net.TCPAddr) {
|
||||
p.SetValue(newTCPAddrValue(target))
|
||||
}
|
||||
|
||||
// ExistingFile sets the parser to one that requires and returns an existing file.
|
||||
func (p *parserMixin) ExistingFile() (target *string) {
|
||||
target = new(string)
|
||||
p.ExistingFileVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
// ExistingDir sets the parser to one that requires and returns an existing directory.
|
||||
func (p *parserMixin) ExistingDir() (target *string) {
|
||||
target = new(string)
|
||||
p.ExistingDirVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
// ExistingFileOrDir sets the parser to one that requires and returns an existing file OR directory.
|
||||
func (p *parserMixin) ExistingFileOrDir() (target *string) {
|
||||
target = new(string)
|
||||
p.ExistingFileOrDirVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
// File returns an os.File against an existing file.
|
||||
func (p *parserMixin) File() (target **os.File) {
|
||||
target = new(*os.File)
|
||||
p.FileVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
// File attempts to open a File with os.OpenFile(flag, perm).
|
||||
func (p *parserMixin) OpenFile(flag int, perm os.FileMode) (target **os.File) {
|
||||
target = new(*os.File)
|
||||
p.OpenFileVar(target, flag, perm)
|
||||
return
|
||||
}
|
||||
|
||||
// URL provides a valid, parsed url.URL.
|
||||
func (p *parserMixin) URL() (target **url.URL) {
|
||||
target = new(*url.URL)
|
||||
p.URLVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
// StringMap provides key=value parsing into a map.
|
||||
func (p *parserMixin) StringMapVar(target *map[string]string) {
|
||||
p.SetValue(newStringMapValue(target))
|
||||
}
|
||||
|
||||
// Float sets the parser to a float64 parser.
|
||||
func (p *parserMixin) Float() (target *float64) {
|
||||
return p.Float64()
|
||||
}
|
||||
|
||||
// Float sets the parser to a float64 parser.
|
||||
func (p *parserMixin) FloatVar(target *float64) {
|
||||
p.Float64Var(target)
|
||||
}
|
||||
|
||||
// Duration sets the parser to a time.Duration parser.
|
||||
func (p *parserMixin) DurationVar(target *time.Duration) {
|
||||
p.SetValue(newDurationValue(target))
|
||||
}
|
||||
|
||||
// BytesVar parses numeric byte units. eg. 1.5KB
|
||||
func (p *parserMixin) BytesVar(target *units.Base2Bytes) {
|
||||
p.SetValue(newBytesValue(target))
|
||||
}
|
||||
|
||||
// IP sets the parser to a net.IP parser.
|
||||
func (p *parserMixin) IPVar(target *net.IP) {
|
||||
p.SetValue(newIPValue(target))
|
||||
}
|
||||
|
||||
// ExistingFile sets the parser to one that requires and returns an existing file.
|
||||
func (p *parserMixin) ExistingFileVar(target *string) {
|
||||
p.SetValue(newExistingFileValue(target))
|
||||
}
|
||||
|
||||
// ExistingDir sets the parser to one that requires and returns an existing directory.
|
||||
func (p *parserMixin) ExistingDirVar(target *string) {
|
||||
p.SetValue(newExistingDirValue(target))
|
||||
}
|
||||
|
||||
// ExistingDir sets the parser to one that requires and returns an existing directory.
|
||||
func (p *parserMixin) ExistingFileOrDirVar(target *string) {
|
||||
p.SetValue(newExistingFileOrDirValue(target))
|
||||
}
|
||||
|
||||
// FileVar opens an existing file.
|
||||
func (p *parserMixin) FileVar(target **os.File) {
|
||||
p.SetValue(newFileValue(target, os.O_RDONLY, 0))
|
||||
}
|
||||
|
||||
// OpenFileVar calls os.OpenFile(flag, perm)
|
||||
func (p *parserMixin) OpenFileVar(target **os.File, flag int, perm os.FileMode) {
|
||||
p.SetValue(newFileValue(target, flag, perm))
|
||||
}
|
||||
|
||||
// URL provides a valid, parsed url.URL.
|
||||
func (p *parserMixin) URLVar(target **url.URL) {
|
||||
p.SetValue(newURLValue(target))
|
||||
}
|
||||
|
||||
// URLList provides a parsed list of url.URL values.
|
||||
func (p *parserMixin) URLList() (target *[]*url.URL) {
|
||||
target = new([]*url.URL)
|
||||
p.URLListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
// URLListVar provides a parsed list of url.URL values.
|
||||
func (p *parserMixin) URLListVar(target *[]*url.URL) {
|
||||
p.SetValue(newURLListValue(target))
|
||||
}
|
||||
|
||||
// Enum allows a value from a set of options.
|
||||
func (p *parserMixin) Enum(options ...string) (target *string) {
|
||||
target = new(string)
|
||||
p.EnumVar(target, options...)
|
||||
return
|
||||
}
|
||||
|
||||
// EnumVar allows a value from a set of options.
|
||||
func (p *parserMixin) EnumVar(target *string, options ...string) {
|
||||
p.SetValue(newEnumFlag(target, options...))
|
||||
}
|
||||
|
||||
// Enums allows a set of values from a set of options.
|
||||
func (p *parserMixin) Enums(options ...string) (target *[]string) {
|
||||
target = new([]string)
|
||||
p.EnumsVar(target, options...)
|
||||
return
|
||||
}
|
||||
|
||||
// EnumVar allows a value from a set of options.
|
||||
func (p *parserMixin) EnumsVar(target *[]string, options ...string) {
|
||||
p.SetValue(newEnumsFlag(target, options...))
|
||||
}
|
||||
|
||||
// A Counter increments a number each time it is encountered.
|
||||
func (p *parserMixin) Counter() (target *int) {
|
||||
target = new(int)
|
||||
p.CounterVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) CounterVar(target *int) {
|
||||
p.SetValue(newCounterValue(target))
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
package kingpin
|
||||
|
||||
// Default usage template.
|
||||
var DefaultUsageTemplate = `{{define "FormatCommand" -}}
|
||||
{{if .FlagSummary}} {{.FlagSummary}}{{end -}}
|
||||
{{range .Args}}{{if not .Hidden}} {{if not .Required}}[{{end}}{{if .PlaceHolder}}{{.PlaceHolder}}{{else}}<{{.Name}}>{{end}}{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}{{end -}}
|
||||
{{end -}}
|
||||
|
||||
{{define "FormatCommands" -}}
|
||||
{{range .FlattenedCommands -}}
|
||||
{{if not .Hidden -}}
|
||||
{{.FullCommand}}{{if .Default}}*{{end}}{{template "FormatCommand" .}}
|
||||
{{.Help|Wrap 4}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
|
||||
{{define "FormatUsage" -}}
|
||||
{{template "FormatCommand" .}}{{if .Commands}} <command> [<args> ...]{{end}}
|
||||
{{if .Help}}
|
||||
{{.Help|Wrap 0 -}}
|
||||
{{end -}}
|
||||
|
||||
{{end -}}
|
||||
|
||||
{{if .Context.SelectedCommand -}}
|
||||
usage: {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatUsage" .Context.SelectedCommand}}
|
||||
{{ else -}}
|
||||
usage: {{.App.Name}}{{template "FormatUsage" .App}}
|
||||
{{end}}
|
||||
{{if .Context.Flags -}}
|
||||
Flags:
|
||||
{{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}}
|
||||
{{end -}}
|
||||
{{if .Context.Args -}}
|
||||
Args:
|
||||
{{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}}
|
||||
{{end -}}
|
||||
{{if .Context.SelectedCommand -}}
|
||||
{{if len .Context.SelectedCommand.Commands -}}
|
||||
Subcommands:
|
||||
{{template "FormatCommands" .Context.SelectedCommand}}
|
||||
{{end -}}
|
||||
{{else if .App.Commands -}}
|
||||
Commands:
|
||||
{{template "FormatCommands" .App}}
|
||||
{{end -}}
|
||||
`
|
||||
|
||||
// Usage template where command's optional flags are listed separately
|
||||
var SeparateOptionalFlagsUsageTemplate = `{{define "FormatCommand" -}}
|
||||
{{if .FlagSummary}} {{.FlagSummary}}{{end -}}
|
||||
{{range .Args}}{{if not .Hidden}} {{if not .Required}}[{{end}}{{if .PlaceHolder}}{{.PlaceHolder}}{{else}}<{{.Name}}>{{end}}{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}{{end -}}
|
||||
{{end -}}
|
||||
|
||||
{{define "FormatCommands" -}}
|
||||
{{range .FlattenedCommands -}}
|
||||
{{if not .Hidden -}}
|
||||
{{.FullCommand}}{{if .Default}}*{{end}}{{template "FormatCommand" .}}
|
||||
{{.Help|Wrap 4}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
|
||||
{{define "FormatUsage" -}}
|
||||
{{template "FormatCommand" .}}{{if .Commands}} <command> [<args> ...]{{end}}
|
||||
{{if .Help}}
|
||||
{{.Help|Wrap 0 -}}
|
||||
{{end -}}
|
||||
|
||||
{{end -}}
|
||||
{{if .Context.SelectedCommand -}}
|
||||
usage: {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatUsage" .Context.SelectedCommand}}
|
||||
{{else -}}
|
||||
usage: {{.App.Name}}{{template "FormatUsage" .App}}
|
||||
{{end -}}
|
||||
|
||||
{{if .Context.Flags|RequiredFlags -}}
|
||||
Required flags:
|
||||
{{.Context.Flags|RequiredFlags|FlagsToTwoColumns|FormatTwoColumns}}
|
||||
{{end -}}
|
||||
{{if .Context.Flags|OptionalFlags -}}
|
||||
Optional flags:
|
||||
{{.Context.Flags|OptionalFlags|FlagsToTwoColumns|FormatTwoColumns}}
|
||||
{{end -}}
|
||||
{{if .Context.Args -}}
|
||||
Args:
|
||||
{{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}}
|
||||
{{end -}}
|
||||
{{if .Context.SelectedCommand -}}
|
||||
Subcommands:
|
||||
{{if .Context.SelectedCommand.Commands -}}
|
||||
{{template "FormatCommands" .Context.SelectedCommand}}
|
||||
{{end -}}
|
||||
{{else if .App.Commands -}}
|
||||
Commands:
|
||||
{{template "FormatCommands" .App}}
|
||||
{{end -}}
|
||||
`
|
||||
|
||||
// Usage template with compactly formatted commands.
|
||||
var CompactUsageTemplate = `{{define "FormatCommand" -}}
|
||||
{{if .FlagSummary}} {{.FlagSummary}}{{end -}}
|
||||
{{range .Args}}{{if not .Hidden}} {{if not .Required}}[{{end}}{{if .PlaceHolder}}{{.PlaceHolder}}{{else}}<{{.Name}}>{{end}}{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}{{end -}}
|
||||
{{end -}}
|
||||
|
||||
{{define "FormatCommandList" -}}
|
||||
{{range . -}}
|
||||
{{if not .Hidden -}}
|
||||
{{.Depth|Indent}}{{.Name}}{{if .Default}}*{{end}}{{template "FormatCommand" .}}
|
||||
{{end -}}
|
||||
{{template "FormatCommandList" .Commands -}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
|
||||
{{define "FormatUsage" -}}
|
||||
{{template "FormatCommand" .}}{{if .Commands}} <command> [<args> ...]{{end}}
|
||||
{{if .Help}}
|
||||
{{.Help|Wrap 0 -}}
|
||||
{{end -}}
|
||||
|
||||
{{end -}}
|
||||
|
||||
{{if .Context.SelectedCommand -}}
|
||||
usage: {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatUsage" .Context.SelectedCommand}}
|
||||
{{else -}}
|
||||
usage: {{.App.Name}}{{template "FormatUsage" .App}}
|
||||
{{end -}}
|
||||
{{if .Context.Flags -}}
|
||||
Flags:
|
||||
{{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}}
|
||||
{{end -}}
|
||||
{{if .Context.Args -}}
|
||||
Args:
|
||||
{{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}}
|
||||
{{end -}}
|
||||
{{if .Context.SelectedCommand -}}
|
||||
{{if .Context.SelectedCommand.Commands -}}
|
||||
Commands:
|
||||
{{.Context.SelectedCommand}}
|
||||
{{template "FormatCommandList" .Context.SelectedCommand.Commands}}
|
||||
{{end -}}
|
||||
{{else if .App.Commands -}}
|
||||
Commands:
|
||||
{{template "FormatCommandList" .App.Commands}}
|
||||
{{end -}}
|
||||
`
|
||||
|
||||
var ManPageTemplate = `{{define "FormatFlags" -}}
|
||||
{{range .Flags -}}
|
||||
{{if not .Hidden -}}
|
||||
.TP
|
||||
\fB{{if .Short}}-{{.Short|Char}}, {{end}}--{{.Name}}{{if not .IsBoolFlag}}={{.FormatPlaceHolder}}{{end -}}\fR
|
||||
{{.Help}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
|
||||
{{define "FormatCommand" -}}
|
||||
{{if .FlagSummary}} {{.FlagSummary}}{{end -}}
|
||||
{{range .Args}}{{if not .Hidden}} {{if not .Required}}[{{end}}{{if .PlaceHolder}}{{.PlaceHolder}}{{else}}<{{.Name}}>{{end}}{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}{{end -}}
|
||||
{{end -}}
|
||||
|
||||
{{define "FormatCommands" -}}
|
||||
{{range .FlattenedCommands -}}
|
||||
{{if not .Hidden -}}
|
||||
.SS
|
||||
\fB{{.FullCommand}}{{template "FormatCommand" . -}}\fR
|
||||
.PP
|
||||
{{.Help}}
|
||||
{{template "FormatFlags" . -}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
|
||||
{{define "FormatUsage" -}}
|
||||
{{template "FormatCommand" .}}{{if .Commands}} <command> [<args> ...]{{end -}}\fR
|
||||
{{end -}}
|
||||
|
||||
.TH {{.App.Name}} 1 {{.App.Version}} "{{.App.Author}}"
|
||||
.SH "NAME"
|
||||
{{.App.Name}}
|
||||
.SH "SYNOPSIS"
|
||||
.TP
|
||||
\fB{{.App.Name}}{{template "FormatUsage" .App}}
|
||||
.SH "DESCRIPTION"
|
||||
{{.App.Help}}
|
||||
.SH "OPTIONS"
|
||||
{{template "FormatFlags" .App -}}
|
||||
{{if .App.Commands -}}
|
||||
.SH "COMMANDS"
|
||||
{{template "FormatCommands" .App -}}
|
||||
{{end -}}
|
||||
`
|
||||
|
||||
// Default usage template.
|
||||
var LongHelpTemplate = `{{define "FormatCommand" -}}
|
||||
{{if .FlagSummary}} {{.FlagSummary}}{{end -}}
|
||||
{{range .Args}}{{if not .Hidden}} {{if not .Required}}[{{end}}{{if .PlaceHolder}}{{.PlaceHolder}}{{else}}<{{.Name}}>{{end}}{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}{{end -}}
|
||||
{{end -}}
|
||||
|
||||
{{define "FormatCommands" -}}
|
||||
{{range .FlattenedCommands -}}
|
||||
{{if not .Hidden -}}
|
||||
{{.FullCommand}}{{template "FormatCommand" .}}
|
||||
{{.Help|Wrap 4}}
|
||||
{{with .Flags|FlagsToTwoColumns}}{{FormatTwoColumnsWithIndent . 4 2}}{{end}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
|
||||
{{define "FormatUsage" -}}
|
||||
{{template "FormatCommand" .}}{{if .Commands}} <command> [<args> ...]{{end}}
|
||||
{{if .Help}}
|
||||
{{.Help|Wrap 0 -}}
|
||||
{{end -}}
|
||||
|
||||
{{end -}}
|
||||
|
||||
usage: {{.App.Name}}{{template "FormatUsage" .App}}
|
||||
{{if .Context.Flags -}}
|
||||
Flags:
|
||||
{{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}}
|
||||
{{end -}}
|
||||
{{if .Context.Args -}}
|
||||
Args:
|
||||
{{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}}
|
||||
{{end -}}
|
||||
{{if .App.Commands -}}
|
||||
Commands:
|
||||
{{template "FormatCommands" .App}}
|
||||
{{end -}}
|
||||
`
|
||||
|
||||
var BashCompletionTemplate = `
|
||||
_{{.App.Name}}_bash_autocomplete() {
|
||||
local cur prev opts base
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
opts=$( ${COMP_WORDS[0]} --completion-bash "${COMP_WORDS[@]:1:$COMP_CWORD}" )
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
}
|
||||
complete -F _{{.App.Name}}_bash_autocomplete -o default {{.App.Name}}
|
||||
|
||||
`
|
||||
|
||||
var ZshCompletionTemplate = `#compdef {{.App.Name}}
|
||||
|
||||
_{{.App.Name}}() {
|
||||
local matches=($(${words[1]} --completion-bash "${(@)words[2,$CURRENT]}"))
|
||||
compadd -a matches
|
||||
|
||||
if [[ $compstate[nmatches] -eq 0 && $words[$CURRENT] != -* ]]; then
|
||||
_files
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$(basename -- ${(%):-%x})" != "_{{.App.Name}}" ]]; then
|
||||
compdef _{{.App.Name}} {{.App.Name}}
|
||||
fi
|
||||
`
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
package kingpin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/doc"
|
||||
"io"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var (
|
||||
preIndent = " "
|
||||
)
|
||||
|
||||
func formatTwoColumns(w io.Writer, indent, padding, width int, rows [][2]string) {
|
||||
// Find size of first column.
|
||||
s := 0
|
||||
for _, row := range rows {
|
||||
if c := len(row[0]); c > s && c < 30 {
|
||||
s = c
|
||||
}
|
||||
}
|
||||
|
||||
indentStr := strings.Repeat(" ", indent)
|
||||
offsetStr := strings.Repeat(" ", s+padding)
|
||||
|
||||
for _, row := range rows {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
doc.ToText(buf, row[1], "", preIndent, width-s-padding-indent)
|
||||
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
|
||||
fmt.Fprintf(w, "%s%-*s%*s", indentStr, s, row[0], padding, "")
|
||||
if len(row[0]) >= 30 {
|
||||
fmt.Fprintf(w, "\n%s%s", indentStr, offsetStr)
|
||||
}
|
||||
fmt.Fprintf(w, "%s\n", lines[0])
|
||||
for _, line := range lines[1:] {
|
||||
fmt.Fprintf(w, "%s%s%s\n", indentStr, offsetStr, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage writes application usage to w. It parses args to determine
|
||||
// appropriate help context, such as which command to show help for.
|
||||
func (a *Application) Usage(args []string) {
|
||||
context, err := a.parseContext(true, args)
|
||||
a.FatalIfError(err, "")
|
||||
if err := a.UsageForContextWithTemplate(context, 2, a.usageTemplate); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func formatAppUsage(app *ApplicationModel) string {
|
||||
s := []string{app.Name}
|
||||
if len(app.Flags) > 0 {
|
||||
s = append(s, app.FlagSummary())
|
||||
}
|
||||
if len(app.Args) > 0 {
|
||||
s = append(s, app.ArgSummary())
|
||||
}
|
||||
return strings.Join(s, " ")
|
||||
}
|
||||
|
||||
func formatCmdUsage(app *ApplicationModel, cmd *CmdModel) string {
|
||||
s := []string{app.Name, cmd.String()}
|
||||
if len(cmd.Flags) > 0 {
|
||||
s = append(s, cmd.FlagSummary())
|
||||
}
|
||||
if len(cmd.Args) > 0 {
|
||||
s = append(s, cmd.ArgSummary())
|
||||
}
|
||||
return strings.Join(s, " ")
|
||||
}
|
||||
|
||||
func formatFlag(haveShort bool, flag *FlagModel) string {
|
||||
flagString := ""
|
||||
flagName := flag.Name
|
||||
if flag.IsBoolFlag() {
|
||||
flagName = "[no-]" + flagName
|
||||
}
|
||||
if flag.Short != 0 {
|
||||
flagString += fmt.Sprintf("-%c, --%s", flag.Short, flagName)
|
||||
} else {
|
||||
if haveShort {
|
||||
flagString += fmt.Sprintf(" --%s", flagName)
|
||||
} else {
|
||||
flagString += fmt.Sprintf("--%s", flagName)
|
||||
}
|
||||
}
|
||||
if !flag.IsBoolFlag() {
|
||||
flagString += fmt.Sprintf("=%s", flag.FormatPlaceHolder())
|
||||
}
|
||||
if v, ok := flag.Value.(repeatableFlag); ok && v.IsCumulative() {
|
||||
flagString += " ..."
|
||||
}
|
||||
return flagString
|
||||
}
|
||||
|
||||
type templateParseContext struct {
|
||||
SelectedCommand *CmdModel
|
||||
*FlagGroupModel
|
||||
*ArgGroupModel
|
||||
}
|
||||
|
||||
type templateContext struct {
|
||||
App *ApplicationModel
|
||||
Width int
|
||||
Context *templateParseContext
|
||||
}
|
||||
|
||||
// UsageForContext displays usage information from a ParseContext (obtained from
|
||||
// Application.ParseContext() or Action(f) callbacks).
|
||||
func (a *Application) UsageForContext(context *ParseContext) error {
|
||||
return a.UsageForContextWithTemplate(context, 2, a.usageTemplate)
|
||||
}
|
||||
|
||||
// UsageForContextWithTemplate is the base usage function. You generally don't need to use this.
|
||||
func (a *Application) UsageForContextWithTemplate(context *ParseContext, indent int, tmpl string) error {
|
||||
width := guessWidth(a.usageWriter)
|
||||
funcs := template.FuncMap{
|
||||
"Indent": func(level int) string {
|
||||
return strings.Repeat(" ", level*indent)
|
||||
},
|
||||
"Wrap": func(indent int, s string) string {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
indentText := strings.Repeat(" ", indent)
|
||||
doc.ToText(buf, s, indentText, " "+indentText, width-indent)
|
||||
return buf.String()
|
||||
},
|
||||
"FormatFlag": formatFlag,
|
||||
"FlagsToTwoColumns": func(f []*FlagModel) [][2]string {
|
||||
rows := [][2]string{}
|
||||
haveShort := false
|
||||
for _, flag := range f {
|
||||
if flag.Short != 0 {
|
||||
haveShort = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, flag := range f {
|
||||
if !flag.Hidden {
|
||||
rows = append(rows, [2]string{formatFlag(haveShort, flag), flag.HelpWithEnvar()})
|
||||
}
|
||||
}
|
||||
return rows
|
||||
},
|
||||
"RequiredFlags": func(f []*FlagModel) []*FlagModel {
|
||||
requiredFlags := []*FlagModel{}
|
||||
for _, flag := range f {
|
||||
if flag.Required {
|
||||
requiredFlags = append(requiredFlags, flag)
|
||||
}
|
||||
}
|
||||
return requiredFlags
|
||||
},
|
||||
"OptionalFlags": func(f []*FlagModel) []*FlagModel {
|
||||
optionalFlags := []*FlagModel{}
|
||||
for _, flag := range f {
|
||||
if !flag.Required {
|
||||
optionalFlags = append(optionalFlags, flag)
|
||||
}
|
||||
}
|
||||
return optionalFlags
|
||||
},
|
||||
"ArgsToTwoColumns": func(a []*ArgModel) [][2]string {
|
||||
rows := [][2]string{}
|
||||
for _, arg := range a {
|
||||
if !arg.Hidden {
|
||||
var s string
|
||||
if arg.PlaceHolder != "" {
|
||||
s = arg.PlaceHolder
|
||||
} else {
|
||||
s = "<" + arg.Name + ">"
|
||||
}
|
||||
if !arg.Required {
|
||||
s = "[" + s + "]"
|
||||
}
|
||||
rows = append(rows, [2]string{s, arg.HelpWithEnvar()})
|
||||
}
|
||||
}
|
||||
return rows
|
||||
},
|
||||
"FormatTwoColumns": func(rows [][2]string) string {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
formatTwoColumns(buf, indent, indent, width, rows)
|
||||
return buf.String()
|
||||
},
|
||||
"FormatTwoColumnsWithIndent": func(rows [][2]string, indent, padding int) string {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
formatTwoColumns(buf, indent, padding, width, rows)
|
||||
return buf.String()
|
||||
},
|
||||
"FormatAppUsage": formatAppUsage,
|
||||
"FormatCommandUsage": formatCmdUsage,
|
||||
"IsCumulative": func(value Value) bool {
|
||||
r, ok := value.(remainderArg)
|
||||
return ok && r.IsCumulative()
|
||||
},
|
||||
"Char": func(c rune) string {
|
||||
return string(c)
|
||||
},
|
||||
}
|
||||
for k, v := range a.usageFuncs {
|
||||
funcs[k] = v
|
||||
}
|
||||
|
||||
t, err := template.New("usage").Funcs(funcs).Parse(tmpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var selectedCommand *CmdModel
|
||||
if context.SelectedCommand != nil {
|
||||
selectedCommand = context.SelectedCommand.Model()
|
||||
}
|
||||
ctx := templateContext{
|
||||
App: a.Model(),
|
||||
Width: width,
|
||||
Context: &templateParseContext{
|
||||
SelectedCommand: selectedCommand,
|
||||
FlagGroupModel: context.flags.Model(),
|
||||
ArgGroupModel: context.arguments.Model(),
|
||||
},
|
||||
}
|
||||
return t.Execute(a.usageWriter, ctx)
|
||||
}
|
||||
|
|
@ -0,0 +1,489 @@
|
|||
package kingpin
|
||||
|
||||
//go:generate go run ./cmd/genvalues/main.go
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/units"
|
||||
"github.com/xhit/go-str2duration/v2"
|
||||
)
|
||||
|
||||
// NOTE: Most of the base type values were lifted from:
|
||||
// http://golang.org/src/pkg/flag/flag.go?s=20146:20222
|
||||
|
||||
// Value is the interface to the dynamic value stored in a flag.
|
||||
// (The default value is represented as a string.)
|
||||
//
|
||||
// If a Value has an IsBoolFlag() bool method returning true, the command-line
|
||||
// parser makes --name equivalent to -name=true rather than using the next
|
||||
// command-line argument, and adds a --no-name counterpart for negating the
|
||||
// flag.
|
||||
type Value interface {
|
||||
String() string
|
||||
Set(string) error
|
||||
}
|
||||
|
||||
// Getter is an interface that allows the contents of a Value to be retrieved.
|
||||
// It wraps the Value interface, rather than being part of it, because it
|
||||
// appeared after Go 1 and its compatibility rules. All Value types provided
|
||||
// by this package satisfy the Getter interface.
|
||||
type Getter interface {
|
||||
Value
|
||||
Get() interface{}
|
||||
}
|
||||
|
||||
// Optional interface to indicate boolean flags that don't accept a value, and
|
||||
// implicitly have a --no-<x> negation counterpart.
|
||||
type boolFlag interface {
|
||||
IsBoolFlag() bool
|
||||
}
|
||||
|
||||
// Optional interface for arguments that cumulatively consume all remaining
|
||||
// input.
|
||||
type remainderArg interface {
|
||||
IsCumulative() bool
|
||||
}
|
||||
|
||||
// Optional interface for flags that can be repeated.
|
||||
type repeatableFlag interface {
|
||||
IsCumulative() bool
|
||||
}
|
||||
|
||||
// Text is the interface to the dynamic value stored in a flag.
|
||||
// (The default value is represented as a string.)
|
||||
type Text interface {
|
||||
encoding.TextMarshaler
|
||||
encoding.TextUnmarshaler
|
||||
}
|
||||
|
||||
type wrapText struct {
|
||||
text Text
|
||||
}
|
||||
|
||||
func (w wrapText) String() string {
|
||||
buf, _ := w.text.MarshalText()
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
func (w *wrapText) Set(s string) error {
|
||||
return w.text.UnmarshalText([]byte(s))
|
||||
}
|
||||
|
||||
type accumulator struct {
|
||||
element func(value interface{}) Value
|
||||
typ reflect.Type
|
||||
slice reflect.Value
|
||||
}
|
||||
|
||||
// Use reflection to accumulate values into a slice.
|
||||
//
|
||||
// target := []string{}
|
||||
// newAccumulator(&target, func (value interface{}) Value {
|
||||
// return newStringValue(value.(*string))
|
||||
// })
|
||||
func newAccumulator(slice interface{}, element func(value interface{}) Value) *accumulator {
|
||||
typ := reflect.TypeOf(slice)
|
||||
if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Slice {
|
||||
panic("expected a pointer to a slice")
|
||||
}
|
||||
return &accumulator{
|
||||
element: element,
|
||||
typ: typ.Elem().Elem(),
|
||||
slice: reflect.ValueOf(slice),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *accumulator) String() string {
|
||||
out := []string{}
|
||||
s := a.slice.Elem()
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
out = append(out, a.element(s.Index(i).Addr().Interface()).String())
|
||||
}
|
||||
return strings.Join(out, ",")
|
||||
}
|
||||
|
||||
func (a *accumulator) Set(value string) error {
|
||||
e := reflect.New(a.typ)
|
||||
if err := a.element(e.Interface()).Set(value); err != nil {
|
||||
return err
|
||||
}
|
||||
slice := reflect.Append(a.slice.Elem(), e.Elem())
|
||||
a.slice.Elem().Set(slice)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *accumulator) Get() interface{} {
|
||||
return a.slice.Interface()
|
||||
}
|
||||
|
||||
func (a *accumulator) IsCumulative() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *boolValue) IsBoolFlag() bool { return true }
|
||||
|
||||
// -- time.Duration Value
|
||||
type durationValue time.Duration
|
||||
|
||||
func newDurationValue(p *time.Duration) *durationValue {
|
||||
return (*durationValue)(p)
|
||||
}
|
||||
|
||||
func (d *durationValue) Set(s string) error {
|
||||
v, err := str2duration.ParseDuration(s)
|
||||
*d = durationValue(v)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *durationValue) Get() interface{} { return time.Duration(*d) }
|
||||
|
||||
func (d *durationValue) String() string { return (*time.Duration)(d).String() }
|
||||
|
||||
// -- map[string]string Value
|
||||
type stringMapValue map[string]string
|
||||
|
||||
func newStringMapValue(p *map[string]string) *stringMapValue {
|
||||
return (*stringMapValue)(p)
|
||||
}
|
||||
|
||||
var stringMapRegex = regexp.MustCompile("[:=]")
|
||||
|
||||
func (s *stringMapValue) Set(value string) error {
|
||||
parts := stringMapRegex.Split(value, 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("expected KEY=VALUE got '%s'", value)
|
||||
}
|
||||
(*s)[parts[0]] = parts[1]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stringMapValue) Get() interface{} {
|
||||
return (map[string]string)(*s)
|
||||
}
|
||||
|
||||
func (s *stringMapValue) String() string {
|
||||
return fmt.Sprintf("%s", map[string]string(*s))
|
||||
}
|
||||
|
||||
func (s *stringMapValue) IsCumulative() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// -- net.IP Value
|
||||
type ipValue net.IP
|
||||
|
||||
func newIPValue(p *net.IP) *ipValue {
|
||||
return (*ipValue)(p)
|
||||
}
|
||||
|
||||
func (i *ipValue) Set(value string) error {
|
||||
if ip := net.ParseIP(value); ip == nil {
|
||||
return fmt.Errorf("'%s' is not an IP address", value)
|
||||
} else {
|
||||
*i = *(*ipValue)(&ip)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ipValue) Get() interface{} {
|
||||
return (net.IP)(*i)
|
||||
}
|
||||
|
||||
func (i *ipValue) String() string {
|
||||
return (*net.IP)(i).String()
|
||||
}
|
||||
|
||||
// -- *net.TCPAddr Value
|
||||
type tcpAddrValue struct {
|
||||
addr **net.TCPAddr
|
||||
}
|
||||
|
||||
func newTCPAddrValue(p **net.TCPAddr) *tcpAddrValue {
|
||||
return &tcpAddrValue{p}
|
||||
}
|
||||
|
||||
func (i *tcpAddrValue) Set(value string) error {
|
||||
if addr, err := net.ResolveTCPAddr("tcp", value); err != nil {
|
||||
return fmt.Errorf("'%s' is not a valid TCP address: %s", value, err)
|
||||
} else {
|
||||
*i.addr = addr
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tcpAddrValue) Get() interface{} {
|
||||
return (*net.TCPAddr)(*t.addr)
|
||||
}
|
||||
|
||||
func (i *tcpAddrValue) String() string {
|
||||
return (*i.addr).String()
|
||||
}
|
||||
|
||||
// -- existingFile Value
|
||||
|
||||
type fileStatValue struct {
|
||||
path *string
|
||||
predicate func(os.FileInfo) error
|
||||
}
|
||||
|
||||
func newFileStatValue(p *string, predicate func(os.FileInfo) error) *fileStatValue {
|
||||
return &fileStatValue{
|
||||
path: p,
|
||||
predicate: predicate,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *fileStatValue) Set(value string) error {
|
||||
if s, err := os.Stat(value); os.IsNotExist(err) {
|
||||
return fmt.Errorf("path '%s' does not exist", value)
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if err := e.predicate(s); err != nil {
|
||||
return err
|
||||
}
|
||||
*e.path = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fileStatValue) Get() interface{} {
|
||||
return (string)(*f.path)
|
||||
}
|
||||
|
||||
func (e *fileStatValue) String() string {
|
||||
return *e.path
|
||||
}
|
||||
|
||||
// -- os.File value
|
||||
|
||||
type fileValue struct {
|
||||
f **os.File
|
||||
flag int
|
||||
perm os.FileMode
|
||||
}
|
||||
|
||||
func newFileValue(p **os.File, flag int, perm os.FileMode) *fileValue {
|
||||
return &fileValue{p, flag, perm}
|
||||
}
|
||||
|
||||
func (f *fileValue) Set(value string) error {
|
||||
if fd, err := os.OpenFile(value, f.flag, f.perm); err != nil {
|
||||
return err
|
||||
} else {
|
||||
*f.f = fd
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fileValue) Get() interface{} {
|
||||
return (*os.File)(*f.f)
|
||||
}
|
||||
|
||||
func (f *fileValue) String() string {
|
||||
if *f.f == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return (*f.f).Name()
|
||||
}
|
||||
|
||||
// -- url.URL Value
|
||||
type urlValue struct {
|
||||
u **url.URL
|
||||
}
|
||||
|
||||
func newURLValue(p **url.URL) *urlValue {
|
||||
return &urlValue{p}
|
||||
}
|
||||
|
||||
func (u *urlValue) Set(value string) error {
|
||||
if url, err := url.Parse(value); err != nil {
|
||||
return fmt.Errorf("invalid URL: %s", err)
|
||||
} else {
|
||||
*u.u = url
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u *urlValue) Get() interface{} {
|
||||
return (*url.URL)(*u.u)
|
||||
}
|
||||
|
||||
func (u *urlValue) String() string {
|
||||
if *u.u == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return (*u.u).String()
|
||||
}
|
||||
|
||||
// -- []*url.URL Value
|
||||
type urlListValue []*url.URL
|
||||
|
||||
func newURLListValue(p *[]*url.URL) *urlListValue {
|
||||
return (*urlListValue)(p)
|
||||
}
|
||||
|
||||
func (u *urlListValue) Set(value string) error {
|
||||
if url, err := url.Parse(value); err != nil {
|
||||
return fmt.Errorf("invalid URL: %s", err)
|
||||
} else {
|
||||
*u = append(*u, url)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u *urlListValue) Get() interface{} {
|
||||
return ([]*url.URL)(*u)
|
||||
}
|
||||
|
||||
func (u *urlListValue) String() string {
|
||||
out := []string{}
|
||||
for _, url := range *u {
|
||||
out = append(out, url.String())
|
||||
}
|
||||
return strings.Join(out, ",")
|
||||
}
|
||||
|
||||
func (u *urlListValue) IsCumulative() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// A flag whose value must be in a set of options.
|
||||
type enumValue struct {
|
||||
value *string
|
||||
options []string
|
||||
}
|
||||
|
||||
func newEnumFlag(target *string, options ...string) *enumValue {
|
||||
return &enumValue{
|
||||
value: target,
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *enumValue) String() string {
|
||||
return *a.value
|
||||
}
|
||||
|
||||
func (a *enumValue) Set(value string) error {
|
||||
for _, v := range a.options {
|
||||
if v == value {
|
||||
*a.value = value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("enum value must be one of %s, got '%s'", strings.Join(a.options, ","), value)
|
||||
}
|
||||
|
||||
func (e *enumValue) Get() interface{} {
|
||||
return (string)(*e.value)
|
||||
}
|
||||
|
||||
// -- []string Enum Value
|
||||
type enumsValue struct {
|
||||
value *[]string
|
||||
options []string
|
||||
}
|
||||
|
||||
func newEnumsFlag(target *[]string, options ...string) *enumsValue {
|
||||
return &enumsValue{
|
||||
value: target,
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *enumsValue) Set(value string) error {
|
||||
for _, v := range s.options {
|
||||
if v == value {
|
||||
*s.value = append(*s.value, value)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("enum value must be one of %s, got '%s'", strings.Join(s.options, ","), value)
|
||||
}
|
||||
|
||||
func (e *enumsValue) Get() interface{} {
|
||||
return ([]string)(*e.value)
|
||||
}
|
||||
|
||||
func (s *enumsValue) String() string {
|
||||
return strings.Join(*s.value, ",")
|
||||
}
|
||||
|
||||
func (s *enumsValue) IsCumulative() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// -- units.Base2Bytes Value
|
||||
type bytesValue units.Base2Bytes
|
||||
|
||||
func newBytesValue(p *units.Base2Bytes) *bytesValue {
|
||||
return (*bytesValue)(p)
|
||||
}
|
||||
|
||||
func (d *bytesValue) Set(s string) error {
|
||||
v, err := units.ParseBase2Bytes(s)
|
||||
*d = bytesValue(v)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *bytesValue) Get() interface{} { return units.Base2Bytes(*d) }
|
||||
|
||||
func (d *bytesValue) String() string { return (*units.Base2Bytes)(d).String() }
|
||||
|
||||
func newExistingFileValue(target *string) *fileStatValue {
|
||||
return newFileStatValue(target, func(s os.FileInfo) error {
|
||||
if s.IsDir() {
|
||||
return fmt.Errorf("'%s' is a directory", s.Name())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func newExistingDirValue(target *string) *fileStatValue {
|
||||
return newFileStatValue(target, func(s os.FileInfo) error {
|
||||
if !s.IsDir() {
|
||||
return fmt.Errorf("'%s' is a file", s.Name())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func newExistingFileOrDirValue(target *string) *fileStatValue {
|
||||
return newFileStatValue(target, func(s os.FileInfo) error { return nil })
|
||||
}
|
||||
|
||||
type counterValue int
|
||||
|
||||
func newCounterValue(n *int) *counterValue {
|
||||
return (*counterValue)(n)
|
||||
}
|
||||
|
||||
func (c *counterValue) Set(s string) error {
|
||||
*c++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *counterValue) Get() interface{} { return (int)(*c) }
|
||||
func (c *counterValue) IsBoolFlag() bool { return true }
|
||||
func (c *counterValue) String() string { return fmt.Sprintf("%d", *c) }
|
||||
func (c *counterValue) IsCumulative() bool { return true }
|
||||
|
||||
func resolveHost(value string) (net.IP, error) {
|
||||
if ip := net.ParseIP(value); ip != nil {
|
||||
return ip, nil
|
||||
} else {
|
||||
if addr, err := net.ResolveIPAddr("ip", value); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return addr.IP, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
[
|
||||
{"type": "bool", "parser": "strconv.ParseBool(s)"},
|
||||
{"type": "string", "parser": "s, error(nil)", "format": "string(*f.v)", "plural": "Strings"},
|
||||
{"type": "uint", "parser": "strconv.ParseUint(s, 0, 64)", "plural": "Uints"},
|
||||
{"type": "uint8", "parser": "strconv.ParseUint(s, 0, 8)"},
|
||||
{"type": "uint16", "parser": "strconv.ParseUint(s, 0, 16)"},
|
||||
{"type": "uint32", "parser": "strconv.ParseUint(s, 0, 32)"},
|
||||
{"type": "uint64", "parser": "strconv.ParseUint(s, 0, 64)"},
|
||||
{"type": "int", "parser": "strconv.ParseFloat(s, 64)", "plural": "Ints"},
|
||||
{"type": "int8", "parser": "strconv.ParseInt(s, 0, 8)"},
|
||||
{"type": "int16", "parser": "strconv.ParseInt(s, 0, 16)"},
|
||||
{"type": "int32", "parser": "strconv.ParseInt(s, 0, 32)"},
|
||||
{"type": "int64", "parser": "strconv.ParseInt(s, 0, 64)"},
|
||||
{"type": "float64", "parser": "strconv.ParseFloat(s, 64)"},
|
||||
{"type": "float32", "parser": "strconv.ParseFloat(s, 32)"},
|
||||
{"name": "Duration", "type": "time.Duration", "no_value_parser": true},
|
||||
{"name": "IP", "type": "net.IP", "no_value_parser": true},
|
||||
{"name": "TCPAddr", "Type": "*net.TCPAddr", "plural": "TCPList", "no_value_parser": true},
|
||||
{"name": "ExistingFile", "Type": "string", "plural": "ExistingFiles", "no_value_parser": true},
|
||||
{"name": "ExistingDir", "Type": "string", "plural": "ExistingDirs", "no_value_parser": true},
|
||||
{"name": "ExistingFileOrDir", "Type": "string", "plural": "ExistingFilesOrDirs", "no_value_parser": true},
|
||||
{"name": "Regexp", "Type": "*regexp.Regexp", "parser": "regexp.Compile(s)"},
|
||||
{"name": "ResolvedIP", "Type": "net.IP", "parser": "resolveHost(s)", "help": "Resolve a hostname or IP to an IP."},
|
||||
{"name": "HexBytes", "Type": "[]byte", "parser": "hex.DecodeString(s)", "help": "Bytes as a hex string."}
|
||||
]
|
||||
|
|
@ -0,0 +1,821 @@
|
|||
package kingpin
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This file is autogenerated by "go generate .". Do not modify.
|
||||
|
||||
// -- bool Value
|
||||
type boolValue struct{ v *bool }
|
||||
|
||||
func newBoolValue(p *bool) *boolValue {
|
||||
return &boolValue{p}
|
||||
}
|
||||
|
||||
func (f *boolValue) Set(s string) error {
|
||||
v, err := strconv.ParseBool(s)
|
||||
if err == nil {
|
||||
*f.v = (bool)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *boolValue) Get() interface{} { return (bool)(*f.v) }
|
||||
|
||||
func (f *boolValue) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Bool parses the next command-line value as bool.
|
||||
func (p *parserMixin) Bool() (target *bool) {
|
||||
target = new(bool)
|
||||
p.BoolVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) BoolVar(target *bool) {
|
||||
p.SetValue(newBoolValue(target))
|
||||
}
|
||||
|
||||
// BoolList accumulates bool values into a slice.
|
||||
func (p *parserMixin) BoolList() (target *[]bool) {
|
||||
target = new([]bool)
|
||||
p.BoolListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) BoolListVar(target *[]bool) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newBoolValue(v.(*bool))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- string Value
|
||||
type stringValue struct{ v *string }
|
||||
|
||||
func newStringValue(p *string) *stringValue {
|
||||
return &stringValue{p}
|
||||
}
|
||||
|
||||
func (f *stringValue) Set(s string) error {
|
||||
v, err := s, error(nil)
|
||||
if err == nil {
|
||||
*f.v = (string)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *stringValue) Get() interface{} { return (string)(*f.v) }
|
||||
|
||||
func (f *stringValue) String() string { return string(*f.v) }
|
||||
|
||||
// String parses the next command-line value as string.
|
||||
func (p *parserMixin) String() (target *string) {
|
||||
target = new(string)
|
||||
p.StringVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) StringVar(target *string) {
|
||||
p.SetValue(newStringValue(target))
|
||||
}
|
||||
|
||||
// Strings accumulates string values into a slice.
|
||||
func (p *parserMixin) Strings() (target *[]string) {
|
||||
target = new([]string)
|
||||
p.StringsVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) StringsVar(target *[]string) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newStringValue(v.(*string))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- uint Value
|
||||
type uintValue struct{ v *uint }
|
||||
|
||||
func newUintValue(p *uint) *uintValue {
|
||||
return &uintValue{p}
|
||||
}
|
||||
|
||||
func (f *uintValue) Set(s string) error {
|
||||
v, err := strconv.ParseUint(s, 0, 64)
|
||||
if err == nil {
|
||||
*f.v = (uint)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *uintValue) Get() interface{} { return (uint)(*f.v) }
|
||||
|
||||
func (f *uintValue) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Uint parses the next command-line value as uint.
|
||||
func (p *parserMixin) Uint() (target *uint) {
|
||||
target = new(uint)
|
||||
p.UintVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) UintVar(target *uint) {
|
||||
p.SetValue(newUintValue(target))
|
||||
}
|
||||
|
||||
// Uints accumulates uint values into a slice.
|
||||
func (p *parserMixin) Uints() (target *[]uint) {
|
||||
target = new([]uint)
|
||||
p.UintsVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) UintsVar(target *[]uint) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newUintValue(v.(*uint))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- uint8 Value
|
||||
type uint8Value struct{ v *uint8 }
|
||||
|
||||
func newUint8Value(p *uint8) *uint8Value {
|
||||
return &uint8Value{p}
|
||||
}
|
||||
|
||||
func (f *uint8Value) Set(s string) error {
|
||||
v, err := strconv.ParseUint(s, 0, 8)
|
||||
if err == nil {
|
||||
*f.v = (uint8)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *uint8Value) Get() interface{} { return (uint8)(*f.v) }
|
||||
|
||||
func (f *uint8Value) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Uint8 parses the next command-line value as uint8.
|
||||
func (p *parserMixin) Uint8() (target *uint8) {
|
||||
target = new(uint8)
|
||||
p.Uint8Var(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Uint8Var(target *uint8) {
|
||||
p.SetValue(newUint8Value(target))
|
||||
}
|
||||
|
||||
// Uint8List accumulates uint8 values into a slice.
|
||||
func (p *parserMixin) Uint8List() (target *[]uint8) {
|
||||
target = new([]uint8)
|
||||
p.Uint8ListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Uint8ListVar(target *[]uint8) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newUint8Value(v.(*uint8))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- uint16 Value
|
||||
type uint16Value struct{ v *uint16 }
|
||||
|
||||
func newUint16Value(p *uint16) *uint16Value {
|
||||
return &uint16Value{p}
|
||||
}
|
||||
|
||||
func (f *uint16Value) Set(s string) error {
|
||||
v, err := strconv.ParseUint(s, 0, 16)
|
||||
if err == nil {
|
||||
*f.v = (uint16)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *uint16Value) Get() interface{} { return (uint16)(*f.v) }
|
||||
|
||||
func (f *uint16Value) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Uint16 parses the next command-line value as uint16.
|
||||
func (p *parserMixin) Uint16() (target *uint16) {
|
||||
target = new(uint16)
|
||||
p.Uint16Var(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Uint16Var(target *uint16) {
|
||||
p.SetValue(newUint16Value(target))
|
||||
}
|
||||
|
||||
// Uint16List accumulates uint16 values into a slice.
|
||||
func (p *parserMixin) Uint16List() (target *[]uint16) {
|
||||
target = new([]uint16)
|
||||
p.Uint16ListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Uint16ListVar(target *[]uint16) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newUint16Value(v.(*uint16))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- uint32 Value
|
||||
type uint32Value struct{ v *uint32 }
|
||||
|
||||
func newUint32Value(p *uint32) *uint32Value {
|
||||
return &uint32Value{p}
|
||||
}
|
||||
|
||||
func (f *uint32Value) Set(s string) error {
|
||||
v, err := strconv.ParseUint(s, 0, 32)
|
||||
if err == nil {
|
||||
*f.v = (uint32)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *uint32Value) Get() interface{} { return (uint32)(*f.v) }
|
||||
|
||||
func (f *uint32Value) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Uint32 parses the next command-line value as uint32.
|
||||
func (p *parserMixin) Uint32() (target *uint32) {
|
||||
target = new(uint32)
|
||||
p.Uint32Var(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Uint32Var(target *uint32) {
|
||||
p.SetValue(newUint32Value(target))
|
||||
}
|
||||
|
||||
// Uint32List accumulates uint32 values into a slice.
|
||||
func (p *parserMixin) Uint32List() (target *[]uint32) {
|
||||
target = new([]uint32)
|
||||
p.Uint32ListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Uint32ListVar(target *[]uint32) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newUint32Value(v.(*uint32))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- uint64 Value
|
||||
type uint64Value struct{ v *uint64 }
|
||||
|
||||
func newUint64Value(p *uint64) *uint64Value {
|
||||
return &uint64Value{p}
|
||||
}
|
||||
|
||||
func (f *uint64Value) Set(s string) error {
|
||||
v, err := strconv.ParseUint(s, 0, 64)
|
||||
if err == nil {
|
||||
*f.v = (uint64)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *uint64Value) Get() interface{} { return (uint64)(*f.v) }
|
||||
|
||||
func (f *uint64Value) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Uint64 parses the next command-line value as uint64.
|
||||
func (p *parserMixin) Uint64() (target *uint64) {
|
||||
target = new(uint64)
|
||||
p.Uint64Var(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Uint64Var(target *uint64) {
|
||||
p.SetValue(newUint64Value(target))
|
||||
}
|
||||
|
||||
// Uint64List accumulates uint64 values into a slice.
|
||||
func (p *parserMixin) Uint64List() (target *[]uint64) {
|
||||
target = new([]uint64)
|
||||
p.Uint64ListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Uint64ListVar(target *[]uint64) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newUint64Value(v.(*uint64))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- int Value
|
||||
type intValue struct{ v *int }
|
||||
|
||||
func newIntValue(p *int) *intValue {
|
||||
return &intValue{p}
|
||||
}
|
||||
|
||||
func (f *intValue) Set(s string) error {
|
||||
v, err := strconv.ParseFloat(s, 64)
|
||||
if err == nil {
|
||||
*f.v = (int)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *intValue) Get() interface{} { return (int)(*f.v) }
|
||||
|
||||
func (f *intValue) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Int parses the next command-line value as int.
|
||||
func (p *parserMixin) Int() (target *int) {
|
||||
target = new(int)
|
||||
p.IntVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) IntVar(target *int) {
|
||||
p.SetValue(newIntValue(target))
|
||||
}
|
||||
|
||||
// Ints accumulates int values into a slice.
|
||||
func (p *parserMixin) Ints() (target *[]int) {
|
||||
target = new([]int)
|
||||
p.IntsVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) IntsVar(target *[]int) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newIntValue(v.(*int))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- int8 Value
|
||||
type int8Value struct{ v *int8 }
|
||||
|
||||
func newInt8Value(p *int8) *int8Value {
|
||||
return &int8Value{p}
|
||||
}
|
||||
|
||||
func (f *int8Value) Set(s string) error {
|
||||
v, err := strconv.ParseInt(s, 0, 8)
|
||||
if err == nil {
|
||||
*f.v = (int8)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *int8Value) Get() interface{} { return (int8)(*f.v) }
|
||||
|
||||
func (f *int8Value) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Int8 parses the next command-line value as int8.
|
||||
func (p *parserMixin) Int8() (target *int8) {
|
||||
target = new(int8)
|
||||
p.Int8Var(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Int8Var(target *int8) {
|
||||
p.SetValue(newInt8Value(target))
|
||||
}
|
||||
|
||||
// Int8List accumulates int8 values into a slice.
|
||||
func (p *parserMixin) Int8List() (target *[]int8) {
|
||||
target = new([]int8)
|
||||
p.Int8ListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Int8ListVar(target *[]int8) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newInt8Value(v.(*int8))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- int16 Value
|
||||
type int16Value struct{ v *int16 }
|
||||
|
||||
func newInt16Value(p *int16) *int16Value {
|
||||
return &int16Value{p}
|
||||
}
|
||||
|
||||
func (f *int16Value) Set(s string) error {
|
||||
v, err := strconv.ParseInt(s, 0, 16)
|
||||
if err == nil {
|
||||
*f.v = (int16)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *int16Value) Get() interface{} { return (int16)(*f.v) }
|
||||
|
||||
func (f *int16Value) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Int16 parses the next command-line value as int16.
|
||||
func (p *parserMixin) Int16() (target *int16) {
|
||||
target = new(int16)
|
||||
p.Int16Var(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Int16Var(target *int16) {
|
||||
p.SetValue(newInt16Value(target))
|
||||
}
|
||||
|
||||
// Int16List accumulates int16 values into a slice.
|
||||
func (p *parserMixin) Int16List() (target *[]int16) {
|
||||
target = new([]int16)
|
||||
p.Int16ListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Int16ListVar(target *[]int16) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newInt16Value(v.(*int16))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- int32 Value
|
||||
type int32Value struct{ v *int32 }
|
||||
|
||||
func newInt32Value(p *int32) *int32Value {
|
||||
return &int32Value{p}
|
||||
}
|
||||
|
||||
func (f *int32Value) Set(s string) error {
|
||||
v, err := strconv.ParseInt(s, 0, 32)
|
||||
if err == nil {
|
||||
*f.v = (int32)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *int32Value) Get() interface{} { return (int32)(*f.v) }
|
||||
|
||||
func (f *int32Value) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Int32 parses the next command-line value as int32.
|
||||
func (p *parserMixin) Int32() (target *int32) {
|
||||
target = new(int32)
|
||||
p.Int32Var(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Int32Var(target *int32) {
|
||||
p.SetValue(newInt32Value(target))
|
||||
}
|
||||
|
||||
// Int32List accumulates int32 values into a slice.
|
||||
func (p *parserMixin) Int32List() (target *[]int32) {
|
||||
target = new([]int32)
|
||||
p.Int32ListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Int32ListVar(target *[]int32) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newInt32Value(v.(*int32))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- int64 Value
|
||||
type int64Value struct{ v *int64 }
|
||||
|
||||
func newInt64Value(p *int64) *int64Value {
|
||||
return &int64Value{p}
|
||||
}
|
||||
|
||||
func (f *int64Value) Set(s string) error {
|
||||
v, err := strconv.ParseInt(s, 0, 64)
|
||||
if err == nil {
|
||||
*f.v = (int64)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *int64Value) Get() interface{} { return (int64)(*f.v) }
|
||||
|
||||
func (f *int64Value) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Int64 parses the next command-line value as int64.
|
||||
func (p *parserMixin) Int64() (target *int64) {
|
||||
target = new(int64)
|
||||
p.Int64Var(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Int64Var(target *int64) {
|
||||
p.SetValue(newInt64Value(target))
|
||||
}
|
||||
|
||||
// Int64List accumulates int64 values into a slice.
|
||||
func (p *parserMixin) Int64List() (target *[]int64) {
|
||||
target = new([]int64)
|
||||
p.Int64ListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Int64ListVar(target *[]int64) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newInt64Value(v.(*int64))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- float64 Value
|
||||
type float64Value struct{ v *float64 }
|
||||
|
||||
func newFloat64Value(p *float64) *float64Value {
|
||||
return &float64Value{p}
|
||||
}
|
||||
|
||||
func (f *float64Value) Set(s string) error {
|
||||
v, err := strconv.ParseFloat(s, 64)
|
||||
if err == nil {
|
||||
*f.v = (float64)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *float64Value) Get() interface{} { return (float64)(*f.v) }
|
||||
|
||||
func (f *float64Value) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Float64 parses the next command-line value as float64.
|
||||
func (p *parserMixin) Float64() (target *float64) {
|
||||
target = new(float64)
|
||||
p.Float64Var(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Float64Var(target *float64) {
|
||||
p.SetValue(newFloat64Value(target))
|
||||
}
|
||||
|
||||
// Float64List accumulates float64 values into a slice.
|
||||
func (p *parserMixin) Float64List() (target *[]float64) {
|
||||
target = new([]float64)
|
||||
p.Float64ListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Float64ListVar(target *[]float64) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newFloat64Value(v.(*float64))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- float32 Value
|
||||
type float32Value struct{ v *float32 }
|
||||
|
||||
func newFloat32Value(p *float32) *float32Value {
|
||||
return &float32Value{p}
|
||||
}
|
||||
|
||||
func (f *float32Value) Set(s string) error {
|
||||
v, err := strconv.ParseFloat(s, 32)
|
||||
if err == nil {
|
||||
*f.v = (float32)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *float32Value) Get() interface{} { return (float32)(*f.v) }
|
||||
|
||||
func (f *float32Value) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Float32 parses the next command-line value as float32.
|
||||
func (p *parserMixin) Float32() (target *float32) {
|
||||
target = new(float32)
|
||||
p.Float32Var(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Float32Var(target *float32) {
|
||||
p.SetValue(newFloat32Value(target))
|
||||
}
|
||||
|
||||
// Float32List accumulates float32 values into a slice.
|
||||
func (p *parserMixin) Float32List() (target *[]float32) {
|
||||
target = new([]float32)
|
||||
p.Float32ListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) Float32ListVar(target *[]float32) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newFloat32Value(v.(*float32))
|
||||
}))
|
||||
}
|
||||
|
||||
// DurationList accumulates time.Duration values into a slice.
|
||||
func (p *parserMixin) DurationList() (target *[]time.Duration) {
|
||||
target = new([]time.Duration)
|
||||
p.DurationListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) DurationListVar(target *[]time.Duration) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newDurationValue(v.(*time.Duration))
|
||||
}))
|
||||
}
|
||||
|
||||
// IPList accumulates net.IP values into a slice.
|
||||
func (p *parserMixin) IPList() (target *[]net.IP) {
|
||||
target = new([]net.IP)
|
||||
p.IPListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) IPListVar(target *[]net.IP) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newIPValue(v.(*net.IP))
|
||||
}))
|
||||
}
|
||||
|
||||
// TCPList accumulates *net.TCPAddr values into a slice.
|
||||
func (p *parserMixin) TCPList() (target *[]*net.TCPAddr) {
|
||||
target = new([]*net.TCPAddr)
|
||||
p.TCPListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) TCPListVar(target *[]*net.TCPAddr) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newTCPAddrValue(v.(**net.TCPAddr))
|
||||
}))
|
||||
}
|
||||
|
||||
// ExistingFiles accumulates string values into a slice.
|
||||
func (p *parserMixin) ExistingFiles() (target *[]string) {
|
||||
target = new([]string)
|
||||
p.ExistingFilesVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) ExistingFilesVar(target *[]string) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newExistingFileValue(v.(*string))
|
||||
}))
|
||||
}
|
||||
|
||||
// ExistingDirs accumulates string values into a slice.
|
||||
func (p *parserMixin) ExistingDirs() (target *[]string) {
|
||||
target = new([]string)
|
||||
p.ExistingDirsVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) ExistingDirsVar(target *[]string) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newExistingDirValue(v.(*string))
|
||||
}))
|
||||
}
|
||||
|
||||
// ExistingFilesOrDirs accumulates string values into a slice.
|
||||
func (p *parserMixin) ExistingFilesOrDirs() (target *[]string) {
|
||||
target = new([]string)
|
||||
p.ExistingFilesOrDirsVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) ExistingFilesOrDirsVar(target *[]string) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newExistingFileOrDirValue(v.(*string))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- *regexp.Regexp Value
|
||||
type regexpValue struct{ v **regexp.Regexp }
|
||||
|
||||
func newRegexpValue(p **regexp.Regexp) *regexpValue {
|
||||
return ®expValue{p}
|
||||
}
|
||||
|
||||
func (f *regexpValue) Set(s string) error {
|
||||
v, err := regexp.Compile(s)
|
||||
if err == nil {
|
||||
*f.v = (*regexp.Regexp)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *regexpValue) Get() interface{} { return (*regexp.Regexp)(*f.v) }
|
||||
|
||||
func (f *regexpValue) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Regexp parses the next command-line value as *regexp.Regexp.
|
||||
func (p *parserMixin) Regexp() (target **regexp.Regexp) {
|
||||
target = new(*regexp.Regexp)
|
||||
p.RegexpVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) RegexpVar(target **regexp.Regexp) {
|
||||
p.SetValue(newRegexpValue(target))
|
||||
}
|
||||
|
||||
// RegexpList accumulates *regexp.Regexp values into a slice.
|
||||
func (p *parserMixin) RegexpList() (target *[]*regexp.Regexp) {
|
||||
target = new([]*regexp.Regexp)
|
||||
p.RegexpListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) RegexpListVar(target *[]*regexp.Regexp) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newRegexpValue(v.(**regexp.Regexp))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- net.IP Value
|
||||
type resolvedIPValue struct{ v *net.IP }
|
||||
|
||||
func newResolvedIPValue(p *net.IP) *resolvedIPValue {
|
||||
return &resolvedIPValue{p}
|
||||
}
|
||||
|
||||
func (f *resolvedIPValue) Set(s string) error {
|
||||
v, err := resolveHost(s)
|
||||
if err == nil {
|
||||
*f.v = (net.IP)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *resolvedIPValue) Get() interface{} { return (net.IP)(*f.v) }
|
||||
|
||||
func (f *resolvedIPValue) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Resolve a hostname or IP to an IP.
|
||||
func (p *parserMixin) ResolvedIP() (target *net.IP) {
|
||||
target = new(net.IP)
|
||||
p.ResolvedIPVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) ResolvedIPVar(target *net.IP) {
|
||||
p.SetValue(newResolvedIPValue(target))
|
||||
}
|
||||
|
||||
// ResolvedIPList accumulates net.IP values into a slice.
|
||||
func (p *parserMixin) ResolvedIPList() (target *[]net.IP) {
|
||||
target = new([]net.IP)
|
||||
p.ResolvedIPListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) ResolvedIPListVar(target *[]net.IP) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newResolvedIPValue(v.(*net.IP))
|
||||
}))
|
||||
}
|
||||
|
||||
// -- []byte Value
|
||||
type hexBytesValue struct{ v *[]byte }
|
||||
|
||||
func newHexBytesValue(p *[]byte) *hexBytesValue {
|
||||
return &hexBytesValue{p}
|
||||
}
|
||||
|
||||
func (f *hexBytesValue) Set(s string) error {
|
||||
v, err := hex.DecodeString(s)
|
||||
if err == nil {
|
||||
*f.v = ([]byte)(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *hexBytesValue) Get() interface{} { return ([]byte)(*f.v) }
|
||||
|
||||
func (f *hexBytesValue) String() string { return fmt.Sprintf("%v", *f.v) }
|
||||
|
||||
// Bytes as a hex string.
|
||||
func (p *parserMixin) HexBytes() (target *[]byte) {
|
||||
target = new([]byte)
|
||||
p.HexBytesVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) HexBytesVar(target *[]byte) {
|
||||
p.SetValue(newHexBytesValue(target))
|
||||
}
|
||||
|
||||
// HexBytesList accumulates []byte values into a slice.
|
||||
func (p *parserMixin) HexBytesList() (target *[][]byte) {
|
||||
target = new([][]byte)
|
||||
p.HexBytesListVar(target)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parserMixin) HexBytesListVar(target *[][]byte) {
|
||||
p.SetValue(newAccumulator(target, func(v interface{}) Value {
|
||||
return newHexBytesValue(v.(*[]byte))
|
||||
}))
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (C) 2014 Alec Thomas
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
[](https://pkg.go.dev/github.com/alecthomas/units)
|
||||
|
||||
# Units - Helpful unit multipliers and functions for Go
|
||||
|
||||
The goal of this package is to have functionality similar to the [time](http://golang.org/pkg/time/) package.
|
||||
|
||||
It allows for code like this:
|
||||
|
||||
```go
|
||||
n, err := ParseBase2Bytes("1KB")
|
||||
// n == 1024
|
||||
n = units.Mebibyte * 512
|
||||
```
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
package units
|
||||
|
||||
// Base2Bytes is the old non-SI power-of-2 byte scale (1024 bytes in a kilobyte,
|
||||
// etc.).
|
||||
type Base2Bytes int64
|
||||
|
||||
// Base-2 byte units.
|
||||
const (
|
||||
Kibibyte Base2Bytes = 1024
|
||||
KiB = Kibibyte
|
||||
Mebibyte = Kibibyte * 1024
|
||||
MiB = Mebibyte
|
||||
Gibibyte = Mebibyte * 1024
|
||||
GiB = Gibibyte
|
||||
Tebibyte = Gibibyte * 1024
|
||||
TiB = Tebibyte
|
||||
Pebibyte = Tebibyte * 1024
|
||||
PiB = Pebibyte
|
||||
Exbibyte = Pebibyte * 1024
|
||||
EiB = Exbibyte
|
||||
)
|
||||
|
||||
var (
|
||||
bytesUnitMap = MakeUnitMap("iB", "B", 1024)
|
||||
oldBytesUnitMap = MakeUnitMap("B", "B", 1024)
|
||||
)
|
||||
|
||||
// ParseBase2Bytes supports both iB and B in base-2 multipliers. That is, KB
|
||||
// and KiB are both 1024.
|
||||
// However "kB", which is the correct SI spelling of 1000 Bytes, is rejected.
|
||||
func ParseBase2Bytes(s string) (Base2Bytes, error) {
|
||||
n, err := ParseUnit(s, bytesUnitMap)
|
||||
if err != nil {
|
||||
n, err = ParseUnit(s, oldBytesUnitMap)
|
||||
}
|
||||
return Base2Bytes(n), err
|
||||
}
|
||||
|
||||
func (b Base2Bytes) String() string {
|
||||
return ToString(int64(b), 1024, "iB", "B")
|
||||
}
|
||||
|
||||
// MarshalText implement encoding.TextMarshaler to process json/yaml.
|
||||
func (b Base2Bytes) MarshalText() ([]byte, error) {
|
||||
return []byte(b.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implement encoding.TextUnmarshaler to process json/yaml.
|
||||
func (b *Base2Bytes) UnmarshalText(text []byte) error {
|
||||
n, err := ParseBase2Bytes(string(text))
|
||||
*b = n
|
||||
return err
|
||||
}
|
||||
|
||||
// Floor returns Base2Bytes with all but the largest unit zeroed out. So that e.g. 1GiB1MiB1KiB → 1GiB.
|
||||
func (b Base2Bytes) Floor() Base2Bytes {
|
||||
switch {
|
||||
case b > Exbibyte:
|
||||
return (b / Exbibyte) * Exbibyte
|
||||
case b > Pebibyte:
|
||||
return (b / Pebibyte) * Pebibyte
|
||||
case b > Tebibyte:
|
||||
return (b / Tebibyte) * Tebibyte
|
||||
case b > Gibibyte:
|
||||
return (b / Gibibyte) * Gibibyte
|
||||
case b > Mebibyte:
|
||||
return (b / Mebibyte) * Mebibyte
|
||||
case b > Kibibyte:
|
||||
return (b / Kibibyte) * Kibibyte
|
||||
default:
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
// Round returns Base2Bytes with all but the first n units zeroed out. So that e.g. 1GiB1MiB1KiB → 1GiB1MiB, if n is 2.
|
||||
func (b Base2Bytes) Round(n int) Base2Bytes {
|
||||
idx := 0
|
||||
|
||||
switch {
|
||||
case b > Exbibyte:
|
||||
idx = n
|
||||
case b > Pebibyte:
|
||||
idx = n + 1
|
||||
case b > Tebibyte:
|
||||
idx = n + 2
|
||||
case b > Gibibyte:
|
||||
idx = n + 3
|
||||
case b > Mebibyte:
|
||||
idx = n + 4
|
||||
case b > Kibibyte:
|
||||
idx = n + 5
|
||||
}
|
||||
|
||||
switch idx {
|
||||
case 1:
|
||||
return b - b%Exbibyte
|
||||
case 2:
|
||||
return b - b%Pebibyte
|
||||
case 3:
|
||||
return b - b%Tebibyte
|
||||
case 4:
|
||||
return b - b%Gibibyte
|
||||
case 5:
|
||||
return b - b%Mebibyte
|
||||
case 6:
|
||||
return b - b%Kibibyte
|
||||
default:
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
var metricBytesUnitMap = MakeUnitMap("B", "B", 1000)
|
||||
|
||||
// MetricBytes are SI byte units (1000 bytes in a kilobyte).
|
||||
type MetricBytes SI
|
||||
|
||||
// SI base-10 byte units.
|
||||
const (
|
||||
Kilobyte MetricBytes = 1000
|
||||
KB = Kilobyte
|
||||
Megabyte = Kilobyte * 1000
|
||||
MB = Megabyte
|
||||
Gigabyte = Megabyte * 1000
|
||||
GB = Gigabyte
|
||||
Terabyte = Gigabyte * 1000
|
||||
TB = Terabyte
|
||||
Petabyte = Terabyte * 1000
|
||||
PB = Petabyte
|
||||
Exabyte = Petabyte * 1000
|
||||
EB = Exabyte
|
||||
)
|
||||
|
||||
// ParseMetricBytes parses base-10 metric byte units. That is, KB is 1000 bytes.
|
||||
func ParseMetricBytes(s string) (MetricBytes, error) {
|
||||
n, err := ParseUnit(s, metricBytesUnitMap)
|
||||
return MetricBytes(n), err
|
||||
}
|
||||
|
||||
// TODO: represents 1000B as uppercase "KB", while SI standard requires "kB".
|
||||
func (m MetricBytes) String() string {
|
||||
return ToString(int64(m), 1000, "B", "B")
|
||||
}
|
||||
|
||||
// Floor returns MetricBytes with all but the largest unit zeroed out. So that e.g. 1GB1MB1KB → 1GB.
|
||||
func (b MetricBytes) Floor() MetricBytes {
|
||||
switch {
|
||||
case b > Exabyte:
|
||||
return (b / Exabyte) * Exabyte
|
||||
case b > Petabyte:
|
||||
return (b / Petabyte) * Petabyte
|
||||
case b > Terabyte:
|
||||
return (b / Terabyte) * Terabyte
|
||||
case b > Gigabyte:
|
||||
return (b / Gigabyte) * Gigabyte
|
||||
case b > Megabyte:
|
||||
return (b / Megabyte) * Megabyte
|
||||
case b > Kilobyte:
|
||||
return (b / Kilobyte) * Kilobyte
|
||||
default:
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
// Round returns MetricBytes with all but the first n units zeroed out. So that e.g. 1GB1MB1KB → 1GB1MB, if n is 2.
|
||||
func (b MetricBytes) Round(n int) MetricBytes {
|
||||
idx := 0
|
||||
|
||||
switch {
|
||||
case b > Exabyte:
|
||||
idx = n
|
||||
case b > Petabyte:
|
||||
idx = n + 1
|
||||
case b > Terabyte:
|
||||
idx = n + 2
|
||||
case b > Gigabyte:
|
||||
idx = n + 3
|
||||
case b > Megabyte:
|
||||
idx = n + 4
|
||||
case b > Kilobyte:
|
||||
idx = n + 5
|
||||
}
|
||||
|
||||
switch idx {
|
||||
case 1:
|
||||
return b - b%Exabyte
|
||||
case 2:
|
||||
return b - b%Petabyte
|
||||
case 3:
|
||||
return b - b%Terabyte
|
||||
case 4:
|
||||
return b - b%Gigabyte
|
||||
case 5:
|
||||
return b - b%Megabyte
|
||||
case 6:
|
||||
return b - b%Kilobyte
|
||||
default:
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
// ParseStrictBytes supports both iB and B suffixes for base 2 and metric,
|
||||
// respectively. That is, KiB represents 1024 and kB, KB represent 1000.
|
||||
func ParseStrictBytes(s string) (int64, error) {
|
||||
n, err := ParseUnit(s, bytesUnitMap)
|
||||
if err != nil {
|
||||
n, err = ParseUnit(s, metricBytesUnitMap)
|
||||
}
|
||||
return int64(n), err
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// Package units provides helpful unit multipliers and functions for Go.
|
||||
//
|
||||
// The goal of this package is to have functionality similar to the time [1] package.
|
||||
//
|
||||
//
|
||||
// [1] http://golang.org/pkg/time/
|
||||
//
|
||||
// It allows for code like this:
|
||||
//
|
||||
// n, err := ParseBase2Bytes("1KB")
|
||||
// // n == 1024
|
||||
// n = units.Mebibyte * 512
|
||||
package units
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
$schema: "https://docs.renovatebot.com/renovate-schema.json",
|
||||
extends: [
|
||||
"config:recommended",
|
||||
":semanticCommits",
|
||||
":semanticCommitTypeAll(chore)",
|
||||
":semanticCommitScope(deps)",
|
||||
"group:allNonMajor",
|
||||
"schedule:earlyMondays", // Run once a week.
|
||||
],
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package units
|
||||
|
||||
// SI units.
|
||||
type SI int64
|
||||
|
||||
// SI unit multiples.
|
||||
const (
|
||||
Kilo SI = 1000
|
||||
Mega = Kilo * 1000
|
||||
Giga = Mega * 1000
|
||||
Tera = Giga * 1000
|
||||
Peta = Tera * 1000
|
||||
Exa = Peta * 1000
|
||||
)
|
||||
|
||||
func MakeUnitMap(suffix, shortSuffix string, scale int64) map[string]float64 {
|
||||
res := map[string]float64{
|
||||
shortSuffix: 1,
|
||||
// see below for "k" / "K"
|
||||
"M" + suffix: float64(scale * scale),
|
||||
"G" + suffix: float64(scale * scale * scale),
|
||||
"T" + suffix: float64(scale * scale * scale * scale),
|
||||
"P" + suffix: float64(scale * scale * scale * scale * scale),
|
||||
"E" + suffix: float64(scale * scale * scale * scale * scale * scale),
|
||||
}
|
||||
|
||||
// Standard SI prefixes use lowercase "k" for kilo = 1000.
|
||||
// For compatibility, and to be fool-proof, we accept both "k" and "K" in metric mode.
|
||||
//
|
||||
// However, official binary prefixes are always capitalized - "KiB" -
|
||||
// and we specifically never parse "kB" as 1024B because:
|
||||
//
|
||||
// (1) people pedantic enough to use lowercase according to SI unlikely to abuse "k" to mean 1024 :-)
|
||||
//
|
||||
// (2) Use of capital K for 1024 was an informal tradition predating IEC prefixes:
|
||||
// "The binary meaning of the kilobyte for 1024 bytes typically uses the symbol KB, with an
|
||||
// uppercase letter K."
|
||||
// -- https://en.wikipedia.org/wiki/Kilobyte#Base_2_(1024_bytes)
|
||||
// "Capitalization of the letter K became the de facto standard for binary notation, although this
|
||||
// could not be extended to higher powers, and use of the lowercase k did persist.[13][14][15]"
|
||||
// -- https://en.wikipedia.org/wiki/Binary_prefix#History
|
||||
// See also the extensive https://en.wikipedia.org/wiki/Timeline_of_binary_prefixes.
|
||||
if scale == 1024 {
|
||||
res["K"+suffix] = float64(scale)
|
||||
} else {
|
||||
res["k"+suffix] = float64(scale)
|
||||
res["K"+suffix] = float64(scale)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package units
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
siUnits = []string{"", "K", "M", "G", "T", "P", "E"}
|
||||
)
|
||||
|
||||
func ToString(n int64, scale int64, suffix, baseSuffix string) string {
|
||||
mn := len(siUnits)
|
||||
out := make([]string, mn)
|
||||
for i, m := range siUnits {
|
||||
if n%scale != 0 || i == 0 && n == 0 {
|
||||
s := suffix
|
||||
if i == 0 {
|
||||
s = baseSuffix
|
||||
}
|
||||
out[mn-1-i] = fmt.Sprintf("%d%s%s", n%scale, m, s)
|
||||
}
|
||||
n /= scale
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return strings.Join(out, "")
|
||||
}
|
||||
|
||||
// Below code ripped straight from http://golang.org/src/pkg/time/format.go?s=33392:33438#L1123
|
||||
var errLeadingInt = errors.New("units: bad [0-9]*") // never printed
|
||||
|
||||
// leadingInt consumes the leading [0-9]* from s.
|
||||
func leadingInt(s string) (x int64, rem string, err error) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c < '0' || c > '9' {
|
||||
break
|
||||
}
|
||||
if x >= (1<<63-10)/10 {
|
||||
// overflow
|
||||
return 0, "", errLeadingInt
|
||||
}
|
||||
x = x*10 + int64(c) - '0'
|
||||
}
|
||||
return x, s[i:], nil
|
||||
}
|
||||
|
||||
func ParseUnit(s string, unitMap map[string]float64) (int64, error) {
|
||||
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
|
||||
orig := s
|
||||
f := float64(0)
|
||||
neg := false
|
||||
|
||||
// Consume [-+]?
|
||||
if s != "" {
|
||||
c := s[0]
|
||||
if c == '-' || c == '+' {
|
||||
neg = c == '-'
|
||||
s = s[1:]
|
||||
}
|
||||
}
|
||||
// Special case: if all that is left is "0", this is zero.
|
||||
if s == "0" {
|
||||
return 0, nil
|
||||
}
|
||||
if s == "" {
|
||||
return 0, errors.New("units: invalid " + orig)
|
||||
}
|
||||
for s != "" {
|
||||
g := float64(0) // this element of the sequence
|
||||
|
||||
var x int64
|
||||
var err error
|
||||
|
||||
// The next character must be [0-9.]
|
||||
if !(s[0] == '.' || ('0' <= s[0] && s[0] <= '9')) {
|
||||
return 0, errors.New("units: invalid " + orig)
|
||||
}
|
||||
// Consume [0-9]*
|
||||
pl := len(s)
|
||||
x, s, err = leadingInt(s)
|
||||
if err != nil {
|
||||
return 0, errors.New("units: invalid " + orig)
|
||||
}
|
||||
g = float64(x)
|
||||
pre := pl != len(s) // whether we consumed anything before a period
|
||||
|
||||
// Consume (\.[0-9]*)?
|
||||
post := false
|
||||
if s != "" && s[0] == '.' {
|
||||
s = s[1:]
|
||||
pl := len(s)
|
||||
x, s, err = leadingInt(s)
|
||||
if err != nil {
|
||||
return 0, errors.New("units: invalid " + orig)
|
||||
}
|
||||
scale := 1.0
|
||||
for n := pl - len(s); n > 0; n-- {
|
||||
scale *= 10
|
||||
}
|
||||
g += float64(x) / scale
|
||||
post = pl != len(s)
|
||||
}
|
||||
if !pre && !post {
|
||||
// no digits (e.g. ".s" or "-.s")
|
||||
return 0, errors.New("units: invalid " + orig)
|
||||
}
|
||||
|
||||
// Consume unit.
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c == '.' || ('0' <= c && c <= '9') {
|
||||
break
|
||||
}
|
||||
}
|
||||
u := s[:i]
|
||||
s = s[i:]
|
||||
unit, ok := unitMap[u]
|
||||
if !ok {
|
||||
return 0, errors.New("units: unknown unit " + u + " in " + orig)
|
||||
}
|
||||
|
||||
f += g * unit
|
||||
}
|
||||
|
||||
if neg {
|
||||
f = -f
|
||||
}
|
||||
if f < float64(-1<<63) || f > float64(1<<63-1) {
|
||||
return 0, errors.New("units: overflow parsing unit")
|
||||
}
|
||||
return int64(f), nil
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
|
||||
/metrics.out
|
||||
|
||||
.idea
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- "1.x"
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
install:
|
||||
- go get ./...
|
||||
|
||||
script:
|
||||
- go test ./...
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Armon Dadgar
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
go-metrics
|
||||
==========
|
||||
|
||||
This library provides a `metrics` package which can be used to instrument code,
|
||||
expose application metrics, and profile runtime performance in a flexible manner.
|
||||
|
||||
Current API: [](https://godoc.org/github.com/armon/go-metrics)
|
||||
|
||||
Sinks
|
||||
-----
|
||||
|
||||
The `metrics` package makes use of a `MetricSink` interface to support delivery
|
||||
to any type of backend. Currently the following sinks are provided:
|
||||
|
||||
* StatsiteSink : Sinks to a [statsite](https://github.com/armon/statsite/) instance (TCP)
|
||||
* StatsdSink: Sinks to a [StatsD](https://github.com/etsy/statsd/) / statsite instance (UDP)
|
||||
* PrometheusSink: Sinks to a [Prometheus](http://prometheus.io/) metrics endpoint (exposed via HTTP for scrapes)
|
||||
* InmemSink : Provides in-memory aggregation, can be used to export stats
|
||||
* FanoutSink : Sinks to multiple sinks. Enables writing to multiple statsite instances for example.
|
||||
* BlackholeSink : Sinks to nowhere
|
||||
|
||||
In addition to the sinks, the `InmemSignal` can be used to catch a signal,
|
||||
and dump a formatted output of recent metrics. For example, when a process gets
|
||||
a SIGUSR1, it can dump to stderr recent performance metrics for debugging.
|
||||
|
||||
Labels
|
||||
------
|
||||
|
||||
Most metrics do have an equivalent ending with `WithLabels`, such methods
|
||||
allow to push metrics with labels and use some features of underlying Sinks
|
||||
(ex: translated into Prometheus labels).
|
||||
|
||||
Since some of these labels may increase greatly cardinality of metrics, the
|
||||
library allow to filter labels using a blacklist/whitelist filtering system
|
||||
which is global to all metrics.
|
||||
|
||||
* If `Config.AllowedLabels` is not nil, then only labels specified in this value will be sent to underlying Sink, otherwise, all labels are sent by default.
|
||||
* If `Config.BlockedLabels` is not nil, any label specified in this value will not be sent to underlying Sinks.
|
||||
|
||||
By default, both `Config.AllowedLabels` and `Config.BlockedLabels` are nil, meaning that
|
||||
no tags are filetered at all, but it allow to a user to globally block some tags with high
|
||||
cardinality at application level.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Here is an example of using the package:
|
||||
|
||||
```go
|
||||
func SlowMethod() {
|
||||
// Profiling the runtime of a method
|
||||
defer metrics.MeasureSince([]string{"SlowMethod"}, time.Now())
|
||||
}
|
||||
|
||||
// Configure a statsite sink as the global metrics sink
|
||||
sink, _ := metrics.NewStatsiteSink("statsite:8125")
|
||||
metrics.NewGlobal(metrics.DefaultConfig("service-name"), sink)
|
||||
|
||||
// Emit a Key/Value pair
|
||||
metrics.EmitKey([]string{"questions", "meaning of life"}, 42)
|
||||
```
|
||||
|
||||
Here is an example of setting up a signal handler:
|
||||
|
||||
```go
|
||||
// Setup the inmem sink and signal handler
|
||||
inm := metrics.NewInmemSink(10*time.Second, time.Minute)
|
||||
sig := metrics.DefaultInmemSignal(inm)
|
||||
metrics.NewGlobal(metrics.DefaultConfig("service-name"), inm)
|
||||
|
||||
// Run some code
|
||||
inm.SetGauge([]string{"foo"}, 42)
|
||||
inm.EmitKey([]string{"bar"}, 30)
|
||||
|
||||
inm.IncrCounter([]string{"baz"}, 42)
|
||||
inm.IncrCounter([]string{"baz"}, 1)
|
||||
inm.IncrCounter([]string{"baz"}, 80)
|
||||
|
||||
inm.AddSample([]string{"method", "wow"}, 42)
|
||||
inm.AddSample([]string{"method", "wow"}, 100)
|
||||
inm.AddSample([]string{"method", "wow"}, 22)
|
||||
|
||||
....
|
||||
```
|
||||
|
||||
When a signal comes in, output like the following will be dumped to stderr:
|
||||
|
||||
[2014-01-28 14:57:33.04 -0800 PST][G] 'foo': 42.000
|
||||
[2014-01-28 14:57:33.04 -0800 PST][P] 'bar': 30.000
|
||||
[2014-01-28 14:57:33.04 -0800 PST][C] 'baz': Count: 3 Min: 1.000 Mean: 41.000 Max: 80.000 Stddev: 39.509
|
||||
[2014-01-28 14:57:33.04 -0800 PST][S] 'method.wow': Count: 3 Min: 22.000 Mean: 54.667 Max: 100.000 Stddev: 40.513
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// +build !windows
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultSignal is used with DefaultInmemSignal
|
||||
DefaultSignal = syscall.SIGUSR1
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// +build windows
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultSignal is used with DefaultInmemSignal
|
||||
// Windows has no SIGUSR1, use SIGBREAK
|
||||
DefaultSignal = syscall.Signal(21)
|
||||
)
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var spaceReplacer = strings.NewReplacer(" ", "_")
|
||||
|
||||
// InmemSink provides a MetricSink that does in-memory aggregation
|
||||
// without sending metrics over a network. It can be embedded within
|
||||
// an application to provide profiling information.
|
||||
type InmemSink struct {
|
||||
// How long is each aggregation interval
|
||||
interval time.Duration
|
||||
|
||||
// Retain controls how many metrics interval we keep
|
||||
retain time.Duration
|
||||
|
||||
// maxIntervals is the maximum length of intervals.
|
||||
// It is retain / interval.
|
||||
maxIntervals int
|
||||
|
||||
// intervals is a slice of the retained intervals
|
||||
intervals []*IntervalMetrics
|
||||
intervalLock sync.RWMutex
|
||||
|
||||
rateDenom float64
|
||||
}
|
||||
|
||||
// IntervalMetrics stores the aggregated metrics
|
||||
// for a specific interval
|
||||
type IntervalMetrics struct {
|
||||
sync.RWMutex
|
||||
|
||||
// The start time of the interval
|
||||
Interval time.Time
|
||||
|
||||
// Gauges maps the key to the last set value
|
||||
Gauges map[string]GaugeValue
|
||||
|
||||
// Points maps the string to the list of emitted values
|
||||
// from EmitKey
|
||||
Points map[string][]float32
|
||||
|
||||
// Counters maps the string key to a sum of the counter
|
||||
// values
|
||||
Counters map[string]SampledValue
|
||||
|
||||
// Samples maps the key to an AggregateSample,
|
||||
// which has the rolled up view of a sample
|
||||
Samples map[string]SampledValue
|
||||
|
||||
// done is closed when this interval has ended, and a new IntervalMetrics
|
||||
// has been created to receive any future metrics.
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// NewIntervalMetrics creates a new IntervalMetrics for a given interval
|
||||
func NewIntervalMetrics(intv time.Time) *IntervalMetrics {
|
||||
return &IntervalMetrics{
|
||||
Interval: intv,
|
||||
Gauges: make(map[string]GaugeValue),
|
||||
Points: make(map[string][]float32),
|
||||
Counters: make(map[string]SampledValue),
|
||||
Samples: make(map[string]SampledValue),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// AggregateSample is used to hold aggregate metrics
|
||||
// about a sample
|
||||
type AggregateSample struct {
|
||||
Count int // The count of emitted pairs
|
||||
Rate float64 // The values rate per time unit (usually 1 second)
|
||||
Sum float64 // The sum of values
|
||||
SumSq float64 `json:"-"` // The sum of squared values
|
||||
Min float64 // Minimum value
|
||||
Max float64 // Maximum value
|
||||
LastUpdated time.Time `json:"-"` // When value was last updated
|
||||
}
|
||||
|
||||
// Computes a Stddev of the values
|
||||
func (a *AggregateSample) Stddev() float64 {
|
||||
num := (float64(a.Count) * a.SumSq) - math.Pow(a.Sum, 2)
|
||||
div := float64(a.Count * (a.Count - 1))
|
||||
if div == 0 {
|
||||
return 0
|
||||
}
|
||||
return math.Sqrt(num / div)
|
||||
}
|
||||
|
||||
// Computes a mean of the values
|
||||
func (a *AggregateSample) Mean() float64 {
|
||||
if a.Count == 0 {
|
||||
return 0
|
||||
}
|
||||
return a.Sum / float64(a.Count)
|
||||
}
|
||||
|
||||
// Ingest is used to update a sample
|
||||
func (a *AggregateSample) Ingest(v float64, rateDenom float64) {
|
||||
a.Count++
|
||||
a.Sum += v
|
||||
a.SumSq += (v * v)
|
||||
if v < a.Min || a.Count == 1 {
|
||||
a.Min = v
|
||||
}
|
||||
if v > a.Max || a.Count == 1 {
|
||||
a.Max = v
|
||||
}
|
||||
a.Rate = float64(a.Sum) / rateDenom
|
||||
a.LastUpdated = time.Now()
|
||||
}
|
||||
|
||||
func (a *AggregateSample) String() string {
|
||||
if a.Count == 0 {
|
||||
return "Count: 0"
|
||||
} else if a.Stddev() == 0 {
|
||||
return fmt.Sprintf("Count: %d Sum: %0.3f LastUpdated: %s", a.Count, a.Sum, a.LastUpdated)
|
||||
} else {
|
||||
return fmt.Sprintf("Count: %d Min: %0.3f Mean: %0.3f Max: %0.3f Stddev: %0.3f Sum: %0.3f LastUpdated: %s",
|
||||
a.Count, a.Min, a.Mean(), a.Max, a.Stddev(), a.Sum, a.LastUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
// NewInmemSinkFromURL creates an InmemSink from a URL. It is used
|
||||
// (and tested) from NewMetricSinkFromURL.
|
||||
func NewInmemSinkFromURL(u *url.URL) (MetricSink, error) {
|
||||
params := u.Query()
|
||||
|
||||
interval, err := time.ParseDuration(params.Get("interval"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Bad 'interval' param: %s", err)
|
||||
}
|
||||
|
||||
retain, err := time.ParseDuration(params.Get("retain"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Bad 'retain' param: %s", err)
|
||||
}
|
||||
|
||||
return NewInmemSink(interval, retain), nil
|
||||
}
|
||||
|
||||
// NewInmemSink is used to construct a new in-memory sink.
|
||||
// Uses an aggregation interval and maximum retention period.
|
||||
func NewInmemSink(interval, retain time.Duration) *InmemSink {
|
||||
rateTimeUnit := time.Second
|
||||
i := &InmemSink{
|
||||
interval: interval,
|
||||
retain: retain,
|
||||
maxIntervals: int(retain / interval),
|
||||
rateDenom: float64(interval.Nanoseconds()) / float64(rateTimeUnit.Nanoseconds()),
|
||||
}
|
||||
i.intervals = make([]*IntervalMetrics, 0, i.maxIntervals)
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *InmemSink) SetGauge(key []string, val float32) {
|
||||
i.SetGaugeWithLabels(key, val, nil)
|
||||
}
|
||||
|
||||
func (i *InmemSink) SetGaugeWithLabels(key []string, val float32, labels []Label) {
|
||||
k, name := i.flattenKeyLabels(key, labels)
|
||||
intv := i.getInterval()
|
||||
|
||||
intv.Lock()
|
||||
defer intv.Unlock()
|
||||
intv.Gauges[k] = GaugeValue{Name: name, Value: val, Labels: labels}
|
||||
}
|
||||
|
||||
func (i *InmemSink) EmitKey(key []string, val float32) {
|
||||
k := i.flattenKey(key)
|
||||
intv := i.getInterval()
|
||||
|
||||
intv.Lock()
|
||||
defer intv.Unlock()
|
||||
vals := intv.Points[k]
|
||||
intv.Points[k] = append(vals, val)
|
||||
}
|
||||
|
||||
func (i *InmemSink) IncrCounter(key []string, val float32) {
|
||||
i.IncrCounterWithLabels(key, val, nil)
|
||||
}
|
||||
|
||||
func (i *InmemSink) IncrCounterWithLabels(key []string, val float32, labels []Label) {
|
||||
k, name := i.flattenKeyLabels(key, labels)
|
||||
intv := i.getInterval()
|
||||
|
||||
intv.Lock()
|
||||
defer intv.Unlock()
|
||||
|
||||
agg, ok := intv.Counters[k]
|
||||
if !ok {
|
||||
agg = SampledValue{
|
||||
Name: name,
|
||||
AggregateSample: &AggregateSample{},
|
||||
Labels: labels,
|
||||
}
|
||||
intv.Counters[k] = agg
|
||||
}
|
||||
agg.Ingest(float64(val), i.rateDenom)
|
||||
}
|
||||
|
||||
func (i *InmemSink) AddSample(key []string, val float32) {
|
||||
i.AddSampleWithLabels(key, val, nil)
|
||||
}
|
||||
|
||||
func (i *InmemSink) AddSampleWithLabels(key []string, val float32, labels []Label) {
|
||||
k, name := i.flattenKeyLabels(key, labels)
|
||||
intv := i.getInterval()
|
||||
|
||||
intv.Lock()
|
||||
defer intv.Unlock()
|
||||
|
||||
agg, ok := intv.Samples[k]
|
||||
if !ok {
|
||||
agg = SampledValue{
|
||||
Name: name,
|
||||
AggregateSample: &AggregateSample{},
|
||||
Labels: labels,
|
||||
}
|
||||
intv.Samples[k] = agg
|
||||
}
|
||||
agg.Ingest(float64(val), i.rateDenom)
|
||||
}
|
||||
|
||||
// Data is used to retrieve all the aggregated metrics
|
||||
// Intervals may be in use, and a read lock should be acquired
|
||||
func (i *InmemSink) Data() []*IntervalMetrics {
|
||||
// Get the current interval, forces creation
|
||||
i.getInterval()
|
||||
|
||||
i.intervalLock.RLock()
|
||||
defer i.intervalLock.RUnlock()
|
||||
|
||||
n := len(i.intervals)
|
||||
intervals := make([]*IntervalMetrics, n)
|
||||
|
||||
copy(intervals[:n-1], i.intervals[:n-1])
|
||||
current := i.intervals[n-1]
|
||||
|
||||
// make its own copy for current interval
|
||||
intervals[n-1] = &IntervalMetrics{}
|
||||
copyCurrent := intervals[n-1]
|
||||
current.RLock()
|
||||
*copyCurrent = *current
|
||||
// RWMutex is not safe to copy, so create a new instance on the copy
|
||||
copyCurrent.RWMutex = sync.RWMutex{}
|
||||
|
||||
copyCurrent.Gauges = make(map[string]GaugeValue, len(current.Gauges))
|
||||
for k, v := range current.Gauges {
|
||||
copyCurrent.Gauges[k] = v
|
||||
}
|
||||
// saved values will be not change, just copy its link
|
||||
copyCurrent.Points = make(map[string][]float32, len(current.Points))
|
||||
for k, v := range current.Points {
|
||||
copyCurrent.Points[k] = v
|
||||
}
|
||||
copyCurrent.Counters = make(map[string]SampledValue, len(current.Counters))
|
||||
for k, v := range current.Counters {
|
||||
copyCurrent.Counters[k] = v.deepCopy()
|
||||
}
|
||||
copyCurrent.Samples = make(map[string]SampledValue, len(current.Samples))
|
||||
for k, v := range current.Samples {
|
||||
copyCurrent.Samples[k] = v.deepCopy()
|
||||
}
|
||||
current.RUnlock()
|
||||
|
||||
return intervals
|
||||
}
|
||||
|
||||
// getInterval returns the current interval. A new interval is created if no
|
||||
// previous interval exists, or if the current time is beyond the window for the
|
||||
// current interval.
|
||||
func (i *InmemSink) getInterval() *IntervalMetrics {
|
||||
intv := time.Now().Truncate(i.interval)
|
||||
|
||||
// Attempt to return the existing interval first, because it only requires
|
||||
// a read lock.
|
||||
i.intervalLock.RLock()
|
||||
n := len(i.intervals)
|
||||
if n > 0 && i.intervals[n-1].Interval == intv {
|
||||
defer i.intervalLock.RUnlock()
|
||||
return i.intervals[n-1]
|
||||
}
|
||||
i.intervalLock.RUnlock()
|
||||
|
||||
i.intervalLock.Lock()
|
||||
defer i.intervalLock.Unlock()
|
||||
|
||||
// Re-check for an existing interval now that the lock is re-acquired.
|
||||
n = len(i.intervals)
|
||||
if n > 0 && i.intervals[n-1].Interval == intv {
|
||||
return i.intervals[n-1]
|
||||
}
|
||||
|
||||
current := NewIntervalMetrics(intv)
|
||||
i.intervals = append(i.intervals, current)
|
||||
if n > 0 {
|
||||
close(i.intervals[n-1].done)
|
||||
}
|
||||
|
||||
n++
|
||||
// Prune old intervals if the count exceeds the max.
|
||||
if n >= i.maxIntervals {
|
||||
copy(i.intervals[0:], i.intervals[n-i.maxIntervals:])
|
||||
i.intervals = i.intervals[:i.maxIntervals]
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
// Flattens the key for formatting, removes spaces
|
||||
func (i *InmemSink) flattenKey(parts []string) string {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
joined := strings.Join(parts, ".")
|
||||
|
||||
spaceReplacer.WriteString(buf, joined)
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Flattens the key for formatting along with its labels, removes spaces
|
||||
func (i *InmemSink) flattenKeyLabels(parts []string, labels []Label) (string, string) {
|
||||
key := i.flattenKey(parts)
|
||||
buf := bytes.NewBufferString(key)
|
||||
|
||||
for _, label := range labels {
|
||||
spaceReplacer.WriteString(buf, fmt.Sprintf(";%s=%s", label.Name, label.Value))
|
||||
}
|
||||
|
||||
return buf.String(), key
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MetricsSummary holds a roll-up of metrics info for a given interval
|
||||
type MetricsSummary struct {
|
||||
Timestamp string
|
||||
Gauges []GaugeValue
|
||||
Points []PointValue
|
||||
Counters []SampledValue
|
||||
Samples []SampledValue
|
||||
}
|
||||
|
||||
type GaugeValue struct {
|
||||
Name string
|
||||
Hash string `json:"-"`
|
||||
Value float32
|
||||
|
||||
Labels []Label `json:"-"`
|
||||
DisplayLabels map[string]string `json:"Labels"`
|
||||
}
|
||||
|
||||
type PointValue struct {
|
||||
Name string
|
||||
Points []float32
|
||||
}
|
||||
|
||||
type SampledValue struct {
|
||||
Name string
|
||||
Hash string `json:"-"`
|
||||
*AggregateSample
|
||||
Mean float64
|
||||
Stddev float64
|
||||
|
||||
Labels []Label `json:"-"`
|
||||
DisplayLabels map[string]string `json:"Labels"`
|
||||
}
|
||||
|
||||
// deepCopy allocates a new instance of AggregateSample
|
||||
func (source *SampledValue) deepCopy() SampledValue {
|
||||
dest := *source
|
||||
if source.AggregateSample != nil {
|
||||
dest.AggregateSample = &AggregateSample{}
|
||||
*dest.AggregateSample = *source.AggregateSample
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
// DisplayMetrics returns a summary of the metrics from the most recent finished interval.
|
||||
func (i *InmemSink) DisplayMetrics(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
data := i.Data()
|
||||
|
||||
var interval *IntervalMetrics
|
||||
n := len(data)
|
||||
switch {
|
||||
case n == 0:
|
||||
return nil, fmt.Errorf("no metric intervals have been initialized yet")
|
||||
case n == 1:
|
||||
// Show the current interval if it's all we have
|
||||
interval = data[0]
|
||||
default:
|
||||
// Show the most recent finished interval if we have one
|
||||
interval = data[n-2]
|
||||
}
|
||||
|
||||
return newMetricSummaryFromInterval(interval), nil
|
||||
}
|
||||
|
||||
func newMetricSummaryFromInterval(interval *IntervalMetrics) MetricsSummary {
|
||||
interval.RLock()
|
||||
defer interval.RUnlock()
|
||||
|
||||
summary := MetricsSummary{
|
||||
Timestamp: interval.Interval.Round(time.Second).UTC().String(),
|
||||
Gauges: make([]GaugeValue, 0, len(interval.Gauges)),
|
||||
Points: make([]PointValue, 0, len(interval.Points)),
|
||||
}
|
||||
|
||||
// Format and sort the output of each metric type, so it gets displayed in a
|
||||
// deterministic order.
|
||||
for name, points := range interval.Points {
|
||||
summary.Points = append(summary.Points, PointValue{name, points})
|
||||
}
|
||||
sort.Slice(summary.Points, func(i, j int) bool {
|
||||
return summary.Points[i].Name < summary.Points[j].Name
|
||||
})
|
||||
|
||||
for hash, value := range interval.Gauges {
|
||||
value.Hash = hash
|
||||
value.DisplayLabels = make(map[string]string)
|
||||
for _, label := range value.Labels {
|
||||
value.DisplayLabels[label.Name] = label.Value
|
||||
}
|
||||
value.Labels = nil
|
||||
|
||||
summary.Gauges = append(summary.Gauges, value)
|
||||
}
|
||||
sort.Slice(summary.Gauges, func(i, j int) bool {
|
||||
return summary.Gauges[i].Hash < summary.Gauges[j].Hash
|
||||
})
|
||||
|
||||
summary.Counters = formatSamples(interval.Counters)
|
||||
summary.Samples = formatSamples(interval.Samples)
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
func formatSamples(source map[string]SampledValue) []SampledValue {
|
||||
output := make([]SampledValue, 0, len(source))
|
||||
for hash, sample := range source {
|
||||
displayLabels := make(map[string]string)
|
||||
for _, label := range sample.Labels {
|
||||
displayLabels[label.Name] = label.Value
|
||||
}
|
||||
|
||||
output = append(output, SampledValue{
|
||||
Name: sample.Name,
|
||||
Hash: hash,
|
||||
AggregateSample: sample.AggregateSample,
|
||||
Mean: sample.AggregateSample.Mean(),
|
||||
Stddev: sample.AggregateSample.Stddev(),
|
||||
DisplayLabels: displayLabels,
|
||||
})
|
||||
}
|
||||
sort.Slice(output, func(i, j int) bool {
|
||||
return output[i].Hash < output[j].Hash
|
||||
})
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
type Encoder interface {
|
||||
Encode(interface{}) error
|
||||
}
|
||||
|
||||
// Stream writes metrics using encoder.Encode each time an interval ends. Runs
|
||||
// until the request context is cancelled, or the encoder returns an error.
|
||||
// The caller is responsible for logging any errors from encoder.
|
||||
func (i *InmemSink) Stream(ctx context.Context, encoder Encoder) {
|
||||
interval := i.getInterval()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-interval.done:
|
||||
summary := newMetricSummaryFromInterval(interval)
|
||||
if err := encoder.Encode(summary); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// update interval to the next one
|
||||
interval = i.getInterval()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// InmemSignal is used to listen for a given signal, and when received,
|
||||
// to dump the current metrics from the InmemSink to an io.Writer
|
||||
type InmemSignal struct {
|
||||
signal syscall.Signal
|
||||
inm *InmemSink
|
||||
w io.Writer
|
||||
sigCh chan os.Signal
|
||||
|
||||
stop bool
|
||||
stopCh chan struct{}
|
||||
stopLock sync.Mutex
|
||||
}
|
||||
|
||||
// NewInmemSignal creates a new InmemSignal which listens for a given signal,
|
||||
// and dumps the current metrics out to a writer
|
||||
func NewInmemSignal(inmem *InmemSink, sig syscall.Signal, w io.Writer) *InmemSignal {
|
||||
i := &InmemSignal{
|
||||
signal: sig,
|
||||
inm: inmem,
|
||||
w: w,
|
||||
sigCh: make(chan os.Signal, 1),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
signal.Notify(i.sigCh, sig)
|
||||
go i.run()
|
||||
return i
|
||||
}
|
||||
|
||||
// DefaultInmemSignal returns a new InmemSignal that responds to SIGUSR1
|
||||
// and writes output to stderr. Windows uses SIGBREAK
|
||||
func DefaultInmemSignal(inmem *InmemSink) *InmemSignal {
|
||||
return NewInmemSignal(inmem, DefaultSignal, os.Stderr)
|
||||
}
|
||||
|
||||
// Stop is used to stop the InmemSignal from listening
|
||||
func (i *InmemSignal) Stop() {
|
||||
i.stopLock.Lock()
|
||||
defer i.stopLock.Unlock()
|
||||
|
||||
if i.stop {
|
||||
return
|
||||
}
|
||||
i.stop = true
|
||||
close(i.stopCh)
|
||||
signal.Stop(i.sigCh)
|
||||
}
|
||||
|
||||
// run is a long running routine that handles signals
|
||||
func (i *InmemSignal) run() {
|
||||
for {
|
||||
select {
|
||||
case <-i.sigCh:
|
||||
i.dumpStats()
|
||||
case <-i.stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dumpStats is used to dump the data to output writer
|
||||
func (i *InmemSignal) dumpStats() {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
data := i.inm.Data()
|
||||
// Skip the last period which is still being aggregated
|
||||
for j := 0; j < len(data)-1; j++ {
|
||||
intv := data[j]
|
||||
intv.RLock()
|
||||
for _, val := range intv.Gauges {
|
||||
name := i.flattenLabels(val.Name, val.Labels)
|
||||
fmt.Fprintf(buf, "[%v][G] '%s': %0.3f\n", intv.Interval, name, val.Value)
|
||||
}
|
||||
for name, vals := range intv.Points {
|
||||
for _, val := range vals {
|
||||
fmt.Fprintf(buf, "[%v][P] '%s': %0.3f\n", intv.Interval, name, val)
|
||||
}
|
||||
}
|
||||
for _, agg := range intv.Counters {
|
||||
name := i.flattenLabels(agg.Name, agg.Labels)
|
||||
fmt.Fprintf(buf, "[%v][C] '%s': %s\n", intv.Interval, name, agg.AggregateSample)
|
||||
}
|
||||
for _, agg := range intv.Samples {
|
||||
name := i.flattenLabels(agg.Name, agg.Labels)
|
||||
fmt.Fprintf(buf, "[%v][S] '%s': %s\n", intv.Interval, name, agg.AggregateSample)
|
||||
}
|
||||
intv.RUnlock()
|
||||
}
|
||||
|
||||
// Write out the bytes
|
||||
i.w.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
// Flattens the key for formatting along with its labels, removes spaces
|
||||
func (i *InmemSignal) flattenLabels(name string, labels []Label) string {
|
||||
buf := bytes.NewBufferString(name)
|
||||
replacer := strings.NewReplacer(" ", "_", ":", "_")
|
||||
|
||||
for _, label := range labels {
|
||||
replacer.WriteString(buf, ".")
|
||||
replacer.WriteString(buf, label.Value)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
iradix "github.com/hashicorp/go-immutable-radix"
|
||||
)
|
||||
|
||||
type Label struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (m *Metrics) SetGauge(key []string, val float32) {
|
||||
m.SetGaugeWithLabels(key, val, nil)
|
||||
}
|
||||
|
||||
func (m *Metrics) SetGaugeWithLabels(key []string, val float32, labels []Label) {
|
||||
if m.HostName != "" {
|
||||
if m.EnableHostnameLabel {
|
||||
labels = append(labels, Label{"host", m.HostName})
|
||||
} else if m.EnableHostname {
|
||||
key = insert(0, m.HostName, key)
|
||||
}
|
||||
}
|
||||
if m.EnableTypePrefix {
|
||||
key = insert(0, "gauge", key)
|
||||
}
|
||||
if m.ServiceName != "" {
|
||||
if m.EnableServiceLabel {
|
||||
labels = append(labels, Label{"service", m.ServiceName})
|
||||
} else {
|
||||
key = insert(0, m.ServiceName, key)
|
||||
}
|
||||
}
|
||||
allowed, labelsFiltered := m.allowMetric(key, labels)
|
||||
if !allowed {
|
||||
return
|
||||
}
|
||||
m.sink.SetGaugeWithLabels(key, val, labelsFiltered)
|
||||
}
|
||||
|
||||
func (m *Metrics) EmitKey(key []string, val float32) {
|
||||
if m.EnableTypePrefix {
|
||||
key = insert(0, "kv", key)
|
||||
}
|
||||
if m.ServiceName != "" {
|
||||
key = insert(0, m.ServiceName, key)
|
||||
}
|
||||
allowed, _ := m.allowMetric(key, nil)
|
||||
if !allowed {
|
||||
return
|
||||
}
|
||||
m.sink.EmitKey(key, val)
|
||||
}
|
||||
|
||||
func (m *Metrics) IncrCounter(key []string, val float32) {
|
||||
m.IncrCounterWithLabels(key, val, nil)
|
||||
}
|
||||
|
||||
func (m *Metrics) IncrCounterWithLabels(key []string, val float32, labels []Label) {
|
||||
if m.HostName != "" && m.EnableHostnameLabel {
|
||||
labels = append(labels, Label{"host", m.HostName})
|
||||
}
|
||||
if m.EnableTypePrefix {
|
||||
key = insert(0, "counter", key)
|
||||
}
|
||||
if m.ServiceName != "" {
|
||||
if m.EnableServiceLabel {
|
||||
labels = append(labels, Label{"service", m.ServiceName})
|
||||
} else {
|
||||
key = insert(0, m.ServiceName, key)
|
||||
}
|
||||
}
|
||||
allowed, labelsFiltered := m.allowMetric(key, labels)
|
||||
if !allowed {
|
||||
return
|
||||
}
|
||||
m.sink.IncrCounterWithLabels(key, val, labelsFiltered)
|
||||
}
|
||||
|
||||
func (m *Metrics) AddSample(key []string, val float32) {
|
||||
m.AddSampleWithLabels(key, val, nil)
|
||||
}
|
||||
|
||||
func (m *Metrics) AddSampleWithLabels(key []string, val float32, labels []Label) {
|
||||
if m.HostName != "" && m.EnableHostnameLabel {
|
||||
labels = append(labels, Label{"host", m.HostName})
|
||||
}
|
||||
if m.EnableTypePrefix {
|
||||
key = insert(0, "sample", key)
|
||||
}
|
||||
if m.ServiceName != "" {
|
||||
if m.EnableServiceLabel {
|
||||
labels = append(labels, Label{"service", m.ServiceName})
|
||||
} else {
|
||||
key = insert(0, m.ServiceName, key)
|
||||
}
|
||||
}
|
||||
allowed, labelsFiltered := m.allowMetric(key, labels)
|
||||
if !allowed {
|
||||
return
|
||||
}
|
||||
m.sink.AddSampleWithLabels(key, val, labelsFiltered)
|
||||
}
|
||||
|
||||
func (m *Metrics) MeasureSince(key []string, start time.Time) {
|
||||
m.MeasureSinceWithLabels(key, start, nil)
|
||||
}
|
||||
|
||||
func (m *Metrics) MeasureSinceWithLabels(key []string, start time.Time, labels []Label) {
|
||||
if m.HostName != "" && m.EnableHostnameLabel {
|
||||
labels = append(labels, Label{"host", m.HostName})
|
||||
}
|
||||
if m.EnableTypePrefix {
|
||||
key = insert(0, "timer", key)
|
||||
}
|
||||
if m.ServiceName != "" {
|
||||
if m.EnableServiceLabel {
|
||||
labels = append(labels, Label{"service", m.ServiceName})
|
||||
} else {
|
||||
key = insert(0, m.ServiceName, key)
|
||||
}
|
||||
}
|
||||
allowed, labelsFiltered := m.allowMetric(key, labels)
|
||||
if !allowed {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
elapsed := now.Sub(start)
|
||||
msec := float32(elapsed.Nanoseconds()) / float32(m.TimerGranularity)
|
||||
m.sink.AddSampleWithLabels(key, msec, labelsFiltered)
|
||||
}
|
||||
|
||||
// UpdateFilter overwrites the existing filter with the given rules.
|
||||
func (m *Metrics) UpdateFilter(allow, block []string) {
|
||||
m.UpdateFilterAndLabels(allow, block, m.AllowedLabels, m.BlockedLabels)
|
||||
}
|
||||
|
||||
// UpdateFilterAndLabels overwrites the existing filter with the given rules.
|
||||
func (m *Metrics) UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels []string) {
|
||||
m.filterLock.Lock()
|
||||
defer m.filterLock.Unlock()
|
||||
|
||||
m.AllowedPrefixes = allow
|
||||
m.BlockedPrefixes = block
|
||||
|
||||
if allowedLabels == nil {
|
||||
// Having a white list means we take only elements from it
|
||||
m.allowedLabels = nil
|
||||
} else {
|
||||
m.allowedLabels = make(map[string]bool)
|
||||
for _, v := range allowedLabels {
|
||||
m.allowedLabels[v] = true
|
||||
}
|
||||
}
|
||||
m.blockedLabels = make(map[string]bool)
|
||||
for _, v := range blockedLabels {
|
||||
m.blockedLabels[v] = true
|
||||
}
|
||||
m.AllowedLabels = allowedLabels
|
||||
m.BlockedLabels = blockedLabels
|
||||
|
||||
m.filter = iradix.New()
|
||||
for _, prefix := range m.AllowedPrefixes {
|
||||
m.filter, _, _ = m.filter.Insert([]byte(prefix), true)
|
||||
}
|
||||
for _, prefix := range m.BlockedPrefixes {
|
||||
m.filter, _, _ = m.filter.Insert([]byte(prefix), false)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Metrics) Shutdown() {
|
||||
if ss, ok := m.sink.(ShutdownSink); ok {
|
||||
ss.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
// labelIsAllowed return true if a should be included in metric
|
||||
// the caller should lock m.filterLock while calling this method
|
||||
func (m *Metrics) labelIsAllowed(label *Label) bool {
|
||||
labelName := (*label).Name
|
||||
if m.blockedLabels != nil {
|
||||
_, ok := m.blockedLabels[labelName]
|
||||
if ok {
|
||||
// If present, let's remove this label
|
||||
return false
|
||||
}
|
||||
}
|
||||
if m.allowedLabels != nil {
|
||||
_, ok := m.allowedLabels[labelName]
|
||||
return ok
|
||||
}
|
||||
// Allow by default
|
||||
return true
|
||||
}
|
||||
|
||||
// filterLabels return only allowed labels
|
||||
// the caller should lock m.filterLock while calling this method
|
||||
func (m *Metrics) filterLabels(labels []Label) []Label {
|
||||
if labels == nil {
|
||||
return nil
|
||||
}
|
||||
toReturn := []Label{}
|
||||
for _, label := range labels {
|
||||
if m.labelIsAllowed(&label) {
|
||||
toReturn = append(toReturn, label)
|
||||
}
|
||||
}
|
||||
return toReturn
|
||||
}
|
||||
|
||||
// Returns whether the metric should be allowed based on configured prefix filters
|
||||
// Also return the applicable labels
|
||||
func (m *Metrics) allowMetric(key []string, labels []Label) (bool, []Label) {
|
||||
m.filterLock.RLock()
|
||||
defer m.filterLock.RUnlock()
|
||||
|
||||
if m.filter == nil || m.filter.Len() == 0 {
|
||||
return m.Config.FilterDefault, m.filterLabels(labels)
|
||||
}
|
||||
|
||||
_, allowed, ok := m.filter.Root().LongestPrefix([]byte(strings.Join(key, ".")))
|
||||
if !ok {
|
||||
return m.Config.FilterDefault, m.filterLabels(labels)
|
||||
}
|
||||
|
||||
return allowed.(bool), m.filterLabels(labels)
|
||||
}
|
||||
|
||||
// Periodically collects runtime stats to publish
|
||||
func (m *Metrics) collectStats() {
|
||||
for {
|
||||
time.Sleep(m.ProfileInterval)
|
||||
m.EmitRuntimeStats()
|
||||
}
|
||||
}
|
||||
|
||||
// Emits various runtime statsitics
|
||||
func (m *Metrics) EmitRuntimeStats() {
|
||||
// Export number of Goroutines
|
||||
numRoutines := runtime.NumGoroutine()
|
||||
m.SetGauge([]string{"runtime", "num_goroutines"}, float32(numRoutines))
|
||||
|
||||
// Export memory stats
|
||||
var stats runtime.MemStats
|
||||
runtime.ReadMemStats(&stats)
|
||||
m.SetGauge([]string{"runtime", "alloc_bytes"}, float32(stats.Alloc))
|
||||
m.SetGauge([]string{"runtime", "sys_bytes"}, float32(stats.Sys))
|
||||
m.SetGauge([]string{"runtime", "malloc_count"}, float32(stats.Mallocs))
|
||||
m.SetGauge([]string{"runtime", "free_count"}, float32(stats.Frees))
|
||||
m.SetGauge([]string{"runtime", "heap_objects"}, float32(stats.HeapObjects))
|
||||
m.SetGauge([]string{"runtime", "total_gc_pause_ns"}, float32(stats.PauseTotalNs))
|
||||
m.SetGauge([]string{"runtime", "total_gc_runs"}, float32(stats.NumGC))
|
||||
|
||||
// Export info about the last few GC runs
|
||||
num := stats.NumGC
|
||||
|
||||
// Handle wrap around
|
||||
if num < m.lastNumGC {
|
||||
m.lastNumGC = 0
|
||||
}
|
||||
|
||||
// Ensure we don't scan more than 256
|
||||
if num-m.lastNumGC >= 256 {
|
||||
m.lastNumGC = num - 255
|
||||
}
|
||||
|
||||
for i := m.lastNumGC; i < num; i++ {
|
||||
pause := stats.PauseNs[i%256]
|
||||
m.AddSample([]string{"runtime", "gc_pause_ns"}, float32(pause))
|
||||
}
|
||||
m.lastNumGC = num
|
||||
}
|
||||
|
||||
// Creates a new slice with the provided string value as the first element
|
||||
// and the provided slice values as the remaining values.
|
||||
// Ordering of the values in the provided input slice is kept in tact in the output slice.
|
||||
func insert(i int, v string, s []string) []string {
|
||||
// Allocate new slice to avoid modifying the input slice
|
||||
newS := make([]string, len(s)+1)
|
||||
|
||||
// Copy s[0, i-1] into newS
|
||||
for j := 0; j < i; j++ {
|
||||
newS[j] = s[j]
|
||||
}
|
||||
|
||||
// Insert provided element at index i
|
||||
newS[i] = v
|
||||
|
||||
// Copy s[i, len(s)-1] into newS starting at newS[i+1]
|
||||
for j := i; j < len(s); j++ {
|
||||
newS[j+1] = s[j]
|
||||
}
|
||||
|
||||
return newS
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// The MetricSink interface is used to transmit metrics information
|
||||
// to an external system
|
||||
type MetricSink interface {
|
||||
// A Gauge should retain the last value it is set to
|
||||
SetGauge(key []string, val float32)
|
||||
SetGaugeWithLabels(key []string, val float32, labels []Label)
|
||||
|
||||
// Should emit a Key/Value pair for each call
|
||||
EmitKey(key []string, val float32)
|
||||
|
||||
// Counters should accumulate values
|
||||
IncrCounter(key []string, val float32)
|
||||
IncrCounterWithLabels(key []string, val float32, labels []Label)
|
||||
|
||||
// Samples are for timing information, where quantiles are used
|
||||
AddSample(key []string, val float32)
|
||||
AddSampleWithLabels(key []string, val float32, labels []Label)
|
||||
}
|
||||
|
||||
type ShutdownSink interface {
|
||||
MetricSink
|
||||
|
||||
// Shutdown the metric sink, flush metrics to storage, and cleanup resources.
|
||||
// Called immediately prior to application exit. Implementations must block
|
||||
// until metrics are flushed to storage.
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
// BlackholeSink is used to just blackhole messages
|
||||
type BlackholeSink struct{}
|
||||
|
||||
func (*BlackholeSink) SetGauge(key []string, val float32) {}
|
||||
func (*BlackholeSink) SetGaugeWithLabels(key []string, val float32, labels []Label) {}
|
||||
func (*BlackholeSink) EmitKey(key []string, val float32) {}
|
||||
func (*BlackholeSink) IncrCounter(key []string, val float32) {}
|
||||
func (*BlackholeSink) IncrCounterWithLabels(key []string, val float32, labels []Label) {}
|
||||
func (*BlackholeSink) AddSample(key []string, val float32) {}
|
||||
func (*BlackholeSink) AddSampleWithLabels(key []string, val float32, labels []Label) {}
|
||||
|
||||
// FanoutSink is used to sink to fanout values to multiple sinks
|
||||
type FanoutSink []MetricSink
|
||||
|
||||
func (fh FanoutSink) SetGauge(key []string, val float32) {
|
||||
fh.SetGaugeWithLabels(key, val, nil)
|
||||
}
|
||||
|
||||
func (fh FanoutSink) SetGaugeWithLabels(key []string, val float32, labels []Label) {
|
||||
for _, s := range fh {
|
||||
s.SetGaugeWithLabels(key, val, labels)
|
||||
}
|
||||
}
|
||||
|
||||
func (fh FanoutSink) EmitKey(key []string, val float32) {
|
||||
for _, s := range fh {
|
||||
s.EmitKey(key, val)
|
||||
}
|
||||
}
|
||||
|
||||
func (fh FanoutSink) IncrCounter(key []string, val float32) {
|
||||
fh.IncrCounterWithLabels(key, val, nil)
|
||||
}
|
||||
|
||||
func (fh FanoutSink) IncrCounterWithLabels(key []string, val float32, labels []Label) {
|
||||
for _, s := range fh {
|
||||
s.IncrCounterWithLabels(key, val, labels)
|
||||
}
|
||||
}
|
||||
|
||||
func (fh FanoutSink) AddSample(key []string, val float32) {
|
||||
fh.AddSampleWithLabels(key, val, nil)
|
||||
}
|
||||
|
||||
func (fh FanoutSink) AddSampleWithLabels(key []string, val float32, labels []Label) {
|
||||
for _, s := range fh {
|
||||
s.AddSampleWithLabels(key, val, labels)
|
||||
}
|
||||
}
|
||||
|
||||
func (fh FanoutSink) Shutdown() {
|
||||
for _, s := range fh {
|
||||
if ss, ok := s.(ShutdownSink); ok {
|
||||
ss.Shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sinkURLFactoryFunc is an generic interface around the *SinkFromURL() function provided
|
||||
// by each sink type
|
||||
type sinkURLFactoryFunc func(*url.URL) (MetricSink, error)
|
||||
|
||||
// sinkRegistry supports the generic NewMetricSink function by mapping URL
|
||||
// schemes to metric sink factory functions
|
||||
var sinkRegistry = map[string]sinkURLFactoryFunc{
|
||||
"statsd": NewStatsdSinkFromURL,
|
||||
"statsite": NewStatsiteSinkFromURL,
|
||||
"inmem": NewInmemSinkFromURL,
|
||||
}
|
||||
|
||||
// NewMetricSinkFromURL allows a generic URL input to configure any of the
|
||||
// supported sinks. The scheme of the URL identifies the type of the sink, the
|
||||
// and query parameters are used to set options.
|
||||
//
|
||||
// "statsd://" - Initializes a StatsdSink. The host and port are passed through
|
||||
// as the "addr" of the sink
|
||||
//
|
||||
// "statsite://" - Initializes a StatsiteSink. The host and port become the
|
||||
// "addr" of the sink
|
||||
//
|
||||
// "inmem://" - Initializes an InmemSink. The host and port are ignored. The
|
||||
// "interval" and "duration" query parameters must be specified with valid
|
||||
// durations, see NewInmemSink for details.
|
||||
func NewMetricSinkFromURL(urlStr string) (MetricSink, error) {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sinkURLFactoryFunc := sinkRegistry[u.Scheme]
|
||||
if sinkURLFactoryFunc == nil {
|
||||
return nil, fmt.Errorf(
|
||||
"cannot create metric sink, unrecognized sink name: %q", u.Scheme)
|
||||
}
|
||||
|
||||
return sinkURLFactoryFunc(u)
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
iradix "github.com/hashicorp/go-immutable-radix"
|
||||
)
|
||||
|
||||
// Config is used to configure metrics settings
|
||||
type Config struct {
|
||||
ServiceName string // Prefixed with keys to separate services
|
||||
HostName string // Hostname to use. If not provided and EnableHostname, it will be os.Hostname
|
||||
EnableHostname bool // Enable prefixing gauge values with hostname
|
||||
EnableHostnameLabel bool // Enable adding hostname to labels
|
||||
EnableServiceLabel bool // Enable adding service to labels
|
||||
EnableRuntimeMetrics bool // Enables profiling of runtime metrics (GC, Goroutines, Memory)
|
||||
EnableTypePrefix bool // Prefixes key with a type ("counter", "gauge", "timer")
|
||||
TimerGranularity time.Duration // Granularity of timers.
|
||||
ProfileInterval time.Duration // Interval to profile runtime metrics
|
||||
|
||||
AllowedPrefixes []string // A list of metric prefixes to allow, with '.' as the separator
|
||||
BlockedPrefixes []string // A list of metric prefixes to block, with '.' as the separator
|
||||
AllowedLabels []string // A list of metric labels to allow, with '.' as the separator
|
||||
BlockedLabels []string // A list of metric labels to block, with '.' as the separator
|
||||
FilterDefault bool // Whether to allow metrics by default
|
||||
}
|
||||
|
||||
// Metrics represents an instance of a metrics sink that can
|
||||
// be used to emit
|
||||
type Metrics struct {
|
||||
Config
|
||||
lastNumGC uint32
|
||||
sink MetricSink
|
||||
filter *iradix.Tree
|
||||
allowedLabels map[string]bool
|
||||
blockedLabels map[string]bool
|
||||
filterLock sync.RWMutex // Lock filters and allowedLabels/blockedLabels access
|
||||
}
|
||||
|
||||
// Shared global metrics instance
|
||||
var globalMetrics atomic.Value // *Metrics
|
||||
|
||||
func init() {
|
||||
// Initialize to a blackhole sink to avoid errors
|
||||
globalMetrics.Store(&Metrics{sink: &BlackholeSink{}})
|
||||
}
|
||||
|
||||
// Default returns the shared global metrics instance.
|
||||
func Default() *Metrics {
|
||||
return globalMetrics.Load().(*Metrics)
|
||||
}
|
||||
|
||||
// DefaultConfig provides a sane default configuration
|
||||
func DefaultConfig(serviceName string) *Config {
|
||||
c := &Config{
|
||||
ServiceName: serviceName, // Use client provided service
|
||||
HostName: "",
|
||||
EnableHostname: true, // Enable hostname prefix
|
||||
EnableRuntimeMetrics: true, // Enable runtime profiling
|
||||
EnableTypePrefix: false, // Disable type prefix
|
||||
TimerGranularity: time.Millisecond, // Timers are in milliseconds
|
||||
ProfileInterval: time.Second, // Poll runtime every second
|
||||
FilterDefault: true, // Don't filter metrics by default
|
||||
}
|
||||
|
||||
// Try to get the hostname
|
||||
name, _ := os.Hostname()
|
||||
c.HostName = name
|
||||
return c
|
||||
}
|
||||
|
||||
// New is used to create a new instance of Metrics
|
||||
func New(conf *Config, sink MetricSink) (*Metrics, error) {
|
||||
met := &Metrics{}
|
||||
met.Config = *conf
|
||||
met.sink = sink
|
||||
met.UpdateFilterAndLabels(conf.AllowedPrefixes, conf.BlockedPrefixes, conf.AllowedLabels, conf.BlockedLabels)
|
||||
|
||||
// Start the runtime collector
|
||||
if conf.EnableRuntimeMetrics {
|
||||
go met.collectStats()
|
||||
}
|
||||
return met, nil
|
||||
}
|
||||
|
||||
// NewGlobal is the same as New, but it assigns the metrics object to be
|
||||
// used globally as well as returning it.
|
||||
func NewGlobal(conf *Config, sink MetricSink) (*Metrics, error) {
|
||||
metrics, err := New(conf, sink)
|
||||
if err == nil {
|
||||
globalMetrics.Store(metrics)
|
||||
}
|
||||
return metrics, err
|
||||
}
|
||||
|
||||
// Proxy all the methods to the globalMetrics instance
|
||||
func SetGauge(key []string, val float32) {
|
||||
globalMetrics.Load().(*Metrics).SetGauge(key, val)
|
||||
}
|
||||
|
||||
func SetGaugeWithLabels(key []string, val float32, labels []Label) {
|
||||
globalMetrics.Load().(*Metrics).SetGaugeWithLabels(key, val, labels)
|
||||
}
|
||||
|
||||
func EmitKey(key []string, val float32) {
|
||||
globalMetrics.Load().(*Metrics).EmitKey(key, val)
|
||||
}
|
||||
|
||||
func IncrCounter(key []string, val float32) {
|
||||
globalMetrics.Load().(*Metrics).IncrCounter(key, val)
|
||||
}
|
||||
|
||||
func IncrCounterWithLabels(key []string, val float32, labels []Label) {
|
||||
globalMetrics.Load().(*Metrics).IncrCounterWithLabels(key, val, labels)
|
||||
}
|
||||
|
||||
func AddSample(key []string, val float32) {
|
||||
globalMetrics.Load().(*Metrics).AddSample(key, val)
|
||||
}
|
||||
|
||||
func AddSampleWithLabels(key []string, val float32, labels []Label) {
|
||||
globalMetrics.Load().(*Metrics).AddSampleWithLabels(key, val, labels)
|
||||
}
|
||||
|
||||
func MeasureSince(key []string, start time.Time) {
|
||||
globalMetrics.Load().(*Metrics).MeasureSince(key, start)
|
||||
}
|
||||
|
||||
func MeasureSinceWithLabels(key []string, start time.Time, labels []Label) {
|
||||
globalMetrics.Load().(*Metrics).MeasureSinceWithLabels(key, start, labels)
|
||||
}
|
||||
|
||||
func UpdateFilter(allow, block []string) {
|
||||
globalMetrics.Load().(*Metrics).UpdateFilter(allow, block)
|
||||
}
|
||||
|
||||
// UpdateFilterAndLabels set allow/block prefixes of metrics while allowedLabels
|
||||
// and blockedLabels - when not nil - allow filtering of labels in order to
|
||||
// block/allow globally labels (especially useful when having large number of
|
||||
// values for a given label). See README.md for more information about usage.
|
||||
func UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels []string) {
|
||||
globalMetrics.Load().(*Metrics).UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels)
|
||||
}
|
||||
|
||||
// Shutdown disables metric collection, then blocks while attempting to flush metrics to storage.
|
||||
// WARNING: Not all MetricSink backends support this functionality, and calling this will cause them to leak resources.
|
||||
// This is intended for use immediately prior to application exit.
|
||||
func Shutdown() {
|
||||
m := globalMetrics.Load().(*Metrics)
|
||||
// Swap whatever MetricSink is currently active with a BlackholeSink. Callers must not have a
|
||||
// reason to expect that calls to the library will successfully collect metrics after Shutdown
|
||||
// has been called.
|
||||
globalMetrics.Store(&Metrics{sink: &BlackholeSink{}})
|
||||
m.Shutdown()
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// statsdMaxLen is the maximum size of a packet
|
||||
// to send to statsd
|
||||
statsdMaxLen = 1400
|
||||
)
|
||||
|
||||
// StatsdSink provides a MetricSink that can be used
|
||||
// with a statsite or statsd metrics server. It uses
|
||||
// only UDP packets, while StatsiteSink uses TCP.
|
||||
type StatsdSink struct {
|
||||
addr string
|
||||
metricQueue chan string
|
||||
}
|
||||
|
||||
// NewStatsdSinkFromURL creates an StatsdSink from a URL. It is used
|
||||
// (and tested) from NewMetricSinkFromURL.
|
||||
func NewStatsdSinkFromURL(u *url.URL) (MetricSink, error) {
|
||||
return NewStatsdSink(u.Host)
|
||||
}
|
||||
|
||||
// NewStatsdSink is used to create a new StatsdSink
|
||||
func NewStatsdSink(addr string) (*StatsdSink, error) {
|
||||
s := &StatsdSink{
|
||||
addr: addr,
|
||||
metricQueue: make(chan string, 4096),
|
||||
}
|
||||
go s.flushMetrics()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Close is used to stop flushing to statsd
|
||||
func (s *StatsdSink) Shutdown() {
|
||||
close(s.metricQueue)
|
||||
}
|
||||
|
||||
func (s *StatsdSink) SetGauge(key []string, val float32) {
|
||||
flatKey := s.flattenKey(key)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val))
|
||||
}
|
||||
|
||||
func (s *StatsdSink) SetGaugeWithLabels(key []string, val float32, labels []Label) {
|
||||
flatKey := s.flattenKeyLabels(key, labels)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val))
|
||||
}
|
||||
|
||||
func (s *StatsdSink) EmitKey(key []string, val float32) {
|
||||
flatKey := s.flattenKey(key)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|kv\n", flatKey, val))
|
||||
}
|
||||
|
||||
func (s *StatsdSink) IncrCounter(key []string, val float32) {
|
||||
flatKey := s.flattenKey(key)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val))
|
||||
}
|
||||
|
||||
func (s *StatsdSink) IncrCounterWithLabels(key []string, val float32, labels []Label) {
|
||||
flatKey := s.flattenKeyLabels(key, labels)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val))
|
||||
}
|
||||
|
||||
func (s *StatsdSink) AddSample(key []string, val float32) {
|
||||
flatKey := s.flattenKey(key)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val))
|
||||
}
|
||||
|
||||
func (s *StatsdSink) AddSampleWithLabels(key []string, val float32, labels []Label) {
|
||||
flatKey := s.flattenKeyLabels(key, labels)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val))
|
||||
}
|
||||
|
||||
// Flattens the key for formatting, removes spaces
|
||||
func (s *StatsdSink) flattenKey(parts []string) string {
|
||||
joined := strings.Join(parts, ".")
|
||||
return strings.Map(func(r rune) rune {
|
||||
switch r {
|
||||
case ':':
|
||||
fallthrough
|
||||
case ' ':
|
||||
return '_'
|
||||
default:
|
||||
return r
|
||||
}
|
||||
}, joined)
|
||||
}
|
||||
|
||||
// Flattens the key along with labels for formatting, removes spaces
|
||||
func (s *StatsdSink) flattenKeyLabels(parts []string, labels []Label) string {
|
||||
for _, label := range labels {
|
||||
parts = append(parts, label.Value)
|
||||
}
|
||||
return s.flattenKey(parts)
|
||||
}
|
||||
|
||||
// Does a non-blocking push to the metrics queue
|
||||
func (s *StatsdSink) pushMetric(m string) {
|
||||
select {
|
||||
case s.metricQueue <- m:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Flushes metrics
|
||||
func (s *StatsdSink) flushMetrics() {
|
||||
var sock net.Conn
|
||||
var err error
|
||||
var wait <-chan time.Time
|
||||
ticker := time.NewTicker(flushInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
CONNECT:
|
||||
// Create a buffer
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
// Attempt to connect
|
||||
sock, err = net.Dial("udp", s.addr)
|
||||
if err != nil {
|
||||
log.Printf("[ERR] Error connecting to statsd! Err: %s", err)
|
||||
goto WAIT
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case metric, ok := <-s.metricQueue:
|
||||
// Get a metric from the queue
|
||||
if !ok {
|
||||
goto QUIT
|
||||
}
|
||||
|
||||
// Check if this would overflow the packet size
|
||||
if len(metric)+buf.Len() > statsdMaxLen {
|
||||
_, err := sock.Write(buf.Bytes())
|
||||
buf.Reset()
|
||||
if err != nil {
|
||||
log.Printf("[ERR] Error writing to statsd! Err: %s", err)
|
||||
goto WAIT
|
||||
}
|
||||
}
|
||||
|
||||
// Append to the buffer
|
||||
buf.WriteString(metric)
|
||||
|
||||
case <-ticker.C:
|
||||
if buf.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := sock.Write(buf.Bytes())
|
||||
buf.Reset()
|
||||
if err != nil {
|
||||
log.Printf("[ERR] Error flushing to statsd! Err: %s", err)
|
||||
goto WAIT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WAIT:
|
||||
// Wait for a while
|
||||
wait = time.After(time.Duration(5) * time.Second)
|
||||
for {
|
||||
select {
|
||||
// Dequeue the messages to avoid backlog
|
||||
case _, ok := <-s.metricQueue:
|
||||
if !ok {
|
||||
goto QUIT
|
||||
}
|
||||
case <-wait:
|
||||
goto CONNECT
|
||||
}
|
||||
}
|
||||
QUIT:
|
||||
s.metricQueue = nil
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// We force flush the statsite metrics after this period of
|
||||
// inactivity. Prevents stats from getting stuck in a buffer
|
||||
// forever.
|
||||
flushInterval = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// NewStatsiteSinkFromURL creates an StatsiteSink from a URL. It is used
|
||||
// (and tested) from NewMetricSinkFromURL.
|
||||
func NewStatsiteSinkFromURL(u *url.URL) (MetricSink, error) {
|
||||
return NewStatsiteSink(u.Host)
|
||||
}
|
||||
|
||||
// StatsiteSink provides a MetricSink that can be used with a
|
||||
// statsite metrics server
|
||||
type StatsiteSink struct {
|
||||
addr string
|
||||
metricQueue chan string
|
||||
}
|
||||
|
||||
// NewStatsiteSink is used to create a new StatsiteSink
|
||||
func NewStatsiteSink(addr string) (*StatsiteSink, error) {
|
||||
s := &StatsiteSink{
|
||||
addr: addr,
|
||||
metricQueue: make(chan string, 4096),
|
||||
}
|
||||
go s.flushMetrics()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Close is used to stop flushing to statsite
|
||||
func (s *StatsiteSink) Shutdown() {
|
||||
close(s.metricQueue)
|
||||
}
|
||||
|
||||
func (s *StatsiteSink) SetGauge(key []string, val float32) {
|
||||
flatKey := s.flattenKey(key)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val))
|
||||
}
|
||||
|
||||
func (s *StatsiteSink) SetGaugeWithLabels(key []string, val float32, labels []Label) {
|
||||
flatKey := s.flattenKeyLabels(key, labels)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val))
|
||||
}
|
||||
|
||||
func (s *StatsiteSink) EmitKey(key []string, val float32) {
|
||||
flatKey := s.flattenKey(key)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|kv\n", flatKey, val))
|
||||
}
|
||||
|
||||
func (s *StatsiteSink) IncrCounter(key []string, val float32) {
|
||||
flatKey := s.flattenKey(key)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val))
|
||||
}
|
||||
|
||||
func (s *StatsiteSink) IncrCounterWithLabels(key []string, val float32, labels []Label) {
|
||||
flatKey := s.flattenKeyLabels(key, labels)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val))
|
||||
}
|
||||
|
||||
func (s *StatsiteSink) AddSample(key []string, val float32) {
|
||||
flatKey := s.flattenKey(key)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val))
|
||||
}
|
||||
|
||||
func (s *StatsiteSink) AddSampleWithLabels(key []string, val float32, labels []Label) {
|
||||
flatKey := s.flattenKeyLabels(key, labels)
|
||||
s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val))
|
||||
}
|
||||
|
||||
// Flattens the key for formatting, removes spaces
|
||||
func (s *StatsiteSink) flattenKey(parts []string) string {
|
||||
joined := strings.Join(parts, ".")
|
||||
return strings.Map(func(r rune) rune {
|
||||
switch r {
|
||||
case ':':
|
||||
fallthrough
|
||||
case ' ':
|
||||
return '_'
|
||||
default:
|
||||
return r
|
||||
}
|
||||
}, joined)
|
||||
}
|
||||
|
||||
// Flattens the key along with labels for formatting, removes spaces
|
||||
func (s *StatsiteSink) flattenKeyLabels(parts []string, labels []Label) string {
|
||||
for _, label := range labels {
|
||||
parts = append(parts, label.Value)
|
||||
}
|
||||
return s.flattenKey(parts)
|
||||
}
|
||||
|
||||
// Does a non-blocking push to the metrics queue
|
||||
func (s *StatsiteSink) pushMetric(m string) {
|
||||
select {
|
||||
case s.metricQueue <- m:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Flushes metrics
|
||||
func (s *StatsiteSink) flushMetrics() {
|
||||
var sock net.Conn
|
||||
var err error
|
||||
var wait <-chan time.Time
|
||||
var buffered *bufio.Writer
|
||||
ticker := time.NewTicker(flushInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
CONNECT:
|
||||
// Attempt to connect
|
||||
sock, err = net.Dial("tcp", s.addr)
|
||||
if err != nil {
|
||||
log.Printf("[ERR] Error connecting to statsite! Err: %s", err)
|
||||
goto WAIT
|
||||
}
|
||||
|
||||
// Create a buffered writer
|
||||
buffered = bufio.NewWriter(sock)
|
||||
|
||||
for {
|
||||
select {
|
||||
case metric, ok := <-s.metricQueue:
|
||||
// Get a metric from the queue
|
||||
if !ok {
|
||||
goto QUIT
|
||||
}
|
||||
|
||||
// Try to send to statsite
|
||||
_, err := buffered.Write([]byte(metric))
|
||||
if err != nil {
|
||||
log.Printf("[ERR] Error writing to statsite! Err: %s", err)
|
||||
goto WAIT
|
||||
}
|
||||
case <-ticker.C:
|
||||
if err := buffered.Flush(); err != nil {
|
||||
log.Printf("[ERR] Error flushing to statsite! Err: %s", err)
|
||||
goto WAIT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WAIT:
|
||||
// Wait for a while
|
||||
wait = time.After(time.Duration(5) * time.Second)
|
||||
for {
|
||||
select {
|
||||
// Dequeue the messages to avoid backlog
|
||||
case _, ok := <-s.metricQueue:
|
||||
if !ok {
|
||||
goto QUIT
|
||||
}
|
||||
case <-wait:
|
||||
goto CONNECT
|
||||
}
|
||||
}
|
||||
QUIT:
|
||||
s.metricQueue = nil
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
bin/
|
||||
.idea/
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
language: go
|
||||
dist: xenial
|
||||
go:
|
||||
- '1.10'
|
||||
- '1.11'
|
||||
- '1.12'
|
||||
- '1.13'
|
||||
- 'tip'
|
||||
|
||||
script:
|
||||
- go test -coverpkg=./... -coverprofile=coverage.info -timeout=5s
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Contributor Code of Conduct
|
||||
|
||||
This project adheres to [The Code Manifesto](http://codemanifesto.com)
|
||||
as its guidelines for contributor interactions.
|
||||
|
||||
## The Code Manifesto
|
||||
|
||||
We want to work in an ecosystem that empowers developers to reach their
|
||||
potential — one that encourages growth and effective collaboration. A space
|
||||
that is safe for all.
|
||||
|
||||
A space such as this benefits everyone that participates in it. It encourages
|
||||
new developers to enter our field. It is through discussion and collaboration
|
||||
that we grow, and through growth that we improve.
|
||||
|
||||
In the effort to create such a place, we hold to these values:
|
||||
|
||||
1. **Discrimination limits us.** This includes discrimination on the basis of
|
||||
race, gender, sexual orientation, gender identity, age, nationality,
|
||||
technology and any other arbitrary exclusion of a group of people.
|
||||
2. **Boundaries honor us.** Your comfort levels are not everyone’s comfort
|
||||
levels. Remember that, and if brought to your attention, heed it.
|
||||
3. **We are our biggest assets.** None of us were born masters of our trade.
|
||||
Each of us has been helped along the way. Return that favor, when and where
|
||||
you can.
|
||||
4. **We are resources for the future.** As an extension of #3, share what you
|
||||
know. Make yourself a resource to help those that come after you.
|
||||
5. **Respect defines us.** Treat others as you wish to be treated. Make your
|
||||
discussions, criticisms and debates from a position of respectfulness. Ask
|
||||
yourself, is it true? Is it necessary? Is it constructive? Anything less is
|
||||
unacceptable.
|
||||
6. **Reactions require grace.** Angry responses are valid, but abusive language
|
||||
and vindictive actions are toxic. When something happens that offends you,
|
||||
handle it assertively, but be respectful. Escalate reasonably, and try to
|
||||
allow the offender an opportunity to explain themselves, and possibly
|
||||
correct the issue.
|
||||
7. **Opinions are just that: opinions.** Each and every one of us, due to our
|
||||
background and upbringing, have varying opinions. That is perfectly
|
||||
acceptable. Remember this: if you respect your own opinions, you should
|
||||
respect the opinions of others.
|
||||
8. **To err is human.** You might not intend it, but mistakes do happen and
|
||||
contribute to build experience. Tolerate honest mistakes, and don't
|
||||
hesitate to apologize if you make one yourself.
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
#### Support
|
||||
If you do have a contribution to the package, feel free to create a Pull Request or an Issue.
|
||||
|
||||
#### What to contribute
|
||||
If you don't know what to do, there are some features and functions that need to be done
|
||||
|
||||
- [ ] Refactor code
|
||||
- [ ] Edit docs and [README](https://github.com/asaskevich/govalidator/README.md): spellcheck, grammar and typo check
|
||||
- [ ] Create actual list of contributors and projects that currently using this package
|
||||
- [ ] Resolve [issues and bugs](https://github.com/asaskevich/govalidator/issues)
|
||||
- [ ] Update actual [list of functions](https://github.com/asaskevich/govalidator#list-of-functions)
|
||||
- [ ] Update [list of validators](https://github.com/asaskevich/govalidator#validatestruct-2) that available for `ValidateStruct` and add new
|
||||
- [ ] Implement new validators: `IsFQDN`, `IsIMEI`, `IsPostalCode`, `IsISIN`, `IsISRC` etc
|
||||
- [x] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
|
||||
- [ ] Implement fuzzing testing
|
||||
- [ ] Implement some struct/map/array utilities
|
||||
- [ ] Implement map/array validation
|
||||
- [ ] Implement benchmarking
|
||||
- [ ] Implement batch of examples
|
||||
- [ ] Look at forks for new features and fixes
|
||||
|
||||
#### Advice
|
||||
Feel free to create what you want, but keep in mind when you implement new features:
|
||||
- Code must be clear and readable, names of variables/constants clearly describes what they are doing
|
||||
- Public functions must be documented and described in source file and added to README.md to the list of available functions
|
||||
- There are must be unit-tests for any new functions and improvements
|
||||
|
||||
## Financial contributions
|
||||
|
||||
We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/govalidator).
|
||||
Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank you to all the people who have already contributed to govalidator!
|
||||
<a href="https://github.com/asaskevich/govalidator/graphs/contributors"><img src="https://opencollective.com/govalidator/contributors.svg?width=890" /></a>
|
||||
|
||||
|
||||
### Backers
|
||||
|
||||
Thank you to all our backers! [[Become a backer](https://opencollective.com/govalidator#backer)]
|
||||
|
||||
<a href="https://opencollective.com/govalidator#backers" target="_blank"><img src="https://opencollective.com/govalidator/backers.svg?width=890"></a>
|
||||
|
||||
|
||||
### Sponsors
|
||||
|
||||
Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/govalidator#sponsor))
|
||||
|
||||
<a href="https://opencollective.com/govalidator/sponsor/0/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/1/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/2/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/3/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/4/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/5/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/6/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/7/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/8/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/9/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/9/avatar.svg"></a>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2020 Alex Saskevich
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,622 @@
|
|||
govalidator
|
||||
===========
|
||||
[](https://gitter.im/asaskevich/govalidator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://godoc.org/github.com/asaskevich/govalidator)
|
||||
[](https://travis-ci.org/asaskevich/govalidator)
|
||||
[](https://codecov.io/gh/asaskevich/govalidator) [](https://goreportcard.com/report/github.com/asaskevich/govalidator) [](http://go-search.org/view?id=github.com%2Fasaskevich%2Fgovalidator) [](#backers) [](#sponsors) [](https://app.fossa.io/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator?ref=badge_shield)
|
||||
|
||||
A package of validators and sanitizers for strings, structs and collections. Based on [validator.js](https://github.com/chriso/validator.js).
|
||||
|
||||
#### Installation
|
||||
Make sure that Go is installed on your computer.
|
||||
Type the following command in your terminal:
|
||||
|
||||
go get github.com/asaskevich/govalidator
|
||||
|
||||
or you can get specified release of the package with `gopkg.in`:
|
||||
|
||||
go get gopkg.in/asaskevich/govalidator.v10
|
||||
|
||||
After it the package is ready to use.
|
||||
|
||||
|
||||
#### Import package in your project
|
||||
Add following line in your `*.go` file:
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
```
|
||||
If you are unhappy to use long `govalidator`, you can do something like this:
|
||||
```go
|
||||
import (
|
||||
valid "github.com/asaskevich/govalidator"
|
||||
)
|
||||
```
|
||||
|
||||
#### Activate behavior to require all fields have a validation tag by default
|
||||
`SetFieldsRequiredByDefault` causes validation to fail when struct fields do not include validations or are not explicitly marked as exempt (using `valid:"-"` or `valid:"email,optional"`). A good place to activate this is a package init function or the main() function.
|
||||
|
||||
`SetNilPtrAllowedByRequired` causes validation to pass when struct fields marked by `required` are set to nil. This is disabled by default for consistency, but some packages that need to be able to determine between `nil` and `zero value` state can use this. If disabled, both `nil` and `zero` values cause validation errors.
|
||||
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
func init() {
|
||||
govalidator.SetFieldsRequiredByDefault(true)
|
||||
}
|
||||
```
|
||||
|
||||
Here's some code to explain it:
|
||||
```go
|
||||
// this struct definition will fail govalidator.ValidateStruct() (and the field values do not matter):
|
||||
type exampleStruct struct {
|
||||
Name string ``
|
||||
Email string `valid:"email"`
|
||||
}
|
||||
|
||||
// this, however, will only fail when Email is empty or an invalid email address:
|
||||
type exampleStruct2 struct {
|
||||
Name string `valid:"-"`
|
||||
Email string `valid:"email"`
|
||||
}
|
||||
|
||||
// lastly, this will only fail when Email is an invalid email address but not when it's empty:
|
||||
type exampleStruct2 struct {
|
||||
Name string `valid:"-"`
|
||||
Email string `valid:"email,optional"`
|
||||
}
|
||||
```
|
||||
|
||||
#### Recent breaking changes (see [#123](https://github.com/asaskevich/govalidator/pull/123))
|
||||
##### Custom validator function signature
|
||||
A context was added as the second parameter, for structs this is the object being validated – this makes dependent validation possible.
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
// old signature
|
||||
func(i interface{}) bool
|
||||
|
||||
// new signature
|
||||
func(i interface{}, o interface{}) bool
|
||||
```
|
||||
|
||||
##### Adding a custom validator
|
||||
This was changed to prevent data races when accessing custom validators.
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
// before
|
||||
govalidator.CustomTypeTagMap["customByteArrayValidator"] = func(i interface{}, o interface{}) bool {
|
||||
// ...
|
||||
}
|
||||
|
||||
// after
|
||||
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", func(i interface{}, o interface{}) bool {
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
#### List of functions:
|
||||
```go
|
||||
func Abs(value float64) float64
|
||||
func BlackList(str, chars string) string
|
||||
func ByteLength(str string, params ...string) bool
|
||||
func CamelCaseToUnderscore(str string) string
|
||||
func Contains(str, substring string) bool
|
||||
func Count(array []interface{}, iterator ConditionIterator) int
|
||||
func Each(array []interface{}, iterator Iterator)
|
||||
func ErrorByField(e error, field string) string
|
||||
func ErrorsByField(e error) map[string]string
|
||||
func Filter(array []interface{}, iterator ConditionIterator) []interface{}
|
||||
func Find(array []interface{}, iterator ConditionIterator) interface{}
|
||||
func GetLine(s string, index int) (string, error)
|
||||
func GetLines(s string) []string
|
||||
func HasLowerCase(str string) bool
|
||||
func HasUpperCase(str string) bool
|
||||
func HasWhitespace(str string) bool
|
||||
func HasWhitespaceOnly(str string) bool
|
||||
func InRange(value interface{}, left interface{}, right interface{}) bool
|
||||
func InRangeFloat32(value, left, right float32) bool
|
||||
func InRangeFloat64(value, left, right float64) bool
|
||||
func InRangeInt(value, left, right interface{}) bool
|
||||
func IsASCII(str string) bool
|
||||
func IsAlpha(str string) bool
|
||||
func IsAlphanumeric(str string) bool
|
||||
func IsBase64(str string) bool
|
||||
func IsByteLength(str string, min, max int) bool
|
||||
func IsCIDR(str string) bool
|
||||
func IsCRC32(str string) bool
|
||||
func IsCRC32b(str string) bool
|
||||
func IsCreditCard(str string) bool
|
||||
func IsDNSName(str string) bool
|
||||
func IsDataURI(str string) bool
|
||||
func IsDialString(str string) bool
|
||||
func IsDivisibleBy(str, num string) bool
|
||||
func IsEmail(str string) bool
|
||||
func IsExistingEmail(email string) bool
|
||||
func IsFilePath(str string) (bool, int)
|
||||
func IsFloat(str string) bool
|
||||
func IsFullWidth(str string) bool
|
||||
func IsHalfWidth(str string) bool
|
||||
func IsHash(str string, algorithm string) bool
|
||||
func IsHexadecimal(str string) bool
|
||||
func IsHexcolor(str string) bool
|
||||
func IsHost(str string) bool
|
||||
func IsIP(str string) bool
|
||||
func IsIPv4(str string) bool
|
||||
func IsIPv6(str string) bool
|
||||
func IsISBN(str string, version int) bool
|
||||
func IsISBN10(str string) bool
|
||||
func IsISBN13(str string) bool
|
||||
func IsISO3166Alpha2(str string) bool
|
||||
func IsISO3166Alpha3(str string) bool
|
||||
func IsISO4217(str string) bool
|
||||
func IsISO693Alpha2(str string) bool
|
||||
func IsISO693Alpha3b(str string) bool
|
||||
func IsIn(str string, params ...string) bool
|
||||
func IsInRaw(str string, params ...string) bool
|
||||
func IsInt(str string) bool
|
||||
func IsJSON(str string) bool
|
||||
func IsLatitude(str string) bool
|
||||
func IsLongitude(str string) bool
|
||||
func IsLowerCase(str string) bool
|
||||
func IsMAC(str string) bool
|
||||
func IsMD4(str string) bool
|
||||
func IsMD5(str string) bool
|
||||
func IsMagnetURI(str string) bool
|
||||
func IsMongoID(str string) bool
|
||||
func IsMultibyte(str string) bool
|
||||
func IsNatural(value float64) bool
|
||||
func IsNegative(value float64) bool
|
||||
func IsNonNegative(value float64) bool
|
||||
func IsNonPositive(value float64) bool
|
||||
func IsNotNull(str string) bool
|
||||
func IsNull(str string) bool
|
||||
func IsNumeric(str string) bool
|
||||
func IsPort(str string) bool
|
||||
func IsPositive(value float64) bool
|
||||
func IsPrintableASCII(str string) bool
|
||||
func IsRFC3339(str string) bool
|
||||
func IsRFC3339WithoutZone(str string) bool
|
||||
func IsRGBcolor(str string) bool
|
||||
func IsRegex(str string) bool
|
||||
func IsRequestURI(rawurl string) bool
|
||||
func IsRequestURL(rawurl string) bool
|
||||
func IsRipeMD128(str string) bool
|
||||
func IsRipeMD160(str string) bool
|
||||
func IsRsaPub(str string, params ...string) bool
|
||||
func IsRsaPublicKey(str string, keylen int) bool
|
||||
func IsSHA1(str string) bool
|
||||
func IsSHA256(str string) bool
|
||||
func IsSHA384(str string) bool
|
||||
func IsSHA512(str string) bool
|
||||
func IsSSN(str string) bool
|
||||
func IsSemver(str string) bool
|
||||
func IsTiger128(str string) bool
|
||||
func IsTiger160(str string) bool
|
||||
func IsTiger192(str string) bool
|
||||
func IsTime(str string, format string) bool
|
||||
func IsType(v interface{}, params ...string) bool
|
||||
func IsURL(str string) bool
|
||||
func IsUTFDigit(str string) bool
|
||||
func IsUTFLetter(str string) bool
|
||||
func IsUTFLetterNumeric(str string) bool
|
||||
func IsUTFNumeric(str string) bool
|
||||
func IsUUID(str string) bool
|
||||
func IsUUIDv3(str string) bool
|
||||
func IsUUIDv4(str string) bool
|
||||
func IsUUIDv5(str string) bool
|
||||
func IsULID(str string) bool
|
||||
func IsUnixTime(str string) bool
|
||||
func IsUpperCase(str string) bool
|
||||
func IsVariableWidth(str string) bool
|
||||
func IsWhole(value float64) bool
|
||||
func LeftTrim(str, chars string) string
|
||||
func Map(array []interface{}, iterator ResultIterator) []interface{}
|
||||
func Matches(str, pattern string) bool
|
||||
func MaxStringLength(str string, params ...string) bool
|
||||
func MinStringLength(str string, params ...string) bool
|
||||
func NormalizeEmail(str string) (string, error)
|
||||
func PadBoth(str string, padStr string, padLen int) string
|
||||
func PadLeft(str string, padStr string, padLen int) string
|
||||
func PadRight(str string, padStr string, padLen int) string
|
||||
func PrependPathToErrors(err error, path string) error
|
||||
func Range(str string, params ...string) bool
|
||||
func RemoveTags(s string) string
|
||||
func ReplacePattern(str, pattern, replace string) string
|
||||
func Reverse(s string) string
|
||||
func RightTrim(str, chars string) string
|
||||
func RuneLength(str string, params ...string) bool
|
||||
func SafeFileName(str string) string
|
||||
func SetFieldsRequiredByDefault(value bool)
|
||||
func SetNilPtrAllowedByRequired(value bool)
|
||||
func Sign(value float64) float64
|
||||
func StringLength(str string, params ...string) bool
|
||||
func StringMatches(s string, params ...string) bool
|
||||
func StripLow(str string, keepNewLines bool) string
|
||||
func ToBoolean(str string) (bool, error)
|
||||
func ToFloat(str string) (float64, error)
|
||||
func ToInt(value interface{}) (res int64, err error)
|
||||
func ToJSON(obj interface{}) (string, error)
|
||||
func ToString(obj interface{}) string
|
||||
func Trim(str, chars string) string
|
||||
func Truncate(str string, length int, ending string) string
|
||||
func TruncatingErrorf(str string, args ...interface{}) error
|
||||
func UnderscoreToCamelCase(s string) string
|
||||
func ValidateMap(inputMap map[string]interface{}, validationMap map[string]interface{}) (bool, error)
|
||||
func ValidateStruct(s interface{}) (bool, error)
|
||||
func WhiteList(str, chars string) string
|
||||
type ConditionIterator
|
||||
type CustomTypeValidator
|
||||
type Error
|
||||
func (e Error) Error() string
|
||||
type Errors
|
||||
func (es Errors) Error() string
|
||||
func (es Errors) Errors() []error
|
||||
type ISO3166Entry
|
||||
type ISO693Entry
|
||||
type InterfaceParamValidator
|
||||
type Iterator
|
||||
type ParamValidator
|
||||
type ResultIterator
|
||||
type UnsupportedTypeError
|
||||
func (e *UnsupportedTypeError) Error() string
|
||||
type Validator
|
||||
```
|
||||
|
||||
#### Examples
|
||||
###### IsURL
|
||||
```go
|
||||
println(govalidator.IsURL(`http://user@pass:domain.com/path/page`))
|
||||
```
|
||||
###### IsType
|
||||
```go
|
||||
println(govalidator.IsType("Bob", "string"))
|
||||
println(govalidator.IsType(1, "int"))
|
||||
i := 1
|
||||
println(govalidator.IsType(&i, "*int"))
|
||||
```
|
||||
|
||||
IsType can be used through the tag `type` which is essential for map validation:
|
||||
```go
|
||||
type User struct {
|
||||
Name string `valid:"type(string)"`
|
||||
Age int `valid:"type(int)"`
|
||||
Meta interface{} `valid:"type(string)"`
|
||||
}
|
||||
result, err := govalidator.ValidateStruct(User{"Bob", 20, "meta"})
|
||||
if err != nil {
|
||||
println("error: " + err.Error())
|
||||
}
|
||||
println(result)
|
||||
```
|
||||
###### ToString
|
||||
```go
|
||||
type User struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
str := govalidator.ToString(&User{"John", "Juan"})
|
||||
println(str)
|
||||
```
|
||||
###### Each, Map, Filter, Count for slices
|
||||
Each iterates over the slice/array and calls Iterator for every item
|
||||
```go
|
||||
data := []interface{}{1, 2, 3, 4, 5}
|
||||
var fn govalidator.Iterator = func(value interface{}, index int) {
|
||||
println(value.(int))
|
||||
}
|
||||
govalidator.Each(data, fn)
|
||||
```
|
||||
```go
|
||||
data := []interface{}{1, 2, 3, 4, 5}
|
||||
var fn govalidator.ResultIterator = func(value interface{}, index int) interface{} {
|
||||
return value.(int) * 3
|
||||
}
|
||||
_ = govalidator.Map(data, fn) // result = []interface{}{1, 6, 9, 12, 15}
|
||||
```
|
||||
```go
|
||||
data := []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
var fn govalidator.ConditionIterator = func(value interface{}, index int) bool {
|
||||
return value.(int)%2 == 0
|
||||
}
|
||||
_ = govalidator.Filter(data, fn) // result = []interface{}{2, 4, 6, 8, 10}
|
||||
_ = govalidator.Count(data, fn) // result = 5
|
||||
```
|
||||
###### ValidateStruct [#2](https://github.com/asaskevich/govalidator/pull/2)
|
||||
If you want to validate structs, you can use tag `valid` for any field in your structure. All validators used with this field in one tag are separated by comma. If you want to skip validation, place `-` in your tag. If you need a validator that is not on the list below, you can add it like this:
|
||||
```go
|
||||
govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
|
||||
return str == "duck"
|
||||
})
|
||||
```
|
||||
For completely custom validators (interface-based), see below.
|
||||
|
||||
Here is a list of available validators for struct fields (validator - used function):
|
||||
```go
|
||||
"email": IsEmail,
|
||||
"url": IsURL,
|
||||
"dialstring": IsDialString,
|
||||
"requrl": IsRequestURL,
|
||||
"requri": IsRequestURI,
|
||||
"alpha": IsAlpha,
|
||||
"utfletter": IsUTFLetter,
|
||||
"alphanum": IsAlphanumeric,
|
||||
"utfletternum": IsUTFLetterNumeric,
|
||||
"numeric": IsNumeric,
|
||||
"utfnumeric": IsUTFNumeric,
|
||||
"utfdigit": IsUTFDigit,
|
||||
"hexadecimal": IsHexadecimal,
|
||||
"hexcolor": IsHexcolor,
|
||||
"rgbcolor": IsRGBcolor,
|
||||
"lowercase": IsLowerCase,
|
||||
"uppercase": IsUpperCase,
|
||||
"int": IsInt,
|
||||
"float": IsFloat,
|
||||
"null": IsNull,
|
||||
"uuid": IsUUID,
|
||||
"uuidv3": IsUUIDv3,
|
||||
"uuidv4": IsUUIDv4,
|
||||
"uuidv5": IsUUIDv5,
|
||||
"creditcard": IsCreditCard,
|
||||
"isbn10": IsISBN10,
|
||||
"isbn13": IsISBN13,
|
||||
"json": IsJSON,
|
||||
"multibyte": IsMultibyte,
|
||||
"ascii": IsASCII,
|
||||
"printableascii": IsPrintableASCII,
|
||||
"fullwidth": IsFullWidth,
|
||||
"halfwidth": IsHalfWidth,
|
||||
"variablewidth": IsVariableWidth,
|
||||
"base64": IsBase64,
|
||||
"datauri": IsDataURI,
|
||||
"ip": IsIP,
|
||||
"port": IsPort,
|
||||
"ipv4": IsIPv4,
|
||||
"ipv6": IsIPv6,
|
||||
"dns": IsDNSName,
|
||||
"host": IsHost,
|
||||
"mac": IsMAC,
|
||||
"latitude": IsLatitude,
|
||||
"longitude": IsLongitude,
|
||||
"ssn": IsSSN,
|
||||
"semver": IsSemver,
|
||||
"rfc3339": IsRFC3339,
|
||||
"rfc3339WithoutZone": IsRFC3339WithoutZone,
|
||||
"ISO3166Alpha2": IsISO3166Alpha2,
|
||||
"ISO3166Alpha3": IsISO3166Alpha3,
|
||||
"ulid": IsULID,
|
||||
```
|
||||
Validators with parameters
|
||||
|
||||
```go
|
||||
"range(min|max)": Range,
|
||||
"length(min|max)": ByteLength,
|
||||
"runelength(min|max)": RuneLength,
|
||||
"stringlength(min|max)": StringLength,
|
||||
"matches(pattern)": StringMatches,
|
||||
"in(string1|string2|...|stringN)": IsIn,
|
||||
"rsapub(keylength)" : IsRsaPub,
|
||||
"minstringlength(int): MinStringLength,
|
||||
"maxstringlength(int): MaxStringLength,
|
||||
```
|
||||
Validators with parameters for any type
|
||||
|
||||
```go
|
||||
"type(type)": IsType,
|
||||
```
|
||||
|
||||
And here is small example of usage:
|
||||
```go
|
||||
type Post struct {
|
||||
Title string `valid:"alphanum,required"`
|
||||
Message string `valid:"duck,ascii"`
|
||||
Message2 string `valid:"animal(dog)"`
|
||||
AuthorIP string `valid:"ipv4"`
|
||||
Date string `valid:"-"`
|
||||
}
|
||||
post := &Post{
|
||||
Title: "My Example Post",
|
||||
Message: "duck",
|
||||
Message2: "dog",
|
||||
AuthorIP: "123.234.54.3",
|
||||
}
|
||||
|
||||
// Add your own struct validation tags
|
||||
govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
|
||||
return str == "duck"
|
||||
})
|
||||
|
||||
// Add your own struct validation tags with parameter
|
||||
govalidator.ParamTagMap["animal"] = govalidator.ParamValidator(func(str string, params ...string) bool {
|
||||
species := params[0]
|
||||
return str == species
|
||||
})
|
||||
govalidator.ParamTagRegexMap["animal"] = regexp.MustCompile("^animal\\((\\w+)\\)$")
|
||||
|
||||
result, err := govalidator.ValidateStruct(post)
|
||||
if err != nil {
|
||||
println("error: " + err.Error())
|
||||
}
|
||||
println(result)
|
||||
```
|
||||
###### ValidateMap [#2](https://github.com/asaskevich/govalidator/pull/338)
|
||||
If you want to validate maps, you can use the map to be validated and a validation map that contain the same tags used in ValidateStruct, both maps have to be in the form `map[string]interface{}`
|
||||
|
||||
So here is small example of usage:
|
||||
```go
|
||||
var mapTemplate = map[string]interface{}{
|
||||
"name":"required,alpha",
|
||||
"family":"required,alpha",
|
||||
"email":"required,email",
|
||||
"cell-phone":"numeric",
|
||||
"address":map[string]interface{}{
|
||||
"line1":"required,alphanum",
|
||||
"line2":"alphanum",
|
||||
"postal-code":"numeric",
|
||||
},
|
||||
}
|
||||
|
||||
var inputMap = map[string]interface{}{
|
||||
"name":"Bob",
|
||||
"family":"Smith",
|
||||
"email":"foo@bar.baz",
|
||||
"address":map[string]interface{}{
|
||||
"line1":"",
|
||||
"line2":"",
|
||||
"postal-code":"",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := govalidator.ValidateMap(inputMap, mapTemplate)
|
||||
if err != nil {
|
||||
println("error: " + err.Error())
|
||||
}
|
||||
println(result)
|
||||
```
|
||||
|
||||
###### WhiteList
|
||||
```go
|
||||
// Remove all characters from string ignoring characters between "a" and "z"
|
||||
println(govalidator.WhiteList("a3a43a5a4a3a2a23a4a5a4a3a4", "a-z") == "aaaaaaaaaaaa")
|
||||
```
|
||||
|
||||
###### Custom validation functions
|
||||
Custom validation using your own domain specific validators is also available - here's an example of how to use it:
|
||||
```go
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
type CustomByteArray [6]byte // custom types are supported and can be validated
|
||||
|
||||
type StructWithCustomByteArray struct {
|
||||
ID CustomByteArray `valid:"customByteArrayValidator,customMinLengthValidator"` // multiple custom validators are possible as well and will be evaluated in sequence
|
||||
Email string `valid:"email"`
|
||||
CustomMinLength int `valid:"-"`
|
||||
}
|
||||
|
||||
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", func(i interface{}, context interface{}) bool {
|
||||
switch v := context.(type) { // you can type switch on the context interface being validated
|
||||
case StructWithCustomByteArray:
|
||||
// you can check and validate against some other field in the context,
|
||||
// return early or not validate against the context at all – your choice
|
||||
case SomeOtherType:
|
||||
// ...
|
||||
default:
|
||||
// expecting some other type? Throw/panic here or continue
|
||||
}
|
||||
|
||||
switch v := i.(type) { // type switch on the struct field being validated
|
||||
case CustomByteArray:
|
||||
for _, e := range v { // this validator checks that the byte array is not empty, i.e. not all zeroes
|
||||
if e != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
govalidator.CustomTypeTagMap.Set("customMinLengthValidator", func(i interface{}, context interface{}) bool {
|
||||
switch v := context.(type) { // this validates a field against the value in another field, i.e. dependent validation
|
||||
case StructWithCustomByteArray:
|
||||
return len(v.ID) >= v.CustomMinLength
|
||||
}
|
||||
return false
|
||||
})
|
||||
```
|
||||
|
||||
###### Loop over Error()
|
||||
By default .Error() returns all errors in a single String. To access each error you can do this:
|
||||
```go
|
||||
if err != nil {
|
||||
errs := err.(govalidator.Errors).Errors()
|
||||
for _, e := range errs {
|
||||
fmt.Println(e.Error())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
###### Custom error messages
|
||||
Custom error messages are supported via annotations by adding the `~` separator - here's an example of how to use it:
|
||||
```go
|
||||
type Ticket struct {
|
||||
Id int64 `json:"id"`
|
||||
FirstName string `json:"firstname" valid:"required~First name is blank"`
|
||||
}
|
||||
```
|
||||
|
||||
#### Notes
|
||||
Documentation is available here: [godoc.org](https://godoc.org/github.com/asaskevich/govalidator).
|
||||
Full information about code coverage is also available here: [govalidator on gocover.io](http://gocover.io/github.com/asaskevich/govalidator).
|
||||
|
||||
#### Support
|
||||
If you do have a contribution to the package, feel free to create a Pull Request or an Issue.
|
||||
|
||||
#### What to contribute
|
||||
If you don't know what to do, there are some features and functions that need to be done
|
||||
|
||||
- [ ] Refactor code
|
||||
- [ ] Edit docs and [README](https://github.com/asaskevich/govalidator/README.md): spellcheck, grammar and typo check
|
||||
- [ ] Create actual list of contributors and projects that currently using this package
|
||||
- [ ] Resolve [issues and bugs](https://github.com/asaskevich/govalidator/issues)
|
||||
- [ ] Update actual [list of functions](https://github.com/asaskevich/govalidator#list-of-functions)
|
||||
- [ ] Update [list of validators](https://github.com/asaskevich/govalidator#validatestruct-2) that available for `ValidateStruct` and add new
|
||||
- [ ] Implement new validators: `IsFQDN`, `IsIMEI`, `IsPostalCode`, `IsISIN`, `IsISRC` etc
|
||||
- [x] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
|
||||
- [ ] Implement fuzzing testing
|
||||
- [ ] Implement some struct/map/array utilities
|
||||
- [ ] Implement map/array validation
|
||||
- [ ] Implement benchmarking
|
||||
- [ ] Implement batch of examples
|
||||
- [ ] Look at forks for new features and fixes
|
||||
|
||||
#### Advice
|
||||
Feel free to create what you want, but keep in mind when you implement new features:
|
||||
- Code must be clear and readable, names of variables/constants clearly describes what they are doing
|
||||
- Public functions must be documented and described in source file and added to README.md to the list of available functions
|
||||
- There are must be unit-tests for any new functions and improvements
|
||||
|
||||
## Credits
|
||||
### Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
|
||||
|
||||
#### Special thanks to [contributors](https://github.com/asaskevich/govalidator/graphs/contributors)
|
||||
* [Daniel Lohse](https://github.com/annismckenzie)
|
||||
* [Attila Oláh](https://github.com/attilaolah)
|
||||
* [Daniel Korner](https://github.com/Dadie)
|
||||
* [Steven Wilkin](https://github.com/stevenwilkin)
|
||||
* [Deiwin Sarjas](https://github.com/deiwin)
|
||||
* [Noah Shibley](https://github.com/slugmobile)
|
||||
* [Nathan Davies](https://github.com/nathj07)
|
||||
* [Matt Sanford](https://github.com/mzsanford)
|
||||
* [Simon ccl1115](https://github.com/ccl1115)
|
||||
|
||||
<a href="https://github.com/asaskevich/govalidator/graphs/contributors"><img src="https://opencollective.com/govalidator/contributors.svg?width=890" /></a>
|
||||
|
||||
|
||||
### Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/govalidator#backer)]
|
||||
|
||||
<a href="https://opencollective.com/govalidator#backers" target="_blank"><img src="https://opencollective.com/govalidator/backers.svg?width=890"></a>
|
||||
|
||||
|
||||
### Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/govalidator#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/govalidator/sponsor/0/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/1/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/2/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/3/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/4/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/5/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/6/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/7/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/8/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/9/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/9/avatar.svg"></a>
|
||||
|
||||
|
||||
|
||||
|
||||
## License
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator?ref=badge_large)
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package govalidator
|
||||
|
||||
// Iterator is the function that accepts element of slice/array and its index
|
||||
type Iterator func(interface{}, int)
|
||||
|
||||
// ResultIterator is the function that accepts element of slice/array and its index and returns any result
|
||||
type ResultIterator func(interface{}, int) interface{}
|
||||
|
||||
// ConditionIterator is the function that accepts element of slice/array and its index and returns boolean
|
||||
type ConditionIterator func(interface{}, int) bool
|
||||
|
||||
// ReduceIterator is the function that accepts two element of slice/array and returns result of merging those values
|
||||
type ReduceIterator func(interface{}, interface{}) interface{}
|
||||
|
||||
// Some validates that any item of array corresponds to ConditionIterator. Returns boolean.
|
||||
func Some(array []interface{}, iterator ConditionIterator) bool {
|
||||
res := false
|
||||
for index, data := range array {
|
||||
res = res || iterator(data, index)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Every validates that every item of array corresponds to ConditionIterator. Returns boolean.
|
||||
func Every(array []interface{}, iterator ConditionIterator) bool {
|
||||
res := true
|
||||
for index, data := range array {
|
||||
res = res && iterator(data, index)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Reduce boils down a list of values into a single value by ReduceIterator
|
||||
func Reduce(array []interface{}, iterator ReduceIterator, initialValue interface{}) interface{} {
|
||||
for _, data := range array {
|
||||
initialValue = iterator(initialValue, data)
|
||||
}
|
||||
return initialValue
|
||||
}
|
||||
|
||||
// Each iterates over the slice and apply Iterator to every item
|
||||
func Each(array []interface{}, iterator Iterator) {
|
||||
for index, data := range array {
|
||||
iterator(data, index)
|
||||
}
|
||||
}
|
||||
|
||||
// Map iterates over the slice and apply ResultIterator to every item. Returns new slice as a result.
|
||||
func Map(array []interface{}, iterator ResultIterator) []interface{} {
|
||||
var result = make([]interface{}, len(array))
|
||||
for index, data := range array {
|
||||
result[index] = iterator(data, index)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Find iterates over the slice and apply ConditionIterator to every item. Returns first item that meet ConditionIterator or nil otherwise.
|
||||
func Find(array []interface{}, iterator ConditionIterator) interface{} {
|
||||
for index, data := range array {
|
||||
if iterator(data, index) {
|
||||
return data
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter iterates over the slice and apply ConditionIterator to every item. Returns new slice.
|
||||
func Filter(array []interface{}, iterator ConditionIterator) []interface{} {
|
||||
var result = make([]interface{}, 0)
|
||||
for index, data := range array {
|
||||
if iterator(data, index) {
|
||||
result = append(result, data)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Count iterates over the slice and apply ConditionIterator to every item. Returns count of items that meets ConditionIterator.
|
||||
func Count(array []interface{}, iterator ConditionIterator) int {
|
||||
count := 0
|
||||
for index, data := range array {
|
||||
if iterator(data, index) {
|
||||
count = count + 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package govalidator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ToString convert the input to a string.
|
||||
func ToString(obj interface{}) string {
|
||||
res := fmt.Sprintf("%v", obj)
|
||||
return res
|
||||
}
|
||||
|
||||
// ToJSON convert the input to a valid JSON string
|
||||
func ToJSON(obj interface{}) (string, error) {
|
||||
res, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
res = []byte("")
|
||||
}
|
||||
return string(res), err
|
||||
}
|
||||
|
||||
// ToFloat convert the input string to a float, or 0.0 if the input is not a float.
|
||||
func ToFloat(value interface{}) (res float64, err error) {
|
||||
val := reflect.ValueOf(value)
|
||||
|
||||
switch value.(type) {
|
||||
case int, int8, int16, int32, int64:
|
||||
res = float64(val.Int())
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
res = float64(val.Uint())
|
||||
case float32, float64:
|
||||
res = val.Float()
|
||||
case string:
|
||||
res, err = strconv.ParseFloat(val.String(), 64)
|
||||
if err != nil {
|
||||
res = 0
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("ToInt: unknown interface type %T", value)
|
||||
res = 0
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ToInt convert the input string or any int type to an integer type 64, or 0 if the input is not an integer.
|
||||
func ToInt(value interface{}) (res int64, err error) {
|
||||
val := reflect.ValueOf(value)
|
||||
|
||||
switch value.(type) {
|
||||
case int, int8, int16, int32, int64:
|
||||
res = val.Int()
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
res = int64(val.Uint())
|
||||
case float32, float64:
|
||||
res = int64(val.Float())
|
||||
case string:
|
||||
if IsInt(val.String()) {
|
||||
res, err = strconv.ParseInt(val.String(), 0, 64)
|
||||
if err != nil {
|
||||
res = 0
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf("ToInt: invalid numeric format %g", value)
|
||||
res = 0
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("ToInt: unknown interface type %T", value)
|
||||
res = 0
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ToBoolean convert the input string to a boolean.
|
||||
func ToBoolean(str string) (bool, error) {
|
||||
return strconv.ParseBool(str)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package govalidator
|
||||
|
||||
// A package of validators and sanitizers for strings, structures and collections.
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package govalidator
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Errors is an array of multiple errors and conforms to the error interface.
|
||||
type Errors []error
|
||||
|
||||
// Errors returns itself.
|
||||
func (es Errors) Errors() []error {
|
||||
return es
|
||||
}
|
||||
|
||||
func (es Errors) Error() string {
|
||||
var errs []string
|
||||
for _, e := range es {
|
||||
errs = append(errs, e.Error())
|
||||
}
|
||||
sort.Strings(errs)
|
||||
return strings.Join(errs, ";")
|
||||
}
|
||||
|
||||
// Error encapsulates a name, an error and whether there's a custom error message or not.
|
||||
type Error struct {
|
||||
Name string
|
||||
Err error
|
||||
CustomErrorMessageExists bool
|
||||
|
||||
// Validator indicates the name of the validator that failed
|
||||
Validator string
|
||||
Path []string
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
if e.CustomErrorMessageExists {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
errName := e.Name
|
||||
if len(e.Path) > 0 {
|
||||
errName = strings.Join(append(e.Path, e.Name), ".")
|
||||
}
|
||||
|
||||
return errName + ": " + e.Err.Error()
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package govalidator
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
// Abs returns absolute value of number
|
||||
func Abs(value float64) float64 {
|
||||
return math.Abs(value)
|
||||
}
|
||||
|
||||
// Sign returns signum of number: 1 in case of value > 0, -1 in case of value < 0, 0 otherwise
|
||||
func Sign(value float64) float64 {
|
||||
if value > 0 {
|
||||
return 1
|
||||
} else if value < 0 {
|
||||
return -1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// IsNegative returns true if value < 0
|
||||
func IsNegative(value float64) bool {
|
||||
return value < 0
|
||||
}
|
||||
|
||||
// IsPositive returns true if value > 0
|
||||
func IsPositive(value float64) bool {
|
||||
return value > 0
|
||||
}
|
||||
|
||||
// IsNonNegative returns true if value >= 0
|
||||
func IsNonNegative(value float64) bool {
|
||||
return value >= 0
|
||||
}
|
||||
|
||||
// IsNonPositive returns true if value <= 0
|
||||
func IsNonPositive(value float64) bool {
|
||||
return value <= 0
|
||||
}
|
||||
|
||||
// InRangeInt returns true if value lies between left and right border
|
||||
func InRangeInt(value, left, right interface{}) bool {
|
||||
value64, _ := ToInt(value)
|
||||
left64, _ := ToInt(left)
|
||||
right64, _ := ToInt(right)
|
||||
if left64 > right64 {
|
||||
left64, right64 = right64, left64
|
||||
}
|
||||
return value64 >= left64 && value64 <= right64
|
||||
}
|
||||
|
||||
// InRangeFloat32 returns true if value lies between left and right border
|
||||
func InRangeFloat32(value, left, right float32) bool {
|
||||
if left > right {
|
||||
left, right = right, left
|
||||
}
|
||||
return value >= left && value <= right
|
||||
}
|
||||
|
||||
// InRangeFloat64 returns true if value lies between left and right border
|
||||
func InRangeFloat64(value, left, right float64) bool {
|
||||
if left > right {
|
||||
left, right = right, left
|
||||
}
|
||||
return value >= left && value <= right
|
||||
}
|
||||
|
||||
// InRange returns true if value lies between left and right border, generic type to handle int, float32, float64 and string.
|
||||
// All types must the same type.
|
||||
// False if value doesn't lie in range or if it incompatible or not comparable
|
||||
func InRange(value interface{}, left interface{}, right interface{}) bool {
|
||||
switch value.(type) {
|
||||
case int:
|
||||
intValue, _ := ToInt(value)
|
||||
intLeft, _ := ToInt(left)
|
||||
intRight, _ := ToInt(right)
|
||||
return InRangeInt(intValue, intLeft, intRight)
|
||||
case float32, float64:
|
||||
intValue, _ := ToFloat(value)
|
||||
intLeft, _ := ToFloat(left)
|
||||
intRight, _ := ToFloat(right)
|
||||
return InRangeFloat64(intValue, intLeft, intRight)
|
||||
case string:
|
||||
return value.(string) >= left.(string) && value.(string) <= right.(string)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsWhole returns true if value is whole number
|
||||
func IsWhole(value float64) bool {
|
||||
return math.Remainder(value, 1) == 0
|
||||
}
|
||||
|
||||
// IsNatural returns true if value is natural number (positive and whole)
|
||||
func IsNatural(value float64) bool {
|
||||
return IsWhole(value) && IsPositive(value)
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package govalidator
|
||||
|
||||
import "regexp"
|
||||
|
||||
// Basic regular expressions for validating strings
|
||||
const (
|
||||
Email string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
|
||||
CreditCard string = "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11}|6[27][0-9]{14})$"
|
||||
ISBN10 string = "^(?:[0-9]{9}X|[0-9]{10})$"
|
||||
ISBN13 string = "^(?:[0-9]{13})$"
|
||||
UUID3 string = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
UUID4 string = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||
UUID5 string = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||
UUID string = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
Alpha string = "^[a-zA-Z]+$"
|
||||
Alphanumeric string = "^[a-zA-Z0-9]+$"
|
||||
Numeric string = "^[0-9]+$"
|
||||
Int string = "^(?:[-+]?(?:0|[1-9][0-9]*))$"
|
||||
Float string = "^(?:[-+]?(?:[0-9]+))?(?:\\.[0-9]*)?(?:[eE][\\+\\-]?(?:[0-9]+))?$"
|
||||
Hexadecimal string = "^[0-9a-fA-F]+$"
|
||||
Hexcolor string = "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
|
||||
RGBcolor string = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*\\)$"
|
||||
ASCII string = "^[\x00-\x7F]+$"
|
||||
Multibyte string = "[^\x00-\x7F]"
|
||||
FullWidth string = "[^\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
|
||||
HalfWidth string = "[\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
|
||||
Base64 string = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
|
||||
PrintableASCII string = "^[\x20-\x7E]+$"
|
||||
DataURI string = "^data:.+\\/(.+);base64$"
|
||||
MagnetURI string = "^magnet:\\?xt=urn:[a-zA-Z0-9]+:[a-zA-Z0-9]{32,40}&dn=.+&tr=.+$"
|
||||
Latitude string = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"
|
||||
Longitude string = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"
|
||||
DNSName string = `^([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*[\._]?$`
|
||||
IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
|
||||
URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)`
|
||||
URLUsername string = `(\S+(:\S*)?@)`
|
||||
URLPath string = `((\/|\?|#)[^\s]*)`
|
||||
URLPort string = `(:(\d{1,5}))`
|
||||
URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3]|24\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-5]))`
|
||||
URLSubdomain string = `((www\.)|([a-zA-Z0-9]+([-_\.]?[a-zA-Z0-9])*[a-zA-Z0-9]\.[a-zA-Z0-9]+))`
|
||||
URL = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$`
|
||||
SSN string = `^\d{3}[- ]?\d{2}[- ]?\d{4}$`
|
||||
WinPath string = `^[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$`
|
||||
UnixPath string = `^(/[^/\x00]*)+/?$`
|
||||
WinARPath string = `^(?:(?:[a-zA-Z]:|\\\\[a-z0-9_.$●-]+\\[a-z0-9_.$●-]+)\\|\\?[^\\/:*?"<>|\r\n]+\\?)(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$`
|
||||
UnixARPath string = `^((\.{0,2}/)?([^/\x00]*))+/?$`
|
||||
Semver string = "^v?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(-(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\\+[0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*)?$"
|
||||
tagName string = "valid"
|
||||
hasLowerCase string = ".*[[:lower:]]"
|
||||
hasUpperCase string = ".*[[:upper:]]"
|
||||
hasWhitespace string = ".*[[:space:]]"
|
||||
hasWhitespaceOnly string = "^[[:space:]]+$"
|
||||
IMEI string = "^[0-9a-f]{14}$|^\\d{15}$|^\\d{18}$"
|
||||
IMSI string = "^\\d{14,15}$"
|
||||
E164 string = `^\+?[1-9]\d{1,14}$`
|
||||
)
|
||||
|
||||
// Used by IsFilePath func
|
||||
const (
|
||||
// Unknown is unresolved OS type
|
||||
Unknown = iota
|
||||
// Win is Windows type
|
||||
Win
|
||||
// Unix is *nix OS types
|
||||
Unix
|
||||
)
|
||||
|
||||
var (
|
||||
userRegexp = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+$")
|
||||
hostRegexp = regexp.MustCompile("^[^\\s]+\\.[^\\s]+$")
|
||||
userDotRegexp = regexp.MustCompile("(^[.]{1})|([.]{1}$)|([.]{2,})")
|
||||
rxEmail = regexp.MustCompile(Email)
|
||||
rxCreditCard = regexp.MustCompile(CreditCard)
|
||||
rxISBN10 = regexp.MustCompile(ISBN10)
|
||||
rxISBN13 = regexp.MustCompile(ISBN13)
|
||||
rxUUID3 = regexp.MustCompile(UUID3)
|
||||
rxUUID4 = regexp.MustCompile(UUID4)
|
||||
rxUUID5 = regexp.MustCompile(UUID5)
|
||||
rxUUID = regexp.MustCompile(UUID)
|
||||
rxAlpha = regexp.MustCompile(Alpha)
|
||||
rxAlphanumeric = regexp.MustCompile(Alphanumeric)
|
||||
rxNumeric = regexp.MustCompile(Numeric)
|
||||
rxInt = regexp.MustCompile(Int)
|
||||
rxFloat = regexp.MustCompile(Float)
|
||||
rxHexadecimal = regexp.MustCompile(Hexadecimal)
|
||||
rxHexcolor = regexp.MustCompile(Hexcolor)
|
||||
rxRGBcolor = regexp.MustCompile(RGBcolor)
|
||||
rxASCII = regexp.MustCompile(ASCII)
|
||||
rxPrintableASCII = regexp.MustCompile(PrintableASCII)
|
||||
rxMultibyte = regexp.MustCompile(Multibyte)
|
||||
rxFullWidth = regexp.MustCompile(FullWidth)
|
||||
rxHalfWidth = regexp.MustCompile(HalfWidth)
|
||||
rxBase64 = regexp.MustCompile(Base64)
|
||||
rxDataURI = regexp.MustCompile(DataURI)
|
||||
rxMagnetURI = regexp.MustCompile(MagnetURI)
|
||||
rxLatitude = regexp.MustCompile(Latitude)
|
||||
rxLongitude = regexp.MustCompile(Longitude)
|
||||
rxDNSName = regexp.MustCompile(DNSName)
|
||||
rxURL = regexp.MustCompile(URL)
|
||||
rxSSN = regexp.MustCompile(SSN)
|
||||
rxWinPath = regexp.MustCompile(WinPath)
|
||||
rxUnixPath = regexp.MustCompile(UnixPath)
|
||||
rxARWinPath = regexp.MustCompile(WinARPath)
|
||||
rxARUnixPath = regexp.MustCompile(UnixARPath)
|
||||
rxSemver = regexp.MustCompile(Semver)
|
||||
rxHasLowerCase = regexp.MustCompile(hasLowerCase)
|
||||
rxHasUpperCase = regexp.MustCompile(hasUpperCase)
|
||||
rxHasWhitespace = regexp.MustCompile(hasWhitespace)
|
||||
rxHasWhitespaceOnly = regexp.MustCompile(hasWhitespaceOnly)
|
||||
rxIMEI = regexp.MustCompile(IMEI)
|
||||
rxIMSI = regexp.MustCompile(IMSI)
|
||||
rxE164 = regexp.MustCompile(E164)
|
||||
)
|
||||
|
|
@ -0,0 +1,656 @@
|
|||
package govalidator
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Validator is a wrapper for a validator function that returns bool and accepts string.
|
||||
type Validator func(str string) bool
|
||||
|
||||
// CustomTypeValidator is a wrapper for validator functions that returns bool and accepts any type.
|
||||
// The second parameter should be the context (in the case of validating a struct: the whole object being validated).
|
||||
type CustomTypeValidator func(i interface{}, o interface{}) bool
|
||||
|
||||
// ParamValidator is a wrapper for validator functions that accept additional parameters.
|
||||
type ParamValidator func(str string, params ...string) bool
|
||||
|
||||
// InterfaceParamValidator is a wrapper for functions that accept variants parameters for an interface value
|
||||
type InterfaceParamValidator func(in interface{}, params ...string) bool
|
||||
type tagOptionsMap map[string]tagOption
|
||||
|
||||
func (t tagOptionsMap) orderedKeys() []string {
|
||||
var keys []string
|
||||
for k := range t {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Slice(keys, func(a, b int) bool {
|
||||
return t[keys[a]].order < t[keys[b]].order
|
||||
})
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
type tagOption struct {
|
||||
name string
|
||||
customErrorMessage string
|
||||
order int
|
||||
}
|
||||
|
||||
// UnsupportedTypeError is a wrapper for reflect.Type
|
||||
type UnsupportedTypeError struct {
|
||||
Type reflect.Type
|
||||
}
|
||||
|
||||
// stringValues is a slice of reflect.Value holding *reflect.StringValue.
|
||||
// It implements the methods to sort by string.
|
||||
type stringValues []reflect.Value
|
||||
|
||||
// InterfaceParamTagMap is a map of functions accept variants parameters for an interface value
|
||||
var InterfaceParamTagMap = map[string]InterfaceParamValidator{
|
||||
"type": IsType,
|
||||
}
|
||||
|
||||
// InterfaceParamTagRegexMap maps interface param tags to their respective regexes.
|
||||
var InterfaceParamTagRegexMap = map[string]*regexp.Regexp{
|
||||
"type": regexp.MustCompile(`^type\((.*)\)$`),
|
||||
}
|
||||
|
||||
// ParamTagMap is a map of functions accept variants parameters
|
||||
var ParamTagMap = map[string]ParamValidator{
|
||||
"length": ByteLength,
|
||||
"range": Range,
|
||||
"runelength": RuneLength,
|
||||
"stringlength": StringLength,
|
||||
"matches": StringMatches,
|
||||
"in": IsInRaw,
|
||||
"rsapub": IsRsaPub,
|
||||
"minstringlength": MinStringLength,
|
||||
"maxstringlength": MaxStringLength,
|
||||
}
|
||||
|
||||
// ParamTagRegexMap maps param tags to their respective regexes.
|
||||
var ParamTagRegexMap = map[string]*regexp.Regexp{
|
||||
"range": regexp.MustCompile("^range\\((\\d+)\\|(\\d+)\\)$"),
|
||||
"length": regexp.MustCompile("^length\\((\\d+)\\|(\\d+)\\)$"),
|
||||
"runelength": regexp.MustCompile("^runelength\\((\\d+)\\|(\\d+)\\)$"),
|
||||
"stringlength": regexp.MustCompile("^stringlength\\((\\d+)\\|(\\d+)\\)$"),
|
||||
"in": regexp.MustCompile(`^in\((.*)\)`),
|
||||
"matches": regexp.MustCompile(`^matches\((.+)\)$`),
|
||||
"rsapub": regexp.MustCompile("^rsapub\\((\\d+)\\)$"),
|
||||
"minstringlength": regexp.MustCompile("^minstringlength\\((\\d+)\\)$"),
|
||||
"maxstringlength": regexp.MustCompile("^maxstringlength\\((\\d+)\\)$"),
|
||||
}
|
||||
|
||||
type customTypeTagMap struct {
|
||||
validators map[string]CustomTypeValidator
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (tm *customTypeTagMap) Get(name string) (CustomTypeValidator, bool) {
|
||||
tm.RLock()
|
||||
defer tm.RUnlock()
|
||||
v, ok := tm.validators[name]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (tm *customTypeTagMap) Set(name string, ctv CustomTypeValidator) {
|
||||
tm.Lock()
|
||||
defer tm.Unlock()
|
||||
tm.validators[name] = ctv
|
||||
}
|
||||
|
||||
// CustomTypeTagMap is a map of functions that can be used as tags for ValidateStruct function.
|
||||
// Use this to validate compound or custom types that need to be handled as a whole, e.g.
|
||||
// `type UUID [16]byte` (this would be handled as an array of bytes).
|
||||
var CustomTypeTagMap = &customTypeTagMap{validators: make(map[string]CustomTypeValidator)}
|
||||
|
||||
// TagMap is a map of functions, that can be used as tags for ValidateStruct function.
|
||||
var TagMap = map[string]Validator{
|
||||
"email": IsEmail,
|
||||
"url": IsURL,
|
||||
"dialstring": IsDialString,
|
||||
"requrl": IsRequestURL,
|
||||
"requri": IsRequestURI,
|
||||
"alpha": IsAlpha,
|
||||
"utfletter": IsUTFLetter,
|
||||
"alphanum": IsAlphanumeric,
|
||||
"utfletternum": IsUTFLetterNumeric,
|
||||
"numeric": IsNumeric,
|
||||
"utfnumeric": IsUTFNumeric,
|
||||
"utfdigit": IsUTFDigit,
|
||||
"hexadecimal": IsHexadecimal,
|
||||
"hexcolor": IsHexcolor,
|
||||
"rgbcolor": IsRGBcolor,
|
||||
"lowercase": IsLowerCase,
|
||||
"uppercase": IsUpperCase,
|
||||
"int": IsInt,
|
||||
"float": IsFloat,
|
||||
"null": IsNull,
|
||||
"notnull": IsNotNull,
|
||||
"uuid": IsUUID,
|
||||
"uuidv3": IsUUIDv3,
|
||||
"uuidv4": IsUUIDv4,
|
||||
"uuidv5": IsUUIDv5,
|
||||
"creditcard": IsCreditCard,
|
||||
"isbn10": IsISBN10,
|
||||
"isbn13": IsISBN13,
|
||||
"json": IsJSON,
|
||||
"multibyte": IsMultibyte,
|
||||
"ascii": IsASCII,
|
||||
"printableascii": IsPrintableASCII,
|
||||
"fullwidth": IsFullWidth,
|
||||
"halfwidth": IsHalfWidth,
|
||||
"variablewidth": IsVariableWidth,
|
||||
"base64": IsBase64,
|
||||
"datauri": IsDataURI,
|
||||
"ip": IsIP,
|
||||
"port": IsPort,
|
||||
"ipv4": IsIPv4,
|
||||
"ipv6": IsIPv6,
|
||||
"dns": IsDNSName,
|
||||
"host": IsHost,
|
||||
"mac": IsMAC,
|
||||
"latitude": IsLatitude,
|
||||
"longitude": IsLongitude,
|
||||
"ssn": IsSSN,
|
||||
"semver": IsSemver,
|
||||
"rfc3339": IsRFC3339,
|
||||
"rfc3339WithoutZone": IsRFC3339WithoutZone,
|
||||
"ISO3166Alpha2": IsISO3166Alpha2,
|
||||
"ISO3166Alpha3": IsISO3166Alpha3,
|
||||
"ISO4217": IsISO4217,
|
||||
"IMEI": IsIMEI,
|
||||
"ulid": IsULID,
|
||||
}
|
||||
|
||||
// ISO3166Entry stores country codes
|
||||
type ISO3166Entry struct {
|
||||
EnglishShortName string
|
||||
FrenchShortName string
|
||||
Alpha2Code string
|
||||
Alpha3Code string
|
||||
Numeric string
|
||||
}
|
||||
|
||||
//ISO3166List based on https://www.iso.org/obp/ui/#search/code/ Code Type "Officially Assigned Codes"
|
||||
var ISO3166List = []ISO3166Entry{
|
||||
{"Afghanistan", "Afghanistan (l')", "AF", "AFG", "004"},
|
||||
{"Albania", "Albanie (l')", "AL", "ALB", "008"},
|
||||
{"Antarctica", "Antarctique (l')", "AQ", "ATA", "010"},
|
||||
{"Algeria", "Algérie (l')", "DZ", "DZA", "012"},
|
||||
{"American Samoa", "Samoa américaines (les)", "AS", "ASM", "016"},
|
||||
{"Andorra", "Andorre (l')", "AD", "AND", "020"},
|
||||
{"Angola", "Angola (l')", "AO", "AGO", "024"},
|
||||
{"Antigua and Barbuda", "Antigua-et-Barbuda", "AG", "ATG", "028"},
|
||||
{"Azerbaijan", "Azerbaïdjan (l')", "AZ", "AZE", "031"},
|
||||
{"Argentina", "Argentine (l')", "AR", "ARG", "032"},
|
||||
{"Australia", "Australie (l')", "AU", "AUS", "036"},
|
||||
{"Austria", "Autriche (l')", "AT", "AUT", "040"},
|
||||
{"Bahamas (the)", "Bahamas (les)", "BS", "BHS", "044"},
|
||||
{"Bahrain", "Bahreïn", "BH", "BHR", "048"},
|
||||
{"Bangladesh", "Bangladesh (le)", "BD", "BGD", "050"},
|
||||
{"Armenia", "Arménie (l')", "AM", "ARM", "051"},
|
||||
{"Barbados", "Barbade (la)", "BB", "BRB", "052"},
|
||||
{"Belgium", "Belgique (la)", "BE", "BEL", "056"},
|
||||
{"Bermuda", "Bermudes (les)", "BM", "BMU", "060"},
|
||||
{"Bhutan", "Bhoutan (le)", "BT", "BTN", "064"},
|
||||
{"Bolivia (Plurinational State of)", "Bolivie (État plurinational de)", "BO", "BOL", "068"},
|
||||
{"Bosnia and Herzegovina", "Bosnie-Herzégovine (la)", "BA", "BIH", "070"},
|
||||
{"Botswana", "Botswana (le)", "BW", "BWA", "072"},
|
||||
{"Bouvet Island", "Bouvet (l'Île)", "BV", "BVT", "074"},
|
||||
{"Brazil", "Brésil (le)", "BR", "BRA", "076"},
|
||||
{"Belize", "Belize (le)", "BZ", "BLZ", "084"},
|
||||
{"British Indian Ocean Territory (the)", "Indien (le Territoire britannique de l'océan)", "IO", "IOT", "086"},
|
||||
{"Solomon Islands", "Salomon (Îles)", "SB", "SLB", "090"},
|
||||
{"Virgin Islands (British)", "Vierges britanniques (les Îles)", "VG", "VGB", "092"},
|
||||
{"Brunei Darussalam", "Brunéi Darussalam (le)", "BN", "BRN", "096"},
|
||||
{"Bulgaria", "Bulgarie (la)", "BG", "BGR", "100"},
|
||||
{"Myanmar", "Myanmar (le)", "MM", "MMR", "104"},
|
||||
{"Burundi", "Burundi (le)", "BI", "BDI", "108"},
|
||||
{"Belarus", "Bélarus (le)", "BY", "BLR", "112"},
|
||||
{"Cambodia", "Cambodge (le)", "KH", "KHM", "116"},
|
||||
{"Cameroon", "Cameroun (le)", "CM", "CMR", "120"},
|
||||
{"Canada", "Canada (le)", "CA", "CAN", "124"},
|
||||
{"Cabo Verde", "Cabo Verde", "CV", "CPV", "132"},
|
||||
{"Cayman Islands (the)", "Caïmans (les Îles)", "KY", "CYM", "136"},
|
||||
{"Central African Republic (the)", "République centrafricaine (la)", "CF", "CAF", "140"},
|
||||
{"Sri Lanka", "Sri Lanka", "LK", "LKA", "144"},
|
||||
{"Chad", "Tchad (le)", "TD", "TCD", "148"},
|
||||
{"Chile", "Chili (le)", "CL", "CHL", "152"},
|
||||
{"China", "Chine (la)", "CN", "CHN", "156"},
|
||||
{"Taiwan (Province of China)", "Taïwan (Province de Chine)", "TW", "TWN", "158"},
|
||||
{"Christmas Island", "Christmas (l'Île)", "CX", "CXR", "162"},
|
||||
{"Cocos (Keeling) Islands (the)", "Cocos (les Îles)/ Keeling (les Îles)", "CC", "CCK", "166"},
|
||||
{"Colombia", "Colombie (la)", "CO", "COL", "170"},
|
||||
{"Comoros (the)", "Comores (les)", "KM", "COM", "174"},
|
||||
{"Mayotte", "Mayotte", "YT", "MYT", "175"},
|
||||
{"Congo (the)", "Congo (le)", "CG", "COG", "178"},
|
||||
{"Congo (the Democratic Republic of the)", "Congo (la République démocratique du)", "CD", "COD", "180"},
|
||||
{"Cook Islands (the)", "Cook (les Îles)", "CK", "COK", "184"},
|
||||
{"Costa Rica", "Costa Rica (le)", "CR", "CRI", "188"},
|
||||
{"Croatia", "Croatie (la)", "HR", "HRV", "191"},
|
||||
{"Cuba", "Cuba", "CU", "CUB", "192"},
|
||||
{"Cyprus", "Chypre", "CY", "CYP", "196"},
|
||||
{"Czech Republic (the)", "tchèque (la République)", "CZ", "CZE", "203"},
|
||||
{"Benin", "Bénin (le)", "BJ", "BEN", "204"},
|
||||
{"Denmark", "Danemark (le)", "DK", "DNK", "208"},
|
||||
{"Dominica", "Dominique (la)", "DM", "DMA", "212"},
|
||||
{"Dominican Republic (the)", "dominicaine (la République)", "DO", "DOM", "214"},
|
||||
{"Ecuador", "Équateur (l')", "EC", "ECU", "218"},
|
||||
{"El Salvador", "El Salvador", "SV", "SLV", "222"},
|
||||
{"Equatorial Guinea", "Guinée équatoriale (la)", "GQ", "GNQ", "226"},
|
||||
{"Ethiopia", "Éthiopie (l')", "ET", "ETH", "231"},
|
||||
{"Eritrea", "Érythrée (l')", "ER", "ERI", "232"},
|
||||
{"Estonia", "Estonie (l')", "EE", "EST", "233"},
|
||||
{"Faroe Islands (the)", "Féroé (les Îles)", "FO", "FRO", "234"},
|
||||
{"Falkland Islands (the) [Malvinas]", "Falkland (les Îles)/Malouines (les Îles)", "FK", "FLK", "238"},
|
||||
{"South Georgia and the South Sandwich Islands", "Géorgie du Sud-et-les Îles Sandwich du Sud (la)", "GS", "SGS", "239"},
|
||||
{"Fiji", "Fidji (les)", "FJ", "FJI", "242"},
|
||||
{"Finland", "Finlande (la)", "FI", "FIN", "246"},
|
||||
{"Åland Islands", "Åland(les Îles)", "AX", "ALA", "248"},
|
||||
{"France", "France (la)", "FR", "FRA", "250"},
|
||||
{"French Guiana", "Guyane française (la )", "GF", "GUF", "254"},
|
||||
{"French Polynesia", "Polynésie française (la)", "PF", "PYF", "258"},
|
||||
{"French Southern Territories (the)", "Terres australes françaises (les)", "TF", "ATF", "260"},
|
||||
{"Djibouti", "Djibouti", "DJ", "DJI", "262"},
|
||||
{"Gabon", "Gabon (le)", "GA", "GAB", "266"},
|
||||
{"Georgia", "Géorgie (la)", "GE", "GEO", "268"},
|
||||
{"Gambia (the)", "Gambie (la)", "GM", "GMB", "270"},
|
||||
{"Palestine, State of", "Palestine, État de", "PS", "PSE", "275"},
|
||||
{"Germany", "Allemagne (l')", "DE", "DEU", "276"},
|
||||
{"Ghana", "Ghana (le)", "GH", "GHA", "288"},
|
||||
{"Gibraltar", "Gibraltar", "GI", "GIB", "292"},
|
||||
{"Kiribati", "Kiribati", "KI", "KIR", "296"},
|
||||
{"Greece", "Grèce (la)", "GR", "GRC", "300"},
|
||||
{"Greenland", "Groenland (le)", "GL", "GRL", "304"},
|
||||
{"Grenada", "Grenade (la)", "GD", "GRD", "308"},
|
||||
{"Guadeloupe", "Guadeloupe (la)", "GP", "GLP", "312"},
|
||||
{"Guam", "Guam", "GU", "GUM", "316"},
|
||||
{"Guatemala", "Guatemala (le)", "GT", "GTM", "320"},
|
||||
{"Guinea", "Guinée (la)", "GN", "GIN", "324"},
|
||||
{"Guyana", "Guyana (le)", "GY", "GUY", "328"},
|
||||
{"Haiti", "Haïti", "HT", "HTI", "332"},
|
||||
{"Heard Island and McDonald Islands", "Heard-et-Îles MacDonald (l'Île)", "HM", "HMD", "334"},
|
||||
{"Holy See (the)", "Saint-Siège (le)", "VA", "VAT", "336"},
|
||||
{"Honduras", "Honduras (le)", "HN", "HND", "340"},
|
||||
{"Hong Kong", "Hong Kong", "HK", "HKG", "344"},
|
||||
{"Hungary", "Hongrie (la)", "HU", "HUN", "348"},
|
||||
{"Iceland", "Islande (l')", "IS", "ISL", "352"},
|
||||
{"India", "Inde (l')", "IN", "IND", "356"},
|
||||
{"Indonesia", "Indonésie (l')", "ID", "IDN", "360"},
|
||||
{"Iran (Islamic Republic of)", "Iran (République Islamique d')", "IR", "IRN", "364"},
|
||||
{"Iraq", "Iraq (l')", "IQ", "IRQ", "368"},
|
||||
{"Ireland", "Irlande (l')", "IE", "IRL", "372"},
|
||||
{"Israel", "Israël", "IL", "ISR", "376"},
|
||||
{"Italy", "Italie (l')", "IT", "ITA", "380"},
|
||||
{"Côte d'Ivoire", "Côte d'Ivoire (la)", "CI", "CIV", "384"},
|
||||
{"Jamaica", "Jamaïque (la)", "JM", "JAM", "388"},
|
||||
{"Japan", "Japon (le)", "JP", "JPN", "392"},
|
||||
{"Kazakhstan", "Kazakhstan (le)", "KZ", "KAZ", "398"},
|
||||
{"Jordan", "Jordanie (la)", "JO", "JOR", "400"},
|
||||
{"Kenya", "Kenya (le)", "KE", "KEN", "404"},
|
||||
{"Korea (the Democratic People's Republic of)", "Corée (la République populaire démocratique de)", "KP", "PRK", "408"},
|
||||
{"Korea (the Republic of)", "Corée (la République de)", "KR", "KOR", "410"},
|
||||
{"Kuwait", "Koweït (le)", "KW", "KWT", "414"},
|
||||
{"Kyrgyzstan", "Kirghizistan (le)", "KG", "KGZ", "417"},
|
||||
{"Lao People's Democratic Republic (the)", "Lao, République démocratique populaire", "LA", "LAO", "418"},
|
||||
{"Lebanon", "Liban (le)", "LB", "LBN", "422"},
|
||||
{"Lesotho", "Lesotho (le)", "LS", "LSO", "426"},
|
||||
{"Latvia", "Lettonie (la)", "LV", "LVA", "428"},
|
||||
{"Liberia", "Libéria (le)", "LR", "LBR", "430"},
|
||||
{"Libya", "Libye (la)", "LY", "LBY", "434"},
|
||||
{"Liechtenstein", "Liechtenstein (le)", "LI", "LIE", "438"},
|
||||
{"Lithuania", "Lituanie (la)", "LT", "LTU", "440"},
|
||||
{"Luxembourg", "Luxembourg (le)", "LU", "LUX", "442"},
|
||||
{"Macao", "Macao", "MO", "MAC", "446"},
|
||||
{"Madagascar", "Madagascar", "MG", "MDG", "450"},
|
||||
{"Malawi", "Malawi (le)", "MW", "MWI", "454"},
|
||||
{"Malaysia", "Malaisie (la)", "MY", "MYS", "458"},
|
||||
{"Maldives", "Maldives (les)", "MV", "MDV", "462"},
|
||||
{"Mali", "Mali (le)", "ML", "MLI", "466"},
|
||||
{"Malta", "Malte", "MT", "MLT", "470"},
|
||||
{"Martinique", "Martinique (la)", "MQ", "MTQ", "474"},
|
||||
{"Mauritania", "Mauritanie (la)", "MR", "MRT", "478"},
|
||||
{"Mauritius", "Maurice", "MU", "MUS", "480"},
|
||||
{"Mexico", "Mexique (le)", "MX", "MEX", "484"},
|
||||
{"Monaco", "Monaco", "MC", "MCO", "492"},
|
||||
{"Mongolia", "Mongolie (la)", "MN", "MNG", "496"},
|
||||
{"Moldova (the Republic of)", "Moldova , République de", "MD", "MDA", "498"},
|
||||
{"Montenegro", "Monténégro (le)", "ME", "MNE", "499"},
|
||||
{"Montserrat", "Montserrat", "MS", "MSR", "500"},
|
||||
{"Morocco", "Maroc (le)", "MA", "MAR", "504"},
|
||||
{"Mozambique", "Mozambique (le)", "MZ", "MOZ", "508"},
|
||||
{"Oman", "Oman", "OM", "OMN", "512"},
|
||||
{"Namibia", "Namibie (la)", "NA", "NAM", "516"},
|
||||
{"Nauru", "Nauru", "NR", "NRU", "520"},
|
||||
{"Nepal", "Népal (le)", "NP", "NPL", "524"},
|
||||
{"Netherlands (the)", "Pays-Bas (les)", "NL", "NLD", "528"},
|
||||
{"Curaçao", "Curaçao", "CW", "CUW", "531"},
|
||||
{"Aruba", "Aruba", "AW", "ABW", "533"},
|
||||
{"Sint Maarten (Dutch part)", "Saint-Martin (partie néerlandaise)", "SX", "SXM", "534"},
|
||||
{"Bonaire, Sint Eustatius and Saba", "Bonaire, Saint-Eustache et Saba", "BQ", "BES", "535"},
|
||||
{"New Caledonia", "Nouvelle-Calédonie (la)", "NC", "NCL", "540"},
|
||||
{"Vanuatu", "Vanuatu (le)", "VU", "VUT", "548"},
|
||||
{"New Zealand", "Nouvelle-Zélande (la)", "NZ", "NZL", "554"},
|
||||
{"Nicaragua", "Nicaragua (le)", "NI", "NIC", "558"},
|
||||
{"Niger (the)", "Niger (le)", "NE", "NER", "562"},
|
||||
{"Nigeria", "Nigéria (le)", "NG", "NGA", "566"},
|
||||
{"Niue", "Niue", "NU", "NIU", "570"},
|
||||
{"Norfolk Island", "Norfolk (l'Île)", "NF", "NFK", "574"},
|
||||
{"Norway", "Norvège (la)", "NO", "NOR", "578"},
|
||||
{"Northern Mariana Islands (the)", "Mariannes du Nord (les Îles)", "MP", "MNP", "580"},
|
||||
{"United States Minor Outlying Islands (the)", "Îles mineures éloignées des États-Unis (les)", "UM", "UMI", "581"},
|
||||
{"Micronesia (Federated States of)", "Micronésie (États fédérés de)", "FM", "FSM", "583"},
|
||||
{"Marshall Islands (the)", "Marshall (Îles)", "MH", "MHL", "584"},
|
||||
{"Palau", "Palaos (les)", "PW", "PLW", "585"},
|
||||
{"Pakistan", "Pakistan (le)", "PK", "PAK", "586"},
|
||||
{"Panama", "Panama (le)", "PA", "PAN", "591"},
|
||||
{"Papua New Guinea", "Papouasie-Nouvelle-Guinée (la)", "PG", "PNG", "598"},
|
||||
{"Paraguay", "Paraguay (le)", "PY", "PRY", "600"},
|
||||
{"Peru", "Pérou (le)", "PE", "PER", "604"},
|
||||
{"Philippines (the)", "Philippines (les)", "PH", "PHL", "608"},
|
||||
{"Pitcairn", "Pitcairn", "PN", "PCN", "612"},
|
||||
{"Poland", "Pologne (la)", "PL", "POL", "616"},
|
||||
{"Portugal", "Portugal (le)", "PT", "PRT", "620"},
|
||||
{"Guinea-Bissau", "Guinée-Bissau (la)", "GW", "GNB", "624"},
|
||||
{"Timor-Leste", "Timor-Leste (le)", "TL", "TLS", "626"},
|
||||
{"Puerto Rico", "Porto Rico", "PR", "PRI", "630"},
|
||||
{"Qatar", "Qatar (le)", "QA", "QAT", "634"},
|
||||
{"Réunion", "Réunion (La)", "RE", "REU", "638"},
|
||||
{"Romania", "Roumanie (la)", "RO", "ROU", "642"},
|
||||
{"Russian Federation (the)", "Russie (la Fédération de)", "RU", "RUS", "643"},
|
||||
{"Rwanda", "Rwanda (le)", "RW", "RWA", "646"},
|
||||
{"Saint Barthélemy", "Saint-Barthélemy", "BL", "BLM", "652"},
|
||||
{"Saint Helena, Ascension and Tristan da Cunha", "Sainte-Hélène, Ascension et Tristan da Cunha", "SH", "SHN", "654"},
|
||||
{"Saint Kitts and Nevis", "Saint-Kitts-et-Nevis", "KN", "KNA", "659"},
|
||||
{"Anguilla", "Anguilla", "AI", "AIA", "660"},
|
||||
{"Saint Lucia", "Sainte-Lucie", "LC", "LCA", "662"},
|
||||
{"Saint Martin (French part)", "Saint-Martin (partie française)", "MF", "MAF", "663"},
|
||||
{"Saint Pierre and Miquelon", "Saint-Pierre-et-Miquelon", "PM", "SPM", "666"},
|
||||
{"Saint Vincent and the Grenadines", "Saint-Vincent-et-les Grenadines", "VC", "VCT", "670"},
|
||||
{"San Marino", "Saint-Marin", "SM", "SMR", "674"},
|
||||
{"Sao Tome and Principe", "Sao Tomé-et-Principe", "ST", "STP", "678"},
|
||||
{"Saudi Arabia", "Arabie saoudite (l')", "SA", "SAU", "682"},
|
||||
{"Senegal", "Sénégal (le)", "SN", "SEN", "686"},
|
||||
{"Serbia", "Serbie (la)", "RS", "SRB", "688"},
|
||||
{"Seychelles", "Seychelles (les)", "SC", "SYC", "690"},
|
||||
{"Sierra Leone", "Sierra Leone (la)", "SL", "SLE", "694"},
|
||||
{"Singapore", "Singapour", "SG", "SGP", "702"},
|
||||
{"Slovakia", "Slovaquie (la)", "SK", "SVK", "703"},
|
||||
{"Viet Nam", "Viet Nam (le)", "VN", "VNM", "704"},
|
||||
{"Slovenia", "Slovénie (la)", "SI", "SVN", "705"},
|
||||
{"Somalia", "Somalie (la)", "SO", "SOM", "706"},
|
||||
{"South Africa", "Afrique du Sud (l')", "ZA", "ZAF", "710"},
|
||||
{"Zimbabwe", "Zimbabwe (le)", "ZW", "ZWE", "716"},
|
||||
{"Spain", "Espagne (l')", "ES", "ESP", "724"},
|
||||
{"South Sudan", "Soudan du Sud (le)", "SS", "SSD", "728"},
|
||||
{"Sudan (the)", "Soudan (le)", "SD", "SDN", "729"},
|
||||
{"Western Sahara*", "Sahara occidental (le)*", "EH", "ESH", "732"},
|
||||
{"Suriname", "Suriname (le)", "SR", "SUR", "740"},
|
||||
{"Svalbard and Jan Mayen", "Svalbard et l'Île Jan Mayen (le)", "SJ", "SJM", "744"},
|
||||
{"Swaziland", "Swaziland (le)", "SZ", "SWZ", "748"},
|
||||
{"Sweden", "Suède (la)", "SE", "SWE", "752"},
|
||||
{"Switzerland", "Suisse (la)", "CH", "CHE", "756"},
|
||||
{"Syrian Arab Republic", "République arabe syrienne (la)", "SY", "SYR", "760"},
|
||||
{"Tajikistan", "Tadjikistan (le)", "TJ", "TJK", "762"},
|
||||
{"Thailand", "Thaïlande (la)", "TH", "THA", "764"},
|
||||
{"Togo", "Togo (le)", "TG", "TGO", "768"},
|
||||
{"Tokelau", "Tokelau (les)", "TK", "TKL", "772"},
|
||||
{"Tonga", "Tonga (les)", "TO", "TON", "776"},
|
||||
{"Trinidad and Tobago", "Trinité-et-Tobago (la)", "TT", "TTO", "780"},
|
||||
{"United Arab Emirates (the)", "Émirats arabes unis (les)", "AE", "ARE", "784"},
|
||||
{"Tunisia", "Tunisie (la)", "TN", "TUN", "788"},
|
||||
{"Turkey", "Turquie (la)", "TR", "TUR", "792"},
|
||||
{"Turkmenistan", "Turkménistan (le)", "TM", "TKM", "795"},
|
||||
{"Turks and Caicos Islands (the)", "Turks-et-Caïcos (les Îles)", "TC", "TCA", "796"},
|
||||
{"Tuvalu", "Tuvalu (les)", "TV", "TUV", "798"},
|
||||
{"Uganda", "Ouganda (l')", "UG", "UGA", "800"},
|
||||
{"Ukraine", "Ukraine (l')", "UA", "UKR", "804"},
|
||||
{"Macedonia (the former Yugoslav Republic of)", "Macédoine (l'ex‑République yougoslave de)", "MK", "MKD", "807"},
|
||||
{"Egypt", "Égypte (l')", "EG", "EGY", "818"},
|
||||
{"United Kingdom of Great Britain and Northern Ireland (the)", "Royaume-Uni de Grande-Bretagne et d'Irlande du Nord (le)", "GB", "GBR", "826"},
|
||||
{"Guernsey", "Guernesey", "GG", "GGY", "831"},
|
||||
{"Jersey", "Jersey", "JE", "JEY", "832"},
|
||||
{"Isle of Man", "Île de Man", "IM", "IMN", "833"},
|
||||
{"Tanzania, United Republic of", "Tanzanie, République-Unie de", "TZ", "TZA", "834"},
|
||||
{"United States of America (the)", "États-Unis d'Amérique (les)", "US", "USA", "840"},
|
||||
{"Virgin Islands (U.S.)", "Vierges des États-Unis (les Îles)", "VI", "VIR", "850"},
|
||||
{"Burkina Faso", "Burkina Faso (le)", "BF", "BFA", "854"},
|
||||
{"Uruguay", "Uruguay (l')", "UY", "URY", "858"},
|
||||
{"Uzbekistan", "Ouzbékistan (l')", "UZ", "UZB", "860"},
|
||||
{"Venezuela (Bolivarian Republic of)", "Venezuela (République bolivarienne du)", "VE", "VEN", "862"},
|
||||
{"Wallis and Futuna", "Wallis-et-Futuna", "WF", "WLF", "876"},
|
||||
{"Samoa", "Samoa (le)", "WS", "WSM", "882"},
|
||||
{"Yemen", "Yémen (le)", "YE", "YEM", "887"},
|
||||
{"Zambia", "Zambie (la)", "ZM", "ZMB", "894"},
|
||||
}
|
||||
|
||||
// ISO4217List is the list of ISO currency codes
|
||||
var ISO4217List = []string{
|
||||
"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN",
|
||||
"BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD",
|
||||
"CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP", "CVE", "CZK",
|
||||
"DJF", "DKK", "DOP", "DZD",
|
||||
"EGP", "ERN", "ETB", "EUR",
|
||||
"FJD", "FKP",
|
||||
"GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD",
|
||||
"HKD", "HNL", "HRK", "HTG", "HUF",
|
||||
"IDR", "ILS", "INR", "IQD", "IRR", "ISK",
|
||||
"JMD", "JOD", "JPY",
|
||||
"KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT",
|
||||
"LAK", "LBP", "LKR", "LRD", "LSL", "LYD",
|
||||
"MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRO", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN",
|
||||
"NAD", "NGN", "NIO", "NOK", "NPR", "NZD",
|
||||
"OMR",
|
||||
"PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG",
|
||||
"QAR",
|
||||
"RON", "RSD", "RUB", "RWF",
|
||||
"SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD", "SSP", "STD", "STN", "SVC", "SYP", "SZL",
|
||||
"THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS",
|
||||
"UAH", "UGX", "USD", "USN", "UYI", "UYU", "UYW", "UZS",
|
||||
"VEF", "VES", "VND", "VUV",
|
||||
"WST",
|
||||
"XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR", "XOF", "XPD", "XPF", "XPT", "XSU", "XTS", "XUA", "XXX",
|
||||
"YER",
|
||||
"ZAR", "ZMW", "ZWL",
|
||||
}
|
||||
|
||||
// ISO693Entry stores ISO language codes
|
||||
type ISO693Entry struct {
|
||||
Alpha3bCode string
|
||||
Alpha2Code string
|
||||
English string
|
||||
}
|
||||
|
||||
//ISO693List based on http://data.okfn.org/data/core/language-codes/r/language-codes-3b2.json
|
||||
var ISO693List = []ISO693Entry{
|
||||
{Alpha3bCode: "aar", Alpha2Code: "aa", English: "Afar"},
|
||||
{Alpha3bCode: "abk", Alpha2Code: "ab", English: "Abkhazian"},
|
||||
{Alpha3bCode: "afr", Alpha2Code: "af", English: "Afrikaans"},
|
||||
{Alpha3bCode: "aka", Alpha2Code: "ak", English: "Akan"},
|
||||
{Alpha3bCode: "alb", Alpha2Code: "sq", English: "Albanian"},
|
||||
{Alpha3bCode: "amh", Alpha2Code: "am", English: "Amharic"},
|
||||
{Alpha3bCode: "ara", Alpha2Code: "ar", English: "Arabic"},
|
||||
{Alpha3bCode: "arg", Alpha2Code: "an", English: "Aragonese"},
|
||||
{Alpha3bCode: "arm", Alpha2Code: "hy", English: "Armenian"},
|
||||
{Alpha3bCode: "asm", Alpha2Code: "as", English: "Assamese"},
|
||||
{Alpha3bCode: "ava", Alpha2Code: "av", English: "Avaric"},
|
||||
{Alpha3bCode: "ave", Alpha2Code: "ae", English: "Avestan"},
|
||||
{Alpha3bCode: "aym", Alpha2Code: "ay", English: "Aymara"},
|
||||
{Alpha3bCode: "aze", Alpha2Code: "az", English: "Azerbaijani"},
|
||||
{Alpha3bCode: "bak", Alpha2Code: "ba", English: "Bashkir"},
|
||||
{Alpha3bCode: "bam", Alpha2Code: "bm", English: "Bambara"},
|
||||
{Alpha3bCode: "baq", Alpha2Code: "eu", English: "Basque"},
|
||||
{Alpha3bCode: "bel", Alpha2Code: "be", English: "Belarusian"},
|
||||
{Alpha3bCode: "ben", Alpha2Code: "bn", English: "Bengali"},
|
||||
{Alpha3bCode: "bih", Alpha2Code: "bh", English: "Bihari languages"},
|
||||
{Alpha3bCode: "bis", Alpha2Code: "bi", English: "Bislama"},
|
||||
{Alpha3bCode: "bos", Alpha2Code: "bs", English: "Bosnian"},
|
||||
{Alpha3bCode: "bre", Alpha2Code: "br", English: "Breton"},
|
||||
{Alpha3bCode: "bul", Alpha2Code: "bg", English: "Bulgarian"},
|
||||
{Alpha3bCode: "bur", Alpha2Code: "my", English: "Burmese"},
|
||||
{Alpha3bCode: "cat", Alpha2Code: "ca", English: "Catalan; Valencian"},
|
||||
{Alpha3bCode: "cha", Alpha2Code: "ch", English: "Chamorro"},
|
||||
{Alpha3bCode: "che", Alpha2Code: "ce", English: "Chechen"},
|
||||
{Alpha3bCode: "chi", Alpha2Code: "zh", English: "Chinese"},
|
||||
{Alpha3bCode: "chu", Alpha2Code: "cu", English: "Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic"},
|
||||
{Alpha3bCode: "chv", Alpha2Code: "cv", English: "Chuvash"},
|
||||
{Alpha3bCode: "cor", Alpha2Code: "kw", English: "Cornish"},
|
||||
{Alpha3bCode: "cos", Alpha2Code: "co", English: "Corsican"},
|
||||
{Alpha3bCode: "cre", Alpha2Code: "cr", English: "Cree"},
|
||||
{Alpha3bCode: "cze", Alpha2Code: "cs", English: "Czech"},
|
||||
{Alpha3bCode: "dan", Alpha2Code: "da", English: "Danish"},
|
||||
{Alpha3bCode: "div", Alpha2Code: "dv", English: "Divehi; Dhivehi; Maldivian"},
|
||||
{Alpha3bCode: "dut", Alpha2Code: "nl", English: "Dutch; Flemish"},
|
||||
{Alpha3bCode: "dzo", Alpha2Code: "dz", English: "Dzongkha"},
|
||||
{Alpha3bCode: "eng", Alpha2Code: "en", English: "English"},
|
||||
{Alpha3bCode: "epo", Alpha2Code: "eo", English: "Esperanto"},
|
||||
{Alpha3bCode: "est", Alpha2Code: "et", English: "Estonian"},
|
||||
{Alpha3bCode: "ewe", Alpha2Code: "ee", English: "Ewe"},
|
||||
{Alpha3bCode: "fao", Alpha2Code: "fo", English: "Faroese"},
|
||||
{Alpha3bCode: "fij", Alpha2Code: "fj", English: "Fijian"},
|
||||
{Alpha3bCode: "fin", Alpha2Code: "fi", English: "Finnish"},
|
||||
{Alpha3bCode: "fre", Alpha2Code: "fr", English: "French"},
|
||||
{Alpha3bCode: "fry", Alpha2Code: "fy", English: "Western Frisian"},
|
||||
{Alpha3bCode: "ful", Alpha2Code: "ff", English: "Fulah"},
|
||||
{Alpha3bCode: "geo", Alpha2Code: "ka", English: "Georgian"},
|
||||
{Alpha3bCode: "ger", Alpha2Code: "de", English: "German"},
|
||||
{Alpha3bCode: "gla", Alpha2Code: "gd", English: "Gaelic; Scottish Gaelic"},
|
||||
{Alpha3bCode: "gle", Alpha2Code: "ga", English: "Irish"},
|
||||
{Alpha3bCode: "glg", Alpha2Code: "gl", English: "Galician"},
|
||||
{Alpha3bCode: "glv", Alpha2Code: "gv", English: "Manx"},
|
||||
{Alpha3bCode: "gre", Alpha2Code: "el", English: "Greek, Modern (1453-)"},
|
||||
{Alpha3bCode: "grn", Alpha2Code: "gn", English: "Guarani"},
|
||||
{Alpha3bCode: "guj", Alpha2Code: "gu", English: "Gujarati"},
|
||||
{Alpha3bCode: "hat", Alpha2Code: "ht", English: "Haitian; Haitian Creole"},
|
||||
{Alpha3bCode: "hau", Alpha2Code: "ha", English: "Hausa"},
|
||||
{Alpha3bCode: "heb", Alpha2Code: "he", English: "Hebrew"},
|
||||
{Alpha3bCode: "her", Alpha2Code: "hz", English: "Herero"},
|
||||
{Alpha3bCode: "hin", Alpha2Code: "hi", English: "Hindi"},
|
||||
{Alpha3bCode: "hmo", Alpha2Code: "ho", English: "Hiri Motu"},
|
||||
{Alpha3bCode: "hrv", Alpha2Code: "hr", English: "Croatian"},
|
||||
{Alpha3bCode: "hun", Alpha2Code: "hu", English: "Hungarian"},
|
||||
{Alpha3bCode: "ibo", Alpha2Code: "ig", English: "Igbo"},
|
||||
{Alpha3bCode: "ice", Alpha2Code: "is", English: "Icelandic"},
|
||||
{Alpha3bCode: "ido", Alpha2Code: "io", English: "Ido"},
|
||||
{Alpha3bCode: "iii", Alpha2Code: "ii", English: "Sichuan Yi; Nuosu"},
|
||||
{Alpha3bCode: "iku", Alpha2Code: "iu", English: "Inuktitut"},
|
||||
{Alpha3bCode: "ile", Alpha2Code: "ie", English: "Interlingue; Occidental"},
|
||||
{Alpha3bCode: "ina", Alpha2Code: "ia", English: "Interlingua (International Auxiliary Language Association)"},
|
||||
{Alpha3bCode: "ind", Alpha2Code: "id", English: "Indonesian"},
|
||||
{Alpha3bCode: "ipk", Alpha2Code: "ik", English: "Inupiaq"},
|
||||
{Alpha3bCode: "ita", Alpha2Code: "it", English: "Italian"},
|
||||
{Alpha3bCode: "jav", Alpha2Code: "jv", English: "Javanese"},
|
||||
{Alpha3bCode: "jpn", Alpha2Code: "ja", English: "Japanese"},
|
||||
{Alpha3bCode: "kal", Alpha2Code: "kl", English: "Kalaallisut; Greenlandic"},
|
||||
{Alpha3bCode: "kan", Alpha2Code: "kn", English: "Kannada"},
|
||||
{Alpha3bCode: "kas", Alpha2Code: "ks", English: "Kashmiri"},
|
||||
{Alpha3bCode: "kau", Alpha2Code: "kr", English: "Kanuri"},
|
||||
{Alpha3bCode: "kaz", Alpha2Code: "kk", English: "Kazakh"},
|
||||
{Alpha3bCode: "khm", Alpha2Code: "km", English: "Central Khmer"},
|
||||
{Alpha3bCode: "kik", Alpha2Code: "ki", English: "Kikuyu; Gikuyu"},
|
||||
{Alpha3bCode: "kin", Alpha2Code: "rw", English: "Kinyarwanda"},
|
||||
{Alpha3bCode: "kir", Alpha2Code: "ky", English: "Kirghiz; Kyrgyz"},
|
||||
{Alpha3bCode: "kom", Alpha2Code: "kv", English: "Komi"},
|
||||
{Alpha3bCode: "kon", Alpha2Code: "kg", English: "Kongo"},
|
||||
{Alpha3bCode: "kor", Alpha2Code: "ko", English: "Korean"},
|
||||
{Alpha3bCode: "kua", Alpha2Code: "kj", English: "Kuanyama; Kwanyama"},
|
||||
{Alpha3bCode: "kur", Alpha2Code: "ku", English: "Kurdish"},
|
||||
{Alpha3bCode: "lao", Alpha2Code: "lo", English: "Lao"},
|
||||
{Alpha3bCode: "lat", Alpha2Code: "la", English: "Latin"},
|
||||
{Alpha3bCode: "lav", Alpha2Code: "lv", English: "Latvian"},
|
||||
{Alpha3bCode: "lim", Alpha2Code: "li", English: "Limburgan; Limburger; Limburgish"},
|
||||
{Alpha3bCode: "lin", Alpha2Code: "ln", English: "Lingala"},
|
||||
{Alpha3bCode: "lit", Alpha2Code: "lt", English: "Lithuanian"},
|
||||
{Alpha3bCode: "ltz", Alpha2Code: "lb", English: "Luxembourgish; Letzeburgesch"},
|
||||
{Alpha3bCode: "lub", Alpha2Code: "lu", English: "Luba-Katanga"},
|
||||
{Alpha3bCode: "lug", Alpha2Code: "lg", English: "Ganda"},
|
||||
{Alpha3bCode: "mac", Alpha2Code: "mk", English: "Macedonian"},
|
||||
{Alpha3bCode: "mah", Alpha2Code: "mh", English: "Marshallese"},
|
||||
{Alpha3bCode: "mal", Alpha2Code: "ml", English: "Malayalam"},
|
||||
{Alpha3bCode: "mao", Alpha2Code: "mi", English: "Maori"},
|
||||
{Alpha3bCode: "mar", Alpha2Code: "mr", English: "Marathi"},
|
||||
{Alpha3bCode: "may", Alpha2Code: "ms", English: "Malay"},
|
||||
{Alpha3bCode: "mlg", Alpha2Code: "mg", English: "Malagasy"},
|
||||
{Alpha3bCode: "mlt", Alpha2Code: "mt", English: "Maltese"},
|
||||
{Alpha3bCode: "mon", Alpha2Code: "mn", English: "Mongolian"},
|
||||
{Alpha3bCode: "nau", Alpha2Code: "na", English: "Nauru"},
|
||||
{Alpha3bCode: "nav", Alpha2Code: "nv", English: "Navajo; Navaho"},
|
||||
{Alpha3bCode: "nbl", Alpha2Code: "nr", English: "Ndebele, South; South Ndebele"},
|
||||
{Alpha3bCode: "nde", Alpha2Code: "nd", English: "Ndebele, North; North Ndebele"},
|
||||
{Alpha3bCode: "ndo", Alpha2Code: "ng", English: "Ndonga"},
|
||||
{Alpha3bCode: "nep", Alpha2Code: "ne", English: "Nepali"},
|
||||
{Alpha3bCode: "nno", Alpha2Code: "nn", English: "Norwegian Nynorsk; Nynorsk, Norwegian"},
|
||||
{Alpha3bCode: "nob", Alpha2Code: "nb", English: "Bokmål, Norwegian; Norwegian Bokmål"},
|
||||
{Alpha3bCode: "nor", Alpha2Code: "no", English: "Norwegian"},
|
||||
{Alpha3bCode: "nya", Alpha2Code: "ny", English: "Chichewa; Chewa; Nyanja"},
|
||||
{Alpha3bCode: "oci", Alpha2Code: "oc", English: "Occitan (post 1500); Provençal"},
|
||||
{Alpha3bCode: "oji", Alpha2Code: "oj", English: "Ojibwa"},
|
||||
{Alpha3bCode: "ori", Alpha2Code: "or", English: "Oriya"},
|
||||
{Alpha3bCode: "orm", Alpha2Code: "om", English: "Oromo"},
|
||||
{Alpha3bCode: "oss", Alpha2Code: "os", English: "Ossetian; Ossetic"},
|
||||
{Alpha3bCode: "pan", Alpha2Code: "pa", English: "Panjabi; Punjabi"},
|
||||
{Alpha3bCode: "per", Alpha2Code: "fa", English: "Persian"},
|
||||
{Alpha3bCode: "pli", Alpha2Code: "pi", English: "Pali"},
|
||||
{Alpha3bCode: "pol", Alpha2Code: "pl", English: "Polish"},
|
||||
{Alpha3bCode: "por", Alpha2Code: "pt", English: "Portuguese"},
|
||||
{Alpha3bCode: "pus", Alpha2Code: "ps", English: "Pushto; Pashto"},
|
||||
{Alpha3bCode: "que", Alpha2Code: "qu", English: "Quechua"},
|
||||
{Alpha3bCode: "roh", Alpha2Code: "rm", English: "Romansh"},
|
||||
{Alpha3bCode: "rum", Alpha2Code: "ro", English: "Romanian; Moldavian; Moldovan"},
|
||||
{Alpha3bCode: "run", Alpha2Code: "rn", English: "Rundi"},
|
||||
{Alpha3bCode: "rus", Alpha2Code: "ru", English: "Russian"},
|
||||
{Alpha3bCode: "sag", Alpha2Code: "sg", English: "Sango"},
|
||||
{Alpha3bCode: "san", Alpha2Code: "sa", English: "Sanskrit"},
|
||||
{Alpha3bCode: "sin", Alpha2Code: "si", English: "Sinhala; Sinhalese"},
|
||||
{Alpha3bCode: "slo", Alpha2Code: "sk", English: "Slovak"},
|
||||
{Alpha3bCode: "slv", Alpha2Code: "sl", English: "Slovenian"},
|
||||
{Alpha3bCode: "sme", Alpha2Code: "se", English: "Northern Sami"},
|
||||
{Alpha3bCode: "smo", Alpha2Code: "sm", English: "Samoan"},
|
||||
{Alpha3bCode: "sna", Alpha2Code: "sn", English: "Shona"},
|
||||
{Alpha3bCode: "snd", Alpha2Code: "sd", English: "Sindhi"},
|
||||
{Alpha3bCode: "som", Alpha2Code: "so", English: "Somali"},
|
||||
{Alpha3bCode: "sot", Alpha2Code: "st", English: "Sotho, Southern"},
|
||||
{Alpha3bCode: "spa", Alpha2Code: "es", English: "Spanish; Castilian"},
|
||||
{Alpha3bCode: "srd", Alpha2Code: "sc", English: "Sardinian"},
|
||||
{Alpha3bCode: "srp", Alpha2Code: "sr", English: "Serbian"},
|
||||
{Alpha3bCode: "ssw", Alpha2Code: "ss", English: "Swati"},
|
||||
{Alpha3bCode: "sun", Alpha2Code: "su", English: "Sundanese"},
|
||||
{Alpha3bCode: "swa", Alpha2Code: "sw", English: "Swahili"},
|
||||
{Alpha3bCode: "swe", Alpha2Code: "sv", English: "Swedish"},
|
||||
{Alpha3bCode: "tah", Alpha2Code: "ty", English: "Tahitian"},
|
||||
{Alpha3bCode: "tam", Alpha2Code: "ta", English: "Tamil"},
|
||||
{Alpha3bCode: "tat", Alpha2Code: "tt", English: "Tatar"},
|
||||
{Alpha3bCode: "tel", Alpha2Code: "te", English: "Telugu"},
|
||||
{Alpha3bCode: "tgk", Alpha2Code: "tg", English: "Tajik"},
|
||||
{Alpha3bCode: "tgl", Alpha2Code: "tl", English: "Tagalog"},
|
||||
{Alpha3bCode: "tha", Alpha2Code: "th", English: "Thai"},
|
||||
{Alpha3bCode: "tib", Alpha2Code: "bo", English: "Tibetan"},
|
||||
{Alpha3bCode: "tir", Alpha2Code: "ti", English: "Tigrinya"},
|
||||
{Alpha3bCode: "ton", Alpha2Code: "to", English: "Tonga (Tonga Islands)"},
|
||||
{Alpha3bCode: "tsn", Alpha2Code: "tn", English: "Tswana"},
|
||||
{Alpha3bCode: "tso", Alpha2Code: "ts", English: "Tsonga"},
|
||||
{Alpha3bCode: "tuk", Alpha2Code: "tk", English: "Turkmen"},
|
||||
{Alpha3bCode: "tur", Alpha2Code: "tr", English: "Turkish"},
|
||||
{Alpha3bCode: "twi", Alpha2Code: "tw", English: "Twi"},
|
||||
{Alpha3bCode: "uig", Alpha2Code: "ug", English: "Uighur; Uyghur"},
|
||||
{Alpha3bCode: "ukr", Alpha2Code: "uk", English: "Ukrainian"},
|
||||
{Alpha3bCode: "urd", Alpha2Code: "ur", English: "Urdu"},
|
||||
{Alpha3bCode: "uzb", Alpha2Code: "uz", English: "Uzbek"},
|
||||
{Alpha3bCode: "ven", Alpha2Code: "ve", English: "Venda"},
|
||||
{Alpha3bCode: "vie", Alpha2Code: "vi", English: "Vietnamese"},
|
||||
{Alpha3bCode: "vol", Alpha2Code: "vo", English: "Volapük"},
|
||||
{Alpha3bCode: "wel", Alpha2Code: "cy", English: "Welsh"},
|
||||
{Alpha3bCode: "wln", Alpha2Code: "wa", English: "Walloon"},
|
||||
{Alpha3bCode: "wol", Alpha2Code: "wo", English: "Wolof"},
|
||||
{Alpha3bCode: "xho", Alpha2Code: "xh", English: "Xhosa"},
|
||||
{Alpha3bCode: "yid", Alpha2Code: "yi", English: "Yiddish"},
|
||||
{Alpha3bCode: "yor", Alpha2Code: "yo", English: "Yoruba"},
|
||||
{Alpha3bCode: "zha", Alpha2Code: "za", English: "Zhuang; Chuang"},
|
||||
{Alpha3bCode: "zul", Alpha2Code: "zu", English: "Zulu"},
|
||||
}
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
package govalidator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"math"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Contains checks if the string contains the substring.
|
||||
func Contains(str, substring string) bool {
|
||||
return strings.Contains(str, substring)
|
||||
}
|
||||
|
||||
// Matches checks if string matches the pattern (pattern is regular expression)
|
||||
// In case of error return false
|
||||
func Matches(str, pattern string) bool {
|
||||
match, _ := regexp.MatchString(pattern, str)
|
||||
return match
|
||||
}
|
||||
|
||||
// LeftTrim trims characters from the left side of the input.
|
||||
// If second argument is empty, it will remove leading spaces.
|
||||
func LeftTrim(str, chars string) string {
|
||||
if chars == "" {
|
||||
return strings.TrimLeftFunc(str, unicode.IsSpace)
|
||||
}
|
||||
r, _ := regexp.Compile("^[" + chars + "]+")
|
||||
return r.ReplaceAllString(str, "")
|
||||
}
|
||||
|
||||
// RightTrim trims characters from the right side of the input.
|
||||
// If second argument is empty, it will remove trailing spaces.
|
||||
func RightTrim(str, chars string) string {
|
||||
if chars == "" {
|
||||
return strings.TrimRightFunc(str, unicode.IsSpace)
|
||||
}
|
||||
r, _ := regexp.Compile("[" + chars + "]+$")
|
||||
return r.ReplaceAllString(str, "")
|
||||
}
|
||||
|
||||
// Trim trims characters from both sides of the input.
|
||||
// If second argument is empty, it will remove spaces.
|
||||
func Trim(str, chars string) string {
|
||||
return LeftTrim(RightTrim(str, chars), chars)
|
||||
}
|
||||
|
||||
// WhiteList removes characters that do not appear in the whitelist.
|
||||
func WhiteList(str, chars string) string {
|
||||
pattern := "[^" + chars + "]+"
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return r.ReplaceAllString(str, "")
|
||||
}
|
||||
|
||||
// BlackList removes characters that appear in the blacklist.
|
||||
func BlackList(str, chars string) string {
|
||||
pattern := "[" + chars + "]+"
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return r.ReplaceAllString(str, "")
|
||||
}
|
||||
|
||||
// StripLow removes characters with a numerical value < 32 and 127, mostly control characters.
|
||||
// If keep_new_lines is true, newline characters are preserved (\n and \r, hex 0xA and 0xD).
|
||||
func StripLow(str string, keepNewLines bool) string {
|
||||
chars := ""
|
||||
if keepNewLines {
|
||||
chars = "\x00-\x09\x0B\x0C\x0E-\x1F\x7F"
|
||||
} else {
|
||||
chars = "\x00-\x1F\x7F"
|
||||
}
|
||||
return BlackList(str, chars)
|
||||
}
|
||||
|
||||
// ReplacePattern replaces regular expression pattern in string
|
||||
func ReplacePattern(str, pattern, replace string) string {
|
||||
r, _ := regexp.Compile(pattern)
|
||||
return r.ReplaceAllString(str, replace)
|
||||
}
|
||||
|
||||
// Escape replaces <, >, & and " with HTML entities.
|
||||
var Escape = html.EscapeString
|
||||
|
||||
func addSegment(inrune, segment []rune) []rune {
|
||||
if len(segment) == 0 {
|
||||
return inrune
|
||||
}
|
||||
if len(inrune) != 0 {
|
||||
inrune = append(inrune, '_')
|
||||
}
|
||||
inrune = append(inrune, segment...)
|
||||
return inrune
|
||||
}
|
||||
|
||||
// UnderscoreToCamelCase converts from underscore separated form to camel case form.
|
||||
// Ex.: my_func => MyFunc
|
||||
func UnderscoreToCamelCase(s string) string {
|
||||
return strings.Replace(strings.Title(strings.Replace(strings.ToLower(s), "_", " ", -1)), " ", "", -1)
|
||||
}
|
||||
|
||||
// CamelCaseToUnderscore converts from camel case form to underscore separated form.
|
||||
// Ex.: MyFunc => my_func
|
||||
func CamelCaseToUnderscore(str string) string {
|
||||
var output []rune
|
||||
var segment []rune
|
||||
for _, r := range str {
|
||||
|
||||
// not treat number as separate segment
|
||||
if !unicode.IsLower(r) && string(r) != "_" && !unicode.IsNumber(r) {
|
||||
output = addSegment(output, segment)
|
||||
segment = nil
|
||||
}
|
||||
segment = append(segment, unicode.ToLower(r))
|
||||
}
|
||||
output = addSegment(output, segment)
|
||||
return string(output)
|
||||
}
|
||||
|
||||
// Reverse returns reversed string
|
||||
func Reverse(s string) string {
|
||||
r := []rune(s)
|
||||
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
|
||||
r[i], r[j] = r[j], r[i]
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
|
||||
// GetLines splits string by "\n" and return array of lines
|
||||
func GetLines(s string) []string {
|
||||
return strings.Split(s, "\n")
|
||||
}
|
||||
|
||||
// GetLine returns specified line of multiline string
|
||||
func GetLine(s string, index int) (string, error) {
|
||||
lines := GetLines(s)
|
||||
if index < 0 || index >= len(lines) {
|
||||
return "", errors.New("line index out of bounds")
|
||||
}
|
||||
return lines[index], nil
|
||||
}
|
||||
|
||||
// RemoveTags removes all tags from HTML string
|
||||
func RemoveTags(s string) string {
|
||||
return ReplacePattern(s, "<[^>]*>", "")
|
||||
}
|
||||
|
||||
// SafeFileName returns safe string that can be used in file names
|
||||
func SafeFileName(str string) string {
|
||||
name := strings.ToLower(str)
|
||||
name = path.Clean(path.Base(name))
|
||||
name = strings.Trim(name, " ")
|
||||
separators, err := regexp.Compile(`[ &_=+:]`)
|
||||
if err == nil {
|
||||
name = separators.ReplaceAllString(name, "-")
|
||||
}
|
||||
legal, err := regexp.Compile(`[^[:alnum:]-.]`)
|
||||
if err == nil {
|
||||
name = legal.ReplaceAllString(name, "")
|
||||
}
|
||||
for strings.Contains(name, "--") {
|
||||
name = strings.Replace(name, "--", "-", -1)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// NormalizeEmail canonicalize an email address.
|
||||
// The local part of the email address is lowercased for all domains; the hostname is always lowercased and
|
||||
// the local part of the email address is always lowercased for hosts that are known to be case-insensitive (currently only GMail).
|
||||
// Normalization follows special rules for known providers: currently, GMail addresses have dots removed in the local part and
|
||||
// are stripped of tags (e.g. some.one+tag@gmail.com becomes someone@gmail.com) and all @googlemail.com addresses are
|
||||
// normalized to @gmail.com.
|
||||
func NormalizeEmail(str string) (string, error) {
|
||||
if !IsEmail(str) {
|
||||
return "", fmt.Errorf("%s is not an email", str)
|
||||
}
|
||||
parts := strings.Split(str, "@")
|
||||
parts[0] = strings.ToLower(parts[0])
|
||||
parts[1] = strings.ToLower(parts[1])
|
||||
if parts[1] == "gmail.com" || parts[1] == "googlemail.com" {
|
||||
parts[1] = "gmail.com"
|
||||
parts[0] = strings.Split(ReplacePattern(parts[0], `\.`, ""), "+")[0]
|
||||
}
|
||||
return strings.Join(parts, "@"), nil
|
||||
}
|
||||
|
||||
// Truncate a string to the closest length without breaking words.
|
||||
func Truncate(str string, length int, ending string) string {
|
||||
var aftstr, befstr string
|
||||
if len(str) > length {
|
||||
words := strings.Fields(str)
|
||||
before, present := 0, 0
|
||||
for i := range words {
|
||||
befstr = aftstr
|
||||
before = present
|
||||
aftstr = aftstr + words[i] + " "
|
||||
present = len(aftstr)
|
||||
if present > length && i != 0 {
|
||||
if (length - before) < (present - length) {
|
||||
return Trim(befstr, " /\\.,\"'#!?&@+-") + ending
|
||||
}
|
||||
return Trim(aftstr, " /\\.,\"'#!?&@+-") + ending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// PadLeft pads left side of a string if size of string is less then indicated pad length
|
||||
func PadLeft(str string, padStr string, padLen int) string {
|
||||
return buildPadStr(str, padStr, padLen, true, false)
|
||||
}
|
||||
|
||||
// PadRight pads right side of a string if size of string is less then indicated pad length
|
||||
func PadRight(str string, padStr string, padLen int) string {
|
||||
return buildPadStr(str, padStr, padLen, false, true)
|
||||
}
|
||||
|
||||
// PadBoth pads both sides of a string if size of string is less then indicated pad length
|
||||
func PadBoth(str string, padStr string, padLen int) string {
|
||||
return buildPadStr(str, padStr, padLen, true, true)
|
||||
}
|
||||
|
||||
// PadString either left, right or both sides.
|
||||
// Note that padding string can be unicode and more then one character
|
||||
func buildPadStr(str string, padStr string, padLen int, padLeft bool, padRight bool) string {
|
||||
|
||||
// When padded length is less then the current string size
|
||||
if padLen < utf8.RuneCountInString(str) {
|
||||
return str
|
||||
}
|
||||
|
||||
padLen -= utf8.RuneCountInString(str)
|
||||
|
||||
targetLen := padLen
|
||||
|
||||
targetLenLeft := targetLen
|
||||
targetLenRight := targetLen
|
||||
if padLeft && padRight {
|
||||
targetLenLeft = padLen / 2
|
||||
targetLenRight = padLen - targetLenLeft
|
||||
}
|
||||
|
||||
strToRepeatLen := utf8.RuneCountInString(padStr)
|
||||
|
||||
repeatTimes := int(math.Ceil(float64(targetLen) / float64(strToRepeatLen)))
|
||||
repeatedString := strings.Repeat(padStr, repeatTimes)
|
||||
|
||||
leftSide := ""
|
||||
if padLeft {
|
||||
leftSide = repeatedString[0:targetLenLeft]
|
||||
}
|
||||
|
||||
rightSide := ""
|
||||
if padRight {
|
||||
rightSide = repeatedString[0:targetLenRight]
|
||||
}
|
||||
|
||||
return leftSide + str + rightSide
|
||||
}
|
||||
|
||||
// TruncatingErrorf removes extra args from fmt.Errorf if not formatted in the str object
|
||||
func TruncatingErrorf(str string, args ...interface{}) error {
|
||||
n := strings.Count(str, "%s")
|
||||
return fmt.Errorf(str, args[:n]...)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,15 @@
|
|||
box: golang
|
||||
build:
|
||||
steps:
|
||||
- setup-go-workspace
|
||||
|
||||
- script:
|
||||
name: go get
|
||||
code: |
|
||||
go version
|
||||
go get -t ./...
|
||||
|
||||
- script:
|
||||
name: go test
|
||||
code: |
|
||||
go test -race -v ./...
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
AWS SDK for Go
|
||||
Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
Copyright 2014-2015 Stripe, Inc.
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package bearer
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Token provides a type wrapping a bearer token and expiration metadata.
|
||||
type Token struct {
|
||||
Value string
|
||||
|
||||
CanExpire bool
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
// Expired returns if the token's Expires time is before or equal to the time
|
||||
// provided. If CanExpire is false, Expired will always return false.
|
||||
func (t Token) Expired(now time.Time) bool {
|
||||
if !t.CanExpire {
|
||||
return false
|
||||
}
|
||||
now = now.Round(0)
|
||||
return now.Equal(t.Expires) || now.After(t.Expires)
|
||||
}
|
||||
|
||||
// TokenProvider provides interface for retrieving bearer tokens.
|
||||
type TokenProvider interface {
|
||||
RetrieveBearerToken(aws.Context) (Token, error)
|
||||
}
|
||||
|
||||
// TokenProviderFunc provides a helper utility to wrap a function as a type
|
||||
// that implements the TokenProvider interface.
|
||||
type TokenProviderFunc func(aws.Context) (Token, error)
|
||||
|
||||
// RetrieveBearerToken calls the wrapped function, returning the Token or
|
||||
// error.
|
||||
func (fn TokenProviderFunc) RetrieveBearerToken(ctx aws.Context) (Token, error) {
|
||||
return fn(ctx)
|
||||
}
|
||||
|
||||
// StaticTokenProvider provides a utility for wrapping a static bearer token
|
||||
// value within an implementation of a token provider.
|
||||
type StaticTokenProvider struct {
|
||||
Token Token
|
||||
}
|
||||
|
||||
// RetrieveBearerToken returns the static token specified.
|
||||
func (s StaticTokenProvider) RetrieveBearerToken(aws.Context) (Token, error) {
|
||||
return s.Token, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
// Package awserr represents API error interface accessors for the SDK.
|
||||
package awserr
|
||||
|
||||
// An Error wraps lower level errors with code, message and an original error.
|
||||
// The underlying concrete error type may also satisfy other interfaces which
|
||||
// can be to used to obtain more specific information about the error.
|
||||
//
|
||||
// Calling Error() or String() will always include the full information about
|
||||
// an error based on its underlying type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// output, err := s3manage.Upload(svc, input, opts)
|
||||
// if err != nil {
|
||||
// if awsErr, ok := err.(awserr.Error); ok {
|
||||
// // Get error details
|
||||
// log.Println("Error:", awsErr.Code(), awsErr.Message())
|
||||
//
|
||||
// // Prints out full error message, including original error if there was one.
|
||||
// log.Println("Error:", awsErr.Error())
|
||||
//
|
||||
// // Get original error
|
||||
// if origErr := awsErr.OrigErr(); origErr != nil {
|
||||
// // operate on original error.
|
||||
// }
|
||||
// } else {
|
||||
// fmt.Println(err.Error())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
type Error interface {
|
||||
// Satisfy the generic error interface.
|
||||
error
|
||||
|
||||
// Returns the short phrase depicting the classification of the error.
|
||||
Code() string
|
||||
|
||||
// Returns the error details message.
|
||||
Message() string
|
||||
|
||||
// Returns the original error if one was set. Nil is returned if not set.
|
||||
OrigErr() error
|
||||
}
|
||||
|
||||
// BatchError is a batch of errors which also wraps lower level errors with
|
||||
// code, message, and original errors. Calling Error() will include all errors
|
||||
// that occurred in the batch.
|
||||
//
|
||||
// Deprecated: Replaced with BatchedErrors. Only defined for backwards
|
||||
// compatibility.
|
||||
type BatchError interface {
|
||||
// Satisfy the generic error interface.
|
||||
error
|
||||
|
||||
// Returns the short phrase depicting the classification of the error.
|
||||
Code() string
|
||||
|
||||
// Returns the error details message.
|
||||
Message() string
|
||||
|
||||
// Returns the original error if one was set. Nil is returned if not set.
|
||||
OrigErrs() []error
|
||||
}
|
||||
|
||||
// BatchedErrors is a batch of errors which also wraps lower level errors with
|
||||
// code, message, and original errors. Calling Error() will include all errors
|
||||
// that occurred in the batch.
|
||||
//
|
||||
// Replaces BatchError
|
||||
type BatchedErrors interface {
|
||||
// Satisfy the base Error interface.
|
||||
Error
|
||||
|
||||
// Returns the original error if one was set. Nil is returned if not set.
|
||||
OrigErrs() []error
|
||||
}
|
||||
|
||||
// New returns an Error object described by the code, message, and origErr.
|
||||
//
|
||||
// If origErr satisfies the Error interface it will not be wrapped within a new
|
||||
// Error object and will instead be returned.
|
||||
func New(code, message string, origErr error) Error {
|
||||
var errs []error
|
||||
if origErr != nil {
|
||||
errs = append(errs, origErr)
|
||||
}
|
||||
return newBaseError(code, message, errs)
|
||||
}
|
||||
|
||||
// NewBatchError returns an BatchedErrors with a collection of errors as an
|
||||
// array of errors.
|
||||
func NewBatchError(code, message string, errs []error) BatchedErrors {
|
||||
return newBaseError(code, message, errs)
|
||||
}
|
||||
|
||||
// A RequestFailure is an interface to extract request failure information from
|
||||
// an Error such as the request ID of the failed request returned by a service.
|
||||
// RequestFailures may not always have a requestID value if the request failed
|
||||
// prior to reaching the service such as a connection error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// output, err := s3manage.Upload(svc, input, opts)
|
||||
// if err != nil {
|
||||
// if reqerr, ok := err.(RequestFailure); ok {
|
||||
// log.Println("Request failed", reqerr.Code(), reqerr.Message(), reqerr.RequestID())
|
||||
// } else {
|
||||
// log.Println("Error:", err.Error())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Combined with awserr.Error:
|
||||
//
|
||||
// output, err := s3manage.Upload(svc, input, opts)
|
||||
// if err != nil {
|
||||
// if awsErr, ok := err.(awserr.Error); ok {
|
||||
// // Generic AWS Error with Code, Message, and original error (if any)
|
||||
// fmt.Println(awsErr.Code(), awsErr.Message(), awsErr.OrigErr())
|
||||
//
|
||||
// if reqErr, ok := err.(awserr.RequestFailure); ok {
|
||||
// // A service error occurred
|
||||
// fmt.Println(reqErr.StatusCode(), reqErr.RequestID())
|
||||
// }
|
||||
// } else {
|
||||
// fmt.Println(err.Error())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
type RequestFailure interface {
|
||||
Error
|
||||
|
||||
// The status code of the HTTP response.
|
||||
StatusCode() int
|
||||
|
||||
// The request ID returned by the service for a request failure. This will
|
||||
// be empty if no request ID is available such as the request failed due
|
||||
// to a connection error.
|
||||
RequestID() string
|
||||
}
|
||||
|
||||
// NewRequestFailure returns a wrapped error with additional information for
|
||||
// request status code, and service requestID.
|
||||
//
|
||||
// Should be used to wrap all request which involve service requests. Even if
|
||||
// the request failed without a service response, but had an HTTP status code
|
||||
// that may be meaningful.
|
||||
func NewRequestFailure(err Error, statusCode int, reqID string) RequestFailure {
|
||||
return newRequestError(err, statusCode, reqID)
|
||||
}
|
||||
|
||||
// UnmarshalError provides the interface for the SDK failing to unmarshal data.
|
||||
type UnmarshalError interface {
|
||||
awsError
|
||||
Bytes() []byte
|
||||
}
|
||||
|
||||
// NewUnmarshalError returns an initialized UnmarshalError error wrapper adding
|
||||
// the bytes that fail to unmarshal to the error.
|
||||
func NewUnmarshalError(err error, msg string, bytes []byte) UnmarshalError {
|
||||
return &unmarshalError{
|
||||
awsError: New("UnmarshalError", msg, err),
|
||||
bytes: bytes,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
package awserr
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SprintError returns a string of the formatted error code.
|
||||
//
|
||||
// Both extra and origErr are optional. If they are included their lines
|
||||
// will be added, but if they are not included their lines will be ignored.
|
||||
func SprintError(code, message, extra string, origErr error) string {
|
||||
msg := fmt.Sprintf("%s: %s", code, message)
|
||||
if extra != "" {
|
||||
msg = fmt.Sprintf("%s\n\t%s", msg, extra)
|
||||
}
|
||||
if origErr != nil {
|
||||
msg = fmt.Sprintf("%s\ncaused by: %s", msg, origErr.Error())
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// A baseError wraps the code and message which defines an error. It also
|
||||
// can be used to wrap an original error object.
|
||||
//
|
||||
// Should be used as the root for errors satisfying the awserr.Error. Also
|
||||
// for any error which does not fit into a specific error wrapper type.
|
||||
type baseError struct {
|
||||
// Classification of error
|
||||
code string
|
||||
|
||||
// Detailed information about error
|
||||
message string
|
||||
|
||||
// Optional original error this error is based off of. Allows building
|
||||
// chained errors.
|
||||
errs []error
|
||||
}
|
||||
|
||||
// newBaseError returns an error object for the code, message, and errors.
|
||||
//
|
||||
// code is a short no whitespace phrase depicting the classification of
|
||||
// the error that is being created.
|
||||
//
|
||||
// message is the free flow string containing detailed information about the
|
||||
// error.
|
||||
//
|
||||
// origErrs is the error objects which will be nested under the new errors to
|
||||
// be returned.
|
||||
func newBaseError(code, message string, origErrs []error) *baseError {
|
||||
b := &baseError{
|
||||
code: code,
|
||||
message: message,
|
||||
errs: origErrs,
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Error returns the string representation of the error.
|
||||
//
|
||||
// See ErrorWithExtra for formatting.
|
||||
//
|
||||
// Satisfies the error interface.
|
||||
func (b baseError) Error() string {
|
||||
size := len(b.errs)
|
||||
if size > 0 {
|
||||
return SprintError(b.code, b.message, "", errorList(b.errs))
|
||||
}
|
||||
|
||||
return SprintError(b.code, b.message, "", nil)
|
||||
}
|
||||
|
||||
// String returns the string representation of the error.
|
||||
// Alias for Error to satisfy the stringer interface.
|
||||
func (b baseError) String() string {
|
||||
return b.Error()
|
||||
}
|
||||
|
||||
// Code returns the short phrase depicting the classification of the error.
|
||||
func (b baseError) Code() string {
|
||||
return b.code
|
||||
}
|
||||
|
||||
// Message returns the error details message.
|
||||
func (b baseError) Message() string {
|
||||
return b.message
|
||||
}
|
||||
|
||||
// OrigErr returns the original error if one was set. Nil is returned if no
|
||||
// error was set. This only returns the first element in the list. If the full
|
||||
// list is needed, use BatchedErrors.
|
||||
func (b baseError) OrigErr() error {
|
||||
switch len(b.errs) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return b.errs[0]
|
||||
default:
|
||||
if err, ok := b.errs[0].(Error); ok {
|
||||
return NewBatchError(err.Code(), err.Message(), b.errs[1:])
|
||||
}
|
||||
return NewBatchError("BatchedErrors",
|
||||
"multiple errors occurred", b.errs)
|
||||
}
|
||||
}
|
||||
|
||||
// OrigErrs returns the original errors if one was set. An empty slice is
|
||||
// returned if no error was set.
|
||||
func (b baseError) OrigErrs() []error {
|
||||
return b.errs
|
||||
}
|
||||
|
||||
// So that the Error interface type can be included as an anonymous field
|
||||
// in the requestError struct and not conflict with the error.Error() method.
|
||||
type awsError Error
|
||||
|
||||
// A requestError wraps a request or service error.
|
||||
//
|
||||
// Composed of baseError for code, message, and original error.
|
||||
type requestError struct {
|
||||
awsError
|
||||
statusCode int
|
||||
requestID string
|
||||
bytes []byte
|
||||
}
|
||||
|
||||
// newRequestError returns a wrapped error with additional information for
|
||||
// request status code, and service requestID.
|
||||
//
|
||||
// Should be used to wrap all request which involve service requests. Even if
|
||||
// the request failed without a service response, but had an HTTP status code
|
||||
// that may be meaningful.
|
||||
//
|
||||
// Also wraps original errors via the baseError.
|
||||
func newRequestError(err Error, statusCode int, requestID string) *requestError {
|
||||
return &requestError{
|
||||
awsError: err,
|
||||
statusCode: statusCode,
|
||||
requestID: requestID,
|
||||
}
|
||||
}
|
||||
|
||||
// Error returns the string representation of the error.
|
||||
// Satisfies the error interface.
|
||||
func (r requestError) Error() string {
|
||||
extra := fmt.Sprintf("status code: %d, request id: %s",
|
||||
r.statusCode, r.requestID)
|
||||
return SprintError(r.Code(), r.Message(), extra, r.OrigErr())
|
||||
}
|
||||
|
||||
// String returns the string representation of the error.
|
||||
// Alias for Error to satisfy the stringer interface.
|
||||
func (r requestError) String() string {
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// StatusCode returns the wrapped status code for the error
|
||||
func (r requestError) StatusCode() int {
|
||||
return r.statusCode
|
||||
}
|
||||
|
||||
// RequestID returns the wrapped requestID
|
||||
func (r requestError) RequestID() string {
|
||||
return r.requestID
|
||||
}
|
||||
|
||||
// OrigErrs returns the original errors if one was set. An empty slice is
|
||||
// returned if no error was set.
|
||||
func (r requestError) OrigErrs() []error {
|
||||
if b, ok := r.awsError.(BatchedErrors); ok {
|
||||
return b.OrigErrs()
|
||||
}
|
||||
return []error{r.OrigErr()}
|
||||
}
|
||||
|
||||
type unmarshalError struct {
|
||||
awsError
|
||||
bytes []byte
|
||||
}
|
||||
|
||||
// Error returns the string representation of the error.
|
||||
// Satisfies the error interface.
|
||||
func (e unmarshalError) Error() string {
|
||||
extra := hex.Dump(e.bytes)
|
||||
return SprintError(e.Code(), e.Message(), extra, e.OrigErr())
|
||||
}
|
||||
|
||||
// String returns the string representation of the error.
|
||||
// Alias for Error to satisfy the stringer interface.
|
||||
func (e unmarshalError) String() string {
|
||||
return e.Error()
|
||||
}
|
||||
|
||||
// Bytes returns the bytes that failed to unmarshal.
|
||||
func (e unmarshalError) Bytes() []byte {
|
||||
return e.bytes
|
||||
}
|
||||
|
||||
// An error list that satisfies the golang interface
|
||||
type errorList []error
|
||||
|
||||
// Error returns the string representation of the error.
|
||||
//
|
||||
// Satisfies the error interface.
|
||||
func (e errorList) Error() string {
|
||||
msg := ""
|
||||
// How do we want to handle the array size being zero
|
||||
if size := len(e); size > 0 {
|
||||
for i := 0; i < size; i++ {
|
||||
msg += e[i].Error()
|
||||
// We check the next index to see if it is within the slice.
|
||||
// If it is, then we append a newline. We do this, because unit tests
|
||||
// could be broken with the additional '\n'
|
||||
if i+1 < size {
|
||||
msg += "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package awsutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Copy deeply copies a src structure to dst. Useful for copying request and
|
||||
// response structures.
|
||||
//
|
||||
// Can copy between structs of different type, but will only copy fields which
|
||||
// are assignable, and exist in both structs. Fields which are not assignable,
|
||||
// or do not exist in both structs are ignored.
|
||||
func Copy(dst, src interface{}) {
|
||||
dstval := reflect.ValueOf(dst)
|
||||
if !dstval.IsValid() {
|
||||
panic("Copy dst cannot be nil")
|
||||
}
|
||||
|
||||
rcopy(dstval, reflect.ValueOf(src), true)
|
||||
}
|
||||
|
||||
// CopyOf returns a copy of src while also allocating the memory for dst.
|
||||
// src must be a pointer type or this operation will fail.
|
||||
func CopyOf(src interface{}) (dst interface{}) {
|
||||
dsti := reflect.New(reflect.TypeOf(src).Elem())
|
||||
dst = dsti.Interface()
|
||||
rcopy(dsti, reflect.ValueOf(src), true)
|
||||
return
|
||||
}
|
||||
|
||||
// rcopy performs a recursive copy of values from the source to destination.
|
||||
//
|
||||
// root is used to skip certain aspects of the copy which are not valid
|
||||
// for the root node of a object.
|
||||
func rcopy(dst, src reflect.Value, root bool) {
|
||||
if !src.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
switch src.Kind() {
|
||||
case reflect.Ptr:
|
||||
if _, ok := src.Interface().(io.Reader); ok {
|
||||
if dst.Kind() == reflect.Ptr && dst.Elem().CanSet() {
|
||||
dst.Elem().Set(src)
|
||||
} else if dst.CanSet() {
|
||||
dst.Set(src)
|
||||
}
|
||||
} else {
|
||||
e := src.Type().Elem()
|
||||
if dst.CanSet() && !src.IsNil() {
|
||||
if _, ok := src.Interface().(*time.Time); !ok {
|
||||
dst.Set(reflect.New(e))
|
||||
} else {
|
||||
tempValue := reflect.New(e)
|
||||
tempValue.Elem().Set(src.Elem())
|
||||
// Sets time.Time's unexported values
|
||||
dst.Set(tempValue)
|
||||
}
|
||||
}
|
||||
if src.Elem().IsValid() {
|
||||
// Keep the current root state since the depth hasn't changed
|
||||
rcopy(dst.Elem(), src.Elem(), root)
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
t := dst.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
name := t.Field(i).Name
|
||||
srcVal := src.FieldByName(name)
|
||||
dstVal := dst.FieldByName(name)
|
||||
if srcVal.IsValid() && dstVal.CanSet() {
|
||||
rcopy(dstVal, srcVal, false)
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
if src.IsNil() {
|
||||
break
|
||||
}
|
||||
|
||||
s := reflect.MakeSlice(src.Type(), src.Len(), src.Cap())
|
||||
dst.Set(s)
|
||||
for i := 0; i < src.Len(); i++ {
|
||||
rcopy(dst.Index(i), src.Index(i), false)
|
||||
}
|
||||
case reflect.Map:
|
||||
if src.IsNil() {
|
||||
break
|
||||
}
|
||||
|
||||
s := reflect.MakeMap(src.Type())
|
||||
dst.Set(s)
|
||||
for _, k := range src.MapKeys() {
|
||||
v := src.MapIndex(k)
|
||||
v2 := reflect.New(v.Type()).Elem()
|
||||
rcopy(v2, v, false)
|
||||
dst.SetMapIndex(k, v2)
|
||||
}
|
||||
default:
|
||||
// Assign the value if possible. If its not assignable, the value would
|
||||
// need to be converted and the impact of that may be unexpected, or is
|
||||
// not compatible with the dst type.
|
||||
if src.Type().AssignableTo(dst.Type()) {
|
||||
dst.Set(src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package awsutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// DeepEqual returns if the two values are deeply equal like reflect.DeepEqual.
|
||||
// In addition to this, this method will also dereference the input values if
|
||||
// possible so the DeepEqual performed will not fail if one parameter is a
|
||||
// pointer and the other is not.
|
||||
//
|
||||
// DeepEqual will not perform indirection of nested values of the input parameters.
|
||||
func DeepEqual(a, b interface{}) bool {
|
||||
ra := reflect.Indirect(reflect.ValueOf(a))
|
||||
rb := reflect.Indirect(reflect.ValueOf(b))
|
||||
|
||||
if raValid, rbValid := ra.IsValid(), rb.IsValid(); !raValid && !rbValid {
|
||||
// If the elements are both nil, and of the same type they are equal
|
||||
// If they are of different types they are not equal
|
||||
return reflect.TypeOf(a) == reflect.TypeOf(b)
|
||||
} else if raValid != rbValid {
|
||||
// Both values must be valid to be equal
|
||||
return false
|
||||
}
|
||||
|
||||
return reflect.DeepEqual(ra.Interface(), rb.Interface())
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
package awsutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jmespath/go-jmespath"
|
||||
)
|
||||
|
||||
var indexRe = regexp.MustCompile(`(.+)\[(-?\d+)?\]$`)
|
||||
|
||||
// rValuesAtPath returns a slice of values found in value v. The values
|
||||
// in v are explored recursively so all nested values are collected.
|
||||
func rValuesAtPath(v interface{}, path string, createPath, caseSensitive, nilTerm bool) []reflect.Value {
|
||||
pathparts := strings.Split(path, "||")
|
||||
if len(pathparts) > 1 {
|
||||
for _, pathpart := range pathparts {
|
||||
vals := rValuesAtPath(v, pathpart, createPath, caseSensitive, nilTerm)
|
||||
if len(vals) > 0 {
|
||||
return vals
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
values := []reflect.Value{reflect.Indirect(reflect.ValueOf(v))}
|
||||
components := strings.Split(path, ".")
|
||||
for len(values) > 0 && len(components) > 0 {
|
||||
var index *int64
|
||||
var indexStar bool
|
||||
c := strings.TrimSpace(components[0])
|
||||
if c == "" { // no actual component, illegal syntax
|
||||
return nil
|
||||
} else if caseSensitive && c != "*" && strings.ToLower(c[0:1]) == c[0:1] {
|
||||
// TODO normalize case for user
|
||||
return nil // don't support unexported fields
|
||||
}
|
||||
|
||||
// parse this component
|
||||
if m := indexRe.FindStringSubmatch(c); m != nil {
|
||||
c = m[1]
|
||||
if m[2] == "" {
|
||||
index = nil
|
||||
indexStar = true
|
||||
} else {
|
||||
i, _ := strconv.ParseInt(m[2], 10, 32)
|
||||
index = &i
|
||||
indexStar = false
|
||||
}
|
||||
}
|
||||
|
||||
nextvals := []reflect.Value{}
|
||||
for _, value := range values {
|
||||
// pull component name out of struct member
|
||||
if value.Kind() != reflect.Struct {
|
||||
continue
|
||||
}
|
||||
|
||||
if c == "*" { // pull all members
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
if f := reflect.Indirect(value.Field(i)); f.IsValid() {
|
||||
nextvals = append(nextvals, f)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
value = value.FieldByNameFunc(func(name string) bool {
|
||||
if c == name {
|
||||
return true
|
||||
} else if !caseSensitive && strings.EqualFold(name, c) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if nilTerm && value.Kind() == reflect.Ptr && len(components[1:]) == 0 {
|
||||
if !value.IsNil() {
|
||||
value.Set(reflect.Zero(value.Type()))
|
||||
}
|
||||
return []reflect.Value{value}
|
||||
}
|
||||
|
||||
if createPath && value.Kind() == reflect.Ptr && value.IsNil() {
|
||||
// TODO if the value is the terminus it should not be created
|
||||
// if the value to be set to its position is nil.
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
value = value.Elem()
|
||||
} else {
|
||||
value = reflect.Indirect(value)
|
||||
}
|
||||
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
|
||||
if !createPath && value.IsNil() {
|
||||
value = reflect.ValueOf(nil)
|
||||
}
|
||||
}
|
||||
|
||||
if value.IsValid() {
|
||||
nextvals = append(nextvals, value)
|
||||
}
|
||||
}
|
||||
values = nextvals
|
||||
|
||||
if indexStar || index != nil {
|
||||
nextvals = []reflect.Value{}
|
||||
for _, valItem := range values {
|
||||
value := reflect.Indirect(valItem)
|
||||
if value.Kind() != reflect.Slice {
|
||||
continue
|
||||
}
|
||||
|
||||
if indexStar { // grab all indices
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
idx := reflect.Indirect(value.Index(i))
|
||||
if idx.IsValid() {
|
||||
nextvals = append(nextvals, idx)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// pull out index
|
||||
i := int(*index)
|
||||
if i >= value.Len() { // check out of bounds
|
||||
if createPath {
|
||||
// TODO resize slice
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else if i < 0 { // support negative indexing
|
||||
i = value.Len() + i
|
||||
}
|
||||
value = reflect.Indirect(value.Index(i))
|
||||
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
|
||||
if !createPath && value.IsNil() {
|
||||
value = reflect.ValueOf(nil)
|
||||
}
|
||||
}
|
||||
|
||||
if value.IsValid() {
|
||||
nextvals = append(nextvals, value)
|
||||
}
|
||||
}
|
||||
values = nextvals
|
||||
}
|
||||
|
||||
components = components[1:]
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// ValuesAtPath returns a list of values at the case insensitive lexical
|
||||
// path inside of a structure.
|
||||
func ValuesAtPath(i interface{}, path string) ([]interface{}, error) {
|
||||
result, err := jmespath.Search(path, i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(result)
|
||||
if !v.IsValid() || (v.Kind() == reflect.Ptr && v.IsNil()) {
|
||||
return nil, nil
|
||||
}
|
||||
if s, ok := result.([]interface{}); ok {
|
||||
return s, err
|
||||
}
|
||||
if v.Kind() == reflect.Map && v.Len() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if v.Kind() == reflect.Slice {
|
||||
out := make([]interface{}, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
out[i] = v.Index(i).Interface()
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
return []interface{}{result}, nil
|
||||
}
|
||||
|
||||
// SetValueAtPath sets a value at the case insensitive lexical path inside
|
||||
// of a structure.
|
||||
func SetValueAtPath(i interface{}, path string, v interface{}) {
|
||||
rvals := rValuesAtPath(i, path, true, false, v == nil)
|
||||
for _, rval := range rvals {
|
||||
if rval.Kind() == reflect.Ptr && rval.IsNil() {
|
||||
continue
|
||||
}
|
||||
setValue(rval, v)
|
||||
}
|
||||
}
|
||||
|
||||
func setValue(dstVal reflect.Value, src interface{}) {
|
||||
if dstVal.Kind() == reflect.Ptr {
|
||||
dstVal = reflect.Indirect(dstVal)
|
||||
}
|
||||
srcVal := reflect.ValueOf(src)
|
||||
|
||||
if !srcVal.IsValid() { // src is literal nil
|
||||
if dstVal.CanAddr() {
|
||||
// Convert to pointer so that pointer's value can be nil'ed
|
||||
// dstVal = dstVal.Addr()
|
||||
}
|
||||
dstVal.Set(reflect.Zero(dstVal.Type()))
|
||||
|
||||
} else if srcVal.Kind() == reflect.Ptr {
|
||||
if srcVal.IsNil() {
|
||||
srcVal = reflect.Zero(dstVal.Type())
|
||||
} else {
|
||||
srcVal = reflect.ValueOf(src).Elem()
|
||||
}
|
||||
dstVal.Set(srcVal)
|
||||
} else {
|
||||
dstVal.Set(srcVal)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package awsutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Prettify returns the string representation of a value.
|
||||
func Prettify(i interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
prettify(reflect.ValueOf(i), 0, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// prettify will recursively walk value v to build a textual
|
||||
// representation of the value.
|
||||
func prettify(v reflect.Value, indent int, buf *bytes.Buffer) {
|
||||
for v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
strtype := v.Type().String()
|
||||
if strtype == "time.Time" {
|
||||
fmt.Fprintf(buf, "%s", v.Interface())
|
||||
break
|
||||
} else if strings.HasPrefix(strtype, "io.") {
|
||||
buf.WriteString("<buffer>")
|
||||
break
|
||||
}
|
||||
|
||||
buf.WriteString("{\n")
|
||||
|
||||
names := []string{}
|
||||
for i := 0; i < v.Type().NumField(); i++ {
|
||||
name := v.Type().Field(i).Name
|
||||
f := v.Field(i)
|
||||
if name[0:1] == strings.ToLower(name[0:1]) {
|
||||
continue // ignore unexported fields
|
||||
}
|
||||
if (f.Kind() == reflect.Ptr || f.Kind() == reflect.Slice || f.Kind() == reflect.Map) && f.IsNil() {
|
||||
continue // ignore unset fields
|
||||
}
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
for i, n := range names {
|
||||
val := v.FieldByName(n)
|
||||
ft, ok := v.Type().FieldByName(n)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("expected to find field %v on type %v, but was not found", n, v.Type()))
|
||||
}
|
||||
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(n + ": ")
|
||||
|
||||
if tag := ft.Tag.Get("sensitive"); tag == "true" {
|
||||
buf.WriteString("<sensitive>")
|
||||
} else {
|
||||
prettify(val, indent+2, buf)
|
||||
}
|
||||
|
||||
if i < len(names)-1 {
|
||||
buf.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
|
||||
case reflect.Slice:
|
||||
strtype := v.Type().String()
|
||||
if strtype == "[]uint8" {
|
||||
fmt.Fprintf(buf, "<binary> len %d", v.Len())
|
||||
break
|
||||
}
|
||||
|
||||
nl, id, id2 := "", "", ""
|
||||
if v.Len() > 3 {
|
||||
nl, id, id2 = "\n", strings.Repeat(" ", indent), strings.Repeat(" ", indent+2)
|
||||
}
|
||||
buf.WriteString("[" + nl)
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
buf.WriteString(id2)
|
||||
prettify(v.Index(i), indent+2, buf)
|
||||
|
||||
if i < v.Len()-1 {
|
||||
buf.WriteString("," + nl)
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString(nl + id + "]")
|
||||
case reflect.Map:
|
||||
buf.WriteString("{\n")
|
||||
|
||||
for i, k := range v.MapKeys() {
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(k.String() + ": ")
|
||||
prettify(v.MapIndex(k), indent+2, buf)
|
||||
|
||||
if i < v.Len()-1 {
|
||||
buf.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
|
||||
default:
|
||||
if !v.IsValid() {
|
||||
fmt.Fprint(buf, "<invalid value>")
|
||||
return
|
||||
}
|
||||
format := "%v"
|
||||
switch v.Interface().(type) {
|
||||
case string:
|
||||
format = "%q"
|
||||
case io.ReadSeeker, io.Reader:
|
||||
format = "buffer(%p)"
|
||||
}
|
||||
fmt.Fprintf(buf, format, v.Interface())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package awsutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StringValue returns the string representation of a value.
|
||||
//
|
||||
// Deprecated: Use Prettify instead.
|
||||
func StringValue(i interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
stringValue(reflect.ValueOf(i), 0, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func stringValue(v reflect.Value, indent int, buf *bytes.Buffer) {
|
||||
for v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
buf.WriteString("{\n")
|
||||
|
||||
for i := 0; i < v.Type().NumField(); i++ {
|
||||
ft := v.Type().Field(i)
|
||||
fv := v.Field(i)
|
||||
|
||||
if ft.Name[0:1] == strings.ToLower(ft.Name[0:1]) {
|
||||
continue // ignore unexported fields
|
||||
}
|
||||
if (fv.Kind() == reflect.Ptr || fv.Kind() == reflect.Slice) && fv.IsNil() {
|
||||
continue // ignore unset fields
|
||||
}
|
||||
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(ft.Name + ": ")
|
||||
|
||||
if tag := ft.Tag.Get("sensitive"); tag == "true" {
|
||||
buf.WriteString("<sensitive>")
|
||||
} else {
|
||||
stringValue(fv, indent+2, buf)
|
||||
}
|
||||
|
||||
buf.WriteString(",\n")
|
||||
}
|
||||
|
||||
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
|
||||
case reflect.Slice:
|
||||
nl, id, id2 := "", "", ""
|
||||
if v.Len() > 3 {
|
||||
nl, id, id2 = "\n", strings.Repeat(" ", indent), strings.Repeat(" ", indent+2)
|
||||
}
|
||||
buf.WriteString("[" + nl)
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
buf.WriteString(id2)
|
||||
stringValue(v.Index(i), indent+2, buf)
|
||||
|
||||
if i < v.Len()-1 {
|
||||
buf.WriteString("," + nl)
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString(nl + id + "]")
|
||||
case reflect.Map:
|
||||
buf.WriteString("{\n")
|
||||
|
||||
for i, k := range v.MapKeys() {
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(k.String() + ": ")
|
||||
stringValue(v.MapIndex(k), indent+2, buf)
|
||||
|
||||
if i < v.Len()-1 {
|
||||
buf.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
|
||||
default:
|
||||
format := "%v"
|
||||
switch v.Interface().(type) {
|
||||
case string:
|
||||
format = "%q"
|
||||
}
|
||||
fmt.Fprintf(buf, format, v.Interface())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/client/metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
)
|
||||
|
||||
// A Config provides configuration to a service client instance.
|
||||
type Config struct {
|
||||
Config *aws.Config
|
||||
Handlers request.Handlers
|
||||
PartitionID string
|
||||
Endpoint string
|
||||
SigningRegion string
|
||||
SigningName string
|
||||
ResolvedRegion string
|
||||
|
||||
// States that the signing name did not come from a modeled source but
|
||||
// was derived based on other data. Used by service client constructors
|
||||
// to determine if the signin name can be overridden based on metadata the
|
||||
// service has.
|
||||
SigningNameDerived bool
|
||||
}
|
||||
|
||||
// ConfigProvider provides a generic way for a service client to receive
|
||||
// the ClientConfig without circular dependencies.
|
||||
type ConfigProvider interface {
|
||||
ClientConfig(serviceName string, cfgs ...*aws.Config) Config
|
||||
}
|
||||
|
||||
// ConfigNoResolveEndpointProvider same as ConfigProvider except it will not
|
||||
// resolve the endpoint automatically. The service client's endpoint must be
|
||||
// provided via the aws.Config.Endpoint field.
|
||||
type ConfigNoResolveEndpointProvider interface {
|
||||
ClientConfigNoResolveEndpoint(cfgs ...*aws.Config) Config
|
||||
}
|
||||
|
||||
// A Client implements the base client request and response handling
|
||||
// used by all service clients.
|
||||
type Client struct {
|
||||
request.Retryer
|
||||
metadata.ClientInfo
|
||||
|
||||
Config aws.Config
|
||||
Handlers request.Handlers
|
||||
}
|
||||
|
||||
// New will return a pointer to a new initialized service client.
|
||||
func New(cfg aws.Config, info metadata.ClientInfo, handlers request.Handlers, options ...func(*Client)) *Client {
|
||||
svc := &Client{
|
||||
Config: cfg,
|
||||
ClientInfo: info,
|
||||
Handlers: handlers.Copy(),
|
||||
}
|
||||
|
||||
switch retryer, ok := cfg.Retryer.(request.Retryer); {
|
||||
case ok:
|
||||
svc.Retryer = retryer
|
||||
case cfg.Retryer != nil && cfg.Logger != nil:
|
||||
s := fmt.Sprintf("WARNING: %T does not implement request.Retryer; using DefaultRetryer instead", cfg.Retryer)
|
||||
cfg.Logger.Log(s)
|
||||
fallthrough
|
||||
default:
|
||||
maxRetries := aws.IntValue(cfg.MaxRetries)
|
||||
if cfg.MaxRetries == nil || maxRetries == aws.UseServiceDefaultRetries {
|
||||
maxRetries = DefaultRetryerMaxNumRetries
|
||||
}
|
||||
svc.Retryer = DefaultRetryer{NumMaxRetries: maxRetries}
|
||||
}
|
||||
|
||||
svc.AddDebugHandlers()
|
||||
|
||||
for _, option := range options {
|
||||
option(svc)
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
// NewRequest returns a new Request pointer for the service API
|
||||
// operation and parameters.
|
||||
func (c *Client) NewRequest(operation *request.Operation, params interface{}, data interface{}) *request.Request {
|
||||
return request.New(c.Config, c.ClientInfo, c.Handlers, c.Retryer, operation, params, data)
|
||||
}
|
||||
|
||||
// AddDebugHandlers injects debug logging handlers into the service to log request
|
||||
// debug information.
|
||||
func (c *Client) AddDebugHandlers() {
|
||||
c.Handlers.Send.PushFrontNamed(LogHTTPRequestHandler)
|
||||
c.Handlers.Send.PushBackNamed(LogHTTPResponseHandler)
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/internal/sdkrand"
|
||||
)
|
||||
|
||||
// DefaultRetryer implements basic retry logic using exponential backoff for
|
||||
// most services. If you want to implement custom retry logic, you can implement the
|
||||
// request.Retryer interface.
|
||||
//
|
||||
type DefaultRetryer struct {
|
||||
// Num max Retries is the number of max retries that will be performed.
|
||||
// By default, this is zero.
|
||||
NumMaxRetries int
|
||||
|
||||
// MinRetryDelay is the minimum retry delay after which retry will be performed.
|
||||
// If not set, the value is 0ns.
|
||||
MinRetryDelay time.Duration
|
||||
|
||||
// MinThrottleRetryDelay is the minimum retry delay when throttled.
|
||||
// If not set, the value is 0ns.
|
||||
MinThrottleDelay time.Duration
|
||||
|
||||
// MaxRetryDelay is the maximum retry delay before which retry must be performed.
|
||||
// If not set, the value is 0ns.
|
||||
MaxRetryDelay time.Duration
|
||||
|
||||
// MaxThrottleDelay is the maximum retry delay when throttled.
|
||||
// If not set, the value is 0ns.
|
||||
MaxThrottleDelay time.Duration
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultRetryerMaxNumRetries sets maximum number of retries
|
||||
DefaultRetryerMaxNumRetries = 3
|
||||
|
||||
// DefaultRetryerMinRetryDelay sets minimum retry delay
|
||||
DefaultRetryerMinRetryDelay = 30 * time.Millisecond
|
||||
|
||||
// DefaultRetryerMinThrottleDelay sets minimum delay when throttled
|
||||
DefaultRetryerMinThrottleDelay = 500 * time.Millisecond
|
||||
|
||||
// DefaultRetryerMaxRetryDelay sets maximum retry delay
|
||||
DefaultRetryerMaxRetryDelay = 300 * time.Second
|
||||
|
||||
// DefaultRetryerMaxThrottleDelay sets maximum delay when throttled
|
||||
DefaultRetryerMaxThrottleDelay = 300 * time.Second
|
||||
)
|
||||
|
||||
// MaxRetries returns the number of maximum returns the service will use to make
|
||||
// an individual API request.
|
||||
func (d DefaultRetryer) MaxRetries() int {
|
||||
return d.NumMaxRetries
|
||||
}
|
||||
|
||||
// setRetryerDefaults sets the default values of the retryer if not set
|
||||
func (d *DefaultRetryer) setRetryerDefaults() {
|
||||
if d.MinRetryDelay == 0 {
|
||||
d.MinRetryDelay = DefaultRetryerMinRetryDelay
|
||||
}
|
||||
if d.MaxRetryDelay == 0 {
|
||||
d.MaxRetryDelay = DefaultRetryerMaxRetryDelay
|
||||
}
|
||||
if d.MinThrottleDelay == 0 {
|
||||
d.MinThrottleDelay = DefaultRetryerMinThrottleDelay
|
||||
}
|
||||
if d.MaxThrottleDelay == 0 {
|
||||
d.MaxThrottleDelay = DefaultRetryerMaxThrottleDelay
|
||||
}
|
||||
}
|
||||
|
||||
// RetryRules returns the delay duration before retrying this request again
|
||||
func (d DefaultRetryer) RetryRules(r *request.Request) time.Duration {
|
||||
|
||||
// if number of max retries is zero, no retries will be performed.
|
||||
if d.NumMaxRetries == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Sets default value for retryer members
|
||||
d.setRetryerDefaults()
|
||||
|
||||
// minDelay is the minimum retryer delay
|
||||
minDelay := d.MinRetryDelay
|
||||
|
||||
var initialDelay time.Duration
|
||||
|
||||
isThrottle := r.IsErrorThrottle()
|
||||
if isThrottle {
|
||||
if delay, ok := getRetryAfterDelay(r); ok {
|
||||
initialDelay = delay
|
||||
}
|
||||
minDelay = d.MinThrottleDelay
|
||||
}
|
||||
|
||||
retryCount := r.RetryCount
|
||||
|
||||
// maxDelay the maximum retryer delay
|
||||
maxDelay := d.MaxRetryDelay
|
||||
|
||||
if isThrottle {
|
||||
maxDelay = d.MaxThrottleDelay
|
||||
}
|
||||
|
||||
var delay time.Duration
|
||||
|
||||
// Logic to cap the retry count based on the minDelay provided
|
||||
actualRetryCount := int(math.Log2(float64(minDelay))) + 1
|
||||
if actualRetryCount < 63-retryCount {
|
||||
delay = time.Duration(1<<uint64(retryCount)) * getJitterDelay(minDelay)
|
||||
if delay > maxDelay {
|
||||
delay = getJitterDelay(maxDelay / 2)
|
||||
}
|
||||
} else {
|
||||
delay = getJitterDelay(maxDelay / 2)
|
||||
}
|
||||
return delay + initialDelay
|
||||
}
|
||||
|
||||
// getJitterDelay returns a jittered delay for retry
|
||||
func getJitterDelay(duration time.Duration) time.Duration {
|
||||
return time.Duration(sdkrand.SeededRand.Int63n(int64(duration)) + int64(duration))
|
||||
}
|
||||
|
||||
// ShouldRetry returns true if the request should be retried.
|
||||
func (d DefaultRetryer) ShouldRetry(r *request.Request) bool {
|
||||
|
||||
// ShouldRetry returns false if number of max retries is 0.
|
||||
if d.NumMaxRetries == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// If one of the other handlers already set the retry state
|
||||
// we don't want to override it based on the service's state
|
||||
if r.Retryable != nil {
|
||||
return *r.Retryable
|
||||
}
|
||||
return r.IsErrorRetryable() || r.IsErrorThrottle()
|
||||
}
|
||||
|
||||
// This will look in the Retry-After header, RFC 7231, for how long
|
||||
// it will wait before attempting another request
|
||||
func getRetryAfterDelay(r *request.Request) (time.Duration, bool) {
|
||||
if !canUseRetryAfterHeader(r) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
delayStr := r.HTTPResponse.Header.Get("Retry-After")
|
||||
if len(delayStr) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
delay, err := strconv.Atoi(delayStr)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return time.Duration(delay) * time.Second, true
|
||||
}
|
||||
|
||||
// Will look at the status code to see if the retry header pertains to
|
||||
// the status code.
|
||||
func canUseRetryAfterHeader(r *request.Request) bool {
|
||||
switch r.HTTPResponse.StatusCode {
|
||||
case 429:
|
||||
case 503:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
)
|
||||
|
||||
const logReqMsg = `DEBUG: Request %s/%s Details:
|
||||
---[ REQUEST POST-SIGN ]-----------------------------
|
||||
%s
|
||||
-----------------------------------------------------`
|
||||
|
||||
const logReqErrMsg = `DEBUG ERROR: Request %s/%s:
|
||||
---[ REQUEST DUMP ERROR ]-----------------------------
|
||||
%s
|
||||
------------------------------------------------------`
|
||||
|
||||
type logWriter struct {
|
||||
// Logger is what we will use to log the payload of a response.
|
||||
Logger aws.Logger
|
||||
// buf stores the contents of what has been read
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func (logger *logWriter) Write(b []byte) (int, error) {
|
||||
return logger.buf.Write(b)
|
||||
}
|
||||
|
||||
type teeReaderCloser struct {
|
||||
// io.Reader will be a tee reader that is used during logging.
|
||||
// This structure will read from a body and write the contents to a logger.
|
||||
io.Reader
|
||||
// Source is used just to close when we are done reading.
|
||||
Source io.ReadCloser
|
||||
}
|
||||
|
||||
func (reader *teeReaderCloser) Close() error {
|
||||
return reader.Source.Close()
|
||||
}
|
||||
|
||||
// LogHTTPRequestHandler is a SDK request handler to log the HTTP request sent
|
||||
// to a service. Will include the HTTP request body if the LogLevel of the
|
||||
// request matches LogDebugWithHTTPBody.
|
||||
var LogHTTPRequestHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogRequest",
|
||||
Fn: logRequest,
|
||||
}
|
||||
|
||||
func logRequest(r *request.Request) {
|
||||
if !r.Config.LogLevel.AtLeast(aws.LogDebug) || r.Config.Logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
|
||||
bodySeekable := aws.IsReaderSeekable(r.Body)
|
||||
|
||||
b, err := httputil.DumpRequestOut(r.HTTPRequest, logBody)
|
||||
if err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
if logBody {
|
||||
if !bodySeekable {
|
||||
r.SetReaderBody(aws.ReadSeekCloser(r.HTTPRequest.Body))
|
||||
}
|
||||
// Reset the request body because dumpRequest will re-wrap the
|
||||
// r.HTTPRequest's Body as a NoOpCloser and will not be reset after
|
||||
// read by the HTTP client reader.
|
||||
if err := r.Error; err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
|
||||
}
|
||||
|
||||
// LogHTTPRequestHeaderHandler is a SDK request handler to log the HTTP request sent
|
||||
// to a service. Will only log the HTTP request's headers. The request payload
|
||||
// will not be read.
|
||||
var LogHTTPRequestHeaderHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogRequestHeader",
|
||||
Fn: logRequestHeader,
|
||||
}
|
||||
|
||||
func logRequestHeader(r *request.Request) {
|
||||
if !r.Config.LogLevel.AtLeast(aws.LogDebug) || r.Config.Logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
b, err := httputil.DumpRequestOut(r.HTTPRequest, false)
|
||||
if err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
|
||||
}
|
||||
|
||||
const logRespMsg = `DEBUG: Response %s/%s Details:
|
||||
---[ RESPONSE ]--------------------------------------
|
||||
%s
|
||||
-----------------------------------------------------`
|
||||
|
||||
const logRespErrMsg = `DEBUG ERROR: Response %s/%s:
|
||||
---[ RESPONSE DUMP ERROR ]-----------------------------
|
||||
%s
|
||||
-----------------------------------------------------`
|
||||
|
||||
// LogHTTPResponseHandler is a SDK request handler to log the HTTP response
|
||||
// received from a service. Will include the HTTP response body if the LogLevel
|
||||
// of the request matches LogDebugWithHTTPBody.
|
||||
var LogHTTPResponseHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogResponse",
|
||||
Fn: logResponse,
|
||||
}
|
||||
|
||||
func logResponse(r *request.Request) {
|
||||
if !r.Config.LogLevel.AtLeast(aws.LogDebug) || r.Config.Logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lw := &logWriter{r.Config.Logger, bytes.NewBuffer(nil)}
|
||||
|
||||
if r.HTTPResponse == nil {
|
||||
lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, "request's HTTPResponse is nil"))
|
||||
return
|
||||
}
|
||||
|
||||
logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
|
||||
if logBody {
|
||||
r.HTTPResponse.Body = &teeReaderCloser{
|
||||
Reader: io.TeeReader(r.HTTPResponse.Body, lw),
|
||||
Source: r.HTTPResponse.Body,
|
||||
}
|
||||
}
|
||||
|
||||
handlerFn := func(req *request.Request) {
|
||||
b, err := httputil.DumpResponse(req.HTTPResponse, false)
|
||||
if err != nil {
|
||||
lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
req.ClientInfo.ServiceName, req.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
lw.Logger.Log(fmt.Sprintf(logRespMsg,
|
||||
req.ClientInfo.ServiceName, req.Operation.Name, string(b)))
|
||||
|
||||
if logBody {
|
||||
b, err := ioutil.ReadAll(lw.buf)
|
||||
if err != nil {
|
||||
lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
req.ClientInfo.ServiceName, req.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
lw.Logger.Log(string(b))
|
||||
}
|
||||
}
|
||||
|
||||
const handlerName = "awsdk.client.LogResponse.ResponseBody"
|
||||
|
||||
r.Handlers.Unmarshal.SetBackNamed(request.NamedHandler{
|
||||
Name: handlerName, Fn: handlerFn,
|
||||
})
|
||||
r.Handlers.UnmarshalError.SetBackNamed(request.NamedHandler{
|
||||
Name: handlerName, Fn: handlerFn,
|
||||
})
|
||||
}
|
||||
|
||||
// LogHTTPResponseHeaderHandler is a SDK request handler to log the HTTP
|
||||
// response received from a service. Will only log the HTTP response's headers.
|
||||
// The response payload will not be read.
|
||||
var LogHTTPResponseHeaderHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogResponseHeader",
|
||||
Fn: logResponseHeader,
|
||||
}
|
||||
|
||||
func logResponseHeader(r *request.Request) {
|
||||
if !r.Config.LogLevel.AtLeast(aws.LogDebug) || r.Config.Logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
b, err := httputil.DumpResponse(r.HTTPResponse, false)
|
||||
if err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
r.Config.Logger.Log(fmt.Sprintf(logRespMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
|
||||
}
|
||||
15
vendor/github.com/aws/aws-sdk-go/aws/client/metadata/client_info.go
generated
vendored
Normal file
15
vendor/github.com/aws/aws-sdk-go/aws/client/metadata/client_info.go
generated
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package metadata
|
||||
|
||||
// ClientInfo wraps immutable data from the client.Client structure.
|
||||
type ClientInfo struct {
|
||||
ServiceName string
|
||||
ServiceID string
|
||||
APIVersion string
|
||||
PartitionID string
|
||||
Endpoint string
|
||||
SigningName string
|
||||
SigningRegion string
|
||||
JSONVersion string
|
||||
TargetPrefix string
|
||||
ResolvedRegion string
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
)
|
||||
|
||||
// NoOpRetryer provides a retryer that performs no retries.
|
||||
// It should be used when we do not want retries to be performed.
|
||||
type NoOpRetryer struct{}
|
||||
|
||||
// MaxRetries returns the number of maximum returns the service will use to make
|
||||
// an individual API; For NoOpRetryer the MaxRetries will always be zero.
|
||||
func (d NoOpRetryer) MaxRetries() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// ShouldRetry will always return false for NoOpRetryer, as it should never retry.
|
||||
func (d NoOpRetryer) ShouldRetry(_ *request.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// RetryRules returns the delay duration before retrying this request again;
|
||||
// since NoOpRetryer does not retry, RetryRules always returns 0.
|
||||
func (d NoOpRetryer) RetryRules(_ *request.Request) time.Duration {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -0,0 +1,670 @@
|
|||
package aws
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||
)
|
||||
|
||||
// UseServiceDefaultRetries instructs the config to use the service's own
|
||||
// default number of retries. This will be the default action if
|
||||
// Config.MaxRetries is nil also.
|
||||
const UseServiceDefaultRetries = -1
|
||||
|
||||
// RequestRetryer is an alias for a type that implements the request.Retryer
|
||||
// interface.
|
||||
type RequestRetryer interface{}
|
||||
|
||||
// A Config provides service configuration for service clients. By default,
|
||||
// all clients will use the defaults.DefaultConfig structure.
|
||||
//
|
||||
// // Create Session with MaxRetries configuration to be shared by multiple
|
||||
// // service clients.
|
||||
// sess := session.Must(session.NewSession(&aws.Config{
|
||||
// MaxRetries: aws.Int(3),
|
||||
// }))
|
||||
//
|
||||
// // Create S3 service client with a specific Region.
|
||||
// svc := s3.New(sess, &aws.Config{
|
||||
// Region: aws.String("us-west-2"),
|
||||
// })
|
||||
type Config struct {
|
||||
// Enables verbose error printing of all credential chain errors.
|
||||
// Should be used when wanting to see all errors while attempting to
|
||||
// retrieve credentials.
|
||||
CredentialsChainVerboseErrors *bool
|
||||
|
||||
// The credentials object to use when signing requests. Defaults to a
|
||||
// chain of credential providers to search for credentials in environment
|
||||
// variables, shared credential file, and EC2 Instance Roles.
|
||||
Credentials *credentials.Credentials
|
||||
|
||||
// An optional endpoint URL (hostname only or fully qualified URI)
|
||||
// that overrides the default generated endpoint for a client. Set this
|
||||
// to `nil` or the value to `""` to use the default generated endpoint.
|
||||
//
|
||||
// Note: You must still provide a `Region` value when specifying an
|
||||
// endpoint for a client.
|
||||
Endpoint *string
|
||||
|
||||
// The resolver to use for looking up endpoints for AWS service clients
|
||||
// to use based on region.
|
||||
EndpointResolver endpoints.Resolver
|
||||
|
||||
// EnforceShouldRetryCheck is used in the AfterRetryHandler to always call
|
||||
// ShouldRetry regardless of whether or not if request.Retryable is set.
|
||||
// This will utilize ShouldRetry method of custom retryers. If EnforceShouldRetryCheck
|
||||
// is not set, then ShouldRetry will only be called if request.Retryable is nil.
|
||||
// Proper handling of the request.Retryable field is important when setting this field.
|
||||
EnforceShouldRetryCheck *bool
|
||||
|
||||
// The region to send requests to. This parameter is required and must
|
||||
// be configured globally or on a per-client basis unless otherwise
|
||||
// noted. A full list of regions is found in the "Regions and Endpoints"
|
||||
// document.
|
||||
//
|
||||
// See http://docs.aws.amazon.com/general/latest/gr/rande.html for AWS
|
||||
// Regions and Endpoints.
|
||||
Region *string
|
||||
|
||||
// Set this to `true` to disable SSL when sending requests. Defaults
|
||||
// to `false`.
|
||||
DisableSSL *bool
|
||||
|
||||
// The HTTP client to use when sending requests. Defaults to
|
||||
// `http.DefaultClient`.
|
||||
HTTPClient *http.Client
|
||||
|
||||
// An integer value representing the logging level. The default log level
|
||||
// is zero (LogOff), which represents no logging. To enable logging set
|
||||
// to a LogLevel Value.
|
||||
LogLevel *LogLevelType
|
||||
|
||||
// The logger writer interface to write logging messages to. Defaults to
|
||||
// standard out.
|
||||
Logger Logger
|
||||
|
||||
// The maximum number of times that a request will be retried for failures.
|
||||
// Defaults to -1, which defers the max retry setting to the service
|
||||
// specific configuration.
|
||||
MaxRetries *int
|
||||
|
||||
// Retryer guides how HTTP requests should be retried in case of
|
||||
// recoverable failures.
|
||||
//
|
||||
// When nil or the value does not implement the request.Retryer interface,
|
||||
// the client.DefaultRetryer will be used.
|
||||
//
|
||||
// When both Retryer and MaxRetries are non-nil, the former is used and
|
||||
// the latter ignored.
|
||||
//
|
||||
// To set the Retryer field in a type-safe manner and with chaining, use
|
||||
// the request.WithRetryer helper function:
|
||||
//
|
||||
// cfg := request.WithRetryer(aws.NewConfig(), myRetryer)
|
||||
//
|
||||
Retryer RequestRetryer
|
||||
|
||||
// Disables semantic parameter validation, which validates input for
|
||||
// missing required fields and/or other semantic request input errors.
|
||||
DisableParamValidation *bool
|
||||
|
||||
// Disables the computation of request and response checksums, e.g.,
|
||||
// CRC32 checksums in Amazon DynamoDB.
|
||||
DisableComputeChecksums *bool
|
||||
|
||||
// Set this to `true` to force the request to use path-style addressing,
|
||||
// i.e., `http://s3.amazonaws.com/BUCKET/KEY`. By default, the S3 client
|
||||
// will use virtual hosted bucket addressing when possible
|
||||
// (`http://BUCKET.s3.amazonaws.com/KEY`).
|
||||
//
|
||||
// Note: This configuration option is specific to the Amazon S3 service.
|
||||
//
|
||||
// See http://docs.aws.amazon.com/AmazonS3/latest/dev/VirtualHosting.html
|
||||
// for Amazon S3: Virtual Hosting of Buckets
|
||||
S3ForcePathStyle *bool
|
||||
|
||||
// Set this to `true` to disable the SDK adding the `Expect: 100-Continue`
|
||||
// header to PUT requests over 2MB of content. 100-Continue instructs the
|
||||
// HTTP client not to send the body until the service responds with a
|
||||
// `continue` status. This is useful to prevent sending the request body
|
||||
// until after the request is authenticated, and validated.
|
||||
//
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html
|
||||
//
|
||||
// 100-Continue is only enabled for Go 1.6 and above. See `http.Transport`'s
|
||||
// `ExpectContinueTimeout` for information on adjusting the continue wait
|
||||
// timeout. https://golang.org/pkg/net/http/#Transport
|
||||
//
|
||||
// You should use this flag to disable 100-Continue if you experience issues
|
||||
// with proxies or third party S3 compatible services.
|
||||
S3Disable100Continue *bool
|
||||
|
||||
// Set this to `true` to enable S3 Accelerate feature. For all operations
|
||||
// compatible with S3 Accelerate will use the accelerate endpoint for
|
||||
// requests. Requests not compatible will fall back to normal S3 requests.
|
||||
//
|
||||
// The bucket must be enable for accelerate to be used with S3 client with
|
||||
// accelerate enabled. If the bucket is not enabled for accelerate an error
|
||||
// will be returned. The bucket name must be DNS compatible to also work
|
||||
// with accelerate.
|
||||
S3UseAccelerate *bool
|
||||
|
||||
// S3DisableContentMD5Validation config option is temporarily disabled,
|
||||
// For S3 GetObject API calls, #1837.
|
||||
//
|
||||
// Set this to `true` to disable the S3 service client from automatically
|
||||
// adding the ContentMD5 to S3 Object Put and Upload API calls. This option
|
||||
// will also disable the SDK from performing object ContentMD5 validation
|
||||
// on GetObject API calls.
|
||||
S3DisableContentMD5Validation *bool
|
||||
|
||||
// Set this to `true` to have the S3 service client to use the region specified
|
||||
// in the ARN, when an ARN is provided as an argument to a bucket parameter.
|
||||
S3UseARNRegion *bool
|
||||
|
||||
// Set this to `true` to enable the SDK to unmarshal API response header maps to
|
||||
// normalized lower case map keys.
|
||||
//
|
||||
// For example S3's X-Amz-Meta prefixed header will be unmarshaled to lower case
|
||||
// Metadata member's map keys. The value of the header in the map is unaffected.
|
||||
//
|
||||
// The AWS SDK for Go v2, uses lower case header maps by default. The v1
|
||||
// SDK provides this opt-in for this option, for backwards compatibility.
|
||||
LowerCaseHeaderMaps *bool
|
||||
|
||||
// Set this to `true` to disable the EC2Metadata client from overriding the
|
||||
// default http.Client's Timeout. This is helpful if you do not want the
|
||||
// EC2Metadata client to create a new http.Client. This options is only
|
||||
// meaningful if you're not already using a custom HTTP client with the
|
||||
// SDK. Enabled by default.
|
||||
//
|
||||
// Must be set and provided to the session.NewSession() in order to disable
|
||||
// the EC2Metadata overriding the timeout for default credentials chain.
|
||||
//
|
||||
// Example:
|
||||
// sess := session.Must(session.NewSession(aws.NewConfig()
|
||||
// .WithEC2MetadataDisableTimeoutOverride(true)))
|
||||
//
|
||||
// svc := s3.New(sess)
|
||||
//
|
||||
EC2MetadataDisableTimeoutOverride *bool
|
||||
|
||||
// Set this to `false` to disable EC2Metadata client from falling back to IMDSv1.
|
||||
// By default, EC2 role credentials will fall back to IMDSv1 as needed for backwards compatibility.
|
||||
// You can disable this behavior by explicitly setting this flag to `false`. When false, the EC2Metadata
|
||||
// client will return any errors encountered from attempting to fetch a token instead of silently
|
||||
// using the insecure data flow of IMDSv1.
|
||||
//
|
||||
// Example:
|
||||
// sess := session.Must(session.NewSession(aws.NewConfig()
|
||||
// .WithEC2MetadataEnableFallback(false)))
|
||||
//
|
||||
// svc := s3.New(sess)
|
||||
//
|
||||
// See [configuring IMDS] for more information.
|
||||
//
|
||||
// [configuring IMDS]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
|
||||
EC2MetadataEnableFallback *bool
|
||||
|
||||
// Instructs the endpoint to be generated for a service client to
|
||||
// be the dual stack endpoint. The dual stack endpoint will support
|
||||
// both IPv4 and IPv6 addressing.
|
||||
//
|
||||
// Setting this for a service which does not support dual stack will fail
|
||||
// to make requests. It is not recommended to set this value on the session
|
||||
// as it will apply to all service clients created with the session. Even
|
||||
// services which don't support dual stack endpoints.
|
||||
//
|
||||
// If the Endpoint config value is also provided the UseDualStack flag
|
||||
// will be ignored.
|
||||
//
|
||||
// Only supported with.
|
||||
//
|
||||
// sess := session.Must(session.NewSession())
|
||||
//
|
||||
// svc := s3.New(sess, &aws.Config{
|
||||
// UseDualStack: aws.Bool(true),
|
||||
// })
|
||||
//
|
||||
// Deprecated: This option will continue to function for S3 and S3 Control for backwards compatibility.
|
||||
// UseDualStackEndpoint should be used to enable usage of a service's dual-stack endpoint for all service clients
|
||||
// moving forward. For S3 and S3 Control, when UseDualStackEndpoint is set to a non-zero value it takes higher
|
||||
// precedence then this option.
|
||||
UseDualStack *bool
|
||||
|
||||
// Sets the resolver to resolve a dual-stack endpoint for the service.
|
||||
UseDualStackEndpoint endpoints.DualStackEndpointState
|
||||
|
||||
// UseFIPSEndpoint specifies the resolver must resolve a FIPS endpoint.
|
||||
UseFIPSEndpoint endpoints.FIPSEndpointState
|
||||
|
||||
// SleepDelay is an override for the func the SDK will call when sleeping
|
||||
// during the lifecycle of a request. Specifically this will be used for
|
||||
// request delays. This value should only be used for testing. To adjust
|
||||
// the delay of a request see the aws/client.DefaultRetryer and
|
||||
// aws/request.Retryer.
|
||||
//
|
||||
// SleepDelay will prevent any Context from being used for canceling retry
|
||||
// delay of an API operation. It is recommended to not use SleepDelay at all
|
||||
// and specify a Retryer instead.
|
||||
SleepDelay func(time.Duration)
|
||||
|
||||
// DisableRestProtocolURICleaning will not clean the URL path when making rest protocol requests.
|
||||
// Will default to false. This would only be used for empty directory names in s3 requests.
|
||||
//
|
||||
// Example:
|
||||
// sess := session.Must(session.NewSession(&aws.Config{
|
||||
// DisableRestProtocolURICleaning: aws.Bool(true),
|
||||
// }))
|
||||
//
|
||||
// svc := s3.New(sess)
|
||||
// out, err := svc.GetObject(&s3.GetObjectInput {
|
||||
// Bucket: aws.String("bucketname"),
|
||||
// Key: aws.String("//foo//bar//moo"),
|
||||
// })
|
||||
DisableRestProtocolURICleaning *bool
|
||||
|
||||
// EnableEndpointDiscovery will allow for endpoint discovery on operations that
|
||||
// have the definition in its model. By default, endpoint discovery is off.
|
||||
// To use EndpointDiscovery, Endpoint should be unset or set to an empty string.
|
||||
//
|
||||
// Example:
|
||||
// sess := session.Must(session.NewSession(&aws.Config{
|
||||
// EnableEndpointDiscovery: aws.Bool(true),
|
||||
// }))
|
||||
//
|
||||
// svc := s3.New(sess)
|
||||
// out, err := svc.GetObject(&s3.GetObjectInput {
|
||||
// Bucket: aws.String("bucketname"),
|
||||
// Key: aws.String("/foo/bar/moo"),
|
||||
// })
|
||||
EnableEndpointDiscovery *bool
|
||||
|
||||
// DisableEndpointHostPrefix will disable the SDK's behavior of prefixing
|
||||
// request endpoint hosts with modeled information.
|
||||
//
|
||||
// Disabling this feature is useful when you want to use local endpoints
|
||||
// for testing that do not support the modeled host prefix pattern.
|
||||
DisableEndpointHostPrefix *bool
|
||||
|
||||
// STSRegionalEndpoint will enable regional or legacy endpoint resolving
|
||||
STSRegionalEndpoint endpoints.STSRegionalEndpoint
|
||||
|
||||
// S3UsEast1RegionalEndpoint will enable regional or legacy endpoint resolving
|
||||
S3UsEast1RegionalEndpoint endpoints.S3UsEast1RegionalEndpoint
|
||||
}
|
||||
|
||||
// NewConfig returns a new Config pointer that can be chained with builder
|
||||
// methods to set multiple configuration values inline without using pointers.
|
||||
//
|
||||
// // Create Session with MaxRetries configuration to be shared by multiple
|
||||
// // service clients.
|
||||
// sess := session.Must(session.NewSession(aws.NewConfig().
|
||||
// WithMaxRetries(3),
|
||||
// ))
|
||||
//
|
||||
// // Create S3 service client with a specific Region.
|
||||
// svc := s3.New(sess, aws.NewConfig().
|
||||
// WithRegion("us-west-2"),
|
||||
// )
|
||||
func NewConfig() *Config {
|
||||
return &Config{}
|
||||
}
|
||||
|
||||
// WithCredentialsChainVerboseErrors sets a config verbose errors boolean and returning
|
||||
// a Config pointer.
|
||||
func (c *Config) WithCredentialsChainVerboseErrors(verboseErrs bool) *Config {
|
||||
c.CredentialsChainVerboseErrors = &verboseErrs
|
||||
return c
|
||||
}
|
||||
|
||||
// WithCredentials sets a config Credentials value returning a Config pointer
|
||||
// for chaining.
|
||||
func (c *Config) WithCredentials(creds *credentials.Credentials) *Config {
|
||||
c.Credentials = creds
|
||||
return c
|
||||
}
|
||||
|
||||
// WithEndpoint sets a config Endpoint value returning a Config pointer for
|
||||
// chaining.
|
||||
func (c *Config) WithEndpoint(endpoint string) *Config {
|
||||
c.Endpoint = &endpoint
|
||||
return c
|
||||
}
|
||||
|
||||
// WithEndpointResolver sets a config EndpointResolver value returning a
|
||||
// Config pointer for chaining.
|
||||
func (c *Config) WithEndpointResolver(resolver endpoints.Resolver) *Config {
|
||||
c.EndpointResolver = resolver
|
||||
return c
|
||||
}
|
||||
|
||||
// WithRegion sets a config Region value returning a Config pointer for
|
||||
// chaining.
|
||||
func (c *Config) WithRegion(region string) *Config {
|
||||
c.Region = ®ion
|
||||
return c
|
||||
}
|
||||
|
||||
// WithDisableSSL sets a config DisableSSL value returning a Config pointer
|
||||
// for chaining.
|
||||
func (c *Config) WithDisableSSL(disable bool) *Config {
|
||||
c.DisableSSL = &disable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithHTTPClient sets a config HTTPClient value returning a Config pointer
|
||||
// for chaining.
|
||||
func (c *Config) WithHTTPClient(client *http.Client) *Config {
|
||||
c.HTTPClient = client
|
||||
return c
|
||||
}
|
||||
|
||||
// WithMaxRetries sets a config MaxRetries value returning a Config pointer
|
||||
// for chaining.
|
||||
func (c *Config) WithMaxRetries(max int) *Config {
|
||||
c.MaxRetries = &max
|
||||
return c
|
||||
}
|
||||
|
||||
// WithDisableParamValidation sets a config DisableParamValidation value
|
||||
// returning a Config pointer for chaining.
|
||||
func (c *Config) WithDisableParamValidation(disable bool) *Config {
|
||||
c.DisableParamValidation = &disable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithDisableComputeChecksums sets a config DisableComputeChecksums value
|
||||
// returning a Config pointer for chaining.
|
||||
func (c *Config) WithDisableComputeChecksums(disable bool) *Config {
|
||||
c.DisableComputeChecksums = &disable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithLogLevel sets a config LogLevel value returning a Config pointer for
|
||||
// chaining.
|
||||
func (c *Config) WithLogLevel(level LogLevelType) *Config {
|
||||
c.LogLevel = &level
|
||||
return c
|
||||
}
|
||||
|
||||
// WithLogger sets a config Logger value returning a Config pointer for
|
||||
// chaining.
|
||||
func (c *Config) WithLogger(logger Logger) *Config {
|
||||
c.Logger = logger
|
||||
return c
|
||||
}
|
||||
|
||||
// WithS3ForcePathStyle sets a config S3ForcePathStyle value returning a Config
|
||||
// pointer for chaining.
|
||||
func (c *Config) WithS3ForcePathStyle(force bool) *Config {
|
||||
c.S3ForcePathStyle = &force
|
||||
return c
|
||||
}
|
||||
|
||||
// WithS3Disable100Continue sets a config S3Disable100Continue value returning
|
||||
// a Config pointer for chaining.
|
||||
func (c *Config) WithS3Disable100Continue(disable bool) *Config {
|
||||
c.S3Disable100Continue = &disable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithS3UseAccelerate sets a config S3UseAccelerate value returning a Config
|
||||
// pointer for chaining.
|
||||
func (c *Config) WithS3UseAccelerate(enable bool) *Config {
|
||||
c.S3UseAccelerate = &enable
|
||||
return c
|
||||
|
||||
}
|
||||
|
||||
// WithS3DisableContentMD5Validation sets a config
|
||||
// S3DisableContentMD5Validation value returning a Config pointer for chaining.
|
||||
func (c *Config) WithS3DisableContentMD5Validation(enable bool) *Config {
|
||||
c.S3DisableContentMD5Validation = &enable
|
||||
return c
|
||||
|
||||
}
|
||||
|
||||
// WithS3UseARNRegion sets a config S3UseARNRegion value and
|
||||
// returning a Config pointer for chaining
|
||||
func (c *Config) WithS3UseARNRegion(enable bool) *Config {
|
||||
c.S3UseARNRegion = &enable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithUseDualStack sets a config UseDualStack value returning a Config
|
||||
// pointer for chaining.
|
||||
func (c *Config) WithUseDualStack(enable bool) *Config {
|
||||
c.UseDualStack = &enable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithUseFIPSEndpoint sets a config UseFIPSEndpoint value returning a Config
|
||||
// pointer for chaining.
|
||||
func (c *Config) WithUseFIPSEndpoint(enable bool) *Config {
|
||||
if enable {
|
||||
c.UseFIPSEndpoint = endpoints.FIPSEndpointStateEnabled
|
||||
} else {
|
||||
c.UseFIPSEndpoint = endpoints.FIPSEndpointStateDisabled
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// WithEC2MetadataDisableTimeoutOverride sets a config EC2MetadataDisableTimeoutOverride value
|
||||
// returning a Config pointer for chaining.
|
||||
func (c *Config) WithEC2MetadataDisableTimeoutOverride(enable bool) *Config {
|
||||
c.EC2MetadataDisableTimeoutOverride = &enable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithEC2MetadataEnableFallback sets a config EC2MetadataEnableFallback value
|
||||
// returning a Config pointer for chaining.
|
||||
func (c *Config) WithEC2MetadataEnableFallback(v bool) *Config {
|
||||
c.EC2MetadataEnableFallback = &v
|
||||
return c
|
||||
}
|
||||
|
||||
// WithSleepDelay overrides the function used to sleep while waiting for the
|
||||
// next retry. Defaults to time.Sleep.
|
||||
func (c *Config) WithSleepDelay(fn func(time.Duration)) *Config {
|
||||
c.SleepDelay = fn
|
||||
return c
|
||||
}
|
||||
|
||||
// WithEndpointDiscovery will set whether or not to use endpoint discovery.
|
||||
func (c *Config) WithEndpointDiscovery(t bool) *Config {
|
||||
c.EnableEndpointDiscovery = &t
|
||||
return c
|
||||
}
|
||||
|
||||
// WithDisableEndpointHostPrefix will set whether or not to use modeled host prefix
|
||||
// when making requests.
|
||||
func (c *Config) WithDisableEndpointHostPrefix(t bool) *Config {
|
||||
c.DisableEndpointHostPrefix = &t
|
||||
return c
|
||||
}
|
||||
|
||||
// WithSTSRegionalEndpoint will set whether or not to use regional endpoint flag
|
||||
// when resolving the endpoint for a service
|
||||
func (c *Config) WithSTSRegionalEndpoint(sre endpoints.STSRegionalEndpoint) *Config {
|
||||
c.STSRegionalEndpoint = sre
|
||||
return c
|
||||
}
|
||||
|
||||
// WithS3UsEast1RegionalEndpoint will set whether or not to use regional endpoint flag
|
||||
// when resolving the endpoint for a service
|
||||
func (c *Config) WithS3UsEast1RegionalEndpoint(sre endpoints.S3UsEast1RegionalEndpoint) *Config {
|
||||
c.S3UsEast1RegionalEndpoint = sre
|
||||
return c
|
||||
}
|
||||
|
||||
// WithLowerCaseHeaderMaps sets a config LowerCaseHeaderMaps value
|
||||
// returning a Config pointer for chaining.
|
||||
func (c *Config) WithLowerCaseHeaderMaps(t bool) *Config {
|
||||
c.LowerCaseHeaderMaps = &t
|
||||
return c
|
||||
}
|
||||
|
||||
// WithDisableRestProtocolURICleaning sets a config DisableRestProtocolURICleaning value
|
||||
// returning a Config pointer for chaining.
|
||||
func (c *Config) WithDisableRestProtocolURICleaning(t bool) *Config {
|
||||
c.DisableRestProtocolURICleaning = &t
|
||||
return c
|
||||
}
|
||||
|
||||
// MergeIn merges the passed in configs into the existing config object.
|
||||
func (c *Config) MergeIn(cfgs ...*Config) {
|
||||
for _, other := range cfgs {
|
||||
mergeInConfig(c, other)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeInConfig(dst *Config, other *Config) {
|
||||
if other == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if other.CredentialsChainVerboseErrors != nil {
|
||||
dst.CredentialsChainVerboseErrors = other.CredentialsChainVerboseErrors
|
||||
}
|
||||
|
||||
if other.Credentials != nil {
|
||||
dst.Credentials = other.Credentials
|
||||
}
|
||||
|
||||
if other.Endpoint != nil {
|
||||
dst.Endpoint = other.Endpoint
|
||||
}
|
||||
|
||||
if other.EndpointResolver != nil {
|
||||
dst.EndpointResolver = other.EndpointResolver
|
||||
}
|
||||
|
||||
if other.Region != nil {
|
||||
dst.Region = other.Region
|
||||
}
|
||||
|
||||
if other.DisableSSL != nil {
|
||||
dst.DisableSSL = other.DisableSSL
|
||||
}
|
||||
|
||||
if other.HTTPClient != nil {
|
||||
dst.HTTPClient = other.HTTPClient
|
||||
}
|
||||
|
||||
if other.LogLevel != nil {
|
||||
dst.LogLevel = other.LogLevel
|
||||
}
|
||||
|
||||
if other.Logger != nil {
|
||||
dst.Logger = other.Logger
|
||||
}
|
||||
|
||||
if other.MaxRetries != nil {
|
||||
dst.MaxRetries = other.MaxRetries
|
||||
}
|
||||
|
||||
if other.Retryer != nil {
|
||||
dst.Retryer = other.Retryer
|
||||
}
|
||||
|
||||
if other.DisableParamValidation != nil {
|
||||
dst.DisableParamValidation = other.DisableParamValidation
|
||||
}
|
||||
|
||||
if other.DisableComputeChecksums != nil {
|
||||
dst.DisableComputeChecksums = other.DisableComputeChecksums
|
||||
}
|
||||
|
||||
if other.S3ForcePathStyle != nil {
|
||||
dst.S3ForcePathStyle = other.S3ForcePathStyle
|
||||
}
|
||||
|
||||
if other.S3Disable100Continue != nil {
|
||||
dst.S3Disable100Continue = other.S3Disable100Continue
|
||||
}
|
||||
|
||||
if other.S3UseAccelerate != nil {
|
||||
dst.S3UseAccelerate = other.S3UseAccelerate
|
||||
}
|
||||
|
||||
if other.S3DisableContentMD5Validation != nil {
|
||||
dst.S3DisableContentMD5Validation = other.S3DisableContentMD5Validation
|
||||
}
|
||||
|
||||
if other.S3UseARNRegion != nil {
|
||||
dst.S3UseARNRegion = other.S3UseARNRegion
|
||||
}
|
||||
|
||||
if other.UseDualStack != nil {
|
||||
dst.UseDualStack = other.UseDualStack
|
||||
}
|
||||
|
||||
if other.UseDualStackEndpoint != endpoints.DualStackEndpointStateUnset {
|
||||
dst.UseDualStackEndpoint = other.UseDualStackEndpoint
|
||||
}
|
||||
|
||||
if other.EC2MetadataDisableTimeoutOverride != nil {
|
||||
dst.EC2MetadataDisableTimeoutOverride = other.EC2MetadataDisableTimeoutOverride
|
||||
}
|
||||
|
||||
if other.EC2MetadataEnableFallback != nil {
|
||||
dst.EC2MetadataEnableFallback = other.EC2MetadataEnableFallback
|
||||
}
|
||||
|
||||
if other.SleepDelay != nil {
|
||||
dst.SleepDelay = other.SleepDelay
|
||||
}
|
||||
|
||||
if other.DisableRestProtocolURICleaning != nil {
|
||||
dst.DisableRestProtocolURICleaning = other.DisableRestProtocolURICleaning
|
||||
}
|
||||
|
||||
if other.EnforceShouldRetryCheck != nil {
|
||||
dst.EnforceShouldRetryCheck = other.EnforceShouldRetryCheck
|
||||
}
|
||||
|
||||
if other.EnableEndpointDiscovery != nil {
|
||||
dst.EnableEndpointDiscovery = other.EnableEndpointDiscovery
|
||||
}
|
||||
|
||||
if other.DisableEndpointHostPrefix != nil {
|
||||
dst.DisableEndpointHostPrefix = other.DisableEndpointHostPrefix
|
||||
}
|
||||
|
||||
if other.STSRegionalEndpoint != endpoints.UnsetSTSEndpoint {
|
||||
dst.STSRegionalEndpoint = other.STSRegionalEndpoint
|
||||
}
|
||||
|
||||
if other.S3UsEast1RegionalEndpoint != endpoints.UnsetS3UsEast1Endpoint {
|
||||
dst.S3UsEast1RegionalEndpoint = other.S3UsEast1RegionalEndpoint
|
||||
}
|
||||
|
||||
if other.LowerCaseHeaderMaps != nil {
|
||||
dst.LowerCaseHeaderMaps = other.LowerCaseHeaderMaps
|
||||
}
|
||||
|
||||
if other.UseDualStackEndpoint != endpoints.DualStackEndpointStateUnset {
|
||||
dst.UseDualStackEndpoint = other.UseDualStackEndpoint
|
||||
}
|
||||
|
||||
if other.UseFIPSEndpoint != endpoints.FIPSEndpointStateUnset {
|
||||
dst.UseFIPSEndpoint = other.UseFIPSEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
// Copy will return a shallow copy of the Config object. If any additional
|
||||
// configurations are provided they will be merged into the new config returned.
|
||||
func (c *Config) Copy(cfgs ...*Config) *Config {
|
||||
dst := &Config{}
|
||||
dst.MergeIn(c)
|
||||
|
||||
for _, cfg := range cfgs {
|
||||
dst.MergeIn(cfg)
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue