dag: test op serialisation with the unmarshaller, to allow resolving entities

Michael MurΓ© created

Change summary

entities/bug/bug.go                  |  2 
entities/bug/op_add_comment_test.go  |  9 +++---
entities/bug/op_create_test.go       |  8 +++---
entities/bug/op_edit_comment_test.go |  9 +++---
entities/bug/op_label_change_test.go | 13 +++++----
entities/bug/op_set_status_test.go   |  5 ++-
entities/bug/op_set_title_test.go    |  5 ++-
entities/bug/operation.go            |  2 
entity/dag/common_test.go            | 11 -------
entity/dag/entity.go                 |  4 ++
entity/dag/example_test.go           |  6 ++--
entity/dag/op_noop_test.go           | 10 ++++++-
entity/dag/op_set_metadata_test.go   | 10 ++++++-
entity/dag/operation_testing.go      | 38 +++++++++--------------------
entity/resolver.go                   | 12 +++++++++
15 files changed, 76 insertions(+), 68 deletions(-)

Detailed changes

entities/bug/bug.go πŸ”—

@@ -23,7 +23,7 @@ const formatVersion = 4
 var def = dag.Definition{
 	Typename:             "bug",
 	Namespace:            "bugs",
-	OperationUnmarshaler: operationUnmarshaller,
+	OperationUnmarshaler: operationUnmarshaler,
 	FormatVersion:        formatVersion,
 }
 

entities/bug/op_add_comment_test.go πŸ”—

@@ -4,15 +4,16 @@ import (
 	"testing"
 
 	"github.com/MichaelMure/git-bug/entities/identity"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/entity/dag"
 	"github.com/MichaelMure/git-bug/repository"
 )
 
 func TestAddCommentSerialize(t *testing.T) {
-	dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *AddCommentOperation {
-		return NewAddCommentOp(author, unixTime, "message", nil)
+	dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*AddCommentOperation, entity.Resolvers) {
+		return NewAddCommentOp(author, unixTime, "message", nil), nil
 	})
-	dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *AddCommentOperation {
-		return NewAddCommentOp(author, unixTime, "message", []repository.Hash{"hash1", "hash2"})
+	dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*AddCommentOperation, entity.Resolvers) {
+		return NewAddCommentOp(author, unixTime, "message", []repository.Hash{"hash1", "hash2"}), nil
 	})
 }

entities/bug/op_create_test.go πŸ”—

@@ -40,10 +40,10 @@ func TestCreate(t *testing.T) {
 }
 
 func TestCreateSerialize(t *testing.T) {
-	dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *CreateOperation {
-		return NewCreateOp(author, unixTime, "title", "message", nil)
+	dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*CreateOperation, entity.Resolvers) {
+		return NewCreateOp(author, unixTime, "title", "message", nil), nil
 	})
-	dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *CreateOperation {
-		return NewCreateOp(author, unixTime, "title", "message", []repository.Hash{"hash1", "hash2"})
+	dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*CreateOperation, entity.Resolvers) {
+		return NewCreateOp(author, unixTime, "title", "message", []repository.Hash{"hash1", "hash2"}), nil
 	})
 }

entities/bug/op_edit_comment_test.go πŸ”—

@@ -7,6 +7,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/MichaelMure/git-bug/entities/identity"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/entity/dag"
 	"github.com/MichaelMure/git-bug/repository"
 )
@@ -75,10 +76,10 @@ func TestEdit(t *testing.T) {
 }
 
 func TestEditCommentSerialize(t *testing.T) {
-	dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *EditCommentOperation {
-		return NewEditCommentOp(author, unixTime, "target", "message", nil)
+	dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*EditCommentOperation, entity.Resolvers) {
+		return NewEditCommentOp(author, unixTime, "target", "message", nil), nil
 	})
-	dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *EditCommentOperation {
-		return NewEditCommentOp(author, unixTime, "target", "message", []repository.Hash{"hash1", "hash2"})
+	dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*EditCommentOperation, entity.Resolvers) {
+		return NewEditCommentOp(author, unixTime, "target", "message", []repository.Hash{"hash1", "hash2"}), nil
 	})
 }

entities/bug/op_label_change_test.go πŸ”—

@@ -4,17 +4,18 @@ import (
 	"testing"
 
 	"github.com/MichaelMure/git-bug/entities/identity"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/entity/dag"
 )
 
 func TestLabelChangeSerialize(t *testing.T) {
-	dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
-		return NewLabelChangeOperation(author, unixTime, []Label{"added"}, []Label{"removed"})
+	dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*LabelChangeOperation, entity.Resolvers) {
+		return NewLabelChangeOperation(author, unixTime, []Label{"added"}, []Label{"removed"}), nil
 	})
