// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package v2 import ( "net/http" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/context" ) // scalarTemplate is the HTML template for Scalar API documentation // Scalar is a modern, AI-friendly API documentation tool const scalarTemplate = ` {{.AppName}} API v2 Reference ` // DocsScalar serves the Scalar API documentation UI for v2 func DocsScalar(ctx *context.APIContext) { data := map[string]string{ "AppName": setting.AppName, "SpecURL": setting.AppSubURL + "/api/v2/swagger.json", } ctx.Resp.Header().Set("Content-Type", "text/html; charset=utf-8") ctx.Resp.WriteHeader(http.StatusOK) // Simple template rendering html := scalarTemplate for key, value := range data { html = replaceTemplateVar(html, key, value) } ctx.Resp.Write([]byte(html)) } func replaceTemplateVar(template, key, value string) string { placeholder := "{{." + key + "}}" result := template for i := 0; i < 10; i++ { // Replace up to 10 occurrences newResult := "" idx := 0 for { pos := indexOf(result[idx:], placeholder) if pos == -1 { newResult += result[idx:] break } newResult += result[idx : idx+pos] newResult += value idx = idx + pos + len(placeholder) } if newResult == result { break } result = newResult } return result } func indexOf(s, substr string) int { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return i } } return -1 } // SwaggerJSON serves the OpenAPI/Swagger JSON specification for v2 func SwaggerJSON(ctx *context.APIContext) { spec := generateOpenAPISpec() ctx.JSON(http.StatusOK, spec) } // generateOpenAPISpec creates the OpenAPI 3.0 specification for v2 API func generateOpenAPISpec() map[string]any { return map[string]any{ "openapi": "3.0.3", "info": map[string]any{ "title": setting.AppName + " API v2", "description": "Gitea API v2 with structured error codes, batch operations, and AI-friendly endpoints.", "version": "2.0.0", "contact": map[string]string{ "name": "Gitea", "url": "https://gitea.com", }, "license": map[string]string{ "name": "MIT", "url": "https://opensource.org/licenses/MIT", }, }, "servers": []map[string]string{ {"url": setting.AppURL + "api/v2", "description": "Current server"}, }, "tags": []map[string]string{ {"name": "miscellaneous", "description": "General API information"}, {"name": "user", "description": "User operations"}, {"name": "repository", "description": "Repository operations"}, {"name": "batch", "description": "Batch operations for bulk actions"}, {"name": "ai", "description": "AI-friendly context endpoints"}, }, "paths": map[string]any{ "/version": map[string]any{ "get": map[string]any{ "tags": []string{"miscellaneous"}, "summary": "Get API version", "operationId": "getVersion", "responses": map[string]any{ "200": map[string]any{ "description": "Version information", "content": map[string]any{ "application/json": map[string]any{ "schema": map[string]any{ "$ref": "#/components/schemas/Version", }, }, }, }, }, }, }, "/user": map[string]any{ "get": map[string]any{ "tags": []string{"user"}, "summary": "Get authenticated user", "operationId": "getAuthenticatedUser", "security": []map[string][]string{{"bearerAuth": {}}}, "responses": map[string]any{ "200": map[string]any{ "description": "User information", "content": map[string]any{ "application/json": map[string]any{ "schema": map[string]any{ "$ref": "#/components/schemas/User", }, }, }, }, "401": map[string]any{ "description": "Authentication required", "content": map[string]any{ "application/json": map[string]any{ "schema": map[string]any{ "$ref": "#/components/schemas/APIError", }, }, }, }, }, }, }, }, "components": map[string]any{ "securitySchemes": map[string]any{ "bearerAuth": map[string]any{ "type": "http", "scheme": "bearer", "description": "API token authentication", "bearerFormat": "token", }, "basicAuth": map[string]any{ "type": "http", "scheme": "basic", "description": "Basic authentication with username and password", }, }, "schemas": map[string]any{ "Version": map[string]any{ "type": "object", "properties": map[string]any{ "version": map[string]string{"type": "string", "description": "Gitea version"}, "api": map[string]string{"type": "string", "description": "API version"}, }, }, "User": map[string]any{ "type": "object", "properties": map[string]any{ "id": map[string]string{"type": "integer", "description": "User ID"}, "login": map[string]string{"type": "string", "description": "Username"}, "email": map[string]string{"type": "string", "description": "Email address"}, "is_admin": map[string]string{"type": "boolean", "description": "Is site admin"}, }, }, "APIError": map[string]any{ "type": "object", "description": "Structured error response following RFC 7807", "properties": map[string]any{ "error": map[string]any{ "type": "object", "properties": map[string]any{ "code": map[string]string{"type": "string", "description": "Machine-readable error code"}, "message": map[string]string{"type": "string", "description": "Human-readable error message"}, "status": map[string]string{"type": "integer", "description": "HTTP status code"}, "documentation_url": map[string]string{"type": "string", "description": "URL to error documentation"}, "request_id": map[string]string{"type": "string", "description": "Request ID for tracing"}, "details": map[string]string{"type": "object", "description": "Additional error context"}, "suggestions": map[string]any{"type": "array", "items": map[string]string{"type": "string"}, "description": "Suggested actions"}, }, }, }, }, }, }, } }