diff --git a/pkg/args/args.go b/pkg/args/args.go index d046d93..eddb9cc 100644 --- a/pkg/args/args.go +++ b/pkg/args/args.go @@ -5,6 +5,7 @@ package args import ( "fmt" + "iter" "sort" "strings" @@ -54,21 +55,36 @@ func (a *Args) Add(key string, val any) error { return nil } +type Iterator interface { + Iter() iter.Seq2[string, ipld.Node] +} + // Include merges the provided arguments into the existing arguments. // // If duplicate keys are encountered, the new value is silently dropped // without causing an error. -func (a *Args) Include(other *Args) { - for _, key := range other.Keys { +func (a *Args) Include(other Iterator) { + for key, value := range other.Iter() { if _, ok := a.Values[key]; ok { // don't overwrite continue } - a.Values[key] = other.Values[key] + a.Values[key] = value a.Keys = append(a.Keys, key) } } +// Iter iterates over the args key/values +func (a *Args) Iter() iter.Seq2[string, ipld.Node] { + return func(yield func(string, ipld.Node) bool) { + for _, key := range a.Keys { + if !yield(key, a.Values[key]) { + return + } + } + } +} + // ToIPLD wraps an instance of an Args with an ipld.Node. func (a *Args) ToIPLD() (ipld.Node, error) { sort.Strings(a.Keys) diff --git a/pkg/args/args_test.go b/pkg/args/args_test.go index 8a1fda2..2a44d0f 100644 --- a/pkg/args/args_test.go +++ b/pkg/args/args_test.go @@ -1,6 +1,7 @@ package args_test import ( + "maps" "sync" "testing" @@ -8,6 +9,7 @@ import ( "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec/dagcbor" "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -133,6 +135,56 @@ func TestArgs_Include(t *testing.T) { assert.Equal(t, "val4", must(argsIn.Values["key4"].AsString())) } +func TestIterCloneEquals(t *testing.T) { + a := args.New() + + require.NoError(t, a.Add("foo", "bar")) + require.NoError(t, a.Add("baz", 1234)) + + expected := map[string]ipld.Node{ + "foo": basicnode.NewString("bar"), + "baz": basicnode.NewInt(1234), + } + + // args -> iter + require.Equal(t, expected, maps.Collect(a.Iter())) + + // readonly -> iter + ro := a.ReadOnly() + require.Equal(t, expected, maps.Collect(ro.Iter())) + + // args -> clone -> iter + clone := a.Clone() + require.Equal(t, expected, maps.Collect(clone.Iter())) + + // readonly -> WriteableClone -> iter + wclone := ro.WriteableClone() + require.Equal(t, expected, maps.Collect(wclone.Iter())) + + require.True(t, a.Equals(wclone)) + require.True(t, ro.Equals(wclone.ReadOnly())) +} + +func TestInclude(t *testing.T) { + a1 := args.New() + + require.NoError(t, a1.Add("samekey", "bar")) + require.NoError(t, a1.Add("baz", 1234)) + + a2 := args.New() + + require.NoError(t, a2.Add("samekey", "othervalue")) // check no overwrite + require.NoError(t, a2.Add("otherkey", 1234)) + + a1.Include(a2) + + require.Equal(t, map[string]ipld.Node{ + "samekey": basicnode.NewString("bar"), + "baz": basicnode.NewInt(1234), + "otherkey": basicnode.NewInt(1234), + }, maps.Collect(a1.Iter())) +} + const ( argsSchema = "type Args { String : Any }" argsName = "Args" diff --git a/pkg/args/readonly.go b/pkg/args/readonly.go index a708807..3cb8428 100644 --- a/pkg/args/readonly.go +++ b/pkg/args/readonly.go @@ -1,17 +1,25 @@ package args -import "github.com/ipld/go-ipld-prime" +import ( + "iter" + + "github.com/ipld/go-ipld-prime" +) type ReadOnly struct { args *Args } +func (r ReadOnly) Iter() iter.Seq2[string, ipld.Node] { + return r.args.Iter() +} + func (r ReadOnly) ToIPLD() (ipld.Node, error) { return r.args.ToIPLD() } -func (r ReadOnly) Equals(other *Args) bool { - return r.args.Equals(other) +func (r ReadOnly) Equals(other ReadOnly) bool { + return r.args.Equals(other.args) } func (r ReadOnly) String() string { diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go index 913530f..4ce89a9 100644 --- a/pkg/meta/meta.go +++ b/pkg/meta/meta.go @@ -3,6 +3,8 @@ package meta import ( "errors" "fmt" + "iter" + "sort" "strings" "github.com/ipld/go-ipld-prime" @@ -171,6 +173,36 @@ func (m *Meta) AddEncrypted(key string, val any, encryptionKey []byte) error { return m.Add(key, encrypted) } +type Iterator interface { + Iter() iter.Seq2[string, ipld.Node] +} + +// Include merges the provided meta into the existing one. +// +// If duplicate keys are encountered, the new value is silently dropped +// without causing an error. +func (m *Meta) Include(other Iterator) { + for key, value := range other.Iter() { + if _, ok := m.Values[key]; ok { + // don't overwrite + continue + } + m.Values[key] = value + m.Keys = append(m.Keys, key) + } +} + +// Iter iterates over the meta key/values +func (m *Meta) Iter() iter.Seq2[string, ipld.Node] { + return func(yield func(string, ipld.Node) bool) { + for _, key := range m.Keys { + if !yield(key, m.Values[key]) { + return + } + } + } +} + // Equals tells if two Meta hold the same key/values. func (m *Meta) Equals(other *Meta) bool { if len(m.Keys) != len(other.Keys) { @@ -188,6 +220,8 @@ func (m *Meta) Equals(other *Meta) bool { } func (m *Meta) String() string { + sort.Strings(m.Keys) + buf := strings.Builder{} buf.WriteString("{") @@ -209,5 +243,18 @@ func (m *Meta) String() string { // ReadOnly returns a read-only version of Meta. func (m *Meta) ReadOnly() ReadOnly { - return ReadOnly{m: m} + return ReadOnly{meta: m} +} + +// Clone makes a deep copy. +func (m *Meta) Clone() *Meta { + res := &Meta{ + Keys: make([]string, len(m.Keys)), + Values: make(map[string]ipld.Node, len(m.Values)), + } + copy(res.Keys, m.Keys) + for k, v := range m.Values { + res.Values[k] = v + } + return res } diff --git a/pkg/meta/meta_test.go b/pkg/meta/meta_test.go index 2fbb176..7fd9eb9 100644 --- a/pkg/meta/meta_test.go +++ b/pkg/meta/meta_test.go @@ -2,8 +2,11 @@ package meta_test import ( "crypto/rand" + "maps" "testing" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/stretchr/testify/require" "github.com/ucan-wg/go-ucan/pkg/meta" @@ -75,3 +78,53 @@ func TestMeta_Add(t *testing.T) { }) }) } + +func TestIterCloneEquals(t *testing.T) { + m := meta.NewMeta() + + require.NoError(t, m.Add("foo", "bar")) + require.NoError(t, m.Add("baz", 1234)) + + expected := map[string]ipld.Node{ + "foo": basicnode.NewString("bar"), + "baz": basicnode.NewInt(1234), + } + + // meta -> iter + require.Equal(t, expected, maps.Collect(m.Iter())) + + // readonly -> iter + ro := m.ReadOnly() + require.Equal(t, expected, maps.Collect(ro.Iter())) + + // meta -> clone -> iter + clone := m.Clone() + require.Equal(t, expected, maps.Collect(clone.Iter())) + + // readonly -> WriteableClone -> iter + wclone := ro.WriteableClone() + require.Equal(t, expected, maps.Collect(wclone.Iter())) + + require.True(t, m.Equals(wclone)) + require.True(t, ro.Equals(wclone.ReadOnly())) +} + +func TestInclude(t *testing.T) { + m1 := meta.NewMeta() + + require.NoError(t, m1.Add("samekey", "bar")) + require.NoError(t, m1.Add("baz", 1234)) + + m2 := meta.NewMeta() + + require.NoError(t, m2.Add("samekey", "othervalue")) // check no overwrite + require.NoError(t, m2.Add("otherkey", 1234)) + + m1.Include(m2) + + require.Equal(t, map[string]ipld.Node{ + "samekey": basicnode.NewString("bar"), + "baz": basicnode.NewInt(1234), + "otherkey": basicnode.NewInt(1234), + }, maps.Collect(m1.Iter())) +} diff --git a/pkg/meta/readonly.go b/pkg/meta/readonly.go index ce3674e..26d667d 100644 --- a/pkg/meta/readonly.go +++ b/pkg/meta/readonly.go @@ -1,50 +1,60 @@ package meta import ( + "iter" + "github.com/ipld/go-ipld-prime" ) // ReadOnly wraps a Meta into a read-only facade. type ReadOnly struct { - m *Meta + meta *Meta } func (r ReadOnly) GetBool(key string) (bool, error) { - return r.m.GetBool(key) + return r.meta.GetBool(key) } func (r ReadOnly) GetString(key string) (string, error) { - return r.m.GetString(key) + return r.meta.GetString(key) } func (r ReadOnly) GetEncryptedString(key string, encryptionKey []byte) (string, error) { - return r.m.GetEncryptedString(key, encryptionKey) + return r.meta.GetEncryptedString(key, encryptionKey) } func (r ReadOnly) GetInt64(key string) (int64, error) { - return r.m.GetInt64(key) + return r.meta.GetInt64(key) } func (r ReadOnly) GetFloat64(key string) (float64, error) { - return r.m.GetFloat64(key) + return r.meta.GetFloat64(key) } func (r ReadOnly) GetBytes(key string) ([]byte, error) { - return r.m.GetBytes(key) + return r.meta.GetBytes(key) } func (r ReadOnly) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) { - return r.m.GetEncryptedBytes(key, encryptionKey) + return r.meta.GetEncryptedBytes(key, encryptionKey) } func (r ReadOnly) GetNode(key string) (ipld.Node, error) { - return r.m.GetNode(key) + return r.meta.GetNode(key) +} + +func (r ReadOnly) Iter() iter.Seq2[string, ipld.Node] { + return r.meta.Iter() } func (r ReadOnly) Equals(other ReadOnly) bool { - return r.m.Equals(other.m) + return r.meta.Equals(other.meta) } func (r ReadOnly) String() string { - return r.m.String() + return r.meta.String() +} + +func (r ReadOnly) WriteableClone() *Meta { + return r.meta.Clone() } diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index 12f0a08..ee85a94 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -164,6 +164,8 @@ func (t *Token) Arguments() args.ReadOnly { // Proof() returns the ordered list of cid.Cid which reference the // delegation Tokens that authorize this invocation. +// Ordering is from the leaf Delegation (with aud matching the invocation's iss) +// to the root delegation. func (t *Token) Proof() []cid.Cid { return t.proof }