diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index 8bd34db8889..7cd46be5582 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -116,15 +116,19 @@ export async function $onEmit(context: EmitContext) { const isValid = await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion); // if the dotnet sdk is valid, the error is not dependency issue, log it as normal if (isValid) { - throw new Error( - `Failed to generate the library. Exit code: ${result.exitCode}.\nStackTrace: \n${result.stderr}`, + sdkContext.logger.error( + `Failed to generate the library. Exit code: ${result.exitCode}.\n${result.stderr}`, ); } } } catch (error: any) { const isValid = await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion); // if the dotnet sdk is valid, the error is not dependency issue, log it as normal - if (isValid) throw new Error(error); + if (isValid) { + sdkContext.logger.error( + `Failed to generate the library. Error: ${error.message ?? error}`, + ); + } } if (!options["save-inputs"]) { // delete diff --git a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts index 6755c79fde1..bdcb5f4c1ce 100644 --- a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts @@ -94,6 +94,26 @@ describe("$onEmit tests", () => { expect(updateCallback).toHaveBeenCalledTimes(1); }); + it("should report diagnostic instead of throwing when generator fails", async () => { + vi.mocked(execCSharpGenerator).mockResolvedValueOnce({ + exitCode: 1, + stdout: "", + stderr: "Unable to parse required option package-name from configuration.", + } as any); + vi.mocked(execAsync).mockResolvedValueOnce({ + exitCode: 0, + stdio: "", + stdout: "9.0.102", + stderr: "", + proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" }, + } as any); + + const context: EmitContext = createEmitterContext(program); + // should not throw + await $onEmit(context); + expect(program.reportDiagnostics).toHaveBeenCalled(); + }); + it("should apply sdk-context-options", async () => { const context: EmitContext = createEmitterContext(program); const additionalDecorators = ["Decorator1", "Decorator2"]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs index 4a41327828e..12b845661df 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs @@ -108,6 +108,12 @@ internal set protected internal virtual void Configure() { + if (Configuration.PackageName == Configuration.DefaultPackageName) + { + Configuration.PackageName = TypeFactory.PrimaryNamespace; + Emitter.Info($"'package-name' was not specified. Defaulting to namespace '{Configuration.PackageName}'."); + } + foreach (var type in CustomCodeAttributeProviders) { AddTypeToKeep(type); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs index 98169440b6f..b52e4ccae68 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs @@ -22,6 +22,7 @@ public enum UnreferencedTypesHandlingOption private const string GeneratedFolderName = "Generated"; private const string ConfigurationFileName = "Configuration.json"; + internal const string DefaultPackageName = "GeneratedClient"; #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. protected Configuration() @@ -84,7 +85,7 @@ private static class Options private string? _testGeneratedDirectory; internal string TestGeneratedDirectory => _testGeneratedDirectory ??= Path.Combine(TestProjectDirectory, GeneratedFolderName); - public string PackageName { get; } + public string PackageName { get; internal set; } /// /// True if a sample project should be generated. @@ -120,7 +121,7 @@ internal static Configuration Load(string outputPath, string? json = null) return new Configuration( Path.GetFullPath(outputPath), ParseAdditionalConfigOptions(root), - ReadRequiredStringOption(root, Options.PackageName), + ReadStringOption(root, Options.PackageName) ?? DefaultPackageName, ReadOption(root, Options.DisableXmlDocs), ReadEnumOption(root, Options.UnreferencedTypesHandling), ReadLicenseInfo(root)); @@ -178,11 +179,6 @@ private static bool ReadOption(JsonElement root, string option) } } - private static string ReadRequiredStringOption(JsonElement root, string option) - { - return ReadStringOption(root, option) ?? throw new InvalidOperationException($"Unable to parse required option {option} from configuration."); - } - private static string? ReadStringOption(JsonElement root, string option) { if (root.TryGetProperty(option, out JsonElement value)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ConfigurationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ConfigurationTests.cs index d0ca28deb43..bd713d9a725 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ConfigurationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ConfigurationTests.cs @@ -66,20 +66,48 @@ public void TestParseConfig_OutputFolder(string mockJson, bool throwsError) // Validates that the LibraryName field is parsed correctly from the configuration [TestCaseSource("ParseConfigLibraryNameTestCases")] - public void TestParseConfig_LibraryName(string mockJson, bool throwsError) + public void TestParseConfig_LibraryName(string mockJson, string expected) { - if (throwsError) - { - Assert.Throws(() => MockHelpers.LoadMockGenerator(configuration: mockJson)); - return; - } + MockHelpers.LoadMockGenerator(configuration: mockJson); var library = CodeModelGenerator.Instance.Configuration.PackageName; - var expected = "libraryName"; Assert.AreEqual(expected, library); } + // Validates that when package-name is not specified, Configure() defaults to the PrimaryNamespace + [Test] + public void TestParseConfig_PackageNameDefaultsToPrimaryNamespace() + { + var mockJson = @"{ + ""output-folder"": ""outputFolder"" + }"; + + MockHelpers.LoadMockGenerator(configuration: mockJson, inputNamespaceName: "My.Custom.Namespace"); + + // Before Configure, package name should be the default + Assert.AreEqual(Configuration.DefaultPackageName, CodeModelGenerator.Instance.Configuration.PackageName); + + // After Configure, package name should be resolved from the namespace + CodeModelGenerator.Instance.Configure(); + Assert.AreEqual("My.Custom.Namespace", CodeModelGenerator.Instance.Configuration.PackageName); + } + + // Validates that when package-name is explicitly set, Configure() does not override it + [Test] + public void TestParseConfig_ExplicitPackageNameNotOverridden() + { + var mockJson = @"{ + ""output-folder"": ""outputFolder"", + ""package-name"": ""ExplicitName"" + }"; + + MockHelpers.LoadMockGenerator(configuration: mockJson, inputNamespaceName: "My.Custom.Namespace"); + + CodeModelGenerator.Instance.Configure(); + Assert.AreEqual("ExplicitName", CodeModelGenerator.Instance.Configuration.PackageName); + } + // Validates that additional configuration options are parsed correctly [Test] public void TestParseConfig_AdditionalConfigOptions() @@ -273,11 +301,11 @@ public static IEnumerable ParseConfigLibraryNameTestCases { yield return new TestCaseData(@"{ ""output-folder"": ""outputFolder"", - ""package-name"": ""libraryName"", - }", false); + ""package-name"": ""libraryName"" + }", "libraryName"); yield return new TestCaseData(@"{ ""output-folder"": ""outputFolder"" - }", true); + }", Configuration.DefaultPackageName); } }