From 68f0609ee775b941b82de8a1c474eac5a74b5209 Mon Sep 17 00:00:00 2001 From: Weidong Xu Date: Thu, 12 Dec 2024 10:35:46 +0800 Subject: [PATCH 1/3] http-client-java, doc, require JDK 17 (#5333) clientcore now requires JDK 17. Therefore since emitter requires JDK too, we'd better just ask user to install 17 --- packages/http-client-java/README.md | 2 +- packages/http-client-java/generator/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/http-client-java/README.md b/packages/http-client-java/README.md index 949431ffd9..b36be08cae 100644 --- a/packages/http-client-java/README.md +++ b/packages/http-client-java/README.md @@ -6,7 +6,7 @@ This is a TypeSpec library that will emit a Java SDK from TypeSpec. Install [Node.js](https://nodejs.org/) 20 or above. (Verify by running `node --version`) -Install [Java](https://docs.microsoft.com/java/openjdk/download) 11 or above. (Verify by running `java --version`) +Install [Java](https://docs.microsoft.com/java/openjdk/download) 17 or above. (Verify by running `java --version`) Install [Maven](https://maven.apache.org/install.html). (Verify by running `mvn --version`) diff --git a/packages/http-client-java/generator/README.md b/packages/http-client-java/generator/README.md index 2fa6f134ca..6efa989f3b 100644 --- a/packages/http-client-java/generator/README.md +++ b/packages/http-client-java/generator/README.md @@ -10,8 +10,8 @@ The **Microsoft Java client generator** tool generates client libraries for acce ## Prerequisites -- [Java 11 or above](https://adoptium.net/temurin/releases/) -- [Maven](https://maven.apache.org/download.cgi) +- [Java 17 or above](https://docs.microsoft.com/java/openjdk/download) +- [Maven](https://maven.apache.org/install.html) ## Build From 53f69799d37762b67b0e050abee76fb5670f1cf2 Mon Sep 17 00:00:00 2001 From: Nisha Bhatia <67986960+nisha-bhatia@users.noreply.github.com> Date: Wed, 11 Dec 2024 18:56:03 -0800 Subject: [PATCH 2/3] Update FormatLines in XmlDocStatement.cs to handle line breaks (#5214) Fixes https://github.com/microsoft/typespec/issues/4377 --------- Co-authored-by: Dapeng Zhang --- .../FormattableStringExpression.cs | 4 +- .../src/Shared/StringExtensions.cs | 8 +- .../src/Statements/XmlDocStatement.cs | 21 +- .../src/Utilities/FormattableStringHelpers.cs | 144 ++++++++++ .../src/Writers/CodeWriter.cs | 2 +- .../FormattableStringHelpersTests.cs | 247 ++++++++++++++++++ .../test/Utilities/StringExtensionsTests.cs | 93 ++++++- .../test/Writers/CodeWriterTests.cs | 14 +- .../SingleLineSummaryWithLineBreaks.cs | 6 + .../Client/Structure/Default/DefaultTests.cs | 1 - 10 files changed, 525 insertions(+), 15 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Utilities/FormattableStringHelpersTests.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TestData/CodeWriterTests/SingleLineSummaryWithLineBreaks.cs diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FormattableStringExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FormattableStringExpression.cs index f33202003f..e0e1c9542a 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FormattableStringExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FormattableStringExpression.cs @@ -30,7 +30,7 @@ internal override void Write(CodeWriter writer) { writer.AppendRaw("$\""); var argumentCount = 0; - foreach ((var span, bool isLiteral) in StringExtensions.GetPathParts(Format)) + foreach ((var span, bool isLiteral) in StringExtensions.GetFormattableStringFormatParts(Format)) { if (isLiteral) { @@ -51,7 +51,7 @@ internal override void Write(CodeWriter writer) private static void Validate(string format, IReadOnlyList args) { var count = 0; - foreach (var (_, isLiteral) in StringExtensions.GetPathParts(format)) + foreach (var (_, isLiteral) in StringExtensions.GetFormattableStringFormatParts(format)) { if (!isLiteral) count++; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Shared/StringExtensions.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Shared/StringExtensions.cs index dcd99996fb..b9b17932ec 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Shared/StringExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Shared/StringExtensions.cs @@ -83,16 +83,18 @@ public static string ToCleanName(this string name, bool isCamelCase = true) [return: NotNullIfNotNull(nameof(name))] public static string ToVariableName(this string name) => ToCleanName(name, isCamelCase: false); - public static GetPathPartsEnumerator GetPathParts(string? path) => new GetPathPartsEnumerator(path); + public static GetPathPartsEnumerator GetFormattableStringFormatParts(string? format) => new GetPathPartsEnumerator(format); + + public static GetPathPartsEnumerator GetFormattableStringFormatParts(ReadOnlySpan format) => new GetPathPartsEnumerator(format); public ref struct GetPathPartsEnumerator { private ReadOnlySpan _path; public Part Current { get; private set; } - public GetPathPartsEnumerator(ReadOnlySpan path) + public GetPathPartsEnumerator(ReadOnlySpan format) { - _path = path; + _path = format; Current = default; } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/XmlDocStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/XmlDocStatement.cs index b9958aea25..722d17bb0f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/XmlDocStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/XmlDocStatement.cs @@ -29,18 +29,29 @@ public XmlDocStatement(string startTag, string endTag, IEnumerable EscapeLines(IEnumerable lines) + private List NormalizeLines(IEnumerable lines) { - List escapedLines = new List(); + List result = new List(); + + // break lines if they have line breaks foreach (var line in lines) { - escapedLines.Add(FormattableStringFactory.Create(EscapeLine(line.Format), EscapeArguments(line.GetArguments()))); + var breakLines = FormattableStringHelpers.BreakLines(line); + result.AddRange(breakLines); } - return escapedLines; + + // escape lines if they have invalid characters + for (int i = 0; i < result.Count; i++) + { + var line = result[i]; + result[i] = FormattableStringFactory.Create(EscapeLine(line.Format), EscapeArguments(line.GetArguments())); + } + + return result; } private static object?[] EscapeArguments(object?[] objects) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/FormattableStringHelpers.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/FormattableStringHelpers.cs index 241fc18c0c..6c9ca2626b 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/FormattableStringHelpers.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/FormattableStringHelpers.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; +using System.Text; using Microsoft.Generator.CSharp.Providers; namespace Microsoft.Generator.CSharp @@ -97,5 +98,148 @@ public static string ReplaceLast(this string text, string oldValue, string newVa var position = text.LastIndexOf(oldValue, StringComparison.Ordinal); return position < 0 ? text : text.Substring(0, position) + newValue + text.Substring(position + oldValue.Length); } + + internal static IReadOnlyList BreakLines(FormattableString input) + { + // handle empty input fs - we should not throw it away when it is empty + if (input.Format.Length == 0) + { + return [input]; // return it as is + } + + StringBuilder formatBuilder = new StringBuilder(); + var args = new List(); + List result = new List(); + + var hasEmptyLastLine = BreakLinesCore(input, formatBuilder, args, result); + + // if formatBuilder is not empty at end, add it to result + // or when the last char is line break, we should also construct one and add it into the result + if (formatBuilder.Length > 0 || hasEmptyLastLine) + { + FormattableString formattableString = FormattableStringFactory.Create(formatBuilder.ToString(), args.ToArray()); + result.Add(formattableString); + } + return result; + } + + private static bool BreakLinesCore(FormattableString input, StringBuilder formatBuilder, List args, List result) + { + // stackalloc cannot be used in a loop, we must allocate it here. + // for a format string with length n, the worst case that produces the most segments is when all its content is the char to split. + // For instance, when the format string is all \n, it will produce n+1 segments (because we did not omit empty entries). + Span splitIndices = stackalloc Range[input.Format.Length + 1]; + ReadOnlySpan formatSpan = input.Format.AsSpan(); + foreach ((ReadOnlySpan span, bool isLiteral, int index) in StringExtensions.GetFormattableStringFormatParts(formatSpan)) + { + // if isLiteral - put in formatBuilder + if (isLiteral) + { + var numSplits = span.SplitAny(splitIndices, ["\r\n", "\n"]); + for (int i = 0; i < numSplits; i++) + { + var part = span[splitIndices[i]]; + // the literals could contain { and }, but they are unescaped. Since we are putting them back into the format, we need to escape them again. + var startsWithCurlyBrace = part.Length > 0 && (part[0] == '{' || part[0] == '}'); + var start = startsWithCurlyBrace ? 1 : 0; + var endsWithCurlyBrace = part.Length > 0 && (part[^1] == '{' || part[^1] == '}'); + var end = endsWithCurlyBrace ? part.Length - 1 : part.Length; + if (startsWithCurlyBrace) + { + formatBuilder.Append(part[0]).Append(part[0]); + } + if (start <= end) // ensure that we have follow up characters before we move on + { + formatBuilder.Append(part[start..end]); + if (endsWithCurlyBrace) + { + formatBuilder.Append(part[^1]).Append(part[^1]); + } + } + if (i < numSplits - 1) + { + FormattableString formattableString = FormattableStringFactory.Create(formatBuilder.ToString(), args.ToArray()); + result.Add(formattableString); + formatBuilder.Clear(); + args.Clear(); + } + } + } + // if not Literal, is Args - recurse through Args and check if args has breaklines + else + { + var arg = input.GetArgument(index); + // we only break lines in the arguments if the argument is a string or FormattableString and it does not have a format specifier (indicating by : in span) + // we do nothing if the argument has a format specifier because we do not really know in which form to break them + // considering the chance of having these cases would be very rare, we are leaving the part of "arguments with formatter specifier" empty + var indexOfFormatSpecifier = span.IndexOf(':'); + switch (arg) + { + case string str when indexOfFormatSpecifier < 0: + BreakLinesCoreForString(str.AsSpan(), formatBuilder, args, result); + break; + case FormattableString fs when indexOfFormatSpecifier < 0: + BreakLinesCore(fs, formatBuilder, args, result); + break; + default: + // if not a string or FormattableString, add to args because we cannot parse it + // add to FormatBuilder to maintain equal count between args and formatBuilder + formatBuilder.Append('{'); + formatBuilder.Append(args.Count); + if (indexOfFormatSpecifier >= 0) + { + formatBuilder.Append(span[indexOfFormatSpecifier..]); + } + formatBuilder.Append('}'); + args.Add(arg); + break; + } + } + } + + return formatSpan[^1] == '\n'; + + static void BreakLinesCoreForString(ReadOnlySpan span, StringBuilder formatBuilder, List args, List result) + { + int start = 0, end = 0; + bool isLast = false; + // go into the loop when there are characters left + while (end < span.Length) + { + // we should not check both `\r\n` and `\n` because `\r\n` contains `\n`, if we use `IndexOf` to check both of them, there must be duplicate searches and we cannot have O(n) time complexity. + var indexOfLF = span[start..].IndexOf('\n'); + // check if the line already ends. + if (indexOfLF < 0) + { + end = span.Length; + isLast = true; + } + else + { + end = start + indexOfLF; + } + // omit \r if there is one before the \n to include the case that line breaks are using \r\n + int partEnd = end; + if (end > 0 && span[end - 1] == '\r') + { + partEnd--; + } + + formatBuilder.Append('{') + .Append(args.Count) + .Append('}'); + args.Add(span[start..partEnd].ToString()); + start = end + 1; // goes to the next char after the \n we found + + if (!isLast) + { + FormattableString formattableString = FormattableStringFactory.Create(formatBuilder.ToString(), args.ToArray()); + result.Add(formattableString); + formatBuilder.Clear(); + args.Clear(); + } + } + } + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.cs index d1281aab16..48b21614f5 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.cs @@ -79,7 +79,7 @@ public CodeWriter Append(FormattableString formattableString) const string declarationFormatString = ":D"; // :D :) const string identifierFormatString = ":I"; const string crefFormatString = ":C"; // wraps content into "see cref" tag, available only in xmlDoc - foreach ((var span, bool isLiteral, int index) in StringExtensions.GetPathParts(formattableString.Format)) + foreach ((var span, bool isLiteral, int index) in StringExtensions.GetFormattableStringFormatParts(formattableString.Format)) { if (isLiteral) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Utilities/FormattableStringHelpersTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Utilities/FormattableStringHelpersTests.cs new file mode 100644 index 0000000000..3c8988e7de --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Utilities/FormattableStringHelpersTests.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; + +namespace Microsoft.Generator.CSharp.Tests.Utilities +{ + public class FormattableStringHelpersTests + { + [TestCaseSource(nameof(TestBuildBreakLines))] + public void TestBreakLines(FormattableString input, List expected) + { + var result = FormattableStringHelpers.BreakLines(input); + Assert.AreEqual(expected.Count, result.Count); + // format in the line we have is the same as expected + for (int i = 0; i < result.Count; i++) + { + Assert.AreEqual(result[i].Format, expected[i].Format); + Assert.AreEqual(result[i].ArgumentCount, expected[i].ArgumentCount); + CollectionAssert.AreEqual(result[i].GetArguments(), expected[i].GetArguments()); + } + } + + public static IEnumerable TestBuildBreakLines + { + get + { + yield return new TestCaseData( + (FormattableString)$"\n\n\n\n", + new List + { + $"", $"", $"", $"", $"" // four line breaks should produce 5 lines. + }) + .SetName("TestBreakLines_AllLineBreaks"); + + yield return new TestCaseData( + (FormattableString)$"A timestamp indicating the last modified time\nclient. The operation will be performed only\nbeen modified since the specified time.", + new List { + $"A timestamp indicating the last modified time", + $"client. The operation will be performed only", + $"been modified since the specified time." + }).SetName("TestBreakLines_AllLiteralsNoArgs"); + + yield return new TestCaseData( + (FormattableString)$"A timestamp indicating \rthe last modified time\nclient. The operation will be performed only\nbeen modified since the specified time.", + new List { + $"A timestamp indicating \rthe last modified time", + $"client. The operation will be performed only", + $"been modified since the specified time." + }).SetName("TestBreakLines_AllLiteralsNoArgsWithCR"); + + yield return new TestCaseData( + (FormattableString)$"A timestamp indicating the last modified time\r\nclient. The operation will be performed only\r\nbeen modified since the specified time.", + new List { + $"A timestamp indicating the last modified time", + $"client. The operation will be performed only", + $"been modified since the specified time." + }).SetName("TestBreakLines_AllLiteralsNoArgsWithCRLF"); + + yield return new TestCaseData( + (FormattableString)$"A timestamp indicating the last modified time\r\nclient. The operation will be performed only\nbeen modified since the specified time.", + new List { + $"A timestamp indicating the last modified time", + $"client. The operation will be performed only", + $"been modified since the specified time." + }).SetName("TestBreakLines_AllLiteralsNoArgsWithMixedCRLF"); + + yield return new TestCaseData( + (FormattableString)$"{"A timestamp indicating the last modified time\nclient. The operation will be performed only\nbeen modified since the specified time."}", + new List { + $"{"A timestamp indicating the last modified time"}", + $"{"client. The operation will be performed only"}", + $"{"been modified since the specified time."}" + }).SetName("TestBreakLines_OneArgOnly"); + + yield return new TestCaseData( + (FormattableString)$"{"A timestamp indicating \rthe last modified time\nclient. The operation will be performed only\nbeen modified since the specified time."}", + new List { + $"{"A timestamp indicating \rthe last modified time"}", + $"{"client. The operation will be performed only"}", + $"{"been modified since the specified time."}" + }).SetName("TestBreakLines_OneArgOnlyWithCR"); + + yield return new TestCaseData( + (FormattableString)$"{"A timestamp indicating \rthe last modified time\r\r\r\nclient. The operation will be performed only\nbeen modified since the specified time."}", + new List { + $"{"A timestamp indicating \rthe last modified time\r\r"}", + $"{"client. The operation will be performed only"}", + $"{"been modified since the specified time."}" + }).SetName("TestBreakLines_OneArgOnlyWithMultipleCRs"); + + yield return new TestCaseData( + (FormattableString)$"{"A timestamp indicating the last modified time\r\nclient. The operation will be performed only\r\nbeen modified since the specified time."}", + new List { + $"{"A timestamp indicating the last modified time"}", + $"{"client. The operation will be performed only"}", + $"{"been modified since the specified time."}" + }).SetName("TestBreakLines_OneArgOnlyWithCRLF"); + + yield return new TestCaseData( + (FormattableString)$"{"A timestamp indicating the last modified time\r\nclient. The operation will be performed only\nbeen modified since the specified time."}", + new List { + $"{"A timestamp indicating the last modified time"}", + $"{"client. The operation will be performed only"}", + $"{"been modified since the specified time."}" + }).SetName("TestBreakLines_OneArgOnlyWithMixedCRLF"); + + yield return new TestCaseData( + (FormattableString)$"first{"x"}second\nthird{"y"}", + new List { + $"first{"x"}second", + $"third{"y"}" + }).SetName("TestBreakLines_LineBreaksInFormat"); + + yield return new TestCaseData( + (FormattableString)$"first{"x\nz"}second\nthird{"y"}", + new List { + $"first{"x"}", + $"{"z"}second", + $"third{"y"}" + }).SetName("TestBreakLines_LineBreakInArgument"); + + yield return new TestCaseData( + (FormattableString)$"first{"x"}second\nthird{"y\n"}", + new List { + $"first{"x"}second", + $"third{"y"}", + $"{""}" + }).SetName("TestBreakLines_LineBreaksAtEndOfArgument"); + + yield return new TestCaseData( + (FormattableString)$"first{"x"}second\nthird{null}", + new List { + $"first{"x"}second", + $"third{null}" + }).SetName("TestBreakLines_NullArgument"); + + yield return new TestCaseData( + (FormattableString)$"first{"x":L}second\nthird{null}", + new List { + $"first{"x":L}second", + $"third{null}" + }).SetName("TestBreakLines_TrivialFormatSpecifier"); + + yield return new TestCaseData( + (FormattableString)$"first{{", + new List { + $"first{{" + }).SetName("TestBreakLines_LiteralOpenBrace"); + + yield return new TestCaseData( + (FormattableString)$"first}}", + new List { + $"first}}" + }).SetName("TestBreakLines_LiteralCloseBrace"); + + yield return new TestCaseData( + (FormattableString)$"first{{}}", + new List { + $"first{{}}" + }).SetName("TestBreakLines_LiteralOpenAndCloseBrace"); + + yield return new TestCaseData( + (FormattableString)$"first{{T}}", + new List { + $"first{{T}}" + }).SetName("TestBreakLines_LiteralOpenAndCloseBraceWithT"); + + yield return new TestCaseData( + (FormattableString)$"first {"name"}: {{T}}, last {"name"}: {{U}}", + new List { + $"first {"name"}: {{T}}, last {"name"}: {{U}}" + }).SetName("TestBreakLines_LiteralOpenAndCloseBraceWithArgs"); + + yield return new TestCaseData( + (FormattableString)$"first{{\n}}", + new List { + $"first{{", + $"}}" + }).SetName("TestBreakLines_LiteralOpenAndCloseBraceWithLineBreaks"); + + yield return new TestCaseData( + (FormattableString)$"first{{T\n}}", + new List { + $"first{{T", + $"}}" + }).SetName("TestBreakLines_LiteralOpenAndCloseBraceWithLineBreaksAndT"); + + yield return new TestCaseData( + (FormattableString)$"first{{T{"name"}\n}}", + new List { + $"first{{T{"name"}", + $"}}" + }).SetName("TestBreakLines_LiteralOpenAndCloseBraceWithLineBreaksAndArgs"); + + yield return new TestCaseData( + (FormattableString)$"first{{T{"last\nname"}\n}}", + new List { + $"first{{T{"last"}", + $"{"name"}", + $"}}" + }).SetName("TestBreakLines_LiteralOpenAndCloseBraceWithLineBreaksAndArgsContainingLineBreaks"); + + FormattableString inner = $"{"x"}\n{"y"}z"; + FormattableString outter = $"first{inner}Second\nthird{null}"; + yield return new TestCaseData( + outter, + new List { + $"first{"x"}", + $"{"y"}zSecond", + $"third{null}" + }).SetName("TestBreakLines_RecursiveFormattableStrings"); + + inner = $"\n\n\n\n"; + outter = $"first{inner}second\nthird{null}"; + yield return new TestCaseData( + outter, + new List { + $"first", + $"", + $"", + $"", + $"second", + $"third{null}" + }).SetName("TestBreakLines_RecursiveFormattableStringsWithAllLineBreaks"); + + yield return new TestCaseData( + (FormattableString)$"first\n\n\n\nsecond\nthird{null}", + new List { + $"first", + $"", + $"", + $"", + $"second", + $"third{null}" + }).SetName("TestBreakLines_MultipleLineBreaks"); + + // current solution of format specifier in argument is that we ignore them during the process of line breaking. + yield return new TestCaseData( + (FormattableString)$"first{"x\ny":L}second\nthird{null}", + new List { + $"first{"x\ny":L}second", + $"third{null}" + }).SetName("TestBreakLines_FormatSpecifierInArg"); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Utilities/StringExtensionsTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Utilities/StringExtensionsTests.cs index 4c67a059a7..1f1a6d8b43 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Utilities/StringExtensionsTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Utilities/StringExtensionsTests.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Generator.CSharp.Input; -using Moq; +using System; +using System.Collections.Generic; using NUnit.Framework; namespace Microsoft.Generator.CSharp.Tests.Utilities @@ -129,5 +129,94 @@ public void TestToApiVersionMemberName(string apiVersion, string expectedApiVers var name = apiVersion.ToApiVersionMemberName(); Assert.AreEqual(expectedApiVersion, name); } + + [TestCaseSource(nameof(BuildFormattableStringFormatParts))] + public void ValidateGetFormattableStringFormatParts(string format, IReadOnlyList parts) + { + var i = 0; + foreach (var (span, isLiteral, index) in StringExtensions.GetFormattableStringFormatParts(format)) + { + Assert.AreEqual(parts[i].Value, span.ToString()); + Assert.AreEqual(parts[i].IsLiteral, isLiteral); + Assert.AreEqual(parts[i].ArgumentIndex, index); + i++; + } + } + + public record Part(string Value, bool IsLiteral, int ArgumentIndex); + + public static IEnumerable BuildFormattableStringFormatParts + { + get + { + // simple case with only arguments + yield return new TestCaseData("{0}{1}", new Part[] + { + new Part("0", false, 0), + new Part("1", false, 1) + }); + // simple case with only literals + yield return new TestCaseData("something", new Part[] + { + new Part("something", true, -1) // literals do not have an argument index + }); + // mixed case with both arguments and literals + yield return new TestCaseData("something{0}else{1}", new Part[] + { + new Part("something", true, -1), + new Part("0", false, 0), + new Part("else", true, -1), + new Part("1", false, 1) + }); + // when the format contains a { or } literal at its end + FormattableString fs = $"This {"fs"} has literal {{"; + yield return new TestCaseData(fs.Format, new Part[] + { + new Part("This ", true, -1), + new Part("0", false, 0), + new Part(" has literal {", true, -1) + }); + // when the format contains a { or } literal at its end + fs = $"This {"fs"} has literal }}"; + yield return new TestCaseData(fs.Format, new Part[] + { + new Part("This ", true, -1), + new Part("0", false, 0), + new Part(" has literal }", true, -1) + }); + // when the format contains a { or } literal in its middle + fs = $"This {"fs"} has literal }} and {"fs"}"; + yield return new TestCaseData(fs.Format, new Part[] + { + new Part("This ", true, -1), + new Part("0", false, 0), + new Part(" has literal }", true, -1), // the implementation will break up the literals by { and } and unescape them + new Part(" and ", true, -1), + new Part("1", false, 1) + }); + // when the format contains both literal { and } in its middle + fs = $"This {"fs"} has literal {{ and }} in the middle"; + yield return new TestCaseData(fs.Format, new Part[] + { + new Part("This ", true, -1), + new Part("0", false, 0), + new Part(" has literal {", true, -1), + new Part(" and }", true, -1), + new Part(" in the middle", true, -1) + }); + // when the format contains both literal { and } in its middle but separated by an argument + fs = $"This {"fs"} has literal {{, {"fs"} and }} in the middle"; + yield return new TestCaseData(fs.Format, new Part[] + { + new Part("This ", true, -1), + new Part("0", false, 0), + new Part(" has literal {", true, -1), + new Part(", ", true, -1), + new Part("1", false, 1), + new Part(" and }", true, -1), + new Part(" in the middle", true, -1) + }); + } + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/CodeWriterTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/CodeWriterTests.cs index 46fd87da39..9f6832748d 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/CodeWriterTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/CodeWriterTests.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Text; using Microsoft.Generator.CSharp.Expressions; @@ -98,6 +97,19 @@ public void SeeCRefType(Type type, bool isNullable) Assert.AreEqual(expected, writer.ToString()); } + [Test] + public void SingleLineSummaryWithLineBreaks() + { + FormattableString fs = $"Some\nmultiline\n{typeof(string)}\nsummary."; + using var writer = new CodeWriter(); + var summary = new XmlDocSummaryStatement([fs]); + summary.Write(writer); + + var expected = Helpers.GetExpectedFromFile(); + + Assert.AreEqual(expected, writer.ToString(false)); + } + [Test] public void MultiLineSummary() { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TestData/CodeWriterTests/SingleLineSummaryWithLineBreaks.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TestData/CodeWriterTests/SingleLineSummaryWithLineBreaks.cs new file mode 100644 index 0000000000..b4b86abc93 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TestData/CodeWriterTests/SingleLineSummaryWithLineBreaks.cs @@ -0,0 +1,6 @@ +/// +/// Some +/// multiline +/// string +/// summary. +/// diff --git a/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Client/Structure/Default/DefaultTests.cs b/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Client/Structure/Default/DefaultTests.cs index ca0fa94f4e..fae20c8f35 100644 --- a/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Client/Structure/Default/DefaultTests.cs +++ b/packages/http-client-csharp/generator/TestProjects/CadlRanch.Tests/Http/Client/Structure/Default/DefaultTests.cs @@ -35,7 +35,6 @@ public void Client_Structure_default_methods(Type client, string[] methodNames) [CadlRanchTest] public Task Client_Structure_default_One() => Test(async (host) => { - //await new RenamedOperationClient(host, ClientType.RenamedOperation, null).RenamedOneAsync(); var response = await new ServiceClient(host, ClientType.Default).OneAsync(); Assert.AreEqual(204, response.GetRawResponse().Status); }); From 339dd17b796ed990de27cd6dc2f586012c8a79d9 Mon Sep 17 00:00:00 2001 From: Alan Zimmer <48699787+alzimmermsft@users.noreply.github.com> Date: Wed, 11 Dec 2024 22:47:33 -0500 Subject: [PATCH 3/3] Move setting retrieval into constructor (#5338) Moves getting the Autorest / TypeSpec settings into the constructor of `JavaSettings` rather than each being an input parameter to the constructor. This should make it less confusing when trying to understand which setting turns into what `JavaSettings` configuration. --- .../core/extension/plugin/JavaSettings.java | 464 +++++++++--------- 1 file changed, 226 insertions(+), 238 deletions(-) diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/JavaSettings.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/JavaSettings.java index 00398cb2cc..0dc5e9e5be 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/JavaSettings.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/JavaSettings.java @@ -108,68 +108,8 @@ public static JavaSettings getInstance() { logger.debug("List of require : {}", autorestSettings.getRequire()); } - final String fluent = getStringValue(host, "fluent"); - - final String flavor = getStringValue(host, "flavor", "azure"); - final String defaultModelsSubPackageName = isBranded(flavor) ? "models" : ""; - setHeader(getStringValue(host, "license-header")); - instance = new JavaSettings(autorestSettings, - host.getValueWithJsonReader("modelerfour", jsonReader -> jsonReader.readMap(JsonReader::readUntyped)), - getBooleanValue(host, "azure-arm", false), getBooleanValue(host, "sdk-integration", false), fluent, - getBooleanValue(host, "regenerate-pom", false), header, getStringValue(host, "service-name"), - getStringValue(host, "namespace", "com.azure.app").toLowerCase(), - getBooleanValue(host, "client-side-validations", false), getStringValue(host, "client-type-prefix"), - getBooleanValue(host, "generate-client-interfaces", false), - getBooleanValue(host, "generate-client-as-impl", false), - getStringValue(host, "implementation-subpackage", "implementation"), - getStringValue(host, "models-subpackage", defaultModelsSubPackageName), - getStringValue(host, "custom-types", ""), getStringValue(host, "custom-types-subpackage", ""), - getStringValue(host, "fluent-subpackage", "fluent"), - getBooleanValue(host, "required-parameter-client-methods", false), - getBooleanValue(host, "generate-sync-async-clients", false), - getBooleanValue(host, "generate-builder-per-client", false), - getStringValue(host, "sync-methods", "essential"), getBooleanValue(host, "client-logger", false), - getBooleanValue(host, "required-fields-as-ctor-args", false), - getBooleanValue(host, "service-interface-as-public", true), getStringValue(host, "artifact-id", ""), - getStringValue(host, "credential-types", "none"), getStringValue(host, "credential-scopes"), - getStringValue(host, "customization-jar-path"), getStringValue(host, "customization-class"), - getBooleanValue(host, "optional-constant-as-enum", false), getBooleanValue(host, "data-plane", false), - getBooleanValue(host, "use-iterable", false), - host.getValueWithJsonReader("service-versions", - jsonReader -> jsonReader.readArray(JsonReader::getString)), - getStringValue(host, "client-flattened-annotation-target", ""), - getStringValue(host, "key-credential-header-name", ""), - getBooleanValue(host, "disable-client-builder", false), - host.getValueWithJsonReader("polling", jsonReader -> jsonReader.readMap(PollingDetails::fromJson)), - getBooleanValue(host, "generate-samples", false), getBooleanValue(host, "generate-tests", false), false, - getBooleanValue(host, "annotate-getters-and-setters-for-serialization", false), - getStringValue(host, "default-http-exception-type"), - getBooleanValue(host, "use-default-http-status-code-to-exception-type-mapping", false), - host.getValueWithJsonReader("http-status-code-to-exception-type-mapping", - JavaSettings::parseStatusCodeMapping), - getBooleanValue(host, "partial-update", false), - // If fluent default to false, this is because the automated test generation ends up with invalid code. - // Once that is fixed, this can be switched over to true. - getBooleanValue(host, "generic-response-type", fluent == null), - getBooleanValue(host, "stream-style-serialization", true), - getBooleanValue(host, "enable-sync-stack", false), - getBooleanValue(host, "output-model-immutable", false), - getBooleanValue(host, "use-input-stream-for-binary", false), - getBooleanValue(host, "no-custom-headers", true), - getBooleanValue(host, "include-read-only-in-constructor-args", false), - // setting the default as true as the Java design guideline recommends using String for URLs. - getBooleanValue(host, "url-as-string", true), getBooleanValue(host, "uuid-as-string", false), - - // setting this to false by default as a lot of existing libraries still use swagger and - // were generated with required = true set in JsonProperty annotation - getBooleanValue(host, "disable-required-property-annotation", false), - getBooleanValue(host, "enable-page-size", false), getBooleanValue(host, "use-key-credential", false), - getBooleanValue(host, "null-byte-array-maps-to-empty-array", false), - getBooleanValue(host, "graal-vm-config", false), flavor, - getBooleanValue(host, "disable-typed-headers-methods", false), - getBooleanValue(host, "share-jsonserializable-code", false), - getBooleanValue(host, "use-object-for-unknown", false), getBooleanValue(host, "android", false)); + instance = new JavaSettings(autorestSettings); } return instance; } @@ -191,159 +131,108 @@ private static Map parseStatusCodeMapping(JsonReader jsonReader * Create a new JavaSettings object with the provided properties. * * @param autorestSettings The autorest settings. - * @param modelerSettings The modeler settings. - * @param azure Whether to generate the Azure. - * @param sdkIntegration Whether to generate the SDK integration. - * @param fluent The fluent generation mode. - * @param regeneratePom Whether to regenerate the POM. - * @param fileHeaderText The file header text. - * @param serviceName The service name. - * @param packageKeyword The package keyword. - * @param clientSideValidations Whether to add client-side validations to the generated clients. - * @param clientTypePrefix The prefix that will be added to each generated client type. - * @param generateClientInterfaces Whether interfaces will be generated for Service and Method Group clients. - * @param generateClientAsImpl Whether Service and Method Group clients will be generated as implementation - * @param implementationSubpackage The sub-package that the Service and Method Group client implementation classes - * will be put into. - * @param modelsSubpackage The sub-package that Enums, Exceptions, and Model types will be put into. - * @param customTypes The custom types that will be generated. - * @param customTypesSubpackage The sub-package that custom types will be put into. - * @param fluentSubpackage The sub-package that Fluent interfaces will be put into. - * @param requiredParameterClientMethods Whether Service and Method Group client method overloads that omit optional - * parameters will be created. - * @param generateSyncAsyncClients Whether Service and Method Group clients will be generated with both synchronous - * and asynchronous methods. - * @param generateBuilderPerClient Whether a builder will be generated for each Service and Method Group client. - * @param syncMethods The sync methods generation mode. - * @param clientLogger Whether to add a logger to the generated clients. - * @param requiredFieldsAsConstructorArgs Whether required fields will be included in the constructor arguments for - * generated models. - * @param serviceInterfaceAsPublic If set to true, proxy method service interface will be marked as public. - * @param artifactId The artifactId for the generated project. - * @param credentialType The type of credential to generate. - * @param credentialScopes The scopes for the generated credential. - * @param customizationJarPath The path to the customization jar. - * @param customizationClass The class to use for customization. - * @param optionalConstantAsEnum Whether to generate optional constants as enums. - * @param dataPlaneClient Whether to generate a data plane client. - * @param useIterable Whether to use Iterable instead of List for collection types. - * @param serviceVersions The versions of the service. - * @param clientFlattenAnnotationTarget The target for the @JsonFlatten annotation for - * x-ms-client-flatten. - * @param keyCredentialHeaderName The header name for the key credential. - * @param clientBuilderDisabled Whether to disable the client builder. - * @param pollingConfig The polling configuration. - * @param generateSamples Whether to generate samples. - * @param generateTests Whether to generate tests. - * @param generateSendRequestMethod Whether to generate the send request method. - * @param annotateGettersAndSettersForSerialization If set to true, Jackson JsonGetter and JsonSetter will annotate - * getters and setters in generated models to handle serialization and deserialization. For now, fields will - * continue being annotated to ensure that there are no backwards compatibility breaks. - * @param defaultHttpExceptionType The fully-qualified class that should be used as the default exception type. This - * class must extend from HttpResponseException. - * @param useDefaultHttpStatusCodeToExceptionTypeMapping Determines whether a well-known HTTP status code to - * exception type mapping should be used if an HTTP status code-exception mapping isn't provided. - * @param httpStatusCodeToExceptionTypeMapping A mapping of HTTP response status code to the exception type that - * should be thrown if that status code is seen. All exception types must be fully-qualified and extend from - * HttpResponseException. - * @param handlePartialUpdate If set to true, the generated model will handle partial updates. - * @param genericResponseTypes If set to true, responses will only use Response, ResponseBase, PagedResponse, and - * PagedResponseBase types with generics instead of creating a specific named type that extends one of those types. - * @param streamStyleSerialization If set to true, models will handle serialization themselves using stream-style - * serialization instead of relying on Jackson Databind. - * @param isSyncStackEnabled If set to true, sync methods are generated using sync stack. i.e these methods do - * not use sync-over-async stack. - * @param outputModelImmutable If set to true, the models that are determined as output only models will be made - * immutable without any public constructors or setter methods. - * @param streamResponseInputStream If set to true, sync methods will use {@code InputStream} for binary responses. - * @param noCustomHeaders If set to true, methods that have custom header types will also have an equivalent - * method that returns just the response with untyped headers. - * @param includeReadOnlyInConstructorArgs If set to true, read-only required properties will be included in the - * constructor if {@code requiredFieldsAsConstructorArgs} is true. This is a backwards compatibility flag as - * previously read-only required were included in constructors. - * @param urlAsString This generates all URLs as String type. This is enabled by default as required by the Java - * design guidelines. For backward compatibility, this can be set to false. - * @param disableRequiredPropertyAnnotation If set to true, the required property annotation will be disabled. - * @param pageSizeEnabled If set to true, the generated client will have support for page size. - * @param useKeyCredential If set to true, the generated client will have support for key credential. - * @param nullByteArrayMapsToEmptyArray If set to true, {@code ArrayType.BYTE_ARRAY} will return an empty array - * instead of null when the default value expression is null. - * @param generateGraalVmConfig If set to true, the generated client will have support for GraalVM. - * @param flavor The brand name we use to generate SDK. - * @param disableTypedHeadersMethods Prevents generating REST API methods that include typed headers. If set to - * true, {@code noCustomHeaders} will be ignored as no REST APIs with typed headers will be generated. - * @param shareJsonSerializableCode Whether models implementing {@code JsonSerializable} can attempt to share code - * for {@code toJson} and {@code fromJson}. - * @param android Whether to generate the Android client. - */ - private JavaSettings(AutorestSettings autorestSettings, Map modelerSettings, boolean azure, - boolean sdkIntegration, String fluent, boolean regeneratePom, String fileHeaderText, String serviceName, - String packageKeyword, boolean clientSideValidations, String clientTypePrefix, boolean generateClientInterfaces, - boolean generateClientAsImpl, String implementationSubpackage, String modelsSubpackage, String customTypes, - String customTypesSubpackage, String fluentSubpackage, boolean requiredParameterClientMethods, - boolean generateSyncAsyncClients, boolean generateBuilderPerClient, String syncMethods, boolean clientLogger, - boolean requiredFieldsAsConstructorArgs, boolean serviceInterfaceAsPublic, String artifactId, - String credentialType, String credentialScopes, String customizationJarPath, String customizationClass, - boolean optionalConstantAsEnum, boolean dataPlaneClient, boolean useIterable, List serviceVersions, - String clientFlattenAnnotationTarget, String keyCredentialHeaderName, boolean clientBuilderDisabled, - Map pollingConfig, boolean generateSamples, boolean generateTests, - boolean generateSendRequestMethod, boolean annotateGettersAndSettersForSerialization, - String defaultHttpExceptionType, boolean useDefaultHttpStatusCodeToExceptionTypeMapping, - Map httpStatusCodeToExceptionTypeMapping, boolean handlePartialUpdate, - boolean genericResponseTypes, boolean streamStyleSerialization, boolean isSyncStackEnabled, - boolean outputModelImmutable, boolean streamResponseInputStream, boolean noCustomHeaders, - boolean includeReadOnlyInConstructorArgs, boolean urlAsString, boolean uuidAsString, - boolean disableRequiredPropertyAnnotation, boolean pageSizeEnabled, boolean useKeyCredential, - boolean nullByteArrayMapsToEmptyArray, boolean generateGraalVmConfig, String flavor, - boolean disableTypedHeadersMethods, boolean shareJsonSerializableCode, boolean useObjectForUnknown, - boolean android) { - + */ + private JavaSettings(AutorestSettings autorestSettings) { this.autorestSettings = autorestSettings; - this.modelerSettings = new ModelerSettings(modelerSettings); - this.azure = azure; - this.sdkIntegration = sdkIntegration; - this.fluent = fluent == null + + // The modeler settings. + this.modelerSettings = new ModelerSettings( + host.getValueWithJsonReader("modelerfour", jsonReader -> jsonReader.readMap(JsonReader::readUntyped))); + + // Whether to generate the Azure. + this.azure = getBooleanValue(host, "azure-arm", false); + + // Whether to generate the SDK integration. + this.sdkIntegration = getBooleanValue(host, "sdk-integration", false); + + // The fluent generation mode. + String fluentString = getStringValue(host, "fluent"); + this.fluent = fluentString == null ? Fluent.NONE - : (fluent.isEmpty() || fluent.equalsIgnoreCase("true") + : (fluentString.isEmpty() || fluentString.equalsIgnoreCase("true") ? Fluent.PREMIUM - : Fluent.valueOf(fluent.toUpperCase(Locale.ROOT))); - this.regeneratePom = regeneratePom; - this.fileHeaderText = fileHeaderText; - this.serviceName = serviceName; - this.packageName = packageKeyword; - this.clientSideValidations = clientSideValidations; - this.clientTypePrefix = clientTypePrefix; - this.generateClientInterfaces = generateClientInterfaces; - this.generateClientAsImpl = generateClientAsImpl || generateSyncAsyncClients || generateClientInterfaces; - this.implementationSubpackage = implementationSubpackage; - this.modelsSubpackage = modelsSubpackage; + : Fluent.valueOf(fluentString.toUpperCase(Locale.ROOT))); + + // Whether to regenerate the POM. + this.regeneratePom = getBooleanValue(host, "regenerate-pom", false); + + // The file header text. + this.fileHeaderText = header; + + // The service name. + this.serviceName = getStringValue(host, "service-name"); + + // The package keyword. + this.packageName = getStringValue(host, "namespace", "com.azure.app").toLowerCase(); + + // Whether to add client-side validations to the generated clients. + this.clientSideValidations = getBooleanValue(host, "client-side-validations", false); + + // The prefix that will be added to each generated client type. + this.clientTypePrefix = getStringValue(host, "client-type-prefix"); + + // Whether interfaces will be generated for Service and Method Group clients. + this.generateClientInterfaces = getBooleanValue(host, "generate-client-interfaces", false); + + // The sub-package that the Service and Method Group client implementation classes will be put into. + this.implementationSubpackage = getStringValue(host, "implementation-subpackage", "implementation"); + + // The brand name we use to generate SDK. + this.flavor = getStringValue(host, "flavor", "azure"); + + this.modelsSubpackage = getStringValue(host, "models-subpackage", isBranded(this.flavor) ? "models" : ""); + + // The custom types that will be generated. + String customTypes = getStringValue(host, "custom-types", ""); this.customTypes = (customTypes == null || customTypes.isEmpty()) ? new ArrayList<>() : Arrays.asList(customTypes.split(",")); - this.customTypesSubpackage = customTypesSubpackage; - this.fluentSubpackage = fluentSubpackage; - this.requiredParameterClientMethods = requiredParameterClientMethods; - this.generateSyncAsyncClients = generateSyncAsyncClients; - this.generateBuilderPerClient = generateBuilderPerClient; - this.syncMethods = SyncMethodsGeneration.fromValue(syncMethods); - this.clientLogger = clientLogger; - this.requiredFieldsAsConstructorArgs = requiredFieldsAsConstructorArgs; - this.serviceInterfaceAsPublic = serviceInterfaceAsPublic; - this.artifactId = artifactId; - this.optionalConstantAsEnum = optionalConstantAsEnum; - this.dataPlaneClient = dataPlaneClient; - this.useIterable = useIterable; - this.serviceVersions = serviceVersions; - this.clientFlattenAnnotationTarget - = (clientFlattenAnnotationTarget == null || clientFlattenAnnotationTarget.isEmpty()) - ? ClientFlattenAnnotationTarget.TYPE - : ClientFlattenAnnotationTarget.valueOf(clientFlattenAnnotationTarget.toUpperCase(Locale.ROOT)); + // The sub-package that custom types will be put into. + this.customTypesSubpackage = getStringValue(host, "custom-types-subpackage", ""); + + // The sub-package that Fluent interfaces will be put into. + this.fluentSubpackage = getStringValue(host, "fluent-subpackage", "fluent"); + + // Whether Service and Method Group client method overloads that omit optional parameters will be created. + this.requiredParameterClientMethods = getBooleanValue(host, "required-parameter-client-methods", false); + + // Whether Service and Method Group clients will be generated with both synchronous and asynchronous methods. + this.generateSyncAsyncClients = getBooleanValue(host, "generate-sync-async-clients", false); + + // Whether Service and Method Group clients will be generated as implementation + this.generateClientAsImpl = getBooleanValue(host, "generate-client-as-impl", false) + || this.generateSyncAsyncClients + || this.generateClientInterfaces; + + // Whether a builder will be generated for each Service and Method Group client. + this.generateBuilderPerClient = getBooleanValue(host, "generate-builder-per-client", false); + + // The sync methods generation mode. + this.syncMethods = SyncMethodsGeneration.fromValue(getStringValue(host, "sync-methods", "essential")); + + // Whether to add a logger to the generated clients. + this.clientLogger = getBooleanValue(host, "client-logger", false); + + // Whether required fields will be included in the constructor arguments for generated models. + this.requiredFieldsAsConstructorArgs = getBooleanValue(host, "required-fields-as-ctor-args", false); + + // If set to true, proxy method service interface will be marked as public. + this.serviceInterfaceAsPublic = getBooleanValue(host, "service-interface-as-public", true); + + // The artifactId for the generated project. + this.artifactId = getStringValue(host, "artifact-id", ""); + + // The types of credentials to generate. + String credentialType = getStringValue(host, "credential-types", "none"); if (credentialType != null) { String[] splits = credentialType.split(","); this.credentialTypes = Arrays.stream(splits).map(String::trim).map(CredentialType::fromValue).collect(Collectors.toSet()); } + + // The scopes for the generated credential. + String credentialScopes = getStringValue(host, "credential-scopes"); if (credentialScopes != null) { String[] splits = credentialScopes.split(","); this.credentialScopes = Arrays.stream(splits).map(String::trim).map(split -> { @@ -353,50 +242,149 @@ private JavaSettings(AutorestSettings autorestSettings, Map mode return split; }).collect(Collectors.toSet()); } - this.customizationJarPath = customizationJarPath; - this.customizationClass = customizationClass; - this.keyCredentialHeaderName = keyCredentialHeaderName; - this.clientBuilderDisabled = clientBuilderDisabled; + + // The path to the customization jar. + this.customizationJarPath = getStringValue(host, "customization-jar-path"); + + // The class to use for customization. + this.customizationClass = getStringValue(host, "customization-class"); + + // Whether to generate optional constants as enums. + this.optionalConstantAsEnum = getBooleanValue(host, "optional-constant-as-enum", false); + + // Whether to generate a data plane client. + this.dataPlaneClient = getBooleanValue(host, "data-plane", false); + + // Whether to use Iterable instead of List for collection types. + this.useIterable = getBooleanValue(host, "use-iterable", false); + + // The versions of the service. + this.serviceVersions = host.getValueWithJsonReader("service-versions", + jsonReader -> jsonReader.readArray(JsonReader::getString)); + + // The target for the @JsonFlatten annotation for x-ms-client-flatten. + String clientFlattenAnnotationTarget = getStringValue(host, "client-flattened-annotation-target", ""); + this.clientFlattenAnnotationTarget + = (clientFlattenAnnotationTarget == null || clientFlattenAnnotationTarget.isEmpty()) + ? ClientFlattenAnnotationTarget.TYPE + : ClientFlattenAnnotationTarget.valueOf(clientFlattenAnnotationTarget.toUpperCase(Locale.ROOT)); + + // The header name for the key credential. + this.keyCredentialHeaderName = getStringValue(host, "key-credential-header-name", ""); + + // Whether to disable the client builder. + this.clientBuilderDisabled = getBooleanValue(host, "disable-client-builder", false); + + // The polling configuration. + Map pollingConfig + = host.getValueWithJsonReader("polling", jsonReader -> jsonReader.readMap(PollingDetails::fromJson)); if (pollingConfig != null) { if (!pollingConfig.containsKey("default")) { pollingConfig.put("default", new PollingDetails()); } } this.pollingConfig = pollingConfig; - this.generateSamples = generateSamples; - this.generateTests = generateTests; - this.generateSendRequestMethod = generateSendRequestMethod; - this.annotateGettersAndSettersForSerialization = annotateGettersAndSettersForSerialization; + + // Whether to generate samples. + this.generateSamples = getBooleanValue(host, "generate-samples", false); + + // Whether to generate tests. + this.generateTests = getBooleanValue(host, "generate-tests", false); + + // Whether to generate the send request method. + this.generateSendRequestMethod = false; + + // If set to true, Jackson JsonGetter and JsonSetter will annotate getters and setters in generated models to + // handle serialization and deserialization. For now, fields will continue being annotated to ensure that there + // are no backwards compatibility breaks. + this.annotateGettersAndSettersForSerialization + = getBooleanValue(host, "annotate-getters-and-setters-for-serialization", false); // Error HTTP status code exception type handling. - this.defaultHttpExceptionType = defaultHttpExceptionType; - this.useDefaultHttpStatusCodeToExceptionTypeMapping = useDefaultHttpStatusCodeToExceptionTypeMapping; - this.httpStatusCodeToExceptionTypeMapping = httpStatusCodeToExceptionTypeMapping; - - this.handlePartialUpdate = handlePartialUpdate; - - this.genericResponseTypes = genericResponseTypes; - - this.streamStyleSerialization = streamStyleSerialization; - this.syncStackEnabled = isSyncStackEnabled; - - this.outputModelImmutable = outputModelImmutable; - - this.isInputStreamForBinary = streamResponseInputStream; - this.noCustomHeaders = noCustomHeaders; - this.includeReadOnlyInConstructorArgs = includeReadOnlyInConstructorArgs; - this.urlAsString = urlAsString; - this.uuidAsString = uuidAsString; - this.disableRequiredJsonAnnotation = disableRequiredPropertyAnnotation; - this.pageSizeEnabled = pageSizeEnabled; - this.useKeyCredential = useKeyCredential; - this.nullByteArrayMapsToEmptyArray = nullByteArrayMapsToEmptyArray; - this.generateGraalVmConfig = generateGraalVmConfig; - this.flavor = flavor; - this.disableTypedHeadersMethods = disableTypedHeadersMethods; - this.shareJsonSerializableCode = shareJsonSerializableCode; - this.useObjectForUnknown = useObjectForUnknown; - this.android = android; + // The fully-qualified class that should be used as the default exception type. This class must extend from + // HttpResponseException + this.defaultHttpExceptionType = getStringValue(host, "default-http-exception-type"); + + // Whether to use the default HTTP status code to exception type mapping. + this.useDefaultHttpStatusCodeToExceptionTypeMapping + = getBooleanValue(host, "use-default-http-status-code-to-exception-type-mapping", false); + + // A mapping of HTTP response status code to the exception type that should be thrown if that status code is + // seen. All exception types must be fully-qualified and extend from HttpResponseException. + this.httpStatusCodeToExceptionTypeMapping = host + .getValueWithJsonReader("http-status-code-to-exception-type-mapping", JavaSettings::parseStatusCodeMapping); + + // Whether to handle partial updates. + this.handlePartialUpdate = getBooleanValue(host, "partial-update", false); + + // If set to true, responses will only use Response, ResponseBase, PagedResponse, and PagedResponseBase types + // with generics instead of creating a specific named type that extends one of those types. + // If fluent default to false, this is because the automated test generation ends up with invalid code. + // Once that is fixed, this can be switched over to true. + this.genericResponseTypes = getBooleanValue(host, "generic-response-type", fluentString == null); + + // If set to true, models will handle serialization themselves using stream-style serialization instead of + // relying on Jackson Databind. + this.streamStyleSerialization = getBooleanValue(host, "stream-style-serialization", true); + + // If set to true, sync methods are generated using sync stack. i.e these methods do not use sync-over-async + // stack. + this.syncStackEnabled = getBooleanValue(host, "enable-sync-stack", false); + + // If set to true, the models that are determined as output only models will be made immutable without any + // public constructors or setter methods. + this.outputModelImmutable = getBooleanValue(host, "output-model-immutable", false); + + // Whether to use InputStream for binary responses. + this.isInputStreamForBinary = getBooleanValue(host, "use-input-stream-for-binary", false); + + // If set to true, methods that have custom header types will also have an equivalent method that returns just + // the response with untyped headers. + this.noCustomHeaders = getBooleanValue(host, "no-custom-headers", true); + + // If set to true, read-only required properties will be included in the constructor if + // {@code requiredFieldsAsConstructorArgs} is true. This is a backwards compatibility flag as previously + // read-only required were included in constructors. + this.includeReadOnlyInConstructorArgs = getBooleanValue(host, "include-read-only-in-constructor-args", false); + + // This generates all URLs as String type. This is enabled by default as required by the Java design guidelines. + // For backward compatibility, this can be set to false. + // setting the default as true as the Java design guideline recommends using String for URLs. + this.urlAsString = getBooleanValue(host, "url-as-string", true); + + // Generate UUID as string. + this.uuidAsString = getBooleanValue(host, "uuid-as-string", false); + + // If set to true, the required property annotation will be disabled. + // setting this to false by default as a lot of existing libraries still use swagger and were generated with + // required = true set in JsonProperty annotation + this.disableRequiredJsonAnnotation = getBooleanValue(host, "disable-required-property-annotation", false); + + // If set to true, the generated client will have support for page size. + this.pageSizeEnabled = getBooleanValue(host, "enable-page-size", false); + + // If set to true, the generated client will have support for key credential. + this.useKeyCredential = getBooleanValue(host, "use-key-credential", false); + + // If set to true, {@code ArrayType.BYTE_ARRAY} will return an empty array instead of null when the default + // value expression is null. + this.nullByteArrayMapsToEmptyArray = getBooleanValue(host, "null-byte-array-maps-to-empty-array", false); + + // If set to true, the generated client will have support for GraalVM. + this.generateGraalVmConfig = getBooleanValue(host, "graal-vm-config", false); + + // Prevents generating REST API methods that include typed headers. If set to true, {@code noCustomHeaders} + // will be ignored as no REST APIs with typed headers will be generated. + this.disableTypedHeadersMethods = getBooleanValue(host, "disable-typed-headers-methods", false); + + // Whether models implementing {@code JsonSerializable} can attempt to share code for toJson and fromJson. + this.shareJsonSerializableCode = getBooleanValue(host, "share-jsonserializable-code", false); + + // Whether to use object for unknown. + this.useObjectForUnknown = getBooleanValue(host, "use-object-for-unknown", false); + + // Whether to generate the Android client. + this.android = getBooleanValue(host, "android", false); } /**