-	dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
-		return NewLabelChangeOperation(author, unixTime, []Label{"added"}, nil)
+	dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*LabelChangeOperation, entity.Resolvers) {
+		return NewLabelChangeOperation(author, unixTime, []Label{"added"}, nil), nil
 	})
-	dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
-		return NewLabelChangeOperation(author, unixTime, nil, []Label{"removed"})
+	dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*LabelChangeOperation, entity.Resolvers) {
+		return NewLabelChangeOperation(author, unixTime, nil, []Label{"removed"}), nil
 	})
 }

entities/bug/op_set_status_test.go πŸ”—

@@ -5,11 +5,12 @@ import (
 
 	"github.com/MichaelMure/git-bug/entities/common"
 	"github.com/MichaelMure/git-bug/entities/identity"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/entity/dag"
 )
 
 func TestSetStatusSerialize(t *testing.T) {
-	dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetStatusOperation {
-		return NewSetStatusOp(author, unixTime, common.ClosedStatus)
+	dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*SetStatusOperation, entity.Resolvers) {
+		return NewSetStatusOp(author, unixTime, common.ClosedStatus), nil
 	})
 }

entities/bug/op_set_title_test.go πŸ”—

@@ -4,11 +4,12 @@ import (
 	"testing"
 
 	"github.com/MichaelMure/git-bug/entities/identity"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/entity/dag"
 )
 
 func TestSetTitleSerialize(t *testing.T) {
-	dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetTitleOperation {
-		return NewSetTitleOp(author, unixTime, "title", "was")
+	dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*SetTitleOperation, entity.Resolvers) {
+		return NewSetTitleOp(author, unixTime, "title", "was"), nil
 	})
 }

entities/bug/operation.go πŸ”—

