Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typescript - introducing index files namespace wise fixing circular dependency #1274

Merged
merged 34 commits into from
Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b244b60
record requestoption
nikithauc Feb 11, 2022
bc3d467
adding typescript namespace gen
nikithauc Feb 22, 2022
5c559d8
Merge branch 'main' into nikithauc/test-index
nikithauc Feb 22, 2022
464a8f0
adding gets enums to namespace
nikithauc Feb 22, 2022
8b5968b
Merge branch 'main' into nikithauc/test-index
nikithauc Feb 24, 2022
7ae65d4
recursive inheritance ordering
nikithauc Feb 25, 2022
bbbafed
checking namespace of parent
nikithauc Feb 25, 2022
c893bfa
adding rendering condition
nikithauc Feb 26, 2022
cd640c7
adding comments
nikithauc Feb 28, 2022
b1ce0a2
- removes testing file
baywet Feb 28, 2022
cf0aa36
adding language specific code renderer
nikithauc Mar 2, 2022
b220ecc
Merge branch 'nikithauc/test-index' of https://github.com/microsoft/k…
nikithauc Mar 2, 2022
cc6ac73
Merge branch 'main' into nikithauc/test-index
baywet Mar 2, 2022
c42dbe2
relativeimport language specific, model only barrel
nikithauc Mar 4, 2022
29bdb85
Merge branch 'main' into nikithauc/test-index
nikithauc Mar 16, 2022
f5cceba
extending relativeimportmanager; relative imports only for class
nikithauc Mar 16, 2022
bf7dc95
adding unit test for codenamespace writer
nikithauc Mar 17, 2022
ea58a17
resetting files
nikithauc Mar 17, 2022
c8672fe
resetting launcsettings
nikithauc Mar 17, 2022
d8dc928
ussing addinnerclass for test
nikithauc Mar 17, 2022
285506d
Apply suggestions from code review
nikithauc Mar 17, 2022
c2e6306
reset relativeimport code, update working test
nikithauc Mar 17, 2022
40e82fe
Merge branch 'nikithauc/test-index' of https://github.com/microsoft/k…
nikithauc Mar 17, 2022
305b819
missing using
nikithauc Mar 17, 2022
e354f9d
Update tests/Kiota.Builder.Tests/Writers/TypeScript/CodeNamespaceWrit…
nikithauc Mar 18, 2022
43bf414
Apply suggestions from code review
baywet Mar 18, 2022
859fb7d
Merge branch 'main' into nikithauc/test-index
nikithauc Mar 18, 2022
afa0305
- fixes a bug wwhere the root namespace would have a null name causin…
baywet Mar 18, 2022
69156a1
- code linting
baywet Mar 18, 2022
5650584
Merge branch 'main' into nikithauc/test-index
nikithauc Mar 18, 2022
1e0af62
renaming relativeimportmanager test
nikithauc Mar 21, 2022
4e0892f
Update CHANGELOG.md
nikithauc Mar 21, 2022
f3804be
- moves the changelog entry to the right place
baywet Mar 21, 2022
7998bec
Merge branch 'main' into nikithauc/test-index
baywet Mar 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed a bug where union types with inline schema member types would fail to generate #1270
- Fixed a bug where referenced types with no titles would fail to generate #1271
- Fixed a bug where the generator would introduce unnecessary union types for nullables. #990
- TypeScript adding index exporting models to fix #870.
baywet marked this conversation as resolved.
Show resolved Hide resolved
- Moved all the dotnet libraries to their own repository. #1409

