package internal
import (
"sort"
"strings"
"gopkg.in/yaml.v3"
)
type (
xtpSchema struct {
Version string `yaml:"version"`
Exports yaml.Node `yaml:"exports,omitempty"`
Components *xtpComponents `yaml:"components,omitempty"`
}
xtpComponents struct {
Schemas yaml.Node `yaml:"schemas"`
}
xtpExport struct {
Description string `yaml:"description,omitempty"`
Input *xtpIOParam `yaml:"input,omitempty"`
Output *xtpIOParam `yaml:"output,omitempty"`
}
xtpIOParam struct {
Ref string `yaml:"$ref,omitempty"`
Type string `yaml:"type,omitempty"`
ContentType string `yaml:"contentType"`
}
xtpObjectSchema struct {
Description string `yaml:"description,omitempty"`
Properties yaml.Node `yaml:"properties"`
Required []string `yaml:"required,omitempty"`
}
xtpEnumSchema struct {
Description string `yaml:"description,omitempty"`
Type string `yaml:"type"`
Enum []string `yaml:"enum"`
}
xtpProperty struct {
Ref string `yaml:"$ref,omitempty"`
Type string `yaml:"type,omitempty"`
Format string `yaml:"format,omitempty"`
Description string `yaml:"description,omitempty"`
Nullable bool `yaml:"nullable,omitempty"`
Items *xtpProperty `yaml:"items,omitempty"`
}
)
func GenerateSchema(cap Capability) ([]byte, error) {
schema := xtpSchema{Version: "v1-draft"}
aliasToCanonical := buildAliasToCanonical(cap)
if len(cap.Methods) > 0 {
schema.Exports = yaml.Node{Kind: yaml.MappingNode}
for _, export := range cap.Methods {
addToMap(&schema.Exports, export.ExportName, buildExport(export, aliasToCanonical))
}
}
schemas := buildSchemas(cap, aliasToCanonical)
if len(schemas.Content) > 0 {
schema.Components = &xtpComponents{Schemas: schemas}
}
return yaml.Marshal(schema)
}
func buildAliasToCanonical(cap Capability) map[string]string {
m := map[string]string{}
for _, a := range cap.SharedAliases {
m[a.Name] = strings.TrimPrefix(a.Target, sharedTypesPrefix)
}
return m
}
func buildExport(export Export, aliasToCanonical map[string]string) xtpExport {
e := xtpExport{Description: cleanDocForYAML(export.Doc)}
if export.Input.Type != "" {
e.Input = &xtpIOParam{
Ref: "#/components/schemas/" + canonicalRefName(fieldBaseType(export.Input.Type), aliasToCanonical),
ContentType: "application/json",
}
}
if export.Output.Type != "" {
outputType := strings.TrimPrefix(export.Output.Type, "*")
if isPrimitiveGoType(outputType) {
e.Output = &xtpIOParam{
Type: goTypeToXTPType(outputType),
ContentType: "application/json",
}
} else {
e.Output = &xtpIOParam{
Ref: "#/components/schemas/" + canonicalRefName(fieldBaseType(outputType), aliasToCanonical),
ContentType: "application/json",
}
}
}
return e
}
func isPrimitiveGoType(goType string) bool {
switch goType {
case "bool", "string", "int", "int32", "int64", "uint", "uint32", "uint64", "float32", "float64", "[]byte":
return true
}
return false
}
func buildSchemas(cap Capability, aliasToCanonical map[string]string) yaml.Node {
schemas := yaml.Node{Kind: yaml.MappingNode}
knownTypes := cap.KnownStructs()
for _, alias := range cap.TypeAliases {
knownTypes[alias.Name] = true
}
sharedDefs := map[string]StructDef{}
for _, def := range cap.SharedTypes {
knownTypes[def.Name] = true
sharedDefs[def.Name] = def
}
for _, a := range cap.SharedAliases {
canonical := strings.TrimPrefix(a.Target, sharedTypesPrefix)
knownTypes[canonical] = true
sharedDefs[canonical] = a.Def
}
usedTypes := collectUsedTypes(cap, knownTypes, sharedDefs)
for alias, canonical := range aliasToCanonical {
if usedTypes[alias] {
usedTypes[canonical] = true
}
}
structNames := make([]string, 0, len(cap.Structs))
structMap := make(map[string]StructDef)
for _, st := range cap.Structs {
if usedTypes[st.Name] {
structNames = append(structNames, st.Name)
structMap[st.Name] = st
}
}
sort.Strings(structNames)
for _, name := range structNames {
st := structMap[name]
addToMap(&schemas, name, buildObjectSchema(st, knownTypes, aliasToCanonical))
}
sharedNames := make([]string, 0, len(sharedDefs))
for name, def := range sharedDefs {
if usedTypes[name] && len(def.Fields) > 0 {
sharedNames = append(sharedNames, name)
}
}
sort.Strings(sharedNames)
for _, name := range sharedNames {
addToMap(&schemas, name, buildObjectSchema(sharedDefs[name], knownTypes, aliasToCanonical))
}
for _, alias := range cap.TypeAliases {
if !usedTypes[alias.Name] {
continue
}
if alias.Type == "string" {
for _, cg := range cap.Consts {
if cg.Type == alias.Name {
addToMap(&schemas, alias.Name, buildEnumSchema(alias, cg))
break
}
}
}
}
return schemas
}
func collectUsedTypes(cap Capability, knownTypes map[string]bool, sharedDefs map[string]StructDef) map[string]bool {
used := make(map[string]bool)
for _, export := range cap.Methods {
if export.Input.Type != "" {
addTypeAndDeps(strings.TrimPrefix(export.Input.Type, "*"), cap, knownTypes, sharedDefs, used)
}
if export.Output.Type != "" {
outputType := strings.TrimPrefix(export.Output.Type, "*")
if !isPrimitiveGoType(outputType) {
addTypeAndDeps(outputType, cap, knownTypes, sharedDefs, used)
}
}
}
return used
}
func addTypeAndDeps(typeName string, cap Capability, knownTypes map[string]bool, sharedDefs map[string]StructDef, used map[string]bool) {
typeName = strings.TrimPrefix(typeName, sharedTypesPrefix)
if used[typeName] || !knownTypes[typeName] {
return
}
used[typeName] = true
for _, st := range cap.Structs {
if st.Name == typeName {
for _, field := range st.Fields {
if base := fieldBaseType(field.Type); knownTypes[base] {
addTypeAndDeps(base, cap, knownTypes, sharedDefs, used)
}
}
return
}
}
if def, ok := sharedDefs[typeName]; ok {
for _, field := range def.Fields {
if base := fieldBaseType(field.Type); knownTypes[base] {
addTypeAndDeps(base, cap, knownTypes, sharedDefs, used)
}
}
}
}
func fieldBaseType(goType string) string {
goType = strings.TrimPrefix(goType, "*")
goType = strings.TrimPrefix(goType, "[]")
return strings.TrimPrefix(goType, sharedTypesPrefix)
}
func canonicalRefName(name string, aliasToCanonical map[string]string) string {
if canonical, ok := aliasToCanonical[name]; ok {
return canonical
}
return name
}
func buildObjectSchema(st StructDef, knownTypes map[string]bool, aliasToCanonical map[string]string) xtpObjectSchema {
schema := xtpObjectSchema{
Description: cleanDocForYAML(st.Doc),
Properties: yaml.Node{Kind: yaml.MappingNode},
}
for _, field := range st.Fields {
propName := getJSONFieldName(field)
addToMap(&schema.Properties, propName, buildProperty(field, knownTypes, aliasToCanonical))
if !strings.HasPrefix(field.Type, "*") && !field.OmitEmpty {
schema.Required = append(schema.Required, propName)
}
}
return schema
}
func buildEnumSchema(alias TypeAlias, cg ConstGroup) xtpEnumSchema {
values := make([]string, 0, len(cg.Values))
for _, cv := range cg.Values {
values = append(values, strings.Trim(cv.Value, `"`))
}
return xtpEnumSchema{
Description: cleanDocForYAML(alias.Doc),
Type: "string",
Enum: values,
}
}
func buildProperty(field FieldDef, knownTypes map[string]bool, aliasToCanonical map[string]string) xtpProperty {
goType := field.Type
isPointer := strings.HasPrefix(goType, "*")
if isPointer {
goType = goType[1:]
}
prop := xtpProperty{
Description: cleanDocForYAML(field.Doc),
Nullable: isPointer,
}
if refType := strings.TrimPrefix(goType, sharedTypesPrefix); isKnownType(refType, knownTypes) && !strings.HasPrefix(goType, "[]") {
prop.Ref = "#/components/schemas/" + canonicalRefName(refType, aliasToCanonical)
return prop
}
if isPrimitiveGoType(goType) {
prop.Type, prop.Format = goTypeToXTPTypeAndFormat(goType)
return prop
}
if strings.HasPrefix(goType, "[]") {
elemType := strings.TrimPrefix(goType[2:], sharedTypesPrefix)
prop.Type = "array"
prop.Items = &xtpProperty{}
if isKnownType(elemType, knownTypes) {
prop.Items.Ref = "#/components/schemas/" + canonicalRefName(elemType, aliasToCanonical)
} else {
prop.Items.Type = goTypeToXTPType(elemType)
}
return prop
}
prop.Type, prop.Format = goTypeToXTPTypeAndFormat(goType)
return prop
}
func addToMap[T any](node *yaml.Node, key string, value T) {
var valNode yaml.Node
_ = valNode.Encode(value)
node.Content = append(node.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: key}, &valNode)
}
func getJSONFieldName(field FieldDef) string {
propName := field.JSONTag
if idx := strings.Index(propName, ","); idx >= 0 {
propName = propName[:idx]
}
if propName == "" {
propName = field.Name
}
return propName
}
func isKnownType(typeName string, knownTypes map[string]bool) bool {
return knownTypes[typeName]
}
func goTypeToXTPType(goType string) string {
typ, _ := goTypeToXTPTypeAndFormat(goType)
return typ
}
func goTypeToXTPTypeAndFormat(goType string) (typ, format string) {
switch goType {
case "string":
return "string", ""
case "int", "int32":
return "integer", "int32"
case "int64":
return "integer", "int64"
case "uint", "uint32":
return "integer", "int64"
case "uint64":
return "integer", "int64"
case "float32":
return "number", "float"
case "float64":
return "number", "float"
case "bool":
return "boolean", ""
case "[]byte":
return "string", "byte"
default:
return "object", ""
}
}
func cleanDocForYAML(doc string) string {
doc = strings.TrimSpace(doc)
lines := strings.Split(doc, "\n")
for i, line := range lines {
lines[i] = strings.TrimPrefix(strings.TrimSpace(line), "// ")
}
return strings.TrimSpace(strings.Join(lines, "\n"))
}