diff --git a/CHANGELOG.md b/CHANGELOG.md index 30ca8bdfd2..b861d2e4bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/Kiota.Builder/CodeDOM/CodeNamespace.cs b/src/Kiota.Builder/CodeDOM/CodeNamespace.cs index 64886b3501..b2e53c984a 100644 --- a/src/Kiota.Builder/CodeDOM/CodeNamespace.cs +++ b/src/Kiota.Builder/CodeDOM/CodeNamespace.cs @@ -11,7 +11,9 @@ public class CodeNamespace : CodeBlock { private CodeNamespace():base() {} public static CodeNamespace InitRootNamespace() { - return new CodeNamespace(); + return new() { + Name = string.Empty, + }; } private string name; public override string Name @@ -60,11 +62,11 @@ 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 { @@ -72,7 +74,7 @@ public CodeNamespace AddNamespace(string namespaceName) { lastPresentSegmentIndex++; } } - foreach(var childSegment in namespaceNameSegements.Skip(lastPresentSegmentIndex)) + foreach(var childSegment in namespaceNameSegments.Skip(lastPresentSegmentIndex)) lastPresentSegmentNamespace = lastPresentSegmentNamespace .AddRange( new CodeNamespace { diff --git a/src/Kiota.Builder/CodeRenderer.cs b/src/Kiota.Builder/CodeRenderers/CodeRenderer.cs similarity index 75% rename from src/Kiota.Builder/CodeRenderer.cs rename to src/Kiota.Builder/CodeRenderers/CodeRenderer.cs index d1224bd9d3..b19c34d869 100644 --- a/src/Kiota.Builder/CodeRenderer.cs +++ b/src/Kiota.Builder/CodeRenderers/CodeRenderer.cs @@ -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 { /// /// Convert CodeDOM classes to strings or files @@ -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(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; @@ -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(namespaceNameLastSegment, false) == null); + } + + public static CodeRenderer GetCodeRender(GenerationConfiguration config) + { + return config.Language switch + { + GenerationLanguage.TypeScript => + new TypeScriptCodeRenderer(config), + _ => new CodeRenderer(config), + }; + } + } } diff --git a/src/Kiota.Builder/CodeRenderers/TypeScriptCodeRenderer.cs b/src/Kiota.Builder/CodeRenderers/TypeScriptCodeRenderer.cs new file mode 100644 index 0000000000..f916b1dfbb --- /dev/null +++ b/src/Kiota.Builder/CodeRenderers/TypeScriptCodeRenderer.cs @@ -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)); + } + } +} diff --git a/src/Kiota.Builder/GenerationConfiguration.cs b/src/Kiota.Builder/GenerationConfiguration.cs index a1b882f2bd..11254c9f18 100644 --- a/src/Kiota.Builder/GenerationConfiguration.cs +++ b/src/Kiota.Builder/GenerationConfiguration.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Kiota.Builder { public class GenerationConfiguration { @@ -23,10 +24,10 @@ public class GenerationConfiguration { }; private static readonly HashSet BarreledLanguages = new () { GenerationLanguage.Ruby, - // TODO: add typescript once we have a barrel writer for it + GenerationLanguage.TypeScript }; private static readonly HashSet BarreledLanguagesWithConstantFileName = new () { - //TODO: add typescript once we have a barrel writer for it + GenerationLanguage.TypeScript }; public bool CleanOutput { get; set;} } diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 7f9c983679..067fa59fbc 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -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; @@ -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); } diff --git a/src/Kiota.Builder/Writers/RelativeImportManager.cs b/src/Kiota.Builder/Writers/RelativeImportManager.cs index 8315dc812b..279803bb3b 100644 --- a/src/Kiota.Builder/Writers/RelativeImportManager.cs +++ b/src/Kiota.Builder/Writers/RelativeImportManager.cs @@ -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; @@ -23,8 +25,9 @@ public RelativeImportManager(string namespacePrefix, char namespaceSeparator) /// The using to import into the current namespace context /// The current namespace /// The import symbol, it's alias if any and the relative import path - 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; @@ -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()); - 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); @@ -66,15 +72,19 @@ 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)) + @@ -82,8 +92,9 @@ private string GetRelativeImportPathFromSegments(CodeNamespace currentNamespace, GetRemainingImportPath(importNamespaceSegments.Skip(deeperMostSegmentIndex)); } } - private static string GetRemainingImportPath(IEnumerable remainingSegments) { - if(remainingSegments.Any()) + protected static string GetRemainingImportPath(IEnumerable remainingSegments) + { + if (remainingSegments.Any()) return remainingSegments.Select(x => x.ToFirstCharacterLowerCase()).Aggregate((x, y) => $"{x}/{y}") + '/'; else return string.Empty; diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeNameSpaceWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeNameSpaceWriter.cs new file mode 100644 index 0000000000..3fdb92be3e --- /dev/null +++ b/src/Kiota.Builder/Writers/TypeScript/CodeNameSpaceWriter.cs @@ -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 + { + public CodeNameSpaceWriter(TypeScriptConventionService conventionService) : base(conventionService) { } + + /// + /// 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. + /// + /// Code element is a code namespace + /// + 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()}'"); + } + } + + /// + /// Visits every child for a given parent class and recursively inserts each class into a list ordered based on inheritance. + /// + /// + /// + /// Lis + /// + private void VisitEveryChild(Dictionary> parentChildrenMap, HashSet visited, List 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); + } + } + + /// + /// 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. + /// + /// Classes in a given code namespace + /// List of class names in the code name space ordered based on inheritance + private List SortClassesInOrderOfInheritance(List classes) + { + var visited = new HashSet(); + var parentChildrenMap = new Dictionary>(); + var inheritanceOrderList = new List(); + + /* + * 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(); + } + 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; + } + } +} diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeUsingWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeUsingWriter.cs index 3845311cba..a0b584f27b 100644 --- a/src/Kiota.Builder/Writers/TypeScript/CodeUsingWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/CodeUsingWriter.cs @@ -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 usings, CodeNamespace parentNamespace, LanguageWriter writer ) { var externalImportSymbolsAndPaths = usings diff --git a/src/Kiota.Builder/Writers/TypeScript/TypeScriptRelativeImportManager.cs b/src/Kiota.Builder/Writers/TypeScript/TypeScriptRelativeImportManager.cs new file mode 100644 index 0000000000..0a744a9f13 --- /dev/null +++ b/src/Kiota.Builder/Writers/TypeScript/TypeScriptRelativeImportManager.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Kiota.Builder.Extensions; + +namespace Kiota.Builder.Writers.TypeScript +{ + public class TypescriptRelativeImportManager : RelativeImportManager + + { + public TypescriptRelativeImportManager(string namespacePrefix, char namespaceSeparator) : base(namespacePrefix,namespaceSeparator) + { + } + /// + /// Returns the relative import path for the given using and import context namespace. + /// + /// The using to import into the current namespace context + /// The current namespace + /// The import symbol, it's alias if any and the relative import path + public override (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; + + var importSymbol = codeUsing.Declaration == null ? codeUsing.Name : codeUsing.Declaration.TypeDefinition switch + { + CodeFunction f => f.Name.ToFirstCharacterLowerCase(), + _ => codeUsing.Declaration.TypeDefinition.Name.ToFirstCharacterUpperCase(), + }; + + if (typeDef == null) + return (importSymbol, codeUsing.Alias, "./"); // it's relative to the folder, with no declaration (default failsafe) + else + { + var importPath = GetImportRelativePathFromNamespaces(currentNamespace, + typeDef.GetImmediateParentOfType()); + var isCodeUsingAModel = codeUsing.Declaration?.TypeDefinition is CodeClass codeClass && codeClass.IsOfKind(CodeClassKind.Model); + if (importPath == "./" && isCodeUsingAModel) + { + importPath += "index"; + } + else if (string.IsNullOrEmpty(importPath)) + importPath += codeUsing.Name; + else if (!isCodeUsingAModel) { + importPath += codeUsing.Declaration.Name.ToFirstCharacterLowerCase(); + } + return (importSymbol, codeUsing.Alias, importPath); + } + } + } +} diff --git a/src/Kiota.Builder/Writers/TypeScript/TypeScriptWriter.cs b/src/Kiota.Builder/Writers/TypeScript/TypeScriptWriter.cs index a64e93f135..a7df287743 100644 --- a/src/Kiota.Builder/Writers/TypeScript/TypeScriptWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/TypeScriptWriter.cs @@ -1,4 +1,4 @@ -namespace Kiota.Builder.Writers.TypeScript +namespace Kiota.Builder.Writers.TypeScript { public class TypeScriptWriter : LanguageWriter { @@ -13,6 +13,7 @@ public TypeScriptWriter(string rootPath, string clientNamespaceName) AddOrReplaceCodeElementWriter(new CodeFunctionWriter(conventionService, clientNamespaceName)); AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService)); AddOrReplaceCodeElementWriter(new CodeTypeWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeNameSpaceWriter(conventionService)); } } } diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeNamespaceWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeNamespaceWriterTests.cs new file mode 100644 index 0000000000..01cae89326 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeNamespaceWriterTests.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using System.Linq; +using Xunit; + +namespace Kiota.Builder.Writers.TypeScript.Tests +{ + public class CodeNameSpaceWriterTests : IDisposable + { + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + + public CodeNameSpaceWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.TypeScript, DefaultPath, DefaultName); + tw = new StringWriter(); + writer.SetTextWriter(tw); + } + + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + + [Fact] + public void WritesOnlyModelClasses() + { + var root = CodeNamespace.InitRootNamespace(); + var requestbuilder = new CodeClass + { + Kind = CodeClassKind.RequestBuilder, + Name = "TestRequestBuilder", + }; + var model = new CodeClass + { + Kind = CodeClassKind.Model, + Name = "TestModel", // The tests should verify if the printed file names start with lower case. + }; + root.AddClass(requestbuilder); + root.AddClass(model); + writer.Write(root); + var result = tw.ToString(); + Console.WriteLine(result); + Assert.Contains("export * from './testModel'", result); + } + + [Fact] + public void SortModelClassesBasedonInheritance() + { + var root = CodeNamespace.InitRootNamespace(); + root.Name = "testNameSpace"; + + var modelA = new CodeClass + { + Kind = CodeClassKind.Model, + Name = "ModelA", + }; + + var modelB = new CodeClass + { + Kind = CodeClassKind.Model, + Name = "ModelB", + }; + + var modelC = new CodeClass + { + Kind = CodeClassKind.Model, + Name = "ModelC", + }; + + root.AddClass(modelA); + root.AddClass(modelB); + root.AddClass(modelC); + + var declarationB = modelB.StartBlock; + + declarationB.Inherits = new CodeType + { + Name = modelA.Name, + TypeDefinition = modelA + }; + var declarationC = modelC.StartBlock; + declarationC.Inherits = new CodeType + { + Name = modelB.Name, + TypeDefinition = modelB + }; + + writer.Write(root); + var result = tw.ToString(); + + Assert.Contains($"export * from './modelA'{Environment.NewLine}export * from './modelB'{Environment.NewLine}export * from './modelC'{Environment.NewLine}", result); + } + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/RelativeImportManagerTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScriptRelativeImportManagerTests.cs similarity index 83% rename from tests/Kiota.Builder.Tests/Writers/RelativeImportManagerTests.cs rename to tests/Kiota.Builder.Tests/Writers/TypeScriptRelativeImportManagerTests.cs index 963a80813a..cb68e98e9b 100644 --- a/tests/Kiota.Builder.Tests/Writers/RelativeImportManagerTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/TypeScriptRelativeImportManagerTests.cs @@ -1,12 +1,13 @@ +using Kiota.Builder.Writers.TypeScript; using Xunit; namespace Kiota.Builder.Writers.Tests { - public class RelativeImportManagerTests { + public class TypeScriptRelativeImportManagerTests { private readonly CodeNamespace root; private readonly CodeNamespace graphNS; private readonly CodeClass parentClass; - private readonly RelativeImportManager importManager = new("graph", '.'); - public RelativeImportManagerTests() { + private readonly TypescriptRelativeImportManager importManager = new("graph", '.'); + public TypeScriptRelativeImportManagerTests() { root = CodeNamespace.InitRootNamespace(); graphNS = root.AddNamespace("graph"); parentClass = new () { @@ -117,5 +118,29 @@ public void ReplacesImportsSameNamespace() { var result = importManager.GetRelativeImportPathForUsing(nUsing, graphNS); Assert.Equal("./message", result.Item3); } + + [Fact] + public void ReplacesImportsSameNamespaceIndex() + { + var declaration = parentClass.StartBlock as ClassDeclaration; + var messageClassDef = new CodeClass + { + Name = "Message", + Kind = CodeClassKind.Model + }; + graphNS.AddClass(messageClassDef); + var nUsing = new CodeUsing + { + Name = "graph", + Declaration = new() + { + Name = "Message", + TypeDefinition = messageClassDef, + } + }; + declaration.AddUsings(nUsing); + var result = importManager.GetRelativeImportPathForUsing(nUsing, graphNS); + Assert.Equal("./index", result.Item3); + } } }