feat(args): export fluent builder

This commit is contained in:
Steve Moyer
2024-11-27 12:05:00 -05:00
parent ce1a4b6e32
commit 170e597e71
6 changed files with 176 additions and 55 deletions

71
pkg/args/builder.go Normal file
View File

@@ -0,0 +1,71 @@
package args
import (
"errors"
"github.com/ipld/go-ipld-prime"
)
// Builder allows the fluid construction of an Args.
type Builder struct {
args *Args
errs error
}
// NewBuilder returns a Builder which will assemble the Args.
func NewBuilder() *Builder {
return &Builder{
args: New(),
}
}
// Add inserts a new key/val into the Args being assembled while collecting
// any errors caused by duplicate keys.
func (b *Builder) Add(key string, val any) *Builder {
b.errs = errors.Join(b.errs, b.args.Add(key, val))
return b
}
// Build returns the assembled Args or an error containing a list of
// errors encountered while trying to build the Args.
func (b *Builder) Build() (*Args, error) {
if b.errs != nil {
return nil, b.errs
}
return b.args, nil
}
// BuildIPLD is the same as Build except it takes the additional step of
// converting the Args to an ipld.Node.
func (b *Builder) BuildIPLD() (ipld.Node, error) {
args, err := b.Build()
if err != nil {
return nil, err
}
return args.ToIPLD()
}
// MustBuild is the same as Build except it panics if an error occurs.
func (b *Builder) MustBuild() *Args {
args, err := b.Build()
if err != nil {
panic(b.errs)
}
return args
}
// MustBuildIPLD is the same as BuildIPLD except it panics if an error
// occurs.
func (b *Builder) MustBuildIPLD() ipld.Node {
node, err := b.BuildIPLD()
if err != nil {
panic(err)
}
return node
}

81
pkg/args/builder_test.go Normal file
View File

@@ -0,0 +1,81 @@
package args_test
import (
"testing"
"github.com/ipld/go-ipld-prime"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/pkg/args"
)
func TestBuilder_XXX(t *testing.T) {
t.Parallel()
const (
keyOne = "key1"
valOne = "string"
keyTwo = "key2"
valTwo = 42
)
exp := args.New()
exp.Add(keyOne, valOne)
exp.Add(keyTwo, valTwo)
expNode, err := exp.ToIPLD()
require.NoError(t, err)
disjointKeys := args.NewBuilder().
Add(keyOne, valOne).
Add(keyTwo, valTwo)
duplicateKeys := args.NewBuilder().
Add(keyOne, valOne).
Add(keyTwo, valTwo).
Add(keyOne, "oh no!")
t.Run("MustBuild succeeds with disjoint keys", func(t *testing.T) {
t.Parallel()
var act *args.Args
require.NotPanics(t, func() {
act = disjointKeys.MustBuild()
})
assert.Equal(t, exp, act)
})
t.Run("MustBuild fails with duplicate keys", func(t *testing.T) {
t.Parallel()
var act *args.Args
require.Panics(t, func() {
act = duplicateKeys.MustBuild()
})
assert.Nil(t, act)
})
t.Run("MustBuildIPLD succeeds with disjoint keys", func(t *testing.T) {
t.Parallel()
var act ipld.Node
require.NotPanics(t, func() {
act = disjointKeys.MustBuildIPLD()
})
assert.Equal(t, expNode, act)
})
t.Run("MustBuildIPLD fails with duplicate keys", func(t *testing.T) {
t.Parallel()
var act ipld.Node
require.Panics(t, func() {
act = duplicateKeys.MustBuildIPLD()
})
assert.Nil(t, act)
})
}

View File

