From 58d1694069cf93b9572f0b7c2b9d0b0efa472597 Mon Sep 17 00:00:00 2001 From: Christopher Puschmann Date: Sun, 11 May 2025 13:33:53 +0200 Subject: [PATCH 1/2] feat: add initial support for custom entry unmarshaler --- v3/search.go | 56 ++++++++++++++++++++++++++------------------ v3/search_test.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 23 deletions(-) diff --git a/v3/search.go b/v3/search.go index 62be1054..fe5c2cd6 100644 --- a/v3/search.go +++ b/v3/search.go @@ -246,38 +246,19 @@ func readTag(f reflect.StructField) (string, bool) { // // ... // } func (e *Entry) Unmarshal(i interface{}) (err error) { - // Make sure it's a ptr - if vo := reflect.ValueOf(i).Kind(); vo != reflect.Ptr { - return fmt.Errorf("ldap: cannot use %s, expected pointer to a struct", vo) - } - - sv, st := reflect.ValueOf(i).Elem(), reflect.TypeOf(i).Elem() - // Make sure it's pointing to a struct - if sv.Kind() != reflect.Struct { - return fmt.Errorf("ldap: expected pointer to a struct, got %s", sv.Kind()) - } - - for n := 0; n < st.NumField(); n++ { - // Holds struct field value and type - fv, ft := sv.Field(n), st.Field(n) - - // skip unexported fields - if ft.PkgPath != "" { - continue - } - + return e.UnmarshalFunc(i, func(entry *Entry, ft reflect.StructField, fv reflect.Value) error { // omitempty can be safely discarded, as it's not needed when unmarshalling fieldTag, _ := readTag(ft) // Fill the field with the distinguishedName if the tag key is `dn` if fieldTag == "dn" { fv.SetString(e.DN) - continue + return nil } values := e.GetAttributeValues(fieldTag) if len(values) == 0 { - continue + return nil } switch fv.Interface().(type) { @@ -320,8 +301,37 @@ func (e *Entry) Unmarshal(i interface{}) (err error) { default: return fmt.Errorf("ldap: expected field to be of type string, *string, []string, int, int64, []byte, *DN, []*DN or time.Time, got %v", ft.Type) } + return nil + }) +} + +func (e *Entry) UnmarshalFunc(i interface{}, + fn func(entry *Entry, fieldType reflect.StructField, fieldValue reflect.Value) error) error { + // Make sure it's a ptr + if vo := reflect.ValueOf(i).Kind(); vo != reflect.Ptr { + return fmt.Errorf("ldap: cannot use %s, expected pointer to a struct", vo) + } + + sv, st := reflect.ValueOf(i).Elem(), reflect.TypeOf(i).Elem() + // Make sure it's pointing to a struct + if sv.Kind() != reflect.Struct { + return fmt.Errorf("ldap: expected pointer to a struct, got %s", sv.Kind()) + } + + for n := 0; n < st.NumField(); n++ { + fv, ft := sv.Field(n), st.Field(n) + + // skip unexported fields + if ft.PkgPath != "" { + continue + } + + if err := fn(e, ft, fv); err != nil { + return err + } } - return + + return nil } // NewEntryAttribute returns a new EntryAttribute with the desired key-value pair diff --git a/v3/search_test.go b/v3/search_test.go index 029029e0..381284ef 100644 --- a/v3/search_test.go +++ b/v3/search_test.go @@ -1,6 +1,7 @@ package ldap import ( + "fmt" "reflect" "testing" "time" @@ -219,3 +220,61 @@ func TestEntry_Unmarshal(t *testing.T) { assert.Equal(t, expect, result) }) } + +func TestEntry_UnmarshalFunc(t *testing.T) { + conn, err := DialURL(ldapServer) + if err != nil { + t.Fatalf("Failed to connect: %s\n", err) + } + defer conn.Close() + + searchResult, err := conn.Search(&SearchRequest{ + BaseDN: baseDN, + Scope: ScopeWholeSubtree, + Filter: "(cn=cis-fac)", + Attributes: []string{"cn", "objectClass"}, + }) + if err != nil { + t.Fatalf("Failed to search: %s\n", err) + } + + type user struct { + ObjectClass string `custom_tag:"objectClass"` + CN string `custom_tag:"cn"` + } + + t.Run("expect custom unmarshal function to be successfull", func(t *testing.T) { + for _, entry := range searchResult.Entries { + var u user + if err := entry.UnmarshalFunc(&u, func(entry *Entry, fieldType reflect.StructField, fieldValue reflect.Value) error { + tagData, ok := fieldType.Tag.Lookup("custom_tag") + if !ok { + return nil + } + + value := entry.GetAttributeValue(tagData) + // log.Printf("Marshaling field %s with tag %s and value '%s'", fieldType.Name, tagData, value) + fieldValue.SetString(value) + return nil + }); err != nil { + t.Errorf("Failed to unmarshal entry: %s\n", err) + } + + if u.CN != entry.GetAttributeValue("cn") { + t.Errorf("UnmarshalFunc did not set the field correctly. Expected: %s, got: %s", entry.GetAttributeValue("cn"), u.CN) + } + } + }) + + t.Run("expect an error within the custom unmarshal function", func(t *testing.T) { + for _, entry := range searchResult.Entries { + var u user + err := entry.UnmarshalFunc(&u, func(entry *Entry, fieldType reflect.StructField, fieldValue reflect.Value) error { + return fmt.Errorf("error from custom unmarshal func on field: %s", fieldType.Name) + }) + if err == nil { + t.Errorf("UnmarshalFunc should have returned an error") + } + } + }) +} From d9fc1573fcd8ee58a8fa00d7a5253c9948d93e00 Mon Sep 17 00:00:00 2001 From: Christopher Puschmann Date: Tue, 13 May 2025 12:10:02 +0200 Subject: [PATCH 2/2] Add function comment --- v3/search.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/v3/search.go b/v3/search.go index fe5c2cd6..72dbd9df 100644 --- a/v3/search.go +++ b/v3/search.go @@ -305,6 +305,8 @@ func (e *Entry) Unmarshal(i interface{}) (err error) { }) } +// UnmarshalFunc allows you to define a custom unmarshaler to parse an Entry values. +// A custom unmarshaler can be found in the Unmarshal function or in the test files. func (e *Entry) UnmarshalFunc(i interface{}, fn func(entry *Entry, fieldType reflect.StructField, fieldValue reflect.Value) error) error { // Make sure it's a ptr