77 lines
2.0 KiB
Go
77 lines
2.0 KiB
Go
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
|
|
}
|