diff --git a/pkg/args/args.go b/pkg/args/args.go index d20616f..4ac9d94 100644 --- a/pkg/args/args.go +++ b/pkg/args/args.go @@ -15,12 +15,13 @@ import ( "github.com/ucan-wg/go-ucan/pkg/policy/literal" ) -// Args are the Command's arguments when an invocation Token is processed -// by the executor. -// -// This type must be compatible with the IPLD type represented by the IPLD -// schema { String : Any }. +// Args are the Command's arguments when an invocation Token is processed by the executor. +// This also serves as a way to construct the underlying IPLD data with minimum allocations +// and transformations, while hiding the IPLD complexity from the caller. type Args struct { + // This type must be compatible with the IPLD type represented by the IPLD + // schema { String : Any }. + Keys []string Values map[string]ipld.Node } @@ -34,9 +35,7 @@ func New() *Args { // Add inserts a key/value pair in the Args set. // -// Accepted types for val are: bool, string, int, int8, int16, -// int32, int64, uint, uint8, uint16, uint32, float32, float64, []byte, -// []any, map[string]any, ipld.Node and nil. +// Accepted types for val are any CBOR compatible type, or directly IPLD values. func (a *Args) Add(key string, val any) error { if _, ok := a.Values[key]; ok { return fmt.Errorf("duplicate key %q", key) diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go index ffc21f7..093ed4a 100644 --- a/pkg/meta/meta.go +++ b/pkg/meta/meta.go @@ -2,23 +2,23 @@ package meta import ( "fmt" - "reflect" "strings" "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/printer" -) -var ErrUnsupported = fmt.Errorf("failure adding unsupported type to meta") + "github.com/ucan-wg/go-ucan/pkg/policy/literal" +) var ErrNotFound = fmt.Errorf("key-value not found in meta") // Meta is a container for meta key-value pairs in a UCAN token. -// This also serves as a way to construct the underlying IPLD data with minimum allocations and transformations, -// while hiding the IPLD complexity from the caller. +// This also serves as a way to construct the underlying IPLD data with minimum allocations +// and transformations, while hiding the IPLD complexity from the caller. type Meta struct { + // This type must be compatible with the IPLD type represented by the IPLD + // schema { String : Any }. + Keys []string Values map[string]ipld.Node } @@ -95,35 +95,20 @@ func (m *Meta) GetNode(key string) (ipld.Node, error) { } // Add adds a key/value pair in the meta set. -// Accepted types for the value are: bool, string, int, int32, int64, []byte, -// and ipld.Node. +// Accepted types for val are any CBOR compatible type, or directly IPLD values. func (m *Meta) Add(key string, val any) error { if _, ok := m.Values[key]; ok { return fmt.Errorf("duplicate key %q", key) } - switch val := val.(type) { - case bool: - m.Values[key] = basicnode.NewBool(val) - case string: - m.Values[key] = basicnode.NewString(val) - case int: - m.Values[key] = basicnode.NewInt(int64(val)) - case int32: - m.Values[key] = basicnode.NewInt(int64(val)) - case int64: - m.Values[key] = basicnode.NewInt(val) - case float32: - m.Values[key] = basicnode.NewFloat(float64(val)) - case float64: - m.Values[key] = basicnode.NewFloat(val) - case []byte: - m.Values[key] = basicnode.NewBytes(val) - case datamodel.Node: - m.Values[key] = val - default: - return fmt.Errorf("%w: %s", ErrUnsupported, fqtn(val)) + + node, err := literal.Any(val) + if err != nil { + return err } + m.Keys = append(m.Keys, key) + m.Values[key] = node + return nil } @@ -166,15 +151,3 @@ func (m *Meta) String() string { func (m *Meta) ReadOnly() ReadOnly { return ReadOnly{m: m} } - -func fqtn(val any) string { - var name string - - t := reflect.TypeOf(val) - for t.Kind() == reflect.Pointer { - name += "*" - t = t.Elem() - } - - return name + t.PkgPath() + "." + t.Name() -} diff --git a/pkg/meta/meta_test.go b/pkg/meta/meta_test.go index 9ad3e2c..ca00be0 100644 --- a/pkg/meta/meta_test.go +++ b/pkg/meta/meta_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/stretchr/testify/require" - "gotest.tools/v3/assert" "github.com/ucan-wg/go-ucan/pkg/meta" ) @@ -18,7 +17,6 @@ func TestMeta_Add(t *testing.T) { t.Parallel() err := (&meta.Meta{}).Add("invalid", &Unsupported{}) - require.ErrorIs(t, err, meta.ErrUnsupported) - assert.ErrorContains(t, err, "*github.com/ucan-wg/go-ucan/pkg/meta_test.Unsupported") + require.Error(t, err) }) } diff --git a/pkg/policy/literal/literal.go b/pkg/policy/literal/literal.go index 5e5df8a..33b0904 100644 --- a/pkg/policy/literal/literal.go +++ b/pkg/policy/literal/literal.go @@ -58,6 +58,47 @@ func List[T any](l []T) (ipld.Node, error) { // Any creates an IPLD node from any value // If possible, use another dedicated function for your type for performance. func Any(v any) (res ipld.Node, err error) { + // TODO: handle uint overflow below + + // some fast path + switch val := v.(type) { + case bool: + return basicnode.NewBool(val), nil + case string: + return basicnode.NewString(val), nil + case int: + return basicnode.NewInt(int64(val)), nil + case int8: + return basicnode.NewInt(int64(val)), nil + case int16: + return basicnode.NewInt(int64(val)), nil + case int32: + return basicnode.NewInt(int64(val)), nil + case int64: + return basicnode.NewInt(val), nil + case uint: + return basicnode.NewInt(int64(val)), nil + case uint8: + return basicnode.NewInt(int64(val)), nil + case uint16: + return basicnode.NewInt(int64(val)), nil + case uint32: + return basicnode.NewInt(int64(val)), nil + case uint64: + return basicnode.NewInt(int64(val)), nil + case float32: + return basicnode.NewFloat(float64(val)), nil + case float64: + return basicnode.NewFloat(val), nil + case []byte: + return basicnode.NewBytes(val), nil + case datamodel.Node: + return val, nil + case cid.Cid: + return LinkCid(val), nil + default: + } + builder := basicnode.Prototype__Any{}.NewBuilder() defer func() { diff --git a/pkg/policy/literal/literal_test.go b/pkg/policy/literal/literal_test.go index 656b82b..45d7c6c 100644 --- a/pkg/policy/literal/literal_test.go +++ b/pkg/policy/literal/literal_test.go @@ -218,6 +218,42 @@ func TestAny(t *testing.T) { require.Error(t, err) } +func BenchmarkAny(b *testing.B) { + b.Run("bool", func(b *testing.B) { + b.ReportAllocs() + + for n := 0; n < b.N; n++ { + _, _ = Any(true) + } + }) + + b.Run("string", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, _ = Any("foobar") + } + }) + + b.Run("bytes", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, _ = Any([]byte{1, 2, 3, 4}) + } + }) + + b.Run("map", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _, _ = Any(map[string]any{ + "foo": "bar", + "foofoo": map[string]string{ + "barbar": "foo", + }, + }) + } + }) +} + func must[T any](t T, err error) T { if err != nil { panic(err) diff --git a/token/invocation/examples_test.go b/token/invocation/examples_test.go index 7ecf349..d3255d0 100644 --- a/token/invocation/examples_test.go +++ b/token/invocation/examples_test.go @@ -11,8 +11,6 @@ import ( "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec/dagcbor" "github.com/ipld/go-ipld-prime/codec/dagjson" - "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/fluent/qp" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/libp2p/go-libp2p/core/crypto" @@ -146,7 +144,7 @@ func prettyDAGJSON(data []byte) (string, error) { return out.String(), nil } -func setupExampleNew() (privKey crypto.PrivKey, iss, sub did.DID, cmd command.Command, args map[string]datamodel.Node, prf []cid.Cid, meta map[string]datamodel.Node, errs error) { +func setupExampleNew() (privKey crypto.PrivKey, iss, sub did.DID, cmd command.Command, args map[string]any, prf []cid.Cid, meta map[string]any, errs error) { var err error privKey, iss, err = did.GenerateEd25519() @@ -164,31 +162,19 @@ func setupExampleNew() (privKey crypto.PrivKey, iss, sub did.DID, cmd command.Co errs = errors.Join(errs, fmt.Errorf("failed to parse command: %w", err)) } - headers, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) { - qp.MapEntry(ma, "Content-Type", qp.String("application/json")) - }) - if err != nil { - errs = errors.Join(errs, fmt.Errorf("failed to build headers: %w", err)) + headers := map[string]string{ + "Content-Type": "application/json", } - // ***** WARNING - do not change the order of these elements. DAG-CBOR - // will order them alphabetically and the unsealed - // result won't match if the input isn't also created in - // alphabetical order. - payload, err := qp.BuildMap(basicnode.Prototype.Any, 4, func(ma datamodel.MapAssembler) { - qp.MapEntry(ma, "body", qp.String("UCAN is great")) - qp.MapEntry(ma, "draft", qp.Bool(true)) - qp.MapEntry(ma, "title", qp.String("UCAN for Fun and Profit")) - qp.MapEntry(ma, "topics", qp.List(2, func(la datamodel.ListAssembler) { - qp.ListEntry(la, qp.String("authz")) - qp.ListEntry(la, qp.String("journal")) - })) - }) - if err != nil { - errs = errors.Join(errs, fmt.Errorf("failed to build payload: %w", err)) + payload := map[string]any{ + "body": "UCAN is great", + "draft": true, + "title": "UCAN for Fun and Profit", + "topics": []string{"authz", "journal"}, } - args = map[string]datamodel.Node{ + args = map[string]any{ + // you can also directly pass IPLD values "uri": basicnode.NewString("https://example.com/blog/posts"), "headers": headers, "payload": payload, @@ -206,22 +192,9 @@ func setupExampleNew() (privKey crypto.PrivKey, iss, sub did.DID, cmd command.Co } } - // ***** WARNING - do not change the order of these elements. DAG-CBOR - // will order them alphabetically and the unsealed - // result won't match if the input isn't also created in - // alphabetical order. - tags, err := qp.BuildList(basicnode.Prototype.Any, 3, func(la datamodel.ListAssembler) { - qp.ListEntry(la, qp.String("blog")) - qp.ListEntry(la, qp.String("post")) - qp.ListEntry(la, qp.String("pr#123")) - }) - if err != nil { - errs = errors.Join(errs, fmt.Errorf("failed to build tags: %w", err)) - } - - meta = map[string]datamodel.Node{ + meta = map[string]any{ "env": basicnode.NewString("development"), - "tags": tags, + "tags": []string{"blog", "post", "pr#123"}, } return // WARNING: named return values