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 }