gitea/routers/api/v2/docs.go
logikonline 9094d8b503 feat(api): add v2 API with AI-friendly features (Phase 2)
This introduces a new v2 API at /api/v2/ with features designed for
AI agents and automation tools while maintaining full backward
compatibility with the existing v1 API.

New features:
- Structured error codes (70+ machine-readable codes) for precise
  error handling by automated tools
- Scalar API documentation at /api/v2/docs (modern replacement for
  Swagger UI)
- Batch operations for bulk file and repository fetching
- NDJSON streaming endpoints for files, commits, and issues
- AI context endpoints providing rich repository summaries,
  navigation hints, and issue context

Files added:
- modules/errors/codes.go - Error code definitions and catalog
- modules/errors/api_error.go - Rich API error response builder
- routers/api/v2/api.go - v2 router with auth middleware
- routers/api/v2/docs.go - Scalar docs and OpenAPI spec
- routers/api/v2/batch.go - Batch file/repo operations
- routers/api/v2/streaming.go - NDJSON streaming endpoints
- routers/api/v2/ai_context.go - AI context endpoints
- routers/api/v2/misc.go - Version and user endpoints

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 11:41:10 -05:00

248 lines
7.5 KiB
Go

// 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 = `<!DOCTYPE html>
<html>
<head>
<title>{{.AppName}} API v2 Reference</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
/* Custom theme to match Gitea */
:root {
--scalar-color-1: #4a90d9;
--scalar-color-accent: #4a90d9;
--scalar-background-1: #ffffff;
--scalar-background-2: #f8f9fa;
--scalar-background-3: #e9ecef;
}
@media (prefers-color-scheme: dark) {
:root {
--scalar-background-1: #1a1a1a;
--scalar-background-2: #252525;
--scalar-background-3: #333333;
}
}
</style>
</head>
<body>
<script
id="api-reference"
data-url="{{.SpecURL}}"
data-configuration='{
"theme": "default",
"layout": "modern",
"showSidebar": true,
"hideModels": false,
"hideDownloadButton": false,
"hiddenClients": [],
"defaultHttpClient": {
"targetKey": "shell",
"clientKey": "curl"
}
}'>
</script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>`
// 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"},
},
},
},
},
},
},
}
}