From 143e1814d9540d781b7a261b00cdd29503c002f3 Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Mon, 4 Nov 2024 17:32:17 -0500 Subject: [PATCH 01/10] feat(go): Improve dynamic snippets error reporting --- .../src/DynamicSnippetsGenerator.ts | 145 ++++++--- .../go-v2/dynamic-snippets/src/Scope.ts | 8 + .../dynamic-snippets/src/TypeInstance.ts | 4 +- .../DynamicSnippetsGeneratorContext.ts | 108 +++++-- .../context/DynamicTypeInstantiationMapper.ts | 305 ++++++++++++++---- .../src/context/DynamicTypeMapper.ts | 6 +- .../src/context/ErrorReporter.ts | 55 +++- 7 files changed, 471 insertions(+), 160 deletions(-) create mode 100644 generators/go-v2/dynamic-snippets/src/Scope.ts diff --git a/generators/go-v2/dynamic-snippets/src/DynamicSnippetsGenerator.ts b/generators/go-v2/dynamic-snippets/src/DynamicSnippetsGenerator.ts index b5f0036f297..5a76a03b7ff 100644 --- a/generators/go-v2/dynamic-snippets/src/DynamicSnippetsGenerator.ts +++ b/generators/go-v2/dynamic-snippets/src/DynamicSnippetsGenerator.ts @@ -3,6 +3,8 @@ import { go } from "@fern-api/go-codegen"; import { DynamicSnippetsGeneratorContext } from "./context/DynamicSnippetsGeneratorContext"; import { dynamic as DynamicSnippets } from "@fern-fern/ir-sdk/api"; import { AbstractDynamicSnippetsGenerator } from "@fern-api/dynamic-snippets"; +import { ErrorReporter, Severity } from "./context/ErrorReporter"; +import { Scope } from "./Scope"; const SNIPPET_PACKAGE_NAME = "example"; const SNIPPET_IMPORT_PATH = "fern"; @@ -26,34 +28,49 @@ export class DynamicSnippetsGenerator extends AbstractDynamicSnippetsGenerator { - const endpoints = this.context.resolveEndpointLocationOrThrow(snippet.endpoint); + const endpoints = this.context.resolveEndpointLocationOrThrow(request.endpoint); if (endpoints.length === 0) { - throw new Error(`No endpoints found for ${JSON.stringify(snippet.endpoint)}`); + throw new Error(`No endpoints found for ${JSON.stringify(request.endpoint)}`); } + let bestReporter = this.context.errors.clone(); + let bestSnippet: string | undefined; let err: Error | undefined; - for (const endpoint of endpoints) { + for (const [index, endpoint] of endpoints.entries()) { + this.context.errors.reset(); try { - const code = this.buildCodeBlock({ endpoint, snippet }); - return { - snippet: await code.toString({ - packageName: SNIPPET_PACKAGE_NAME, - importPath: SNIPPET_IMPORT_PATH, - rootImportPath: this.context.rootImportPath, - customConfig: this.context.customConfig ?? {}, - formatter: this.formatter - }) - }; + const code = this.buildCodeBlock({ endpoint, snippet: request }); + const snippet = await code.toString({ + packageName: SNIPPET_PACKAGE_NAME, + importPath: SNIPPET_IMPORT_PATH, + rootImportPath: this.context.rootImportPath, + customConfig: this.context.customConfig ?? {}, + formatter: this.formatter + }); + if (this.context.errors.empty()) { + return { + snippet + }; + } + if (bestReporter.size() > this.context.errors.size()) { + bestReporter = this.context.errors.clone(); + bestSnippet = snippet; + } } catch (error) { if (err == null) { - // Report the first error that occurs. err = error as Error; } } } - throw err ?? new Error(`Failed to generate snippet for ${JSON.stringify(snippet.endpoint)}`); + if (bestSnippet != null) { + return { + snippet: bestSnippet + // TODO: Add errors from the reporter, if any. + }; + } + throw err ?? new Error(`Failed to generate snippet for ${JSON.stringify(request.endpoint)}`); } private buildCodeBlock({ @@ -114,12 +131,17 @@ export class DynamicSnippetsGenerator extends AbstractDynamicSnippetsGenerator ({ + name: queryParameter.name.name.pascalCase.unsafeName, + value: this.context.dynamicTypeInstantiationMapper.convert(queryParameter) + })); + this.context.errors.unscope(); + + this.context.errors.scope(Scope.Headers); + const headers = this.context.associateByWireValue({ + parameters: request.headers ?? [], + values: snippet.headers ?? {} + }); + const headerFields = headers.map((header) => ({ + name: header.name.name.pascalCase.unsafeName, + value: this.context.dynamicTypeInstantiationMapper.convert(header) + })); + this.context.errors.unscope(); + + this.context.errors.scope(Scope.RequestBody); + const requestBodyFields = + request.body != null + ? this.getInlinedRequestBodyStructFields({ body: request.body, value: snippet.requestBody }) + : []; + this.context.errors.unscope(); return go.TypeInstantiation.structPointer({ typeReference: go.typeReference({ name: this.context.getMethodName(request.declaration.name), importPath: this.context.getImportPath(request.declaration.fernFilepath) }), - fields + fields: [...queryParameterFields, ...headerFields, ...requestBodyFields] }); } @@ -428,11 +485,11 @@ export class DynamicSnippetsGenerator extends AbstractDynamicSnippetsGenerator param.name.wireValue === key); - if (parameter == null) { - throw this.newParameterNotRecognizedError(key); + this.errors.scope(key); + try { + const parameter = parameters.find((param) => param.name.wireValue === key); + if (parameter == null) { + throw this.newParameterNotRecognizedError(key); + } + // If this query parameter supports allow-multiple, the user-provided values + // must be wrapped in an array. + const typeInstanceValue = + this.isListTypeReference(parameter.typeReference) && !Array.isArray(value) ? [value] : value; + instances.push({ + name: parameter.name, + typeReference: parameter.typeReference, + value: typeInstanceValue + }); + } finally { + this.errors.unscope(); } - // If this query parameter supports allow-multiple, the user-provided values - // must be wrapped in an array. - const typeInstanceValue = - this.isListTypeReference(parameter.typeReference) && !Array.isArray(value) ? [value] : value; - instances.push({ - name: parameter.name.name, - typeReference: parameter.typeReference, - value: typeInstanceValue - }); } return instances; } @@ -72,27 +80,40 @@ export class DynamicSnippetsGeneratorContext { }): TypeInstance[] { const instances: TypeInstance[] = []; for (const [key, value] of Object.entries(values)) { - const parameter = parameters.find((param) => param.name.wireValue === key); - if (parameter == null) { - if (ignoreMissingParameters) { - // Required for request payloads that include more information than - // just the target parameters (e.g. union base properties). + this.errors.scope(key); + try { + const parameter = parameters.find((param) => param.name.wireValue === key); + if (parameter == null) { + if (ignoreMissingParameters) { + // Required for request payloads that include more information than + // just the target parameters (e.g. union base properties). + continue; + } + this.errors.add({ + severity: Severity.Critical, + message: this.newParameterNotRecognizedError(key).message + }); continue; } - throw this.newParameterNotRecognizedError(key); + instances.push({ + name: parameter.name, + typeReference: parameter.typeReference, + value + }); + } finally { + this.errors.unscope(); } - instances.push({ - name: parameter.name.name, - typeReference: parameter.typeReference, - value - }); } return instances; } - public getRecordOrThrow(value: unknown): Record { + public getRecord(value: unknown): Record | undefined { if (typeof value !== "object" || Array.isArray(value)) { - throw new Error(`Expected object with key, value pairs but got: ${JSON.stringify(value)}`); + this.errors.add({ + severity: Severity.Critical, + message: `Expected object with key, value pairs but got: ${JSON.stringify(value)}` + }); + return undefined; } if (value == null) { return {}; @@ -100,37 +121,54 @@ export class DynamicSnippetsGeneratorContext { return value as Record; } - public resolveNamedTypeOrThrow({ typeId }: { typeId: TypeId }): DynamicSnippets.NamedType { + public resolveNamedType({ typeId }: { typeId: TypeId }): DynamicSnippets.NamedType | undefined { const namedType = this.ir.types[typeId]; if (namedType == null) { - throw new Error(`Type identified by "${typeId}" could not be found`); + this.errors.add({ + severity: Severity.Critical, + message: `Type identified by "${typeId}" could not be found` + }); + return undefined; } return namedType; } - public resolveDiscriminatedUnionTypeInstanceOrThrow({ + public resolveDiscriminatedUnionTypeInstance({ discriminatedUnion, value }: { discriminatedUnion: DynamicSnippets.DiscriminatedUnionType; value: unknown; - }): DiscriminatedUnionTypeInstance { - const record = this.getRecordOrThrow(value); + }): DiscriminatedUnionTypeInstance | undefined { + const record = this.getRecord(value); + if (record == null) { + return undefined; + } const discriminantFieldName = discriminatedUnion.discriminant.wireValue; const discriminantValue = record[discriminantFieldName]; if (discriminantValue == null) { - throw new Error( - `Missing required discriminant field "${discriminantFieldName}" got ${JSON.stringify(value)}` - ); + this.errors.add({ + severity: Severity.Critical, + message: this.newParameterNotRecognizedError(discriminantFieldName).message + }); + return undefined; } if (typeof discriminantValue !== "string") { - throw new Error(`Expected discriminant value to be a string but got: ${JSON.stringify(discriminantValue)}`); + this.errors.add({ + severity: Severity.Critical, + message: `Expected discriminant value to be a string but got: ${JSON.stringify(discriminantValue)}` + }); + return undefined; } const singleDiscriminatedUnionType = discriminatedUnion.types[discriminantValue]; if (singleDiscriminatedUnionType == null) { - throw new Error(`No type found for discriminant value "${discriminantValue}"`); + this.errors.add({ + severity: Severity.Critical, + message: `No type found for discriminant value "${discriminantValue}"` + }); + return undefined; } // Remove the discriminant from the record so that the value is valid for the type. diff --git a/generators/go-v2/dynamic-snippets/src/context/DynamicTypeInstantiationMapper.ts b/generators/go-v2/dynamic-snippets/src/context/DynamicTypeInstantiationMapper.ts index 1ea71cc9f47..c1e0b7755e5 100644 --- a/generators/go-v2/dynamic-snippets/src/context/DynamicTypeInstantiationMapper.ts +++ b/generators/go-v2/dynamic-snippets/src/context/DynamicTypeInstantiationMapper.ts @@ -3,6 +3,7 @@ import { go } from "@fern-api/go-codegen"; import { DynamicSnippetsGeneratorContext } from "./DynamicSnippetsGeneratorContext"; import { dynamic as DynamicSnippets, PrimitiveTypeV1 } from "@fern-fern/ir-sdk/api"; import { DiscriminatedUnionTypeInstance } from "../DiscriminatedUnionTypeInstance"; +import { Severity } from "./ErrorReporter"; export declare namespace DynamicTypeInstantiationMapper { interface Args { @@ -35,7 +36,10 @@ export class DynamicTypeInstantiationMapper { case "map": return this.convertMap({ map: args.typeReference, value: args.value }); case "named": { - const named = this.context.resolveNamedTypeOrThrow({ typeId: args.typeReference.value }); + const named = this.context.resolveNamedType({ typeId: args.typeReference.value }); + if (named == null) { + return go.TypeInstantiation.nop(); + } return this.convertNamed({ named, value: args.value, as: args.as }); } case "optional": @@ -61,25 +65,47 @@ export class DynamicTypeInstantiationMapper { value: unknown; }): go.TypeInstantiation { if (!Array.isArray(value)) { - throw new Error(`Expected array but got: ${JSON.stringify(value)}`); + this.context.errors.add({ + severity: Severity.Critical, + message: `Expected array but got: ${JSON.stringify(value)}` + }); + return go.TypeInstantiation.nop(); } return go.TypeInstantiation.slice({ valueType: this.context.dynamicTypeMapper.convert({ typeReference: list }), - values: value.map((v) => this.convert({ typeReference: list, value: v })) + values: value.map((v, index) => { + this.context.errors.scope({ index }); + try { + return this.convert({ typeReference: list, value: v }); + } finally { + this.context.errors.unscope(); + } + }) }); } private convertMap({ map, value }: { map: DynamicSnippets.MapType; value: unknown }): go.TypeInstantiation { if (typeof value !== "object" || value == null) { - throw new Error(`Expected object but got: ${JSON.stringify(value)}`); + this.context.errors.add({ + severity: Severity.Critical, + message: `Expected object but got: ${JSON.stringify(value)}` + }); + return go.TypeInstantiation.nop(); } return go.TypeInstantiation.map({ keyType: this.context.dynamicTypeMapper.convert({ typeReference: map.key }), valueType: this.context.dynamicTypeMapper.convert({ typeReference: map.value }), - entries: Object.entries(value).map(([key, value]) => ({ - key: this.convert({ typeReference: map.key, value: key, as: "key" }), - value: this.convert({ typeReference: map.value, value }) - })) + entries: Object.entries(value).map(([key, value]) => { + this.context.errors.scope(key); + try { + return { + key: this.convert({ typeReference: map.key, value: key, as: "key" }), + value: this.convert({ typeReference: map.value, value }) + }; + } finally { + this.context.errors.unscope(); + } + }) }); } @@ -121,10 +147,13 @@ export class DynamicTypeInstantiationMapper { const structTypeReference = this.context.getGoTypeReferenceFromDeclaration({ declaration: discriminatedUnion.declaration }); - const discriminatedUnionTypeInstance = this.context.resolveDiscriminatedUnionTypeInstanceOrThrow({ + const discriminatedUnionTypeInstance = this.context.resolveDiscriminatedUnionTypeInstance({ discriminatedUnion, value }); + if (discriminatedUnionTypeInstance == null) { + return go.TypeInstantiation.nop(); + } const unionVariant = discriminatedUnionTypeInstance.singleDiscriminatedUnionType; const baseFields = this.getBaseFields({ discriminatedUnionTypeInstance, @@ -132,9 +161,12 @@ export class DynamicTypeInstantiationMapper { }); switch (unionVariant.type) { case "samePropertiesAsObject": { - const named = this.context.resolveNamedTypeOrThrow({ + const named = this.context.resolveNamedType({ typeId: unionVariant.typeId }); + if (named == null) { + return go.TypeInstantiation.nop(); + } return go.TypeInstantiation.structPointer({ typeReference: structTypeReference, fields: [ @@ -147,7 +179,10 @@ export class DynamicTypeInstantiationMapper { }); } case "singleProperty": { - const record = this.context.getRecordOrThrow(discriminatedUnionTypeInstance.value); + const record = this.context.getRecord(discriminatedUnionTypeInstance.value); + if (record == null) { + return go.TypeInstantiation.nop(); + } return go.TypeInstantiation.structPointer({ typeReference: structTypeReference, fields: [ @@ -188,16 +223,23 @@ export class DynamicTypeInstantiationMapper { }): go.StructField[] { const properties = this.context.associateByWireValue({ parameters: singleDiscriminatedUnionType.properties ?? [], - values: this.context.getRecordOrThrow(discriminatedUnionTypeInstance.value), + values: this.context.getRecord(discriminatedUnionTypeInstance.value) ?? {}, // We're only selecting the base properties here. The rest of the properties // are handled by the union variant. ignoreMissingParameters: true }); - return properties.map((property) => ({ - name: this.context.getTypeName(property.name), - value: this.convert(property) - })); + return properties.map((property) => { + this.context.errors.scope(property.name.wireValue); + try { + return { + name: this.context.getTypeName(property.name.name), + value: this.convert(property) + }; + } finally { + this.context.errors.unscope(); + } + }); } private convertObject({ @@ -209,36 +251,61 @@ export class DynamicTypeInstantiationMapper { }): go.TypeInstantiation { const properties = this.context.associateByWireValue({ parameters: object_.properties, - values: this.context.getRecordOrThrow(value) + values: this.context.getRecord(value) ?? {} }); return go.TypeInstantiation.structPointer({ typeReference: go.typeReference({ name: this.context.getTypeName(object_.declaration.name), importPath: this.context.getImportPath(object_.declaration.fernFilepath) }), - fields: properties.map((property) => ({ - name: this.context.getTypeName(property.name), - value: this.convert(property) - })) + fields: properties.map((property) => { + this.context.errors.scope(property.name.wireValue); + try { + return { + name: this.context.getTypeName(property.name.name), + value: this.convert(property) + }; + } finally { + this.context.errors.unscope(); + } + }) }); } private convertEnum({ enum_, value }: { enum_: DynamicSnippets.EnumType; value: unknown }): go.TypeInstantiation { + const name = this.getEnumValueName({ enum_, value }); + if (name == null) { + return go.TypeInstantiation.nop(); + } return go.TypeInstantiation.enum( go.typeReference({ - name: this.getEnumValueNameOrThrow({ enum_, value }), + name, importPath: this.context.getImportPath(enum_.declaration.fernFilepath) }) ); } - private getEnumValueNameOrThrow({ enum_, value }: { enum_: DynamicSnippets.EnumType; value: unknown }): string { + private getEnumValueName({ + enum_, + value + }: { + enum_: DynamicSnippets.EnumType; + value: unknown; + }): string | undefined { if (typeof value !== "string") { - throw new Error(`Expected enum value string, got: ${JSON.stringify(value)}`); + this.context.errors.add({ + severity: Severity.Critical, + message: `Expected enum value string, got: ${JSON.stringify(value)}` + }); + return undefined; } const enumValue = enum_.values.find((v) => v.wireValue === value); if (enumValue == null) { - throw new Error(`An enum value named "${value}" does not exist in this context`); + this.context.errors.add({ + severity: Severity.Critical, + message: `An enum value named "${value}" does not exist in this context` + }); + return undefined; } return `${this.context.getTypeName(enum_.declaration.name)}${this.context.getTypeName(enumValue.name)}`; } @@ -250,18 +317,25 @@ export class DynamicTypeInstantiationMapper { undicriminatedUnion: DynamicSnippets.UndiscriminatedUnionType; value: unknown; }): go.TypeInstantiation { - const { valueTypeReference, typeInstantiation } = this.findMatchingUndiscriminatedUnionType({ + const result = this.findMatchingUndiscriminatedUnionType({ undicriminatedUnion, value }); + if (result == null) { + return go.TypeInstantiation.nop(); + } + const fieldName = this.getUndiscriminatedUnionFieldName({ typeReference: result.valueTypeReference }); + if (fieldName == null) { + return go.TypeInstantiation.nop(); + } return go.TypeInstantiation.structPointer({ typeReference: this.context.getGoTypeReferenceFromDeclaration({ declaration: undicriminatedUnion.declaration }), fields: [ { - name: this.getUndiscriminatedUnionFieldName({ typeReference: valueTypeReference }), - value: typeInstantiation + name: fieldName, + value: result.typeInstantiation } ] }); @@ -273,7 +347,7 @@ export class DynamicTypeInstantiationMapper { }: { undicriminatedUnion: DynamicSnippets.UndiscriminatedUnionType; value: unknown; - }): { valueTypeReference: DynamicSnippets.TypeReference; typeInstantiation: go.TypeInstantiation } { + }): { valueTypeReference: DynamicSnippets.TypeReference; typeInstantiation: go.TypeInstantiation } | undefined { for (const typeReference of undicriminatedUnion.types) { try { const typeInstantiation = this.convert({ typeReference, value }); @@ -282,16 +356,18 @@ export class DynamicTypeInstantiationMapper { continue; } } - throw new Error( - `None of the types in the undicriminated union matched the given value: ${JSON.stringify(value)}` - ); + this.context.errors.add({ + severity: Severity.Critical, + message: `None of the types in the undicriminated union matched the given value: ${JSON.stringify(value)}` + }); + return undefined; } private getUndiscriminatedUnionFieldName({ typeReference }: { typeReference: DynamicSnippets.TypeReference; - }): string { + }): string | undefined { switch (typeReference.type) { case "list": return this.getUndiscriminatedUnionFieldNameForList({ list: typeReference }); @@ -300,7 +376,10 @@ export class DynamicTypeInstantiationMapper { case "map": return this.getUndiscriminatedUnionFieldNameForMap({ map: typeReference }); case "named": { - const named = this.context.resolveNamedTypeOrThrow({ typeId: typeReference.value }); + const named = this.context.resolveNamedType({ typeId: typeReference.value }); + if (named == null) { + return undefined; + } return this.context.getTypeName(named.declaration.name); } case "optional": @@ -314,29 +393,59 @@ export class DynamicTypeInstantiationMapper { } } - private getUndiscriminatedUnionFieldNameForList({ list }: { list: DynamicSnippets.TypeReference.List }): string { - return `${this.getUndiscriminatedUnionFieldName({ typeReference: list })}List`; + private getUndiscriminatedUnionFieldNameForList({ + list + }: { + list: DynamicSnippets.TypeReference.List; + }): string | undefined { + const fieldName = this.getUndiscriminatedUnionFieldName({ typeReference: list }); + if (fieldName == null) { + return undefined; + } + return `${fieldName}List`; } - private getUndiscriminatedUnionFieldNameForMap({ map }: { map: DynamicSnippets.MapType }): string { - return `${this.getUndiscriminatedUnionFieldName({ - typeReference: map.key - })}${this.getUndiscriminatedUnionFieldName({ typeReference: map.value })}Map`; + private getUndiscriminatedUnionFieldNameForMap({ map }: { map: DynamicSnippets.MapType }): string | undefined { + const keyFieldName = this.getUndiscriminatedUnionFieldName({ typeReference: map.key }); + if (keyFieldName == null) { + return undefined; + } + const valueFieldName = this.getUndiscriminatedUnionFieldName({ typeReference: map.value }); + if (valueFieldName == null) { + return undefined; + } + return `${keyFieldName}${valueFieldName}Map`; } private getUndiscriminatedUnionFieldNameForOptional({ optional }: { optional: DynamicSnippets.TypeReference.Optional; - }): string { - return `${this.getUndiscriminatedUnionFieldName({ typeReference: optional })}Optional`; + }): string | undefined { + const fieldName = this.getUndiscriminatedUnionFieldName({ typeReference: optional }); + if (fieldName == null) { + return undefined; + } + return `${fieldName}Optional`; } - private getUndiscriminatedUnionFieldNameForSet({ set }: { set: DynamicSnippets.TypeReference.Set }): string { - return `${this.getUndiscriminatedUnionFieldName({ typeReference: set })}Set`; + private getUndiscriminatedUnionFieldNameForSet({ + set + }: { + set: DynamicSnippets.TypeReference.Set; + }): string | undefined { + const fieldName = this.getUndiscriminatedUnionFieldName({ typeReference: set }); + if (fieldName == null) { + return undefined; + } + return `${fieldName}Set`; } - private getUndiscriminatedUnionFieldNameForLiteral({ literal }: { literal: DynamicSnippets.LiteralType }): string { + private getUndiscriminatedUnionFieldNameForLiteral({ + literal + }: { + literal: DynamicSnippets.LiteralType; + }): string | undefined { switch (literal.type) { case "boolean": return `${literal.value}BoolLiteral`; @@ -392,68 +501,126 @@ export class DynamicTypeInstantiationMapper { switch (primitive) { case "INTEGER": case "UINT": { - return go.TypeInstantiation.int(this.getValueAsNumberOrThrow({ value, as })); + const num = this.getValueAsNumber({ value, as }); + if (num == null) { + return go.TypeInstantiation.nop(); + } + return go.TypeInstantiation.int(num); } case "LONG": case "UINT_64": { - return go.TypeInstantiation.int64(this.getValueAsNumberOrThrow({ value, as })); + const num = this.getValueAsNumber({ value, as }); + if (num == null) { + return go.TypeInstantiation.nop(); + } + return go.TypeInstantiation.int64(num); } case "FLOAT": case "DOUBLE": { - return go.TypeInstantiation.float64(this.getValueAsNumberOrThrow({ value, as })); + const num = this.getValueAsNumber({ value, as }); + if (num == null) { + return go.TypeInstantiation.nop(); + } + return go.TypeInstantiation.float64(num); } case "BOOLEAN": { - return go.TypeInstantiation.bool(this.getValueAsBooleanOrThrow({ value, as })); + const bool = this.getValueAsBoolean({ value, as }); + if (bool == null) { + return go.TypeInstantiation.nop(); + } + return go.TypeInstantiation.bool(bool); + } + case "STRING": { + const str = this.getValueAsString({ value }); + if (str == null) { + return go.TypeInstantiation.nop(); + } + return go.TypeInstantiation.string(str); + } + case "DATE": { + const date = this.getValueAsString({ value }); + if (date == null) { + return go.TypeInstantiation.nop(); + } + return go.TypeInstantiation.date(date); + } + case "DATE_TIME": { + const dateTime = this.getValueAsString({ value }); + if (dateTime == null) { + return go.TypeInstantiation.nop(); + } + return go.TypeInstantiation.dateTime(dateTime); + } + case "UUID": { + const uuid = this.getValueAsString({ value }); + if (uuid == null) { + return go.TypeInstantiation.nop(); + } + return go.TypeInstantiation.uuid(uuid); + } + case "BASE_64": { + const base64 = this.getValueAsString({ value }); + if (base64 == null) { + return go.TypeInstantiation.nop(); + } + return go.TypeInstantiation.bytes(base64); + } + case "BIG_INTEGER": { + const bigInt = this.getValueAsString({ value }); + if (bigInt == null) { + return go.TypeInstantiation.nop(); + } + return go.TypeInstantiation.string(bigInt); } - case "STRING": - return go.TypeInstantiation.string(this.getValueAsStringOrThrow({ value })); - case "DATE": - return go.TypeInstantiation.date(this.getValueAsStringOrThrow({ value })); - case "DATE_TIME": - return go.TypeInstantiation.dateTime(this.getValueAsStringOrThrow({ value })); - case "UUID": - return go.TypeInstantiation.uuid(this.getValueAsStringOrThrow({ value })); - case "BASE_64": - return go.TypeInstantiation.bytes(this.getValueAsStringOrThrow({ value })); - case "BIG_INTEGER": - return go.TypeInstantiation.string(this.getValueAsStringOrThrow({ value })); default: assertNever(primitive); } } - private getValueAsNumberOrThrow({ + private getValueAsNumber({ value, as }: { value: unknown; as?: DynamicTypeInstantiationMapper.ConvertedAs; - }): number { + }): number | undefined { const num = as === "key" ? (typeof value === "string" ? Number(value) : value) : value; if (typeof num !== "number") { - throw this.newTypeMismatchError({ expected: "number", value }); + this.context.errors.add({ + severity: Severity.Critical, + message: this.newTypeMismatchError({ expected: "number", value }).message + }); + return undefined; } return num; } - private getValueAsBooleanOrThrow({ + private getValueAsBoolean({ value, as }: { value: unknown; as?: DynamicTypeInstantiationMapper.ConvertedAs; - }): boolean { + }): boolean | undefined { const bool = as === "key" ? (typeof value === "string" ? value === "true" : value === "false" ? false : value) : value; if (typeof bool !== "boolean") { - throw this.newTypeMismatchError({ expected: "boolean", value }); + this.context.errors.add({ + severity: Severity.Critical, + message: this.newTypeMismatchError({ expected: "boolean", value }).message + }); + return undefined; } return bool; } - private getValueAsStringOrThrow({ value }: { value: unknown }): string { + private getValueAsString({ value }: { value: unknown }): string | undefined { if (typeof value !== "string") { - throw this.newTypeMismatchError({ expected: "string", value }); + this.context.errors.add({ + severity: Severity.Critical, + message: this.newTypeMismatchError({ expected: "string", value }).message + }); + return undefined; } return value; } diff --git a/generators/go-v2/dynamic-snippets/src/context/DynamicTypeMapper.ts b/generators/go-v2/dynamic-snippets/src/context/DynamicTypeMapper.ts index 753261c427e..a67f9cc8668 100644 --- a/generators/go-v2/dynamic-snippets/src/context/DynamicTypeMapper.ts +++ b/generators/go-v2/dynamic-snippets/src/context/DynamicTypeMapper.ts @@ -2,6 +2,7 @@ import { assertNever } from "@fern-api/core-utils"; import { go } from "@fern-api/go-codegen"; import { DynamicSnippetsGeneratorContext } from "./DynamicSnippetsGeneratorContext"; import { dynamic as DynamicSnippets, PrimitiveTypeV1 } from "@fern-fern/ir-sdk/api"; +import { Severity } from "./ErrorReporter"; export declare namespace DynamicTypeMapper { interface Args { @@ -28,7 +29,10 @@ export class DynamicTypeMapper { this.convert({ typeReference: args.typeReference.value }) ); case "named": { - const named = this.context.resolveNamedTypeOrThrow({ typeId: args.typeReference.value }); + const named = this.context.resolveNamedType({ typeId: args.typeReference.value }); + if (named == null) { + return this.convertUnknown(); + } return this.convertNamed({ named }); } case "optional": diff --git a/generators/go-v2/dynamic-snippets/src/context/ErrorReporter.ts b/generators/go-v2/dynamic-snippets/src/context/ErrorReporter.ts index 071fbfae660..eea274510f6 100644 --- a/generators/go-v2/dynamic-snippets/src/context/ErrorReporter.ts +++ b/generators/go-v2/dynamic-snippets/src/context/ErrorReporter.ts @@ -1,14 +1,18 @@ +export const Severity = { + Critical: "CRITICAL", + Warning: "WARNING" +} as const; + export declare namespace ErrorReporter { type Path = readonly PathItem[]; - type PathItem = string | DetailedPathItem; + type PathItem = string | ArrayPathItem; - interface DetailedPathItem { - key: string; - arrayIndex?: number; - } + type Severity = typeof Severity[keyof typeof Severity]; - type Severity = "critical" | "warning"; + interface ArrayPathItem { + index: number; + } interface Error { path?: Path; @@ -19,21 +23,54 @@ export declare namespace ErrorReporter { export class ErrorReporter { private errors: ErrorReporter.Error[]; + private path: ErrorReporter.PathItem[]; constructor() { this.errors = []; + this.path = []; + } + + public add(err: Omit): void { + this.errors.push({ + ...err, + path: this.path + }); + } + + public scope(path: ErrorReporter.PathItem): void { + this.path.push(path); } - public addError(err: ErrorReporter.Error): void { - this.errors.push(err); + public unscope(): void { + this.path.pop(); } public getBySeverity(severity: ErrorReporter.Severity): ErrorReporter.Error[] { return this.errors.filter((err) => err.severity === severity); } + public empty(): boolean { + return this.errors.length === 0; + } + + public size(): number { + return this.errors.length; + } + + public clone(): ErrorReporter { + const clone = new ErrorReporter(); + clone.errors = [...this.errors]; + clone.path = [...this.path]; + return clone; + } + + public reset(): void { + this.errors = []; + this.path = []; + } + public reportAsString(err: ErrorReporter.Error): string { - if (err.path == null) { + if (err.path == null || err.path.length === 0) { return err.message; } return `${err.path.join(".")}: ${err.message}`; From d1ef4f8e012176fe341b2d38d3a7412cc1790729 Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Mon, 4 Nov 2024 18:22:17 -0500 Subject: [PATCH 02/10] Add dynamic error to IR --- .../definition/dynamic/snippets.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/ir-sdk/fern/apis/ir-types-latest/definition/dynamic/snippets.yml b/packages/ir-sdk/fern/apis/ir-types-latest/definition/dynamic/snippets.yml index af44595f191..ddb48eb3905 100644 --- a/packages/ir-sdk/fern/apis/ir-types-latest/definition/dynamic/snippets.yml +++ b/packages/ir-sdk/fern/apis/ir-types-latest/definition/dynamic/snippets.yml @@ -3,6 +3,20 @@ imports: endpoints: ./endpoints.yml types: + ErrorSeverity: + audiences: + - dynamic + enum: + - CRITICAL + - WARNING + + Error: + audiences: + - dynamic + properties: + severity: ErrorSeverity + message: string + Values: audiences: - dynamic @@ -31,5 +45,10 @@ types: - dynamic docs: | The user-facing response type containing the generated snippet. + + If there are any errors, the snippet will still sometimes represent a + partial and/or valid result. This is useful for rendering a snippet alongside + error messages the user can use to diagnose and resolve the problem. properties: snippet: string + errors: optional> From c2dffe1b3e5b44c89c82695e9b863a6ebba5c993 Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Mon, 4 Nov 2024 18:27:06 -0500 Subject: [PATCH 03/10] Update VERSION and fill out CHANGELOG.md --- .../ir-sdk/fern/apis/ir-types-latest/VERSION | 2 +- .../apis/ir-types-latest/changelog/CHANGELOG.md | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/ir-sdk/fern/apis/ir-types-latest/VERSION b/packages/ir-sdk/fern/apis/ir-types-latest/VERSION index d1206f7cbd3..1fbbd0efb72 100644 --- a/packages/ir-sdk/fern/apis/ir-types-latest/VERSION +++ b/packages/ir-sdk/fern/apis/ir-types-latest/VERSION @@ -1 +1 @@ -53.18.0 +53.19.0 diff --git a/packages/ir-sdk/fern/apis/ir-types-latest/changelog/CHANGELOG.md b/packages/ir-sdk/fern/apis/ir-types-latest/changelog/CHANGELOG.md index 5ad28e67319..7b8f45015cf 100644 --- a/packages/ir-sdk/fern/apis/ir-types-latest/changelog/CHANGELOG.md +++ b/packages/ir-sdk/fern/apis/ir-types-latest/changelog/CHANGELOG.md @@ -5,15 +5,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v53.19.0] - 2024-11-04 + +- Internal: Add errors property to dynamic `EndpointSnippetResponse`. + ## [v53.18.0] - 2024-11-04 - Internal: Add `transport` to `HttpEndpoint`. `transport` on the endpoint overrides the `transport` on the `HttpService`. -## [v53.15.0] - 2024-10-07 +## [v53.17.0] - 2024-11-01 + +- Internal: Add dynamic audience to endpoint snippet request and response. + +## [v53.16.0] - 2024-10-31 + +- Internal: Publish @fern-api/dynamic-ir-sdk + +## [v53.15.0] - 2024-10-23 - Internal: Introduce dynamic IR types. -## [v53.14.0] - 2024-10-07 +## [v53.14.0] - 2024-10-16 - Feature: Add `inline` to type declarations so that generators can nest unnamed types. From 7b74ed51534b4ed5e36be9c1b152ed9b23904cd1 Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Mon, 4 Nov 2024 18:36:25 -0500 Subject: [PATCH 04/10] Run 'pnpm ir:generate' --- .../snippets/types/EndpointSnippetResponse.ts | 7 +++++ .../resources/snippets/types/ErrorSeverity.ts | 28 +++++++++++++++++++ .../resources/snippets/types/Error_.ts | 10 +++++++ .../dynamic/resources/snippets/types/index.ts | 2 ++ .../snippets/types/EndpointSnippetResponse.ts | 3 ++ .../resources/snippets/types/ErrorSeverity.ts | 16 +++++++++++ .../resources/snippets/types/Error_.ts | 21 ++++++++++++++ .../dynamic/resources/snippets/types/index.ts | 2 ++ 8 files changed, 89 insertions(+) create mode 100644 packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/ErrorSeverity.ts create mode 100644 packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/Error_.ts create mode 100644 packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/ErrorSeverity.ts create mode 100644 packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/Error_.ts diff --git a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/EndpointSnippetResponse.ts b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/EndpointSnippetResponse.ts index 725b249483c..b9c13eda46e 100644 --- a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/EndpointSnippetResponse.ts +++ b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/EndpointSnippetResponse.ts @@ -2,9 +2,16 @@ * This file was auto-generated by Fern from our API Definition. */ +import * as FernIr from "../../../../../index"; + /** * The user-facing response type containing the generated snippet. + * + * If there are any errors, the snippet will still sometimes represent a + * partial and/or valid result. This is useful for rendering a snippet alongside + * error messages the user can use to diagnose and resolve the problem. */ export interface EndpointSnippetResponse { snippet: string; + errors: FernIr.dynamic.Error_[] | undefined; } diff --git a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/ErrorSeverity.ts b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/ErrorSeverity.ts new file mode 100644 index 00000000000..09ba4f5b4e5 --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/ErrorSeverity.ts @@ -0,0 +1,28 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export type ErrorSeverity = "CRITICAL" | "WARNING"; + +export const ErrorSeverity = { + Critical: "CRITICAL", + Warning: "WARNING", + _visit: (value: ErrorSeverity, visitor: ErrorSeverity.Visitor) => { + switch (value) { + case ErrorSeverity.Critical: + return visitor.critical(); + case ErrorSeverity.Warning: + return visitor.warning(); + default: + return visitor._other(); + } + }, +} as const; + +export declare namespace ErrorSeverity { + interface Visitor { + critical: () => R; + warning: () => R; + _other: () => R; + } +} diff --git a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/Error_.ts b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/Error_.ts new file mode 100644 index 00000000000..ce86dbe639b --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/Error_.ts @@ -0,0 +1,10 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as FernIr from "../../../../../index"; + +export interface Error_ { + severity: FernIr.dynamic.ErrorSeverity; + message: string; +} diff --git a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/index.ts b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/index.ts index bcb5d69b737..79f6ee55117 100644 --- a/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/index.ts +++ b/packages/ir-sdk/src/sdk/api/resources/dynamic/resources/snippets/types/index.ts @@ -1,3 +1,5 @@ +export * from "./ErrorSeverity"; +export * from "./Error_"; export * from "./Values"; export * from "./EndpointSnippetRequest"; export * from "./EndpointSnippetResponse"; diff --git a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/EndpointSnippetResponse.ts b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/EndpointSnippetResponse.ts index 15a733d5f6f..47ba2b40c01 100644 --- a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/EndpointSnippetResponse.ts +++ b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/EndpointSnippetResponse.ts @@ -5,16 +5,19 @@ import * as serializers from "../../../../../index"; import * as FernIr from "../../../../../../api/index"; import * as core from "../../../../../../core"; +import { Error_ } from "./Error_"; export const EndpointSnippetResponse: core.serialization.ObjectSchema< serializers.dynamic.EndpointSnippetResponse.Raw, FernIr.dynamic.EndpointSnippetResponse > = core.serialization.objectWithoutOptionalProperties({ snippet: core.serialization.string(), + errors: core.serialization.list(Error_).optional(), }); export declare namespace EndpointSnippetResponse { interface Raw { snippet: string; + errors?: Error_.Raw[] | null; } } diff --git a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/ErrorSeverity.ts b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/ErrorSeverity.ts new file mode 100644 index 00000000000..3c370ea92a3 --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/ErrorSeverity.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../../../index"; +import * as FernIr from "../../../../../../api/index"; +import * as core from "../../../../../../core"; + +export const ErrorSeverity: core.serialization.Schema< + serializers.dynamic.ErrorSeverity.Raw, + FernIr.dynamic.ErrorSeverity +> = core.serialization.enum_(["CRITICAL", "WARNING"]); + +export declare namespace ErrorSeverity { + type Raw = "CRITICAL" | "WARNING"; +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/Error_.ts b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/Error_.ts new file mode 100644 index 00000000000..ea316d80599 --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/Error_.ts @@ -0,0 +1,21 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../../../index"; +import * as FernIr from "../../../../../../api/index"; +import * as core from "../../../../../../core"; +import { ErrorSeverity } from "./ErrorSeverity"; + +export const Error_: core.serialization.ObjectSchema = + core.serialization.objectWithoutOptionalProperties({ + severity: ErrorSeverity, + message: core.serialization.string(), + }); + +export declare namespace Error_ { + interface Raw { + severity: ErrorSeverity.Raw; + message: string; + } +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/index.ts b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/index.ts index bcb5d69b737..79f6ee55117 100644 --- a/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/index.ts +++ b/packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/snippets/types/index.ts @@ -1,3 +1,5 @@ +export * from "./ErrorSeverity"; +export * from "./Error_"; export * from "./Values"; export * from "./EndpointSnippetRequest"; export * from "./EndpointSnippetResponse"; From b6561178572fc40ef21b286155a2419bdce53b17 Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Mon, 4 Nov 2024 18:50:55 -0500 Subject: [PATCH 05/10] Add errors property to response --- .../src/DynamicSnippetsGenerator.ts | 32 +++++++++++++------ .../src/context/ErrorReporter.ts | 12 +++++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/generators/go-v2/dynamic-snippets/src/DynamicSnippetsGenerator.ts b/generators/go-v2/dynamic-snippets/src/DynamicSnippetsGenerator.ts index 5a76a03b7ff..12d8ac3bde5 100644 --- a/generators/go-v2/dynamic-snippets/src/DynamicSnippetsGenerator.ts +++ b/generators/go-v2/dynamic-snippets/src/DynamicSnippetsGenerator.ts @@ -3,7 +3,7 @@ import { go } from "@fern-api/go-codegen"; import { DynamicSnippetsGeneratorContext } from "./context/DynamicSnippetsGeneratorContext"; import { dynamic as DynamicSnippets } from "@fern-fern/ir-sdk/api"; import { AbstractDynamicSnippetsGenerator } from "@fern-api/dynamic-snippets"; -import { ErrorReporter, Severity } from "./context/ErrorReporter"; +import { Severity } from "./context/ErrorReporter"; import { Scope } from "./Scope"; const SNIPPET_PACKAGE_NAME = "example"; @@ -11,6 +11,16 @@ const SNIPPET_IMPORT_PATH = "fern"; const SNIPPET_FUNC_NAME = "do"; const CLIENT_VAR_NAME = "client"; +// TODO(amckinney): Use the latest DynamicSnippets.EndpointSnippetResponse type directly when available. +interface EndpointSnippetResponse extends DynamicSnippets.EndpointSnippetResponse { + errors: + | { + severity: "CRITICAL" | "WARNING"; + message: string; + }[] + | undefined; +} + export class DynamicSnippetsGenerator extends AbstractDynamicSnippetsGenerator { private formatter: AbstractFormatter | undefined; @@ -27,18 +37,16 @@ export class DynamicSnippetsGenerator extends AbstractDynamicSnippetsGenerator { + public async generate(request: DynamicSnippets.EndpointSnippetRequest): Promise { const endpoints = this.context.resolveEndpointLocationOrThrow(request.endpoint); if (endpoints.length === 0) { - throw new Error(`No endpoints found for ${JSON.stringify(request.endpoint)}`); + throw new Error(`No endpoints found that match "${request.endpoint.method} ${request.endpoint.path}"`); } let bestReporter = this.context.errors.clone(); let bestSnippet: string | undefined; let err: Error | undefined; - for (const [index, endpoint] of endpoints.entries()) { + for (const endpoint of endpoints) { this.context.errors.reset(); try { const code = this.buildCodeBlock({ endpoint, snippet: request }); @@ -51,7 +59,8 @@ export class DynamicSnippetsGenerator extends AbstractDynamicSnippetsGenerator this.context.errors.size()) { @@ -66,11 +75,14 @@ export class DynamicSnippetsGenerator extends AbstractDynamicSnippetsGenerator ({ + severity: err.severity, + message: err.message + })); + } } From b629caadecaa4c231689abdaeb3057dc76791a4a Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Mon, 4 Nov 2024 18:58:47 -0500 Subject: [PATCH 06/10] Refactor all JSON.stringify error messages --- .../DynamicSnippetsGeneratorContext.ts | 8 ++-- .../context/DynamicTypeInstantiationMapper.ts | 41 +++++++++++-------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/generators/go-v2/dynamic-snippets/src/context/DynamicSnippetsGeneratorContext.ts b/generators/go-v2/dynamic-snippets/src/context/DynamicSnippetsGeneratorContext.ts index 72a988499d8..9c9575912ae 100644 --- a/generators/go-v2/dynamic-snippets/src/context/DynamicSnippetsGeneratorContext.ts +++ b/generators/go-v2/dynamic-snippets/src/context/DynamicSnippetsGeneratorContext.ts @@ -111,7 +111,9 @@ export class DynamicSnippetsGeneratorContext { if (typeof value !== "object" || Array.isArray(value)) { this.errors.add({ severity: Severity.Critical, - message: `Expected object with key, value pairs but got: ${JSON.stringify(value)}` + message: `Expected object with key, value pairs but got: ${ + Array.isArray(value) ? "array" : typeof value + }` }); return undefined; } @@ -157,7 +159,7 @@ export class DynamicSnippetsGeneratorContext { if (typeof discriminantValue !== "string") { this.errors.add({ severity: Severity.Critical, - message: `Expected discriminant value to be a string but got: ${JSON.stringify(discriminantValue)}` + message: `Expected discriminant value to be a string but got: ${typeof discriminantValue}` }); return undefined; } @@ -235,7 +237,7 @@ export class DynamicSnippetsGeneratorContext { } } if (endpoints.length === 0) { - throw new Error(`Failed to find endpoint identified by "${JSON.stringify(location)}"`); + throw new Error(`Failed to find endpoint identified by "${location.method} ${location.path}"`); } return endpoints; } diff --git a/generators/go-v2/dynamic-snippets/src/context/DynamicTypeInstantiationMapper.ts b/generators/go-v2/dynamic-snippets/src/context/DynamicTypeInstantiationMapper.ts index c1e0b7755e5..f156ec182e9 100644 --- a/generators/go-v2/dynamic-snippets/src/context/DynamicTypeInstantiationMapper.ts +++ b/generators/go-v2/dynamic-snippets/src/context/DynamicTypeInstantiationMapper.ts @@ -67,7 +67,7 @@ export class DynamicTypeInstantiationMapper { if (!Array.isArray(value)) { this.context.errors.add({ severity: Severity.Critical, - message: `Expected array but got: ${JSON.stringify(value)}` + message: `Expected array but got: ${typeof value}` }); return go.TypeInstantiation.nop(); } @@ -88,7 +88,7 @@ export class DynamicTypeInstantiationMapper { if (typeof value !== "object" || value == null) { this.context.errors.add({ severity: Severity.Critical, - message: `Expected object but got: ${JSON.stringify(value)}` + message: `Expected object but got: ${value == null ? "null" : typeof value}` }); return go.TypeInstantiation.nop(); } @@ -183,19 +183,24 @@ export class DynamicTypeInstantiationMapper { if (record == null) { return go.TypeInstantiation.nop(); } - return go.TypeInstantiation.structPointer({ - typeReference: structTypeReference, - fields: [ - { - name: this.context.getTypeName(unionVariant.discriminantValue.name), - value: this.convert({ - typeReference: unionVariant.typeReference, - value: record[unionVariant.discriminantValue.wireValue] - }) - }, - ...baseFields - ] - }); + try { + this.context.errors.scope(unionVariant.discriminantValue.wireValue); + return go.TypeInstantiation.structPointer({ + typeReference: structTypeReference, + fields: [ + { + name: this.context.getTypeName(unionVariant.discriminantValue.name), + value: this.convert({ + typeReference: unionVariant.typeReference, + value: record[unionVariant.discriminantValue.wireValue] + }) + }, + ...baseFields + ] + }); + } finally { + this.context.errors.unscope(); + } } case "noProperties": return go.TypeInstantiation.structPointer({ @@ -295,7 +300,7 @@ export class DynamicTypeInstantiationMapper { if (typeof value !== "string") { this.context.errors.add({ severity: Severity.Critical, - message: `Expected enum value string, got: ${JSON.stringify(value)}` + message: `Expected enum value string, got: ${typeof value}` }); return undefined; } @@ -358,7 +363,7 @@ export class DynamicTypeInstantiationMapper { } this.context.errors.add({ severity: Severity.Critical, - message: `None of the types in the undicriminated union matched the given value: ${JSON.stringify(value)}` + message: `None of the types in the undicriminated union matched the given "${typeof value}" value` }); return undefined; } @@ -626,6 +631,6 @@ export class DynamicTypeInstantiationMapper { } private newTypeMismatchError({ expected, value }: { expected: string; value: unknown }): Error { - return new Error(`Expected ${expected}, got: ${JSON.stringify(value)}`); + return new Error(`Expected ${expected}, got: ${typeof value}`); } } From 68842dbe5fdf92aa6d173c4c2abb7160cdfcfbd7 Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Mon, 4 Nov 2024 19:22:06 -0500 Subject: [PATCH 07/10] Add in test cases for error reporting --- .../src/DynamicSnippetsGenerator.ts | 8 +- .../__test__/__snapshots__/imdb.test.ts.snap | 51 ++++++++++++- .../src/__test__/imdb.test.ts | 75 ++++++++++++++++++- .../context/DynamicTypeInstantiationMapper.ts | 2 +- .../src/context/ErrorReporter.ts | 15 ++-- 5 files changed, 135 insertions(+), 16 deletions(-) diff --git a/generators/go-v2/dynamic-snippets/src/DynamicSnippetsGenerator.ts b/generators/go-v2/dynamic-snippets/src/DynamicSnippetsGenerator.ts index 12d8ac3bde5..6554c24cdef 100644 --- a/generators/go-v2/dynamic-snippets/src/DynamicSnippetsGenerator.ts +++ b/generators/go-v2/dynamic-snippets/src/DynamicSnippetsGenerator.ts @@ -3,7 +3,7 @@ import { go } from "@fern-api/go-codegen"; import { DynamicSnippetsGeneratorContext } from "./context/DynamicSnippetsGeneratorContext"; import { dynamic as DynamicSnippets } from "@fern-fern/ir-sdk/api"; import { AbstractDynamicSnippetsGenerator } from "@fern-api/dynamic-snippets"; -import { Severity } from "./context/ErrorReporter"; +import { ErrorReporter, Severity } from "./context/ErrorReporter"; import { Scope } from "./Scope"; const SNIPPET_PACKAGE_NAME = "example"; @@ -43,7 +43,7 @@ export class DynamicSnippetsGenerator extends AbstractDynamicSnippetsGenerator this.context.errors.size()) { + if (bestReporter == null || bestReporter.size() > this.context.errors.size()) { bestReporter = this.context.errors.clone(); bestSnippet = snippet; } @@ -73,7 +73,7 @@ export class DynamicSnippetsGenerator extends AbstractDynamicSnippetsGenerator 'GET /movies/{movieId} (simple)' 1`] = ` +exports[`imdb (errors) > invalid path parameter 1`] = ` +[ + { + "message": ""invalid" is not a recognized parameter for this endpoint", + "path": [ + "pathParameters", + "invalid", + ], + "severity": "CRITICAL", + }, +] +`; + +exports[`imdb (errors) > invalid request body 1`] = ` +[ + { + "message": ""invalid" is not a recognized parameter for this endpoint", + "path": [ + "requestBody", + "invalid", + ], + "severity": "CRITICAL", + }, +] +`; + +exports[`imdb (errors) > invalid request body property type 1`] = ` +[ + { + "message": "Expected string but got number", + "path": [ + "requestBody", + "title", + ], + "severity": "CRITICAL", + }, + { + "message": "Expected number but got string", + "path": [ + "requestBody", + "rating", + ], + "severity": "CRITICAL", + }, +] +`; + +exports[`imdb (success) > 'GET /movies/{movieId} (simple)' 1`] = ` "package example import ( @@ -23,7 +70,7 @@ func do() { " `; -exports[`imdb > 'POST /movies/create-movie (simple)' 1`] = ` +exports[`imdb (success) > 'POST /movies/create-movie (simple)' 1`] = ` "package example import ( diff --git a/generators/go-v2/dynamic-snippets/src/__test__/imdb.test.ts b/generators/go-v2/dynamic-snippets/src/__test__/imdb.test.ts index 4bdb76a6757..3a47c55dbb2 100644 --- a/generators/go-v2/dynamic-snippets/src/__test__/imdb.test.ts +++ b/generators/go-v2/dynamic-snippets/src/__test__/imdb.test.ts @@ -5,7 +5,7 @@ import { buildGeneratorConfig } from "./utils/buildGeneratorConfig"; import { AuthValues } from "@fern-fern/ir-sdk/api/resources/dynamic"; import { TestCase } from "./utils/TestCase"; -describe("imdb", () => { +describe("imdb (success)", () => { const testCases: TestCase[] = [ { description: "GET /movies/{movieId} (simple)", @@ -54,3 +54,76 @@ describe("imdb", () => { expect(response.snippet).toMatchSnapshot(); }); }); + +describe("imdb (errors)", () => { + it("invalid path parameter", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, RelativeFilePath.of("imdb.json")), + config: buildGeneratorConfig() + }); + const response = await generator.generate({ + endpoint: { + method: "GET", + path: "/movies/{movieId}" + }, + auth: AuthValues.bearer({ + token: "" + }), + pathParameters: { + invalid: "movie_xyz" + }, + queryParameters: undefined, + headers: undefined, + requestBody: undefined + }); + expect(response.errors).toMatchSnapshot(); + }); + + it("invalid request body", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, RelativeFilePath.of("imdb.json")), + config: buildGeneratorConfig() + }); + const response = await generator.generate({ + endpoint: { + method: "POST", + path: "/movies/create-movie" + }, + auth: AuthValues.bearer({ + token: "" + }), + pathParameters: undefined, + queryParameters: undefined, + headers: undefined, + requestBody: { + title: "The Matrix", + invalid: 8.2 + } + }); + expect(response.errors).toMatchSnapshot(); + }); + + it("invalid request body property type", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, RelativeFilePath.of("imdb.json")), + config: buildGeneratorConfig() + }); + const response = await generator.generate({ + endpoint: { + method: "POST", + path: "/movies/create-movie" + }, + auth: AuthValues.bearer({ + token: "" + }), + pathParameters: undefined, + queryParameters: undefined, + headers: undefined, + requestBody: { + title: 42, + rating: "8.2" + } + }); + expect(response.errors).toMatchSnapshot(); + }); +}); diff --git a/generators/go-v2/dynamic-snippets/src/context/DynamicTypeInstantiationMapper.ts b/generators/go-v2/dynamic-snippets/src/context/DynamicTypeInstantiationMapper.ts index f156ec182e9..2c2dd5d6e9b 100644 --- a/generators/go-v2/dynamic-snippets/src/context/DynamicTypeInstantiationMapper.ts +++ b/generators/go-v2/dynamic-snippets/src/context/DynamicTypeInstantiationMapper.ts @@ -631,6 +631,6 @@ export class DynamicTypeInstantiationMapper { } private newTypeMismatchError({ expected, value }: { expected: string; value: unknown }): Error { - return new Error(`Expected ${expected}, got: ${typeof value}`); + return new Error(`Expected ${expected} but got ${typeof value}`); } } diff --git a/generators/go-v2/dynamic-snippets/src/context/ErrorReporter.ts b/generators/go-v2/dynamic-snippets/src/context/ErrorReporter.ts index 0542caac79c..2f2b8575ed8 100644 --- a/generators/go-v2/dynamic-snippets/src/context/ErrorReporter.ts +++ b/generators/go-v2/dynamic-snippets/src/context/ErrorReporter.ts @@ -23,6 +23,7 @@ export declare namespace ErrorReporter { interface Error_ { severity: "CRITICAL" | "WARNING"; + path: string[] | undefined; message: string; } @@ -38,7 +39,7 @@ export class ErrorReporter { public add(err: Omit): void { this.errors.push({ ...err, - path: this.path + path: [...this.path] }); } @@ -74,17 +75,15 @@ export class ErrorReporter { this.path = []; } - public reportAsString(err: ErrorReporter.Error): string { - if (err.path == null || err.path.length === 0) { - return err.message; - } - return `${err.path.join(".")}: ${err.message}`; - } - public toDynamicSnippetErrors(): Error_[] { return this.errors.map((err) => ({ severity: err.severity, + path: err.path != null ? this.pathToStringArray(err.path) : undefined, message: err.message })); } + + private pathToStringArray(path: ErrorReporter.Path): string[] { + return path.map((item) => (typeof item === "string" ? item : `[${item.index}]`)); + } } From f709c8ec99fb044f8f6e5fb65b1337d1d36d2bab Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Mon, 4 Nov 2024 19:32:33 -0500 Subject: [PATCH 08/10] Add array test case --- .../__snapshots__/exhaustive.test.ts.snap | 32 ++++++++++++++++++ .../src/__test__/exhaustive.test.ts | 33 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/exhaustive.test.ts.snap b/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/exhaustive.test.ts.snap index f54553ca2ee..647fe419058 100644 --- a/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/exhaustive.test.ts.snap +++ b/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/exhaustive.test.ts.snap @@ -1,5 +1,37 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`exhaustive (errors) > invalid request body 1`] = ` +[ + { + "message": "Expected string but got boolean", + "path": [ + "requestBody", + "[0]", + "string", + ], + "severity": "CRITICAL", + }, + { + "message": ""invalid" is not a recognized parameter for this endpoint", + "path": [ + "requestBody", + "[1]", + "invalid", + ], + "severity": "CRITICAL", + }, + { + "message": "Expected string but got number", + "path": [ + "requestBody", + "[2]", + "string", + ], + "severity": "CRITICAL", + }, +] +`; + exports[`exhaustive > 'POST /container/list-of-objects (simp…' 1`] = ` "package example diff --git a/generators/go-v2/dynamic-snippets/src/__test__/exhaustive.test.ts b/generators/go-v2/dynamic-snippets/src/__test__/exhaustive.test.ts index 3ba50713e4f..f7495626497 100644 --- a/generators/go-v2/dynamic-snippets/src/__test__/exhaustive.test.ts +++ b/generators/go-v2/dynamic-snippets/src/__test__/exhaustive.test.ts @@ -59,3 +59,36 @@ describe("exhaustive", () => { expect(response.snippet).toMatchSnapshot(); }); }); + +describe("exhaustive (errors)", () => { + it("invalid request body", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, RelativeFilePath.of("exhaustive.json")), + config: buildGeneratorConfig() + }); + const response = await generator.generate({ + endpoint: { + method: "POST", + path: "/container/list-of-objects" + }, + auth: AuthValues.bearer({ + token: "" + }), + pathParameters: undefined, + queryParameters: undefined, + headers: undefined, + requestBody: [ + { + string: true + }, + { + invalid: "two" + }, + { + string: 42 + } + ] + }); + expect(response.errors).toMatchSnapshot(); + }); +}); From 8c4c4f9bf7165393a95d8059e4f58cb551bb2138 Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Mon, 4 Nov 2024 19:43:35 -0500 Subject: [PATCH 09/10] Improve array error syntax --- .../src/__test__/__snapshots__/exhaustive.test.ts.snap | 9 +++------ .../dynamic-snippets/src/context/ErrorReporter.ts | 10 +++++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/exhaustive.test.ts.snap b/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/exhaustive.test.ts.snap index 647fe419058..cac52cfe5be 100644 --- a/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/exhaustive.test.ts.snap +++ b/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/exhaustive.test.ts.snap @@ -5,8 +5,7 @@ exports[`exhaustive (errors) > invalid request body 1`] = ` { "message": "Expected string but got boolean", "path": [ - "requestBody", - "[0]", + "requestBody[0]", "string", ], "severity": "CRITICAL", @@ -14,8 +13,7 @@ exports[`exhaustive (errors) > invalid request body 1`] = ` { "message": ""invalid" is not a recognized parameter for this endpoint", "path": [ - "requestBody", - "[1]", + "requestBody[1]", "invalid", ], "severity": "CRITICAL", @@ -23,8 +21,7 @@ exports[`exhaustive (errors) > invalid request body 1`] = ` { "message": "Expected string but got number", "path": [ - "requestBody", - "[2]", + "requestBody[2]", "string", ], "severity": "CRITICAL", diff --git a/generators/go-v2/dynamic-snippets/src/context/ErrorReporter.ts b/generators/go-v2/dynamic-snippets/src/context/ErrorReporter.ts index 2f2b8575ed8..73cf131c5e2 100644 --- a/generators/go-v2/dynamic-snippets/src/context/ErrorReporter.ts +++ b/generators/go-v2/dynamic-snippets/src/context/ErrorReporter.ts @@ -84,6 +84,14 @@ export class ErrorReporter { } private pathToStringArray(path: ErrorReporter.Path): string[] { - return path.map((item) => (typeof item === "string" ? item : `[${item.index}]`)); + const result: string[] = []; + for (const item of path) { + if (typeof item === "string") { + result.push(item); + continue; + } + result[result.length - 1] += `[${item.index}]`; + } + return result; } } From ccbba4feb6e744cf55ce10da12b60bc0e7983461 Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Mon, 4 Nov 2024 19:48:29 -0500 Subject: [PATCH 10/10] Add large errors examples test case --- .../__snapshots__/examples.test.ts.snap | 13 ++++++ .../src/__test__/examples.test.ts | 40 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/examples.test.ts.snap b/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/examples.test.ts.snap index 2d79a7d664c..4559760aee0 100644 --- a/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/examples.test.ts.snap +++ b/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/examples.test.ts.snap @@ -1,5 +1,18 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`examples (errors) > invalid request body 1`] = ` +[ + { + "message": "Expected string but got number", + "path": [ + "requestBody", + "title", + ], + "severity": "CRITICAL", + }, +] +`; + exports[`examples > 'GET /metadata (allow-multiple)' 1`] = ` "package example diff --git a/generators/go-v2/dynamic-snippets/src/__test__/examples.test.ts b/generators/go-v2/dynamic-snippets/src/__test__/examples.test.ts index d0a87d2fab5..bb69719ef00 100644 --- a/generators/go-v2/dynamic-snippets/src/__test__/examples.test.ts +++ b/generators/go-v2/dynamic-snippets/src/__test__/examples.test.ts @@ -113,3 +113,43 @@ describe("examples", () => { expect(response.snippet).toMatchSnapshot(); }); }); + +describe("examples (errors)", () => { + it("invalid request body", async () => { + const generator = buildDynamicSnippetsGenerator({ + irFilepath: join(DYNAMIC_IR_TEST_DEFINITIONS_DIRECTORY, RelativeFilePath.of("examples.json")), + config: buildGeneratorConfig() + }); + const response = await generator.generate({ + endpoint: { + method: "POST", + path: "/movie" + }, + auth: AuthValues.bearer({ + token: "" + }), + pathParameters: undefined, + queryParameters: undefined, + headers: undefined, + requestBody: { + id: "movie-c06a4ad7", + prequel: "movie-cv9b914f", + title: 42, // invalid + from: "Hayao Miyazaki", + rating: 8.0, + type: "movie", + tag: "development", + metadata: { + actors: ["Christian Bale", "Florence Pugh", "Willem Dafoe"], + releaseDate: "2023-12-08", + ratings: { + rottenTomatoes: 97, + imdb: 7.6 + } + }, + revenue: 1000000 + } + }); + expect(response.errors).toMatchSnapshot(); + }); +});