470 lines
12 KiB
Go
470 lines
12 KiB
Go
package barkfile
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
func ParseFile(path string) (Config, error) {
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("read Barkfile: %w", err)
|
|
}
|
|
return Parse(contents)
|
|
}
|
|
|
|
// Parse parses a Barkfile document using the current Barkfile schema.
|
|
func Parse(input []byte) (Config, error) {
|
|
tokens, err := lex(string(input))
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
p := parser{tokens: tokens}
|
|
return p.parse()
|
|
}
|
|
|
|
type tokenKind uint8
|
|
|
|
const (
|
|
tokenWord tokenKind = iota
|
|
tokenOpenBrace
|
|
tokenCloseBrace
|
|
tokenNewline
|
|
tokenEOF
|
|
)
|
|
|
|
type token struct {
|
|
kind tokenKind
|
|
text string
|
|
line int
|
|
}
|
|
|
|
type parser struct {
|
|
tokens []token
|
|
index int
|
|
}
|
|
|
|
func (p *parser) parse() (Config, error) {
|
|
var cfg Config
|
|
pawSQLSeen := false
|
|
for {
|
|
p.skipNewlines()
|
|
if p.current().kind == tokenEOF {
|
|
break
|
|
}
|
|
if p.current().kind != tokenWord {
|
|
return Config{}, p.errorf("expected top-level service block")
|
|
}
|
|
switch p.current().text {
|
|
case "pawsql":
|
|
if pawSQLSeen {
|
|
return Config{}, p.errorf("pawsql may only be specified once")
|
|
}
|
|
pawSQLSeen = true
|
|
p.index++
|
|
pawSQL, err := p.parsePawSQL()
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.Listen = pawSQL.Listen
|
|
cfg.TLS = pawSQL.TLS
|
|
cfg.Databases = pawSQL.Databases
|
|
case "treatvault":
|
|
if cfg.TreatVault != nil {
|
|
return Config{}, p.errorf("treatvault may only be specified once")
|
|
}
|
|
p.index++
|
|
treatVault, err := p.parseTreatVault()
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.TreatVault = &treatVault
|
|
default:
|
|
return Config{}, p.errorf("unknown top-level service %q", p.current().text)
|
|
}
|
|
}
|
|
if !pawSQLSeen {
|
|
return Config{}, p.errorf("pawsql block is required")
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (p *parser) parsePawSQL() (Config, error) {
|
|
if err := p.expect(tokenOpenBrace, "{"); err != nil {
|
|
return Config{}, err
|
|
}
|
|
var cfg Config
|
|
for {
|
|
p.skipNewlines()
|
|
if p.current().kind == tokenCloseBrace {
|
|
p.index++
|
|
return cfg, nil
|
|
}
|
|
if p.current().kind == tokenEOF {
|
|
return Config{}, p.errorf("expected } to close pawsql block")
|
|
}
|
|
if p.current().kind != tokenWord {
|
|
return Config{}, p.errorf("expected directive")
|
|
}
|
|
switch p.current().text {
|
|
case "listen":
|
|
if cfg.Listen != "" {
|
|
return Config{}, p.errorf("listen may only be specified once")
|
|
}
|
|
p.index++
|
|
value, err := p.value("listen address")
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.Listen = value
|
|
if err := p.endLine(); err != nil {
|
|
return Config{}, err
|
|
}
|
|
case "tls":
|
|
if cfg.TLS.CertFile != "" || cfg.TLS.KeyFile != "" {
|
|
return Config{}, p.errorf("tls may only be specified once")
|
|
}
|
|
p.index++
|
|
tlsConfig, err := p.parseTLS()
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.TLS = tlsConfig
|
|
case "database":
|
|
p.index++
|
|
name, err := p.value("database name")
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
database, err := p.parseDatabase(name)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.Databases = append(cfg.Databases, database)
|
|
default:
|
|
return Config{}, p.errorf("unknown directive %q", p.current().text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseTreatVault() (TreatVaultConfig, error) {
|
|
if err := p.expect(tokenOpenBrace, "{"); err != nil {
|
|
return TreatVaultConfig{}, err
|
|
}
|
|
var cfg TreatVaultConfig
|
|
for {
|
|
p.skipNewlines()
|
|
if p.current().kind == tokenCloseBrace {
|
|
p.index++
|
|
return cfg, nil
|
|
}
|
|
if p.current().kind == tokenEOF {
|
|
return TreatVaultConfig{}, p.errorf("expected } to close treatvault block")
|
|
}
|
|
directive := p.current()
|
|
if directive.kind != tokenWord {
|
|
return TreatVaultConfig{}, p.errorf("expected treatvault directive")
|
|
}
|
|
p.index++
|
|
value, err := p.value("treatvault value")
|
|
if err != nil {
|
|
return TreatVaultConfig{}, err
|
|
}
|
|
switch directive.text {
|
|
case "file":
|
|
if cfg.File != "" {
|
|
return TreatVaultConfig{}, fmt.Errorf("line %d: file may only be specified once", directive.line)
|
|
}
|
|
cfg.File = value
|
|
case "identity_secret":
|
|
if cfg.IdentitySecret != "" {
|
|
return TreatVaultConfig{}, fmt.Errorf("line %d: identity_secret may only be specified once", directive.line)
|
|
}
|
|
cfg.IdentitySecret = value
|
|
default:
|
|
return TreatVaultConfig{}, fmt.Errorf("line %d: unknown treatvault directive %q", directive.line, directive.text)
|
|
}
|
|
if err := p.endLine(); err != nil {
|
|
return TreatVaultConfig{}, err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseTLS() (TLSConfig, error) {
|
|
if err := p.expect(tokenOpenBrace, "{"); err != nil {
|
|
return TLSConfig{}, err
|
|
}
|
|
var cfg TLSConfig
|
|
for {
|
|
p.skipNewlines()
|
|
if p.current().kind == tokenCloseBrace {
|
|
p.index++
|
|
return cfg, nil
|
|
}
|
|
if p.current().kind == tokenEOF {
|
|
return TLSConfig{}, p.errorf("expected } to close tls block")
|
|
}
|
|
name := p.current()
|
|
if name.kind != tokenWord {
|
|
return TLSConfig{}, p.errorf("expected tls directive")
|
|
}
|
|
p.index++
|
|
value, err := p.value("tls value")
|
|
if err != nil {
|
|
return TLSConfig{}, err
|
|
}
|
|
switch name.text {
|
|
case "cert":
|
|
if cfg.CertFile != "" {
|
|
return TLSConfig{}, fmt.Errorf("line %d: cert may only be specified once", name.line)
|
|
}
|
|
cfg.CertFile = value
|
|
case "key":
|
|
if cfg.KeyFile != "" {
|
|
return TLSConfig{}, fmt.Errorf("line %d: key may only be specified once", name.line)
|
|
}
|
|
cfg.KeyFile = value
|
|
default:
|
|
return TLSConfig{}, fmt.Errorf("line %d: unknown tls directive %q", name.line, name.text)
|
|
}
|
|
if err := p.endLine(); err != nil {
|
|
return TLSConfig{}, err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseDatabase(name string) (DatabaseConfig, error) {
|
|
if err := p.expect(tokenOpenBrace, "{"); err != nil {
|
|
return DatabaseConfig{}, err
|
|
}
|
|
database := DatabaseConfig{Name: name}
|
|
for {
|
|
p.skipNewlines()
|
|
if p.current().kind == tokenCloseBrace {
|
|
p.index++
|
|
return database, nil
|
|
}
|
|
if p.current().kind == tokenEOF {
|
|
return DatabaseConfig{}, p.errorf("expected } to close database block")
|
|
}
|
|
directive := p.current()
|
|
if directive.kind != tokenWord {
|
|
return DatabaseConfig{}, p.errorf("expected database directive")
|
|
}
|
|
p.index++
|
|
if directive.text == "postgres" {
|
|
if database.Postgres != nil {
|
|
return DatabaseConfig{}, fmt.Errorf("line %d: postgres may only be specified once", directive.line)
|
|
}
|
|
postgres, err := p.parsePostgres()
|
|
if err != nil {
|
|
return DatabaseConfig{}, err
|
|
}
|
|
database.Postgres = &postgres
|
|
continue
|
|
}
|
|
value, err := p.value("database value")
|
|
if err != nil {
|
|
return DatabaseConfig{}, err
|
|
}
|
|
switch directive.text {
|
|
case "hostname":
|
|
if database.Hostname != "" {
|
|
return DatabaseConfig{}, fmt.Errorf("line %d: hostname may only be specified once", directive.line)
|
|
}
|
|
database.Hostname = value
|
|
case "upstream":
|
|
if database.Upstream != "" {
|
|
return DatabaseConfig{}, fmt.Errorf("line %d: upstream may only be specified once", directive.line)
|
|
}
|
|
database.Upstream = value
|
|
default:
|
|
return DatabaseConfig{}, fmt.Errorf("line %d: unknown database directive %q", directive.line, directive.text)
|
|
}
|
|
if err := p.endLine(); err != nil {
|
|
return DatabaseConfig{}, err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *parser) parsePostgres() (PostgresConfig, error) {
|
|
if err := p.expect(tokenOpenBrace, "{"); err != nil {
|
|
return PostgresConfig{}, err
|
|
}
|
|
var cfg PostgresConfig
|
|
idleTimeoutSet := false
|
|
trafficIdleTimeoutSet := false
|
|
for {
|
|
p.skipNewlines()
|
|
if p.current().kind == tokenCloseBrace {
|
|
p.index++
|
|
return cfg, nil
|
|
}
|
|
if p.current().kind == tokenEOF {
|
|
return PostgresConfig{}, p.errorf("expected } to close postgres block")
|
|
}
|
|
directive := p.current()
|
|
if directive.kind != tokenWord {
|
|
return PostgresConfig{}, p.errorf("expected postgres directive")
|
|
}
|
|
p.index++
|
|
value, err := p.value("postgres value")
|
|
if err != nil {
|
|
return PostgresConfig{}, err
|
|
}
|
|
switch directive.text {
|
|
case "image":
|
|
if cfg.Image != "" {
|
|
return PostgresConfig{}, fmt.Errorf("line %d: image may only be specified once", directive.line)
|
|
}
|
|
cfg.Image = value
|
|
case "volume":
|
|
if cfg.Volume != "" {
|
|
return PostgresConfig{}, fmt.Errorf("line %d: volume may only be specified once", directive.line)
|
|
}
|
|
cfg.Volume = value
|
|
case "password_secret":
|
|
if cfg.PasswordSecret != "" {
|
|
return PostgresConfig{}, fmt.Errorf("line %d: password_secret may only be specified once", directive.line)
|
|
}
|
|
cfg.PasswordSecret = value
|
|
case "idle_timeout":
|
|
if idleTimeoutSet {
|
|
return PostgresConfig{}, fmt.Errorf("line %d: idle_timeout may only be specified once", directive.line)
|
|
}
|
|
idleTimeoutSet = true
|
|
timeout, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return PostgresConfig{}, fmt.Errorf("line %d: invalid idle_timeout %q: %w", directive.line, value, err)
|
|
}
|
|
cfg.IdleTimeout = timeout
|
|
case "traffic_idle_timeout":
|
|
if trafficIdleTimeoutSet {
|
|
return PostgresConfig{}, fmt.Errorf("line %d: traffic_idle_timeout may only be specified once", directive.line)
|
|
}
|
|
trafficIdleTimeoutSet = true
|
|
timeout, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return PostgresConfig{}, fmt.Errorf("line %d: invalid traffic_idle_timeout %q: %w", directive.line, value, err)
|
|
}
|
|
cfg.TrafficIdleTimeout = timeout
|
|
default:
|
|
return PostgresConfig{}, fmt.Errorf("line %d: unknown postgres directive %q", directive.line, directive.text)
|
|
}
|
|
if err := p.endLine(); err != nil {
|
|
return PostgresConfig{}, err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *parser) value(description string) (string, error) {
|
|
current := p.current()
|
|
if current.kind != tokenWord {
|
|
return "", p.errorf("expected %s", description)
|
|
}
|
|
p.index++
|
|
return current.text, nil
|
|
}
|
|
|
|
func (p *parser) endLine() error {
|
|
if p.current().kind == tokenNewline {
|
|
p.skipNewlines()
|
|
return nil
|
|
}
|
|
if p.current().kind == tokenCloseBrace || p.current().kind == tokenEOF {
|
|
return nil
|
|
}
|
|
return p.errorf("expected end of line")
|
|
}
|
|
|
|
func (p *parser) expectWord(word string) error {
|
|
if p.current().kind != tokenWord || p.current().text != word {
|
|
return p.errorf("expected %q", word)
|
|
}
|
|
p.index++
|
|
return nil
|
|
}
|
|
func (p *parser) expect(kind tokenKind, name string) error {
|
|
if p.current().kind != kind {
|
|
return p.errorf("expected %s", name)
|
|
}
|
|
p.index++
|
|
return nil
|
|
}
|
|
func (p *parser) skipNewlines() {
|
|
for p.current().kind == tokenNewline {
|
|
p.index++
|
|
}
|
|
}
|
|
func (p *parser) current() token { return p.tokens[p.index] }
|
|
func (p *parser) errorf(format string, args ...any) error {
|
|
return fmt.Errorf("line %d: %s", p.current().line, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
func lex(input string) ([]token, error) {
|
|
var tokens []token
|
|
line := 1
|
|
for index := 0; index < len(input); {
|
|
ch := input[index]
|
|
switch {
|
|
case ch == '#':
|
|
for index < len(input) && input[index] != '\n' {
|
|
index++
|
|
}
|
|
case ch == '\n':
|
|
tokens = append(tokens, token{kind: tokenNewline, line: line})
|
|
index++
|
|
line++
|
|
case unicode.IsSpace(rune(ch)):
|
|
index++
|
|
case ch == '{':
|
|
tokens = append(tokens, token{kind: tokenOpenBrace, text: "{", line: line})
|
|
index++
|
|
case ch == '}':
|
|
tokens = append(tokens, token{kind: tokenCloseBrace, text: "}", line: line})
|
|
index++
|
|
case ch == '"':
|
|
startLine := line
|
|
index++
|
|
var value strings.Builder
|
|
terminated := false
|
|
for index < len(input) {
|
|
if input[index] == '\n' {
|
|
return nil, fmt.Errorf("line %d: unterminated quoted string", startLine)
|
|
}
|
|
if input[index] == '"' {
|
|
index++
|
|
terminated = true
|
|
break
|
|
}
|
|
if input[index] == '\\' && index+1 < len(input) {
|
|
index++
|
|
value.WriteByte(input[index])
|
|
index++
|
|
continue
|
|
}
|
|
value.WriteByte(input[index])
|
|
index++
|
|
}
|
|
if !terminated {
|
|
return nil, fmt.Errorf("line %d: unterminated quoted string", startLine)
|
|
}
|
|
tokens = append(tokens, token{kind: tokenWord, text: value.String(), line: startLine})
|
|
default:
|
|
start := index
|
|
for index < len(input) && !unicode.IsSpace(rune(input[index])) && !strings.ContainsRune("{}#\"", rune(input[index])) {
|
|
index++
|
|
}
|
|
if start == index {
|
|
return nil, fmt.Errorf("line %d: unexpected character %q", line, input[index])
|
|
}
|
|
tokens = append(tokens, token{kind: tokenWord, text: input[start:index], line: line})
|
|
}
|
|
}
|
|
tokens = append(tokens, token{kind: tokenEOF, line: line})
|
|
return tokens, nil
|
|
}
|