dynabuf

package module
v0.0.0-...-852522b Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2025 License: MPL-2.0 Imports: 8 Imported by: 0

README

dynabuf

Dynabuf converts Protocol Buffers to attribute maps for DynamoDB, and back again.

Installation

$ go get -v github.com/picatz/dynabuf@latest

How Does It Work?

Dynabuf leverages protojson as an intermediate format to convert between Protocol Buffers and DynamoDB attribute maps. This approach utilizes the standard JSON mapping provided by Protocol Buffers, ensuring a consistent and predictable conversion process. By using Dynabuf, you maintain the ability to perform DynamoDB queries on your data structures without sacrificing type safety or requiring additional boilerplate code. No manual field mapping or protoc plugins required.

[!IMPORTANT]

This library is designed to be used with the AWS SDK for Go (v2).

Usage

The dynabuf package provides two main functions: Marshal and Unmarshal that should feel familiar to most Go developers. The expr package provides a way to build DynamoDB expressions using Common Expression Language (CEL) syntax.

Marshal and Unmarshal

You define a Protocol Buffer message, generate the corresponding Go struct, and then use the Marshal and Unmarshal functions to convert between the Protocol Buffer message and a DynamoDB attribute value, such as an attribute map.

syntax = "proto3";

package example;

message User {
  string id = 1;
  string name = 2;
  string email = 3;
}
resp, _ := dynamoClient.CreateTable(
	ctx,
	&dynamodb.CreateTableInput{
		AttributeDefinitions: []types.AttributeDefinition{
			{
				AttributeName: aws.String("id"),
				AttributeType: types.ScalarAttributeTypeS,
			},
		},
		KeySchema: []types.KeySchemaElement{
			{
				AttributeName: aws.String("id"),
				KeyType:       types.KeyTypeHash,
			},
		},
		TableName:   aws.String(table),
		BillingMode: types.BillingModePayPerRequest,
	},
)

user := &example.User{
    Id: "123",
    Name: "John Doe",
    Email: "...",
}

item, _ := dynabuf.Marshal(user)

dynamoClient.PutItem(ctx, &dynamodb.PutItemInput{
	TableName: tableName,
	Item:      item,
})

output, _ := dynamoClient.GetItem(ctx, &dynamodb.GetItemInput{
	TableName: tableName,
	Key: map[string]types.AttributeValue{
		"id": &types.AttributeValueMemberS{Value: user.Id},
	},
})

outputProto := &example.User{}
dynabuf.Unmarshal(output.Item, outputProto)

fmt.Println(outputProto)
// &example.User{Id: "123", Name: "John Doe", Email: "..."}

[!TIP]

The Marshal and Unmarshal functions can be used to convert single messages or a slice of messages. This is particularly useful when working with batch operations in DynamoDB, where you may need to convert multiple attribute maps at once, without the extra loop logic.

Expressions

You can use the expr package to build DynamoDB expressions for use with DynamoDB operations like Query, Scan, UpdateItem, and DeleteItem.

syntax = "proto3";

package example;

message UserOrder {
  string user = 1;
  string order = 2;
  string status = 3;
  repeated string tags = 4;
}
userOrder := &example.UserOrder{
	User:  "123",
	Order: "456",
	Status: "pending",
	Tags:  []string{"urgent"},
}

item, _ := dynabuf.Marshal(userOrder)

dynamoClient.PutItem(ctx, &dynamodb.PutItemInput{
	TableName: tableName,
	Item:      item,
})

env, _ := expr.NewEnv(
	expr.MessageFieldVariables(userOrder)...,
	// cel.Variable("user", types.StringType),  // partition key
	// cel.Variable("order", types.StringType), // sort key
	// cel.Variable("status", types.StringType),
	// cel.Variable("tags", types.NewListType(types.StringType)),
)

keyAst, _ := env.Compile(`user == "123" && order == "456"`)
keyCond, _ := expr.KeyCondition(keyAst)

filterAst, _ := env.Compile(`status == "pending" && "urgent" in tags`)
filterCond, _ := expr.Filter(filterAst)

keyCondAndFilterExpr, _ := expression.NewBuilder().WithKeyCondition(keyCond).WithFilter(filterCond).Build()

queryInput := dynamodb.QueryInput{
	TableName:                 aws.String("example-table"),
	KeyConditionExpression:    keyCondAndFilterExpr.KeyCondition(),
	FilterExpression:          keyCondAndFilterExpr.Filter(),
	ExpressionAttributeNames:  keyCondAndFilterExpr.Names(),
	ExpressionAttributeValues: keyCondAndFilterExpr.Values(),
}

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrFailedToMarshal is returned when the function fails to marshal a protobuf message to a DynamoDB attribute value.
	ErrFailedToMarshal = errors.New("dynabuf: failed to marshal protobuf to DynamoDB attribute value")

	// ErrFailedToMarshalIntermediary is returned when the function fails to marshal the intermediary map to JSON.
	ErrFailedToMarshalIntermediary = errors.New("dynabuf: failed to marshal intermediary map to JSON")

	// ErrFailedToUnmarshalIntermediary is returned when the function fails to unmarshal the DynamoDB attribute value to an intermediary map.
	ErrFailedToUnmarshalIntermediary = errors.New("dynabuf: failed to unmarshal DynamoDB attribute value to intermediary map")

	// ErrFailedToUnmarshal is returned when the function fails to unmarshal a DynamoDB attribute value to a protobuf message.
	ErrFailedToUnmarshal = errors.New("dynabuf: failed to unmarshal DynamoDB attribute value to protobuf")

	// ErrInvalidInput is returned when the input is not a protobuf message or slice of messages.
	ErrInvalidInput = errors.New("dynabuf: invalid input, must be a protobuf message or slice of messages")

	// ErrInvalidOutput is returned when the output is not a pointer to a protobuf message or slice of messages.
	ErrInvalidOutput = errors.New("dynabuf: invalid output, must be a pointer to a protobuf message or slice of messages")
)

