Last active
May 29, 2025 08:11
-
-
Save 17twenty/346db477b73867c51d80e844004fea36 to your computer and use it in GitHub Desktop.
Create a Golang server from TypeAPI JSON
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"operations": { | |
"user.getById": { | |
"description": "Get user profile by ID", | |
"method": "GET", | |
"path": "/users/{id}", | |
"arguments": { | |
"id": { | |
"in": "path", | |
"schema": { | |
"type": "integer" | |
} | |
} | |
}, | |
"return": { | |
"schema": { | |
"type": "reference", | |
"target": "UserProfile" | |
} | |
}, | |
"throws": [{ | |
"code": 404, | |
"schema": { | |
"type": "reference", | |
"target": "Error" | |
} | |
}] | |
}, | |
"user.create": { | |
"description": "Create a new user", | |
"method": "POST", | |
"path": "/users", | |
"arguments": { | |
"payload": { | |
"in": "body", | |
"schema": { | |
"type": "reference", | |
"target": "CreateUserRequest" | |
} | |
} | |
}, | |
"return": { | |
"schema": { | |
"type": "reference", | |
"target": "UserProfile" | |
} | |
}, | |
"throws": [{ | |
"code": 400, | |
"schema": { | |
"type": "reference", | |
"target": "Error" | |
} | |
}] | |
}, | |
"user.list": { | |
"description": "List users with pagination", | |
"method": "GET", | |
"path": "/users", | |
"arguments": { | |
"limit": { | |
"in": "query", | |
"schema": { | |
"type": "integer" | |
} | |
}, | |
"offset": { | |
"in": "query", | |
"schema": { | |
"type": "integer" | |
} | |
} | |
}, | |
"return": { | |
"schema": { | |
"type": "reference", | |
"target": "UserList" | |
} | |
} | |
}, | |
"health.getData": { | |
"description": "Get user health data", | |
"method": "GET", | |
"path": "/users/{userId}/health", | |
"arguments": { | |
"userId": { | |
"in": "path", | |
"schema": { | |
"type": "integer" | |
} | |
} | |
}, | |
"return": { | |
"schema": { | |
"type": "reference", | |
"target": "HealthData" | |
} | |
}, | |
"throws": [{ | |
"code": 404, | |
"schema": { | |
"type": "reference", | |
"target": "Error" | |
} | |
}] | |
} | |
}, | |
"definitions": { | |
"UserProfile": { | |
"type": "struct", | |
"properties": { | |
"id": { | |
"type": "integer" | |
}, | |
"email": { | |
"type": "string" | |
}, | |
"name": { | |
"type": "string" | |
}, | |
"dateOfBirth": { | |
"type": "string" | |
}, | |
"bioGender": { | |
"type": "string" | |
}, | |
"height": { | |
"type": "number" | |
}, | |
"weight": { | |
"type": "number" | |
}, | |
"goal": { | |
"type": "string" | |
}, | |
"isEmailVerified": { | |
"type": "boolean" | |
}, | |
"createdAt": { | |
"type": "string" | |
} | |
} | |
}, | |
"CreateUserRequest": { | |
"type": "struct", | |
"properties": { | |
"email": { | |
"type": "string" | |
}, | |
"name": { | |
"type": "string" | |
}, | |
"password": { | |
"type": "string" | |
}, | |
"dateOfBirth": { | |
"type": "string" | |
}, | |
"bioGender": { | |
"type": "string" | |
} | |
} | |
}, | |
"UserList": { | |
"type": "struct", | |
"properties": { | |
"users": { | |
"type": "array", | |
"items": { | |
"type": "reference", | |
"target": "UserProfile" | |
} | |
}, | |
"total": { | |
"type": "integer" | |
}, | |
"limit": { | |
"type": "integer" | |
}, | |
"offset": { | |
"type": "integer" | |
} | |
} | |
}, | |
"HealthData": { | |
"type": "struct", | |
"properties": { | |
"userId": { | |
"type": "integer" | |
}, | |
"existingMedications": { | |
"type": "string" | |
}, | |
"medicalConditions": { | |
"type": "string" | |
}, | |
"allergies": { | |
"type": "string" | |
}, | |
"updatedAt": { | |
"type": "string" | |
} | |
} | |
}, | |
"Error": { | |
"type": "struct", | |
"properties": { | |
"code": { | |
"type": "integer" | |
}, | |
"message": { | |
"type": "string" | |
}, | |
"details": { | |
"type": "string" | |
} | |
} | |
} | |
}, | |
"security": { | |
"type": "httpBearer" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{"operations":{"user.getById":{"description":"Get user profile by ID","method":"GET","path":"/users/{id}","arguments":{"id":{"in":"path","schema":{"type":"integer"}}},"return":{"schema":{"type":"reference","target":"UserProfile"}},"throws":[{"code":404,"schema":{"type":"reference","target":"Error"}}]},"user.create":{"description":"Create a new user","method":"POST","path":"/users","arguments":{"payload":{"in":"body","schema":{"type":"reference","target":"CreateUserRequest"}}},"return":{"schema":{"type":"reference","target":"UserProfile"}},"throws":[{"code":400,"schema":{"type":"reference","target":"Error"}}]},"user.list":{"description":"List users with pagination","method":"GET","path":"/users","arguments":{"limit":{"in":"query","schema":{"type":"integer"}},"offset":{"in":"query","schema":{"type":"integer"}}},"return":{"schema":{"type":"reference","target":"UserList"}}}},"definitions":{"UserProfile":{"type":"struct","properties":{"id":{"type":"integer"},"email":{"type":"string"},"name":{"type":"string"},"dateOfBirth":{"type":"string"},"bioGender":{"type":"string"}}},"CreateUserRequest":{"type":"struct","properties":{"email":{"type":"string"},"name":{"type":"string"}}},"UserList":{"type":"struct","properties":{"users":{"type":"array","items":{"type":"reference","target":"UserProfile"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"Error":{"type":"struct","properties":{"code":{"type":"integer"},"message":{"type":"string"},"details":{"type":"string"}}}},"security":{"type":"httpBearer"}} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
// Run me like this: | |
// go run typeapi-gen.go "$SPEC_FILE" "$OUTPUT_DIR" | |
import ( | |
"encoding/json" | |
"fmt" | |
"io" | |
"log" | |
"os" | |
"path/filepath" | |
"sort" | |
"strings" | |
"text/template" | |
) | |
type TypeAPISpec struct { | |
Operations map[string]Operation `json:"operations"` | |
Definitions map[string]Definition `json:"definitions"` | |
Security *Security `json:"security,omitempty"` | |
} | |
type Operation struct { | |
Description string `json:"description,omitempty"` | |
Method string `json:"method"` | |
Path string `json:"path"` | |
Arguments map[string]Argument `json:"arguments,omitempty"` | |
Return *Return `json:"return,omitempty"` | |
Throws []ThrowsSpec `json:"throws,omitempty"` | |
} | |
type Argument struct { | |
In string `json:"in"` | |
Schema Schema `json:"schema"` | |
} | |
type Return struct { | |
Schema Schema `json:"schema"` | |
} | |
type ThrowsSpec struct { | |
Code int `json:"code"` | |
Schema Schema `json:"schema"` | |
} | |
type Schema struct { | |
Type string `json:"type"` | |
Target string `json:"target,omitempty"` | |
Properties map[string]Schema `json:"properties,omitempty"` | |
Items *Schema `json:"items,omitempty"` | |
} | |
type Definition struct { | |
Type string `json:"type"` | |
Properties map[string]Schema `json:"properties,omitempty"` | |
} | |
type Security struct { | |
Type string `json:"type"` | |
In string `json:"in,omitempty"` | |
Name string `json:"name,omitempty"` | |
TokenURL string `json:"tokenUrl,omitempty"` | |
AuthorizationURL string `json:"authorizationUrl,omitempty"` | |
Scopes []string `json:"scopes,omitempty"` | |
} | |
type GeneratedHandler struct { | |
Name string | |
Method string | |
Path string | |
Description string | |
Args []HandlerArg | |
ReturnType string | |
Errors []ErrorCase | |
} | |
type HandlerArg struct { | |
Name string | |
Type string | |
Source string // "query", "path", "body", "header" | |
GoType string | |
} | |
type ErrorCase struct { | |
Code int | |
Type string | |
} | |
const handlerTemplate = `package main | |
import ( | |
"encoding/json" | |
"log" | |
"net/http" | |
"strconv" | |
"github.com/gorilla/mux" | |
) | |
{{range .Definitions}} | |
{{.}} | |
{{end}} | |
{{range .Handlers}} | |
// {{.Description}} | |
func {{.Name}}Handler(w http.ResponseWriter, r *http.Request) { | |
{{range .Args}} | |
{{if eq .Source "query"}} | |
{{.Name}}Str := r.URL.Query().Get("{{.Name}}") | |
{{if eq .GoType "int"}} | |
var {{.Name}} int | |
if {{.Name}}Str != "" { | |
var err error | |
{{.Name}}, err = strconv.Atoi({{.Name}}Str) | |
if err != nil { | |
http.Error(w, "Invalid {{.Name}} parameter", http.StatusBadRequest) | |
return | |
} | |
} | |
{{else if eq .GoType "string"}} | |
{{.Name}} := {{.Name}}Str | |
{{end}} | |
{{else if eq .Source "path"}} | |
{{.Name}}Str := mux.Vars(r)["{{.Name}}"] | |
{{if eq .GoType "int"}} | |
{{.Name}}, err := strconv.Atoi({{.Name}}Str) | |
if err != nil { | |
http.Error(w, "Invalid {{.Name}} parameter", http.StatusBadRequest) | |
return | |
} | |
{{else if eq .GoType "string"}} | |
{{.Name}} := {{.Name}}Str | |
{{end}} | |
{{else if eq .Source "body"}} | |
var {{.Name}} {{.GoType}} | |
if err := json.NewDecoder(r.Body).Decode(&{{.Name}}); err != nil { | |
http.Error(w, "Invalid request body", http.StatusBadRequest) | |
return | |
} | |
{{end}} | |
{{end}} | |
// TODO: Implement your business logic here | |
// Call your service layer with the parsed arguments | |
{{if .ReturnType}} | |
result := {{.ReturnType}}{} | |
// Example implementation: | |
// result, err := yourService.{{.Name}}({{range $i, $arg := .Args}}{{if $i}}, {{end}}{{$arg.Name}}{{end}}) | |
// if err != nil { | |
// http.Error(w, err.Error(), http.StatusInternalServerError) | |
// return | |
// } | |
{{end}} | |
// Use variables to prevent "declared and not used" errors | |
{{range .Args}} | |
_ = {{.Name}} | |
{{end}} | |
w.Header().Set("Content-Type", "application/json") | |
{{if .ReturnType}} | |
if err := json.NewEncoder(w).Encode(result); err != nil { | |
http.Error(w, "Failed to encode response", http.StatusInternalServerError) | |
return | |
} | |
{{else}} | |
w.WriteHeader(http.StatusOK) | |
{{end}} | |
} | |
{{end}} | |
func SetupRoutes() *mux.Router { | |
router := mux.NewRouter() | |
{{range .Handlers}} | |
router.HandleFunc("{{.Path}}", {{.Name}}Handler).Methods("{{.Method}}") | |
{{end}} | |
return router | |
} | |
func main() { | |
router := SetupRoutes() | |
log.Println("Server starting on :8080") | |
log.Fatal(http.ListenAndServe(":8080", router)) | |
} | |
` | |
func main() { | |
if len(os.Args) < 2 { | |
log.Fatal("Usage: typeapi-gen <spec-file> [output-dir]") | |
} | |
specFile := os.Args[1] | |
outputDir := "." | |
if len(os.Args) > 2 { | |
outputDir = os.Args[2] | |
} | |
spec, err := parseTypeAPISpec(specFile) | |
if err != nil { | |
log.Fatalf("Failed to parse TypeAPI spec: %v", err) | |
} | |
generator := &Generator{spec: spec} | |
if err := generator.Generate(outputDir); err != nil { | |
log.Fatalf("Failed to generate code: %v", err) | |
} | |
fmt.Println("Successfully generated Go server code") | |
} | |
func parseTypeAPISpec(filename string) (*TypeAPISpec, error) { | |
file, err := os.Open(filename) | |
if err != nil { | |
return nil, err | |
} | |
defer file.Close() | |
data, err := io.ReadAll(file) | |
if err != nil { | |
return nil, err | |
} | |
var spec TypeAPISpec | |
if err := json.Unmarshal(data, &spec); err != nil { | |
return nil, err | |
} | |
return &spec, nil | |
} | |
type Generator struct { | |
spec *TypeAPISpec | |
} | |
func (g *Generator) Generate(outputDir string) error { | |
handlers := g.generateHandlers() | |
definitions := g.generateDefinitions() | |
data := struct { | |
Handlers []GeneratedHandler | |
Definitions []string | |
}{ | |
Handlers: handlers, | |
Definitions: definitions, | |
} | |
tmpl, err := template.New("handlers").Parse(handlerTemplate) | |
if err != nil { | |
return err | |
} | |
outputFile := filepath.Join(outputDir, "generated_server.go") | |
file, err := os.Create(outputFile) | |
if err != nil { | |
return err | |
} | |
defer file.Close() | |
return tmpl.Execute(file, data) | |
} | |
func (g *Generator) generateHandlers() []GeneratedHandler { | |
var handlers []GeneratedHandler | |
// Sort operation names for consistent output | |
var opNames []string | |
for name := range g.spec.Operations { | |
opNames = append(opNames, name) | |
} | |
sort.Strings(opNames) | |
for _, opName := range opNames { | |
op := g.spec.Operations[opName] | |
handler := GeneratedHandler{ | |
Name: toGoFunctionName(opName), | |
Method: strings.ToUpper(op.Method), | |
Path: g.convertPath(op.Path), | |
Description: op.Description, | |
Args: g.generateArguments(op.Arguments), | |
ReturnType: g.getReturnType(op.Return), | |
Errors: g.generateErrors(op.Throws), | |
} | |
handlers = append(handlers, handler) | |
} | |
return handlers | |
} | |
func (g *Generator) generateArguments(args map[string]Argument) []HandlerArg { | |
var handlerArgs []HandlerArg | |
for name, arg := range args { | |
handlerArg := HandlerArg{ | |
Name: name, | |
Type: arg.Schema.Type, | |
Source: arg.In, | |
GoType: g.schemaToGoType(arg.Schema), | |
} | |
handlerArgs = append(handlerArgs, handlerArg) | |
} | |
return handlerArgs | |
} | |
func (g *Generator) generateErrors(throws []ThrowsSpec) []ErrorCase { | |
var errors []ErrorCase | |
for _, t := range throws { | |
errors = append(errors, ErrorCase{ | |
Code: t.Code, | |
Type: g.schemaToGoType(t.Schema), | |
}) | |
} | |
return errors | |
} | |
func (g *Generator) getReturnType(ret *Return) string { | |
if ret == nil { | |
return "" | |
} | |
return g.schemaToGoType(ret.Schema) | |
} | |
func (g *Generator) generateDefinitions() []string { | |
var definitions []string | |
// Sort definition names for consistent output | |
var defNames []string | |
for name := range g.spec.Definitions { | |
defNames = append(defNames, name) | |
} | |
sort.Strings(defNames) | |
for _, name := range defNames { | |
def := g.spec.Definitions[name] | |
goStruct := g.generateStruct(name, def) | |
definitions = append(definitions, goStruct) | |
} | |
return definitions | |
} | |
func (g *Generator) generateStruct(name string, def Definition) string { | |
var builder strings.Builder | |
builder.WriteString(fmt.Sprintf("type %s struct {\n", toGoTypeName(name))) | |
// Sort property names for consistent output | |
var propNames []string | |
for propName := range def.Properties { | |
propNames = append(propNames, propName) | |
} | |
sort.Strings(propNames) | |
for _, propName := range propNames { | |
prop := def.Properties[propName] | |
goType := g.schemaToGoType(prop) | |
jsonTag := fmt.Sprintf("`json:\"%s\"`", propName) | |
builder.WriteString(fmt.Sprintf("\t%s %s %s\n", toGoFieldName(propName), goType, jsonTag)) | |
} | |
builder.WriteString("}") | |
return builder.String() | |
} | |
func (g *Generator) schemaToGoType(schema Schema) string { | |
switch schema.Type { | |
case "string": | |
return "string" | |
case "integer": | |
return "int" | |
case "number": | |
return "float64" | |
case "boolean": | |
return "bool" | |
case "array": | |
if schema.Items != nil { | |
itemType := g.schemaToGoType(*schema.Items) | |
return "[]" + itemType | |
} | |
return "[]interface{}" | |
case "reference": | |
if schema.Target != "" { | |
return toGoTypeName(schema.Target) | |
} | |
return "interface{}" | |
default: | |
return "interface{}" | |
} | |
} | |
func (g *Generator) convertPath(path string) string { | |
// Convert TypeAPI path parameters to Gorilla mux format | |
// e.g., /users/{id} stays as /users/{id} | |
return path | |
} | |
func toPascalCase(s string) string { | |
// Split by common delimiters | |
parts := strings.FieldsFunc(s, func(c rune) bool { | |
return c == '_' || c == '-' || c == '.' || c == ' ' | |
}) | |
var result strings.Builder | |
for _, part := range parts { | |
if len(part) > 0 { | |
result.WriteString(strings.ToUpper(string(part[0]))) | |
if len(part) > 1 { | |
result.WriteString(strings.ToLower(part[1:])) | |
} | |
} | |
} | |
return result.String() | |
} | |
func toGoTypeName(s string) string { | |
return toPascalCase(s) | |
} | |
func toGoFieldName(s string) string { | |
return toPascalCase(s) | |
} | |
func toGoFunctionName(s string) string { | |
// Convert operation names like "user.getById" to "UserGetById" | |
return toPascalCase(s) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment