diff --git a/structs/structs.go b/structs/structs.go index 95fd645..237fd7c 100644 --- a/structs/structs.go +++ b/structs/structs.go @@ -4,6 +4,8 @@ import ( "errors" "reflect" "strings" + + mapsutil "github.com/projectdiscovery/utils/maps" ) // CallbackFunc on the struct field @@ -100,8 +102,8 @@ func FilterStruct[T any](input T, includeFields, excludeFields []string) (T, err return filteredStruct.Interface().(T), nil } -func FilterStructToMap[T any](input T, includeFields, excludeFields []string) (map[string]any, error) { - resultMap := make(map[string]any) +func FilterStructToMap[T any](input T, includeFields, excludeFields []string) (*mapsutil.OrderedMap[string, any], error) { + resultMap := mapsutil.NewOrderedMap[string, any]() walker := func(field reflect.StructField, value reflect.Value) { jsonTag := field.Tag.Get("json") @@ -116,14 +118,14 @@ func FilterStructToMap[T any](input T, includeFields, excludeFields []string) (m return } - resultMap[jsonKey] = fieldValue + resultMap.Set(jsonKey, fieldValue) } if err := walkFilteredFields(input, includeFields, excludeFields, walker); err != nil { return nil, err } - return resultMap, nil + return &resultMap, nil } // GetStructFields returns all the top-level field names from the given struct. diff --git a/structs/structs_test.go b/structs/structs_test.go index 9e1c9b5..1f514a3 100644 --- a/structs/structs_test.go +++ b/structs/structs_test.go @@ -3,6 +3,8 @@ package structs import ( "reflect" "testing" + + mapsutil "github.com/projectdiscovery/utils/maps" ) type TestStruct struct { @@ -103,7 +105,7 @@ func TestFilterStructToMap(t *testing.T) { input any includeFields []string excludeFields []string - want map[string]any + want *mapsutil.OrderedMap[string, any] wantErr bool }{ { @@ -111,12 +113,14 @@ func TestFilterStructToMap(t *testing.T) { input: s, includeFields: nil, excludeFields: nil, - want: map[string]any{ - "name": "John", - "age": 30, - "is_active": true, - "address": "New York", - }, + want: func() *mapsutil.OrderedMap[string, any] { + om := mapsutil.NewOrderedMap[string, any]() + om.Set("name", "John") + om.Set("age", 30) + om.Set("is_active", true) + om.Set("address", "New York") + return &om + }(), wantErr: false, }, { @@ -124,10 +128,12 @@ func TestFilterStructToMap(t *testing.T) { input: s, includeFields: []string{"Name", "Address"}, excludeFields: []string{}, - want: map[string]any{ - "name": "John", - "address": "New York", - }, + want: func() *mapsutil.OrderedMap[string, any] { + om := mapsutil.NewOrderedMap[string, any]() + om.Set("name", "John") + om.Set("address", "New York") + return &om + }(), wantErr: false, }, { @@ -135,10 +141,12 @@ func TestFilterStructToMap(t *testing.T) { input: s, includeFields: []string{}, excludeFields: []string{"Address", "IsActive"}, - want: map[string]any{ - "name": "John", - "age": 30, - }, + want: func() *mapsutil.OrderedMap[string, any] { + om := mapsutil.NewOrderedMap[string, any]() + om.Set("name", "John") + om.Set("age", 30) + return &om + }(), wantErr: false, }, { @@ -146,10 +154,12 @@ func TestFilterStructToMap(t *testing.T) { input: s, includeFields: []string{"Name", "Age", "Address"}, excludeFields: []string{"Age"}, - want: map[string]any{ - "name": "John", - "address": "New York", - }, + want: func() *mapsutil.OrderedMap[string, any] { + om := mapsutil.NewOrderedMap[string, any]() + om.Set("name", "John") + om.Set("address", "New York") + return &om + }(), wantErr: false, }, { @@ -176,6 +186,94 @@ func TestFilterStructToMap(t *testing.T) { } } +func TestFilterStructToMapOrder(t *testing.T) { + type OrderTestStruct struct { + FirstField string `json:"first_field"` + SecondField int `json:"second_field"` + ThirdField bool `json:"third_field"` + FourthField string `json:"fourth_field"` + } + + s := OrderTestStruct{ + FirstField: "first", + SecondField: 42, + ThirdField: true, + FourthField: "fourth", + } + + t.Run("field order follows struct definition", func(t *testing.T) { + result, err := FilterStructToMap(s, nil, nil) + if err != nil { + t.Fatalf("FilterStructToMap() error = %v", err) + } + + keys := result.GetKeys() + expectedOrder := []string{"first_field", "second_field", "third_field", "fourth_field"} + + if len(keys) != len(expectedOrder) { + t.Errorf("Expected %d keys, got %d", len(expectedOrder), len(keys)) + } + + for i, expectedKey := range expectedOrder { + if i >= len(keys) { + t.Errorf("Missing key at index %d: expected %s", i, expectedKey) + continue + } + if keys[i] != expectedKey { + t.Errorf("Key at index %d: expected %s, got %s", i, expectedKey, keys[i]) + } + } + }) + + t.Run("field order preserved with include filter", func(t *testing.T) { + result, err := FilterStructToMap(s, []string{"ThirdField", "FirstField"}, nil) + if err != nil { + t.Fatalf("FilterStructToMap() error = %v", err) + } + + keys := result.GetKeys() + expectedOrder := []string{"first_field", "third_field"} // Struct order, not include order + + if len(keys) != len(expectedOrder) { + t.Errorf("Expected %d keys, got %d", len(expectedOrder), len(keys)) + } + + for i, expectedKey := range expectedOrder { + if i >= len(keys) { + t.Errorf("Missing key at index %d: expected %s", i, expectedKey) + continue + } + if keys[i] != expectedKey { + t.Errorf("Key at index %d: expected %s, got %s", i, expectedKey, keys[i]) + } + } + }) + + t.Run("field order preserved with exclude filter", func(t *testing.T) { + result, err := FilterStructToMap(s, nil, []string{"SecondField"}) + if err != nil { + t.Fatalf("FilterStructToMap() error = %v", err) + } + + keys := result.GetKeys() + expectedOrder := []string{"first_field", "third_field", "fourth_field"} + + if len(keys) != len(expectedOrder) { + t.Errorf("Expected %d keys, got %d", len(expectedOrder), len(keys)) + } + + for i, expectedKey := range expectedOrder { + if i >= len(keys) { + t.Errorf("Missing key at index %d: expected %s", i, expectedKey) + continue + } + if keys[i] != expectedKey { + t.Errorf("Key at index %d: expected %s, got %s", i, expectedKey, keys[i]) + } + } + }) +} + func TestGetStructFields(t *testing.T) { s := TestStruct{ Name: "John",