Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added []api.Fields Generation From Go Struct #183

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
21 changes: 21 additions & 0 deletions typesense/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
137 changes: 137 additions & 0 deletions typesense/fields.go
Original file line number Diff line number Diff line change
@@ -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
}
80 changes: 80 additions & 0 deletions typesense/fields_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}