select executor

This commit is contained in:
booleanmaybe 2026-04-03 15:39:57 -04:00
parent 308c1f8a5e
commit 1ee7c5a5fe
7 changed files with 3082 additions and 37 deletions

884
ruki/executor.go Normal file
View file

@ -0,0 +1,884 @@
package ruki
import (
"fmt"
"sort"
"strings"
"time"
"github.com/boolean-maybe/tiki/task"
)
// Executor evaluates parsed ruki statements against a set of tasks.
type Executor struct {
schema Schema
userFunc func() string
}
// NewExecutor constructs an Executor with the given schema and user function.
// If userFunc is nil, calling user() at runtime will return "".
func NewExecutor(schema Schema, userFunc func() string) *Executor {
if userFunc == nil {
userFunc = func() string { return "" }
}
return &Executor{schema: schema, userFunc: userFunc}
}
// Result holds the output of executing a statement.
// Exactly one variant is non-nil.
type Result struct {
Select *TaskProjection
}
// TaskProjection holds the filtered, sorted tasks and the requested field list.
type TaskProjection struct {
Tasks []*task.Task
Fields []string // user-requested fields; nil/empty = all fields
}
// Execute dispatches on the statement type and returns results.
func (e *Executor) Execute(stmt *Statement, tasks []*task.Task) (*Result, error) {
if stmt == nil {
return nil, fmt.Errorf("nil statement")
}
switch {
case stmt.Select != nil:
return e.executeSelect(stmt.Select, tasks)
case stmt.Create != nil:
return nil, fmt.Errorf("create is not supported yet")
case stmt.Update != nil:
return nil, fmt.Errorf("update is not supported yet")
case stmt.Delete != nil:
return nil, fmt.Errorf("delete is not supported yet")
default:
return nil, fmt.Errorf("empty statement")
}
}
func (e *Executor) executeSelect(sel *SelectStmt, tasks []*task.Task) (*Result, error) {
filtered, err := e.filterTasks(sel.Where, tasks)
if err != nil {
return nil, err
}
if len(sel.OrderBy) > 0 {
e.sortTasks(filtered, sel.OrderBy)
}
return &Result{
Select: &TaskProjection{
Tasks: filtered,
Fields: sel.Fields,
},
}, nil
}
// --- filtering ---
func (e *Executor) filterTasks(where Condition, tasks []*task.Task) ([]*task.Task, error) {
if where == nil {
result := make([]*task.Task, len(tasks))
copy(result, tasks)
return result, nil
}
var result []*task.Task
for _, t := range tasks {
match, err := e.evalCondition(where, t, tasks)
if err != nil {
return nil, err
}
if match {
result = append(result, t)
}
}
return result, nil
}
// --- condition evaluation ---
func (e *Executor) evalCondition(c Condition, t *task.Task, allTasks []*task.Task) (bool, error) {
switch c := c.(type) {
case *BinaryCondition:
return e.evalBinaryCondition(c, t, allTasks)
case *NotCondition:
val, err := e.evalCondition(c.Inner, t, allTasks)
if err != nil {
return false, err
}
return !val, nil
case *CompareExpr:
return e.evalCompare(c, t, allTasks)
case *IsEmptyExpr:
return e.evalIsEmpty(c, t, allTasks)
case *InExpr:
return e.evalIn(c, t, allTasks)
case *QuantifierExpr:
return e.evalQuantifier(c, t, allTasks)
default:
return false, fmt.Errorf("unknown condition type %T", c)
}
}
func (e *Executor) evalBinaryCondition(c *BinaryCondition, t *task.Task, allTasks []*task.Task) (bool, error) {
left, err := e.evalCondition(c.Left, t, allTasks)
if err != nil {
return false, err
}
switch c.Op {
case "and":
if !left {
return false, nil
}
return e.evalCondition(c.Right, t, allTasks)
case "or":
if left {
return true, nil
}
return e.evalCondition(c.Right, t, allTasks)
default:
return false, fmt.Errorf("unknown binary operator %q", c.Op)
}
}
func (e *Executor) evalCompare(c *CompareExpr, t *task.Task, allTasks []*task.Task) (bool, error) {
leftVal, err := e.evalExpr(c.Left, t, allTasks)
if err != nil {
return false, err
}
rightVal, err := e.evalExpr(c.Right, t, allTasks)
if err != nil {
return false, err
}
return e.compareValues(leftVal, rightVal, c.Op, c.Left, c.Right)
}
func (e *Executor) evalIsEmpty(c *IsEmptyExpr, t *task.Task, allTasks []*task.Task) (bool, error) {
val, err := e.evalExpr(c.Expr, t, allTasks)
if err != nil {
return false, err
}
empty := isZeroValue(val)
if c.Negated {
return !empty, nil
}
return empty, nil
}
func (e *Executor) evalIn(c *InExpr, t *task.Task, allTasks []*task.Task) (bool, error) {
val, err := e.evalExpr(c.Value, t, allTasks)
if err != nil {
return false, err
}
collVal, err := e.evalExpr(c.Collection, t, allTasks)
if err != nil {
return false, err
}
list, ok := collVal.([]interface{})
if !ok {
return false, fmt.Errorf("in: collection is not a list")
}
valStr := normalizeToString(val)
found := false
for _, elem := range list {
if normalizeToString(elem) == valStr {
found = true
break
}
}
if c.Negated {
return !found, nil
}
return found, nil
}
func (e *Executor) evalQuantifier(q *QuantifierExpr, t *task.Task, allTasks []*task.Task) (bool, error) {
listVal, err := e.evalExpr(q.Expr, t, allTasks)
if err != nil {
return false, err
}
refs, ok := listVal.([]interface{})
if !ok {
return false, fmt.Errorf("quantifier: expression is not a list")
}
// find referenced tasks
refTasks := make([]*task.Task, 0, len(refs))
for _, ref := range refs {
refID := normalizeToString(ref)
for _, at := range allTasks {
if strings.EqualFold(at.ID, refID) {
refTasks = append(refTasks, at)
break
}
}
}
switch q.Kind {
case "any":
for _, rt := range refTasks {
match, err := e.evalCondition(q.Condition, rt, allTasks)
if err != nil {
return false, err
}
if match {
return true, nil
}
}
return false, nil
case "all":
if len(refTasks) == 0 {
return true, nil // vacuous truth
}
for _, rt := range refTasks {
match, err := e.evalCondition(q.Condition, rt, allTasks)
if err != nil {
return false, err
}
if !match {
return false, nil
}
}
return true, nil
default:
return false, fmt.Errorf("unknown quantifier %q", q.Kind)
}
}
// --- expression evaluation ---
func (e *Executor) evalExpr(expr Expr, t *task.Task, allTasks []*task.Task) (interface{}, error) {
switch expr := expr.(type) {
case *FieldRef:
return extractField(t, expr.Name), nil
case *QualifiedRef:
return nil, fmt.Errorf("qualified references (old./new.) are not supported in standalone SELECT")
case *StringLiteral:
return expr.Value, nil
case *IntLiteral:
return expr.Value, nil
case *DateLiteral:
return expr.Value, nil
case *DurationLiteral:
return durationToTimeDelta(expr.Value, expr.Unit), nil
case *ListLiteral:
return e.evalListLiteral(expr, t, allTasks)
case *EmptyLiteral:
return nil, nil
case *FunctionCall:
return e.evalFunctionCall(expr, t, allTasks)
case *BinaryExpr:
return e.evalBinaryExpr(expr, t, allTasks)
case *SubQuery:
return nil, fmt.Errorf("subquery is only valid as argument to count()")
default:
return nil, fmt.Errorf("unknown expression type %T", expr)
}
}
func (e *Executor) evalListLiteral(ll *ListLiteral, t *task.Task, allTasks []*task.Task) (interface{}, error) {
result := make([]interface{}, len(ll.Elements))
for i, elem := range ll.Elements {
val, err := e.evalExpr(elem, t, allTasks)
if err != nil {
return nil, err
}
result[i] = val
}
return result, nil
}
// --- function evaluation ---
func (e *Executor) evalFunctionCall(fc *FunctionCall, t *task.Task, allTasks []*task.Task) (interface{}, error) {
switch fc.Name {
case "now":
return time.Now(), nil
case "user":
return e.userFunc(), nil
case "contains":
return e.evalContains(fc, t, allTasks)
case "count":
return e.evalCount(fc, allTasks)
case "next_date":
return e.evalNextDate(fc, t, allTasks)
case "blocks":
return e.evalBlocks(fc, t, allTasks)
case "call":
return nil, fmt.Errorf("call() is not supported yet")
default:
return nil, fmt.Errorf("unknown function %q", fc.Name)
}
}
func (e *Executor) evalContains(fc *FunctionCall, t *task.Task, allTasks []*task.Task) (interface{}, error) {
haystack, err := e.evalExpr(fc.Args[0], t, allTasks)
if err != nil {
return nil, err
}
needle, err := e.evalExpr(fc.Args[1], t, allTasks)
if err != nil {
return nil, err
}
return strings.Contains(normalizeToString(haystack), normalizeToString(needle)), nil
}
func (e *Executor) evalCount(fc *FunctionCall, allTasks []*task.Task) (interface{}, error) {
sq, ok := fc.Args[0].(*SubQuery)
if !ok {
return nil, fmt.Errorf("count() argument must be a select subquery")
}
if sq.Where == nil {
return len(allTasks), nil
}
count := 0
for _, t := range allTasks {
match, err := e.evalCondition(sq.Where, t, allTasks)
if err != nil {
return nil, err
}
if match {
count++
}
}
return count, nil
}
func (e *Executor) evalNextDate(fc *FunctionCall, t *task.Task, allTasks []*task.Task) (interface{}, error) {
val, err := e.evalExpr(fc.Args[0], t, allTasks)
if err != nil {
return nil, err
}
rec, ok := val.(task.Recurrence)
if !ok {
return nil, fmt.Errorf("next_date() argument must be a recurrence value")
}
return task.NextOccurrence(rec), nil
}
func (e *Executor) evalBlocks(fc *FunctionCall, t *task.Task, allTasks []*task.Task) (interface{}, error) {
val, err := e.evalExpr(fc.Args[0], t, allTasks)
if err != nil {
return nil, err
}
targetID := strings.ToUpper(normalizeToString(val))
var blockers []interface{}
for _, at := range allTasks {
for _, dep := range at.DependsOn {
if strings.EqualFold(dep, targetID) {
blockers = append(blockers, at.ID)
break
}
}
}
if blockers == nil {
blockers = []interface{}{}
}
return blockers, nil
}
// --- binary expression evaluation ---
func (e *Executor) evalBinaryExpr(b *BinaryExpr, t *task.Task, allTasks []*task.Task) (interface{}, error) {
leftVal, err := e.evalExpr(b.Left, t, allTasks)
if err != nil {
return nil, err
}
rightVal, err := e.evalExpr(b.Right, t, allTasks)
if err != nil {
return nil, err
}
switch b.Op {
case "+":
return addValues(leftVal, rightVal)
case "-":
return subtractValues(leftVal, rightVal)
default:
return nil, fmt.Errorf("unknown binary operator %q", b.Op)
}
}
func addValues(left, right interface{}) (interface{}, error) {
switch l := left.(type) {
case int:
if r, ok := right.(int); ok {
return l + r, nil
}
case time.Time:
if r, ok := right.(time.Duration); ok {
return l.Add(r), nil
}
case string:
if r, ok := right.(string); ok {
return l + r, nil
}
}
return nil, fmt.Errorf("cannot add %T + %T", left, right)
}
func subtractValues(left, right interface{}) (interface{}, error) {
switch l := left.(type) {
case int:
if r, ok := right.(int); ok {
return l - r, nil
}
case time.Time:
switch r := right.(type) {
case time.Duration:
return l.Add(-r), nil
case time.Time:
return l.Sub(r), nil
}
}
return nil, fmt.Errorf("cannot subtract %T - %T", left, right)
}
// --- sorting ---
func (e *Executor) sortTasks(tasks []*task.Task, clauses []OrderByClause) {
sort.SliceStable(tasks, func(i, j int) bool {
for _, c := range clauses {
vi := extractField(tasks[i], c.Field)
vj := extractField(tasks[j], c.Field)
cmp := compareForSort(vi, vj)
if cmp == 0 {
continue
}
if c.Desc {
return cmp > 0
}
return cmp < 0
}
return false
})
}
func compareForSort(a, b interface{}) int {
if a == nil && b == nil {
return 0
}
if a == nil {
return -1
}
if b == nil {
return 1
}
switch av := a.(type) {
case int:
bv, _ := b.(int)
return compareInts(av, bv)
case string:
bv, _ := b.(string)
return strings.Compare(av, bv)
case task.Status:
bv, _ := b.(task.Status)
return strings.Compare(string(av), string(bv))
case task.Type:
bv, _ := b.(task.Type)
return strings.Compare(string(av), string(bv))
case time.Time:
bv, _ := b.(time.Time)
if av.Before(bv) {
return -1
}
if av.After(bv) {
return 1
}
return 0
case task.Recurrence:
bv, _ := b.(task.Recurrence)
return strings.Compare(string(av), string(bv))
case time.Duration:
bv, _ := b.(time.Duration)
if av < bv {
return -1
}
if av > bv {
return 1
}
return 0
default:
return strings.Compare(fmt.Sprint(a), fmt.Sprint(b))
}
}
func compareInts(a, b int) int {
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}
// --- comparison ---
func (e *Executor) compareValues(left, right interface{}, op string, leftExpr, rightExpr Expr) (bool, error) {
if left == nil || right == nil {
return compareWithNil(left, right, op)
}
if leftList, ok := left.([]interface{}); ok {
if rightList, ok := right.([]interface{}); ok {
return compareListEquality(leftList, rightList, op)
}
}
if lb, ok := left.(bool); ok {
if rb, ok := right.(bool); ok {
return compareBools(lb, rb, op)
}
}
compType := e.resolveComparisonType(leftExpr, rightExpr)
switch compType {
case ValueID:
return compareStringsCI(normalizeToString(left), normalizeToString(right), op)
case ValueStatus:
ls := e.normalizeStatusStr(normalizeToString(left))
rs := e.normalizeStatusStr(normalizeToString(right))
return compareStrings(ls, rs, op)
case ValueTaskType:
ls := e.normalizeTypeStr(normalizeToString(left))
rs := e.normalizeTypeStr(normalizeToString(right))
return compareStrings(ls, rs, op)
}
switch lv := left.(type) {
case string:
return compareStrings(lv, normalizeToString(right), op)
case int:
rv, ok := toInt(right)
if !ok {
return false, fmt.Errorf("cannot compare int with %T", right)
}
return compareIntValues(lv, rv, op)
case time.Time:
rv, ok := right.(time.Time)
if !ok {
return false, fmt.Errorf("cannot compare time with %T", right)
}
return compareTimes(lv, rv, op)
case time.Duration:
rv, ok := right.(time.Duration)
if !ok {
return false, fmt.Errorf("cannot compare duration with %T", right)
}
return compareDurations(lv, rv, op)
case task.Status:
return compareStrings(string(lv), normalizeToString(right), op)
case task.Type:
return compareStrings(string(lv), normalizeToString(right), op)
case task.Recurrence:
return compareStrings(string(lv), normalizeToString(right), op)
default:
return false, fmt.Errorf("unsupported comparison type %T", left)
}
}
// resolveComparisonType returns the dominant field type for a comparison,
// checking both sides for enum/id fields that need special handling.
func (e *Executor) resolveComparisonType(left, right Expr) ValueType {
if t := e.exprFieldType(left); t == ValueID || t == ValueStatus || t == ValueTaskType {
return t
}
if t := e.exprFieldType(right); t == ValueID || t == ValueStatus || t == ValueTaskType {
return t
}
return -1
}
func (e *Executor) exprFieldType(expr Expr) ValueType {
fr, ok := expr.(*FieldRef)
if !ok {
return -1
}
fs, ok := e.schema.Field(fr.Name)
if !ok {
return -1
}
return fs.Type
}
func (e *Executor) normalizeStatusStr(s string) string {
if norm, ok := e.schema.NormalizeStatus(s); ok {
return norm
}
return s
}
func (e *Executor) normalizeTypeStr(s string) string {
if norm, ok := e.schema.NormalizeType(s); ok {
return norm
}
return s
}
func compareWithNil(left, right interface{}, op string) (bool, error) {
// treat nil as empty; treat zero-valued non-nil as also matching empty
leftEmpty := isZeroValue(left)
rightEmpty := isZeroValue(right)
bothEmpty := leftEmpty && rightEmpty
switch op {
case "=":
return bothEmpty, nil
case "!=":
return !bothEmpty, nil
default:
return false, nil
}
}
func compareListEquality(a, b []interface{}, op string) (bool, error) {
switch op {
case "=":
return sortedMultisetEqual(a, b), nil
case "!=":
return !sortedMultisetEqual(a, b), nil
default:
return false, fmt.Errorf("operator %s not supported for list comparison", op)
}
}
func sortedMultisetEqual(a, b []interface{}) bool {
if len(a) != len(b) {
return false
}
as := toSortedStrings(a)
bs := toSortedStrings(b)
for i := range as {
if as[i] != bs[i] {
return false
}
}
return true
}
func toSortedStrings(list []interface{}) []string {
s := make([]string, len(list))
for i, v := range list {
s[i] = normalizeToString(v)
}
sort.Strings(s)
return s
}
func compareBools(a, b bool, op string) (bool, error) {
switch op {
case "=":
return a == b, nil
case "!=":
return a != b, nil
default:
return false, fmt.Errorf("operator %s not supported for bool comparison", op)
}
}
func compareStrings(a, b, op string) (bool, error) {
switch op {
case "=":
return a == b, nil
case "!=":
return a != b, nil
default:
return false, fmt.Errorf("operator %s not supported for string comparison", op)
}
}
func compareStringsCI(a, b, op string) (bool, error) {
a = strings.ToUpper(a)
b = strings.ToUpper(b)
switch op {
case "=":
return a == b, nil
case "!=":
return a != b, nil
default:
return false, fmt.Errorf("operator %s not supported for id comparison", op)
}
}
func compareIntValues(a, b int, op string) (bool, error) {
switch op {
case "=":
return a == b, nil
case "!=":
return a != b, nil
case "<":
return a < b, nil
case ">":
return a > b, nil
case "<=":
return a <= b, nil
case ">=":
return a >= b, nil
default:
return false, fmt.Errorf("unknown operator %q", op)
}
}
func compareTimes(a, b time.Time, op string) (bool, error) {
switch op {
case "=":
return a.Equal(b), nil
case "!=":
return !a.Equal(b), nil
case "<":
return a.Before(b), nil
case ">":
return a.After(b), nil
case "<=":
return !a.After(b), nil
case ">=":
return !a.Before(b), nil
default:
return false, fmt.Errorf("unknown operator %q", op)
}
}
func compareDurations(a, b time.Duration, op string) (bool, error) {
switch op {
case "=":
return a == b, nil
case "!=":
return a != b, nil
case "<":
return a < b, nil
case ">":
return a > b, nil
case "<=":
return a <= b, nil
case ">=":
return a >= b, nil
default:
return false, fmt.Errorf("unknown operator %q", op)
}
}
// --- field extraction ---
func extractField(t *task.Task, name string) interface{} {
switch name {
case "id":
return t.ID
case "title":
return t.Title
case "description":
return t.Description
case "status":
return t.Status
case "type":
return t.Type
case "priority":
return t.Priority
case "points":
return t.Points
case "tags":
return toInterfaceSlice(t.Tags)
case "dependsOn":
return toInterfaceSlice(t.DependsOn)
case "due":
return t.Due
case "recurrence":
return t.Recurrence
case "assignee":
return t.Assignee
case "createdBy":
return t.CreatedBy
case "createdAt":
return t.CreatedAt
case "updatedAt":
return t.UpdatedAt
default:
return nil
}
}
// --- helpers ---
func toInterfaceSlice(ss []string) []interface{} {
if ss == nil {
return []interface{}{}
}
result := make([]interface{}, len(ss))
for i, s := range ss {
result[i] = s
}
return result
}
func normalizeToString(v interface{}) string {
switch v := v.(type) {
case string:
return v
case task.Status:
return string(v)
case task.Type:
return string(v)
case task.Recurrence:
return string(v)
default:
return fmt.Sprint(v)
}
}
func toInt(v interface{}) (int, bool) {
switch v := v.(type) {
case int:
return v, true
default:
return 0, false
}
}
func isZeroValue(v interface{}) bool {
if v == nil {
return true
}
switch v := v.(type) {
case string:
return v == ""
case int:
return v == 0
case time.Time:
return v.IsZero()
case time.Duration:
return v == 0
case bool:
return !v
case task.Status:
return v == ""
case task.Type:
return v == ""
case task.Recurrence:
return v == ""
case []interface{}:
return len(v) == 0
default:
return false
}
}
func durationToTimeDelta(value int, unit string) time.Duration {
switch unit {
case "day":
return time.Duration(value) * 24 * time.Hour
case "week":
return time.Duration(value) * 7 * 24 * time.Hour
case "month":
return time.Duration(value) * 30 * 24 * time.Hour
case "year":
return time.Duration(value) * 365 * 24 * time.Hour
case "hour":
return time.Duration(value) * time.Hour
case "minute":
return time.Duration(value) * time.Minute
default:
return time.Duration(value) * 24 * time.Hour
}
}

