feat(cmd): add version, plain json, struct inference and schema modes
Test / test (push) Successful in 1m35s
Test / test (push) Successful in 1m35s
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -22,9 +22,13 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sourcedock.dev/petrbalvin/interpres/v2"
|
||||
@@ -40,21 +44,54 @@ func main() {
|
||||
func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
fs := flag.NewFlagSet("interpres-decode", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
version := fs.Bool("version", false, "print the version and exit")
|
||||
validate := fs.Bool("validate", false, "validate the documents instead of emitting tagged JSON")
|
||||
encode := fs.Bool("encode", false, "read tagged JSON from stdin and write TOML instead")
|
||||
plainJSON := fs.Bool("json", false, "with the default mode, print plain indented JSON instead of tagged JSON")
|
||||
infer := fs.Bool("struct", false, "infer a Go struct definition from the document on stdin and print it")
|
||||
schemaType := fs.String("schema", "", "write a TOML template for the named struct type; the source file follows as the first argument")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return 0
|
||||
}
|
||||
return 2
|
||||
}
|
||||
if *validate && *encode {
|
||||
fmt.Fprintln(stderr, "interpres-decode: -validate and -encode cannot be combined")
|
||||
if *version {
|
||||
fmt.Fprintf(stdout, "interpres-decode %s\n", versionString())
|
||||
return 0
|
||||
}
|
||||
modes := 0
|
||||
for _, on := range []*bool{validate, encode, infer} {
|
||||
if *on {
|
||||
modes++
|
||||
}
|
||||
}
|
||||
if *schemaType != "" {
|
||||
modes++
|
||||
}
|
||||
if modes > 1 {
|
||||
fmt.Fprintln(stderr, "interpres-decode: -validate, -encode, -struct and -schema cannot be combined")
|
||||
return 2
|
||||
}
|
||||
if *schemaType != "" {
|
||||
rest := fs.Args()
|
||||
if len(rest) != 1 {
|
||||
fmt.Fprintln(stderr, "interpres-decode: -schema needs the type name and exactly one Go source file")
|
||||
return 2
|
||||
}
|
||||
if err := runSchema(*schemaType, rest[0], stdout); err != nil {
|
||||
fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if *validate {
|
||||
return validatePaths(fs.Args(), stdin, stderr)
|
||||
}
|
||||
if *encode && *plainJSON {
|
||||
fmt.Fprintln(stderr, "interpres-decode: -json shapes the decoder output and cannot be combined with -encode")
|
||||
return 2
|
||||
}
|
||||
if fs.NArg() > 0 {
|
||||
fmt.Fprintln(stderr, "interpres-decode: the adapter mode takes no arguments; name files with -validate")
|
||||
return 2
|
||||
@@ -67,11 +104,28 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
fmt.Fprintln(stderr, "read stdin:", err)
|
||||
return 2
|
||||
}
|
||||
if *infer {
|
||||
if err := inferStruct(data, stdout); err != nil {
|
||||
fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
tree, err := interpres.ParseMap(data)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
return 1
|
||||
}
|
||||
if *plainJSON {
|
||||
enc := json.NewEncoder(stdout)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(plainJSONValue(tree)); err != nil {
|
||||
fmt.Fprintln(stderr, "encode:", err)
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
tagged, err := tag(tree)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
@@ -86,15 +140,95 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// versionString names the version the binary was built at: the module
|
||||
// version the toolchain recorded, which is the tag when the release pipeline
|
||||
// builds it, and (devel) for an ordinary build from a working tree.
|
||||
func versionString() string {
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
if v := info.Main.Version; strings.HasPrefix(v, "v") {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "(devel)"
|
||||
}
|
||||
|
||||
// plainJSONValue converts the parsed tree into the values encoding/json
|
||||
// renders: the date-time wrappers print in their TOML form, which is the
|
||||
// same text a reader of the document saw.
|
||||
func plainJSONValue(v any) any {
|
||||
switch x := v.(type) {
|
||||
case map[string]any:
|
||||
for k, val := range x {
|
||||
x[k] = plainJSONValue(val)
|
||||
}
|
||||
return x
|
||||
case []any:
|
||||
for i, val := range x {
|
||||
x[i] = plainJSONValue(val)
|
||||
}
|
||||
return x
|
||||
case []map[string]any:
|
||||
out := make([]any, len(x))
|
||||
for i, val := range x {
|
||||
out[i] = plainJSONValue(val)
|
||||
}
|
||||
return out
|
||||
case time.Time:
|
||||
return x.Format(time.RFC3339Nano)
|
||||
case interpres.OffsetDateTime:
|
||||
return x.String()
|
||||
case interpres.LocalDateTime:
|
||||
return x.String()
|
||||
case interpres.LocalDate:
|
||||
return x.String()
|
||||
case interpres.LocalTime:
|
||||
return x.String()
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// validatePaths parses every named file, or standard input when none are
|
||||
// named, and reports each invalid document on stderr. It returns 0 when all
|
||||
// documents parse, 1 when one does not, and 2 on a usage or read failure.
|
||||
// named, and reports each invalid document on stderr. A named directory is
|
||||
// walked for .toml files. It returns 0 when all documents parse, 1 when one
|
||||
// does not, and 2 on a usage or read failure. A summary names the counts.
|
||||
func validatePaths(paths []string, stdin io.Reader, stderr io.Writer) int {
|
||||
if len(paths) == 0 {
|
||||
paths = []string{"-"}
|
||||
}
|
||||
valid := true
|
||||
var files []string
|
||||
dirs := 0
|
||||
for _, p := range paths {
|
||||
if p == "-" {
|
||||
files = append(files, "-")
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "interpres-decode: %s: %v\n", p, err)
|
||||
return 2
|
||||
}
|
||||
if !info.IsDir() {
|
||||
files = append(files, p)
|
||||
continue
|
||||
}
|
||||
dirs++
|
||||
err = filepath.WalkDir(p, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() && strings.EqualFold(filepath.Ext(path), ".toml") {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "interpres-decode: walk %s: %v\n", p, err)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
valid := true
|
||||
checked := 0
|
||||
for _, p := range files {
|
||||
name := p
|
||||
var data []byte
|
||||
var err error
|
||||
@@ -108,11 +242,18 @@ func validatePaths(paths []string, stdin io.Reader, stderr io.Writer) int {
|
||||
fmt.Fprintf(stderr, "interpres-decode: %s: %v\n", name, err)
|
||||
return 2
|
||||
}
|
||||
checked++
|
||||
if _, err := interpres.ParseMap(data); err != nil {
|
||||
fmt.Fprintf(stderr, "%s: %v\n", name, err)
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
// The single-document run stays quiet on success, the contract the
|
||||
// compliance tooling relies on; a directory walk closes with the
|
||||
// summary that makes the sweep readable.
|
||||
if dirs > 0 {
|
||||
fmt.Fprintf(stderr, "checked %d documents, %d invalid\n", checked, map[bool]int{true: 0, false: 1}[valid])
|
||||
}
|
||||
if !valid {
|
||||
return 1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user