Skip to content

Commit

Permalink
Merge pull request #3862 from microsoft/enumOptionsTs
Browse files Browse the repository at this point in the history
Express Enums better in typescript
  • Loading branch information
koros authored Dec 8, 2023
2 parents 19bbf80 + 38894b6 commit a1f7418
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 44 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Fixed a bug in the vscode extension where the "Paste API Manifest" button would not be able to parse the manifest.
- Enhance the way Enums are expressed in Typescript. [#2105](https://github.com/microsoft/kiota/issues/2105)

## [1.9.0] - 2023-12-07

Expand Down
15 changes: 13 additions & 2 deletions src/Kiota.Builder/CodeDOM/CodeConstant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Kiota.Builder.CodeDOM;

public class CodeConstant : CodeTerminalWithKind<CodeConstantKind>
{
public CodeInterface? OriginalInterface
public CodeElement? OriginalCodeElement
{
get;
set;
Expand All @@ -20,11 +20,22 @@ public CodeInterface? OriginalInterface
{
Name = $"{source.Name.ToFirstCharacterLowerCase()}Mapper",
Kind = CodeConstantKind.QueryParametersMapper,
OriginalInterface = source,
OriginalCodeElement = source,
};
}
public static CodeConstant? FromCodeEnum(CodeEnum source)
{
ArgumentNullException.ThrowIfNull(source);
return new CodeConstant
{
Name = $"{source.Name.ToFirstCharacterLowerCase()}Object",
Kind = CodeConstantKind.EnumObject,
OriginalCodeElement = source,
};
}
}
public enum CodeConstantKind
{
QueryParametersMapper,
EnumObject,
}
7 changes: 6 additions & 1 deletion src/Kiota.Builder/CodeDOM/CodeEnum.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

Expand Down Expand Up @@ -36,4 +37,8 @@ public DeprecationInformation? Deprecation
{
get; set;
}
public CodeConstant? CodeEnumObject
{
get; set;
}
}
48 changes: 46 additions & 2 deletions src/Kiota.Builder/Refiners/TypeScriptRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
generatedCode,
factoryNameCallbackFromType
);

AddStaticMethodsUsingsForRequestExecutor(
generatedCode,
factoryNameCallbackFromType
Expand All @@ -159,6 +160,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
],
static s => s.ToCamelCase(UnderscoreArray));
IntroducesInterfacesAndFunctions(generatedCode, factoryNameCallbackFromType);
GenerateEnumObjects(generatedCode);
AliasUsingsWithSameSymbol(generatedCode);
var modelsNamespace = generatedCode.FindOrAddNamespace(_configuration.ModelsNamespaceName); // ensuring we have a models namespace in case we don't have any reusable model
GenerateReusableModelsCodeFiles(modelsNamespace);
Expand All @@ -168,6 +170,11 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
cancellationToken.ThrowIfCancellationRequested();
}, cancellationToken);
}
private static void GenerateEnumObjects(CodeElement currentElement)
{
AddEnumObject(currentElement);
AddEnumObjectUsings(currentElement);
}
private const string FileNameForModels = "index";
private static void GroupReusableModelsInSingleFile(CodeElement currentElement)
{
Expand All @@ -182,7 +189,10 @@ private static void GroupReusableModelsInSingleFile(CodeElement currentElement)
}
if (codeNamespace.Enums.ToArray() is { Length: > 0 } enums)
{
var enumObjects = enums.Select(static x => x.CodeEnumObject).OfType<CodeConstant>().ToArray();
targetFile.AddElements(enumObjects);
targetFile.AddElements(enums);
codeNamespace.RemoveChildElement(enumObjects);
codeNamespace.RemoveChildElement(enums);
}
RemoveSelfReferencingUsingForFile(targetFile, codeNamespace);
Expand Down Expand Up @@ -232,7 +242,6 @@ codeFunction.OriginalLocalMethod.Kind is CodeMethodKind.Factory &&
return null;
return codeNamespace.TryAddCodeFile(codeInterface.Name, [codeInterface, .. functions]);
}

