diff --git a/cmd/glot/breaking.go b/cmd/glot/breaking.go index cca8d7d..a78bfc9 100644 --- a/cmd/glot/breaking.go +++ b/cmd/glot/breaking.go @@ -22,7 +22,7 @@ var cmdBreaking = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lexicons-dir", - Value: "./lexicons/", + Value: "lexicons/", Usage: "base directory for project Lexicon files", Sources: cli.EnvVars("LEXICONS_DIR"), }, @@ -35,10 +35,10 @@ var cmdBreaking = &cli.Command{ } func runBreaking(ctx context.Context, cmd *cli.Command) error { - return compareSchemas(ctx, cmd, breakingCompare) + return runComparisons(ctx, cmd, compareBreaking) } -func breakingCompare(ctx context.Context, cmd *cli.Command, nsid syntax.NSID, localJSON, remoteJSON json.RawMessage) error { +func compareBreaking(ctx context.Context, cmd *cli.Command, nsid syntax.NSID, localJSON, remoteJSON json.RawMessage) error { // skip schemas which aren't in both locations if localJSON == nil || remoteJSON == nil { diff --git a/cmd/glot/dns.go b/cmd/glot/check_dns.go similarity index 70% rename from cmd/glot/dns.go rename to cmd/glot/check_dns.go index 4a79200..f0ad81e 100644 --- a/cmd/glot/dns.go +++ b/cmd/glot/check_dns.go @@ -2,12 +2,7 @@ package main import ( "context" - "encoding/json" "fmt" - "io/fs" - "os" - "path" - "path/filepath" "sort" "github.com/bluesky-social/indigo/atproto/identity" @@ -24,7 +19,7 @@ var cmdCheckDNS = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lexicons-dir", - Value: "./lexicons/", + Value: "lexicons/", Usage: "base directory for project Lexicon files", Sources: cli.EnvVars("LEXICONS_DIR"), }, @@ -44,44 +39,10 @@ var cmdCheckDNS = &cli.Command{ */ func runCheckDNS(ctx context.Context, cmd *cli.Command) error { - paths := cmd.Args().Slice() - if !cmd.Args().Present() { - paths = []string{cmd.String("lexicons-dir")} - _, err := os.Stat(paths[0]) - if err != nil { - return fmt.Errorf("no path arguments specified and default lexicon directory not found\n%w", err) - } - } - // collect all NSID/path mappings - localSchemas := map[syntax.NSID]json.RawMessage{} - - for _, p := range paths { - finfo, err := os.Stat(p) - if err != nil { - return fmt.Errorf("failed loading %s: %w", p, err) - } - if finfo.IsDir() { - if err := filepath.WalkDir(p, func(fp string, d fs.DirEntry, err error) error { - if d.IsDir() || path.Ext(fp) != ".json" { - return nil - } - nsid, rec, err := loadSchemaPath(fp) - if err != nil { - return err - } - localSchemas[nsid] = *rec - return nil - }); err != nil { - return err - } - continue - } - nsid, rec, err := loadSchemaPath(p) - if err != nil { - return err - } - localSchemas[nsid] = *rec + localSchemas, err := collectSchemaJSON(cmd) + if err != nil { + return err } localGroups := map[string]bool{} diff --git a/cmd/glot/diff.go b/cmd/glot/diff.go index b835cfc..fb7dbf5 100644 --- a/cmd/glot/diff.go +++ b/cmd/glot/diff.go @@ -21,7 +21,7 @@ var cmdDiff = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lexicons-dir", - Value: "./lexicons/", + Value: "lexicons/", Usage: "base directory for project Lexicon files", Sources: cli.EnvVars("LEXICONS_DIR"), }, @@ -30,10 +30,10 @@ var cmdDiff = &cli.Command{ } func runDiff(ctx context.Context, cmd *cli.Command) error { - return compareSchemas(ctx, cmd, diffCompare) + return runComparisons(ctx, cmd, compareDiff) } -func diffCompare(ctx context.Context, cmd *cli.Command, nsid syntax.NSID, localJSON, remoteJSON json.RawMessage) error { +func compareDiff(ctx context.Context, cmd *cli.Command, nsid syntax.NSID, localJSON, remoteJSON json.RawMessage) error { // skip schemas which aren't in both locations if localJSON == nil || remoteJSON == nil { diff --git a/cmd/glot/lint.go b/cmd/glot/lint.go index 03ccdc4..cc11ee1 100644 --- a/cmd/glot/lint.go +++ b/cmd/glot/lint.go @@ -6,11 +6,8 @@ import ( "encoding/json" "errors" "fmt" - "io/fs" "log/slog" "os" - "path" - "path/filepath" "regexp" "slices" @@ -33,7 +30,7 @@ var cmdLint = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lexicons-dir", - Value: "./lexicons/", + Value: "lexicons/", Usage: "base directory for project Lexicon files", Sources: cli.EnvVars("LEXICONS_DIR"), }, @@ -46,41 +43,20 @@ var cmdLint = &cli.Command{ } func runLint(ctx context.Context, cmd *cli.Command) error { - paths := cmd.Args().Slice() - if !cmd.Args().Present() { - paths = []string{cmd.String("lexicons-dir")} - _, err := os.Stat(paths[0]) - if err != nil { - return fmt.Errorf("no path arguments specified and default lexicon directory not found\n%w", err) - } + + // enumerate lexicon JSON file paths + filePaths, err := collectPaths(cmd) + if err != nil { + return err } // TODO: load up entire directory in to a catalog? or have a "linter" struct? slog.Debug("starting lint run") anyFailures := false - for _, p := range paths { - finfo, err := os.Stat(p) + for _, fp := range filePaths { + err = lintFilePath(ctx, cmd, fp) if err != nil { - return fmt.Errorf("failed loading %s: %w", p, err) - } - if finfo.IsDir() { - if err := filepath.WalkDir(p, func(fp string, d fs.DirEntry, err error) error { - if d.IsDir() || path.Ext(fp) != ".json" { - return nil - } - err = lintFilePath(ctx, cmd, fp) - if err == ErrLintFailures { - anyFailures = true - return nil - } - return err - }); err != nil { - return err - } - continue - } - if err := lintFilePath(ctx, cmd, p); err != nil { if err == ErrLintFailures { anyFailures = true } else { diff --git a/cmd/glot/new.go b/cmd/glot/new.go index 0d799f3..6362ba0 100644 --- a/cmd/glot/new.go +++ b/cmd/glot/new.go @@ -41,7 +41,7 @@ var cmdNew = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lexicons-dir", - Value: "./lexicons/", + Value: "lexicons/", Usage: "base directory for project Lexicon files", Sources: cli.EnvVars("LEXICONS_DIR"), }, diff --git a/cmd/glot/publish.go b/cmd/glot/publish.go index 8b411c1..f6f1f10 100644 --- a/cmd/glot/publish.go +++ b/cmd/glot/publish.go @@ -4,10 +4,6 @@ import ( "context" "encoding/json" "fmt" - "io/fs" - "os" - "path" - "path/filepath" "reflect" "sort" @@ -28,7 +24,7 @@ var cmdPublish = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lexicons-dir", - Value: "./lexicons/", + Value: "lexicons/", Usage: "base directory for project Lexicon files", Sources: cli.EnvVars("LEXICONS_DIR"), }, @@ -86,46 +82,12 @@ func runPublish(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("require API client to have DID configured") } - paths := cmd.Args().Slice() - if !cmd.Args().Present() { - paths = []string{cmd.String("lexicons-dir")} - _, err := os.Stat(paths[0]) - if err != nil { - return fmt.Errorf("no path arguments specified and default lexicon directory not found\n%w", err) - } - } - // collect all NSID/path mappings - localSchemas := map[syntax.NSID]json.RawMessage{} - remoteSchemas := map[syntax.NSID]json.RawMessage{} - - for _, p := range paths { - finfo, err := os.Stat(p) - if err != nil { - return fmt.Errorf("failed loading %s: %w", p, err) - } - if finfo.IsDir() { - if err := filepath.WalkDir(p, func(fp string, d fs.DirEntry, err error) error { - if d.IsDir() || path.Ext(fp) != ".json" { - return nil - } - nsid, rec, err := loadSchemaPath(fp) - if err != nil { - return err - } - localSchemas[nsid] = *rec - return nil - }); err != nil { - return err - } - continue - } - nsid, rec, err := loadSchemaPath(p) - if err != nil { - return err - } - localSchemas[nsid] = *rec + localSchemas, err := collectSchemaJSON(cmd) + if err != nil { + return err } + remoteSchemas := map[syntax.NSID]json.RawMessage{} localGroups := map[string]bool{} allNSIDMap := map[syntax.NSID]bool{} @@ -136,7 +98,7 @@ func runPublish(ctx context.Context, cmd *cli.Command) error { } for g := range localGroups { - if err := fetchLexiconGroup(ctx, cmd, g, &remoteSchemas); err != nil { + if err := resolveLexiconGroup(ctx, cmd, g, &remoteSchemas); err != nil { return err } } diff --git a/cmd/glot/pull.go b/cmd/glot/pull.go index 34f76e2..7c5b136 100644 --- a/cmd/glot/pull.go +++ b/cmd/glot/pull.go @@ -27,7 +27,7 @@ var cmdPull = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lexicons-dir", - Value: "./lexicons/", + Value: "lexicons/", Usage: "base directory for project Lexicon files", Sources: cli.EnvVars("LEXICONS_DIR"), }, diff --git a/cmd/glot/status.go b/cmd/glot/status.go index 4f4fd9d..09da0a4 100644 --- a/cmd/glot/status.go +++ b/cmd/glot/status.go @@ -4,19 +4,9 @@ import ( "context" "encoding/json" "fmt" - "io/fs" - "log/slog" - "os" - "path" - "path/filepath" "reflect" - "sort" - "github.com/bluesky-social/indigo/api/agnostic" - "github.com/bluesky-social/indigo/atproto/atclient" "github.com/bluesky-social/indigo/atproto/atdata" - "github.com/bluesky-social/indigo/atproto/identity" - "github.com/bluesky-social/indigo/atproto/lexicon" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/urfave/cli/v3" @@ -30,7 +20,7 @@ var cmdStatus = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lexicons-dir", - Value: "./lexicons/", + Value: "lexicons/", Usage: "base directory for project Lexicon files", Sources: cli.EnvVars("LEXICONS_DIR"), }, @@ -38,38 +28,11 @@ var cmdStatus = &cli.Command{ Action: runStatus, } -func loadSchemaPath(fpath string) (syntax.NSID, *json.RawMessage, error) { - b, err := os.ReadFile(fpath) - if err != nil { - return "", nil, err - } - - // parse file to check for errors - // TODO: use json/v2 when available for case-sensitivity - var sf lexicon.SchemaFile - err = json.Unmarshal(b, &sf) - if err == nil { - err = sf.FinishParse() - } - if err == nil { - err = sf.CheckSchema() - } - if err != nil { - return "", nil, err - } - - var rec json.RawMessage - if err := json.Unmarshal(b, &rec); err != nil { - return "", nil, err - } - return syntax.NSID(sf.ID), &rec, nil -} - func runStatus(ctx context.Context, cmd *cli.Command) error { - return compareSchemas(ctx, cmd, statusCompare) + return runComparisons(ctx, cmd, compareStatus) } -func statusCompare(ctx context.Context, cmd *cli.Command, nsid syntax.NSID, localJSON, remoteJSON json.RawMessage) error { +func compareStatus(ctx context.Context, cmd *cli.Command, nsid syntax.NSID, localJSON, remoteJSON json.RawMessage) error { // new remote schema (missing local) if localJSON == nil { @@ -100,154 +63,3 @@ func statusCompare(ctx context.Context, cmd *cli.Command, nsid syntax.NSID, loca } return nil } - -func compareSchemas(ctx context.Context, cmd *cli.Command, comp func(ctx context.Context, cmd *cli.Command, nsid syntax.NSID, localJSON, remoteJSON json.RawMessage) error) error { - paths := cmd.Args().Slice() - if !cmd.Args().Present() { - paths = []string{cmd.String("lexicons-dir")} - _, err := os.Stat(paths[0]) - if err != nil { - return fmt.Errorf("no path arguments specified and default lexicon directory not found\n%w", err) - } - } - - // collect all NSID/path mappings - localSchemas := map[syntax.NSID]json.RawMessage{} - remoteSchemas := map[syntax.NSID]json.RawMessage{} - - for _, p := range paths { - finfo, err := os.Stat(p) - if err != nil { - return fmt.Errorf("failed loading %s: %w", p, err) - } - if finfo.IsDir() { - if err := filepath.WalkDir(p, func(fp string, d fs.DirEntry, err error) error { - if d.IsDir() || path.Ext(fp) != ".json" { - return nil - } - nsid, rec, err := loadSchemaPath(fp) - if err != nil { - return err - } - localSchemas[nsid] = *rec - return nil - }); err != nil { - return err - } - continue - } - nsid, rec, err := loadSchemaPath(p) - if err != nil { - return err - } - localSchemas[nsid] = *rec - } - - localGroups := map[string]bool{} - allNSIDMap := map[syntax.NSID]bool{} - for k := range localSchemas { - g := nsidGroup(k) - localGroups[g] = true - allNSIDMap[k] = true - } - - for g := range localGroups { - if err := fetchLexiconGroup(ctx, cmd, g, &remoteSchemas); err != nil { - return err - } - } - - for k := range remoteSchemas { - allNSIDMap[k] = true - } - allNSID := []string{} - for k := range allNSIDMap { - allNSID = append(allNSID, string(k)) - } - sort.Strings(allNSID) - - anyFailures := false - for _, k := range allNSID { - nsid := syntax.NSID(k) - if err := comp(ctx, cmd, nsid, localSchemas[nsid], remoteSchemas[nsid]); err != nil { - if err != ErrLintFailures { - return err - } - anyFailures = true - } - } - - if anyFailures { - return ErrLintFailures - } - return nil -} - -func fetchLexiconGroup(ctx context.Context, cmd *cli.Command, group string, remote *map[syntax.NSID]json.RawMessage) error { - - slog.Debug("trying to load NSID group", "group", group) - - // TODO: netclient support for listing records - dir := identity.BaseDirectory{} - did, err := dir.ResolveNSID(ctx, syntax.NSID(group+"name")) - if err != nil { - // if NSID isn't registered, just skip comparison - slog.Debug("skipping NSID pattern which did not resolve", "group", group) - return nil - } - ident, err := dir.LookupDID(ctx, did) - if err != nil { - return err - } - c := atclient.NewAPIClient(ident.PDSEndpoint()) - - cursor := "" - for { - // collection string, cursor string, limit int64, repo string, reverse bool - resp, err := agnostic.RepoListRecords(ctx, c, schemaNSID.String(), cursor, 100, ident.DID.String(), false) - if err != nil { - return err - } - for _, rec := range resp.Records { - aturi, err := syntax.ParseATURI(rec.Uri) - if err != nil { - return err - } - nsid, err := syntax.ParseNSID(aturi.RecordKey().String()) - if err != nil { - slog.Warn("ignoring invalid schema NSID", "did", ident.DID, "rkey", aturi.RecordKey()) - continue - } - if nsidGroup(nsid) != group { - // ignoring other NSIDs - continue - } - if rec.Value == nil { - return fmt.Errorf("missing record value: %s", nsid) - } - - // parse file to check for errors - // TODO: use json/v2 when available for case-sensitivity - var sf lexicon.SchemaFile - err = json.Unmarshal(*rec.Value, &sf) - if err == nil { - err = sf.FinishParse() - } - if err == nil { - err = sf.CheckSchema() - } - if err != nil { - return fmt.Errorf("invalid lexicon schema record (%s): %w", nsid, err) - } - - (*remote)[nsid] = *rec.Value - - } - if resp.Cursor != nil && *resp.Cursor != "" { - cursor = *resp.Cursor - } else { - break - } - } - return nil -} diff --git a/cmd/glot/util.go b/cmd/glot/util.go index 1eef853..6ecc43c 100644 --- a/cmd/glot/util.go +++ b/cmd/glot/util.go @@ -2,12 +2,9 @@ package main import ( "fmt" - "path" "strings" "github.com/bluesky-social/indigo/atproto/syntax" - - "github.com/urfave/cli/v3" ) var ( @@ -34,15 +31,3 @@ func ParseNSIDGroup(raw string) (string, error) { } return raw, nil } - -func pathForNSID(cmd *cli.Command, nsid syntax.NSID) string { - - odir := cmd.String("output-dir") - if odir != "" { - return path.Join(odir, nsid.Name()+".json") - } - - base := cmd.String("lexicons-dir") - sub := strings.ReplaceAll(nsid.String(), ".", "/") - return path.Join(base, sub+".json") -} diff --git a/cmd/glot/util_compare.go b/cmd/glot/util_compare.go new file mode 100644 index 0000000..45c3881 --- /dev/null +++ b/cmd/glot/util_compare.go @@ -0,0 +1,60 @@ +package main + +import ( + "context" + "encoding/json" + "sort" + + "github.com/bluesky-social/indigo/atproto/syntax" + + "github.com/urfave/cli/v3" +) + +func runComparisons(ctx context.Context, cmd *cli.Command, comp func(ctx context.Context, cmd *cli.Command, nsid syntax.NSID, localJSON, remoteJSON json.RawMessage) error) error { + + // collect all NSID/path mappings + localSchemas, err := collectSchemaJSON(cmd) + if err != nil { + return err + } + remoteSchemas := map[syntax.NSID]json.RawMessage{} + + localGroups := map[string]bool{} + allNSIDMap := map[syntax.NSID]bool{} + for k := range localSchemas { + g := nsidGroup(k) + localGroups[g] = true + allNSIDMap[k] = true + } + + for g := range localGroups { + if err := resolveLexiconGroup(ctx, cmd, g, &remoteSchemas); err != nil { + return err + } + } + + for k := range remoteSchemas { + allNSIDMap[k] = true + } + allNSID := []string{} + for k := range allNSIDMap { + allNSID = append(allNSID, string(k)) + } + sort.Strings(allNSID) + + anyFailures := false + for _, k := range allNSID { + nsid := syntax.NSID(k) + if err := comp(ctx, cmd, nsid, localSchemas[nsid], remoteSchemas[nsid]); err != nil { + if err != ErrLintFailures { + return err + } + anyFailures = true + } + } + + if anyFailures { + return ErrLintFailures + } + return nil +} diff --git a/cmd/glot/util_fetch.go b/cmd/glot/util_fetch.go new file mode 100644 index 0000000..126400f --- /dev/null +++ b/cmd/glot/util_fetch.go @@ -0,0 +1,86 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + + "github.com/bluesky-social/indigo/api/agnostic" + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/identity" + "github.com/bluesky-social/indigo/atproto/lexicon" + "github.com/bluesky-social/indigo/atproto/syntax" + + "github.com/urfave/cli/v3" +) + +// helper which resolves and fetches all lexicon schemas (as JSON), storing them in provided map +func resolveLexiconGroup(ctx context.Context, cmd *cli.Command, group string, remote *map[syntax.NSID]json.RawMessage) error { + + slog.Debug("resolving schemas for NSID group", "group", group) + + // TODO: netclient support for listing records + dir := identity.BaseDirectory{} + did, err := dir.ResolveNSID(ctx, syntax.NSID(group+"name")) + if err != nil { + // if NSID isn't registered, just skip comparison + slog.Warn("skipping NSID pattern which did not resolve", "group", group) + return nil + } + ident, err := dir.LookupDID(ctx, did) + if err != nil { + return err + } + c := atclient.NewAPIClient(ident.PDSEndpoint()) + + cursor := "" + for { + // collection string, cursor string, limit int64, repo string, reverse bool + resp, err := agnostic.RepoListRecords(ctx, c, schemaNSID.String(), cursor, 100, ident.DID.String(), false) + if err != nil { + return err + } + for _, rec := range resp.Records { + aturi, err := syntax.ParseATURI(rec.Uri) + if err != nil { + return err + } + nsid, err := syntax.ParseNSID(aturi.RecordKey().String()) + if err != nil { + slog.Warn("ignoring invalid schema NSID", "did", ident.DID, "rkey", aturi.RecordKey()) + continue + } + if nsidGroup(nsid) != group { + // ignoring other NSIDs + continue + } + if rec.Value == nil { + return fmt.Errorf("missing record value: %s", nsid) + } + + // parse file to check for errors + // TODO: use json/v2 when available for case-sensitivity + var sf lexicon.SchemaFile + err = json.Unmarshal(*rec.Value, &sf) + if err == nil { + err = sf.FinishParse() + } + if err == nil { + err = sf.CheckSchema() + } + if err != nil { + return fmt.Errorf("invalid lexicon schema record (%s): %w", nsid, err) + } + + (*remote)[nsid] = *rec.Value + + } + if resp.Cursor != nil && *resp.Cursor != "" { + cursor = *resp.Cursor + } else { + break + } + } + return nil +} diff --git a/cmd/glot/util_files.go b/cmd/glot/util_files.go new file mode 100644 index 0000000..606b910 --- /dev/null +++ b/cmd/glot/util_files.go @@ -0,0 +1,181 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/bluesky-social/indigo/atproto/lexicon" + "github.com/bluesky-social/indigo/atproto/syntax" + + "github.com/urfave/cli/v3" +) + +func pathForNSID(cmd *cli.Command, nsid syntax.NSID) string { + + odir := cmd.String("output-dir") + if odir != "" { + return path.Join(odir, nsid.Name()+".json") + } + + base := cmd.String("lexicons-dir") + sub := strings.ReplaceAll(nsid.String(), ".", "/") + return path.Join(base, sub+".json") +} + +// parses through directories and files provided as CLI args, and returns a list of recursively enumerated .json files +func collectPaths(cmd *cli.Command) ([]string, error) { + + paths := cmd.Args().Slice() + if !cmd.Args().Present() { + paths = []string{cmd.String("lexicons-dir")} + _, err := os.Stat(paths[0]) + if err != nil { + return nil, fmt.Errorf("no path arguments specified and default lexicon directory not found") + } + } + + filePaths := []string{} + + for _, p := range paths { + finfo, err := os.Stat(p) + if err != nil { + return nil, fmt.Errorf("failed reading path %s: %w", p, err) + } + if finfo.IsDir() { + if err := filepath.WalkDir(p, func(fp string, d fs.DirEntry, err error) error { + if d.IsDir() || path.Ext(fp) != ".json" { + return nil + } + filePaths = append(filePaths, fp) + return nil + }); err != nil { + return nil, err + } + continue + } + filePaths = append(filePaths, p) + } + + sort.Strings(filePaths) + return filePaths, nil +} + +// parses through directories and files provided as CLI args, and returns broadly inclusive lexicon catalog. +// +// includes 'lexicons-dir', which may be broader than collectPaths() would return +func collectCatalog(cmd *cli.Command) (lexicon.Catalog, error) { + + cat := lexicon.NewBaseCatalog() + + lexDir := cmd.String("lexicons-dir") + paths := cmd.Args().Slice() + if !cmd.Args().Present() { + _, err := os.Stat(lexDir) + if err != nil { + return nil, fmt.Errorf("no path arguments specified and default lexicon directory not found") + } + } + + // load lexicon dir (recursively) + ldinfo, err := os.Stat(lexDir) + if err == nil && ldinfo.IsDir() { + if err := cat.LoadDirectory(lexDir); err != nil { + return nil, err + } + } + + for _, p := range paths { + + if strings.HasPrefix(p, lexDir) { + // if path is under lexdir, we have already loaded, so skip + // NOTE: this isn't particularly reliable + continue + } + + finfo, err := os.Stat(p) + if err != nil { + return nil, fmt.Errorf("failed reading path %s: %w", p, err) + } + if finfo.IsDir() { + if p != lexDir { + if err := cat.LoadDirectory(p); err != nil { + return nil, err + } + } + continue + } + if !finfo.Mode().IsRegular() && path.Ext(p) == ".json" { + // load schema file in to catalog + f, err := os.Open(p) + if err != nil { + return nil, err + } + defer func() { _ = f.Close() }() + + b, err := io.ReadAll(f) + if err != nil { + return nil, err + } + var sf lexicon.SchemaFile + if err := json.Unmarshal(b, &sf); err != nil { + return nil, err + } + if err := cat.AddSchemaFile(sf); err != nil { + return nil, err + } + } + } + return &cat, nil +} + +func loadSchemaJSON(fpath string) (syntax.NSID, *json.RawMessage, error) { + b, err := os.ReadFile(fpath) + if err != nil { + return "", nil, err + } + + // parse file to check for errors + // TODO: use json/v2 when available for case-sensitivity + var sf lexicon.SchemaFile + err = json.Unmarshal(b, &sf) + if err == nil { + err = sf.FinishParse() + } + if err == nil { + err = sf.CheckSchema() + } + if err != nil { + return "", nil, err + } + + var rec json.RawMessage + if err := json.Unmarshal(b, &rec); err != nil { + return "", nil, err + } + return syntax.NSID(sf.ID), &rec, nil +} + +func collectSchemaJSON(cmd *cli.Command) (map[syntax.NSID]json.RawMessage, error) { + schemas := map[syntax.NSID]json.RawMessage{} + + filePaths, err := collectPaths(cmd) + if err != nil { + return nil, err + } + + for _, fp := range filePaths { + nsid, rec, err := loadSchemaJSON(fp) + if err != nil { + return nil, err + } + schemas[nsid] = *rec + } + return schemas, nil +}