add go modules

This commit is contained in:
Arpad Ryszka 2025-08-19 00:56:08 +02:00
parent a96580b2fd
commit a8bec182b1
13 changed files with 30 additions and 817 deletions

View File

@ -8,6 +8,6 @@
### Examples ### Examples
- JSON: https://github.com/aryszka/treerack/blob/master/examples/json.treerack - JSON: https://code.squareroundforest.org/arpio/treerack/blob/master/examples/json.treerack
- Scheme: https://github.com/aryszka/treerack/blob/master/examples/scheme.treerack - Scheme: https://code.squareroundforest.org/arpio/treerack/blob/master/examples/scheme.treerack
- Treerack (itself): https://github.com/aryszka/treerack/blob/master/syntax.treerack - Treerack (itself): https://code.squareroundforest.org/arpio/treerack/blob/master/syntax.treerack

View File

@ -5,7 +5,7 @@ import (
"unicode/utf8" "unicode/utf8"
) )
const summary = `treerack - parser generator - https://github.com/aryszka/treerack` const summary = `treerack - parser generator - https://code.squareroundforest.org/aryszka/treerack`
const commandsHelp = `Available commands: const commandsHelp = `Available commands:
check validates an arbitrary input against a syntax definition check validates an arbitrary input against a syntax definition
@ -18,7 +18,7 @@ See more details about a particular command by calling:
treerack <command> -help` treerack <command> -help`
const docRef = `See more documentation about the definition syntax and the parser output at const docRef = `See more documentation about the definition syntax and the parser output at
https://github.com/aryszka/treerack.` https://code.squareroundforest.org/arpio/treerack.`
const positionalSyntaxUsage = "The path to the syntax file is accepted as a positional argument." const positionalSyntaxUsage = "The path to the syntax file is accepted as a positional argument."

View File

@ -1,6 +1,6 @@
package main package main
import "github.com/aryszka/treerack" import "code.squareroundforest.org/arpio/treerack"
type generateOptions struct { type generateOptions struct {
command *commandOptions command *commandOptions

View File

@ -6,8 +6,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"code.squareroundforest.org/arpio/treerack"
"github.com/aryszka/treerack"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
) )

View File

