Skip to content

Commit

Permalink
feat: Global headers and better enum parsing in OpenApi Parser v2 (#1995
Browse files Browse the repository at this point in the history
)
  • Loading branch information
RohinBhargava authored Jan 14, 2025
1 parent 69227e2 commit 4520edf
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 20 deletions.
2 changes: 1 addition & 1 deletion packages/parsers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fern-api/docs-parsers",
"version": "0.0.25",
"version": "0.0.29",
"repository": {
"type": "git",
"url": "https://github.com/fern-api/fern-platform.git",
Expand Down
12 changes: 10 additions & 2 deletions packages/parsers/src/openapi/3.1/OpenApiDocumentConverter.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { coalesceServers } from "../utils/3.1/coalesceServers";
import { SecurityRequirementObjectConverterNode } from "./auth/SecurityRequirementObjectConverter.node";
import { XFernBasePathConverterNode } from "./extensions/XFernBasePathConverter.node";
import { XFernGlobalHeadersConverterNode } from "./extensions/XFernGlobalHeadersConverter.node";
import { XFernGroupsConverterNode } from "./extensions/XFernGroupsConverter.node";
import { PathsObjectConverterNode } from "./paths/PathsObjectConverter.node";
import { ServerObjectConverterNode } from "./paths/ServerObjectConverter.node";
Expand All @@ -29,6 +30,7 @@ export class OpenApiDocumentConverterNode extends BaseOpenApiV3_1ConverterNode<
basePath: XFernBasePathConverterNode | undefined;
fernGroups: XFernGroupsConverterNode | undefined;
tags: TagObjectConverterNode[] | undefined;
globalHeaders: XFernGlobalHeadersConverterNode | undefined;

constructor(
args: BaseOpenApiV3_1ConverterNodeConstructorArgs<OpenAPIV3_1.Document>
Expand Down Expand Up @@ -128,6 +130,13 @@ export class OpenApiDocumentConverterNode extends BaseOpenApiV3_1ConverterNode<
pathId: "components",
});
}

this.globalHeaders = new XFernGlobalHeadersConverterNode({
input: this.input,
context: this.context,
accessPath: this.accessPath,
pathId: "x-fern-global-headers",
});
}

