package main import ( "fmt" "net/http" "slices" "sort" "strings" ) // The API reference page: the REST operations as the OpenAPI document has // them (both come from apiRoutes and the tools' schemas), each with a curl // example for this router. type apiParam struct { Name, Type, Description string Required bool } type apiDoc struct { Method, Path, Summary, Scope, Description string Params []apiParam Body string // what the request body is, if any Curl string } // apiExamples are the query strings of the examples (what a person would try). var apiExamples = map[string]string{ "get_logs": "?unit=router-pppoe&lines=50&grep=connect", } func apiReference(host string) []apiDoc { var docs []apiDoc for _, rt := range apiRoutes { tool := toolNamed(rt.tool) d := apiDoc{Method: rt.method, Path: rt.path, Summary: rt.summary, Scope: tool.scope, Description: tool.description} url := "https://" + host + rt.path switch rt.method { case "GET": props, _ := tool.schema["properties"].(map[string]any) required, _ := tool.schema["required"].([]string) for name, p := range props { pm, _ := p.(map[string]any) typ, _ := pm["type"].(string) desc, _ := pm["description"].(string) d.Params = append(d.Params, apiParam{name, typ, desc, slices.Contains(required, name)}) } slices.SortFunc(d.Params, func(a, b apiParam) int { if a.Required != b.Required { if a.Required { return -1 } return 1 } return strings.Compare(a.Name, b.Name) }) d.Curl = fmt.Sprintf("curl -sk -H \"Authorization: Bearer $TOKEN\" '%s%s'", url, apiExamples[rt.tool]) case "PATCH": d.Params = []apiParam{{"If-Match (header)", "string", "The ETag from GET /api/v1/settings; the change is refused (409) if the settings changed since.", true}} d.Body = "A JSON merge patch (RFC 7386) on the settings: objects merge, a value replaces, null removes a key, arrays are replaced whole. Content-Type application/merge-patch+json or application/json." d.Curl = fmt.Sprintf("curl -sk -X PATCH -H \"Authorization: Bearer $TOKEN\" -H 'If-Match: \"ETAG\"' \\\n -H 'Content-Type: application/merge-patch+json' \\\n -d '{\"router\":{\"ddns\":{\"ipv6\":false}}}' %s", url) default: d.Curl = fmt.Sprintf("curl -sk -X %s -H \"Authorization: Bearer $TOKEN\" %s", rt.method, url) } docs = append(docs, d) } return docs } func (a *app) apiReference(w http.ResponseWriter, r *http.Request, sess session) { a.render(w, "apiref", page{Title: "API reference", CSRF: sess.csrf, LoggedIn: true, Data: map[string]any{ "Base": "https://" + r.Host, "Ops": apiReference(r.Host), "HasOptions": a.optionsFile != "", "Areas": settingsAreas(a.optionsFile), "Host": r.Host, }}) } type areaOption struct{ Name, Type, Default, Description string } type areaDoc struct { Path string Options []areaOption } // settingsAreas lists the areas below router/ with their options (from the // option documentation): what a person can read and write there. func settingsAreas(optionsFile string) []areaDoc { if optionsFile == "" { return nil } entries, err := loadOptions(optionsFile) if err != nil { return nil } byArea := map[string][]areaOption{} for _, e := range entries { if len(e.Loc) < 3 || e.Loc[0] != "router" { continue } desc := strings.TrimSpace(e.Description) if i := strings.Index(desc, "\n\n"); i > 0 { desc = desc[:i] } desc = strings.Join(strings.Fields(desc), " ") if len(desc) > 300 { desc = desc[:300] + "…" } name := strings.Join(e.Loc[2:], ".") byArea[e.Loc[1]] = append(byArea[e.Loc[1]], areaOption{name, e.Type, literal(e.Default), desc}) } var areas []areaDoc for k, opts := range byArea { sort.Slice(opts, func(i, j int) bool { return opts[i].Name < opts[j].Name }) areas = append(areas, areaDoc{Path: "/api/v1/settings/router/" + k, Options: opts}) } sort.Slice(areas, func(i, j int) bool { return areas[i].Path < areas[j].Path }) return areas }