feat: add module and decimal wire helpers

This commit is contained in:
2026-09-01 11:12:10 -05:00
parent 7e391c5e5e
commit 7d549fda58
5 changed files with 191 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
package wire
import (
"encoding/json"
"fmt"
decimal "github.com/alpacahq/alpacadecimal"
)
// Unwrap returns the inner JSON of an optional {"data": ...} envelope.
func Unwrap(raw json.RawMessage) json.RawMessage {
if len(raw) == 0 {
return raw
}
var wrap struct {
Data json.RawMessage `json:"data"`
}
if json.Unmarshal(raw, &wrap) == nil && len(wrap.Data) > 0 {
return wrap.Data
}
return raw
}
// Dec parses a required money/size/price value.
func Dec(v any) (decimal.Decimal, error) {
switch x := v.(type) {
case nil:
return decimal.Zero, nil
case float64:
return decimal.NewFromFloat(x), nil
case json.Number:
d, err := decimal.NewFromString(string(x))
if err != nil {
return decimal.Zero, fmt.Errorf("parse decimal: %v", v)
}
return d, nil
case string:
if x == "" {
return decimal.Zero, nil
}
d, err := decimal.NewFromString(x)
if err != nil {
return decimal.Zero, fmt.Errorf("parse decimal: %v", v)
}
return d, nil
default:
return decimal.Zero, fmt.Errorf("parse decimal: %v", v)
}
}
// DecOpt parses an optional money/size/price value.
func DecOpt(v any) (*decimal.Decimal, error) {
if v == nil {
return nil, nil
}
if s, ok := v.(string); ok && s == "" {
return nil, nil
}
d, err := Dec(v)
if err != nil {
return nil, err
}
return &d, nil
}
// Encode is Decimal.String for Robinhood string wire fields.
func Encode(d decimal.Decimal) string {
return d.String()
}
+85
View File
@@ -0,0 +1,85 @@
package wire_test
import (
"encoding/json"
"testing"
decimal "github.com/alpacahq/alpacadecimal"
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
)
func TestUnwrap_dataEnvelope(t *testing.T) {
t.Parallel()
in := json.RawMessage(`{"data":{"cash":"1000"}}`)
got := wire.Unwrap(in)
if string(got) != `{"cash":"1000"}` {
t.Fatalf("got %s", got)
}
}
func TestDec_table(t *testing.T) {
t.Parallel()
zero := decimal.Zero
tests := []struct {
name string
in any
want decimal.Decimal
wantErr bool
}{
{"number", float64(99.6), decimal.RequireFromString("99.6"), false},
{"string", "99.60", decimal.RequireFromString("99.60"), false},
{"zeroNum", float64(0), zero, false},
{"zeroStr", "0", zero, false},
{"emptyStr", "", zero, false},
{"nil", nil, zero, false},
{"bad", "n/a", zero, true},
{"obj", map[string]any{"x": 1}, zero, true},
{"bool", true, zero, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := wire.Dec(tc.in)
if tc.wantErr {
if err == nil {
t.Fatalf("want error")
}
return
}
if err != nil {
t.Fatal(err)
}
if !got.Equal(tc.want) {
t.Fatalf("got %s want %s", got, tc.want)
}
})
}
}
func TestDecOpt_nullIsNil(t *testing.T) {
t.Parallel()
got, err := wire.DecOpt(nil)
if err != nil || got != nil {
t.Fatalf("got %v err %v", got, err)
}
got, err = wire.DecOpt("")
if err != nil || got != nil {
t.Fatalf("empty string: %v %v", got, err)
}
got, err = wire.DecOpt("0")
if err != nil || got == nil || !got.IsZero() {
t.Fatalf("zero: %v %v", got, err)
}
_, err = wire.DecOpt("n/a")
if err == nil {
t.Fatal("unparseable must error")
}
}
func TestEncode(t *testing.T) {
t.Parallel()
d := decimal.RequireFromString("99.6")
if wire.Encode(d) != d.String() {
t.Fatalf("%q", wire.Encode(d))
}
}