AWS dynamo stream processing using Go lambda - go

I'm using an AWS Lambda function written in Go with Dynamo stream but I don't see any way where I can marshall the old & new image to my struct because it's returning the image as map[string]DynamoDBAttributeValue.
I can check individual key and then assign it to my struct one by one, but is there an direct way to Marshall directly?
func HandleRequest(ctx context.Context, event events.DynamoDBEvent) {
for _, record := range event.Records {
var newStruct models.MyStruct // want to marshall newImage in this struct
logger.Debugf("New: %#v", record.Change.NewImage)
}
}
UPDATE:
Here is the custom DynamoDBEvent that I'm using now:
type DynamoDBEvent struct {
Records []DynamoDBEventRecord `json:"Records"`
}
type DynamoDBEventRecord struct {
AWSRegion string `json:"awsRegion"`
Change DynamoDBStreamRecord `json:"dynamodb"`
EventID string `json:"eventID"`
EventName string `json:"eventName"`
EventSource string `json:"eventSource"`
EventVersion string `json:"eventVersion"`
EventSourceArn string `json:"eventSourceARN"`
UserIdentity *events.DynamoDBUserIdentity `json:"userIdentity,omitempty"`
}
type DynamoDBStreamRecord struct {
ApproximateCreationDateTime events.SecondsEpochTime `json:"ApproximateCreationDateTime,omitempty"`
Keys map[string]*dynamodb.AttributeValue `json:"Keys,omitempty"`
NewImage map[string]*dynamodb.AttributeValue `json:"NewImage,omitempty"`
OldImage map[string]*dynamodb.AttributeValue `json:"OldImage,omitempty"`
SequenceNumber string `json:"SequenceNumber"`
SizeBytes int64 `json:"SizeBytes"`
StreamViewType string `json:"StreamViewType"`
}

Unfortunately there is no elegant way yet, sot you have to check each key/value pairs and assign them to your struct.
PS: As small sugar you can use func FromDynamoDBMap from this package and work with map[string]interface{} not with map[string]events.DynamoDBAttributeValue which is way easier.

