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) { p.skipNewlines() if err := p.expectWord("pawsql"); err != nil { return Config{}, err } if err := p.expect(tokenOpenBrace, "{"); err != nil { return Config{}, err } var cfg Config for { p.skipNewlines() if p.current().kind == tokenCloseBrace { p.index++ break } 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) } } p.skipNewlines() if p.current().kind != tokenEOF { return Config{}, p.errorf("unexpected content after pawsql block") } return cfg, nil } 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_env": if cfg.PasswordEnv != "" { return PostgresConfig{}, fmt.Errorf("line %d: password_env may only be specified once", directive.line) } cfg.PasswordEnv = 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 }