diff --git a/CHANGELOG.md b/CHANGELOG.md index c75b714c5a..fc39416369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- C#: primary error messages nested in collections now read from the first collection item. [#7926](https://github.com/microsoft/kiota/issues/7926) - golang: generated code now always uses LF line endings, so `gofmt` no longer reports formatting differences when generating on Windows. - golang: make sure all generated code adheres to golangs coding standards - Fixed non-deterministic model class descriptions when a component schema is referenced from multiple properties with differing reference-level descriptions. [#7927](https://github.com/microsoft/kiota/issues/7927) diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index 371f54068a..0bdb170634 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -6,6 +6,7 @@ using Kiota.Builder.CodeDOM; using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; +using Kiota.Builder.Writers; namespace Kiota.Builder.Refiners; @@ -166,6 +167,10 @@ protected static void MakeEnumPropertiesNullable(CodeElement currentElement) "System", "String"), new (static x => x is CodeClass, "System.Collections.Generic", "List", "Dictionary"), + new (static x => x is CodeClass { IsErrorDefinition: true } @class && + @class.GetPrimaryMessageCodePath(static y => y.Name, static y => y.Name, pathSegmentFactory: static y => y.IsCollection ? ".FirstOrDefault()." : ".") + .Contains(".FirstOrDefault().", StringComparison.Ordinal), + "System.Linq", "Enumerable"), new (static x => x is CodeClass @class && @class.IsOfKind(CodeClassKind.Model, CodeClassKind.RequestBuilder), "System.IO", "Stream"), new (static x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.RequestExecutor), diff --git a/src/Kiota.Builder/Writers/CSharp/CodePropertyWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodePropertyWriter.cs index 7159c9d63b..7ad17a2cb4 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodePropertyWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodePropertyWriter.cs @@ -60,7 +60,7 @@ private void WritePropertyInternal(CodeProperty codeElement, LanguageWriter writ writer.CloseBlock(); break; case CodePropertyKind.ErrorMessageOverride when parentClass.IsErrorDefinition: - if (parentClass.GetPrimaryMessageCodePath(static x => x.Name.ToFirstCharacterUpperCase(), static x => x.Name.ToFirstCharacterUpperCase(), "?.") is string primaryMessageCodePath && !string.IsNullOrEmpty(primaryMessageCodePath)) + if (parentClass.GetPrimaryMessageCodePath(static x => x.Name.ToFirstCharacterUpperCase(), static x => x.Name.ToFirstCharacterUpperCase(), "?.", pathSegmentFactory: static x => x.IsCollection ? "?.FirstOrDefault()?." : "?.") is string primaryMessageCodePath && !string.IsNullOrEmpty(primaryMessageCodePath)) writer.WriteLine($"public override {propertyType} {codeElement.Name.ToFirstCharacterUpperCase()} {{ get => {primaryMessageCodePath} ?? string.Empty; }}"); else writer.WriteLine($"public override {propertyType} {codeElement.Name.ToFirstCharacterUpperCase()} {{ get => base.Message; }}"); diff --git a/src/Kiota.Builder/Writers/ProprietableBlockExtensions.cs b/src/Kiota.Builder/Writers/ProprietableBlockExtensions.cs index 8500c1cea7..9fac1bfdbf 100644 --- a/src/Kiota.Builder/Writers/ProprietableBlockExtensions.cs +++ b/src/Kiota.Builder/Writers/ProprietableBlockExtensions.cs @@ -14,7 +14,8 @@ internal static string GetPrimaryMessageCodePath( Func propertyNameNormalization, Func methodNameNormalization, string pathSegment = ".", - HashSet? visitedElements = default) where TBlockKind : Enum where TBlockDeclaration : ProprietableBlockDeclaration, new() + HashSet? visitedElements = default, + Func? pathSegmentFactory = default) where TBlockKind : Enum where TBlockDeclaration : ProprietableBlockDeclaration, new() { visitedElements ??= new(); if (visitedElements.Contains(block)) @@ -29,10 +30,10 @@ internal static string GetPrimaryMessageCodePath( return propertyNameNormalization(primaryErrorMessageProperty); else if (currentInterface.Methods .Where(isGetterMethod) - .Select(x => new { Value = x.ReturnType is CodeType codeType && codeType.TypeDefinition is CodeInterface codeInterface && codeInterface.GetPrimaryMessageCodePath(propertyNameNormalization, methodNameNormalization, pathSegment, visitedElements) is string segment && !string.IsNullOrEmpty(segment) ? $"{methodNameNormalization(x)}{pathSegment}{segment}" : string.Empty, IsMethod = true }) + .Select(x => new { Value = x.ReturnType is CodeType codeType && codeType.TypeDefinition is CodeInterface codeInterface && codeInterface.GetPrimaryMessageCodePath(propertyNameNormalization, methodNameNormalization, pathSegment, visitedElements, pathSegmentFactory) is string segment && !string.IsNullOrEmpty(segment) ? $"{methodNameNormalization(x)}{pathSegmentFactory?.Invoke(x.ReturnType) ?? pathSegment}{segment}" : string.Empty, IsMethod = true }) .Union(currentInterface.Properties .Where(isCustomProperty) - .Select(x => new { Value = x.Type is CodeType codeType && codeType.TypeDefinition is CodeInterface codeInterface && codeInterface.GetPrimaryMessageCodePath(propertyNameNormalization, methodNameNormalization, pathSegment, visitedElements) is string segment && !string.IsNullOrEmpty(segment) ? $"{propertyNameNormalization(x)}{pathSegment}{segment}" : string.Empty, IsMethod = false })) + .Select(x => new { Value = x.Type is CodeType codeType && codeType.TypeDefinition is CodeInterface codeInterface && codeInterface.GetPrimaryMessageCodePath(propertyNameNormalization, methodNameNormalization, pathSegment, visitedElements, pathSegmentFactory) is string segment && !string.IsNullOrEmpty(segment) ? $"{propertyNameNormalization(x)}{pathSegmentFactory?.Invoke(x.Type) ?? pathSegment}{segment}" : string.Empty, IsMethod = false })) .OrderBy(static x => x.IsMethod) .ThenBy(static x => x.Value, StringComparer.OrdinalIgnoreCase) .FirstOrDefault(static x => !string.IsNullOrEmpty(x.Value)) is { } primaryMessageCodePath) @@ -47,10 +48,10 @@ internal static string GetPrimaryMessageCodePath( return propertyNameNormalization(primaryErrorMessageProperty); else if (currentClass.Methods .Where(isGetterMethod) - .Select(x => new { Value = x.ReturnType is CodeType codeType && codeType.TypeDefinition is CodeClass codeClass && codeClass.GetPrimaryMessageCodePath(propertyNameNormalization, methodNameNormalization, pathSegment, visitedElements) is string segment && !string.IsNullOrEmpty(segment) ? $"{methodNameNormalization(x)}{pathSegment}{segment}" : string.Empty, IsMethod = true }) + .Select(x => new { Value = x.ReturnType is CodeType codeType && codeType.TypeDefinition is CodeClass codeClass && codeClass.GetPrimaryMessageCodePath(propertyNameNormalization, methodNameNormalization, pathSegment, visitedElements, pathSegmentFactory) is string segment && !string.IsNullOrEmpty(segment) ? $"{methodNameNormalization(x)}{pathSegmentFactory?.Invoke(x.ReturnType) ?? pathSegment}{segment}" : string.Empty, IsMethod = true }) .Union(currentClass.Properties .Where(isCustomProperty) - .Select(x => new { Value = x.Type is CodeType codeType && codeType.TypeDefinition is CodeClass codeClass && codeClass.GetPrimaryMessageCodePath(propertyNameNormalization, methodNameNormalization, pathSegment, visitedElements) is string segment && !string.IsNullOrEmpty(segment) ? $"{propertyNameNormalization(x)}{pathSegment}{segment}" : string.Empty, IsMethod = false })) + .Select(x => new { Value = x.Type is CodeType codeType && codeType.TypeDefinition is CodeClass codeClass && codeClass.GetPrimaryMessageCodePath(propertyNameNormalization, methodNameNormalization, pathSegment, visitedElements, pathSegmentFactory) is string segment && !string.IsNullOrEmpty(segment) ? $"{propertyNameNormalization(x)}{pathSegmentFactory?.Invoke(x.Type) ?? pathSegment}{segment}" : string.Empty, IsMethod = false })) .OrderBy(static x => x.IsMethod) .ThenBy(static x => x.Value, StringComparer.OrdinalIgnoreCase) .FirstOrDefault(static x => !string.IsNullOrEmpty(x.Value)) is { } primaryMessageCodePath) diff --git a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs index fb2c89a4e6..26ccfbb261 100644 --- a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs @@ -500,6 +500,43 @@ public async Task RenamesMatchAndAddsPrimaryErrorMessageIfMatchAlreadyExistsAsyn Assert.True(properties[1].IsPrimaryErrorMessage);// property is IsPrimaryErrorMessage so that information deserialized into it shows up in the error information. } [Fact] + public async Task AddsLinqImportForPrimaryErrorMessageInCollectionAsync() + { + var processResult = root.AddClass(new CodeClass + { + Name = "processResult", + Kind = CodeClassKind.Model, + }).First(); + processResult.AddProperty(new CodeProperty + { + Name = "message", + Kind = CodePropertyKind.Custom, + IsPrimaryErrorMessage = true, + Type = new CodeType { Name = "string" }, + }); + var exception = root.AddClass(new CodeClass + { + Name = "error403", + Kind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + exception.AddProperty(new CodeProperty + { + Name = "processResults", + Kind = CodePropertyKind.Custom, + Type = new CodeType + { + Name = processResult.Name, + TypeDefinition = processResult, + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + }, + }); + + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root, cancellationToken: TestContext.Current.CancellationToken); + + Assert.Contains(exception.StartBlock.Usings, static x => x.Declaration?.Name.Equals("System.Linq", StringComparison.Ordinal) is true); + } + [Fact] public async Task RenamesExceptionClassWithReservedPropertyNameAsync() { var exception = root.AddClass(new CodeClass diff --git a/tests/Kiota.Builder.Tests/Writers/CSharp/CodePropertyWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/CSharp/CodePropertyWriterTests.cs index f46d93933e..92f4e5d664 100644 --- a/tests/Kiota.Builder.Tests/Writers/CSharp/CodePropertyWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/CSharp/CodePropertyWriterTests.cs @@ -271,5 +271,43 @@ public void WritesMessageOverrideOnPrimary() // Then Assert.Contains("public override string Message { get => Prop1 ?? string.Empty; }", result); } -} + [Fact] + public void WritesMessageOverrideOnPrimaryInCollection() + { + parentClass.IsErrorDefinition = true; + var processResult = rootNamespace.AddClass(new CodeClass + { + Name = "processResult", + Kind = CodeClassKind.Model, + }).First(); + processResult.AddProperty(new CodeProperty + { + Name = "message", + Kind = CodePropertyKind.Custom, + IsPrimaryErrorMessage = true, + Type = new CodeType { Name = "string" }, + }); + parentClass.AddProperty(new CodeProperty + { + Name = "processResults", + Kind = CodePropertyKind.Custom, + Type = new CodeType + { + Name = processResult.Name, + TypeDefinition = processResult, + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + }, + }); + var overrideProperty = parentClass.AddProperty(new CodeProperty + { + Name = "Message", + Kind = CodePropertyKind.ErrorMessageOverride, + Type = new CodeType { Name = "string" }, + }).First(); + + writer.Write(overrideProperty); + var result = tw.ToString(); + Assert.Contains("public override string Message { get => ProcessResults?.FirstOrDefault()?.Message ?? string.Empty; }", result); + } +}