package main import ( "fmt" "strings" ) // AddComment — appends a comment to a post. Bumps the post. Computes the // comment path from the parent comment's path. Pre-validated; the lock is // held during apply so the path computation + index update is atomic. type AddComment struct { PostID string ParentID string // empty for top-level AuthorID string Body string // Set by Apply. resolvedCommentID string resolvedPath string resolvedDepth int } func (m *AddComment) Kind() string { return "add_comment" } func (m *AddComment) AffectedAuthors() []string { return []string{m.AuthorID} } func (m *AddComment) Validate(s *Store) error { if strings.TrimSpace(m.AuthorID) == "" { return &ValidationError{Field: "author_id", Message: "required"} } if _, ok := s.GetAuthor(m.AuthorID); !ok { return &ValidationError{Field: "author_id", Message: fmt.Sprintf("unknown author %q", m.AuthorID)} } if strings.TrimSpace(m.PostID) == "" { return &ValidationError{Field: "post_id", Message: "required"} } post, ok := s.GetPost(m.PostID) if !ok { return &ValidationError{Field: "post_id", Message: fmt.Sprintf("unknown post %q", m.PostID)} } if post.IsDeleted { return &ValidationError{Field: "post_id", Message: "post is deleted"} } if m.ParentID != "" { comments := s.ListComments(m.PostID) found := false for _, c := range comments { if c.ID == m.ParentID && !c.IsDeleted { found = true break } } if !found { return &ValidationError{Field: "parent_id", Message: fmt.Sprintf("unknown parent %q on post %q", m.ParentID, m.PostID)} } } if strings.TrimSpace(m.Body) == "" { return &ValidationError{Field: "body", Message: "required"} } return nil } func (m *AddComment) Apply(s *Store, lock LockRegion) error { now := s.Now() id := s.NewID() // Compute path under the lock. Look up parent's path if any. s.mu.Lock() defer s.mu.Unlock() var path string var depth int if m.ParentID != "" { parent, ok := s.comments[m.ParentID] if !ok { return fmt.Errorf("parent comment disappeared mid-apply: %s", m.ParentID) } path = parent.Path + "." + id depth = parent.Depth + 1 } else { path = id depth = 0 } c := &Comment{ ID: id, PostID: m.PostID, ParentID: m.ParentID, Path: path, Depth: depth, AuthorID: m.AuthorID, Body: m.Body, CreatedAt: now, UpdatedAt: now, } m.resolvedCommentID = id m.resolvedPath = path m.resolvedDepth = depth s.comments[id] = c s.indexInsertComment(c) // Bump the post. post, ok := s.posts[m.PostID] if !ok { return fmt.Errorf("post disappeared mid-apply: %s", m.PostID) } post.BumpedAt = now post.UpdatedAt = now s.indexUpdatePost(post) return nil } func (m *AddComment) ResolvedCommentID() string { return m.resolvedCommentID } func (m *AddComment) ResolvedPath() string { return m.resolvedPath } func (m *AddComment) ResolvedDepth() int { return m.resolvedDepth }