From 170e597e7108635a1c8e71b52379830f28e9e769 Mon Sep 17 00:00:00 2001 From: Steve Moyer Date: Wed, 27 Nov 2024 12:05:00 -0500 Subject: [PATCH] feat(args): export fluent builder --- pkg/args/builder.go | 71 ++++++++++++++++ pkg/args/builder_test.go | 81 +++++++++++++++++++ pkg/policy/policytest/{example.go => spec.go} | 67 +++++---------- .../{example_test.go => spec_test.go} | 4 +- .../delegationtest/generator/generator.go | 2 +- token/invocation/invocation_test.go | 6 +- 6 files changed, 176 insertions(+), 55 deletions(-) create mode 100644 pkg/args/builder.go create mode 100644 pkg/args/builder_test.go rename pkg/policy/policytest/{example.go => spec.go} (54%) rename pkg/policy/policytest/{example_test.go => spec_test.go} (83%) diff --git a/pkg/args/builder.go b/pkg/args/builder.go new file mode 100644 index 0000000..8704c1d --- /dev/null +++ b/pkg/args/builder.go @@ -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 +} diff --git a/pkg/args/builder_test.go b/pkg/args/builder_test.go new file mode 100644 index 0000000..2f586f3 --- /dev/null +++ b/pkg/args/builder_test.go @@ -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) + }) +} diff --git a/pkg/policy/policytest/example.go b/pkg/policy/policytest/spec.go similarity index 54% rename from pkg/policy/policytest/example.go rename to pkg/policy/policytest/spec.go index e9d20c6..ad6f679 100644 --- a/pkg/policy/policytest/example.go +++ b/pkg/policy/policytest/spec.go @@ -1,8 +1,6 @@ package policytest import ( - "errors" - "github.com/ipld/go-ipld-prime" "github.com/ucan-wg/go-ucan/pkg/args" "github.com/ucan-wg/go-ucan/pkg/policy" @@ -12,12 +10,12 @@ import ( // EmptyPolicy provides a Policy with no statements. 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] // section of the delegation specification. // // [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.Any(".to", policy.Like(".", "*@example.com")), ) @@ -25,68 +23,39 @@ var ExamplePolicy = policy.MustConstruct( // TODO: Replace the URL for [Validation] above when the delegation // 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 // block of the [Validation] section of the delegation specification. // // [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation -var ExampleValidArguments = newBuilder(nil). - add("from", "alice@example.com"). - add("to", []string{ +var SpecValidArguments = args.NewBuilder(). + Add("from", "alice@example.com"). + Add("to", []string{ "bob@example.com", "carol@not.example.com", }). - add("title", "Coffee"). - add("body", "Still on for coffee"). - mustBuild() + Add("title", "Coffee"). + Add("body", "Still on for coffee"). + 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 // block of the [Validation] section of the delegation specification. // // [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation -var ExampleInvalidArguments = newBuilder(nil). - add("from", "alice@example.com"). - add("to", []string{ +var SpecInvalidArguments = args.NewBuilder(). + Add("from", "alice@example.com"). + Add("to", []string{ "bob@null.com", "carol@elsewhere.example.com", }). - add("title", "Coffee"). - add("body", "Still on for coffee"). - mustBuild() + Add("title", "Coffee"). + Add("body", "Still on for coffee"). + MustBuild() -var exampleInvalidArgumentsIPLD = mustIPLD(ExampleInvalidArguments) - -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 -} +var specInvalidArgumentsIPLD = mustIPLD(SpecInvalidArguments) func mustIPLD(args *args.Args) ipld.Node { node, err := args.ToIPLD() diff --git a/pkg/policy/policytest/example_test.go b/pkg/policy/policytest/spec_test.go similarity index 83% rename from pkg/policy/policytest/example_test.go rename to pkg/policy/policytest/spec_test.go index 4025d8a..2da6e61 100644 --- a/pkg/policy/policytest/example_test.go +++ b/pkg/policy/policytest/spec_test.go @@ -17,7 +17,7 @@ func TestInvocationValidationSpecExamples(t *testing.T) { t.Run("with passing args", func(t *testing.T) { t.Parallel() - exec, stmt := ExamplePolicy.Match(exampleValidArgumentsIPLD) + exec, stmt := SpecPolicy.Match(specValidArgumentsIPLD) assert.True(t, exec) 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.Parallel() - exec, stmt := ExamplePolicy.Match(exampleInvalidArgumentsIPLD) + exec, stmt := SpecPolicy.Match(specInvalidArgumentsIPLD) assert.False(t, exec) assert.NotNil(t, stmt) }) diff --git a/token/delegation/delegationtest/generator/generator.go b/token/delegation/delegationtest/generator/generator.go index 3fca615..c15f29f 100644 --- a/token/delegation/delegationtest/generator/generator.go +++ b/token/delegation/delegationtest/generator/generator.go @@ -130,7 +130,7 @@ func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari vari p.opts = append(p.opts, delegation.WithNotBefore(nbf)) }}, {name: "ValidExamplePolicy", variant: func(p *newDelegationParams) { - p.pol = policytest.ExamplePolicy + p.pol = policytest.SpecPolicy }}, } diff --git a/token/invocation/invocation_test.go b/token/invocation/invocation_test.go index 5e991fc..a8d2455 100644 --- a/token/invocation/invocation_test.go +++ b/token/invocation/invocation_test.go @@ -52,13 +52,13 @@ func TestToken_ExecutionAllowed(t *testing.T) { t.Run("passes - arguments satisfy empty policy", func(t *testing.T) { 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.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) { @@ -132,7 +132,7 @@ func TestToken_ExecutionAllowed(t *testing.T) { t.Run("passes - arguments satify example policy", func(t *testing.T) { 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) }) }