From d2fbc8cce4b6f50dab04263b61fc9550a60a170a Mon Sep 17 00:00:00 2001 From: Oren Shomron Date: Fri, 6 Nov 2020 20:07:51 -0800 Subject: [PATCH] Write location parser for Mutations The location parser parses the location specification language used for matching Kubernetes object fields. Example: ``` p := NewParser("spec.containers[name:*].securityContext") root, err := p.Parse() ... Closes #915 Signed-off-by: Oren Shomron --- pkg/path/parser/doc.go | 19 +++ pkg/path/parser/node.go | 61 ++++++++ pkg/path/parser/parser.go | 153 ++++++++++++++++++++ pkg/path/parser/parser_test.go | 246 +++++++++++++++++++++++++++++++++ pkg/path/token/scanner.go | 180 ++++++++++++++++++++++++ pkg/path/token/scanner_test.go | 242 ++++++++++++++++++++++++++++++++ pkg/path/token/token.go | 39 ++++++ 7 files changed, 940 insertions(+) create mode 100644 pkg/path/parser/doc.go create mode 100644 pkg/path/parser/node.go create mode 100644 pkg/path/parser/parser.go create mode 100644 pkg/path/parser/parser_test.go create mode 100644 pkg/path/token/scanner.go create mode 100644 pkg/path/token/scanner_test.go create mode 100644 pkg/path/token/token.go diff --git a/pkg/path/parser/doc.go b/pkg/path/parser/doc.go new file mode 100644 index 00000000000..31b3579f75b --- /dev/null +++ b/pkg/path/parser/doc.go @@ -0,0 +1,19 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package parser provides a parser for a path specification language used +// in expressing Kubernetes object paths. +// See specification: https://docs.google.com/document/d/1MdchNFz9guycX__QMGxpJviPaT_MZs8iXaAFqCvoXYQ/edit#heading=h.ryydvhafooho +package parser diff --git a/pkg/path/parser/node.go b/pkg/path/parser/node.go new file mode 100644 index 00000000000..87715a87309 --- /dev/null +++ b/pkg/path/parser/node.go @@ -0,0 +1,61 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parser + +type NodeType string + +const ( + RootNode NodeType = "Root" + ListNode NodeType = "List" + ObjectNode NodeType = "Object" +) + +type Node interface { + Type() NodeType +} + +type Root struct { + Nodes []Node +} + +func (r Root) Type() NodeType { + return RootNode +} + +type Object struct { + Value string +} + +func (o Object) Type() NodeType { + return ObjectNode +} + +type List struct { + KeyField string + KeyValue *string + Glob bool +} + +func (l List) Type() NodeType { + return ListNode +} + +func (l List) Value() (string, bool) { + if l.KeyValue == nil { + return "", false + } + return *l.KeyValue, true +} diff --git a/pkg/path/parser/parser.go b/pkg/path/parser/parser.go new file mode 100644 index 00000000000..245146ac1fc --- /dev/null +++ b/pkg/path/parser/parser.go @@ -0,0 +1,153 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parser + +import ( + "errors" + "fmt" + + "github.com/open-policy-agent/gatekeeper/pkg/path/token" +) + +type Parser struct { + input string + scanner *token.Scanner + curToken token.Token + peekToken token.Token + err error +} + +func NewParser(input string) *Parser { + s := token.NewScanner(input) + p := &Parser{ + input: input, + scanner: s, + } + p.curToken = p.scanner.Next() + p.peekToken = p.scanner.Next() + return p +} + +// next advances to the next token in the stream. +func (p *Parser) next() { + p.curToken = p.peekToken + p.peekToken = p.scanner.Next() +} + +// expect returns whether the next token matches our expectation, +// and if so advances to that token. +// Otherwise returns false and doesn't advance. +func (p *Parser) expect(t token.Type) bool { + if p.peekToken.Type == t { + p.next() + return true + } + return false +} + +// expectPeek returns whether the next token matches our expectation. +// The current token is not advanced either way. +func (p *Parser) expectPeek(t token.Type) bool { + return p.peekToken.Type == t +} + +func (p *Parser) Parse() (*Root, error) { + root := &Root{} +loop: + for p.curToken.Type != token.EOF && p.err == nil { + var node Node + switch p.curToken.Type { + case token.IDENT: + node = p.parseObject() + case token.LBRACKET: + node = p.parseList() + default: + p.setError(fmt.Errorf("unexpected token: expected field name or eof, got: %s", p.peekToken.String())) + } + + if p.err != nil { + // Encountered parsing error, abort + return nil, p.err + } + + if node != nil { + root.Nodes = append(root.Nodes, node) + } + + // Advance past separator if needed and ensure no unexpected tokens follow + switch { + case p.expect(token.SEPARATOR): + if p.expectPeek(token.EOF) { + // block trailing separators + p.setError(errors.New("trailing separators are forbidden")) + return nil, p.err + } + // Skip past the separator + p.next() + case p.expect(token.LBRACKET): + // Allowed but don't advance past the bracket + case p.expect(token.EOF): + break loop + default: + p.setError(fmt.Errorf("expected '.' or eof, got: %s", p.peekToken.String())) + return nil, p.err + } + } + return root, nil +} + +// parseList tries to parse the current position as List match node, e.g. [key: val] +// returns nil if it cannot be parsed as a List. +func (p *Parser) parseList() Node { + out := &List{} + + // keyField is required + if !p.expect(token.IDENT) { + p.setError(fmt.Errorf("expected keyField in listSpec, got: %s", p.peekToken.String())) + return nil + } + + out.KeyField = p.curToken.Literal + + if !p.expect(token.COLON) { + p.setError(fmt.Errorf("expected ':' following keyField %s, got: %s", out.KeyField, p.peekToken.String())) + return nil + } + + switch { + case p.expect(token.GLOB): + out.Glob = true + case p.expect(token.IDENT): + // Optional + val := p.curToken.Literal + out.KeyValue = &val + } + + if !p.expect(token.RBRACKET) { + p.setError(fmt.Errorf("expected ']' following listSpec, got: %s", p.peekToken.String())) + return nil + } + return out +} + +func (p *Parser) parseObject() Node { + out := &Object{Value: p.curToken.Literal} + return out +} + +func (p *Parser) setError(err error) { + p.err = err +} diff --git a/pkg/path/parser/parser_test.go b/pkg/path/parser/parser_test.go new file mode 100644 index 00000000000..11225f64363 --- /dev/null +++ b/pkg/path/parser/parser_test.go @@ -0,0 +1,246 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parser + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestParser(t *testing.T) { + tests := []struct { + input string + expected []Node + expectErr bool + }{ + { + // empty returns empty + input: ``, + expected: nil, + }, + { + // we don't allow a leading separator + input: `.spec`, + expected: nil, + expectErr: true, + }, + { + // we don't allow a trailing separator + input: `spec.`, + expected: nil, + expectErr: true, + }, + { + input: `single_field`, + expected: []Node{ + &Object{Value: "single_field"}, + }, + }, + { + input: `spec.containers[name: *].securityContext`, + expected: []Node{ + &Object{Value: "spec"}, + &Object{Value: "containers"}, + &List{KeyField: "name", Glob: true}, + &Object{Value: "securityContext"}, + }, + }, + { + // A quoted '*' is a fieldValue, not a glob + input: `spec.containers[name: "*"].securityContext`, + expected: []Node{ + &Object{Value: "spec"}, + &Object{Value: "containers"}, + &List{KeyField: "name", KeyValue: strPtr("*"), Glob: false}, + &Object{Value: "securityContext"}, + }, + }, + { + input: `spec.containers[name: foo].securityContext`, + expected: []Node{ + &Object{Value: "spec"}, + &Object{Value: "containers"}, + &List{KeyField: "name", KeyValue: strPtr("foo")}, + &Object{Value: "securityContext"}, + }, + }, + { + input: `spec.containers["my key": "foo bar"]`, + expected: []Node{ + &Object{Value: "spec"}, + &Object{Value: "containers"}, + &List{KeyField: "my key", KeyValue: strPtr("foo bar")}, + }, + }, + { + // Error: keys with whitespace must be quoted + input: `spec.containers[my key: "foo bar"]`, + expectErr: true, + }, + { + // Error: values with whitespace must be quoted + input: `spec.containers[key: foo bar]`, + expectErr: true, + }, + { + input: `spec.containers[name: ""].securityContext`, + expected: []Node{ + &Object{Value: "spec"}, + &Object{Value: "containers"}, + &List{KeyField: "name", KeyValue: strPtr("")}, + &Object{Value: "securityContext"}, + }, + }, + { + // TODO: Is this useful or should we make it a parsing error? + input: `spec.containers[name: ].securityContext`, + expected: []Node{ + &Object{Value: "spec"}, + &Object{Value: "containers"}, + &List{KeyField: "name", KeyValue: nil}, + &Object{Value: "securityContext"}, + }, + }, + { + // parse error: listSpec requires keyField + input: `spec.containers[].securityContext`, + expectErr: true, + }, + { + input: `spec.containers[:].securityContext`, + expectErr: true, + }, + { + input: `spec.containers[:foo].securityContext`, + expectErr: true, + }, + { + input: `spec.containers[foo].securityContext`, + expectErr: true, + }, + { + // parse error: we don't allow empty segments + input: `foo..bar`, + expectErr: true, + }, + { + // ...but we do allow zero-string-named segments + input: `foo."".bar`, + expected: []Node{ + &Object{Value: "foo"}, + &Object{Value: ""}, + &Object{Value: "bar"}, + }, + }, + { + // whitespace can surround tokens + input: ` spec . containers `, + expected: []Node{ + &Object{Value: "spec"}, + &Object{Value: "containers"}, + }, + }, + { + // whitespace can surround tokens + input: ` spec . "containers" `, + expected: []Node{ + &Object{Value: "spec"}, + &Object{Value: "containers"}, + }, + }, + { + // TODO: Should this be allowed? + input: `[foo: bar][bar: *]`, + expected: []Node{ + &List{KeyField: "foo", KeyValue: strPtr("bar")}, + &List{KeyField: "bar", Glob: true}, + }, + }, + { + // allow leading dash + input: `-123-_456_`, + expected: []Node{ + &Object{Value: "-123-_456_"}, + }, + }, + { + // allow leading digits + input: `012345`, + expected: []Node{ + &Object{Value: "012345"}, + }, + }, + { + // whitespace must be quoted + input: `spec.foo bar`, + expectErr: true, + }, + { + // whitespace must be quoted + input: `spec."foo bar"`, + expected: []Node{ + &Object{Value: "spec"}, + &Object{Value: "foo bar"}, + }, + }, + { + // unexpected tokens + input: `*`, + expectErr: true, + }, + { + input: `][`, + expectErr: true, + }, + { + input: `foo[`, + expectErr: true, + }, + { + input: `[`, + expectErr: true, + }, + { + input: `:`, + expectErr: true, + }, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) { + p := NewParser(tc.input) + root, err := p.Parse() + if tc.expectErr != (err != nil) { + t.Fatalf("for input: %s\nunexpected error: %v", tc.input, err) + } + var nodes []Node + if root != nil { + nodes = root.Nodes + } + diff := cmp.Diff(tc.expected, nodes) + if diff != "" { + t.Errorf("for input: %s\ngot unexpected results: %s", tc.input, diff) + } + }) + } + +} + +func strPtr(s string) *string { + return &s +} diff --git a/pkg/path/token/scanner.go b/pkg/path/token/scanner.go new file mode 100644 index 00000000000..fe9db2f2d98 --- /dev/null +++ b/pkg/path/token/scanner.go @@ -0,0 +1,180 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package token + +import ( + "errors" + "fmt" + "strings" + "unicode/utf8" +) + +const eof = rune(-1) + +type Scanner struct { + input string + pos int // Current position + readPos int // Next position to read + ch rune + err error // Last error if any +} + +func NewScanner(input string) *Scanner { + s := &Scanner{input: input} + s.read() + return s +} + +func (s *Scanner) Next() Token { + var tok Token + s.skipWhitespace() + + switch s.ch { + case eof: + tok = Token{Type: EOF, Literal: ""} + case '.': + tok = Token{Type: SEPARATOR, Literal: string(s.ch)} + case '[': + tok = Token{Type: LBRACKET, Literal: string(s.ch)} + case ']': + tok = Token{Type: RBRACKET, Literal: string(s.ch)} + case '*': + tok = Token{Type: GLOB, Literal: string(s.ch)} + case ':': + tok = Token{Type: COLON, Literal: string(s.ch)} + case '"', '\'': + tok.Type = IDENT + str, err := s.readString() + if err != nil { + tok.Type = ERROR + } + tok.Literal = str + default: + if isAlphaNum(s.ch) { + tok.Type = IDENT + str, err := s.readIdent() + if err != nil { + tok.Type = ERROR + } + tok.Literal = str + return tok // Return early to avoid consuming the separator we may be positioned on below. + } + + // default: current character is invalid at this location + s.setError(errors.New("invalid character")) + tok = Token{Type: ERROR, Literal: string(s.ch)} + } + + s.read() + return tok +} + +// read consumes the next rune and advances. +func (s *Scanner) read() rune { + if s.readPos >= len(s.input) { + s.ch = eof + s.pos = len(s.input) + return eof + } + r, w := utf8.DecodeRuneInString(s.input[s.readPos:]) + s.pos = s.readPos // Mark last read position + s.readPos += w // Advance for next read + s.ch = r + return r +} + +// readString consumes a string token. +func (s *Scanner) readString() (string, error) { + quote := s.ch // Will be ' or " + var out strings.Builder + + for { + s.read() + switch { + case s.ch == quote: + // String terminated + return out.String(), nil + case s.ch == '\\': + // Escaped character + s.read() + if s.ch == eof { + continue + } + out.WriteRune(s.ch) + case s.ch == eof: + // Unterminated string + s.setError(errors.New("unterminated string")) + return out.String(), s.err + + default: + out.WriteRune(s.ch) + } + } +} + +func (s *Scanner) readIdent() (string, error) { + start := s.pos + for isAlphaNum(s.ch) { + s.read() + } + return s.input[start:s.pos], s.err +} + +func (s *Scanner) setError(err error) { + s.err = ScanError{ + Inner: err, + Position: s.pos, + } +} + +func isSpace(r rune) bool { + return r == ' ' || r == '\t' +} + +func isAlphaNum(r rune) bool { + switch { + case 'a' <= r && r <= 'z': + case 'A' <= r && r <= 'Z': + case '0' <= r && r <= '9': + case r == '_': + case r == '-': + + default: + return false + + } + return true +} + +func (s *Scanner) skipWhitespace() { + for isSpace(s.ch) { + s.read() + } +} + +type ScanError struct { + Inner error + Position int +} + +func (e ScanError) Error() string { + var innerMsg string + if e.Inner != nil { + innerMsg = e.Inner.Error() + + } + return fmt.Sprintf("error at position %d: %s", e.Position, innerMsg) +} diff --git a/pkg/path/token/scanner_test.go b/pkg/path/token/scanner_test.go new file mode 100644 index 00000000000..141509e3802 --- /dev/null +++ b/pkg/path/token/scanner_test.go @@ -0,0 +1,242 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package token + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestScanner(t *testing.T) { + tests := []struct { + input string + expected []Token + }{ + { + input: `foo.bar.baz`, + expected: []Token{ + {Type: IDENT, Literal: "foo"}, + {Type: SEPARATOR, Literal: "."}, + {Type: IDENT, Literal: "bar"}, + {Type: SEPARATOR, Literal: "."}, + {Type: IDENT, Literal: "baz"}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: ` foo . bar . baz `, + expected: []Token{ + {Type: IDENT, Literal: "foo"}, + {Type: SEPARATOR, Literal: "."}, + {Type: IDENT, Literal: "bar"}, + {Type: SEPARATOR, Literal: "."}, + {Type: IDENT, Literal: "baz"}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: ` "foo bar" . . baz `, + expected: []Token{ + {Type: IDENT, Literal: "foo bar"}, + {Type: SEPARATOR, Literal: "."}, + {Type: SEPARATOR, Literal: "."}, + {Type: IDENT, Literal: "baz"}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: ` `, + expected: []Token{ + {Type: EOF, Literal: ""}, + }, + }, + { + input: `0123_foobar_baz`, + expected: []Token{ + {Type: IDENT, Literal: "0123_foobar_baz"}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: ` .0123_foobar_baz .`, + expected: []Token{ + {Type: SEPARATOR, Literal: "."}, + {Type: IDENT, Literal: "0123_foobar_baz"}, + {Type: SEPARATOR, Literal: "."}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `0123_foobar-baz`, + expected: []Token{ + {Type: IDENT, Literal: "0123_foobar-baz"}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `-valid-identifier-`, + expected: []Token{ + {Type: IDENT, Literal: `-valid-identifier-`}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `'...'."..."`, + expected: []Token{ + {Type: IDENT, Literal: `...`}, + {Type: SEPARATOR, Literal: `.`}, + {Type: IDENT, Literal: `...`}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `..''`, + expected: []Token{ + {Type: SEPARATOR, Literal: `.`}, + {Type: SEPARATOR, Literal: `.`}, + {Type: IDENT, Literal: ``}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `spec."this object"."is very"["much full": 'of everyone\'s'].'favorite thing'`, + expected: []Token{ + {Type: IDENT, Literal: "spec"}, + {Type: SEPARATOR, Literal: "."}, + {Type: IDENT, Literal: "this object"}, + {Type: SEPARATOR, Literal: "."}, + {Type: IDENT, Literal: "is very"}, + {Type: LBRACKET, Literal: "["}, + {Type: IDENT, Literal: "much full"}, + {Type: COLON, Literal: ":"}, + {Type: IDENT, Literal: "of everyone's"}, + {Type: RBRACKET, Literal: "]"}, + {Type: SEPARATOR, Literal: "."}, + {Type: IDENT, Literal: "favorite thing"}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `"won't \"confuse\" the [scanner: *nope*]"`, + expected: []Token{ + {Type: IDENT, Literal: `won't "confuse" the [scanner: *nope*]`}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `'won\'t "confuse" the [scanner: *nope*]'`, + expected: []Token{ + {Type: IDENT, Literal: `won't "confuse" the [scanner: *nope*]`}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `][**:**][`, + expected: []Token{ + {Type: RBRACKET, Literal: `]`}, + {Type: LBRACKET, Literal: `[`}, + {Type: GLOB, Literal: `*`}, + {Type: GLOB, Literal: `*`}, + {Type: COLON, Literal: `:`}, + {Type: GLOB, Literal: `*`}, + {Type: GLOB, Literal: `*`}, + {Type: RBRACKET, Literal: `]`}, + {Type: LBRACKET, Literal: `[`}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `"foo" "bar"`, + expected: []Token{ + {Type: IDENT, Literal: `foo`}, + {Type: IDENT, Literal: `bar`}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `foo bar`, + expected: []Token{ + {Type: IDENT, Literal: `foo`}, + {Type: IDENT, Literal: `bar`}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `"unterminated string '`, + expected: []Token{ + {Type: ERROR, Literal: `unterminated string '`}, + }, + }, + { + input: `"also unterminated\"`, + expected: []Token{ + {Type: ERROR, Literal: `also unterminated"`}, + }, + }, + { + input: `"🤔☕️❗️"`, + expected: []Token{ + {Type: IDENT, Literal: `🤔☕️❗️`}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `"🦋bb\🐥aa🦄"`, + expected: []Token{ + {Type: IDENT, Literal: `🦋bb🐥aa🦄`}, + {Type: EOF, Literal: ""}, + }, + }, + { + input: `Moo🐙`, + expected: []Token{ + {Type: IDENT, Literal: `Moo`}, + {Type: ERROR, Literal: `🐙`}, + }, + }, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + s := NewScanner(tc.input) + var tokens []Token + for { + tok := s.Next() + tokens = append(tokens, tok) + if tok.Type == EOF || tok.Type == ERROR { + break + } + } + + diff := cmp.Diff(tc.expected, tokens) + if diff != "" { + t.Errorf("for input: %s\nunexpected tokens: %s", tc.input, diff) + } + }) + } +} + +func TestScanner_EOF(t *testing.T) { + s := NewScanner("") + expected := Token{Type: EOF, Literal: ""} + for i := 0; i < 5; i++ { + if tok := s.Next(); tok != expected { + t.Errorf("[%d]: unexpected token: %s", i, tok.String()) + } + } +} diff --git a/pkg/path/token/token.go b/pkg/path/token/token.go new file mode 100644 index 00000000000..b1bd05bc9e7 --- /dev/null +++ b/pkg/path/token/token.go @@ -0,0 +1,39 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package token + +import "fmt" + +const ( + ERROR = "ERROR" + EOF = "EOF" + IDENT = "IDENT" + LBRACKET = "LBRACKET" + RBRACKET = "RBRACKET" + SEPARATOR = "SEPARATOR" + GLOB = "GLOB" + COLON = "COLON" +) + +type Type string +type Token struct { + Type Type + Literal string +} + +func (t Token) String() string { + return fmt.Sprintf("%s: %q", t.Type, t.Literal) +}