convert(): FernRegistry.api.latest.ApiDefinition | undefined {
Expand Down Expand Up @@ -188,8 +197,7 @@ export class OpenApiDocumentConverterNode extends BaseOpenApiV3_1ConverterNode<
// This is not necessary and will be removed
subpackages,
auths: this.auth?.convert() ?? {},
// TODO: Implement globalHeaders
globalHeaders: undefined,
globalHeaders: this.globalHeaders?.convert(),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { isNonNullish } from "@fern-api/ui-core-utils";
import { OpenAPIV3_1 } from "openapi-types";
import { FernRegistry } from "../../../client/generated";
import {
BaseOpenApiV3_1ConverterNode,
BaseOpenApiV3_1ConverterNodeConstructorArgs,
} from "../../BaseOpenApiV3_1Converter.node";
import { extendType } from "../../utils/extendType";
import { maybeSingleValueToArray } from "../../utils/maybeSingleValueToArray";
import { SchemaConverterNode } from "../schemas";
import { X_FERN_GLOBAL_HEADERS } from "./fernExtension.consts";

export declare namespace XFernGlobalHeadersConverterNode {
export interface Input {
[X_FERN_GLOBAL_HEADERS]?: ((
| OpenAPIV3_1.SchemaObject
| OpenAPIV3_1.ReferenceObject
) & {
header: string;
})[];
}
}

export class XFernGlobalHeadersConverterNode extends BaseOpenApiV3_1ConverterNode<
unknown,
FernRegistry.api.latest.ObjectProperty[]
> {
globalHeaders?: [string, SchemaConverterNode][] | undefined;

constructor(args: BaseOpenApiV3_1ConverterNodeConstructorArgs<unknown>) {
super(args);
this.safeParse();
}

// This would be used to set a member on the node
parse(): void {
this.globalHeaders = extendType<XFernGlobalHeadersConverterNode.Input>(
this.input
)[X_FERN_GLOBAL_HEADERS]?.map((header) => {
const { header: headerName, ...schema } = header;
return [
headerName,
new SchemaConverterNode({
input: schema,
context: this.context,
accessPath: this.accessPath,
pathId: this.pathId,
}),
];
});
}

convert(): FernRegistry.api.latest.ObjectProperty[] | undefined {
return this.globalHeaders
?.flatMap(([headerName, headerSchema]) => {
const convertedSchema = maybeSingleValueToArray(headerSchema.convert());

return convertedSchema?.map((schema) => ({
key: FernRegistry.PropertyKey(headerName),
valueShape: schema,
description: headerSchema.description,
availability: headerSchema.availability?.convert(),
}));
})
.filter(isNonNullish);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { createMockContext } from "../../../__test__/createMockContext.util";
import { X_FERN_GLOBAL_HEADERS } from "../fernExtension.consts";
import { XFernGlobalHeadersConverterNode } from "../XFernGlobalHeadersConverter.node";

describe("XFernGlobalHeadersConverterNode", () => {
const mockContext = createMockContext();
const mockAccessPath = ["test", "path"];
const mockPathId = "test-path";

beforeEach(() => {
mockContext.errors.warning.mockClear();
});

it("should parse global headers correctly", () => {
const input = {
[X_FERN_GLOBAL_HEADERS]: [
{
header: "X-API-Key",
type: "string",
description: "API Key for authentication",
},
],
};

const converter = new XFernGlobalHeadersConverterNode({
input,
context: mockContext,
accessPath: mockAccessPath,
pathId: mockPathId,
});

expect(converter.globalHeaders).toBeDefined();
expect(converter.globalHeaders?.length).toBe(1);
expect(converter.globalHeaders?.[0][0]).toBe("X-API-Key");
});

it("should handle empty input", () => {
const input = {};

const converter = new XFernGlobalHeadersConverterNode({
input,
context: mockContext,
accessPath: mockAccessPath,
pathId: mockPathId,
});

expect(converter.globalHeaders).toBeUndefined();
});

it("should convert global headers to ObjectProperties", () => {
const input = {
[X_FERN_GLOBAL_HEADERS]: [
{
header: "X-API-Key",
type: "string",
description: "API Key for authentication",
},
],
};

const converter = new XFernGlobalHeadersConverterNode({
input,
context: mockContext,
accessPath: mockAccessPath,
pathId: mockPathId,
});

const result = converter.convert();

expect(result).toBeDefined();
expect(result?.length).toBe(1);
expect(result?.[0].key).toBe("X-API-Key");
expect(result?.[0].description).toBe("API Key for authentication");
});

it("should handle multiple global headers", () => {
const input = {
[X_FERN_GLOBAL_HEADERS]: [
{
header: "X-API-Key",
type: "string",
},
{
header: "X-Client-ID",
type: "string",
},
],
};

const converter = new XFernGlobalHeadersConverterNode({
input,
context: mockContext,
accessPath: mockAccessPath,
pathId: mockPathId,
});

const result = converter.convert();

expect(result).toBeDefined();
expect(result?.length).toBe(2);
expect(result?.[0].key).toBe("X-API-Key");
expect(result?.[1].key).toBe("X-Client-ID");
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const X_FERN_BASE_PATH = "x-fern-base-path";
export const X_FERN_AVAILABILITY = "x-fern-availability";
export const X_FERN_GROUP_NAME = "x-fern-sdk-group-name";
export const X_FERN_GLOBAL_HEADERS = "x-fern-global-headers";
export const X_FERN_SDK_METHOD_NAME = "x-fern-sdk-method-name";
export const X_FERN_GROUPS = "x-fern-groups";
export const X_FERN_WEBHOOK = "x-fern-webhook";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
BaseOpenApiV3_1ConverterNodeConstructorArgs,
BaseOpenApiV3_1ConverterNodeWithExample,
} from "../../../BaseOpenApiV3_1Converter.node";
import { resolveSchemaReference } from "../../../utils/3.1/resolveSchemaReference";
import { isReferenceObject } from "../../guards/isReferenceObject";

export class EnumConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample<
OpenAPIV3_1.NonArraySchemaObject,
Expand All @@ -29,6 +31,22 @@ export class EnumConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample<
return undefined;
}

if (isReferenceObject(value)) {
const resolvedReference = resolveSchemaReference(
value,
this.context.document
);

value =
typeof resolvedReference === "string"
? resolvedReference
: typeof resolvedReference?.default === "string"
? resolvedReference?.default
: typeof resolvedReference?.example === "string"
? resolvedReference.example
: undefined;
}

// TODO: Support { name?: .., description?: .., casing?: .. } here as well
if (typeof value !== "string") {
this.context.errors.error({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { OpenAPIV3_1 } from "openapi-types";
import { expect } from "vitest";
import { createMockContext } from "../../../../__test__/createMockContext.util";
import { EnumConverterNode } from "../EnumConverter.node";

describe("EnumConverterNode", () => {
const mockContext = createMockContext();
const mockContext = createMockContext({
components: {
schemas: {
Status: { type: "string", default: "ACTIVE" },
},
},
} as unknown as OpenAPIV3_1.Document);

beforeEach(() => {
vi.clearAllMocks();
Expand Down Expand Up @@ -70,6 +77,26 @@ describe("EnumConverterNode", () => {
path: ["test", "enum[1]"],
});
});

it("should handle enum schema with $ref", () => {
const input: OpenAPIV3_1.SchemaObject = {
enum: [{ $ref: "#/components/schemas/Status" }],
};
const node = new EnumConverterNode({
input,
context: mockContext,
accessPath: [],
pathId: "test",
});
expect(node.convert()).toEqual({
type: "enum",
values: [
{
value: "ACTIVE",
},
],
});
});
});

describe("convert", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15496,5 +15496,33 @@
"headerWireValue": "Authorization",
"prefix": "Uploadcare"
}
}
},
"globalHeaders": [
{
"key": "Authorization",
"valueShape": {
"type": "alias",
"value": {
"type": "primitive",
"value": {
"type": "string"
}
}
},
"description": "With the `Uploadcare` authentication method:\n* `auth-param` is a `public_key:signature` pair, where your `secret_key` is used to derive `signature` but is _not included in every request_ itself.\n* You MUST also provide the `Date` header in [RFC2822](https://datatracker.ietf.org/doc/html/rfc2822#section-3.3) format with the time zone set to `GMT` (see the example below).\n* The date you provide MUST NOT exceed the 15-minute offset from the server time of the API endpoint.\n\n```http\nAccept: application/vnd.uploadcare-v0.7+json\nDate: Fri, 30 Sep 2016 11:10:54 GMT\nAuthorization: Uploadcare public_key:6ff75027649aadd4dc98c1f784444445d1e6ed82\n```\n\nThe `signature` part of the `Uploadcare` authentication method `auth-param` MUST be constructed from the following components:\n* Request type (`POST`, `GET`, `HEAD`, `OPTIONS`)\n* Hex md5 hash of the request body\n* `Content-Type` header value\n* `Date` header value\n* URI including path and parameters\n\nThe parameters are then concatenated in textual order using LF: every value sits in a separate line. The result is then signed with [HMAC/SHA1](https://en.wikipedia.org/wiki/HMAC) using your project's `secret_key`.\n\nTake a look at the Python example of deriving `signature`; the example request is made to get a list of files:\n\n```py\nimport time\nimport hashlib\nimport hmac\nfrom email import utils\n\n# Specifying the project’s key\nSECRET_KEY = 'YOUR_SECRET_KEY'\n\n# Specifying request type\nverb = 'GET'\n\n# Calculate [md5](https://en.wikipedia.org/wiki/MD5) checksum for the request's HTTP body.\n# Note: Taking into account that in our example, we are sending an HTTP GET request,\n# and the request does not have anything in its HTTP body, we use an empty string as an input to the md5 hash function.\n# If we were to send an HTTP POST request with, for example, JSON in the request's body,\n# we would have to pass the JSON as the input to the md5 hash function.\ncontent_md5 = hashlib.md5(b'').hexdigest()\n\n# Content-Type header\ncontent_type = 'application/json'\n\n# Current time, e.g. 1541423681\ntimestamp = int(time.time())\n# Date header ('Mon, 05 Nov 2018 13:14:41 GMT')\ndate_header = utils.formatdate(timestamp, usegmt=True)\n\n# The request URI\nuri = '/files/?limit=1&stored=true'\n\n# Forming the final string: concatenating\nsign_string = '\\n'.join([verb, content_md5, content_type, date_header, uri])\n\n# Calculating the signature,\n# the result may look like this: \"3cbc4d2cf91f80c1ba162b926f8a975e8bec7995\"\nsignature = hmac.new(SECRET_KEY.encode(), sign_string.encode(), hashlib.sha1).hexdigest()\n```\n\nOnce `signature` is derived, it SHOULD be implemented into the request body:\n\n```bash\ncurl \\\n -H 'Content-Type: application/json' \\\n -H 'Accept: application/vnd.uploadcare-v0.7+json' \\\n -H 'Date: Mon, 05 Nov 2018 13:14:41 GMT' \\\n -H 'Authorization: Uploadcare YOUR_PUBLIC_KEY:SIGNATURE' \\\n 'https://api.uploadcare.com/files/?limit=1&stored=true'\n```\n"
},
{
"key": "Authorization",
"valueShape": {
"type": "alias",
"value": {
"type": "primitive",
"value": {
"type": "string"
}
}
},
"description": "Note: We DO NOT recommend using this authentication method in production.\n\nWith the `Uploadcare.Simple` authentication method, `auth-param` is your `public_key:secret_key` pair. Note that in this scheme, your Uploadcare project `secret_key` is _included in every request as plain text_.\n\n```http\nAccept: application/vnd.uploadcare-v0.7+json\nAuthorization: Uploadcare.Simple public_key:secret_key\n```\n"
}
]
}
Loading

0 comments on commit 4520edf

Please sign in to comment.