2146
ruki/executor_test.go Normal file

File diff suppressed because it is too large Load diff

42
ruki/keyword/keyword.go Normal file
View file

@ -0,0 +1,42 @@
package keyword
import "strings"
// reserved is the canonical, immutable list of ruki reserved words.
var reserved = [...]string{
"select", "create", "update", "delete",
"where", "set", "order", "by",
"asc", "desc",
"before", "after", "deny", "run",
"and", "or", "not",
"is", "empty", "in",
"any", "all",
"old", "new",
}
var reservedSet map[string]struct{}
func init() {
reservedSet = make(map[string]struct{}, len(reserved))
for _, kw := range reserved {
reservedSet[strings.ToLower(kw)] = struct{}{}
}
}
// IsReserved reports whether name is a ruki reserved word (case-insensitive).
func IsReserved(name string) bool {
_, ok := reservedSet[strings.ToLower(name)]
return ok
}
// List returns a copy of the reserved keyword list.
func List() []string {
result := make([]string, len(reserved))
copy(result, reserved[:])
return result
}
// Pattern returns the regex alternation for the lexer Keyword rule.
func Pattern() string {
return `\b(` + strings.Join(reserved[:], "|") + `)\b`
}

View file

@ -1,45 +1,18 @@
package ruki
import "strings"
// reservedKeywords is the canonical, immutable list of ruki reserved words.
// the lexer regex and IsReservedKeyword both derive from this single source.
var reservedKeywords = [...]string{
"select", "create", "update", "delete",
"where", "set", "order", "by",
"asc", "desc",
"before", "after", "deny", "run",
"and", "or", "not",
"is", "empty", "in",
"any", "all",
"old", "new",
}
// reservedSet is a case-insensitive lookup built from reservedKeywords.
var reservedSet map[string]struct{}
func init() {
reservedSet = make(map[string]struct{}, len(reservedKeywords))
for _, kw := range reservedKeywords {
reservedSet[strings.ToLower(kw)] = struct{}{}
}
}
import "github.com/boolean-maybe/tiki/ruki/keyword"
// IsReservedKeyword reports whether name is a ruki reserved word (case-insensitive).
func IsReservedKeyword(name string) bool {
_, ok := reservedSet[strings.ToLower(name)]
return ok
return keyword.IsReserved(name)
}
// ReservedKeywordsList returns a copy of the reserved keyword list.
func ReservedKeywordsList() []string {
result := make([]string, len(reservedKeywords))
copy(result, reservedKeywords[:])
return result
return keyword.List()
}
// keywordPattern returns the regex alternation for the lexer Keyword rule.
// called once during lexer init.
func keywordPattern() string {
return `\b(` + strings.Join(reservedKeywords[:], "|") + `)\b`
return keyword.Pattern()
}

