feat: Migrate OpenAPI documentation to static HTML structure
- Remove OpenApiDocs.php controller and swagger.php view - Delete legacy bundler scripts (bundle-api-docs.php, bundle-api-docs.py) - Remove old documentation files (API_DOCS_README.md, docs.html) - Update Routes.php to remove OpenAPI documentation routes - Modify PagesController.php to reflect changes - Add new public/swagger/index.html as standalone documentation
This commit is contained in:
parent
fcaf9b74ea
commit
46e52b124b
@ -21,12 +21,6 @@ $routes->group('api', ['filter' => 'auth'], function ($routes) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Swagger API Documentation (public - no filters)
|
|
||||||
$routes->add('swagger', 'PagesController::swagger');
|
|
||||||
|
|
||||||
// OpenAPI Specification (server-side merged - public)
|
|
||||||
$routes->get('api-docs', 'OpenApiDocs::index');
|
|
||||||
|
|
||||||
// V2 Auth API Routes (public - no auth required)
|
// V2 Auth API Routes (public - no auth required)
|
||||||
$routes->group('v2/auth', function ($routes) {
|
$routes->group('v2/auth', function ($routes) {
|
||||||
$routes->post('login', 'AuthV2Controller::login');
|
$routes->post('login', 'AuthV2Controller::login');
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controllers;
|
|
||||||
|
|
||||||
class OpenApiDocs extends BaseController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Serve pre-bundled OpenAPI specification
|
|
||||||
* Returns the bundled file with all paths and schemas resolved
|
|
||||||
*/
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
// Load pre-bundled OpenAPI spec
|
|
||||||
$bundledPath = FCPATH . 'api-docs.bundled.yaml';
|
|
||||||
|
|
||||||
if (!file_exists($bundledPath)) {
|
|
||||||
throw new \Exception("Bundled API docs not found: api-docs.bundled.yaml. Run: node bundle-api-docs.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = file_get_contents($bundledPath);
|
|
||||||
|
|
||||||
// Output as YAML
|
|
||||||
return $this->response
|
|
||||||
->setContentType('application/x-yaml')
|
|
||||||
->setHeader('Access-Control-Allow-Origin', '*')
|
|
||||||
->setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
|
|
||||||
->setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
|
||||||
->setHeader('Cache-Control', 'no-cache, must-revalidate')
|
|
||||||
->setBody($content);
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
log_message('error', 'OpenApiDocs Error: ' . $e->getMessage());
|
|
||||||
return $this->response
|
|
||||||
->setStatusCode(500)
|
|
||||||
->setContentType('application/json')
|
|
||||||
->setBody(json_encode([
|
|
||||||
'error' => 'Failed to serve OpenAPI spec',
|
|
||||||
'message' => $e->getMessage()
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -10,13 +10,5 @@ namespace App\Controllers;
|
|||||||
*/
|
*/
|
||||||
class PagesController extends BaseController
|
class PagesController extends BaseController
|
||||||
{
|
{
|
||||||
|
// Add page methods here as needed
|
||||||
|
|
||||||
/**
|
|
||||||
* API Documentation / Swagger UI page
|
|
||||||
*/
|
|
||||||
public function swagger()
|
|
||||||
{
|
|
||||||
return view('swagger');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,405 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>API Documentation - CLQMS</title>
|
|
||||||
|
|
||||||
<!-- Swagger UI CSS -->
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: -moz-scrollbars-vertical;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Scrollbar for Webkit */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #888;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*:before,
|
|
||||||
*:after {
|
|
||||||
box-sizing: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
display: flex; /* Sidebar layout */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar Styles */
|
|
||||||
#custom-sidebar {
|
|
||||||
width: 280px;
|
|
||||||
height: 100vh;
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
background: #fafafa;
|
|
||||||
border-right: 1px solid #ddd;
|
|
||||||
padding: 20px 0;
|
|
||||||
z-index: 1000;
|
|
||||||
transition: background 0.3s, border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-sidebar h2 {
|
|
||||||
padding: 0 20px;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: #3b4151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-link {
|
|
||||||
display: block;
|
|
||||||
padding: 8px 20px;
|
|
||||||
color: #3b4151;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
border-left: 3px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-link:hover {
|
|
||||||
background: rgba(0,0,0,0.05);
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-link.active {
|
|
||||||
border-left-color: #49cc90; /* Swagger Green */
|
|
||||||
background: rgba(73, 204, 144, 0.1);
|
|
||||||
font-weight: 600;
|
|
||||||
color: #49cc90;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Content Adjustments */
|
|
||||||
#swagger-ui {
|
|
||||||
margin-left: 280px; /* Width of sidebar */
|
|
||||||
width: calc(100% - 280px);
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger-ui .information-container {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark theme support */
|
|
||||||
[data-theme="dark"] body {
|
|
||||||
background: #0d1117;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] #custom-sidebar {
|
|
||||||
background: #161b22;
|
|
||||||
border-right: 1px solid #30363d;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] #custom-sidebar h2 {
|
|
||||||
color: #c9d1d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .sidebar-link {
|
|
||||||
color: #8b949e;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .sidebar-link:hover {
|
|
||||||
background: rgba(255,255,255,0.05);
|
|
||||||
color: #c9d1d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .sidebar-link.active {
|
|
||||||
border-left-color: #58a6ff;
|
|
||||||
background: rgba(88, 166, 255, 0.1);
|
|
||||||
color: #58a6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] #swagger-ui {
|
|
||||||
--swagger-ui-color: #8b949e;
|
|
||||||
--swagger-ui-bg: #0d1117;
|
|
||||||
--swagger-ui-primary: #58a6ff;
|
|
||||||
--swagger-ui-text: #c9d1d9;
|
|
||||||
--swagger-ui-border: #30363d;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .info {
|
|
||||||
margin: 20px 0;
|
|
||||||
background: #161b22;
|
|
||||||
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.3);
|
|
||||||
border: 1px solid #30363d;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .info .title {
|
|
||||||
color: #58a6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .info .description {
|
|
||||||
color: #8b949e;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .opblock {
|
|
||||||
background: #0d1117;
|
|
||||||
border: 1px solid #30363d;
|
|
||||||
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .opblock .opblock-summary {
|
|
||||||
border-color: #30363d;
|
|
||||||
background: #161b22;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .opblock .opblock-summary-description {
|
|
||||||
color: #8b949e;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .opblock.opblock-get {
|
|
||||||
border-color: #3fb950;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .opblock.opblock-post {
|
|
||||||
border-color: #58a6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .opblock.opblock-put {
|
|
||||||
border-color: #d29922;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .opblock.opblock-delete {
|
|
||||||
border-color: #f85149;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .opblock.opblock-patch {
|
|
||||||
border-color: #a371f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .scheme-container {
|
|
||||||
background: #161b22;
|
|
||||||
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .loading-container {
|
|
||||||
background: #0d1117;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .btn {
|
|
||||||
background: #21262d;
|
|
||||||
border-color: #30363d;
|
|
||||||
color: #c9d1d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .btn:hover {
|
|
||||||
background: #30363d;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui input[type="text"],
|
|
||||||
[data-theme="dark"] .swagger-ui textarea {
|
|
||||||
background: #0d1117;
|
|
||||||
border-color: #30363d;
|
|
||||||
color: #c9d1d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui select {
|
|
||||||
background: #0d1117;
|
|
||||||
border-color: #30363d;
|
|
||||||
color: #c9d1d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .model-title {
|
|
||||||
color: #58a6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .property-name {
|
|
||||||
color: #8b949e;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .tab li {
|
|
||||||
color: #8b949e;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .tab li.active {
|
|
||||||
color: #58a6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .responses-inner h4,
|
|
||||||
[data-theme="dark"] .swagger-ui .responses-inner h5 {
|
|
||||||
color: #c9d1d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .response-col_status {
|
|
||||||
color: #8b949e;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .swagger-ui .response-col_description {
|
|
||||||
color: #8b949e;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
body {
|
|
||||||
display: block; /* Stack on mobile */
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-sidebar {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
max-height: 200px;
|
|
||||||
position: relative;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger-ui {
|
|
||||||
margin-left: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger-ui .information-container {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger-ui .info {
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="custom-sidebar">
|
|
||||||
<h2>Resources</h2>
|
|
||||||
<div id="sidebar-links"></div>
|
|
||||||
</div>
|
|
||||||
<div id="swagger-ui"></div>
|
|
||||||
|
|
||||||
<!-- Swagger UI Bundle -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Custom Sidebar Logic
|
|
||||||
function initCustomSidebar(ui) {
|
|
||||||
const sidebarLinks = document.getElementById('sidebar-links');
|
|
||||||
|
|
||||||
// Wait for Swagger UI to render tags
|
|
||||||
// We'll poll every 100ms for up to 5 seconds
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = 50;
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
attempts++;
|
|
||||||
const tags = document.querySelectorAll('.opblock-tag-section');
|
|
||||||
|
|
||||||
if (tags.length > 0 || attempts >= maxAttempts) {
|
|
||||||
clearInterval(interval);
|
|
||||||
buildSidebar(tags);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSidebar(tags) {
|
|
||||||
const sidebarLinksContainer = document.getElementById('sidebar-links');
|
|
||||||
sidebarLinksContainer.innerHTML = ''; // Clear existing
|
|
||||||
|
|
||||||
if (tags.length === 0) {
|
|
||||||
sidebarLinksContainer.innerHTML = '<div style="padding:0 20px;color:#888;">No resources found</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.forEach(tagSection => {
|
|
||||||
const tagHeader = tagSection.querySelector('.opblock-tag');
|
|
||||||
if (!tagHeader) return;
|
|
||||||
|
|
||||||
const tagName = tagHeader.getAttribute('data-tag');
|
|
||||||
const tagText = tagHeader.querySelector('a span') ? tagHeader.querySelector('a span').innerText : tagName;
|
|
||||||
const tagId = tagHeader.id; // e.g., operations-tag-Auth
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.className = 'sidebar-link';
|
|
||||||
link.textContent = tagText;
|
|
||||||
link.href = '#' + tagId;
|
|
||||||
|
|
||||||
link.onclick = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const target = document.getElementById(tagId);
|
|
||||||
if (target) {
|
|
||||||
target.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
// Update active state
|
|
||||||
document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
|
|
||||||
link.classList.add('active');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sidebarLinksContainer.appendChild(link);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onload = function() {
|
|
||||||
// Get JWT token from localStorage
|
|
||||||
// JWT token handled via HttpOnly cookie automatically
|
|
||||||
|
|
||||||
// Initialize Swagger UI
|
|
||||||
const ui = SwaggerUIBundle({
|
|
||||||
url: "<?= base_url('api-docs') ?>?v=<?= time() ?>",
|
|
||||||
dom_id: '#swagger-ui',
|
|
||||||
deepLinking: true,
|
|
||||||
presets: [
|
|
||||||
SwaggerUIBundle.presets.apis,
|
|
||||||
SwaggerUIStandalonePreset
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
SwaggerUIBundle.plugins.DownloadUrl
|
|
||||||
],
|
|
||||||
layout: "StandaloneLayout",
|
|
||||||
defaultModelsExpandDepth: 1,
|
|
||||||
defaultModelExpandDepth: 1,
|
|
||||||
displayOperationId: false,
|
|
||||||
displayRequestDuration: true,
|
|
||||||
docExpansion: "list",
|
|
||||||
filter: true,
|
|
||||||
showRequestHeaders: true,
|
|
||||||
showCommonExtensions: true,
|
|
||||||
tryItOutEnabled: true,
|
|
||||||
persistAuthorization: true,
|
|
||||||
withCredentials: true, // Enable cookies
|
|
||||||
syntaxHighlight: {
|
|
||||||
activate: true,
|
|
||||||
theme: "monokai"
|
|
||||||
},
|
|
||||||
validatorUrl: null,
|
|
||||||
|
|
||||||
onComplete: () => {
|
|
||||||
// Initialize Custom Sidebar
|
|
||||||
initCustomSidebar(ui);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Request interceptor to ensure cookies are sent
|
|
||||||
requestInterceptor: (request) => {
|
|
||||||
// Ensure credentials (cookies) are included in the request
|
|
||||||
request.credentials = 'include';
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.ui = ui;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,162 +0,0 @@
|
|||||||
# CLQMS OpenAPI Documentation Structure
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The OpenAPI documentation has been reorganized into a modular structure for better maintainability.
|
|
||||||
|
|
||||||
**Architecture:** Server-side resolution (CodeIgniter 4)
|
|
||||||
- Modular files in `paths/` and `components/schemas/` directories
|
|
||||||
- CodeIgniter controller merges files on-the-fly
|
|
||||||
- Single endpoint `/api-docs` serves complete specification
|
|
||||||
- No build step or bundling required
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
1. **Modular Development:** Edit files in `paths/` and `components/schemas/`
|
|
||||||
2. **Server-Side Merge:** `OpenApiDocs` controller loads and merges all files
|
|
||||||
3. **Single Endpoint:** Access `/api-docs` to get complete merged specification
|
|
||||||
4. **Cache Support:** Can add caching layer for production
|
|
||||||
|
|
||||||
## Recommended Usage
|
|
||||||
|
|
||||||
**For Swagger UI:** Use the CodeIgniter route `/api-docs` (already configured in `app/Views/swagger.php`)
|
|
||||||
|
|
||||||
**For development:** Edit modular files directly - changes are reflected immediately on next request
|
|
||||||
|
|
||||||
## Structure
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
public/
|
|
||||||
├── api-docs.yaml # Main entry point with schema references
|
|
||||||
├── api-docs.bundled.yaml # Bundled version (use this for Swagger UI)
|
|
||||||
├── components/
|
|
||||||
│ └── schemas/
|
|
||||||
│ ├── authentication.yaml
|
|
||||||
│ ├── common.yaml
|
|
||||||
│ ├── edge-api.yaml
|
|
||||||
│ ├── master-data.yaml
|
|
||||||
│ ├── orders.yaml
|
|
||||||
│ ├── organization.yaml
|
|
||||||
│ ├── patient-visit.yaml
|
|
||||||
│ ├── patient.yaml
|
|
||||||
│ ├── specimen.yaml
|
|
||||||
│ ├── tests.yaml
|
|
||||||
│ └── valuesets.yaml
|
|
||||||
├── paths/
|
|
||||||
│ ├── authentication.yaml
|
|
||||||
│ ├── demo.yaml
|
|
||||||
│ ├── edge-api.yaml
|
|
||||||
│ ├── master-data.yaml
|
|
||||||
│ ├── orders.yaml
|
|
||||||
│ ├── organization.yaml
|
|
||||||
│ ├── patient-visits.yaml
|
|
||||||
│ ├── patients.yaml
|
|
||||||
│ ├── results.yaml
|
|
||||||
│ ├── specimen.yaml
|
|
||||||
│ ├── tests.yaml
|
|
||||||
│ └── valuesets.yaml
|
|
||||||
└── bundle-api-docs.py # Bundler script
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Server-Side Resolution
|
|
||||||
|
|
||||||
The application uses a CodeIgniter 4 controller (`app/Controllers/OpenApiDocs.php`) that:
|
|
||||||
1. Loads `public/api-docs.yaml` (base spec with schemas)
|
|
||||||
2. Scans `public/paths/*.yaml` files
|
|
||||||
3. Merges all paths into the base spec
|
|
||||||
4. Outputs complete YAML response
|
|
||||||
|
|
||||||
### Benefits
|
|
||||||
- ✅ **No bundling step** - Files merged at runtime
|
|
||||||
- ✅ **Immediate updates** - Changes reflect immediately
|
|
||||||
- ✅ **Modular structure** - Separate files by domain
|
|
||||||
- ✅ **CI4 compatible** - Works with CodeIgniter 4
|
|
||||||
- ✅ **Nginx ready** - Can migrate to Nginx later
|
|
||||||
- ✅ **Cacheable** - Can add caching for production
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### For Development
|
|
||||||
|
|
||||||
Edit files directly:
|
|
||||||
- **Paths:** `public/paths/*.yaml`
|
|
||||||
- **Schemas:** `public/components/schemas/*.yaml`
|
|
||||||
|
|
||||||
### For Production/Swagger UI
|
|
||||||
|
|
||||||
**Endpoint:** `http://localhost/clqms01/api-docs`
|
|
||||||
|
|
||||||
The endpoint merges all files server-side and returns a complete OpenAPI specification that Swagger UI can consume.
|
|
||||||
|
|
||||||
## Caching (Optional)
|
|
||||||
|
|
||||||
For production, you can add caching to the `OpenApiDocs` controller:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// In OpenApiDocs::index()
|
|
||||||
$cacheKey = 'openapi_spec_' . filemtime(FCPATH . 'api-docs.yaml');
|
|
||||||
if ($cached = cache()->get($cacheKey)) {
|
|
||||||
return $this->response->setBody($cached);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... build spec ...
|
|
||||||
|
|
||||||
// Cache for 5 minutes
|
|
||||||
cache()->save($cacheKey, $yaml, 300);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Update
|
|
||||||
|
|
||||||
Update your application configuration to use the bundled file:
|
|
||||||
|
|
||||||
**PHP (CodeIgniter):**
|
|
||||||
```php
|
|
||||||
// Already updated in app/Views/swagger.php:
|
|
||||||
url: "<?= base_url('api-docs.bundled.yaml') ?>"
|
|
||||||
```
|
|
||||||
|
|
||||||
**JavaScript/Node:**
|
|
||||||
```javascript
|
|
||||||
// Change from:
|
|
||||||
const spec = await fetch('/api-docs.yaml');
|
|
||||||
|
|
||||||
// To:
|
|
||||||
const spec = await fetch('/api-docs.bundled.yaml');
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
1. **Modular**: Each domain has its own file
|
|
||||||
2. **Maintainable**: Easier to find and update specific endpoints
|
|
||||||
3. **Team-friendly**: Multiple developers can work on different files
|
|
||||||
4. **Schema Reuse**: Schemas defined once, referenced everywhere
|
|
||||||
5. **Better Versioning**: Individual domains can be versioned separately
|
|
||||||
|
|
||||||
## Migration Notes
|
|
||||||
|
|
||||||
- **From bundled to server-side:** Already done! The system now uses server-side resolution.
|
|
||||||
- **Nginx migration:** When ready to migrate to Nginx, the approach remains the same (CI4 handles the merging).
|
|
||||||
- **Back to bundling:** If needed, the bundler script is still available: `python bundle-api-docs.py`
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Schema Resolution Errors
|
|
||||||
|
|
||||||
If you see errors like "Could not resolve reference", ensure:
|
|
||||||
1. Schema files exist in `components/schemas/`
|
|
||||||
2. References use correct relative paths (e.g., `./components/schemas/patient.yaml#/Patient`)
|
|
||||||
3. You're using the bundled file (`api-docs.bundled.yaml`) for OpenAPI tools
|
|
||||||
|
|
||||||
### Path Resolution Errors
|
|
||||||
|
|
||||||
OpenAPI 3.0 doesn't support external file references for paths. Always use the bundled version for serving the documentation.
|
|
||||||
|
|
||||||
## Migration Notes
|
|
||||||
|
|
||||||
- The original `api-docs.yaml` is preserved with schema references only
|
|
||||||
- All paths have been moved to individual files in `paths/` directory
|
|
||||||
- A bundler script merges everything into `api-docs.bundled.yaml`
|
|
||||||
- Update your application to serve `api-docs.bundled.yaml` instead
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OpenAPI Documentation Bundler
|
|
||||||
*
|
|
||||||
* This script merges the modular OpenAPI specification files into a single
|
|
||||||
* api-docs.yaml file that can be served to Swagger UI or other OpenAPI tools.
|
|
||||||
*
|
|
||||||
* Usage: php bundle-api-docs.php
|
|
||||||
*/
|
|
||||||
|
|
||||||
$publicDir = __DIR__;
|
|
||||||
$componentsDir = $publicDir . '/components/schemas';
|
|
||||||
$pathsDir = $publicDir . '/paths';
|
|
||||||
$outputFile = $publicDir . '/api-docs.bundled.yaml';
|
|
||||||
|
|
||||||
// Read the base api-docs.yaml
|
|
||||||
$apiDocsContent = file_get_contents($publicDir . '/api-docs.yaml');
|
|
||||||
$apiDocs = yaml_parse($apiDocsContent);
|
|
||||||
|
|
||||||
if ($apiDocs === false) {
|
|
||||||
die("Error: Failed to parse api-docs.yaml\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge paths from all path files
|
|
||||||
$paths = [];
|
|
||||||
$pathFiles = glob($pathsDir . '/*.yaml');
|
|
||||||
|
|
||||||
echo "Found " . count($pathFiles) . " path files to merge...\n";
|
|
||||||
|
|
||||||
foreach ($pathFiles as $filepath) {
|
|
||||||
$filename = basename($filepath);
|
|
||||||
$content = file_get_contents($filepath);
|
|
||||||
$parsed = yaml_parse($content);
|
|
||||||
|
|
||||||
if ($parsed === false) {
|
|
||||||
echo " ⚠ Warning: Failed to parse $filename\n";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$paths = array_merge($paths, $parsed);
|
|
||||||
echo " ✓ Merged " . count($parsed) . " paths from $filename\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the empty paths with merged paths
|
|
||||||
$apiDocs['paths'] = $paths;
|
|
||||||
|
|
||||||
// Write the bundled file
|
|
||||||
$bundledYaml = yaml_emit($apiDocs, YAML_UTF8_ENCODING);
|
|
||||||
|
|
||||||
file_put_contents($outputFile, $bundledYaml);
|
|
||||||
|
|
||||||
echo "\n✅ Successfully bundled OpenAPI spec to: $outputFile\n";
|
|
||||||
echo " Total paths: " . count($paths) . "\n";
|
|
||||||
echo " Total schemas: " . count($apiDocs['components']['schemas']) . "\n";
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
"""
|
|
||||||
OpenAPI Documentation Bundler
|
|
||||||
|
|
||||||
This script merges the modular OpenAPI specification files into a single
|
|
||||||
api-docs.bundled.yaml file that can be served to Swagger UI or other OpenAPI tools.
|
|
||||||
|
|
||||||
It merges paths from the paths/ directory and then uses Redocly CLI to resolve
|
|
||||||
all $ref references into inline definitions.
|
|
||||||
|
|
||||||
Usage: python bundle-api-docs.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
import os
|
|
||||||
import glob
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
public_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
components_dir = os.path.join(public_dir, "components", "schemas")
|
|
||||||
paths_dir = os.path.join(public_dir, "paths")
|
|
||||||
temp_file = os.path.join(public_dir, "api-docs.merged.yaml")
|
|
||||||
output_file = os.path.join(public_dir, "api-docs.bundled.yaml")
|
|
||||||
|
|
||||||
# Read the base api-docs.yaml
|
|
||||||
with open(os.path.join(public_dir, "api-docs.yaml"), "r", encoding="utf-8") as f:
|
|
||||||
api_docs = yaml.safe_load(f)
|
|
||||||
|
|
||||||
# Merge paths from all path files
|
|
||||||
paths = {}
|
|
||||||
path_files = glob.glob(os.path.join(paths_dir, "*.yaml"))
|
|
||||||
|
|
||||||
print(f"Found {len(path_files)} path files to merge...")
|
|
||||||
|
|
||||||
for filepath in path_files:
|
|
||||||
filename = os.path.basename(filepath)
|
|
||||||
try:
|
|
||||||
with open(filepath, "r", encoding="utf-8") as f:
|
|
||||||
parsed = yaml.safe_load(f)
|
|
||||||
|
|
||||||
if parsed:
|
|
||||||
paths.update(parsed)
|
|
||||||
print(f" [OK] Merged {len(parsed)} paths from {filename}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" [WARN] Failed to parse {filename}: {e}")
|
|
||||||
|
|
||||||
# Replace the empty paths with merged paths
|
|
||||||
api_docs["paths"] = paths
|
|
||||||
|
|
||||||
# Write the merged file (still has $refs)
|
|
||||||
with open(temp_file, "w", encoding="utf-8") as f:
|
|
||||||
yaml.dump(
|
|
||||||
api_docs,
|
|
||||||
f,
|
|
||||||
default_flow_style=False,
|
|
||||||
allow_unicode=True,
|
|
||||||
sort_keys=False,
|
|
||||||
width=4096,
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"\n[SUCCESS] Merged paths into: {temp_file}")
|
|
||||||
print(f" Total paths: {len(paths)}")
|
|
||||||
|
|
||||||
# Now use Redocly CLI to resolve all $ref references
|
|
||||||
print("\nResolving $ref references with Redocly CLI...")
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["redocly", "bundle", temp_file, "-o", output_file],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
cwd=public_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f" [SUCCESS] Resolved all $ref references to: {output_file}")
|
|
||||||
# Clean up temp file
|
|
||||||
os.remove(temp_file)
|
|
||||||
print(f" [CLEANUP] Removed temp file: {temp_file}")
|
|
||||||
else:
|
|
||||||
print(f" [ERROR] Redocly CLI failed:")
|
|
||||||
print(f" {result.stderr}")
|
|
||||||
exit(1)
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(" [ERROR] Redocly CLI not found. Install with: npm install -g @redocly/cli")
|
|
||||||
exit(1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f" [ERROR] Failed to run Redocly CLI: {e}")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
print(f"\n{'=' * 60}")
|
|
||||||
print(f"Bundling complete!")
|
|
||||||
print(f" Output: {output_file}")
|
|
||||||
print(f" Total paths: {len(paths)}")
|
|
||||||
print(f" Total schemas: {len(api_docs.get('components', {}).get('schemas', {}))}")
|
|
||||||
print(f"\nYou can now serve this file to Swagger UI.")
|
|
||||||
print(f"{'=' * 60}")
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<title>Elements in HTML</title>
|
|
||||||
<!-- Embed elements Elements via Web Component -->
|
|
||||||
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<elements-api apiDescriptionUrl="openapi.yaml" router="hash" layout="sidebar" />
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
235
public/swagger/index.html
Normal file
235
public/swagger/index.html
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CLQMS API Documentation</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sidebar-bg: #f8f9fa;
|
||||||
|
--sidebar-border: #dee2e6;
|
||||||
|
--text-primary: #212529;
|
||||||
|
--text-secondary: #495057;
|
||||||
|
--accent-color: #49cc90;
|
||||||
|
--hover-bg: rgba(0,0,0,0.05);
|
||||||
|
--active-bg: rgba(73, 204, 144, 0.1);
|
||||||
|
--toggle-bg: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--sidebar-bg: #1a1a1a;
|
||||||
|
--sidebar-border: #333;
|
||||||
|
--text-primary: #e9ecef;
|
||||||
|
--text-secondary: #adb5bd;
|
||||||
|
--accent-color: #5cb85c;
|
||||||
|
--hover-bg: rgba(255,255,255,0.05);
|
||||||
|
--active-bg: rgba(92, 184, 92, 0.15);
|
||||||
|
--toggle-bg: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
width: 260px;
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--sidebar-bg);
|
||||||
|
border-right: 1px solid var(--sidebar-border);
|
||||||
|
overflow-y: auto;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
transition: background 0.3s, border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar h2 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
border-left-color: var(--accent-color);
|
||||||
|
background: var(--active-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger-ui {
|
||||||
|
margin-left: 260px;
|
||||||
|
width: calc(100% - 260px);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] body {
|
||||||
|
background: #0d1117;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] #swagger-ui {
|
||||||
|
background: #0d1117;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .swagger-ui {
|
||||||
|
filter: invert(100%) hue-rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .swagger-ui .topbar img,
|
||||||
|
[data-theme="dark"] .swagger-ui svg,
|
||||||
|
[data-theme="dark"] .swagger-ui img,
|
||||||
|
[data-theme="dark"] .swagger-ui .authorization__btn svg,
|
||||||
|
[data-theme="dark"] .swagger-ui .expand-methods svg,
|
||||||
|
[data-theme="dark"] .swagger-ui .expand-operation svg,
|
||||||
|
[data-theme="dark"] .swagger-ui .copy-to-clipboard svg,
|
||||||
|
[data-theme="dark"] .swagger-ui .opblock-control-arrow svg {
|
||||||
|
filter: invert(100%) hue-rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 20px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--toggle-bg);
|
||||||
|
border: 2px solid var(--sidebar-border);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body { display: block; }
|
||||||
|
#sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 200px;
|
||||||
|
position: relative;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--sidebar-border);
|
||||||
|
}
|
||||||
|
#swagger-ui {
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.theme-toggle {
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav id="sidebar">
|
||||||
|
<h2>Resources</h2>
|
||||||
|
<div id="nav-links"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<button class="theme-toggle" id="themeToggle" title="Toggle theme">
|
||||||
|
<span id="themeIcon">🌙</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||||
|
<script>
|
||||||
|
// Theme management
|
||||||
|
const themeToggle = document.getElementById('themeToggle');
|
||||||
|
const themeIcon = document.getElementById('themeIcon');
|
||||||
|
const html = document.documentElement;
|
||||||
|
|
||||||
|
const savedTheme = localStorage.getItem('swagger-theme') || 'light';
|
||||||
|
html.setAttribute('data-theme', savedTheme);
|
||||||
|
updateThemeIcon(savedTheme);
|
||||||
|
|
||||||
|
themeToggle.addEventListener('click', () => {
|
||||||
|
const currentTheme = html.getAttribute('data-theme');
|
||||||
|
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||||
|
html.setAttribute('data-theme', newTheme);
|
||||||
|
localStorage.setItem('swagger-theme', newTheme);
|
||||||
|
updateThemeIcon(newTheme);
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateThemeIcon(theme) {
|
||||||
|
themeIcon.textContent = theme === 'light' ? '🌙' : '☀️';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ui = SwaggerUIBundle({
|
||||||
|
url: '../api-docs.bundled.yaml',
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
deepLinking: true,
|
||||||
|
presets: [SwaggerUIBundle.presets.apis],
|
||||||
|
layout: 'BaseLayout',
|
||||||
|
onComplete: () => buildSidebar()
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildSidebar() {
|
||||||
|
const container = document.getElementById('nav-links');
|
||||||
|
const tags = document.querySelectorAll('.opblock-tag-section');
|
||||||
|
|
||||||
|
if (!tags.length) {
|
||||||
|
setTimeout(buildSidebar, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
tags.forEach(tag => {
|
||||||
|
const header = tag.querySelector('.opblock-tag');
|
||||||
|
if (!header) return;
|
||||||
|
|
||||||
|
const name = header.getAttribute('data-tag') || header.textContent;
|
||||||
|
const id = header.id;
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.className = 'nav-link';
|
||||||
|
link.textContent = name;
|
||||||
|
link.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
|
||||||
|
link.classList.add('active');
|
||||||
|
};
|
||||||
|
|
||||||
|
container.appendChild(link);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user