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’s arrayValue, 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.

Catégorisé: