commit fa4f403c4658e71680b1b0e50d51967b7e383502 Author: Aneurin Barker Snook Date: Sat Oct 7 10:22:56 2023 +0100 add arango package with aql generation diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d9f1d41 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/recipeer/go/arango + +go 1.21.1 diff --git a/params.go b/params.go new file mode 100644 index 0000000..6087213 --- /dev/null +++ b/params.go @@ -0,0 +1,22 @@ +package arango + +import ( + "regexp" +) + +// https://docs.arangodb.com/3.11/aql/fundamentals/bind-parameters/#syntax +var paramRegexp = regexp.MustCompile("@{1,2}([A-z0-9_]+)") + +// ReadParams reads out named parameters from an AQL string. +func ReadParams(input string) []string { + params := []string{} + + matches := paramRegexp.FindAllStringSubmatch(input, -1) + if matches != nil { + for _, match := range matches { + params = append(params, match[1]) + } + } + + return params +} diff --git a/params_test.go b/params_test.go new file mode 100644 index 0000000..c25f5e7 --- /dev/null +++ b/params_test.go @@ -0,0 +1,35 @@ +package arango + +import "testing" + +func TestReadParams(t *testing.T) { + type TestCase struct { + Input string + Output []string + } + + testCases := []TestCase{ + { + Input: "FOR doc IN @@collection FILTER doc.title == @title RETURN doc", + Output: []string{"collection", "title"}, + }, + } + + for _, testCase := range testCases { + t.Log("Input:", testCase.Input) + t.Log("Expected output:", testCase.Output) + + params := ReadParams(testCase.Input) + + if len(params) != len(testCase.Output) { + t.Errorf("Expected %d parameters", len(testCase.Output)) + break + } + + for i, name := range testCase.Output { + if params[i] != name { + t.Errorf("Expected parameter %d to be %q", i, name) + } + } + } +} diff --git a/query.go b/query.go new file mode 100644 index 0000000..d45dc4e --- /dev/null +++ b/query.go @@ -0,0 +1,87 @@ +package arango + +import ( + "strings" +) + +// Query provides a simple way to build ArangoDB queries. +type Query struct { + Lines []string + Params map[string]any +} + +// Append adds a line to the query, attaching (optional) values to bind parameters. +// +// Any variables given are assigned to bind parameters in the same order as they appear in the line. +// Keep in mind the following behaviours: +// +// - If more variables are provided than there are parameters in the line, leftover variables are discarded +// - If more parameters are used than variables are given, remaining parameters are left unmapped +// - Reused parameters are overwritten +func (query *Query) Append(line string, values ...any) *Query { + var params map[string]any = nil + + names := ReadParams(line) + if len(names) > 0 { + params = map[string]any{} + for i, name := range names { + if i > len(values) { + break + } + params[name] = values[i] + } + } + + query.Lines = append(query.Lines, line) + return query.AssignMap(params) +} + +// Assign assigns a value to a single bind parameter. +func (query *Query) Assign(name string, value any) *Query { + query.Params[name] = value + return query +} + +// AssignMap assigns values to bind parameters. +func (query *Query) AssignMap(params map[string]any) *Query { + if params != nil { + for name, value := range params { + query.Params[name] = value + } + } + return query +} + +// Copy creates a copy of the query. +func (query *Query) Copy() *Query { + newQuery := NewQuery() + for _, line := range query.Lines { + newQuery.Lines = append(newQuery.Lines, line) + } + for name, value := range query.Params { + newQuery.Params[name] = value + } + return newQuery +} + +// L (for "Line") is a shorthand for Append. +func (query *Query) L(line string, values ...any) *Query { + return query.Append(line, values...) +} + +// P (for "Parameter") is a shorthand for Assign. +func (query *Query) P(name string, value any) *Query { + return query.Assign(name, value) +} + +func (query *Query) String() string { + return strings.Join(query.Lines, "\n") +} + +// NewQuery creates a new Query. +func NewQuery() *Query { + return &Query{ + Lines: []string{}, + Params: map[string]any{}, + } +} diff --git a/query_test.go b/query_test.go new file mode 100644 index 0000000..b5afdc3 --- /dev/null +++ b/query_test.go @@ -0,0 +1,51 @@ +package arango + +import ( + "testing" +) + +func TestQueryAppend(t *testing.T) { + type TestCase struct { + Input *Query + ExpectedStr string + ExpectedParams map[string]any + } + + testCases := []TestCase{ + { + Input: NewQuery(). + Append("FOR doc IN @@collection", "recipes"). + Append("FILTER doc.title == @title", "Spaghetti"). + Append("RETURN doc"), + ExpectedStr: `FOR doc IN @@collection +FILTER doc.title == @title +RETURN doc`, + ExpectedParams: map[string]any{ + "collection": "recipes", + "title": "Spaghetti", + }, + }, + } + + for _, testCase := range testCases { + actualStr := testCase.Input.String() + + if actualStr != testCase.ExpectedStr { + t.Logf("Expected: %q", testCase.ExpectedStr) + t.Logf("Actual: %q", actualStr) + t.Fail() + } + + if len(testCase.Input.Params) != len(testCase.ExpectedParams) { + t.Errorf("Expected %d parameters; got %d", len(testCase.ExpectedParams), len(testCase.Input.Params)) + } + + for name, value := range testCase.ExpectedParams { + if testCase.Input.Params[name] == nil { + t.Errorf("Expected parameter %q to be %q; got nil", name, value) + } else if testCase.Input.Params[name] != value { + t.Errorf("Expected parameter %q to be %q; got %q", name, value, testCase.Input.Params[name]) + } + } + } +}