├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── eslint.config.mjs ├── jest.config.ts ├── jest.setup.ts ├── package.json ├── src ├── Api.ts ├── Field.ts ├── Operation.ts ├── Parameter.ts ├── Resource.ts ├── graphql │ ├── fetchQuery.ts │ ├── index.ts │ └── parseGraphQl.ts ├── hydra │ ├── fetchJsonLd.test.ts │ ├── fetchJsonLd.ts │ ├── fetchResource.ts │ ├── getParameters.ts │ ├── getType.ts │ ├── index.ts │ ├── parseHydraDocumentation.test.ts │ ├── parseHydraDocumentation.ts │ └── types.ts ├── index.ts ├── openapi3 │ ├── getType.ts │ ├── handleJson.test.ts │ ├── handleJson.ts │ ├── index.ts │ ├── parseOpenApi3Documentation.ts │ └── types.ts ├── swagger │ ├── handleJson.test.ts │ ├── handleJson.ts │ ├── index.ts │ └── parseSwaggerDocumentation.ts ├── types.ts └── utils │ ├── assignSealed.ts │ ├── getResources.test.ts │ ├── getResources.ts │ └── parsedJsonReplacer.ts ├── tsconfig.eslint.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | jobs: 8 | ci: 9 | name: Continuous integration 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Setup Node 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 'current' 18 | - name: Install dependencies 19 | run: yarn install 20 | - name: Check build 21 | run: yarn build 22 | - name: Check coding standards 23 | run: yarn lint 24 | - name: Run tests 25 | run: yarn test 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: ~ 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | release: 11 | name: Create and publish a release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Setup Node 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 'current' 20 | registry-url: https://registry.npmjs.org 21 | - name: Install dependencies 22 | run: yarn install 23 | - name: Check build 24 | run: yarn build 25 | - name: Check coding standards 26 | run: yarn lint 27 | - name: Run tests 28 | run: yarn test 29 | - name: Publish to npm 30 | run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | /node_modules/ 3 | /yarn-error.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kévin Dunglas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # API Doc Parser 2 | 3 | [![GitHub Actions](https://github.com/api-platform/api-doc-parser/workflows/CI/badge.svg?branch=main)](https://github.com/api-platform/api-doc-parser/actions?query=workflow%3ACI+branch%3Amain) 4 | [![npm version](https://badge.fury.io/js/%40api-platform%2Fapi-doc-parser.svg)](https://badge.fury.io/js/%40api-platform%2Fapi-doc-parser) 5 | 6 | `api-doc-parser` is a standalone TypeScript library to parse [Hydra](http://hydra-cg.com), [Swagger](https://swagger.io/specification/v2/), [OpenAPI](https://github.com/OAI/OpenAPI-Specification#the-openapi-specification) and [GraphQL](https://graphql.org/) documentations 7 | and transform them in an intermediate representation. 8 | This data structure can then be used for various tasks such as creating smart API clients, 9 | scaffolding code or building administration interfaces. 10 | 11 | It plays well with the [API Platform](https://api-platform.com) framework. 12 | 13 | ## Install 14 | 15 | With [Yarn](https://yarnpkg.com/): 16 | 17 | yarn add @api-platform/api-doc-parser 18 | 19 | Using [NPM](https://www.npmjs.com/): 20 | 21 | npm install @api-platform/api-doc-parser 22 | 23 | If you plan to use the library with Node, you also need a polyfill for the `fetch` function: 24 | 25 | yarn add isomorphic-fetch 26 | 27 | ## Usage 28 | 29 | **Hydra** 30 | ```javascript 31 | import { parseHydraDocumentation } from '@api-platform/api-doc-parser'; 32 | 33 | parseHydraDocumentation('https://demo.api-platform.com').then(({api}) => console.log(api)); 34 | ``` 35 | 36 | **OpenAPI v2 (formerly known as Swagger)** 37 | ```javascript 38 | import { parseSwaggerDocumentation } from '@api-platform/api-doc-parser'; 39 | 40 | parseSwaggerDocumentation('https://demo.api-platform.com/docs.json').then(({api}) => console.log(api)); 41 | ``` 42 | 43 | **OpenAPI v3** 44 | ```javascript 45 | import { parseOpenApi3Documentation } from '@api-platform/api-doc-parser'; 46 | 47 | parseOpenApi3Documentation('https://demo.api-platform.com/docs.json?spec_version=3').then(({api}) => console.log(api)); 48 | ``` 49 | 50 | **GraphQL** 51 | ```javascript 52 | import { parseGraphQl } from '@api-platform/api-doc-parser'; 53 | 54 | parseGraphQl('https://demo.api-platform.com/graphql').then(({api}) => console.log(api)); 55 | ``` 56 | 57 | ## OpenAPI Support 58 | 59 | In order to support OpenAPI, the library makes some assumptions about how the documentation relates to a corresponding ressource: 60 | - The path to get (`GET`) or edit (`PUT`) one resource looks like `/books/{id}` (regular expression used: `^[^{}]+/{[^{}]+}/?$`). 61 | Note that `books` may be a singular noun (`book`). 62 | If there is no path like this, the library skips the resource. 63 | - The corresponding path schema is retrieved for `get` either in the [`response` / `200` / `content` / `application/json`] path section or in the `components` section of the documentation. 64 | If retrieved from the `components` section, the component name needs to look like `Book` (singular noun). 65 | For `put`, the schema is only retrieved in the [`requestBody` / `content` / `application/json`] path section. 66 | If no schema is found, the resource is skipped. 67 | - If there are two schemas (one for `get` and one for `put`), resource fields are merged. 68 | - The library looks for a creation (`POST`) and list (`GET`) path. They need to look like `/books` (plural noun). 69 | - The deletion (`DELETE`) path needs to be inside the get / edit path. 70 | - In order to reference the resources between themselves (embeddeds or relations), the library guesses embeddeds or references from property names. 71 | For instance if a book schema has a `reviews` property, the library tries to find a `Review` resource. 72 | If there is, a relation or an embedded between `Book` and `Review` resources is made for the `reviews` field. 73 | The property name can also be like `review_id`, `reviewId`, `review_ids` or `reviewIds` for references. 74 | - Parameters are only retrieved in the list path. 75 | 76 | ## Support for other formats (JSON:API...) 77 | 78 | API Doc Parser is designed to parse any API documentation format and convert it in the same intermediate representation. 79 | If you develop a parser for another format, please [open a Pull Request](https://github.com/api-platform/api-doc-parser/pulls) 80 | to include it in the library. 81 | 82 | ## Run tests 83 | 84 | yarn test 85 | yarn lint 86 | 87 | ## Credits 88 | 89 | Created by [Kévin Dunglas](https://dunglas.fr). Sponsored by [Les-Tilleuls.coop](https://les-tilleuls.coop). 90 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from "@eslint/js"; 4 | import tseslint from "typescript-eslint"; 5 | import eslintConfigPrettier from "eslint-config-prettier"; 6 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; 7 | 8 | export default tseslint.config( 9 | eslint.configs.recommended, 10 | tseslint.configs.recommended, 11 | eslintConfigPrettier, 12 | eslintPluginPrettierRecommended, 13 | { 14 | rules: { 15 | "@typescript-eslint/no-unsafe-declaration-merging": "off", 16 | "@typescript-eslint/no-explicit-any": "off", 17 | "@typescript-eslint/no-empty-object-type": [ 18 | "error", 19 | { 20 | allowInterfaces: "with-single-extends", 21 | }, 22 | ], 23 | }, 24 | }, 25 | ); 26 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | 3 | const config: Config.InitialOptions = { 4 | extensionsToTreatAsEsm: [".ts"], 5 | setupFilesAfterEnv: ["./jest.setup.ts"], 6 | moduleNameMapper: { 7 | "^(\\.{1,2}/.*)\\.js$": "$1", 8 | }, 9 | transform: { 10 | "^.+\\.ts$": [ 11 | "ts-jest", 12 | { 13 | useESM: true, 14 | }, 15 | ], 16 | }, 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import jestMock from "jest-fetch-mock"; 2 | 3 | jestMock.enableMocks(); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@api-platform/api-doc-parser", 3 | "version": "0.16.8", 4 | "description": "Transform an API documentation (Hydra, OpenAPI, GraphQL) in an intermediate representation that can be used for various tasks such as creating smart API clients, scaffolding code or building administration interfaces.", 5 | "files": [ 6 | "*.md", 7 | "lib", 8 | "src" 9 | ], 10 | "type": "module", 11 | "exports": "./lib/index.js", 12 | "main": "./lib/index.js", 13 | "repository": "api-platform/api-doc-parser", 14 | "homepage": "https://github.com/api-platform/api-doc-parser", 15 | "bugs": "https://github.com/api-platform/api-doc-parser/issues", 16 | "author": "Kévin Dunglas", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@eslint/compat": "^1.2.5", 20 | "@eslint/eslintrc": "^3.2.0", 21 | "@eslint/js": "^9.19.0", 22 | "@types/inflection": "^1.13.0", 23 | "@types/jest": "^29.0.0", 24 | "@types/jsonld": "^1.5.0", 25 | "@types/lodash.get": "^4.4.0", 26 | "@types/node": "^22.0.0", 27 | "eslint": "^9.0.0", 28 | "eslint-config-prettier": "^10.0.0", 29 | "eslint-import-resolver-typescript": "^3.5.1", 30 | "eslint-plugin-import": "^2.26.0", 31 | "eslint-plugin-prettier": "^5.0.0", 32 | "eslint-watch": "^8.0.0", 33 | "globals": "^15.14.0", 34 | "jest": "^29.0.0", 35 | "jest-fetch-mock": "^3.0.0", 36 | "openapi-types": "^12.0.0", 37 | "prettier": "^3.0.0", 38 | "ts-jest": "^29.0.0", 39 | "ts-node": "^10.9.0", 40 | "typescript": "^5.7.0", 41 | "typescript-eslint": "^8.22.0" 42 | }, 43 | "dependencies": { 44 | "graphql": "^16.0.0", 45 | "inflection": "^3.0.0", 46 | "jsonld": "^8.3.2", 47 | "jsonref": "^9.0.0", 48 | "lodash.get": "^4.4.0", 49 | "tslib": "^2.0.0" 50 | }, 51 | "scripts": { 52 | "test": "NODE_OPTIONS=--experimental-vm-modules jest", 53 | "lint": "eslint src", 54 | "fix": "yarn lint --fix", 55 | "eslint-check": "eslint-config-prettier src/index.ts", 56 | "build": "rm -rf lib/* && tsc", 57 | "watch": "tsc --watch" 58 | }, 59 | "sideEffects": false, 60 | "publishConfig": { 61 | "access": "public" 62 | }, 63 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 64 | } 65 | -------------------------------------------------------------------------------- /src/Api.ts: -------------------------------------------------------------------------------- 1 | import { assignSealed } from "./utils/assignSealed.js"; 2 | import type { Resource } from "./Resource.js"; 3 | import type { Nullable } from "./types.js"; 4 | 5 | export interface ApiOptions 6 | extends Nullable<{ 7 | title?: string; 8 | resources?: Resource[]; 9 | }> {} 10 | 11 | export interface Api extends ApiOptions {} 12 | export class Api { 13 | constructor( 14 | public entrypoint: string, 15 | options: ApiOptions = {}, 16 | ) { 17 | assignSealed(this, options); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Field.ts: -------------------------------------------------------------------------------- 1 | import { assignSealed } from "./utils/assignSealed.js"; 2 | import type { Resource } from "./Resource.js"; 3 | import type { Nullable } from "./types.js"; 4 | 5 | export type FieldType = 6 | | "string" 7 | | "integer" 8 | | "negativeInteger" 9 | | "nonNegativeInteger" 10 | | "positiveInteger" 11 | | "nonPositiveInteger" 12 | | "number" 13 | | "decimal" 14 | | "double" 15 | | "float" 16 | | "boolean" 17 | | "date" 18 | | "dateTime" 19 | | "duration" 20 | | "time" 21 | | "byte" 22 | | "binary" 23 | | "hexBinary" 24 | | "base64Binary" 25 | | "array" 26 | | "object" 27 | | "email" 28 | | "url" 29 | | "uuid" 30 | | "password" 31 | | string; 32 | 33 | export interface FieldOptions 34 | extends Nullable<{ 35 | id?: string; 36 | range?: string; 37 | type?: FieldType; 38 | arrayType?: FieldType; 39 | enum?: { [key: string | number]: string | number }; 40 | reference?: string | Resource; 41 | embedded?: Resource; 42 | required?: boolean; 43 | nullable?: boolean; 44 | description?: string; 45 | maxCardinality?: number; 46 | deprecated?: boolean; 47 | }> {} 48 | 49 | export interface Field extends FieldOptions {} 50 | export class Field { 51 | constructor( 52 | public name: string, 53 | options: FieldOptions = {}, 54 | ) { 55 | assignSealed(this, options); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Operation.ts: -------------------------------------------------------------------------------- 1 | import { assignSealed } from "./utils/assignSealed.js"; 2 | import type { Nullable } from "./types.js"; 3 | 4 | export type OperationType = "show" | "edit" | "delete" | "list" | "create"; 5 | 6 | export interface OperationOptions 7 | extends Nullable<{ 8 | method?: string; 9 | expects?: any; 10 | returns?: string; 11 | types?: string[]; 12 | deprecated?: boolean; 13 | }> {} 14 | 15 | export interface Operation extends OperationOptions {} 16 | export class Operation { 17 | constructor( 18 | public name: string, 19 | public type: OperationType, 20 | options: OperationOptions = {}, 21 | ) { 22 | assignSealed(this, options); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Parameter.ts: -------------------------------------------------------------------------------- 1 | export class Parameter { 2 | constructor( 3 | public variable: string, 4 | public range: string | null, 5 | public required: boolean, 6 | public description: string, 7 | public deprecated?: boolean, 8 | ) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/Resource.ts: -------------------------------------------------------------------------------- 1 | import { assignSealed } from "./utils/assignSealed.js"; 2 | import type { Field } from "./Field.js"; 3 | import type { Operation } from "./Operation.js"; 4 | import type { Parameter } from "./Parameter.js"; 5 | import type { Nullable } from "./types.js"; 6 | 7 | export interface ResourceOptions 8 | extends Nullable<{ 9 | id?: string; 10 | title?: string; 11 | description?: string; 12 | deprecated?: boolean; 13 | fields?: Field[]; 14 | readableFields?: Field[]; 15 | writableFields?: Field[]; 16 | parameters?: Parameter[]; 17 | getParameters?: () => Promise; 18 | operations?: Operation[]; 19 | }> {} 20 | 21 | export interface Resource extends ResourceOptions {} 22 | export class Resource { 23 | constructor( 24 | public name: string, 25 | public url: string, 26 | options: ResourceOptions = {}, 27 | ) { 28 | assignSealed(this, options); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/graphql/fetchQuery.ts: -------------------------------------------------------------------------------- 1 | import type { ExecutionResult } from "graphql"; 2 | 3 | const setOptions = (query: string, options: RequestInit): RequestInit => { 4 | if (!options.method) { 5 | options.method = "POST"; 6 | } 7 | 8 | if (!(options.headers instanceof Headers)) { 9 | options.headers = new Headers(options.headers); 10 | } 11 | 12 | if (null === options.headers.get("Content-Type")) { 13 | options.headers.set("Content-Type", "application/json"); 14 | } 15 | 16 | if ("GET" !== options.method && !options.body) { 17 | options.body = JSON.stringify({ query }); 18 | } 19 | 20 | return options; 21 | }; 22 | 23 | export default async ( 24 | url: string, 25 | query: string, 26 | options: RequestInit = {}, 27 | ): Promise<{ 28 | response: Response; 29 | body: ExecutionResult; 30 | }> => { 31 | const response = await fetch(url, setOptions(query, options)); 32 | const body = (await response.json()) as ExecutionResult; 33 | 34 | if (body?.errors) { 35 | return Promise.reject({ response, body }); 36 | } 37 | 38 | return Promise.resolve({ response, body }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/graphql/index.ts: -------------------------------------------------------------------------------- 1 | export { default as fetchQuery } from "./fetchQuery.js"; 2 | export { default as parseGraphQl } from "./parseGraphQl.js"; 3 | -------------------------------------------------------------------------------- /src/graphql/parseGraphQl.ts: -------------------------------------------------------------------------------- 1 | import { getIntrospectionQuery } from "graphql/utilities/index.js"; 2 | import fetchQuery from "./fetchQuery.js"; 3 | import { Api } from "../Api.js"; 4 | import { Field } from "../Field.js"; 5 | import { Resource } from "../Resource.js"; 6 | import type { 7 | IntrospectionObjectType, 8 | IntrospectionOutputTypeRef, 9 | IntrospectionQuery, 10 | } from "graphql/utilities"; 11 | 12 | const getRangeFromGraphQlType = (type: IntrospectionOutputTypeRef): string => { 13 | if (type.kind === "NON_NULL") { 14 | if (type.ofType.kind === "LIST") { 15 | return `Array<${getRangeFromGraphQlType(type.ofType.ofType)}>`; 16 | } 17 | 18 | return type.ofType.name; 19 | } 20 | 21 | if (type.kind === "LIST") { 22 | return `Array<${getRangeFromGraphQlType(type.ofType)}>`; 23 | } 24 | 25 | return type.name; 26 | }; 27 | 28 | const getReferenceFromGraphQlType = ( 29 | type: IntrospectionOutputTypeRef, 30 | ): null | string => { 31 | if (type.kind === "OBJECT" && type.name.endsWith("Connection")) { 32 | return type.name.slice(0, type.name.lastIndexOf("Connection")); 33 | } 34 | 35 | return null; 36 | }; 37 | 38 | export default async ( 39 | entrypointUrl: string, 40 | options: RequestInit = {}, 41 | ): Promise<{ 42 | api: Api; 43 | response: Response; 44 | }> => { 45 | const introspectionQuery = getIntrospectionQuery(); 46 | 47 | const { 48 | response, 49 | body: { data }, 50 | } = await fetchQuery( 51 | entrypointUrl, 52 | introspectionQuery, 53 | options, 54 | ); 55 | if (!data?.__schema) { 56 | throw new Error( 57 | "Schema has not been retrieved from the introspection query.", 58 | ); 59 | } 60 | const schema = data?.__schema; 61 | 62 | const typeResources = schema.types.filter( 63 | (type) => 64 | type.kind === "OBJECT" && 65 | type.name !== schema.queryType.name && 66 | type.name !== schema.mutationType?.name && 67 | type.name !== schema.subscriptionType?.name && 68 | !type.name.startsWith("__") && 69 | // mutation 70 | !type.name.startsWith(type.name[0].toLowerCase()) && 71 | !type.name.endsWith("Connection") && 72 | !type.name.endsWith("Edge") && 73 | !type.name.endsWith("PageInfo"), 74 | ) as IntrospectionObjectType[]; 75 | 76 | const resources: Resource[] = []; 77 | typeResources.forEach((typeResource) => { 78 | const fields: Field[] = []; 79 | const readableFields: Field[] = []; 80 | const writableFields: Field[] = []; 81 | 82 | typeResource.fields.forEach((resourceFieldType) => { 83 | const field = new Field(resourceFieldType.name, { 84 | range: getRangeFromGraphQlType(resourceFieldType.type), 85 | reference: getReferenceFromGraphQlType(resourceFieldType.type), 86 | required: resourceFieldType.type.kind === "NON_NULL", 87 | description: resourceFieldType.description, 88 | deprecated: resourceFieldType.isDeprecated, 89 | }); 90 | 91 | fields.push(field); 92 | readableFields.push(field); 93 | writableFields.push(field); 94 | }); 95 | 96 | resources.push( 97 | new Resource(typeResource.name, "", { 98 | fields, 99 | readableFields, 100 | writableFields, 101 | }), 102 | ); 103 | }); 104 | 105 | resources.forEach((resource) => { 106 | resource.fields?.forEach((field) => { 107 | if (null !== field.reference) { 108 | field.reference = 109 | resources.find((resource) => resource.name === field.reference) || 110 | null; 111 | } else if (null !== field.range) { 112 | field.reference = 113 | resources.find((resource) => resource.name === field.range) || null; 114 | } 115 | }); 116 | }); 117 | 118 | return { 119 | api: new Api(entrypointUrl, { resources }), 120 | response, 121 | }; 122 | }; 123 | -------------------------------------------------------------------------------- /src/hydra/fetchJsonLd.test.ts: -------------------------------------------------------------------------------- 1 | import fetchJsonLd from "./fetchJsonLd.js"; 2 | import type { FetchMock } from "jest-fetch-mock"; 3 | import type { ResponseDocument } from "./fetchJsonLd.js"; 4 | 5 | const fetchMock = fetch as FetchMock; 6 | 7 | test("fetch a JSON-LD document", () => { 8 | fetchMock.mockResponseOnce( 9 | `{ 10 | "@context": "http://json-ld.org/contexts/person.jsonld", 11 | "@id": "http://dbpedia.org/resource/John_Lennon", 12 | "name": "John Lennon", 13 | "born": "1940-10-09", 14 | "spouse": "http://dbpedia.org/resource/Cynthia_Lennon" 15 | }`, 16 | { 17 | status: 200, 18 | statusText: "OK", 19 | headers: { "Content-Type": "application/ld+json" }, 20 | }, 21 | ); 22 | 23 | return fetchJsonLd("/foo.jsonld").then((data) => { 24 | expect(data.response.ok).toBe(true); 25 | expect(((data as ResponseDocument).body as { name: string }).name).toBe( 26 | "John Lennon", 27 | ); 28 | }); 29 | }); 30 | 31 | test("fetch a non JSON-LD document", () => { 32 | fetchMock.mockResponseOnce(`Hello`, { 33 | status: 200, 34 | statusText: "OK", 35 | headers: { "Content-Type": "text/html" }, 36 | }); 37 | 38 | return fetchJsonLd("/foo.jsonld").catch( 39 | (data: { response: Response; body: undefined }) => { 40 | expect(data.response.ok).toBe(true); 41 | expect(typeof data.body).toBe("undefined"); 42 | }, 43 | ); 44 | }); 45 | 46 | test("fetch an error with Content-Type application/ld+json", () => { 47 | fetchMock.mockResponseOnce( 48 | `{ 49 | "@context": "http://json-ld.org/contexts/person.jsonld", 50 | "@id": "http://dbpedia.org/resource/John_Lennon", 51 | "name": "John Lennon", 52 | "born": "1940-10-09", 53 | "spouse": "http://dbpedia.org/resource/Cynthia_Lennon" 54 | }`, 55 | { 56 | status: 400, 57 | statusText: "Bad Request", 58 | headers: { "Content-Type": "application/ld+json" }, 59 | }, 60 | ); 61 | 62 | return fetchJsonLd("/foo.jsonld").catch( 63 | ({ response }: { response: Response }) => { 64 | void response.json().then((body: { born: string }) => { 65 | expect(response.ok).toBe(false); 66 | expect(body.born).toBe("1940-10-09"); 67 | }); 68 | }, 69 | ); 70 | }); 71 | 72 | test("fetch an error with Content-Type application/error+json", () => { 73 | fetchMock.mockResponseOnce( 74 | `{ 75 | "@context": "http://json-ld.org/contexts/person.jsonld", 76 | "@id": "http://dbpedia.org/resource/John_Lennon", 77 | "name": "John Lennon", 78 | "born": "1940-10-09", 79 | "spouse": "http://dbpedia.org/resource/Cynthia_Lennon" 80 | }`, 81 | { 82 | status: 400, 83 | statusText: "Bad Request", 84 | headers: { "Content-Type": "application/error+json" }, 85 | }, 86 | ); 87 | 88 | return fetchJsonLd("/foo.jsonld").catch( 89 | ({ response }: { response: Response }) => { 90 | void response.json().then((body: { born: string }) => { 91 | expect(response.ok).toBe(false); 92 | expect(body.born).toBe("1940-10-09"); 93 | }); 94 | }, 95 | ); 96 | }); 97 | 98 | test("fetch an empty document", () => { 99 | fetchMock.mockResponseOnce("", { 100 | status: 204, 101 | statusText: "No Content", 102 | headers: { "Content-Type": "text/html" }, 103 | }); 104 | 105 | return fetchJsonLd("/foo.jsonld").then((data) => { 106 | expect(data.response.ok).toBe(true); 107 | expect((data as ResponseDocument).body).toBe(undefined); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/hydra/fetchJsonLd.ts: -------------------------------------------------------------------------------- 1 | import type { Document, JsonLd, RemoteDocument } from "jsonld/jsonld-spec"; 2 | import type { RequestInitExtended } from "./types.js"; 3 | 4 | const jsonLdMimeType = "application/ld+json"; 5 | const jsonProblemMimeType = "application/problem+json"; 6 | 7 | export type RejectedResponseDocument = { 8 | response: Response; 9 | }; 10 | 11 | export type EmptyResponseDocument = { 12 | response: Response; 13 | }; 14 | 15 | export interface ResponseDocument extends RemoteDocument { 16 | response: Response; 17 | body: Document; 18 | } 19 | 20 | /** 21 | * Sends a JSON-LD request to the API. 22 | */ 23 | export default async function fetchJsonLd( 24 | url: string, 25 | options: RequestInitExtended = {}, 26 | ): Promise { 27 | const response = await fetch(url, setHeaders(options)); 28 | const { headers, status } = response; 29 | const contentType = headers.get("Content-Type"); 30 | 31 | if (204 === status) { 32 | return Promise.resolve({ response }); 33 | } 34 | 35 | if ( 36 | 500 <= status || 37 | !contentType || 38 | (!contentType.includes(jsonLdMimeType) && 39 | !contentType.includes(jsonProblemMimeType)) 40 | ) { 41 | const reason: RejectedResponseDocument = { response }; 42 | return Promise.reject(reason); 43 | } 44 | 45 | return response.json().then((body: JsonLd) => ({ 46 | response, 47 | body, 48 | document: body, 49 | documentUrl: url, 50 | })); 51 | } 52 | 53 | function setHeaders(options: RequestInitExtended): RequestInit { 54 | if (!options.headers) { 55 | return { ...options, headers: {} }; 56 | } 57 | 58 | let headers: HeadersInit = 59 | typeof options.headers === "function" ? options.headers() : options.headers; 60 | 61 | headers = new Headers(headers); 62 | 63 | if (null === headers.get("Accept")) { 64 | headers.set("Accept", jsonLdMimeType); 65 | } 66 | 67 | const result = { ...options, headers }; 68 | 69 | if ( 70 | "undefined" !== result.body && 71 | !(typeof FormData !== "undefined" && result.body instanceof FormData) && 72 | null === result.headers.get("Content-Type") 73 | ) { 74 | result.headers.set("Content-Type", jsonLdMimeType); 75 | } 76 | 77 | return result; 78 | } 79 | -------------------------------------------------------------------------------- /src/hydra/fetchResource.ts: -------------------------------------------------------------------------------- 1 | import type { EmptyResponseDocument, ResponseDocument } from "./fetchJsonLd.js"; 2 | import fetchJsonLd from "./fetchJsonLd.js"; 3 | import type { IriTemplateMapping, RequestInitExtended } from "./types.js"; 4 | 5 | export default ( 6 | resourceUrl: string, 7 | options: RequestInitExtended = {}, 8 | ): Promise<{ parameters: IriTemplateMapping[] }> => { 9 | return fetchJsonLd( 10 | resourceUrl, 11 | Object.assign({ itemsPerPage: 0 }, options), 12 | ).then((d: ResponseDocument | EmptyResponseDocument) => { 13 | let hasPrefix = true; 14 | if ((d as ResponseDocument).body) { 15 | hasPrefix = "hydra:search" in (d as ResponseDocument).body; 16 | } 17 | return { 18 | parameters: (hasPrefix 19 | ? (d as any)?.body?.["hydra:search"]?.["hydra:mapping"] 20 | : (d as any)?.body?.search?.mapping) as unknown as IriTemplateMapping[], 21 | }; 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/hydra/getParameters.ts: -------------------------------------------------------------------------------- 1 | import { Parameter } from "../Parameter.js"; 2 | import fetchResource from "./fetchResource.js"; 3 | import type { Resource } from "../Resource.js"; 4 | import type { RequestInitExtended } from "./types.js"; 5 | 6 | export default ( 7 | resource: Resource, 8 | options: RequestInitExtended = {}, 9 | ): Promise => 10 | fetchResource(resource.url, options).then(({ parameters = [] }) => { 11 | const resourceParameters: Parameter[] = []; 12 | parameters.forEach(({ property = null, required, variable }) => { 13 | if (null === property) { 14 | return; 15 | } 16 | 17 | const { range = null } = resource.fields 18 | ? resource.fields.find(({ name }) => property === name) || {} 19 | : {}; 20 | 21 | resourceParameters.push(new Parameter(variable, range, required, "")); 22 | }); 23 | resource.parameters = resourceParameters; 24 | 25 | return resourceParameters; 26 | }); 27 | -------------------------------------------------------------------------------- /src/hydra/getType.ts: -------------------------------------------------------------------------------- 1 | import type { FieldType } from "../Field.js"; 2 | 3 | const getType = (id: string, range: string): FieldType => { 4 | switch (id) { 5 | case "http://schema.org/email": 6 | case "https://schema.org/email": 7 | return "email"; 8 | case "http://schema.org/url": 9 | case "https://schema.org/url": 10 | return "url"; 11 | default: 12 | } 13 | 14 | switch (range) { 15 | case "http://www.w3.org/2001/XMLSchema#array": 16 | return "array"; 17 | case "http://www.w3.org/2001/XMLSchema#integer": 18 | case "http://www.w3.org/2001/XMLSchema#int": 19 | case "http://www.w3.org/2001/XMLSchema#long": 20 | case "http://www.w3.org/2001/XMLSchema#short": 21 | case "https://schema.org/Integer": 22 | return "integer"; 23 | case "http://www.w3.org/2001/XMLSchema#negativeInteger": 24 | return "negativeInteger"; 25 | case "http://www.w3.org/2001/XMLSchema#nonNegativeInteger": 26 | case "http://www.w3.org/2001/XMLSchema#unsignedInt": 27 | case "http://www.w3.org/2001/XMLSchema#unsignedLong": 28 | case "http://www.w3.org/2001/XMLSchema#unsignedShort": 29 | return "nonNegativeInteger"; 30 | case "http://www.w3.org/2001/XMLSchema#positiveInteger": 31 | return "positiveInteger"; 32 | case "http://www.w3.org/2001/XMLSchema#nonPositiveInteger": 33 | return "nonPositiveInteger"; 34 | case "http://www.w3.org/2001/XMLSchema#decimal": 35 | return "decimal"; 36 | case "http://www.w3.org/2001/XMLSchema#double": 37 | return "double"; 38 | case "http://www.w3.org/2001/XMLSchema#float": 39 | case "https://schema.org/Float": 40 | return "float"; 41 | case "http://www.w3.org/2001/XMLSchema#boolean": 42 | case "https://schema.org/Boolean": 43 | return "boolean"; 44 | case "http://www.w3.org/2001/XMLSchema#date": 45 | case "http://www.w3.org/2001/XMLSchema#gYear": 46 | case "http://www.w3.org/2001/XMLSchema#gYearMonth": 47 | case "http://www.w3.org/2001/XMLSchema#gMonth": 48 | case "http://www.w3.org/2001/XMLSchema#gMonthDay": 49 | case "http://www.w3.org/2001/XMLSchema#gDay": 50 | case "https://schema.org/Date": 51 | return "date"; 52 | case "http://www.w3.org/2001/XMLSchema#dateTime": 53 | case "https://schema.org/DateTime": 54 | return "dateTime"; 55 | case "http://www.w3.org/2001/XMLSchema#duration": 56 | return "duration"; 57 | case "http://www.w3.org/2001/XMLSchema#time": 58 | case "https://schema.org/Time": 59 | return "time"; 60 | case "http://www.w3.org/2001/XMLSchema#byte": 61 | case "http://www.w3.org/2001/XMLSchema#unsignedByte": 62 | return "byte"; 63 | case "http://www.w3.org/2001/XMLSchema#hexBinary": 64 | return "hexBinary"; 65 | case "http://www.w3.org/2001/XMLSchema#base64Binary": 66 | return "base64Binary"; 67 | default: 68 | return "string"; 69 | } 70 | }; 71 | 72 | export default getType; 73 | -------------------------------------------------------------------------------- /src/hydra/index.ts: -------------------------------------------------------------------------------- 1 | export { default as fetchJsonLd } from "./fetchJsonLd.js"; 2 | export { 3 | default as parseHydraDocumentation, 4 | getDocumentationUrlFromHeaders, 5 | } from "./parseHydraDocumentation.js"; 6 | -------------------------------------------------------------------------------- /src/hydra/parseHydraDocumentation.test.ts: -------------------------------------------------------------------------------- 1 | import parseHydraDocumentation from "./parseHydraDocumentation.js"; 2 | import parsedJsonReplacer from "../utils/parsedJsonReplacer.js"; 3 | import type { FetchMock, MockParams } from "jest-fetch-mock"; 4 | import type { Api } from "../Api.js"; 5 | 6 | const fetchMock = fetch as FetchMock; 7 | 8 | const entrypoint = `{ 9 | "@context": { 10 | "@vocab": "http://localhost/docs.jsonld#", 11 | "hydra": "http://www.w3.org/ns/hydra/core#", 12 | "book": { 13 | "@id": "Entrypoint/book", 14 | "@type": "@id" 15 | }, 16 | "review": { 17 | "@id": "Entrypoint/review", 18 | "@type": "@id" 19 | }, 20 | "customResource": { 21 | "@id": "Entrypoint/customResource", 22 | "@type": "@id" 23 | }, 24 | "deprecatedResource": { 25 | "@id": "Entrypoint/deprecatedResource", 26 | "@type": "@id" 27 | } 28 | }, 29 | "@id": "/", 30 | "@type": "Entrypoint", 31 | "book": "/books", 32 | "review": "/reviews", 33 | "customResource": "/customResources", 34 | "deprecatedResource": "/deprecated_resources" 35 | }`; 36 | 37 | const docs = `{ 38 | "@context": { 39 | "@vocab": "http://localhost/docs.jsonld#", 40 | "hydra": "http://www.w3.org/ns/hydra/core#", 41 | "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 42 | "rdfs": "http://www.w3.org/2000/01/rdf-schema#", 43 | "xmls": "http://www.w3.org/2001/XMLSchema#", 44 | "owl": "http://www.w3.org/2002/07/owl#", 45 | "domain": { 46 | "@id": "rdfs:domain", 47 | "@type": "@id" 48 | }, 49 | "range": { 50 | "@id": "rdfs:range", 51 | "@type": "@id" 52 | }, 53 | "subClassOf": { 54 | "@id": "rdfs:subClassOf", 55 | "@type": "@id" 56 | }, 57 | "expects": { 58 | "@id": "hydra:expects", 59 | "@type": "@id" 60 | }, 61 | "returns": { 62 | "@id": "hydra:returns", 63 | "@type": "@id" 64 | } 65 | }, 66 | "@id": "/docs.jsonld", 67 | "hydra:title": "API Platform's demo", 68 | "hydra:description": "A test", 69 | "hydra:entrypoint": "/", 70 | "hydra:supportedClass": [ 71 | { 72 | "@id": "http://schema.org/Book", 73 | "@type": "hydra:Class", 74 | "rdfs:label": "Book", 75 | "hydra:title": "Book", 76 | "hydra:supportedProperty": [ 77 | { 78 | "@type": "hydra:SupportedProperty", 79 | "hydra:property": { 80 | "@id": "http://schema.org/isbn", 81 | "@type": "rdf:Property", 82 | "rdfs:label": "isbn", 83 | "domain": "http://schema.org/Book", 84 | "range": "xmls:string" 85 | }, 86 | "hydra:title": "isbn", 87 | "hydra:required": true, 88 | "hydra:readable": true, 89 | "hydra:writeable": true, 90 | "hydra:description": "The ISBN of the book" 91 | }, 92 | { 93 | "@type": "hydra:SupportedProperty", 94 | "hydra:property": { 95 | "@id": "http://schema.org/name", 96 | "@type": "rdf:Property", 97 | "rdfs:label": "name", 98 | "domain": "http://schema.org/Book", 99 | "range": "xmls:string" 100 | }, 101 | "hydra:title": "name", 102 | "hydra:required": true, 103 | "hydra:readable": true, 104 | "hydra:writeable": true, 105 | "hydra:description": "The name of the item" 106 | }, 107 | { 108 | "@type": "hydra:SupportedProperty", 109 | "hydra:property": { 110 | "@id": "http://schema.org/description", 111 | "@type": "rdf:Property", 112 | "rdfs:label": "description", 113 | "domain": "http://schema.org/Book", 114 | "range": "xmls:string" 115 | }, 116 | "hydra:title": "description", 117 | "hydra:required": false, 118 | "hydra:readable": true, 119 | "hydra:writeable": true, 120 | "hydra:description": "A description of the item" 121 | }, 122 | { 123 | "@type": "hydra:SupportedProperty", 124 | "hydra:property": { 125 | "@id": "http://schema.org/author", 126 | "@type": "rdf:Property", 127 | "rdfs:label": "author", 128 | "domain": "http://schema.org/Book", 129 | "range": "xmls:string" 130 | }, 131 | "hydra:title": "author", 132 | "hydra:required": true, 133 | "hydra:readable": true, 134 | "hydra:writeable": true, 135 | "hydra:description": "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably" 136 | }, 137 | { 138 | "@type": "hydra:SupportedProperty", 139 | "hydra:property": { 140 | "@id": "http://schema.org/dateCreated", 141 | "@type": "rdf:Property", 142 | "rdfs:label": "dateCreated", 143 | "domain": "http://schema.org/Book", 144 | "range": "xmls:dateTime" 145 | }, 146 | "hydra:title": "dateCreated", 147 | "hydra:required": true, 148 | "hydra:readable": true, 149 | "hydra:writeable": true, 150 | "hydra:description": "The date on which the CreativeWork was created or the item was added to a DataFeed" 151 | }, 152 | { 153 | "@type": "hydra:SupportedProperty", 154 | "hydra:property": { 155 | "@id": "http://schema.org/reviews", 156 | "@type": "hydra:Link", 157 | "rdfs:label": "reviews", 158 | "domain": "http://schema.org/Book", 159 | "range": "http://schema.org/Review" 160 | }, 161 | "hydra:title": "reviews", 162 | "hydra:required": false, 163 | "hydra:readable": true, 164 | "hydra:writable": true, 165 | "hydra:description": "The book's reviews" 166 | }, 167 | { 168 | "@type": "hydra:SupportedProperty", 169 | "hydra:property": { 170 | "@id": "http://schema.org/reviews", 171 | "@type": "rdf:Property", 172 | "rdfs:label": "embeddedReviews", 173 | "domain": "http://schema.org/Book", 174 | "range": "http://schema.org/Review" 175 | }, 176 | "hydra:title": "embeddedReviews", 177 | "hydra:required": false, 178 | "hydra:readable": true, 179 | "hydra:writable": true, 180 | "hydra:description": "The book's reviews" 181 | } 182 | ], 183 | "hydra:supportedOperation": [ 184 | { 185 | "@type": "hydra:Operation", 186 | "hydra:method": "GET", 187 | "hydra:title": "Retrieves Book resource.", 188 | "rdfs:label": "Retrieves Book resource.", 189 | "returns": "http://schema.org/Book" 190 | }, 191 | { 192 | "@type": "hydra:ReplaceResourceOperation", 193 | "expects": "http://schema.org/Book", 194 | "hydra:method": "PUT", 195 | "hydra:title": "Replaces the Book resource.", 196 | "rdfs:label": "Replaces the Book resource.", 197 | "returns": "http://schema.org/Book" 198 | }, 199 | { 200 | "@type": "hydra:Operation", 201 | "hydra:method": "DELETE", 202 | "hydra:title": "Deletes the Book resource.", 203 | "rdfs:label": "Deletes the Book resource.", 204 | "returns": "owl:Nothing" 205 | }, 206 | { 207 | "@type": "hydra:Operation", 208 | "hydra:method": "GET" 209 | } 210 | ] 211 | }, 212 | { 213 | "@id": "http://schema.org/Review", 214 | "@type": "hydra:Class", 215 | "rdfs:label": "Review", 216 | "hydra:title": "Review", 217 | "hydra:supportedProperty": [ 218 | { 219 | "@type": "hydra:SupportedProperty", 220 | "hydra:property": { 221 | "@id": "http://schema.org/reviewBody", 222 | "@type": "rdf:Property", 223 | "rdfs:label": "reviewBody", 224 | "domain": "http://schema.org/Review", 225 | "range": "xmls:string" 226 | }, 227 | "hydra:title": "reviewBody", 228 | "hydra:required": false, 229 | "hydra:readable": true, 230 | "hydra:writeable": true, 231 | "hydra:description": "The actual body of the review" 232 | }, 233 | { 234 | "@type": "hydra:SupportedProperty", 235 | "hydra:property": { 236 | "@id": "#Review/rating", 237 | "@type": "rdf:Property", 238 | "rdfs:label": "rating", 239 | "domain": "http://schema.org/Review", 240 | "range": "xmls:integer" 241 | }, 242 | "hydra:title": "rating", 243 | "hydra:required": false, 244 | "hydra:readable": true, 245 | "hydra:writeable": true 246 | }, 247 | { 248 | "@type": "hydra:SupportedProperty", 249 | "hydra:property": { 250 | "@id": "http://schema.org/itemReviewed", 251 | "@type": "hydra:Link", 252 | "rdfs:label": "itemReviewed", 253 | "domain": "http://schema.org/Review", 254 | "owl:maxCardinality": 1, 255 | "range": "http://schema.org/Book" 256 | }, 257 | "hydra:title": "itemReviewed", 258 | "hydra:required": true, 259 | "hydra:readable": true, 260 | "hydra:writeable": true, 261 | "hydra:description": "The item that is being reviewed/rated" 262 | } 263 | ], 264 | "hydra:supportedOperation": [ 265 | { 266 | "@type": "hydra:Operation", 267 | "hydra:method": "GET", 268 | "hydra:title": "Retrieves Review resource.", 269 | "rdfs:label": "Retrieves Review resource.", 270 | "returns": "http://schema.org/Review" 271 | }, 272 | { 273 | "@type": "hydra:ReplaceResourceOperation", 274 | "expects": "http://schema.org/Review", 275 | "hydra:method": "PUT", 276 | "hydra:title": "Replaces the Review resource.", 277 | "rdfs:label": "Replaces the Review resource.", 278 | "returns": "http://schema.org/Review" 279 | }, 280 | { 281 | "@type": "hydra:Operation", 282 | "hydra:method": "DELETE", 283 | "hydra:title": "Deletes the Review resource.", 284 | "rdfs:label": "Deletes the Review resource.", 285 | "returns": "owl:Nothing" 286 | } 287 | ] 288 | }, 289 | { 290 | "@id": "#CustomResource", 291 | "@type": "hydra:Class", 292 | "rdfs:label": "CustomResource", 293 | "hydra:title": "CustomResource", 294 | "hydra:description": "A custom resource.", 295 | "hydra:supportedProperty": [ 296 | { 297 | "@type": "hydra:SupportedProperty", 298 | "hydra:property": { 299 | "@id": "#CustomResource/label", 300 | "@type": "rdf:Property", 301 | "rdfs:label": "label", 302 | "domain": "#CustomResource", 303 | "range": "xmls:string" 304 | }, 305 | "hydra:title": "label", 306 | "hydra:required": true, 307 | "hydra:readable": true, 308 | "hydra:writeable": true 309 | }, 310 | { 311 | "@type": "hydra:SupportedProperty", 312 | "hydra:property": { 313 | "@id": "#CustomResource/description", 314 | "@type": "rdf:Property", 315 | "rdfs:label": "description", 316 | "domain": "#CustomResource", 317 | "range": "xmls:string" 318 | }, 319 | "hydra:title": "description", 320 | "hydra:required": true, 321 | "hydra:readable": true, 322 | "hydra:writeable": true 323 | }, 324 | { 325 | "@type": "hydra:SupportedProperty", 326 | "hydra:property": { 327 | "@id": "#CustomResource/sanitizedDescription", 328 | "@type": "rdf:Property", 329 | "rdfs:label": "sanitizedDescription", 330 | "domain": "#CustomResource" 331 | }, 332 | "hydra:title": "sanitizedDescription", 333 | "hydra:required": false, 334 | "hydra:readable": true, 335 | "hydra:writeable": false 336 | } 337 | ], 338 | "hydra:supportedOperation": [ 339 | { 340 | "@type": "hydra:Operation", 341 | "hydra:method": "GET", 342 | "hydra:title": "Retrieves custom resources.", 343 | "rdfs:label": "Retrieves custom resources.", 344 | "returns": "#CustomResource" 345 | }, 346 | { 347 | "@type": "hydra:CreateResourceOperation", 348 | "expects": "#CustomResource", 349 | "hydra:method": "POST", 350 | "hydra:title": "Creates a custom resource.", 351 | "rdfs:label": "Creates a custom resource.", 352 | "returns": "#CustomResource" 353 | } 354 | ] 355 | }, 356 | { 357 | "@id": "#DeprecatedResource", 358 | "@type": "hydra:Class", 359 | "rdfs:label": "DeprecatedResource", 360 | "hydra:title": "DeprecatedResource", 361 | "hydra:supportedProperty": [ 362 | { 363 | "@type": "hydra:SupportedProperty", 364 | "hydra:property": { 365 | "@id": "#DeprecatedResource/deprecatedField", 366 | "@type": "rdf:Property", 367 | "rdfs:label": "deprecatedField", 368 | "domain": "#DeprecatedResource", 369 | "range": "xmls:string" 370 | }, 371 | "hydra:title": "deprecatedField", 372 | "hydra:required": true, 373 | "hydra:readable": true, 374 | "hydra:writeable": true, 375 | "hydra:description": "", 376 | "owl:deprecated": true 377 | } 378 | ], 379 | "hydra:supportedOperation": [ 380 | { 381 | "@type": [ 382 | "hydra:Operation", 383 | "schema:FindAction" 384 | ], 385 | "hydra:method": "GET", 386 | "hydra:title": "Retrieves DeprecatedResource resource.", 387 | "owl:deprecated": true, 388 | "rdfs:label": "Retrieves DeprecatedResource resource.", 389 | "returns": "#DeprecatedResource" 390 | } 391 | ], 392 | "hydra:description": "This is a dummy entity. Remove it!", 393 | "owl:deprecated": true 394 | }, 395 | { 396 | "@id": "#Entrypoint", 397 | "@type": "hydra:Class", 398 | "hydra:title": "The API entrypoint", 399 | "hydra:supportedProperty": [ 400 | { 401 | "@type": "hydra:SupportedProperty", 402 | "hydra:property": { 403 | "@id": "#Entrypoint/book", 404 | "@type": "hydra:Link", 405 | "domain": "#Entrypoint", 406 | "rdfs:label": "The collection of Book resources", 407 | "rdfs:range": [ 408 | {"@id": "hydra:PagedCollection"}, 409 | { 410 | "owl:equivalentClass": { 411 | "owl:onProperty": {"@id": "hydra:member"}, 412 | "owl:allValuesFrom": {"@id": "http://schema.org/Book"} 413 | } 414 | } 415 | ] 416 | }, 417 | "hydra:title": "The collection of Book resources", 418 | "hydra:readable": true, 419 | "hydra:writeable": false 420 | }, 421 | { 422 | "@type": "hydra:SupportedProperty", 423 | "hydra:property": { 424 | "@id": "#Entrypoint/review", 425 | "@type": "hydra:Link", 426 | "domain": "#Entrypoint", 427 | "rdfs:label": "The collection of Review resources", 428 | "range": "hydra:PagedCollection", 429 | "hydra:supportedOperation": [ 430 | { 431 | "@type": "hydra:Operation", 432 | "hydra:method": "GET", 433 | "hydra:title": "Retrieves the collection of Review resources.", 434 | "rdfs:label": "Retrieves the collection of Review resources.", 435 | "returns": "hydra:PagedCollection" 436 | }, 437 | { 438 | "@type": "hydra:CreateResourceOperation", 439 | "expects": "http://schema.org/Review", 440 | "hydra:method": "POST", 441 | "hydra:title": "Creates a Review resource.", 442 | "rdfs:label": "Creates a Review resource.", 443 | "returns": "http://schema.org/Review" 444 | } 445 | ] 446 | }, 447 | "hydra:title": "The collection of Review resources", 448 | "hydra:readable": true, 449 | "hydra:writeable": false 450 | }, 451 | { 452 | "@type": "hydra:SupportedProperty", 453 | "hydra:property": { 454 | "@id": "#Entrypoint/customResource", 455 | "@type": "hydra:Link", 456 | "domain": "#Entrypoint", 457 | "rdfs:label": "The collection of custom resources", 458 | "range": "hydra:PagedCollection", 459 | "hydra:supportedOperation": [ 460 | { 461 | "@type": "hydra:Operation", 462 | "hydra:method": "GET", 463 | "hydra:title": "Retrieves the collection of custom resources.", 464 | "rdfs:label": "Retrieves the collection of custom resources.", 465 | "returns": "hydra:PagedCollection" 466 | }, 467 | { 468 | "@type": "hydra:CreateResourceOperation", 469 | "expects": "#CustomResource", 470 | "hydra:method": "POST", 471 | "hydra:title": "Creates a custom resource.", 472 | "rdfs:label": "Creates a custom resource.", 473 | "returns": "#CustomResource" 474 | } 475 | ] 476 | }, 477 | "hydra:title": "The collection of custom resources", 478 | "hydra:readable": true, 479 | "hydra:writeable": false 480 | }, 481 | { 482 | "@type": "hydra:SupportedProperty", 483 | "hydra:property": { 484 | "@id": "#Entrypoint/deprecatedResource", 485 | "@type": "hydra:Link", 486 | "domain": "#Entrypoint", 487 | "rdfs:label": "The collection of DeprecatedResource resources", 488 | "rdfs:range": [ 489 | { 490 | "@id": "hydra:Collection" 491 | }, 492 | { 493 | "owl:equivalentClass": { 494 | "owl:onProperty": { 495 | "@id": "hydra:member" 496 | }, 497 | "owl:allValuesFrom": { 498 | "@id": "#DeprecatedResource" 499 | } 500 | } 501 | } 502 | ], 503 | "hydra:supportedOperation": [ 504 | { 505 | "@type": [ 506 | "hydra:Operation", 507 | "schema:FindAction" 508 | ], 509 | "hydra:method": "GET", 510 | "hydra:title": "Retrieves the collection of DeprecatedResource resources.", 511 | "owl:deprecated": true, 512 | "rdfs:label": "Retrieves the collection of DeprecatedResource resources.", 513 | "returns": "hydra:Collection" 514 | } 515 | ] 516 | }, 517 | "hydra:title": "The collection of DeprecatedResource resources", 518 | "hydra:readable": true, 519 | "hydra:writeable": false, 520 | "owl:deprecated": true 521 | } 522 | ], 523 | "hydra:supportedOperation": { 524 | "@type": "hydra:Operation", 525 | "hydra:method": "GET", 526 | "rdfs:label": "The API entrypoint.", 527 | "returns": "#EntryPoint" 528 | } 529 | }, 530 | { 531 | "@id": "#ConstraintViolation", 532 | "@type": "hydra:Class", 533 | "hydra:title": "A constraint violation", 534 | "hydra:supportedProperty": [ 535 | { 536 | "@type": "hydra:SupportedProperty", 537 | "hydra:property": { 538 | "@id": "#ConstraintViolation/propertyPath", 539 | "@type": "rdf:Property", 540 | "rdfs:label": "propertyPath", 541 | "domain": "#ConstraintViolation", 542 | "range": "xmls:string" 543 | }, 544 | "hydra:title": "propertyPath", 545 | "hydra:description": "The property path of the violation", 546 | "hydra:readable": true, 547 | "hydra:writeable": false 548 | }, 549 | { 550 | "@type": "hydra:SupportedProperty", 551 | "hydra:property": { 552 | "@id": "#ConstraintViolation/message", 553 | "@type": "rdf:Property", 554 | "rdfs:label": "message", 555 | "domain": "#ConstraintViolation", 556 | "range": "xmls:string" 557 | }, 558 | "hydra:title": "message", 559 | "hydra:description": "The message associated with the violation", 560 | "hydra:readable": true, 561 | "hydra:writeable": false 562 | } 563 | ] 564 | }, 565 | { 566 | "@id": "#ConstraintViolationList", 567 | "@type": "hydra:Class", 568 | "subClassOf": "hydra:Error", 569 | "hydra:title": "A constraint violation list", 570 | "hydra:supportedProperty": [ 571 | { 572 | "@type": "hydra:SupportedProperty", 573 | "hydra:property": { 574 | "@id": "#ConstraintViolationList/violation", 575 | "@type": "rdf:Property", 576 | "rdfs:label": "violation", 577 | "domain": "#ConstraintViolationList", 578 | "range": "#ConstraintViolation" 579 | }, 580 | "hydra:title": "violation", 581 | "hydra:description": "The violations", 582 | "hydra:readable": true, 583 | "hydra:writeable": false 584 | } 585 | ] 586 | } 587 | ] 588 | }`; 589 | 590 | const resourceCollectionWithParameters = `{ 591 | "hydra:search": { 592 | "hydra:mapping": [ 593 | { 594 | "property": "isbn", 595 | "variable": "isbn", 596 | "range": "http://www.w3.org/2001/XMLSchema#string", 597 | "required": false 598 | } 599 | ] 600 | } 601 | }`; 602 | 603 | const book = { 604 | name: "books", 605 | url: "http://localhost/books", 606 | id: "http://schema.org/Book", 607 | title: "Book", 608 | fields: [ 609 | { 610 | name: "isbn", 611 | id: "http://schema.org/isbn", 612 | range: "http://www.w3.org/2001/XMLSchema#string", 613 | type: "string", 614 | reference: null, 615 | embedded: null, 616 | required: true, 617 | description: "The ISBN of the book", 618 | maxCardinality: null, 619 | deprecated: false, 620 | }, 621 | { 622 | name: "name", 623 | id: "http://schema.org/name", 624 | range: "http://www.w3.org/2001/XMLSchema#string", 625 | type: "string", 626 | reference: null, 627 | embedded: null, 628 | required: true, 629 | description: "The name of the item", 630 | maxCardinality: null, 631 | deprecated: false, 632 | }, 633 | { 634 | name: "description", 635 | id: "http://schema.org/description", 636 | range: "http://www.w3.org/2001/XMLSchema#string", 637 | type: "string", 638 | reference: null, 639 | embedded: null, 640 | required: false, 641 | description: "A description of the item", 642 | maxCardinality: null, 643 | deprecated: false, 644 | }, 645 | { 646 | name: "author", 647 | id: "http://schema.org/author", 648 | range: "http://www.w3.org/2001/XMLSchema#string", 649 | type: "string", 650 | reference: null, 651 | embedded: null, 652 | required: true, 653 | description: 654 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 655 | maxCardinality: null, 656 | deprecated: false, 657 | }, 658 | { 659 | name: "dateCreated", 660 | id: "http://schema.org/dateCreated", 661 | range: "http://www.w3.org/2001/XMLSchema#dateTime", 662 | type: "dateTime", 663 | reference: null, 664 | embedded: null, 665 | required: true, 666 | description: 667 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 668 | maxCardinality: null, 669 | deprecated: false, 670 | }, 671 | { 672 | name: "reviews", 673 | id: "http://schema.org/reviews", 674 | range: "http://schema.org/Review", 675 | type: "string", 676 | reference: "Object http://schema.org/Review", 677 | embedded: null, 678 | required: false, 679 | description: "The book's reviews", 680 | maxCardinality: null, 681 | deprecated: false, 682 | }, 683 | { 684 | name: "embeddedReviews", 685 | id: "http://schema.org/reviews", 686 | range: "http://schema.org/Review", 687 | type: "string", 688 | reference: null, 689 | embedded: "Object http://schema.org/Review", 690 | required: false, 691 | description: "The book's reviews", 692 | maxCardinality: null, 693 | deprecated: false, 694 | }, 695 | ], 696 | readableFields: [ 697 | { 698 | name: "isbn", 699 | id: "http://schema.org/isbn", 700 | range: "http://www.w3.org/2001/XMLSchema#string", 701 | type: "string", 702 | reference: null, 703 | embedded: null, 704 | required: true, 705 | description: "The ISBN of the book", 706 | maxCardinality: null, 707 | deprecated: false, 708 | }, 709 | { 710 | name: "name", 711 | id: "http://schema.org/name", 712 | range: "http://www.w3.org/2001/XMLSchema#string", 713 | type: "string", 714 | reference: null, 715 | embedded: null, 716 | required: true, 717 | description: "The name of the item", 718 | maxCardinality: null, 719 | deprecated: false, 720 | }, 721 | { 722 | name: "description", 723 | id: "http://schema.org/description", 724 | range: "http://www.w3.org/2001/XMLSchema#string", 725 | type: "string", 726 | reference: null, 727 | embedded: null, 728 | required: false, 729 | description: "A description of the item", 730 | maxCardinality: null, 731 | deprecated: false, 732 | }, 733 | { 734 | name: "author", 735 | id: "http://schema.org/author", 736 | range: "http://www.w3.org/2001/XMLSchema#string", 737 | type: "string", 738 | reference: null, 739 | embedded: null, 740 | required: true, 741 | description: 742 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 743 | maxCardinality: null, 744 | deprecated: false, 745 | }, 746 | { 747 | name: "dateCreated", 748 | id: "http://schema.org/dateCreated", 749 | range: "http://www.w3.org/2001/XMLSchema#dateTime", 750 | type: "dateTime", 751 | reference: null, 752 | embedded: null, 753 | required: true, 754 | description: 755 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 756 | maxCardinality: null, 757 | deprecated: false, 758 | }, 759 | { 760 | name: "reviews", 761 | id: "http://schema.org/reviews", 762 | range: "http://schema.org/Review", 763 | type: "string", 764 | reference: "Object http://schema.org/Review", 765 | embedded: null, 766 | required: false, 767 | description: "The book's reviews", 768 | maxCardinality: null, 769 | deprecated: false, 770 | }, 771 | { 772 | name: "embeddedReviews", 773 | id: "http://schema.org/reviews", 774 | range: "http://schema.org/Review", 775 | type: "string", 776 | reference: null, 777 | embedded: "Object http://schema.org/Review", 778 | required: false, 779 | description: "The book's reviews", 780 | maxCardinality: null, 781 | deprecated: false, 782 | }, 783 | ], 784 | writableFields: [ 785 | { 786 | name: "isbn", 787 | id: "http://schema.org/isbn", 788 | range: "http://www.w3.org/2001/XMLSchema#string", 789 | type: "string", 790 | reference: null, 791 | embedded: null, 792 | required: true, 793 | description: "The ISBN of the book", 794 | maxCardinality: null, 795 | deprecated: false, 796 | }, 797 | { 798 | name: "name", 799 | id: "http://schema.org/name", 800 | range: "http://www.w3.org/2001/XMLSchema#string", 801 | type: "string", 802 | reference: null, 803 | embedded: null, 804 | required: true, 805 | description: "The name of the item", 806 | maxCardinality: null, 807 | deprecated: false, 808 | }, 809 | { 810 | name: "description", 811 | id: "http://schema.org/description", 812 | range: "http://www.w3.org/2001/XMLSchema#string", 813 | type: "string", 814 | reference: null, 815 | embedded: null, 816 | required: false, 817 | description: "A description of the item", 818 | maxCardinality: null, 819 | deprecated: false, 820 | }, 821 | { 822 | name: "author", 823 | id: "http://schema.org/author", 824 | range: "http://www.w3.org/2001/XMLSchema#string", 825 | type: "string", 826 | reference: null, 827 | embedded: null, 828 | required: true, 829 | description: 830 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 831 | maxCardinality: null, 832 | deprecated: false, 833 | }, 834 | { 835 | name: "dateCreated", 836 | id: "http://schema.org/dateCreated", 837 | range: "http://www.w3.org/2001/XMLSchema#dateTime", 838 | type: "dateTime", 839 | reference: null, 840 | embedded: null, 841 | required: true, 842 | description: 843 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 844 | maxCardinality: null, 845 | deprecated: false, 846 | }, 847 | { 848 | name: "reviews", 849 | id: "http://schema.org/reviews", 850 | range: "http://schema.org/Review", 851 | type: "string", 852 | reference: "Object http://schema.org/Review", 853 | embedded: null, 854 | required: false, 855 | description: "The book's reviews", 856 | maxCardinality: null, 857 | deprecated: false, 858 | }, 859 | { 860 | name: "embeddedReviews", 861 | id: "http://schema.org/reviews", 862 | range: "http://schema.org/Review", 863 | type: "string", 864 | reference: null, 865 | embedded: "Object http://schema.org/Review", 866 | required: false, 867 | description: "The book's reviews", 868 | maxCardinality: null, 869 | deprecated: false, 870 | }, 871 | ], 872 | operations: [ 873 | { 874 | name: "Retrieves Book resource.", 875 | type: "show", 876 | method: "GET", 877 | returns: "http://schema.org/Book", 878 | types: ["http://www.w3.org/ns/hydra/core#Operation"], 879 | deprecated: false, 880 | }, 881 | { 882 | name: "Replaces the Book resource.", 883 | type: "edit", 884 | method: "PUT", 885 | expects: "http://schema.org/Book", 886 | returns: "http://schema.org/Book", 887 | types: ["http://www.w3.org/ns/hydra/core#ReplaceResourceOperation"], 888 | deprecated: false, 889 | }, 890 | { 891 | name: "Deletes the Book resource.", 892 | type: "delete", 893 | method: "DELETE", 894 | returns: "http://www.w3.org/2002/07/owl#Nothing", 895 | types: ["http://www.w3.org/ns/hydra/core#Operation"], 896 | deprecated: false, 897 | }, 898 | ], 899 | deprecated: false, 900 | parameters: [], 901 | }; 902 | 903 | const review = { 904 | name: "reviews", 905 | url: "http://localhost/reviews", 906 | id: "http://schema.org/Review", 907 | title: "Review", 908 | fields: [ 909 | { 910 | name: "reviewBody", 911 | id: "http://schema.org/reviewBody", 912 | range: "http://www.w3.org/2001/XMLSchema#string", 913 | type: "string", 914 | reference: null, 915 | embedded: null, 916 | required: false, 917 | description: "The actual body of the review", 918 | maxCardinality: null, 919 | deprecated: false, 920 | }, 921 | { 922 | name: "rating", 923 | id: "http://localhost/docs.jsonld#Review/rating", 924 | range: "http://www.w3.org/2001/XMLSchema#integer", 925 | type: "integer", 926 | reference: null, 927 | embedded: null, 928 | required: false, 929 | description: "", 930 | maxCardinality: null, 931 | deprecated: false, 932 | }, 933 | { 934 | name: "itemReviewed", 935 | id: "http://schema.org/itemReviewed", 936 | range: "http://schema.org/Book", 937 | type: "string", 938 | reference: "Object http://schema.org/Book", 939 | embedded: null, 940 | required: true, 941 | description: "The item that is being reviewed/rated", 942 | maxCardinality: 1, 943 | deprecated: false, 944 | }, 945 | ], 946 | readableFields: [ 947 | { 948 | name: "reviewBody", 949 | id: "http://schema.org/reviewBody", 950 | range: "http://www.w3.org/2001/XMLSchema#string", 951 | type: "string", 952 | reference: null, 953 | embedded: null, 954 | required: false, 955 | description: "The actual body of the review", 956 | maxCardinality: null, 957 | deprecated: false, 958 | }, 959 | { 960 | name: "rating", 961 | id: "http://localhost/docs.jsonld#Review/rating", 962 | range: "http://www.w3.org/2001/XMLSchema#integer", 963 | type: "integer", 964 | reference: null, 965 | embedded: null, 966 | required: false, 967 | description: "", 968 | maxCardinality: null, 969 | deprecated: false, 970 | }, 971 | { 972 | name: "itemReviewed", 973 | id: "http://schema.org/itemReviewed", 974 | range: "http://schema.org/Book", 975 | type: "string", 976 | reference: "Object http://schema.org/Book", 977 | embedded: null, 978 | required: true, 979 | description: "The item that is being reviewed/rated", 980 | maxCardinality: 1, 981 | deprecated: false, 982 | }, 983 | ], 984 | writableFields: [ 985 | { 986 | name: "reviewBody", 987 | id: "http://schema.org/reviewBody", 988 | range: "http://www.w3.org/2001/XMLSchema#string", 989 | type: "string", 990 | reference: null, 991 | embedded: null, 992 | required: false, 993 | description: "The actual body of the review", 994 | maxCardinality: null, 995 | deprecated: false, 996 | }, 997 | { 998 | name: "rating", 999 | id: "http://localhost/docs.jsonld#Review/rating", 1000 | range: "http://www.w3.org/2001/XMLSchema#integer", 1001 | type: "integer", 1002 | reference: null, 1003 | embedded: null, 1004 | required: false, 1005 | description: "", 1006 | maxCardinality: null, 1007 | deprecated: false, 1008 | }, 1009 | { 1010 | name: "itemReviewed", 1011 | id: "http://schema.org/itemReviewed", 1012 | range: "http://schema.org/Book", 1013 | type: "string", 1014 | reference: "Object http://schema.org/Book", 1015 | embedded: null, 1016 | required: true, 1017 | description: "The item that is being reviewed/rated", 1018 | maxCardinality: 1, 1019 | deprecated: false, 1020 | }, 1021 | ], 1022 | operations: [ 1023 | { 1024 | name: "Retrieves the collection of Review resources.", 1025 | type: "list", 1026 | method: "GET", 1027 | returns: "http://www.w3.org/ns/hydra/core#PagedCollection", 1028 | types: ["http://www.w3.org/ns/hydra/core#Operation"], 1029 | deprecated: false, 1030 | }, 1031 | { 1032 | name: "Creates a Review resource.", 1033 | type: "create", 1034 | method: "POST", 1035 | expects: "http://schema.org/Review", 1036 | returns: "http://schema.org/Review", 1037 | types: ["http://www.w3.org/ns/hydra/core#CreateResourceOperation"], 1038 | deprecated: false, 1039 | }, 1040 | { 1041 | name: "Retrieves Review resource.", 1042 | type: "show", 1043 | method: "GET", 1044 | returns: "http://schema.org/Review", 1045 | types: ["http://www.w3.org/ns/hydra/core#Operation"], 1046 | deprecated: false, 1047 | }, 1048 | { 1049 | name: "Replaces the Review resource.", 1050 | type: "edit", 1051 | method: "PUT", 1052 | expects: "http://schema.org/Review", 1053 | returns: "http://schema.org/Review", 1054 | types: ["http://www.w3.org/ns/hydra/core#ReplaceResourceOperation"], 1055 | deprecated: false, 1056 | }, 1057 | { 1058 | name: "Deletes the Review resource.", 1059 | type: "delete", 1060 | method: "DELETE", 1061 | returns: "http://www.w3.org/2002/07/owl#Nothing", 1062 | types: ["http://www.w3.org/ns/hydra/core#Operation"], 1063 | deprecated: false, 1064 | }, 1065 | ], 1066 | deprecated: false, 1067 | parameters: [], 1068 | }; 1069 | 1070 | const customResource = { 1071 | name: "customResources", 1072 | url: "http://localhost/customResources", 1073 | id: "http://localhost/docs.jsonld#CustomResource", 1074 | title: "CustomResource", 1075 | fields: [ 1076 | { 1077 | name: "label", 1078 | id: "http://localhost/docs.jsonld#CustomResource/label", 1079 | range: "http://www.w3.org/2001/XMLSchema#string", 1080 | type: "string", 1081 | reference: null, 1082 | embedded: null, 1083 | required: true, 1084 | description: "", 1085 | maxCardinality: null, 1086 | deprecated: false, 1087 | }, 1088 | { 1089 | name: "description", 1090 | id: "http://localhost/docs.jsonld#CustomResource/description", 1091 | range: "http://www.w3.org/2001/XMLSchema#string", 1092 | type: "string", 1093 | reference: null, 1094 | embedded: null, 1095 | required: true, 1096 | description: "", 1097 | maxCardinality: null, 1098 | deprecated: false, 1099 | }, 1100 | { 1101 | name: "sanitizedDescription", 1102 | id: "http://localhost/docs.jsonld#CustomResource/sanitizedDescription", 1103 | range: null, 1104 | type: "string", 1105 | reference: null, 1106 | embedded: null, 1107 | required: false, 1108 | description: "", 1109 | maxCardinality: null, 1110 | deprecated: false, 1111 | }, 1112 | ], 1113 | readableFields: [ 1114 | { 1115 | name: "label", 1116 | id: "http://localhost/docs.jsonld#CustomResource/label", 1117 | range: "http://www.w3.org/2001/XMLSchema#string", 1118 | type: "string", 1119 | reference: null, 1120 | embedded: null, 1121 | required: true, 1122 | description: "", 1123 | maxCardinality: null, 1124 | deprecated: false, 1125 | }, 1126 | { 1127 | name: "description", 1128 | id: "http://localhost/docs.jsonld#CustomResource/description", 1129 | range: "http://www.w3.org/2001/XMLSchema#string", 1130 | type: "string", 1131 | reference: null, 1132 | embedded: null, 1133 | required: true, 1134 | description: "", 1135 | maxCardinality: null, 1136 | deprecated: false, 1137 | }, 1138 | { 1139 | name: "sanitizedDescription", 1140 | id: "http://localhost/docs.jsonld#CustomResource/sanitizedDescription", 1141 | range: null, 1142 | type: "string", 1143 | reference: null, 1144 | embedded: null, 1145 | required: false, 1146 | description: "", 1147 | maxCardinality: null, 1148 | deprecated: false, 1149 | }, 1150 | ], 1151 | writableFields: [ 1152 | { 1153 | name: "label", 1154 | id: "http://localhost/docs.jsonld#CustomResource/label", 1155 | range: "http://www.w3.org/2001/XMLSchema#string", 1156 | type: "string", 1157 | reference: null, 1158 | embedded: null, 1159 | required: true, 1160 | description: "", 1161 | maxCardinality: null, 1162 | deprecated: false, 1163 | }, 1164 | { 1165 | name: "description", 1166 | id: "http://localhost/docs.jsonld#CustomResource/description", 1167 | range: "http://www.w3.org/2001/XMLSchema#string", 1168 | type: "string", 1169 | reference: null, 1170 | embedded: null, 1171 | required: true, 1172 | description: "", 1173 | maxCardinality: null, 1174 | deprecated: false, 1175 | }, 1176 | ], 1177 | operations: [ 1178 | { 1179 | name: "Retrieves the collection of custom resources.", 1180 | type: "list", 1181 | method: "GET", 1182 | returns: "http://www.w3.org/ns/hydra/core#PagedCollection", 1183 | types: ["http://www.w3.org/ns/hydra/core#Operation"], 1184 | deprecated: false, 1185 | }, 1186 | { 1187 | name: "Creates a custom resource.", 1188 | type: "create", 1189 | method: "POST", 1190 | expects: "http://localhost/docs.jsonld#CustomResource", 1191 | returns: "http://localhost/docs.jsonld#CustomResource", 1192 | types: ["http://www.w3.org/ns/hydra/core#CreateResourceOperation"], 1193 | deprecated: false, 1194 | }, 1195 | { 1196 | name: "Retrieves custom resources.", 1197 | type: "show", 1198 | method: "GET", 1199 | returns: "http://localhost/docs.jsonld#CustomResource", 1200 | types: ["http://www.w3.org/ns/hydra/core#Operation"], 1201 | deprecated: false, 1202 | }, 1203 | { 1204 | name: "Creates a custom resource.", 1205 | type: "create", 1206 | method: "POST", 1207 | expects: "http://localhost/docs.jsonld#CustomResource", 1208 | returns: "http://localhost/docs.jsonld#CustomResource", 1209 | types: ["http://www.w3.org/ns/hydra/core#CreateResourceOperation"], 1210 | deprecated: false, 1211 | }, 1212 | ], 1213 | deprecated: false, 1214 | parameters: [], 1215 | }; 1216 | 1217 | const deprecatedResource = { 1218 | name: "deprecated_resources", 1219 | url: "http://localhost/deprecated_resources", 1220 | id: "http://localhost/docs.jsonld#DeprecatedResource", 1221 | title: "DeprecatedResource", 1222 | fields: [ 1223 | { 1224 | name: "deprecatedField", 1225 | id: "http://localhost/docs.jsonld#DeprecatedResource/deprecatedField", 1226 | range: "http://www.w3.org/2001/XMLSchema#string", 1227 | type: "string", 1228 | reference: null, 1229 | embedded: null, 1230 | required: true, 1231 | description: "", 1232 | maxCardinality: null, 1233 | deprecated: true, 1234 | }, 1235 | ], 1236 | readableFields: [ 1237 | { 1238 | name: "deprecatedField", 1239 | id: "http://localhost/docs.jsonld#DeprecatedResource/deprecatedField", 1240 | range: "http://www.w3.org/2001/XMLSchema#string", 1241 | type: "string", 1242 | reference: null, 1243 | embedded: null, 1244 | required: true, 1245 | description: "", 1246 | maxCardinality: null, 1247 | deprecated: true, 1248 | }, 1249 | ], 1250 | writableFields: [ 1251 | { 1252 | name: "deprecatedField", 1253 | id: "http://localhost/docs.jsonld#DeprecatedResource/deprecatedField", 1254 | range: "http://www.w3.org/2001/XMLSchema#string", 1255 | type: "string", 1256 | reference: null, 1257 | embedded: null, 1258 | required: true, 1259 | description: "", 1260 | maxCardinality: null, 1261 | deprecated: true, 1262 | }, 1263 | ], 1264 | operations: [ 1265 | { 1266 | name: "Retrieves the collection of DeprecatedResource resources.", 1267 | type: "list", 1268 | method: "GET", 1269 | returns: "http://www.w3.org/ns/hydra/core#Collection", 1270 | types: ["http://www.w3.org/ns/hydra/core#Operation", "schema:FindAction"], 1271 | deprecated: true, 1272 | }, 1273 | { 1274 | name: "Retrieves DeprecatedResource resource.", 1275 | type: "show", 1276 | method: "GET", 1277 | returns: "http://localhost/docs.jsonld#DeprecatedResource", 1278 | types: ["http://www.w3.org/ns/hydra/core#Operation", "schema:FindAction"], 1279 | deprecated: true, 1280 | }, 1281 | ], 1282 | deprecated: true, 1283 | parameters: [], 1284 | }; 1285 | 1286 | const resources = [book, review, customResource, deprecatedResource]; 1287 | 1288 | const expectedApi = { 1289 | entrypoint: "http://localhost", 1290 | title: "API Platform's demo", 1291 | resources: resources, 1292 | }; 1293 | 1294 | const init: MockParams = { 1295 | status: 200, 1296 | statusText: "OK", 1297 | headers: { 1298 | Link: '; rel="http://example.com",; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', 1299 | "Content-Type": "application/ld+json", 1300 | }, 1301 | }; 1302 | 1303 | test("parse a Hydra documentation", async () => { 1304 | fetchMock.mockResponses([entrypoint, init], [docs, init]); 1305 | 1306 | const options = { headers: new Headers({ CustomHeader: "customValue" }) }; 1307 | 1308 | await parseHydraDocumentation("http://localhost", options).then((data) => { 1309 | expect(JSON.stringify(data.api, parsedJsonReplacer, 2)).toBe( 1310 | JSON.stringify(expectedApi, null, 2), 1311 | ); 1312 | expect(data.response).toBeDefined(); 1313 | expect(data.status).toBe(200); 1314 | 1315 | expect(fetch).toHaveBeenCalledTimes(2); 1316 | expect(fetch).toHaveBeenNthCalledWith(2, "http://localhost/docs.jsonld", { 1317 | headers: new Headers({ 1318 | Accept: "application/ld+json", 1319 | "Content-Type": "application/ld+json", 1320 | CustomHeader: "customValue", 1321 | }), 1322 | }); 1323 | }); 1324 | }); 1325 | 1326 | test("parse a Hydra documentation using dynamic headers", async () => { 1327 | fetchMock.mockResponses([entrypoint, init], [docs, init]); 1328 | 1329 | const getHeaders = (): Headers => 1330 | new Headers({ CustomHeader: "customValue" }); 1331 | 1332 | await parseHydraDocumentation("http://localhost", { 1333 | headers: getHeaders, 1334 | }).then((data) => { 1335 | expect(JSON.stringify(data.api, parsedJsonReplacer, 2)).toBe( 1336 | JSON.stringify(expectedApi, null, 2), 1337 | ); 1338 | expect(data.response).toBeDefined(); 1339 | expect(data.status).toBe(200); 1340 | 1341 | expect(fetch).toHaveBeenCalledTimes(4); 1342 | expect(fetch).toHaveBeenNthCalledWith(2, "http://localhost/docs.jsonld", { 1343 | headers: new Headers({ 1344 | CustomHeader: "customValue", 1345 | Accept: "application/ld+json", 1346 | "Content-Type": "application/ld+json", 1347 | }), 1348 | }); 1349 | }); 1350 | }); 1351 | 1352 | test("parse a Hydra documentation (http://localhost/)", async () => { 1353 | fetchMock.mockResponses([entrypoint, init], [docs, init]); 1354 | 1355 | await parseHydraDocumentation("http://localhost/").then((data) => { 1356 | expect(JSON.stringify(data.api, parsedJsonReplacer, 2)).toBe( 1357 | JSON.stringify(expectedApi, null, 2), 1358 | ); 1359 | expect(data.response).toBeDefined(); 1360 | expect(data.status).toBe(200); 1361 | }); 1362 | }); 1363 | 1364 | test("parse a Hydra documentation without authorization", () => { 1365 | const init = { 1366 | status: 401, 1367 | statusText: "Unauthorized", 1368 | }; 1369 | 1370 | const expectedApi = { 1371 | entrypoint: "http://localhost", 1372 | resources: [], 1373 | }; 1374 | 1375 | const expectedResponse = { 1376 | code: 401, 1377 | message: "JWT Token not found", 1378 | }; 1379 | 1380 | fetchMock.mockResponses([JSON.stringify(expectedResponse), init]); 1381 | 1382 | return parseHydraDocumentation("http://localhost").catch( 1383 | async (data: { api: Api; response: Response; status: number }) => { 1384 | expect(data.api).toEqual(expectedApi); 1385 | expect(data.response).toBeDefined(); 1386 | await expect(data.response.json()).resolves.toEqual(expectedResponse); 1387 | expect(data.status).toBe(401); 1388 | }, 1389 | ); 1390 | }); 1391 | 1392 | test('Parse entrypoint without "@type" key', async () => { 1393 | const entrypoint = `{ 1394 | "@context": { 1395 | "@vocab": "http://localhost/docs.jsonld#", 1396 | "hydra": "http://www.w3.org/ns/hydra/core#", 1397 | "book": { 1398 | "@id": "Entrypoint/book", 1399 | "@type": "@id" 1400 | }, 1401 | "review": { 1402 | "@id": "Entrypoint/review", 1403 | "@type": "@id" 1404 | }, 1405 | "customResource": { 1406 | "@id": "Entrypoint/customResource", 1407 | "@type": "@id" 1408 | } 1409 | }, 1410 | "@id": "/", 1411 | "book": "/books", 1412 | "review": "/reviews", 1413 | "customResource": "/customResources" 1414 | }`; 1415 | 1416 | fetchMock.mockResponses([entrypoint, init], [docs, init]); 1417 | 1418 | const expectedError = { message: "" }; 1419 | 1420 | try { 1421 | await parseHydraDocumentation("http://localhost/"); 1422 | } catch (error) { 1423 | expectedError.message = (error as Error).message; 1424 | } 1425 | 1426 | expect(expectedError.message).toBe('The API entrypoint has no "@type" key.'); 1427 | }); 1428 | 1429 | test('Parse entrypoint class without "supportedClass" key', async () => { 1430 | const docs = `{ 1431 | "@context": { 1432 | "@vocab": "http://localhost/docs.jsonld#", 1433 | "hydra": "http://www.w3.org/ns/hydra/core#", 1434 | "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 1435 | "rdfs": "http://www.w3.org/2000/01/rdf-schema#", 1436 | "xmls": "http://www.w3.org/2001/XMLSchema#", 1437 | "owl": "http://www.w3.org/2002/07/owl#", 1438 | "domain": { 1439 | "@id": "rdfs:domain", 1440 | "@type": "@id" 1441 | }, 1442 | "range": { 1443 | "@id": "rdfs:range", 1444 | "@type": "@id" 1445 | }, 1446 | "subClassOf": { 1447 | "@id": "rdfs:subClassOf", 1448 | "@type": "@id" 1449 | }, 1450 | "expects": { 1451 | "@id": "hydra:expects", 1452 | "@type": "@id" 1453 | }, 1454 | "returns": { 1455 | "@id": "hydra:returns", 1456 | "@type": "@id" 1457 | } 1458 | }, 1459 | "@id": "/docs.jsonld", 1460 | "hydra:title": "API Platform's demo", 1461 | "hydra:description": "A test", 1462 | "hydra:entrypoint": "/" 1463 | }`; 1464 | 1465 | fetchMock.mockResponses([entrypoint, init], [docs, init]); 1466 | 1467 | const expectedError = { message: "" }; 1468 | 1469 | try { 1470 | await parseHydraDocumentation("http://localhost/"); 1471 | } catch (error) { 1472 | expectedError.message = (error as Error).message; 1473 | } 1474 | 1475 | expect(expectedError.message).toBe( 1476 | 'The API documentation has no "http://www.w3.org/ns/hydra/core#supportedClass" key or its value is not an array.', 1477 | ); 1478 | }); 1479 | 1480 | test('Parse entrypoint class without "supportedProperty" key', async () => { 1481 | const docs = `{ 1482 | "@context": { 1483 | "@vocab": "http://localhost/docs.jsonld#", 1484 | "hydra": "http://www.w3.org/ns/hydra/core#", 1485 | "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 1486 | "rdfs": "http://www.w3.org/2000/01/rdf-schema#", 1487 | "xmls": "http://www.w3.org/2001/XMLSchema#", 1488 | "owl": "http://www.w3.org/2002/07/owl#", 1489 | "domain": { 1490 | "@id": "rdfs:domain", 1491 | "@type": "@id" 1492 | }, 1493 | "range": { 1494 | "@id": "rdfs:range", 1495 | "@type": "@id" 1496 | }, 1497 | "subClassOf": { 1498 | "@id": "rdfs:subClassOf", 1499 | "@type": "@id" 1500 | }, 1501 | "expects": { 1502 | "@id": "hydra:expects", 1503 | "@type": "@id" 1504 | }, 1505 | "returns": { 1506 | "@id": "hydra:returns", 1507 | "@type": "@id" 1508 | } 1509 | }, 1510 | "@id": "/docs.jsonld", 1511 | "hydra:title": "API Platform's demo", 1512 | "hydra:description": "A test", 1513 | "hydra:entrypoint": "/", 1514 | "hydra:supportedClass": [ 1515 | { 1516 | "@id": "#Entrypoint", 1517 | "@type": "hydra:Class", 1518 | "hydra:title": "The API entrypoint", 1519 | "hydra:supportedOperation": { 1520 | "@type": "hydra:Operation", 1521 | "hydra:method": "GET", 1522 | "rdfs:label": "The API entrypoint.", 1523 | "returns": "#EntryPoint" 1524 | } 1525 | } 1526 | ] 1527 | }`; 1528 | 1529 | fetchMock.mockResponses([entrypoint, init], [docs, init]); 1530 | 1531 | const expectedError = { message: "" }; 1532 | 1533 | try { 1534 | await parseHydraDocumentation("http://localhost/"); 1535 | } catch (error) { 1536 | expectedError.message = (error as Error).message; 1537 | } 1538 | 1539 | expect(expectedError.message).toBe( 1540 | 'The entrypoint definition has no "http://www.w3.org/ns/hydra/core#supportedProperty" key or it is not an array.', 1541 | ); 1542 | }); 1543 | 1544 | test("Invalid docs JSON", async () => { 1545 | const docs = `{foo,}`; 1546 | 1547 | fetchMock.mockResponses([entrypoint, init], [docs, init]); 1548 | 1549 | let expectedError = {}; 1550 | 1551 | try { 1552 | await parseHydraDocumentation("http://localhost/"); 1553 | } catch (error) { 1554 | expectedError = error as Error; 1555 | } 1556 | 1557 | expect(expectedError).toHaveProperty("api"); 1558 | expect(expectedError).toHaveProperty("response"); 1559 | expect(expectedError).toHaveProperty("status"); 1560 | }); 1561 | 1562 | test("Invalid entrypoint JSON", async () => { 1563 | const entrypoint = `{foo,}`; 1564 | 1565 | fetchMock.mockResponses([entrypoint, init]); 1566 | 1567 | let expectedError = {}; 1568 | 1569 | try { 1570 | await parseHydraDocumentation("http://localhost/"); 1571 | } catch (error) { 1572 | expectedError = error as Error; 1573 | } 1574 | 1575 | expect(expectedError).toHaveProperty("api"); 1576 | expect(expectedError).toHaveProperty("response"); 1577 | expect(expectedError).toHaveProperty("status"); 1578 | }); 1579 | 1580 | test("Resource parameters can be retrieved", async () => { 1581 | fetchMock.mockResponses( 1582 | [entrypoint, init], 1583 | [docs, init], 1584 | [resourceCollectionWithParameters, init], 1585 | ); 1586 | 1587 | const data: { api: Api } = await parseHydraDocumentation("http://localhost"); 1588 | const resource = data.api.resources?.[0]; 1589 | if (!resource?.getParameters) { 1590 | return; 1591 | } 1592 | 1593 | const parameters = await resource.getParameters(); 1594 | expect(parameters).toEqual([ 1595 | { 1596 | description: "", 1597 | range: "http://www.w3.org/2001/XMLSchema#string", 1598 | required: false, 1599 | variable: "isbn", 1600 | }, 1601 | ]); 1602 | }); 1603 | -------------------------------------------------------------------------------- /src/hydra/parseHydraDocumentation.ts: -------------------------------------------------------------------------------- 1 | import jsonld from "jsonld"; 2 | import get from "lodash.get"; 3 | import { Api } from "../Api.js"; 4 | import { Field } from "../Field.js"; 5 | import { Resource } from "../Resource.js"; 6 | import { Operation } from "../Operation.js"; 7 | import fetchJsonLd from "./fetchJsonLd.js"; 8 | import getParameters from "./getParameters.js"; 9 | import getType from "./getType.js"; 10 | import type { OperationType } from "../Operation.js"; 11 | import type { Parameter } from "../Parameter.js"; 12 | import type { 13 | ExpandedClass, 14 | ExpandedDoc, 15 | Entrypoint, 16 | ExpandedOperation, 17 | ExpandedRdfProperty, 18 | RequestInitExtended, 19 | } from "./types.js"; 20 | 21 | /** 22 | * Extracts the short name of a resource. 23 | */ 24 | function guessNameFromUrl(url: string, entrypointUrl: string): string { 25 | return url.substr(entrypointUrl.length + 1); 26 | } 27 | 28 | function getTitleOrLabel(obj: ExpandedOperation): string { 29 | const a = 30 | obj["http://www.w3.org/2000/01/rdf-schema#label"] ?? 31 | obj["http://www.w3.org/ns/hydra/core#title"] ?? 32 | null; 33 | 34 | if (a === null) { 35 | throw new Error("No title nor label defined on this operation."); 36 | } 37 | 38 | return a[0]["@value"]; 39 | } 40 | 41 | /** 42 | * Finds the description of the class with the given id. 43 | */ 44 | function findSupportedClass( 45 | docs: ExpandedDoc[], 46 | classToFind: string, 47 | ): ExpandedClass { 48 | const supportedClasses = get( 49 | docs, 50 | '[0]["http://www.w3.org/ns/hydra/core#supportedClass"]', 51 | ) as ExpandedClass[] | undefined; 52 | if (!Array.isArray(supportedClasses)) { 53 | throw new Error( 54 | 'The API documentation has no "http://www.w3.org/ns/hydra/core#supportedClass" key or its value is not an array.', 55 | ); 56 | } 57 | 58 | for (const supportedClass of supportedClasses) { 59 | if (supportedClass["@id"] === classToFind) { 60 | return supportedClass; 61 | } 62 | } 63 | 64 | throw new Error( 65 | `The class "${classToFind}" is not defined in the API documentation.`, 66 | ); 67 | } 68 | 69 | export function getDocumentationUrlFromHeaders(headers: Headers): string { 70 | const linkHeader = headers.get("Link"); 71 | if (!linkHeader) { 72 | throw new Error('The response has no "Link" HTTP header.'); 73 | } 74 | 75 | const matches = 76 | /<([^<]+)>; rel="http:\/\/www.w3.org\/ns\/hydra\/core#apiDocumentation"/.exec( 77 | linkHeader, 78 | ); 79 | if (matches === null) { 80 | throw new Error( 81 | 'The "Link" HTTP header is not of the type "http://www.w3.org/ns/hydra/core#apiDocumentation".', 82 | ); 83 | } 84 | 85 | return matches[1]; 86 | } 87 | 88 | /** 89 | * Retrieves Hydra's entrypoint and API docs. 90 | */ 91 | async function fetchEntrypointAndDocs( 92 | entrypointUrl: string, 93 | options: RequestInitExtended = {}, 94 | ): Promise<{ 95 | entrypointUrl: string; 96 | docsUrl: string; 97 | response: Response; 98 | entrypoint: Entrypoint[]; 99 | docs: ExpandedDoc[]; 100 | }> { 101 | const d = await fetchJsonLd(entrypointUrl, options); 102 | if (!("body" in d)) { 103 | throw new Error("An empty response was received for the entrypoint URL."); 104 | } 105 | const entrypointJsonLd = d.body; 106 | const docsUrl = getDocumentationUrlFromHeaders(d.response.headers); 107 | 108 | const documentLoader = (input: string) => 109 | fetchJsonLd(input, options).then((response) => { 110 | if (!("body" in response)) { 111 | throw new Error( 112 | "An empty response was received when expanding documentation or entrypoint JSON-LD documents.", 113 | ); 114 | } 115 | return response; 116 | }); 117 | 118 | const docsResponse = await fetchJsonLd(docsUrl, options); 119 | if (!("body" in docsResponse)) { 120 | throw new Error( 121 | "An empty response was received for the documentation URL.", 122 | ); 123 | } 124 | const docsJsonLd = docsResponse.body; 125 | 126 | const [docs, entrypoint] = (await Promise.all([ 127 | jsonld.expand(docsJsonLd, { 128 | base: docsUrl, 129 | documentLoader, 130 | }), 131 | jsonld.expand(entrypointJsonLd, { 132 | base: entrypointUrl, 133 | documentLoader, 134 | }), 135 | ])) as unknown as [ExpandedDoc[], Entrypoint[]]; 136 | 137 | return { 138 | entrypointUrl, 139 | docsUrl, 140 | entrypoint, 141 | response: d.response, 142 | docs, 143 | }; 144 | } 145 | 146 | function removeTrailingSlash(url: string): string { 147 | if (url.endsWith("/")) { 148 | url = url.slice(0, -1); 149 | } 150 | 151 | return url; 152 | } 153 | 154 | function findRelatedClass( 155 | docs: ExpandedDoc[], 156 | property: ExpandedRdfProperty, 157 | ): ExpandedClass { 158 | // Use the entrypoint property's owl:equivalentClass if available 159 | if (Array.isArray(property["http://www.w3.org/2000/01/rdf-schema#range"])) { 160 | for (const range of property[ 161 | "http://www.w3.org/2000/01/rdf-schema#range" 162 | ]) { 163 | const onProperty = get( 164 | range, 165 | '["http://www.w3.org/2002/07/owl#equivalentClass"][0]["http://www.w3.org/2002/07/owl#onProperty"][0]["@id"]', 166 | ) as unknown as string; 167 | const allValuesFrom = get( 168 | range, 169 | '["http://www.w3.org/2002/07/owl#equivalentClass"][0]["http://www.w3.org/2002/07/owl#allValuesFrom"][0]["@id"]', 170 | ) as unknown as string; 171 | 172 | if ( 173 | allValuesFrom && 174 | "http://www.w3.org/ns/hydra/core#member" === onProperty 175 | ) { 176 | return findSupportedClass(docs, allValuesFrom); 177 | } 178 | } 179 | } 180 | 181 | // As a fallback, find an operation available on the property of the entrypoint returning the searched type (usually POST) 182 | for (const entrypointSupportedOperation of property[ 183 | "http://www.w3.org/ns/hydra/core#supportedOperation" 184 | ] || []) { 185 | if ( 186 | !entrypointSupportedOperation["http://www.w3.org/ns/hydra/core#returns"] 187 | ) { 188 | continue; 189 | } 190 | 191 | const returns = get( 192 | entrypointSupportedOperation, 193 | '["http://www.w3.org/ns/hydra/core#returns"][0]["@id"]', 194 | ) as string | undefined; 195 | if ( 196 | "string" === typeof returns && 197 | 0 !== returns.indexOf("http://www.w3.org/ns/hydra/core") 198 | ) { 199 | return findSupportedClass(docs, returns); 200 | } 201 | } 202 | 203 | throw new Error(`Cannot find the class related to ${property["@id"]}.`); 204 | } 205 | 206 | /** 207 | * Parses Hydra documentation and converts it to an intermediate representation. 208 | */ 209 | export default function parseHydraDocumentation( 210 | entrypointUrl: string, 211 | options: RequestInitExtended = {}, 212 | ): Promise<{ 213 | api: Api; 214 | response: Response; 215 | status: number; 216 | }> { 217 | entrypointUrl = removeTrailingSlash(entrypointUrl); 218 | 219 | return fetchEntrypointAndDocs(entrypointUrl, options).then( 220 | ({ entrypoint, docs, response }) => { 221 | const resources = [], 222 | fields = [], 223 | operations = []; 224 | const title = get( 225 | docs, 226 | '[0]["http://www.w3.org/ns/hydra/core#title"][0]["@value"]', 227 | "API Platform", 228 | ) as string; 229 | 230 | const entrypointType = get(entrypoint, '[0]["@type"][0]') as 231 | | string 232 | | undefined; 233 | if (!entrypointType) { 234 | throw new Error('The API entrypoint has no "@type" key.'); 235 | } 236 | 237 | const entrypointClass = findSupportedClass(docs, entrypointType); 238 | if ( 239 | !Array.isArray( 240 | entrypointClass["http://www.w3.org/ns/hydra/core#supportedProperty"], 241 | ) 242 | ) { 243 | throw new Error( 244 | 'The entrypoint definition has no "http://www.w3.org/ns/hydra/core#supportedProperty" key or it is not an array.', 245 | ); 246 | } 247 | 248 | // Add resources 249 | for (const properties of entrypointClass[ 250 | "http://www.w3.org/ns/hydra/core#supportedProperty" 251 | ]) { 252 | const readableFields = [], 253 | resourceFields = [], 254 | writableFields = [], 255 | resourceOperations = []; 256 | 257 | const property = get( 258 | properties, 259 | '["http://www.w3.org/ns/hydra/core#property"][0]', 260 | ) as ExpandedRdfProperty | undefined; 261 | 262 | if (!property) { 263 | continue; 264 | } 265 | 266 | const url = get(entrypoint, `[0]["${property["@id"]}"][0]["@id"]`) as 267 | | string 268 | | undefined; 269 | 270 | if (!url) { 271 | console.error( 272 | new Error( 273 | `Unable to find the URL for "${property["@id"]}" in the entrypoint, make sure your API resource has at least one GET collection operation declared.`, 274 | ), 275 | ); 276 | continue; 277 | } 278 | 279 | // Add fields 280 | const relatedClass = findRelatedClass(docs, property); 281 | for (const supportedProperties of relatedClass[ 282 | "http://www.w3.org/ns/hydra/core#supportedProperty" 283 | ]) { 284 | const supportedProperty = get( 285 | supportedProperties, 286 | '["http://www.w3.org/ns/hydra/core#property"][0]', 287 | ) as unknown as ExpandedRdfProperty; 288 | const id = supportedProperty["@id"]; 289 | const range = get( 290 | supportedProperty, 291 | '["http://www.w3.org/2000/01/rdf-schema#range"][0]["@id"]', 292 | null, 293 | ) as unknown as string; 294 | 295 | const field = new Field( 296 | supportedProperties["http://www.w3.org/ns/hydra/core#title"][0][ 297 | "@value" 298 | ] ?? 299 | supportedProperty[ 300 | "http://www.w3.org/2000/01/rdf-schema#label" 301 | ][0]["@value"], 302 | { 303 | id, 304 | range, 305 | type: getType(id, range), 306 | reference: 307 | "http://www.w3.org/ns/hydra/core#Link" === 308 | get(supportedProperty, '["@type"][0]') 309 | ? range // Will be updated in a subsequent pass 310 | : null, 311 | embedded: 312 | "http://www.w3.org/ns/hydra/core#Link" !== 313 | get(supportedProperty, '["@type"][0]') 314 | ? (range as unknown as Resource) // Will be updated in a subsequent pass 315 | : null, 316 | required: get( 317 | supportedProperties, 318 | '["http://www.w3.org/ns/hydra/core#required"][0]["@value"]', 319 | false, 320 | ) as boolean, 321 | description: get( 322 | supportedProperties, 323 | '["http://www.w3.org/ns/hydra/core#description"][0]["@value"]', 324 | "", 325 | ) as string, 326 | maxCardinality: get( 327 | supportedProperty, 328 | '["http://www.w3.org/2002/07/owl#maxCardinality"][0]["@value"]', 329 | null, 330 | ) as number | null, 331 | deprecated: get( 332 | supportedProperties, 333 | '["http://www.w3.org/2002/07/owl#deprecated"][0]["@value"]', 334 | false, 335 | ) as boolean, 336 | }, 337 | ); 338 | 339 | fields.push(field); 340 | resourceFields.push(field); 341 | 342 | if ( 343 | get( 344 | supportedProperties, 345 | '["http://www.w3.org/ns/hydra/core#readable"][0]["@value"]', 346 | ) 347 | ) { 348 | readableFields.push(field); 349 | } 350 | 351 | if ( 352 | get( 353 | supportedProperties, 354 | '["http://www.w3.org/ns/hydra/core#writeable"][0]["@value"]', 355 | ) || 356 | get( 357 | supportedProperties, 358 | '["http://www.w3.org/ns/hydra/core#writable"][0]["@value"]', 359 | ) 360 | ) { 361 | writableFields.push(field); 362 | } 363 | } 364 | 365 | // parse entrypoint's operations (a.k.a. collection operations) 366 | if (property["http://www.w3.org/ns/hydra/core#supportedOperation"]) { 367 | for (const entrypointOperation of property[ 368 | "http://www.w3.org/ns/hydra/core#supportedOperation" 369 | ]) { 370 | if ( 371 | !entrypointOperation["http://www.w3.org/ns/hydra/core#returns"] 372 | ) { 373 | continue; 374 | } 375 | 376 | const range = 377 | entrypointOperation["http://www.w3.org/ns/hydra/core#returns"][0][ 378 | "@id" 379 | ]; 380 | const method = 381 | entrypointOperation["http://www.w3.org/ns/hydra/core#method"][0][ 382 | "@value" 383 | ]; 384 | let type: OperationType = "list"; 385 | if (method === "POST") { 386 | type = "create"; 387 | } 388 | const operation = new Operation( 389 | getTitleOrLabel(entrypointOperation), 390 | type, 391 | { 392 | method, 393 | expects: 394 | entrypointOperation[ 395 | "http://www.w3.org/ns/hydra/core#expects" 396 | ] && 397 | entrypointOperation[ 398 | "http://www.w3.org/ns/hydra/core#expects" 399 | ][0]["@id"], 400 | returns: range, 401 | types: entrypointOperation["@type"], 402 | deprecated: get( 403 | entrypointOperation, 404 | '["http://www.w3.org/2002/07/owl#deprecated"][0]["@value"]', 405 | false, 406 | ) as boolean, 407 | }, 408 | ); 409 | 410 | resourceOperations.push(operation); 411 | operations.push(operation); 412 | } 413 | } 414 | 415 | // parse resource operations (a.k.a. item operations) 416 | for (const supportedOperation of relatedClass[ 417 | "http://www.w3.org/ns/hydra/core#supportedOperation" 418 | ] || []) { 419 | if (!supportedOperation["http://www.w3.org/ns/hydra/core#returns"]) { 420 | continue; 421 | } 422 | 423 | const range = 424 | supportedOperation["http://www.w3.org/ns/hydra/core#returns"][0][ 425 | "@id" 426 | ]; 427 | const method = 428 | supportedOperation["http://www.w3.org/ns/hydra/core#method"][0][ 429 | "@value" 430 | ]; 431 | let type: OperationType = "show"; 432 | if (method === "POST") { 433 | type = "create"; 434 | } 435 | if (method === "PUT" || method === "PATCH") { 436 | type = "edit"; 437 | } 438 | if (method === "DELETE") { 439 | type = "delete"; 440 | } 441 | const operation = new Operation( 442 | getTitleOrLabel(supportedOperation), 443 | type, 444 | { 445 | method, 446 | expects: 447 | supportedOperation["http://www.w3.org/ns/hydra/core#expects"] && 448 | supportedOperation[ 449 | "http://www.w3.org/ns/hydra/core#expects" 450 | ][0]["@id"], 451 | returns: range, 452 | types: supportedOperation["@type"], 453 | deprecated: get( 454 | supportedOperation, 455 | '["http://www.w3.org/2002/07/owl#deprecated"][0]["@value"]', 456 | false, 457 | ) as boolean, 458 | }, 459 | ); 460 | 461 | resourceOperations.push(operation); 462 | operations.push(operation); 463 | } 464 | 465 | const resource = new Resource( 466 | guessNameFromUrl(url, entrypointUrl), 467 | url, 468 | { 469 | id: relatedClass["@id"], 470 | title: get( 471 | relatedClass, 472 | '["http://www.w3.org/ns/hydra/core#title"][0]["@value"]', 473 | "", 474 | ) as string, 475 | fields: resourceFields, 476 | readableFields, 477 | writableFields, 478 | operations: resourceOperations, 479 | deprecated: get( 480 | relatedClass, 481 | '["http://www.w3.org/2002/07/owl#deprecated"][0]["@value"]', 482 | false, 483 | ) as boolean, 484 | }, 485 | ); 486 | 487 | resource.parameters = []; 488 | resource.getParameters = (): Promise => 489 | getParameters(resource, options); 490 | 491 | resources.push(resource); 492 | } 493 | 494 | // Resolve references and embedded 495 | for (const field of fields) { 496 | if (null !== field.reference) { 497 | field.reference = 498 | resources.find((resource) => resource.id === field.reference) || 499 | null; 500 | } 501 | if (null !== field.embedded) { 502 | field.embedded = 503 | resources.find((resource) => resource.id === field.embedded) || 504 | null; 505 | } 506 | } 507 | 508 | return { 509 | api: new Api(entrypointUrl, { title, resources }), 510 | response, 511 | status: response.status, 512 | }; 513 | }, 514 | (data: { response: Response }) => 515 | Promise.reject({ 516 | api: new Api(entrypointUrl, { resources: [] }), 517 | error: data, 518 | response: data.response, 519 | status: get(data.response, "status"), 520 | }), 521 | ); 522 | } 523 | -------------------------------------------------------------------------------- /src/hydra/types.ts: -------------------------------------------------------------------------------- 1 | export interface RequestInitExtended extends Omit { 2 | headers?: HeadersInit | (() => HeadersInit); 3 | } 4 | 5 | export type IriTemplateMapping = { 6 | "@type": "IriTemplateMapping"; 7 | variable: "string"; 8 | property: string | null; 9 | required: boolean; 10 | }; 11 | 12 | export type ExpandedOperation = { 13 | "@type": ["http://www.w3.org/ns/hydra/core#Operation"]; 14 | "http://www.w3.org/2000/01/rdf-schema#label": [ 15 | { 16 | "@value": string; 17 | }, 18 | ]; 19 | "http://www.w3.org/ns/hydra/core#title": [ 20 | { 21 | "@value": string; 22 | }, 23 | ]; 24 | "http://www.w3.org/ns/hydra/core#expects"?: [ 25 | { 26 | "@id": string; 27 | }, 28 | ]; 29 | "http://www.w3.org/ns/hydra/core#method": [ 30 | { 31 | "@value": string; 32 | }, 33 | ]; 34 | "http://www.w3.org/ns/hydra/core#returns"?: [ 35 | { 36 | "@id": string; 37 | }, 38 | ]; 39 | "http://www.w3.org/2002/07/owl#deprecated"?: [ 40 | { 41 | "@value": boolean; 42 | }, 43 | ]; 44 | }; 45 | 46 | export type ExpandedRdfProperty = { 47 | "@id": string; 48 | "@type": [ 49 | | "http://www.w3.org/1999/02/22-rdf-syntax-ns#Property" 50 | | "http://www.w3.org/ns/hydra/core#Link", 51 | ]; 52 | "http://www.w3.org/2000/01/rdf-schema#label": [ 53 | { 54 | "@value": string; 55 | }, 56 | ]; 57 | "http://www.w3.org/2000/01/rdf-schema#domain": [ 58 | { 59 | "@id": string; 60 | }, 61 | ]; 62 | "http://www.w3.org/2000/01/rdf-schema#range": 63 | | [ 64 | { 65 | "@id": string; 66 | }, 67 | ] 68 | | [ 69 | { 70 | "@id": string; 71 | }, 72 | { 73 | "http://www.w3.org/2002/07/owl#equivalentClass": [ 74 | { 75 | "http://www.w3.org/2002/07/owl#allValuesFrom": [ 76 | { 77 | "@id": string; 78 | }, 79 | ]; 80 | "http://www.w3.org/2002/07/owl#onProperty": [ 81 | { 82 | "@id": string; 83 | }, 84 | ]; 85 | }, 86 | ]; 87 | }, 88 | ]; 89 | "http://www.w3.org/ns/hydra/core#supportedOperation"?: ExpandedOperation[]; 90 | "http://www.w3.org/2002/07/owl#maxCardinality": [ 91 | { 92 | "@value": number; 93 | }, 94 | ]; 95 | }; 96 | 97 | export type ExpandedSupportedProperty = { 98 | "@type": ["http://www.w3.org/ns/hydra/core#SupportedProperty"]; 99 | "http://www.w3.org/ns/hydra/core#title": [ 100 | { 101 | "@value": string; 102 | }, 103 | ]; 104 | "http://www.w3.org/ns/hydra/core#description": [ 105 | { 106 | "@value": string; 107 | }, 108 | ]; 109 | "http://www.w3.org/ns/hydra/core#required"?: [ 110 | { 111 | "@value": boolean; 112 | }, 113 | ]; 114 | "http://www.w3.org/ns/hydra/core#readable": [ 115 | { 116 | "@value": boolean; 117 | }, 118 | ]; 119 | "http://www.w3.org/ns/hydra/core#writeable": [ 120 | { 121 | "@value": boolean; 122 | }, 123 | ]; 124 | "http://www.w3.org/ns/hydra/core#property": [ExpandedRdfProperty]; 125 | "http://www.w3.org/2002/07/owl#deprecated"?: [ 126 | { 127 | "@value": boolean; 128 | }, 129 | ]; 130 | }; 131 | 132 | export type ExpandedClass = { 133 | "@id": string; 134 | "@type": ["http://www.w3.org/ns/hydra/core#Class"]; 135 | "http://www.w3.org/2000/01/rdf-schema#label"?: [ 136 | { 137 | "@value": string; 138 | }, 139 | ]; 140 | "http://www.w3.org/2000/01/rdf-schema#subClassOf"?: [ 141 | { 142 | "@id": string; 143 | }, 144 | ]; 145 | "http://www.w3.org/ns/hydra/core#title": [ 146 | { 147 | "@value": string; 148 | }, 149 | ]; 150 | "http://www.w3.org/ns/hydra/core#description"?: [ 151 | { 152 | "@value": string; 153 | }, 154 | ]; 155 | "http://www.w3.org/ns/hydra/core#supportedProperty": ExpandedSupportedProperty[]; 156 | "http://www.w3.org/ns/hydra/core#supportedOperation"?: ExpandedOperation[]; 157 | "http://www.w3.org/2002/07/owl#deprecated"?: [ 158 | { 159 | "@value": boolean; 160 | }, 161 | ]; 162 | }; 163 | 164 | export type ExpandedDoc = { 165 | "@id": string; 166 | "@type": ["http://www.w3.org/ns/hydra/core#ApiDocumentation"]; 167 | "http://www.w3.org/ns/hydra/core#title": [ 168 | { 169 | "@value": string; 170 | }, 171 | ]; 172 | "http://www.w3.org/ns/hydra/core#description": [ 173 | { 174 | "@value": string; 175 | }, 176 | ]; 177 | "http://www.w3.org/ns/hydra/core#entrypoint": [ 178 | { 179 | "@value": string; 180 | }, 181 | ]; 182 | "http://www.w3.org/ns/hydra/core#supportedClass": ExpandedClass[]; 183 | }; 184 | 185 | export type Entrypoint = { 186 | "@id": string; 187 | "@type": [string]; 188 | [key: string]: 189 | | [ 190 | { 191 | "@id": string; 192 | }, 193 | ] 194 | | string 195 | | [string]; 196 | }; 197 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Api.js"; 2 | export * from "./Field.js"; 3 | export * from "./Operation.js"; 4 | export * from "./Parameter.js"; 5 | export * from "./Resource.js"; 6 | export * from "./graphql/index.js"; 7 | export * from "./hydra/index.js"; 8 | export * from "./openapi3/index.js"; 9 | export * from "./swagger/index.js"; 10 | -------------------------------------------------------------------------------- /src/openapi3/getType.ts: -------------------------------------------------------------------------------- 1 | import inflection from "inflection"; 2 | import type { FieldType } from "../Field.js"; 3 | 4 | const getType = (openApiType: string, format?: string): FieldType => { 5 | if (format) { 6 | switch (format) { 7 | case "int32": 8 | case "int64": 9 | return "integer"; 10 | default: 11 | return inflection.camelize(format.replace("-", "_"), true); 12 | } 13 | } 14 | 15 | return openApiType; 16 | }; 17 | 18 | export default getType; 19 | -------------------------------------------------------------------------------- /src/openapi3/handleJson.test.ts: -------------------------------------------------------------------------------- 1 | import handleJson from "./handleJson.js"; 2 | import parsedJsonReplacer from "../utils/parsedJsonReplacer.js"; 3 | import type { OpenAPIV3 } from "openapi-types"; 4 | 5 | const openApi3Definition: OpenAPIV3.Document = { 6 | openapi: "3.0.2", 7 | info: { 8 | title: "", 9 | version: "0.0.0", 10 | }, 11 | paths: { 12 | "/books": { 13 | get: { 14 | tags: ["Book"], 15 | operationId: "getBookCollection", 16 | summary: "Retrieves the collection of Book resources.", 17 | responses: { 18 | "200": { 19 | description: "Book collection response", 20 | content: { 21 | "application/ld+json": { 22 | schema: { 23 | type: "array", 24 | items: { 25 | $ref: "#/components/schemas/Book-book.read", 26 | }, 27 | }, 28 | }, 29 | "application/json": { 30 | schema: { 31 | type: "array", 32 | items: { 33 | $ref: "#/components/schemas/Book-book.read", 34 | }, 35 | }, 36 | }, 37 | "text/html": { 38 | schema: { 39 | type: "array", 40 | items: { 41 | $ref: "#/components/schemas/Book-book.read", 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | }, 48 | parameters: [ 49 | { 50 | name: "page", 51 | in: "query", 52 | required: false, 53 | description: "The collection page number", 54 | schema: { 55 | type: "integer", 56 | }, 57 | }, 58 | ], 59 | }, 60 | post: { 61 | tags: ["Book"], 62 | operationId: "postBookCollection", 63 | summary: "Creates a Book resource.", 64 | responses: { 65 | "201": { 66 | description: "Book resource created", 67 | content: { 68 | "application/ld+json": { 69 | schema: { 70 | $ref: "#/components/schemas/Book-book.read", 71 | }, 72 | }, 73 | "application/json": { 74 | schema: { 75 | $ref: "#/components/schemas/Book-book.read", 76 | }, 77 | }, 78 | "text/html": { 79 | schema: { 80 | $ref: "#/components/schemas/Book-book.read", 81 | }, 82 | }, 83 | }, 84 | links: { 85 | GetBookItem: { 86 | parameters: { 87 | id: "$response.body#/id", 88 | }, 89 | operationId: "getBookItem", 90 | description: 91 | "The `id` value returned in the response can be used as the `id` parameter in `GET /books/{id}`.", 92 | }, 93 | }, 94 | }, 95 | "400": { 96 | description: "Invalid input", 97 | }, 98 | "404": { 99 | description: "Resource not found", 100 | }, 101 | }, 102 | requestBody: { 103 | content: { 104 | "application/ld+json": { 105 | schema: { 106 | $ref: "#/components/schemas/Book", 107 | }, 108 | }, 109 | "application/json": { 110 | schema: { 111 | $ref: "#/components/schemas/Book", 112 | }, 113 | }, 114 | "text/html": { 115 | schema: { 116 | $ref: "#/components/schemas/Book", 117 | }, 118 | }, 119 | }, 120 | description: "The new Book resource", 121 | }, 122 | }, 123 | }, 124 | "/books/{id}": { 125 | get: { 126 | tags: ["Book"], 127 | operationId: "getBookItem", 128 | summary: "Retrieves a Book resource.", 129 | parameters: [ 130 | { 131 | name: "id", 132 | in: "path", 133 | required: true, 134 | schema: { 135 | type: "string", 136 | }, 137 | }, 138 | ], 139 | responses: { 140 | "200": { 141 | description: "Book resource response", 142 | content: { 143 | "application/ld+json": { 144 | schema: { 145 | $ref: "#/components/schemas/Book-book.read", 146 | }, 147 | }, 148 | "application/json": { 149 | schema: { 150 | $ref: "#/components/schemas/Book-book.read", 151 | }, 152 | }, 153 | "text/html": { 154 | schema: { 155 | $ref: "#/components/schemas/Book-book.read", 156 | }, 157 | }, 158 | }, 159 | }, 160 | "404": { 161 | description: "Resource not found", 162 | }, 163 | }, 164 | }, 165 | delete: { 166 | tags: ["Book"], 167 | operationId: "deleteBookItem", 168 | summary: "Removes the Book resource.", 169 | responses: { 170 | "204": { 171 | description: "Book resource deleted", 172 | }, 173 | "404": { 174 | description: "Resource not found", 175 | }, 176 | }, 177 | parameters: [ 178 | { 179 | name: "id", 180 | in: "path", 181 | required: true, 182 | schema: { 183 | type: "string", 184 | }, 185 | }, 186 | ], 187 | }, 188 | put: { 189 | tags: ["Book"], 190 | operationId: "putBookItem", 191 | summary: "Replaces the Book resource.", 192 | parameters: [ 193 | { 194 | name: "id", 195 | in: "path", 196 | required: true, 197 | schema: { 198 | type: "string", 199 | }, 200 | }, 201 | ], 202 | responses: { 203 | "200": { 204 | description: "Book resource updated", 205 | content: { 206 | "application/ld+json": { 207 | schema: { 208 | $ref: "#/components/schemas/Book-book.read", 209 | }, 210 | }, 211 | "application/json": { 212 | schema: { 213 | $ref: "#/components/schemas/Book-book.read", 214 | }, 215 | }, 216 | "text/html": { 217 | schema: { 218 | $ref: "#/components/schemas/Book-book.read", 219 | }, 220 | }, 221 | }, 222 | }, 223 | "400": { 224 | description: "Invalid input", 225 | }, 226 | "404": { 227 | description: "Resource not found", 228 | }, 229 | }, 230 | requestBody: { 231 | content: { 232 | "application/ld+json": { 233 | schema: { 234 | $ref: "#/components/schemas/Book", 235 | }, 236 | }, 237 | "application/json": { 238 | schema: { 239 | $ref: "#/components/schemas/Book", 240 | }, 241 | }, 242 | "text/html": { 243 | schema: { 244 | $ref: "#/components/schemas/Book", 245 | }, 246 | }, 247 | }, 248 | description: "The updated Book resource", 249 | }, 250 | }, 251 | }, 252 | "/reviews": { 253 | get: { 254 | tags: ["Review"], 255 | operationId: "getReviewCollection", 256 | summary: "Retrieves the collection of Review resources.", 257 | responses: { 258 | "200": { 259 | description: "Review collection response", 260 | content: { 261 | "application/ld+json": { 262 | schema: { 263 | type: "array", 264 | items: { 265 | $ref: "#/components/schemas/Review", 266 | }, 267 | }, 268 | }, 269 | "application/json": { 270 | schema: { 271 | type: "array", 272 | items: { 273 | $ref: "#/components/schemas/Review", 274 | }, 275 | }, 276 | }, 277 | "text/html": { 278 | schema: { 279 | type: "array", 280 | items: { 281 | $ref: "#/components/schemas/Review", 282 | }, 283 | }, 284 | }, 285 | }, 286 | }, 287 | }, 288 | parameters: [ 289 | { 290 | name: "page", 291 | in: "query", 292 | required: false, 293 | description: "The collection page number", 294 | schema: { 295 | type: "integer", 296 | }, 297 | }, 298 | ], 299 | }, 300 | post: { 301 | tags: ["Review"], 302 | operationId: "postReviewCollection", 303 | summary: "Creates a Review resource.", 304 | responses: { 305 | "201": { 306 | description: "Review resource created", 307 | content: { 308 | "application/ld+json": { 309 | schema: { 310 | $ref: "#/components/schemas/Review", 311 | }, 312 | }, 313 | "application/json": { 314 | schema: { 315 | $ref: "#/components/schemas/Review", 316 | }, 317 | }, 318 | "text/html": { 319 | schema: { 320 | $ref: "#/components/schemas/Review", 321 | }, 322 | }, 323 | }, 324 | links: { 325 | GetReviewItem: { 326 | parameters: { 327 | id: "$response.body#/id", 328 | }, 329 | operationId: "getReviewItem", 330 | description: 331 | "The `id` value returned in the response can be used as the `id` parameter in `GET /reviews/{id}`.", 332 | }, 333 | }, 334 | }, 335 | "400": { 336 | description: "Invalid input", 337 | }, 338 | "404": { 339 | description: "Resource not found", 340 | }, 341 | }, 342 | requestBody: { 343 | content: { 344 | "application/ld+json": { 345 | schema: { 346 | $ref: "#/components/schemas/Review", 347 | }, 348 | }, 349 | "application/json": { 350 | schema: { 351 | $ref: "#/components/schemas/Review", 352 | }, 353 | }, 354 | "text/html": { 355 | schema: { 356 | $ref: "#/components/schemas/Review", 357 | }, 358 | }, 359 | }, 360 | description: "The new Review resource", 361 | }, 362 | }, 363 | }, 364 | "/reviews/{id}": { 365 | get: { 366 | tags: ["Review"], 367 | operationId: "getReviewItem", 368 | summary: "Retrieves a Review resource.", 369 | parameters: [ 370 | { 371 | name: "id", 372 | in: "path", 373 | required: true, 374 | schema: { 375 | type: "string", 376 | }, 377 | }, 378 | ], 379 | responses: { 380 | "200": { 381 | description: "Review resource response", 382 | content: { 383 | "application/ld+json": { 384 | schema: { 385 | $ref: "#/components/schemas/Review", 386 | }, 387 | }, 388 | "application/json": { 389 | schema: { 390 | $ref: "#/components/schemas/Review", 391 | }, 392 | }, 393 | "text/html": { 394 | schema: { 395 | $ref: "#/components/schemas/Review", 396 | }, 397 | }, 398 | }, 399 | }, 400 | "404": { 401 | description: "Resource not found", 402 | }, 403 | }, 404 | }, 405 | delete: { 406 | tags: ["Review"], 407 | operationId: "deleteReviewItem", 408 | summary: "Removes the Review resource.", 409 | responses: { 410 | "204": { 411 | description: "Review resource deleted", 412 | }, 413 | "404": { 414 | description: "Resource not found", 415 | }, 416 | }, 417 | parameters: [ 418 | { 419 | name: "id", 420 | in: "path", 421 | required: true, 422 | schema: { 423 | type: "string", 424 | }, 425 | }, 426 | ], 427 | }, 428 | put: { 429 | tags: ["Review"], 430 | operationId: "putReviewItem", 431 | summary: "Replaces the Review resource.", 432 | parameters: [ 433 | { 434 | name: "id", 435 | in: "path", 436 | required: true, 437 | schema: { 438 | type: "string", 439 | }, 440 | }, 441 | ], 442 | responses: { 443 | "200": { 444 | description: "Review resource updated", 445 | content: { 446 | "application/ld+json": { 447 | schema: { 448 | $ref: "#/components/schemas/Review", 449 | }, 450 | }, 451 | "application/json": { 452 | schema: { 453 | $ref: "#/components/schemas/Review", 454 | }, 455 | }, 456 | "text/html": { 457 | schema: { 458 | $ref: "#/components/schemas/Review", 459 | }, 460 | }, 461 | }, 462 | }, 463 | "400": { 464 | description: "Invalid input", 465 | }, 466 | "404": { 467 | description: "Resource not found", 468 | }, 469 | }, 470 | requestBody: { 471 | content: { 472 | "application/ld+json": { 473 | schema: { 474 | $ref: "#/components/schemas/Review", 475 | }, 476 | }, 477 | "application/json": { 478 | schema: { 479 | $ref: "#/components/schemas/Review", 480 | }, 481 | }, 482 | "text/html": { 483 | schema: { 484 | $ref: "#/components/schemas/Review", 485 | }, 486 | }, 487 | }, 488 | description: "The updated Review resource", 489 | }, 490 | }, 491 | }, 492 | }, 493 | components: { 494 | schemas: { 495 | Book: { 496 | type: "object", 497 | description: "", 498 | properties: { 499 | isbn: { 500 | type: "string", 501 | description: "The ISBN of the book", 502 | }, 503 | description: { 504 | type: "string", 505 | description: "A description of the item", 506 | }, 507 | author: { 508 | type: "string", 509 | description: 510 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 511 | }, 512 | title: { 513 | type: "string", 514 | description: "The title of the book", 515 | }, 516 | bookFormat: { 517 | type: "string", 518 | description: "The publication format of the book.", 519 | enum: ["AUDIOBOOK_FORMAT", "E_BOOK", "PAPERBACK", "HARDCOVER"], 520 | }, 521 | publicationDate: { 522 | type: "string", 523 | format: "date-time", 524 | description: 525 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 526 | }, 527 | reviews: { 528 | type: "array", 529 | items: { 530 | type: "string", 531 | }, 532 | }, 533 | archivedAt: { 534 | writeOnly: true, 535 | type: "string", 536 | format: "date-time", 537 | nullable: true, 538 | }, 539 | }, 540 | required: [ 541 | "description", 542 | "author", 543 | "title", 544 | "bookFormat", 545 | "publicationDate", 546 | ], 547 | }, 548 | "Book-book.read": { 549 | type: "object", 550 | description: "", 551 | properties: { 552 | id: { 553 | readOnly: true, 554 | type: "integer", 555 | }, 556 | isbn: { 557 | type: "string", 558 | description: "The ISBN of the book", 559 | }, 560 | description: { 561 | type: "string", 562 | description: "A description of the item", 563 | }, 564 | author: { 565 | type: "string", 566 | description: 567 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 568 | }, 569 | title: { 570 | type: "string", 571 | description: "The title of the book", 572 | }, 573 | bookFormat: { 574 | type: "string", 575 | description: "The publication format of the book.", 576 | enum: ["AUDIOBOOK_FORMAT", "E_BOOK", "PAPERBACK", "HARDCOVER"], 577 | }, 578 | publicationDate: { 579 | type: "string", 580 | format: "date-time", 581 | description: 582 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 583 | }, 584 | reviews: { 585 | type: "array", 586 | items: { 587 | type: "string", 588 | }, 589 | }, 590 | }, 591 | required: [ 592 | "description", 593 | "author", 594 | "title", 595 | "bookFormat", 596 | "publicationDate", 597 | ], 598 | }, 599 | Review: { 600 | type: "object", 601 | description: "", 602 | properties: { 603 | id: { 604 | readOnly: true, 605 | type: "integer", 606 | }, 607 | rating: { 608 | type: "integer", 609 | }, 610 | body: { 611 | type: "string", 612 | }, 613 | book: { 614 | type: "string", 615 | }, 616 | author: { 617 | type: "string", 618 | }, 619 | publicationDate: { 620 | type: "string", 621 | format: "date-time", 622 | }, 623 | }, 624 | }, 625 | }, 626 | }, 627 | }; 628 | const parsed = [ 629 | { 630 | name: "books", 631 | url: "https://demo.api-platform.com/books", 632 | id: null, 633 | title: "Book", 634 | description: "", 635 | fields: [ 636 | { 637 | name: "id", 638 | id: null, 639 | range: null, 640 | type: "integer", 641 | arrayType: null, 642 | enum: null, 643 | reference: null, 644 | embedded: null, 645 | nullable: false, 646 | required: false, 647 | description: "", 648 | }, 649 | { 650 | name: "isbn", 651 | id: null, 652 | range: null, 653 | type: "string", 654 | arrayType: null, 655 | enum: null, 656 | reference: null, 657 | embedded: null, 658 | nullable: false, 659 | required: false, 660 | description: "The ISBN of the book", 661 | }, 662 | { 663 | name: "description", 664 | id: null, 665 | range: null, 666 | type: "string", 667 | arrayType: null, 668 | enum: null, 669 | reference: null, 670 | embedded: null, 671 | nullable: false, 672 | required: true, 673 | description: "A description of the item", 674 | }, 675 | { 676 | name: "author", 677 | id: null, 678 | range: null, 679 | type: "string", 680 | arrayType: null, 681 | enum: null, 682 | reference: null, 683 | embedded: null, 684 | nullable: false, 685 | required: true, 686 | description: 687 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 688 | }, 689 | { 690 | name: "title", 691 | id: null, 692 | range: null, 693 | type: "string", 694 | arrayType: null, 695 | enum: null, 696 | reference: null, 697 | embedded: null, 698 | nullable: false, 699 | required: true, 700 | description: "The title of the book", 701 | }, 702 | { 703 | name: "bookFormat", 704 | id: null, 705 | range: null, 706 | type: "string", 707 | arrayType: null, 708 | enum: { 709 | "Audiobook format": "AUDIOBOOK_FORMAT", 710 | "E book": "E_BOOK", 711 | Paperback: "PAPERBACK", 712 | Hardcover: "HARDCOVER", 713 | }, 714 | reference: null, 715 | embedded: null, 716 | nullable: false, 717 | required: true, 718 | description: "The publication format of the book.", 719 | }, 720 | { 721 | name: "publicationDate", 722 | id: null, 723 | range: null, 724 | type: "dateTime", 725 | arrayType: null, 726 | enum: null, 727 | reference: null, 728 | embedded: null, 729 | nullable: false, 730 | required: true, 731 | description: 732 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 733 | }, 734 | { 735 | name: "reviews", 736 | id: null, 737 | range: null, 738 | type: "array", 739 | arrayType: "string", 740 | enum: null, 741 | reference: { 742 | title: "Review", 743 | }, 744 | embedded: null, 745 | nullable: false, 746 | required: false, 747 | description: "", 748 | maxCardinality: null, 749 | }, 750 | { 751 | name: "archivedAt", 752 | id: null, 753 | range: null, 754 | type: "dateTime", 755 | arrayType: null, 756 | enum: null, 757 | reference: null, 758 | embedded: null, 759 | nullable: true, 760 | required: false, 761 | description: "", 762 | }, 763 | ], 764 | readableFields: [ 765 | { 766 | name: "id", 767 | id: null, 768 | range: null, 769 | type: "integer", 770 | arrayType: null, 771 | enum: null, 772 | reference: null, 773 | embedded: null, 774 | nullable: false, 775 | required: false, 776 | description: "", 777 | }, 778 | { 779 | name: "isbn", 780 | id: null, 781 | range: null, 782 | type: "string", 783 | arrayType: null, 784 | enum: null, 785 | reference: null, 786 | embedded: null, 787 | nullable: false, 788 | required: false, 789 | description: "The ISBN of the book", 790 | }, 791 | { 792 | name: "description", 793 | id: null, 794 | range: null, 795 | type: "string", 796 | arrayType: null, 797 | enum: null, 798 | reference: null, 799 | embedded: null, 800 | nullable: false, 801 | required: true, 802 | description: "A description of the item", 803 | }, 804 | { 805 | name: "author", 806 | id: null, 807 | range: null, 808 | type: "string", 809 | arrayType: null, 810 | enum: null, 811 | reference: null, 812 | embedded: null, 813 | nullable: false, 814 | required: true, 815 | description: 816 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 817 | }, 818 | { 819 | name: "title", 820 | id: null, 821 | range: null, 822 | type: "string", 823 | arrayType: null, 824 | enum: null, 825 | reference: null, 826 | embedded: null, 827 | nullable: false, 828 | required: true, 829 | description: "The title of the book", 830 | }, 831 | { 832 | name: "bookFormat", 833 | id: null, 834 | range: null, 835 | type: "string", 836 | arrayType: null, 837 | enum: { 838 | "Audiobook format": "AUDIOBOOK_FORMAT", 839 | "E book": "E_BOOK", 840 | Paperback: "PAPERBACK", 841 | Hardcover: "HARDCOVER", 842 | }, 843 | reference: null, 844 | embedded: null, 845 | nullable: false, 846 | required: true, 847 | description: "The publication format of the book.", 848 | }, 849 | { 850 | name: "publicationDate", 851 | id: null, 852 | range: null, 853 | type: "dateTime", 854 | arrayType: null, 855 | enum: null, 856 | reference: null, 857 | embedded: null, 858 | nullable: false, 859 | required: true, 860 | description: 861 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 862 | }, 863 | { 864 | name: "reviews", 865 | id: null, 866 | range: null, 867 | type: "array", 868 | arrayType: "string", 869 | enum: null, 870 | reference: { 871 | title: "Review", 872 | }, 873 | embedded: null, 874 | nullable: false, 875 | required: false, 876 | description: "", 877 | maxCardinality: null, 878 | }, 879 | ], 880 | writableFields: [ 881 | { 882 | name: "isbn", 883 | id: null, 884 | range: null, 885 | type: "string", 886 | arrayType: null, 887 | enum: null, 888 | reference: null, 889 | embedded: null, 890 | nullable: false, 891 | required: false, 892 | description: "The ISBN of the book", 893 | }, 894 | { 895 | name: "description", 896 | id: null, 897 | range: null, 898 | type: "string", 899 | arrayType: null, 900 | enum: null, 901 | reference: null, 902 | embedded: null, 903 | nullable: false, 904 | required: true, 905 | description: "A description of the item", 906 | }, 907 | { 908 | name: "author", 909 | id: null, 910 | range: null, 911 | type: "string", 912 | arrayType: null, 913 | enum: null, 914 | reference: null, 915 | embedded: null, 916 | nullable: false, 917 | required: true, 918 | description: 919 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 920 | }, 921 | { 922 | name: "title", 923 | id: null, 924 | range: null, 925 | type: "string", 926 | arrayType: null, 927 | enum: null, 928 | reference: null, 929 | embedded: null, 930 | nullable: false, 931 | required: true, 932 | description: "The title of the book", 933 | }, 934 | { 935 | name: "bookFormat", 936 | id: null, 937 | range: null, 938 | type: "string", 939 | arrayType: null, 940 | enum: { 941 | "Audiobook format": "AUDIOBOOK_FORMAT", 942 | "E book": "E_BOOK", 943 | Paperback: "PAPERBACK", 944 | Hardcover: "HARDCOVER", 945 | }, 946 | reference: null, 947 | embedded: null, 948 | nullable: false, 949 | required: true, 950 | description: "The publication format of the book.", 951 | }, 952 | { 953 | name: "publicationDate", 954 | id: null, 955 | range: null, 956 | type: "dateTime", 957 | arrayType: null, 958 | enum: null, 959 | reference: null, 960 | embedded: null, 961 | nullable: false, 962 | required: true, 963 | description: 964 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 965 | }, 966 | { 967 | name: "reviews", 968 | id: null, 969 | range: null, 970 | type: "array", 971 | arrayType: "string", 972 | enum: null, 973 | reference: { 974 | title: "Review", 975 | }, 976 | embedded: null, 977 | nullable: false, 978 | required: false, 979 | description: "", 980 | maxCardinality: null, 981 | }, 982 | { 983 | name: "archivedAt", 984 | id: null, 985 | range: null, 986 | type: "dateTime", 987 | arrayType: null, 988 | enum: null, 989 | reference: null, 990 | embedded: null, 991 | nullable: true, 992 | required: false, 993 | description: "", 994 | }, 995 | ], 996 | parameters: [ 997 | { 998 | variable: "page", 999 | range: "integer", 1000 | required: false, 1001 | description: "The collection page number", 1002 | }, 1003 | ], 1004 | operations: [ 1005 | { 1006 | name: "Retrieves a Book resource.", 1007 | type: "show", 1008 | method: "GET", 1009 | deprecated: false, 1010 | }, 1011 | { 1012 | name: "Replaces the Book resource.", 1013 | type: "edit", 1014 | method: "PUT", 1015 | deprecated: false, 1016 | }, 1017 | { 1018 | name: "Removes the Book resource.", 1019 | type: "delete", 1020 | method: "DELETE", 1021 | deprecated: false, 1022 | }, 1023 | { 1024 | name: "Retrieves the collection of Book resources.", 1025 | type: "list", 1026 | method: "GET", 1027 | deprecated: false, 1028 | }, 1029 | { 1030 | name: "Creates a Book resource.", 1031 | type: "create", 1032 | method: "POST", 1033 | deprecated: false, 1034 | }, 1035 | ], 1036 | }, 1037 | { 1038 | name: "reviews", 1039 | url: "https://demo.api-platform.com/reviews", 1040 | id: null, 1041 | title: "Review", 1042 | description: "", 1043 | fields: [ 1044 | { 1045 | name: "id", 1046 | id: null, 1047 | range: null, 1048 | type: "integer", 1049 | arrayType: null, 1050 | enum: null, 1051 | reference: null, 1052 | embedded: null, 1053 | nullable: false, 1054 | required: false, 1055 | description: "", 1056 | }, 1057 | { 1058 | name: "rating", 1059 | id: null, 1060 | range: null, 1061 | type: "integer", 1062 | arrayType: null, 1063 | enum: null, 1064 | reference: null, 1065 | embedded: null, 1066 | nullable: false, 1067 | required: false, 1068 | description: "", 1069 | }, 1070 | { 1071 | name: "body", 1072 | id: null, 1073 | range: null, 1074 | type: "string", 1075 | arrayType: null, 1076 | enum: null, 1077 | reference: null, 1078 | embedded: null, 1079 | nullable: false, 1080 | required: false, 1081 | description: "", 1082 | }, 1083 | { 1084 | name: "book", 1085 | id: null, 1086 | range: null, 1087 | type: "string", 1088 | arrayType: null, 1089 | enum: null, 1090 | reference: { 1091 | title: "Book", 1092 | }, 1093 | embedded: null, 1094 | nullable: false, 1095 | required: false, 1096 | description: "", 1097 | maxCardinality: 1, 1098 | }, 1099 | { 1100 | name: "author", 1101 | id: null, 1102 | range: null, 1103 | type: "string", 1104 | arrayType: null, 1105 | enum: null, 1106 | reference: null, 1107 | embedded: null, 1108 | nullable: false, 1109 | required: false, 1110 | description: "", 1111 | }, 1112 | { 1113 | name: "publicationDate", 1114 | id: null, 1115 | range: null, 1116 | type: "dateTime", 1117 | arrayType: null, 1118 | enum: null, 1119 | reference: null, 1120 | embedded: null, 1121 | nullable: false, 1122 | required: false, 1123 | description: "", 1124 | }, 1125 | ], 1126 | readableFields: [ 1127 | { 1128 | name: "id", 1129 | id: null, 1130 | range: null, 1131 | type: "integer", 1132 | arrayType: null, 1133 | enum: null, 1134 | reference: null, 1135 | embedded: null, 1136 | nullable: false, 1137 | required: false, 1138 | description: "", 1139 | }, 1140 | { 1141 | name: "rating", 1142 | id: null, 1143 | range: null, 1144 | type: "integer", 1145 | arrayType: null, 1146 | enum: null, 1147 | reference: null, 1148 | embedded: null, 1149 | nullable: false, 1150 | required: false, 1151 | description: "", 1152 | }, 1153 | { 1154 | name: "body", 1155 | id: null, 1156 | range: null, 1157 | type: "string", 1158 | arrayType: null, 1159 | enum: null, 1160 | reference: null, 1161 | embedded: null, 1162 | nullable: false, 1163 | required: false, 1164 | description: "", 1165 | }, 1166 | { 1167 | name: "book", 1168 | id: null, 1169 | range: null, 1170 | type: "string", 1171 | arrayType: null, 1172 | enum: null, 1173 | reference: { 1174 | title: "Book", 1175 | }, 1176 | embedded: null, 1177 | nullable: false, 1178 | required: false, 1179 | description: "", 1180 | maxCardinality: 1, 1181 | }, 1182 | { 1183 | name: "author", 1184 | id: null, 1185 | range: null, 1186 | type: "string", 1187 | arrayType: null, 1188 | enum: null, 1189 | reference: null, 1190 | embedded: null, 1191 | nullable: false, 1192 | required: false, 1193 | description: "", 1194 | }, 1195 | { 1196 | name: "publicationDate", 1197 | id: null, 1198 | range: null, 1199 | type: "dateTime", 1200 | arrayType: null, 1201 | enum: null, 1202 | reference: null, 1203 | embedded: null, 1204 | nullable: false, 1205 | required: false, 1206 | description: "", 1207 | }, 1208 | ], 1209 | writableFields: [ 1210 | { 1211 | name: "rating", 1212 | id: null, 1213 | range: null, 1214 | type: "integer", 1215 | arrayType: null, 1216 | enum: null, 1217 | reference: null, 1218 | embedded: null, 1219 | nullable: false, 1220 | required: false, 1221 | description: "", 1222 | }, 1223 | { 1224 | name: "body", 1225 | id: null, 1226 | range: null, 1227 | type: "string", 1228 | arrayType: null, 1229 | enum: null, 1230 | reference: null, 1231 | embedded: null, 1232 | nullable: false, 1233 | required: false, 1234 | description: "", 1235 | }, 1236 | { 1237 | name: "book", 1238 | id: null, 1239 | range: null, 1240 | type: "string", 1241 | arrayType: null, 1242 | enum: null, 1243 | reference: { 1244 | title: "Book", 1245 | }, 1246 | embedded: null, 1247 | nullable: false, 1248 | required: false, 1249 | description: "", 1250 | maxCardinality: 1, 1251 | }, 1252 | { 1253 | name: "author", 1254 | id: null, 1255 | range: null, 1256 | type: "string", 1257 | arrayType: null, 1258 | enum: null, 1259 | reference: null, 1260 | embedded: null, 1261 | nullable: false, 1262 | required: false, 1263 | description: "", 1264 | }, 1265 | { 1266 | name: "publicationDate", 1267 | id: null, 1268 | range: null, 1269 | type: "dateTime", 1270 | arrayType: null, 1271 | enum: null, 1272 | reference: null, 1273 | embedded: null, 1274 | nullable: false, 1275 | required: false, 1276 | description: "", 1277 | }, 1278 | ], 1279 | parameters: [ 1280 | { 1281 | variable: "page", 1282 | range: "integer", 1283 | required: false, 1284 | description: "The collection page number", 1285 | }, 1286 | ], 1287 | operations: [ 1288 | { 1289 | name: "Retrieves a Review resource.", 1290 | type: "show", 1291 | method: "GET", 1292 | deprecated: false, 1293 | }, 1294 | { 1295 | name: "Replaces the Review resource.", 1296 | type: "edit", 1297 | method: "PUT", 1298 | deprecated: false, 1299 | }, 1300 | { 1301 | name: "Removes the Review resource.", 1302 | type: "delete", 1303 | method: "DELETE", 1304 | deprecated: false, 1305 | }, 1306 | { 1307 | name: "Retrieves the collection of Review resources.", 1308 | type: "list", 1309 | method: "GET", 1310 | deprecated: false, 1311 | }, 1312 | { 1313 | name: "Creates a Review resource.", 1314 | type: "create", 1315 | method: "POST", 1316 | deprecated: false, 1317 | }, 1318 | ], 1319 | }, 1320 | ]; 1321 | 1322 | test(`Parse OpenApi v3 Documentation from Json`, async () => { 1323 | const toBeParsed = await handleJson( 1324 | openApi3Definition, 1325 | "https://demo.api-platform.com", 1326 | ); 1327 | 1328 | expect(JSON.stringify(toBeParsed, parsedJsonReplacer)).toEqual( 1329 | JSON.stringify(parsed, parsedJsonReplacer), 1330 | ); 1331 | }); 1332 | -------------------------------------------------------------------------------- /src/openapi3/handleJson.ts: -------------------------------------------------------------------------------- 1 | import { parse as dereference } from "jsonref"; 2 | import get from "lodash.get"; 3 | import inflection from "inflection"; 4 | import { Field } from "../Field.js"; 5 | import { Operation } from "../Operation.js"; 6 | import { Parameter } from "../Parameter.js"; 7 | import { Resource } from "../Resource.js"; 8 | import getResourcePaths from "../utils/getResources.js"; 9 | import getType from "./getType.js"; 10 | import type { OpenAPIV3 } from "openapi-types"; 11 | import type { OperationType } from "../Operation.js"; 12 | 13 | const isRef = ( 14 | maybeRef: T | OpenAPIV3.ReferenceObject, 15 | ): maybeRef is T => !("$ref" in maybeRef); 16 | 17 | export const removeTrailingSlash = (url: string): string => { 18 | if (url.endsWith("/")) { 19 | url = url.slice(0, -1); 20 | } 21 | return url; 22 | }; 23 | 24 | const mergeResources = (resourceA: Resource, resourceB: Resource) => { 25 | resourceB.fields?.forEach((fieldB) => { 26 | if (!resourceA.fields?.some((fieldA) => fieldA.name === fieldB.name)) { 27 | resourceA.fields?.push(fieldB); 28 | } 29 | }); 30 | resourceB.readableFields?.forEach((fieldB) => { 31 | if ( 32 | !resourceA.readableFields?.some((fieldA) => fieldA.name === fieldB.name) 33 | ) { 34 | resourceA.readableFields?.push(fieldB); 35 | } 36 | }); 37 | resourceB.writableFields?.forEach((fieldB) => { 38 | if ( 39 | !resourceA.writableFields?.some((fieldA) => fieldA.name === fieldB.name) 40 | ) { 41 | resourceA.writableFields?.push(fieldB); 42 | } 43 | }); 44 | 45 | return resourceA; 46 | }; 47 | 48 | const buildResourceFromSchema = ( 49 | schema: OpenAPIV3.SchemaObject, 50 | name: string, 51 | title: string, 52 | url: string, 53 | ) => { 54 | const description = schema.description; 55 | const properties = schema.properties || {}; 56 | 57 | const fieldNames = Object.keys(properties); 58 | const requiredFields = schema.required || []; 59 | 60 | const readableFields: Field[] = []; 61 | const writableFields: Field[] = []; 62 | 63 | const fields = fieldNames.map((fieldName) => { 64 | const property = properties[fieldName] as OpenAPIV3.SchemaObject; 65 | 66 | const type = getType(property.type || "string", property.format); 67 | const field = new Field(fieldName, { 68 | id: null, 69 | range: null, 70 | type, 71 | arrayType: 72 | type === "array" && "items" in property 73 | ? getType( 74 | (property.items as OpenAPIV3.SchemaObject).type || "string", 75 | (property.items as OpenAPIV3.SchemaObject).format, 76 | ) 77 | : null, 78 | enum: property.enum 79 | ? Object.fromEntries( 80 | // Object.values is used because the array is annotated: it contains the __meta symbol used by jsonref. 81 | Object.values(property.enum).map((enumValue) => [ 82 | typeof enumValue === "string" 83 | ? inflection.humanize(enumValue) 84 | : enumValue, 85 | enumValue, 86 | ]), 87 | ) 88 | : null, 89 | reference: null, 90 | embedded: null, 91 | nullable: property.nullable || false, 92 | required: !!requiredFields.find((value) => value === fieldName), 93 | description: property.description || "", 94 | }); 95 | 96 | if (!property.writeOnly) { 97 | readableFields.push(field); 98 | } 99 | if (!property.readOnly) { 100 | writableFields.push(field); 101 | } 102 | 103 | return field; 104 | }); 105 | 106 | return new Resource(name, url, { 107 | id: null, 108 | title, 109 | description, 110 | fields, 111 | readableFields, 112 | writableFields, 113 | parameters: [], 114 | getParameters: () => Promise.resolve([]), 115 | }); 116 | }; 117 | 118 | const buildOperationFromPathItem = ( 119 | httpMethod: `${OpenAPIV3.HttpMethods}`, 120 | operationType: OperationType, 121 | pathItem: OpenAPIV3.OperationObject, 122 | ): Operation => { 123 | return new Operation(pathItem.summary || operationType, operationType, { 124 | method: httpMethod.toUpperCase(), 125 | deprecated: !!pathItem.deprecated, 126 | }); 127 | }; 128 | 129 | /* 130 | Assumptions: 131 | RESTful APIs typically have two paths per resources: a `/noun` path and a 132 | `/noun/{id}` path. `getResources` strips out the former, allowing us to focus 133 | on the latter. 134 | 135 | In OpenAPI 3, the `/noun/{id}` path will typically have a `get` action, that 136 | probably accepts parameters and would respond with an object. 137 | */ 138 | 139 | export default async function ( 140 | response: OpenAPIV3.Document, 141 | entrypointUrl: string, 142 | ): Promise { 143 | const document = (await dereference(response, { 144 | scope: entrypointUrl, 145 | })) as OpenAPIV3.Document; 146 | 147 | const paths = getResourcePaths(document.paths); 148 | 149 | let serverUrlOrRelative = "/"; 150 | if (document.servers) { 151 | serverUrlOrRelative = document.servers[0].url; 152 | } 153 | 154 | const serverUrl = new URL(serverUrlOrRelative, entrypointUrl).href; 155 | 156 | const resources: Resource[] = []; 157 | 158 | paths.forEach((path) => { 159 | const splittedPath = removeTrailingSlash(path).split("/"); 160 | const name = inflection.pluralize(splittedPath[splittedPath.length - 2]); 161 | const url = `${removeTrailingSlash(serverUrl)}/${name}`; 162 | const pathItem = document.paths[path]; 163 | if (!pathItem) { 164 | throw new Error(); 165 | } 166 | 167 | const title = inflection.classify(splittedPath[splittedPath.length - 2]); 168 | 169 | const showOperation = pathItem.get; 170 | const editOperation = pathItem.put || pathItem.patch; 171 | if (!showOperation && !editOperation) return; 172 | 173 | const showSchema = showOperation 174 | ? (get( 175 | showOperation, 176 | "responses.200.content.application/json.schema", 177 | get(document, `components.schemas[${title}]`), 178 | ) as OpenAPIV3.SchemaObject) 179 | : null; 180 | const editSchema = editOperation 181 | ? (get( 182 | editOperation, 183 | "requestBody.content.application/json.schema", 184 | ) as unknown as OpenAPIV3.SchemaObject) 185 | : null; 186 | 187 | if (!showSchema && !editSchema) return; 188 | 189 | const showResource = showSchema 190 | ? buildResourceFromSchema(showSchema, name, title, url) 191 | : null; 192 | const editResource = editSchema 193 | ? buildResourceFromSchema(editSchema, name, title, url) 194 | : null; 195 | let resource = showResource ?? editResource; 196 | if (!resource) return; 197 | if (showResource && editResource) { 198 | resource = mergeResources(showResource, editResource); 199 | } 200 | 201 | const putOperation = pathItem.put; 202 | const patchOperation = pathItem.patch; 203 | const deleteOperation = pathItem.delete; 204 | const pathCollection = document.paths[`/${name}`]; 205 | const listOperation = pathCollection && pathCollection.get; 206 | const createOperation = pathCollection && pathCollection.post; 207 | resource.operations = [ 208 | ...(showOperation 209 | ? [buildOperationFromPathItem("get", "show", showOperation)] 210 | : []), 211 | ...(putOperation 212 | ? [buildOperationFromPathItem("put", "edit", putOperation)] 213 | : []), 214 | ...(patchOperation 215 | ? [buildOperationFromPathItem("patch", "edit", patchOperation)] 216 | : []), 217 | ...(deleteOperation 218 | ? [buildOperationFromPathItem("delete", "delete", deleteOperation)] 219 | : []), 220 | ...(listOperation 221 | ? [buildOperationFromPathItem("get", "list", listOperation)] 222 | : []), 223 | ...(createOperation 224 | ? [buildOperationFromPathItem("post", "create", createOperation)] 225 | : []), 226 | ]; 227 | 228 | if (listOperation && listOperation.parameters) { 229 | resource.parameters = listOperation.parameters 230 | .filter(isRef) 231 | .map( 232 | (parameter) => 233 | new Parameter( 234 | parameter.name, 235 | parameter.schema && isRef(parameter.schema) 236 | ? parameter.schema.type 237 | ? getType(parameter.schema.type) 238 | : null 239 | : null, 240 | parameter.required || false, 241 | parameter.description || "", 242 | parameter.deprecated, 243 | ), 244 | ); 245 | } 246 | 247 | resources.push(resource); 248 | }); 249 | 250 | // Guess embeddeds and references from property names 251 | resources.forEach((resource) => { 252 | resource.fields?.forEach((field) => { 253 | const name = inflection.camelize(field.name).replace(/Ids?$/, ""); 254 | 255 | const guessedResource = resources.find( 256 | (res) => res.title === inflection.classify(name), 257 | ); 258 | if (!guessedResource) { 259 | return; 260 | } 261 | field.maxCardinality = field.type === "array" ? null : 1; 262 | if (field.type === "object" || field.arrayType === "object") { 263 | field.embedded = guessedResource; 264 | } else { 265 | field.reference = guessedResource; 266 | } 267 | }); 268 | }); 269 | 270 | return resources; 271 | } 272 | -------------------------------------------------------------------------------- /src/openapi3/index.ts: -------------------------------------------------------------------------------- 1 | export { default as parseOpenApi3Documentation } from "./parseOpenApi3Documentation.js"; 2 | export * from "./parseOpenApi3Documentation.js"; 3 | -------------------------------------------------------------------------------- /src/openapi3/parseOpenApi3Documentation.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../Api.js"; 2 | import handleJson, { removeTrailingSlash } from "./handleJson.js"; 3 | import type { OpenAPIV3 } from "openapi-types"; 4 | import type { RequestInitExtended } from "./types"; 5 | 6 | export interface ParsedOpenApi3Documentation { 7 | api: Api; 8 | response: OpenAPIV3.Document; 9 | status: number; 10 | } 11 | 12 | export default function parseOpenApi3Documentation( 13 | entrypointUrl: string, 14 | options: RequestInitExtended = {}, 15 | ): Promise { 16 | entrypointUrl = removeTrailingSlash(entrypointUrl); 17 | let headers: HeadersInit | undefined = 18 | typeof options.headers === "function" ? options.headers() : options.headers; 19 | headers = new Headers(headers); 20 | headers.append("Accept", "application/vnd.openapi+json"); 21 | 22 | return fetch(entrypointUrl, { ...options, headers: headers }) 23 | .then((res) => Promise.all([res, res.json()])) 24 | .then( 25 | ([res, response]: [res: Response, response: OpenAPIV3.Document]) => { 26 | const title = response.info.title; 27 | return handleJson(response, entrypointUrl).then((resources) => ({ 28 | api: new Api(entrypointUrl, { title, resources }), 29 | response, 30 | status: res.status, 31 | })); 32 | }, 33 | ([res, response]: [res: Response, response: OpenAPIV3.Document]) => 34 | Promise.reject({ 35 | api: new Api(entrypointUrl, { resources: [] }), 36 | response, 37 | status: res.status, 38 | }), 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/openapi3/types.ts: -------------------------------------------------------------------------------- 1 | export interface RequestInitExtended extends Omit { 2 | headers?: HeadersInit | (() => HeadersInit); 3 | } 4 | -------------------------------------------------------------------------------- /src/swagger/handleJson.test.ts: -------------------------------------------------------------------------------- 1 | import handleJson from "./handleJson.js"; 2 | import type { OpenAPIV2 } from "openapi-types"; 3 | import type { Field } from "../Field.js"; 4 | 5 | const swaggerApiDefinition: OpenAPIV2.Document = { 6 | swagger: "2.0", 7 | basePath: "/", 8 | info: { 9 | title: "API Platform's demo", 10 | version: "0.0.0", 11 | description: 12 | "This is a demo application of the [API Platform](https://api-platform.com) framework.\n[Its source code](https://github.com/api-platform/demo) includes various examples, check it out!\n", 13 | }, 14 | paths: { 15 | "/books": { 16 | get: { 17 | tags: ["Book"], 18 | operationId: "getBookCollection", 19 | produces: [ 20 | "application/ld+json", 21 | "application/hal+json", 22 | "application/xml", 23 | "text/xml", 24 | "application/json", 25 | "application/x-yaml", 26 | "text/csv", 27 | "text/html", 28 | ], 29 | summary: "Retrieves the collection of Book resources.", 30 | responses: { 31 | "200": { 32 | description: "Book collection response", 33 | schema: { 34 | type: "array", 35 | items: { $ref: "#/definitions/Book" }, 36 | }, 37 | }, 38 | }, 39 | }, 40 | post: { 41 | tags: ["Book"], 42 | operationId: "postBookCollection", 43 | consumes: [ 44 | "application/ld+json", 45 | "application/hal+json", 46 | "application/xml", 47 | "text/xml", 48 | "application/json", 49 | "application/x-yaml", 50 | "text/csv", 51 | "text/html", 52 | ], 53 | produces: [ 54 | "application/ld+json", 55 | "application/hal+json", 56 | "application/xml", 57 | "text/xml", 58 | "application/json", 59 | "application/x-yaml", 60 | "text/csv", 61 | "text/html", 62 | ], 63 | summary: "Creates a Book resource.", 64 | parameters: [ 65 | { 66 | name: "book", 67 | in: "body", 68 | description: "The new Book resource", 69 | schema: { $ref: "#/definitions/Book" }, 70 | }, 71 | ], 72 | responses: { 73 | "201": { 74 | description: "Book resource created", 75 | schema: { $ref: "#/definitions/Book" }, 76 | }, 77 | "400": { description: "Invalid input" }, 78 | "404": { description: "Resource not found" }, 79 | }, 80 | }, 81 | }, 82 | "/books/{id}": { 83 | get: { 84 | tags: ["Book"], 85 | operationId: "getBookItem", 86 | produces: [ 87 | "application/ld+json", 88 | "application/hal+json", 89 | "application/xml", 90 | "text/xml", 91 | "application/json", 92 | "application/x-yaml", 93 | "text/csv", 94 | "text/html", 95 | ], 96 | summary: "Retrieves a Book resource.", 97 | parameters: [ 98 | { name: "id", in: "path", required: true, type: "integer" }, 99 | ], 100 | responses: { 101 | "200": { 102 | description: "Book resource response", 103 | schema: { $ref: "#/definitions/Book" }, 104 | }, 105 | "404": { description: "Resource not found" }, 106 | }, 107 | }, 108 | put: { 109 | tags: ["Book"], 110 | operationId: "putBookItem", 111 | consumes: [ 112 | "application/ld+json", 113 | "application/hal+json", 114 | "application/xml", 115 | "text/xml", 116 | "application/json", 117 | "application/x-yaml", 118 | "text/csv", 119 | "text/html", 120 | ], 121 | produces: [ 122 | "application/ld+json", 123 | "application/hal+json", 124 | "application/xml", 125 | "text/xml", 126 | "application/json", 127 | "application/x-yaml", 128 | "text/csv", 129 | "text/html", 130 | ], 131 | summary: "Replaces the Book resource.", 132 | parameters: [ 133 | { name: "id", in: "path", type: "integer", required: true }, 134 | { 135 | name: "book", 136 | in: "body", 137 | description: "The updated Book resource", 138 | schema: { $ref: "#/definitions/Book" }, 139 | }, 140 | ], 141 | responses: { 142 | "200": { 143 | description: "Book resource updated", 144 | schema: { $ref: "#/definitions/Book" }, 145 | }, 146 | "400": { description: "Invalid input" }, 147 | "404": { description: "Resource not found" }, 148 | }, 149 | }, 150 | delete: { 151 | tags: ["Book"], 152 | operationId: "deleteBookItem", 153 | summary: "Removes the Book resource.", 154 | responses: { 155 | "204": { description: "Book resource deleted" }, 156 | "404": { description: "Resource not found" }, 157 | }, 158 | parameters: [ 159 | { name: "id", in: "path", type: "integer", required: true }, 160 | ], 161 | }, 162 | }, 163 | "/reviews": { 164 | get: { 165 | tags: ["Review"], 166 | operationId: "getReviewCollection", 167 | produces: [ 168 | "application/ld+json", 169 | "application/hal+json", 170 | "application/xml", 171 | "text/xml", 172 | "application/json", 173 | "application/x-yaml", 174 | "text/csv", 175 | "text/html", 176 | ], 177 | summary: "Retrieves the collection of Review resources.", 178 | responses: { 179 | "200": { 180 | description: "Review collection response", 181 | schema: { 182 | type: "array", 183 | items: { $ref: "#/definitions/Review" }, 184 | }, 185 | }, 186 | }, 187 | }, 188 | post: { 189 | tags: ["Review"], 190 | operationId: "postReviewCollection", 191 | consumes: [ 192 | "application/ld+json", 193 | "application/hal+json", 194 | "application/xml", 195 | "text/xml", 196 | "application/json", 197 | "application/x-yaml", 198 | "text/csv", 199 | "text/html", 200 | ], 201 | produces: [ 202 | "application/ld+json", 203 | "application/hal+json", 204 | "application/xml", 205 | "text/xml", 206 | "application/json", 207 | "application/x-yaml", 208 | "text/csv", 209 | "text/html", 210 | ], 211 | summary: "Creates a Review resource.", 212 | parameters: [ 213 | { 214 | name: "review", 215 | in: "body", 216 | description: "The new Review resource", 217 | schema: { $ref: "#/definitions/Review" }, 218 | }, 219 | ], 220 | responses: { 221 | "201": { 222 | description: "Review resource created", 223 | schema: { $ref: "#/definitions/Review" }, 224 | }, 225 | "400": { description: "Invalid input" }, 226 | "404": { description: "Resource not found" }, 227 | }, 228 | }, 229 | }, 230 | "/reviews/{id}": { 231 | get: { 232 | tags: ["Review"], 233 | operationId: "getReviewItem", 234 | produces: [ 235 | "application/ld+json", 236 | "application/hal+json", 237 | "application/xml", 238 | "text/xml", 239 | "application/json", 240 | "application/x-yaml", 241 | "text/csv", 242 | "text/html", 243 | ], 244 | summary: "Retrieves a Review resource.", 245 | parameters: [ 246 | { name: "id", in: "path", required: true, type: "integer" }, 247 | ], 248 | responses: { 249 | "200": { 250 | description: "Review resource response", 251 | schema: { $ref: "#/definitions/Review" }, 252 | }, 253 | "404": { description: "Resource not found" }, 254 | }, 255 | }, 256 | put: { 257 | tags: ["Review"], 258 | operationId: "putReviewItem", 259 | consumes: [ 260 | "application/ld+json", 261 | "application/hal+json", 262 | "application/xml", 263 | "text/xml", 264 | "application/json", 265 | "application/x-yaml", 266 | "text/csv", 267 | "text/html", 268 | ], 269 | produces: [ 270 | "application/ld+json", 271 | "application/hal+json", 272 | "application/xml", 273 | "text/xml", 274 | "application/json", 275 | "application/x-yaml", 276 | "text/csv", 277 | "text/html", 278 | ], 279 | summary: "Replaces the Review resource.", 280 | parameters: [ 281 | { name: "id", in: "path", type: "integer", required: true }, 282 | { 283 | name: "review", 284 | in: "body", 285 | description: "The updated Review resource", 286 | schema: { $ref: "#/definitions/Review" }, 287 | }, 288 | ], 289 | responses: { 290 | "200": { 291 | description: "Review resource updated", 292 | schema: { $ref: "#/definitions/Review" }, 293 | }, 294 | "400": { description: "Invalid input" }, 295 | "404": { description: "Resource not found" }, 296 | }, 297 | }, 298 | delete: { 299 | tags: ["Review"], 300 | operationId: "deleteReviewItem", 301 | summary: "Removes the Review resource.", 302 | responses: { 303 | "204": { description: "Review resource deleted" }, 304 | "404": { description: "Resource not found" }, 305 | }, 306 | parameters: [ 307 | { name: "id", in: "path", type: "integer", required: true }, 308 | ], 309 | }, 310 | }, 311 | }, 312 | definitions: { 313 | Book: { 314 | type: "object", 315 | externalDocs: { url: "http://schema.org/Book" }, 316 | properties: { 317 | id: { type: "integer" }, 318 | isbn: { description: "The ISBN of the book", type: "string" }, 319 | description: { 320 | description: "A description of the item", 321 | type: "string", 322 | }, 323 | author: { 324 | description: 325 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 326 | type: "string", 327 | }, 328 | title: { description: "The title of the book", type: "string" }, 329 | bookFormat: { 330 | type: "string", 331 | description: "The publication format of the book.", 332 | enum: ["AUDIOBOOK_FORMAT", "E_BOOK", "PAPERBACK", "HARDCOVER"], 333 | }, 334 | publicationDate: { 335 | description: 336 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 337 | type: "string", 338 | format: "date-time", 339 | }, 340 | }, 341 | required: [ 342 | "description", 343 | "author", 344 | "title", 345 | "bookFormat", 346 | "publicationDate", 347 | ], 348 | }, 349 | Review: { 350 | type: "object", 351 | externalDocs: { url: "http://schema.org/Review" }, 352 | properties: { 353 | id: { type: "integer" }, 354 | rating: { type: "integer" }, 355 | body: { 356 | description: "The actual body of the review", 357 | type: "string", 358 | }, 359 | book: { 360 | description: "The item that is being reviewed/rated", 361 | type: "string", 362 | }, 363 | author: { 364 | description: "Author the author of the review", 365 | type: "string", 366 | }, 367 | publicationDate: { 368 | description: "Author the author of the review", 369 | type: "string", 370 | format: "date-time", 371 | }, 372 | }, 373 | required: ["book"], 374 | }, 375 | }, 376 | }; 377 | 378 | const parsed = [ 379 | { 380 | name: "books", 381 | url: "https://demo.api-platform.com/books", 382 | id: null, 383 | title: "Book", 384 | fields: [ 385 | { 386 | name: "id", 387 | id: null, 388 | range: null, 389 | type: "integer", 390 | reference: null, 391 | required: false, 392 | embedded: null, 393 | enum: null, 394 | description: "", 395 | }, 396 | { 397 | name: "isbn", 398 | id: null, 399 | range: null, 400 | type: "string", 401 | reference: null, 402 | required: false, 403 | embedded: null, 404 | enum: null, 405 | description: "The ISBN of the book", 406 | }, 407 | { 408 | name: "description", 409 | id: null, 410 | range: null, 411 | type: "string", 412 | reference: null, 413 | required: true, 414 | embedded: null, 415 | enum: null, 416 | description: "A description of the item", 417 | }, 418 | { 419 | name: "author", 420 | id: null, 421 | range: null, 422 | type: "string", 423 | reference: null, 424 | required: true, 425 | embedded: null, 426 | enum: null, 427 | description: 428 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 429 | }, 430 | { 431 | name: "title", 432 | id: null, 433 | range: null, 434 | type: "string", 435 | reference: null, 436 | required: true, 437 | embedded: null, 438 | enum: null, 439 | description: "The title of the book", 440 | }, 441 | { 442 | name: "bookFormat", 443 | id: null, 444 | range: null, 445 | type: "string", 446 | reference: null, 447 | embedded: null, 448 | enum: { 449 | "Audiobook format": "AUDIOBOOK_FORMAT", 450 | "E book": "E_BOOK", 451 | Paperback: "PAPERBACK", 452 | Hardcover: "HARDCOVER", 453 | }, 454 | required: true, 455 | description: "The publication format of the book.", 456 | }, 457 | { 458 | name: "publicationDate", 459 | id: null, 460 | range: null, 461 | type: "dateTime", 462 | reference: null, 463 | required: true, 464 | embedded: null, 465 | enum: null, 466 | description: 467 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 468 | }, 469 | ], 470 | readableFields: [ 471 | { 472 | name: "id", 473 | id: null, 474 | range: null, 475 | reference: null, 476 | required: false, 477 | description: "", 478 | }, 479 | { 480 | name: "isbn", 481 | id: null, 482 | range: null, 483 | reference: null, 484 | required: false, 485 | description: "The ISBN of the book", 486 | }, 487 | { 488 | name: "description", 489 | id: null, 490 | range: null, 491 | reference: null, 492 | required: true, 493 | description: "A description of the item", 494 | }, 495 | { 496 | name: "author", 497 | id: null, 498 | range: null, 499 | reference: null, 500 | required: true, 501 | description: 502 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 503 | }, 504 | { 505 | name: "title", 506 | id: null, 507 | range: null, 508 | reference: null, 509 | required: true, 510 | description: "The title of the book", 511 | }, 512 | { 513 | name: "publicationDate", 514 | id: null, 515 | range: null, 516 | reference: null, 517 | required: true, 518 | description: 519 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 520 | }, 521 | ], 522 | writableFields: [ 523 | { 524 | name: "id", 525 | id: null, 526 | range: null, 527 | reference: null, 528 | required: false, 529 | description: "", 530 | }, 531 | { 532 | name: "isbn", 533 | id: null, 534 | range: null, 535 | reference: null, 536 | required: false, 537 | description: "The ISBN of the book", 538 | }, 539 | { 540 | name: "description", 541 | id: null, 542 | range: null, 543 | reference: null, 544 | required: true, 545 | description: "A description of the item", 546 | }, 547 | { 548 | name: "author", 549 | id: null, 550 | range: null, 551 | reference: null, 552 | required: true, 553 | description: 554 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 555 | }, 556 | { 557 | name: "title", 558 | id: null, 559 | range: null, 560 | reference: null, 561 | required: true, 562 | description: "The title of the book", 563 | }, 564 | { 565 | name: "publicationDate", 566 | id: null, 567 | range: null, 568 | reference: null, 569 | required: true, 570 | description: 571 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 572 | }, 573 | ], 574 | }, 575 | { 576 | name: "reviews", 577 | url: "https://demo.api-platform.com/reviews", 578 | id: null, 579 | title: "Review", 580 | fields: [ 581 | { 582 | name: "id", 583 | id: null, 584 | range: null, 585 | type: "integer", 586 | reference: null, 587 | required: false, 588 | embedded: null, 589 | enum: null, 590 | description: "", 591 | }, 592 | { 593 | name: "rating", 594 | id: null, 595 | range: null, 596 | type: "integer", 597 | reference: null, 598 | required: false, 599 | embedded: null, 600 | enum: null, 601 | description: "", 602 | }, 603 | { 604 | name: "body", 605 | id: null, 606 | range: null, 607 | type: "string", 608 | reference: null, 609 | required: false, 610 | embedded: null, 611 | enum: null, 612 | description: "The actual body of the review", 613 | }, 614 | { 615 | name: "book", 616 | id: null, 617 | range: null, 618 | reference: { 619 | name: "books", 620 | url: "https://demo.api-platform.com/books", 621 | id: null, 622 | title: "Book", 623 | fields: [ 624 | { 625 | name: "id", 626 | id: null, 627 | range: null, 628 | reference: null, 629 | required: false, 630 | description: "", 631 | }, 632 | { 633 | name: "isbn", 634 | id: null, 635 | range: null, 636 | reference: null, 637 | required: false, 638 | description: "The ISBN of the book", 639 | }, 640 | { 641 | name: "description", 642 | id: null, 643 | range: null, 644 | reference: null, 645 | required: true, 646 | description: "A description of the item", 647 | }, 648 | { 649 | name: "author", 650 | id: null, 651 | range: null, 652 | reference: null, 653 | required: true, 654 | description: 655 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 656 | }, 657 | { 658 | name: "title", 659 | id: null, 660 | range: null, 661 | reference: null, 662 | required: true, 663 | description: "The title of the book", 664 | }, 665 | { 666 | name: "publicationDate", 667 | id: null, 668 | range: null, 669 | reference: null, 670 | required: true, 671 | description: 672 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 673 | }, 674 | ], 675 | readableFields: [ 676 | { 677 | name: "id", 678 | id: null, 679 | range: null, 680 | reference: null, 681 | required: false, 682 | description: "", 683 | }, 684 | { 685 | name: "isbn", 686 | id: null, 687 | range: null, 688 | reference: null, 689 | required: false, 690 | description: "The ISBN of the book", 691 | }, 692 | { 693 | name: "description", 694 | id: null, 695 | range: null, 696 | reference: null, 697 | required: true, 698 | description: "A description of the item", 699 | }, 700 | { 701 | name: "author", 702 | id: null, 703 | range: null, 704 | reference: null, 705 | required: true, 706 | description: 707 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 708 | }, 709 | { 710 | name: "title", 711 | id: null, 712 | range: null, 713 | reference: null, 714 | required: true, 715 | description: "The title of the book", 716 | }, 717 | { 718 | name: "publicationDate", 719 | id: null, 720 | range: null, 721 | reference: null, 722 | required: true, 723 | description: 724 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 725 | }, 726 | ], 727 | writableFields: [ 728 | { 729 | name: "id", 730 | id: null, 731 | range: null, 732 | reference: null, 733 | required: false, 734 | description: "", 735 | }, 736 | { 737 | name: "isbn", 738 | id: null, 739 | range: null, 740 | reference: null, 741 | required: false, 742 | description: "The ISBN of the book", 743 | }, 744 | { 745 | name: "description", 746 | id: null, 747 | range: null, 748 | reference: null, 749 | required: true, 750 | description: "A description of the item", 751 | }, 752 | { 753 | name: "author", 754 | id: null, 755 | range: null, 756 | reference: null, 757 | required: true, 758 | description: 759 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 760 | }, 761 | { 762 | name: "title", 763 | id: null, 764 | range: null, 765 | reference: null, 766 | required: true, 767 | description: "The title of the book", 768 | }, 769 | { 770 | name: "publicationDate", 771 | id: null, 772 | range: null, 773 | reference: null, 774 | required: true, 775 | description: 776 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 777 | }, 778 | ], 779 | }, 780 | required: true, 781 | description: "The item that is being reviewed/rated", 782 | }, 783 | { 784 | name: "author", 785 | id: null, 786 | range: null, 787 | type: "string", 788 | reference: null, 789 | required: false, 790 | embedded: null, 791 | enum: null, 792 | description: "Author the author of the review", 793 | }, 794 | { 795 | name: "publicationDate", 796 | id: null, 797 | range: null, 798 | type: "string", 799 | reference: null, 800 | required: false, 801 | embedded: null, 802 | enum: null, 803 | description: "Author the author of the review", 804 | }, 805 | ], 806 | readableFields: [ 807 | { 808 | name: "id", 809 | id: null, 810 | range: null, 811 | reference: null, 812 | required: false, 813 | description: "", 814 | }, 815 | { 816 | name: "rating", 817 | id: null, 818 | range: null, 819 | reference: null, 820 | required: false, 821 | description: "", 822 | }, 823 | { 824 | name: "body", 825 | id: null, 826 | range: null, 827 | reference: null, 828 | required: false, 829 | description: "The actual body of the review", 830 | }, 831 | { 832 | name: "book", 833 | id: null, 834 | range: null, 835 | reference: { 836 | name: "books", 837 | url: "https://demo.api-platform.com/books", 838 | id: null, 839 | title: "Book", 840 | fields: [ 841 | { 842 | name: "id", 843 | id: null, 844 | range: null, 845 | reference: null, 846 | required: false, 847 | description: "", 848 | }, 849 | { 850 | name: "isbn", 851 | id: null, 852 | range: null, 853 | reference: null, 854 | required: false, 855 | description: "The ISBN of the book", 856 | }, 857 | { 858 | name: "description", 859 | id: null, 860 | range: null, 861 | reference: null, 862 | required: true, 863 | description: "A description of the item", 864 | }, 865 | { 866 | name: "author", 867 | id: null, 868 | range: null, 869 | reference: null, 870 | required: true, 871 | description: 872 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 873 | }, 874 | { 875 | name: "title", 876 | id: null, 877 | range: null, 878 | reference: null, 879 | required: true, 880 | description: "The title of the book", 881 | }, 882 | { 883 | name: "publicationDate", 884 | id: null, 885 | range: null, 886 | reference: null, 887 | required: true, 888 | description: 889 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 890 | }, 891 | ], 892 | readableFields: [ 893 | { 894 | name: "id", 895 | id: null, 896 | range: null, 897 | reference: null, 898 | required: false, 899 | description: "", 900 | }, 901 | { 902 | name: "isbn", 903 | id: null, 904 | range: null, 905 | reference: null, 906 | required: false, 907 | description: "The ISBN of the book", 908 | }, 909 | { 910 | name: "description", 911 | id: null, 912 | range: null, 913 | reference: null, 914 | required: true, 915 | description: "A description of the item", 916 | }, 917 | { 918 | name: "author", 919 | id: null, 920 | range: null, 921 | reference: null, 922 | required: true, 923 | description: 924 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 925 | }, 926 | { 927 | name: "title", 928 | id: null, 929 | range: null, 930 | reference: null, 931 | required: true, 932 | description: "The title of the book", 933 | }, 934 | { 935 | name: "publicationDate", 936 | id: null, 937 | range: null, 938 | reference: null, 939 | required: true, 940 | description: 941 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 942 | }, 943 | ], 944 | writableFields: [ 945 | { 946 | name: "id", 947 | id: null, 948 | range: null, 949 | reference: null, 950 | required: false, 951 | description: "", 952 | }, 953 | { 954 | name: "isbn", 955 | id: null, 956 | range: null, 957 | reference: null, 958 | required: false, 959 | description: "The ISBN of the book", 960 | }, 961 | { 962 | name: "description", 963 | id: null, 964 | range: null, 965 | reference: null, 966 | required: true, 967 | description: "A description of the item", 968 | }, 969 | { 970 | name: "author", 971 | id: null, 972 | range: null, 973 | reference: null, 974 | required: true, 975 | description: 976 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 977 | }, 978 | { 979 | name: "title", 980 | id: null, 981 | range: null, 982 | reference: null, 983 | required: true, 984 | description: "The title of the book", 985 | }, 986 | { 987 | name: "publicationDate", 988 | id: null, 989 | range: null, 990 | reference: null, 991 | required: true, 992 | description: 993 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 994 | }, 995 | ], 996 | }, 997 | required: true, 998 | description: "The item that is being reviewed/rated", 999 | }, 1000 | { 1001 | name: "author", 1002 | id: null, 1003 | range: null, 1004 | reference: null, 1005 | required: false, 1006 | description: "Author the author of the review", 1007 | }, 1008 | { 1009 | name: "publicationDate", 1010 | id: null, 1011 | range: null, 1012 | reference: null, 1013 | required: false, 1014 | description: "Author the author of the review", 1015 | }, 1016 | ], 1017 | writableFields: [ 1018 | { 1019 | name: "id", 1020 | id: null, 1021 | range: null, 1022 | reference: null, 1023 | required: false, 1024 | description: "", 1025 | }, 1026 | { 1027 | name: "rating", 1028 | id: null, 1029 | range: null, 1030 | reference: null, 1031 | required: false, 1032 | description: "", 1033 | }, 1034 | { 1035 | name: "body", 1036 | id: null, 1037 | range: null, 1038 | reference: null, 1039 | required: false, 1040 | description: "The actual body of the review", 1041 | }, 1042 | { 1043 | name: "book", 1044 | id: null, 1045 | range: null, 1046 | reference: { 1047 | name: "books", 1048 | url: "https://demo.api-platform.com/books", 1049 | id: null, 1050 | title: "Book", 1051 | fields: [ 1052 | { 1053 | name: "id", 1054 | id: null, 1055 | range: null, 1056 | reference: null, 1057 | required: false, 1058 | description: "", 1059 | }, 1060 | { 1061 | name: "isbn", 1062 | id: null, 1063 | range: null, 1064 | reference: null, 1065 | required: false, 1066 | description: "The ISBN of the book", 1067 | }, 1068 | { 1069 | name: "description", 1070 | id: null, 1071 | range: null, 1072 | reference: null, 1073 | required: true, 1074 | description: "A description of the item", 1075 | }, 1076 | { 1077 | name: "author", 1078 | id: null, 1079 | range: null, 1080 | reference: null, 1081 | required: true, 1082 | description: 1083 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 1084 | }, 1085 | { 1086 | name: "title", 1087 | id: null, 1088 | range: null, 1089 | reference: null, 1090 | required: true, 1091 | description: "The title of the book", 1092 | }, 1093 | { 1094 | name: "publicationDate", 1095 | id: null, 1096 | range: null, 1097 | reference: null, 1098 | required: true, 1099 | description: 1100 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 1101 | }, 1102 | ], 1103 | readableFields: [ 1104 | { 1105 | name: "id", 1106 | id: null, 1107 | range: null, 1108 | reference: null, 1109 | required: false, 1110 | description: "", 1111 | }, 1112 | { 1113 | name: "isbn", 1114 | id: null, 1115 | range: null, 1116 | reference: null, 1117 | required: false, 1118 | description: "The ISBN of the book", 1119 | }, 1120 | { 1121 | name: "description", 1122 | id: null, 1123 | range: null, 1124 | reference: null, 1125 | required: true, 1126 | description: "A description of the item", 1127 | }, 1128 | { 1129 | name: "author", 1130 | id: null, 1131 | range: null, 1132 | reference: null, 1133 | required: true, 1134 | description: 1135 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 1136 | }, 1137 | { 1138 | name: "title", 1139 | id: null, 1140 | range: null, 1141 | reference: null, 1142 | required: true, 1143 | description: "The title of the book", 1144 | }, 1145 | { 1146 | name: "publicationDate", 1147 | id: null, 1148 | range: null, 1149 | reference: null, 1150 | required: true, 1151 | description: 1152 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 1153 | }, 1154 | ], 1155 | writableFields: [ 1156 | { 1157 | name: "id", 1158 | id: null, 1159 | range: null, 1160 | reference: null, 1161 | required: false, 1162 | description: "", 1163 | }, 1164 | { 1165 | name: "isbn", 1166 | id: null, 1167 | range: null, 1168 | reference: null, 1169 | required: false, 1170 | description: "The ISBN of the book", 1171 | }, 1172 | { 1173 | name: "description", 1174 | id: null, 1175 | range: null, 1176 | reference: null, 1177 | required: true, 1178 | description: "A description of the item", 1179 | }, 1180 | { 1181 | name: "author", 1182 | id: null, 1183 | range: null, 1184 | reference: null, 1185 | required: true, 1186 | description: 1187 | "The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably", 1188 | }, 1189 | { 1190 | name: "title", 1191 | id: null, 1192 | range: null, 1193 | reference: null, 1194 | required: true, 1195 | description: "The title of the book", 1196 | }, 1197 | { 1198 | name: "publicationDate", 1199 | id: null, 1200 | range: null, 1201 | reference: null, 1202 | required: true, 1203 | description: 1204 | "The date on which the CreativeWork was created or the item was added to a DataFeed", 1205 | }, 1206 | ], 1207 | }, 1208 | required: true, 1209 | description: "The item that is being reviewed/rated", 1210 | }, 1211 | { 1212 | name: "author", 1213 | id: null, 1214 | range: null, 1215 | reference: null, 1216 | required: false, 1217 | description: "Author the author of the review", 1218 | }, 1219 | { 1220 | name: "publicationDate", 1221 | id: null, 1222 | range: null, 1223 | reference: null, 1224 | required: false, 1225 | description: "Author the author of the review", 1226 | }, 1227 | ], 1228 | }, 1229 | ]; 1230 | 1231 | describe(`Parse Swagger Documentation from Json`, () => { 1232 | const toBeParsed = handleJson( 1233 | swaggerApiDefinition, 1234 | "https://demo.api-platform.com", 1235 | ); 1236 | 1237 | test(`Properties to be equal`, () => { 1238 | expect(toBeParsed[0].name).toBe(parsed[0].name); 1239 | expect(toBeParsed[0].url).toBe(parsed[0].url); 1240 | expect(toBeParsed[0].id).toBe(parsed[0].id); 1241 | 1242 | const toBeParsedFields = toBeParsed[0].fields as Field[]; 1243 | expect(toBeParsedFields[0]).toEqual(parsed[0].fields[0]); 1244 | expect(toBeParsedFields[1]).toEqual(parsed[0].fields[1]); 1245 | expect(toBeParsedFields[2]).toEqual(parsed[0].fields[2]); 1246 | expect(toBeParsedFields[3]).toEqual(parsed[0].fields[3]); 1247 | expect(toBeParsedFields[4]).toEqual(parsed[0].fields[4]); 1248 | expect(toBeParsedFields[5]).toEqual(parsed[0].fields[5]); 1249 | 1250 | expect(toBeParsed[1].name).toBe(parsed[1].name); 1251 | expect(toBeParsed[1].url).toBe(parsed[1].url); 1252 | expect(toBeParsed[1].id).toBe(parsed[1].id); 1253 | 1254 | expect((toBeParsed[1].fields as Field[])[0]).toEqual(parsed[1].fields[0]); 1255 | }); 1256 | }); 1257 | -------------------------------------------------------------------------------- /src/swagger/handleJson.ts: -------------------------------------------------------------------------------- 1 | import get from "lodash.get"; 2 | import inflection from "inflection"; 3 | import { Field } from "../Field.js"; 4 | import { Resource } from "../Resource.js"; 5 | import getResourcePaths from "../utils/getResources.js"; 6 | import getType from "../openapi3/getType.js"; 7 | import type { OpenAPIV2 } from "openapi-types"; 8 | 9 | export const removeTrailingSlash = (url: string): string => { 10 | if (url.endsWith("/")) { 11 | url = url.slice(0, -1); 12 | } 13 | return url; 14 | }; 15 | 16 | export default function ( 17 | response: OpenAPIV2.Document, 18 | entrypointUrl: string, 19 | ): Resource[] { 20 | const paths = getResourcePaths(response.paths); 21 | 22 | return paths.map((path) => { 23 | const splittedPath = removeTrailingSlash(path).split("/"); 24 | const name = inflection.pluralize(splittedPath[splittedPath.length - 2]); 25 | const url = `${removeTrailingSlash(entrypointUrl)}/${name}`; 26 | 27 | const title = inflection.classify(splittedPath[splittedPath.length - 2]); 28 | 29 | if (!response.definitions) { 30 | throw new Error(); // @TODO 31 | } 32 | 33 | const definition = response.definitions[title]; 34 | 35 | if (!definition) { 36 | throw new Error(); // @TODO 37 | } 38 | 39 | const description = definition.description; 40 | const properties = definition.properties; 41 | 42 | if (!properties) { 43 | throw new Error(); // @TODO 44 | } 45 | 46 | const fieldNames = Object.keys(properties); 47 | const requiredFields = get( 48 | response, 49 | ["definitions", title, "required"], 50 | [], 51 | ) as string[]; 52 | 53 | const fields = fieldNames.map((fieldName) => { 54 | const property = properties[fieldName]; 55 | 56 | return new Field(fieldName, { 57 | id: null, 58 | range: null, 59 | type: getType( 60 | get(property, "type", "") as string, 61 | get(property, "format", "") as string, 62 | ), 63 | enum: property.enum 64 | ? Object.fromEntries( 65 | property.enum.map((enumValue: string | number) => [ 66 | typeof enumValue === "string" 67 | ? inflection.humanize(enumValue) 68 | : enumValue, 69 | enumValue, 70 | ]), 71 | ) 72 | : null, 73 | reference: null, 74 | embedded: null, 75 | required: !!requiredFields.find((value) => value === fieldName), 76 | description: property.description || "", 77 | }); 78 | }); 79 | 80 | return new Resource(name, url, { 81 | id: null, 82 | title, 83 | description, 84 | fields, 85 | readableFields: fields, 86 | writableFields: fields, 87 | }); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /src/swagger/index.ts: -------------------------------------------------------------------------------- 1 | export { default as parseSwaggerDocumentation } from "./parseSwaggerDocumentation.js"; 2 | export * from "./parseSwaggerDocumentation.js"; 3 | -------------------------------------------------------------------------------- /src/swagger/parseSwaggerDocumentation.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../Api.js"; 2 | import handleJson, { removeTrailingSlash } from "./handleJson.js"; 3 | import type { OpenAPIV2 } from "openapi-types"; 4 | 5 | export interface ParsedSwaggerDocumentation { 6 | api: Api; 7 | response: OpenAPIV2.Document; 8 | status: number; 9 | } 10 | 11 | export default function parseSwaggerDocumentation( 12 | entrypointUrl: string, 13 | ): Promise { 14 | entrypointUrl = removeTrailingSlash(entrypointUrl); 15 | return fetch(entrypointUrl) 16 | .then((res) => Promise.all([res, res.json()])) 17 | .then( 18 | ([res, response]: [res: Response, response: OpenAPIV2.Document]) => { 19 | const title = response.info.title; 20 | const resources = handleJson(response, entrypointUrl); 21 | 22 | return Promise.resolve({ 23 | api: new Api(entrypointUrl, { title, resources }), 24 | response, 25 | status: res.status, 26 | }); 27 | }, 28 | ([res, response]: [res: Response, response: OpenAPIV2.Document]) => 29 | Promise.reject({ 30 | api: new Api(entrypointUrl, { resources: [] }), 31 | response, 32 | status: res.status, 33 | }), 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Nullable> = { 2 | [P in keyof T]: T[P] | null; 3 | }; 4 | -------------------------------------------------------------------------------- /src/utils/assignSealed.ts: -------------------------------------------------------------------------------- 1 | export function assignSealed< 2 | TSrc extends { [key: string]: any }, 3 | TTarget extends TSrc, 4 | >(target: TTarget, src: TSrc): void { 5 | Object.keys(src).forEach((key) => { 6 | Object.defineProperty(target, key, { 7 | writable: true, 8 | enumerable: true, 9 | configurable: false, 10 | 11 | value: src[key], 12 | }); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/getResources.test.ts: -------------------------------------------------------------------------------- 1 | import getResourcePaths from "./getResources.js"; 2 | 3 | test("should get resource paths", () => { 4 | const paths = { 5 | "/test": {}, 6 | "/test/{id}": {}, 7 | "/test/{id}/subpath": {}, 8 | "/foo": {}, 9 | "/test/bar": {}, 10 | }; 11 | 12 | const resources = getResourcePaths(paths); 13 | expect(resources).toEqual(["/test/{id}"]); 14 | }); 15 | -------------------------------------------------------------------------------- /src/utils/getResources.ts: -------------------------------------------------------------------------------- 1 | import type { OpenAPIV2, OpenAPIV3 } from "openapi-types"; 2 | 3 | const getResources = ( 4 | paths: OpenAPIV2.PathsObject | OpenAPIV3.PathsObject, 5 | ): string[] => 6 | Array.from( 7 | new Set( 8 | Object.keys(paths).filter((path) => { 9 | return new RegExp("^[^{}]+/{[^{}]+}/?$").test(path); 10 | }), 11 | ), 12 | ); 13 | 14 | export default getResources; 15 | -------------------------------------------------------------------------------- /src/utils/parsedJsonReplacer.ts: -------------------------------------------------------------------------------- 1 | interface ResourceValue { 2 | id?: string; 3 | title: string; 4 | } 5 | 6 | type ParsedJsonReplacerResult = ResourceValue | string | null; 7 | 8 | const parsedJsonReplacer = ( 9 | key: string, 10 | value: null | ResourceValue, 11 | ): ParsedJsonReplacerResult => { 12 | if ( 13 | ["reference", "embedded"].includes(key) && 14 | typeof value === "object" && 15 | value !== null 16 | ) { 17 | return `Object ${value.id || value.title}`; 18 | } 19 | 20 | return value; 21 | }; 22 | 23 | export default parsedJsonReplacer; 24 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["jest.config.ts", "jest.setup.ts", "./src"], 4 | "exclude": [] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "outDir": "./lib", 8 | "declaration": true, 9 | "declarationMap": true, 10 | "rootDir": "./src", 11 | "importHelpers": true, 12 | "strict": true, 13 | "esModuleInterop": true 14 | }, 15 | "exclude": [ 16 | "src/**/*.test.ts", 17 | ], 18 | "include": ["./src"] 19 | } 20 | --------------------------------------------------------------------------------