this repo has no description
1package main
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "log/slog"
8 "sort"
9 "strings"
10
11 "github.com/bluesky-social/indigo/api/agnostic"
12 "github.com/bluesky-social/indigo/atproto/atclient"
13 "github.com/bluesky-social/indigo/atproto/identity"
14 "github.com/bluesky-social/indigo/atproto/lexicon"
15 "github.com/bluesky-social/indigo/atproto/syntax"
16
17 "github.com/urfave/cli/v3"
18)
19
20var (
21 schemaNSID = syntax.NSID("com.atproto.lexicon.schema")
22)
23
24func nsidGroup(nsid syntax.NSID) string {
25 parts := strings.Split(string(nsid), ".")
26 g := strings.Join(parts[0:len(parts)-1], ".") + "."
27 return g
28}
29
30// Checks if a string is a valid NSID group pattern, which is a partial NSID ending in '.' or '.*'
31func ParseNSIDGroup(raw string) (string, error) {
32 if strings.HasSuffix(raw, ".*") {
33 raw = raw[:len(raw)-1]
34 }
35 if !strings.HasSuffix(raw, ".") {
36 return "", fmt.Errorf("not an NSID group pattern")
37 }
38 _, err := syntax.ParseNSID(raw + "name")
39 if err != nil {
40 return "", fmt.Errorf("not an NSID group pattern")
41 }
42 return raw, nil
43}
44
45// helper which runs a comparison function across local and remote schemas, based on 'cmd' configuration
46func runComparisons(ctx context.Context, cmd *cli.Command, comp func(ctx context.Context, cmd *cli.Command, nsid syntax.NSID, localJSON, remoteJSON json.RawMessage) error) error {
47
48 // collect all NSID/path mappings
49 localSchemas, err := collectSchemaJSON(cmd)
50 if err != nil {
51 return err
52 }
53 remoteSchemas := map[syntax.NSID]json.RawMessage{}
54
55 localGroups := map[string]bool{}
56 allNSIDMap := map[syntax.NSID]bool{}
57 for k := range localSchemas {
58 g := nsidGroup(k)
59 localGroups[g] = true
60 allNSIDMap[k] = true
61 }
62
63 for g := range localGroups {
64 if err := resolveLexiconGroup(ctx, cmd, g, &remoteSchemas); err != nil {
65 return err
66 }
67 }
68
69 for k := range remoteSchemas {
70 allNSIDMap[k] = true
71 }
72 allNSID := []string{}
73 for k := range allNSIDMap {
74 allNSID = append(allNSID, string(k))
75 }
76 sort.Strings(allNSID)
77
78 anyFailures := false
79 for _, k := range allNSID {
80 nsid := syntax.NSID(k)
81 if err := comp(ctx, cmd, nsid, localSchemas[nsid], remoteSchemas[nsid]); err != nil {
82 if err != ErrLintFailures {
83 return err
84 }
85 anyFailures = true
86 }
87 }
88
89 if anyFailures {
90 return ErrLintFailures
91 }
92 return nil
93}
94
95// helper which resolves and fetches all lexicon schemas (as JSON), storing them in provided map
96func resolveLexiconGroup(ctx context.Context, cmd *cli.Command, group string, remote *map[syntax.NSID]json.RawMessage) error {
97
98 slog.Debug("resolving schemas for NSID group", "group", group)
99
100 // TODO: netclient support for listing records
101 dir := identity.BaseDirectory{}
102 did, err := dir.ResolveNSID(ctx, syntax.NSID(group+"name"))
103 if err != nil {
104 // if NSID isn't registered, just skip comparison
105 slog.Warn("skipping NSID pattern which did not resolve", "group", group)
106 return nil
107 }
108 ident, err := dir.LookupDID(ctx, did)
109 if err != nil {
110 return err
111 }
112 c := atclient.NewAPIClient(ident.PDSEndpoint())
113
114 cursor := ""
115 for {
116 // collection string, cursor string, limit int64, repo string, reverse bool
117 resp, err := agnostic.RepoListRecords(ctx, c, schemaNSID.String(), cursor, 100, ident.DID.String(), false)
118 if err != nil {
119 return err
120 }
121 for _, rec := range resp.Records {
122 aturi, err := syntax.ParseATURI(rec.Uri)
123 if err != nil {
124 return err
125 }
126 nsid, err := syntax.ParseNSID(aturi.RecordKey().String())
127 if err != nil {
128 slog.Warn("ignoring invalid schema NSID", "did", ident.DID, "rkey", aturi.RecordKey())
129 continue
130 }
131 if nsidGroup(nsid) != group {
132 // ignoring other NSIDs
133 continue
134 }
135 if rec.Value == nil {
136 return fmt.Errorf("missing record value: %s", nsid)
137 }
138
139 // parse file to check for errors
140 // TODO: use json/v2 when available for case-sensitivity
141 var sf lexicon.SchemaFile
142 err = json.Unmarshal(*rec.Value, &sf)
143 if err == nil {
144 err = sf.FinishParse()
145 }
146 if err == nil {
147 err = sf.CheckSchema()
148 }
149 if err != nil {
150 return fmt.Errorf("invalid lexicon schema record (%s): %w", nsid, err)
151 }
152
153 (*remote)[nsid] = *rec.Value
154
155 }
156 if resp.Cursor != nil && *resp.Cursor != "" {
157 cursor = *resp.Cursor
158 } else {
159 break
160 }
161 }
162 return nil
163}