diff --git a/package.json b/package.json index 789991d..4c3c032 100644 --- a/package.json +++ b/package.json @@ -52,13 +52,11 @@ "graphql": "^16.0.0", "inflection": "^3.0.0", "jsonld": "^8.3.2", - "jsonref": "^9.0.0", - "lodash.get": "^4.4.0" + "jsonref": "^9.0.0" }, "devDependencies": { "@types/inflection": "^1.13.0", "@types/jsonld": "^1.5.0", - "@types/lodash.get": "^4.4.0", "@types/node": "^22.0.0", "@vitest/coverage-v8": "3.2.2", "globals": "^15.14.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e02052..f8000f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: jsonref: specifier: ^9.0.0 version: 9.0.0 - lodash.get: - specifier: ^4.4.0 - version: 4.4.2 devDependencies: '@types/inflection': specifier: ^1.13.0 @@ -30,9 +27,6 @@ importers: '@types/jsonld': specifier: ^1.5.0 version: 1.5.15 - '@types/lodash.get': - specifier: ^4.4.0 - version: 4.4.9 '@types/node': specifier: ^22.0.0 version: 22.15.29 @@ -487,12 +481,6 @@ packages: '@types/jsonld@1.5.15': resolution: {integrity: sha512-PlAFPZjL+AuGYmwlqwKEL0IMP8M8RexH0NIPGfCVWSQ041H2rR/8OlyZSD7KsCVoN8vCfWdtWDBxX8yBVP+xow==} - '@types/lodash.get@4.4.9': - resolution: {integrity: sha512-J5dvW98sxmGnamqf+/aLP87PYXyrha9xIgc2ZlHl6OHMFR2Ejdxep50QfU0abO1+CH6+ugx+8wEUN1toImAinA==} - - '@types/lodash@4.17.17': - resolution: {integrity: sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==} - '@types/node@22.15.29': resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==} @@ -773,10 +761,6 @@ packages: resolution: {integrity: sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==} engines: {node: '>=14.16'} - lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. - loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} @@ -1438,12 +1422,6 @@ snapshots: '@types/jsonld@1.5.15': {} - '@types/lodash.get@4.4.9': - dependencies: - '@types/lodash': 4.17.17 - - '@types/lodash@4.17.17': {} - '@types/node@22.15.29': dependencies: undici-types: 6.21.0 @@ -1735,8 +1713,6 @@ snapshots: ky@0.33.3: {} - lodash.get@4.4.2: {} - loupe@3.1.3: {} lru-cache@10.4.3: {} diff --git a/src/hydra/fetchJsonLd.ts b/src/hydra/fetchJsonLd.ts index d972dcf..90b453d 100644 --- a/src/hydra/fetchJsonLd.ts +++ b/src/hydra/fetchJsonLd.ts @@ -1,5 +1,5 @@ import type { Document, JsonLd, RemoteDocument } from "jsonld/jsonld-spec.js"; -import type { RequestInitExtended } from "./types.js"; +import type { RequestInitExtended } from "../types.js"; const jsonLdMimeType = "application/ld+json"; const jsonProblemMimeType = "application/problem+json"; diff --git a/src/hydra/fetchResource.ts b/src/hydra/fetchResource.ts index ba23a8c..edf0d8c 100644 --- a/src/hydra/fetchResource.ts +++ b/src/hydra/fetchResource.ts @@ -1,5 +1,6 @@ import fetchJsonLd from "./fetchJsonLd.js"; -import type { IriTemplateMapping, RequestInitExtended } from "./types.js"; +import type { IriTemplateMapping } from "./types.js"; +import type { RequestInitExtended } from "../types.js"; export default async function fetchResource( resourceUrl: string, diff --git a/src/hydra/getParameters.ts b/src/hydra/getParameters.ts index 35e8d68..eee599c 100644 --- a/src/hydra/getParameters.ts +++ b/src/hydra/getParameters.ts @@ -1,7 +1,7 @@ import { Parameter } from "../Parameter.js"; import fetchResource from "./fetchResource.js"; import type { Resource } from "../Resource.js"; -import type { RequestInitExtended } from "./types.js"; +import type { RequestInitExtended } from "../types.js"; export default async function getParameters( resource: Resource, diff --git a/src/hydra/parseHydraDocumentation.ts b/src/hydra/parseHydraDocumentation.ts index c7f4dc3..321097d 100644 --- a/src/hydra/parseHydraDocumentation.ts +++ b/src/hydra/parseHydraDocumentation.ts @@ -1,5 +1,4 @@ import jsonld from "jsonld"; -import get from "lodash.get"; import { Api } from "../Api.js"; import { Field } from "../Field.js"; import { Resource } from "../Resource.js"; @@ -15,8 +14,8 @@ import type { Entrypoint, ExpandedOperation, ExpandedRdfProperty, - RequestInitExtended, } from "./types.js"; +import type { RequestInitExtended } from "../types.js"; /** * Extracts the short name of a resource. @@ -45,10 +44,8 @@ function findSupportedClass( docs: ExpandedDoc[], classToFind: string, ): ExpandedClass { - const supportedClasses = get( - docs, - '[0]["http://www.w3.org/ns/hydra/core#supportedClass"]', - ) as ExpandedClass[] | undefined; + const supportedClasses = + docs?.[0]?.["http://www.w3.org/ns/hydra/core#supportedClass"]; if (!Array.isArray(supportedClasses)) { throw new TypeError( 'The API documentation has no "http://www.w3.org/ns/hydra/core#supportedClass" key or its value is not an array.', @@ -155,7 +152,7 @@ async function fetchEntrypointAndDocs( api: new Api(entrypointUrl, { resources: [] }), error, response, - status: get(response, "status"), + status: response?.status, }; } } @@ -173,25 +170,30 @@ function findRelatedClass( property: ExpandedRdfProperty, ): ExpandedClass { // Use the entrypoint property's owl:equivalentClass if available - if (Array.isArray(property["http://www.w3.org/2000/01/rdf-schema#range"])) { - for (const range of property[ - "http://www.w3.org/2000/01/rdf-schema#range" - ]) { - const onProperty = get( - range, - '["http://www.w3.org/2002/07/owl#equivalentClass"][0]["http://www.w3.org/2002/07/owl#onProperty"][0]["@id"]', - ) as unknown as string; - const allValuesFrom = get( - range, - '["http://www.w3.org/2002/07/owl#equivalentClass"][0]["http://www.w3.org/2002/07/owl#allValuesFrom"][0]["@id"]', - ) as unknown as string; - if ( - allValuesFrom && - onProperty === "http://www.w3.org/ns/hydra/core#member" - ) { - return findSupportedClass(docs, allValuesFrom); - } + for (const range of property["http://www.w3.org/2000/01/rdf-schema#range"] ?? + []) { + const equivalentClass = + "http://www.w3.org/2002/07/owl#equivalentClass" in range + ? range?.["http://www.w3.org/2002/07/owl#equivalentClass"]?.[0] + : undefined; + + if (!equivalentClass) { + continue; + } + + const onProperty = + equivalentClass["http://www.w3.org/2002/07/owl#onProperty"]?.[0]?.["@id"]; + const allValuesFrom = + equivalentClass["http://www.w3.org/2002/07/owl#allValuesFrom"]?.[0]?.[ + "@id" + ]; + + if ( + allValuesFrom && + onProperty === "http://www.w3.org/ns/hydra/core#member" + ) { + return findSupportedClass(docs, allValuesFrom); } } @@ -205,10 +207,10 @@ function findRelatedClass( continue; } - const returns = get( - entrypointSupportedOperation, - '["http://www.w3.org/ns/hydra/core#returns"][0]["@id"]', - ) as string | undefined; + const returns = + entrypointSupportedOperation?.[ + "http://www.w3.org/ns/hydra/core#returns" + ]?.[0]?.["@id"]; if ( typeof returns === "string" && returns.indexOf("http://www.w3.org/ns/hydra/core") !== 0 @@ -241,15 +243,11 @@ export default async function parseHydraDocumentation( const resources = [], fields = [], operations = []; - const title = get( - docs, - '[0]["http://www.w3.org/ns/hydra/core#title"][0]["@value"]', - "API Platform", - ) as string; - - const entrypointType = get(entrypoint, '[0]["@type"][0]') as - | string - | undefined; + const title = + docs?.[0]?.["http://www.w3.org/ns/hydra/core#title"]?.[0]?.["@value"] ?? + "API Platform"; + + const entrypointType = entrypoint?.[0]?.["@type"]?.[0]; if (!entrypointType) { throw new Error('The API entrypoint has no "@type" key.'); } @@ -274,23 +272,25 @@ export default async function parseHydraDocumentation( writableFields = [], resourceOperations = []; - const property = get( - properties, - '["http://www.w3.org/ns/hydra/core#property"][0]', - ) as ExpandedRdfProperty | undefined; + const property = + properties?.["http://www.w3.org/ns/hydra/core#property"]?.[0]; + const propertyIri = property?.["@id"]; - if (!property) { + if (!property || !propertyIri) { continue; } - const url = get(entrypoint, `[0]["${property["@id"]}"][0]["@id"]`) as - | string - | undefined; + const resourceProperty = entrypoint?.[0]?.[propertyIri]?.[0]; + + const url = + typeof resourceProperty === "object" + ? resourceProperty["@id"] + : undefined; if (!url) { console.error( new Error( - `Unable to find the URL for "${property["@id"]}" in the entrypoint, make sure your API resource has at least one GET collection operation declared.`, + `Unable to find the URL for "${propertyIri}" in the entrypoint, make sure your API resource has at least one GET collection operation declared.`, ), ); continue; @@ -298,19 +298,16 @@ export default async function parseHydraDocumentation( // Add fields const relatedClass = findRelatedClass(docs, property); - for (const supportedProperties of relatedClass[ + for (const supportedProperties of relatedClass?.[ "http://www.w3.org/ns/hydra/core#supportedProperty" - ]) { - const supportedProperty = get( - supportedProperties, - '["http://www.w3.org/ns/hydra/core#property"][0]', - ) as unknown as ExpandedRdfProperty; + ] ?? []) { + const supportedProperty = + supportedProperties?.["http://www.w3.org/ns/hydra/core#property"]?.[0]; const id = supportedProperty?.["@id"]; - const range = get( - supportedProperty, - '["http://www.w3.org/2000/01/rdf-schema#range"][0]["@id"]', - null, - ) as unknown as string; + const range = + supportedProperty?.[ + "http://www.w3.org/2000/01/rdf-schema#range" + ]?.[0]?.["@id"] ?? null; const field = new Field( supportedProperties?.["http://www.w3.org/ns/hydra/core#title"]?.[0]?.[ @@ -324,35 +321,31 @@ export default async function parseHydraDocumentation( range, type: getType(id, range), reference: - get(supportedProperty, '["@type"][0]') === + supportedProperty?.["@type"]?.[0] === "http://www.w3.org/ns/hydra/core#Link" ? range // Will be updated in a subsequent pass : null, embedded: - get(supportedProperty, '["@type"][0]') === + supportedProperty?.["@type"]?.[0] === "http://www.w3.org/ns/hydra/core#Link" ? null : (range as unknown as Resource), // Will be updated in a subsequent pass - required: get( - supportedProperties, - '["http://www.w3.org/ns/hydra/core#required"][0]["@value"]', - false, - ) as boolean, - description: get( - supportedProperties, - '["http://www.w3.org/ns/hydra/core#description"][0]["@value"]', - "", - ) as string, - maxCardinality: get( - supportedProperty, - '["http://www.w3.org/2002/07/owl#maxCardinality"][0]["@value"]', - null, - ) as number | null, - deprecated: get( - supportedProperties, - '["http://www.w3.org/2002/07/owl#deprecated"][0]["@value"]', - false, - ) as boolean, + required: + supportedProperties?.[ + "http://www.w3.org/ns/hydra/core#required" + ]?.[0]?.["@value"] ?? false, + description: + supportedProperties?.[ + "http://www.w3.org/ns/hydra/core#description" + ]?.[0]?.["@value"] ?? "", + maxCardinality: + supportedProperty?.[ + "http://www.w3.org/2002/07/owl#maxCardinality" + ]?.[0]?.["@value"] ?? null, + deprecated: + supportedProperties?.[ + "http://www.w3.org/2002/07/owl#deprecated" + ]?.[0]?.["@value"] ?? false, }, ); @@ -360,23 +353,20 @@ export default async function parseHydraDocumentation( resourceFields.push(field); if ( - get( - supportedProperties, - '["http://www.w3.org/ns/hydra/core#readable"][0]["@value"]', - ) + supportedProperties?.[ + "http://www.w3.org/ns/hydra/core#readable" + ]?.[0]?.["@value"] ) { readableFields.push(field); } if ( - get( - supportedProperties, - '["http://www.w3.org/ns/hydra/core#writeable"][0]["@value"]', - ) || - get( - supportedProperties, - '["http://www.w3.org/ns/hydra/core#writable"][0]["@value"]', - ) + supportedProperties?.[ + "http://www.w3.org/ns/hydra/core#writeable" + ]?.[0]?.["@value"] || + supportedProperties?.[ + "http://www.w3.org/ns/hydra/core#writable" + ]?.[0]?.["@value"] ) { writableFields.push(field); } @@ -414,11 +404,10 @@ export default async function parseHydraDocumentation( ]?.[0]?.["@id"], returns: range, types: entrypointOperation["@type"], - deprecated: get( - entrypointOperation, - '["http://www.w3.org/2002/07/owl#deprecated"][0]["@value"]', - false, - ) as boolean, + deprecated: + entrypointOperation?.[ + "http://www.w3.org/2002/07/owl#deprecated" + ]?.[0]?.["@value"] ?? false, }, ); @@ -464,11 +453,10 @@ export default async function parseHydraDocumentation( ]?.[0]?.["@id"], returns: range, types: supportedOperation["@type"], - deprecated: get( - supportedOperation, - '["http://www.w3.org/2002/07/owl#deprecated"][0]["@value"]', - false, - ) as boolean, + deprecated: + supportedOperation?.[ + "http://www.w3.org/2002/07/owl#deprecated" + ]?.[0]?.["@value"] ?? false, }, ); @@ -478,20 +466,18 @@ export default async function parseHydraDocumentation( const resource = new Resource(guessNameFromUrl(url, entrypointUrl), url, { id: relatedClass["@id"], - title: get( - relatedClass, - '["http://www.w3.org/ns/hydra/core#title"][0]["@value"]', - "", - ) as string, + title: + relatedClass?.["http://www.w3.org/ns/hydra/core#title"]?.[0]?.[ + "@value" + ] ?? "", fields: resourceFields, readableFields, writableFields, operations: resourceOperations, - deprecated: get( - relatedClass, - '["http://www.w3.org/2002/07/owl#deprecated"][0]["@value"]', - false, - ) as boolean, + deprecated: + relatedClass?.["http://www.w3.org/2002/07/owl#deprecated"]?.[0]?.[ + "@value" + ] ?? false, }); resource.parameters = []; diff --git a/src/hydra/types.ts b/src/hydra/types.ts index 6bba55f..41150a8 100644 --- a/src/hydra/types.ts +++ b/src/hydra/types.ts @@ -1,7 +1,3 @@ -export interface RequestInitExtended extends Omit { - headers?: HeadersInit | (() => HeadersInit); -} - export interface IriTemplateMapping { "@type": "IriTemplateMapping"; variable: "string"; diff --git a/src/openapi3/dereferencedOpenApiv3.ts b/src/openapi3/dereferencedOpenApiv3.ts new file mode 100644 index 0000000..e0cd32a --- /dev/null +++ b/src/openapi3/dereferencedOpenApiv3.ts @@ -0,0 +1,156 @@ +// oxlint-disable consistent-indexed-object-style +import type { OpenAPIV3 } from "openapi-types"; + +interface ArraySchemaObjectDereferenced extends BaseSchemaObjectDereferenced { + type: OpenAPIV3.ArraySchemaObjectType; + items: SchemaObjectDereferenced; +} + +interface NonArraySchemaObjectDereferenced + extends BaseSchemaObjectDereferenced { + type?: OpenAPIV3.NonArraySchemaObjectType; +} + +export type SchemaObjectDereferenced = + | ArraySchemaObjectDereferenced + | NonArraySchemaObjectDereferenced; + +type BaseSchemaObjectDereferenced = Omit< + OpenAPIV3.BaseSchemaObject, + "additionalProperties" | "properties" | "allOf" | "oneOf" | "anyOf" | "not" +> & { + additionalProperties?: boolean | SchemaObjectDereferenced; + properties?: { + [name: string]: SchemaObjectDereferenced; + }; + allOf?: SchemaObjectDereferenced[]; + oneOf?: SchemaObjectDereferenced[]; + anyOf?: SchemaObjectDereferenced[]; + not?: SchemaObjectDereferenced; +}; + +type EncodingObjectDereferenced = Omit & { + headers?: { + [header: string]: HeaderObjectDereferenced; + }; +}; + +type MediaTypeObjectDereferenced = Omit< + OpenAPIV3.MediaTypeObject, + "schema" | "encoding" +> & { + schema?: SchemaObjectDereferenced; + encoding?: { + [media: string]: EncodingObjectDereferenced; + }; +}; + +type ParameterBaseObjectDereferenced = Omit< + OpenAPIV3.ParameterObject, + "schema" | "content" +> & { + schema?: SchemaObjectDereferenced; + content?: { + [media: string]: MediaTypeObjectDereferenced; + }; +}; + +interface HeaderObjectDereferenced extends ParameterBaseObjectDereferenced {} + +type RequestBodyObjectDereferenced = Omit< + OpenAPIV3.RequestBodyObject, + "content" +> & { + content: { + [media: string]: MediaTypeObjectDereferenced; + }; +}; + +type ResponseObjectDereferenced = Omit< + OpenAPIV3.ResponseObject, + "headers" | "content" +> & { + headers?: { + [header: string]: HeaderObjectDereferenced; + }; + content?: { + [media: string]: MediaTypeObjectDereferenced; + }; +}; + +interface ResponsesObjectDereferenced { + [code: string]: ResponseObjectDereferenced; +} + +interface ParameterObjectDereferenced extends ParameterBaseObjectDereferenced { + name: string; + in: string; +} + +type PathItemObjectDereferenced = Omit< + OpenAPIV3.PathItemObject, + "parameters" | `${OpenAPIV3.HttpMethods}` +> & { + parameters?: ParameterObjectDereferenced[]; +} & { + [method in OpenAPIV3.HttpMethods]?: OperationObjectDereferenced; +}; +interface CallbackObjectDereferenced { + [url: string]: PathItemObjectDereferenced; +} + +export type OperationObjectDereferenced = Omit< + OpenAPIV3.OperationObject, + "parameters" | "requestBody" | "responses" | "callbacks" +> & { + parameters?: ParameterObjectDereferenced[]; + requestBody?: RequestBodyObjectDereferenced; + responses: ResponsesObjectDereferenced; + callbacks?: { + [callback: string]: CallbackObjectDereferenced; + }; +} & T; + +interface PathsObjectDereferenced< + T extends object = object, + P extends object = object, +> { + [pattern: string]: (PathItemObjectDereferenced & P) | undefined; +} + +type ComponentsObjectDereferenced = Omit< + OpenAPIV3.ComponentsObject, + | "schemas" + | "responses" + | "parameters" + | "requestBodies" + | "headers" + | "callbacks" +> & { + schemas?: { + [key: string]: SchemaObjectDereferenced; + }; + responses?: { + [key: string]: ResponseObjectDereferenced; + }; + parameters?: { + [key: string]: ParameterObjectDereferenced; + }; + requestBodies?: { + [key: string]: RequestBodyObjectDereferenced; + }; + headers?: { + [key: string]: HeaderObjectDereferenced; + }; + callbacks?: { + [key: string]: CallbackObjectDereferenced; + }; +}; + +export type OpenAPIV3DocumentDereferenced = Omit< + OpenAPIV3.Document, + "paths" | "components" +> & { + paths: PathsObjectDereferenced; + components?: ComponentsObjectDereferenced; +}; diff --git a/src/openapi3/handleJson.ts b/src/openapi3/handleJson.ts index 2dfdcbc..388f45b 100644 --- a/src/openapi3/handleJson.ts +++ b/src/openapi3/handleJson.ts @@ -1,34 +1,23 @@ -import { parse as dereference } from "jsonref"; -import get from "lodash.get"; import inflection from "inflection"; +import type { ParseOptions } from "jsonref"; +import { parse } from "jsonref"; +import type { OpenAPIV3 } from "openapi-types"; import { Field } from "../Field.js"; +import type { OperationType } from "../Operation.js"; import { Operation } from "../Operation.js"; import { Parameter } from "../Parameter.js"; import { Resource } from "../Resource.js"; import getResourcePaths from "../utils/getResources.js"; +import type { + OpenAPIV3DocumentDereferenced, + OperationObjectDereferenced, + SchemaObjectDereferenced, +} from "./dereferencedOpenApiv3.js"; import getType from "./getType.js"; -import type { OpenAPIV3 } from "openapi-types"; -import type { OperationType } from "../Operation.js"; - -function isParameter( - maybeParameter: OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject, -): maybeParameter is OpenAPIV3.ParameterObject { - return ( - maybeParameter !== undefined && - "name" in maybeParameter && - "in" in maybeParameter - ); -} - -function isSchema( - maybeSchema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined, -): maybeSchema is OpenAPIV3.SchemaObject { - return maybeSchema !== undefined && !("$ref" in maybeSchema); -} export function removeTrailingSlash(url: string): string { if (url.endsWith("/")) { - url = url.slice(0, -1); + return url.slice(0, -1); } return url; } @@ -57,45 +46,47 @@ function mergeResources(resourceA: Resource, resourceB: Resource) { return resourceA; } +function buildEnumObject(enumArray: SchemaObjectDereferenced["enum"]) { + if (!enumArray) { + return null; + } + return Object.fromEntries( + // Object.values is used because the array is annotated: it contains the __meta symbol used by jsonref. + Object.values(enumArray).map((enumValue) => [ + typeof enumValue === "string" + ? inflection.humanize(enumValue) + : enumValue, + enumValue, + ]), + ); +} + +function getArrayType(property: SchemaObjectDereferenced) { + if (property.type !== "array") { + return null; + } + return getType(property.items.type || "string", property.items.format); +} + function buildResourceFromSchema( - schema: OpenAPIV3.SchemaObject, + schema: SchemaObjectDereferenced, name: string, title: string, url: string, ) { const { description = "", properties = {} } = schema; - const fieldNames = Object.keys(properties); const requiredFields = schema.required || []; - + const fields: Field[] = []; const readableFields: Field[] = []; const writableFields: Field[] = []; - const fields = fieldNames.map((fieldName) => { - const property = properties[fieldName] as OpenAPIV3.SchemaObject; - - const type = getType(property.type || "string", property.format); + for (const [fieldName, property] of Object.entries(properties)) { const field = new Field(fieldName, { id: null, range: null, - type, - arrayType: - type === "array" && "items" in property - ? getType( - (property.items as OpenAPIV3.SchemaObject).type || "string", - (property.items as OpenAPIV3.SchemaObject).format, - ) - : null, - enum: property.enum - ? Object.fromEntries( - // Object.values is used because the array is annotated: it contains the __meta symbol used by jsonref. - Object.values(property.enum).map((enumValue) => [ - typeof enumValue === "string" - ? inflection.humanize(enumValue) - : enumValue, - enumValue, - ]), - ) - : null, + type: getType(property.type || "string", property.format), + arrayType: getArrayType(property), + enum: buildEnumObject(property.enum), reference: null, embedded: null, nullable: property.nullable || false, @@ -109,9 +100,8 @@ function buildResourceFromSchema( if (!property.readOnly) { writableFields.push(field); } - - return field; - }); + fields.push(field); + } return new Resource(name, url, { id: null, @@ -129,7 +119,7 @@ function buildResourceFromSchema( function buildOperationFromPathItem( httpMethod: `${OpenAPIV3.HttpMethods}`, operationType: OperationType, - pathItem: OpenAPIV3.OperationObject, + pathItem: OperationObjectDereferenced, ): Operation { return new Operation(pathItem.summary || operationType, operationType, { method: httpMethod.toUpperCase(), @@ -137,6 +127,13 @@ function buildOperationFromPathItem( }); } +function dereferenceOpenAPIV3( + response: OpenAPIV3.Document, + options: ParseOptions, +): Promise { + return parse(response, options); +} + /* Assumptions: RESTful APIs typically have two paths per resources: a `/noun` path and a @@ -151,9 +148,9 @@ export default async function handleJson( response: OpenAPIV3.Document, entrypointUrl: string, ): Promise { - const document = (await dereference(response, { + const document = await dereferenceOpenAPIV3(response, { scope: entrypointUrl, - })) as OpenAPIV3.Document; + }); const paths = getResourcePaths(document.paths); @@ -183,20 +180,12 @@ export default async function handleJson( if (!showOperation && !editOperation) { continue; } + const showSchema = + showOperation?.responses?.["200"]?.content?.["application/json"] + ?.schema || document.components?.schemas?.[title]; - const showSchema = showOperation - ? (get( - showOperation, - "responses.200.content.application/json.schema", - get(document, `components.schemas[${title}]`), - ) as OpenAPIV3.SchemaObject) - : null; - const editSchema = editOperation - ? (get( - editOperation, - "requestBody.content.application/json.schema", - ) as unknown as OpenAPIV3.SchemaObject) - : null; + const editSchema = + editOperation?.requestBody?.content?.["application/json"]?.schema || null; if (!showSchema && !editSchema) { continue; @@ -244,22 +233,16 @@ export default async function handleJson( ]; if (listOperation && listOperation.parameters) { - resource.parameters = listOperation.parameters - .filter(isParameter) - .map( - (parameter) => - new Parameter( - parameter.name, - parameter.schema && - isSchema(parameter.schema) && - parameter.schema.type - ? getType(parameter.schema.type) - : null, - parameter.required || false, - parameter.description || "", - parameter.deprecated, - ), - ); + resource.parameters = listOperation.parameters.map( + (parameter) => + new Parameter( + parameter.name, + parameter.schema?.type ? getType(parameter.schema.type) : null, + parameter.required || false, + parameter.description || "", + parameter.deprecated, + ), + ); } resources.push(resource); diff --git a/src/openapi3/parseOpenApi3Documentation.ts b/src/openapi3/parseOpenApi3Documentation.ts index c92adca..1311ce7 100644 --- a/src/openapi3/parseOpenApi3Documentation.ts +++ b/src/openapi3/parseOpenApi3Documentation.ts @@ -2,7 +2,7 @@ import { Api } from "../Api.js"; import handleJson, { removeTrailingSlash } from "./handleJson.js"; import type { OpenAPIV3 } from "openapi-types"; -import type { RequestInitExtended } from "./types.js"; +import type { RequestInitExtended } from "../types.js"; export interface ParsedOpenApi3Documentation { api: Api; diff --git a/src/openapi3/types.ts b/src/openapi3/types.ts deleted file mode 100644 index 17ba138..0000000 --- a/src/openapi3/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface RequestInitExtended extends Omit { - headers?: HeadersInit | (() => HeadersInit); -} diff --git a/src/swagger/handleJson.ts b/src/swagger/handleJson.ts index 761219a..23b540d 100644 --- a/src/swagger/handleJson.ts +++ b/src/swagger/handleJson.ts @@ -1,4 +1,3 @@ -import get from "lodash.get"; import inflection from "inflection"; import { Field } from "../Field.js"; import { Resource } from "../Resource.js"; @@ -8,7 +7,7 @@ import type { OpenAPIV2 } from "openapi-types"; export function removeTrailingSlash(url: string): string { if (url.endsWith("/")) { - url = url.slice(0, -1); + return url.slice(0, -1); } return url; } @@ -41,31 +40,23 @@ export default function handleJson( throw new Error(); // @TODO } - const description = definition.description || ""; - const properties = definition.properties; + const { description = "", properties } = definition; if (!properties) { throw new Error(); // @TODO } - const fieldNames = Object.keys(properties); - const requiredFields = get( - response, - ["definitions", title, "required"], - [], - ) as string[]; - - const fields = fieldNames.map((fieldName) => { - const property = properties[fieldName]; + const requiredFields = response.definitions?.[title]?.required ?? []; + const fields = Object.entries(properties).map(([fieldName, property]) => { return new Field(fieldName, { id: null, range: null, type: getType( - get(property, "type", "") as string, - get(property, "format", "") as string, + typeof property?.type === "string" ? property.type : "", + property?.["format"] ?? "", ), - enum: property?.enum + enum: property.enum ? Object.fromEntries( property.enum.map((enumValue: string | number) => [ typeof enumValue === "string" @@ -78,7 +69,7 @@ export default function handleJson( reference: null, embedded: null, required: requiredFields.some((value) => value === fieldName), - description: property?.description || "", + description: property.description || "", }); }); diff --git a/src/types.ts b/src/types.ts index a840fcc..257db4f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,7 @@ export type Nullable> = { [P in keyof T]: T[P] | null; }; + +export interface RequestInitExtended extends Omit { + headers?: HeadersInit | (() => HeadersInit); +}