Add CLI JSON value parsing
This commit is contained in:
76
cli_json.go
Normal file
76
cli_json.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
44
cli_json_test.go
Normal file
44
cli_json_test.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package commandkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseCLIJSONValues(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
input string
|
||||||
|
want any
|
||||||
|
}{
|
||||||
|
{`{"a":[null,true,"x",-9223372036854775808,9223372036854775807,9223372036854775808,18446744073709551615],"b":{"n":1.0,"exp":1e3}}`, map[string]any{
|
||||||
|
"a": []any{nil, true, "x", int64(math.MinInt64), int64(math.MaxInt64), uint64(1 << 63), uint64(math.MaxUint64)},
|
||||||
|
"b": map[string]any{"n": float64(1), "exp": float64(1000)},
|
||||||
|
}},
|
||||||
|
{`0`, int64(0)}, {`-0`, int64(0)}, {`42.0`, float64(42)}, {`1e0`, float64(1)},
|
||||||
|
{`0.1`, float64(0.1)}, {`"1"`, "1"}, {`false`, false}, {`null`, nil},
|
||||||
|
} {
|
||||||
|
t.Run(tc.input, func(t *testing.T) {
|
||||||
|
got, err := ParseCLIJSON(tc.input)
|
||||||
|
if err != nil || !reflect.DeepEqual(got, tc.want) {
|
||||||
|
t.Fatalf("got %#v, %v; want %#v", got, err, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCLIJSONRejectsInvalidInput(t *testing.T) {
|
||||||
|
for _, input := range []string{"", "[", "01", `1 2`, `true false`, `{} garbage`, `-9223372036854775809`, `18446744073709551616`, `1e400`, `[-1e400]`} {
|
||||||
|
t.Run(input, func(t *testing.T) {
|
||||||
|
if _, err := ParseCLIJSON(input); err == nil {
|
||||||
|
t.Fatalf("accepted %q", input)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_, err := ParseCLIJSON(`18446744073709551616`)
|
||||||
|
if !errors.Is(err, strconv.ErrRange) {
|
||||||
|
t.Fatalf("range error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Shared reporting and PromptKit v0.20.0 implementation plan
|
# Shared reporting and PromptKit v0.20.0 implementation plan
|
||||||
|
|
||||||
Status: implementation in progress; Stages 1–4 complete.
|
Status: implementation in progress; Stages 1–5 complete.
|
||||||
|
|
||||||
## Scope, authority, and execution
|
## Scope, authority, and execution
|
||||||
|
|
||||||
@@ -193,6 +193,10 @@ boundary; later documentation stages complete the integration inventory.
|
|||||||
|
|
||||||
## Stage 5: Public CLI JSON parsing
|
## Stage 5: Public CLI JSON parsing
|
||||||
|
|
||||||
|
**Complete.** CLI JSON parsing and numeric conversion are implemented. Focused
|
||||||
|
boundary tests and the root workspace suite (`GOTOOLCHAIN=local go test ./...`
|
||||||
|
with Go 1.27.0) passed.
|
||||||
|
|
||||||
Add `ParseCLIJSON(string) (any, error)` in core. Decode exactly one JSON value
|
Add `ParseCLIJSON(string) (any, error)` in core. Decode exactly one JSON value
|
||||||
with `UseNumber`, require completion of that value, and recursively convert
|
with `UseNumber`, require completion of that value, and recursively convert
|
||||||
numbers under [OD-2](promptkit-v0.20.0.md#od-2-request-controls-and-provider-parameter-grammar).
|
numbers under [OD-2](promptkit-v0.20.0.md#od-2-request-controls-and-provider-parameter-grammar).
|
||||||
|
|||||||
Reference in New Issue
Block a user