Skip to content

Commit

Permalink
(fix, csharp): support List<OneOf> deserialization (#3745)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsinghvi authored May 31, 2024
1 parent e64f3ca commit 0436951
Show file tree
Hide file tree
Showing 244 changed files with 9,987 additions and 208 deletions.
4 changes: 3 additions & 1 deletion generators/csharp/codegen/src/AsIs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const STRING_ENUM_SERIALIZER_CLASS_NAME = "StringEnumSerializer";
export const ONE_OF_SERIALIZER_CLASS_NAME = "OneOfSerializer";
export const COLLECTION_ITEM_SERIALIZER_CLASS_NAME = "CollectionItemSerializer";

export enum AsIsFiles {
EnumConverter = "EnumConverter.Template.cs",
Expand All @@ -11,5 +12,6 @@ export enum AsIsFiles {
RawClient = "RawClient.Template.cs",
CiYaml = "github-ci.yml",
StringEnumSerializer = "StringEnumSerializer.cs",
OneOfSerializer = "OneOfSerializer.cs"
OneOfSerializer = "OneOfSerializer.cs",
CollectionItemSerializer = "CollectionItemSerializer.cs"
}
76 changes: 76 additions & 0 deletions generators/csharp/codegen/src/asIs/CollectionItemSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace <%= namespace%>;

/// <summary>
/// Json collection converter.
/// </summary>
/// <typeparam name="TDatatype">Type of item to convert.</typeparam>
/// <typeparam name="TConverterType">Converter to use for individual items.</typeparam>
public class CollectionItemSerializer<TDatatype, TConverterType> : JsonConverter<IEnumerable<TDatatype>>
where TConverterType : JsonConverter
{
/// <summary>
/// Reads a json string and deserializes it into an object.
/// </summary>
/// <param name="reader">Json reader.</param>
/// <param name="typeToConvert">Type to convert.</param>
/// <param name="options">Serializer options.</param>
/// <returns>Created object.</returns>
public override IEnumerable<TDatatype> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return default(IEnumerable<TDatatype>);
}

JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(options);
jsonSerializerOptions.Converters.Clear();
jsonSerializerOptions.Converters.Add(Activator.CreateInstance<TConverterType>());

List<TDatatype> returnValue = new List<TDatatype>();

while (reader.TokenType != JsonTokenType.EndArray)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
returnValue.Add((TDatatype)JsonSerializer.Deserialize(ref reader, typeof(TDatatype), jsonSerializerOptions));
}

reader.Read();
}

return returnValue;
}

/// <summary>
/// Writes a json string.
/// </summary>
/// <param name="writer">Json writer.</param>
/// <param name="value">Value to write.</param>
/// <param name="options">Serializer options.</param>
public override void Write(Utf8JsonWriter writer, IEnumerable<TDatatype> value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}

JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(options);
jsonSerializerOptions.Converters.Clear();
jsonSerializerOptions.Converters.Add(Activator.CreateInstance<TConverterType>());

writer.WriteStartArray();

foreach (TDatatype data in value)
{
JsonSerializer.Serialize(writer, data, jsonSerializerOptions);
}

writer.WriteEndArray();
}
}
17 changes: 16 additions & 1 deletion generators/csharp/codegen/src/ast/ClassReference.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { csharp } from "..";
import { AstNode } from "./core/AstNode";
import { Writer } from "./core/Writer";

Expand All @@ -7,22 +8,36 @@ export declare namespace ClassReference {
name: string;
/* The namespace of the C# class*/
namespace: string;
/* Any generics used in the class reference */
generics?: csharp.Type[];
}
}

