Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dsinghvi/try again #1821

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/parsers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
openapi/shared/temporary
31 changes: 31 additions & 0 deletions packages/parsers/BaseAPIConverterNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ErrorCollector } from "./ErrorCollector";

/**
* Base context class for API converter nodes.
* Provides logging and error collection capabilities.
*/
export abstract class BaseAPIConverterNodeContext {
public readonly logger: Console = console;
public readonly errors: ErrorCollector = new ErrorCollector();
}

/**
* APIConverterNode is responsible for converting API concepts between different API definition formats.
* It takes an input from one API definition format and transforms it into an equivalent output
* in another format. For example, it can convert an OpenAPI operation into an FDR endpoint definition,
* preserving the semantic meaning while adapting to the target format's structure.
*
* @typeparam Input - The type from the source format
* @typeparam Output - The type from the target format
*/
export abstract class BaseAPIConverterNode<Input, Output> {
constructor(
protected readonly input: Input,
protected readonly context: BaseAPIConverterNodeContext,
) {}

/**
* @returns The converted API definition in the target output format
*/
public abstract convert(): Output;
}
35 changes: 35 additions & 0 deletions packages/parsers/ErrorCollector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export declare namespace ErrorCollector {
interface ValidationError {
message: string;
path: string[];
}

interface ValidationWarning {
message: string;
path: string[];
}
}

/**
* ErrorCollector is used to collect validation errors and warnings during parsing.
* It provides methods to track both blocking errors and non-blocking warnings.
*/
export class ErrorCollector {
public readonly warnings: ErrorCollector.ValidationWarning[] = [];
public readonly errors: ErrorCollector.ValidationError[] = [];

/**
* An error will block parsing
* @param error
*/
public error(error: ErrorCollector.ValidationError): void {
this.errors.push(error);
}

/**
* A warning will not block parsing
*/
public warning(warning: ErrorCollector.ValidationWarning): void {
this.warnings.push(warning);
}
}
16 changes: 16 additions & 0 deletions packages/parsers/__test__/createMockContext.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { vi } from "vitest";
import { ApiNodeContext } from "../openapi/ApiNode";

import { createLogger } from "@fern-api/logger";

export function createMockContext(): ApiNodeContext {
return {
orgId: "orgId",
apiId: "apiId",
logger: createLogger(() => undefined),
errorCollector: {
addError: vi.fn(),
errors: [],
},
};
}
87 changes: 87 additions & 0 deletions packages/parsers/__test__/shared/nodes/object.node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { FdrAPI } from "@fern-api/fdr-sdk";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ObjectNode } from "../../../openapi/shared/nodes/object.node";
import { SchemaObject } from "../../../openapi/shared/openapi.types";
import { createMockContext } from "../../createMockContext.util";

describe("ObjectNode", () => {
const mockContext = createMockContext();

beforeEach(() => {
vi.clearAllMocks();
});

describe("constructor", () => {
it("should handle object with no properties or extends", () => {
const input: SchemaObject = {
type: "object",
};
const node = new ObjectNode(mockContext, input, []);
expect(node.properties).toEqual([]);
expect(node.extends).toEqual([]);
expect(node.extraProperties).toBeUndefined();
});

it("should handle object with properties", () => {
const input: SchemaObject = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "integer" },
},
};
const node = new ObjectNode(mockContext, input, []);
expect(node.properties).toHaveLength(2);
});

it("should handle object with allOf/extends", () => {
const input: SchemaObject = {
type: "object",
allOf: [{ $ref: "TypeA" }, { $ref: "TypeB" }],
};
const node = new ObjectNode(mockContext, input, []);
// This needs to change to the computed generated type id for FDR
expect(node.extends).toEqual([FdrAPI.TypeId("TypeA"), FdrAPI.TypeId("TypeB")]);
});

it("should filter out non-reference allOf items", () => {
const input: SchemaObject = {
type: "object",
allOf: [{ $ref: "TypeA" }, { type: "object" }],
};
const node = new ObjectNode(mockContext, input, []);
expect(node.extends).toEqual([FdrAPI.TypeId("TypeA")]);
});
});

