package internal import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "gopkg.in/yaml.v3" ) var _ = Describe("XTP Schema Generation", func() { parseSchema := func(schema []byte) map[string]any { var doc map[string]any Expect(yaml.Unmarshal(schema, &doc)).To(Succeed()) return doc } Describe("GenerateSchema", func() { Context("basic capability with one export", func() { var schema []byte BeforeEach(func() { capability := Capability{ Name: "test", Doc: "Test capability", SourceFile: "test", Methods: []Export{ { ExportName: "test_method", Doc: "Test method does something", Input: NewParam("input", "TestInput"), Output: NewParam("output", "TestOutput"), }, }, Structs: []StructDef{ { Name: "TestInput", Doc: "Input for test", Fields: []FieldDef{ {Name: "Name", Type: "string", JSONTag: "name", Doc: "The name"}, {Name: "Count", Type: "int", JSONTag: "count", Doc: "The count"}, }, }, { Name: "TestOutput", Doc: "Output for test", Fields: []FieldDef{ {Name: "Result", Type: "string", JSONTag: "result", Doc: "The result"}, }, }, }, } var err error schema, err = GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(schema).NotTo(BeEmpty()) }) It("should validate against XTP JSONSchema", func() { Expect(ValidateXTPSchema(schema)).To(Succeed()) }) It("should have correct version", func() { doc := parseSchema(schema) Expect(doc["version"]).To(Equal("v1-draft")) }) It("should include exports with description", func() { doc := parseSchema(schema) exports := doc["exports"].(map[string]any) Expect(exports).To(HaveKey("test_method")) method := exports["test_method"].(map[string]any) Expect(method["description"]).To(Equal("Test method does something")) }) It("should include schemas for input and output types", func() { doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) Expect(schemas).To(HaveKey("TestInput")) Expect(schemas).To(HaveKey("TestOutput")) }) It("should define input schema with correct properties", func() { doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) input := schemas["TestInput"].(map[string]any) // Per XTP spec, ObjectSchema does NOT have a type field - only properties, required, description Expect(input).NotTo(HaveKey("type")) props := input["properties"].(map[string]any) Expect(props).To(HaveKey("name")) Expect(props).To(HaveKey("count")) }) It("should mark non-pointer, non-omitempty fields as required", func() { doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) input := schemas["TestInput"].(map[string]any) required := input["required"].([]any) Expect(required).To(ContainElement("name")) Expect(required).To(ContainElement("count")) }) }) Context("capability with pointer fields (nullable)", func() { var schema []byte BeforeEach(func() { capability := Capability{ Name: "nullable_test", SourceFile: "nullable_test", Methods: []Export{ {ExportName: "test", Input: NewParam("input", "Input"), Output: NewParam("output", "Output")}, }, Structs: []StructDef{ { Name: "Input", Fields: []FieldDef{ {Name: "Required", Type: "string", JSONTag: "required"}, {Name: "Optional", Type: "*string", JSONTag: "optional,omitempty", OmitEmpty: true}, }, }, { Name: "Output", Fields: []FieldDef{ {Name: "Value", Type: "string", JSONTag: "value"}, }, }, }, } var err error schema, err = GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) }) It("should validate against XTP JSONSchema", func() { Expect(ValidateXTPSchema(schema)).To(Succeed()) }) It("should not mark required field as nullable", func() { doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) input := schemas["Input"].(map[string]any) props := input["properties"].(map[string]any) requiredField := props["required"].(map[string]any) Expect(requiredField).NotTo(HaveKey("nullable")) }) It("should mark optional pointer field as nullable", func() { doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) input := schemas["Input"].(map[string]any) props := input["properties"].(map[string]any) optionalField := props["optional"].(map[string]any) Expect(optionalField["nullable"]).To(BeTrue()) }) It("should only include non-pointer fields in required array", func() { doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) input := schemas["Input"].(map[string]any) required := input["required"].([]any) Expect(required).To(ContainElement("required")) Expect(required).NotTo(ContainElement("optional")) }) }) Context("capability with enum", func() { var schema []byte BeforeEach(func() { capability := Capability{ Name: "enum_test", SourceFile: "enum_test", Methods: []Export{ {ExportName: "test", Input: NewParam("input", "Input"), Output: NewParam("output", "Output")}, }, Structs: []StructDef{ { Name: "Input", Fields: []FieldDef{ {Name: "Status", Type: "Status", JSONTag: "status"}, }, }, { Name: "Output", Fields: []FieldDef{ {Name: "Value", Type: "string", JSONTag: "value"}, }, }, }, TypeAliases: []TypeAlias{ {Name: "Status", Type: "string", Doc: "Status type"}, }, Consts: []ConstGroup{ { Type: "Status", Values: []ConstDef{ {Name: "StatusPending", Value: `"pending"`}, {Name: "StatusActive", Value: `"active"`}, {Name: "StatusDone", Value: `"done"`}, }, }, }, } var err error schema, err = GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) }) It("should validate against XTP JSONSchema", func() { Expect(ValidateXTPSchema(schema)).To(Succeed()) }) It("should define enum type with correct values", func() { doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) Expect(schemas).To(HaveKey("Status")) status := schemas["Status"].(map[string]any) Expect(status["type"]).To(Equal("string")) enum := status["enum"].([]any) Expect(enum).To(ConsistOf("pending", "active", "done")) }) It("should use $ref for enum field in struct", func() { doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) input := schemas["Input"].(map[string]any) props := input["properties"].(map[string]any) statusRef := props["status"].(map[string]any) Expect(statusRef["$ref"]).To(Equal("#/components/schemas/Status")) }) }) Context("capability with array types", func() { var schema []byte BeforeEach(func() { capability := Capability{ Name: "array_test", SourceFile: "array_test", Methods: []Export{ {ExportName: "test", Input: NewParam("input", "Input"), Output: NewParam("output", "Output")}, }, Structs: []StructDef{ { Name: "Input", Fields: []FieldDef{ {Name: "Tags", Type: "[]string", JSONTag: "tags"}, {Name: "Items", Type: "[]Item", JSONTag: "items"}, }, }, { Name: "Output", Fields: []FieldDef{ {Name: "Value", Type: "string", JSONTag: "value"}, }, }, { Name: "Item", Fields: []FieldDef{ {Name: "ID", Type: "string", JSONTag: "id"}, }, }, }, } var err error schema, err = GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) }) It("should validate against XTP JSONSchema", func() { Expect(ValidateXTPSchema(schema)).To(Succeed()) }) It("should define string array with primitive type", func() { doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) input := schemas["Input"].(map[string]any) props := input["properties"].(map[string]any) tags := props["tags"].(map[string]any) Expect(tags["type"]).To(Equal("array")) tagItems := tags["items"].(map[string]any) Expect(tagItems["type"]).To(Equal("string")) }) It("should define struct array with $ref", func() { doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) input := schemas["Input"].(map[string]any) props := input["properties"].(map[string]any) items := props["items"].(map[string]any) Expect(items["type"]).To(Equal("array")) itemItems := items["items"].(map[string]any) Expect(itemItems["$ref"]).To(Equal("#/components/schemas/Item")) }) }) Context("capability with []byte field", func() { It("should map []byte to string with byte format, not array", func() { capability := Capability{ Name: "byte_test", SourceFile: "byte_test", Methods: []Export{ {ExportName: "test", Input: NewParam("input", "Input"), Output: NewParam("output", "Output")}, }, Structs: []StructDef{ { Name: "Input", Fields: []FieldDef{ {Name: "Data", Type: "[]byte", JSONTag: "data"}, }, }, { Name: "Output", Fields: []FieldDef{ {Name: "Value", Type: "string", JSONTag: "value"}, }, }, }, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(ValidateXTPSchema(schema)).To(Succeed()) doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) input := schemas["Input"].(map[string]any) props := input["properties"].(map[string]any) data := props["data"].(map[string]any) Expect(data["type"]).To(Equal("string")) Expect(data["format"]).To(Equal("byte")) Expect(data).NotTo(HaveKey("items")) }) }) Context("capability with nullable ref", func() { It("should mark pointer to enum as nullable with $ref", func() { capability := Capability{ Name: "nullable_ref_test", SourceFile: "nullable_ref_test", Methods: []Export{ {ExportName: "test", Input: NewParam("input", "Input"), Output: NewParam("output", "Output")}, }, Structs: []StructDef{ { Name: "Input", Fields: []FieldDef{ {Name: "Value", Type: "string", JSONTag: "value"}, }, }, { Name: "Output", Fields: []FieldDef{ {Name: "Status", Type: "*ErrorType", JSONTag: "status,omitempty", OmitEmpty: true}, }, }, }, TypeAliases: []TypeAlias{ {Name: "ErrorType", Type: "string"}, }, Consts: []ConstGroup{ { Type: "ErrorType", Values: []ConstDef{ {Name: "ErrorNone", Value: `"none"`}, {Name: "ErrorFatal", Value: `"fatal"`}, }, }, }, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) // Validate against XTP JSONSchema Expect(ValidateXTPSchema(schema)).To(Succeed()) doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) output := schemas["Output"].(map[string]any) props := output["properties"].(map[string]any) status := props["status"].(map[string]any) Expect(status["$ref"]).To(Equal("#/components/schemas/ErrorType")) Expect(status["nullable"]).To(BeTrue()) }) }) }) Describe("goTypeToXTPTypeAndFormat", func() { DescribeTable("should convert Go types to XTP types", func(goType, wantType, wantFormat string) { gotType, gotFormat := goTypeToXTPTypeAndFormat(goType) Expect(gotType).To(Equal(wantType)) Expect(gotFormat).To(Equal(wantFormat)) }, Entry("string", "string", "string", ""), Entry("int", "int", "integer", "int32"), Entry("int32", "int32", "integer", "int32"), Entry("int64", "int64", "integer", "int64"), Entry("float32", "float32", "number", "float"), Entry("float64", "float64", "number", "float"), Entry("bool", "bool", "boolean", ""), Entry("[]byte", "[]byte", "string", "byte"), Entry("unknown types default to object", "CustomType", "object", ""), ) }) Describe("cleanDocForYAML", func() { DescribeTable("should clean documentation strings", func(doc, want string) { Expect(cleanDocForYAML(doc)).To(Equal(want)) }, Entry("empty", "", ""), Entry("single line", "Simple description", "Simple description"), Entry("multiline", "First line\nSecond line", "First line\nSecond line"), Entry("trailing newline", "Description\n", "Description"), Entry("whitespace", " Description ", "Description"), ) }) Describe("isPrimitiveGoType", func() { DescribeTable("should identify primitive Go types", func(goType string, want bool) { Expect(isPrimitiveGoType(goType)).To(Equal(want)) }, Entry("bool", "bool", true), Entry("string", "string", true), Entry("int", "int", true), Entry("int32", "int32", true), Entry("int64", "int64", true), Entry("float32", "float32", true), Entry("float64", "float64", true), Entry("[]byte", "[]byte", true), Entry("custom type", "CustomType", false), Entry("struct type", "MyStruct", false), Entry("slice of string", "[]string", false), Entry("map type", "map[string]int", false), ) }) Describe("GenerateSchema with primitive output types", func() { inputStruct := StructDef{ Name: "Input", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}, } Context("export with primitive string output", func() { It("should use type instead of $ref and validate against XTP JSONSchema", func() { capability := Capability{ Name: "test", SourceFile: "test", Methods: []Export{ {ExportName: "get_name", Input: NewParam("input", "Input"), Output: NewParam("output", "string")}, }, Structs: []StructDef{inputStruct}, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(schema).NotTo(BeEmpty()) Expect(ValidateXTPSchema(schema)).To(Succeed()) doc := parseSchema(schema) exports := doc["exports"].(map[string]any) method := exports["get_name"].(map[string]any) output := method["output"].(map[string]any) Expect(output["type"]).To(Equal("string")) Expect(output).NotTo(HaveKey("$ref")) Expect(output["contentType"]).To(Equal("application/json")) }) }) Context("export with primitive bool output", func() { It("should use boolean type and validate against XTP JSONSchema", func() { capability := Capability{ Name: "test", SourceFile: "test", Methods: []Export{ {ExportName: "is_valid", Input: NewParam("input", "Input"), Output: NewParam("output", "bool")}, }, Structs: []StructDef{inputStruct}, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(ValidateXTPSchema(schema)).To(Succeed()) doc := parseSchema(schema) exports := doc["exports"].(map[string]any) method := exports["is_valid"].(map[string]any) output := method["output"].(map[string]any) Expect(output["type"]).To(Equal("boolean")) Expect(output).NotTo(HaveKey("$ref")) }) }) Context("export with primitive int output", func() { It("should use integer type and validate against XTP JSONSchema", func() { capability := Capability{ Name: "test", SourceFile: "test", Methods: []Export{ {ExportName: "get_count", Input: NewParam("input", "Input"), Output: NewParam("output", "int32")}, }, Structs: []StructDef{inputStruct}, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(ValidateXTPSchema(schema)).To(Succeed()) doc := parseSchema(schema) exports := doc["exports"].(map[string]any) method := exports["get_count"].(map[string]any) output := method["output"].(map[string]any) Expect(output["type"]).To(Equal("integer")) Expect(output).NotTo(HaveKey("$ref")) }) }) Context("export with pointer to primitive output", func() { It("should strip pointer and use primitive type and validate against XTP JSONSchema", func() { capability := Capability{ Name: "test", SourceFile: "test", Methods: []Export{ {ExportName: "get_optional_string", Input: NewParam("input", "Input"), Output: NewParam("output", "*string")}, }, Structs: []StructDef{inputStruct}, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(ValidateXTPSchema(schema)).To(Succeed()) doc := parseSchema(schema) exports := doc["exports"].(map[string]any) method := exports["get_optional_string"].(map[string]any) output := method["output"].(map[string]any) Expect(output["type"]).To(Equal("string")) Expect(output).NotTo(HaveKey("$ref")) }) }) Context("export with struct output", func() { It("should still use $ref and validate against XTP JSONSchema", func() { capability := Capability{ Name: "test", SourceFile: "test", Methods: []Export{ {ExportName: "get_result", Input: NewParam("input", "Input"), Output: NewParam("output", "Output")}, }, Structs: []StructDef{ inputStruct, {Name: "Output", Fields: []FieldDef{{Name: "Value", Type: "string", JSONTag: "value"}}}, }, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(ValidateXTPSchema(schema)).To(Succeed()) doc := parseSchema(schema) exports := doc["exports"].(map[string]any) method := exports["get_result"].(map[string]any) output := method["output"].(map[string]any) Expect(output["$ref"]).To(Equal("#/components/schemas/Output")) Expect(output).NotTo(HaveKey("type")) }) }) }) Describe("collectUsedTypes", func() { getSchemas := func(schema []byte) map[string]any { doc := parseSchema(schema) components, hasComponents := doc["components"].(map[string]any) if !hasComponents { return make(map[string]any) } schemas, ok := components["schemas"].(map[string]any) if !ok { return make(map[string]any) } return schemas } It("should only include types referenced by exports", func() { capability := Capability{ Name: "test", SourceFile: "test", Methods: []Export{ {ExportName: "test", Input: NewParam("input", "UsedInput"), Output: NewParam("output", "UsedOutput")}, }, Structs: []StructDef{ {Name: "UsedInput", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}}, {Name: "UsedOutput", Fields: []FieldDef{{Name: "Value", Type: "string", JSONTag: "value"}}}, {Name: "UnusedStruct", Fields: []FieldDef{{Name: "Foo", Type: "string", JSONTag: "foo"}}}, }, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(ValidateXTPSchema(schema)).To(Succeed()) schemas := getSchemas(schema) Expect(schemas).To(HaveKey("UsedInput")) Expect(schemas).To(HaveKey("UsedOutput")) Expect(schemas).NotTo(HaveKey("UnusedStruct")) }) It("should include transitively referenced types", func() { capability := Capability{ Name: "test", SourceFile: "test", Methods: []Export{ {ExportName: "test", Input: NewParam("input", "Input"), Output: NewParam("output", "Output")}, }, Structs: []StructDef{ {Name: "Input", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}}, {Name: "Output", Fields: []FieldDef{{Name: "Nested", Type: "NestedType", JSONTag: "nested"}}}, {Name: "NestedType", Fields: []FieldDef{{Name: "Value", Type: "string", JSONTag: "value"}}}, }, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(ValidateXTPSchema(schema)).To(Succeed()) schemas := getSchemas(schema) Expect(schemas).To(HaveKey("Input")) Expect(schemas).To(HaveKey("Output")) Expect(schemas).To(HaveKey("NestedType")) }) It("should include array element types", func() { capability := Capability{ Name: "test", SourceFile: "test", Methods: []Export{ {ExportName: "test", Input: NewParam("input", "Input"), Output: NewParam("output", "Output")}, }, Structs: []StructDef{ {Name: "Input", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}}, {Name: "Output", Fields: []FieldDef{{Name: "Items", Type: "[]Item", JSONTag: "items"}}}, {Name: "Item", Fields: []FieldDef{{Name: "Name", Type: "string", JSONTag: "name"}}}, }, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(ValidateXTPSchema(schema)).To(Succeed()) schemas := getSchemas(schema) Expect(schemas).To(HaveKey("Input")) Expect(schemas).To(HaveKey("Output")) Expect(schemas).To(HaveKey("Item")) }) It("should include pointer types", func() { capability := Capability{ Name: "test", SourceFile: "test", Methods: []Export{ {ExportName: "test", Input: NewParam("input", "Input"), Output: NewParam("output", "Output")}, }, Structs: []StructDef{ {Name: "Input", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}}, {Name: "Output", Fields: []FieldDef{{Name: "Optional", Type: "*OptionalType", JSONTag: "optional"}}}, {Name: "OptionalType", Fields: []FieldDef{{Name: "Value", Type: "string", JSONTag: "value"}}}, }, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(ValidateXTPSchema(schema)).To(Succeed()) schemas := getSchemas(schema) Expect(schemas).To(HaveKey("Input")) Expect(schemas).To(HaveKey("Output")) Expect(schemas).To(HaveKey("OptionalType")) }) It("should exclude primitive output types from schema", func() { capability := Capability{ Name: "test", SourceFile: "test", Methods: []Export{ {ExportName: "test", Input: NewParam("input", "Input"), Output: NewParam("output", "string")}, }, Structs: []StructDef{ {Name: "Input", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}}, }, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(ValidateXTPSchema(schema)).To(Succeed()) schemas := getSchemas(schema) Expect(schemas).To(HaveKey("Input")) }) }) Describe("GenerateSchema enum filtering", func() { It("should only include enums that are actually used by exports", func() { capability := Capability{ Name: "test", SourceFile: "test", Methods: []Export{ {ExportName: "test", Input: NewParam("input", "Input"), Output: NewParam("output", "Output")}, }, Structs: []StructDef{ { Name: "Input", Fields: []FieldDef{{Name: "Status", Type: "UsedStatus", JSONTag: "status"}}, }, { Name: "Output", Fields: []FieldDef{{Name: "Value", Type: "string", JSONTag: "value"}}, }, }, TypeAliases: []TypeAlias{ {Name: "UsedStatus", Type: "string"}, {Name: "UnusedStatus", Type: "string"}, }, Consts: []ConstGroup{ { Type: "UsedStatus", Values: []ConstDef{ {Name: "StatusActive", Value: `"active"`}, {Name: "StatusInactive", Value: `"inactive"`}, }, }, { Type: "UnusedStatus", Values: []ConstDef{ {Name: "UnusedPending", Value: `"pending"`}, }, }, }, } schema, err := GenerateSchema(capability) Expect(err).NotTo(HaveOccurred()) Expect(ValidateXTPSchema(schema)).To(Succeed()) doc := parseSchema(schema) components := doc["components"].(map[string]any) schemas := components["schemas"].(map[string]any) // UsedStatus should be included because it's referenced by Input Expect(schemas).To(HaveKey("UsedStatus")) usedStatus := schemas["UsedStatus"].(map[string]any) Expect(usedStatus["type"]).To(Equal("string")) enum := usedStatus["enum"].([]any) Expect(enum).To(ConsistOf("active", "inactive")) // UnusedStatus should NOT be included Expect(schemas).NotTo(HaveKey("UnusedStatus")) }) }) })