Implement opal-task Phase 3: Filter and Modifier Parsing

- Add filter.go: Parse filters (+tag, -tag, attribute:value, IDs)
- Implement Filter.ToSQL() for WHERE clause generation
- Add modifier.go: Parse modifiers (set/clear attributes, add/remove tags)
- Implement Modifier.Apply() to update existing tasks
- Add dateparse.go: Smart date parsing (ISO, today, tomorrow, weekdays)
- Implement nextWeekday logic (smart Sunday interpretation)
- Update GetTasks() to accept Filter parameter
- Add CreateTaskWithModifier() for task creation with modifiers
- Add comprehensive test suite (13 new tests, all passing)
- Support filtering by status, project, priority, tags, UUIDs, display IDs
- Support modifying priority, project, dates, recurrence, tags
This commit is contained in:
2026-01-04 14:48:43 +01:00
parent 7c6ec97c62
commit c99a4a2d95
7 changed files with 999 additions and 7 deletions
+204
View File
@@ -0,0 +1,204 @@
package engine
import (
"fmt"
"strings"
)
type Modifier struct {
SetAttributes map[string]*string // key -> value (nil = clear)
AddTags []string
RemoveTags []string
}
func NewModifier() *Modifier {
return &Modifier{
SetAttributes: make(map[string]*string),
AddTags: []string{},
RemoveTags: []string{},
}
}
// ParseModifier parses command-line args into Modifier
func ParseModifier(args []string) (*Modifier, error) {
m := NewModifier()
for _, arg := range args {
if strings.HasPrefix(arg, "+") {
// Add tag
m.AddTags = append(m.AddTags, strings.TrimPrefix(arg, "+"))
} else if strings.HasPrefix(arg, "-") && !strings.Contains(arg, ":") {
// Remove tag
m.RemoveTags = append(m.RemoveTags, strings.TrimPrefix(arg, "-"))
} else if strings.Contains(arg, ":") {
// Attribute modification
parts := strings.SplitN(arg, ":", 2)
key := parts[0]
value := parts[1]
if value == "" {
// Clear attribute (priority: with no value)
m.SetAttributes[key] = nil
} else {
m.SetAttributes[key] = &value
}
}
}
return m, nil
}
// Apply applies modifier to task
func (m *Modifier) Apply(task *Task) error {
// Apply attribute changes
for key, valuePtr := range m.SetAttributes {
switch key {
case "priority":
if valuePtr == nil {
task.Priority = PriorityDefault
} else {
task.Priority = Priority(priorityStringToInt(*valuePtr))
}
case "project":
task.Project = valuePtr
case "due":
if valuePtr == nil {
task.Due = nil
} else {
parsed, err := ParseDate(*valuePtr)
if err != nil {
return fmt.Errorf("invalid due date: %w", err)
}
task.Due = &parsed
}
case "scheduled":
if valuePtr == nil {
task.Scheduled = nil
} else {
parsed, err := ParseDate(*valuePtr)
if err != nil {
return fmt.Errorf("invalid scheduled date: %w", err)
}
task.Scheduled = &parsed
}
case "wait":
if valuePtr == nil {
task.Wait = nil
} else {
parsed, err := ParseDate(*valuePtr)
if err != nil {
return fmt.Errorf("invalid wait date: %w", err)
}
task.Wait = &parsed
}
case "until":
if valuePtr == nil {
task.Until = nil
} else {
parsed, err := ParseDate(*valuePtr)
if err != nil {
return fmt.Errorf("invalid until date: %w", err)
}
task.Until = &parsed
}
case "recur":
if valuePtr == nil {
task.RecurrenceDuration = nil
} else {
duration, err := ParseRecurrencePattern(*valuePtr)
if err != nil {
return fmt.Errorf("invalid recurrence: %w", err)
}
task.RecurrenceDuration = &duration
}
}
}
// Apply tag changes
for _, tag := range m.AddTags {
if err := task.AddTag(tag); err != nil {
return err
}
}
for _, tag := range m.RemoveTags {
if err := task.RemoveTag(tag); err != nil {
return err
}
}
task.Modified = timeNow()
return task.Save()
}
// ApplyToNew applies modifier to a new task (before it's saved)
// This is used when creating tasks with modifiers
func (m *Modifier) ApplyToNew(task *Task) error {
// Apply attribute changes (same as Apply but without Save)
for key, valuePtr := range m.SetAttributes {
switch key {
case "priority":
if valuePtr == nil {
task.Priority = PriorityDefault
} else {
task.Priority = Priority(priorityStringToInt(*valuePtr))
}
case "project":
task.Project = valuePtr
case "due":
if valuePtr == nil {
task.Due = nil
} else {
parsed, err := ParseDate(*valuePtr)
if err != nil {
return fmt.Errorf("invalid due date: %w", err)
}
task.Due = &parsed
}
case "scheduled":
if valuePtr == nil {
task.Scheduled = nil
} else {
parsed, err := ParseDate(*valuePtr)
if err != nil {
return fmt.Errorf("invalid scheduled date: %w", err)
}
task.Scheduled = &parsed
}
case "wait":
if valuePtr == nil {
task.Wait = nil
} else {
parsed, err := ParseDate(*valuePtr)
if err != nil {
return fmt.Errorf("invalid wait date: %w", err)
}
task.Wait = &parsed
}
case "until":
if valuePtr == nil {
task.Until = nil
} else {
parsed, err := ParseDate(*valuePtr)
if err != nil {
return fmt.Errorf("invalid until date: %w", err)
}
task.Until = &parsed
}
case "recur":
if valuePtr == nil {
task.RecurrenceDuration = nil
} else {
duration, err := ParseRecurrencePattern(*valuePtr)
if err != nil {
return fmt.Errorf("invalid recurrence: %w", err)
}
task.RecurrenceDuration = &duration
}
}
}
// Note: Tags are added after task is saved (in CreateTask function)
return nil
}