···11+package main
22+33+import (
44+ "github.com/bluesky-social/indigo/atproto/lexicon"
55+ "github.com/bluesky-social/indigo/atproto/syntax"
66+)
77+88+// Intermediate representation of a complete lexicon schema file, containing one or more definitions.
99+type FlatLexicon struct {
1010+ NSID syntax.NSID
1111+ ExternalRefs map[string]bool // NSID with optional ref
1212+ SelfRefs map[string]bool // def names
1313+ Defs map[string]FlatDef
1414+ Types []FlatType
1515+}
1616+1717+// Minimal context about an individual top-level schema definition: just the short name and schema type.
1818+type FlatDef struct {
1919+ Name string
2020+ Type string
2121+}
2222+2323+// An individual "type definition", which is a small unit of schema definition that corresponds to a named unit of generated code. For example, a struct or API endpoint.
2424+type FlatType struct {
2525+ // the short name of the schema def that this type is under
2626+ DefName string
2727+ Path []string
2828+ Type string
2929+ Schema *lexicon.SchemaDef
3030+}
3131+3232+func FlattenSchemaFile(sf *lexicon.SchemaFile) (*FlatLexicon, error) {
3333+ // XXX
3434+}
3535+3636+func (fl *FlatLexicon) flattenDef(name string, def *lexicon.SchemaDef) error {
3737+ // XXX
3838+}
3939+4040+func (fl *FlatLexicon) flattenType(fd *FlatDef, tpath []string, def *lexicon.SchemaDef) error {
4141+ // XXX
4242+}
+81-151
cmd/lexgen/main.go
···11package main
2233import (
44- "errors"
44+ "context"
55+ "encoding/json"
56 "fmt"
67 "io/fs"
88+ "log/slog"
79 "os"
1010+ "path"
811 "path/filepath"
99- "strings"
10121111- "github.com/bluesky-social/indigo/lex"
1212- "github.com/urfave/cli/v2"
1313-)
1313+ _ "github.com/joho/godotenv/autoload"
14141515-func findSchemas(dir string, out []string) ([]string, error) {
1616- err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
1717- if err != nil {
1818- return err
1919- }
1515+ "github.com/bluesky-social/indigo/atproto/lexicon"
20162121- if info.IsDir() {
2222- return nil
2323- }
2424-2525- if strings.HasSuffix(path, ".json") {
2626- out = append(out, path)
2727- }
1717+ "github.com/earthboundkid/versioninfo/v2"
1818+ "github.com/urfave/cli/v3"
1919+)
28202929- return nil
3030- })
3131- if err != nil {
3232- return out, err
2121+func main() {
2222+ if err := run(os.Args); err != nil {
2323+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
2424+ os.Exit(-1)
3325 }
3434-3535- return out, nil
3636-3726}
38273939-// for direct .json lexicon files or directories containing lexicon .json files, get one flat list of all paths to .json files
4040-func expandArgs(args []string) ([]string, error) {
4141- var out []string
4242- for _, a := range args {
4343- st, err := os.Stat(a)
4444- if err != nil {
4545- return nil, err
4646- }
4747- if st.IsDir() {
4848- out, err = findSchemas(a, out)
4949- if err != nil {
5050- return nil, err
5151- }
5252- } else if strings.HasSuffix(a, ".json") {
5353- out = append(out, a)
5454- }
5555- }
2828+func run(args []string) error {
56295757- return out, nil
3030+ app := cli.Command{
3131+ Name: "lexgen",
3232+ Usage: "AT lexicon code generation for Go",
3333+ //Description: "",
3434+ Version: versioninfo.Short(),
3535+ }
3636+ app.Commands = []*cli.Command{
3737+ cmdGenerate,
3838+ }
3939+ return app.Run(context.Background(), args)
5840}
59416060-func main() {
6161- app := cli.NewApp()
6262-6363- app.Flags = []cli.Flag{
4242+var cmdGenerate = &cli.Command{
4343+ Name: "generate",
4444+ Usage: "check schema syntax, best practices, and style",
4545+ ArgsUsage: `<file-or-dir>*`,
4646+ Flags: []cli.Flag{
6447 &cli.StringFlag{
6565- Name: "outdir",
6666- },
6767- &cli.BoolFlag{
6868- Name: "gen-server",
4848+ Name: "lexicons-dir",
4949+ Value: "./lexicons/",
5050+ Usage: "base directory for project Lexicon files",
5151+ Sources: cli.EnvVars("LEXICONS_DIR"),
6952 },
7053 &cli.BoolFlag{
7171- Name: "gen-handlers",
7272- },
7373- &cli.StringSliceFlag{
7474- Name: "types-import",
5454+ Name: "json",
5555+ Usage: "output structured JSON",
7556 },
7676- &cli.StringSliceFlag{
7777- Name: "external-lexicons",
7878- },
7979- &cli.StringFlag{
8080- Name: "package",
8181- Value: "schemagen",
8282- },
8383- &cli.StringFlag{
8484- Name: "build",
8585- Value: "",
8686- },
8787- &cli.StringFlag{
8888- Name: "build-file",
8989- Value: "",
9090- },
9191- }
9292- app.Action = func(cctx *cli.Context) error {
9393- paths, err := expandArgs(cctx.Args().Slice())
5757+ },
5858+ Action: runGenerate,
5959+}
6060+6161+func runGenerate(ctx context.Context, cmd *cli.Command) error {
6262+ paths := cmd.Args().Slice()
6363+ if !cmd.Args().Present() {
6464+ paths = []string{cmd.String("lexicons-dir")}
6565+ _, err := os.Stat(paths[0])
9466 if err != nil {
9595- return err
6767+ return fmt.Errorf("no path arguments specified and default lexicon directory not found\n%w", err)
9668 }
6969+ }
97709898- var schemas []*lex.Schema
9999- for _, arg := range paths {
100100- if strings.HasSuffix(arg, "com/atproto/temp/importRepo.json") {
101101- fmt.Printf("skipping schema: %s\n", arg)
102102- continue
103103- }
104104- s, err := lex.ReadSchema(arg)
105105- if err != nil {
106106- return fmt.Errorf("failed to read file %q: %w", arg, err)
107107- }
7171+ // TODO: load up entire directory in to a catalog? or have a "linter" struct?
10872109109- schemas = append(schemas, s)
110110- }
111111-112112- externalPaths, err := expandArgs(cctx.StringSlice("external-lexicons"))
7373+ slog.Debug("starting lint run")
7474+ for _, p := range paths {
7575+ finfo, err := os.Stat(p)
11376 if err != nil {
114114- return err
7777+ return fmt.Errorf("failed loading %s: %w", p, err)
11578 }
116116- var externalSchemas []*lex.Schema
117117- for _, arg := range externalPaths {
118118- s, err := lex.ReadSchema(arg)
119119- if err != nil {
120120- return fmt.Errorf("failed to read file %q: %w", arg, err)
7979+ if finfo.IsDir() {
8080+ if err := filepath.WalkDir(p, func(fp string, d fs.DirEntry, err error) error {
8181+ if d.IsDir() || path.Ext(fp) != ".json" {
8282+ return nil
8383+ }
8484+ return blah(ctx, cmd, fp)
8585+ }); err != nil {
8686+ return err
12187 }
122122-123123- externalSchemas = append(externalSchemas, s)
8888+ continue
12489 }
125125-126126- buildLiteral := cctx.String("build")
127127- buildPath := cctx.String("build-file")
128128- var packages []lex.Package
129129- if buildLiteral != "" {
130130- if buildPath != "" {
131131- return errors.New("must not set both --build and --build-file")
132132- }
133133- packages, err = lex.ParsePackages([]byte(buildLiteral))
134134- if err != nil {
135135- return fmt.Errorf("--build error, %w", err)
136136- }
137137- if len(packages) == 0 {
138138- return errors.New("--build must specify at least one Package{}")
139139- }
140140- } else if buildPath != "" {
141141- blob, err := os.ReadFile(buildPath)
142142- if err != nil {
143143- return fmt.Errorf("--build-file error, %w", err)
144144- }
145145- packages, err = lex.ParsePackages(blob)
146146- if err != nil {
147147- return fmt.Errorf("--build-file error, %w", err)
148148- }
149149- if len(packages) == 0 {
150150- return errors.New("--build-file must specify at least one Package{}")
151151- }
152152- } else {
153153- return errors.New("need exactly one of --build or --build-file")
9090+ if err := blah(ctx, cmd, p); err != nil {
9191+ return err
15492 }
155155-156156- if cctx.Bool("gen-server") {
157157- pkgname := cctx.String("package")
158158- outdir := cctx.String("outdir")
159159- if outdir == "" {
160160- return fmt.Errorf("must specify output directory (--outdir)")
161161- }
162162- defmap := lex.BuildExtDefMap(append(schemas, externalSchemas...), packages)
163163- _ = defmap
164164-165165- paths := cctx.StringSlice("types-import")
166166- importmap := make(map[string]string)
167167- for _, p := range paths {
168168- parts := strings.Split(p, ":")
169169- importmap[parts[0]] = parts[1]
170170- }
171171-172172- handlers := cctx.Bool("gen-handlers")
9393+ }
9494+ return nil
9595+}
17396174174- if err := lex.CreateHandlerStub(pkgname, importmap, outdir, schemas, handlers); err != nil {
175175- return err
176176- }
9797+func blah(ctx context.Context, cmd *cli.Command, p string) error {
9898+ b, err := os.ReadFile(p)
9999+ if err != nil {
100100+ return err
101101+ }
177102178178- } else {
179179- return lex.Run(schemas, externalSchemas, packages)
180180- }
103103+ // parse file regularly
104104+ // TODO: use json/v2 when available for case-sensitivity
105105+ var sf lexicon.SchemaFile
181106182182- return nil
107107+ // two-part parsing before looking at errors
108108+ err = json.Unmarshal(b, &sf)
109109+ if err == nil {
110110+ err = sf.FinishParse()
111111+ }
112112+ if err != nil {
113113+ return err
183114 }
184184-185185- app.RunAndExitOnError()
115115+ return nil
186116}