UNPKG

@rxap/open-api

Version:

This package provides tools for working with OpenAPI specifications in Angular applications. It includes services for configuring and loading OpenAPI definitions, validating requests and responses against schemas, and handling errors. It also offers utili

907 lines (689 loc) 30.6 kB
<!doctype html> <html class="no-js" lang=""> <head> <meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <title>angular-open-api</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="../images/favicon.ico"> <link rel="stylesheet" href="../styles/style.css"> <link rel="stylesheet" href="../styles/dark.css"> </head> <body> <script> // Blocking script to avoid flickering dark mode // Dark mode toggle button var useDark = window.matchMedia('(prefers-color-scheme: dark)'); var darkModeState = useDark.matches; var $darkModeToggleSwitchers = document.querySelectorAll('.dark-mode-switch input'); var $darkModeToggles = document.querySelectorAll('.dark-mode-switch'); var darkModeStateLocal = localStorage.getItem('compodoc_darkmode-state'); function checkToggle(check) { for (var i = 0; i < $darkModeToggleSwitchers.length; i++) { $darkModeToggleSwitchers[i].checked = check; } } function toggleDarkMode(state) { if (window.localStorage) { localStorage.setItem('compodoc_darkmode-state', state); } checkToggle(state); const hasClass = document.body.classList.contains('dark'); if (state) { for (var i = 0; i < $darkModeToggles.length; i++) { $darkModeToggles[i].classList.add('dark'); } if (!hasClass) { document.body.classList.add('dark'); } } else { for (var i = 0; i < $darkModeToggles.length; i++) { $darkModeToggles[i].classList.remove('dark'); } if (hasClass) { document.body.classList.remove('dark'); } } } useDark.addEventListener('change', function (evt) { toggleDarkMode(evt.matches); }); if (darkModeStateLocal) { darkModeState = darkModeStateLocal === 'true'; } toggleDarkMode(darkModeState); </script> <div class="navbar navbar-default navbar-fixed-top d-md-none p-0"> <div class="d-flex"> <a href="../" class="navbar-brand">angular-open-api</a> <button type="button" class="btn btn-default btn-menu ion-ios-menu" id="btn-menu"></button> </div> </div> <div class="xs-menu menu" id="mobile-menu"> <div id="book-search-input" role="search"><input type="text" placeholder="Type to search"></div> <compodoc-menu></compodoc-menu> </div> <div class="container-fluid main"> <div class="row main"> <div class="d-none d-md-block menu"> <compodoc-menu mode="normal"></compodoc-menu> </div> <!-- START CONTENT --> <div class="content interface"> <div class="content-data"> <ol class="breadcrumb"> <li class="breadcrumb-item">Interfaces</li> <li class="breadcrumb-item" > SchemaValidationResponse</li> </ol> <ul class="nav nav-tabs" role="tablist"> <li class="nav-item"> <a href="#info" class="nav-link" class="nav-link active" role="tab" id="info-tab" data-bs-toggle="tab" data-link="info">Info</a> </li> <li class="nav-item"> <a href="#source" class="nav-link" role="tab" id="source-tab" data-bs-toggle="tab" data-link="source">Source</a> </li> </ul> <div class="tab-content"> <div class="tab-pane fade active in" id="info"> <p class="comment"> <h3>File</h3> </p> <p class="comment"> <code>src/lib/schema-validation.mixin.ts</code> </p> <section data-compodoc="block-index"> <h3 id="index">Index</h3> <table class="table table-sm table-bordered index-table"> <tbody> <tr> <td class="col-md-4"> <h6><b>Properties</b></h6> </td> </tr> <tr> <td class="col-md-4"> <ul class="index-list"> <li> <span class="modifier">Optional</span> <a href="#body" > body </a> </li> <li> <span class="modifier">Optional</span> <a href="#data" > data </a> </li> <li> <a href="#headers" > headers </a> </li> <li> <a href="#status" > status </a> </li> </ul> </td> </tr> </tbody> </table> </section> <section data-compodoc="block-properties"> <h3 id="inputs">Properties</h3> <table class="table table-sm table-bordered"> <tbody> <tr> <td class="col-md-4"> <a name="body"></a> <span class="name "><b>body</b> <a href="#body"> <span class="icon ion-ios-link"></span> </a> </span> </td> </tr> <tr> <td class="col-md-4"> <code>body: <code>Data | null</code> </code> </td> </tr> <tr> <td class="col-md-4"> <i>Type : </i> <code>Data | null</code> </td> </tr> <tr> <td class="col-md-4"> <i>Optional</i> </td> </tr> </tbody> </table> <table class="table table-sm table-bordered"> <tbody> <tr> <td class="col-md-4"> <a name="data"></a> <span class="name "><b>data</b> <a href="#data"> <span class="icon ion-ios-link"></span> </a> </span> </td> </tr> <tr> <td class="col-md-4"> <code>data: <code>Data</code> </code> </td> </tr> <tr> <td class="col-md-4"> <i>Type : </i> <code>Data</code> </td> </tr> <tr> <td class="col-md-4"> <i>Optional</i> </td> </tr> </tbody> </table> <table class="table table-sm table-bordered"> <tbody> <tr> <td class="col-md-4"> <a name="headers"></a> <span class="name "><b>headers</b> <a href="#headers"> <span class="icon ion-ios-link"></span> </a> </span> </td> </tr> <tr> <td class="col-md-4"> <code>headers: <code>HttpHeaders</code> </code> </td> </tr> <tr> <td class="col-md-4"> <i>Type : </i> <code>HttpHeaders</code> </td> </tr> </tbody> </table> <table class="table table-sm table-bordered"> <tbody> <tr> <td class="col-md-4"> <a name="status"></a> <span class="name "><b>status</b> <a href="#status"> <span class="icon ion-ios-link"></span> </a> </span> </td> </tr> <tr> <td class="col-md-4"> <code>status: <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/number" target="_blank" >number</a></code> </code> </td> </tr> <tr> <td class="col-md-4"> <i>Type : </i> <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/number" target="_blank" >number</a></code> </td> </tr> </tbody> </table> </section> </div> <div class="tab-pane fade tab-source-code" id="source"> <pre class="line-numbers compodoc-sourcecode"><code class="language-typescript">import { HttpHeaders, HttpParams, } from &#x27;@angular/common/http&#x27;; import { isDevMode } from &#x27;@angular/core&#x27;; import { HttpRemoteMethodParameter } from &#x27;@rxap/remote-method/http&#x27;; import { assertsObject, coerceArray, isPromiseLike, IsRecord, } from &#x27;@rxap/utilities&#x27;; import Ajv from &#x27;ajv&#x27;; import { OpenAPIV3 } from &#x27;openapi-types&#x27;; import { RxapOpenApiError } from &#x27;./error&#x27;; import { OperationObjectWithMetadata } from &#x27;./open-api&#x27;; import { IsReferenceObject, NotContainsReferenceObjects, } from &#x27;./utilities&#x27;; export interface SchemaValidationResponse&lt;Data&gt; { headers: HttpHeaders; status: number; body?: Data | null; data?: Data; } export class SchemaValidationMixin&lt;Response &#x3D; any, Parameters extends Record&lt;string, any&gt; | void &#x3D; any, RequestBody &#x3D; any&gt; { public static STRICT &#x3D; false; protected disableSchemaValidation?: boolean; /** * Validates the parameters against the schema specified in the operation object * * @param operation * @param parameters * @param strict * @protected */ public validateParameters(operation: OperationObjectWithMetadata, parameters?: Parameters, strict &#x3D; false): void { const operationParameters &#x3D; coerceArray(operation.parameters); if (!NotContainsReferenceObjects&lt;OpenAPIV3.ParameterObject&gt;(operationParameters)) { throw new RxapOpenApiError(&#x27;The operation parameters contains ReferenceObject!&#x27;); } if (parameters &#x3D;&#x3D;&#x3D; undefined) { // TODO : find concept to definition witch parameter should not be checked if required // header parameters are never required if changes the semantic release manager breaks const requiredParameters &#x3D; operationParameters.filter(parameter &#x3D;&gt; parameter.required &amp;&amp; parameter.in !&#x3D;&#x3D; &#x27;header&#x27;); if (requiredParameters.length) { if (isDevMode()) { console.debug(&#x27;Some operation parameters are required!&#x27;, requiredParameters.map(p &#x3D;&gt; p.name)); } this.validationError(&#x27;Some operation parameters are required!&#x27;, strict); } } else if (IsRecord(parameters)) { for (const parameter of operationParameters) { if (parameter.required) { // header parameters are never required if changes the semantic release manager breaks if (parameter.in !&#x3D;&#x3D; &#x27;header&#x27;) { if (!parameters.hasOwnProperty(parameter.name)) { this.validationError(&#x60;The operation parameter &#x27;${ parameter.name }&#x27; is required!&#x60;, strict); } } } if (parameter.schema) { if (parameters.hasOwnProperty(parameter.name)) { const value &#x3D; parameters[parameter.name]; if (!this.validate(parameter.schema, value)) { this.validationError( &#x60;The parameter &#x27;${ parameter.name }&#x27; is not valid against the schema!&#x60;, strict, parameter.schema, value, ); } } } } } else { if (operationParameters.length &#x3D;&#x3D;&#x3D; 0 || operationParameters.every(op &#x3D;&gt; !op.required)) { if (isDevMode()) { console.warn( &#x60;The operation ${ operation.operationId } does not expect any parameters. But a parameter is provided.&#x60;, parameters, ); } } else { throw new Error(&#x27;The parameters object is not a record&#x27;); } } } /** * Validates the http response against the schema specified in the operation object * * @param operation * @param response * @param strict * @protected */ public validateResponse( operation: OperationObjectWithMetadata, response: SchemaValidationResponse&lt;Response&gt;, strict &#x3D; false, ): void { // region only validate the response if the content type is undefined or application/json const contentType &#x3D; response.headers.get(&#x27;Content-Type&#x27;); if (contentType &amp;&amp; contentType !&#x3D;&#x3D; &#x27;application/json&#x27;) { if (isDevMode()) { console.warn(&#x27;Response validation is only supported for content type application/json&#x27;); } return; } // endregion if (operation.responses) { const status &#x3D; response.status; // region extract the response object based on the response status let responseObject: OpenAPIV3.ResponseObject | OpenAPIV3.ReferenceObject | undefined; if (operation.responses[status]) { responseObject &#x3D; operation.responses[status]; } else { // use the default response object if no matching was found. responseObject &#x3D; operation.responses[&#x27;default&#x27;]; } // endregion if (responseObject) { if (IsReferenceObject(responseObject)) { throw new RxapOpenApiError(&#x27;Found a reference object. The operation config is not expand!&#x27;); } // region validate the response against the schema if defined // TODO : create schema that validates all parameters at once if (responseObject.content &amp;&amp; responseObject.content[&#x27;application/json&#x27;] &amp;&amp; responseObject.content[&#x27;application/json&#x27;].schema) { const schema &#x3D; responseObject.content[&#x27;application/json&#x27;].schema; const data &#x3D; response.body ?? response.data; if (!this.validate(schema, data)) { this.validationError(&#x27;The response is not valid ageist the operation schema!&#x27;, strict, schema, data); } } // endregion } } } public validateRequestBody(operation: OperationObjectWithMetadata, body?: RequestBody, strict &#x3D; false): void { // only validate body if type of object if (typeof body !&#x3D;&#x3D; &#x27;object&#x27;) { return; } if (operation.requestBody) { if (IsReferenceObject(operation.requestBody)) { throw new RxapOpenApiError(&#x27;Found a reference object. The operation config is not expand!&#x27;); } if (body &#x3D;&#x3D;&#x3D; undefined &amp;&amp; operation.requestBody.required) { this.validationError(&#x27;The request body is required!&#x27;, strict); } if (operation.requestBody.content &amp;&amp; operation.requestBody.content[&#x27;application/json&#x27;]) { const schema &#x3D; operation.requestBody.content[&#x27;application/json&#x27;].schema; if (schema) { if (!this.validate(schema, body)) { this.validationError(&#x27;The request body is not valid!&#x27;, strict, schema, body); } } } } } public validationError( message: string, strict: boolean, schema?: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, value?: any, ) { if (isDevMode() &amp;&amp; schema !&#x3D;&#x3D; undefined) { console.debug( message, { schema, value, }, ); } if (strict || SchemaValidationMixin.STRICT) { throw new RxapOpenApiError(message, &#x27;&#x27;, &#x27;SchemaValidationMixin&#x27;); } else { console.warn(message); } } public buildHttpParams( operationParameters: OpenAPIV3.ParameterObject[], parameters?: Parameters, ignoreUndefined &#x3D; true, ): HttpParams { let params &#x3D; new HttpParams(); if (IsRecord(parameters)) { for (const parameter of operationParameters.filter(p &#x3D;&gt; p.in &#x3D;&#x3D;&#x3D; &#x27;query&#x27;)) { const parameterName &#x3D; parameter.name; if (parameters.hasOwnProperty(parameter.name)) { const value &#x3D; parameters[parameter.name]; if (value &#x3D;&#x3D;&#x3D; undefined &amp;&amp; ignoreUndefined) { continue; } if (Array.isArray(value)) { if (value.length) { const first &#x3D; value.shift(); params &#x3D; params.set(parameterName, typeof first &#x3D;&#x3D;&#x3D; &#x27;object&#x27; ? JSON.stringify(first) : first); for (const item of value) { params &#x3D; params.append(parameterName, typeof item &#x3D;&#x3D;&#x3D; &#x27;object&#x27; ? JSON.stringify(item) : item); } } } else { params &#x3D; params.set(parameter.name, typeof value &#x3D;&#x3D;&#x3D; &#x27;object&#x27; ? JSON.stringify(value) : value); } } } } return params; } public buildHttpHeaders(operationParameters: OpenAPIV3.ParameterObject[], parameters?: Parameters): HttpHeaders { let headers &#x3D; new HttpHeaders(); if (IsRecord(parameters)) { for (const parameter of operationParameters.filter(p &#x3D;&gt; p.in &#x3D;&#x3D;&#x3D; &#x27;header&#x27;)) { if (parameters.hasOwnProperty(parameter.name)) { const value &#x3D; parameters[parameter.name]; if (Array.isArray(value)) { if (value.length) { const first &#x3D; value.shift(); headers &#x3D; headers.set(parameters[&#x27;name&#x27;], typeof first &#x3D;&#x3D;&#x3D; &#x27;object&#x27; ? JSON.stringify(first) : first); for (const item of value) { headers &#x3D; headers.append(parameters[&#x27;name&#x27;], typeof item &#x3D;&#x3D;&#x3D; &#x27;object&#x27; ? JSON.stringify(item) : item); } } } else { headers &#x3D; headers.set(parameter.name, typeof value &#x3D;&#x3D;&#x3D; &#x27;object&#x27; ? JSON.stringify(value) : value); } } } } return headers; } public buildHttpPathParams( operationParameters: OpenAPIV3.ParameterObject[], parameters?: Parameters, ): Record&lt;string, string&gt; { const pathParams: Record&lt;string, any&gt; &#x3D; {}; if (IsRecord(parameters)) { for (const parameter of operationParameters.filter(p &#x3D;&gt; p.in &#x3D;&#x3D;&#x3D; &#x27;path&#x27;)) { if (parameters.hasOwnProperty(parameter.name)) { pathParams[parameter.name] &#x3D; encodeURIComponent(typeof parameters[parameter.name] &#x3D;&#x3D;&#x3D; &#x27;object&#x27; ? JSON.stringify(parameters[parameter.name]) : parameters[parameter.name]); } } } return pathParams; } /** * Converts open api parameters into the corresponding http options to * create a http request. The transformation is guided by the openapi definition * * @param operation * @param parameters * @param requestBody * @param ignoreUndefined */ public buildHttpOptions( operation: OperationObjectWithMetadata, parameters?: Parameters, requestBody?: RequestBody, ignoreUndefined &#x3D; true, ): HttpRemoteMethodParameter { const options: HttpRemoteMethodParameter &#x3D; {}; const operationParameters &#x3D; coerceArray(operation.parameters); if (!NotContainsReferenceObjects&lt;OpenAPIV3.ParameterObject&gt;(operationParameters)) { throw new RxapOpenApiError(&#x27;The operation parameters contains ReferenceObject!&#x27;); } const params &#x3D; this.buildHttpParams(operationParameters, parameters, ignoreUndefined); const headers &#x3D; this.buildHttpHeaders(operationParameters, parameters); const pathParams: Record&lt;string, any&gt; &#x3D; this.buildHttpPathParams(operationParameters, parameters); if (params.keys().length) { options.params &#x3D; params; } if (headers.keys().length) { options.headers &#x3D; headers; } if (Object.keys(pathParams).length) { options.pathParams &#x3D; pathParams; } if (requestBody !&#x3D;&#x3D; undefined) { const [ body, contentType ] &#x3D; this.buildBody(operation, requestBody); if (body !&#x3D;&#x3D; undefined) { options.body &#x3D; body; if (contentType !&#x3D;&#x3D; undefined) { options.headers ??&#x3D; new HttpHeaders(); options.headers.set(&#x27;Content-Type&#x27;, contentType); } } } return options; } private buildBody(operation: OperationObjectWithMetadata, requestBody: RequestBody): [ any, string | undefined ] { const accept: string[] &#x3D; []; if (operation.requestBody &amp;&amp; !IsReferenceObject(operation.requestBody)) { if (operation.requestBody.content) { for (const contentType of Object.keys(operation.requestBody.content)) { accept.push(contentType); } } } if (!accept.length) { console.warn(&#x27;No content type found for the request body! Omitting the body!&#x27;); return [ undefined, undefined ]; } if (accept.length &gt; 1) { console.warn(&#x27;Multiple content types found for the request body! Using the first one!&#x27;); } const contentType &#x3D; accept[0]; switch (contentType) { case &#x27;application/json&#x27;: assertsObject(requestBody); return [ requestBody, contentType ]; case &#x27;application/x-www-form-urlencoded&#x27;: assertsObject(requestBody); // eslint-disable-next-line no-case-declarations let params &#x3D; new HttpParams(); for (const [ key, value ] of Object.entries(requestBody)) { params &#x3D; params.set(key, value); } return [ params.toString(), contentType ]; case &#x27;multipart/form-data&#x27;: assertsObject(requestBody); // eslint-disable-next-line no-case-declarations const formData &#x3D; new FormData(); // Iterate through the JSON object and append each field to FormData for (const [ key, value ] of Object.entries(requestBody)) { formData.append(key, value); } return [ formData, contentType ]; default: return [ requestBody, contentType ]; } } private validate(schema: string | boolean | object, value: any): boolean { if (this.disableSchemaValidation) { return true; } if (typeof schema !&#x3D;&#x3D; &#x27;object&#x27;) { throw new Error(&#x27;The schema must be an object!&#x27;); } if (!schema) { throw new Error(&#x27;The schema must not be null or undefined!&#x27;); } let result: boolean | PromiseLike&lt;any&gt;; const ajv &#x3D; new Ajv(); const validate &#x3D; ajv.compile(schema); try { result &#x3D; validate(value); } catch (e: any) { console.error(e.message); return false; } if (isPromiseLike(result)) { throw new Error( &#x27;Async schema validation is not yet supported. Ensure the all refs in the openapi schema are internal!&#x27;); } return result; } } </code></pre> </div> </div> </div><div class="search-results"> <div class="has-results"> <h1 class="search-results-title"><span class='search-results-count'></span> results matching "<span class='search-query'></span>"</h1> <ul class="search-results-list"></ul> </div> <div class="no-results"> <h1 class="search-results-title">No results matching "<span class='search-query'></span>"</h1> </div> </div> </div> <!-- END CONTENT --> </div> </div> <label class="dark-mode-switch"> <input type="checkbox"> <span class="slider"> <svg class="slider-icon" viewBox="0 0 24 24" fill="none" height="20" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" width="20" xmlns="http://www.w3.org/2000/svg"> <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"></path> </svg> </span> </label> <script> var COMPODOC_CURRENT_PAGE_DEPTH = 1; var COMPODOC_CURRENT_PAGE_CONTEXT = 'interface'; var COMPODOC_CURRENT_PAGE_URL = 'SchemaValidationResponse.html'; var MAX_SEARCH_RESULTS = 15; </script> <script> $darkModeToggleSwitchers = document.querySelectorAll('.dark-mode-switch input'); checkToggle(darkModeState); if ($darkModeToggleSwitchers.length > 0) { for (var i = 0; i < $darkModeToggleSwitchers.length; i++) { $darkModeToggleSwitchers[i].addEventListener('change', function (event) { darkModeState = !darkModeState; toggleDarkMode(darkModeState); }); } } </script> <script src="../js/libs/custom-elements.min.js"></script> <script src="../js/libs/lit-html.js"></script> <script src="../js/menu-wc.js" defer></script> <script nomodule src="../js/menu-wc_es5.js" defer></script> <script src="../js/libs/bootstrap-native.js"></script> <script src="../js/libs/es6-shim.min.js"></script> <script src="../js/libs/EventDispatcher.js"></script> <script src="../js/libs/promise.min.js"></script> <script src="../js/libs/zepto.min.js"></script> <script src="../js/compodoc.js"></script> <script src="../js/tabs.js"></script> <script src="../js/menu.js"></script> <script src="../js/libs/clipboard.min.js"></script> <script src="../js/libs/prism.js"></script> <script src="../js/sourceCode.js"></script> <script src="../js/search/search.js"></script> <script src="../js/search/lunr.min.js"></script> <script src="../js/search/search-lunr.js"></script> <script src="../js/search/search_index.js"></script> <script src="../js/lazy-load-graphs.js"></script> </body> </html>