Monorepo for Tangled

appview/searchquery: add dynamic tag extraction and shared resolution helpers

Add KnownTags map and GetDynamicTags/GetNegatedDynamicTags methods to
extract label-value search filters from parsed queries. Any tag:value
pair whose key is not a known system tag (state, author, label) is
treated as a dynamic label filter.

Add resolve.go with shared helpers: IdentResolver type, ResolveAuthor,
ExtractTextFilters, and ResolveDIDLabelValues. These keep resolution
logic in the searchquery package without coupling it to idresolver.

Signed-off-by: Thomas Karpiniec <tom.karpiniec@outlook.com>

authored by

Thomas Karpiniec and committed by tangled.org 91350966 f7c35840

+155
+99
appview/searchquery/resolve.go
··· 1 + package searchquery 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + "strings" 7 + ) 8 + 9 + // IdentResolver converts a handle/identifier string to a DID string. 10 + type IdentResolver func(ctx context.Context, ident string) (string, error) 11 + 12 + // ResolveAuthor extracts the "author" tag from the query and resolves 13 + // both the positive and negated values to DIDs using the provided resolver. 14 + // Returns empty string / nil on missing tags or resolution failure. 15 + func ResolveAuthor(ctx context.Context, q *Query, resolve IdentResolver, log *slog.Logger) (authorDid string, negatedAuthorDids []string) { 16 + if authorHandle := q.Get("author"); authorHandle != nil { 17 + did, err := resolve(ctx, *authorHandle) 18 + if err != nil { 19 + log.Debug("failed to resolve author handle", "handle", *authorHandle, "err", err) 20 + } else { 21 + authorDid = did 22 + } 23 + } 24 + 25 + for _, handle := range q.GetAllNegated("author") { 26 + did, err := resolve(ctx, handle) 27 + if err != nil { 28 + log.Debug("failed to resolve negated author handle", "handle", handle, "err", err) 29 + continue 30 + } 31 + negatedAuthorDids = append(negatedAuthorDids, did) 32 + } 33 + 34 + return authorDid, negatedAuthorDids 35 + } 36 + 37 + // TextFilters holds the keyword and phrase filters extracted from a query. 38 + type TextFilters struct { 39 + Keywords []string 40 + NegatedKeywords []string 41 + Phrases []string 42 + NegatedPhrases []string 43 + } 44 + 45 + // ExtractTextFilters extracts keyword and quoted-phrase items from the query, 46 + // separated into positive and negated slices. 47 + func ExtractTextFilters(q *Query) TextFilters { 48 + var tf TextFilters 49 + for _, item := range q.Items() { 50 + switch item.Kind { 51 + case KindKeyword: 52 + if item.Negated { 53 + tf.NegatedKeywords = append(tf.NegatedKeywords, item.Value) 54 + } else { 55 + tf.Keywords = append(tf.Keywords, item.Value) 56 + } 57 + case KindQuoted: 58 + if item.Negated { 59 + tf.NegatedPhrases = append(tf.NegatedPhrases, item.Value) 60 + } else { 61 + tf.Phrases = append(tf.Phrases, item.Value) 62 + } 63 + } 64 + } 65 + return tf 66 + } 67 + 68 + // ResolveDIDLabelValues resolves handle values to DIDs for dynamic label 69 + // tags whose name appears in didLabels. Tags not in didLabels are returned 70 + // unchanged. On resolution failure the original value is kept. 71 + func ResolveDIDLabelValues( 72 + ctx context.Context, 73 + tags []string, 74 + didLabels map[string]bool, 75 + resolve IdentResolver, 76 + log *slog.Logger, 77 + ) []string { 78 + resolved := make([]string, 0, len(tags)) 79 + for _, tag := range tags { 80 + idx := strings.Index(tag, ":") 81 + if idx < 0 { 82 + resolved = append(resolved, tag) 83 + continue 84 + } 85 + name, val := tag[:idx], tag[idx+1:] 86 + if didLabels[name] { 87 + did, err := resolve(ctx, val) 88 + if err != nil { 89 + log.Debug("failed to resolve DID label value", "label", name, "value", val, "err", err) 90 + resolved = append(resolved, tag) 91 + continue 92 + } 93 + resolved = append(resolved, name+":"+did) 94 + } else { 95 + resolved = append(resolved, tag) 96 + } 97 + } 98 + return resolved 99 + }
+33
appview/searchquery/searchquery.go
··· 158 158 return q.Get(key) != nil 159 159 } 160 160 161 + // KnownTags is the set of tag keys with special system-defined handling. 162 + // Any tag:value pair whose key is not in this set is treated as a dynamic 163 + // label filter. 164 + var KnownTags = map[string]bool{ 165 + "state": true, 166 + "author": true, 167 + "label": true, 168 + } 169 + 170 + // GetDynamicTags returns composite "key:value" strings for all non-negated 171 + // tag:value items whose key is not a known system tag. 172 + func (q *Query) GetDynamicTags() []string { 173 + var result []string 174 + for _, item := range q.items { 175 + if item.Kind == KindTagValue && !item.Negated && !KnownTags[item.Key] { 176 + result = append(result, item.Key+":"+item.Value) 177 + } 178 + } 179 + return result 180 + } 181 + 182 + // GetNegatedDynamicTags returns composite "key:value" strings for all negated 183 + // tag:value items whose key is not a known system tag. 184 + func (q *Query) GetNegatedDynamicTags() []string { 185 + var result []string 186 + for _, item := range q.items { 187 + if item.Kind == KindTagValue && item.Negated && !KnownTags[item.Key] { 188 + result = append(result, item.Key+":"+item.Value) 189 + } 190 + } 191 + return result 192 + } 193 + 161 194 func (q *Query) Set(key, value string) { 162 195 raw := key + ":" + value 163 196 found := false
+23
appview/searchquery/searchquery_test.go
··· 250 250 assert.Equal(t, "-label", items[0].Key) 251 251 assert.Equal(t, "bug", items[0].Value) 252 252 } 253 + 254 + func TestDynamicTags(t *testing.T) { 255 + q := Parse("state:open label:bug priority:high severity:critical -priority:low author:alice -severity:minor keyword") 256 + 257 + // Known tags are not included 258 + dynamic := q.GetDynamicTags() 259 + assert.Equal(t, []string{"priority:high", "severity:critical"}, dynamic) 260 + 261 + negated := q.GetNegatedDynamicTags() 262 + assert.Equal(t, []string{"priority:low", "severity:minor"}, negated) 263 + 264 + // Known tags still work as before 265 + assert.Equal(t, []string{"bug"}, q.GetAll("label")) 266 + val := q.Get("state") 267 + assert.NotNil(t, val) 268 + assert.Equal(t, "open", *val) 269 + } 270 + 271 + func TestDynamicTagsEmpty(t *testing.T) { 272 + q := Parse("state:open label:bug author:alice keyword") 273 + assert.Equal(t, 0, len(q.GetDynamicTags())) 274 + assert.Equal(t, 0, len(q.GetNegatedDynamicTags())) 275 + }