## [0.0.18] - 2022-03-14
Expand Down
12 changes: 7 additions & 5 deletions src/Kiota.Builder/CodeDOM/CodeNamespace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ public class CodeNamespace : CodeBlock<BlockDeclaration, BlockEnd>
{
private CodeNamespace():base() {}
public static CodeNamespace InitRootNamespace() {
return new CodeNamespace();
return new() {
Name = string.Empty,
};
}
private string name;
public override string Name
Expand Down Expand Up @@ -60,19 +62,19 @@ public CodeNamespace FindNamespaceByName(string nsName) {
public CodeNamespace AddNamespace(string namespaceName) {
if(string.IsNullOrEmpty(namespaceName))
throw new ArgumentNullException(nameof(namespaceName));
var namespaceNameSegements = namespaceName.Split(namespaceNameSeparator, StringSplitOptions.RemoveEmptyEntries);
var namespaceNameSegments = namespaceName.Split(namespaceNameSeparator, StringSplitOptions.RemoveEmptyEntries);
var lastPresentSegmentIndex = default(int);
var lastPresentSegmentNamespace = GetRootNamespace();
while(lastPresentSegmentIndex < namespaceNameSegements.Length) {
var segmentNameSpace = lastPresentSegmentNamespace.FindNamespaceByName(namespaceNameSegements.Take(lastPresentSegmentIndex + 1).Aggregate((x, y) => $"{x}.{y}"));
while(lastPresentSegmentIndex < namespaceNameSegments.Length) {
var segmentNameSpace = lastPresentSegmentNamespace.FindNamespaceByName(namespaceNameSegments.Take(lastPresentSegmentIndex + 1).Aggregate((x, y) => $"{x}.{y}"));
if(segmentNameSpace == null)
break;
else {
lastPresentSegmentNamespace = segmentNameSpace;
lastPresentSegmentIndex++;
}
}
foreach(var childSegment in namespaceNameSegements.Skip(lastPresentSegmentIndex))
foreach(var childSegment in namespaceNameSegments.Skip(lastPresentSegmentIndex))
lastPresentSegmentNamespace = lastPresentSegmentNamespace
.AddRange(
new CodeNamespace {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Kiota.Builder.Writers;

namespace Kiota.Builder
namespace Kiota.Builder.CodeRenderers
{
/// <summary>
/// Convert CodeDOM classes to strings or files
Expand Down Expand Up @@ -51,13 +52,10 @@ private async Task RenderBarrel(LanguageWriter writer, CodeNamespace root, CodeN
if (!string.IsNullOrEmpty(codeNamespace.Name) &&
!string.IsNullOrEmpty(root.Name) &&
_configuration.ShouldWriteNamespaceIndices &&
!_configuration.ClientNamespaceName.Contains(codeNamespace.Name, StringComparison.OrdinalIgnoreCase))
!_configuration.ClientNamespaceName.Contains(codeNamespace.Name, StringComparison.OrdinalIgnoreCase) &&
ShouldRenderNamespaceFile(codeNamespace))
{
var namespaceNameLastSegment = codeNamespace.Name.Split('.').Last().ToLowerInvariant();
// if the module already has a class with the same name, it's going to be declared automatically
if (_configuration.ShouldWriteBarrelsIfClassExists ||
codeNamespace.FindChildByName<CodeClass>(namespaceNameLastSegment, false) == null)
await RenderCodeNamespaceToSingleFileAsync(writer, codeNamespace, writer.PathSegmenter.GetPath(root, codeNamespace), cancellationToken);
await RenderCodeNamespaceToSingleFileAsync(writer, codeNamespace, writer.PathSegmenter.GetPath(root, codeNamespace), cancellationToken);
}
}
private readonly CodeElementOrderComparer _rendererElementComparer;
Expand All @@ -73,5 +71,23 @@ private void RenderCode(LanguageWriter writer, CodeElement element)
}

}

public virtual bool ShouldRenderNamespaceFile(CodeNamespace codeNamespace)
{
// if the module already has a class with the same name, it's going to be declared automatically
var namespaceNameLastSegment = codeNamespace.Name.Split('.').Last().ToLowerInvariant();
return (_configuration.ShouldWriteBarrelsIfClassExists || codeNamespace.FindChildByName<CodeClass>(namespaceNameLastSegment, false) == null);
}

public static CodeRenderer GetCodeRender(GenerationConfiguration config)
{
return config.Language switch
{
GenerationLanguage.TypeScript =>
new TypeScriptCodeRenderer(config),
_ => new CodeRenderer(config),
};
}

}
}
13 changes: 13 additions & 0 deletions src/Kiota.Builder/CodeRenderers/TypeScriptCodeRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Linq;

namespace Kiota.Builder.CodeRenderers
{
public class TypeScriptCodeRenderer : CodeRenderer
{
public TypeScriptCodeRenderer(GenerationConfiguration configuration) : base(configuration) { }
public override bool ShouldRenderNamespaceFile(CodeNamespace codeNamespace)
{
return codeNamespace.Classes.Any(c => c.IsOfKind(CodeClassKind.Model));
}
}
}
7 changes: 4 additions & 3 deletions src/Kiota.Builder/GenerationConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;

namespace Kiota.Builder {
public class GenerationConfiguration {
Expand All @@ -23,10 +24,10 @@ public class GenerationConfiguration {
};
private static readonly HashSet<GenerationLanguage> BarreledLanguages = new () {
GenerationLanguage.Ruby,
// TODO: add typescript once we have a barrel writer for it
GenerationLanguage.TypeScript
};
private static readonly HashSet<GenerationLanguage> BarreledLanguagesWithConstantFileName = new () {
//TODO: add typescript once we have a barrel writer for it
GenerationLanguage.TypeScript
};
public bool CleanOutput { get; set;}
}
Expand Down
4 changes: 3 additions & 1 deletion src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Kiota.Builder.Writers;
using Microsoft.OpenApi.Any;
using Kiota.Builder.Refiners;
using Kiota.Builder.CodeRenderers;
using System.Security;
using Microsoft.OpenApi.Services;
using System.Threading;
Expand Down Expand Up @@ -238,7 +239,8 @@ public async Task CreateLanguageSourceFilesAsync(GenerationLanguage language, Co
var languageWriter = LanguageWriter.GetLanguageWriter(language, config.OutputPath, config.ClientNamespaceName);
var stopwatch = new Stopwatch();
stopwatch.Start();
await new CodeRenderer(config).RenderCodeNamespaceToFilePerClassAsync(languageWriter, generatedCode, cancellationToken);
var codeRenderer = CodeRenderer.GetCodeRender(config);
await codeRenderer.RenderCodeNamespaceToFilePerClassAsync(languageWriter, generatedCode, cancellationToken);
stopwatch.Stop();
logger.LogTrace("{timestamp}ms: Files written to {path}", stopwatch.ElapsedMilliseconds, config.OutputPath);
}
Expand Down
59 changes: 35 additions & 24 deletions src/Kiota.Builder/Writers/RelativeImportManager.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Kiota.Builder.Extensions;

namespace Kiota.Builder.Writers {
public class RelativeImportManager {
namespace Kiota.Builder.Writers
{
public class RelativeImportManager
{
private readonly int prefixLength;
private readonly char separator;
public RelativeImportManager(string namespacePrefix, char namespaceSeparator)
{
if(string.IsNullOrEmpty(namespacePrefix))
if (string.IsNullOrEmpty(namespacePrefix))
throw new ArgumentNullException(nameof(namespacePrefix));
if(namespaceSeparator == default)
if (namespaceSeparator == default)
throw new ArgumentNullException(nameof(namespaceSeparator));

prefixLength = namespacePrefix.Length;
Expand All @@ -23,8 +25,9 @@ public RelativeImportManager(string namespacePrefix, char namespaceSeparator)
/// <param name="codeUsing">The using to import into the current namespace context</param>
/// <param name="currentNamespace">The current namespace</param>
/// <returns>The import symbol, it's alias if any and the relative import path</returns>
public (string, string, string) GetRelativeImportPathForUsing(CodeUsing codeUsing, CodeNamespace currentNamespace) {
if(codeUsing?.IsExternal ?? true)
public virtual (string, string, string) GetRelativeImportPathForUsing(CodeUsing codeUsing, CodeNamespace currentNamespace)
{
if (codeUsing?.IsExternal ?? true)
return (string.Empty, string.Empty, string.Empty);//it's an external import, add nothing
var typeDef = codeUsing.Declaration.TypeDefinition;

Expand All @@ -34,29 +37,32 @@ public RelativeImportManager(string namespacePrefix, char namespaceSeparator)
_ => codeUsing.Declaration.TypeDefinition.Name.ToFirstCharacterUpperCase(),
};

if(typeDef == null)
if (typeDef == null)
return (importSymbol, codeUsing.Alias, "./"); // it's relative to the folder, with no declaration (default failsafe)
else {
var importPath = GetImportRelativePathFromNamespaces(currentNamespace,
else
{
var importPath = GetImportRelativePathFromNamespaces(currentNamespace,
typeDef.GetImmediateParentOfType<CodeNamespace>());
if(string.IsNullOrEmpty(importPath))
importPath+= codeUsing.Name;
if (string.IsNullOrEmpty(importPath))
importPath += codeUsing.Name;
else
importPath+= codeUsing.Declaration.Name.ToFirstCharacterLowerCase();
importPath += codeUsing.Declaration.Name.ToFirstCharacterLowerCase();
return (importSymbol, codeUsing.Alias, importPath);
}
}
private string GetImportRelativePathFromNamespaces(CodeNamespace currentNamespace, CodeNamespace importNamespace) {
if(currentNamespace == null)
protected string GetImportRelativePathFromNamespaces(CodeNamespace currentNamespace, CodeNamespace importNamespace)
{
if (currentNamespace == null)
throw new ArgumentNullException(nameof(currentNamespace));
else if (importNamespace == null)
throw new ArgumentNullException(nameof(importNamespace));
else if(currentNamespace == importNamespace || currentNamespace.Name.Equals(importNamespace.Name, StringComparison.OrdinalIgnoreCase)) // we're in the same namespace
else if (currentNamespace == importNamespace || currentNamespace.Name.Equals(importNamespace.Name, StringComparison.OrdinalIgnoreCase)) // we're in the same namespace
return "./";
else
return GetRelativeImportPathFromSegments(currentNamespace, importNamespace);
return GetRelativeImportPathFromSegments(currentNamespace, importNamespace);
}
private string GetRelativeImportPathFromSegments(CodeNamespace currentNamespace, CodeNamespace importNamespace) {
protected string GetRelativeImportPathFromSegments(CodeNamespace currentNamespace, CodeNamespace importNamespace)
{
var currentNamespaceSegments = currentNamespace
.Name[prefixLength..]
.Split(separator, StringSplitOptions.RemoveEmptyEntries);
Expand All @@ -66,24 +72,29 @@ private string GetRelativeImportPathFromSegments(CodeNamespace currentNamespace,
var importNamespaceSegmentsCount = importNamespaceSegments.Length;
var currentNamespaceSegmentsCount = currentNamespaceSegments.Length;
var deeperMostSegmentIndex = 0;
while(deeperMostSegmentIndex < Math.Min(importNamespaceSegmentsCount, currentNamespaceSegmentsCount)) {
if(currentNamespaceSegments.ElementAt(deeperMostSegmentIndex).Equals(importNamespaceSegments.ElementAt(deeperMostSegmentIndex), StringComparison.OrdinalIgnoreCase))
while (deeperMostSegmentIndex < Math.Min(importNamespaceSegmentsCount, currentNamespaceSegmentsCount))
{
if (currentNamespaceSegments.ElementAt(deeperMostSegmentIndex).Equals(importNamespaceSegments.ElementAt(deeperMostSegmentIndex), StringComparison.OrdinalIgnoreCase))
deeperMostSegmentIndex++;
else
break;
}
if (deeperMostSegmentIndex == currentNamespaceSegmentsCount) { // we're in a parent namespace and need to import with a relative path
if (deeperMostSegmentIndex == currentNamespaceSegmentsCount)
{ // we're in a parent namespace and need to import with a relative path
return "./" + GetRemainingImportPath(importNamespaceSegments.Skip(deeperMostSegmentIndex));
} else { // we're in a sub namespace and need to go "up" with dot dots
}
else
{ // we're in a sub namespace and need to go "up" with dot dots
var upMoves = currentNamespaceSegmentsCount - deeperMostSegmentIndex;
var pathSegmentSeparator = upMoves > 0 ? "/" : string.Empty;
return string.Join("/", Enumerable.Repeat("..", upMoves)) +
pathSegmentSeparator +
GetRemainingImportPath(importNamespaceSegments.Skip(deeperMostSegmentIndex));
}
}
private static string GetRemainingImportPath(IEnumerable<string> remainingSegments) {
if(remainingSegments.Any())
protected static string GetRemainingImportPath(IEnumerable<string> remainingSegments)
{
if (remainingSegments.Any())
return remainingSegments.Select(x => x.ToFirstCharacterLowerCase()).Aggregate((x, y) => $"{x}/{y}") + '/';
else
return string.Empty;
Expand Down
98 changes: 98 additions & 0 deletions src/Kiota.Builder/Writers/TypeScript/CodeNameSpaceWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Kiota.Builder.Extensions;

namespace Kiota.Builder.Writers.TypeScript
baywet marked this conversation as resolved.
Show resolved Hide resolved
{
public class CodeNameSpaceWriter : BaseElementWriter<CodeNamespace, TypeScriptConventionService>
{
public CodeNameSpaceWriter(TypeScriptConventionService conventionService) : base(conventionService) { }

/// <summary>
/// Writes export statements for classes and enums belonging to a namespace into a generated index.ts file.
/// The classes should be export in the order of inheritance so as to avoid circular dependency issues in javascript.
/// </summary>
/// <param name="codeElement">Code element is a code namespace</param>
/// <param name="writer"></param>
public override void WriteCodeElement(CodeNamespace codeElement, LanguageWriter writer)
{
var sortedClassNames = SortClassesInOrderOfInheritance(codeElement.Classes.ToList());
baywet marked this conversation as resolved.
Show resolved Hide resolved

foreach (var className in sortedClassNames)
{
writer.WriteLine($"export * from './{className.ToFirstCharacterLowerCase()}'");
}
}

/// <summary>
/// Visits every child for a given parent class and recursively inserts each class into a list ordered based on inheritance.
/// </summary>
/// <param name="parentListChildren"></param>
/// <param name="visited"></param>
/// <param name="orderedList"> Lis</param>
/// <param name="current"></param>
private void VisitEveryChild(Dictionary<string, List<string>> parentChildrenMap, HashSet<string> visited, List<string> inheritanceOrderList, string current)
{
if (!visited.Contains(current))
{
visited.Add(current);

foreach (var child in parentChildrenMap[current].Where(x => parentChildrenMap.ContainsKey(x)))
{
VisitEveryChild(parentChildrenMap, visited, inheritanceOrderList, child);
}
inheritanceOrderList.Insert(0, current);
}
}

/// <summary>
/// Orders given list of classes in a namespace based on inheritance.
/// That is, if class B extends class A then A should exported before class B.
/// </summary>
/// <param name="classes"> Classes in a given code namespace</param>
/// <returns> List of class names in the code name space ordered based on inheritance</returns>
private List<string> SortClassesInOrderOfInheritance(List<CodeClass> classes)
{
var visited = new HashSet<string>();
baywet marked this conversation as resolved.
Show resolved Hide resolved
var parentChildrenMap = new Dictionary<string, List<string>>();
var inheritanceOrderList = new List<string>();

/*
* 1. Create a dictionary containing all the parent classes.
*/
foreach (var @class in classes.Where(c => c.IsOfKind(CodeClassKind.Model)))
{
// Verify if parent class is from the same namespace
baywet marked this conversation as resolved.
Show resolved Hide resolved
var inheritsFrom = @class.Parent.Name.Equals(@class.StartBlock.Inherits?.TypeDefinition?.Parent?.Name, StringComparison.OrdinalIgnoreCase) ? @class.StartBlock.Inherits?.Name : null;

if (!string.IsNullOrEmpty(inheritsFrom))
{
if (!parentChildrenMap.ContainsKey(inheritsFrom))
{
parentChildrenMap[inheritsFrom] = new List<string>();
}
parentChildrenMap[inheritsFrom].Add(@class.Name);
baywet marked this conversation as resolved.
Show resolved Hide resolved
}
}

/*
* 2. Print the export command for every parent node before the child node.
*/
foreach (var key in parentChildrenMap.Keys)
{
VisitEveryChild(parentChildrenMap, visited, inheritanceOrderList, key);
}

/*
* 3. Print all remaining classes which have not been visted or those do not have any parent/child relationship.
*/
foreach (var className in classes.Where(c => c.IsOfKind(CodeClassKind.Model) && !visited.Contains(c.Name)).Select(x => x.Name))
{
visited.Add(className);
inheritanceOrderList.Add(className);
}
return inheritanceOrderList;
}
}
}
6 changes: 3 additions & 3 deletions src/Kiota.Builder/Writers/TypeScript/CodeUsingWriter.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;

namespace Kiota.Builder.Writers.TypeScript;
public class CodeUsingWriter {
private readonly RelativeImportManager _relativeImportManager;
private readonly TypescriptRelativeImportManager _relativeImportManager;
public CodeUsingWriter(string clientNamespaceName)
{
_relativeImportManager = new RelativeImportManager(clientNamespaceName, '.');
_relativeImportManager = new TypescriptRelativeImportManager(clientNamespaceName, '.');
}
public void WriteCodeElement(IEnumerable<CodeUsing> usings, CodeNamespace parentNamespace, LanguageWriter writer ) {
var externalImportSymbolsAndPaths = usings
Expand Down
Loading