diff --git a/packages/parsers/src/openrpc/1.x/MethodConverter.node.ts b/packages/parsers/src/openrpc/1.x/MethodConverter.node.ts new file mode 100644 index 0000000000..d154f995cb --- /dev/null +++ b/packages/parsers/src/openrpc/1.x/MethodConverter.node.ts @@ -0,0 +1,125 @@ +import { isNonNullish } from "@fern-api/ui-core-utils"; +import { MethodObject } from "@open-rpc/meta-schema"; +import { UnreachableCaseError } from "ts-essentials"; +import { FernRegistry } from "../../client/generated"; +import { SchemaConverterNode } from "../../openapi"; +import { maybeSingleValueToArray } from "../../openapi/utils/maybeSingleValueToArray"; +import { + BaseOpenrpcConverterNode, + BaseOpenrpcConverterNodeConstructorArgs, +} from "../BaseOpenrpcConverter.node"; +import { resolveContentDescriptorObject } from "../utils/resolveContentDescriptorObject"; + +export class MethodConverterNode extends BaseOpenrpcConverterNode< + MethodObject, + FernRegistry.api.latest.EndpointDefinition +> { + private method: MethodObject; + + constructor(args: BaseOpenrpcConverterNodeConstructorArgs) { + super(args); + this.method = args.input; + this.safeParse(); + } + + parse(): void { + // Parse method object + } + + convert(): FernRegistry.api.latest.EndpointDefinition | undefined { + try { + const resolvedResult = this.method.result + ? resolveContentDescriptorObject( + this.method.result, + this.context.openrpc + ) + : undefined; + + const response = resolvedResult + ? new SchemaConverterNode({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + input: resolvedResult.schema as any, + context: this.context, + accessPath: this.accessPath, + pathId: "result", + }).convert() + : undefined; + + // Convert method to HTTP endpoint + // This is a basic implementation that needs to be expanded + return { + id: FernRegistry.EndpointId(this.input.name), + displayName: this.input.name, + method: "POST", + path: [{ type: "literal", value: "" }], + auth: undefined, + pathParameters: [], + queryParameters: [], + requests: undefined, + responses: + response != null + ? [ + this.convertToHttpResponse(response, this.input.description), + ].filter(isNonNullish) + : [], + errors: [], + examples: [], + description: this.input.description, + operationId: this.input.name, + defaultEnvironment: undefined, + environments: [], + availability: undefined, + requestHeaders: [], + responseHeaders: [], + snippetTemplates: undefined, + namespace: [], + }; + } catch (_error) { + this.context.errors.error({ + message: "Failed to convert method", + path: this.accessPath, + }); + return undefined; + } + } + + private convertToHttpResponse( + shape: + | FernRegistry.api.latest.TypeShape + | FernRegistry.api.latest.TypeShape[], + description?: string + ): FernRegistry.api.latest.HttpResponse | undefined { + if (shape == null) { + return undefined; + } + const maybeShapes = maybeSingleValueToArray(shape); + const validShape = maybeShapes + ?.map((shape) => { + const type = shape.type; + switch (type) { + case "alias": + return shape; + case "discriminatedUnion": + case "undiscriminatedUnion": + case "enum": + return undefined; + case "object": + return shape; + default: + new UnreachableCaseError(type); + return undefined; + } + }) + .filter(isNonNullish)[0]; + + if (!validShape) { + return undefined; + } + + return { + statusCode: 200, + body: validShape, + description, + }; + } +} diff --git a/packages/parsers/src/openrpc/1.x/OpenrpcDocumentConverter.node.ts b/packages/parsers/src/openrpc/1.x/OpenrpcDocumentConverter.node.ts index 377ab9b189..154b0059dc 100644 --- a/packages/parsers/src/openrpc/1.x/OpenrpcDocumentConverter.node.ts +++ b/packages/parsers/src/openrpc/1.x/OpenrpcDocumentConverter.node.ts @@ -1,3 +1,4 @@ +import { isNonNullish } from "@fern-api/ui-core-utils"; import { OpenrpcDocument } from "@open-rpc/meta-schema"; import { v4 } from "uuid"; import { FernRegistry } from "../../client/generated"; @@ -6,11 +7,14 @@ import { BaseOpenrpcConverterNode, BaseOpenrpcConverterNodeConstructorArgs, } from "../BaseOpenrpcConverter.node"; +import { resolveMethodReference } from "../utils/resolveMethodReference"; +import { MethodConverterNode } from "./MethodConverter.node"; export class OpenrpcDocumentConverterNode extends BaseOpenrpcConverterNode< OpenrpcDocument, FernRegistry.api.latest.ApiDefinition > { + methods: MethodConverterNode[] = []; components: ComponentsConverterNode | undefined; constructor(args: BaseOpenrpcConverterNodeConstructorArgs) { @@ -19,9 +23,28 @@ export class OpenrpcDocumentConverterNode extends BaseOpenrpcConverterNode< } parse(): void { - if (this.context.document.components != null) { + if (this.input.methods != null) { + for (const method of this.input.methods) { + const resolvedMethod = resolveMethodReference( + method, + this.context.openrpc + ); + if (resolvedMethod == null) { + continue; + } + this.methods.push( + new MethodConverterNode({ + input: resolvedMethod, + context: this.context, + accessPath: this.accessPath, + pathId: "methods", + }) + ); + } + } + if (this.context.openrpc.components != null) { this.components = new ComponentsConverterNode({ - input: this.context.document.components, + input: this.context.openrpc.components, context: this.context, accessPath: this.accessPath, pathId: "components", @@ -33,12 +56,22 @@ export class OpenrpcDocumentConverterNode extends BaseOpenrpcConverterNode< const apiDefinitionId = v4(); const types = this.components?.convert(); + console.log(this.methods?.length); + + const methods = this.methods + ?.map((method) => { + return method.convert(); + }) + .filter(isNonNullish); + return { id: FernRegistry.ApiDefinitionId(apiDefinitionId), types: Object.fromEntries( Object.entries(types ?? {}).map(([id, type]) => [id, type]) ), - endpoints: {}, + endpoints: Object.fromEntries( + methods?.map((method) => [method.id, method]) ?? [] + ), websockets: {}, webhooks: {}, subpackages: {}, diff --git a/packages/parsers/src/openrpc/__test__/__snapshots__/petstore.json b/packages/parsers/src/openrpc/__test__/__snapshots__/petstore.json index ce2e090f5a..7ebdc53030 100644 --- a/packages/parsers/src/openrpc/__test__/__snapshots__/petstore.json +++ b/packages/parsers/src/openrpc/__test__/__snapshots__/petstore.json @@ -80,7 +80,104 @@ } } }, - "endpoints": {}, + "endpoints": { + "list_pets": { + "id": "list_pets", + "displayName": "list_pets", + "method": "POST", + "path": [ + { + "type": "literal", + "value": "" + } + ], + "pathParameters": [], + "queryParameters": [], + "responses": [ + { + "statusCode": 200, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "Pets" + } + } + } + ], + "errors": [], + "examples": [], + "operationId": "list_pets", + "environments": [], + "requestHeaders": [], + "responseHeaders": [], + "namespace": [] + }, + "create_pet": { + "id": "create_pet", + "displayName": "create_pet", + "method": "POST", + "path": [ + { + "type": "literal", + "value": "" + } + ], + "pathParameters": [], + "queryParameters": [], + "responses": [ + { + "statusCode": 200, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "PetId" + } + } + } + ], + "errors": [], + "examples": [], + "operationId": "create_pet", + "environments": [], + "requestHeaders": [], + "responseHeaders": [], + "namespace": [] + }, + "get_pet": { + "id": "get_pet", + "displayName": "get_pet", + "method": "POST", + "path": [ + { + "type": "literal", + "value": "" + } + ], + "pathParameters": [], + "queryParameters": [], + "responses": [ + { + "statusCode": 200, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "Pet" + } + } + } + ], + "errors": [], + "examples": [], + "operationId": "get_pet", + "environments": [], + "requestHeaders": [], + "responseHeaders": [], + "namespace": [] + } + }, "websockets": {}, "webhooks": {}, "subpackages": {}, diff --git a/packages/parsers/src/openrpc/utils/isReferenceObject.ts b/packages/parsers/src/openrpc/utils/isReferenceObject.ts new file mode 100644 index 0000000000..e97180956d --- /dev/null +++ b/packages/parsers/src/openrpc/utils/isReferenceObject.ts @@ -0,0 +1,11 @@ +import { isNonNullish } from "@fern-api/ui-core-utils"; +import { ReferenceObject } from "@open-rpc/meta-schema"; + +export function isReferenceObject(input: unknown): input is ReferenceObject { + return ( + typeof input === "object" && + isNonNullish(input) && + "$ref" in input && + typeof input.$ref === "string" + ); +} diff --git a/packages/parsers/src/openrpc/utils/resolveContentDescriptorObject.ts b/packages/parsers/src/openrpc/utils/resolveContentDescriptorObject.ts new file mode 100644 index 0000000000..8ea46a1260 --- /dev/null +++ b/packages/parsers/src/openrpc/utils/resolveContentDescriptorObject.ts @@ -0,0 +1,21 @@ +import { + ContentDescriptorObject, + OpenrpcDocument, + ReferenceObject, +} from "@open-rpc/meta-schema"; +import { isReferenceObject } from "./isReferenceObject"; +import { resolveReference } from "./resolveReference"; + +export function resolveContentDescriptorObject( + contentDescriptor: ContentDescriptorObject | ReferenceObject | undefined, + document: OpenrpcDocument +): ContentDescriptorObject | undefined { + if (isReferenceObject(contentDescriptor)) { + return resolveReference( + contentDescriptor, + document, + { name: "", schema: { type: "object" } } + ); + } + return contentDescriptor; +} diff --git a/packages/parsers/src/openrpc/utils/resolveMethodReference.ts b/packages/parsers/src/openrpc/utils/resolveMethodReference.ts new file mode 100644 index 0000000000..c96e45c580 --- /dev/null +++ b/packages/parsers/src/openrpc/utils/resolveMethodReference.ts @@ -0,0 +1,21 @@ +import { + MethodObject, + OpenrpcDocument, + ReferenceObject, +} from "@open-rpc/meta-schema"; +import { isReferenceObject } from "./isReferenceObject"; +import { resolveReference } from "./resolveReference"; + +export function resolveMethodReference( + method: MethodObject | ReferenceObject | undefined, + document: OpenrpcDocument +): MethodObject | undefined { + if (isReferenceObject(method)) { + return resolveReference(method, document, { + name: "", + params: [], + result: { name: "", schema: { type: "object" } }, + }); + } + return method; +} diff --git a/packages/parsers/src/openrpc/utils/resolveReference.ts b/packages/parsers/src/openrpc/utils/resolveReference.ts new file mode 100644 index 0000000000..ce170d03a3 --- /dev/null +++ b/packages/parsers/src/openrpc/utils/resolveReference.ts @@ -0,0 +1,32 @@ +import { OpenrpcDocument, ReferenceObject } from "@open-rpc/meta-schema"; +import { isReferenceObject } from "./isReferenceObject"; + +export function resolveReference( + referenceObject: ReferenceObject, + document: OpenrpcDocument, + defaultOutput: Output +): Output { + const keys = referenceObject.$ref + .substring(2) + .split("/") + .map((key) => key.replace(/~1/g, "/")); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let resolvedSchema: any = document; + for (const key of keys) { + if (typeof resolvedSchema !== "object" || resolvedSchema == null) { + return defaultOutput; + } + resolvedSchema = resolvedSchema[key]; + } + if (resolvedSchema == null) { + return defaultOutput; + } + + // If the result is another reference object, make a recursive call + if (isReferenceObject(resolvedSchema)) { + resolvedSchema = resolveReference(resolvedSchema, document, defaultOutput); + } + + return resolvedSchema; +}