package docs import ( "os" "path/filepath" "strings" ) // DocFrontmatter is the machine-readable identity header of an API doc page. // It is the contract the drift gate (drift_test.go) checks against the real // router registrations and the schema registry (schema.go). type DocFrontmatter struct { Method string // GET|POST|PUT|PATCH|DELETE Path string // Echo syntax, e.g. /api/collections/:id Auth string // none|jwt|jwt+admin|device-token|optional-jwt Area string // sidebar area, e.g. collections } // APIDoc is one endpoint doc file: its frontmatter plus the labeled example // blocks extracted from the body. type APIDoc struct { RelPath string Frontmatter DocFrontmatter ExampleRequest []byte ExampleResponse []byte HasExampleResponse bool Unverified []string // UNVERIFIED markers found in the body } // parseFrontmatter splits a leading "---" delimited block of flat key:value // lines off content. Recognized keys: method, path, auth, area. Returns the // frontmatter and the remaining body; hasFM is false when no well-formed // block is present (prose pages like overviews have none and are skipped by // the gate). func parseFrontmatter(content string) (fm DocFrontmatter, body string, hasFM bool) { lines := strings.Split(content, "\n") if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" { return fm, content, false } end := -1 for i := 1; i < len(lines); i++ { if strings.TrimSpace(lines[i]) == "---" { end = i break } } if end == -1 { return fm, content, false } for _, line := range lines[1:end] { key, value, ok := strings.Cut(line, ":") if !ok { continue } key = strings.ToLower(strings.TrimSpace(key)) value = strings.TrimSpace(value) switch key { case "method": fm.Method = strings.ToUpper(value) case "path": fm.Path = value case "auth": fm.Auth = strings.ToLower(value) case "area": fm.Area = strings.ToLower(value) } } if fm.Method == "" || fm.Path == "" { return fm, content, false // identity is the minimum for a gateable doc } return fm, strings.Join(lines[end+1:], "\n"), true } // extractExampleBlocks pulls fenced code blocks tagged // "json example-request" / "json example-response" out of a doc body, and // counts UNVERIFIED markers. func extractExampleBlocks(body string, doc *APIDoc) { lines := strings.Split(body, "\n") for i := 0; i < len(lines); i++ { fence := strings.TrimSpace(lines[i]) switch { case fence == "```json example-request": doc.ExampleRequest = extractUntilClose(lines, i+1) i = skipBlock(lines, i+1) case fence == "```json example-response": doc.ExampleResponse = extractUntilClose(lines, i+1) doc.HasExampleResponse = true i = skipBlock(lines, i+1) } if strings.Contains(lines[i], "UNVERIFIED:") { doc.Unverified = append(doc.Unverified, strings.TrimSpace(lines[i])) } } } func extractUntilClose(lines []string, start int) []byte { var b strings.Builder for i := start; i < len(lines); i++ { if strings.TrimSpace(lines[i]) == "```" { break } b.WriteString(lines[i]) b.WriteString("\n") } return []byte(b.String()) } // skipBlock returns the index of the closing fence line so the outer loop // resumes after it. func skipBlock(lines []string, start int) int { for i := start; i < len(lines); i++ { if strings.TrimSpace(lines[i]) == "```" { return i } } return len(lines) - 1 } // LoadAPIDocs walks dir recursively and returns every markdown file carrying // well-formed endpoint frontmatter. Prose pages (overviews, contracts) are // skipped: the gate only checks docs that claim to describe an endpoint. func LoadAPIDocs(dir string) ([]APIDoc, error) { var docs []APIDoc err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { if err != nil || d.IsDir() || !strings.HasSuffix(path, ".md") { return err } raw, err := os.ReadFile(path) if err != nil { return err } fm, body, hasFM := parseFrontmatter(string(raw)) if !hasFM { return nil } doc := APIDoc{ RelPath: path, Frontmatter: fm, } extractExampleBlocks(body, &doc) docs = append(docs, doc) return nil }) return docs, err }