"""Interactive API docs: self-hosted Redoc over the generated OpenAPI spec. Served at /api/docs (relative to the mount). The Redoc bundle is vendored in staticdocs/ so this works fully offline on the air-gapped prod box - no CDN. The spec is docs/openapi.json in the repo (regenerate with scripts/gen_openapi.py after API changes). Two renderers over ONE spec. Redoc is read-only by design and is the default, because what is published is a contract. Swagger UI at /api/docs/try adds the one thing Redoc will not do: fire a real request from the page, against this server, with whatever session the browser is already carrying. That is its whole value and the whole reason it is off unless a site turns it on - see API_DOCS_TRYIT_ENABLED in shopdb/config.py. """ import json import os from markupsafe import escape from flask import Blueprint, Response, current_app, send_file, url_for docs_bp = Blueprint('docs', __name__) _HERE = os.path.dirname(os.path.abspath(__file__)) # shopdb/core/api -> repo root _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(_HERE))) _SPEC = os.path.join(_REPO_ROOT, 'docs', 'openapi.json') _LLMS = os.path.join(_REPO_ROOT, 'docs', 'llms.txt') _REDOC = os.path.join(_HERE, 'staticdocs', 'redoc.standalone.js') # Vendored from swagger-ui-dist 5.33.0. TWO files out of the ~30 in that dist, # because those are the two the page below actually loads: # swagger-ui.css - the stylesheet. Its images are inline data: URIs, # so it pulls nothing else. The trailing # sourceMappingURL comment was stripped: the .map # is not vendored, and left in place it turns every # devtools open into a 404 on a box with no # internet to fall back on. # swagger-ui-bundle.min.js - the renderer plus the core presets, self- # contained. NOT vendored beside it: # swagger-ui-standalone-preset.js (the topbar with # the "load any spec URL" box - this page serves # one spec on purpose), the ES bundles (module # builds of the same code), the .map files (~10 MB # of source maps that only devtools reads), # oauth2-redirect.html (no OAuth flow here), and # the favicons. # The upstream name is swagger-ui-bundle.js; the vendored copy carries .min.js, # which is what it is - a minified single-line build. That suffix is also what # scripts/check-naming-and-style.sh already filters vendored bundles by, so the # style gate does not report thousands of hits in code nobody here wrote. # swagger-ui.LICENSE.txt is the Apache-2.0 text that ships with the dist. It is # never served; it is here because redistributing the bundle requires it. _SWAGGER_CSS = os.path.join(_HERE, 'staticdocs', 'swagger-ui.css') _SWAGGER_JS = os.path.join(_HERE, 'staticdocs', 'swagger-ui-bundle.min.js') _TRYIT_ENDPOINT = 'docs.swagger_tryit' def _tryit_is_mounted(): """True when the try-it page exists on THIS app. Reads the url map rather than the config flag on purpose: the routes are bound once, at blueprint registration, so a config value changed after boot would have the Redoc page advertise a link that answers 404. """ return _TRYIT_ENDPOINT in current_app.view_functions @docs_bp.route('/', strict_slashes=False) def docs_index(): """Redoc page. url_for keeps the asset URLs correct under any mount.""" spec_url = url_for('docs.openapi_spec') redoc_url = url_for('docs.redoc_js') # Mentioned only when it is actually there. An unconditional note would be # an advert for a 404 on every box that ships the shipped default. tryit_note = '' if _tryit_is_mounted(): tryit_note = ( '
' 'This page is read-only. To send a real request against this ' 'server, use the try-it view.
' % url_for(_TRYIT_ENDPOINT) ) page = ( '' 'Execute sends a real request to this ' 'server, as whoever is signed in on this browser. A write ' 'operation changes live data. The read-only contract is at ' '/api/docs.
' '' '' '' '' % {'css': escape(css_url), 'js': escape(js_url), 'spec': json.dumps(spec_url), 'redoc': escape(redoc_page_url)} ) return Response(page, mimetype='text/html') def swagger_css(): """Vendored Swagger UI stylesheet (offline).""" return send_file(_SWAGGER_CSS, mimetype='text/css') def swagger_js(): """Vendored Swagger UI bundle (offline).""" return send_file(_SWAGGER_JS, mimetype='application/javascript') @docs_bp.record_once def _mount_tryit(state): """Bind the try-it routes only when the flag is on for this app. Binding at registration rather than checking the flag inside the view, so that when it is off the routes DO NOT EXIST: the URL gets Flask's own 404 and confirms nothing, and the unauthenticated-GET sweep in tests/test_core/test_public_read_surface.py never has to make an exception for three routes that are only meant to be reachable on a dev box. """ if not state.app.config.get('API_DOCS_TRYIT_ENABLED'): return state.add_url_rule('/try', endpoint='swagger_tryit', view_func=swagger_tryit, strict_slashes=False) state.add_url_rule('/swagger-ui.css', endpoint='swagger_css', view_func=swagger_css) state.add_url_rule('/swagger-ui-bundle.min.js', endpoint='swagger_js', view_func=swagger_js)