2026-10-01 18:54:02 +02:00
|
|
|
package openapi
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"net/http"
|
|
|
|
|
"reflect"
|
|
|
|
|
"regexp"
|
|
|
|
|
"sort"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
2026-10-02 21:44:26 +02:00
|
|
|
|
|
|
|
|
"github.com/danyel/go-loose/client"
|
2026-10-01 18:54:02 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Operation struct {
|
|
|
|
|
Summary string
|
|
|
|
|
Request any
|
|
|
|
|
Response any
|
|
|
|
|
SuccessStatus int
|
|
|
|
|
Protected bool
|
|
|
|
|
RequestContentType string
|
|
|
|
|
ResponseContentType string
|
|
|
|
|
QueryParameters []string
|
|
|
|
|
NoTenant bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Builder struct {
|
|
|
|
|
paths map[string]any
|
|
|
|
|
schemas map[string]any
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ParticipantForm struct {
|
|
|
|
|
FirstName string `json:"firstName"`
|
|
|
|
|
LastName string `json:"lastName"`
|
|
|
|
|
Birthday string `json:"birthday,omitempty"`
|
|
|
|
|
Email string `json:"email"`
|
|
|
|
|
ContactInfo string `json:"contactInfo"`
|
|
|
|
|
Photo []byte `json:"photo,omitempty" openapi:"binary"`
|
|
|
|
|
CV []byte `json:"cv,omitempty" openapi:"binary"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CVForm struct {
|
|
|
|
|
CV []byte `json:"cv" openapi:"binary"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func New() *Builder {
|
|
|
|
|
return &Builder{paths: make(map[string]any), schemas: make(map[string]any)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Builder) Add(method, path string, operation Operation) {
|
|
|
|
|
method = strings.ToLower(method)
|
|
|
|
|
status := operation.SuccessStatus
|
|
|
|
|
if status == 0 {
|
|
|
|
|
status = http.StatusOK
|
|
|
|
|
}
|
|
|
|
|
contentType := operation.ResponseContentType
|
|
|
|
|
if contentType == "" {
|
|
|
|
|
contentType = "application/json"
|
|
|
|
|
}
|
|
|
|
|
success := map[string]any{"description": http.StatusText(status)}
|
|
|
|
|
if operation.Response != nil {
|
|
|
|
|
responseFormat := ""
|
|
|
|
|
if contentType == "application/octet-stream" {
|
|
|
|
|
responseFormat = "binary"
|
|
|
|
|
}
|
|
|
|
|
success["content"] = map[string]any{
|
|
|
|
|
contentType: map[string]any{"schema": b.schema(reflect.TypeOf(operation.Response), responseFormat)},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
responses := map[string]any{
|
|
|
|
|
strconv.Itoa(status): success,
|
|
|
|
|
"default": map[string]any{
|
|
|
|
|
"description": "Error response",
|
|
|
|
|
"content": map[string]any{
|
|
|
|
|
"application/json": map[string]any{
|
|
|
|
|
"schema": b.schema(reflect.TypeOf(errorResponse{}), ""),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
value := map[string]any{
|
|
|
|
|
"operationId": operationID(method, path),
|
|
|
|
|
"summary": operation.Summary,
|
|
|
|
|
"tags": []string{tagFor(path)},
|
|
|
|
|
"parameters": b.parameters(path, operation.QueryParameters, !operation.NoTenant),
|
|
|
|
|
"responses": responses,
|
|
|
|
|
}
|
|
|
|
|
if operation.Protected {
|
2026-10-02 21:44:26 +02:00
|
|
|
value["security"] = []map[string][]string{{
|
|
|
|
|
"bearerAuth": {},
|
|
|
|
|
}}
|
2026-10-01 18:54:02 +02:00
|
|
|
}
|
|
|
|
|
if operation.Request != nil {
|
|
|
|
|
requestContentType := operation.RequestContentType
|
|
|
|
|
if requestContentType == "" {
|
|
|
|
|
requestContentType = "application/json"
|
|
|
|
|
}
|
|
|
|
|
value["requestBody"] = map[string]any{
|
|
|
|
|
"required": true,
|
|
|
|
|
"content": map[string]any{
|
|
|
|
|
requestContentType: map[string]any{
|
|
|
|
|
"schema": b.schema(reflect.TypeOf(operation.Request), ""),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pathItem, _ := b.paths[path].(map[string]any)
|
|
|
|
|
if pathItem == nil {
|
|
|
|
|
pathItem = make(map[string]any)
|
|
|
|
|
b.paths[path] = pathItem
|
|
|
|
|
}
|
|
|
|
|
pathItem[method] = value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Builder) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_ = json.NewEncoder(w).Encode(b.Document())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Builder) Document() map[string]any {
|
|
|
|
|
return map[string]any{
|
|
|
|
|
"openapi": "3.0.3",
|
|
|
|
|
"info": map[string]any{
|
|
|
|
|
"title": "Go Guess API",
|
|
|
|
|
"version": "1.0.0",
|
|
|
|
|
},
|
|
|
|
|
"servers": []map[string]string{{"url": "/"}},
|
|
|
|
|
"paths": b.paths,
|
|
|
|
|
"components": map[string]any{
|
|
|
|
|
"securitySchemes": map[string]any{
|
|
|
|
|
"bearerAuth": map[string]any{"type": "http", "scheme": "bearer", "bearerFormat": "JWT"},
|
|
|
|
|
},
|
|
|
|
|
"schemas": b.schemas,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Builder) parameters(path string, queries []string, tenantRequired bool) []map[string]any {
|
|
|
|
|
result := make([]map[string]any, 0)
|
|
|
|
|
if tenantRequired {
|
|
|
|
|
result = append(result, map[string]any{
|
2026-10-02 21:44:26 +02:00
|
|
|
"name": client.XTenantId, "in": "header", "required": true,
|
2026-10-01 18:54:02 +02:00
|
|
|
"schema": map[string]any{"type": "string"},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
for _, match := range regexp.MustCompile(`\{([^}]+)\}`).FindAllStringSubmatch(path, -1) {
|
|
|
|
|
name := match[1]
|
|
|
|
|
kind := "integer"
|
|
|
|
|
if name == "token" {
|
|
|
|
|
kind = "string"
|
|
|
|
|
}
|
|
|
|
|
result = append(result, map[string]any{
|
|
|
|
|
"name": name, "in": "path", "required": true,
|
|
|
|
|
"schema": map[string]any{"type": kind},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
for _, name := range queries {
|
|
|
|
|
result = append(result, map[string]any{
|
|
|
|
|
"name": name, "in": "query", "required": false,
|
|
|
|
|
"schema": map[string]any{"type": "string"},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Builder) schema(value reflect.Type, format string) map[string]any {
|
|
|
|
|
for value.Kind() == reflect.Pointer {
|
|
|
|
|
value = value.Elem()
|
|
|
|
|
}
|
|
|
|
|
if format == "binary" {
|
|
|
|
|
return map[string]any{"type": "string", "format": "binary"}
|
|
|
|
|
}
|
|
|
|
|
if value == reflect.TypeOf(time.Time{}) {
|
|
|
|
|
return map[string]any{"type": "string", "format": "date-time"}
|
|
|
|
|
}
|
|
|
|
|
switch value.Kind() {
|
|
|
|
|
case reflect.Struct:
|
|
|
|
|
name := value.Name()
|
|
|
|
|
if _, exists := b.schemas[name]; !exists {
|
|
|
|
|
b.schemas[name] = map[string]any{}
|
|
|
|
|
properties := make(map[string]any)
|
|
|
|
|
required := make([]string, 0)
|
|
|
|
|
for index := range value.NumField() {
|
|
|
|
|
field := value.Field(index)
|
|
|
|
|
tag := strings.Split(field.Tag.Get("json"), ",")
|
|
|
|
|
if tag[0] == "-" || tag[0] == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
properties[tag[0]] = b.schema(field.Type, field.Tag.Get("openapi"))
|
|
|
|
|
if !contains(tag[1:], "omitempty") {
|
|
|
|
|
required = append(required, tag[0])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sort.Strings(required)
|
|
|
|
|
component := map[string]any{"type": "object", "properties": properties}
|
|
|
|
|
if len(required) > 0 {
|
|
|
|
|
component["required"] = required
|
|
|
|
|
}
|
|
|
|
|
b.schemas[name] = component
|
|
|
|
|
}
|
|
|
|
|
return map[string]any{"$ref": "#/components/schemas/" + name}
|
|
|
|
|
case reflect.Slice, reflect.Array:
|
|
|
|
|
if value.Elem().Kind() == reflect.Uint8 {
|
|
|
|
|
return map[string]any{"type": "string", "format": "byte"}
|
|
|
|
|
}
|
|
|
|
|
return map[string]any{"type": "array", "items": b.schema(value.Elem(), "")}
|
|
|
|
|
case reflect.Map:
|
|
|
|
|
return map[string]any{"type": "object", "additionalProperties": b.schema(value.Elem(), "")}
|
|
|
|
|
case reflect.Bool:
|
|
|
|
|
return map[string]any{"type": "boolean"}
|
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
|
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
|
|
|
return map[string]any{"type": "integer"}
|
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
|
|
|
return map[string]any{"type": "number"}
|
|
|
|
|
default:
|
|
|
|
|
return map[string]any{"type": "string"}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type errorResponse struct {
|
|
|
|
|
Error string `json:"error"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func contains(values []string, expected string) bool {
|
|
|
|
|
for _, value := range values {
|
|
|
|
|
if value == expected {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func operationID(method, path string) string {
|
|
|
|
|
value := strings.NewReplacer("/", "_", "{", "", "}", "", "-", "_").Replace(path)
|
|
|
|
|
return method + strings.Trim(value, "_")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func tagFor(path string) string {
|
|
|
|
|
parts := strings.Split(strings.TrimPrefix(path, "/api/"), "/")
|
|
|
|
|
if len(parts) == 0 || parts[0] == "" {
|
|
|
|
|
return "system"
|
|
|
|
|
}
|
|
|
|
|
return parts[0]
|
|
|
|
|
}
|