args,meta: harmonize supported types, with fast paths

This commit is contained in:
Michael Muré
2024-11-12 13:05:48 +01:00
parent 522181b16a
commit c4a53f42b6
6 changed files with 112 additions and 92 deletions

View File

@@ -15,12 +15,13 @@ import (
"github.com/ucan-wg/go-ucan/pkg/policy/literal" "github.com/ucan-wg/go-ucan/pkg/policy/literal"
) )
// Args are the Command's arguments when an invocation Token is processed // Args are the Command's arguments when an invocation Token is processed by the executor.
// 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.
// This type must be compatible with the IPLD type represented by the IPLD
// schema { String : Any }.
type Args struct { type Args struct {
// This type must be compatible with the IPLD type represented by the IPLD
// schema { String : Any }.
Keys []string Keys []string
Values map[string]ipld.Node Values map[string]ipld.Node
} }
@@ -34,9 +35,7 @@ func New() *Args {
// Add inserts a key/value pair in the Args set. // Add inserts a key/value pair in the Args set.
// //
// Accepted types for val are: bool, string, int, int8, int16, // Accepted types for val are any CBOR compatible type, or directly IPLD values.
// int32, int64, uint, uint8, uint16, uint32, float32, float64, []byte,
// []any, map[string]any, ipld.Node and nil.
func (a *Args) Add(key string, val any) error { func (a *Args) Add(key string, val any) error {
if _, ok := a.Values[key]; ok { if _, ok := a.Values[key]; ok {
return fmt.Errorf("duplicate key %q", key) return fmt.Errorf("duplicate key %q", key)

View File

@@ -2,23 +2,23 @@ package meta
import ( import (
"fmt" "fmt"
"reflect"
"strings" "strings"
"github.com/ipld/go-ipld-prime" "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" "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") var ErrNotFound = fmt.Errorf("key-value not found in meta")
// Meta is a container for meta key-value pairs in a UCAN token. // 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, // This also serves as a way to construct the underlying IPLD data with minimum allocations
// while hiding the IPLD complexity from the caller. // and transformations, while hiding the IPLD complexity from the caller.
type Meta struct { type Meta struct {
// This type must be compatible with the IPLD type represented by the IPLD
// schema { String : Any }.
Keys []string Keys []string
Values map[string]ipld.Node 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. // Add adds a key/value pair in the meta set.
// Accepted types for the value are: bool, string, int, int32, int64, []byte, // Accepted types for val are any CBOR compatible type, or directly IPLD values.
// and ipld.Node.
func (m *Meta) Add(key string, val any) error { func (m *Meta) Add(key string, val any) error {
if _, ok := m.Values[key]; ok { if _, ok := m.Values[key]; ok {
return fmt.Errorf("duplicate key %q", key) return fmt.Errorf("duplicate key %q", key)
} }
switch val := val.(type) {
case bool: node, err := literal.Any(val)
m.Values[key] = basicnode.NewBool(val) if err != nil {
case string: return err
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))
} }
m.Keys = append(m.Keys, key) m.Keys = append(m.Keys, key)
m.Values[key] = node
return nil return nil
} }
@@ -166,15 +151,3 @@ func (m *Meta) String() string {
func (m *Meta) ReadOnly() ReadOnly { func (m *Meta) ReadOnly() ReadOnly {
return ReadOnly{m: m} 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()
}

View File

@@ -4,7 +4,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
"github.com/ucan-wg/go-ucan/pkg/meta" "github.com/ucan-wg/go-ucan/pkg/meta"
) )
@@ -18,7 +17,6 @@ func TestMeta_Add(t *testing.T) {
t.Parallel() t.Parallel()
err := (&meta.Meta{}).Add("invalid", &Unsupported{}) err := (&meta.Meta{}).Add("invalid", &Unsupported{})
require.ErrorIs(t, err, meta.ErrUnsupported) require.Error(t, err)
assert.ErrorContains(t, err, "*github.com/ucan-wg/go-ucan/pkg/meta_test.Unsupported")
}) })
} }

View File

@@ -58,6 +58,47 @@ func List[T any](l []T) (ipld.Node, error) {
// Any creates an IPLD node from any value // Any creates an IPLD node from any value
// If possible, use another dedicated function for your type for performance. // If possible, use another dedicated function for your type for performance.
func Any(v any) (res ipld.Node, err error) { 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() builder := basicnode.Prototype__Any{}.NewBuilder()
defer func() { defer func() {

View File

@@ -218,6 +218,42 @@ func TestAny(t *testing.T) {
require.Error(t, err) 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 { func must[T any](t T, err error) T {
if err != nil { if err != nil {
panic(err) panic(err)

View File

@@ -11,8 +11,6 @@ import (
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor" "github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/dagjson" "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/ipld/go-ipld-prime/node/basicnode"
"github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/crypto"
@@ -146,7 +144,7 @@ func prettyDAGJSON(data []byte) (string, error) {
return out.String(), nil 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 var err error
privKey, iss, err = did.GenerateEd25519() 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)) errs = errors.Join(errs, fmt.Errorf("failed to parse command: %w", err))
} }
headers, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) { headers := map[string]string{
qp.MapEntry(ma, "Content-Type", qp.String("application/json")) "Content-Type": "application/json",
})
if err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to build headers: %w", err))
} }
// ***** WARNING - do not change the order of these elements. DAG-CBOR payload := map[string]any{
// will order them alphabetically and the unsealed "body": "UCAN is great",
// result won't match if the input isn't also created in "draft": true,
// alphabetical order. "title": "UCAN for Fun and Profit",
payload, err := qp.BuildMap(basicnode.Prototype.Any, 4, func(ma datamodel.MapAssembler) { "topics": []string{"authz", "journal"},
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))
} }
args = map[string]datamodel.Node{ args = map[string]any{
// you can also directly pass IPLD values
"uri": basicnode.NewString("https://example.com/blog/posts"), "uri": basicnode.NewString("https://example.com/blog/posts"),
"headers": headers, "headers": headers,
"payload": payload, "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 meta = map[string]any{
// 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{
"env": basicnode.NewString("development"), "env": basicnode.NewString("development"),
"tags": tags, "tags": []string{"blog", "post", "pr#123"},
} }
return // WARNING: named return values return // WARNING: named return values