export class ClassReference extends AstNode {
public readonly name: string;
public readonly namespace: string;
public readonly generics: csharp.Type[];

constructor({ name, namespace }: ClassReference.Args) {
constructor({ name, namespace, generics }: ClassReference.Args) {
super();
this.name = name;
this.namespace = namespace;
this.generics = generics ?? [];
}

public write(writer: Writer): void {
writer.addReference(this);
writer.write(`${this.name}`);
if (this.generics != null && this.generics.length > 0) {
writer.write("<");
this.generics.forEach((generic, idx) => {
writer.writeNode(generic);
if (idx < this.generics.length - 1) {
writer.write(", ");
}
});
writer.write(">");
}
}
}

Expand Down
5 changes: 2 additions & 3 deletions generators/csharp/codegen/src/ast/Type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class Type extends AstNode {
writer.write("object");
break;
case "list":
writer.write("List<");
writer.write("IEnumerable<");
this.internalType.value.write(writer);
writer.write(">");
break;
Expand All @@ -157,8 +157,7 @@ export class Type extends AstNode {
writer.write("?");
break;
case "reference":
writer.addReference(this.internalType.value);
writer.write(this.internalType.value.name);
writer.writeNode(this.internalType.value);
break;
case "coreReference":
writer.write(this.internalType.value.name);
Expand Down
22 changes: 22 additions & 0 deletions generators/csharp/codegen/src/ast/__test__/ClassReference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { csharp } from "../..";

describe("class reference", () => {
it("generics", async () => {
const clazz = csharp.classReference({
name: "OneOf",
namespace: "OneOf",
generics: [
csharp.Type.string(),
csharp.Type.boolean(),
csharp.Type.reference(
csharp.classReference({
namespace: "System",
name: "List",
generics: [csharp.Type.string()]
})
)
]
});
expect(clazz.toString()).toContain("OneOf<string, bool, List<string>>");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import {
} from "@fern-fern/ir-sdk/api";
import { camelCase, upperFirst } from "lodash-es";
import { csharp } from "..";
import { ONE_OF_SERIALIZER_CLASS_NAME, STRING_ENUM_SERIALIZER_CLASS_NAME } from "../AsIs";
import {
COLLECTION_ITEM_SERIALIZER_CLASS_NAME,
ONE_OF_SERIALIZER_CLASS_NAME,
STRING_ENUM_SERIALIZER_CLASS_NAME
} from "../AsIs";
import { BaseCsharpCustomConfigSchema } from "../custom-config/BaseCsharpCustomConfigSchema";
import { CsharpProject } from "../project";
import { CORE_DIRECTORY_NAME } from "../project/CsharpProject";
Expand Down Expand Up @@ -76,17 +80,30 @@ export abstract class AbstractCsharpGeneratorContext<
});
}

public getOneOfSerializerClassReference(): csharp.ClassReference {
public getCollectionItemSerializerReference(
itemType: csharp.ClassReference,
serializer: csharp.ClassReference
): csharp.ClassReference {
return csharp.classReference({
namespace: this.getCoreNamespace(),
name: ONE_OF_SERIALIZER_CLASS_NAME
name: COLLECTION_ITEM_SERIALIZER_CLASS_NAME,
generics: [csharp.Type.reference(itemType), csharp.Type.reference(serializer)]
});
}

public getOneOfClassReference(): csharp.ClassReference {
public getOneOfSerializerClassReference(oneof: csharp.ClassReference): csharp.ClassReference {
return csharp.classReference({
namespace: this.getCoreNamespace(),
name: ONE_OF_SERIALIZER_CLASS_NAME,
generics: [csharp.Type.reference(oneof)]
});
}

public getOneOfClassReference(generics: csharp.Type[]): csharp.ClassReference {
return csharp.classReference({
namespace: "OneOf",
name: "OneOf"
name: "OneOf",
generics
});
}

Expand All @@ -108,25 +125,35 @@ export abstract class AbstractCsharpGeneratorContext<

public getAsUndiscriminatedUnionTypeDeclaration(
reference: TypeReference
): UndiscriminatedUnionTypeDeclaration | undefined {
): { declaration: UndiscriminatedUnionTypeDeclaration; isList: boolean } | undefined {
if (reference.type === "container" && reference.container.type === "optional") {
return this.getAsUndiscriminatedUnionTypeDeclaration(reference.container.optional);
}
if (reference.type === "container" && reference.container.type === "list") {
const maybeDeclaration = this.getAsUndiscriminatedUnionTypeDeclaration(reference.container.list);
if (maybeDeclaration != null) {
return {
...maybeDeclaration,
isList: true
};
}
}

if (reference.type !== "named") {
return undefined;
}

let declaration = this.getTypeDeclarationOrThrow(reference.typeId);
if (declaration.shape.type === "undiscriminatedUnion") {
return declaration.shape;
return { declaration: declaration.shape, isList: false };
}

// handle aliases by visiting resolved types
if (declaration.shape.type === "alias") {
if (declaration.shape.resolvedType.type === "named") {
declaration = this.getTypeDeclarationOrThrow(reference.typeId);
if (declaration.shape.type === "undiscriminatedUnion") {
return declaration.shape;
return { declaration: declaration.shape, isList: false };
}
} else if (
declaration.shape.resolvedType.type === "container" &&
Expand Down
4 changes: 2 additions & 2 deletions generators/csharp/codegen/src/context/CsharpTypeMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ export class CsharpTypeMapper {
}): Type {
switch (container.type) {
case "list":
return Type.list(this.convert({ reference: container.list }));
return Type.list(this.convert({ reference: container.list, unboxOptionals: true }));
case "map":
return Type.map(
this.convert({ reference: container.keyType }),
this.convert({ reference: container.valueType })
);
case "set":
return Type.set(this.convert({ reference: container.set }));
return Type.set(this.convert({ reference: container.set, unboxOptionals: true }));
case "optional":
return unboxOptionals
? this.convert({ reference: container.optional, unboxOptionals })
Expand Down
2 changes: 1 addition & 1 deletion generators/csharp/codegen/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { AsIsFiles, ONE_OF_SERIALIZER_CLASS_NAME, STRING_ENUM_SERIALIZER_CLASS_NAME } from "./AsIs";
export * from "./AsIs";
export * as dependencies from "./ast/dependencies";
export * from "./cli";
export * as csharp from "./csharp";
Expand Down
2 changes: 1 addition & 1 deletion generators/csharp/model/src/ModelGeneratorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ export class ModelGeneratorContext extends AbstractCsharpGeneratorContext<ModelC
}

public getAsIsFiles(): string[] {
return [AsIsFiles.StringEnumSerializer, AsIsFiles.OneOfSerializer];
return [AsIsFiles.StringEnumSerializer, AsIsFiles.OneOfSerializer, AsIsFiles.CollectionItemSerializer];
}
}
3 changes: 2 additions & 1 deletion generators/csharp/model/src/object/ObjectGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export class ObjectGenerator extends FileGenerator<CSharpFile, ModelCustomConfig
annotations.push(
getUndiscriminatedUnionSerializerAnnotation({
context: this.context,
undiscriminatedUnionDeclaration: maybeUndiscriminatedUnion
undiscriminatedUnionDeclaration: maybeUndiscriminatedUnion.declaration,
isList: maybeUndiscriminatedUnion.isList
})
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,68 @@ import { UndiscriminatedUnionTypeDeclaration } from "@fern-fern/ir-sdk/api";
*/
export function getUndiscriminatedUnionSerializerAnnotation({
context,
undiscriminatedUnionDeclaration
undiscriminatedUnionDeclaration,
isList
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: AbstractCsharpGeneratorContext<any>;
undiscriminatedUnionDeclaration: UndiscriminatedUnionTypeDeclaration;
isList: boolean;
}): csharp.Annotation {
if (isList) {
return csharp.annotation({
reference: csharp.classReference({
name: "JsonConverter",
namespace: "System.Text.Json.Serialization"
}),
argument: csharp.codeblock((writer) => {
writer.write("typeof(");

const oneOf = getOneOf({ context, undiscriminatedUnionDeclaration });
const oneOfSerializer = getOneOfSerializer({ context, undiscriminatedUnionDeclaration });
const collectionSerializer = context.getCollectionItemSerializerReference(oneOf, oneOfSerializer);
writer.writeNode(collectionSerializer);
writer.write(")");
})
});
}
return csharp.annotation({
reference: csharp.classReference({
name: "JsonConverter",
namespace: "System.Text.Json.Serialization"
}),
argument: csharp.codeblock((writer) => {
writer.write("typeof(");

writer.writeNode(context.getOneOfSerializerClassReference());
writer.write("<");

writer.writeNode(context.getOneOfClassReference());
writer.write("<");
undiscriminatedUnionDeclaration.members.forEach((member, idx) => {
const type = context.csharpTypeMapper.convert({ reference: member.type, unboxOptionals: true });
writer.writeNode(type);
if (idx < undiscriminatedUnionDeclaration.members.length - 1) {
writer.write(", ");
}
});
writer.write(">");

writer.write(">");

const oneOfSerializer = getOneOfSerializer({ context, undiscriminatedUnionDeclaration });
writer.writeNode(oneOfSerializer);
writer.write(")");
})
});
}

function getOneOfSerializer({
context,
undiscriminatedUnionDeclaration
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: AbstractCsharpGeneratorContext<any>;
undiscriminatedUnionDeclaration: UndiscriminatedUnionTypeDeclaration;
}): csharp.ClassReference {
const oneOf = getOneOf({ context, undiscriminatedUnionDeclaration });
return context.getOneOfSerializerClassReference(oneOf);
}

function getOneOf({
context,
undiscriminatedUnionDeclaration
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: AbstractCsharpGeneratorContext<any>;
undiscriminatedUnionDeclaration: UndiscriminatedUnionTypeDeclaration;
}): csharp.ClassReference {
return context.getOneOfClassReference(
undiscriminatedUnionDeclaration.members.map((member) => {
return context.csharpTypeMapper.convert({ reference: member.type, unboxOptionals: true });
})
);
}
Loading

0 comments on commit 0436951

Please sign in to comment.