Skip to content

Commit

Permalink
feat(cli): support contentType on multipart requests in the fern de…
Browse files Browse the repository at this point in the history
…finition (#4651)
  • Loading branch information
dsinghvi authored Sep 16, 2024
1 parent 072c0a2 commit ff5cb31
Show file tree
Hide file tree
Showing 89 changed files with 2,877 additions and 167 deletions.
24 changes: 24 additions & 0 deletions packages/cli/cli/versions.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
- changelogEntry:
- summary: |
The Fern Definition now supports `conten-type` on multipart request properties.
For example, to specify an `application/octet-stream` and `application/json`
contnet types, use the snippet below:
```ts
service:
endpoints:
upload:
request:
body:
properties:
file:
type: file
content-type: application/octet-stream
metadata:
type: unknown
content-type: application/json
```
type: feat
irVersion: 53
version: 0.42.0-rc0

- changelogEntry:
- summary: Remove bang operator and fix eslint warning in `compatible-ir-versions.ts`.
type: internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1514,6 +1514,7 @@ exports[`ir > {"name":"file-upload"} 1`] = `
"properties": [
{
"type": "bodyProperty",
"contentType": null,
"name": {
"name": {
"originalName": "foo",
Expand Down Expand Up @@ -1576,7 +1577,8 @@ exports[`ir > {"name":"file-upload"} 1`] = `
},
"wireValue": "file"
},
"isOptional": false
"isOptional": false,
"contentType": null
}
},
{
Expand Down Expand Up @@ -1605,11 +1607,13 @@ exports[`ir > {"name":"file-upload"} 1`] = `
},
"wireValue": "optionalFile"
},
"isOptional": true
"isOptional": true,
"contentType": null
}
},
{
"type": "bodyProperty",
"contentType": null,
"name": {
"name": {
"originalName": "bar",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from "zod";
import { DeclarationWithNameSchema } from "./DeclarationWithNameSchema";
import { extendTypeReferenceSchema } from "./TypeReferenceSchema";

export const HttpInlineRequestBodyPropertySchema = extendTypeReferenceSchema(
DeclarationWithNameSchema.extend({
// For multipart form uploads
["content-type"]: z.string().optional()
}).shape
);

export type HttpInlineRequestBodyPropertySchema = z.infer<typeof HttpInlineRequestBodyPropertySchema>;
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { z } from "zod";
import { ObjectExtendsSchema } from "./ObjectExtendsSchema";
import { ObjectPropertySchema } from "./ObjectPropertySchema";
import { HttpInlineRequestBodyPropertySchema } from "./HttpInlineRequestBodyPropertySchema";

// for inline request schemas, you need either extends/properties (or both).
export const HttpInlineRequestBodySchema = z.union([
z.strictObject({
extends: ObjectExtendsSchema,
properties: z.optional(z.record(ObjectPropertySchema)),
properties: z.optional(z.record(HttpInlineRequestBodyPropertySchema)),
["extra-properties"]: z.optional(z.boolean())
}),
z.strictObject({
extends: z.optional(ObjectExtendsSchema),
properties: z.record(ObjectPropertySchema),
properties: z.record(HttpInlineRequestBodyPropertySchema),
["extra-properties"]: z.optional(z.boolean())
})
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export declare namespace RawFileUploadRequest {
docs: string | undefined;
availability?: AvailabilityUnionSchema | undefined;
key: string;
contentType?: string;
}
}

Expand Down Expand Up @@ -82,6 +83,7 @@ function createRawFileUploadRequest(
const properties = Object.entries(requestBody.properties ?? []).reduce<RawFileUploadRequest.Property[]>(
(acc, [key, propertyType]) => {
const docs = typeof propertyType !== "string" ? propertyType.docs : undefined;
const contentType = typeof propertyType !== "string" ? propertyType["content-type"] : undefined;
const maybeParsedFileType = parseRawFileType(
typeof propertyType === "string" ? propertyType : propertyType.type
);
Expand All @@ -91,10 +93,11 @@ function createRawFileUploadRequest(
key,
docs,
isOptional: maybeParsedFileType.isOptional,
isArray: maybeParsedFileType.isArray
isArray: maybeParsedFileType.isArray,
contentType
});
} else {
acc.push({ isFile: false, key, propertyType, docs });
acc.push({ isFile: false, key, propertyType, docs, contentType });
}
return acc;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ async function visitEndpoint({
validation: property.validation
});
},
"content-type": noop,
audiences: noop,
encoding: noop,
default: noop,
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/fern-definition/validator/src/getAllRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { ValidStreamConditionRule } from "./rules/valid-stream-condition";
import { ValidTypeNameRule } from "./rules/valid-type-name";
import { ValidTypeReferenceWithDefaultAndValidationRule } from "./rules/valid-type-reference-with-default-and-validation";
import { ValidVersionRule } from "./rules/valid-version";
import { ContentTypeOnlyForMultipartRule } from "./rules/content-type-only-for-multipart";

export function getAllRules(): Rule[] {
return [
Expand Down Expand Up @@ -87,7 +88,8 @@ export function getAllRules(): Rule[] {
ValidVersionRule,
NoUnusedGenericRule,
ValidGenericRule,
CompatibleIrVersionsRule
CompatibleIrVersionsRule,
ContentTypeOnlyForMultipartRule
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`compatible-ir-versions > simple failure 1`] = `
[
{
"message": "baz has content-type, but the request is not multipart",
"nodePath": [
"service",
"endpoints",
"json",
],
"relativeFilepath": "simple.yml",
"severity": "error",
},
]
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/fs-utils";
import { getViolationsForRule } from "../../../testing-utils/getViolationsForRule";
import { ContentTypeOnlyForMultipartRule } from "../content-type-only-for-multipart";

describe("compatible-ir-versions", () => {
it("simple failure", async () => {
const violations = await getViolationsForRule({
rule: ContentTypeOnlyForMultipartRule,
absolutePathToWorkspace: join(
AbsoluteFilePath.of(__dirname),
RelativeFilePath.of("fixtures"),
RelativeFilePath.of("simple")
)
});

expect(violations).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name: simple-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
service:
base-path: /
auth: false
endpoints:
json:
path: ""
method: POST
request:
name: ExampleReq
body:
properties:
foo: integer
baz:
type: string
content-type: "application/json"

multipart:
path: ""
method: PUT
request:
name: MultipartReq
body:
properties:
foo: file
baz:
type: string
content-type: "application/json"
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
groups:
python-sdk:
audiences:
- external
generators:
- name: fernapi/fern-python-sdk
version: 2.0.0
output:
location: pypi
package-name: fern-api
token: ${PYPI_TOKEN}
github:
repository: fern-api/python-sdk
license: MIT
config:
client_class_name: Fern
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { parseFileUploadRequest, isInlineRequestBody } from "@fern-api/fern-definition-schema";
import { Rule, RuleViolation } from "../../Rule";

export const ContentTypeOnlyForMultipartRule: Rule = {
name: "content-type-only-for-multipart",
DISABLE_RULE: false,
create: () => {
return {
definitionFile: {
httpEndpoint: ({ endpoint }) => {
if (endpoint.request == null) {
return [];
}

const parsedFileUploadRequest = parseFileUploadRequest(endpoint.request);
if (parsedFileUploadRequest != null) {
return [];
}

if (
typeof endpoint.request !== "string" &&
endpoint.request.body != null &&
isInlineRequestBody(endpoint.request.body)
) {
const violations: RuleViolation[] = [];
for (const [propertyName, propertyDeclaration] of Object.entries(
endpoint.request.body.properties ?? {}
)) {
if (typeof propertyDeclaration === "string") {
continue;
}
if (propertyDeclaration["content-type"] != null) {
violations.push({
severity: "error",
message: `${propertyName} has content-type, but the request is not multipart`
});
}
}
return violations;
}

return [];
}
}
};
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ContentTypeOnlyForMultipartRule } from "./content-type-only-for-multipart";
Loading

0 comments on commit ff5cb31

Please sign in to comment.