All files / src CamSwagger.js

97.88% Statements 139/142
82.6% Branches 19/23
100% Functions 4/4
97.88% Lines 139/142

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 1431x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 1x 1x 1x 1x 1x 32x 1x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 32x 1x 1x 1x 1x 1x 1x 1x 151x 151x 151x 151x       151x 151x 151x 151x 151x 151x 151x 151x 151x 151x 5x 5x 10x 10x 10x 10x 10x 5x 5x 151x 6x 6x 6x 6x 6x 6x 151x 151x 151x 151x 1x 1x 1x 1x 1x 1x 32x 32x 32x 32x 90x 151x 151x 151x 151x 90x 32x 32x 27x 27x 27x 27x 32x 27x 27x 27x 27x 32x 32x 32x 32x 1x 1x 1x  
// @ts-check
// SPDX-License-Identifier: Zlib
// SPDX-FileCopyrightText: 2024 CERN (home.cern)
 
import path from "node:path";
import swaggerJSDoc from "swagger-jsdoc";
import swaggerUi from "swagger-ui-express";
import { cloneDeep, merge } from "@cern/nodash";
import { fileURLToPath } from "node:url";
 
/**
 * @typedef {import('express').Router} Router
 */
 
const dirname = path.dirname(fileURLToPath(import.meta.url));
 
class CamSwagger {
 
  /**
   * @param {CamExpress.CamSwagger.Options} [options]
   * @param {CamExpress.CamSwagger.Info} [extraDoc]
   */
  constructor(options, extraDoc) {
 
    // fill api defintion
    const definition = options?.definition ?? {
      info: {
        title: "cam-express",
        version: "1.0.0"
      }
    };
 
    /** @type {any} */
    this.doc = swaggerJSDoc({
      definition,
      apis: [
        path.join(dirname, "**/**.js"),
        path.join(dirname, "../src/**/**.js")
      ]
    });
 
    this.extra = extraDoc;
  }
 
  /**
   * @param {Router} router
   */
  register(router) {
    router.get("/api-docs.json", (req, res) => {
      res.json(this.doc);
    });
 
    const opts = {
      swaggerOptions: {
        docExpansion: "none",
        displayRequestDuration: true
      }
    };
 
    router.use("/api-docs",
      swaggerUi.serveFiles(this.doc, opts),
      swaggerUi.setup(this.doc, opts)
    );
  }
 
  /**
   * @brief generate swagger doc for given group and camera
   * @param {string} groupName
   * @param {CamExpress.CameraInfo} camera
   */
  camera(groupName, camera) {
    const path = `/cameras/${groupName}/${camera.name}`;
    const cameraDoc = cloneDeep(this.doc?.paths?.["/cameras/{group}/{camera}"]);
 
    if (!cameraDoc) {
      console.warn("Unable to find Swagger camera doc");
      return;
    }
 
    // remove path parameters to be filled
    cameraDoc["get"].parameters.splice(0, 2);
 
    // replace summary by camera description
    cameraDoc["get"].summary = camera.description ||
      "no description provided for this camera";
 
 
    if (camera.ptz) {
      [ "/ptz", "/ptz/limits" ].forEach(
        (sub) => {
          const template = "/cameras/{group}/{camera}" + sub;
          const doc = cloneDeep(this.doc?.paths?.[template]);
          // remove path parameters to be filled
          doc["get"].parameters.splice(0, 2);
          this.doc.paths[path + sub] = doc;
        });
    }
    if (camera.screenshot) {
      const template = "/cameras/{group}/{camera}/screenshot";
      const doc = cloneDeep(this.doc?.paths?.[template]);
      // remove path parameters to be filled
      doc["get"].parameters.splice(0, 2);
      this.doc.paths[path + "/screenshot"] = doc;
    }
 
    // replace var path with current cam path
    this.doc.paths[path] = cameraDoc;
  }
 
  /**
   * Add dynamic doc from cameras config
   * @param {CamExpress.CamService} service
   */
  swag(service) {
    let hasPTZ = false;
    let hasSShot = false;
 
    Object.entries(service.camConfig ?? {}).forEach(([ groupName, group ]) => {
      group.cameras?.forEach((camera) => {
        this.camera(groupName, camera);
 
        hasPTZ = hasPTZ || (camera?.ptz ?? false);
        hasSShot = hasSShot || (camera?.screenshot ?? false);
      });
    });
    if (!hasPTZ) {
      /* remove ptz documentation if not needed */
      [ "/ptz", "/ptz/limits" ].forEach(
        (sub) => { delete this.doc.paths["/cameras/{group}/{camera}" + sub]; });
    }
    if (!hasSShot) {
      /* remove ptz documentation if not needed */
      [ "/screenshot" ].forEach(
        (sub) => { delete this.doc.paths["/cameras/{group}/{camera}" + sub]; });
    }
 
    // Insert additional paths in doc
    merge(this.doc?.paths, this.extra?.paths);
  }
}
 
export default CamSwagger;