kong/autodoc/admin-api/generate.lua (872 lines of code) (raw):

#!/usr/bin/env resty setmetatable(_G, nil) local lfs = require("lfs") local cjson = require("cjson") local general = require("autodoc.admin-api.general") local method_array = { "POST", "HEAD", "GET", "PATCH", "PUT", "DELETE", "OPTIONS", } -- Chicago-style prepositions to be lowercased, -- based on https://capitalizemytitle.com/ for _, p in ipairs({ "about", "above", "across", "after", "against", "along", "among", "around", "at", "before", "behind", "below", "beneath", "beside", "beyond", "by", "down", "during", "for", "from", "in", "inside", "into", "near", "of", "off", "on", "out", "outside", "over", "past", "through", "throughout", "to", "toward", "under", }) do general.title_exceptions[p] = p end local utils = { -- "EXAMPLE of teXT using dns" => "Example of Text Using DNS". titleize = function(str) local text = str:gsub("(%a[%w_'-]*)", function(word) local exception = general.title_exceptions[word:lower()] if exception then return exception else return word:sub(1,1):upper()..word:sub(2):lower() end end) -- force very first character uppercase return text:sub(1,1):upper()..text:sub(2) end } local KONG_PATH = os.getenv("KONG_PATH") or "." package.path = KONG_PATH .. "/?.lua;" .. KONG_PATH .. "/?/init.lua;" .. package.path local pok, kong_meta = pcall(require, "kong.meta") -- luacheck: ignore if not pok then error("failed loading Kong modules. please set the KONG_PATH environment variable.") end local admin_api_data = require("autodoc.admin-api.data.admin-api") local Endpoints = require("kong.api.endpoints") -- Minimal boilerplate so that module files can be loaded _KONG = require("kong.meta") -- luacheck: ignore kong = require("kong.global").new() -- luacheck: ignore kong.configuration = { -- luacheck: ignore loaded_plugins = {}, loaded_vaults = {}, } kong.db = require("kong.db").new({ -- luacheck: ignore database = "postgres", }) kong.configuration = { -- luacheck: ignore loaded_plugins = {}, loaded_vaults = {}, } -------------------------------------------------------------------------------- local function sortedpairs(tbl) local keys = {} for key, _ in pairs(tbl) do table.insert(keys, key) end table.sort(keys) local i = 0 return function() i = i + 1 local k = keys[i] return k, tbl[k] end end local function render(template, subs) subs = setmetatable(subs, { __index = function(_, k) error("failed applying autodoc template: no variable ${" .. k .. "}") end }) return (template:gsub("${([^}]+)}", subs)) end local function get_or_create(tbl, key) local v = tbl[key] if not v then v = {} tbl[key] = v end return v end local function to_singular(plural) return plural:gsub("s$", "") end local function entity_to_api_path(entity) return "kong/api/routes/" .. entity .. ".lua" end local function entity_to_schema_path(entity) return "kong/db/schema/entities/" .. entity .. ".lua" end local function cjson_encode(value) return (cjson.encode(value):gsub("\\/", "/"):gsub(",", ", ")) end -- A deterministic pseudo-UUID generator, to make autodoc idempotent. local gen_uuid local reset_uuid do local uuids = { "9748f662-7711-4a90-8186-dc02f10eb0f5", "4e3ad2e4-0bc4-4638-8e34-c84a417ba39b", "a5fb8d9b-a99d-40e9-9d35-72d42a62d83a", "51e77dc2-8f3e-4afa-9d0e-0e3bbbcfd515", "fc73f2af-890d-4f9b-8363-af8945001f7f", "4506673d-c825-444c-a25b-602e3c2ec16e", "d35165e2-d03e-461a-bdeb-dad0a112abfe", "af8330d3-dbdc-48bd-b1be-55b98608834b", "a9daa3ba-8186-4a0d-96e8-00d80ce7240b", "127dfc88-ed57-45bf-b77a-a9d3a152ad31", "9aa116fd-ef4a-4efa-89bf-a0b17c4be982", "ba641b07-e74a-430a-ab46-94b61e5ea66b", "ec1a1f6f-2aa4-4e58-93ff-b56368f19b27", "a4407883-c166-43fd-80ca-3ca035b0cdb7", "01c23299-839c-49a5-a6d5-8864c09184af", "ce44eef5-41ed-47f6-baab-f725cecf98c7", "02621eee-8309-4bf6-b36b-a82017a5393e", "66c7b5c4-4aaf-4119-af1e-ee3ad75d0af4", "7fca84d6-7d37-4a74-a7b0-93e576089a41", "d044b7d4-3dc2-4bbc-8e9f-6b7a69416df6", "a9b2107f-a214-47b3-add4-46b942187924", "04fbeacf-a9f1-4a5d-ae4a-b0407445db3f", "43429efd-b3a5-4048-94cb-5cc4029909bb", "d26761d5-83a4-4f24-ac6c-cff276f2b79c", "91020192-062d-416f-a275-9addeeaffaf2", "a2e013e8-7623-4494-a347-6d29108ff68b", "147f5ef0-1ed6-4711-b77f-489262f8bff7", "a3ad71a8-6685-4b03-a101-980a953544f6", "b87eb55d-69a1-41d2-8653-8d706eecefc0", "4e8d95d4-40f2-4818-adcb-30e00c349618", "58c8ccbb-eafb-4566-991f-2ed4f678fa70", "ea29aaa3-3b2d-488c-b90c-56df8e0dd8c6", "4fe14415-73d5-4f00-9fbc-c72a0fccfcb2", "a3395f66-2af6-4c79-bea2-1b6933764f80", "885a0392-ef1b-4de3-aacf-af3f1697ce2c", "f5a9c0ca-bdbb-490f-8928-2ca95836239a", "173a6cee-90d1-40a7-89cf-0329eca780a6", "bdab0e47-4e37-4f0b-8fd0-87d95cc4addc", "f00c6da4-3679-4b44-b9fb-36a19bd3ae83", "0c61e164-6171-4837-8836-8f5298726d53", "5027BBC1-508C-41F8-87F2-AB1801E9D5C3", "68FDB05B-7B08-47E9-9727-AF7F897CFF1A", "B2A30E8F-C542-49CF-8015-FB674987D1A5", "518BBE43-2454-4559-99B0-8E7D1CD3E8C8", "7C4747E9-E831-4ED8-9377-83A6F8A37603", } local ctr = 0 gen_uuid = function() ctr = ctr + 1 return assert(uuids[ctr]) end reset_uuid = function() ctr = 0 end end -------------------------------------------------------------------------------- -- Unindent a multi-line string for proper indenting in -- square brackets. -- -- Ex: -- unindent([[ -- hello world -- foo bar -- ]]) -- -- will return: "hello world\nfoo bar" local function unindent(str) local min = 2^31 local lines = {} str = (str:sub(-1) == "\n") and str or (str .. "\n") for line in str:gmatch("([^\n]*)\n") do local nonblank = line:match("()[^%s]") if nonblank and nonblank < min then min = nonblank end table.insert(lines, line) end for i, line in ipairs(lines) do lines[i] = line:sub(min) end return table.concat(lines, "\n") end local function each_field(fields) local i = 0 return function() i = i + 1 local f = fields[i] if f then local k = next(f) local v = f[k] return k, v end end end -------------------------------------------------------------------------------- local function assert_data(value, description) if value == nil then error("\n\n" .. "****************************************\n" .. "Missing " .. description .. "\n" .. "-- please document it in autodoc/data/admin-api.lua\n" .. "****************************************\n", 2) end return value end -------------------------------------------------------------------------------- local function gen_kind(finfo, field_data) if field_data.kind then return "<br>*" .. field_data.kind .. "*" elseif finfo.required ~= true then return "<br>*optional*" else return "" end end local function break_long_code_block(code, separator, max_row_len) local escaped_separator = separator == "." and "%." or separator local word_break_separator = separator .. "`<wbr>`" local buffer = { "`" } local row_len = 0 local first = true for part in code:gmatch("[^" .. escaped_separator .."]+") do local part_len = #part if not first then if row_len + part_len + 1 < max_row_len then buffer[#buffer + 1] = separator row_len = row_len + 1 else buffer[#buffer + 1] = word_break_separator row_len = 0 end end buffer[#buffer + 1] = part row_len = row_len + part_len first = false end buffer[#buffer + 1] = "`" return table.concat(buffer) end local function gen_field_info(finfo) local out = {} if finfo.one_of then local vals = {} for _, f in ipairs(finfo.one_of) do local v = type(f) == "string" and ("%q"):format(f) or tostring(f) table.insert(vals, "`" .. v .. "`") end table.insert(out, " Accepted values are: " .. table.concat(vals, ", ") .. ". ") end if finfo.default then local json = cjson_encode(finfo.default) local default = break_long_code_block(json, ",", 30) table.insert(out, " Default: " .. default .. ".") end return table.concat(out) end local function gen_notation(fname, finfo, field_data) if finfo.type == "array" then local form_example = {} local example = field_data.examples and (field_data.examples[1] or field_data.examples[2]) or field_data.example for i, item in ipairs(example or finfo.default) do table.insert(form_example, fname .. "[]=" .. item) if i == 2 then break end end return [[ With form-encoded, the notation is `]] .. table.concat(form_example, "&") .. [[`. With JSON, use an Array.]] elseif finfo.type == "foreign" then local fschema = assert(require("kong.db.schema.entities." .. finfo.reference)) local ek = fschema.endpoint_key if ek then return ([[With form-encoded, the notation is ]] .. [[`$FNAME.id=<$FNAME id>` or ]] .. [[`$FNAME.$ENDPOINT_KEY=<$FNAME $ENDPOINT_KEY>`. ]] .. [[With JSON, use "]] .. [[`"$FNAME":{"id":"<$FNAME id>"}` or ]] .. [[`"$FNAME":{"$ENDPOINT_KEY":"<$FNAME $ENDPOINT_KEY>"}`.]]): gsub("$([A-Z_]*)", { FNAME = fname, ENDPOINT_KEY = ek, }) else return ([[With form-encoded, the notation is ]] .. [[`$FNAME.id=<$FNAME id>`. ]] .. [[With JSON, use "]] .. [[`"$FNAME":{"id":"<$FNAME id>"}`.]]): gsub("$([A-Z_]*)", { FNAME = fname, }) end else return "" end end local function write_field(outfd, fname, finfo, fullname, field_data, entity_name) local kind = gen_kind(finfo, field_data) local description = assert_data(field_data.description, "description for " .. entity_name .. "." .. fullname) :gsub("%s+", " ") local field_info = gen_field_info(finfo) local notation = gen_notation(fname, finfo, field_data) fullname = break_long_code_block(fullname, ".", 25) outfd:write(" " .. fullname .. kind .. " | " .. description .. field_info .. notation .. "\n") end local function process_field(outfd, entity_data, entity_name, fname, finfo, prefix) local fullname = (prefix or "") .. fname local field_data = entity_data.fields[fullname] if not field_data then if finfo.type == "record" then for rfname, rfinfo in each_field(finfo.fields) do process_field(outfd, entity_data, entity_name, rfname, rfinfo, fullname .. ".") end return else error("Missing autodoc data for field " .. entity_name .. "." .. fullname) end end if field_data.skip then return end write_field(outfd, fname, finfo, fullname, field_data, entity_name) end local function gen_example(exn, entity, entity_data, fields, indent, prefix) local csv = {} for fname, finfo in each_field(fields) do local fullname = (prefix or "") .. fname local value local field_data = entity_data.fields[fullname] if finfo.type == "record" and not finfo.abstract then value = gen_example(exn, entity, entity_data, finfo.fields, indent .. " ", fullname .. ".") elseif finfo.default ~= nil and field_data.examples == nil and field_data.example == nil then value = cjson_encode(finfo.default) else local example = field_data.examples and field_data.examples[exn] if example == nil then example = field_data.example end if example == nil then if finfo.uuid then example = gen_uuid() elseif finfo.type == "foreign" then example = { id = gen_uuid() } elseif finfo.timestamp then example = 1422386534 elseif fname == "name" then example = "my-" .. to_singular(entity) end end if example ~= nil then value = cjson_encode(example) elseif not field_data.skip_in_example then error("missing example value for " .. entity .. "." .. fname) end end if value ~= nil then table.insert(csv, indent .. " " .. '"' .. fname .. '": ' .. value) end end local out = {"{\n"} table.insert(out, table.concat(csv, ",\n")) table.insert(out, "\n") table.insert(out, indent .. "}") return table.concat(out) end local function write_entity_templates(outfd, entity, entity_data) local schema = assert(require("kong.db.schema.entities." .. entity)) local singular = to_singular(entity) assert_data(entity_data.fields, "'fields' entry for " .. entity) outfd:write(singular .. "_body: |\n") outfd:write(" Attributes | Description\n") outfd:write(" ---:| ---\n") for fname, finfo in each_field(schema.fields) do process_field(outfd, entity_data, entity, fname, finfo) end if entity_data.extra_fields then for efname, efinfo in each_field(entity_data.extra_fields) do write_field(outfd, efname, efinfo, efname, efinfo, entity) end end outfd:write("\n") outfd:write(singular .. "_json: |\n") outfd:write(" " .. gen_example(1, entity, entity_data, schema.fields, " ") .. "\n") outfd:write("\n") outfd:write(singular .. "_data: |\n") outfd:write(' "data": [' .. gen_example(1, entity, entity_data, schema.fields, " ") .. ", ") outfd:write(gen_example(2, entity, entity_data, schema.fields, " ") .. "],\n") outfd:write("\n") end local titles = {} local function write_title(outfd, level, title, label) if not title then return end title = utils.titleize(title):gsub("^%s*", ""):gsub("%s*$", "") table.insert(titles, { level = level, title = title, }) if label then label = "\n" .. label else label = "" end outfd:write((("#"):rep(level) .. " " .. title .. label .. "\n\n")) end local function section(outfd, title, content) if not content then return end write_title(outfd, 4, title) outfd:write(unindent(content) .. "\n") outfd:write("\n") end local function each_line(str) if str:sub(-1)~="\n" then str = str .. "\n" end return str:gmatch("(.-)\n") end local function blockquote(content) local buffer = {} for line in each_line(content) do buffer[#buffer + 1] = "> " .. line end return table.concat(buffer) end local function warning_message(outfd, content) outfd:write("\n\n{:.note}\n") outfd:write(blockquote(content)) outfd:write("\n\n") end local function write_endpoint(outfd, endpoint, ep_data, dbless_methods) assert_data(ep_data, "data for endpoint " .. endpoint) if ep_data.done or ep_data.skip then return end -- check for endpoint-specific overrides (useful for db-less) for i, method in ipairs(method_array) do local meth_data = ep_data[method] if meth_data and meth_data.endpoint ~= false then assert_data(meth_data.title, "info for " .. method .. " " .. endpoint) if dbless_methods and not dbless_methods[method] and (not dbless_methods[endpoint] or not dbless_methods[endpoint][method]) then write_title(outfd, 3, meth_data.title) warning_message(outfd, "**Note**: This API is not available in DB-less mode.") else write_title(outfd, 3, meth_data.title, "{:.badge .dbless}") end section(outfd, nil, meth_data.description) local fk_endpoints = meth_data.fk_endpoints or {} section(outfd, nil, meth_data.endpoint) for _, fk_endpoint in ipairs(fk_endpoints) do section(outfd, nil, fk_endpoint) end section(outfd, "Request Querystring Parameters", meth_data.request_query) section(outfd, "Request Body", meth_data.request_body) section(outfd, nil, meth_data.details) section(outfd, "Response", meth_data.response) outfd:write("---\n\n") end end ep_data.done = true end local function write_endpoints(outfd, info, all_endpoints, dbless_methods) for endpoint, ep_data in sortedpairs(info.data) do if endpoint:match("^/") then write_endpoint(outfd, endpoint, ep_data, dbless_methods) all_endpoints[endpoint] = ep_data end end return all_endpoints end local function write_general_section(outfd, filename, all_endpoints, name, data_general) local file_data = assert_data(data_general[name], "data for " .. filename) if file_data.skip == true then return end write_title(outfd, 2, file_data.title) assert_data(file_data.description, "'description' field for " .. filename) outfd:write(unindent(file_data.description)) outfd:write("\n\n") local info = { filename = filename, data = file_data, mod = assert(loadfile(KONG_PATH .. "/" .. filename))() } write_endpoints(outfd, info, all_endpoints) return info end local active_verbs = { GET = "retrieve", POST = "create", PATCH = "update", PUT = "create or update", DELETE = "delete", } local passive_verbs = { GET = "retrieved", POST = "created", PATCH = "updated", PUT = "created or updated", DELETE = "deleted", } local function adjust_for_method(subs, method) subs.method = method:lower() subs.METHOD = method:upper() subs.active_verb = active_verbs[subs.METHOD] subs.passive_verb = passive_verbs[subs.METHOD] subs.Active_verb = utils.titleize(subs.active_verb) subs.Passive_verb = utils.titleize(subs.passive_verb) end local gen_endpoint do local template_keys = { "title", "description", "details", "request_querystring", "request_body", "response", "endpoint", } gen_endpoint = function(edata, templates, subs, endpoint, method, has_ek) local ep_data = get_or_create(edata, endpoint) if ep_data.skip then return end local meth_data = get_or_create(ep_data, method) assert_data(templates, "templates definition for " .. endpoint) local meth_tpls = templates[method] assert_data(meth_tpls, "templates definition for " .. method .. " " .. endpoint) adjust_for_method(subs, method) for _, k in ipairs(template_keys) do local tk = (k == "endpoint") and (has_ek and "endpoint_w_ek" or "endpoint") or k local template = meth_tpls[tk] or templates[tk] if meth_data[k] == nil and ep_data[k] ~= nil then meth_data[k] = ep_data[k] end if meth_data[k] == nil and template then meth_data[k] = render(template, subs) end end end end local function gen_fk_endpoint(edata, templates, subs, parent_endpoint, method, has_ek, has_fek, nested) local ep_data = assert_data(edata[parent_endpoint], "entity data for endpoint " .. parent_endpoint) local meth_data if nested then meth_data = ep_data[method] if not meth_data then return end else meth_data = assert(ep_data[method]) -- get_or_create(ep_data, method) end assert_data(templates, "templates definition for " .. parent_endpoint) local meth_tpls = templates[method] assert_data(meth_tpls, "templates definition for " .. method .. " " .. parent_endpoint) local tk if nested then if has_ek and has_fek then tk = "nested_endpoint_w_eks" elseif has_ek then tk = "nested_endpoint_w_ek" elseif has_fek then tk = "nested_endpoint_w_fek" else tk = "nested_endpoint" end else if has_ek then tk = "fk_endpoint_w_ek" elseif has_fek then tk = "fk_endpoint_w_fek" else tk = "fk_endpoint" end end local tpl = meth_tpls[tk] or templates[tk] assert_data(tpl, tk .. " template for " .. method .. " " .. parent_endpoint) adjust_for_method(subs, method) assert_data(meth_data.title, "'title' field for " .. method .. " " .. parent_endpoint) local fk_endpoints = get_or_create(meth_data, "fk_endpoints") table.insert(fk_endpoints, render(tpl, subs)) end local function gen_template_subs_table(edata, plural, schema, fedata, fplural, fschema, fname) local api = edata.entity_url_collection_name or schema.admin_api_name or schema.name or plural local singular = to_singular(plural) local subs = { ["Entity"] = edata.entity_title or utils.titleize(singular), ["Entities"] = edata.entity_title_plural or utils.titleize(plural), ["entity"] = edata.entity_lower or singular:lower(), ["entities"] = edata.entity_lower_plural or plural:lower(), ["entities_url"] = api, ["entity_url"] = edata.entity_url_name or singular, ["endpoint_key"] = edata.entity_endpoint_key or schema.endpoint_key or "name", } if fedata then local fapi = fedata.entity_url_collection_name or fschema.admin_api_name or fschema.name or fplural local fsingular = to_singular(fplural) subs["ForeignEntity"] = fedata.entity_title or utils.titleize(fsingular) subs["ForeignEntities"] = fedata.entity_title_plural or utils.titleize(fplural) subs["foreign_entity"] = fedata.entity_lower or fsingular:lower() subs["foreign_entities"] = fedata.entity_lower_plural or fplural:lower() subs["foreign_entities_url"] = fapi subs["foreign_entity_url"] = fedata.entity_url_name or fname or fsingular subs["foreign_endpoint_key"] = fedata.entity_endpoint_key or fschema.endpoint_key or "name" end return subs end local function prepare_entity(data, entity_file, entity_data) local out = {} assert_data(entity_data.description, "'description' field for " .. entity_file) local schema = assert(loadfile(KONG_PATH .. "/" .. entity_to_schema_path(entity_file)))() local subs = gen_template_subs_table(entity_data, entity_file, schema) local title = entity_data.title or (subs.Entity .. " Object") table.insert(out, unindent(entity_data.description)) if entity_data.fields.tags then table.insert(out, "\n") table.insert(out, unindent(render(data.entity_templates.tags, subs))) end table.insert(out, "\n\n") table.insert(out, "```json\n") table.insert(out, "{{ page." .. subs.entity .. "_json }}\n") table.insert(out, "```\n\n") if entity_data.details then table.insert(out, unindent(entity_data.details)) table.insert(out, "\n\n") end local filename = "kong/api/routes/" .. entity_file .. ".lua" local modtbl = loadfile(KONG_PATH .. "/" .. filename) local mod = modtbl and modtbl() or {} local ename = schema.admin_api_name or schema.name local eapi = entity_data.admin_api_name or ename -- e.g. /services local collection_endpoint = "/" .. eapi gen_endpoint(entity_data, data.collection_templates, subs, collection_endpoint, "GET") gen_endpoint(entity_data, data.collection_templates, subs, collection_endpoint, "POST") -- e.g. /services/{name or id} local entity_endpoint = "/" .. eapi .. "/:" .. ename local has_ek = schema.endpoint_key ~= nil gen_endpoint(entity_data, data.entity_templates, subs, entity_endpoint, "GET", has_ek) gen_endpoint(entity_data, data.entity_templates, subs, entity_endpoint, "PUT", has_ek) gen_endpoint(entity_data, data.entity_templates, subs, entity_endpoint, "PATCH", has_ek) gen_endpoint(entity_data, data.entity_templates, subs, entity_endpoint, "DELETE", has_ek) return { filename = filename, entity = entity_file, schema = schema, title = title, intro = table.concat(out), data = entity_data, mod = mod, } end local function skip_fk_endpoint(edata, endpoint, method) local ret = edata and edata[endpoint] and ((edata[endpoint].endpoint == false) or (edata[endpoint][method] and edata[endpoint][method].endpoint == false)) return ret end local function prepare_foreign_key_endpoints(data, entity_infos, entity) local einfo = entity_infos[entity] local edata = einfo.data for fname, finfo in each_field(einfo.schema.fields) do local foreigns = finfo.reference if finfo.type == "foreign" and not data.known.nodoc_entities[foreigns] then local feinfo = entity_infos[foreigns] local fedata = feinfo.data local subs = gen_template_subs_table(einfo.data, entity, einfo.schema, fedata, foreigns, feinfo.schema, fname) local has_ek = einfo.schema.endpoint_key ~= nil local has_fek = feinfo.schema.endpoint_key ~= nil local function gen_fk_endpoints(parent_endpoint, endpoint, meths, templates, srcdata, dstdata, nested) for _, method in ipairs(meths) do if not skip_fk_endpoint(edata, endpoint, method) then gen_fk_endpoint(dstdata, templates, subs, parent_endpoint, method, has_ek, has_fek, nested) local ep_data = get_or_create(srcdata, endpoint) ep_data.done = true end end end local ename = einfo.schema.name local eapi = einfo.schema.admin_api_name or ename local enapi = einfo.schema.admin_api_nested_name or eapi local fename = feinfo.schema.name local feapi = feinfo.schema.admin_api_name or fename -- e.g. /services/{service name or id}/routes gen_fk_endpoints( "/" .. eapi, "/" .. feapi .. "/:" .. fename .. "/" .. enapi, {"GET", "POST"}, data.collection_templates, fedata, edata ) -- e.g. /services/{service name or id}/routes/{route name or id} gen_fk_endpoints( "/" .. eapi .. "/:" .. ename, "/" .. feapi .. "/:" .. fename .. "/" .. enapi .. "/:" .. ename, {"GET", "PUT", "PATCH", "DELETE"}, data.entity_templates, fedata, edata, true ) -- e.g. /routes/{route name or id}/service gen_fk_endpoints( "/" .. feapi .. "/:" .. fename, "/" .. eapi .. "/:" .. ename .. "/" .. fname, {"GET", "PUT", "PATCH", "DELETE"}, data.entity_templates, edata, fedata ) end end end -------------------------------------------------------------------------------- -- Check that all modules present in the Admin API are known by this script. local function check_admin_api_modules(data) local file_set = {} for _, item in ipairs(data.known.general_files) do file_set[item] = "use" data.known.general_files[item] = true end for _, item in ipairs(data.known.entities) do file_set[entity_to_api_path(item)] = "use" data.known.entities[item] = true end for _, item in ipairs(data.known.nodoc_entities) do file_set[entity_to_api_path(item)] = "nodoc" data.known.nodoc_entities[item] = true end for _, item in ipairs(data.known.nodoc_files) do file_set[item] = "nodoc" data.known.nodoc_files[item] = true end for file in lfs.dir(KONG_PATH .. "/kong/api/routes") do if file:match("%.lua$") then local name = "kong/api/routes/" .. file if not file_set[name] then error("File " .. name .. " not known to autodoc/admin-api/generate.lua! " .. "Please add to the data.known tables.") end end end end local function check_endpoints(all_endpoints, infos) for _, info in ipairs(infos) do for endpoint, handler in pairs(info.mod) do if handler ~= Endpoints.not_found then assert_data(all_endpoints[endpoint], "data for implemented endpoint " .. endpoint) assert_data(all_endpoints[endpoint].done or all_endpoints[endpoint].skip, "done or skip mark in endpoint " .. endpoint) end end end end -------------------------------------------------------------------------------- local function write_admin_api(filename, data, title) lfs.mkdir("autodoc") lfs.mkdir("autodoc/output") lfs.mkdir("autodoc/output/admin-api") local outpath = "autodoc/output/admin-api/" .. filename local outfd = assert(io.open(outpath, "w+")) reset_uuid() outfd:write("---\n") outfd:write("#\n") outfd:write("# WARNING: this file was auto-generated by a script.\n") outfd:write("# DO NOT edit this file directly. Instead, send a pull request to change\n") outfd:write("# https://github.com/Kong/kong/blob/master/scripts/autodoc/admin-api/generate.lua\n") outfd:write("# or its associated files instead.\n") outfd:write("#\n") outfd:write("title: " .. utils.titleize(title) .. "\n") outfd:write("toc: false\n\n") for _, entity in ipairs(data.known.entities) do local entity_data = assert_data(data.entities[entity], "entity data for " .. entity) write_entity_templates(outfd, entity, entity_data) end outfd:write("\n---\n") for _, ipart in ipairs(assert_data(data.intro, "intro string")) do outfd:write("\n") write_title(outfd, 2, ipart.title) outfd:write(unindent(ipart.text)) outfd:write("\n---\n\n") end local all_endpoints = {} local general_infos = {} for _, fullname in ipairs(data.known.general_files) do local name = fullname:match("/([^/]+)%.lua$") local ginfo = write_general_section(outfd, fullname, all_endpoints, name, data.general) table.insert(general_infos, ginfo) general_infos[name] = ginfo end local entity_infos = {} for _, entity in ipairs(data.known.entities) do local einfo = prepare_entity(data, entity, data.entities[entity]) table.insert(entity_infos, einfo) entity_infos[entity] = einfo end for _, entity in ipairs(data.known.entities) do prepare_foreign_key_endpoints(data, entity_infos, entity) end for _, entity_info in ipairs(entity_infos) do write_title(outfd, 2, entity_info.title) outfd:write(entity_info.intro) write_endpoints(outfd, entity_info, all_endpoints, data.dbless_entities_methods) end -- Check that all endpoints were traversed check_endpoints(all_endpoints, entity_infos) check_endpoints(all_endpoints, general_infos) outfd:write(unindent(assert_data(data.footer, "footer string"))) outfd:close() print(" Wrote " .. outpath) end -------------------------------------------------------------------------------- local function write_admin_api_nav(filename, data) lfs.mkdir("autodoc") lfs.mkdir("autodoc/output") lfs.mkdir("autodoc/output/nav") local outpath = "autodoc/output/nav/" .. filename local outfd = assert(io.open(outpath, "w+")) outfd:write(unindent(data.nav.header)) local max_level = 3 local level = 3 for _, t in ipairs(titles) do if t.level <= max_level then if t.level <= level then outfd:write("\n") elseif t.level > level then outfd:write((" "):rep(level - 1) .. " items:\n") end level = t.level outfd:write((" "):rep(level - 1) .. "- text: " .. t.title .. "\n") outfd:write((" "):rep(level - 1) .. " url: /admin-api/#" .. t.title:lower():gsub(" ", "-") .. "\n") end end outfd:close() print(" Wrote " .. outpath) end -------------------------------------------------------------------------------- local function main() print("Building Admin API docs...") check_admin_api_modules(admin_api_data) write_admin_api( "admin-api.md", admin_api_data, "Admin API" ) write_admin_api_nav( "docs_nav.yml.admin-api.in", admin_api_data ) end main()