args,meta: harmonize supported types, with fast paths
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user