View file

@ -32,7 +32,7 @@ func tokenize(t *testing.T, input string) []lexer.Token {
func TestTokenizeKeywords(t *testing.T) {
keywordType := rukiLexer.Symbols()["Keyword"]
for _, kw := range reservedKeywords {
for _, kw := range ReservedKeywordsList() {
t.Run(kw, func(t *testing.T) {
tokens := tokenize(t, kw)
if len(tokens) != 1 {

View file

@ -3,7 +3,7 @@ package workflow
import (
"fmt"
"github.com/boolean-maybe/tiki/ruki"
"github.com/boolean-maybe/tiki/ruki/keyword"
)
// ValueType identifies the semantic type of a task field.
@ -56,7 +56,7 @@ var fieldByName map[string]FieldDef
func init() {
fieldByName = make(map[string]FieldDef, len(fieldCatalog))
for _, f := range fieldCatalog {
if ruki.IsReservedKeyword(f.Name) {
if keyword.IsReserved(f.Name) {
panic(fmt.Sprintf("field catalog contains reserved keyword: %q", f.Name))
}
fieldByName[f.Name] = f
@ -71,7 +71,7 @@ func Field(name string) (FieldDef, bool) {
// ValidateFieldName rejects names that collide with ruki reserved keywords.
func ValidateFieldName(name string) error {
if ruki.IsReservedKeyword(name) {
if keyword.IsReserved(name) {
return fmt.Errorf("field name %q is reserved", name)
}
return nil

View file

@ -3,7 +3,7 @@ package workflow
import (
"testing"
"github.com/boolean-maybe/tiki/ruki"
"github.com/boolean-maybe/tiki/ruki/keyword"
)
func TestField(t *testing.T) {
@ -78,7 +78,7 @@ func TestDateVsTimestamp(t *testing.T) {
}
func TestValidateFieldName_RejectsKeywords(t *testing.T) {
for _, kw := range ruki.ReservedKeywordsList() {
for _, kw := range keyword.List() {
t.Run(kw, func(t *testing.T) {
err := ValidateFieldName(kw)
if err == nil {