summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDave Henderson <dhenderson@gmail.com>2019-02-28 20:34:31 -0500
committerDave Henderson <dhenderson@gmail.com>2019-03-17 13:00:23 -0400
commit0347dd1f69480206fb23ff754cbf5b459bf06817 (patch)
tree91cc47762c0cb13c53d66d969fda29c511523a5a
parent13c1c4bf680acb6c9421c30c72ec21a626897b70 (diff)
New random namespace for generating random strings and numbers
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
-rw-r--r--docs-src/content/functions/random.yml169
-rw-r--r--docs/content/functions/random.md251
-rw-r--r--funcs.go1
-rw-r--r--funcs/random.go162
-rw-r--r--funcs/random_test.go175
-rw-r--r--random/random.go109
-rw-r--r--random/random_test.go127
7 files changed, 994 insertions, 0 deletions
diff --git a/docs-src/content/functions/random.yml b/docs-src/content/functions/random.yml
new file mode 100644
index 00000000..b4f8c4cd
--- /dev/null
+++ b/docs-src/content/functions/random.yml
@@ -0,0 +1,169 @@
+ns: random
+title: random functions
+preamble: |
+ Functions for generating random values.
+
+ ### About randomness
+
+ `gomplate` uses Go's [`math/rand`](https://golang.org/pkg/math/rand/) package
+ to generate pseudo-random numbers. Note that these functions are not suitable
+ for use in security-sensitive applications, such as cryptography. However,
+ these functions will not deplete system entropy.
+funcs:
+ - name: random.ASCII
+ description: |
+ Generates a random string of a desired length, containing the set of
+ printable characters from the 7-bit [ASCII](https://en.wikipedia.org/wiki/ASCII)
+ set. This includes _space_ (' '), but no other whitespace characters.
+ pipeline: false
+ arguments:
+ - name: count
+ required: true
+ description: the length of the string to produce (number of characters)
+ examples:
+ - |
+ $ gomplate -i '{{ random.ASCII 8 }}'
+ _woJ%D&K
+ - name: random.Alpha
+ description: |
+ Generates a random alphabetical (`A-Z`, `a-z`) string of a desired length.
+ pipeline: false
+ arguments:
+ - name: count
+ required: true
+ description: the length of the string to produce (number of characters)
+ examples:
+ - |
+ $ gomplate -i '{{ random.Alpha 42 }}'
+ oAqHKxHiytYicMxTMGHnUnAfltPVZDhFkVkgDvatJK
+ - name: random.AlphaNum
+ description: |
+ Generates a random alphanumeric (`0-9`, `A-Z`, `a-z`) string of a desired length.
+ pipeline: false
+ arguments:
+ - name: count
+ required: true
+ description: the length of the string to produce (number of characters)
+ examples:
+ - |
+ $ gomplate -i '{{ random.AlphaNum 16 }}'
+ 4olRl9mRmVp1nqSm
+ - name: random.String
+ description: |
+ Generates a random string of a desired length.
+
+ By default, the possible characters are those represented by the
+ regular expression `[a-zA-Z0-9_.-]` (alphanumeric, plus `_`, `.`, and `-`).
+
+ A different set of characters can be specified with a regular expression,
+ or by giving a range of possible characters by specifying the lower and
+ upper bounds. Lower/upper bounds can be specified as characters (e.g.
+ `"q"`, or escape sequences such as `"\U0001f0AF"`), or numeric Unicode
+ code-points (e.g. `48` or `0x30` for the character `0`).
+
+ When given a range of Unicode code-points, `random.String` will discard
+ non-printable characters from the selection. This may result in a much
+ smaller set of possible characters than intended, so check
+ the [Unicode character code charts](http://www.unicode.org/charts/) to
+ verify the correct code-points.
+ pipeline: false
+ arguments:
+ - name: count
+ required: true
+ description: the length of the string to produce (number of characters)
+ - name: regex
+ required: false
+ description: the regular expression that each character must match (defaults to `[a-zA-Z0-9_.-]`)
+ - name: lower
+ required: false
+ description: lower bound for a range of characters (number or single character)
+ - name: upper
+ required: false
+ description: upper bound for a range of characters (number or single character)
+ examples:
+ - |
+ $ gomplate -i '{{ random.String 8 }}'
+ FODZ01u_
+ - |
+ $ gomplate -i '{{ random.String 16 `[[:xdigit:]]` }}'
+ B9e0527C3e45E1f3
+ - |
+ $ gomplate -i '{{ random.String 20 `[\p{Canadian_Aboriginal}]` }}'
+ ᗄᖖᣡᕔᕫᗝᖴᒙᗌᘔᓰᖫᗵᐕᗵᙔᗠᓅᕎᔹ
+ - |
+ $ gomplate -i '{{ random.String 8 "c" "m" }}'
+ ffmidgjc
+ - |
+ $ gomplate -i 'You rolled... {{ random.String 3 "⚀" "⚅" }}'
+ You rolled... ⚅⚂⚁
+ - |
+ $ gomplate -i 'Poker time! {{ random.String 5 "\U0001f0a1" "\U0001f0de" }}'
+ Poker time! 🂼🂺🂳🃅🂪
+ - name: random.Item
+ description: |
+ Pick an element at a random from a given slice or array.
+ pipeline: true
+ arguments:
+ - name: items
+ required: true
+ description: the input array
+ examples:
+ - |
+ $ gomplate -i '{{ random.Item (seq 0 5) }}'
+ 4
+ - |
+ $ export SLICE='["red", "green", "blue"]'
+ $ gomplate -i '{{ getenv "SLICE" | jsonArray | random.Item }}'
+ blue
+ - name: random.Number
+ description: |
+ Pick a random integer. By default, a number between `0` and `100`
+ (inclusive) is chosen, but this range can be overridden.
+
+ Note that the difference between `min` and `max` can not be larger than a
+ 63-bit integer (i.e. the unsigned portion of a 64-bit signed integer).
+ The result is given as an `int64`.
+ pipeline: false
+ arguments:
+ - name: min
+ required: false
+ description: The minimum value, defaults to `0`. Must be less than `max`.
+ - name: max
+ required: false
+ description: The maximum value, defaults to `100` (if no args provided)
+ examples:
+ - |
+ $ gomplate -i '{{ random.Number }}'
+ 55
+ - |
+ $ gomplate -i '{{ random.Number -10 10 }}'
+ -3
+ - |
+ $ gomplate -i '{{ random.Number 5 }}'
+ 2
+ - name: random.Float
+ description: |
+ Pick a random decimal floating-point number. By default, a number between
+ `0.0` and `1.0` (_exclusive_, i.e. `[0.0,1.0)`) is chosen, but this range
+ can be overridden.
+
+ The result is given as a `float64`.
+ pipeline: false
+ arguments:
+ - name: min
+ required: false
+ description: The minimum value, defaults to `0.0`. Must be less than `max`.
+ - name: max
+ required: false
+ description: The maximum value, defaults to `1.0` (if no args provided).
+ examples:
+ - |
+ $ gomplate -i '{{ random.Float }}'
+ 0.2029946480303966
+ - |
+ $ gomplate -i '{{ random.Float 100 }}'
+ 71.28595374161743
+ - |
+ $ gomplate -i '{{ random.Float -100 200 }}'
+ 105.59119437834909
+ \ No newline at end of file
diff --git a/docs/content/functions/random.md b/docs/content/functions/random.md
new file mode 100644
index 00000000..8396e0f9
--- /dev/null
+++ b/docs/content/functions/random.md
@@ -0,0 +1,251 @@
+---
+title: random functions
+menu:
+ main:
+ parent: functions
+---
+
+Functions for generating random values.
+
+### About randomness
+
+`gomplate` uses Go's [`math/rand`](https://golang.org/pkg/math/rand/) package
+to generate pseudo-random numbers. Note that these functions are not suitable
+for use in security-sensitive applications, such as cryptography. However,
+these functions will not deplete system entropy.
+
+## `random.ASCII`
+
+Generates a random string of a desired length, containing the set of
+printable characters from the 7-bit [ASCII](https://en.wikipedia.org/wiki/ASCII)
+set. This includes _space_ (' '), but no other whitespace characters.
+
+### Usage
+
+```go
+random.ASCII count
+```
+
+### Arguments
+
+| name | description |
+|------|-------------|
+| `count` | _(required)_ the length of the string to produce (number of characters) |
+
+### Examples
+
+```console
+$ gomplate -i '{{ random.ASCII 8 }}'
+_woJ%D&K
+```
+
+## `random.Alpha`
+
+Generates a random alphabetical (`A-Z`, `a-z`) string of a desired length.
+
+### Usage
+
+```go
+random.Alpha count
+```
+
+### Arguments
+
+| name | description |
+|------|-------------|
+| `count` | _(required)_ the length of the string to produce (number of characters) |
+
+### Examples
+
+```console
+$ gomplate -i '{{ random.Alpha 42 }}'
+oAqHKxHiytYicMxTMGHnUnAfltPVZDhFkVkgDvatJK
+```
+
+## `random.AlphaNum`
+
+Generates a random alphanumeric (`0-9`, `A-Z`, `a-z`) string of a desired length.
+
+### Usage
+
+```go
+random.AlphaNum count
+```
+
+### Arguments
+
+| name | description |
+|------|-------------|
+| `count` | _(required)_ the length of the string to produce (number of characters) |
+
+### Examples
+
+```console
+$ gomplate -i '{{ random.AlphaNum 16 }}'
+4olRl9mRmVp1nqSm
+```
+
+## `random.String`
+
+Generates a random string of a desired length.
+
+By default, the possible characters are those represented by the
+regular expression `[a-zA-Z0-9_.-]` (alphanumeric, plus `_`, `.`, and `-`).
+
+A different set of characters can be specified with a regular expression,
+or by giving a range of possible characters by specifying the lower and
+upper bounds. Lower/upper bounds can be specified as characters (e.g.
+`"q"`, or escape sequences such as `"\U0001f0AF"`), or numeric Unicode
+code-points (e.g. `48` or `0x30` for the character `0`).
+
+When given a range of Unicode code-points, `random.String` will discard
+non-printable characters from the selection. This may result in a much
+smaller set of possible characters than intended, so check
+the [Unicode character code charts](http://www.unicode.org/charts/) to
+verify the correct code-points.
+
+### Usage
+
+```go
+random.String count [regex] [lower] [upper]
+```
+
+### Arguments
+
+| name | description |
+|------|-------------|
+| `count` | _(required)_ the length of the string to produce (number of characters) |
+| `regex` | _(optional)_ the regular expression that each character must match (defaults to `[a-zA-Z0-9_.-]`) |
+| `lower` | _(optional)_ lower bound for a range of characters (number or single character) |
+| `upper` | _(optional)_ upper bound for a range of characters (number or single character) |
+
+### Examples
+
+```console
+$ gomplate -i '{{ random.String 8 }}'
+FODZ01u_
+```
+```console
+$ gomplate -i '{{ random.String 16 `[[:xdigit:]]` }}'
+B9e0527C3e45E1f3
+```
+```console
+$ gomplate -i '{{ random.String 20 `[\p{Canadian_Aboriginal}]` }}'
+ᗄᖖᣡᕔᕫᗝᖴᒙᗌᘔᓰᖫᗵᐕᗵᙔᗠᓅᕎᔹ
+```
+```console
+$ gomplate -i '{{ random.String 8 "c" "m" }}'
+ffmidgjc
+```
+```console
+$ gomplate -i 'You rolled... {{ random.String 3 "⚀" "⚅" }}'
+You rolled... ⚅⚂⚁
+```
+```console
+$ gomplate -i 'Poker time! {{ random.String 5 "\U0001f0a1" "\U0001f0de" }}'
+Poker time! 🂼🂺🂳🃅🂪
+```
+
+## `random.Item`
+
+Pick an element at a random from a given slice or array.
+
+### Usage
+
+```go
+random.Item items
+```
+```go
+items | random.Item
+```
+
+### Arguments
+
+| name | description |
+|------|-------------|
+| `items` | _(required)_ the input array |
+
+### Examples
+
+```console
+$ gomplate -i '{{ random.Item (seq 0 5) }}'
+4
+```
+```console
+$ export SLICE='["red", "green", "blue"]'
+$ gomplate -i '{{ getenv "SLICE" | jsonArray | random.Item }}'
+blue
+```
+
+## `random.Number`
+
+Pick a random integer. By default, a number between `0` and `100`
+(inclusive) is chosen, but this range can be overridden.
+
+Note that the difference between `min` and `max` can not be larger than a
+63-bit integer (i.e. the unsigned portion of a 64-bit signed integer).
+The result is given as an `int64`.
+
+### Usage
+
+```go
+random.Number [min] [max]
+```
+
+### Arguments
+
+| name | description |
+|------|-------------|
+| `min` | _(optional)_ The minimum value, defaults to `0`. Must be less than `max`. |
+| `max` | _(optional)_ The maximum value, defaults to `100` (if no args provided) |
+
+### Examples
+
+```console
+$ gomplate -i '{{ random.Number }}'
+55
+```
+```console
+$ gomplate -i '{{ random.Number -10 10 }}'
+-3
+```
+```console
+$ gomplate -i '{{ random.Number 5 }}'
+2
+```
+
+## `random.Float`
+
+Pick a random decimal floating-point number. By default, a number between
+`0.0` and `1.0` (_exclusive_, i.e. `[0.0,1.0)`) is chosen, but this range
+can be overridden.
+
+The result is given as a `float64`.
+
+### Usage
+
+```go
+random.Float [min] [max]
+```
+
+### Arguments
+
+| name | description |
+|------|-------------|
+| `min` | _(optional)_ The minimum value, defaults to `0.0`. Must be less than `max`. |
+| `max` | _(optional)_ The maximum value, defaults to `1.0` (if no args provided). |
+
+### Examples
+
+```console
+$ gomplate -i '{{ random.Float }}'
+0.2029946480303966
+```
+```console
+$ gomplate -i '{{ random.Float 100 }}'
+71.28595374161743
+```
+```console
+$ gomplate -i '{{ random.Float -100 200 }}'
+105.59119437834909
+```
diff --git a/funcs.go b/funcs.go
index 7d2c69ea..7d8c7378 100644
--- a/funcs.go
+++ b/funcs.go
@@ -28,5 +28,6 @@ func Funcs(d *data.Data) template.FuncMap {
funcs.AddTestFuncs(f)
funcs.AddCollFuncs(f)
funcs.AddUUIDFuncs(f)
+ funcs.AddRandomFuncs(f)
return f
}
diff --git a/funcs/random.go b/funcs/random.go
new file mode 100644
index 00000000..d4dce3e7
--- /dev/null
+++ b/funcs/random.go
@@ -0,0 +1,162 @@
+package funcs
+
+import (
+ "reflect"
+ "strconv"
+ "sync"
+ "unicode/utf8"
+
+ "github.com/hairyhenderson/gomplate/conv"
+ "github.com/hairyhenderson/gomplate/random"
+ "github.com/pkg/errors"
+)
+
+var (
+ randomNS *RandomFuncs
+ randomNSInit sync.Once
+)
+
+// RandomNS -
+func RandomNS() *RandomFuncs {
+ randomNSInit.Do(func() { randomNS = &RandomFuncs{} })
+ return randomNS
+}
+
+// AddRandomFuncs -
+func AddRandomFuncs(f map[string]interface{}) {
+ f["random"] = RandomNS
+}
+
+// RandomFuncs -
+type RandomFuncs struct{}
+
+// ASCII -
+func (f *RandomFuncs) ASCII(count interface{}) (string, error) {
+ return random.StringBounds(conv.ToInt(count), ' ', '~')
+}
+
+// Alpha -
+func (f *RandomFuncs) Alpha(count interface{}) (string, error) {
+ return random.StringRE(conv.ToInt(count), "[[:alpha:]]")
+}
+
+// AlphaNum -
+func (f *RandomFuncs) AlphaNum(count interface{}) (string, error) {
+ return random.StringRE(conv.ToInt(count), "[[:alnum:]]")
+}
+
+// String -
+func (f *RandomFuncs) String(count interface{}, args ...interface{}) (s string, err error) {
+ c := conv.ToInt(count)
+ if c == 0 {
+ return "", errors.New("count must be greater than 0")
+ }
+ m := ""
+ switch len(args) {
+ case 0:
+ m = ""
+ case 1:
+ m = conv.ToString(args[0])
+ case 2:
+ var l, u rune
+ if isString(args[0]) && isString(args[1]) {
+ l, u, err = toCodePoints(args[0].(string), args[1].(string))
+ if err != nil {
+ return "", err
+ }
+ } else {
+ l = rune(conv.ToInt(args[0]))
+ u = rune(conv.ToInt(args[1]))
+ }
+
+ return random.StringBounds(c, l, u)
+ }
+
+ return random.StringRE(c, m)
+}
+
+func isString(s interface{}) bool {
+ switch s.(type) {
+ case string:
+ return true
+ default:
+ return false
+ }
+}
+
+var rlen = utf8.RuneCountInString
+
+func toCodePoints(l, u string) (rune, rune, error) {
+ // no way are these representing valid printable codepoints - we'll treat
+ // them as runes
+ if rlen(l) == rlen(u) && rlen(l) == 1 {
+ lower, _ := utf8.DecodeRuneInString(l)
+ upper, _ := utf8.DecodeRuneInString(u)
+ return lower, upper, nil
+ }
+
+ li, err := strconv.ParseInt(l, 0, 32)
+ if err != nil {
+ return 0, 0, err
+ }
+ ui, err := strconv.ParseInt(u, 0, 32)
+ if err != nil {
+ return 0, 0, err
+ }
+
+ return rune(li), rune(ui), nil
+}
+
+func interfaceSlice(slice interface{}) ([]interface{}, error) {
+ s := reflect.ValueOf(slice)
+ kind := s.Kind()
+ switch kind {
+ case reflect.Slice, reflect.Array:
+ ret := make([]interface{}, s.Len())
+ for i := 0; i < s.Len(); i++ {
+ ret[i] = s.Index(i).Interface()
+ }
+ return ret, nil
+ default:
+ return nil, errors.Errorf("expected an array or slice, but got a %T", s)
+ }
+}
+
+// Item -
+func (f *RandomFuncs) Item(items interface{}) (interface{}, error) {
+ i, err := interfaceSlice(items)
+ if err != nil {
+ return nil, err
+ }
+ return random.Item(i)
+}
+
+// Number -
+func (f *RandomFuncs) Number(args ...interface{}) (int64, error) {
+ var min, max int64
+ min, max = 0, 100
+ switch len(args) {
+ case 0:
+ case 1:
+ max = conv.ToInt64(args[0])
+ case 2:
+ min = conv.ToInt64(args[0])
+ max = conv.ToInt64(args[1])
+ }
+ return random.Number(min, max)
+}
+
+// Float -
+func (f *RandomFuncs) Float(args ...interface{}) (float64, error) {
+ var min, max float64
+ min, max = 0, 1.0
+ switch len(args) {
+ case 0:
+ case 1:
+ max = conv.ToFloat64(args[0])
+ case 2:
+ min = conv.ToFloat64(args[0])
+ max = conv.ToFloat64(args[1])
+ }
+ return random.Float(min, max)
+}
diff --git a/funcs/random_test.go b/funcs/random_test.go
new file mode 100644
index 00000000..8f1a5a2f
--- /dev/null
+++ b/funcs/random_test.go
@@ -0,0 +1,175 @@
+package funcs
+
+import (
+ "testing"
+ "unicode/utf8"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestASCII(t *testing.T) {
+ f := &RandomFuncs{}
+ s, err := f.ASCII(0)
+ assert.NoError(t, err)
+ assert.Empty(t, s)
+
+ s, err = f.ASCII(100)
+ assert.NoError(t, err)
+ assert.Len(t, s, 100)
+ assert.Regexp(t, "^[[:print:]]*$", s)
+}
+
+func TestAlpha(t *testing.T) {
+ f := &RandomFuncs{}
+ s, err := f.Alpha(0)
+ assert.NoError(t, err)
+ assert.Empty(t, s)
+
+ s, err = f.Alpha(100)
+ assert.NoError(t, err)
+ assert.Len(t, s, 100)
+ assert.Regexp(t, "^[[:alpha:]]*$", s)
+}
+
+func TestAlphaNum(t *testing.T) {
+ f := &RandomFuncs{}
+ s, err := f.AlphaNum(0)
+ assert.NoError(t, err)
+ assert.Empty(t, s)
+
+ s, err = f.AlphaNum(100)
+ assert.NoError(t, err)
+ assert.Len(t, s, 100)
+ assert.Regexp(t, "^[[:alnum:]]*$", s)
+}
+
+func TestToCodePoints(t *testing.T) {
+ l, u, err := toCodePoints("a", "b")
+ assert.NoError(t, err)
+ assert.Equal(t, 'a', l)
+ assert.Equal(t, 'b', u)
+
+ _, _, err = toCodePoints("foo", "bar")
+ assert.Error(t, err)
+
+ _, _, err = toCodePoints("0755", "bar")
+ assert.Error(t, err)
+
+ l, u, err = toCodePoints("0xD700", "0x0001FFFF")
+ assert.NoError(t, err)
+ assert.Equal(t, '\ud700', l)
+ assert.Equal(t, '\U0001ffff', u)
+
+ l, u, err = toCodePoints("0011", "0777")
+ assert.NoError(t, err)
+ assert.Equal(t, rune(0011), l)
+ assert.Equal(t, rune(0777), u)
+
+ l, u, err = toCodePoints("♬", "♟")
+ assert.NoError(t, err)
+ assert.Equal(t, rune(0x266C), l)
+ assert.Equal(t, '♟', u)
+}
+
+func TestString(t *testing.T) {
+ f := &RandomFuncs{}
+ out, err := f.String(1)
+ assert.NoError(t, err)
+ assert.Len(t, out, 1)
+
+ out, err = f.String(42)
+ assert.NoError(t, err)
+ assert.Len(t, out, 42)
+
+ _, err = f.String(0)
+ assert.Error(t, err)
+
+ out, err = f.String(8, "[a-z]")
+ assert.NoError(t, err)
+ assert.Regexp(t, "^[a-z]{8}$", out)
+
+ out, err = f.String(10, 0x23, 0x26)
+ assert.NoError(t, err)
+ assert.Regexp(t, "^[#$%&]{10}$", out)
+
+ out, err = f.String(8, '\U0001f062', '\U0001f093')
+ assert.NoError(t, err)
+ assert.Regexp(t, "^[🁢-🂓]{8}$", out)
+
+ out, err = f.String(8, '\U0001f062', '\U0001f093')
+ assert.NoError(t, err)
+ assert.Regexp(t, "^[🁢-🂓]{8}$", out)
+
+ out, err = f.String(8, "♚", "♟")
+ assert.NoError(t, err)
+ assert.Regexp(t, "^[♚-♟]{8}$", out)
+
+ out, err = f.String(100, "♠", "♣")
+ assert.NoError(t, err)
+ assert.Equal(t, 100, utf8.RuneCountInString(out))
+ assert.Regexp(t, "^[♠-♣]{100}$", out)
+}
+
+func TestItem(t *testing.T) {
+ f := &RandomFuncs{}
+ _, err := f.Item(nil)
+ assert.Error(t, err)
+
+ _, err = f.Item("foo")
+ assert.Error(t, err)
+
+ i, err := f.Item([]string{"foo"})
+ assert.NoError(t, err)
+ assert.Equal(t, "foo", i)
+
+ in := []string{"foo", "bar"}
+ got := ""
+ for j := 0; j < 10; j++ {
+ i, err = f.Item(in)
+ assert.NoError(t, err)
+ got += i.(string)
+ }
+ assert.NotEqual(t, "foofoofoofoofoofoofoofoofoofoo", got)
+ assert.NotEqual(t, "barbarbarbarbarbarbarbarbarbar", got)
+}
+
+func TestNumber(t *testing.T) {
+ f := &RandomFuncs{}
+ n, err := f.Number()
+ assert.NoError(t, err)
+ assert.True(t, 0 < n && n < 100)
+
+ _, err = f.Number(-1)
+ assert.Error(t, err)
+
+ n, err = f.Number(0)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(0), n)
+
+ n, err = f.Number(9, 9)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(9), n)
+
+ n, err = f.Number(-10, -10)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(-10), n)
+}
+
+func TestFloat(t *testing.T) {
+ f := &RandomFuncs{}
+ n, err := f.Float()
+ assert.NoError(t, err)
+ assert.InDelta(t, 0.5, n, 0.5)
+
+ n, err = f.Float(0.5)
+ assert.NoError(t, err)
+ assert.InDelta(t, 0.25, n, 0.25)
+
+ n, err = f.Float(490, 500)
+ assert.NoError(t, err)
+ assert.InDelta(t, 495, n, 5)
+
+ n, err = f.Float(-500, 500)
+ assert.NoError(t, err)
+ assert.InDelta(t, 0, n, 500)
+}
diff --git a/random/random.go b/random/random.go
new file mode 100644
index 00000000..58a44d7d
--- /dev/null
+++ b/random/random.go
@@ -0,0 +1,109 @@
+package random
+
+import (
+ "math"
+ "math/rand"
+ "regexp"
+ "time"
+ "unicode"
+
+ "github.com/pkg/errors"
+)
+
+// Rnd -
+var Rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
+
+// Default set, matches "[a-zA-Z0-9_.-]"
+const defaultSet = "-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"
+
+// StringRE - Generate a random string that matches a given regular
+// expression. Defaults to "[a-zA-Z0-9_.-]"
+func StringRE(count int, match string) (r string, err error) {
+ var chars []rune
+ chars = []rune(defaultSet)
+ if match != "" {
+ chars, err = matchChars(match)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ return rndString(count, chars)
+}
+
+// StringBounds returns a random string of characters with a codepoint
+// between the lower and upper bounds. Only valid characters are returned
+// and if a range is given where no valid characters can be found, an error
+// will be returned.
+func StringBounds(count int, lower, upper rune) (r string, err error) {
+ chars := filterRange(lower, upper)
+ if len(chars) == 0 {
+ return "", errors.Errorf("No printable codepoints found between U%#q and U%#q.", lower, upper)
+ }
+ return rndString(count, chars)
+}
+
+// produce a string containing a random selection of given characters
+func rndString(count int, chars []rune) (string, error) {
+ s := make([]rune, count)
+ for i := range s {
+ s[i] = chars[Rnd.Intn(len(chars))]
+ }
+ return string(s), nil
+}
+
+func filterRange(lower, upper rune) []rune {
+ out := []rune{}
+ for r := lower; r <= upper; r++ {
+ if unicode.IsGraphic(r) {
+ out = append(out, r)
+ }
+ }
+ return out
+}
+
+func matchChars(match string) ([]rune, error) {
+ r, err := regexp.Compile(match)
+ if err != nil {
+ return nil, err
+ }
+ candidates := filterRange(0, unicode.MaxRune)
+ out := []rune{}
+ for _, c := range candidates {
+ if r.MatchString(string(c)) {
+ out = append(out, c)
+ }
+ }
+ return out, nil
+}
+
+// Item -
+func Item(items []interface{}) (interface{}, error) {
+ if len(items) == 0 {
+ return nil, errors.Errorf("expected a non-empty array or slice")
+ }
+ if len(items) == 1 {
+ return items[0], nil
+ }
+ n := Rnd.Intn(len(items))
+ return items[n], nil
+}
+
+// Number -
+func Number(min, max int64) (int64, error) {
+ if min > max {
+ return 0, errors.Errorf("min must not be greater than max (was %d, %d)", min, max)
+ }
+ if min == math.MinInt64 {
+ min++
+ }
+ if max-min >= (math.MaxInt64 >> 1) {
+ return 0, errors.Errorf("spread between min and max too high - must not be greater than 63-bit maximum (%d - %d = %d)", max, min, max-min)
+ }
+ return Rnd.Int63n(max-min+1) + min, nil
+}
+
+// Float - For now this is really just a wrapper around `rand.Float64`
+func Float(min, max float64) (float64, error) {
+ return min + Rnd.Float64()*(max-min), nil
+}
diff --git a/random/random_test.go b/random/random_test.go
new file mode 100644
index 00000000..7b6b49af
--- /dev/null
+++ b/random/random_test.go
@@ -0,0 +1,127 @@
+package random
+
+import (
+ "math"
+ "testing"
+ "unicode/utf8"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMatchChars(t *testing.T) {
+ in := "[a-g]"
+ expected := []rune("abcdefg")
+ out, err := matchChars(in)
+ assert.NoError(t, err)
+ assert.EqualValues(t, expected, out)
+
+ in = "[a-zA-Z0-9_.-]"
+ expected = []rune(defaultSet)
+ out, err = matchChars(in)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, out)
+
+ in = "[[:alpha:]]"
+ expected = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
+ out, err = matchChars(in)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, out)
+}
+
+func TestStringRE(t *testing.T) {
+ r, err := StringRE(15, "[\\p{Yi}[:alnum:]]")
+ assert.NoError(t, err)
+ assert.Equal(t, 15, utf8.RuneCountInString(r))
+
+ _, err = StringRE(1, "[bogus")
+ assert.Error(t, err)
+}
+
+func TestStringBounds(t *testing.T) {
+ _, err := StringBounds(15, 0, 19)
+ assert.Error(t, err)
+
+ // surrogate range isn't valid, should error
+ _, err = StringBounds(15, 0xd800, 0xdfff)
+ assert.Error(t, err)
+
+ r, err := StringBounds(1, 'a', 'a')
+ assert.NoError(t, err)
+ assert.Equal(t, "a", r)
+
+ r, err = StringBounds(99, 'a', 'b')
+ assert.NoError(t, err)
+ assert.Regexp(t, "^[a-b]+$", r)
+
+ r, err = StringBounds(100, 0x0020, 0x007f)
+ assert.NoError(t, err)
+ assert.Regexp(t, "^[\u0020-\u007f]*$", r)
+
+ // only 🂱 (\U0001F0B1) in this range is "graphic"
+ r, err = StringBounds(8, 0x0001f0af, 0x0001f0b1)
+ assert.NoError(t, err)
+ assert.Regexp(t, "^🂱🂱🂱🂱🂱🂱🂱🂱$", r)
+}
+
+func TestItem(t *testing.T) {
+ _, err := Item(nil)
+ assert.Error(t, err)
+
+ i, err := Item([]interface{}{"foo"})
+ assert.NoError(t, err)
+ assert.Equal(t, "foo", i)
+
+ in := []interface{}{"foo", "bar"}
+ got := ""
+ for j := 0; j < 10; j++ {
+ i, err = Item(in)
+ assert.NoError(t, err)
+ got += i.(string)
+ }
+ assert.NotEqual(t, "foofoofoofoofoofoofoofoofoofoo", got)
+ assert.NotEqual(t, "barbarbarbarbarbarbarbarbarbar", got)
+}
+
+func TestNumber(t *testing.T) {
+ _, err := Number(0, -1)
+ assert.Error(t, err)
+ _, err = Number(0, math.MaxInt64)
+ assert.Error(t, err)
+ _, err = Number(math.MinInt64, 0)
+ assert.Error(t, err)
+
+ testdata := []struct {
+ min, max, expected int64
+ delta float64
+ }{
+ {0, 100, 50, 50},
+ {0, 0, 0, 0},
+ {9, 9, 9, 0},
+ {-10, -10, -10, 0},
+ {-10, -0, -5, 5},
+ }
+ for _, d := range testdata {
+ n, err := Number(d.min, d.max)
+ assert.NoError(t, err)
+ assert.InDelta(t, d.expected, n, d.delta)
+ }
+}
+
+func TestFloat(t *testing.T) {
+ testdata := []struct {
+ min, max, expected float64
+ delta float64
+ }{
+ {0, 1.0, 0.5, 0.5},
+ {0, 0.5, 0.25, 0.25},
+ {490, 500, 495, 5},
+ {-500, 500, 0, 500},
+ {0, math.MaxFloat64, math.MaxFloat64 / 2, math.MaxFloat64 / 2},
+ }
+
+ for _, d := range testdata {
+ n, err := Float(d.min, d.max)
+ assert.NoError(t, err)
+ assert.InDelta(t, d.expected, n, d.delta)
+ }
+}