@@ -32,7 +32,7 @@ type Operation interface {
 var _ Operation = &dag.NoOpOperation[*Snapshot]{}
 var _ Operation = &dag.SetMetadataOperation[*Snapshot]{}
 
-func operationUnmarshaller(raw json.RawMessage, resolvers entity.Resolvers) (dag.Operation, error) {
+func operationUnmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (dag.Operation, error) {
 	var t struct {
 		OperationType dag.OperationType `json:"type"`
 	}

entity/dag/common_test.go πŸ”—

@@ -141,16 +141,7 @@ func makeTestContextInternal(repo repository.ClockedRepo) (identity.Interface, i
 	}
 
 	resolvers := entity.Resolvers{
-		&identity.Identity{}: entity.ResolverFunc(func(id entity.Id) (entity.Interface, error) {
-			switch id {
-			case id1.Id():
-				return id1, nil
-			case id2.Id():
-				return id2, nil
-			default:
-				return nil, identity.ErrIdentityNotExist
-			}
-		}),
+		&identity.Identity{}: entity.MakeResolver(id1, id2),
 	}
 
 	def := Definition{

entity/dag/entity.go πŸ”—

@@ -19,6 +19,8 @@ const refsPattern = "refs/%s/%s"
 const creationClockPattern = "%s-create"
 const editClockPattern = "%s-edit"
 
+type OperationUnmarshaler func(raw json.RawMessage, resolver entity.Resolvers) (Operation, error)
+
 // Definition hold the details defining one specialization of an Entity.
 type Definition struct {
 	// the name of the entity (bug, pull-request, ...), for human consumption
@@ -26,7 +28,7 @@ type Definition struct {
 	// the Namespace in git references (bugs, prs, ...)
 	Namespace string
 	// a function decoding a JSON message into an Operation
-	OperationUnmarshaler func(raw json.RawMessage, resolver entity.Resolvers) (Operation, error)
+	OperationUnmarshaler OperationUnmarshaler
 	// the expected format version number, that can be used for data migration/upgrade
 	FormatVersion uint
 }

entity/dag/example_test.go πŸ”—

@@ -208,13 +208,13 @@ func NewProjectConfig() *ProjectConfig {
 var def = dag.Definition{
 	Typename:             "project config",
 	Namespace:            "conf",
-	OperationUnmarshaler: operationUnmarshaller,
+	OperationUnmarshaler: operationUnmarshaler,
 	FormatVersion:        1,
 }
 
-// operationUnmarshaller is a function doing the de-serialization of the JSON data into our own
+// operationUnmarshaler is a function doing the de-serialization of the JSON data into our own
 // concrete Operations. If needed, we can use the resolver to connect to other entities.
-func operationUnmarshaller(raw json.RawMessage, resolvers entity.Resolvers) (dag.Operation, error) {
+func operationUnmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (dag.Operation, error) {
 	var t struct {
 		OperationType dag.OperationType `json:"type"`
 	}

entity/dag/op_noop_test.go πŸ”—

@@ -1,13 +1,19 @@
 package dag
 
 import (
+	"encoding/json"
 	"testing"
 
 	"github.com/MichaelMure/git-bug/entities/identity"
+	"github.com/MichaelMure/git-bug/entity"
 )
 
 func TestNoopSerialize(t *testing.T) {
-	SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *NoOpOperation[*snapshotMock] {
-		return NewNoOpOp[*snapshotMock](1, author, unixTime)
+	SerializeRoundTripTest(t, func(raw json.RawMessage, resolver entity.Resolvers) (Operation, error) {
+		var op NoOpOperation[*snapshotMock]
+		err := json.Unmarshal(raw, &op)
+		return &op, err
+	}, func(author identity.Interface, unixTime int64) (*NoOpOperation[*snapshotMock], entity.Resolvers) {
+		return NewNoOpOp[*snapshotMock](1, author, unixTime), nil
 	})
 }

entity/dag/op_set_metadata_test.go πŸ”—

@@ -1,10 +1,12 @@
 package dag
 
 import (
+	"encoding/json"
 	"testing"
 	"time"
 
 	"github.com/MichaelMure/git-bug/entities/identity"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/repository"
 
 	"github.com/stretchr/testify/require"
@@ -97,10 +99,14 @@ func TestSetMetadata(t *testing.T) {
 }
 
 func TestSetMetadataSerialize(t *testing.T) {
-	SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetMetadataOperation[*snapshotMock] {
+	SerializeRoundTripTest(t, func(raw json.RawMessage, resolver entity.Resolvers) (Operation, error) {
+		var op SetMetadataOperation[*snapshotMock]
+		err := json.Unmarshal(raw, &op)
+		return &op, err
+	}, func(author identity.Interface, unixTime int64) (*SetMetadataOperation[*snapshotMock], entity.Resolvers) {
 		return NewSetMetadataOp[*snapshotMock](1, author, unixTime, "message", map[string]string{
 			"key1": "value1",
 			"key2": "value2",
-		})
+		}), nil
 	})
 }

entity/dag/operation_testing.go πŸ”—

@@ -14,44 +14,30 @@ import (
 
 // SerializeRoundTripTest realize a marshall/unmarshall round-trip in the same condition as with OperationPack,
 // and check if the recovered operation is identical.
-func SerializeRoundTripTest[OpT Operation](t *testing.T, maker func(author identity.Interface, unixTime int64) OpT) {
+func SerializeRoundTripTest[OpT Operation](
+	t *testing.T,
+	unmarshaler OperationUnmarshaler,
+	maker func(author identity.Interface, unixTime int64) (OpT, entity.Resolvers),
+) {
 	repo := repository.NewMockRepo()
 
 	rene, err := identity.NewIdentity(repo, "RenΓ© Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
-	op := maker(rene, time.Now().Unix())
+	op, resolvers := maker(rene, time.Now().Unix())
 	// enforce having an id
 	op.Id()
 
-	rdt := &roundTripper[OpT]{Before: op, author: rene}
-
-	data, err := json.Marshal(rdt)
+	data, err := json.Marshal(op)
 	require.NoError(t, err)
 
-	err = json.Unmarshal(data, &rdt)
+	after, err := unmarshaler(data, resolvers)
 	require.NoError(t, err)
 
-	require.Equal(t, op, rdt.after)
-}
-
-type roundTripper[OpT Operation] struct {
-	Before OpT
-	author identity.Interface
-	after  OpT
-}
-
-func (r *roundTripper[OpT]) MarshalJSON() ([]byte, error) {
-	return json.Marshal(r.Before)
-}
-
-func (r *roundTripper[OpT]) UnmarshalJSON(data []byte) error {
-	if err := json.Unmarshal(data, &r.after); err != nil {
-		return err
-	}
 	// Set the id from the serialized data
-	r.after.setId(entity.DeriveId(data))
+	after.setId(entity.DeriveId(data))
 	// Set the author, as OperationPack would do
-	r.after.setAuthor(r.author)
-	return nil
+	after.setAuthor(rene)
+
+	require.Equal(t, op, after)
 }

entity/resolver.go πŸ”—

@@ -72,3 +72,15 @@ type ResolverFunc func(id Id) (Interface, error)
 func (fn ResolverFunc) Resolve(id Id) (Interface, error) {
 	return fn(id)
 }
+
+// MakeResolver create a resolver able to return the given entities.
+func MakeResolver(entities ...Interface) Resolver {
+	return ResolverFunc(func(id Id) (Interface, error) {
+		for _, entity := range entities {
+			if entity.Id() == id {
+				return entity, nil
+			}
+		}
+		return nil, fmt.Errorf("entity not found")
+	})
+}