Skip to content

Commit

Permalink
Typescript - introducing index files namespace wise fixing circular d…
Browse files Browse the repository at this point in the history
…ependency (#1274)

* record requestoption

* adding typescript namespace gen

* adding gets enums to namespace

* recursive inheritance ordering

* checking namespace of parent

* adding rendering condition

* adding comments

* - removes testing file

* adding language specific code renderer

* relativeimport language specific, model only barrel

* extending relativeimportmanager; relative imports only for  class

* adding unit test for codenamespace writer

* resetting files

* resetting launcsettings

* ussing addinnerclass for test

* Apply suggestions from code review

Co-authored-by: Vincent Biret <[email protected]>

* reset relativeimport code, update working test

* missing using

* Update tests/Kiota.Builder.Tests/Writers/TypeScript/CodeNamespaceWriterTests.cs

Co-authored-by: Mustafa Zengin <[email protected]>

* Apply suggestions from code review

* - fixes a bug wwhere the root namespace would have a null name causing unit tests to fail

* - code linting

Signed-off-by: Vincent Biret <[email protected]>

* renaming relativeimportmanager test

* Update CHANGELOG.md

* - moves the changelog entry to the right place

Co-authored-by: Vincent Biret <[email protected]>
Co-authored-by: Mustafa Zengin <[email protected]>
  • Loading branch information
3 people authored Mar 21, 2022
1 parent ff9526e commit dbb3616
Show file tree
Hide file tree
Showing 13 changed files with 367 additions and 47 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- TypeScript adding index exporting models to fix #870.
- Fixed a bug where JSON serialization would fail on nil properties in Go.

## [0.0.19] - 2022-03-18
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
{
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());

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>();
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
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);
}
}

/*
* 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

0 comments on commit dbb3616

Please sign in to comment.