V0.6.2 docs openapi (#37)
All checks were successful
All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #37.
This commit is contained in:
134
server/handlers/docs.go
Normal file
134
server/handlers/docs.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DocsHandler serves documentation files.
|
||||
type DocsHandler struct {
|
||||
docsDir string
|
||||
}
|
||||
|
||||
// NewDocsHandler creates a new DocsHandler.
|
||||
func NewDocsHandler(docsDir string) *DocsHandler {
|
||||
return &DocsHandler{docsDir: docsDir}
|
||||
}
|
||||
|
||||
// docEntry describes a documentation file.
|
||||
type docEntry struct {
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// docsOrder defines the display order and metadata for documentation files.
|
||||
var docsOrder = []docEntry{
|
||||
{Slug: "GETTING-STARTED", Title: "Getting Started", Description: "Quick start guide"},
|
||||
{Slug: "ARCHITECTURE", Title: "Architecture", Description: "Kernel design and components"},
|
||||
{Slug: "EXTENSION-GUIDE", Title: "Extension Guide", Description: "How to author extensions"},
|
||||
{Slug: "PACKAGE-FORMAT", Title: "Package Format", Description: "The .pkg archive format"},
|
||||
{Slug: "API-REFERENCE", Title: "API Reference", Description: "REST API overview"},
|
||||
{Slug: "DEPLOYMENT", Title: "Deployment", Description: "Production deployment guide"},
|
||||
{Slug: "DISTRIBUTION", Title: "Distribution", Description: "Docker image and bundled packages"},
|
||||
}
|
||||
|
||||
// ListDocs returns the list of available documentation files.
|
||||
// GET /api/v1/docs
|
||||
func (h *DocsHandler) ListDocs(c *gin.Context) {
|
||||
if h.docsDir == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"data": []docEntry{}})
|
||||
return
|
||||
}
|
||||
|
||||
var available []docEntry
|
||||
for _, d := range docsOrder {
|
||||
path := filepath.Join(h.docsDir, d.Slug+".md")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
available = append(available, d)
|
||||
}
|
||||
}
|
||||
|
||||
// Also add any .md files not in the ordered list
|
||||
entries, err := os.ReadDir(h.docsDir)
|
||||
if err == nil {
|
||||
known := make(map[string]bool)
|
||||
for _, d := range docsOrder {
|
||||
known[d.Slug] = true
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
|
||||
continue
|
||||
}
|
||||
slug := strings.TrimSuffix(e.Name(), ".md")
|
||||
if known[slug] {
|
||||
continue
|
||||
}
|
||||
// Skip design docs and other non-user-facing files
|
||||
if strings.HasPrefix(slug, "DESIGN-") || strings.HasPrefix(slug, "DEMO-") {
|
||||
continue
|
||||
}
|
||||
available = append(available, docEntry{
|
||||
Slug: slug,
|
||||
Title: slugToTitle(slug),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if available == nil {
|
||||
available = []docEntry{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": available})
|
||||
}
|
||||
|
||||
// GetDoc returns the raw markdown content of a documentation file.
|
||||
// GET /api/v1/docs/:name
|
||||
func (h *DocsHandler) GetDoc(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if !isValidDocName(name) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid document name"})
|
||||
return
|
||||
}
|
||||
|
||||
if h.docsDir == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "docs not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
path := filepath.Join(h.docsDir, name+".md")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "document not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": gin.H{
|
||||
"slug": name,
|
||||
"title": slugToTitle(name),
|
||||
"content": string(data),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
var validDocNameRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||
|
||||
func isValidDocName(name string) bool {
|
||||
return validDocNameRe.MatchString(name) && !strings.Contains(name, "..")
|
||||
}
|
||||
|
||||
func slugToTitle(slug string) string {
|
||||
// Convert "GETTING-STARTED" → "Getting Started"
|
||||
parts := strings.Split(slug, "-")
|
||||
for i, p := range parts {
|
||||
if len(p) > 0 {
|
||||
parts[i] = strings.ToUpper(p[:1]) + strings.ToLower(p[1:])
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
Reference in New Issue
Block a user