mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 03:38:42 -07:00
First pass variable support.
This allows blocked scoped variables to be set and retrieved as values using the $ prefix. e.g. foo = 22; bar = $foo Also supports env variables being used as variables and will properly parse to the correct type.
This commit is contained in:
21
conf/lex.go
21
conf/lex.go
@@ -38,6 +38,7 @@ const (
|
|||||||
itemMapStart
|
itemMapStart
|
||||||
itemMapEnd
|
itemMapEnd
|
||||||
itemCommentStart
|
itemCommentStart
|
||||||
|
itemVariable
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -178,7 +179,7 @@ func (lx *lexer) errorf(format string, values ...interface{}) stateFn {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// lexTop consumes elements at the top level of TOML data.
|
// lexTop consumes elements at the top level of data structure.
|
||||||
func lexTop(lx *lexer) stateFn {
|
func lexTop(lx *lexer) stateFn {
|
||||||
r := lx.next()
|
r := lx.next()
|
||||||
if isWhitespace(r) || isNL(r) {
|
if isWhitespace(r) || isNL(r) {
|
||||||
@@ -290,7 +291,7 @@ func lexQuotedKey(lx *lexer) stateFn {
|
|||||||
// is not whitespace) has already been consumed.
|
// is not whitespace) has already been consumed.
|
||||||
func lexKey(lx *lexer) stateFn {
|
func lexKey(lx *lexer) stateFn {
|
||||||
r := lx.peek()
|
r := lx.peek()
|
||||||
if isWhitespace(r) || isNL(r) || isKeySeparator(r) {
|
if isWhitespace(r) || isNL(r) || isKeySeparator(r) || r == eof {
|
||||||
lx.emit(itemKey)
|
lx.emit(itemKey)
|
||||||
return lexKeyEnd
|
return lexKeyEnd
|
||||||
}
|
}
|
||||||
@@ -308,6 +309,9 @@ func lexKeyEnd(lx *lexer) stateFn {
|
|||||||
return lexSkip(lx, lexKeyEnd)
|
return lexSkip(lx, lexKeyEnd)
|
||||||
case isKeySeparator(r):
|
case isKeySeparator(r):
|
||||||
return lexSkip(lx, lexValue)
|
return lexSkip(lx, lexValue)
|
||||||
|
case r == eof:
|
||||||
|
lx.emit(itemEOF)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
// We start the value here
|
// We start the value here
|
||||||
lx.backup()
|
lx.backup()
|
||||||
@@ -570,6 +574,15 @@ func (lx *lexer) isBool() bool {
|
|||||||
return str == "true" || str == "false" || str == "TRUE" || str == "FALSE"
|
return str == "true" || str == "false" || str == "TRUE" || str == "FALSE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the unquoted string is a variable reference, starting with $.
|
||||||
|
func (lx *lexer) isVariable() bool {
|
||||||
|
if lx.input[lx.start] == '$' {
|
||||||
|
lx.start += 1
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// lexQuotedString consumes the inner contents of a string. It assumes that the
|
// lexQuotedString consumes the inner contents of a string. It assumes that the
|
||||||
// beginning '"' has already been consumed and ignored. It will not interpret any
|
// beginning '"' has already been consumed and ignored. It will not interpret any
|
||||||
// internal contents.
|
// internal contents.
|
||||||
@@ -616,6 +629,8 @@ func lexString(lx *lexer) stateFn {
|
|||||||
lx.backup()
|
lx.backup()
|
||||||
if lx.isBool() {
|
if lx.isBool() {
|
||||||
lx.emit(itemBool)
|
lx.emit(itemBool)
|
||||||
|
} else if lx.isVariable() {
|
||||||
|
lx.emit(itemVariable)
|
||||||
} else {
|
} else {
|
||||||
lx.emit(itemString)
|
lx.emit(itemString)
|
||||||
}
|
}
|
||||||
@@ -918,6 +933,8 @@ func (itype itemType) String() string {
|
|||||||
return "MapEnd"
|
return "MapEnd"
|
||||||
case itemCommentStart:
|
case itemCommentStart:
|
||||||
return "CommentStart"
|
return "CommentStart"
|
||||||
|
case itemVariable:
|
||||||
|
return "Variable"
|
||||||
}
|
}
|
||||||
panic(fmt.Sprintf("BUG: Unknown type '%s'.", itype.String()))
|
panic(fmt.Sprintf("BUG: Unknown type '%s'.", itype.String()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ func expect(t *testing.T, lx *lexer, items []item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlainValue(t *testing.T) {
|
||||||
|
expectedItems := []item{
|
||||||
|
{itemKey, "foo", 1},
|
||||||
|
{itemEOF, "", 1},
|
||||||
|
}
|
||||||
|
lx := lex("foo")
|
||||||
|
expect(t, lx, expectedItems)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSimpleKeyStringValues(t *testing.T) {
|
func TestSimpleKeyStringValues(t *testing.T) {
|
||||||
expectedItems := []item{
|
expectedItems := []item{
|
||||||
{itemKey, "foo", 1},
|
{itemKey, "foo", 1},
|
||||||
@@ -192,6 +201,20 @@ func TestDateValues(t *testing.T) {
|
|||||||
expect(t, lx, expectedItems)
|
expect(t, lx, expectedItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVariableValues(t *testing.T) {
|
||||||
|
expectedItems := []item{
|
||||||
|
{itemKey, "foo", 1},
|
||||||
|
{itemVariable, "bar", 1},
|
||||||
|
{itemEOF, "", 1},
|
||||||
|
}
|
||||||
|
lx := lex("foo = $bar")
|
||||||
|
expect(t, lx, expectedItems)
|
||||||
|
lx = lex("foo =$bar")
|
||||||
|
expect(t, lx, expectedItems)
|
||||||
|
lx = lex("foo $bar")
|
||||||
|
expect(t, lx, expectedItems)
|
||||||
|
}
|
||||||
|
|
||||||
func TestArrays(t *testing.T) {
|
func TestArrays(t *testing.T) {
|
||||||
expectedItems := []item{
|
expectedItems := []item{
|
||||||
{itemKey, "foo", 1},
|
{itemKey, "foo", 1},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package conf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -157,11 +158,50 @@ func (p *parser) processItem(it item) error {
|
|||||||
array := p.ctx
|
array := p.ctx
|
||||||
p.popContext()
|
p.popContext()
|
||||||
p.setValue(array)
|
p.setValue(array)
|
||||||
|
case itemVariable:
|
||||||
|
if value, ok := p.lookupVariable(it.val); ok {
|
||||||
|
p.setValue(value)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Variable reference for '%s' on line %d can not be found.",
|
||||||
|
it.val, it.line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used to map an environment value into a temporary map to pass to secondary Parse call.
|
||||||
|
const pkey = "pk"
|
||||||
|
|
||||||
|
// lookupVariable will lookup a variable reference. It will use block scoping on keys
|
||||||
|
// it has seen before, with the top level scoping being the environment variables. We
|
||||||
|
// ignore array contexts and only process the map contexts..
|
||||||
|
//
|
||||||
|
// Returns true for ok if it finds something, similar to map.
|
||||||
|
func (p *parser) lookupVariable(varReference string) (interface{}, bool) {
|
||||||
|
// Loop through contexts currently on the stack.
|
||||||
|
for i := len(p.ctxs) - 1; i >= 0; i -= 1 {
|
||||||
|
ctx := p.ctxs[i]
|
||||||
|
// Process if it is a map context
|
||||||
|
if m, ok := ctx.(map[string]interface{}); ok {
|
||||||
|
if v, ok := m[varReference]; ok {
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are here, we have exhausted our context maps and still not found anything.
|
||||||
|
// Parse from the environment.
|
||||||
|
if vStr, ok := os.LookupEnv(varReference); ok {
|
||||||
|
// Everything we get here will be a string value, so we need to process as a parser would.
|
||||||
|
if vmap, err := Parse(fmt.Sprintf("%s=%s", pkey, vStr)); err == nil {
|
||||||
|
v, ok := vmap[pkey]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
func (p *parser) setValue(val interface{}) {
|
func (p *parser) setValue(val interface{}) {
|
||||||
// Test to see if we are on an array or a map
|
// Test to see if we are on an array or a map
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package conf
|
package conf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -32,6 +35,60 @@ func TestSimpleTopLevel(t *testing.T) {
|
|||||||
test(t, "foo='1'; bar=2.2; baz=true; boo=22", ex)
|
test(t, "foo='1'; bar=2.2; baz=true; boo=22", ex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var varSample = `
|
||||||
|
index = 22
|
||||||
|
foo = $index
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestSimpleVariable(t *testing.T) {
|
||||||
|
ex := map[string]interface{}{
|
||||||
|
"index": int64(22),
|
||||||
|
"foo": int64(22),
|
||||||
|
}
|
||||||
|
test(t, varSample, ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
var varNestedSample = `
|
||||||
|
index = 22
|
||||||
|
nest {
|
||||||
|
index = 11
|
||||||
|
foo = $index
|
||||||
|
}
|
||||||
|
bar = $index
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestNestedVariable(t *testing.T) {
|
||||||
|
ex := map[string]interface{}{
|
||||||
|
"index": int64(22),
|
||||||
|
"nest": map[string]interface{}{
|
||||||
|
"index": int64(11),
|
||||||
|
"foo": int64(11),
|
||||||
|
},
|
||||||
|
"bar": int64(22),
|
||||||
|
}
|
||||||
|
test(t, varNestedSample, ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMissingVariable(t *testing.T) {
|
||||||
|
_, err := Parse("foo=$index")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected an error for a missing variable, got none")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(err.Error(), "Variable reference") {
|
||||||
|
t.Fatalf("Wanted a variable reference err, got %q\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvVariable(t *testing.T) {
|
||||||
|
ex := map[string]interface{}{
|
||||||
|
"foo": int64(22),
|
||||||
|
}
|
||||||
|
evar := "__UNIQ22__"
|
||||||
|
os.Setenv(evar, "22")
|
||||||
|
defer os.Unsetenv(evar)
|
||||||
|
test(t, fmt.Sprintf("foo = $%s", evar), ex)
|
||||||
|
}
|
||||||
|
|
||||||
var sample1 = `
|
var sample1 = `
|
||||||
foo {
|
foo {
|
||||||
host {
|
host {
|
||||||
|
|||||||
Reference in New Issue
Block a user