diff --git a/README.md b/README.md index 630fda4..873a4e0 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,6 @@ ### Examples -- JSON: https://github.com/aryszka/treerack/blob/master/examples/json.treerack -- Scheme: https://github.com/aryszka/treerack/blob/master/examples/scheme.treerack -- Treerack (itself): https://github.com/aryszka/treerack/blob/master/syntax.treerack +- JSON: https://code.squareroundforest.org/arpio/treerack/blob/master/examples/json.treerack +- Scheme: https://code.squareroundforest.org/arpio/treerack/blob/master/examples/scheme.treerack +- Treerack (itself): https://code.squareroundforest.org/arpio/treerack/blob/master/syntax.treerack diff --git a/cmd/treerack/doc.go b/cmd/treerack/doc.go index 8361f7d..29b4abe 100644 --- a/cmd/treerack/doc.go +++ b/cmd/treerack/doc.go @@ -5,7 +5,7 @@ import ( "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: check validates an arbitrary input against a syntax definition @@ -18,7 +18,7 @@ See more details about a particular command by calling: treerack -help` 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." diff --git a/cmd/treerack/generate.go b/cmd/treerack/generate.go index dea26f8..fd1f334 100644 --- a/cmd/treerack/generate.go +++ b/cmd/treerack/generate.go @@ -1,6 +1,6 @@ package main -import "github.com/aryszka/treerack" +import "code.squareroundforest.org/arpio/treerack" type generateOptions struct { command *commandOptions diff --git a/cmd/treerack/open.go b/cmd/treerack/open.go index 91ad9db..0fc3894 100644 --- a/cmd/treerack/open.go +++ b/cmd/treerack/open.go @@ -6,8 +6,7 @@ import ( "io" "io/ioutil" "os" - - "github.com/aryszka/treerack" + "code.squareroundforest.org/arpio/treerack" "golang.org/x/crypto/ssh/terminal" ) diff --git a/cmd/treerack/show.go b/cmd/treerack/show.go index cb94814..a016e46 100644 --- a/cmd/treerack/show.go +++ b/cmd/treerack/show.go @@ -2,8 +2,7 @@ package main import ( "encoding/json" - - "github.com/aryszka/treerack" + "code.squareroundforest.org/arpio/treerack" ) type showOptions struct { diff --git a/eskip_test.go b/eskip_test.go deleted file mode 100644 index 68e1136..0000000 --- a/eskip_test.go +++ /dev/null @@ -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) -} diff --git a/examples/eskip.treerack b/examples/eskip.treerack deleted file mode 100644 index 3a6cdcf..0000000 --- a/examples/eskip.treerack +++ /dev/null @@ -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 = ""; -loopback = ""; -backend:alias = address | shunt | loopback; - -expression = predicates ("->" filters)? "->" backend; - -id:alias = symbol; -definition = id ":" expression; - -definitions:alias = ";"* definition (";"+ definition)* ";"*; diff --git a/gendoc.go b/gendoc.go index 022548f..ac7b0e0 100644 --- a/gendoc.go +++ b/gendoc.go @@ -4,12 +4,12 @@ package treerack // only to the source code generated with treerack. 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 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 license of this source code. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1922b7e --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fd8f3eb --- /dev/null +++ b/go.sum @@ -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= diff --git a/node.go b/node.go index 5c4140a..2dac3e5 100644 --- a/node.go +++ b/node.go @@ -1,6 +1,6 @@ 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 { var nn []*Node diff --git a/self/self.go b/self/self.go index 668fb7b..cdd529f 100644 --- a/self/self.go +++ b/self/self.go @@ -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 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 license of this source code. diff --git a/syntax.go b/syntax.go index 91c6905..40d6644 100644 --- a/syntax.go +++ b/syntax.go @@ -4,8 +4,7 @@ import ( "errors" "fmt" "io" - - "github.com/aryszka/treerack/self" + "code.squareroundforest.org/arpio/treerack/self" ) // if min=0&&max=0, it means min=1,max=1