private static void GenerateRequestBuilderCodeFile(CodeClass codeClass, CodeNamespace codeNamespace)
{
var executorMethods = codeClass.Methods
Expand All @@ -243,6 +252,11 @@ private static void GenerateRequestBuilderCodeFile(CodeClass codeClass, CodeName
.Enums
.ToArray();

var enumObjects = inlineEnums
.Select(static x => x.CodeEnumObject)
.OfType<CodeConstant>()
.ToArray();

var queryParameterInterfaces = executorMethods
.SelectMany(static x => x.Parameters)
.Where(static x => x.IsOfKind(CodeParameterKind.RequestConfiguration))
Expand Down Expand Up @@ -272,6 +286,7 @@ private static void GenerateRequestBuilderCodeFile(CodeClass codeClass, CodeName
.Union(queryParametersMapperConstants)
.Union(inlineRequestAndResponseBodyFiles.SelectMany(static x => x.GetChildElements(true)))
.Union(inlineEnums)
.Union(enumObjects)
.Distinct()
.ToArray();

Expand Down Expand Up @@ -561,7 +576,6 @@ codeClass.Parent is CodeClass parentClass &&
var props = codeClass.Properties.ToArray();
codeInterface.AddProperty(props);


if (CodeConstant.FromQueryParametersMapping(codeInterface) is CodeConstant constant)
targetNS.AddConstant(constant);

Expand Down Expand Up @@ -1015,6 +1029,36 @@ private static void SetUsingsOfPropertyInSerializationFunctions(string propertyS
}
}

protected static void AddEnumObject(CodeElement currentElement)
{
if (currentElement is CodeEnum codeEnum && CodeConstant.FromCodeEnum(codeEnum) is CodeConstant constant)
{
codeEnum.CodeEnumObject = constant;
var nameSpace = codeEnum.GetImmediateParentOfType<CodeNamespace>();
nameSpace.AddConstant(constant);
}
CrawlTree(currentElement, AddEnumObject);
}

protected static void AddEnumObjectUsings(CodeElement currentElement)
{
if (currentElement is CodeFunction codeFunction && codeFunction.OriginalLocalMethod.IsOfKind(CodeMethodKind.Deserializer, CodeMethodKind.Serializer))
{
foreach (var propertyEnum in codeFunction.OriginalMethodParentClass.Properties.Select(static x => x.Type).OfType<CodeType>().Select(static x => x.TypeDefinition).OfType<CodeEnum>())
{
codeFunction.AddUsing(new CodeUsing
{
Name = propertyEnum.Name,
Declaration = new CodeType
{
TypeDefinition = propertyEnum.CodeEnumObject
}
});
}
}
CrawlTree(currentElement, AddEnumObjectUsings);
}

private static void ProcessModelClassProperties(CodeClass modelClass, CodeInterface modelInterface, IEnumerable<CodeProperty> properties, Func<CodeClass, string> interfaceNamingCallback)
{
/*
Expand Down
2 changes: 1 addition & 1 deletion src/Kiota.Builder/Writers/TypeScript/CodeBlockEndWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public override void WriteCodeElement(BlockEnd codeElement, LanguageWriter write
{
ArgumentNullException.ThrowIfNull(codeElement);
ArgumentNullException.ThrowIfNull(writer);
if (codeElement.Parent is CodeNamespace) return;
if (codeElement.Parent is CodeNamespace or CodeEnum) return;
writer.CloseBlock();
if (codeElement.Parent?.Parent is CodeNamespace)
conventions.WriteAutoGeneratedEnd(writer);
Expand Down
32 changes: 30 additions & 2 deletions src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using Kiota.Builder.CodeDOM;
using Kiota.Builder.Extensions;
using Kiota.Builder.Writers.Go;

namespace Kiota.Builder.Writers.TypeScript;
public class CodeConstantWriter : BaseElementWriter<CodeConstant, TypeScriptConventionService>
Expand All @@ -11,9 +12,22 @@ public override void WriteCodeElement(CodeConstant codeElement, LanguageWriter w
{
ArgumentNullException.ThrowIfNull(codeElement);
ArgumentNullException.ThrowIfNull(writer);
if (codeElement.OriginalInterface is null) throw new InvalidOperationException("Original interface cannot be null");
if (codeElement.OriginalCodeElement is null) throw new InvalidOperationException("Original CodeElement cannot be null");
switch (codeElement.Kind)
{
case CodeConstantKind.QueryParametersMapper:
WriteQueryParametersMapperConstant(codeElement, writer);
break;
case CodeConstantKind.EnumObject:
WriteEnumObjectConstant(codeElement, writer);
break;
}
}
private static void WriteQueryParametersMapperConstant(CodeConstant codeElement, LanguageWriter writer)
{
if (codeElement.OriginalCodeElement is not CodeInterface codeInterface) throw new InvalidOperationException("Original CodeElement cannot be null");
writer.StartBlock($"const {codeElement.Name.ToFirstCharacterLowerCase()}: Record<string, string> = {{");
foreach (var property in codeElement.OriginalInterface
foreach (var property in codeInterface
.Properties
.OfKind(CodePropertyKind.QueryParameter)
.Where(static x => !string.IsNullOrEmpty(x.SerializationName))
Expand All @@ -23,4 +37,18 @@ public override void WriteCodeElement(CodeConstant codeElement, LanguageWriter w
}
writer.CloseBlock("};");
}
private void WriteEnumObjectConstant(CodeConstant codeElement, LanguageWriter writer)
{
if (codeElement.OriginalCodeElement is not CodeEnum codeEnum) throw new InvalidOperationException("Original CodeElement cannot be null");
if (!codeEnum.Options.Any())
return;
conventions.WriteLongDescription(codeEnum, writer);
writer.StartBlock($"export const {codeElement.Name.ToFirstCharacterUpperCase()} = {{");
codeEnum.Options.ToList().ForEach(x =>
{
conventions.WriteShortDescription(x.Documentation.Description, writer);
writer.WriteLine($"{x.Name.ToFirstCharacterUpperCase()}: \"{x.WireName}\",");
});
writer.CloseBlock("} as const;");
}
}
12 changes: 3 additions & 9 deletions src/Kiota.Builder/Writers/TypeScript/CodeEnumWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,10 @@ public override void WriteCodeElement(CodeEnum codeElement, LanguageWriter write
{
ArgumentNullException.ThrowIfNull(codeElement);
ArgumentNullException.ThrowIfNull(writer);
ArgumentNullException.ThrowIfNull(codeElement.CodeEnumObject);
if (!codeElement.Options.Any())
return;

conventions.WriteLongDescription(codeElement, writer);
writer.WriteLine($"export enum {codeElement.Name.ToFirstCharacterUpperCase()} {{");
writer.IncreaseIndent();
codeElement.Options.ToList().ForEach(x =>
{
conventions.WriteShortDescription(x.Documentation.Description, writer);
writer.WriteLine($"{x.Name.ToFirstCharacterUpperCase()} = \"{x.WireName}\",");
});
var enumObjectName = codeElement.CodeEnumObject.Name.ToFirstCharacterUpperCase();
writer.WriteLine($"export type {codeElement.Name.ToFirstCharacterUpperCase()} = (typeof {enumObjectName})[keyof typeof {enumObjectName}];");
}
}
4 changes: 2 additions & 2 deletions src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ private string GetDeserializationMethodName(CodeTypeBase propType, CodeFunction
var propertyType = conventions.GetTypeString(propType, codeFunction, false);
if (!string.IsNullOrEmpty(propertyType) && propType is CodeType currentType)
{
if (currentType.TypeDefinition is CodeEnum currentEnum)
return $"{(currentEnum.Flags || isCollection ? "getCollectionOfEnumValues" : "getEnumValue")}<{currentEnum.Name.ToFirstCharacterUpperCase()}>({propertyType.ToFirstCharacterUpperCase()})";
if (currentType.TypeDefinition is CodeEnum currentEnum && currentEnum.CodeEnumObject is not null)
return $"{(currentEnum.Flags || isCollection ? "getCollectionOfEnumValues" : "getEnumValue")}<{currentEnum.Name.ToFirstCharacterUpperCase()}>({currentEnum.CodeEnumObject.Name.ToFirstCharacterUpperCase()})";
else if (conventions.StreamTypeName.Equals(propertyType, StringComparison.OrdinalIgnoreCase))
return "getByteArrayValue";
else if (isCollection)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using System.IO;
using System.Linq;
using Kiota.Builder.CodeDOM;
using Kiota.Builder.Writers;
using Kiota.Builder.Writers.Go;
using Kiota.Builder.Writers.TypeScript;

using Xunit;

namespace Kiota.Builder.Tests.Writers.TypeScript;
public sealed class CodeConstantWriterTests : IDisposable
{
private const string DefaultPath = "./";
private const string DefaultName = "name";
private readonly StringWriter tw;
private readonly LanguageWriter writer;
private readonly CodeEnum currentEnum;
private const string EnumName = "someEnum";
private readonly CodeConstantWriter codeConstantWriter;

public CodeConstantWriterTests()
{
writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.TypeScript, DefaultPath, DefaultName);
codeConstantWriter = new CodeConstantWriter(new());
tw = new StringWriter();
writer.SetTextWriter(tw);
var root = CodeNamespace.InitRootNamespace();
currentEnum = root.AddEnum(new CodeEnum
{
Name = EnumName,
}).First();
if (CodeConstant.FromCodeEnum(currentEnum) is CodeConstant constant)
{
currentEnum.CodeEnumObject = constant;
root.AddConstant(constant);
}
}
public void Dispose()
{
tw?.Dispose();
GC.SuppressFinalize(this);
}

[Fact]
public void WriteCodeElement_ThrowsException_WhenCodeElementIsNull()
{
Assert.Throws<ArgumentNullException>(() => codeConstantWriter.WriteCodeElement(null, writer));
}

[Fact]
public void WriteCodeElement_ThrowsException_WhenWriterIsNull()
{
var codeElement = new CodeConstant();
Assert.Throws<ArgumentNullException>(() => codeConstantWriter.WriteCodeElement(codeElement, null));
}

[Fact]
public void WriteCodeElement_ThrowsException_WhenOriginalCodeElementIsNull()
{
var codeElement = new CodeConstant();
Assert.Throws<InvalidOperationException>(() => codeConstantWriter.WriteCodeElement(codeElement, writer));
}

[Fact]
public void WritesEnumOptionDescription()
{
var option = new CodeEnumOption
{
Documentation = new()
{
Description = "Some option description",
},
Name = "option1",
};
currentEnum.AddOption(option);
codeConstantWriter.WriteCodeElement(currentEnum.CodeEnumObject, writer);
var result = tw.ToString();
Assert.Contains($"/** {option.Documentation.Description} */", result);
AssertExtensions.CurlyBracesAreClosed(result, 0);
}

[Fact]
public void WritesEnum()
{
const string optionName = "option1";
currentEnum.AddOption(new CodeEnumOption { Name = optionName });
codeConstantWriter.WriteCodeElement(currentEnum.CodeEnumObject, writer);
var result = tw.ToString();
Assert.Contains("export const SomeEnumObject = {", result);
Assert.Contains("Option1: \"option1\"", result);
Assert.Contains("as const;", result);
Assert.Contains(optionName, result);
AssertExtensions.CurlyBracesAreClosed(result, 0);
}

[Fact]
public void DoesntWriteAnythingOnNoOption()
{
codeConstantWriter.WriteCodeElement(currentEnum.CodeEnumObject, writer);
var result = tw.ToString();
Assert.Empty(result);
}
}
Loading

0 comments on commit a1f7418

Please sign in to comment.