From cf428608e83a8078dbd5a080dd3a3a6281affa46 Mon Sep 17 00:00:00 2001 From: Rohin Bhargava Date: Fri, 15 Nov 2024 12:55:23 -0500 Subject: [PATCH] add primitive and objects --- packages/parsers/__test__/demo.test.ts | 2 +- .../{openapi.node.ts => openApi3_1.node.ts} | 0 .../parsers/openapi/base.node.interface.ts | 49 ++++++++ packages/parsers/openapi/demo.ts | 37 +++--- .../shared/interfaces/api.node.interface.ts | 33 ------ .../interfaces/container.node.interface.ts | 5 - .../shared/interfaces/fdr.stage.interface.ts | 3 - .../interfaces/primitive.node.interface.ts | 3 - .../openapi/shared/nodes/object.node.ts | 40 +++++++ .../shared/nodes/objectProperty.node.ts | 47 ++++++++ .../openapi/shared/nodes/pathPart.node.ts | 29 ----- .../shared/nodes/primitives/enum.node.ts | 39 +++++++ .../shared/nodes/primitives/number.node.ts | 60 ++++++++++ .../nodes/primitives/number/float.node.ts | 59 ++++++++++ .../nodes/primitives/number/integer.node.ts | 58 ++++++++++ .../shared/nodes/primitives/string.node.ts | 105 ++++++++++++++++++ .../nodes/primitives/types/fdr.types.ts | 15 +++ .../nodes/primitives/types/format.types.ts | 48 ++++++++ .../openapi/shared/nodes/schema.node.ts | 39 +++++++ .../openapi/shared/nodes/string.node.ts | 43 ------- .../shared/nodes/typeReference.node.ts | 55 +++++++++ .../openapi/shared/nodes/typeShape.node.ts | 45 ++++++++ .../parsers/openapi/shared/openapi.types.ts | 3 + .../{nodes => temporary}/authScheme.node.ts | 0 .../availability.node.ts | 0 .../{nodes => temporary}/endpoint.node.ts | 0 .../shared/{nodes => temporary}/path.node.ts | 0 .../openapi/shared/temporary/pathPart.node.ts | 29 +++++ .../{ => temporary}/stages/fdr/api.stage.ts | 0 .../{nodes => temporary}/webhook.node.ts | 0 .../{nodes => temporary}/websocket.node.ts | 0 packages/parsers/package.json | 1 + pnpm-lock.yaml | 3 + 33 files changed, 712 insertions(+), 138 deletions(-) rename packages/parsers/openapi/3.1/{openapi.node.ts => openApi3_1.node.ts} (100%) create mode 100644 packages/parsers/openapi/base.node.interface.ts delete mode 100644 packages/parsers/openapi/shared/interfaces/api.node.interface.ts delete mode 100644 packages/parsers/openapi/shared/interfaces/container.node.interface.ts delete mode 100644 packages/parsers/openapi/shared/interfaces/fdr.stage.interface.ts delete mode 100644 packages/parsers/openapi/shared/interfaces/primitive.node.interface.ts create mode 100644 packages/parsers/openapi/shared/nodes/object.node.ts create mode 100644 packages/parsers/openapi/shared/nodes/objectProperty.node.ts delete mode 100644 packages/parsers/openapi/shared/nodes/pathPart.node.ts create mode 100644 packages/parsers/openapi/shared/nodes/primitives/enum.node.ts create mode 100644 packages/parsers/openapi/shared/nodes/primitives/number.node.ts create mode 100644 packages/parsers/openapi/shared/nodes/primitives/number/float.node.ts create mode 100644 packages/parsers/openapi/shared/nodes/primitives/number/integer.node.ts create mode 100644 packages/parsers/openapi/shared/nodes/primitives/string.node.ts create mode 100644 packages/parsers/openapi/shared/nodes/primitives/types/fdr.types.ts create mode 100644 packages/parsers/openapi/shared/nodes/primitives/types/format.types.ts create mode 100644 packages/parsers/openapi/shared/nodes/schema.node.ts delete mode 100644 packages/parsers/openapi/shared/nodes/string.node.ts create mode 100644 packages/parsers/openapi/shared/nodes/typeReference.node.ts create mode 100644 packages/parsers/openapi/shared/nodes/typeShape.node.ts create mode 100644 packages/parsers/openapi/shared/openapi.types.ts rename packages/parsers/openapi/shared/{nodes => temporary}/authScheme.node.ts (100%) rename packages/parsers/openapi/shared/{composable => temporary}/availability.node.ts (100%) rename packages/parsers/openapi/shared/{nodes => temporary}/endpoint.node.ts (100%) rename packages/parsers/openapi/shared/{nodes => temporary}/path.node.ts (100%) create mode 100644 packages/parsers/openapi/shared/temporary/pathPart.node.ts rename packages/parsers/openapi/shared/{ => temporary}/stages/fdr/api.stage.ts (100%) rename packages/parsers/openapi/shared/{nodes => temporary}/webhook.node.ts (100%) rename packages/parsers/openapi/shared/{nodes => temporary}/websocket.node.ts (100%) diff --git a/packages/parsers/__test__/demo.test.ts b/packages/parsers/__test__/demo.test.ts index a9e49ae30e..8fea9bca9d 100644 --- a/packages/parsers/__test__/demo.test.ts +++ b/packages/parsers/__test__/demo.test.ts @@ -5,7 +5,7 @@ import { ComponentsNode } from "../openapi/demo"; import { FdrAPI } from "@fern-api/fdr-sdk"; import { OpenAPIV3_1 } from "openapi-types"; import { describe, it } from "vitest"; -import { ApiNodeContext, ErrorCollector } from "../openapi/shared/interfaces/api.node.interface"; +import { ApiNodeContext, ErrorCollector } from "../openapi/base.node.interface"; describe("ComponentsNode", () => { describe("outputFdrShape", () => { diff --git a/packages/parsers/openapi/3.1/openapi.node.ts b/packages/parsers/openapi/3.1/openApi3_1.node.ts similarity index 100% rename from packages/parsers/openapi/3.1/openapi.node.ts rename to packages/parsers/openapi/3.1/openApi3_1.node.ts diff --git a/packages/parsers/openapi/base.node.interface.ts b/packages/parsers/openapi/base.node.interface.ts new file mode 100644 index 0000000000..129b741a87 --- /dev/null +++ b/packages/parsers/openapi/base.node.interface.ts @@ -0,0 +1,49 @@ +import { Logger } from "@playwright/test"; + +export class ErrorCollector { + errors: { + message: string; + path: string; + }[] = []; + + addError(error: string, accessPath: string[], pathId?: string): void { + this.errors.push({ + message: error, + path: `#/${accessPath.join("/")}${pathId ? `/${pathId}` : ""}`, + }); + } +} + +export interface ApiNodeContext { + orgId: string; + apiId: string; + logger: Logger; + errorCollector: ErrorCollector; +} + +abstract class ApiNode { + context: ApiNodeContext; + input: InputShape; + + accessPath: string[]; + + constructor(context: ApiNodeContext, input: InputShape, accessPath: string[]) { + this.context = context; + this.input = input; + this.accessPath = accessPath; + } + + abstract outputFdrShape: () => FdrShape | undefined; +} + +export abstract class InputApiNode extends ApiNode { + constructor(context: ApiNodeContext, input: InputShape, accessPath: string[], pathId?: string) { + if (pathId) { + accessPath.push(pathId); + context.logger.log("a", "info", `Processing #/${accessPath.join("/")}/${pathId}`); + } + super(context, input, accessPath); + } +} + +export abstract class OutputApiNode extends ApiNode {} diff --git a/packages/parsers/openapi/demo.ts b/packages/parsers/openapi/demo.ts index 6902c2b7d3..04b9efa31a 100644 --- a/packages/parsers/openapi/demo.ts +++ b/packages/parsers/openapi/demo.ts @@ -1,22 +1,17 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; import { OpenAPIV3_1 } from "openapi-types"; import { z } from "zod"; -import { ApiNode, ApiNodeContext } from "./shared/interfaces/api.node.interface"; +import { ApiNodeContext, InputApiNode } from "./base.node.interface"; import { FdrStage } from "./shared/interfaces/fdr.stage.interface"; -export class AvailabilityNode implements ApiNode { +export class AvailabilityNode implements InputApiNode { id: string; availability: FdrAPI.Availability | undefined; "x-fern-availability-shape" = z.object({ - "x-fern-availability": z.enum([ - "beta", - "deprecated", - "development", - "pre-release", - "stable", - "generally-available", - ]), + "x-fern-availability": z.optional( + z.enum(["beta", "deprecated", "development", "pre-release", "stable", "generally-available"]), + ), }); deprecatedShape = z.object({ @@ -26,13 +21,13 @@ export class AvailabilityNode implements ApiNode[], + readonly accessPath: InputApiNode[], ) { this.id = `${accessPath.map((node) => node.id).join(".")}.AvailabilityNode`; if (input && typeof input === "object" && "x-fern-availability" in input) { const result = this["x-fern-availability-shape"].safeParse(input); if (result.success) { - this.availability = this.convertAvailability(result.data["x-fern-availability"]); + this.availability = this.convertAvailability(result.data["x-fern-availability"] ?? ""); } else { context.errorCollector.addError(`Availability is not defined for ${this.id}`); } @@ -74,7 +69,7 @@ export class AvailabilityNode implements ApiNode { +export class DemoStringNode implements InputApiNode { id: string; regex: string | undefined; @@ -86,7 +81,7 @@ export class DemoStringNode implements ApiNode[], + readonly accessPath: InputApiNode[], ) { this.id = `${accessPath.map((node) => node.id).join(".")}.DemoStringNode`; this.regex = input.pattern; @@ -115,7 +110,7 @@ export class DemoTypeShapeStage implements FdrStage[], + readonly accessPath: InputApiNode[], ) { this.id = `${accessPath.map((node) => node.id).join(".")}.DemoTypeShapeStage`; @@ -151,7 +146,7 @@ export class DemoTypeShapeStage implements FdrStage + implements InputApiNode { id: string; @@ -162,7 +157,7 @@ export class DemoPropertyNode readonly name: string, readonly context: ApiNodeContext, readonly input: OpenAPIV3_1.SchemaObject, - readonly accessPath: ApiNode[], + readonly accessPath: InputApiNode[], ) { this.id = `${accessPath.map((node) => node.id).join(".")}.DemoPropertyNode`; if (input.type === "string") { @@ -186,7 +181,7 @@ export class DemoPropertyNode }; } -export class DemoSchemaNode implements ApiNode { +export class DemoSchemaNode implements InputApiNode { id: string; shape: DemoTypeShapeStage | undefined = undefined; @@ -196,7 +191,7 @@ export class DemoSchemaNode implements ApiNode[], + readonly accessPath: InputApiNode[], ) { this.id = `${accessPath.map((node) => node.id).join(".")}.DemoTypeDefinitionNode`; if (this.input.type === "object") { @@ -220,7 +215,7 @@ export class DemoSchemaNode implements ApiNode { +export class ComponentsNode implements InputApiNode { id: string; schemas: DemoSchemaNode[] = []; @@ -228,7 +223,7 @@ export class ComponentsNode implements ApiNode[], + readonly accessPath: InputApiNode[], ) { this.id = `${accessPath.map((node) => node.id).join(".")}.ComponentsNode`; this.schemas = Object.entries(this.input.schemas ?? {}).map(([name, schema]) => { diff --git a/packages/parsers/openapi/shared/interfaces/api.node.interface.ts b/packages/parsers/openapi/shared/interfaces/api.node.interface.ts deleted file mode 100644 index b257982f08..0000000000 --- a/packages/parsers/openapi/shared/interfaces/api.node.interface.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Logger } from "@playwright/test"; - -export class ErrorCollector { - errors: string[] = []; - - addError(error: string): void { - this.errors.push(error); - } -} - -export interface ApiNodeContext { - orgId: string; - apiId: string; - logger: Logger; - errorCollector: ErrorCollector; -} - -export interface ApiNode { - context: ApiNodeContext; - input: InputShape; - - accessPath: ApiNode[]; - id: string; - - outputFdrShape: () => FdrShape; -} - -export interface ComposableApiNode, FdrShape> - extends ApiNode { - inputNode: InputNode; - - outputFdrShape: () => T & FdrShape; -} diff --git a/packages/parsers/openapi/shared/interfaces/container.node.interface.ts b/packages/parsers/openapi/shared/interfaces/container.node.interface.ts deleted file mode 100644 index a58ddec4e8..0000000000 --- a/packages/parsers/openapi/shared/interfaces/container.node.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ApiNode } from "./api.node.interface"; - -export interface ContainerNode extends ApiNode { - innerNodeInputShape: InnerNodeInputShape; -} diff --git a/packages/parsers/openapi/shared/interfaces/fdr.stage.interface.ts b/packages/parsers/openapi/shared/interfaces/fdr.stage.interface.ts deleted file mode 100644 index 1f54110787..0000000000 --- a/packages/parsers/openapi/shared/interfaces/fdr.stage.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ApiNode } from "./api.node.interface"; - -export interface FdrStage extends ApiNode {} diff --git a/packages/parsers/openapi/shared/interfaces/primitive.node.interface.ts b/packages/parsers/openapi/shared/interfaces/primitive.node.interface.ts deleted file mode 100644 index d661752822..0000000000 --- a/packages/parsers/openapi/shared/interfaces/primitive.node.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ApiNode } from "./api.node.interface"; - -export interface PrimitiveNode extends ApiNode {} diff --git a/packages/parsers/openapi/shared/nodes/object.node.ts b/packages/parsers/openapi/shared/nodes/object.node.ts new file mode 100644 index 0000000000..52a6ee2756 --- /dev/null +++ b/packages/parsers/openapi/shared/nodes/object.node.ts @@ -0,0 +1,40 @@ +import { FdrAPI } from "@fern-api/fdr-sdk"; +import { ApiNodeContext, OutputApiNode } from "../../base.node.interface"; +import { SchemaObject } from "../openapi.types"; +import { ObjectPropertyNode } from "./objectProperty.node"; +import { TypeReferenceNode, isReferenceObject } from "./typeReference.node"; + +export class ObjectNode extends OutputApiNode { + extends: FdrAPI.TypeId[] = []; + properties: ObjectPropertyNode[] = []; + extraProperties: TypeReferenceNode | undefined; + + constructor(context: ApiNodeContext, input: SchemaObject, accessPath: string[], accessorKey?: string) { + super(context, input, accessPath); + + if (input.allOf !== undefined) { + this.extends = input.allOf + .map((type) => (isReferenceObject(type) ? FdrAPI.TypeId(type.$ref) : undefined)) + .filter((id): id is FdrAPI.TypeId => id !== undefined); + } + + if (input.properties !== undefined) { + Object.entries(input.properties).forEach(([key, property]) => { + this.properties.push(new ObjectPropertyNode(key, context, property, accessPath, accessorKey)); + }); + } + } + + outputFdrShape = (): FdrAPI.api.latest.ObjectType | undefined => { + const properties = this.properties + .map((property) => property.outputFdrShape()) + .filter((property): property is FdrAPI.api.latest.ObjectProperty => property !== undefined); + + return { + extends: this.extends, + properties, + extraProperties: undefined, + // TODO: add extraProperties + }; + }; +} diff --git a/packages/parsers/openapi/shared/nodes/objectProperty.node.ts b/packages/parsers/openapi/shared/nodes/objectProperty.node.ts new file mode 100644 index 0000000000..4cb326cf55 --- /dev/null +++ b/packages/parsers/openapi/shared/nodes/objectProperty.node.ts @@ -0,0 +1,47 @@ +import { FdrAPI } from "@fern-api/fdr-sdk"; +import { ApiNodeContext, OutputApiNode } from "../../base.node.interface"; +import { ReferenceObject, SchemaObject } from "../openapi.types"; +import { isReferenceObject, mapReferenceObject } from "./typeReference.node"; +import { TypeShapeNode } from "./typeShape.node"; + +export class ObjectPropertyNode extends OutputApiNode< + SchemaObject | ReferenceObject, + FdrAPI.api.latest.ObjectProperty +> { + valueShape: TypeShapeNode; + description: string | undefined; + // availability: AvailabilityNode; + + constructor( + private readonly key: string, + context: ApiNodeContext, + input: SchemaObject | ReferenceObject, + accessPath: string[], + accessorKey?: string, + ) { + super(context, input, accessPath); + + if (isReferenceObject(input)) { + input = mapReferenceObject(input); + } + + this.valueShape = new TypeShapeNode(context, input, accessPath, accessorKey); + this.description = input.description; + // this.availability = input.availability; + } + + outputFdrShape = (): FdrAPI.api.latest.ObjectProperty | undefined => { + const valueShape = this.valueShape.outputFdrShape(); + if (valueShape === undefined) { + return undefined; + } + + return { + key: FdrAPI.PropertyKey(this.key), + valueShape, + description: this.description, + availability: undefined, + // TODO: update to availability: this.availability.outputFdrShape(), + }; + }; +} diff --git a/packages/parsers/openapi/shared/nodes/pathPart.node.ts b/packages/parsers/openapi/shared/nodes/pathPart.node.ts deleted file mode 100644 index 04929767e0..0000000000 --- a/packages/parsers/openapi/shared/nodes/pathPart.node.ts +++ /dev/null @@ -1,29 +0,0 @@ -// import { ApiNode, ApiNodeContext } from "../interfaces/api.node.interface"; - -// import { FdrAPI } from "@fern-api/fdr-sdk"; - -// export class PathPartNode implements ApiNode { -// name = "pathPart"; -// qualifiedId: string; - -// constructor( -// readonly context: ApiNodeContext, -// readonly preProcessedInput: string, -// readonly accessPath: ApiNode[], -// ) { -// this.qualifiedId = `${this.accessPath.map((node) => node.qualifiedId).join(".")}.${this.name}`; -// this.accessPath.push(this); -// } - -// outputFdrShape = (): FdrAPI.api.latest.PathPart => { -// return this.preProcessedInput.startsWith("{") && this.preProcessedInput.endsWith("}") -// ? { -// type: "pathParameter", -// value: FdrAPI.PropertyKey(this.preProcessedInput.slice(1, -1)), -// } -// : { -// type: "literal", -// value: this.preProcessedInput, -// }; -// }; -// } diff --git a/packages/parsers/openapi/shared/nodes/primitives/enum.node.ts b/packages/parsers/openapi/shared/nodes/primitives/enum.node.ts new file mode 100644 index 0000000000..f7862e850e --- /dev/null +++ b/packages/parsers/openapi/shared/nodes/primitives/enum.node.ts @@ -0,0 +1,39 @@ +import { FdrAPI } from "@fern-api/fdr-sdk"; +import { OpenAPIV3_1 } from "openapi-types"; +import { ApiNodeContext, InputApiNode } from "../../../base.node.interface"; + +export class EnumNode extends InputApiNode { + default: number | undefined; + format: string | undefined; + minimum: number | undefined; + maximum: number | undefined; + + constructor( + context: ApiNodeContext, + input: OpenAPIV3_1.NonArraySchemaObject, + accessPath: string[], + accessorKey?: string, + ) { + super(context, input, accessPath, accessorKey); + if (input.type !== "integer") { + context.errorCollector.addError( + `Expected type "integer" for primitive, but got "${input.type}"`, + accessPath, + accessorKey, + ); + } + this.format = input.format; + this.default = input.default; + this.minimum = input.minimum; + this.maximum = input.maximum; + } + + outputFdrShape = (): FdrAPI.api.v1.read.PrimitiveType.Integer => { + return { + type: "integer", + minimum: this.minimum, + maximum: this.maximum, + default: this.default, + }; + }; +} diff --git a/packages/parsers/openapi/shared/nodes/primitives/number.node.ts b/packages/parsers/openapi/shared/nodes/primitives/number.node.ts new file mode 100644 index 0000000000..a4e64b1947 --- /dev/null +++ b/packages/parsers/openapi/shared/nodes/primitives/number.node.ts @@ -0,0 +1,60 @@ +import { FdrAPI } from "@fern-api/fdr-sdk"; +import { ApiNodeContext, InputApiNode } from "../../../base.node.interface"; +import { SchemaObject } from "../../openapi.types"; +import { FloatNode } from "./number/float.node"; +import { IntegerNode } from "./number/integer.node"; + +type FdrNumberType = + | FdrAPI.api.v1.read.PrimitiveType.Integer + | FdrAPI.api.v1.read.PrimitiveType.Long + | FdrAPI.api.v1.read.PrimitiveType.Double + | FdrAPI.api.v1.read.PrimitiveType.Uint + | FdrAPI.api.v1.read.PrimitiveType.Uint64; + +export class NumberNode extends InputApiNode { + typeNode: IntegerNode | FloatNode | undefined; + default: number | undefined; + minimum: number | undefined; + maximum: number | undefined; + + constructor(context: ApiNodeContext, input: SchemaObject, accessPath: string[], accessorKey?: string) { + super(context, input, accessPath); + + if (input.type !== "integer" && input.type !== "number") { + context.errorCollector.addError( + `Expected type "integer" or "number" for numerical primitive, but got "${input.type}"`, + accessPath, + accessorKey, + ); + return; + } + + switch (input.type) { + case "integer": + this.typeNode = new IntegerNode(context, input, accessPath, accessorKey); + break; + case "number": + this.typeNode = new FloatNode(context, input, accessPath, accessorKey); + break; + } + + this.default = input.default; + this.minimum = input.minimum; + this.maximum = input.maximum; + } + + outputFdrShape = (): FdrNumberType | undefined => { + const typeProperties = this.typeNode?.outputFdrShape(); + + if (typeProperties === undefined) { + return undefined; + } + + return { + ...typeProperties, + minimum: this.minimum, + maximum: this.maximum, + default: this.default, + }; + }; +} diff --git a/packages/parsers/openapi/shared/nodes/primitives/number/float.node.ts b/packages/parsers/openapi/shared/nodes/primitives/number/float.node.ts new file mode 100644 index 0000000000..d0ab31a841 --- /dev/null +++ b/packages/parsers/openapi/shared/nodes/primitives/number/float.node.ts @@ -0,0 +1,59 @@ +import { UnreachableCaseError } from "ts-essentials"; +import { ApiNodeContext, OutputApiNode } from "../../../../base.node.interface"; +import { SchemaObject } from "../../../openapi.types"; +import { FdrFloatType } from "../types/fdr.types"; +import { OpenApiNumberTypeFormat } from "../types/format.types"; + +function isOpenApiNumberTypeFormat(format: string | undefined): format is OpenApiNumberTypeFormat { + return format === "float" || format === "double" || format === undefined; +} + +export class FloatNode extends OutputApiNode> { + type: FdrFloatType["type"] | undefined = undefined; + + constructor(context: ApiNodeContext, input: SchemaObject, accessPath: string[], accessorKey?: string) { + super(context, input, accessPath); + + if (input.type !== "number") { + context.errorCollector.addError( + `Expected type "number" for numerical primitive, but got "${input.type}"`, + accessPath, + accessorKey, + ); + return; + } + + if (!isOpenApiNumberTypeFormat(input.format)) { + context.errorCollector.addError( + `Expected format for number primitive, but got "${input.format}"`, + accessPath, + accessorKey, + ); + return; + } + + switch (input.format) { + case "decimal": + case "decimal128": + case "double-int": + case "double": + case "float": + case "sf-decimal": + case undefined: + this.type = "double"; + break; + default: + new UnreachableCaseError(input.format); + } + } + + // In this, we pick only the non-shared types, so that we can push up the shared types to the parent for maximum reuse + outputFdrShape = (): Pick | undefined => { + if (this.type === undefined) { + return undefined; + } + return { + type: this.type, + }; + }; +} diff --git a/packages/parsers/openapi/shared/nodes/primitives/number/integer.node.ts b/packages/parsers/openapi/shared/nodes/primitives/number/integer.node.ts new file mode 100644 index 0000000000..f6aec50bfd --- /dev/null +++ b/packages/parsers/openapi/shared/nodes/primitives/number/integer.node.ts @@ -0,0 +1,58 @@ +import { UnreachableCaseError } from "ts-essentials"; +import { ApiNodeContext, OutputApiNode } from "../../../../base.node.interface"; +import { SchemaObject } from "../../../openapi.types"; +import { FdrIntegerType } from "../types/fdr.types"; +import { OpenApiIntegerTypeFormat } from "../types/format.types"; + +function isOpenApiIntegerTypeFormat(format: string | undefined): format is OpenApiIntegerTypeFormat { + return format === "int32" || format === "int64" || format === undefined; +} + +export class IntegerNode extends OutputApiNode> { + type: FdrIntegerType["type"] = "integer"; + + constructor(context: ApiNodeContext, input: SchemaObject, accessPath: string[], accessorKey?: string) { + super(context, input, accessPath); + + if (input.type !== "integer") { + context.errorCollector.addError( + `Expected type "integer" for numerical primitive, but got "${input.type}"`, + accessPath, + accessorKey, + ); + return; + } + + if (!isOpenApiIntegerTypeFormat(input.format)) { + context.errorCollector.addError( + `Expected format for integer primitive, but got "${input.format}"`, + accessPath, + accessorKey, + ); + return; + } + + switch (input.format) { + case "int64": + this.type = "long"; + break; + case "int8": + case "int16": + case "int32": + case "uint8": + case "sf-integer": + case undefined: + this.type = "integer"; + break; + default: + new UnreachableCaseError(input.format); + } + } + + // In this, we pick only the non-shared types, so that we can push up the shared types to the parent for maximum reuse + outputFdrShape = (): Pick => { + return { + type: this.type, + }; + }; +} diff --git a/packages/parsers/openapi/shared/nodes/primitives/string.node.ts b/packages/parsers/openapi/shared/nodes/primitives/string.node.ts new file mode 100644 index 0000000000..36fba0a4b4 --- /dev/null +++ b/packages/parsers/openapi/shared/nodes/primitives/string.node.ts @@ -0,0 +1,105 @@ +import { UnreachableCaseError } from "ts-essentials"; +import { ApiNodeContext, InputApiNode } from "../../../base.node.interface"; +import { SchemaObject } from "../../openapi.types"; +import { FdrStringType } from "./types/fdr.types"; +import { OpenApiStringTypeFormat } from "./types/format.types"; + +export class StringNode extends InputApiNode { + type: FdrStringType["type"] | undefined; + regex: string | undefined; + default: string | undefined; + minLength: number | undefined; + maxLength: number | undefined; + + mapToFdrType = (format: OpenApiStringTypeFormat): FdrStringType["type"] | undefined => { + switch (format) { + case "base64url": + case "binary": + case "byte": + case "sf-binary": + return "base64"; + case "date-time": + return "datetime"; + case "int64": + return "bigInteger"; + case "date": + return "date"; + case "uuid": + return "uuid"; + case "char": + case "commonmark": + case "decimal": + case "decimal128": + case "duration": + case "email": + case "hostname": + case "html": + case "http-date": + case "idn-email": + case "idn-hostname": + case "ipv4": + case "ipv6": + case "iri-reference": + case "iri": + case "json-pointer": + case "media-range": + case "password": + case "regex": + case "relative-json-pointer": + case "sf-boolean": + case "sf-string": + case "sf-token": + case "time": + case "uri-reference": + case "uri-template": + case "uri": + case undefined: + return "string"; + default: + new UnreachableCaseError(format); + return undefined; + } + }; + + constructor(context: ApiNodeContext, input: SchemaObject, accessPath: string[], accessorKey?: string) { + super(context, input, accessPath, accessorKey); + if (input.type !== "string") { + context.errorCollector.addError( + `Expected type "string" for primitive, but got "${input.type}"`, + accessPath, + accessorKey, + ); + return; + } + + this.type = this.mapToFdrType(input.format); + + if (this.type === undefined) { + context.errorCollector.addError( + `Expected proper "string" format, but got "${input.format}"`, + accessPath, + accessorKey, + ); + return; + } + + this.regex = input.pattern; + this.default = input.default; + this.minLength = input.minLength; + this.maxLength = input.maxLength; + } + + outputFdrShape = (): FdrStringType | undefined => { + if (this.type === undefined) { + return undefined; + } + + return { + type: this.type, + regex: this.regex, + minLength: this.minLength, + maxLength: this.maxLength, + default: this.default, + }; + }; +} diff --git a/packages/parsers/openapi/shared/nodes/primitives/types/fdr.types.ts b/packages/parsers/openapi/shared/nodes/primitives/types/fdr.types.ts new file mode 100644 index 0000000000..3b40386d43 --- /dev/null +++ b/packages/parsers/openapi/shared/nodes/primitives/types/fdr.types.ts @@ -0,0 +1,15 @@ +import { FdrAPI } from "@fern-api/fdr-sdk"; + +export type FdrFloatType = FdrAPI.api.v1.read.PrimitiveType.Double; +export type FdrIntegerType = + | FdrAPI.api.v1.read.PrimitiveType.Integer + | FdrAPI.api.v1.read.PrimitiveType.Long + | FdrAPI.api.v1.read.PrimitiveType.Uint + | FdrAPI.api.v1.read.PrimitiveType.Uint64; +export type FdrStringType = + | FdrAPI.api.v1.read.PrimitiveType.String + | FdrAPI.api.v1.read.PrimitiveType.BigInteger + | FdrAPI.api.v1.read.PrimitiveType.Datetime + | FdrAPI.api.v1.read.PrimitiveType.Uuid + | FdrAPI.api.v1.read.PrimitiveType.Base64 + | FdrAPI.api.v1.read.PrimitiveType.Date_; diff --git a/packages/parsers/openapi/shared/nodes/primitives/types/format.types.ts b/packages/parsers/openapi/shared/nodes/primitives/types/format.types.ts new file mode 100644 index 0000000000..b8872f7de1 --- /dev/null +++ b/packages/parsers/openapi/shared/nodes/primitives/types/format.types.ts @@ -0,0 +1,48 @@ +// Copied from https://spec.openapis.org/registry/format/ + +export type OpenApiNumberTypeFormat = + | "decimal" + | "decimal128" + | "double-int" + | "double" + | "float" + | "sf-decimal" + | undefined; +export type OpenApiIntegerTypeFormat = "int16" | "int32" | "int64" | "int8" | "sf-integer" | "uint8" | undefined; +export type OpenApiStringTypeFormat = + | "base64url" + | "binary" + | "byte" + | "char" + | "commonmark" + | "date-time" + | "date" + | "decimal" + | "decimal128" + | "duration" + | "email" + | "hostname" + | "html" + | "http-date" + | "idn-email" + | "idn-hostname" + | "int64" + | "ipv4" + | "ipv6" + | "iri-reference" + | "iri" + | "json-pointer" + | "media-range" + | "password" + | "regex" + | "relative-json-pointer" + | "sf-binary" + | "sf-boolean" + | "sf-string" + | "sf-token" + | "time" + | "uri-reference" + | "uri-template" + | "uri" + | "uuid" + | undefined; diff --git a/packages/parsers/openapi/shared/nodes/schema.node.ts b/packages/parsers/openapi/shared/nodes/schema.node.ts new file mode 100644 index 0000000000..d272539da6 --- /dev/null +++ b/packages/parsers/openapi/shared/nodes/schema.node.ts @@ -0,0 +1,39 @@ +import { FdrAPI } from "@fern-api/fdr-sdk"; +import { OpenAPIV3_1 } from "openapi-types"; +import { ApiNodeContext, InputApiNode } from "../../base.node.interface"; +import { SchemaObject } from "../openapi.types"; +import { TypeShapeNode } from "./typeShape.node"; + +export class SchemaNode extends InputApiNode { + shape: TypeShapeNode; + description: string | undefined; + // availability: AvailabilityNode; + + constructor( + private readonly name: string, + context: ApiNodeContext, + input: OpenAPIV3_1.SchemaObject, + accessPath: string[], + accessorKey?: string, + ) { + super(context, input, accessPath, accessorKey); + + this.shape = new TypeShapeNode(context, input, accessPath, accessorKey); + this.description = input.description; + } + + outputFdrShape = (): FdrAPI.api.latest.TypeDefinition | undefined => { + const typeShape = this.shape.outputFdrShape(); + if (typeShape === undefined) { + return undefined; + } + + return { + name: this.name, + shape: typeShape, + description: this.description, + availability: undefined, + // TODO: update to availability: this.availability.outputFdrShape(), + }; + }; +} diff --git a/packages/parsers/openapi/shared/nodes/string.node.ts b/packages/parsers/openapi/shared/nodes/string.node.ts deleted file mode 100644 index dc42a4b12b..0000000000 --- a/packages/parsers/openapi/shared/nodes/string.node.ts +++ /dev/null @@ -1,43 +0,0 @@ -// import { ApiNode, ApiNodeContext } from "../interfaces/api.node.interface"; -// import { PrimitiveNode } from "../interfaces/primitive.node.interface"; - -// type FdrStringShape = { -// value: string; -// format?: "uuid" | "email"; -// pattern?: RegExp; -// }; - -// type RedocStringNode = { -// stringValue: string; -// format?: "uuid" | "email"; -// pattern?: RegExp; -// }; - -// export class StringNode implements PrimitiveNode { -// name = "String"; -// qualifiedId: string; - -// private readonly stringValue: string; -// private readonly format?: "uuid" | "email"; -// private readonly pattern?: RegExp; - -// constructor( -// readonly context: ApiNodeContext, -// readonly preProcessedInput: RedocStringNode, -// readonly accessPath: ApiNode[], -// ) { -// this.qualifiedId = accessPath.map((node) => node.qualifiedId).join("."); - -// this.stringValue = preProcessedInput.stringValue; -// this.format = preProcessedInput.format; -// this.pattern = preProcessedInput.pattern; -// } - -// outputFdrShape(): FdrStringShape { -// return { -// value: this.stringValue, -// ...(this.format ? {} : { format: this.format }), -// ...(this.pattern ? {} : { pattern: this.pattern }), -// }; -// } -// } diff --git a/packages/parsers/openapi/shared/nodes/typeReference.node.ts b/packages/parsers/openapi/shared/nodes/typeReference.node.ts new file mode 100644 index 0000000000..dbeb9aeb87 --- /dev/null +++ b/packages/parsers/openapi/shared/nodes/typeReference.node.ts @@ -0,0 +1,55 @@ +import { FdrAPI } from "@fern-api/fdr-sdk"; +import { ApiNodeContext, OutputApiNode } from "../../base.node.interface"; +import { ReferenceObject, SchemaObject } from "../openapi.types"; +import { NumberNode } from "./primitives/number.node"; +import { StringNode } from "./primitives/string.node"; + +export const isReferenceObject = (input: unknown): input is ReferenceObject => { + return typeof input === "object" && input != null && "$ref" in input && typeof input.$ref === "string"; +}; +export const mapReferenceObject = (referenceObject: ReferenceObject): SchemaObject => { + return referenceObject.$ref as SchemaObject; +}; + +// might want to split this out into AliasNode, PrimitiveNode, etc. +export class TypeReferenceNode extends OutputApiNode { + type: FdrAPI.api.latest.TypeReference["type"] | undefined; + typeNode: StringNode | NumberNode | undefined; + ref: string | undefined; + default: FdrAPI.api.latest.TypeReferenceIdDefault | undefined; + + constructor(context: ApiNodeContext, input: SchemaObject | ReferenceObject, accessPath: string[]) { + super(context, input, accessPath); + + if (isReferenceObject(input)) { + this.type = "id"; + this.ref = input.$ref; + this.default = undefined; + } else { + this.type = "primitive"; + this.typeNode = new NumberNode(context, input, accessPath); + } + // just support primitives and ids for now + } + + outputFdrShape = (): FdrAPI.api.latest.TypeReference | undefined => { + const primitiveShape = this.typeNode?.outputFdrShape(); + if (primitiveShape === undefined || this.ref === undefined) { + return undefined; + } + if (this.type === "id") { + return { + type: this.type, + id: FdrAPI.TypeId(this.ref), + default: this.default, + }; + } + if (this.type === "primitive") { + return { + type: this.type, + value: primitiveShape, + }; + } + return undefined; + }; +} diff --git a/packages/parsers/openapi/shared/nodes/typeShape.node.ts b/packages/parsers/openapi/shared/nodes/typeShape.node.ts new file mode 100644 index 0000000000..45ac300e6c --- /dev/null +++ b/packages/parsers/openapi/shared/nodes/typeShape.node.ts @@ -0,0 +1,45 @@ +import { FdrAPI } from "@fern-api/fdr-sdk"; +import { ApiNodeContext, OutputApiNode } from "../../base.node.interface"; +import { SchemaObject } from "../openapi.types"; +import { ObjectNode } from "./object.node"; +import { TypeReferenceNode } from "./typeReference.node"; + +export class TypeShapeNode extends OutputApiNode { + // For now, we will just support Object nodes, in the future, this will need to be updated to an exhaustive switch + type: FdrAPI.api.latest.TypeShape["type"] | undefined; + typeNode: ObjectNode | TypeReferenceNode | undefined; + + constructor(context: ApiNodeContext, input: SchemaObject, accessPath: string[], accessorKey?: string) { + super(context, input, accessPath); + + // For now, we will just support Object and alias nodes, in the future, this will need to be updated to an exhaustive switch + if (input.type === "alias") { + this.type = "alias"; + this.typeNode = new TypeReferenceNode(context, input, accessPath); + } else if (input.type === "object") { + this.type = "object"; + this.typeNode = new ObjectNode(context, input, accessPath, accessorKey); + } + } + + outputFdrShape = (): FdrAPI.api.latest.TypeShape | undefined => { + const typeShape = this.typeNode?.outputFdrShape(); + if (typeShape === undefined || this.type === undefined) { + return undefined; + } + switch (this.type) { + case "object": + return { + type: "object", + ...(typeShape as FdrAPI.api.latest.ObjectType), + }; + case "alias": + return { + type: this.type, + value: typeShape as FdrAPI.api.latest.TypeReference, + }; + default: + return undefined; + } + }; +} diff --git a/packages/parsers/openapi/shared/openapi.types.ts b/packages/parsers/openapi/shared/openapi.types.ts new file mode 100644 index 0000000000..a7bd020749 --- /dev/null +++ b/packages/parsers/openapi/shared/openapi.types.ts @@ -0,0 +1,3 @@ +import { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from "openapi-types"; +export type SchemaObject = OpenAPIV3_1.SchemaObject | OpenAPIV3.SchemaObject | OpenAPIV2.SchemaObject; +export type ReferenceObject = OpenAPIV3_1.ReferenceObject | OpenAPIV3.ReferenceObject | OpenAPIV2.ReferenceObject; diff --git a/packages/parsers/openapi/shared/nodes/authScheme.node.ts b/packages/parsers/openapi/shared/temporary/authScheme.node.ts similarity index 100% rename from packages/parsers/openapi/shared/nodes/authScheme.node.ts rename to packages/parsers/openapi/shared/temporary/authScheme.node.ts diff --git a/packages/parsers/openapi/shared/composable/availability.node.ts b/packages/parsers/openapi/shared/temporary/availability.node.ts similarity index 100% rename from packages/parsers/openapi/shared/composable/availability.node.ts rename to packages/parsers/openapi/shared/temporary/availability.node.ts diff --git a/packages/parsers/openapi/shared/nodes/endpoint.node.ts b/packages/parsers/openapi/shared/temporary/endpoint.node.ts similarity index 100% rename from packages/parsers/openapi/shared/nodes/endpoint.node.ts rename to packages/parsers/openapi/shared/temporary/endpoint.node.ts diff --git a/packages/parsers/openapi/shared/nodes/path.node.ts b/packages/parsers/openapi/shared/temporary/path.node.ts similarity index 100% rename from packages/parsers/openapi/shared/nodes/path.node.ts rename to packages/parsers/openapi/shared/temporary/path.node.ts diff --git a/packages/parsers/openapi/shared/temporary/pathPart.node.ts b/packages/parsers/openapi/shared/temporary/pathPart.node.ts new file mode 100644 index 0000000000..4a0d34b70d --- /dev/null +++ b/packages/parsers/openapi/shared/temporary/pathPart.node.ts @@ -0,0 +1,29 @@ +import { ApiNode, ApiNodeContext } from "../../base.node.interface"; + +import { FdrAPI } from "@fern-api/fdr-sdk"; + +export class PathPartNode implements ApiNode { + name = "pathPart"; + qualifiedId: string; + + constructor( + readonly context: ApiNodeContext, + readonly preProcessedInput: string, + readonly accessPath: ApiNode[], + ) { + this.qualifiedId = `${this.accessPath.map((node) => node.qualifiedId).join(".")}.${this.name}`; + this.accessPath.push(this); + } + + outputFdrShape = (): FdrAPI.api.latest.PathPart => { + return this.preProcessedInput.startsWith("{") && this.preProcessedInput.endsWith("}") + ? { + type: "pathParameter", + value: FdrAPI.PropertyKey(this.preProcessedInput.slice(1, -1)), + } + : { + type: "literal", + value: this.preProcessedInput, + }; + }; +} diff --git a/packages/parsers/openapi/shared/stages/fdr/api.stage.ts b/packages/parsers/openapi/shared/temporary/stages/fdr/api.stage.ts similarity index 100% rename from packages/parsers/openapi/shared/stages/fdr/api.stage.ts rename to packages/parsers/openapi/shared/temporary/stages/fdr/api.stage.ts diff --git a/packages/parsers/openapi/shared/nodes/webhook.node.ts b/packages/parsers/openapi/shared/temporary/webhook.node.ts similarity index 100% rename from packages/parsers/openapi/shared/nodes/webhook.node.ts rename to packages/parsers/openapi/shared/temporary/webhook.node.ts diff --git a/packages/parsers/openapi/shared/nodes/websocket.node.ts b/packages/parsers/openapi/shared/temporary/websocket.node.ts similarity index 100% rename from packages/parsers/openapi/shared/nodes/websocket.node.ts rename to packages/parsers/openapi/shared/temporary/websocket.node.ts diff --git a/packages/parsers/package.json b/packages/parsers/package.json index eabb37b625..75b6274c73 100644 --- a/packages/parsers/package.json +++ b/packages/parsers/package.json @@ -26,6 +26,7 @@ "dependencies": { "@fern-api/fdr-sdk": "workspace:*", "openapi-types": "^12.1.3", + "ts-essentials": "^10.0.1", "uuid": "^9.0.0", "zod": "^3.23.8" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04c3716321..05f7a98a62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -973,6 +973,9 @@ importers: openapi-types: specifier: ^12.1.3 version: 12.1.3 + ts-essentials: + specifier: ^10.0.1 + version: 10.0.1(typescript@5.4.3) uuid: specifier: ^9.0.0 version: 9.0.1