@ -2,8 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"code.squareroundforest.org/arpio/treerack"
"github.com/aryszka/treerack"
) )
type showOptions struct { type showOptions struct {

View File

@ -1,750 +0,0 @@
package treerack
import (
"bytes"
"errors"
"fmt"
"math/rand"
"strconv"
"strings"
"testing"
"github.com/zalando/skipper/eskip"
)
const (
maxID = 27
meanID = 9
setPathChance = 0.72
maxPathTags = 12
meanPathTags = 2
maxPathTag = 24
meanPathTag = 9
setHostChance = 0.5
maxHost = 48
meanHost = 24
setPathRegexpChance = 0.45
maxPathRegexp = 36
meanPathRegexp = 12
setMethodChance = 0.1
setHeadersChance = 0.3
maxHeadersLength = 6
meanHeadersLength = 1
maxHeaderKeyLength = 18
meanHeaderKeyLength = 12
maxHeaderValueLength = 48
meanHeaderValueLength = 6
setHeaderRegexpChance = 0.05
maxHeaderRegexpsLength = 3
meanHeaderRegexpsLength = 1
maxHeaderRegexpLength = 12
meanHeaderRegexpLength = 6
maxTermNameLength = 15
meanTermNameLength = 6
maxTermArgsLength = 6
meanTermArgsLength = 1
floatArgChance = 0.1
intArgChance = 0.3
maxTermStringLength = 24
meanTermStringLength = 6
maxPredicatesLength = 4
meanPredicatesLength = 1
maxFiltersLength = 18
meanFiltersLength = 3
loopBackendChance = 0.05
shuntBackendChance = 0.1
maxBackend = 48
meanBackend = 15
)
func takeChance(c float64) bool {
return rand.Float64() < c
}
func generateID() string {
return generateString(maxID, meanID)
}
func generatePath() string {
if !takeChance(setPathChance) {
return ""
}
l := randomLength(maxPathTags, meanPathTags)
p := append(make([]string, 0, l+1), "")
for i := 0; i < l; i++ {
p = append(p, generateString(maxPathTag, meanPathTag))
}
return strings.Join(p, "/")
}
func generateHostRegexps() []string {
if !takeChance(setHostChance) {
return nil
}
return []string{generateString(maxHost, meanHost)}
}
func generatePathRegexps() []string {
if !takeChance(setPathRegexpChance) {
return nil
}
return []string{generateString(maxPathRegexp, meanPathRegexp)}
}
func generateMethod() string {
if !takeChance(setMethodChance) {
return ""
}
methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}
return methods[rand.Intn(len(methods))]
}
func generateHeaders() map[string]string {
if !takeChance(setHeadersChance) {
return nil
}
h := make(map[string]string)
for i := 0; i < randomLength(maxHeadersLength, meanHeadersLength); i++ {
h[generateString(maxHeaderKeyLength, meanHeaderKeyLength)] =
generateString(maxHeaderValueLength, meanHeaderValueLength)
}
return h
}
func generateHeaderRegexps() map[string][]string {
if !takeChance(setHeaderRegexpChance) {
return nil
}
h := make(map[string][]string)
for i := 0; i < randomLength(maxHeaderRegexpsLength, meanHeaderRegexpsLength); i++ {
k := generateString(maxHeaderKeyLength, meanHeaderKeyLength)
for i := 0; i < randomLength(maxHeaderRegexpLength, meanHeaderRegexpLength); i++ {
h[k] = append(h[k], generateString(maxHeaderValueLength, meanHeaderValueLength))
}
}
return h
}
func generateTerm() (string, []interface{}) {
n := generateString(maxTermNameLength, meanTermNameLength)
al := randomLength(maxTermArgsLength, meanTermArgsLength)
a := make([]interface{}, 0, al)
for i := 0; i < al; i++ {
at := rand.Float64()
switch {
case at < floatArgChance:
a = append(a, rand.NormFloat64())
case at < intArgChance:
a = append(a, rand.Int())
default:
a = append(a, generateString(maxTermStringLength, meanTermStringLength))
}
}
return n, a
}
func generatePredicates() []*eskip.Predicate {
l := randomLength(maxPredicatesLength, meanPredicatesLength)
p := make([]*eskip.Predicate, 0, l)
for i := 0; i < l; i++ {
pi := &eskip.Predicate{}
pi.Name, pi.Args = generateTerm()
p = append(p, pi)
}
return p
}
func generateFilters() []*eskip.Filter {
l := randomLength(maxFiltersLength, meanFiltersLength)
f := make([]*eskip.Filter, 0, l)
for i := 0; i < l; i++ {
fi := &eskip.Filter{}
fi.Name, fi.Args = generateTerm()
f = append(f, fi)
}
return f
}
func generateBackend() (eskip.BackendType, string) {
t := rand.Float64()
switch {
case t < loopBackendChance:
return eskip.LoopBackend, ""
case t < loopBackendChance+shuntBackendChance:
return eskip.ShuntBackend, ""
default:
return eskip.NetworkBackend, generateString(maxBackend, meanBackend)
}
}
func generateRoute() *eskip.Route {
r := &eskip.Route{}
r.Id = generateID()
r.Path = generatePath()
r.HostRegexps = generateHostRegexps()
r.PathRegexps = generatePathRegexps()
r.Method = generateMethod()
r.Headers = generateHeaders()
r.HeaderRegexps = generateHeaderRegexps()
r.Predicates = generatePredicates()
r.Filters = generateFilters()
r.BackendType, r.Backend = generateBackend()
return r
}
func generateEskip(l int) []*eskip.Route {
r := make([]*eskip.Route, 0, l)
for i := 0; i < l; i++ {
r = append(r, generateRoute())
}
return r
}
func parseEskipInt(s string) (int, error) {
i, err := strconv.ParseInt(s, 0, 64)
return int(i), err
}
func parseEskipFloat(s string) (float64, error) {
f, err := strconv.ParseFloat(s, 64)
return f, err
}
func unquote(s string, escapedChars string) (string, error) {
if len(s) < 2 {
return "", nil
}
b := make([]byte, 0, len(s)-2)
var escaped bool
for _, bi := range []byte(s[1 : len(s)-1]) {
if escaped {
switch bi {
case 'b':
bi = '\b'
case 'f':
bi = '\f'
case 'n':
bi = '\n'
case 'r':
bi = '\r'
case 't':
bi = '\t'
case 'v':
bi = '\v'
}
b = append(b, bi)
escaped = false
continue
}
for _, ec := range []byte(escapedChars) {
if ec == bi {
return "", errors.New("invalid quote")
}
}
if bi == '\\' {
escaped = true
continue
}
b = append(b, bi)
}
return string(b), nil
}
func unquoteString(s string) (string, error) {
return unquote(s, "\"")
}
func unquoteRegexp(s string) (string, error) {
return unquote(s, "/")
}
func nodeToArg(n *Node) (interface{}, error) {
switch n.Name {
case "int":
return parseEskipInt(n.Text())
case "float":
return parseEskipFloat(n.Text())
case "string":
return unquoteString(n.Text())
case "regexp":
return unquoteRegexp(n.Text())
default:
return nil, errors.New("invalid arg")
}
}
func nodeToTerm(n *Node) (string, []interface{}, error) {
if len(n.Nodes) < 1 || n.Nodes[0].Name != "symbol" {
return "", nil, errors.New("invalid term")
}
name := n.Nodes[0].Text()
var args []interface{}
for _, ni := range n.Nodes[1:] {
a, err := nodeToArg(ni)
if err != nil {
return "", nil, err
}
args = append(args, a)
}
return name, args, nil
}
func nodeToPredicate(r *eskip.Route, n *Node) error {
name, args, err := nodeToTerm(n)
if err != nil {
return err
}
switch name {
case "Path":
if len(args) != 1 {
return errors.New("invalid path predicate")
}
p, ok := args[0].(string)
if !ok {
return errors.New("invalid path predicate")
}
r.Path = p
case "Host":
if len(args) != 1 {
return errors.New("invalid host predicate")
}
h, ok := args[0].(string)
if !ok {
return errors.New("invalid host predicate")
}
r.HostRegexps = append(r.HostRegexps, h)
case "PathRegexp":
if len(args) != 1 {
return errors.New("invalid path regexp predicate")
}
p, ok := args[0].(string)
if !ok {
return errors.New("invalid path regexp predicate")
}
r.PathRegexps = append(r.PathRegexps, p)
case "Method":
if len(args) != 1 {
return errors.New("invalid method predicate")
}
m, ok := args[0].(string)
if !ok {
return errors.New("invalid method predicate")
}
r.Method = m
case "Header":
if len(args) != 2 {
return errors.New("invalid header predicate")
}
name, ok := args[0].(string)
if !ok {
return errors.New("invalid header predicate")
}
value, ok := args[1].(string)
if !ok {
return errors.New("invalid header predicate")
}
if r.Headers == nil {
r.Headers = make(map[string]string)
}
r.Headers[name] = value
case "HeaderRegexp":
if len(args) != 2 {
return errors.New("invalid header regexp predicate")
}
name, ok := args[0].(string)
if !ok {
return errors.New("invalid header regexp predicate")
}
value, ok := args[1].(string)
if !ok {
return errors.New("invalid header regexp predicate")
}
if r.HeaderRegexps == nil {
r.HeaderRegexps = make(map[string][]string)
}
r.HeaderRegexps[name] = append(r.HeaderRegexps[name], value)
default:
r.Predicates = append(r.Predicates, &eskip.Predicate{Name: name, Args: args})
}
return nil
}
func nodeToFilter(n *Node) (*eskip.Filter, error) {
name, args, err := nodeToTerm(n)
if err != nil {
return nil, err
}
return &eskip.Filter{Name: name, Args: args}, nil
}
func nodeToBackend(r *eskip.Route, n *Node) error {
switch n.Name {
case "string":
b, err := unquoteString(n.Text())
if err != nil {
return err
}
r.BackendType = eskip.NetworkBackend
r.Backend = b
case "shunt":
r.BackendType = eskip.ShuntBackend
case "loopback":
r.BackendType = eskip.LoopBackend
default:
return errors.New("invalid backend type")
}
return nil
}
func nodeToEskipDefinition(n *Node) (*eskip.Route, error) {
ns := n.Nodes
if len(ns) < 2 || len(ns[1].Nodes) == 0 {
return nil, fmt.Errorf("invalid definition length: %d", len(ns))
}
r := &eskip.Route{}
if ns[0].Name != "symbol" {
return nil, errors.New("invalid definition id")
}
r.Id, ns = ns[0].Text(), ns[1].Nodes
predicates:
for i, ni := range ns {
switch ni.Name {
case "predicate":
if err := nodeToPredicate(r, ni); err != nil {
return nil, err
}
case "filter", "string", "shunt", "loopback":
ns = ns[i:]
break predicates
default:
return nil, errors.New("invalid definition item among predicates")
}
}
filters:
for i, ni := range ns {
switch ni.Name {
case "filter":
f, err := nodeToFilter(ni)
if err != nil {
return nil, err
}
r.Filters = append(r.Filters, f)
case "string", "shunt", "loopback":
ns = ns[i:]
break filters
default:
return nil, errors.New("invalid definition item among filters")
}
}
if len(ns) != 1 {
return nil, fmt.Errorf("invalid definition backend, remaining definition length: %d, %s",
len(ns), n.Text())
}
if err := nodeToBackend(r, ns[0]); err != nil {
return nil, err
}
return r, nil
}
func eskipTreeToEskip(n []*Node) ([]*eskip.Route, error) {
r := make([]*eskip.Route, 0, len(n))
for _, ni := range n {
d, err := nodeToEskipDefinition(ni)
if err != nil {
return nil, err
}
r = append(r, d)
}
return r, nil
}
func checkTerm(t *testing.T, gotName, expectedName string, gotArgs, expectedArgs []interface{}) {
if gotName != expectedName {
t.Error("invalid term name")
return
}
if len(gotArgs) != len(expectedArgs) {
t.Error("invalid term args length in:", gotName, len(gotArgs), len(expectedArgs))
return
}
// legacy bug support, dropping numeric arguments:
for i, a := range gotArgs {
ea := expectedArgs[i]
switch a.(type) {
case int, float64:
switch ea.(type) {
case int, float64:
gotArgs = append(gotArgs[:i], gotArgs[i+1:]...)
expectedArgs = append(expectedArgs[:i], expectedArgs[i+1:]...)
default:
t.Error("invalid argument type at:", i)
}
}
}
for i, a := range gotArgs {
if a != expectedArgs[i] {
t.Error("invalid term arg")
return
}
}
}
func checkPredicates(t *testing.T, got, expected *eskip.Route) {
if got.Path != expected.Path {
t.Error("invalid path")
return
}
if len(got.HostRegexps) != len(expected.HostRegexps) {
t.Error("invalid host length")
return
}
for i, h := range got.HostRegexps {
if h != expected.HostRegexps[i] {
t.Error("invalid host")
return
}
}
if len(got.PathRegexps) != len(expected.PathRegexps) {
t.Error("invalid path regexp length", len(got.PathRegexps), len(expected.PathRegexps))
return
}
for i, h := range got.PathRegexps {
if h != expected.PathRegexps[i] {
t.Error("invalid path regexp")
return
}
}
if got.Method != expected.Method {
t.Error("invalid method")
return
}
if len(got.Headers) != len(expected.Headers) {
t.Error("invalid headers length")
return
}
for n, h := range got.Headers {
he, ok := expected.Headers[n]
if !ok {
t.Error("invalid header name")
return
}
if he != h {
t.Error("invalid header")
return
}
}
if len(got.HeaderRegexps) != len(expected.HeaderRegexps) {
t.Error("invalid header regexp length")
return
}
for n, h := range got.HeaderRegexps {
he, ok := expected.HeaderRegexps[n]
if !ok {
t.Error("invalid header regexp name")
return
}
if len(h) != len(he) {
t.Error("invalid header regexp item length")
return
}
for i, hi := range h {
if hi != he[i] {
t.Error("invalid header regexp")
return
}
}
}
if len(got.Predicates) != len(expected.Predicates) {
t.Error("invalid predicates length")
return
}
for i, p := range got.Predicates {
checkTerm(
t,
p.Name, expected.Predicates[i].Name,
p.Args, expected.Predicates[i].Args,
)
if t.Failed() {
t.Log(p.Name, expected.Predicates[i].Name)
t.Log(p.Args, expected.Predicates[i].Args)
return
}
}
}
func checkFilters(t *testing.T, got, expected []*eskip.Filter) {
if len(got) != len(expected) {
t.Error("invalid filters length")
return
}
for i, f := range got {
checkTerm(
t,
f.Name, expected[i].Name,
f.Args, expected[i].Args,
)
if t.Failed() {
return
}
}
}
func checkBackend(t *testing.T, got, expected *eskip.Route) {
if got.BackendType != expected.BackendType {
t.Error("invalid backend type")
return
}
if got.Backend != expected.Backend {
t.Error("invalid backend")
return
}
}
func checkRoute(t *testing.T, got, expected *eskip.Route) {
if got.Id != expected.Id {
t.Error("invalid route id")
return
}
checkPredicates(t, got, expected)
if t.Failed() {
return
}
checkFilters(t, got.Filters, expected.Filters)
if t.Failed() {
return
}
checkBackend(t, got, expected)
}
func checkEskip(t *testing.T, got, expected []*eskip.Route) {
if len(got) != len(expected) {
t.Error("invalid length", len(got), len(expected))
return
}
for i, ri := range got {
checkRoute(t, ri, expected[i])
if t.Failed() {
t.Log(ri.String())
t.Log(expected[i].String())
return
}
}
}
func TestEskip(t *testing.T) {
const count = 1 << 9
r := generateEskip(count)
e := eskip.Print(eskip.PrettyPrintInfo{Pretty: true}, r...)
b := bytes.NewBufferString(e)
s, err := openSyntaxFile("examples/eskip.treerack")
if err != nil {
t.Error(err)
return
}
n, err := s.Parse(b)
if err != nil {
t.Error(err)
return
}
rback, err := eskipTreeToEskip(n.Nodes)
if err != nil {
t.Error(err)
return
}
checkEskip(t, rback, r)
}

View File

@ -1,50 +0,0 @@
/*
Eskip routing configuration format for Skipper: https://github.com/zalando/skipper
*/
eskip:root = (expression | definitions)?;
space:ws = [ \n\b\f\r\t\v];
comment:ws = "//" [^\n]*;
decimal-digit:alias = [0-9];
octal-digit:alias = [0-7];
hexa-digit:alias = [0-9a-fA-F];
decimal:alias:nows = [1-9] decimal-digit*;
octal:alias:nows = "0" octal-digit*;
hexa:alias:nows = "0" [xX] hexa-digit+;
int = decimal | octal | hexa;
exponent:alias:nows = [eE] [+\-]? decimal-digit+;
float:nows = decimal-digit+ "." decimal-digit* exponent?
| "." decimal-digit+ exponent?
| decimal-digit+ exponent;
number:alias:nows = "-"? (int | float);
string:nows = "\"" ([^\\"] | "\\" .)* "\"";
regexp:nows = "/" ([^\\/] | "\\" .)* "/";
symbol:nows = [a-zA-Z_] [a-zA-z0-9_]*;
arg:alias = number | string | regexp;
args:alias = arg ("," arg)*;
term:alias = symbol "(" args? ")";
predicate = term;
predicates:alias = "*" | predicate ("&&" predicate)*;
filter = term;
filters:alias = filter ("->" filter)*;
address:alias = string;
shunt = "<shunt>";
loopback = "<loopback>";
backend:alias = address | shunt | loopback;
expression = predicates ("->" filters)? "->" backend;
id:alias = symbol;
definition = id ":" expression;
definitions:alias = ";"* definition (";"+ definition)* ";"*;

View File

@ -4,12 +4,12 @@ package treerack
// only to the source code generated with treerack. // only to the source code generated with treerack.
const gendoc = ` const gendoc = `
/* /*
This file was generated with treerack (https://github.com/aryszka/treerack). This file was generated with treerack (https://code.squareroundforest.org/arpio/treerack).
The contents of this file fall under different licenses. The contents of this file fall under different licenses.
The code between the "// head" and "// eo head" lines falls under the same The code between the "// head" and "// eo head" lines falls under the same
license as the source code of treerack (https://github.com/aryszka/treerack), license as the source code of treerack (https://code.squareroundforest.org/arpio/treerack),
unless explicitly stated otherwise, if treerack's license allows changing the unless explicitly stated otherwise, if treerack's license allows changing the
license of this source code. license of this source code.

10
go.mod Normal file
View File

@ -0,0 +1,10 @@
module code.squareroundforest.org/arpio/treerack
go 1.24.6
require golang.org/x/crypto v0.41.0
require (
golang.org/x/sys v0.35.0 // indirect
golang.org/x/term v0.34.0 // indirect
)

6
go.sum Normal file
View File

@ -0,0 +1,6 @@
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=

View File

@ -1,6 +1,6 @@
package treerack package treerack
import "github.com/aryszka/treerack/self" import "code.squareroundforest.org/arpio/treerack/self"
func mapNodes(m func(n *Node) *Node, n []*Node) []*Node { func mapNodes(m func(n *Node) *Node, n []*Node) []*Node {
var nn []*Node var nn []*Node

View File

@ -1,10 +1,10 @@
/* /*
This file was generated with treerack (https://github.com/aryszka/treerack). This file was generated with treerack (https://code.squareroundforest.org/arpio/treerack).
The contents of this file fall under different licenses. The contents of this file fall under different licenses.
The code between the "// head" and "// eo head" lines falls under the same The code between the "// head" and "// eo head" lines falls under the same
license as the source code of treerack (https://github.com/aryszka/treerack), license as the source code of treerack (https://code.squareroundforest.org/arpio/treerack),
unless explicitly stated otherwise, if treerack's license allows changing the unless explicitly stated otherwise, if treerack's license allows changing the
license of this source code. license of this source code.

View File

@ -4,8 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"code.squareroundforest.org/arpio/treerack/self"
"github.com/aryszka/treerack/self"
) )
// if min=0&&max=0, it means min=1,max=1 // if min=0&&max=0, it means min=1,max=1