@@ -1,8 +1,6 @@
package policytest package policytest
import ( import (
"errors"
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
"github.com/ucan-wg/go-ucan/pkg/args" "github.com/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/pkg/policy"
@@ -12,12 +10,12 @@ import (
// EmptyPolicy provides a Policy with no statements. // EmptyPolicy provides a Policy with no statements.
var EmptyPolicy = policy.Policy{} var EmptyPolicy = policy.Policy{}
// ExampleValidationPolicy provides a instantiated Policy containing the // ExampleValidationPolicy provides a instantiated SpecPolicy containing the
// statements that are included in the second code block of the [Validation] // statements that are included in the second code block of the [Validation]
// section of the delegation specification. // section of the delegation specification.
// //
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation // [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
var ExamplePolicy = policy.MustConstruct( var SpecPolicy = policy.MustConstruct(
policy.Equal(".from", literal.String("alice@example.com")), policy.Equal(".from", literal.String("alice@example.com")),
policy.Any(".to", policy.Like(".", "*@example.com")), policy.Any(".to", policy.Like(".", "*@example.com")),
) )
@@ -25,68 +23,39 @@ var ExamplePolicy = policy.MustConstruct(
// TODO: Replace the URL for [Validation] above when the delegation // TODO: Replace the URL for [Validation] above when the delegation
// specification has been finished/merged. // specification has been finished/merged.
// ExampleValidArguments provides valid, instantiated Arguments containing // SpecValidArguments provides valid, instantiated Arguments containing
// the key/value pairs that are included in portion of the the second code // the key/value pairs that are included in portion of the the second code
// block of the [Validation] section of the delegation specification. // block of the [Validation] section of the delegation specification.
// //
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation // [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
var ExampleValidArguments = newBuilder(nil). var SpecValidArguments = args.NewBuilder().
add("from", "alice@example.com"). Add("from", "alice@example.com").
add("to", []string{ Add("to", []string{
"bob@example.com", "bob@example.com",
"carol@not.example.com", "carol@not.example.com",
}). }).
add("title", "Coffee"). Add("title", "Coffee").
add("body", "Still on for coffee"). Add("body", "Still on for coffee").
mustBuild() MustBuild()
var exampleValidArgumentsIPLD = mustIPLD(ExampleValidArguments) var specValidArgumentsIPLD = mustIPLD(SpecValidArguments)
// ExampleInvalidArguments provides invalid, instantiated Arguments containing // SpecInvalidArguments provides invalid, instantiated Arguments containing
// the key/value pairs that are included in portion of the the second code // the key/value pairs that are included in portion of the the second code
// block of the [Validation] section of the delegation specification. // block of the [Validation] section of the delegation specification.
// //
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation // [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
var ExampleInvalidArguments = newBuilder(nil). var SpecInvalidArguments = args.NewBuilder().
add("from", "alice@example.com"). Add("from", "alice@example.com").
add("to", []string{ Add("to", []string{
"bob@null.com", "bob@null.com",
"carol@elsewhere.example.com", "carol@elsewhere.example.com",
}). }).
add("title", "Coffee"). Add("title", "Coffee").
add("body", "Still on for coffee"). Add("body", "Still on for coffee").
mustBuild() MustBuild()
var exampleInvalidArgumentsIPLD = mustIPLD(ExampleInvalidArguments) var specInvalidArgumentsIPLD = mustIPLD(SpecInvalidArguments)
type builder struct {
args *args.Args
errs error
}
func newBuilder(a *args.Args) *builder {
if a == nil {
a = args.New()
}
return &builder{
args: a,
}
}
func (b *builder) add(key string, val any) *builder {
b.errs = errors.Join(b.errs, b.args.Add(key, val))
return b
}
func (b *builder) mustBuild() *args.Args {
if b.errs != nil {
panic(b.errs)
}
return b.args
}
func mustIPLD(args *args.Args) ipld.Node { func mustIPLD(args *args.Args) ipld.Node {
node, err := args.ToIPLD() node, err := args.ToIPLD()

View File

@@ -17,7 +17,7 @@ func TestInvocationValidationSpecExamples(t *testing.T) {
t.Run("with passing args", func(t *testing.T) { t.Run("with passing args", func(t *testing.T) {
t.Parallel() t.Parallel()
exec, stmt := ExamplePolicy.Match(exampleValidArgumentsIPLD) exec, stmt := SpecPolicy.Match(specValidArgumentsIPLD)
assert.True(t, exec) assert.True(t, exec)
assert.Nil(t, stmt) assert.Nil(t, stmt)
}) })
@@ -25,7 +25,7 @@ func TestInvocationValidationSpecExamples(t *testing.T) {
t.Run("fails on recipients (second statement)", func(t *testing.T) { t.Run("fails on recipients (second statement)", func(t *testing.T) {
t.Parallel() t.Parallel()
exec, stmt := ExamplePolicy.Match(exampleInvalidArgumentsIPLD) exec, stmt := SpecPolicy.Match(specInvalidArgumentsIPLD)
assert.False(t, exec) assert.False(t, exec)
assert.NotNil(t, stmt) assert.NotNil(t, stmt)
}) })

View File

@@ -130,7 +130,7 @@ func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari vari
p.opts = append(p.opts, delegation.WithNotBefore(nbf)) p.opts = append(p.opts, delegation.WithNotBefore(nbf))
}}, }},
{name: "ValidExamplePolicy", variant: func(p *newDelegationParams) { {name: "ValidExamplePolicy", variant: func(p *newDelegationParams) {
p.pol = policytest.ExamplePolicy p.pol = policytest.SpecPolicy
}}, }},
} }

View File

@@ -52,13 +52,13 @@ func TestToken_ExecutionAllowed(t *testing.T) {
t.Run("passes - arguments satisfy empty policy", func(t *testing.T) { t.Run("passes - arguments satisfy empty policy", func(t *testing.T) {
t.Parallel() t.Parallel()
testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.ExampleValidArguments, delegationtest.ProofAliceBobCarolDanErinFrank) testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecValidArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
}) })
t.Run("passes - arguments satify example policy", func(t *testing.T) { t.Run("passes - arguments satify example policy", func(t *testing.T) {
t.Parallel() t.Parallel()
testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.ExampleValidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy) testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecValidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy)
}) })
t.Run("fails - no proof", func(t *testing.T) { t.Run("fails - no proof", func(t *testing.T) {
@@ -132,7 +132,7 @@ func TestToken_ExecutionAllowed(t *testing.T) {
t.Run("passes - arguments satify example policy", func(t *testing.T) { t.Run("passes - arguments satify example policy", func(t *testing.T) {
t.Parallel() t.Parallel()
testFails(t, invocation.ErrPolicyNotSatisfied, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.ExampleInvalidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy) testFails(t, invocation.ErrPolicyNotSatisfied, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecInvalidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy)
}) })
} }