Files
commandkit/cli_json.go

77 lines
2.0 KiB
Go
Raw Normal View History

2026-11-07 03:46:25 +00:00
package commandkit
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
)
// ParseCLIJSON parses exactly one JSON value for use in CLI inputs. Integer
// tokens become int64, or uint64 for larger nonnegative integers; decimal and
// exponent tokens become float64. Nested values follow the same rules. Syntax
// errors and numbers outside the selected Go types' ranges are returned.
func ParseCLIJSON(input string) (any, error) {
decoder := json.NewDecoder(strings.NewReader(input))
decoder.UseNumber()
var value any
if err := decoder.Decode(&value); err != nil {
return nil, err
}
var extra any
if err := decoder.Decode(&extra); err != io.EOF {
if err != nil {
return nil, err
}
return nil, fmt.Errorf("unexpected additional JSON value")
}
return convertCLIJSON(value)
}
// CLI inputs use ordinary Go numbers for upstream consumers. Classification
// follows the token's syntax, so 1.0 and 1e0 remain float64. Unlike report
// serialization, this boundary deliberately accepts float64 approximation.
func convertCLIJSON(value any) (any, error) {
switch v := value.(type) {
case json.Number:
token := string(v)
if strings.ContainsAny(token, ".eE") {
result, err := strconv.ParseFloat(token, 64)
if err != nil {
return nil, fmt.Errorf("JSON number %q: %w", token, err)
}
return result, nil
}
result, err := strconv.ParseInt(token, 10, 64)
if err == nil {
return result, nil
}
if !strings.HasPrefix(token, "-") {
unsigned, unsignedErr := strconv.ParseUint(token, 10, 64)
if unsignedErr == nil {
return unsigned, nil
}
return nil, fmt.Errorf("JSON integer %q: %w", token, unsignedErr)
}
return nil, fmt.Errorf("JSON integer %q: %w", token, err)
case []any:
for index, item := range v {
converted, err := convertCLIJSON(item)
if err != nil {
return nil, err
}
v[index] = converted
}
case map[string]any:
for key, item := range v {
converted, err := convertCLIJSON(item)
if err != nil {
return nil, err
}
v[key] = converted
}
}
return value, nil
}