porting all github actions from bluesky-social/indigo to tangled CI
at main 3.9 kB view raw
1package lexicon 2 3import ( 4 "embed" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/fs" 9 "log/slog" 10 "os" 11 "path/filepath" 12 "strings" 13) 14 15// Interface type for a resolver or container of lexicon schemas, and methods for validating generic data against those schemas. 16type Catalog interface { 17 // Looks up a schema reference (NSID string with optional fragment) to a Schema object. 18 Resolve(ref string) (*Schema, error) 19} 20 21// Trivial in-memory Lexicon Catalog implementation. 22type BaseCatalog struct { 23 schemas map[string]Schema 24} 25 26// Creates a new empty BaseCatalog 27func NewBaseCatalog() BaseCatalog { 28 return BaseCatalog{ 29 schemas: make(map[string]Schema), 30 } 31} 32 33// Returns a scheman definition (`Schema` struct) for a Lexicon reference. 34// 35// A Lexicon ref string is an NSID with an optional #-separated fragment. If the fragment isn't specified, '#main' is used by default. 36func (c *BaseCatalog) Resolve(ref string) (*Schema, error) { 37 if ref == "" { 38 return nil, fmt.Errorf("tried to resolve empty string name") 39 } 40 // default to #main if name doesn't have a fragment 41 if !strings.Contains(ref, "#") { 42 ref = ref + "#main" 43 } 44 s, ok := c.schemas[ref] 45 if !ok { 46 return nil, fmt.Errorf("schema not found in catalog: %s", ref) 47 } 48 return &s, nil 49} 50 51// Inserts a schema loaded from a JSON file in to the catalog. 52func (c *BaseCatalog) AddSchemaFile(sf SchemaFile) error { 53 if sf.Lexicon != 1 { 54 return fmt.Errorf("unsupported lexicon language version: %d", sf.Lexicon) 55 } 56 base := sf.ID 57 for frag, def := range sf.Defs { 58 if len(frag) == 0 || strings.Contains(frag, "#") || strings.Contains(frag, ".") { 59 // TODO: more validation here? 60 return fmt.Errorf("schema name invalid: %s", frag) 61 } 62 name := base + "#" + frag 63 if _, ok := c.schemas[name]; ok { 64 return fmt.Errorf("catalog already contained a schema with name: %s", name) 65 } 66 // "A file can have at most one definition with one of the "primary" types. Primary types should always have the name main. It is possible for main to describe a non-primary type." 67 switch s := def.Inner.(type) { 68 case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription: 69 if frag != "main" { 70 return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag) 71 } 72 case SchemaToken: 73 // add fully-qualified name to token 74 s.fullName = name 75 def.Inner = s 76 } 77 def.SetBase(base) 78 if err := def.CheckSchema(); err != nil { 79 return err 80 } 81 s := Schema{ 82 ID: name, 83 Def: def.Inner, 84 } 85 c.schemas[name] = s 86 } 87 return nil 88} 89 90// internal helper for loading JSON files from bytes 91func (c *BaseCatalog) addSchemaFromBytes(b []byte) error { 92 var sf SchemaFile 93 if err := json.Unmarshal(b, &sf); err != nil { 94 return err 95 } 96 if err := c.AddSchemaFile(sf); err != nil { 97 return err 98 } 99 return nil 100} 101 102// Recursively loads all '.json' files from a directory in to the catalog. 103func (c *BaseCatalog) LoadDirectory(dirPath string) error { 104 walkFunc := func(p string, d fs.DirEntry, err error) error { 105 if err != nil { 106 return err 107 } 108 if d.IsDir() { 109 return nil 110 } 111 if !strings.HasSuffix(p, ".json") { 112 return nil 113 } 114 slog.Debug("loading Lexicon schema file", "path", p) 115 f, err := os.Open(p) 116 if err != nil { 117 return err 118 } 119 defer func() { _ = f.Close() }() 120 121 b, err := io.ReadAll(f) 122 if err != nil { 123 return err 124 } 125 return c.addSchemaFromBytes(b) 126 } 127 return filepath.WalkDir(dirPath, walkFunc) 128} 129 130// Recursively loads all '.json' files from an embed.FS 131func (c *BaseCatalog) LoadEmbedFS(efs embed.FS) error { 132 walkFunc := func(p string, d fs.DirEntry, err error) error { 133 if err != nil { 134 return err 135 } 136 if d.IsDir() { 137 return nil 138 } 139 if !strings.HasSuffix(p, ".json") { 140 return nil 141 } 142 143 slog.Debug("loading embedded Lexicon schema file", "path", p) 144 b, err := efs.ReadFile(p) 145 if err != nil { 146 return err 147 } 148 return c.addSchemaFromBytes(b) 149 } 150 return fs.WalkDir(efs, ".", walkFunc) 151}