Set of error messages that can be returned by the Marshal and Unmarshal functions.

Functions

func Marshal

func Marshal(v any) (any, error)

Marshal returns the DynamoDB attribute value encoding of the given protobuf message or slice of messages. If there are any issues with marshaling, an error is returned.

Protocol Buffer to DynamoDB Attribute Value Marshaling

We use a three-step process to marshal a protobuf message to a DynamoDB attribute value, using JSON as the logical intermediary.

  1. The function first marshals the protobuf message to JSON using the google.golang.org/protobuf/encoding/protojson package.

  2. Then, it unmarshals the JSON to a map using the standard library's encoding/json package.

  3. Finally, it uses the github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue package to marshal the map to a DynamoDB attribute value.

The process is similar for a slice of protobuf messages, but the function iterates over each message in the slice and marshals them individually.

Example

import (
  "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
  "google.golang.org/protobuf/types/known/structpb"
  "github.com/picatz/dynabuf"
)

input := &structpb.Struct{
  Fields: map[string]*structpb.Value{
    "bar": {
      Kind: &structpb.Value_StringValue{
        StringValue: "hello world",
      },
    },
  },
}

outputAny, _ := dynabuf.Marshal(input)

output := outputAny.(map[string]types.AttributeValue)
Example
package main

import (
	"fmt"

	dbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
	"github.com/picatz/dynabuf"
	"google.golang.org/protobuf/types/known/structpb"
)

func main() {
	userOrder := &structpb.Struct{
		Fields: map[string]*structpb.Value{
			"user":   structpb.NewStringValue("user#123"),
			"order":  structpb.NewStringValue("order#123"),
			"status": structpb.NewStringValue("pending"),
			"tags": structpb.NewListValue(&structpb.ListValue{
				Values: []*structpb.Value{
					structpb.NewStringValue("tag-1"),
				},
			}),
		},
	}

	item, err := dynabuf.Marshal(userOrder)
	if err != nil {
		panic(err)
	}

	itemMap := item.(map[string]dbtypes.AttributeValue)
	fmt.Println(itemMap["user"].(*dbtypes.AttributeValueMemberS).Value)
	fmt.Println(itemMap["order"].(*dbtypes.AttributeValueMemberS).Value)
	fmt.Println(itemMap["status"].(*dbtypes.AttributeValueMemberS).Value)
	fmt.Println(itemMap["tags"].(*dbtypes.AttributeValueMemberL).Value[0])

}
Output:
user#123
order#123
pending
&{tag-1 {}}

func Unmarshal

func Unmarshal(av any, v any) error

Unmarshal parses the DynamoDB attribute values in av and stores the result in v. v must be a pointer to a single protobuf message or a slice of protobuf messages. If there are any issues with unmarshaling, an error is returned.

DynamoDB Attribute Value to Protocol Buffer Unmarshaling

Similar to marshalling, we use a three-step process to unmarshal a DynamoDB attribute value to a protobuf message, using JSON as the logical intermediary.

  1. The function first unmarshals the DynamoDB attribute value to a map using the github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue package.

  2. Then, it marshals the map to JSON using the standard library's encoding/json package.

  3. Finally, it unmarshals the JSON to a protobuf message using the google.golang.org/protobuf/encoding/protojson package.

The process is similar for a slice of protobuf messages, but the function iterates over each item in the slice and unmarshals them individually.

Example

import (
  "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
  "google.golang.org/protobuf/types/known/structpb"
  "github.com/picatz/dynabuf"
)

av := map[string]types.AttributeValue{
  "bar": &types.AttributeValueMemberS{
    Value: "hello world",
  },
}

var output structpb.Struct
_ = dynabuf.Unmarshal(av, &output)
Example
package main

import (
	"fmt"

	dbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
	"github.com/picatz/dynabuf"
	"google.golang.org/protobuf/types/known/structpb"
)

func main() {
	userOrderAV := map[string]dbtypes.AttributeValue{
		"user": &dbtypes.AttributeValueMemberS{
			Value: "user#123",
		},
		"order": &dbtypes.AttributeValueMemberS{
			Value: "order#123",
		},
		"status": &dbtypes.AttributeValueMemberS{
			Value: "pending",
		},
		"tags": &dbtypes.AttributeValueMemberL{
			Value: []dbtypes.AttributeValue{
				&dbtypes.AttributeValueMemberS{
					Value: "tag-1",
				},
			},
		},
	}

	userOrderPB := &structpb.Struct{}

	err := dynabuf.Unmarshal(userOrderAV, userOrderPB)
	if err != nil {
		panic(err)
	}

	fmt.Println(userOrderPB.Fields["user"].GetStringValue())
	fmt.Println(userOrderPB.Fields["order"].GetStringValue())
	fmt.Println(userOrderPB.Fields["status"].GetStringValue())
	fmt.Println(userOrderPB.Fields["tags"].GetListValue().Values[0].GetStringValue())
}
Output:
user#123
order#123
pending
tag-1

Types

This section is empty.

Directories

Path Synopsis
Package expr provides a [transpiler] that converts [Common Expression Language] (CEL) expressions to Amazon [DynamoDB] expressions.
Package expr provides a [transpiler] that converts [Common Expression Language] (CEL) expressions to Amazon [DynamoDB] expressions.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL