Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
.PHONY: generate
generate:
go generate ./...
cp ./spec.schema.json ./validate/spec.schema.json
cp ./spec.schema.json ./web/src/spec.schema.json

.PHONY: update-schema-version
update-schema-version:
go run ./internal/genschema \
-schema ./spec.schema.json \
-out ./spec/schema_version_gen.go \
-update-documents \
-document-root .
$(MAKE) generate

.PHONY: test
test: generate
Expand Down
4 changes: 1 addition & 3 deletions adapters/ocobra/ocobra.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ import (
"github.com/spf13/pflag"
)

const defaultOpenCLIVersion = "1.0.0-alpha.14"

// config holds the functional-option state.
type config struct {
info *spec.Info
Expand Down Expand Up @@ -115,7 +113,7 @@ func GenerateDocument(rootCmd *cobra.Command, opts ...Option) *spec.Document {

func documentFromCommand(rootCmd *cobra.Command, c *config) *spec.Document {
var doc spec.Document
doc.OpenCLIVersion = defaultOpenCLIVersion
doc.OpenCLIVersion = spec.SchemaVersion
doc.Info = c.getInfo()
if doc.Info.Binary == "" {
doc.Info.Binary = GetBinaryName(rootCmd)
Expand Down
2 changes: 1 addition & 1 deletion adapters/ocobra/ocobra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestGenerateDocument_Basic(t *testing.T) {

doc := GenerateDocument(root)

if doc.OpenCLIVersion != defaultOpenCLIVersion {
if doc.OpenCLIVersion != spec.SchemaVersion {
t.Fatalf("unexpected version: %s", doc.OpenCLIVersion)
}
if doc.Info.Binary != "myapp" {
Expand Down
4 changes: 1 addition & 3 deletions adapters/ourfave/ourfave.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ import (
"github.com/urfave/cli/v3"
)

const defaultOpenCLIVersion = "1.0.0-alpha.14"

// config holds the functional-option state.
type config struct {
info *spec.Info
Expand Down Expand Up @@ -115,7 +113,7 @@ func GenerateDocument(rootCmd *cli.Command, opts ...Option) *spec.Document {

func documentFromCommand(rootCmd *cli.Command, c *config) *spec.Document {
var doc spec.Document
doc.OpenCLIVersion = defaultOpenCLIVersion
doc.OpenCLIVersion = spec.SchemaVersion
doc.Info = c.getInfo()
if doc.Info.Binary == "" {
doc.Info.Binary = GetBinaryName(rootCmd)
Expand Down
2 changes: 1 addition & 1 deletion adapters/ourfave/ourfave_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestGenerateDocument_Basic(t *testing.T) {

doc := GenerateDocument(root)

if doc.OpenCLIVersion != defaultOpenCLIVersion {
if doc.OpenCLIVersion != spec.SchemaVersion {
t.Fatalf("unexpected version: %s", doc.OpenCLIVersion)
}
if doc.Info.Binary != "myapp" {
Expand Down
8 changes: 4 additions & 4 deletions codec/default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func defaultTestSpecYAML(t *testing.T, frag string) []byte {
t.Helper()

doc := strings.Join([]string{
"opencliVersion: 1.0.0-alpha.14",
"opencliVersion: " + spec.SchemaVersion,
"info:",
" title: default test",
" version: 0.1.0",
Expand All @@ -34,7 +34,7 @@ func defaultTestSpecYAML(t *testing.T, frag string) []byte {
func defaultTestSpecJSON(t *testing.T, flagObj string) []byte {
t.Helper()

doc := `{"opencliVersion":"1.0.0-alpha.14","info":{"title":"default test","version":"0.1.0","binary":"deftest"},"commands":{"root":{"flags":[` + flagObj + `]}}}`
doc := `{"opencliVersion":"` + spec.SchemaVersion + `","info":{"title":"default test","version":"0.1.0","binary":"deftest"},"commands":{"root":{"flags":[` + flagObj + `]}}}`

return []byte(doc)
}
Expand All @@ -44,7 +44,7 @@ func globalFlagSpecYAML(t *testing.T, frag string) []byte {
t.Helper()

doc := strings.Join([]string{
"opencliVersion: 1.0.0-alpha.14",
"opencliVersion: " + spec.SchemaVersion,
"info:",
" title: default test",
" version: 0.1.0",
Expand All @@ -64,7 +64,7 @@ func globalFlagSpecYAML(t *testing.T, frag string) []byte {
func globalFlagSpecJSON(t *testing.T, flagObj string) []byte {
t.Helper()

doc := `{"opencliVersion":"1.0.0-alpha.14","info":{"title":"default test","version":"0.1.0","binary":"deftest"},"global":{"flags":[` + flagObj + `]},"commands":{"root":{}}}`
doc := `{"opencliVersion":"` + spec.SchemaVersion + `","info":{"title":"default test","version":"0.1.0","binary":"deftest"},"global":{"flags":[` + flagObj + `]},"commands":{"root":{}}}`

return []byte(doc)
}
Expand Down
2 changes: 1 addition & 1 deletion gen/cli_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// which validation forbids but callers may skip (e.g., ocli gen cli).
func groupTestDoc(root *spec.CommandItem) *spec.Document {
return &spec.Document{
OpenCLIVersion: "1.0.0-alpha.14",
OpenCLIVersion: spec.SchemaVersion,
Info: spec.Info{Title: "Group Test CLI", Binary: "grouptest"},
Commands: root,
}
Expand Down
6 changes: 3 additions & 3 deletions gen/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ func TestCLI_UrfaveCli_ConfigEmissionConditional(t *testing.T) {
}

t.Run("no_alt_sources", func(t *testing.T) {
files := generate(t, `opencliVersion: 1.0.0-alpha.14
files := generate(t, `opencliVersion: `+spec.SchemaVersion+`
Comment thread
bcdxn marked this conversation as resolved.
info:
title: minimal cli for alt-source emission test
binary: minicli
Expand All @@ -668,7 +668,7 @@ commands:
})

t.Run("with_alt_sources", func(t *testing.T) {
files := generate(t, `opencliVersion: 1.0.0-alpha.14
files := generate(t, `opencliVersion: `+spec.SchemaVersion+`
info:
title: minimal cli for alt-source emission test
binary: minicli
Expand Down Expand Up @@ -868,7 +868,7 @@ func TestCLI_Yargs_ConfigEmissionConditional(t *testing.T) {
}

t.Run("no_alt_sources", func(t *testing.T) {
files := generate(t, `opencliVersion: 1.0.0-alpha.14
files := generate(t, `opencliVersion: `+spec.SchemaVersion+`
info:
title: minimal cli for alt-source emission test
binary: minicli
Expand Down
2 changes: 1 addition & 1 deletion gen/cli_urfave_cli_globalchoices_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
// their plain Go types (no cast, no validation) to avoid golden churn for specs without choices.
func TestCLI_UrfaveCliGlobalChoicesValidation(t *testing.T) {
doc := &spec.Document{
OpenCLIVersion: "1.0.0-alpha.14",
OpenCLIVersion: spec.SchemaVersion,
Info: spec.Info{Title: "GChoice CLI", Binary: "gchoice"},
Global: &spec.Global{
Flags: []spec.FlagItem{
Expand Down
4 changes: 2 additions & 2 deletions gen/cli_yargs_requiredaltsources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
// must not be validated, and required flags WITHOUT alternative sources keep plain demandOption.
func TestCLI_YargsRequiredAltSourcesValidation(t *testing.T) {
doc := &spec.Document{
OpenCLIVersion: "1.0.0-alpha.14",
OpenCLIVersion: spec.SchemaVersion,
Info: spec.Info{Title: "ReqAlt CLI", Binary: "reqalt"},
Commands: &spec.CommandItem{
Segment: "login",
Expand Down Expand Up @@ -111,7 +111,7 @@ func TestCLI_YargsRequiredAltSourcesValidation(t *testing.T) {
func TestCLI_YargsRequiredAltSourcesGlobalValidation(t *testing.T) {
makeDoc := func(globalFlags []spec.FlagItem, cmdFlags []spec.FlagItem) *spec.Document {
return &spec.Document{
OpenCLIVersion: "1.0.0-alpha.14",
OpenCLIVersion: spec.SchemaVersion,
Info: spec.Info{Title: "ReqAlt Global CLI", Binary: "reqaltg"},
Global: &spec.Global{Flags: globalFlags},
Commands: &spec.CommandItem{
Expand Down
6 changes: 3 additions & 3 deletions gen/cli_yargs_variadic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
// typed. String variadics must remain byte-identical to prior output.
func TestCLI_YargsVariadicNonString(t *testing.T) {
doc := &spec.Document{
OpenCLIVersion: "1.0.0-alpha.14",
OpenCLIVersion: spec.SchemaVersion,
Info: spec.Info{Title: "VarTest CLI", Binary: "vartest"},
Commands: &spec.CommandItem{
Segment: "greet",
Expand Down Expand Up @@ -78,7 +78,7 @@ func TestCLI_YargsVariadicNonString(t *testing.T) {
// on GlobalFlags. String variadics must keep string:true (golden parity).
func TestCLI_YargsGlobalVariadicNonString(t *testing.T) {
doc := &spec.Document{
OpenCLIVersion: "1.0.0-alpha.14",
OpenCLIVersion: spec.SchemaVersion,
Info: spec.Info{Title: "GVarTest CLI", Binary: "gvar"},
Global: &spec.Global{
Config: spec.Configuration{},
Expand Down Expand Up @@ -135,7 +135,7 @@ func TestCLI_YargsGlobalVariadicNonString(t *testing.T) {
// the boolean coerce function rather than string:true.
func TestCLI_YargsGlobalVariadicBoolean(t *testing.T) {
doc := &spec.Document{
OpenCLIVersion: "1.0.0-alpha.14",
OpenCLIVersion: spec.SchemaVersion,
Info: spec.Info{Title: "GBoolTest CLI", Binary: "gbool"},
Global: &spec.Global{
Config: spec.Configuration{},
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/app/actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func (m *MockFileWriter) Fd() uintptr {

// validSpecJSON is a minimal valid OpenCLI specification in JSON format
const validSpecJSON = `{
"opencliVersion": "1.0.0-alpha.14",
"opencliVersion": "` + spec.SchemaVersion + `",
"info": {
"title": "Test CLI",
"binary": "test",
Expand All @@ -40,7 +40,7 @@ const validSpecJSON = `{
}`

// validSpecYAML is a minimal valid OpenCLI specification in YAML format
const validSpecYAML = `opencliVersion: 1.0.0-alpha.14
const validSpecYAML = `opencliVersion: ` + spec.SchemaVersion + `
info:
title: Test CLI
binary: test
Expand All @@ -55,7 +55,7 @@ commands:

// invalidSpec is an invalid OpenCLI specification (missing required fields)
const invalidSpec = `{
"opencliVersion": "1.0.0-alpha.14"
"opencliVersion": "` + spec.SchemaVersion + `"
}`

func TestOcliCheckValidJSON(t *testing.T) {
Expand Down
145 changes: 145 additions & 0 deletions internal/genschema/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Command genschema derives generated constants and synchronized schema copies
// from the repository's canonical spec.schema.json.
package main

import (
"encoding/json"
"flag"
"fmt"
"go/format"
"io/fs"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)

type schema struct {
ID string `json:"$id"`
Properties struct {
OpenCLIVersion struct {
Enum []string `json:"enum"`
} `json:"opencliVersion"`
} `json:"properties"`
}

func main() {
schemaPath := flag.String("schema", "spec.schema.json", "path to the canonical schema")
goOutput := flag.String("out", "schema_version_gen.go", "path to the generated Go file")
updateDocs := flag.Bool("update-documents", false, "update checked-in document and documentation versions")
documentRoot := flag.String("document-root", ".", "root directory to scan when updating documents")
flag.Parse()

version, err := readVersion(*schemaPath)
if err != nil {
fail(err)
}

contents := fmt.Appendf(nil, `// Code generated by genschema; DO NOT EDIT.

package spec

// SchemaVersion is the version declared by the canonical OpenCLI schema.
const SchemaVersion = %s
`, strconv.Quote(version))
contents, err = format.Source(contents)
if err != nil {
fail(fmt.Errorf("format generated Go source: %w", err))
}
if err := os.WriteFile(*goOutput, contents, 0644); err != nil {
fail(fmt.Errorf("write %s: %w", *goOutput, err))
}
if *updateDocs {
if err := updateDocumentVersions(*documentRoot, version); err != nil {
fail(err)
}
}
}

func readVersion(path string) (string, error) {
contents, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read schema: %w", err)
}

var document schema
if err := json.Unmarshal(contents, &document); err != nil {
return "", fmt.Errorf("parse schema: %w", err)
}

versions := document.Properties.OpenCLIVersion.Enum

if len(versions) < 1 || versions[len(versions)-1] == "" {
return "", fmt.Errorf("schema properties.opencliVersion.enum must contain at least one non-empty string")
}
// take most recent version (versions will be in ascending order in the schema)
version := versions[len(versions)-1]

const schemaSuffix = "/spec.schema.json"
if !strings.HasSuffix(document.ID, schemaSuffix) {
return "", fmt.Errorf("schema $id must end with %q", schemaSuffix)
}
idWithoutSuffix := strings.TrimSuffix(document.ID, schemaSuffix)
idVersion := idWithoutSuffix[strings.LastIndex(idWithoutSuffix, "/")+1:]
if idVersion != version {
return "", fmt.Errorf("schema $id version %q does not match opencliVersion enum %q", idVersion, version)
}

return version, nil
}

var (
quotedVersionPattern = regexp.MustCompile(`(opencliVersion["']?\s*:\s*["'])([^"']+)(["'])`)
bareVersionPattern = regexp.MustCompile(`(opencliVersion\s*:\s*)([0-9][^[:space:]#,}]+)`)
)

func updateDocumentVersions(root, version string) error {
return filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
switch entry.Name() {
case ".git", ".next", ".scratch", "build", "dist", "node_modules", "out", "scratch":
return filepath.SkipDir
}
return nil
}
if entry.Name() == "spec.schema.json" {
return nil
}

extension := strings.ToLower(filepath.Ext(path))
switch extension {
case ".json", ".md", ".txt", ".yaml", ".yml":
default:
return nil
}

contents, err := os.ReadFile(path)
if err != nil {
return err
}
updated := quotedVersionPattern.ReplaceAll(contents, []byte(`${1}`+version+`${3}`))
updated = bareVersionPattern.ReplaceAll(updated, []byte(`${1}`+version))
if string(updated) == string(contents) {
return nil
}

info, err := entry.Info()
if err != nil {
return err
}
if err := os.WriteFile(path, updated, info.Mode().Perm()); err != nil {
return fmt.Errorf("update %s: %w", path, err)
}
fmt.Println("updated", path)
return nil
})
}

func fail(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
6 changes: 6 additions & 0 deletions spec/schema_version_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading