Skip to content

Commit

Permalink
feat: struct attributes parser
Browse files Browse the repository at this point in the history
  • Loading branch information
oleoneto committed Oct 13, 2022
1 parent 0c099a1 commit d589af4
Show file tree
Hide file tree
Showing 5 changed files with 741 additions and 0 deletions.
45 changes: 45 additions & 0 deletions structs/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package structs

import (
"errors"
"reflect"
)

// Check if the element is contained within the given collection.
//
// Example:
//
// contains([]string{"hello", "world", "!"}, "world") // -> true
func contains[T comparable](collection []T, element T) bool {
for _, item := range collection {
if item == element {
return true
}
}

return false
}

// MARK: - Reflection Helpers

func pointerElement(rv reflect.Value) (reflect.Value, error) {
el := rv

for el.Kind() == reflect.Pointer {
if el.IsNil() {
return el, errors.New("nil pointer")
}

el = el.Elem()
}

return el, nil
}

func stringPointer(v string) *string {
return &v
}

func boolPointer(v bool) *bool {
return &v
}
44 changes: 44 additions & 0 deletions structs/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package structs

import (
"reflect"
"testing"
)

// MARK: Collection Helpers

func Test_Contains(t *testing.T) {
collection := []string{"something", "else", "any", "thing"}

key := "any"
if !contains(collection, key) {
t.Errorf(`expected %v to be in collection`, key)
}

keys := []string{"test", "art", "think"}
for _, key := range keys {
ok := contains(collection, key)
if ok {
t.Errorf(`expected %v to not be in collection`, key)
}
}
}

// MARK: Reflection Helpers

func Test_PointerElement(t *testing.T) {
var value *string = stringPointer("something")
_, err := pointerElement(reflect.ValueOf(value))

if err != nil {
t.Errorf(`expected error to be nil, but got %v`, err)
}
}
func Test_PointerElement_WhenNil(t *testing.T) {
var value *string
_, err := pointerElement(reflect.ValueOf(value))

if err == nil {
t.Errorf(`expected an error but got nil`)
}
}
208 changes: 208 additions & 0 deletions structs/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package structs

import (
"reflect"
"strings"

"github.com/google/uuid"
)

// Fetches all the fields of the given struct instance and returns a flattened list with all of its attributes.
//
// Params:
//
// - filterTags
// when set, any fields not containing at least one of these tags
// will be ignored. An empty list allows all fields to be included.
// - ignoredFields
// when set, any fields contained in this list will be ignored.
// Note that the name of the field should be the one defined in the struct.
//
// Usage:
//
// Imagine you have the struct:
// type Person struct {
// Name string `json:"name"`
// Emails []string `json:"emails"`
// }
//
// and create the following instance:
// person := Person{Name: "Leonardo Ribeiro", Emails: []string{"[email protected]", "[email protected]"}}
//
// `GetAttributes(person, ...)` would return a slice containing the following elements:
//
// name
// -> Value: "Leonardo Ribeiro"
// emails
// -> Value: ["[email protected]", "[email protected]"]
// -> Children: [emails[0], emails[1]]
// emails[0]
// -> Value: "[email protected]"
// -> Parents: [emails]
// emails[1]
// -> Value: "[email protected]"
// -> Parents: [emails]
//
// Each returned attribute will expose its underlying value as well as
// the definitions for its field type as found in the parent struct type.
func GetAttributes(entity reflect.Value, filterTags []string, ignoredFields ...string) (attributes []StructAttribute) {
currentIndex := 0
parents := []StructAttribute{}

return getAttributes(entity, parents, filterTags, ignoredFields, currentIndex)
}

// Get the first value of the `json` tag.
//
// This is equivalent to calling:
// GetTagValue(sf, "json")
func GetJSONTagValue(sf reflect.StructField) string {
return GetTagValue(sf, "json")
}

// Get the first value of the given tag.
//
// Usage:
//
// Imagine you have the struct:
// type Person struct {
// Name string `json:"name,omitempty" orm:"pk=name,noupdate,required,pk"`
// Emails []string `json:"emails"`
// }
//
// You can obtain the `orm` tag the following way:
// GetTagValue(name_sf, "orm") // -> "pk=name"
func GetTagValue(sf reflect.StructField, tagName string) string {
name := sf.Name

// Attribute name should come from json tag
tag := strings.Split(sf.Tag.Get(tagName), ",")

if len(tag) != 0 && tag[0] != "" {
name = tag[0]
}

return name
}

// Get the full value of the given tag.
//
// Usage:
//
// Imagine you have the struct:
// type Person struct {
// Name string `json:"name,omitempty" orm:"pk=name,noupdate,required,pk"`
// Emails []string `json:"emails"`
// }
//
// You can obtain the `orm` tag the following way:
// GetTagValues(name_sf, "orm") // -> "pk=name,noupdate,required,pk"
func GetTagValues(sf reflect.StructField, tagName string) []string {
r, exists := sf.Tag.Lookup(tagName)

if exists {
return strings.Split(r, ",")
}

return []string{}
}

// Fetches all the fields of the given struct.
func getAttributes(rv reflect.Value, parents []StructAttribute, filterTags, ignoredFields []string, currentIndex int) (attributes []StructAttribute) {
if rv.Kind() == reflect.Pointer {
rv, _ = pointerElement(rv)
}

if rv.Kind() != reflect.Struct {
return attributes
}

for position := 0; position < rv.NumField(); position++ {
// Concrete value type of the field at this position
value := rv.Field(position)
value, _ = pointerElement(value)

// Struct field definition
rsf := rv.Type().Field(position)

sa := StructAttribute{
Value: value,
Field: rsf,
Parents: parents,
ListPosition: currentIndex,
}

// Do not include an anonymous field at the top level.
// Only include its inner fields.
if sa.Field.Anonymous {
anonValues := getAttributes(value, parents, filterTags, ignoredFields, currentIndex)
sa.Children = append(sa.Children, anonValues...)
attributes = append(attributes, anonValues...)
continue
}

shouldBeIncluded := len(filterTags) == 0
for _, tag := range filterTags {
_, shouldBeIncluded = sa.Field.Tag.Lookup(tag)
}

if !shouldBeIncluded || contains(ignoredFields, rsf.Name) {
continue
}

// Save field
attributes = append(attributes, sa)

// Check if the field needs further processing.
switch value.Kind() {
case reflect.Slice, reflect.Array:
isListOfPrimitives := false
newParents := append(parents, sa)

if value.Len() > 0 {
containsStructs := value.Index(0).Kind() == reflect.Struct

// Google's UUID is a special case. Should not be considered a primitive type.
isGoogleUUID := value.Type() == reflect.TypeOf(uuid.New())

// Primitive types as in int, string, bool, etc
isListOfPrimitives = !containsStructs && !isGoogleUUID
}

// Process each element in slice/array
for l := 0; l < value.Len(); l++ {
el := value.Index(l)

if isListOfPrimitives {
child := StructAttribute{
Value: el,
Parents: newParents,
ListPosition: l,
isPrimitive: true,
}

// Copy information from parent StructField
child.Field = reflect.StructField{
Type: el.Type(),
Name: child.FullName(),
Tag: sa.Field.Tag,
PkgPath: sa.Field.PkgPath,
}

attributes[len(attributes)-1].Children = append(sa.Children, child)
attributes = append(attributes, child)
continue
}

nestedValues := getAttributes(el, newParents, filterTags, ignoredFields, l)
if len(attributes) != 0 {
attributes[len(attributes)-1].Children = append(sa.Children, nestedValues...)
}

attributes = append(attributes, nestedValues...)
}
}
}

return attributes
}
Loading

0 comments on commit d589af4

Please sign in to comment.