Skip to content

Commit

Permalink
feat(cli): Add support for nullable<T> (#5601)
Browse files Browse the repository at this point in the history
  • Loading branch information
amckinney authored Jan 14, 2025
1 parent 151378b commit 6970850
Show file tree
Hide file tree
Showing 73 changed files with 6,011 additions and 228 deletions.
12 changes: 12 additions & 0 deletions fern/pages/changelogs/cli/2025-01-14.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## 0.48.0
**`(feat):`** Adds support for nullable types in the Fern definition, such as the following:

```yaml
types:
User:
properties:
name: string
email: nullable<string>
```
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,19 @@ export function getSchemaFromFernType({
inline: undefined
})
: undefined,
nullable: (itemType) =>
itemType != null
? SchemaWithExample.nullable({
nameOverride,
generatedName,
title,
value: itemType,
description,
availability,
groupName,
inline: undefined
})
: undefined,
set: (itemType) =>
itemType != null
? SchemaWithExample.array({
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/cli/versions.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
- changelogEntry:
- summary: |
Adds support for nullable types in the Fern definition, such as the following:
```yaml
types:
User:
properties:
name: string
email: nullable<string>
```
type: feat
irVersion: 53
version: 0.48.0

- changelogEntry:
- summary: |
The IR now pulls in additional request properties from the OAuth getToken endpoint to support custom OAuth schemas.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ export function convertContainerToJsonSchema({
{ type: "null" }
]
};
case "nullable":
return {
oneOf: [
convertTypeReferenceToJsonSchema({ typeReference: container.nullable, context }),
{ type: "null" }
]
};
case "set":
return {
type: "array",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const RawContainerType = {
optional: "optional",
nullable: "nullable",
set: "set",
list: "list",
map: "map",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface RecursiveRawTypeReferenceVisitor<R> {
list: (valueType: R) => R;
set: (valueType: R) => R;
optional: (valueType: R) => R;
nullable: (valueType: R) => R;
literal: (literal: Literal) => R;
named: (named: string) => R;
unknown: () => R;
Expand Down Expand Up @@ -66,6 +67,8 @@ export function recursivelyVisitRawTypeReference<R>({
),
optional: (valueType) =>
visitor.optional(recursivelyVisitRawTypeReference({ type: valueType, _default, validation, visitor })),
nullable: (valueType) =>
visitor.nullable(recursivelyVisitRawTypeReference({ type: valueType, _default, validation, visitor })),
literal: visitor.literal,
named: visitor.named,
unknown: visitor.unknown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const FernContainerRegex = {
LIST: /^list<\s*(.*)\s*>$/,
SET: /^set<\s*(.*)\s*>$/,
OPTIONAL: /^optional<\s*(.*)\s*>$/,
NULLABLE: /^nullable<\s*(.*)\s*>$/,
LITERAL: /^literal<\s*(?:"(.*)"|(true|false))\s*>$/
};

Expand All @@ -19,6 +20,7 @@ export interface RawTypeReferenceVisitor<R> {
list: (valueType: string) => R;
set: (valueType: string) => R;
optional: (valueType: string) => R;
nullable: (valueType: string) => R;
literal: (literal: Literal) => R;
named: (named: string) => R;
unknown: () => R;
Expand Down Expand Up @@ -175,6 +177,11 @@ export function visitRawTypeReference<R>({
return visitor.optional(optionalMatch[1]);
}

const nullableMatch = type.match(FernContainerRegex.NULLABLE);
if (nullableMatch?.[1] != null) {
return visitor.nullable(nullableMatch[1]);
}

const literalMatch = type.match(FernContainerRegex.LITERAL);
if (literalMatch?.[1] != null) {
return visitor.literal(Literal.string(literalMatch[1]));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export class ComplexQueryParamTypeDetector {
)
);
case "optional":
case "nullable":
case "list":
case "set":
return this.isResolvedReferenceComplex({
Expand Down Expand Up @@ -254,6 +255,7 @@ export class ComplexQueryParamTypeDetector {
})
);
case "optional":
case "nullable":
case "list":
case "set":
return this.isComplex({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ function getAllNamedTypes({
list: (namesInValueType) => namesInValueType,
set: (namesInValueType) => namesInValueType,
optional: (namesInValueType) => namesInValueType,
nullable: (namesInValueType) => namesInValueType,
literal: () => [],
named: (named) => {
const reference = parseReferenceToTypeName({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,25 @@ export function convertTypeReferenceExample({
})
);
},
nullable: (itemType) => {
return ExampleTypeReferenceShape.container(
ExampleContainer.nullable({
nullable:
resolvedExample != null
? convertTypeReferenceExample({
example: resolvedExample,
fileContainingExample: fileContainingResolvedExample,
rawTypeBeingExemplified: itemType,
fileContainingRawTypeReference,
typeResolver,
exampleResolver,
workspace
})
: undefined,
valueType: fileContainingRawTypeReference.parseTypeReference(itemType)
})
);
},
literal: (literal) => {
switch (literal.type) {
case "boolean":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export function getReferencedTypesFromRawDeclaration({
map: ({ keyType, valueType }) => [...keyType, ...valueType],
list: (valueType) => valueType,
optional: (valueType) => valueType,
nullable: (valueType) => valueType,
set: (valueType) => valueType,
named: (name) => [name],
literal: () => [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,8 @@ export class DynamicSnippetsConverter {
});
case "optional":
return DynamicSnippets.TypeReference.optional(this.convertTypeReference(container.optional));
case "nullable":
return DynamicSnippets.TypeReference.nullable(this.convertTypeReference(container.nullable));
case "set":
return DynamicSnippets.TypeReference.set(this.convertTypeReference(container.set));
case "literal":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,30 @@ export function generateContainerExample({
jsonExample: example.jsonExample
};
}
case "nullable": {
if (skipOptionalProperties) {
return generateEmptyContainerExample({ containerType });
}
const example = generateTypeReferenceExample({
fieldName,
typeReference: containerType.nullable,
maxDepth,
currentDepth: currentDepth + 1,
typeDeclarations,
skipOptionalProperties
});
if (example.type === "failure") {
return generateEmptyContainerExample({ containerType });
}
return {
type: "success",
example: ExampleContainer.nullable({
nullable: example.example,
valueType: containerType.nullable
}),
jsonExample: example.jsonExample
};
}
case "set": {
const example = generateTypeReferenceExample({
fieldName,
Expand Down Expand Up @@ -206,6 +230,16 @@ export function generateEmptyContainerExample({
jsonExample: undefined
};
}
case "nullable": {
return {
type: "success",
example: ExampleContainer.nullable({
nullable: undefined,
valueType: containerType.nullable
}),
jsonExample: undefined
};
}
case "set": {
return {
type: "success",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,21 @@ export function validateTypeReferenceExample({
depth: depth + 1
});
},
nullable: (itemType) => {
if (example == null) {
return [];
}
return validateTypeReferenceExample({
rawTypeReference: itemType,
example,
typeResolver,
exampleResolver,
file,
workspace,
breadcrumbs,
depth: depth + 1
});
},
unknown: () => {
return [];
},
Expand Down Expand Up @@ -488,6 +503,15 @@ function areResolvedTypesEquivalent({ expected, actual }: { expected: ResolvedTy
? actual.container.itemType
: actual
});
case "nullable":
// special case: if expected is a nullable but actual is not, that's okay
return areResolvedTypesEquivalent({
expected: expected.container.itemType,
actual:
actual._type === "container" && actual.container._type === "nullable"
? actual.container.itemType
: actual
});
case "map":
return (
actual._type === "container" &&
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/generation/ir-generator/src/filterExamples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,23 @@ function filterExampleTypeReference({
}
: undefined;
},
nullable: (n) => {
const filteredNullableTypReference =
n.nullable != null
? filterExampleTypeReference({ filteredIr, exampleTypeReference: n.nullable })
: undefined;
return filteredNullableTypReference != null
? {
...exampleTypeReference,
shape: ExampleTypeReferenceShape.container(
ExampleContainer.nullable({
nullable: filteredNullableTypReference,
valueType: n.valueType
})
)
}
: undefined;
},
map: (m) => ({
...exampleTypeReference,
shape: ExampleTypeReferenceShape.container(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,9 @@ function populateReferencesFromContainer(
optional: (optionalType) => {
populateReferencesFromTypeReference(optionalType, referencedTypes, referencedSubpackages);
},
nullable: (nullableType) => {
populateReferencesFromTypeReference(nullableType, referencedTypes, referencedSubpackages);
},
set: (setType) => {
populateReferencesFromTypeReference(setType, referencedTypes, referencedSubpackages);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export declare type ResolvedContainerType =
| ResolvedContainerType.Map
| ResolvedContainerType.List
| ResolvedContainerType.Optional
| ResolvedContainerType.Nullable
| ResolvedContainerType.Set
| ResolvedContainerType.Literal;

Expand All @@ -69,6 +70,11 @@ export declare namespace ResolvedContainerType {
itemType: ResolvedType;
}

interface Nullable {
_type: "nullable";
itemType: ResolvedType;
}

interface Set {
_type: "set";
itemType: ResolvedType;
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/generation/ir-generator/src/resolvers/TypeResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,19 @@ export class TypeResolverImpl implements TypeResolver {
)
}
: undefined,
nullable: (itemType) =>
itemType != null
? {
_type: "container",
container: {
_type: "nullable",
itemType
},
originalTypeReference: TypeReference.container(
ContainerType.nullable(itemType.originalTypeReference)
)
}
: undefined,
set: (itemType) =>
itemType != null
? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function parseInlineType({ type, file, _default, validation }: parseInlin
list: (valueType) => TypeReference.container(ContainerType.list(valueType)),
set: (valueType) => TypeReference.container(ContainerType.set(valueType)),
optional: (valueType) => TypeReference.container(ContainerType.optional(valueType)),
nullable: (valueType) => TypeReference.container(ContainerType.nullable(valueType)),
literal: (literal) => TypeReference.container(ContainerType.literal(literal)),
named: (namedType) =>
TypeReference.named({
Expand Down
1 change: 1 addition & 0 deletions packages/cli/generation/ir-migrations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"@fern-fern/ir-v51-sdk": "0.0.1",
"@fern-fern/ir-v52-sdk": "0.0.1",
"@fern-fern/ir-v53-sdk": "0.0.1",
"@fern-fern/ir-v54-sdk": "0.0.1",
"@fern-fern/ir-v6-model": "0.0.33",
"@fern-fern/ir-v7-model": "0.0.2",
"@fern-fern/ir-v8-model": "0.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { V51_TO_V50_MIGRATION } from "./migrations/v51-to-v50/migrateFromV51ToV5
import { V52_TO_V51_MIGRATION } from "./migrations/v52-to-v51/migrateFromV52ToV51";
import { V53_TO_V52_MIGRATION } from "./migrations/v53-to-v52/migrateFromV53ToV52";
import { V54_TO_V53_MIGRATION } from "./migrations/v54-to-v53/migrateFromV54ToV53";
import { V55_TO_V54_MIGRATION } from "./migrations/v55-to-v54/migrateFromV55ToV54";
import { GeneratorWasNeverUpdatedToConsumeNewIR, GeneratorWasNotCreatedYet, IrMigration } from "./types/IrMigration";

export function getIntermediateRepresentationMigrator(): IntermediateRepresentationMigrator {
Expand Down Expand Up @@ -298,6 +299,7 @@ const IntermediateRepresentationMigrator = {

export const INTERMEDIATE_REPRESENTATION_MIGRATOR = IntermediateRepresentationMigrator.Builder
// put new migrations here
.withMigration(V55_TO_V54_MIGRATION)
.withMigration(V54_TO_V53_MIGRATION)
.withMigration(V53_TO_V52_MIGRATION)
.withMigration(V52_TO_V51_MIGRATION)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { serialization as V54 } from "@fern-api/ir-sdk";
export { serialization as V55 } from "@fern-api/ir-sdk";
export * as V23 from "@fern-fern/ir-v23-sdk/serialization";
export * as V24 from "@fern-fern/ir-v24-sdk/serialization";
export * as V25 from "@fern-fern/ir-v25-sdk/serialization";
Expand Down Expand Up @@ -30,3 +30,4 @@ export * as V50 from "@fern-fern/ir-v50-sdk/serialization";
export * as V51 from "@fern-fern/ir-v51-sdk/serialization";
export * as V52 from "@fern-fern/ir-v52-sdk/serialization";
export * as V53 from "@fern-fern/ir-v53-sdk/serialization";
export * as V54 from "@fern-fern/ir-v54-sdk/serialization";
Loading

0 comments on commit 6970850

Please sign in to comment.