diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..59ed4b2 Binary files /dev/null and b/.DS_Store differ diff --git a/typesense/api/types.go b/typesense/api/types.go index d61a33b..805846c 100644 --- a/typesense/api/types.go +++ b/typesense/api/types.go @@ -5,3 +5,24 @@ type ImportDocumentResponse struct { Error string `json:"error"` Document string `json:"document"` } + +type Type string + +// Enum for all Types in Typesense +const ( + STRING Type = "string" + STRINGARRAY = "string[]" + INT32 = "int32" + INT32ARRAY = "int32[]" + INT64 = "int64" + INT64ARRAY = "int64[]" + FLOAT = "float" + FLOATARRAY = "float[]" + BOOL = "bool" + BOOLARRAY = "bool[]" + GEOPOINT = "geopoint" + GEOPOINTARRAY = "geopoint[]" + OBJECT = "object" // object is comparable to a go struct + OBJECTARRAY = "object[]" + STRINGPTR = "string*" // special type that can be string or []string +) diff --git a/typesense/fields.go b/typesense/fields.go new file mode 100644 index 0000000..b3770dd --- /dev/null +++ b/typesense/fields.go @@ -0,0 +1,137 @@ +package typesense + +import ( + "errors" + "fmt" + "github.com/typesense/typesense-go/v2/typesense/api" + "reflect" + "strings" +) + +/* +ToFields takes a struct as input and converts its fields into a slice of Typesense field schema definitions. +This function is useful for automatically generating field schemas for Typesense from your Go structs. +It expects a struct type as input and will return an error if the input is not a struct. +Usage example: + + type MyStruct struct { + Id string `json:"id,index"` + Name string `json:"name"` + Age int `json:"age,facet"` + Email string `json:"email,optional"` + UserId string `json:"user_id,index,join:user.id"` // creates a reference to the collection user + } + +fields, err := ToFields(MyStruct{}) +fields now contains the field schema definitions for MyStruct +Supported tags: +index,name,facet,optional,sort,infix +join:{collectionName}.{id} -> e.g. join:user.id -> This will automatically create a reference to the user schema +*/ +func ToFields(Struct any) ([]api.Field, error) { + val := reflect.ValueOf(Struct) + if val.Kind() == reflect.Ptr || val.Kind() != reflect.Struct { + return nil, errors.New("input should be a struct") + } + return lexField(val.Type()) +} + +func lexField(typ reflect.Type) ([]api.Field, error) { + var collectionFields []api.Field + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + if field.PkgPath != "" { + continue // Skip unexported fields + } + fieldType := typeAllowed(field.Type) + if fieldType == api.OBJECT { + // Check if Object is embedded. + if field.Anonymous { + // Get each Individual Field to be Parsed + composited, err := lexField(field.Type) + if err != nil { + return nil, err + } + collectionFields = append(collectionFields, composited...) + continue + } + } + tags := field.Tag.Get("json") // tags save the field_name and Options like facet, index, join, optional etc. + apiField, err := parseField(fieldType, tags) + if err != nil { + return nil, err + } + collectionFields = append(collectionFields, apiField) + } + return collectionFields, nil +} + +func parseField(T api.Type, tag string) (api.Field, error) { + params := strings.Split(tag, ",") + var field api.Field + var True bool = true + + // We need the json Field.Tag + if len(params) == 0 { + return api.Field{}, errors.New("field name has to be provided for matching") + } + + field.Name = params[0] + field.Type = string(T) + + for _, key := range params[1:] { + switch key { + case "optional": // optional fields, can be null + field.Optional = &True + case "facet": // If a field is facet its also automatically indexed, correct? + field.Facet = &True + field.Index = &True + case "index": + field.Index = &True + case "sort": + field.Sort = &True + case "infix": + field.Infix = &True + default: + if ref, ok := strings.CutPrefix(key, "join:"); ok { + field.Reference = Pointer(ref) + continue + } + } + } + + return field, nil +} + +func typeAllowed(t reflect.Type) api.Type { + switch t.Kind() { + case reflect.String: + return api.STRING + case reflect.Int32, reflect.Int: + return api.INT32 + case reflect.Int64: + return api.INT64 + case reflect.Float32, reflect.Float64: + return api.FLOAT + case reflect.Bool: + return api.BOOL + case reflect.Slice: + elemType := typeAllowed(t.Elem()) + if elemType != "" { + return elemType + "[]" + } + case reflect.Struct: + return api.OBJECT + case reflect.Pointer: + return typeAllowed(t.Elem()) + default: + panic("type not allowed") + } + fmt.Println(t.Kind()) + return "" +} + +// Pointer returns the Pointer of a Type v +func Pointer[T any](v T) *T { + return &v +} diff --git a/typesense/fields_test.go b/typesense/fields_test.go new file mode 100644 index 0000000..6e98076 --- /dev/null +++ b/typesense/fields_test.go @@ -0,0 +1,80 @@ +package typesense + +import ( + "github.com/stretchr/testify/assert" + "github.com/typesense/typesense-go/v2/typesense/api" + "testing" +) + +type GenerationTest struct { + ID string `json:"id,index"` + Name string `json:"name,index,sort"` + UserId string `json:"user_id,index,join:user.id"` // creates a reference to the collection use + Birthdate int64 `json:"birthdate"` + LastTreatment int64 `json:"last_treatment,index,optional"` + LocationId string `json:"location_id,facet"` +} + +type User struct { + ID string `json:"id,index"` + Name string `json:"name,index"` + Type int32 `json:"type,facet"` +} + +func TestToFields_GenerationTest(t *testing.T) { + testStruct := GenerationTest{} + expectedFields := []api.Field{ + { + Name: "id", + Type: "string", + Index: Pointer(true), + }, + { + Name: "name", + Type: "string", + Index: Pointer(true), + }, + { + Name: "user_id", + Type: "string", + Index: Pointer(true), + Reference: Pointer("user.id"), + }, + { + Name: "birthdate", + Type: "int64", + }, + { + Name: "last_treatment", + Type: "int64", + Index: Pointer(true), + Optional: Pointer(true), + }, + { + Name: "location_id", + Type: "string", + Facet: Pointer(true), + }, + } + + fields, err := ToFields(testStruct) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + compareFields(t, expectedFields, fields) +} + +// Helper function to compare slices of api.Field +func compareFields(t *testing.T, expected, actual []api.Field) { + assert.Equal(t, len(expected), len(actual)) + + for i, exp := range expected { + act := actual[i] + assert.Equal(t, exp.Type, act.Type) + assert.Equal(t, exp.Name, act.Name) + if exp.Index != nil && act.Index != nil { + assert.Equal(t, *exp.Index, *act.Index) + } + } +}