package cli import ( "bytes" "errors" "fmt" "go/token" "os" "path/filepath" "strings" "unicode" "github.com/spf13/cobra" ) func newGenerateCmd(opts *options) *cobra.Command { var ( out string pkg string check bool excludeFieldDoc bool ) cmd := &cobra.Command{ Use: "generate", Short: "Generate Go types from the config", Long: `Generate fetches the schemas and extensions named in the config and writes the Go types to --out. Use --out - to write to stdout. With --check, nothing is written; the command fails if the output file is missing or differs from what would be generated. Use this in CI to catch generated code that is stale or was edited by hand.`, Example: ` ocdsgen generate -c extensions.json -o ocds_gen.go -p tenders ocdsgen generate --check --strict //go:generate go tool ocdsgen generate -c extensions.json -o ocds_gen.go -p tenders`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { if pkg == "" { var err error if pkg, err = packageFromPath(out); err != nil { return err } } cfg, g, err := opts.setup(cmd) if err != nil { return err } src, err := g.Run(cfg, pkg, filepath.Base(opts.config), excludeFieldDoc) if err != nil { return err } switch { case out == "-": if _, err := cmd.OutOrStdout().Write(src); err != nil { return err } case check: existing, err := os.ReadFile(out) if errors.Is(err, os.ErrNotExist) { return fmt.Errorf("%s does not exist; run ocdsgen generate", out) } else if err != nil { return err } if !bytes.Equal(existing, src) { return fmt.Errorf("%s is out of date; run ocdsgen generate", out) } fmt.Fprintf(cmd.ErrOrStderr(), "%s is up to date\n", out) default: if err := os.WriteFile(out, src, 0o644); err != nil { return err } } return opts.finish(g) }, } f := cmd.Flags() f.StringVarP(&out, "out", "o", "ocds_gen.go", `output Go file, or "-" for stdout`) f.StringVarP(&pkg, "package", "p", "", "Go package name (default: name of the output file's directory)") f.BoolVar(&check, "check", false, "verify the output file is up to date instead of writing it") f.BoolVar(&excludeFieldDoc, "exclude-field-doc", false, `exclude documentation for definition fields`) return cmd } // packageFromPath derives a package name from the directory the output file // will be written to, e.g. internal/tenders/ocds_gen.go -> tenders. func packageFromPath(out string) (string, error) { if out == "-" { return "", errors.New("--package is required when writing to stdout") } abs, err := filepath.Abs(out) if err != nil { return "", err } dir := filepath.Base(filepath.Dir(abs)) name := strings.Map(func(r rune) rune { if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' { return unicode.ToLower(r) } return -1 }, dir) if !token.IsIdentifier(name) || token.IsKeyword(name) { return "", fmt.Errorf("can't derive a package name from directory %q; pass --package", dir) } return name, nil }