From 7b426eb094efb2993c06b0d264e18f2299939673 Mon Sep 17 00:00:00 2001 From: dieg0code Date: Fri, 6 Dec 2024 06:43:12 -0300 Subject: [PATCH 1/3] Added support for enum tag in schema generation --- jsonschema/json.go | 11 +++++++++++ test.mp3 | 1 + 2 files changed, 12 insertions(+) create mode 100644 test.mp3 diff --git a/jsonschema/json.go b/jsonschema/json.go index bcb253fae..3036b99ab 100644 --- a/jsonschema/json.go +++ b/jsonschema/json.go @@ -131,6 +131,12 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) { required = false } + enumTag := field.Tag.Get("enum") + var enumValues []string + if enumTag != "" { + enumValues = strings.Split(enumTag, ",") + } + item, err := reflectSchema(field.Type) if err != nil { return nil, err @@ -139,6 +145,11 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) { if description != "" { item.Description = description } + + if len(enumValues) > 0 { + item.Enum = enumValues + } + properties[jsonTag] = *item if s := field.Tag.Get("required"); s != "" { diff --git a/test.mp3 b/test.mp3 new file mode 100644 index 000000000..b6fc4c620 --- /dev/null +++ b/test.mp3 @@ -0,0 +1 @@ +hello \ No newline at end of file From 7449908c749a6003dc88064f8df4316d4de2fcaa Mon Sep 17 00:00:00 2001 From: dieg0code Date: Sun, 8 Dec 2024 12:14:00 -0300 Subject: [PATCH 2/3] feat: add schema generation tests for enums, GenerateSchemaForType and Comparasion test for manual created schema and struct-generated one | Also modify previous tests --- jsonschema/json_test.go | 270 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 249 insertions(+), 21 deletions(-) diff --git a/jsonschema/json_test.go b/jsonschema/json_test.go index 744706082..2dd172394 100644 --- a/jsonschema/json_test.go +++ b/jsonschema/json_test.go @@ -8,6 +8,234 @@ import ( "github.com/sashabaranov/go-openai/jsonschema" ) +func TestDefinition_GenerateSchemaForType(t *testing.T) { + type MenuItem struct { + ItemName string `json:"item_name" description:"Menu item name" required:"true"` + Quantity int `json:"quantity" description:"Quantity of menu item ordered" required:"true"` + Price int `json:"price" description:"Price of the menu item" required:"true"` + } + + type UserOrder struct { + MenuItems []MenuItem `json:"menu_items" description:"List of menu items ordered by the user" required:"true"` + DeliveryAddress string `json:"delivery_address" description:"Delivery address for the order" required:"true"` + UserName string `json:"user_name" description:"Name of the user placing the order" required:"true"` + PhoneNumber string `json:"phone_number" description:"Phone number of the user placing the order" required:"true"` + PaymentMethod string `json:"payment_method" description:"Payment method for the order, only transfer or cash accepted" required:"true" enum:"cash,transfer,check,card"` + } + + tests := []struct { + name string + input any + want string + wantErr bool + }{ + { + name: "Test MenuItem Schema", + input: MenuItem{}, + want: `{ + "type":"object", + "additionalProperties":false, + "properties":{ + "item_name":{ + "type":"string", + "description":"Menu item name" + }, + "quantity":{ + "type":"integer", + "description":"Quantity of menu item ordered" + }, + "price":{ + "type":"integer", + "description":"Price of the menu item" + } + }, + "required":[ + "item_name", + "quantity", + "price" + ] +}`, + }, + { + name: "Test UserOrder Schema", + input: UserOrder{}, + want: `{ + "type":"object", + "additionalProperties":false, + "properties":{ + "menu_items":{ + "type":"array", + "description":"List of menu items ordered by the user", + "items":{ + "type":"object", + "additionalProperties":false, + "properties":{ + "item_name":{ + "type":"string", + "description":"Menu item name" + }, + "quantity":{ + "type":"integer", + "description":"Quantity of menu item ordered" + }, + "price":{ + "type":"integer", + "description":"Price of the menu item" + } + }, + "required":[ + "item_name", + "quantity", + "price" + ] + } + }, + "delivery_address":{ + "type":"string", + "description":"Delivery address for the order" + }, + "user_name":{ + "type":"string", + "description":"Name of the user placing the order" + }, + "phone_number":{ + "type":"string", + "description":"Phone number of the user placing the order" + }, + "payment_method":{ + "type":"string", + "description":"Payment method for the order, only transfer or cash accepted", + "enum":[ + "cash", + "transfer", + "check", + "card" + ] + } + }, + "required":[ + "menu_items", + "delivery_address", + "user_name", + "phone_number", + "payment_method" + ] +}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Generate schema + got, err := jsonschema.GenerateSchemaForType(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("GenerateSchemaForType() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Convert both the generated schema and the expected JSON to maps for comparison + wantBytes := []byte(tt.want) + var want map[string]interface{} + err = json.Unmarshal(wantBytes, &want) + if err != nil { + t.Errorf("Failed to Unmarshal expected JSON: error = %v", err) + return + } + + gotMap := structToMap(t, got) + + // Compare the maps + if !reflect.DeepEqual(gotMap, want) { + t.Errorf("GenerateSchemaForType() got = %v, want %v", gotMap, want) + } + }) + } +} + +func TestDefinition_SchemaGenerationComparison(t *testing.T) { + type MenuItem struct { + ItemName string `json:"item_name" description:"Menu item name" required:"true"` + Quantity int `json:"quantity" description:"Quantity of menu item ordered" required:"true"` + Price float64 `json:"price" description:"Price of the menu item" required:"true"` + } + + type UserOrder struct { + MenuItems []MenuItem `json:"menu_items" description:"List of menu items ordered by the user" required:"true"` + DeliveryAddress string `json:"delivery_address" description:"Delivery address for the order" required:"true"` + UserName string `json:"user_name" description:"Name of the user placing the order" required:"true"` + PhoneNumber string `json:"phone_number" description:"Phone number of the user placing the order" required:"true"` + PaymentMethod string `json:"payment_method" description:"Payment method for the order" required:"true" enum:"cash,transfer"` + } + + // Manually created schema to compare against struct-generated schema + manualSchema := &jsonschema.Definition{ + Type: jsonschema.Object, + Required: []string{"menu_items", "delivery_address", "user_name", "phone_number", "payment_method"}, + Properties: map[string]jsonschema.Definition{ + "menu_items": { + Type: jsonschema.Array, + Items: &jsonschema.Definition{ + Type: jsonschema.Object, + Properties: map[string]jsonschema.Definition{ + "item_name": { + Type: jsonschema.String, + Description: "Menu item name", + }, + "quantity": { + Type: jsonschema.Integer, + Description: "Quantity of menu item ordered", + }, + "price": { + Type: jsonschema.Number, + Description: "Price of the menu item", + }, + }, + Required: []string{"item_name", "quantity", "price"}, + AdditionalProperties: false, + }, + Description: "List of menu items ordered by the user", + }, + "delivery_address": { + Type: jsonschema.String, + Description: "Delivery address for the order", + }, + "user_name": { + Type: jsonschema.String, + Description: "Name of the user placing the order", + }, + "phone_number": { + Type: jsonschema.String, + Description: "Phone number of the user placing the order", + }, + "payment_method": { + Type: jsonschema.String, + Description: "Payment method for the order", + Enum: []string{"cash", "transfer"}, + }, + }, + AdditionalProperties: false, + } + + t.Run("Compare Struct-Generated and Manual Schema", func(t *testing.T) { + // Generate schema from struct + structSchema, err := jsonschema.GenerateSchemaForType(UserOrder{}) + if err != nil { + t.Fatalf("Failed to generate schema from struct: %v", err) + } + + // Convert both schemas to maps for comparison + structMap := structToMap(t, structSchema) + manualMap := structToMap(t, manualSchema) + + // Compare the maps + if !reflect.DeepEqual(structMap, manualMap) { + t.Errorf("Schema generated from struct and manual schema do not match") + t.Errorf("Struct generated schema: %v", structMap) + t.Errorf("Manual schema: %v", manualMap) + } + }) +} + func TestDefinition_MarshalJSON(t *testing.T) { tests := []struct { name string @@ -17,7 +245,7 @@ func TestDefinition_MarshalJSON(t *testing.T) { { name: "Test with empty Definition", def: jsonschema.Definition{}, - want: `{"properties":{}}`, + want: `{}`, }, { name: "Test with Definition properties set", @@ -35,8 +263,7 @@ func TestDefinition_MarshalJSON(t *testing.T) { "description":"A string type", "properties":{ "name":{ - "type":"string", - "properties":{} + "type":"string" } } }`, @@ -66,12 +293,10 @@ func TestDefinition_MarshalJSON(t *testing.T) { "type":"object", "properties":{ "name":{ - "type":"string", - "properties":{} + "type":"string" }, "age":{ - "type":"integer", - "properties":{} + "type":"integer" } } } @@ -114,23 +339,19 @@ func TestDefinition_MarshalJSON(t *testing.T) { "type":"object", "properties":{ "name":{ - "type":"string", - "properties":{} + "type":"string" }, "age":{ - "type":"integer", - "properties":{} + "type":"integer" }, "address":{ "type":"object", "properties":{ "city":{ - "type":"string", - "properties":{} + "type":"string" }, "country":{ - "type":"string", - "properties":{} + "type":"string" } } } @@ -155,19 +376,26 @@ func TestDefinition_MarshalJSON(t *testing.T) { want: `{ "type":"array", "items":{ - "type":"string", - "properties":{ - - } + "type":"string" }, "properties":{ "name":{ - "type":"string", - "properties":{} + "type":"string" } } }`, }, + { + name: "Test with Enum type Definition", + def: jsonschema.Definition{ + Type: jsonschema.String, + Enum: []string{"celsius", "fahrenheit"}, + }, + want: `{ + "type":"string", + "enum":["celsius","fahrenheit"] + }`, + }, } for _, tt := range tests { From fe5cc78a6c2650761324664cf1ef467597bc2598 Mon Sep 17 00:00:00 2001 From: dieg0code Date: Sun, 8 Dec 2024 13:17:51 -0300 Subject: [PATCH 3/3] fix: char length linter error in jsconschema test --- jsonschema/json_test.go | 36 +++++++++++++++++------------------- test.mp3 | 1 - 2 files changed, 17 insertions(+), 20 deletions(-) delete mode 100644 test.mp3 diff --git a/jsonschema/json_test.go b/jsonschema/json_test.go index 2dd172394..2513ec15b 100644 --- a/jsonschema/json_test.go +++ b/jsonschema/json_test.go @@ -19,8 +19,8 @@ func TestDefinition_GenerateSchemaForType(t *testing.T) { MenuItems []MenuItem `json:"menu_items" description:"List of menu items ordered by the user" required:"true"` DeliveryAddress string `json:"delivery_address" description:"Delivery address for the order" required:"true"` UserName string `json:"user_name" description:"Name of the user placing the order" required:"true"` - PhoneNumber string `json:"phone_number" description:"Phone number of the user placing the order" required:"true"` - PaymentMethod string `json:"payment_method" description:"Payment method for the order, only transfer or cash accepted" required:"true" enum:"cash,transfer,check,card"` + PhoneNumber string `json:"phone_number" description:"Phone number of the user" required:"true"` + PaymentMethod string `json:"payment_method" description:"Payment method" required:"true" enum:"cash,transfer"` } tests := []struct { @@ -100,16 +100,14 @@ func TestDefinition_GenerateSchemaForType(t *testing.T) { }, "phone_number":{ "type":"string", - "description":"Phone number of the user placing the order" + "description":"Phone number of the user" }, "payment_method":{ "type":"string", - "description":"Payment method for the order, only transfer or cash accepted", + "description":"Payment method", "enum":[ "cash", - "transfer", - "check", - "card" + "transfer" ] } }, @@ -163,19 +161,21 @@ func TestDefinition_SchemaGenerationComparison(t *testing.T) { MenuItems []MenuItem `json:"menu_items" description:"List of menu items ordered by the user" required:"true"` DeliveryAddress string `json:"delivery_address" description:"Delivery address for the order" required:"true"` UserName string `json:"user_name" description:"Name of the user placing the order" required:"true"` - PhoneNumber string `json:"phone_number" description:"Phone number of the user placing the order" required:"true"` - PaymentMethod string `json:"payment_method" description:"Payment method for the order" required:"true" enum:"cash,transfer"` + PhoneNumber string `json:"phone_number" description:"Phone number of the user" required:"true"` + PaymentMethod string `json:"payment_method" description:"Payment method" required:"true" enum:"cash,transfer"` } // Manually created schema to compare against struct-generated schema manualSchema := &jsonschema.Definition{ - Type: jsonschema.Object, - Required: []string{"menu_items", "delivery_address", "user_name", "phone_number", "payment_method"}, + Type: jsonschema.Object, + AdditionalProperties: false, Properties: map[string]jsonschema.Definition{ "menu_items": { - Type: jsonschema.Array, + Type: jsonschema.Array, + Description: "List of menu items ordered by the user", Items: &jsonschema.Definition{ - Type: jsonschema.Object, + Type: jsonschema.Object, + AdditionalProperties: false, Properties: map[string]jsonschema.Definition{ "item_name": { Type: jsonschema.String, @@ -190,10 +190,8 @@ func TestDefinition_SchemaGenerationComparison(t *testing.T) { Description: "Price of the menu item", }, }, - Required: []string{"item_name", "quantity", "price"}, - AdditionalProperties: false, + Required: []string{"item_name", "quantity", "price"}, }, - Description: "List of menu items ordered by the user", }, "delivery_address": { Type: jsonschema.String, @@ -205,15 +203,15 @@ func TestDefinition_SchemaGenerationComparison(t *testing.T) { }, "phone_number": { Type: jsonschema.String, - Description: "Phone number of the user placing the order", + Description: "Phone number of the user", }, "payment_method": { Type: jsonschema.String, - Description: "Payment method for the order", + Description: "Payment method", Enum: []string{"cash", "transfer"}, }, }, - AdditionalProperties: false, + Required: []string{"menu_items", "delivery_address", "user_name", "phone_number", "payment_method"}, } t.Run("Compare Struct-Generated and Manual Schema", func(t *testing.T) { diff --git a/test.mp3 b/test.mp3 deleted file mode 100644 index b6fc4c620..000000000 --- a/test.mp3 +++ /dev/null @@ -1 +0,0 @@ -hello \ No newline at end of file