feat: add module and decimal wire helpers
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
module s1d3sw1ped/robinhood-agentic-mcp
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require github.com/alpacahq/alpacadecimal v0.0.9
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/modelcontextprotocol/go-sdk v1.7.0 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.35.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
github.com/alpacahq/alpacadecimal v0.0.9 h1:geeT3ZMyfgBV1mrqRDJWQ/4u+FaHep9MmQNFoHKFDNs=
|
||||||
|
github.com/alpacahq/alpacadecimal v0.0.9/go.mod h1:DmR0Qs+sFJ7nyhfYD0/UUzE+9tCbEF1Wa4iz1lZnlwg=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44=
|
||||||
|
github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||||
|
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user