I have created a solution for the problem. This was crucial to my implementation since my implementation uses custom keys.
Unfortunately, the Number type has to be cast to Float64 to make sure that it is not lost.
This will result in a completely usable struct against the changeset. You may need a map of your models and a known field
Usage
newResult, _ := UnmarshalStreamImage(record.Change.NewImage)
newMarshal, _ := attributevalue.MarshalMap(newResult)
model := SomeModel{}
err := attributevalue.UnmarshalMap(newMarshal, &model)
Function Code
// UnmarshalStreamImage converts events.DynamoDBAttributeValue to struct
func UnmarshalStreamImage(attribute map[string]events.DynamoDBAttributeValue) (map[string]interface{}, error) {
baseAttrMap := make(map[string]interface{})
for k, v := range attribute {
baseAttrMap[k] = extractVal(v)
}
return baseAttrMap, nil
}
func extractVal(v events.DynamoDBAttributeValue) interface{} {
var val interface{}
switch v.DataType() {
case events.DataTypeString:
val = v.String()
case events.DataTypeNumber:
val, _ = v.Float()
case events.DataTypeBinary:
val = v.Binary()
case events.DataTypeBoolean:
val = v.Boolean()
case events.DataTypeNull:
val = nil
case events.DataTypeList:
list := make([]interface{}, len(v.List()))
for _, item := range v.List() {
list = append(list, extractVal(item))
}
val = list
case events.DataTypeMap:
mapAttr := make(map[string]interface{}, len(v.Map()))
for k, v := range v.Map() {
mapAttr[k] = extractVal(v)
}
val = mapAttr
case events.DataTypeBinarySet:
set := make([][]byte, len(v.BinarySet()))
for _, item := range v.BinarySet() {
set = append(set, item)
}
val = set
case events.DataTypeNumberSet:
set := make([]string, len(v.NumberSet()))
for _, item := range v.NumberSet() {
set = append(set, item)
}
val = set
case events.DataTypeStringSet:
set := make([]string, len(v.StringSet()))
for _, item := range v.StringSet() {
set = append(set, item)
}
val = set
}
return val
}```

Related

Golang map put if absent?

I have a map of the format:
map[string]map[string]int
In this main map, I want to do something like putIfAbsent("key", new HashMap<>() as we have in Java. What is a clean and shorthand way to do it in Go?
You can do:
var val map[string]int
val, exists := m[key]
if !exists {
val = make(map[string]int)
m[key] = val
}
If you don't need the val in the code coming below this:
if _,exists := m[key]; !exists {
m[key]=make(map[string]int)
}
If you don't intend to use the value right away, here you go...
m := make(map[string]map[string]int)
if _, ok := m["unknown"]; !ok {
m["unknown"] = make(map[string]int)
}
Below is a suggestion for improvement:
To keep things clean and easy to understand, you can define your own types. For example, if your data is a mapping of "cities to persons to age", I would do it like this:
type Person map[string]int
type City map[string]Person
m := make(City)
if _, ok := m["Dhaka"]; !ok {
m["Dhaka"] = make(Person)
}
func main() {
var testMap map[int]interface{}
testMap = make(map[int]interface{})
var addMap map[int]string
addMap = make(map[int]string)
addMap[1] = "999"
addMap[2] = "888"
Add(testMap, 111, addMap)
for key, val := range testMap {
fmt.Println(key)
for key2, val2 := range val.(map[int]string) {
fmt.Println(key2, val2)
}
}
}
func Add(_testMap map[int]interface{}, _key int, _val map[int]string) {
_, exist := _testMap[_key] // _ -> value
if exist == false {
//addmap
_testMap[_key] = _val
} else {
//whatever wanna to do
}
}

Using reflect to assign a typed value

I'm working on one of our system applications, specifically in the configuration file handling bits. We currently have 3 different places where a configuration file can be stored, and that can possibly be extended later. What I'm trying to do is simplify the way we need to add a new managed field.
The solution I have so far looks something like this:
package main
import (
"reflect"
"strconv"
"strings"
)
type Datastore interface{}
type MyInt struct {
intVal int
}
func NewMyInt(key string, dv int, db *Datastore) *MyInt {
// Do something here to construct MyInt
return &MyInt{intVal: dv}
}
type Config struct {
myInts map[string]*MyInt
// Tag is of form "<key in DB>:<default value>"
Value1 MyInt "value1_key:12345"
Value2 MyInt "value2_key:54321"
}
func NewConfig(db *Datastore) *Config {
c := &Config{
myInts: make(map[string]*MyInt),
}
cType := reflect.TypeOf(c)
for i := 0; i < cType.NumField(); i++ {
f := cType.Field(i)
if f.Name == "myInts" {
continue
}
tag := string(f.Tag)
fields := strings.Split(tag, ":")
switch f.Type.Name() {
case "myInt":
intVal, _ := strconv.Atoi(fields[1])
val := NewMyInt(fields[0], intVal, db)
c.myInts[fields[0]] = val
// How do I set the i'th field to this newly constructed value?
}
}
return c
}
So far I'm just missing this piece to do the assignment.
For this question, you can try
func NewConfig(db *Datastore) *Config {
c := &Config{
myInts: make(map[string]*MyInt),
}
cType := reflect.TypeOf(c).Elem() // have to use Elem() to get actual value
cValue := reflect.ValueOf(c).Elem()
for i := 0; i < cType.NumField(); i++ {
f := cType.Field(i)
if f.Name == "myInts" {
continue
}
tag := string(f.Tag)
fields := strings.Split(tag, ":")
switch f.Type.Name() {
case "MyInt":
intVal, _ := strconv.Atoi(fields[1])
val := NewMyInt(fields[0], intVal, db)
c.myInts[fields[0]] = val
// How do I set the i'th field to this newly constructed value?
cValue.Field(i).Set(reflect.ValueOf(val).Elem())
}
}
fmt.Println(c.Value1.intVal, c.Value2.intVal)
return c
}

Deserialising aws struct to string mapping

I'm trying to deserialize an AWS struct in go so that I can pass in parameters and output values based on those parameters, for example;
func DisplayResults(conf *config.Configuration,
regionalData []*ec2.DescribeInstancesOutput) {
log.Debug("Displaying results")
log.Debug("Table view [%v]", conf.Display)
for _, rv := range regionalData {
for _, reservation := range rv.Reservations {
for _, instance := range reservation.Instances {
var i map[string]interface{}
json.Unmarshal(instance, i)
}
}
}
}
I've also tried using:
"github.com/fatih/structs"
i := structs.Map(instance)
log.Print("%v", i)
however, I think that I need to somehow derefence the struct because my output looks like this:
RamdiskId:<nil> SriovNetSupport:<nil> VpcId:0xc420321ba0 State:map[Code:0xc42031ea08 Name:0xc420321890] VirtualizationType:0xc420321b90 CapacityReservationId:<nil> ClientToken:0xc420321560 HibernationOptions:map[Configured:0xc42031e96b] IamInstanceProfile:map[Arn:0xc4203215a0 Id:0xc4203215b0] ImageId:0xc4203215c0
I've also tried this:
func DisplayResults(conf *config.Configuration, regionalData []*ec2.DescribeInstancesOutput, parameters []string) {
log.Debug("Displaying results")
log.Debug("Table view [%v]", conf.Display)
for _, rv := range regionalData {
for _, reservation := range rv.Reservations {
for _, instance := range reservation.Instances {
y := deref(instance)
log.Print("%v", y["InstanceId"])
}
}
}
}
func deref(instance *ec2.Instance) ec2.Instance {
var i ec2.Instance
x := &i
*x = *instance
return i
}

Unmarshal map[string]DynamoDBAttributeValue into a struct

I'm trying to set-up an AWS-lambda using aws-sdk-go that is triggered whenever a new user is added to a certain dynamodb table.
Everything is working just fine but I can't find a way to unmarshal a map map[string]DynamoDBAttributeValue like:
{
"name": {
"S" : "John"
},
"residence_address": {
"M": {
"address": {
"S": "some place"
}
}
}
}
To a given struct, for instance, a User struct. Here is shown an example of unsmarhaling a map[string]*dynamodb.AttributeValue into a given interface, but I can't find a way to do the same thing with map[string]DynamoDBAttributeValue even though these types seem to fit the same purposes.
map[string]DynamoDBAttributeValue is returned by a events.DynamoDBEvents from package github.com/aws/aws-lambda-go/events. This is my code:
package handler
import (
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
func HandleDynamoDBRequest(ctx context.Context, e events.DynamoDBEvent) {
for _, record := range e.Records {
if record.EventName == "INSERT" {
// User Struct
var dynamoUser model.DynamoDBUser
// Of course this can't be done for incompatible types
_ := dynamodbattribute.UnmarshalMap(record.Change.NewImage, &dynamoUser)
}
}
}
Of course, I can marshal record.Change.NewImage to JSON and unmarshal it back to a given struct, but then, I would have to manually initialize dynamoUser attributes starting from the latter ones.
Or I could even write a function that parses map[string]DynamoDBAttributeValue to map[string]*dynamodb.AttributeValue like:
func getAttributeValueMapFromDynamoDBStreamRecord(e events.DynamoDBStreamRecord) map[string]*dynamodb.AttributeValue {
image := e.NewImage
m := make(map[string]*dynamodb.AttributeValue)
for k, v := range image {
if v.DataType() == events.DataTypeString {
s := v.String()
m[k] = &dynamodb.AttributeValue{
S : &s,
}
}
if v.DataType() == events.DataTypeBoolean {
b := v.Boolean()
m[k] = &dynamodb.AttributeValue{
BOOL : &b,
}
}
// . . .
if v.DataType() == events.DataTypeMap {
// ?
}
}
return m
}
And then simply use dynamodbattribute.UnmarshalMap, but on events.DataTypeMap it would be quite a tricky process.
Is there a way through which I can unmarshal a DynamoDB record coming from a events.DynamoDBEvent into a struct with a similar method shown for map[string]*dynamodb.AttributeValue?
I tried the function you provided, and I met some problems with events.DataTypeList, so I managed to write the following function that does the trick:
// UnmarshalStreamImage converts events.DynamoDBAttributeValue to struct
func UnmarshalStreamImage(attribute map[string]events.DynamoDBAttributeValue, out interface{}) error {
dbAttrMap := make(map[string]*dynamodb.AttributeValue)
for k, v := range attribute {
var dbAttr dynamodb.AttributeValue
bytes, marshalErr := v.MarshalJSON(); if marshalErr != nil {
return marshalErr
}
json.Unmarshal(bytes, &dbAttr)
dbAttrMap[k] = &dbAttr
}
return dynamodbattribute.UnmarshalMap(dbAttrMap, out)
}
I was frustrated that the type of NewImage from the record wasn't map[string]*dynamodb.AttributeValue so I could use the dynamodbattribute package.
The JSON representation of events.DynamoDBAttributeValue seems to be the same as the JSON represenation of dynamodb.AttributeValue.
So I tried creating my own DynamoDBEvent type and changed the type of OldImage and NewImage, so it would be marshalled into map[string]*dynamodb.AttributeValue instead of map[string]events.DynamoDBAttributeValue
It is a little bit ugly but it works for me.
package main
import (
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
"fmt"
)
func main() {
lambda.Start(lambdaHandler)
}
// changed type of event from: events.DynamoDBEvent to DynamoDBEvent (see below)
func lambdaHandler(event DynamoDBEvent) error {
for _, record := range event.Records {
change := record.Change
newImage := change.NewImage // now of type: map[string]*dynamodb.AttributeValue
var item IdOnly
err := dynamodbattribute.UnmarshalMap(newImage, &item)
if err != nil {
return err
}
fmt.Println(item.Id)
}
return nil
}
type IdOnly struct {
Id string `json:"id"`
}
type DynamoDBEvent struct {
Records []DynamoDBEventRecord `json:"Records"`
}
type DynamoDBEventRecord struct {
AWSRegion string `json:"awsRegion"`
Change DynamoDBStreamRecord `json:"dynamodb"`
EventID string `json:"eventID"`
EventName string `json:"eventName"`
EventSource string `json:"eventSource"`
EventVersion string `json:"eventVersion"`
EventSourceArn string `json:"eventSourceARN"`
UserIdentity *events.DynamoDBUserIdentity `json:"userIdentity,omitempty"`
}
type DynamoDBStreamRecord struct {
ApproximateCreationDateTime events.SecondsEpochTime `json:"ApproximateCreationDateTime,omitempty"`
// changed to map[string]*dynamodb.AttributeValue
Keys map[string]*dynamodb.AttributeValue `json:"Keys,omitempty"`
// changed to map[string]*dynamodb.AttributeValue
NewImage map[string]*dynamodb.AttributeValue `json:"NewImage,omitempty"`
// changed to map[string]*dynamodb.AttributeValue
OldImage map[string]*dynamodb.AttributeValue `json:"OldImage,omitempty"`
SequenceNumber string `json:"SequenceNumber"`
SizeBytes int64 `json:"SizeBytes"`
StreamViewType string `json:"StreamViewType"`
}
I have found the same problem and the solution is to perform a simple conversion of types. This is possible because in the end the type received by lambda events events.DynamoDBAttributeValue and the type used by the SDK V2 of AWS DynamoDB types.AttributeValue are the same. Next I show you the conversion code.
package aws_lambda
import (
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
func UnmarshalDynamoEventsMap(
record map[string]events.DynamoDBAttributeValue, out interface{}) error {
asTypesMap := DynamoDbEventsMapToTypesMap(record)
err := attributevalue.UnmarshalMap(asTypesMap, out)
if err != nil {
return err
}
return nil
}
func DynamoDbEventsMapToTypesMap(
record map[string]events.DynamoDBAttributeValue) map[string]types.AttributeValue {
resultMap := make(map[string]types.AttributeValue)
for key, rec := range record {
resultMap[key] = DynamoDbEventsToTypes(rec)
}
return resultMap
}
// DynamoDbEventsToTypes relates the dynamo event received by AWS Lambda with the data type that is
// used in the Amazon SDK V2 to deal with DynamoDB data.
// This function is necessary because Amazon does not provide any kind of solution to make this
// relationship between types of data.
func DynamoDbEventsToTypes(record events.DynamoDBAttributeValue) types.AttributeValue {
var val types.AttributeValue
switch record.DataType() {
case events.DataTypeBinary:
val = &types.AttributeValueMemberB{
Value: record.Binary(),
}
case events.DataTypeBinarySet:
val = &types.AttributeValueMemberBS{
Value: record.BinarySet(),
}
case events.DataTypeBoolean:
val = &types.AttributeValueMemberBOOL{
Value: record.Boolean(),
}
case events.DataTypeList:
var items []types.AttributeValue
for _, value := range record.List() {
items = append(items, DynamoDbEventsToTypes(value))
}
val = &types.AttributeValueMemberL{
Value: items,
}
case events.DataTypeMap:
items := make(map[string]types.AttributeValue)
for k, v := range record.Map() {
items[k] = DynamoDbEventsToTypes(v)
}
val = &types.AttributeValueMemberM{
Value: items,
}
case events.DataTypeNull:
val = nil
case events.DataTypeNumber:
val = &types.AttributeValueMemberN{
Value: record.Number(),
}
case events.DataTypeNumberSet:
val = &types.AttributeValueMemberNS{
Value: record.NumberSet(),
}
case events.DataTypeString:
val = &types.AttributeValueMemberS{
Value: record.String(),
}
case events.DataTypeStringSet:
val = &types.AttributeValueMemberSS{
Value: record.StringSet(),
}
}
return val
}
There is a package that allows conversion from events.DynamoDBAttributeValue to dynamodb.AttributeValue
https://pkg.go.dev/github.com/aereal/go-dynamodb-attribute-conversions/v2
From there one can unmarshal AttributeValue into struct
func Unmarshal(attribute map[string]events.DynamoDBAttributeValue, out interface{}) error {
av := ddbconversions.AttributeValueMapFrom(attribute)
return attributevalue.UnmarshalMap(av, out)
}

Golang Structs/Interfaces

I'm trying to do something to bring the SQL results dynamically structure, basically I want to run a function by passing the rows and the structure(to get the data type and make one) and return in interface array.
Anyone know how I can do?
I dont want pass the direct "User" struct as param.. that is not dynamically
type User struct{
Id_user int `json:"id_user"`
Name string `json:"name"`
Email string `json:"email"`
Username string `json: "username"`
}
func main() {
var user User
rows, _ := db.Query("SELECT id_user, name, email, username FROM users")
json.NewEncoder(w).Encode(StructRow(user, rows))
}
func StructRow(u interface{}, rows *sql.Rows)[]interface{}{
var data []interface{}
for rows.Next() {
//How i can create a "user" here, dynamically
//for Example
//var user reflect.TypeOf(u)
_ = rows.Scan(StrutForScan(&user)...)
data = append(data, user)
}
return data
}
func StrutForScan(u interface{}) []interface{} {
val := reflect.ValueOf(u).Elem()
v := make([]interface{}, val.NumField())
for i := 0; i < val.NumField(); i++ {
valueField := val.Field(i)
v[i] = valueField.Addr().Interface()
}
return v
}
Changing your function StructRow to
func StructRow(u interface{}, rows *sql.Rows) []interface{} {
var data []interface{}
for rows.Next() {
t := reflect.TypeOf(u)
val := reflect.New(t).Interface()
errScan := rows.Scan(StrutForScan(val)...)
if errScan != nil {
//proper err handling
}
data = append(data, val)
}
return data
}
will fix it. I guess.
For more on reflect package go to: https://golang.org/pkg/reflect/

Resources