mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-03-18 21:30:44 +01:00
copy subset of Sprig template functions
This commit is contained in:
@@ -253,3 +253,4 @@ Third-party libraries and resources:
|
||||
* [Statically linking go-sqlite3](https://www.arp242.net/static-go.html)
|
||||
* [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs)
|
||||
* [webpush-go](https://github.com/SherClockHolmes/webpush-go) (MIT) is used to send web push notifications
|
||||
* [Sprig](https://github.com/Masterminds/sprig) (MIT) is used to add template parsing functions
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1440,6 +1440,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||
* Full [IPv6 support](config.md#ipv6-support) for ntfy and the official ntfy.sh server ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4))
|
||||
* Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha))
|
||||
* Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn))
|
||||
* You can now use [Slim-Sprig](https://github.com/go-task/slim-sprig) functions in message/title templates ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
|
||||
|
||||
**Languages**
|
||||
|
||||
|
||||
24
docs/sprig.md
Normal file
24
docs/sprig.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Template Functions
|
||||
|
||||
ntfy includes a (reduced) version of [Sprig](https://github.com/Masterminds/sprig) to add functions that can be used
|
||||
when you are using the [message template](publish.md#message-templating) feature.
|
||||
|
||||
Below are the functions that are available to use inside your message/title templates.
|
||||
|
||||
* [String Functions](./sprig/strings.md): `trim`, `trunc`, `substr`, `plural`, etc.
|
||||
* [String List Functions](./sprig/string_slice.md): `splitList`, `sortAlpha`, etc.
|
||||
* [Integer Math Functions](./sprig/math.md): `add`, `max`, `mul`, etc.
|
||||
* [Integer List Functions](./sprig/integer_slice.md): `until`, `untilStep`
|
||||
* [Date Functions](./sprig/date.md): `now`, `date`, etc.
|
||||
* [Defaults Functions](./sprig/defaults.md): `default`, `empty`, `coalesce`, `fromJSON`, `toJSON`, `toPrettyJSON`, `toRawJSON`, `ternary`
|
||||
* [Encoding Functions](./sprig/encoding.md): `b64enc`, `b64dec`, etc.
|
||||
* [Lists and List Functions](./sprig/lists.md): `list`, `first`, `uniq`, etc.
|
||||
* [Dictionaries and Dict Functions](./sprig/dicts.md): `get`, `set`, `dict`, `hasKey`, `pluck`, `dig`, etc.
|
||||
* [Type Conversion Functions](./sprig/conversion.md): `atoi`, `int64`, `toString`, etc.
|
||||
* [Path and Filepath Functions](./sprig/paths.md): `base`, `dir`, `ext`, `clean`, `isAbs`, `osBase`, `osDir`, `osExt`, `osClean`, `osIsAbs`
|
||||
* [Flow Control Functions](./sprig/flow_control.md): `fail`
|
||||
* Advanced Functions
|
||||
* [UUID Functions](./sprig/uuid.md): `uuidv4`
|
||||
* [Reflection](./sprig/reflection.md): `typeOf`, `kindIs`, `typeIsLike`, etc.
|
||||
* [Cryptographic and Security Functions](./sprig/crypto.md): `sha256sum`, etc.
|
||||
* [URL](./sprig/url.md): `urlParse`, `urlJoin`
|
||||
36
docs/sprig/conversion.md
Normal file
36
docs/sprig/conversion.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Type Conversion Functions
|
||||
|
||||
The following type conversion functions are provided by Sprig:
|
||||
|
||||
- `atoi`: Convert a string to an integer.
|
||||
- `float64`: Convert to a `float64`.
|
||||
- `int`: Convert to an `int` at the system's width.
|
||||
- `int64`: Convert to an `int64`.
|
||||
- `toDecimal`: Convert a unix octal to a `int64`.
|
||||
- `toString`: Convert to a string.
|
||||
- `toStrings`: Convert a list, slice, or array to a list of strings.
|
||||
|
||||
Only `atoi` requires that the input be a specific type. The others will attempt
|
||||
to convert from any type to the destination type. For example, `int64` can convert
|
||||
floats to ints, and it can also convert strings to ints.
|
||||
|
||||
## toStrings
|
||||
|
||||
Given a list-like collection, produce a slice of strings.
|
||||
|
||||
```
|
||||
list 1 2 3 | toStrings
|
||||
```
|
||||
|
||||
The above converts `1` to `"1"`, `2` to `"2"`, and so on, and then returns
|
||||
them as a list.
|
||||
|
||||
## toDecimal
|
||||
|
||||
Given a unix octal permission, produce a decimal.
|
||||
|
||||
```
|
||||
"0777" | toDecimal
|
||||
```
|
||||
|
||||
The above converts `0777` to `511` and returns the value as an int64.
|
||||
41
docs/sprig/crypto.md
Normal file
41
docs/sprig/crypto.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Cryptographic and Security Functions
|
||||
|
||||
Sprig provides a couple of advanced cryptographic functions.
|
||||
|
||||
## sha1sum
|
||||
|
||||
The `sha1sum` function receives a string, and computes it's SHA1 digest.
|
||||
|
||||
```
|
||||
sha1sum "Hello world!"
|
||||
```
|
||||
|
||||
## sha256sum
|
||||
|
||||
The `sha256sum` function receives a string, and computes it's SHA256 digest.
|
||||
|
||||
```
|
||||
sha256sum "Hello world!"
|
||||
```
|
||||
|
||||
The above will compute the SHA 256 sum in an "ASCII armored" format that is
|
||||
safe to print.
|
||||
|
||||
## sha512sum
|
||||
|
||||
The `sha512sum` function receives a string, and computes it's SHA512 digest.
|
||||
|
||||
```
|
||||
sha512sum "Hello world!"
|
||||
```
|
||||
|
||||
The above will compute the SHA 512 sum in an "ASCII armored" format that is
|
||||
safe to print.
|
||||
|
||||
## adler32sum
|
||||
|
||||
The `adler32sum` function receives a string, and computes its Adler-32 checksum.
|
||||
|
||||
```
|
||||
adler32sum "Hello world!"
|
||||
```
|
||||
126
docs/sprig/date.md
Normal file
126
docs/sprig/date.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Date Functions
|
||||
|
||||
## now
|
||||
|
||||
The current date/time. Use this in conjunction with other date functions.
|
||||
|
||||
## ago
|
||||
|
||||
The `ago` function returns duration from time.Now in seconds resolution.
|
||||
|
||||
```
|
||||
ago .CreatedAt
|
||||
```
|
||||
|
||||
returns in `time.Duration` String() format
|
||||
|
||||
```
|
||||
2h34m7s
|
||||
```
|
||||
|
||||
## date
|
||||
|
||||
The `date` function formats a date.
|
||||
|
||||
Format the date to YEAR-MONTH-DAY:
|
||||
|
||||
```
|
||||
now | date "2006-01-02"
|
||||
```
|
||||
|
||||
Date formatting in Go is a [little bit different](https://pauladamsmith.com/blog/2011/05/go_time.html).
|
||||
|
||||
In short, take this as the base date:
|
||||
|
||||
```
|
||||
Mon Jan 2 15:04:05 MST 2006
|
||||
```
|
||||
|
||||
Write it in the format you want. Above, `2006-01-02` is the same date, but
|
||||
in the format we want.
|
||||
|
||||
## dateInZone
|
||||
|
||||
Same as `date`, but with a timezone.
|
||||
|
||||
```
|
||||
dateInZone "2006-01-02" (now) "UTC"
|
||||
```
|
||||
|
||||
## duration
|
||||
|
||||
Formats a given amount of seconds as a `time.Duration`.
|
||||
|
||||
This returns 1m35s
|
||||
|
||||
```
|
||||
duration "95"
|
||||
```
|
||||
|
||||
## durationRound
|
||||
|
||||
Rounds a given duration to the most significant unit. Strings and `time.Duration`
|
||||
gets parsed as a duration, while a `time.Time` is calculated as the duration since.
|
||||
|
||||
This return 2h
|
||||
|
||||
```
|
||||
durationRound "2h10m5s"
|
||||
```
|
||||
|
||||
This returns 3mo
|
||||
|
||||
```
|
||||
durationRound "2400h10m5s"
|
||||
```
|
||||
|
||||
## unixEpoch
|
||||
|
||||
Returns the seconds since the unix epoch for a `time.Time`.
|
||||
|
||||
```
|
||||
now | unixEpoch
|
||||
```
|
||||
|
||||
## dateModify, mustDateModify
|
||||
|
||||
The `dateModify` takes a modification and a date and returns the timestamp.
|
||||
|
||||
Subtract an hour and thirty minutes from the current time:
|
||||
|
||||
```
|
||||
now | date_modify "-1.5h"
|
||||
```
|
||||
|
||||
If the modification format is wrong `dateModify` will return the date unmodified. `mustDateModify` will return an error otherwise.
|
||||
|
||||
## htmlDate
|
||||
|
||||
The `htmlDate` function formats a date for inserting into an HTML date picker
|
||||
input field.
|
||||
|
||||
```
|
||||
now | htmlDate
|
||||
```
|
||||
|
||||
## htmlDateInZone
|
||||
|
||||
Same as htmlDate, but with a timezone.
|
||||
|
||||
```
|
||||
htmlDateInZone (now) "UTC"
|
||||
```
|
||||
|
||||
## toDate, mustToDate
|
||||
|
||||
`toDate` converts a string to a date. The first argument is the date layout and
|
||||
the second the date string. If the string can't be convert it returns the zero
|
||||
value.
|
||||
`mustToDate` will return an error in case the string cannot be converted.
|
||||
|
||||
This is useful when you want to convert a string date to another format
|
||||
(using pipe). The example below converts "2017-12-31" to "31/12/2017".
|
||||
|
||||
```
|
||||
toDate "2006-01-02" "2017-12-31" | date "02/01/2006"
|
||||
```
|
||||
169
docs/sprig/defaults.md
Normal file
169
docs/sprig/defaults.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Default Functions
|
||||
|
||||
Sprig provides tools for setting default values for templates.
|
||||
|
||||
## default
|
||||
|
||||
To set a simple default value, use `default`:
|
||||
|
||||
```
|
||||
default "foo" .Bar
|
||||
```
|
||||
|
||||
In the above, if `.Bar` evaluates to a non-empty value, it will be used. But if
|
||||
it is empty, `foo` will be returned instead.
|
||||
|
||||
The definition of "empty" depends on type:
|
||||
|
||||
- Numeric: 0
|
||||
- String: ""
|
||||
- Lists: `[]`
|
||||
- Dicts: `{}`
|
||||
- Boolean: `false`
|
||||
- And always `nil` (aka null)
|
||||
|
||||
For structs, there is no definition of empty, so a struct will never return the
|
||||
default.
|
||||
|
||||
## empty
|
||||
|
||||
The `empty` function returns `true` if the given value is considered empty, and
|
||||
`false` otherwise. The empty values are listed in the `default` section.
|
||||
|
||||
```
|
||||
empty .Foo
|
||||
```
|
||||
|
||||
Note that in Go template conditionals, emptiness is calculated for you. Thus,
|
||||
you rarely need `if empty .Foo`. Instead, just use `if .Foo`.
|
||||
|
||||
## coalesce
|
||||
|
||||
The `coalesce` function takes a list of values and returns the first non-empty
|
||||
one.
|
||||
|
||||
```
|
||||
coalesce 0 1 2
|
||||
```
|
||||
|
||||
The above returns `1`.
|
||||
|
||||
This function is useful for scanning through multiple variables or values:
|
||||
|
||||
```
|
||||
coalesce .name .parent.name "Matt"
|
||||
```
|
||||
|
||||
The above will first check to see if `.name` is empty. If it is not, it will return
|
||||
that value. If it _is_ empty, `coalesce` will evaluate `.parent.name` for emptiness.
|
||||
Finally, if both `.name` and `.parent.name` are empty, it will return `Matt`.
|
||||
|
||||
## all
|
||||
|
||||
The `all` function takes a list of values and returns true if all values are non-empty.
|
||||
|
||||
```
|
||||
all 0 1 2
|
||||
```
|
||||
|
||||
The above returns `false`.
|
||||
|
||||
This function is useful for evaluating multiple conditions of variables or values:
|
||||
|
||||
```
|
||||
all (eq .Request.TLS.Version 0x0304) (.Request.ProtoAtLeast 2 0) (eq .Request.Method "POST")
|
||||
```
|
||||
|
||||
The above will check http.Request is POST with tls 1.3 and http/2.
|
||||
|
||||
## any
|
||||
|
||||
The `any` function takes a list of values and returns true if any value is non-empty.
|
||||
|
||||
```
|
||||
any 0 1 2
|
||||
```
|
||||
|
||||
The above returns `true`.
|
||||
|
||||
This function is useful for evaluating multiple conditions of variables or values:
|
||||
|
||||
```
|
||||
any (eq .Request.Method "GET") (eq .Request.Method "POST") (eq .Request.Method "OPTIONS")
|
||||
```
|
||||
|
||||
The above will check http.Request method is one of GET/POST/OPTIONS.
|
||||
|
||||
## fromJSON, mustFromJSON
|
||||
|
||||
`fromJSON` decodes a JSON document into a structure. If the input cannot be decoded as JSON the function will return an empty string.
|
||||
`mustFromJSON` will return an error in case the JSON is invalid.
|
||||
|
||||
```
|
||||
fromJSON "{\"foo\": 55}"
|
||||
```
|
||||
|
||||
## toJSON, mustToJSON
|
||||
|
||||
The `toJSON` function encodes an item into a JSON string. If the item cannot be converted to JSON the function will return an empty string.
|
||||
`mustToJSON` will return an error in case the item cannot be encoded in JSON.
|
||||
|
||||
```
|
||||
toJSON .Item
|
||||
```
|
||||
|
||||
The above returns JSON string representation of `.Item`.
|
||||
|
||||
## toPrettyJSON, mustToPrettyJSON
|
||||
|
||||
The `toPrettyJSON` function encodes an item into a pretty (indented) JSON string.
|
||||
|
||||
```
|
||||
toPrettyJSON .Item
|
||||
```
|
||||
|
||||
The above returns indented JSON string representation of `.Item`.
|
||||
|
||||
## toRawJSON, mustToRawJSON
|
||||
|
||||
The `toRawJSON` function encodes an item into JSON string with HTML characters unescaped.
|
||||
|
||||
```
|
||||
toRawJSON .Item
|
||||
```
|
||||
|
||||
The above returns unescaped JSON string representation of `.Item`.
|
||||
|
||||
## ternary
|
||||
|
||||
The `ternary` function takes two values, and a test value. If the test value is
|
||||
true, the first value will be returned. If the test value is empty, the second
|
||||
value will be returned. This is similar to the c ternary operator.
|
||||
|
||||
### true test value
|
||||
|
||||
```
|
||||
ternary "foo" "bar" true
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
true | ternary "foo" "bar"
|
||||
```
|
||||
|
||||
The above returns `"foo"`.
|
||||
|
||||
### false test value
|
||||
|
||||
```
|
||||
ternary "foo" "bar" false
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
false | ternary "foo" "bar"
|
||||
```
|
||||
|
||||
The above returns `"bar"`.
|
||||
172
docs/sprig/dicts.md
Normal file
172
docs/sprig/dicts.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Dictionaries and Dict Functions
|
||||
|
||||
Sprig provides a key/value storage type called a `dict` (short for "dictionary",
|
||||
as in Python). A `dict` is an _unorder_ type.
|
||||
|
||||
The key to a dictionary **must be a string**. However, the value can be any
|
||||
type, even another `dict` or `list`.
|
||||
|
||||
Unlike `list`s, `dict`s are not immutable. The `set` and `unset` functions will
|
||||
modify the contents of a dictionary.
|
||||
|
||||
## dict
|
||||
|
||||
Creating dictionaries is done by calling the `dict` function and passing it a
|
||||
list of pairs.
|
||||
|
||||
The following creates a dictionary with three items:
|
||||
|
||||
```
|
||||
$myDict := dict "name1" "value1" "name2" "value2" "name3" "value 3"
|
||||
```
|
||||
|
||||
## get
|
||||
|
||||
Given a map and a key, get the value from the map.
|
||||
|
||||
```
|
||||
get $myDict "name1"
|
||||
```
|
||||
|
||||
The above returns `"value1"`
|
||||
|
||||
Note that if the key is not found, this operation will simply return `""`. No error
|
||||
will be generated.
|
||||
|
||||
## set
|
||||
|
||||
Use `set` to add a new key/value pair to a dictionary.
|
||||
|
||||
```
|
||||
$_ := set $myDict "name4" "value4"
|
||||
```
|
||||
|
||||
Note that `set` _returns the dictionary_ (a requirement of Go template functions),
|
||||
so you may need to trap the value as done above with the `$_` assignment.
|
||||
|
||||
## unset
|
||||
|
||||
Given a map and a key, delete the key from the map.
|
||||
|
||||
```
|
||||
$_ := unset $myDict "name4"
|
||||
```
|
||||
|
||||
As with `set`, this returns the dictionary.
|
||||
|
||||
Note that if the key is not found, this operation will simply return. No error
|
||||
will be generated.
|
||||
|
||||
## hasKey
|
||||
|
||||
The `hasKey` function returns `true` if the given dict contains the given key.
|
||||
|
||||
```
|
||||
hasKey $myDict "name1"
|
||||
```
|
||||
|
||||
If the key is not found, this returns `false`.
|
||||
|
||||
## pluck
|
||||
|
||||
The `pluck` function makes it possible to give one key and multiple maps, and
|
||||
get a list of all of the matches:
|
||||
|
||||
```
|
||||
pluck "name1" $myDict $myOtherDict
|
||||
```
|
||||
|
||||
The above will return a `list` containing every found value (`[value1 otherValue1]`).
|
||||
|
||||
If the give key is _not found_ in a map, that map will not have an item in the
|
||||
list (and the length of the returned list will be less than the number of dicts
|
||||
in the call to `pluck`.
|
||||
|
||||
If the key is _found_ but the value is an empty value, that value will be
|
||||
inserted.
|
||||
|
||||
A common idiom in Sprig templates is to uses `pluck... | first` to get the first
|
||||
matching key out of a collection of dictionaries.
|
||||
|
||||
## dig
|
||||
|
||||
The `dig` function traverses a nested set of dicts, selecting keys from a list
|
||||
of values. It returns a default value if any of the keys are not found at the
|
||||
associated dict.
|
||||
|
||||
```
|
||||
dig "user" "role" "humanName" "guest" $dict
|
||||
```
|
||||
|
||||
Given a dict structured like
|
||||
```
|
||||
{
|
||||
user: {
|
||||
role: {
|
||||
humanName: "curator"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
the above would return `"curator"`. If the dict lacked even a `user` field,
|
||||
the result would be `"guest"`.
|
||||
|
||||
Dig can be very useful in cases where you'd like to avoid guard clauses,
|
||||
especially since Go's template package's `and` doesn't shortcut. For instance
|
||||
`and a.maybeNil a.maybeNil.iNeedThis` will always evaluate
|
||||
`a.maybeNil.iNeedThis`, and panic if `a` lacks a `maybeNil` field.)
|
||||
|
||||
`dig` accepts its dict argument last in order to support pipelining.
|
||||
|
||||
## keys
|
||||
|
||||
The `keys` function will return a `list` of all of the keys in one or more `dict`
|
||||
types. Since a dictionary is _unordered_, the keys will not be in a predictable order.
|
||||
They can be sorted with `sortAlpha`.
|
||||
|
||||
```
|
||||
keys $myDict | sortAlpha
|
||||
```
|
||||
|
||||
When supplying multiple dictionaries, the keys will be concatenated. Use the `uniq`
|
||||
function along with `sortAlpha` to get a unqiue, sorted list of keys.
|
||||
|
||||
```
|
||||
keys $myDict $myOtherDict | uniq | sortAlpha
|
||||
```
|
||||
|
||||
## pick
|
||||
|
||||
The `pick` function selects just the given keys out of a dictionary, creating a
|
||||
new `dict`.
|
||||
|
||||
```
|
||||
$new := pick $myDict "name1" "name2"
|
||||
```
|
||||
|
||||
The above returns `{name1: value1, name2: value2}`
|
||||
|
||||
## omit
|
||||
|
||||
The `omit` function is similar to `pick`, except it returns a new `dict` with all
|
||||
the keys that _do not_ match the given keys.
|
||||
|
||||
```
|
||||
$new := omit $myDict "name1" "name3"
|
||||
```
|
||||
|
||||
The above returns `{name2: value2}`
|
||||
|
||||
## values
|
||||
|
||||
The `values` function is similar to `keys`, except it returns a new `list` with
|
||||
all the values of the source `dict` (only one dictionary is supported).
|
||||
|
||||
```
|
||||
$vals := values $myDict
|
||||
```
|
||||
|
||||
The above returns `list["value1", "value2", "value 3"]`. Note that the `values`
|
||||
function gives no guarantees about the result ordering- if you care about this,
|
||||
then use `sortAlpha`.
|
||||
6
docs/sprig/encoding.md
Normal file
6
docs/sprig/encoding.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Encoding Functions
|
||||
|
||||
Sprig has the following encoding and decoding functions:
|
||||
|
||||
- `b64enc`/`b64dec`: Encode or decode with Base64
|
||||
- `b32enc`/`b32dec`: Encode or decode with Base32
|
||||
11
docs/sprig/flow_control.md
Normal file
11
docs/sprig/flow_control.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Flow Control Functions
|
||||
|
||||
## fail
|
||||
|
||||
Unconditionally returns an empty `string` and an `error` with the specified
|
||||
text. This is useful in scenarios where other conditionals have determined that
|
||||
template rendering should fail.
|
||||
|
||||
```
|
||||
fail "Please accept the end user license agreement"
|
||||
```
|
||||
41
docs/sprig/integer_slice.md
Normal file
41
docs/sprig/integer_slice.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Integer List Functions
|
||||
|
||||
## until
|
||||
|
||||
The `until` function builds a range of integers.
|
||||
|
||||
```
|
||||
until 5
|
||||
```
|
||||
|
||||
The above generates the list `[0, 1, 2, 3, 4]`.
|
||||
|
||||
This is useful for looping with `range $i, $e := until 5`.
|
||||
|
||||
## untilStep
|
||||
|
||||
Like `until`, `untilStep` generates a list of counting integers. But it allows
|
||||
you to define a start, stop, and step:
|
||||
|
||||
```
|
||||
untilStep 3 6 2
|
||||
```
|
||||
|
||||
The above will produce `[3 5]` by starting with 3, and adding 2 until it is equal
|
||||
or greater than 6. This is similar to Python's `range` function.
|
||||
|
||||
## seq
|
||||
|
||||
Works like the bash `seq` command.
|
||||
* 1 parameter (end) - will generate all counting integers between 1 and `end` inclusive.
|
||||
* 2 parameters (start, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by 1.
|
||||
* 3 parameters (start, step, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by `step`.
|
||||
|
||||
```
|
||||
seq 5 => 1 2 3 4 5
|
||||
seq -3 => 1 0 -1 -2 -3
|
||||
seq 0 2 => 0 1 2
|
||||
seq 2 -2 => 2 1 0 -1 -2
|
||||
seq 0 2 10 => 0 2 4 6 8 10
|
||||
seq 0 -2 -5 => 0 -2 -4
|
||||
```
|
||||
188
docs/sprig/lists.md
Normal file
188
docs/sprig/lists.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Lists and List Functions
|
||||
|
||||
Sprig provides a simple `list` type that can contain arbitrary sequential lists
|
||||
of data. This is similar to arrays or slices, but lists are designed to be used
|
||||
as immutable data types.
|
||||
|
||||
Create a list of integers:
|
||||
|
||||
```
|
||||
$myList := list 1 2 3 4 5
|
||||
```
|
||||
|
||||
The above creates a list of `[1 2 3 4 5]`.
|
||||
|
||||
## first, mustFirst
|
||||
|
||||
To get the head item on a list, use `first`.
|
||||
|
||||
`first $myList` returns `1`
|
||||
|
||||
`first` panics if there is a problem while `mustFirst` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## rest, mustRest
|
||||
|
||||
To get the tail of the list (everything but the first item), use `rest`.
|
||||
|
||||
`rest $myList` returns `[2 3 4 5]`
|
||||
|
||||
`rest` panics if there is a problem while `mustRest` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## last, mustLast
|
||||
|
||||
To get the last item on a list, use `last`:
|
||||
|
||||
`last $myList` returns `5`. This is roughly analogous to reversing a list and
|
||||
then calling `first`.
|
||||
|
||||
`last` panics if there is a problem while `mustLast` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## initial, mustInitial
|
||||
|
||||
This compliments `last` by returning all _but_ the last element.
|
||||
`initial $myList` returns `[1 2 3 4]`.
|
||||
|
||||
`initial` panics if there is a problem while `mustInitial` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## append, mustAppend
|
||||
|
||||
Append a new item to an existing list, creating a new list.
|
||||
|
||||
```
|
||||
$new = append $myList 6
|
||||
```
|
||||
|
||||
The above would set `$new` to `[1 2 3 4 5 6]`. `$myList` would remain unaltered.
|
||||
|
||||
`append` panics if there is a problem while `mustAppend` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## prepend, mustPrepend
|
||||
|
||||
Push an element onto the front of a list, creating a new list.
|
||||
|
||||
```
|
||||
prepend $myList 0
|
||||
```
|
||||
|
||||
The above would produce `[0 1 2 3 4 5]`. `$myList` would remain unaltered.
|
||||
|
||||
`prepend` panics if there is a problem while `mustPrepend` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## concat
|
||||
|
||||
Concatenate arbitrary number of lists into one.
|
||||
|
||||
```
|
||||
concat $myList ( list 6 7 ) ( list 8 )
|
||||
```
|
||||
|
||||
The above would produce `[1 2 3 4 5 6 7 8]`. `$myList` would remain unaltered.
|
||||
|
||||
## reverse, mustReverse
|
||||
|
||||
Produce a new list with the reversed elements of the given list.
|
||||
|
||||
```
|
||||
reverse $myList
|
||||
```
|
||||
|
||||
The above would generate the list `[5 4 3 2 1]`.
|
||||
|
||||
`reverse` panics if there is a problem while `mustReverse` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## uniq, mustUniq
|
||||
|
||||
Generate a list with all of the duplicates removed.
|
||||
|
||||
```
|
||||
list 1 1 1 2 | uniq
|
||||
```
|
||||
|
||||
The above would produce `[1 2]`
|
||||
|
||||
`uniq` panics if there is a problem while `mustUniq` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## without, mustWithout
|
||||
|
||||
The `without` function filters items out of a list.
|
||||
|
||||
```
|
||||
without $myList 3
|
||||
```
|
||||
|
||||
The above would produce `[1 2 4 5]`
|
||||
|
||||
Without can take more than one filter:
|
||||
|
||||
```
|
||||
without $myList 1 3 5
|
||||
```
|
||||
|
||||
That would produce `[2 4]`
|
||||
|
||||
`without` panics if there is a problem while `mustWithout` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## has, mustHas
|
||||
|
||||
Test to see if a list has a particular element.
|
||||
|
||||
```
|
||||
has 4 $myList
|
||||
```
|
||||
|
||||
The above would return `true`, while `has "hello" $myList` would return false.
|
||||
|
||||
`has` panics if there is a problem while `mustHas` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## compact, mustCompact
|
||||
|
||||
Accepts a list and removes entries with empty values.
|
||||
|
||||
```
|
||||
$list := list 1 "a" "foo" ""
|
||||
$copy := compact $list
|
||||
```
|
||||
|
||||
`compact` will return a new list with the empty (i.e., "") item removed.
|
||||
|
||||
`compact` panics if there is a problem and `mustCompact` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## slice, mustSlice
|
||||
|
||||
To get partial elements of a list, use `slice list [n] [m]`. It is
|
||||
equivalent of `list[n:m]`.
|
||||
|
||||
- `slice $myList` returns `[1 2 3 4 5]`. It is same as `myList[:]`.
|
||||
- `slice $myList 3` returns `[4 5]`. It is same as `myList[3:]`.
|
||||
- `slice $myList 1 3` returns `[2 3]`. It is same as `myList[1:3]`.
|
||||
- `slice $myList 0 3` returns `[1 2 3]`. It is same as `myList[:3]`.
|
||||
|
||||
`slice` panics if there is a problem while `mustSlice` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## chunk
|
||||
|
||||
To split a list into chunks of given size, use `chunk size list`. This is useful for pagination.
|
||||
|
||||
```
|
||||
chunk 3 (list 1 2 3 4 5 6 7 8)
|
||||
```
|
||||
|
||||
This produces list of lists `[ [ 1 2 3 ] [ 4 5 6 ] [ 7 8 ] ]`.
|
||||
|
||||
## A Note on List Internals
|
||||
|
||||
A list is implemented in Go as a `[]interface{}`. For Go developers embedding
|
||||
Sprig, you may pass `[]interface{}` items into your template context and be
|
||||
able to use all of the `list` functions on those items.
|
||||
78
docs/sprig/math.md
Normal file
78
docs/sprig/math.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Integer Math Functions
|
||||
|
||||
The following math functions operate on `int64` values.
|
||||
|
||||
## add
|
||||
|
||||
Sum numbers with `add`. Accepts two or more inputs.
|
||||
|
||||
```
|
||||
add 1 2 3
|
||||
```
|
||||
|
||||
## add1
|
||||
|
||||
To increment by 1, use `add1`
|
||||
|
||||
## sub
|
||||
|
||||
To subtract, use `sub`
|
||||
|
||||
## div
|
||||
|
||||
Perform integer division with `div`
|
||||
|
||||
## mod
|
||||
|
||||
Modulo with `mod`
|
||||
|
||||
## mul
|
||||
|
||||
Multiply with `mul`. Accepts two or more inputs.
|
||||
|
||||
```
|
||||
mul 1 2 3
|
||||
```
|
||||
|
||||
## max
|
||||
|
||||
Return the largest of a series of integers:
|
||||
|
||||
This will return `3`:
|
||||
|
||||
```
|
||||
max 1 2 3
|
||||
```
|
||||
|
||||
## min
|
||||
|
||||
Return the smallest of a series of integers.
|
||||
|
||||
`min 1 2 3` will return `1`
|
||||
|
||||
## floor
|
||||
|
||||
Returns the greatest float value less than or equal to input value
|
||||
|
||||
`floor 123.9999` will return `123.0`
|
||||
|
||||
## ceil
|
||||
|
||||
Returns the greatest float value greater than or equal to input value
|
||||
|
||||
`ceil 123.001` will return `124.0`
|
||||
|
||||
## round
|
||||
|
||||
Returns a float value with the remainder rounded to the given number to digits after the decimal point.
|
||||
|
||||
`round 123.555555 3` will return `123.556`
|
||||
|
||||
## randInt
|
||||
Returns a random integer value from min (inclusive) to max (exclusive).
|
||||
|
||||
```
|
||||
randInt 12 30
|
||||
```
|
||||
|
||||
The above will produce a random number in the range [12,30].
|
||||
24
docs/sprig/os.md
Normal file
24
docs/sprig/os.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# OS Functions
|
||||
|
||||
_WARNING:_ These functions can lead to information leakage if not used
|
||||
appropriately.
|
||||
|
||||
_WARNING:_ Some notable implementations of Sprig (such as
|
||||
[Kubernetes Helm](http://helm.sh)) _do not provide these functions for security
|
||||
reasons_.
|
||||
|
||||
## env
|
||||
|
||||
The `env` function reads an environment variable:
|
||||
|
||||
```
|
||||
env "HOME"
|
||||
```
|
||||
|
||||
## expandenv
|
||||
|
||||
To substitute environment variables in a string, use `expandenv`:
|
||||
|
||||
```
|
||||
expandenv "Your path is set to $PATH"
|
||||
```
|
||||
114
docs/sprig/paths.md
Normal file
114
docs/sprig/paths.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Path and Filepath Functions
|
||||
|
||||
While Sprig does not grant access to the filesystem, it does provide functions
|
||||
for working with strings that follow file path conventions.
|
||||
|
||||
## Paths
|
||||
|
||||
Paths separated by the slash character (`/`), processed by the `path` package.
|
||||
|
||||
Examples:
|
||||
|
||||
* The [Linux](https://en.wikipedia.org/wiki/Linux) and
|
||||
[MacOS](https://en.wikipedia.org/wiki/MacOS)
|
||||
[filesystems](https://en.wikipedia.org/wiki/File_system):
|
||||
`/home/user/file`, `/etc/config`;
|
||||
* The path component of
|
||||
[URIs](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier):
|
||||
`https://example.com/some/content/`, `ftp://example.com/file/`.
|
||||
|
||||
### base
|
||||
|
||||
Return the last element of a path.
|
||||
|
||||
```
|
||||
base "foo/bar/baz"
|
||||
```
|
||||
|
||||
The above prints "baz".
|
||||
|
||||
### dir
|
||||
|
||||
Return the directory, stripping the last part of the path. So `dir "foo/bar/baz"`
|
||||
returns `foo/bar`.
|
||||
|
||||
### clean
|
||||
|
||||
Clean up a path.
|
||||
|
||||
```
|
||||
clean "foo/bar/../baz"
|
||||
```
|
||||
|
||||
The above resolves the `..` and returns `foo/baz`.
|
||||
|
||||
### ext
|
||||
|
||||
Return the file extension.
|
||||
|
||||
```
|
||||
ext "foo.bar"
|
||||
```
|
||||
|
||||
The above returns `.bar`.
|
||||
|
||||
### isAbs
|
||||
|
||||
To check whether a path is absolute, use `isAbs`.
|
||||
|
||||
## Filepaths
|
||||
|
||||
Paths separated by the `os.PathSeparator` variable, processed by the `path/filepath` package.
|
||||
|
||||
These are the recommended functions to use when parsing paths of local filesystems, usually when dealing with local files, directories, etc.
|
||||
|
||||
Examples:
|
||||
|
||||
* Running on Linux or MacOS the filesystem path is separated by the slash character (`/`):
|
||||
`/home/user/file`, `/etc/config`;
|
||||
* Running on [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows)
|
||||
the filesystem path is separated by the backslash character (`\`):
|
||||
`C:\Users\Username\`, `C:\Program Files\Application\`;
|
||||
|
||||
### osBase
|
||||
|
||||
Return the last element of a filepath.
|
||||
|
||||
```
|
||||
osBase "/foo/bar/baz"
|
||||
osBase "C:\\foo\\bar\\baz"
|
||||
```
|
||||
|
||||
The above prints "baz" on Linux and Windows, respectively.
|
||||
|
||||
### osDir
|
||||
|
||||
Return the directory, stripping the last part of the path. So `osDir "/foo/bar/baz"`
|
||||
returns `/foo/bar` on Linux, and `osDir "C:\\foo\\bar\\baz"`
|
||||
returns `C:\\foo\\bar` on Windows.
|
||||
|
||||
### osClean
|
||||
|
||||
Clean up a path.
|
||||
|
||||
```
|
||||
osClean "/foo/bar/../baz"
|
||||
osClean "C:\\foo\\bar\\..\\baz"
|
||||
```
|
||||
|
||||
The above resolves the `..` and returns `foo/baz` on Linux and `C:\\foo\\baz` on Windows.
|
||||
|
||||
### osExt
|
||||
|
||||
Return the file extension.
|
||||
|
||||
```
|
||||
osExt "/foo.bar"
|
||||
osExt "C:\\foo.bar"
|
||||
```
|
||||
|
||||
The above returns `.bar` on Linux and Windows, respectively.
|
||||
|
||||
### osIsAbs
|
||||
|
||||
To check whether a file path is absolute, use `osIsAbs`.
|
||||
50
docs/sprig/reflection.md
Normal file
50
docs/sprig/reflection.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Reflection Functions
|
||||
|
||||
Sprig provides rudimentary reflection tools. These help advanced template
|
||||
developers understand the underlying Go type information for a particular value.
|
||||
|
||||
Go has several primitive _kinds_, like `string`, `slice`, `int64`, and `bool`.
|
||||
|
||||
Go has an open _type_ system that allows developers to create their own types.
|
||||
|
||||
Sprig provides a set of functions for each.
|
||||
|
||||
## Kind Functions
|
||||
|
||||
There are two Kind functions: `kindOf` returns the kind of an object.
|
||||
|
||||
```
|
||||
kindOf "hello"
|
||||
```
|
||||
|
||||
The above would return `string`. For simple tests (like in `if` blocks), the
|
||||
`kindIs` function will let you verify that a value is a particular kind:
|
||||
|
||||
```
|
||||
kindIs "int" 123
|
||||
```
|
||||
|
||||
The above will return `true`
|
||||
|
||||
## Type Functions
|
||||
|
||||
Types are slightly harder to work with, so there are three different functions:
|
||||
|
||||
- `typeOf` returns the underlying type of a value: `typeOf $foo`
|
||||
- `typeIs` is like `kindIs`, but for types: `typeIs "*io.Buffer" $myVal`
|
||||
- `typeIsLike` works as `typeIs`, except that it also dereferences pointers.
|
||||
|
||||
**Note:** None of these can test whether or not something implements a given
|
||||
interface, since doing so would require compiling the interface in ahead of time.
|
||||
|
||||
## deepEqual
|
||||
|
||||
`deepEqual` returns true if two values are ["deeply equal"](https://golang.org/pkg/reflect/#DeepEqual)
|
||||
|
||||
Works for non-primitive types as well (compared to the built-in `eq`).
|
||||
|
||||
```
|
||||
deepEqual (list 1 2 3) (list 1 2 3)
|
||||
```
|
||||
|
||||
The above will return `true`
|
||||
151
docs/sprig/semver.md
Normal file
151
docs/sprig/semver.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Semantic Version Functions
|
||||
|
||||
Some version schemes are easily parseable and comparable. Sprig provides functions
|
||||
for working with [SemVer 2](http://semver.org) versions.
|
||||
|
||||
## semver
|
||||
|
||||
The `semver` function parses a string into a Semantic Version:
|
||||
|
||||
```
|
||||
$version := semver "1.2.3-alpha.1+123"
|
||||
```
|
||||
|
||||
_If the parser fails, it will cause template execution to halt with an error._
|
||||
|
||||
At this point, `$version` is a pointer to a `Version` object with the following
|
||||
properties:
|
||||
|
||||
- `$version.Major`: The major number (`1` above)
|
||||
- `$version.Minor`: The minor number (`2` above)
|
||||
- `$version.Patch`: The patch number (`3` above)
|
||||
- `$version.Prerelease`: The prerelease (`alpha.1` above)
|
||||
- `$version.Metadata`: The build metadata (`123` above)
|
||||
- `$version.Original`: The original version as a string
|
||||
|
||||
Additionally, you can compare a `Version` to another `version` using the `Compare`
|
||||
function:
|
||||
|
||||
```
|
||||
semver "1.4.3" | (semver "1.2.3").Compare
|
||||
```
|
||||
|
||||
The above will return `-1`.
|
||||
|
||||
The return values are:
|
||||
|
||||
- `-1` if the given semver is greater than the semver whose `Compare` method was called
|
||||
- `1` if the version who's `Compare` function was called is greater.
|
||||
- `0` if they are the same version
|
||||
|
||||
(Note that in SemVer, the `Metadata` field is not compared during version
|
||||
comparison operations.)
|
||||
|
||||
## semverCompare
|
||||
|
||||
A more robust comparison function is provided as `semverCompare`. It returns `true` if
|
||||
the constraint matches, or `false` if it does not match. This version supports version ranges:
|
||||
|
||||
- `semverCompare "1.2.3" "1.2.3"` checks for an exact match
|
||||
- `semverCompare "^1.2.0" "1.2.3"` checks that the major and minor versions match, and that the patch
|
||||
number of the second version is _greater than or equal to_ the first parameter.
|
||||
|
||||
The SemVer functions use the [Masterminds semver library](https://github.com/Masterminds/semver),
|
||||
from the creators of Sprig.
|
||||
|
||||
## Basic Comparisons
|
||||
|
||||
There are two elements to the comparisons. First, a comparison string is a list
|
||||
of space or comma separated AND comparisons. These are then separated by || (OR)
|
||||
comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a
|
||||
comparison that's greater than or equal to 1.2 and less than 3.0.0 or is
|
||||
greater than or equal to 4.2.3.
|
||||
|
||||
The basic comparisons are:
|
||||
|
||||
- `=`: equal (aliased to no operator)
|
||||
- `!=`: not equal
|
||||
- `>`: greater than
|
||||
- `<`: less than
|
||||
- `>=`: greater than or equal to
|
||||
- `<=`: less than or equal to
|
||||
|
||||
_Note, according to the Semantic Version specification pre-releases may not be
|
||||
API compliant with their release counterpart. It says,_
|
||||
|
||||
## Working With Prerelease Versions
|
||||
|
||||
Pre-releases, for those not familiar with them, are used for software releases
|
||||
prior to stable or generally available releases. Examples of prereleases include
|
||||
development, alpha, beta, and release candidate releases. A prerelease may be
|
||||
a version such as `1.2.3-beta.1` while the stable release would be `1.2.3`. In the
|
||||
order of precedence, prereleases come before their associated releases. In this
|
||||
example `1.2.3-beta.1 < 1.2.3`.
|
||||
|
||||
According to the Semantic Version specification prereleases may not be
|
||||
API compliant with their release counterpart. It says,
|
||||
|
||||
> A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version.
|
||||
|
||||
SemVer comparisons using constraints without a prerelease comparator will skip
|
||||
prerelease versions. For example, `>=1.2.3` will skip prereleases when looking
|
||||
at a list of releases while `>=1.2.3-0` will evaluate and find prereleases.
|
||||
|
||||
The reason for the `0` as a pre-release version in the example comparison is
|
||||
because pre-releases can only contain ASCII alphanumerics and hyphens (along with
|
||||
`.` separators), per the spec. Sorting happens in ASCII sort order, again per the
|
||||
spec. The lowest character is a `0` in ASCII sort order
|
||||
(see an [ASCII Table](http://www.asciitable.com/))
|
||||
|
||||
Understanding ASCII sort ordering is important because A-Z comes before a-z. That
|
||||
means `>=1.2.3-BETA` will return `1.2.3-alpha`. What you might expect from case
|
||||
sensitivity doesn't apply here. This is due to ASCII sort ordering which is what
|
||||
the spec specifies.
|
||||
|
||||
## Hyphen Range Comparisons
|
||||
|
||||
There are multiple methods to handle ranges and the first is hyphens ranges.
|
||||
These look like:
|
||||
|
||||
- `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5`
|
||||
- `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5`
|
||||
|
||||
## Wildcards In Comparisons
|
||||
|
||||
The `x`, `X`, and `*` characters can be used as a wildcard character. This works
|
||||
for all comparison operators. When used on the `=` operator it falls
|
||||
back to the patch level comparison (see tilde below). For example,
|
||||
|
||||
- `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
|
||||
- `>= 1.2.x` is equivalent to `>= 1.2.0`
|
||||
- `<= 2.x` is equivalent to `< 3`
|
||||
- `*` is equivalent to `>= 0.0.0`
|
||||
|
||||
## Tilde Range Comparisons (Patch)
|
||||
|
||||
The tilde (`~`) comparison operator is for patch level ranges when a minor
|
||||
version is specified and major level changes when the minor number is missing.
|
||||
For example,
|
||||
|
||||
- `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0`
|
||||
- `~1` is equivalent to `>= 1, < 2`
|
||||
- `~2.3` is equivalent to `>= 2.3, < 2.4`
|
||||
- `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
|
||||
- `~1.x` is equivalent to `>= 1, < 2`
|
||||
|
||||
## Caret Range Comparisons (Major)
|
||||
|
||||
The caret (`^`) comparison operator is for major level changes once a stable
|
||||
(1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts
|
||||
as the API stability level. This is useful when comparisons of API versions as a
|
||||
major change is API breaking. For example,
|
||||
|
||||
- `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0`
|
||||
- `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0`
|
||||
- `^2.3` is equivalent to `>= 2.3, < 3`
|
||||
- `^2.x` is equivalent to `>= 2.0.0, < 3`
|
||||
- `^0.2.3` is equivalent to `>=0.2.3 <0.3.0`
|
||||
- `^0.2` is equivalent to `>=0.2.0 <0.3.0`
|
||||
- `^0.0.3` is equivalent to `>=0.0.3 <0.0.4`
|
||||
- `^0.0` is equivalent to `>=0.0.0 <0.1.0`
|
||||
- `^0` is equivalent to `>=0.0.0 <1.0.0`
|
||||
72
docs/sprig/string_slice.md
Normal file
72
docs/sprig/string_slice.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# String List Functions
|
||||
|
||||
These function operate on or generate slices of strings. In Go, a slice is a
|
||||
growable array. In Sprig, it's a special case of a `list`.
|
||||
|
||||
## join
|
||||
|
||||
Join a list of strings into a single string, with the given separator.
|
||||
|
||||
```
|
||||
list "hello" "world" | join "_"
|
||||
```
|
||||
|
||||
The above will produce `hello_world`
|
||||
|
||||
`join` will try to convert non-strings to a string value:
|
||||
|
||||
```
|
||||
list 1 2 3 | join "+"
|
||||
```
|
||||
|
||||
The above will produce `1+2+3`
|
||||
|
||||
## splitList and split
|
||||
|
||||
Split a string into a list of strings:
|
||||
|
||||
```
|
||||
splitList "$" "foo$bar$baz"
|
||||
```
|
||||
|
||||
The above will return `[foo bar baz]`
|
||||
|
||||
The older `split` function splits a string into a `dict`. It is designed to make
|
||||
it easy to use template dot notation for accessing members:
|
||||
|
||||
```
|
||||
$a := split "$" "foo$bar$baz"
|
||||
```
|
||||
|
||||
The above produces a map with index keys. `{_0: foo, _1: bar, _2: baz}`
|
||||
|
||||
```
|
||||
$a._0
|
||||
```
|
||||
|
||||
The above produces `foo`
|
||||
|
||||
## splitn
|
||||
|
||||
`splitn` function splits a string into a `dict` with `n` keys. It is designed to make
|
||||
it easy to use template dot notation for accessing members:
|
||||
|
||||
```
|
||||
$a := splitn "$" 2 "foo$bar$baz"
|
||||
```
|
||||
|
||||
The above produces a map with index keys. `{_0: foo, _1: bar$baz}`
|
||||
|
||||
```
|
||||
$a._0
|
||||
```
|
||||
|
||||
The above produces `foo`
|
||||
|
||||
## sortAlpha
|
||||
|
||||
The `sortAlpha` function sorts a list of strings into alphabetical (lexicographical)
|
||||
order.
|
||||
|
||||
It does _not_ sort in place, but returns a sorted copy of the list, in keeping
|
||||
with the immutability of lists.
|
||||
309
docs/sprig/strings.md
Normal file
309
docs/sprig/strings.md
Normal file
@@ -0,0 +1,309 @@
|
||||
# String Functions
|
||||
|
||||
Sprig has a number of string manipulation functions.
|
||||
|
||||
## trim
|
||||
|
||||
The `trim` function removes space from either side of a string:
|
||||
|
||||
```
|
||||
trim " hello "
|
||||
```
|
||||
|
||||
The above produces `hello`
|
||||
|
||||
## trimAll
|
||||
|
||||
Remove given characters from the front or back of a string:
|
||||
|
||||
```
|
||||
trimAll "$" "$5.00"
|
||||
```
|
||||
|
||||
The above returns `5.00` (as a string).
|
||||
|
||||
## trimSuffix
|
||||
|
||||
Trim just the suffix from a string:
|
||||
|
||||
```
|
||||
trimSuffix "-" "hello-"
|
||||
```
|
||||
|
||||
The above returns `hello`
|
||||
|
||||
## trimPrefix
|
||||
|
||||
Trim just the prefix from a string:
|
||||
|
||||
```
|
||||
trimPrefix "-" "-hello"
|
||||
```
|
||||
|
||||
The above returns `hello`
|
||||
|
||||
## upper
|
||||
|
||||
Convert the entire string to uppercase:
|
||||
|
||||
```
|
||||
upper "hello"
|
||||
```
|
||||
|
||||
The above returns `HELLO`
|
||||
|
||||
## lower
|
||||
|
||||
Convert the entire string to lowercase:
|
||||
|
||||
```
|
||||
lower "HELLO"
|
||||
```
|
||||
|
||||
The above returns `hello`
|
||||
|
||||
## title
|
||||
|
||||
Convert to title case:
|
||||
|
||||
```
|
||||
title "hello world"
|
||||
```
|
||||
|
||||
The above returns `Hello World`
|
||||
|
||||
## repeat
|
||||
|
||||
Repeat a string multiple times:
|
||||
|
||||
```
|
||||
repeat 3 "hello"
|
||||
```
|
||||
|
||||
The above returns `hellohellohello`
|
||||
|
||||
## substr
|
||||
|
||||
Get a substring from a string. It takes three parameters:
|
||||
|
||||
- start (int)
|
||||
- end (int)
|
||||
- string (string)
|
||||
|
||||
```
|
||||
substr 0 5 "hello world"
|
||||
```
|
||||
|
||||
The above returns `hello`
|
||||
|
||||
## trunc
|
||||
|
||||
Truncate a string (and add no suffix)
|
||||
|
||||
```
|
||||
trunc 5 "hello world"
|
||||
```
|
||||
|
||||
The above produces `hello`.
|
||||
|
||||
```
|
||||
trunc -5 "hello world"
|
||||
```
|
||||
|
||||
The above produces `world`.
|
||||
|
||||
## contains
|
||||
|
||||
Test to see if one string is contained inside of another:
|
||||
|
||||
```
|
||||
contains "cat" "catch"
|
||||
```
|
||||
|
||||
The above returns `true` because `catch` contains `cat`.
|
||||
|
||||
## hasPrefix and hasSuffix
|
||||
|
||||
The `hasPrefix` and `hasSuffix` functions test whether a string has a given
|
||||
prefix or suffix:
|
||||
|
||||
```
|
||||
hasPrefix "cat" "catch"
|
||||
```
|
||||
|
||||
The above returns `true` because `catch` has the prefix `cat`.
|
||||
|
||||
## quote and squote
|
||||
|
||||
These functions wrap a string in double quotes (`quote`) or single quotes
|
||||
(`squote`).
|
||||
|
||||
## cat
|
||||
|
||||
The `cat` function concatenates multiple strings together into one, separating
|
||||
them with spaces:
|
||||
|
||||
```
|
||||
cat "hello" "beautiful" "world"
|
||||
```
|
||||
|
||||
The above produces `hello beautiful world`
|
||||
|
||||
## indent
|
||||
|
||||
The `indent` function indents every line in a given string to the specified
|
||||
indent width. This is useful when aligning multi-line strings:
|
||||
|
||||
```
|
||||
indent 4 $lots_of_text
|
||||
```
|
||||
|
||||
The above will indent every line of text by 4 space characters.
|
||||
|
||||
## nindent
|
||||
|
||||
The `nindent` function is the same as the indent function, but prepends a new
|
||||
line to the beginning of the string.
|
||||
|
||||
```
|
||||
nindent 4 $lots_of_text
|
||||
```
|
||||
|
||||
The above will indent every line of text by 4 space characters and add a new
|
||||
line to the beginning.
|
||||
|
||||
## replace
|
||||
|
||||
Perform simple string replacement.
|
||||
|
||||
It takes three arguments:
|
||||
|
||||
- string to replace
|
||||
- string to replace with
|
||||
- source string
|
||||
|
||||
```
|
||||
"I Am Henry VIII" | replace " " "-"
|
||||
```
|
||||
|
||||
The above will produce `I-Am-Henry-VIII`
|
||||
|
||||
## plural
|
||||
|
||||
Pluralize a string.
|
||||
|
||||
```
|
||||
len $fish | plural "one anchovy" "many anchovies"
|
||||
```
|
||||
|
||||
In the above, if the length of the string is 1, the first argument will be
|
||||
printed (`one anchovy`). Otherwise, the second argument will be printed
|
||||
(`many anchovies`).
|
||||
|
||||
The arguments are:
|
||||
|
||||
- singular string
|
||||
- plural string
|
||||
- length integer
|
||||
|
||||
NOTE: Sprig does not currently support languages with more complex pluralization
|
||||
rules. And `0` is considered a plural because the English language treats it
|
||||
as such (`zero anchovies`). The Sprig developers are working on a solution for
|
||||
better internationalization.
|
||||
|
||||
## regexMatch, mustRegexMatch
|
||||
|
||||
Returns true if the input string contains any match of the regular expression.
|
||||
|
||||
```
|
||||
regexMatch "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" "test@acme.com"
|
||||
```
|
||||
|
||||
The above produces `true`
|
||||
|
||||
`regexMatch` panics if there is a problem and `mustRegexMatch` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## regexFindAll, mustRegexFindAll
|
||||
|
||||
Returns a slice of all matches of the regular expression in the input string.
|
||||
The last parameter n determines the number of substrings to return, where -1 means return all matches
|
||||
|
||||
```
|
||||
regexFindAll "[2,4,6,8]" "123456789" -1
|
||||
```
|
||||
|
||||
The above produces `[2 4 6 8]`
|
||||
|
||||
`regexFindAll` panics if there is a problem and `mustRegexFindAll` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## regexFind, mustRegexFind
|
||||
|
||||
Return the first (left most) match of the regular expression in the input string
|
||||
|
||||
```
|
||||
regexFind "[a-zA-Z][1-9]" "abcd1234"
|
||||
```
|
||||
|
||||
The above produces `d1`
|
||||
|
||||
`regexFind` panics if there is a problem and `mustRegexFind` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## regexReplaceAll, mustRegexReplaceAll
|
||||
|
||||
Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement.
|
||||
Inside string replacement, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first submatch
|
||||
|
||||
```
|
||||
regexReplaceAll "a(x*)b" "-ab-axxb-" "${1}W"
|
||||
```
|
||||
|
||||
The above produces `-W-xxW-`
|
||||
|
||||
`regexReplaceAll` panics if there is a problem and `mustRegexReplaceAll` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## regexReplaceAllLiteral, mustRegexReplaceAllLiteral
|
||||
|
||||
Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement
|
||||
The replacement string is substituted directly, without using Expand
|
||||
|
||||
```
|
||||
regexReplaceAllLiteral "a(x*)b" "-ab-axxb-" "${1}"
|
||||
```
|
||||
|
||||
The above produces `-${1}-${1}-`
|
||||
|
||||
`regexReplaceAllLiteral` panics if there is a problem and `mustRegexReplaceAllLiteral` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## regexSplit, mustRegexSplit
|
||||
|
||||
Slices the input string into substrings separated by the expression and returns a slice of the substrings between those expression matches. The last parameter `n` determines the number of substrings to return, where `-1` means return all matches
|
||||
|
||||
```
|
||||
regexSplit "z+" "pizza" -1
|
||||
```
|
||||
|
||||
The above produces `[pi a]`
|
||||
|
||||
`regexSplit` panics if there is a problem and `mustRegexSplit` returns an error to the
|
||||
template engine if there is a problem.
|
||||
|
||||
## regexQuoteMeta
|
||||
|
||||
Returns a string that escapes all regular expression metacharacters inside the argument text;
|
||||
the returned string is a regular expression matching the literal text.
|
||||
|
||||
```
|
||||
regexQuoteMeta "1.2.3"
|
||||
```
|
||||
|
||||
The above produces `1\.2\.3`
|
||||
|
||||
## See Also...
|
||||
|
||||
The [Conversion Functions](conversion.md) contain functions for converting strings. The [String List Functions](string_slice.md) contains
|
||||
functions for working with an array of strings.
|
||||
33
docs/sprig/url.md
Normal file
33
docs/sprig/url.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# URL Functions
|
||||
|
||||
## urlParse
|
||||
Parses string for URL and produces dict with URL parts
|
||||
|
||||
```
|
||||
urlParse "http://admin:secret@server.com:8080/api?list=false#anchor"
|
||||
```
|
||||
|
||||
The above returns a dict, containing URL object:
|
||||
```yaml
|
||||
scheme: 'http'
|
||||
host: 'server.com:8080'
|
||||
path: '/api'
|
||||
query: 'list=false'
|
||||
opaque: nil
|
||||
fragment: 'anchor'
|
||||
userinfo: 'admin:secret'
|
||||
```
|
||||
|
||||
For more info, check https://golang.org/pkg/net/url/#URL
|
||||
|
||||
## urlJoin
|
||||
Joins map (produced by `urlParse`) to produce URL string
|
||||
|
||||
```
|
||||
urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "http")
|
||||
```
|
||||
|
||||
The above returns the following string:
|
||||
```
|
||||
proto://host:80/path?query#fragment
|
||||
```
|
||||
9
docs/sprig/uuid.md
Normal file
9
docs/sprig/uuid.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# UUID Functions
|
||||
|
||||
Sprig can generate UUID v4 universally unique IDs.
|
||||
|
||||
```
|
||||
uuidv4
|
||||
```
|
||||
|
||||
The above returns a new UUID of the v4 (randomly generated) type.
|
||||
@@ -94,6 +94,7 @@ nav:
|
||||
- "Integrations + projects": integrations.md
|
||||
- "Release notes": releases.md
|
||||
- "Emojis 🥳 🎉": emojis.md
|
||||
- "Template Functions": sprig.md
|
||||
- "Troubleshooting": troubleshooting.md
|
||||
- "Known issues": known-issues.md
|
||||
- "Deprecation notices": deprecations.md
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"heckel.io/ntfy/v2/util/sprig"
|
||||
)
|
||||
|
||||
// Server is the main server, providing the UI and API for ntfy
|
||||
@@ -1132,7 +1133,11 @@ func replaceTemplate(tpl string, source string) (string, error) {
|
||||
if err := json.Unmarshal([]byte(source), &data); err != nil {
|
||||
return "", errHTTPBadRequestTemplateMessageNotJSON
|
||||
}
|
||||
t, err := template.New("").Parse(tpl)
|
||||
sprigFuncs := sprig.FuncMap()
|
||||
// remove unsafe functions
|
||||
delete(sprigFuncs, "env")
|
||||
delete(sprigFuncs, "expandenv")
|
||||
t, err := template.New("").Funcs(sprigFuncs).Parse(tpl)
|
||||
if err != nil {
|
||||
return "", errHTTPBadRequestTemplateInvalid
|
||||
}
|
||||
|
||||
@@ -3024,6 +3024,51 @@ template ""}}`,
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_SprigFunctions(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
bodies := []string{
|
||||
`{"foo":"bar","nested":{"title":"here"}}`,
|
||||
`{"topic":"ntfy-test"}`,
|
||||
`{"topic":"another-topic"}`,
|
||||
}
|
||||
templates := []string{
|
||||
`{{.foo | upper}} is {{.nested.title | repeat 3}}`,
|
||||
`{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`,
|
||||
`{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`,
|
||||
}
|
||||
targets := []string{
|
||||
`BAR is hereherehere`,
|
||||
`Topic: test`,
|
||||
`Topic: another-topic`,
|
||||
}
|
||||
for i, body := range bodies {
|
||||
template := templates[i]
|
||||
target := targets[i]
|
||||
t.Run(template, func(t *testing.T) {
|
||||
response := request(t, s, "PUT", `/mytopic`, body, map[string]string{
|
||||
"Template": "yes",
|
||||
"Message": template,
|
||||
})
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, target, m.Message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_UnsafeSprigFunctions(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{
|
||||
"X-Message": `{{ env "PATH" }}`,
|
||||
"X-Template": "1",
|
||||
})
|
||||
|
||||
require.Equal(t, 400, response.Code)
|
||||
require.Equal(t, 40043, toHTTPError(t, response.Body.String()).Code)
|
||||
}
|
||||
|
||||
func newTestConfig(t *testing.T) *Config {
|
||||
conf := NewConfig()
|
||||
conf.BaseURL = "http://127.0.0.1:12345"
|
||||
|
||||
19
util/sprig/LICENSE.txt
Normal file
19
util/sprig/LICENSE.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (C) 2013-2020 Masterminds
|
||||
|
||||
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.
|
||||
37
util/sprig/crypto.go
Normal file
37
util/sprig/crypto.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash/adler32"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func sha512sum(input string) string {
|
||||
hash := sha512.Sum512([]byte(input))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func sha256sum(input string) string {
|
||||
hash := sha256.Sum256([]byte(input))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func sha1sum(input string) string {
|
||||
hash := sha1.Sum([]byte(input))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func adler32sum(input string) string {
|
||||
hash := adler32.Checksum([]byte(input))
|
||||
return fmt.Sprintf("%d", hash)
|
||||
}
|
||||
|
||||
// uuidv4 provides a safe and secure UUID v4 implementation
|
||||
func uuidv4() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
54
util/sprig/crypto_test.go
Normal file
54
util/sprig/crypto_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSha512Sum(t *testing.T) {
|
||||
tpl := `{{"abc" | sha512sum}}`
|
||||
if err := runt(tpl, "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSha256Sum(t *testing.T) {
|
||||
tpl := `{{"abc" | sha256sum}}`
|
||||
if err := runt(tpl, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSha1Sum(t *testing.T) {
|
||||
tpl := `{{"abc" | sha1sum}}`
|
||||
if err := runt(tpl, "a9993e364706816aba3e25717850c26c9cd0d89d"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdler32Sum(t *testing.T) {
|
||||
tpl := `{{"abc" | adler32sum}}`
|
||||
if err := runt(tpl, "38600999"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUUIDGeneration(t *testing.T) {
|
||||
tpl := `{{uuidv4}}`
|
||||
out, err := runRaw(tpl, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if len(out) != 36 {
|
||||
t.Error("Expected UUID of length 36")
|
||||
}
|
||||
|
||||
out2, err := runRaw(tpl, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if out == out2 {
|
||||
t.Error("Expected subsequent UUID generations to be different")
|
||||
}
|
||||
}
|
||||
152
util/sprig/date.go
Normal file
152
util/sprig/date.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Given a format and a date, format the date string.
|
||||
//
|
||||
// Date can be a `time.Time` or an `int, int32, int64`.
|
||||
// In the later case, it is treated as seconds since UNIX
|
||||
// epoch.
|
||||
func date(fmt string, date interface{}) string {
|
||||
return dateInZone(fmt, date, "Local")
|
||||
}
|
||||
|
||||
func htmlDate(date interface{}) string {
|
||||
return dateInZone("2006-01-02", date, "Local")
|
||||
}
|
||||
|
||||
func htmlDateInZone(date interface{}, zone string) string {
|
||||
return dateInZone("2006-01-02", date, zone)
|
||||
}
|
||||
|
||||
func dateInZone(fmt string, date interface{}, zone string) string {
|
||||
var t time.Time
|
||||
switch date := date.(type) {
|
||||
default:
|
||||
t = time.Now()
|
||||
case time.Time:
|
||||
t = date
|
||||
case *time.Time:
|
||||
t = *date
|
||||
case int64:
|
||||
t = time.Unix(date, 0)
|
||||
case int:
|
||||
t = time.Unix(int64(date), 0)
|
||||
case int32:
|
||||
t = time.Unix(int64(date), 0)
|
||||
}
|
||||
|
||||
loc, err := time.LoadLocation(zone)
|
||||
if err != nil {
|
||||
loc, _ = time.LoadLocation("UTC")
|
||||
}
|
||||
|
||||
return t.In(loc).Format(fmt)
|
||||
}
|
||||
|
||||
func dateModify(fmt string, date time.Time) time.Time {
|
||||
d, err := time.ParseDuration(fmt)
|
||||
if err != nil {
|
||||
return date
|
||||
}
|
||||
return date.Add(d)
|
||||
}
|
||||
|
||||
func mustDateModify(fmt string, date time.Time) (time.Time, error) {
|
||||
d, err := time.ParseDuration(fmt)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return date.Add(d), nil
|
||||
}
|
||||
|
||||
func dateAgo(date interface{}) string {
|
||||
var t time.Time
|
||||
|
||||
switch date := date.(type) {
|
||||
default:
|
||||
t = time.Now()
|
||||
case time.Time:
|
||||
t = date
|
||||
case int64:
|
||||
t = time.Unix(date, 0)
|
||||
case int:
|
||||
t = time.Unix(int64(date), 0)
|
||||
}
|
||||
// Drop resolution to seconds
|
||||
duration := time.Since(t).Round(time.Second)
|
||||
return duration.String()
|
||||
}
|
||||
|
||||
func duration(sec interface{}) string {
|
||||
var n int64
|
||||
switch value := sec.(type) {
|
||||
default:
|
||||
n = 0
|
||||
case string:
|
||||
n, _ = strconv.ParseInt(value, 10, 64)
|
||||
case int64:
|
||||
n = value
|
||||
}
|
||||
return (time.Duration(n) * time.Second).String()
|
||||
}
|
||||
|
||||
func durationRound(duration interface{}) string {
|
||||
var d time.Duration
|
||||
switch duration := duration.(type) {
|
||||
default:
|
||||
d = 0
|
||||
case string:
|
||||
d, _ = time.ParseDuration(duration)
|
||||
case int64:
|
||||
d = time.Duration(duration)
|
||||
case time.Time:
|
||||
d = time.Since(duration)
|
||||
}
|
||||
|
||||
u := uint64(d)
|
||||
neg := d < 0
|
||||
if neg {
|
||||
u = -u
|
||||
}
|
||||
|
||||
var (
|
||||
year = uint64(time.Hour) * 24 * 365
|
||||
month = uint64(time.Hour) * 24 * 30
|
||||
day = uint64(time.Hour) * 24
|
||||
hour = uint64(time.Hour)
|
||||
minute = uint64(time.Minute)
|
||||
second = uint64(time.Second)
|
||||
)
|
||||
switch {
|
||||
case u > year:
|
||||
return strconv.FormatUint(u/year, 10) + "y"
|
||||
case u > month:
|
||||
return strconv.FormatUint(u/month, 10) + "mo"
|
||||
case u > day:
|
||||
return strconv.FormatUint(u/day, 10) + "d"
|
||||
case u > hour:
|
||||
return strconv.FormatUint(u/hour, 10) + "h"
|
||||
case u > minute:
|
||||
return strconv.FormatUint(u/minute, 10) + "m"
|
||||
case u > second:
|
||||
return strconv.FormatUint(u/second, 10) + "s"
|
||||
}
|
||||
return "0s"
|
||||
}
|
||||
|
||||
func toDate(fmt, str string) time.Time {
|
||||
t, _ := time.ParseInLocation(fmt, str, time.Local)
|
||||
return t
|
||||
}
|
||||
|
||||
func mustToDate(fmt, str string) (time.Time, error) {
|
||||
return time.ParseInLocation(fmt, str, time.Local)
|
||||
}
|
||||
|
||||
func unixEpoch(date time.Time) string {
|
||||
return strconv.FormatInt(date.Unix(), 10)
|
||||
}
|
||||
120
util/sprig/date_test.go
Normal file
120
util/sprig/date_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestHtmlDate(t *testing.T) {
|
||||
t.Skip()
|
||||
tpl := `{{ htmlDate 0}}`
|
||||
if err := runt(tpl, "1970-01-01"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgo(t *testing.T) {
|
||||
tpl := "{{ ago .Time }}"
|
||||
if err := runtv(tpl, "2m5s", map[string]interface{}{"Time": time.Now().Add(-125 * time.Second)}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := runtv(tpl, "2h34m17s", map[string]interface{}{"Time": time.Now().Add(-(2*3600 + 34*60 + 17) * time.Second)}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := runtv(tpl, "-5s", map[string]interface{}{"Time": time.Now().Add(5 * time.Second)}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToDate(t *testing.T) {
|
||||
tpl := `{{toDate "2006-01-02" "2017-12-31" | date "02/01/2006"}}`
|
||||
if err := runt(tpl, "31/12/2017"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnixEpoch(t *testing.T) {
|
||||
tm, err := time.Parse("02 Jan 06 15:04:05 MST", "13 Jun 19 20:39:39 GMT")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl := `{{unixEpoch .Time}}`
|
||||
|
||||
if err = runtv(tpl, "1560458379", map[string]interface{}{"Time": tm}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateInZone(t *testing.T) {
|
||||
tm, err := time.Parse("02 Jan 06 15:04:05 MST", "13 Jun 19 20:39:39 GMT")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl := `{{ date_in_zone "02 Jan 06 15:04 -0700" .Time "UTC" }}`
|
||||
|
||||
// Test time.Time input
|
||||
if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": tm}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test pointer to time.Time input
|
||||
if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": &tm}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test no time input. This should be close enough to time.Now() we can test
|
||||
loc, _ := time.LoadLocation("UTC")
|
||||
if err = runtv(tpl, time.Now().In(loc).Format("02 Jan 06 15:04 -0700"), map[string]interface{}{"Time": ""}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test unix timestamp as int64
|
||||
if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int64(1560458379)}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test unix timestamp as int32
|
||||
if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int32(1560458379)}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test unix timestamp as int
|
||||
if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int(1560458379)}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test case of invalid timezone
|
||||
tpl = `{{ date_in_zone "02 Jan 06 15:04 -0700" .Time "foobar" }}`
|
||||
if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": tm}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuration(t *testing.T) {
|
||||
tpl := "{{ duration .Secs }}"
|
||||
if err := runtv(tpl, "1m1s", map[string]interface{}{"Secs": "61"}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := runtv(tpl, "1h0m0s", map[string]interface{}{"Secs": "3600"}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// 1d2h3m4s but go is opinionated
|
||||
if err := runtv(tpl, "26h3m4s", map[string]interface{}{"Secs": "93784"}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDurationRound(t *testing.T) {
|
||||
tpl := "{{ durationRound .Time }}"
|
||||
if err := runtv(tpl, "2h", map[string]interface{}{"Time": "2h5s"}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := runtv(tpl, "1d", map[string]interface{}{"Time": "24h5s"}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := runtv(tpl, "3mo", map[string]interface{}{"Time": "2400h5s"}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
163
util/sprig/defaults.go
Normal file
163
util/sprig/defaults.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// dfault checks whether `given` is set, and returns default if not set.
|
||||
//
|
||||
// This returns `d` if `given` appears not to be set, and `given` otherwise.
|
||||
//
|
||||
// For numeric types 0 is unset.
|
||||
// For strings, maps, arrays, and slices, len() = 0 is considered unset.
|
||||
// For bool, false is unset.
|
||||
// Structs are never considered unset.
|
||||
//
|
||||
// For everything else, including pointers, a nil value is unset.
|
||||
func dfault(d interface{}, given ...interface{}) interface{} {
|
||||
|
||||
if empty(given) || empty(given[0]) {
|
||||
return d
|
||||
}
|
||||
return given[0]
|
||||
}
|
||||
|
||||
// empty returns true if the given value has the zero value for its type.
|
||||
func empty(given interface{}) bool {
|
||||
g := reflect.ValueOf(given)
|
||||
if !g.IsValid() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Basically adapted from text/template.isTrue
|
||||
switch g.Kind() {
|
||||
default:
|
||||
return g.IsNil()
|
||||
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
||||
return g.Len() == 0
|
||||
case reflect.Bool:
|
||||
return !g.Bool()
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return g.Complex() == 0
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return g.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return g.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return g.Float() == 0
|
||||
case reflect.Struct:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// coalesce returns the first non-empty value.
|
||||
func coalesce(v ...interface{}) interface{} {
|
||||
for _, val := range v {
|
||||
if !empty(val) {
|
||||
return val
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// all returns true if empty(x) is false for all values x in the list.
|
||||
// If the list is empty, return true.
|
||||
func all(v ...interface{}) bool {
|
||||
for _, val := range v {
|
||||
if empty(val) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// any returns true if empty(x) is false for any x in the list.
|
||||
// If the list is empty, return false.
|
||||
func any(v ...interface{}) bool {
|
||||
for _, val := range v {
|
||||
if !empty(val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// fromJSON decodes JSON into a structured value, ignoring errors.
|
||||
func fromJSON(v string) interface{} {
|
||||
output, _ := mustFromJSON(v)
|
||||
return output
|
||||
}
|
||||
|
||||
// mustFromJSON decodes JSON into a structured value, returning errors.
|
||||
func mustFromJSON(v string) (interface{}, error) {
|
||||
var output interface{}
|
||||
err := json.Unmarshal([]byte(v), &output)
|
||||
return output, err
|
||||
}
|
||||
|
||||
// toJSON encodes an item into a JSON string
|
||||
func toJSON(v interface{}) string {
|
||||
output, _ := json.Marshal(v)
|
||||
return string(output)
|
||||
}
|
||||
|
||||
func mustToJSON(v interface{}) (string, error) {
|
||||
output, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// toPrettyJSON encodes an item into a pretty (indented) JSON string
|
||||
func toPrettyJSON(v interface{}) string {
|
||||
output, _ := json.MarshalIndent(v, "", " ")
|
||||
return string(output)
|
||||
}
|
||||
|
||||
func mustToPrettyJSON(v interface{}) (string, error) {
|
||||
output, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// toRawJSON encodes an item into a JSON string with no escaping of HTML characters.
|
||||
func toRawJSON(v interface{}) string {
|
||||
output, err := mustToRawJSON(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(output)
|
||||
}
|
||||
|
||||
// mustToRawJSON encodes an item into a JSON string with no escaping of HTML characters.
|
||||
func mustToRawJSON(v interface{}) (string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
err := enc.Encode(&v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSuffix(buf.String(), "\n"), nil
|
||||
}
|
||||
|
||||
// ternary returns the first value if the last value is true, otherwise returns the second value.
|
||||
func ternary(vt interface{}, vf interface{}, v bool) interface{} {
|
||||
if v {
|
||||
return vt
|
||||
}
|
||||
|
||||
return vf
|
||||
}
|
||||
196
util/sprig/defaults_test.go
Normal file
196
util/sprig/defaults_test.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDefault(t *testing.T) {
|
||||
tpl := `{{"" | default "foo"}}`
|
||||
if err := runt(tpl, "foo"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{default "foo" 234}}`
|
||||
if err := runt(tpl, "234"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{default "foo" 2.34}}`
|
||||
if err := runt(tpl, "2.34"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tpl = `{{ .Nothing | default "123" }}`
|
||||
if err := runt(tpl, "123"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{ default "123" }}`
|
||||
if err := runt(tpl, "123"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmpty(t *testing.T) {
|
||||
tpl := `{{if empty 1}}1{{else}}0{{end}}`
|
||||
if err := runt(tpl, "0"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tpl = `{{if empty 0}}1{{else}}0{{end}}`
|
||||
if err := runt(tpl, "1"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{if empty ""}}1{{else}}0{{end}}`
|
||||
if err := runt(tpl, "1"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{if empty 0.0}}1{{else}}0{{end}}`
|
||||
if err := runt(tpl, "1"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{if empty false}}1{{else}}0{{end}}`
|
||||
if err := runt(tpl, "1"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
dict := map[string]interface{}{"top": map[string]interface{}{}}
|
||||
tpl = `{{if empty .top.NoSuchThing}}1{{else}}0{{end}}`
|
||||
if err := runtv(tpl, "1", dict); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{if empty .bottom.NoSuchThing}}1{{else}}0{{end}}`
|
||||
if err := runtv(tpl, "1", dict); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoalesce(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ coalesce 1 }}`: "1",
|
||||
`{{ coalesce "" 0 nil 2 }}`: "2",
|
||||
`{{ $two := 2 }}{{ coalesce "" 0 nil $two }}`: "2",
|
||||
`{{ $two := 2 }}{{ coalesce "" $two 0 0 0 }}`: "2",
|
||||
`{{ $two := 2 }}{{ coalesce "" $two 3 4 5 }}`: "2",
|
||||
`{{ coalesce }}`: "<no value>",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
|
||||
dict := map[string]interface{}{"top": map[string]interface{}{}}
|
||||
tpl := `{{ coalesce .top.NoSuchThing .bottom .bottom.dollar "airplane"}}`
|
||||
if err := runtv(tpl, "airplane", dict); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ all 1 }}`: "true",
|
||||
`{{ all "" 0 nil 2 }}`: "false",
|
||||
`{{ $two := 2 }}{{ all "" 0 nil $two }}`: "false",
|
||||
`{{ $two := 2 }}{{ all "" $two 0 0 0 }}`: "false",
|
||||
`{{ $two := 2 }}{{ all "" $two 3 4 5 }}`: "false",
|
||||
`{{ all }}`: "true",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
|
||||
dict := map[string]interface{}{"top": map[string]interface{}{}}
|
||||
tpl := `{{ all .top.NoSuchThing .bottom .bottom.dollar "airplane"}}`
|
||||
if err := runtv(tpl, "false", dict); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAny(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ any 1 }}`: "true",
|
||||
`{{ any "" 0 nil 2 }}`: "true",
|
||||
`{{ $two := 2 }}{{ any "" 0 nil $two }}`: "true",
|
||||
`{{ $two := 2 }}{{ any "" $two 3 4 5 }}`: "true",
|
||||
`{{ $zero := 0 }}{{ any "" $zero 0 0 0 }}`: "false",
|
||||
`{{ any }}`: "false",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
|
||||
dict := map[string]interface{}{"top": map[string]interface{}{}}
|
||||
tpl := `{{ any .top.NoSuchThing .bottom .bottom.dollar "airplane"}}`
|
||||
if err := runtv(tpl, "true", dict); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromJSON(t *testing.T) {
|
||||
dict := map[string]interface{}{"Input": `{"foo": 55}`}
|
||||
|
||||
tpl := `{{.Input | fromJSON}}`
|
||||
expected := `map[foo:55]`
|
||||
if err := runtv(tpl, expected, dict); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tpl = `{{(.Input | fromJSON).foo}}`
|
||||
expected = `55`
|
||||
if err := runtv(tpl, expected, dict); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToJSON(t *testing.T) {
|
||||
dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42}}
|
||||
|
||||
tpl := `{{.Top | toJSON}}`
|
||||
expected := `{"bool":true,"number":42,"string":"test"}`
|
||||
if err := runtv(tpl, expected, dict); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPrettyJSON(t *testing.T) {
|
||||
dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42}}
|
||||
tpl := `{{.Top | toPrettyJSON}}`
|
||||
expected := `{
|
||||
"bool": true,
|
||||
"number": 42,
|
||||
"string": "test"
|
||||
}`
|
||||
if err := runtv(tpl, expected, dict); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToRawJSON(t *testing.T) {
|
||||
dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42, "html": "<HEAD>"}}
|
||||
tpl := `{{.Top | toRawJSON}}`
|
||||
expected := `{"bool":true,"html":"<HEAD>","number":42,"string":"test"}`
|
||||
|
||||
if err := runtv(tpl, expected, dict); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTernary(t *testing.T) {
|
||||
tpl := `{{true | ternary "foo" "bar"}}`
|
||||
if err := runt(tpl, "foo"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tpl = `{{ternary "foo" "bar" true}}`
|
||||
if err := runt(tpl, "foo"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tpl = `{{false | ternary "foo" "bar"}}`
|
||||
if err := runt(tpl, "bar"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tpl = `{{ternary "foo" "bar" false}}`
|
||||
if err := runt(tpl, "bar"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
118
util/sprig/dict.go
Normal file
118
util/sprig/dict.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package sprig
|
||||
|
||||
func get(d map[string]interface{}, key string) interface{} {
|
||||
if val, ok := d[key]; ok {
|
||||
return val
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func set(d map[string]interface{}, key string, value interface{}) map[string]interface{} {
|
||||
d[key] = value
|
||||
return d
|
||||
}
|
||||
|
||||
func unset(d map[string]interface{}, key string) map[string]interface{} {
|
||||
delete(d, key)
|
||||
return d
|
||||
}
|
||||
|
||||
func hasKey(d map[string]interface{}, key string) bool {
|
||||
_, ok := d[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func pluck(key string, d ...map[string]interface{}) []interface{} {
|
||||
res := []interface{}{}
|
||||
for _, dict := range d {
|
||||
if val, ok := dict[key]; ok {
|
||||
res = append(res, val)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func keys(dicts ...map[string]interface{}) []string {
|
||||
k := []string{}
|
||||
for _, dict := range dicts {
|
||||
for key := range dict {
|
||||
k = append(k, key)
|
||||
}
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
func pick(dict map[string]interface{}, keys ...string) map[string]interface{} {
|
||||
res := map[string]interface{}{}
|
||||
for _, k := range keys {
|
||||
if v, ok := dict[k]; ok {
|
||||
res[k] = v
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func omit(dict map[string]interface{}, keys ...string) map[string]interface{} {
|
||||
res := map[string]interface{}{}
|
||||
|
||||
omit := make(map[string]bool, len(keys))
|
||||
for _, k := range keys {
|
||||
omit[k] = true
|
||||
}
|
||||
|
||||
for k, v := range dict {
|
||||
if _, ok := omit[k]; !ok {
|
||||
res[k] = v
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func dict(v ...interface{}) map[string]interface{} {
|
||||
dict := map[string]interface{}{}
|
||||
lenv := len(v)
|
||||
for i := 0; i < lenv; i += 2 {
|
||||
key := strval(v[i])
|
||||
if i+1 >= lenv {
|
||||
dict[key] = ""
|
||||
continue
|
||||
}
|
||||
dict[key] = v[i+1]
|
||||
}
|
||||
return dict
|
||||
}
|
||||
|
||||
func values(dict map[string]interface{}) []interface{} {
|
||||
values := []interface{}{}
|
||||
for _, value := range dict {
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func dig(ps ...interface{}) (interface{}, error) {
|
||||
if len(ps) < 3 {
|
||||
panic("dig needs at least three arguments")
|
||||
}
|
||||
dict := ps[len(ps)-1].(map[string]interface{})
|
||||
def := ps[len(ps)-2]
|
||||
ks := make([]string, len(ps)-2)
|
||||
for i := 0; i < len(ks); i++ {
|
||||
ks[i] = ps[i].(string)
|
||||
}
|
||||
|
||||
return digFromDict(dict, def, ks)
|
||||
}
|
||||
|
||||
func digFromDict(dict map[string]interface{}, d interface{}, ks []string) (interface{}, error) {
|
||||
k, ns := ks[0], ks[1:]
|
||||
step, has := dict[k]
|
||||
if !has {
|
||||
return d, nil
|
||||
}
|
||||
if len(ns) == 0 {
|
||||
return step, nil
|
||||
}
|
||||
return digFromDict(step.(map[string]interface{}), d, ns)
|
||||
}
|
||||
166
util/sprig/dict_test.go
Normal file
166
util/sprig/dict_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDict(t *testing.T) {
|
||||
tpl := `{{$d := dict 1 2 "three" "four" 5}}{{range $k, $v := $d}}{{$k}}{{$v}}{{end}}`
|
||||
out, err := runRaw(tpl, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(out) != 12 {
|
||||
t.Errorf("Expected length 12, got %d", len(out))
|
||||
}
|
||||
// dict does not guarantee ordering because it is backed by a map.
|
||||
if !strings.Contains(out, "12") {
|
||||
t.Error("Expected grouping 12")
|
||||
}
|
||||
if !strings.Contains(out, "threefour") {
|
||||
t.Error("Expected grouping threefour")
|
||||
}
|
||||
if !strings.Contains(out, "5") {
|
||||
t.Error("Expected 5")
|
||||
}
|
||||
tpl = `{{$t := dict "I" "shot" "the" "albatross"}}{{$t.the}} {{$t.I}}`
|
||||
if err := runt(tpl, "albatross shot"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnset(t *testing.T) {
|
||||
tpl := `{{- $d := dict "one" 1 "two" 222222 -}}
|
||||
{{- $_ := unset $d "two" -}}
|
||||
{{- range $k, $v := $d}}{{$k}}{{$v}}{{- end -}}
|
||||
`
|
||||
|
||||
expect := "one1"
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
func TestHasKey(t *testing.T) {
|
||||
tpl := `{{- $d := dict "one" 1 "two" 222222 -}}
|
||||
{{- if hasKey $d "one" -}}1{{- end -}}
|
||||
`
|
||||
|
||||
expect := "1"
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluck(t *testing.T) {
|
||||
tpl := `
|
||||
{{- $d := dict "one" 1 "two" 222222 -}}
|
||||
{{- $d2 := dict "one" 1 "two" 33333 -}}
|
||||
{{- $d3 := dict "one" 1 -}}
|
||||
{{- $d4 := dict "one" 1 "two" 4444 -}}
|
||||
{{- pluck "two" $d $d2 $d3 $d4 -}}
|
||||
`
|
||||
|
||||
expect := "[222222 33333 4444]"
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeys(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ dict "foo" 1 "bar" 2 | keys | sortAlpha }}`: "[bar foo]",
|
||||
`{{ dict | keys }}`: "[]",
|
||||
`{{ keys (dict "foo" 1) (dict "bar" 2) (dict "bar" 3) | uniq | sortAlpha }}`: "[bar foo]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPick(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "two" | len -}}`: "1",
|
||||
`{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "two" -}}`: "map[two:222222]",
|
||||
`{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "one" "two" | len -}}`: "2",
|
||||
`{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "one" "two" "three" | len -}}`: "2",
|
||||
`{{- $d := dict }}{{ pick $d "two" | len -}}`: "0",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestOmit(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" | len -}}`: "1",
|
||||
`{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" -}}`: "map[two:222222]",
|
||||
`{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" "two" | len -}}`: "0",
|
||||
`{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "two" "three" | len -}}`: "1",
|
||||
`{{- $d := dict }}{{ omit $d "two" | len -}}`: "0",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{- $d := dict "one" 1 }}{{ get $d "one" -}}`: "1",
|
||||
`{{- $d := dict "one" 1 "two" "2" }}{{ get $d "two" -}}`: "2",
|
||||
`{{- $d := dict }}{{ get $d "two" -}}`: "",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
tpl := `{{- $d := dict "one" 1 "two" 222222 -}}
|
||||
{{- $_ := set $d "two" 2 -}}
|
||||
{{- $_ := set $d "three" 3 -}}
|
||||
{{- if hasKey $d "one" -}}{{$d.one}}{{- end -}}
|
||||
{{- if hasKey $d "two" -}}{{$d.two}}{{- end -}}
|
||||
{{- if hasKey $d "three" -}}{{$d.three}}{{- end -}}
|
||||
`
|
||||
|
||||
expect := "123"
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValues(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{- $d := dict "a" 1 "b" 2 }}{{ values $d | sortAlpha | join "," }}`: "1,2",
|
||||
`{{- $d := dict "a" "first" "b" 2 }}{{ values $d | sortAlpha | join "," }}`: "2,first",
|
||||
}
|
||||
|
||||
for tpl, expect := range tests {
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDig(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{- $d := dict "a" (dict "b" (dict "c" 1)) }}{{ dig "a" "b" "c" "" $d }}`: "1",
|
||||
`{{- $d := dict "a" (dict "b" (dict "c" 1)) }}{{ dig "a" "b" "z" "2" $d }}`: "2",
|
||||
`{{ dict "a" 1 | dig "a" "" }}`: "1",
|
||||
`{{ dict "a" 1 | dig "z" "2" }}`: "2",
|
||||
}
|
||||
|
||||
for tpl, expect := range tests {
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
19
util/sprig/doc.go
Normal file
19
util/sprig/doc.go
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Package sprig provides template functions for Go.
|
||||
|
||||
This package contains a number of utility functions for working with data
|
||||
inside of Go `html/template` and `text/template` files.
|
||||
|
||||
To add these functions, use the `template.Funcs()` method:
|
||||
|
||||
t := template.New("foo").Funcs(sprig.FuncMap())
|
||||
|
||||
Note that you should add the function map before you parse any template files.
|
||||
|
||||
In several cases, Sprig reverses the order of arguments from the way they
|
||||
appear in the standard library. This is to make it easier to pipe
|
||||
arguments into functions.
|
||||
|
||||
See http://masterminds.github.io/sprig/ for more detailed documentation on each of the available functions.
|
||||
*/
|
||||
package sprig
|
||||
25
util/sprig/example_test.go
Normal file
25
util/sprig/example_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
// Set up variables and template.
|
||||
vars := map[string]interface{}{"Name": " John Jacob Jingleheimer Schmidt "}
|
||||
tpl := `Hello {{.Name | trim | lower}}`
|
||||
|
||||
// Get the Sprig function map.
|
||||
fmap := TxtFuncMap()
|
||||
t := template.Must(template.New("test").Funcs(fmap).Parse(tpl))
|
||||
|
||||
err := t.Execute(os.Stdout, vars)
|
||||
if err != nil {
|
||||
fmt.Printf("Error during template execution: %s", err)
|
||||
return
|
||||
}
|
||||
// Output:
|
||||
// Hello john jacob jingleheimer schmidt
|
||||
}
|
||||
16
util/sprig/flow_control_test.go
Normal file
16
util/sprig/flow_control_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFail(t *testing.T) {
|
||||
const msg = "This is an error!"
|
||||
tpl := fmt.Sprintf(`{{fail "%s"}}`, msg)
|
||||
_, err := runRaw(tpl, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), msg)
|
||||
}
|
||||
302
util/sprig/functions.go
Normal file
302
util/sprig/functions.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"math/rand"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
ttemplate "text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FuncMap produces the function map.
|
||||
//
|
||||
// Use this to pass the functions into the template engine:
|
||||
//
|
||||
// tpl := template.New("foo").Funcs(sprig.FuncMap()))
|
||||
func FuncMap() template.FuncMap {
|
||||
return HTMLFuncMap()
|
||||
}
|
||||
|
||||
// HermeticTxtFuncMap returns a 'text/template'.FuncMap with only repeatable functions.
|
||||
func HermeticTxtFuncMap() ttemplate.FuncMap {
|
||||
r := TxtFuncMap()
|
||||
for _, name := range nonhermeticFunctions {
|
||||
delete(r, name)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// HermeticHTMLFuncMap returns an 'html/template'.Funcmap with only repeatable functions.
|
||||
func HermeticHTMLFuncMap() template.FuncMap {
|
||||
r := HTMLFuncMap()
|
||||
for _, name := range nonhermeticFunctions {
|
||||
delete(r, name)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// TxtFuncMap returns a 'text/template'.FuncMap
|
||||
func TxtFuncMap() ttemplate.FuncMap {
|
||||
return ttemplate.FuncMap(GenericFuncMap())
|
||||
}
|
||||
|
||||
// HTMLFuncMap returns an 'html/template'.Funcmap
|
||||
func HTMLFuncMap() template.FuncMap {
|
||||
return template.FuncMap(GenericFuncMap())
|
||||
}
|
||||
|
||||
// GenericFuncMap returns a copy of the basic function map as a map[string]interface{}.
|
||||
func GenericFuncMap() map[string]interface{} {
|
||||
gfm := make(map[string]interface{}, len(genericMap))
|
||||
for k, v := range genericMap {
|
||||
gfm[k] = v
|
||||
}
|
||||
return gfm
|
||||
}
|
||||
|
||||
// These functions are not guaranteed to evaluate to the same result for given input, because they
|
||||
// refer to the environment or global state.
|
||||
var nonhermeticFunctions = []string{
|
||||
// Date functions
|
||||
"date",
|
||||
"date_in_zone",
|
||||
"date_modify",
|
||||
"now",
|
||||
"htmlDate",
|
||||
"htmlDateInZone",
|
||||
"dateInZone",
|
||||
"dateModify",
|
||||
|
||||
// Strings
|
||||
"randAlphaNum",
|
||||
"randAlpha",
|
||||
"randAscii",
|
||||
"randNumeric",
|
||||
"randBytes",
|
||||
"uuidv4",
|
||||
}
|
||||
|
||||
var genericMap = map[string]interface{}{
|
||||
"hello": func() string { return "Hello!" },
|
||||
|
||||
// Date functions
|
||||
"ago": dateAgo,
|
||||
"date": date,
|
||||
"date_in_zone": dateInZone,
|
||||
"date_modify": dateModify,
|
||||
"dateInZone": dateInZone,
|
||||
"dateModify": dateModify,
|
||||
"duration": duration,
|
||||
"durationRound": durationRound,
|
||||
"htmlDate": htmlDate,
|
||||
"htmlDateInZone": htmlDateInZone,
|
||||
"must_date_modify": mustDateModify,
|
||||
"mustDateModify": mustDateModify,
|
||||
"mustToDate": mustToDate,
|
||||
"now": time.Now,
|
||||
"toDate": toDate,
|
||||
"unixEpoch": unixEpoch,
|
||||
|
||||
// Strings
|
||||
"trunc": trunc,
|
||||
"trim": strings.TrimSpace,
|
||||
"upper": strings.ToUpper,
|
||||
"lower": strings.ToLower,
|
||||
"title": strings.Title,
|
||||
"substr": substring,
|
||||
// Switch order so that "foo" | repeat 5
|
||||
"repeat": func(count int, str string) string { return strings.Repeat(str, count) },
|
||||
// Deprecated: Use trimAll.
|
||||
"trimall": func(a, b string) string { return strings.Trim(b, a) },
|
||||
// Switch order so that "$foo" | trimall "$"
|
||||
"trimAll": func(a, b string) string { return strings.Trim(b, a) },
|
||||
"trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) },
|
||||
"trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) },
|
||||
// Switch order so that "foobar" | contains "foo"
|
||||
"contains": func(substr string, str string) bool { return strings.Contains(str, substr) },
|
||||
"hasPrefix": func(substr string, str string) bool { return strings.HasPrefix(str, substr) },
|
||||
"hasSuffix": func(substr string, str string) bool { return strings.HasSuffix(str, substr) },
|
||||
"quote": quote,
|
||||
"squote": squote,
|
||||
"cat": cat,
|
||||
"indent": indent,
|
||||
"nindent": nindent,
|
||||
"replace": replace,
|
||||
"plural": plural,
|
||||
"sha1sum": sha1sum,
|
||||
"sha256sum": sha256sum,
|
||||
"sha512sum": sha512sum,
|
||||
"adler32sum": adler32sum,
|
||||
"toString": strval,
|
||||
|
||||
// Wrap Atoi to stop errors.
|
||||
"atoi": func(a string) int { i, _ := strconv.Atoi(a); return i },
|
||||
"seq": seq,
|
||||
"toDecimal": toDecimal,
|
||||
|
||||
//"gt": func(a, b int) bool {return a > b},
|
||||
//"gte": func(a, b int) bool {return a >= b},
|
||||
//"lt": func(a, b int) bool {return a < b},
|
||||
//"lte": func(a, b int) bool {return a <= b},
|
||||
|
||||
// split "/" foo/bar returns map[int]string{0: foo, 1: bar}
|
||||
"split": split,
|
||||
"splitList": func(sep, orig string) []string { return strings.Split(orig, sep) },
|
||||
// splitn "/" foo/bar/fuu returns map[int]string{0: foo, 1: bar/fuu}
|
||||
"splitn": splitn,
|
||||
"toStrings": strslice,
|
||||
|
||||
"until": until,
|
||||
"untilStep": untilStep,
|
||||
|
||||
// VERY basic arithmetic.
|
||||
"add1": func(i interface{}) int64 { return toInt64(i) + 1 },
|
||||
"add": func(i ...interface{}) int64 {
|
||||
var a int64 = 0
|
||||
for _, b := range i {
|
||||
a += toInt64(b)
|
||||
}
|
||||
return a
|
||||
},
|
||||
"sub": func(a, b interface{}) int64 { return toInt64(a) - toInt64(b) },
|
||||
"div": func(a, b interface{}) int64 { return toInt64(a) / toInt64(b) },
|
||||
"mod": func(a, b interface{}) int64 { return toInt64(a) % toInt64(b) },
|
||||
"mul": func(a interface{}, v ...interface{}) int64 {
|
||||
val := toInt64(a)
|
||||
for _, b := range v {
|
||||
val = val * toInt64(b)
|
||||
}
|
||||
return val
|
||||
},
|
||||
"randInt": func(min, max int) int { return rand.Intn(max-min) + min },
|
||||
"biggest": max,
|
||||
"max": max,
|
||||
"min": min,
|
||||
"maxf": maxf,
|
||||
"minf": minf,
|
||||
"ceil": ceil,
|
||||
"floor": floor,
|
||||
"round": round,
|
||||
|
||||
// string slices. Note that we reverse the order b/c that's better
|
||||
// for template processing.
|
||||
"join": join,
|
||||
"sortAlpha": sortAlpha,
|
||||
|
||||
// Defaults
|
||||
"default": dfault,
|
||||
"empty": empty,
|
||||
"coalesce": coalesce,
|
||||
"all": all,
|
||||
"any": any,
|
||||
"compact": compact,
|
||||
"mustCompact": mustCompact,
|
||||
"fromJSON": fromJSON,
|
||||
"toJSON": toJSON,
|
||||
"toPrettyJSON": toPrettyJSON,
|
||||
"toRawJSON": toRawJSON,
|
||||
"mustFromJSON": mustFromJSON,
|
||||
"mustToJSON": mustToJSON,
|
||||
"mustToPrettyJSON": mustToPrettyJSON,
|
||||
"mustToRawJSON": mustToRawJSON,
|
||||
"ternary": ternary,
|
||||
|
||||
// Reflection
|
||||
"typeOf": typeOf,
|
||||
"typeIs": typeIs,
|
||||
"typeIsLike": typeIsLike,
|
||||
"kindOf": kindOf,
|
||||
"kindIs": kindIs,
|
||||
"deepEqual": reflect.DeepEqual,
|
||||
|
||||
// Paths:
|
||||
"base": path.Base,
|
||||
"dir": path.Dir,
|
||||
"clean": path.Clean,
|
||||
"ext": path.Ext,
|
||||
"isAbs": path.IsAbs,
|
||||
|
||||
// Filepaths:
|
||||
"osBase": filepath.Base,
|
||||
"osClean": filepath.Clean,
|
||||
"osDir": filepath.Dir,
|
||||
"osExt": filepath.Ext,
|
||||
"osIsAbs": filepath.IsAbs,
|
||||
|
||||
// Encoding:
|
||||
"b64enc": base64encode,
|
||||
"b64dec": base64decode,
|
||||
"b32enc": base32encode,
|
||||
"b32dec": base32decode,
|
||||
|
||||
// Data Structures:
|
||||
"tuple": list, // FIXME: with the addition of append/prepend these are no longer immutable.
|
||||
"list": list,
|
||||
"dict": dict,
|
||||
"get": get,
|
||||
"set": set,
|
||||
"unset": unset,
|
||||
"hasKey": hasKey,
|
||||
"pluck": pluck,
|
||||
"keys": keys,
|
||||
"pick": pick,
|
||||
"omit": omit,
|
||||
"values": values,
|
||||
|
||||
"append": push, "push": push,
|
||||
"mustAppend": mustPush, "mustPush": mustPush,
|
||||
"prepend": prepend,
|
||||
"mustPrepend": mustPrepend,
|
||||
"first": first,
|
||||
"mustFirst": mustFirst,
|
||||
"rest": rest,
|
||||
"mustRest": mustRest,
|
||||
"last": last,
|
||||
"mustLast": mustLast,
|
||||
"initial": initial,
|
||||
"mustInitial": mustInitial,
|
||||
"reverse": reverse,
|
||||
"mustReverse": mustReverse,
|
||||
"uniq": uniq,
|
||||
"mustUniq": mustUniq,
|
||||
"without": without,
|
||||
"mustWithout": mustWithout,
|
||||
"has": has,
|
||||
"mustHas": mustHas,
|
||||
"slice": slice,
|
||||
"mustSlice": mustSlice,
|
||||
"concat": concat,
|
||||
"dig": dig,
|
||||
"chunk": chunk,
|
||||
"mustChunk": mustChunk,
|
||||
|
||||
// UUIDs:
|
||||
"uuidv4": uuidv4,
|
||||
|
||||
// Flow Control:
|
||||
"fail": func(msg string) (string, error) { return "", errors.New(msg) },
|
||||
|
||||
// Regex
|
||||
"regexMatch": regexMatch,
|
||||
"mustRegexMatch": mustRegexMatch,
|
||||
"regexFindAll": regexFindAll,
|
||||
"mustRegexFindAll": mustRegexFindAll,
|
||||
"regexFind": regexFind,
|
||||
"mustRegexFind": mustRegexFind,
|
||||
"regexReplaceAll": regexReplaceAll,
|
||||
"mustRegexReplaceAll": mustRegexReplaceAll,
|
||||
"regexReplaceAllLiteral": regexReplaceAllLiteral,
|
||||
"mustRegexReplaceAllLiteral": mustRegexReplaceAllLiteral,
|
||||
"regexSplit": regexSplit,
|
||||
"mustRegexSplit": mustRegexSplit,
|
||||
"regexQuoteMeta": regexQuoteMeta,
|
||||
|
||||
// URLs:
|
||||
"urlParse": urlParse,
|
||||
"urlJoin": urlJoin,
|
||||
}
|
||||
28
util/sprig/functions_linux_test.go
Normal file
28
util/sprig/functions_linux_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOsBase(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ osBase "foo/bar" }}`, "bar"))
|
||||
}
|
||||
|
||||
func TestOsDir(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ osDir "foo/bar/baz" }}`, "foo/bar"))
|
||||
}
|
||||
|
||||
func TestOsIsAbs(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ osIsAbs "/foo" }}`, "true"))
|
||||
assert.NoError(t, runt(`{{ osIsAbs "foo" }}`, "false"))
|
||||
}
|
||||
|
||||
func TestOsClean(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ osClean "/foo/../foo/../bar" }}`, "/bar"))
|
||||
}
|
||||
|
||||
func TestOsExt(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ osExt "/foo/bar/baz.txt" }}`, ".txt"))
|
||||
}
|
||||
70
util/sprig/functions_test.go
Normal file
70
util/sprig/functions_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBase(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ base "foo/bar" }}`, "bar"))
|
||||
}
|
||||
|
||||
func TestDir(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ dir "foo/bar/baz" }}`, "foo/bar"))
|
||||
}
|
||||
|
||||
func TestIsAbs(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ isAbs "/foo" }}`, "true"))
|
||||
assert.NoError(t, runt(`{{ isAbs "foo" }}`, "false"))
|
||||
}
|
||||
|
||||
func TestClean(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ clean "/foo/../foo/../bar" }}`, "/bar"))
|
||||
}
|
||||
|
||||
func TestExt(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ ext "/foo/bar/baz.txt" }}`, ".txt"))
|
||||
}
|
||||
|
||||
func TestRegex(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ regexQuoteMeta "1.2.3" }}`, "1\\.2\\.3"))
|
||||
assert.NoError(t, runt(`{{ regexQuoteMeta "pretzel" }}`, "pretzel"))
|
||||
}
|
||||
|
||||
// runt runs a template and checks that the output exactly matches the expected string.
|
||||
func runt(tpl, expect string) error {
|
||||
return runtv(tpl, expect, map[string]string{})
|
||||
}
|
||||
|
||||
// runtv takes a template, and expected return, and values for substitution.
|
||||
//
|
||||
// It runs the template and verifies that the output is an exact match.
|
||||
func runtv(tpl, expect string, vars interface{}) error {
|
||||
fmap := TxtFuncMap()
|
||||
t := template.Must(template.New("test").Funcs(fmap).Parse(tpl))
|
||||
var b bytes.Buffer
|
||||
err := t.Execute(&b, vars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if expect != b.String() {
|
||||
return fmt.Errorf("Expected '%s', got '%s'", expect, b.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runRaw runs a template with the given variables and returns the result.
|
||||
func runRaw(tpl string, vars interface{}) (string, error) {
|
||||
fmap := TxtFuncMap()
|
||||
t := template.Must(template.New("test").Funcs(fmap).Parse(tpl))
|
||||
var b bytes.Buffer
|
||||
err := t.Execute(&b, vars)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
28
util/sprig/functions_windows_test.go
Normal file
28
util/sprig/functions_windows_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOsBase(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ osBase "C:\\foo\\bar" }}`, "bar"))
|
||||
}
|
||||
|
||||
func TestOsDir(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ osDir "C:\\foo\\bar\\baz" }}`, "C:\\foo\\bar"))
|
||||
}
|
||||
|
||||
func TestOsIsAbs(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ osIsAbs "C:\\foo" }}`, "true"))
|
||||
assert.NoError(t, runt(`{{ osIsAbs "foo" }}`, "false"))
|
||||
}
|
||||
|
||||
func TestOsClean(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ osClean "C:\\foo\\..\\foo\\..\\bar" }}`, "C:\\bar"))
|
||||
}
|
||||
|
||||
func TestOsExt(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ osExt "C:\\foo\\bar\\baz.txt" }}`, ".txt"))
|
||||
}
|
||||
464
util/sprig/list.go
Normal file
464
util/sprig/list.go
Normal file
@@ -0,0 +1,464 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Reflection is used in these functions so that slices and arrays of strings,
|
||||
// ints, and other types not implementing []interface{} can be worked with.
|
||||
// For example, this is useful if you need to work on the output of regexs.
|
||||
|
||||
func list(v ...interface{}) []interface{} {
|
||||
return v
|
||||
}
|
||||
|
||||
func push(list interface{}, v interface{}) []interface{} {
|
||||
l, err := mustPush(list, v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustPush(list interface{}, v interface{}) ([]interface{}, error) {
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
nl := make([]interface{}, l)
|
||||
for i := 0; i < l; i++ {
|
||||
nl[i] = l2.Index(i).Interface()
|
||||
}
|
||||
|
||||
return append(nl, v), nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("Cannot push on type %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
func prepend(list interface{}, v interface{}) []interface{} {
|
||||
l, err := mustPrepend(list, v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustPrepend(list interface{}, v interface{}) ([]interface{}, error) {
|
||||
//return append([]interface{}{v}, list...)
|
||||
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
nl := make([]interface{}, l)
|
||||
for i := 0; i < l; i++ {
|
||||
nl[i] = l2.Index(i).Interface()
|
||||
}
|
||||
|
||||
return append([]interface{}{v}, nl...), nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("Cannot prepend on type %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
func chunk(size int, list interface{}) [][]interface{} {
|
||||
l, err := mustChunk(size, list)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustChunk(size int, list interface{}) ([][]interface{}, error) {
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
|
||||
cs := int(math.Floor(float64(l-1)/float64(size)) + 1)
|
||||
nl := make([][]interface{}, cs)
|
||||
|
||||
for i := 0; i < cs; i++ {
|
||||
clen := size
|
||||
if i == cs-1 {
|
||||
clen = int(math.Floor(math.Mod(float64(l), float64(size))))
|
||||
if clen == 0 {
|
||||
clen = size
|
||||
}
|
||||
}
|
||||
|
||||
nl[i] = make([]interface{}, clen)
|
||||
|
||||
for j := 0; j < clen; j++ {
|
||||
ix := i*size + j
|
||||
nl[i][j] = l2.Index(ix).Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return nl, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("Cannot chunk type %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
func last(list interface{}) interface{} {
|
||||
l, err := mustLast(list)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustLast(list interface{}) (interface{}, error) {
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
if l == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return l2.Index(l - 1).Interface(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Cannot find last on type %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
func first(list interface{}) interface{} {
|
||||
l, err := mustFirst(list)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustFirst(list interface{}) (interface{}, error) {
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
if l == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return l2.Index(0).Interface(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Cannot find first on type %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
func rest(list interface{}) []interface{} {
|
||||
l, err := mustRest(list)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustRest(list interface{}) ([]interface{}, error) {
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
if l == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
nl := make([]interface{}, l-1)
|
||||
for i := 1; i < l; i++ {
|
||||
nl[i-1] = l2.Index(i).Interface()
|
||||
}
|
||||
|
||||
return nl, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Cannot find rest on type %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
func initial(list interface{}) []interface{} {
|
||||
l, err := mustInitial(list)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustInitial(list interface{}) ([]interface{}, error) {
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
if l == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
nl := make([]interface{}, l-1)
|
||||
for i := 0; i < l-1; i++ {
|
||||
nl[i] = l2.Index(i).Interface()
|
||||
}
|
||||
|
||||
return nl, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Cannot find initial on type %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
func sortAlpha(list interface{}) []string {
|
||||
k := reflect.Indirect(reflect.ValueOf(list)).Kind()
|
||||
switch k {
|
||||
case reflect.Slice, reflect.Array:
|
||||
a := strslice(list)
|
||||
s := sort.StringSlice(a)
|
||||
s.Sort()
|
||||
return s
|
||||
}
|
||||
return []string{strval(list)}
|
||||
}
|
||||
|
||||
func reverse(v interface{}) []interface{} {
|
||||
l, err := mustReverse(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustReverse(v interface{}) ([]interface{}, error) {
|
||||
tp := reflect.TypeOf(v).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(v)
|
||||
|
||||
l := l2.Len()
|
||||
// We do not sort in place because the incoming array should not be altered.
|
||||
nl := make([]interface{}, l)
|
||||
for i := 0; i < l; i++ {
|
||||
nl[l-i-1] = l2.Index(i).Interface()
|
||||
}
|
||||
|
||||
return nl, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Cannot find reverse on type %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
func compact(list interface{}) []interface{} {
|
||||
l, err := mustCompact(list)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustCompact(list interface{}) ([]interface{}, error) {
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
nl := []interface{}{}
|
||||
var item interface{}
|
||||
for i := 0; i < l; i++ {
|
||||
item = l2.Index(i).Interface()
|
||||
if !empty(item) {
|
||||
nl = append(nl, item)
|
||||
}
|
||||
}
|
||||
|
||||
return nl, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Cannot compact on type %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
func uniq(list interface{}) []interface{} {
|
||||
l, err := mustUniq(list)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustUniq(list interface{}) ([]interface{}, error) {
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
dest := []interface{}{}
|
||||
var item interface{}
|
||||
for i := 0; i < l; i++ {
|
||||
item = l2.Index(i).Interface()
|
||||
if !inList(dest, item) {
|
||||
dest = append(dest, item)
|
||||
}
|
||||
}
|
||||
|
||||
return dest, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Cannot find uniq on type %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
func inList(haystack []interface{}, needle interface{}) bool {
|
||||
for _, h := range haystack {
|
||||
if reflect.DeepEqual(needle, h) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func without(list interface{}, omit ...interface{}) []interface{} {
|
||||
l, err := mustWithout(list, omit...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustWithout(list interface{}, omit ...interface{}) ([]interface{}, error) {
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
res := []interface{}{}
|
||||
var item interface{}
|
||||
for i := 0; i < l; i++ {
|
||||
item = l2.Index(i).Interface()
|
||||
if !inList(omit, item) {
|
||||
res = append(res, item)
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Cannot find without on type %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
func has(needle interface{}, haystack interface{}) bool {
|
||||
l, err := mustHas(needle, haystack)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustHas(needle interface{}, haystack interface{}) (bool, error) {
|
||||
if haystack == nil {
|
||||
return false, nil
|
||||
}
|
||||
tp := reflect.TypeOf(haystack).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(haystack)
|
||||
var item interface{}
|
||||
l := l2.Len()
|
||||
for i := 0; i < l; i++ {
|
||||
item = l2.Index(i).Interface()
|
||||
if reflect.DeepEqual(needle, item) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("Cannot find has on type %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
// $list := [1, 2, 3, 4, 5]
|
||||
// slice $list -> list[0:5] = list[:]
|
||||
// slice $list 0 3 -> list[0:3] = list[:3]
|
||||
// slice $list 3 5 -> list[3:5]
|
||||
// slice $list 3 -> list[3:5] = list[3:]
|
||||
func slice(list interface{}, indices ...interface{}) interface{} {
|
||||
l, err := mustSlice(list, indices...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func mustSlice(list interface{}, indices ...interface{}) (interface{}, error) {
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
if l == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var start, end int
|
||||
if len(indices) > 0 {
|
||||
start = toInt(indices[0])
|
||||
}
|
||||
if len(indices) < 2 {
|
||||
end = l
|
||||
} else {
|
||||
end = toInt(indices[1])
|
||||
}
|
||||
|
||||
return l2.Slice(start, end).Interface(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("list should be type of slice or array but %s", tp)
|
||||
}
|
||||
}
|
||||
|
||||
func concat(lists ...interface{}) interface{} {
|
||||
var res []interface{}
|
||||
for _, list := range lists {
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
for i := 0; i < l2.Len(); i++ {
|
||||
res = append(res, l2.Index(i).Interface())
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("Cannot concat type %s as list", tp))
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
364
util/sprig/list_test.go
Normal file
364
util/sprig/list_test.go
Normal file
@@ -0,0 +1,364 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTuple(t *testing.T) {
|
||||
tpl := `{{$t := tuple 1 "a" "foo"}}{{index $t 2}}{{index $t 0 }}{{index $t 1}}`
|
||||
if err := runt(tpl, "foo1a"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
tpl := `{{$t := list 1 "a" "foo"}}{{index $t 2}}{{index $t 0 }}{{index $t 1}}`
|
||||
if err := runt(tpl, "foo1a"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPush(t *testing.T) {
|
||||
// Named `append` in the function map
|
||||
tests := map[string]string{
|
||||
`{{ $t := tuple 1 2 3 }}{{ append $t 4 | len }}`: "4",
|
||||
`{{ $t := tuple 1 2 3 4 }}{{ append $t 5 | join "-" }}`: "1-2-3-4-5",
|
||||
`{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ append $t "qux" | join "-" }}`: "foo-bar-baz-qux",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustPush(t *testing.T) {
|
||||
// Named `append` in the function map
|
||||
tests := map[string]string{
|
||||
`{{ $t := tuple 1 2 3 }}{{ mustAppend $t 4 | len }}`: "4",
|
||||
`{{ $t := tuple 1 2 3 4 }}{{ mustAppend $t 5 | join "-" }}`: "1-2-3-4-5",
|
||||
`{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ mustPush $t "qux" | join "-" }}`: "foo-bar-baz-qux",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunk(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ tuple 1 2 3 4 5 6 7 | chunk 3 | len }}`: "3",
|
||||
`{{ tuple | chunk 3 | len }}`: "0",
|
||||
`{{ range ( tuple 1 2 3 4 5 6 7 8 9 | chunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8-9|",
|
||||
`{{ range ( tuple 1 2 3 4 5 6 7 8 | chunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8|",
|
||||
`{{ range ( tuple 1 2 | chunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2|",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustChunk(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ tuple 1 2 3 4 5 6 7 | mustChunk 3 | len }}`: "3",
|
||||
`{{ tuple | mustChunk 3 | len }}`: "0",
|
||||
`{{ range ( tuple 1 2 3 4 5 6 7 8 9 | mustChunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8-9|",
|
||||
`{{ range ( tuple 1 2 3 4 5 6 7 8 | mustChunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8|",
|
||||
`{{ range ( tuple 1 2 | mustChunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2|",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepend(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ $t := tuple 1 2 3 }}{{ prepend $t 0 | len }}`: "4",
|
||||
`{{ $t := tuple 1 2 3 4 }}{{ prepend $t 0 | join "-" }}`: "0-1-2-3-4",
|
||||
`{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ prepend $t "qux" | join "-" }}`: "qux-foo-bar-baz",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustPrepend(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ $t := tuple 1 2 3 }}{{ mustPrepend $t 0 | len }}`: "4",
|
||||
`{{ $t := tuple 1 2 3 4 }}{{ mustPrepend $t 0 | join "-" }}`: "0-1-2-3-4",
|
||||
`{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ mustPrepend $t "qux" | join "-" }}`: "qux-foo-bar-baz",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirst(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 | first }}`: "1",
|
||||
`{{ list | first }}`: "<no value>",
|
||||
`{{ regexSplit "/src/" "foo/src/bar" -1 | first }}`: "foo",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustFirst(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 | mustFirst }}`: "1",
|
||||
`{{ list | mustFirst }}`: "<no value>",
|
||||
`{{ regexSplit "/src/" "foo/src/bar" -1 | mustFirst }}`: "foo",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLast(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 | last }}`: "3",
|
||||
`{{ list | last }}`: "<no value>",
|
||||
`{{ regexSplit "/src/" "foo/src/bar" -1 | last }}`: "bar",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustLast(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 | mustLast }}`: "3",
|
||||
`{{ list | mustLast }}`: "<no value>",
|
||||
`{{ regexSplit "/src/" "foo/src/bar" -1 | mustLast }}`: "bar",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitial(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 | initial | len }}`: "2",
|
||||
`{{ list 1 2 3 | initial | last }}`: "2",
|
||||
`{{ list 1 2 3 | initial | first }}`: "1",
|
||||
`{{ list | initial }}`: "[]",
|
||||
`{{ regexSplit "/" "foo/bar/baz" -1 | initial }}`: "[foo bar]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustInitial(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 | mustInitial | len }}`: "2",
|
||||
`{{ list 1 2 3 | mustInitial | last }}`: "2",
|
||||
`{{ list 1 2 3 | mustInitial | first }}`: "1",
|
||||
`{{ list | mustInitial }}`: "[]",
|
||||
`{{ regexSplit "/" "foo/bar/baz" -1 | mustInitial }}`: "[foo bar]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRest(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 | rest | len }}`: "2",
|
||||
`{{ list 1 2 3 | rest | last }}`: "3",
|
||||
`{{ list 1 2 3 | rest | first }}`: "2",
|
||||
`{{ list | rest }}`: "[]",
|
||||
`{{ regexSplit "/" "foo/bar/baz" -1 | rest }}`: "[bar baz]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustRest(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 | mustRest | len }}`: "2",
|
||||
`{{ list 1 2 3 | mustRest | last }}`: "3",
|
||||
`{{ list 1 2 3 | mustRest | first }}`: "2",
|
||||
`{{ list | mustRest }}`: "[]",
|
||||
`{{ regexSplit "/" "foo/bar/baz" -1 | mustRest }}`: "[bar baz]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverse(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 | reverse | first }}`: "3",
|
||||
`{{ list 1 2 3 | reverse | rest | first }}`: "2",
|
||||
`{{ list 1 2 3 | reverse | last }}`: "1",
|
||||
`{{ list 1 2 3 4 | reverse }}`: "[4 3 2 1]",
|
||||
`{{ list 1 | reverse }}`: "[1]",
|
||||
`{{ list | reverse }}`: "[]",
|
||||
`{{ regexSplit "/" "foo/bar/baz" -1 | reverse }}`: "[baz bar foo]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustReverse(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 | mustReverse | first }}`: "3",
|
||||
`{{ list 1 2 3 | mustReverse | rest | first }}`: "2",
|
||||
`{{ list 1 2 3 | mustReverse | last }}`: "1",
|
||||
`{{ list 1 2 3 4 | mustReverse }}`: "[4 3 2 1]",
|
||||
`{{ list 1 | mustReverse }}`: "[1]",
|
||||
`{{ list | mustReverse }}`: "[]",
|
||||
`{{ regexSplit "/" "foo/bar/baz" -1 | mustReverse }}`: "[baz bar foo]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompact(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 0 "" "hello" | compact }}`: `[1 hello]`,
|
||||
`{{ list "" "" | compact }}`: `[]`,
|
||||
`{{ list | compact }}`: `[]`,
|
||||
`{{ regexSplit "/" "foo//bar" -1 | compact }}`: "[foo bar]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustCompact(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 0 "" "hello" | mustCompact }}`: `[1 hello]`,
|
||||
`{{ list "" "" | mustCompact }}`: `[]`,
|
||||
`{{ list | mustCompact }}`: `[]`,
|
||||
`{{ regexSplit "/" "foo//bar" -1 | mustCompact }}`: "[foo bar]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniq(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 4 | uniq }}`: `[1 2 3 4]`,
|
||||
`{{ list "a" "b" "c" "d" | uniq }}`: `[a b c d]`,
|
||||
`{{ list 1 1 1 1 2 2 2 2 | uniq }}`: `[1 2]`,
|
||||
`{{ list "foo" 1 1 1 1 "foo" "foo" | uniq }}`: `[foo 1]`,
|
||||
`{{ list | uniq }}`: `[]`,
|
||||
`{{ regexSplit "/" "foo/foo/bar" -1 | uniq }}`: "[foo bar]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustUniq(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 4 | mustUniq }}`: `[1 2 3 4]`,
|
||||
`{{ list "a" "b" "c" "d" | mustUniq }}`: `[a b c d]`,
|
||||
`{{ list 1 1 1 1 2 2 2 2 | mustUniq }}`: `[1 2]`,
|
||||
`{{ list "foo" 1 1 1 1 "foo" "foo" | mustUniq }}`: `[foo 1]`,
|
||||
`{{ list | mustUniq }}`: `[]`,
|
||||
`{{ regexSplit "/" "foo/foo/bar" -1 | mustUniq }}`: "[foo bar]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithout(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ without (list 1 2 3 4) 1 }}`: `[2 3 4]`,
|
||||
`{{ without (list "a" "b" "c" "d") "a" }}`: `[b c d]`,
|
||||
`{{ without (list 1 1 1 1 2) 1 }}`: `[2]`,
|
||||
`{{ without (list) 1 }}`: `[]`,
|
||||
`{{ without (list 1 2 3) }}`: `[1 2 3]`,
|
||||
`{{ without list }}`: `[]`,
|
||||
`{{ without (regexSplit "/" "foo/bar/baz" -1 ) "foo" }}`: "[bar baz]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustWithout(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ mustWithout (list 1 2 3 4) 1 }}`: `[2 3 4]`,
|
||||
`{{ mustWithout (list "a" "b" "c" "d") "a" }}`: `[b c d]`,
|
||||
`{{ mustWithout (list 1 1 1 1 2) 1 }}`: `[2]`,
|
||||
`{{ mustWithout (list) 1 }}`: `[]`,
|
||||
`{{ mustWithout (list 1 2 3) }}`: `[1 2 3]`,
|
||||
`{{ mustWithout list }}`: `[]`,
|
||||
`{{ mustWithout (regexSplit "/" "foo/bar/baz" -1 ) "foo" }}`: "[bar baz]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHas(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 | has 1 }}`: `true`,
|
||||
`{{ list 1 2 3 | has 4 }}`: `false`,
|
||||
`{{ regexSplit "/" "foo/bar/baz" -1 | has "bar" }}`: `true`,
|
||||
`{{ has "bar" nil }}`: `false`,
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustHas(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ list 1 2 3 | mustHas 1 }}`: `true`,
|
||||
`{{ list 1 2 3 | mustHas 4 }}`: `false`,
|
||||
`{{ regexSplit "/" "foo/bar/baz" -1 | mustHas "bar" }}`: `true`,
|
||||
`{{ mustHas "bar" nil }}`: `false`,
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlice(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ slice (list 1 2 3) }}`: "[1 2 3]",
|
||||
`{{ slice (list 1 2 3) 0 1 }}`: "[1]",
|
||||
`{{ slice (list 1 2 3) 1 3 }}`: "[2 3]",
|
||||
`{{ slice (list 1 2 3) 1 }}`: "[2 3]",
|
||||
`{{ slice (regexSplit "/" "foo/bar/baz" -1) 1 2 }}`: "[bar]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustSlice(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ mustSlice (list 1 2 3) }}`: "[1 2 3]",
|
||||
`{{ mustSlice (list 1 2 3) 0 1 }}`: "[1]",
|
||||
`{{ mustSlice (list 1 2 3) 1 3 }}`: "[2 3]",
|
||||
`{{ mustSlice (list 1 2 3) 1 }}`: "[2 3]",
|
||||
`{{ mustSlice (regexSplit "/" "foo/bar/baz" -1) 1 2 }}`: "[bar]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcat(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ concat (list 1 2 3) }}`: "[1 2 3]",
|
||||
`{{ concat (list 1 2 3) (list 4 5) }}`: "[1 2 3 4 5]",
|
||||
`{{ concat (list 1 2 3) (list 4 5) (list) }}`: "[1 2 3 4 5]",
|
||||
`{{ concat (list 1 2 3) (list 4 5) (list nil) }}`: "[1 2 3 4 5 <nil>]",
|
||||
`{{ concat (list 1 2 3) (list 4 5) (list ( list "foo" ) ) }}`: "[1 2 3 4 5 [foo]]",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
228
util/sprig/numeric.go
Normal file
228
util/sprig/numeric.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// toFloat64 converts 64-bit floats
|
||||
func toFloat64(v interface{}) float64 {
|
||||
if str, ok := v.(string); ok {
|
||||
iv, err := strconv.ParseFloat(str, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return iv
|
||||
}
|
||||
|
||||
val := reflect.Indirect(reflect.ValueOf(v))
|
||||
switch val.Kind() {
|
||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||
return float64(val.Int())
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
||||
return float64(val.Uint())
|
||||
case reflect.Uint, reflect.Uint64:
|
||||
return float64(val.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return val.Float()
|
||||
case reflect.Bool:
|
||||
if val.Bool() {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func toInt(v interface{}) int {
|
||||
// It's not optimal. But I don't want duplicate toInt64 code.
|
||||
return int(toInt64(v))
|
||||
}
|
||||
|
||||
// toInt64 converts integer types to 64-bit integers
|
||||
func toInt64(v interface{}) int64 {
|
||||
if str, ok := v.(string); ok {
|
||||
iv, err := strconv.ParseInt(str, 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return iv
|
||||
}
|
||||
|
||||
val := reflect.Indirect(reflect.ValueOf(v))
|
||||
switch val.Kind() {
|
||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||
return val.Int()
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
||||
return int64(val.Uint())
|
||||
case reflect.Uint, reflect.Uint64:
|
||||
tv := val.Uint()
|
||||
if tv <= math.MaxInt64 {
|
||||
return int64(tv)
|
||||
}
|
||||
// TODO: What is the sensible thing to do here?
|
||||
return math.MaxInt64
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return int64(val.Float())
|
||||
case reflect.Bool:
|
||||
if val.Bool() {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func max(a interface{}, i ...interface{}) int64 {
|
||||
aa := toInt64(a)
|
||||
for _, b := range i {
|
||||
bb := toInt64(b)
|
||||
if bb > aa {
|
||||
aa = bb
|
||||
}
|
||||
}
|
||||
return aa
|
||||
}
|
||||
|
||||
func maxf(a interface{}, i ...interface{}) float64 {
|
||||
aa := toFloat64(a)
|
||||
for _, b := range i {
|
||||
bb := toFloat64(b)
|
||||
aa = math.Max(aa, bb)
|
||||
}
|
||||
return aa
|
||||
}
|
||||
|
||||
func min(a interface{}, i ...interface{}) int64 {
|
||||
aa := toInt64(a)
|
||||
for _, b := range i {
|
||||
bb := toInt64(b)
|
||||
if bb < aa {
|
||||
aa = bb
|
||||
}
|
||||
}
|
||||
return aa
|
||||
}
|
||||
|
||||
func minf(a interface{}, i ...interface{}) float64 {
|
||||
aa := toFloat64(a)
|
||||
for _, b := range i {
|
||||
bb := toFloat64(b)
|
||||
aa = math.Min(aa, bb)
|
||||
}
|
||||
return aa
|
||||
}
|
||||
|
||||
func until(count int) []int {
|
||||
step := 1
|
||||
if count < 0 {
|
||||
step = -1
|
||||
}
|
||||
return untilStep(0, count, step)
|
||||
}
|
||||
|
||||
func untilStep(start, stop, step int) []int {
|
||||
v := []int{}
|
||||
|
||||
if stop < start {
|
||||
if step >= 0 {
|
||||
return v
|
||||
}
|
||||
for i := start; i > stop; i += step {
|
||||
v = append(v, i)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
if step <= 0 {
|
||||
return v
|
||||
}
|
||||
for i := start; i < stop; i += step {
|
||||
v = append(v, i)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func floor(a interface{}) float64 {
|
||||
aa := toFloat64(a)
|
||||
return math.Floor(aa)
|
||||
}
|
||||
|
||||
func ceil(a interface{}) float64 {
|
||||
aa := toFloat64(a)
|
||||
return math.Ceil(aa)
|
||||
}
|
||||
|
||||
func round(a interface{}, p int, rOpt ...float64) float64 {
|
||||
roundOn := .5
|
||||
if len(rOpt) > 0 {
|
||||
roundOn = rOpt[0]
|
||||
}
|
||||
val := toFloat64(a)
|
||||
places := toFloat64(p)
|
||||
|
||||
var round float64
|
||||
pow := math.Pow(10, places)
|
||||
digit := pow * val
|
||||
_, div := math.Modf(digit)
|
||||
if div >= roundOn {
|
||||
round = math.Ceil(digit)
|
||||
} else {
|
||||
round = math.Floor(digit)
|
||||
}
|
||||
return round / pow
|
||||
}
|
||||
|
||||
// converts unix octal to decimal
|
||||
func toDecimal(v interface{}) int64 {
|
||||
result, err := strconv.ParseInt(fmt.Sprint(v), 8, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func seq(params ...int) string {
|
||||
increment := 1
|
||||
switch len(params) {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
start := 1
|
||||
end := params[0]
|
||||
if end < start {
|
||||
increment = -1
|
||||
}
|
||||
return intArrayToString(untilStep(start, end+increment, increment), " ")
|
||||
case 3:
|
||||
start := params[0]
|
||||
end := params[2]
|
||||
step := params[1]
|
||||
if end < start {
|
||||
increment = -1
|
||||
if step > 0 {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return intArrayToString(untilStep(start, end+increment, step), " ")
|
||||
case 2:
|
||||
start := params[0]
|
||||
end := params[1]
|
||||
step := 1
|
||||
if end < start {
|
||||
step = -1
|
||||
}
|
||||
return intArrayToString(untilStep(start, end+step, step), " ")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func intArrayToString(slice []int, delimeter string) string {
|
||||
return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimeter), "[]")
|
||||
}
|
||||
307
util/sprig/numeric_test.go
Normal file
307
util/sprig/numeric_test.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUntil(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{range $i, $e := until 5}}{{$i}}{{$e}}{{end}}`: "0011223344",
|
||||
`{{range $i, $e := until -5}}{{$i}}{{$e}} {{end}}`: "00 1-1 2-2 3-3 4-4 ",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestUntilStep(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{range $i, $e := untilStep 0 5 1}}{{$i}}{{$e}}{{end}}`: "0011223344",
|
||||
`{{range $i, $e := untilStep 3 6 1}}{{$i}}{{$e}}{{end}}`: "031425",
|
||||
`{{range $i, $e := untilStep 0 -10 -2}}{{$i}}{{$e}} {{end}}`: "00 1-2 2-4 3-6 4-8 ",
|
||||
`{{range $i, $e := untilStep 3 0 1}}{{$i}}{{$e}}{{end}}`: "",
|
||||
`{{range $i, $e := untilStep 3 99 0}}{{$i}}{{$e}}{{end}}`: "",
|
||||
`{{range $i, $e := untilStep 3 99 -1}}{{$i}}{{$e}}{{end}}`: "",
|
||||
`{{range $i, $e := untilStep 3 0 0}}{{$i}}{{$e}}{{end}}`: "",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
func TestBiggest(t *testing.T) {
|
||||
tpl := `{{ biggest 1 2 3 345 5 6 7}}`
|
||||
if err := runt(tpl, `345`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tpl = `{{ max 345}}`
|
||||
if err := runt(tpl, `345`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
func TestMaxf(t *testing.T) {
|
||||
tpl := `{{ maxf 1 2 3 345.7 5 6 7}}`
|
||||
if err := runt(tpl, `345.7`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tpl = `{{ max 345 }}`
|
||||
if err := runt(tpl, `345`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
func TestMin(t *testing.T) {
|
||||
tpl := `{{ min 1 2 3 345 5 6 7}}`
|
||||
if err := runt(tpl, `1`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tpl = `{{ min 345}}`
|
||||
if err := runt(tpl, `345`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinf(t *testing.T) {
|
||||
tpl := `{{ minf 1.4 2 3 345.6 5 6 7}}`
|
||||
if err := runt(tpl, `1.4`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tpl = `{{ minf 345 }}`
|
||||
if err := runt(tpl, `345`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToFloat64(t *testing.T) {
|
||||
target := float64(102)
|
||||
if target != toFloat64(int8(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toFloat64(int(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toFloat64(int32(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toFloat64(int16(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toFloat64(int64(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toFloat64("102") {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if 0 != toFloat64("frankie") {
|
||||
t.Errorf("Expected 0")
|
||||
}
|
||||
if target != toFloat64(uint16(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toFloat64(uint64(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if 102.1234 != toFloat64(float64(102.1234)) {
|
||||
t.Errorf("Expected 102.1234")
|
||||
}
|
||||
if 1 != toFloat64(true) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
}
|
||||
func TestToInt64(t *testing.T) {
|
||||
target := int64(102)
|
||||
if target != toInt64(int8(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt64(int(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt64(int32(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt64(int16(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt64(int64(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt64("102") {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if 0 != toInt64("frankie") {
|
||||
t.Errorf("Expected 0")
|
||||
}
|
||||
if target != toInt64(uint16(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt64(uint64(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt64(float64(102.1234)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if 1 != toInt64(true) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToInt(t *testing.T) {
|
||||
target := int(102)
|
||||
if target != toInt(int8(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt(int(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt(int32(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt(int16(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt(int64(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt("102") {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if 0 != toInt("frankie") {
|
||||
t.Errorf("Expected 0")
|
||||
}
|
||||
if target != toInt(uint16(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt(uint64(102)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if target != toInt(float64(102.1234)) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
if 1 != toInt(true) {
|
||||
t.Errorf("Expected 102")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToDecimal(t *testing.T) {
|
||||
tests := map[interface{}]int64{
|
||||
"777": 511,
|
||||
777: 511,
|
||||
770: 504,
|
||||
755: 493,
|
||||
}
|
||||
|
||||
for input, expectedResult := range tests {
|
||||
result := toDecimal(input)
|
||||
if result != expectedResult {
|
||||
t.Errorf("Expected %v but got %v", expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdd1(t *testing.T) {
|
||||
tpl := `{{ 3 | add1 }}`
|
||||
if err := runt(tpl, `4`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
tpl := `{{ 3 | add 1 2}}`
|
||||
if err := runt(tpl, `6`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiv(t *testing.T) {
|
||||
tpl := `{{ 4 | div 5 }}`
|
||||
if err := runt(tpl, `1`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMul(t *testing.T) {
|
||||
tpl := `{{ 1 | mul "2" 3 "4"}}`
|
||||
if err := runt(tpl, `24`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSub(t *testing.T) {
|
||||
tpl := `{{ 3 | sub 14 }}`
|
||||
if err := runt(tpl, `11`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCeil(t *testing.T) {
|
||||
assert.Equal(t, 123.0, ceil(123))
|
||||
assert.Equal(t, 123.0, ceil("123"))
|
||||
assert.Equal(t, 124.0, ceil(123.01))
|
||||
assert.Equal(t, 124.0, ceil("123.01"))
|
||||
}
|
||||
|
||||
func TestFloor(t *testing.T) {
|
||||
assert.Equal(t, 123.0, floor(123))
|
||||
assert.Equal(t, 123.0, floor("123"))
|
||||
assert.Equal(t, 123.0, floor(123.9999))
|
||||
assert.Equal(t, 123.0, floor("123.9999"))
|
||||
}
|
||||
|
||||
func TestRound(t *testing.T) {
|
||||
assert.Equal(t, 123.556, round(123.5555, 3))
|
||||
assert.Equal(t, 123.556, round("123.55555", 3))
|
||||
assert.Equal(t, 124.0, round(123.500001, 0))
|
||||
assert.Equal(t, 123.0, round(123.49999999, 0))
|
||||
assert.Equal(t, 123.23, round(123.2329999, 2, .3))
|
||||
assert.Equal(t, 123.24, round(123.233, 2, .3))
|
||||
}
|
||||
|
||||
func TestRandomInt(t *testing.T) {
|
||||
var tests = []struct {
|
||||
min int
|
||||
max int
|
||||
}{
|
||||
{10, 11},
|
||||
{10, 13},
|
||||
{0, 1},
|
||||
{5, 50},
|
||||
}
|
||||
for _, v := range tests {
|
||||
x, _ := runRaw(fmt.Sprintf(`{{ randInt %d %d }}`, v.min, v.max), nil)
|
||||
r, err := strconv.Atoi(x)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, func(min, max, r int) bool {
|
||||
return r >= v.min && r < v.max
|
||||
}(v.min, v.max, r))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeq(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{seq 0 1 3}}`: "0 1 2 3",
|
||||
`{{seq 0 3 10}}`: "0 3 6 9",
|
||||
`{{seq 3 3 2}}`: "",
|
||||
`{{seq 3 -3 2}}`: "3",
|
||||
`{{seq}}`: "",
|
||||
`{{seq 0 4}}`: "0 1 2 3 4",
|
||||
`{{seq 5}}`: "1 2 3 4 5",
|
||||
`{{seq -5}}`: "1 0 -1 -2 -3 -4 -5",
|
||||
`{{seq 0}}`: "1 0",
|
||||
`{{seq 0 1 2 3}}`: "",
|
||||
`{{seq 0 -4}}`: "0 -1 -2 -3 -4",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
util/sprig/reflect.go
Normal file
28
util/sprig/reflect.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// typeIs returns true if the src is the type named in target.
|
||||
func typeIs(target string, src interface{}) bool {
|
||||
return target == typeOf(src)
|
||||
}
|
||||
|
||||
func typeIsLike(target string, src interface{}) bool {
|
||||
t := typeOf(src)
|
||||
return target == t || "*"+target == t
|
||||
}
|
||||
|
||||
func typeOf(src interface{}) string {
|
||||
return fmt.Sprintf("%T", src)
|
||||
}
|
||||
|
||||
func kindIs(target string, src interface{}) bool {
|
||||
return target == kindOf(src)
|
||||
}
|
||||
|
||||
func kindOf(src interface{}) string {
|
||||
return reflect.ValueOf(src).Kind().String()
|
||||
}
|
||||
73
util/sprig/reflect_test.go
Normal file
73
util/sprig/reflect_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fixtureTO struct {
|
||||
Name, Value string
|
||||
}
|
||||
|
||||
func TestTypeOf(t *testing.T) {
|
||||
f := &fixtureTO{"hello", "world"}
|
||||
tpl := `{{typeOf .}}`
|
||||
if err := runtv(tpl, "*sprig.fixtureTO", f); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKindOf(t *testing.T) {
|
||||
tpl := `{{kindOf .}}`
|
||||
|
||||
f := fixtureTO{"hello", "world"}
|
||||
if err := runtv(tpl, "struct", f); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
f2 := []string{"hello"}
|
||||
if err := runtv(tpl, "slice", f2); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var f3 *fixtureTO
|
||||
if err := runtv(tpl, "ptr", f3); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeIs(t *testing.T) {
|
||||
f := &fixtureTO{"hello", "world"}
|
||||
tpl := `{{if typeIs "*sprig.fixtureTO" .}}t{{else}}f{{end}}`
|
||||
if err := runtv(tpl, "t", f); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
f2 := "hello"
|
||||
if err := runtv(tpl, "f", f2); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
func TestTypeIsLike(t *testing.T) {
|
||||
f := "foo"
|
||||
tpl := `{{if typeIsLike "string" .}}t{{else}}f{{end}}`
|
||||
if err := runtv(tpl, "t", f); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Now make a pointer. Should still match.
|
||||
f2 := &f
|
||||
if err := runtv(tpl, "t", f2); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
func TestKindIs(t *testing.T) {
|
||||
f := &fixtureTO{"hello", "world"}
|
||||
tpl := `{{if kindIs "ptr" .}}t{{else}}f{{end}}`
|
||||
if err := runtv(tpl, "t", f); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
f2 := "hello"
|
||||
if err := runtv(tpl, "f", f2); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
83
util/sprig/regex.go
Normal file
83
util/sprig/regex.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func regexMatch(regex string, s string) bool {
|
||||
match, _ := regexp.MatchString(regex, s)
|
||||
return match
|
||||
}
|
||||
|
||||
func mustRegexMatch(regex string, s string) (bool, error) {
|
||||
return regexp.MatchString(regex, s)
|
||||
}
|
||||
|
||||
func regexFindAll(regex string, s string, n int) []string {
|
||||
r := regexp.MustCompile(regex)
|
||||
return r.FindAllString(s, n)
|
||||
}
|
||||
|
||||
func mustRegexFindAll(regex string, s string, n int) ([]string, error) {
|
||||
r, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
return r.FindAllString(s, n), nil
|
||||
}
|
||||
|
||||
func regexFind(regex string, s string) string {
|
||||
r := regexp.MustCompile(regex)
|
||||
return r.FindString(s)
|
||||
}
|
||||
|
||||
func mustRegexFind(regex string, s string) (string, error) {
|
||||
r, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.FindString(s), nil
|
||||
}
|
||||
|
||||
func regexReplaceAll(regex string, s string, repl string) string {
|
||||
r := regexp.MustCompile(regex)
|
||||
return r.ReplaceAllString(s, repl)
|
||||
}
|
||||
|
||||
func mustRegexReplaceAll(regex string, s string, repl string) (string, error) {
|
||||
r, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.ReplaceAllString(s, repl), nil
|
||||
}
|
||||
|
||||
func regexReplaceAllLiteral(regex string, s string, repl string) string {
|
||||
r := regexp.MustCompile(regex)
|
||||
return r.ReplaceAllLiteralString(s, repl)
|
||||
}
|
||||
|
||||
func mustRegexReplaceAllLiteral(regex string, s string, repl string) (string, error) {
|
||||
r, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.ReplaceAllLiteralString(s, repl), nil
|
||||
}
|
||||
|
||||
func regexSplit(regex string, s string, n int) []string {
|
||||
r := regexp.MustCompile(regex)
|
||||
return r.Split(s, n)
|
||||
}
|
||||
|
||||
func mustRegexSplit(regex string, s string, n int) ([]string, error) {
|
||||
r, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
return r.Split(s, n), nil
|
||||
}
|
||||
|
||||
func regexQuoteMeta(s string) string {
|
||||
return regexp.QuoteMeta(s)
|
||||
}
|
||||
203
util/sprig/regex_test.go
Normal file
203
util/sprig/regex_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRegexMatch(t *testing.T) {
|
||||
regex := "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
|
||||
|
||||
assert.True(t, regexMatch(regex, "test@acme.com"))
|
||||
assert.True(t, regexMatch(regex, "Test@Acme.Com"))
|
||||
assert.False(t, regexMatch(regex, "test"))
|
||||
assert.False(t, regexMatch(regex, "test.com"))
|
||||
assert.False(t, regexMatch(regex, "test@acme"))
|
||||
}
|
||||
|
||||
func TestMustRegexMatch(t *testing.T) {
|
||||
regex := "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
|
||||
|
||||
o, err := mustRegexMatch(regex, "test@acme.com")
|
||||
assert.True(t, o)
|
||||
assert.Nil(t, err)
|
||||
|
||||
o, err = mustRegexMatch(regex, "Test@Acme.Com")
|
||||
assert.True(t, o)
|
||||
assert.Nil(t, err)
|
||||
|
||||
o, err = mustRegexMatch(regex, "test")
|
||||
assert.False(t, o)
|
||||
assert.Nil(t, err)
|
||||
|
||||
o, err = mustRegexMatch(regex, "test.com")
|
||||
assert.False(t, o)
|
||||
assert.Nil(t, err)
|
||||
|
||||
o, err = mustRegexMatch(regex, "test@acme")
|
||||
assert.False(t, o)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestRegexFindAll(t *testing.T) {
|
||||
regex := "a{2}"
|
||||
assert.Equal(t, 1, len(regexFindAll(regex, "aa", -1)))
|
||||
assert.Equal(t, 1, len(regexFindAll(regex, "aaaaaaaa", 1)))
|
||||
assert.Equal(t, 2, len(regexFindAll(regex, "aaaa", -1)))
|
||||
assert.Equal(t, 0, len(regexFindAll(regex, "none", -1)))
|
||||
}
|
||||
|
||||
func TestMustRegexFindAll(t *testing.T) {
|
||||
type args struct {
|
||||
regex, s string
|
||||
n int
|
||||
}
|
||||
cases := []struct {
|
||||
expected int
|
||||
args args
|
||||
}{
|
||||
{1, args{"a{2}", "aa", -1}},
|
||||
{1, args{"a{2}", "aaaaaaaa", 1}},
|
||||
{2, args{"a{2}", "aaaa", -1}},
|
||||
{0, args{"a{2}", "none", -1}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
res, err := mustRegexFindAll(c.args.regex, c.args.s, c.args.n)
|
||||
if err != nil {
|
||||
t.Errorf("regexFindAll test case %v failed with err %s", c, err)
|
||||
}
|
||||
assert.Equal(t, c.expected, len(res), "case %#v", c.args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexFindl(t *testing.T) {
|
||||
regex := "fo.?"
|
||||
assert.Equal(t, "foo", regexFind(regex, "foorbar"))
|
||||
assert.Equal(t, "foo", regexFind(regex, "foo foe fome"))
|
||||
assert.Equal(t, "", regexFind(regex, "none"))
|
||||
}
|
||||
|
||||
func TestMustRegexFindl(t *testing.T) {
|
||||
type args struct{ regex, s string }
|
||||
cases := []struct {
|
||||
expected string
|
||||
args args
|
||||
}{
|
||||
{"foo", args{"fo.?", "foorbar"}},
|
||||
{"foo", args{"fo.?", "foo foe fome"}},
|
||||
{"", args{"fo.?", "none"}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
res, err := mustRegexFind(c.args.regex, c.args.s)
|
||||
if err != nil {
|
||||
t.Errorf("regexFind test case %v failed with err %s", c, err)
|
||||
}
|
||||
assert.Equal(t, c.expected, res, "case %#v", c.args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexReplaceAll(t *testing.T) {
|
||||
regex := "a(x*)b"
|
||||
assert.Equal(t, "-T-T-", regexReplaceAll(regex, "-ab-axxb-", "T"))
|
||||
assert.Equal(t, "--xx-", regexReplaceAll(regex, "-ab-axxb-", "$1"))
|
||||
assert.Equal(t, "---", regexReplaceAll(regex, "-ab-axxb-", "$1W"))
|
||||
assert.Equal(t, "-W-xxW-", regexReplaceAll(regex, "-ab-axxb-", "${1}W"))
|
||||
}
|
||||
|
||||
func TestMustRegexReplaceAll(t *testing.T) {
|
||||
type args struct{ regex, s, repl string }
|
||||
cases := []struct {
|
||||
expected string
|
||||
args args
|
||||
}{
|
||||
{"-T-T-", args{"a(x*)b", "-ab-axxb-", "T"}},
|
||||
{"--xx-", args{"a(x*)b", "-ab-axxb-", "$1"}},
|
||||
{"---", args{"a(x*)b", "-ab-axxb-", "$1W"}},
|
||||
{"-W-xxW-", args{"a(x*)b", "-ab-axxb-", "${1}W"}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
res, err := mustRegexReplaceAll(c.args.regex, c.args.s, c.args.repl)
|
||||
if err != nil {
|
||||
t.Errorf("regexReplaceAll test case %v failed with err %s", c, err)
|
||||
}
|
||||
assert.Equal(t, c.expected, res, "case %#v", c.args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexReplaceAllLiteral(t *testing.T) {
|
||||
regex := "a(x*)b"
|
||||
assert.Equal(t, "-T-T-", regexReplaceAllLiteral(regex, "-ab-axxb-", "T"))
|
||||
assert.Equal(t, "-$1-$1-", regexReplaceAllLiteral(regex, "-ab-axxb-", "$1"))
|
||||
assert.Equal(t, "-${1}-${1}-", regexReplaceAllLiteral(regex, "-ab-axxb-", "${1}"))
|
||||
}
|
||||
|
||||
func TestMustRegexReplaceAllLiteral(t *testing.T) {
|
||||
type args struct{ regex, s, repl string }
|
||||
cases := []struct {
|
||||
expected string
|
||||
args args
|
||||
}{
|
||||
{"-T-T-", args{"a(x*)b", "-ab-axxb-", "T"}},
|
||||
{"-$1-$1-", args{"a(x*)b", "-ab-axxb-", "$1"}},
|
||||
{"-${1}-${1}-", args{"a(x*)b", "-ab-axxb-", "${1}"}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
res, err := mustRegexReplaceAllLiteral(c.args.regex, c.args.s, c.args.repl)
|
||||
if err != nil {
|
||||
t.Errorf("regexReplaceAllLiteral test case %v failed with err %s", c, err)
|
||||
}
|
||||
assert.Equal(t, c.expected, res, "case %#v", c.args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexSplit(t *testing.T) {
|
||||
regex := "a"
|
||||
assert.Equal(t, 4, len(regexSplit(regex, "banana", -1)))
|
||||
assert.Equal(t, 0, len(regexSplit(regex, "banana", 0)))
|
||||
assert.Equal(t, 1, len(regexSplit(regex, "banana", 1)))
|
||||
assert.Equal(t, 2, len(regexSplit(regex, "banana", 2)))
|
||||
|
||||
regex = "z+"
|
||||
assert.Equal(t, 2, len(regexSplit(regex, "pizza", -1)))
|
||||
assert.Equal(t, 0, len(regexSplit(regex, "pizza", 0)))
|
||||
assert.Equal(t, 1, len(regexSplit(regex, "pizza", 1)))
|
||||
assert.Equal(t, 2, len(regexSplit(regex, "pizza", 2)))
|
||||
}
|
||||
|
||||
func TestMustRegexSplit(t *testing.T) {
|
||||
type args struct {
|
||||
regex, s string
|
||||
n int
|
||||
}
|
||||
cases := []struct {
|
||||
expected int
|
||||
args args
|
||||
}{
|
||||
{4, args{"a", "banana", -1}},
|
||||
{0, args{"a", "banana", 0}},
|
||||
{1, args{"a", "banana", 1}},
|
||||
{2, args{"a", "banana", 2}},
|
||||
{2, args{"z+", "pizza", -1}},
|
||||
{0, args{"z+", "pizza", 0}},
|
||||
{1, args{"z+", "pizza", 1}},
|
||||
{2, args{"z+", "pizza", 2}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
res, err := mustRegexSplit(c.args.regex, c.args.s, c.args.n)
|
||||
if err != nil {
|
||||
t.Errorf("regexSplit test case %v failed with err %s", c, err)
|
||||
}
|
||||
assert.Equal(t, c.expected, len(res), "case %#v", c.args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexQuoteMeta(t *testing.T) {
|
||||
assert.Equal(t, "1\\.2\\.3", regexQuoteMeta("1.2.3"))
|
||||
assert.Equal(t, "pretzel", regexQuoteMeta("pretzel"))
|
||||
}
|
||||
189
util/sprig/strings.go
Normal file
189
util/sprig/strings.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func base64encode(v string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(v))
|
||||
}
|
||||
|
||||
func base64decode(v string) string {
|
||||
data, err := base64.StdEncoding.DecodeString(v)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func base32encode(v string) string {
|
||||
return base32.StdEncoding.EncodeToString([]byte(v))
|
||||
}
|
||||
|
||||
func base32decode(v string) string {
|
||||
data, err := base32.StdEncoding.DecodeString(v)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func quote(str ...interface{}) string {
|
||||
out := make([]string, 0, len(str))
|
||||
for _, s := range str {
|
||||
if s != nil {
|
||||
out = append(out, fmt.Sprintf("%q", strval(s)))
|
||||
}
|
||||
}
|
||||
return strings.Join(out, " ")
|
||||
}
|
||||
|
||||
func squote(str ...interface{}) string {
|
||||
out := make([]string, 0, len(str))
|
||||
for _, s := range str {
|
||||
if s != nil {
|
||||
out = append(out, fmt.Sprintf("'%v'", s))
|
||||
}
|
||||
}
|
||||
return strings.Join(out, " ")
|
||||
}
|
||||
|
||||
func cat(v ...interface{}) string {
|
||||
v = removeNilElements(v)
|
||||
r := strings.TrimSpace(strings.Repeat("%v ", len(v)))
|
||||
return fmt.Sprintf(r, v...)
|
||||
}
|
||||
|
||||
func indent(spaces int, v string) string {
|
||||
pad := strings.Repeat(" ", spaces)
|
||||
return pad + strings.Replace(v, "\n", "\n"+pad, -1)
|
||||
}
|
||||
|
||||
func nindent(spaces int, v string) string {
|
||||
return "\n" + indent(spaces, v)
|
||||
}
|
||||
|
||||
func replace(old, new, src string) string {
|
||||
return strings.Replace(src, old, new, -1)
|
||||
}
|
||||
|
||||
func plural(one, many string, count int) string {
|
||||
if count == 1 {
|
||||
return one
|
||||
}
|
||||
return many
|
||||
}
|
||||
|
||||
func strslice(v interface{}) []string {
|
||||
switch v := v.(type) {
|
||||
case []string:
|
||||
return v
|
||||
case []interface{}:
|
||||
b := make([]string, 0, len(v))
|
||||
for _, s := range v {
|
||||
if s != nil {
|
||||
b = append(b, strval(s))
|
||||
}
|
||||
}
|
||||
return b
|
||||
default:
|
||||
val := reflect.ValueOf(v)
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
l := val.Len()
|
||||
b := make([]string, 0, l)
|
||||
for i := 0; i < l; i++ {
|
||||
value := val.Index(i).Interface()
|
||||
if value != nil {
|
||||
b = append(b, strval(value))
|
||||
}
|
||||
}
|
||||
return b
|
||||
default:
|
||||
if v == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return []string{strval(v)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeNilElements(v []interface{}) []interface{} {
|
||||
newSlice := make([]interface{}, 0, len(v))
|
||||
for _, i := range v {
|
||||
if i != nil {
|
||||
newSlice = append(newSlice, i)
|
||||
}
|
||||
}
|
||||
return newSlice
|
||||
}
|
||||
|
||||
func strval(v interface{}) string {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return v
|
||||
case []byte:
|
||||
return string(v)
|
||||
case error:
|
||||
return v.Error()
|
||||
case fmt.Stringer:
|
||||
return v.String()
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func trunc(c int, s string) string {
|
||||
if c < 0 && len(s)+c > 0 {
|
||||
return s[len(s)+c:]
|
||||
}
|
||||
if c >= 0 && len(s) > c {
|
||||
return s[:c]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func join(sep string, v interface{}) string {
|
||||
return strings.Join(strslice(v), sep)
|
||||
}
|
||||
|
||||
func split(sep, orig string) map[string]string {
|
||||
parts := strings.Split(orig, sep)
|
||||
res := make(map[string]string, len(parts))
|
||||
for i, v := range parts {
|
||||
res["_"+strconv.Itoa(i)] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func splitn(sep string, n int, orig string) map[string]string {
|
||||
parts := strings.SplitN(orig, sep, n)
|
||||
res := make(map[string]string, len(parts))
|
||||
for i, v := range parts {
|
||||
res["_"+strconv.Itoa(i)] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// substring creates a substring of the given string.
|
||||
//
|
||||
// If start is < 0, this calls string[:end].
|
||||
//
|
||||
// If start is >= 0 and end < 0 or end bigger than s length, this calls string[start:]
|
||||
//
|
||||
// Otherwise, this calls string[start, end].
|
||||
func substring(start, end int, s string) string {
|
||||
if start < 0 {
|
||||
return s[:end]
|
||||
}
|
||||
if end < 0 || end > len(s) {
|
||||
return s[start:]
|
||||
}
|
||||
return s[start:end]
|
||||
}
|
||||
233
util/sprig/strings_test.go
Normal file
233
util/sprig/strings_test.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSubstr(t *testing.T) {
|
||||
tpl := `{{"fooo" | substr 0 3 }}`
|
||||
if err := runt(tpl, "foo"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubstr_shorterString(t *testing.T) {
|
||||
tpl := `{{"foo" | substr 0 10 }}`
|
||||
if err := runt(tpl, "foo"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrunc(t *testing.T) {
|
||||
tpl := `{{ "foooooo" | trunc 3 }}`
|
||||
if err := runt(tpl, "foo"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{ "baaaaaar" | trunc -3 }}`
|
||||
if err := runt(tpl, "aar"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{ "baaaaaar" | trunc -999 }}`
|
||||
if err := runt(tpl, "baaaaaar"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{ "baaaaaz" | trunc 0 }}`
|
||||
if err := runt(tpl, ""); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuote(t *testing.T) {
|
||||
tpl := `{{quote "a" "b" "c"}}`
|
||||
if err := runt(tpl, `"a" "b" "c"`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{quote "\"a\"" "b" "c"}}`
|
||||
if err := runt(tpl, `"\"a\"" "b" "c"`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{quote 1 2 3 }}`
|
||||
if err := runt(tpl, `"1" "2" "3"`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{ .value | quote }}`
|
||||
values := map[string]interface{}{"value": nil}
|
||||
if err := runtv(tpl, ``, values); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
func TestSquote(t *testing.T) {
|
||||
tpl := `{{squote "a" "b" "c"}}`
|
||||
if err := runt(tpl, `'a' 'b' 'c'`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{squote 1 2 3 }}`
|
||||
if err := runt(tpl, `'1' '2' '3'`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{ .value | squote }}`
|
||||
values := map[string]interface{}{"value": nil}
|
||||
if err := runtv(tpl, ``, values); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
// Mainly, we're just verifying the paramater order swap.
|
||||
tests := []string{
|
||||
`{{if contains "cat" "fair catch"}}1{{end}}`,
|
||||
`{{if hasPrefix "cat" "catch"}}1{{end}}`,
|
||||
`{{if hasSuffix "cat" "ducat"}}1{{end}}`,
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if err := runt(tt, "1"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrim(t *testing.T) {
|
||||
tests := []string{
|
||||
`{{trim " 5.00 "}}`,
|
||||
`{{trimAll "$" "$5.00$"}}`,
|
||||
`{{trimPrefix "$" "$5.00"}}`,
|
||||
`{{trimSuffix "$" "5.00$"}}`,
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if err := runt(tt, "5.00"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
tpl := `{{$v := "foo$bar$baz" | split "$"}}{{$v._0}}`
|
||||
if err := runt(tpl, "foo"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitn(t *testing.T) {
|
||||
tpl := `{{$v := "foo$bar$baz" | splitn "$" 2}}{{$v._0}}`
|
||||
if err := runt(tpl, "foo"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToString(t *testing.T) {
|
||||
tpl := `{{ toString 1 | kindOf }}`
|
||||
assert.NoError(t, runt(tpl, "string"))
|
||||
}
|
||||
|
||||
func TestToStrings(t *testing.T) {
|
||||
tpl := `{{ $s := list 1 2 3 | toStrings }}{{ index $s 1 | kindOf }}`
|
||||
assert.NoError(t, runt(tpl, "string"))
|
||||
tpl = `{{ list 1 .value 2 | toStrings }}`
|
||||
values := map[string]interface{}{"value": nil}
|
||||
if err := runtv(tpl, `[1 2]`, values); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoin(t *testing.T) {
|
||||
assert.NoError(t, runt(`{{ tuple "a" "b" "c" | join "-" }}`, "a-b-c"))
|
||||
assert.NoError(t, runt(`{{ tuple 1 2 3 | join "-" }}`, "1-2-3"))
|
||||
assert.NoError(t, runtv(`{{ join "-" .V }}`, "a-b-c", map[string]interface{}{"V": []string{"a", "b", "c"}}))
|
||||
assert.NoError(t, runtv(`{{ join "-" .V }}`, "abc", map[string]interface{}{"V": "abc"}))
|
||||
assert.NoError(t, runtv(`{{ join "-" .V }}`, "1-2-3", map[string]interface{}{"V": []int{1, 2, 3}}))
|
||||
assert.NoError(t, runtv(`{{ join "-" .value }}`, "1-2", map[string]interface{}{"value": []interface{}{"1", nil, "2"}}))
|
||||
}
|
||||
|
||||
func TestSortAlpha(t *testing.T) {
|
||||
// Named `append` in the function map
|
||||
tests := map[string]string{
|
||||
`{{ list "c" "a" "b" | sortAlpha | join "" }}`: "abc",
|
||||
`{{ list 2 1 4 3 | sortAlpha | join "" }}`: "1234",
|
||||
}
|
||||
for tpl, expect := range tests {
|
||||
assert.NoError(t, runt(tpl, expect))
|
||||
}
|
||||
}
|
||||
func TestBase64EncodeDecode(t *testing.T) {
|
||||
magicWord := "coffee"
|
||||
expect := base64.StdEncoding.EncodeToString([]byte(magicWord))
|
||||
|
||||
if expect == magicWord {
|
||||
t.Fatal("Encoder doesn't work.")
|
||||
}
|
||||
|
||||
tpl := `{{b64enc "coffee"}}`
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = fmt.Sprintf("{{b64dec %q}}", expect)
|
||||
if err := runt(tpl, magicWord); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
func TestBase32EncodeDecode(t *testing.T) {
|
||||
magicWord := "coffee"
|
||||
expect := base32.StdEncoding.EncodeToString([]byte(magicWord))
|
||||
|
||||
if expect == magicWord {
|
||||
t.Fatal("Encoder doesn't work.")
|
||||
}
|
||||
|
||||
tpl := `{{b32enc "coffee"}}`
|
||||
if err := runt(tpl, expect); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = fmt.Sprintf("{{b32dec %q}}", expect)
|
||||
if err := runt(tpl, magicWord); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCat(t *testing.T) {
|
||||
tpl := `{{$b := "b"}}{{"c" | cat "a" $b}}`
|
||||
if err := runt(tpl, "a b c"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{ .value | cat "a" "b"}}`
|
||||
values := map[string]interface{}{"value": nil}
|
||||
if err := runtv(tpl, "a b", values); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndent(t *testing.T) {
|
||||
tpl := `{{indent 4 "a\nb\nc"}}`
|
||||
if err := runt(tpl, " a\n b\n c"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNindent(t *testing.T) {
|
||||
tpl := `{{nindent 4 "a\nb\nc"}}`
|
||||
if err := runt(tpl, "\n a\n b\n c"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplace(t *testing.T) {
|
||||
tpl := `{{"I Am Henry VIII" | replace " " "-"}}`
|
||||
if err := runt(tpl, "I-Am-Henry-VIII"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlural(t *testing.T) {
|
||||
tpl := `{{$num := len "two"}}{{$num}} {{$num | plural "1 char" "chars"}}`
|
||||
if err := runt(tpl, "3 chars"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tpl = `{{len "t" | plural "cheese" "%d chars"}}`
|
||||
if err := runt(tpl, "cheese"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
66
util/sprig/url.go
Normal file
66
util/sprig/url.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func dictGetOrEmpty(dict map[string]interface{}, key string) string {
|
||||
value, ok := dict[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
tp := reflect.TypeOf(value).Kind()
|
||||
if tp != reflect.String {
|
||||
panic(fmt.Sprintf("unable to parse %s key, must be of type string, but %s found", key, tp.String()))
|
||||
}
|
||||
return reflect.ValueOf(value).String()
|
||||
}
|
||||
|
||||
// parses given URL to return dict object
|
||||
func urlParse(v string) map[string]interface{} {
|
||||
dict := map[string]interface{}{}
|
||||
parsedURL, err := url.Parse(v)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("unable to parse url: %s", err))
|
||||
}
|
||||
dict["scheme"] = parsedURL.Scheme
|
||||
dict["host"] = parsedURL.Host
|
||||
dict["hostname"] = parsedURL.Hostname()
|
||||
dict["path"] = parsedURL.Path
|
||||
dict["query"] = parsedURL.RawQuery
|
||||
dict["opaque"] = parsedURL.Opaque
|
||||
dict["fragment"] = parsedURL.Fragment
|
||||
if parsedURL.User != nil {
|
||||
dict["userinfo"] = parsedURL.User.String()
|
||||
} else {
|
||||
dict["userinfo"] = ""
|
||||
}
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
// join given dict to URL string
|
||||
func urlJoin(d map[string]interface{}) string {
|
||||
resURL := url.URL{
|
||||
Scheme: dictGetOrEmpty(d, "scheme"),
|
||||
Host: dictGetOrEmpty(d, "host"),
|
||||
Path: dictGetOrEmpty(d, "path"),
|
||||
RawQuery: dictGetOrEmpty(d, "query"),
|
||||
Opaque: dictGetOrEmpty(d, "opaque"),
|
||||
Fragment: dictGetOrEmpty(d, "fragment"),
|
||||
}
|
||||
userinfo := dictGetOrEmpty(d, "userinfo")
|
||||
var user *url.Userinfo
|
||||
if userinfo != "" {
|
||||
tempURL, err := url.Parse(fmt.Sprintf("proto://%s@host", userinfo))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("unable to parse userinfo in dict: %s", err))
|
||||
}
|
||||
user = tempURL.User
|
||||
}
|
||||
|
||||
resURL.User = user
|
||||
return resURL.String()
|
||||
}
|
||||
87
util/sprig/url_test.go
Normal file
87
util/sprig/url_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package sprig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var urlTests = map[string]map[string]interface{}{
|
||||
"proto://auth@host:80/path?query#fragment": {
|
||||
"fragment": "fragment",
|
||||
"host": "host:80",
|
||||
"hostname": "host",
|
||||
"opaque": "",
|
||||
"path": "/path",
|
||||
"query": "query",
|
||||
"scheme": "proto",
|
||||
"userinfo": "auth",
|
||||
},
|
||||
"proto://host:80/path": {
|
||||
"fragment": "",
|
||||
"host": "host:80",
|
||||
"hostname": "host",
|
||||
"opaque": "",
|
||||
"path": "/path",
|
||||
"query": "",
|
||||
"scheme": "proto",
|
||||
"userinfo": "",
|
||||
},
|
||||
"something": {
|
||||
"fragment": "",
|
||||
"host": "",
|
||||
"hostname": "",
|
||||
"opaque": "",
|
||||
"path": "something",
|
||||
"query": "",
|
||||
"scheme": "",
|
||||
"userinfo": "",
|
||||
},
|
||||
"proto://user:passwor%20d@host:80/path": {
|
||||
"fragment": "",
|
||||
"host": "host:80",
|
||||
"hostname": "host",
|
||||
"opaque": "",
|
||||
"path": "/path",
|
||||
"query": "",
|
||||
"scheme": "proto",
|
||||
"userinfo": "user:passwor%20d",
|
||||
},
|
||||
"proto://host:80/pa%20th?key=val%20ue": {
|
||||
"fragment": "",
|
||||
"host": "host:80",
|
||||
"hostname": "host",
|
||||
"opaque": "",
|
||||
"path": "/pa th",
|
||||
"query": "key=val%20ue",
|
||||
"scheme": "proto",
|
||||
"userinfo": "",
|
||||
},
|
||||
}
|
||||
|
||||
func TestUrlParse(t *testing.T) {
|
||||
// testing that function is exported and working properly
|
||||
assert.NoError(t, runt(
|
||||
`{{ index ( urlParse "proto://auth@host:80/path?query#fragment" ) "host" }}`,
|
||||
"host:80"))
|
||||
|
||||
// testing scenarios
|
||||
for url, expected := range urlTests {
|
||||
assert.EqualValues(t, expected, urlParse(url))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUrlJoin(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`{{ urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "proto") }}`: "proto://host:80/path?query#fragment",
|
||||
`{{ urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "scheme" "proto" "userinfo" "ASDJKJSD") }}`: "proto://ASDJKJSD@host:80/path#fragment",
|
||||
}
|
||||
for tpl, expected := range tests {
|
||||
assert.NoError(t, runt(tpl, expected))
|
||||
}
|
||||
|
||||
for expected, urlMap := range urlTests {
|
||||
assert.EqualValues(t, expected, urlJoin(urlMap))
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user