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}