describe("toFdrShape", () => {
it("should output shape with no properties", () => {
const node = new ObjectNode(mockContext, { type: "object" }, []);
expect(node.toFdrShape()).toEqual({
extends: [],
properties: [],
extraProperties: undefined,
});
});

it("should output shape with multiple properties and extends", () => {
const input: SchemaObject = {
type: "object",
properties: {
firstName: { type: "string" },
lastName: { type: "string" },
age: { type: "integer" },
height: { type: "number" },
id: { type: "string" },
score: { type: "number" },
},
allOf: [{ $ref: "BaseType" }, { $ref: "PersonType" }],
};
const node = new ObjectNode(mockContext, input, []);
const shape = node.toFdrShape();
expect(shape?.extends).toEqual([FdrAPI.TypeId("BaseType"), FdrAPI.TypeId("PersonType")]);
expect(shape?.properties).toHaveLength(6);
expect(shape?.extraProperties).toBeUndefined();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { FdrAPI } from "@fern-api/fdr-sdk";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ObjectPropertyNode } from "../../../openapi/shared/nodes/objectProperty.node";
import { ReferenceObject, SchemaObject } from "../../../openapi/shared/openapi.types";
import { createMockContext } from "../../createMockContext.util";

describe("ObjectPropertyNode", () => {
const mockContext = createMockContext();

beforeEach(() => {
vi.clearAllMocks();
});

describe("constructor", () => {
it("should handle basic schema object", () => {
const input: SchemaObject = {
type: "string",
description: "test description",
};
const node = new ObjectPropertyNode("testKey", mockContext, input, []);
expect(node.description).toBe("test description");
});

it("should handle reference object", () => {
const input: ReferenceObject = {
$ref: "TypeA",
};
const node = new ObjectPropertyNode("testKey", mockContext, input, []);
expect(node.valueShape).toBeDefined();
});
});

describe("toFdrShape", () => {
it("should output shape with primitive type", () => {
const input: SchemaObject = {
type: "string",
description: "test description",
};
const node = new ObjectPropertyNode("testKey", mockContext, input, []);
const shape = node.toFdrShape();
expect(shape).toBeDefined();
expect(shape?.key).toEqual(FdrAPI.PropertyKey("testKey"));
expect(shape?.description).toBe("test description");
expect(shape?.availability).toBeUndefined();
});

it("should return undefined if valueShape is undefined and collect error", () => {
const input: SchemaObject = {
type: "invalid",
};
const node = new ObjectPropertyNode("testKey", mockContext, input, []);
vi.spyOn(node.valueShape, "toFdrShape").mockReturnValue(undefined);
expect(node.toFdrShape()).toBeUndefined();
// this should show up, but since the examples are terse and non-exhaustive, we do not have any validation checking
// expect(mockContext.errorCollector.addError).toHaveBeenCalledWith(
// "Failed to generate shape for property testKey",
// [],
// );
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { expect } from "vitest";

import { beforeEach, describe, it, vi } from "vitest";
import { NumberNode } from "../../../../openapi/shared/nodes/primitives/number.node";
import { FloatNode } from "../../../../openapi/shared/nodes/primitives/number/float.node";
import { IntegerNode } from "../../../../openapi/shared/nodes/primitives/number/integer.node";
import { createMockContext } from "../../../createMockContext.util";

describe("NumberNode", () => {
const mockContext = createMockContext();

beforeEach(() => {
vi.clearAllMocks();
});

describe("constructor", () => {
it("should handle valid integer input", () => {
const input = {
type: "integer",
minimum: 1,
maximum: 10,
default: 5,
};
const node = new NumberNode(mockContext, input, []);
expect(node.typeNode).toBeInstanceOf(IntegerNode);
expect(node.minimum).toBe(1);
expect(node.maximum).toBe(10);
expect(node.default).toBe(5);
expect(mockContext.errorCollector.addError).not.toHaveBeenCalled();
});

it("should handle valid number input", () => {
const input = {
type: "number",
minimum: 1.5,
maximum: 10.5,
default: 5.5,
};
const node = new NumberNode(mockContext, input, []);
expect(node.typeNode).toBeInstanceOf(FloatNode);
expect(node.minimum).toBe(1.5);
expect(node.maximum).toBe(10.5);
expect(node.default).toBe(5.5);
expect(mockContext.errorCollector.addError).not.toHaveBeenCalled();
});

it("should handle invalid type", () => {
const input = { type: "string" };
const node = new NumberNode(mockContext, input, []);
expect(node.typeNode).toBeUndefined();
expect(mockContext.errorCollector.addError).toHaveBeenCalledWith(
'Expected type "integer" or "number" for numerical primitive, but got "string"',
[],
undefined,
);
});
});

describe("toFdrShape", () => {
it("should return undefined when typeNode shape is undefined", () => {
const input = { type: "string" };
const node = new NumberNode(mockContext, input, []);
expect(node.toFdrShape()).toBeUndefined();
});

it("should return complete shape for integer type", () => {
const input = {
type: "integer",
minimum: 1,
maximum: 10,
default: 5,
};
const node = new NumberNode(mockContext, input, []);
const shape = node.toFdrShape();
expect(shape).toEqual({
type: "integer",
minimum: 1,
maximum: 10,
default: 5,
});
});

it("should return complete shape for number type", () => {
const input = {
type: "number",
minimum: 1.5,
maximum: 10.5,
default: 5.5,
};
const node = new NumberNode(mockContext, input, []);
const shape = node.toFdrShape();
expect(shape).toEqual({
type: "double",
minimum: 1.5,
maximum: 10.5,
default: 5.5,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ApiNodeContext } from "../../../../../openapi/ApiNode";
import { FloatNode } from "../../../../../openapi/shared/nodes/primitives/number/float.node";
import { createMockContext } from "../../../../createMockContext.util";

describe("FloatNode", () => {
const mockContext = createMockContext();

beforeEach(() => {
vi.clearAllMocks();
});

it("should handle valid number input with float format", () => {
const input = { type: "number", format: "float" };
const node = new FloatNode(mockContext, input, []);
expect(node.type).toBe("double");
expect(node.toFdrShape()).toEqual({ type: "double" });
expect(mockContext.errorCollector.addError).not.toHaveBeenCalled();
});

it("should handle valid number input with double format", () => {
const input = { type: "number", format: "double" };
const node = new FloatNode(mockContext, input, []);
expect(node.type).toBe("double");
expect(node.toFdrShape()).toEqual({ type: "double" });
expect(mockContext.errorCollector.addError).not.toHaveBeenCalled();
});

it("should handle valid number input with no format", () => {
const input = { type: "number" };
const node = new FloatNode(mockContext, input, []);
expect(node.type).toBe("double");
expect(node.toFdrShape()).toEqual({ type: "double" });
expect(mockContext.errorCollector.addError).not.toHaveBeenCalled();
});

it("should handle invalid type", () => {
const input = { type: "string" };
const node = new FloatNode(mockContext, input, []);
expect(node.type).toBeUndefined();
expect(node.toFdrShape()).toBeUndefined();
expect(mockContext.errorCollector.addError).toHaveBeenCalledWith(
'Expected type "number" for numerical primitive, but got "string"',
[],
undefined,
);
});

it("should handle invalid format", () => {
const input = { type: "number", format: "invalid" };
const node = new FloatNode(mockContext as ApiNodeContext, input, []);
expect(node.type).toBeUndefined();
expect(node.toFdrShape()).toBeUndefined();
expect(mockContext.errorCollector.addError).toHaveBeenCalledWith(
'Expected format for number primitive, but got "invalid"',
[],
undefined,
);
});
});
Loading
Loading