Managing Firestore events in Golang can be quite challenging, especially considering the flexible data structures of Firestore and the rigid type system of Golang. Additionally, there doesn’t seem to be an existing library that readily transforms Firestore Events into a Golang struct. Many developers often struggle with converting Firestore events, usually in JSON, into Golang structs. In this article, we’ll break down how to make this process easier.
TLDR;
Here is the full code :
import (
"fmt"
"reflect" "strconv" "strings" "time")
type FirestoreString struct {
StringValue string `json:"stringValue"`
}
type FirestoreArrayValue struct {
Values []FirestoreString `json:"values"`
}
type FirestoreArray struct {
ArrayValue FirestoreArrayValue `json:"arrayValue"`
}
type FirestoreMap struct {
Fields map[string]interface{} `json:"fields"`
}
type FirestoreFields struct {
Data map[string]interface{} `json:"fields"`
Name string `json:"name"`
}
func (f FirestoreFields) Equals(other FirestoreFields) bool {
// Compare the Name fields
if f.Name != other.Name {
return false
}
// Compare the Data fields using reflect.DeepEqual
return reflect.DeepEqual(f.Data, other.Data)
}
type FirestoreEvent struct {
OldValue FirestoreFields `json:"oldValue" `
Value FirestoreFields `json:"value" `
}
func (ev FirestoreFields) ValueToStruct(data interface{}) error {
// Ensure data is a pointer
if reflect.TypeOf(data).Kind() != reflect.Ptr {
return fmt.Errorf("data must be a pointer to a struct")
}
// Get the underlying type (dereferenced type) of the pointer
typeValue := reflect.TypeOf(data).Elem()
// Get the actual value the pointer points to
elemValue := reflect.ValueOf(data).Elem()
mapValue := ev.Data
err := setValue(mapValue, typeValue, elemValue)
return err
}
func setValue(mapValue map[string]interface{}, typeValue reflect.Type, elemValue reflect.Value) error {
for key, val := range mapValue {
upperCase := strings.ToUpper(key[0:1]) + key[1:]
title := strings.ReplaceAll(upperCase, " ", "")
reflectType, isValid := typeValue.FieldByName(title)
reflectValue := elemValue.FieldByName(title)
if !isValid && !reflectValue.IsValid() {
continue
}
//GET FIRST KEY
var firstKey string
for k := range val.(map[string]interface{}) {
firstKey = k
break
}
raw, ok := mapValue[key].(map[string]interface{})
if !ok {
continue
}
switch firstKey {
case "stringValue":
extractString, _ := ExtractString(raw)
if reflectValue.Kind() == reflect.Ptr {
handlePtr(extractString, reflectValue)
} else {
reflectValue.SetString(extractString)
}
case "arrayValue":
if arrayData, ok := raw["arrayValue"]; ok {
arr, err := ExtractArray(arrayData)
if err != nil {
return fmt.Errorf("Error on extract array: %v", err)
}
if reflectValue.Kind() == reflect.Ptr {
handlePtr(arr, reflectValue)
} else {
reflectValue.Set(reflect.ValueOf(arr))
}
}
case "integerValue":
if reflectValue.Kind() == reflect.Ptr {
extractInt, err := ExtractInt(raw, reflectType.Type.Elem().Kind())
if err != nil {
return fmt.Errorf("Error on extract int: %v", err)
}
handlePtr(extractInt, reflectValue)
} else {
extractInt, err := ExtractInt(raw, reflectValue.Kind())
if err != nil {
return fmt.Errorf("Error on extract int: %v", err)
}
reflectValue.Set(reflect.ValueOf(extractInt))
}
case "doubleValue":
if reflectValue.Kind() == reflect.Ptr {
extractDouble, err := ExtractDouble(raw, reflectType.Type.Elem().Kind())
if err != nil {
return fmt.Errorf("Error on extract double: %v", err)
}
handlePtr(extractDouble, reflectValue)
} else {
extractDouble, err := ExtractDouble(raw, reflectValue.Kind())
if err != nil {
return fmt.Errorf("Error on extract double: %v", err)
}
reflectValue.Set(reflect.ValueOf(extractDouble))
}
case "booleanValue":
extractBool, err := ExtractBool(raw)
if err != nil {
return fmt.Errorf("Error on extract bool: %v", err)
}
if reflectValue.Kind() == reflect.Ptr {
handlePtr(extractBool, reflectValue)
} else {
reflectValue.SetBool(extractBool)
}
case "timestampValue":
extractTime, err := ExtractTimestamp(raw)
if err != nil {
return fmt.Errorf("Error on extract timestamp: %v", err)
}
if reflectValue.Kind() == reflect.Ptr {
handlePtr(extractTime, reflectValue)
} else {
reflectValue.Set(reflect.ValueOf(extractTime))
}
case "mapValue":
if nestedData, ok := raw[firstKey].(map[string]interface{}); ok {
if reflectValue.Kind() == reflect.Ptr {
ptr := reflect.New(reflectType.Type.Elem()).Elem()
err := setValue(nestedData["fields"].(map[string]interface{}), reflectType.Type.Elem(), ptr)
if err != nil {
return err
}
reflectValue.Set(ptr.Addr())
} else {
err := setValue(nestedData["fields"].(map[string]interface{}), reflectType.Type, reflectValue)
if err != nil {
return err
}
}
}
}
}
return nil
}
func handlePtr(extractData interface{}, reflectValue reflect.Value) {
ptr := reflect.New(reflect.TypeOf(extractData)).Elem()
ptr.Set(reflect.ValueOf(extractData))
reflectValue.Set(ptr.Addr())
}
func ExtractString(raw interface{}) (string, error) {
strMap, ok := raw.(map[string]interface{})
if !ok {
return "", fmt.Errorf("expected map for stringValue, got %T", raw)
}
str, ok := strMap["stringValue"].(string)
if !ok {
return "", fmt.Errorf("expected string for stringValue, got %T", strMap["stringValue"])
}
return str, nil
}
func ExtractArray(raw interface{}) ([]string, error) {
arrayMap, ok := raw.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("expected map for arrayValue, got %T", raw)
}
rawValues, ok := arrayMap["values"].([]interface{})
if !ok {
return nil, fmt.Errorf("expected []interface{} for arrayValue.values, got %T", arrayMap["values"])
}
var strs []string
for _, rawValue := range rawValues {
str, err := ExtractString(rawValue)
if err != nil {
return nil, err
}
strs = append(strs, str)
}
return strs, nil
}
func ExtractBool(raw interface{}) (bool, error) {
boolMap, ok := raw.(map[string]interface{})
if !ok {
return false, fmt.Errorf("expected map for booleanValue, got %T", raw)
}
boolVal, ok := boolMap["booleanValue"].(string)
if !ok {
return false, fmt.Errorf("expected bool for booleanValue, got %T", boolMap["booleanValue"])
}
return strconv.ParseBool(boolVal)
}
func ExtractInt(raw interface{}, kind reflect.Kind) (interface{}, error) {
intMap, ok := raw.(map[string]interface{})
// Try to get it as a string
stringVal, ok := intMap["integerValue"].(string)
if ok {
var intVal interface{}
var err error
switch kind {
case reflect.Int:
intVal, err = strconv.Atoi(stringVal)
case reflect.Int8:
intVal, err = strconv.ParseInt(stringVal, 10, 8)
intVal = int8(intVal.(int64))
case reflect.Int16:
intVal, err = strconv.ParseInt(stringVal, 10, 16)
intVal = int16(intVal.(int64))
case reflect.Int32:
intVal, err = strconv.ParseInt(stringVal, 10, 32)
intVal = int32(intVal.(int64))
case reflect.Int64:
intVal, err = strconv.ParseInt(stringVal, 10, 64)
default:
return nil, fmt.Errorf("unexpected kind for integerValue: %v", kind)
}
if err != nil {
return 0, fmt.Errorf("failed to parse string as int: %v", err)
}
return intVal, nil
}
return 0, fmt.Errorf("unexpected type for integerValue: %T", intMap["integerValue"])
}
func ExtractDouble(raw interface{}, kind reflect.Kind) (interface{}, error) {
doubleValue, ok := raw.(map[string]interface{})
// Try to get it as a string
stringVal, ok := doubleValue["doubleValue"].(string)
if ok {
var doubleVal interface{}
var err error
switch kind {
case reflect.Float32:
doubleVal, err = strconv.ParseFloat(stringVal, 32)
doubleVal = float32(doubleVal.(float64))
case reflect.Float64:
doubleVal, err = strconv.ParseFloat(stringVal, 64)
default:
return nil, fmt.Errorf("unexpected kind for integerValue: %v", kind)
}
if err != nil {
return 0, fmt.Errorf("failed to parse string as float: %v", err)
}
return doubleVal, nil
}
return 0, fmt.Errorf("unexpected type for integerValue: %T", doubleValue["integerValue"])
}
func ExtractTimestamp(raw interface{}) (time.Time, error) {
tsMap, ok := raw.(map[string]interface{})
if !ok {
return time.Time{}, fmt.Errorf("expected map for timestampValue, got %T", raw)
}
tsStr, ok := tsMap["timestampValue"].(string)
if !ok {
return time.Time{}, fmt.Errorf("expected string for timestampValue, got %T", tsMap["timestampValue"])
}
parsedTime, err := time.Parse(time.RFC3339Nano, tsStr)
if err != nil {
return time.Time{}, fmt.Errorf("failed to parse timestamp: %v", err)
}
return parsedTime, nil
}
Defining Firestore Event Structures
Before we can begin the transformation, we need to define the basic Firestore data structures in Golang. Here they are:
type FirestoreString struct {
StringValue string `json:"stringValue"`
}
type FirestoreArrayValue struct {
Values []FirestoreString `json:"values"`
}
type FirestoreArray struct {
ArrayValue FirestoreArrayValue `json:"arrayValue"`
}
type FirestoreMap struct {
Fields map[string]interface{} `json:"fields"`
}
type FirestoreFields struct {
Data map[string]interface{} `json:"fields"`
Name string `json:"name"`
}
type FirestoreEvent struct {
OldValue FirestoreFields `json:"oldValue" `
Value FirestoreFields `json:"value" `
}
This is where the magic happens. We utilize Go’s reflection package to dynamically set values in our target struct.
Transforming Firestore Events into Go Structs: A Deep Dive
Navigating the dynamic world of Firestore events and mapping them to the statically-typed universe of Go can be quite the challenge.
Imagine getting data from Firestore. The format is versatile – sometimes it’s a string, at other times an array, a map, or any mix of other types. Our mission? Seamlessly transform this data into a Go struct, ready for further processing or manipulation.
Enter Go’s reflection capabilities. Our approach harnesses the power of reflection to dynamically inspect and populate Go structs. This means, irrespective of your struct’s design, the code’s got you covered.
Here’s the breakdown:
- String Fields: When your struct expects a string, the code scouts for the
stringValue
in the Firestore event, extracts it, and slots it right into your struct. - Arrays: Got an array? The
ExtractArray
function dives into the Firestore’sarrayValue
, fishes out the elements, and neatly arranges them in your Go struct. - Booleans, Integers & More: Each data type has its dedicated extraction function, like
ExtractBool
for booleans. These ensure that the data from Firestore is accurately reflected in the Go struct, regardless of the type.
The heart of this transformation beats in the FirestoreEventToStruct
function. Feed it your Firestore event and watch it orchestrate the whole conversion, leaving you with a Go struct filled with the data you need.
func (f FirestoreFields) Equals(other FirestoreFields) bool {
// Compare the Name fields
if f.Name != other.Name {
return false
}
// Compare the Data fields using reflect.DeepEqual
return reflect.DeepEqual(f.Data, other.Data)
}
func (ev FirestoreFields) ValueToStruct(data interface{}) error {
// Ensure data is a pointer
if reflect.TypeOf(data).Kind() != reflect.Ptr {
return fmt.Errorf("data must be a pointer to a struct")
}
// Get the underlying type (dereferenced type) of the pointer
typeValue := reflect.TypeOf(data).Elem()
// Get the actual value the pointer points to
elemValue := reflect.ValueOf(data).Elem()
mapValue := ev.Data
err := setValue(mapValue, typeValue, elemValue)
return err
}
func setValue(mapValue map[string]interface{}, typeValue reflect.Type, elemValue reflect.Value) error {
for key, val := range mapValue {
upperCase := strings.ToUpper(key[0:1]) + key[1:]
title := strings.ReplaceAll(upperCase, " ", "")
reflectType, isValid := typeValue.FieldByName(title)
reflectValue := elemValue.FieldByName(title)
if !isValid && !reflectValue.IsValid() {
continue
}
//GET FIRST KEY
var firstKey string
for k := range val.(map[string]interface{}) {
firstKey = k
break
}
raw, ok := mapValue[key].(map[string]interface{})
if !ok {
continue
}
switch firstKey {
case "stringValue":
extractString, _ := ExtractString(raw)
if reflectValue.Kind() == reflect.Ptr {
handlePtr(extractString, reflectValue)
} else {
reflectValue.SetString(extractString)
}
case "arrayValue":
if arrayData, ok := raw["arrayValue"]; ok {
arr, err := ExtractArray(arrayData)
if err != nil {
return fmt.Errorf("Error on extract array: %v", err)
}
if reflectValue.Kind() == reflect.Ptr {
handlePtr(arr, reflectValue)
} else {
reflectValue.Set(reflect.ValueOf(arr))
}
}
case "integerValue":
if reflectValue.Kind() == reflect.Ptr {
extractInt, err := ExtractInt(raw, reflectType.Type.Elem().Kind())
if err != nil {
return fmt.Errorf("Error on extract int: %v", err)
}
handlePtr(extractInt, reflectValue)
} else {
extractInt, err := ExtractInt(raw, reflectValue.Kind())
if err != nil {
return fmt.Errorf("Error on extract int: %v", err)
}
reflectValue.Set(reflect.ValueOf(extractInt))
}
case "doubleValue":
if reflectValue.Kind() == reflect.Ptr {
extractDouble, err := ExtractDouble(raw, reflectType.Type.Elem().Kind())
if err != nil {
return fmt.Errorf("Error on extract double: %v", err)
}
handlePtr(extractDouble, reflectValue)
} else {
extractDouble, err := ExtractDouble(raw, reflectValue.Kind())
if err != nil {
return fmt.Errorf("Error on extract double: %v", err)
}
reflectValue.Set(reflect.ValueOf(extractDouble))
}
case "booleanValue":
extractBool, err := ExtractBool(raw)
if err != nil {
return fmt.Errorf("Error on extract bool: %v", err)
}
if reflectValue.Kind() == reflect.Ptr {
handlePtr(extractBool, reflectValue)
} else {
reflectValue.SetBool(extractBool)
}
case "timestampValue":
extractTime, err := ExtractTimestamp(raw)
if err != nil {
return fmt.Errorf("Error on extract timestamp: %v", err)
}
if reflectValue.Kind() == reflect.Ptr {
handlePtr(extractTime, reflectValue)
} else {
reflectValue.Set(reflect.ValueOf(extractTime))
}
case "mapValue":
if nestedData, ok := raw[firstKey].(map[string]interface{}); ok {
if reflectValue.Kind() == reflect.Ptr {
ptr := reflect.New(reflectType.Type.Elem()).Elem()
err := setValue(nestedData["fields"].(map[string]interface{}), reflectType.Type.Elem(), ptr)
if err != nil {
return err
}
reflectValue.Set(ptr.Addr())
} else {
err := setValue(nestedData["fields"].(map[string]interface{}), reflectType.Type, reflectValue)
if err != nil {
return err
}
}
}
}
}
return nil
}
func handlePtr(extractData interface{}, reflectValue reflect.Value) {
ptr := reflect.New(reflect.TypeOf(extractData)).Elem()
ptr.Set(reflect.ValueOf(extractData))
reflectValue.Set(ptr.Addr())
}
func ExtractString(raw interface{}) (string, error) {
strMap, ok := raw.(map[string]interface{})
if !ok {
return "", fmt.Errorf("expected map for stringValue, got %T", raw)
}
str, ok := strMap["stringValue"].(string)
if !ok {
return "", fmt.Errorf("expected string for stringValue, got %T", strMap["stringValue"])
}
return str, nil
}
func ExtractArray(raw interface{}) ([]string, error) {
arrayMap, ok := raw.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("expected map for arrayValue, got %T", raw)
}
rawValues, ok := arrayMap["values"].([]interface{})
if !ok {
return nil, fmt.Errorf("expected []interface{} for arrayValue.values, got %T", arrayMap["values"])
}
var strs []string
for _, rawValue := range rawValues {
str, err := ExtractString(rawValue)
if err != nil {
return nil, err
}
strs = append(strs, str)
}
return strs, nil
}
func ExtractBool(raw interface{}) (bool, error) {
boolMap, ok := raw.(map[string]interface{})
if !ok {
return false, fmt.Errorf("expected map for booleanValue, got %T", raw)
}
boolVal, ok := boolMap["booleanValue"].(string)
if !ok {
return false, fmt.Errorf("expected bool for booleanValue, got %T", boolMap["booleanValue"])
}
return strconv.ParseBool(boolVal)
}
func ExtractInt(raw interface{}, kind reflect.Kind) (interface{}, error) {
intMap, ok := raw.(map[string]interface{})
// Try to get it as a string
stringVal, ok := intMap["integerValue"].(string)
if ok {
var intVal interface{}
var err error
switch kind {
case reflect.Int:
intVal, err = strconv.Atoi(stringVal)
case reflect.Int8:
intVal, err = strconv.ParseInt(stringVal, 10, 8)
intVal = int8(intVal.(int64))
case reflect.Int16:
intVal, err = strconv.ParseInt(stringVal, 10, 16)
intVal = int16(intVal.(int64))
case reflect.Int32:
intVal, err = strconv.ParseInt(stringVal, 10, 32)
intVal = int32(intVal.(int64))
case reflect.Int64:
intVal, err = strconv.ParseInt(stringVal, 10, 64)
default:
return nil, fmt.Errorf("unexpected kind for integerValue: %v", kind)
}
if err != nil {
return 0, fmt.Errorf("failed to parse string as int: %v", err)
}
return intVal, nil
}
return 0, fmt.Errorf("unexpected type for integerValue: %T", intMap["integerValue"])
}
func ExtractDouble(raw interface{}, kind reflect.Kind) (interface{}, error) {
doubleValue, ok := raw.(map[string]interface{})
// Try to get it as a string
stringVal, ok := doubleValue["doubleValue"].(string)
if ok {
var doubleVal interface{}
var err error
switch kind {
case reflect.Float32:
doubleVal, err = strconv.ParseFloat(stringVal, 32)
doubleVal = float32(doubleVal.(float64))
case reflect.Float64:
doubleVal, err = strconv.ParseFloat(stringVal, 64)
default:
return nil, fmt.Errorf("unexpected kind for integerValue: %v", kind)
}
if err != nil {
return 0, fmt.Errorf("failed to parse string as float: %v", err)
}
return doubleVal, nil
}
return 0, fmt.Errorf("unexpected type for integerValue: %T", doubleValue["integerValue"])
}
func ExtractTimestamp(raw interface{}) (time.Time, error) {
tsMap, ok := raw.(map[string]interface{})
if !ok {
return time.Time{}, fmt.Errorf("expected map for timestampValue, got %T", raw)
}
tsStr, ok := tsMap["timestampValue"].(string)
if !ok {
return time.Time{}, fmt.Errorf("expected string for timestampValue, got %T", tsMap["timestampValue"])
}
parsedTime, err := time.Parse(time.RFC3339Nano, tsStr)
if err != nil {
return time.Time{}, fmt.Errorf("failed to parse timestamp: %v", err)
}
return parsedTime, nil
}
Usage
var event FirestoreEvent //YOU FIRESTORE EVENT
err = json.Unmarshal(data, &event)
if err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
value := event.Value
fmt.Printf("value: %+v\n", value)
err = value.ValueToStruct(&generatedWorkout) // TRANSFORMATION TO STRUCT
if err != nil {
fmt.Printf("Error: %+v\n", err)
}
fmt.Printf("struct: %+v\n", generatedWorkout)
Conclusion
Transforming Firestore events into Golang structs is essential for efficient data processing. By utilizing Go’s reflection capabilities and a well-defined structure, we can achieve this transformation seamlessly. With the code provided above, you now have the tools necessary to handle Firestore events in your Golang application with ease.