snapshot of eee7b5cfc0776c68816d7fdaa8af9d7a0e0da2e6 Annotations about the code that implements koment.

internal/store/record.go

1 // Package store reads and writes the annotation records that live in .koment.
2 package store
3
4 import (
5 "fmt"
6 "strconv"
7 "strings"
8 "time"
9
10 yaml "go.yaml.in/yaml/v3"
11 )
12
13 const RecordVersion = 1
14
15 const SchemaURL = "https://raw.githubusercontent.com/janpuc/koment/main/schema/annotation.schema.json"
16
17 type Kind string
18
19 const (
20 KindWhy Kind = "why"
21 KindGotcha Kind = "gotcha"
22 KindInvariant Kind = "invariant"
23 KindAntiPattern Kind = "anti-pattern"
24 )
25
26 var Kinds = []Kind{KindWhy, KindGotcha, KindInvariant, KindAntiPattern}
27
28 func ParseKind(text string) (Kind, error) {
29 for _, kind := range Kinds {
30 if Kind(text) == kind {
31 return kind, nil
32 }
33 }
34 return "", fmt.Errorf("unknown kind %q, want one of %s", text, joinKinds())
35 }
36
37 func joinKinds() string {
38 names := make([]string, len(Kinds))
39 for index, kind := range Kinds {
40 names[index] = string(kind)
41 }
42 return strings.Join(names, ", ")
43 }
44
45 type Scope string
46
47 const (
48 ScopeFile Scope = "file"
49 ScopeExcerpt Scope = "excerpt"
50 )
51
52 func ParseScope(text string) (Scope, error) {
53 switch Scope(text) {
54 case ScopeFile:
55 return ScopeFile, nil
56 case ScopeExcerpt:
57 return ScopeExcerpt, nil
58 }
59 return "", fmt.Errorf("unknown scope %q, want one of %s, %s", text, ScopeFile, ScopeExcerpt)
60 }
61
62 type Anchor struct {
63 Scope Scope `yaml:"scope"`
64 Excerpt string `yaml:"excerpt,omitempty"`
65 Before string `yaml:"before,omitempty"`
66 After string `yaml:"after,omitempty"`
67 LastSeenLine int `yaml:"last_seen_line,omitempty"`
68 }
69
70 func (a Anchor) MarshalYAML() (any, error) {
71 node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
72 appendScalar := func(key, value, tag string, style yaml.Style) {
73 node.Content = append(node.Content,
74 &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key},
75 &yaml.Node{Kind: yaml.ScalarNode, Tag: tag, Value: value, Style: style},
76 )
77 }
78 appendScalar("scope", string(a.Scope), "!!str", 0)
79 if a.Excerpt != "" {
80 appendScalar("excerpt", a.Excerpt, "!!str", safeStringStyle(a.Excerpt))
81 }
82 if a.Before != "" {
83 appendScalar("before", a.Before, "!!str", safeStringStyle(a.Before))
84 }
85 if a.After != "" {
86 appendScalar("after", a.After, "!!str", safeStringStyle(a.After))
87 }
88 if a.LastSeenLine != 0 {
89 appendScalar("last_seen_line", strconv.Itoa(a.LastSeenLine), "!!int", 0)
90 }
91 return node, nil
92 }
93
94 func safeStringStyle(value string) yaml.Style {
95 if strings.Contains(value, "\t") {
96 return yaml.DoubleQuotedStyle
97 }
98 if strings.Contains(value, "\n") {
99 return yaml.LiteralStyle
100 }
101 return 0
102 }
103
104 func (a Anchor) Validate(id string) error {
105 switch a.Scope {
106 case ScopeFile:
107 if a.Excerpt != "" || a.Before != "" || a.After != "" || a.LastSeenLine != 0 {
108 return fmt.Errorf("annotation %s: file anchor must not carry excerpt context or a line", id)
109 }
110 return nil
111 case ScopeExcerpt:
112 if a.Excerpt == "" {
113 return fmt.Errorf("annotation %s: excerpt anchor requires a non-empty excerpt", id)
114 }
115 if a.LastSeenLine < 1 {
116 return fmt.Errorf("annotation %s: last_seen_line %d is not a positive line number", id, a.LastSeenLine)
117 }
118 if err := validateContext("before", a.Before); err != nil {
119 return fmt.Errorf("annotation %s: %w", id, err)
120 }
121 if err := validateContext("after", a.After); err != nil {
122 return fmt.Errorf("annotation %s: %w", id, err)
123 }
124 return nil
125 default:
126 _, err := ParseScope(string(a.Scope))
127 return fmt.Errorf("annotation %s: %w", id, err)
128 }
129 }
130
131 func validateContext(name, context string) error {
132 if context == "" {
133 return nil
134 }
135 if strings.Count(strings.TrimSuffix(context, "\n"), "\n") >= 3 {
136 return fmt.Errorf("anchor.%s contains more than three lines", name)
137 }
138 return nil
139 }
140
141 type Policy struct {
142 Exception string `yaml:"exception"`
143 Acknowledged bool `yaml:"acknowledged"`
144 }
145
146 func (p Policy) Validate(annotation Annotation) error {
147 if p.Exception != "inline-comment" || !p.Acknowledged {
148 return fmt.Errorf("annotation %s: policy must explicitly acknowledge an inline-comment exception", annotation.ID)
149 }
150 if annotation.Kind != KindWhy || annotation.Anchor.Scope != ScopeExcerpt {
151 return fmt.Errorf("annotation %s: inline-comment policy requires a why annotation with an excerpt anchor", annotation.ID)
152 }
153 return nil
154 }
155
156 type Annotation struct {
157 Version int `yaml:"version"`
158 ID string `yaml:"id"`
159 File string `yaml:"file"`
160 Kind Kind `yaml:"kind"`
161 Body string `yaml:"body"`
162 Created Date `yaml:"created"`
163 Anchor Anchor `yaml:"anchor"`
164 Git *GitContext `yaml:"git,omitempty"`
165 Author Author `yaml:"author"`
166 Policy *Policy `yaml:"policy,omitempty"`
167 }
168
169 func (a Annotation) Validate() error {
170 if a.Version != RecordVersion {
171 return fmt.Errorf("annotation %s has version %d, want %d", a.ID, a.Version, RecordVersion)
172 }
173 if !ValidID(a.ID) {
174 return fmt.Errorf("annotation id %q is not a canonical ULID", a.ID)
175 }
176 if _, err := validSourcePath(a.File); err != nil {
177 return fmt.Errorf("annotation %s file: %w", a.ID, err)
178 }
179 if _, err := ParseKind(string(a.Kind)); err != nil {
180 return fmt.Errorf("annotation %s: %w", a.ID, err)
181 }
182 if strings.TrimSpace(a.Body) == "" {
183 return fmt.Errorf("annotation %s: empty body", a.ID)
184 }
185 if a.Created.IsZero() {
186 return fmt.Errorf("annotation %s: missing created date", a.ID)
187 }
188 if err := a.Anchor.Validate(a.ID); err != nil {
189 return err
190 }
191 if a.Git != nil {
192 if err := a.Git.Validate(); err != nil {
193 return fmt.Errorf("annotation %s: %w", a.ID, err)
194 }
195 }
196 if err := a.Author.Validate(); err != nil {
197 return fmt.Errorf("annotation %s: %w", a.ID, err)
198 }
199 if a.Policy != nil {
200 if err := a.Policy.Validate(a); err != nil {
201 return err
202 }
203 }
204 return nil
205 }
206
207 // Date is a calendar date with no time or zone, written as YYYY-MM-DD.
208 type Date struct{ time.Time }
209
210 const dateLayout = "2006-01-02"
211
212 func Today() Date { return Date{time.Now().UTC().Truncate(24 * time.Hour)} }
213
214 func (d Date) MarshalYAML() (any, error) { return d.Format(dateLayout), nil }
215
216 func (d *Date) UnmarshalYAML(unmarshal func(any) error) error {
217 var text string
218 if err := unmarshal(&text); err != nil {
219 return fmt.Errorf("created must be a %s date: %w", dateLayout, err)
220 }
221 parsed, err := time.Parse(dateLayout, text)
222 if err != nil {
223 return fmt.Errorf("created %q is not a %s date", text, dateLayout)
224 }
225 d.Time = parsed
226 return nil
227 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close