Skip to content

Commit

Permalink
feat(openrpc): start parsing methods in openrpc
Browse files Browse the repository at this point in the history
  • Loading branch information
dsinghvi committed Jan 13, 2025
1 parent 3e7642c commit ed90d82
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 4 deletions.
125 changes: 125 additions & 0 deletions packages/parsers/src/openrpc/1.x/MethodConverter.node.ts
Original file line number Diff line number Diff line change
@@ -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<MethodObject>) {
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,
};
}
}
39 changes: 36 additions & 3 deletions packages/parsers/src/openrpc/1.x/OpenrpcDocumentConverter.node.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<OpenrpcDocument>) {
Expand All @@ -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",
Expand All @@ -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: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
Expand Down
11 changes: 11 additions & 0 deletions packages/parsers/src/openrpc/utils/isReferenceObject.ts
Original file line number Diff line number Diff line change
@@ -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"
);
}
Original file line number Diff line number Diff line change
@@ -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<ContentDescriptorObject>(
contentDescriptor,
document,
{ name: "", schema: { type: "object" } }
);
}
return contentDescriptor;
}
21 changes: 21 additions & 0 deletions packages/parsers/src/openrpc/utils/resolveMethodReference.ts
Original file line number Diff line number Diff line change
@@ -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<MethodObject>(method, document, {
name: "",
params: [],
result: { name: "", schema: { type: "object" } },
});
}
return method;
}
32 changes: 32 additions & 0 deletions packages/parsers/src/openrpc/utils/resolveReference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { OpenrpcDocument, ReferenceObject } from "@open-rpc/meta-schema";
import { isReferenceObject } from "./isReferenceObject";

export function resolveReference<Output>(
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;
}

0 comments on commit ed90d82

Please sign in to comment.