// 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"},
},
},
},
},
},
},
}
}