In my project, I have two go packages, world
and database
and two go programs client
and server
:
gopath
- src
- world (imports encoding/json)
- database (imports world, imports dynamodb)
- client (imports world, encoding/json)
- server (imports world, database, encoding/json)
The world
package contains many such enumerators that are common to client
and server
:
const (
HammerTool ToolType = iota
TrowelTool
FrameTool
PistolTool
CannonTool
)
And since encoding/json
is used to communicate between client and server, world
contains the associated JSON marshalers and unmarshalers for each enumerator.
func (toolType ToolType) MarshalText() ([]byte, error) {
switch toolType {
case HammerTool:
return []byte("hammer"), nil
case TrowelTool:
return []byte("trowel"), nil
case FrameTool:
return []byte("frame"), nil
case PistolTool:
return []byte("pistol"), nil
case CannonTool:
return []byte("cannon"), nil
default:
return nil, errors.New("Unknown tool " + fmt.Sprint(toolType))
}
}
func (toolType *ToolType) UnmarshalText(text []byte) error {
switch string(text) {
case "hammer":
*toolType = HammerTool
case "trowel":
*toolType = TrowelTool
case "frame":
*toolType = FrameTool
case "pistol":
*toolType = PistolTool
case "cannon":
*toolType = CannonTool
default:
return errors.New("Unknown tool " + string(text))
}
return nil
}
The problem arises when the enumerators are saved to DynamoDB in the database
package. DynamoDB has its own marshalers and unmarshalers:
// Note: These methods were never tested because they were not compiled.
// They may contain errors but should convey the general idea
func (toolType ToolType) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
txt, err := toolType.MarshalText()
if err != nil {
return err
}
av.S = aws.String(txt)
return nil
}
func (toolType *ToolType) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
return toolType.UnmarshalText([]byte(*av.S))
}
These methods are thin wrappers of the JSON marshalers/unmarshalers. However, they cannot be compiled into the database
package as Go does not allow adding additional receivers on an imported type. Additionally, they cannot be added into the world
package as world
is imported by client
and client
cannot afford to be megabytes larger due to an import of the DynamoDB package exposing the dynamodb.AttributeValue
type. After all, client
gets compiled to js/wasm
and downloaded to web browsers over the network…and has no use for DynamoDB.
My question is: How can I implement custom marshalers/unmarshalers on types that I import from other packages?
Notes:
- I don’t want to keep marshaling enumerators into DynamoDB as integers because then I couldn’t add additional enumerator values without risking corruption
- It would be nice if everything related to DynamoDB was limited to the
database
package, including all DynamoDB specific marshalers. - The DynamoDB SDK provides a
string
struct tag but, for example, it would encodeTrowelTool
as{S:"1"}
not{S:"trowel"}
, as opposed the normal encoding of{N:"1"}