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

wip: foundation for OpenApi Parser #1814

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
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
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", () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mega nit: i think it might be nicer to put these test files through out the code base as opposed to the top, that way you can see object.node.ts and object.node.test.ts side-by-side

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