package models import ( "encoding/json" "fmt" "slices" "strings" "time" ) // Date is a date-only time value that marshals as "YYYY-MM-DD" in JSON. type Date struct{ time.Time } // ParseDate parses a date string in "YYYY-MM-DD" or RFC3339 format. func ParseDate(s string) (Date, error) { for _, layout := range []string{"2006-01-02", time.RFC3339} { if t, err := time.Parse(layout, s); err == nil { return Date{t.UTC()}, nil } } return Date{}, fmt.Errorf("cannot parse date %q: expected YYYY-MM-DD", s) } func (d Date) MarshalJSON() ([]byte, error) { return json.Marshal(d.Format("2006-01-02")) } func (d *Date) UnmarshalJSON(b []byte) error { var s string if err := json.Unmarshal(b, &s); err != nil { return err } parsed, err := ParseDate(s) if err != nil { return err } *d = parsed return nil } type Node struct { ID string `json:"id"` Title string `json:"title"` Content string `json:"content,omitempty"` DueDate *Date `json:"due_date,omitempty"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` Tags []string `json:"tags,omitempty"` Relations map[string][]string `json:"relations,omitempty"` } func NewNode() *Node { return &Node{ Relations: make(map[string][]string), } } func (n *Node) AddTag(tag string) { if tag == "" { return } // If it's a property (name::value format), replace any existing tag with the same prefix. if idx := strings.Index(tag, "::"); idx >= 0 { prefix := tag[:idx+2] var newTags []string for _, t := range n.Tags { if !strings.HasPrefix(t, prefix) { newTags = append(newTags, t) } } n.Tags = newTags } if !slices.Contains(n.Tags, tag) { n.Tags = append(n.Tags, tag) } } func (n *Node) RemoveTag(tag string) { var newTags []string for _, t := range n.Tags { if t != tag { newTags = append(newTags, t) } } n.Tags = newTags } func (n *Node) AddRelation(relType RelType, target string) { if n.Relations == nil { n.Relations = make(map[string][]string) } if relType == RelAssignee || relType == RelCreated { n.Relations[string(relType)] = []string{target} return } if !slices.Contains(n.Relations[string(relType)], target) { n.Relations[string(relType)] = append(n.Relations[string(relType)], target) } } func (n *Node) RemoveRelation(relType RelType, target string) { if n.Relations == nil { return } var newTgts []string for _, tgt := range n.Relations[string(relType)] { if tgt != target { newTgts = append(newTgts, tgt) } } if len(newTgts) == 0 { delete(n.Relations, string(relType)) } else { n.Relations[string(relType)] = newTgts } } func (n *Node) GetProperty(k string) string { prefix := "_" + k + "::" for _, t := range n.Tags { if strings.HasPrefix(t, prefix) { return strings.TrimPrefix(t, prefix) } } return "" } func (n *Node) GetDisplayTags() []string { var tags []string for _, t := range n.Tags { if !strings.HasPrefix(t, "_") { tags = append(tags, t) } } return tags }