diff --git a/.gitignore b/.gitignore index ae82a3cb09..dcfeddf2d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +openapi + src/**/bin/** src/**/obj/** src/packages/** diff --git a/src/NSwag.CodeGeneration.CSharp.Tests/PathPatternValidationTests.cs b/src/NSwag.CodeGeneration.CSharp.Tests/PathPatternValidationTests.cs new file mode 100644 index 0000000000..af207a44cb --- /dev/null +++ b/src/NSwag.CodeGeneration.CSharp.Tests/PathPatternValidationTests.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Xunit; +using System.IO; + + +namespace NSwag.CodeGeneration.CSharp.Tests +{ + public class PathPatternValidationTests + { + private static string GenerateSpec(bool withPattern) + { + return $@"{{ + ""openapi"": ""3.0.1"", + ""info"": {{ + ""title"": ""NSwager Test Server API"", + ""description"": ""An api used to test NSwager."", + ""version"": ""2.0"" + }}, + ""paths"": {{ + ""/api/v2/test/{{PathVariable}}/ping"": {{ + ""get"": {{ + ""tags"": [ + ""TestControllerVersionTwo"" + ], + ""summary"": ""Used to ping a valid user name."", + ""operationId"": ""PingpathVariableV2"", + ""parameters"": [ + {{ + ""name"": ""pathVariable"", + ""in"": ""path"", + ""description"": ""A System.String"", + ""required"": true, + ""schema"": {{ + {(withPattern ? @"""pattern"": ""^[a-zA-Z0-9_]+$"", " : "")} + ""type"": ""string"" + }} + }} + ], + ""responses"": {{ + ""200"": {{ + ""description"": ""With the user name"", + ""content"": {{ + ""application/json"": {{ + ""schema"": {{ + ""$ref"": ""#/components/schemas/PathVariableDTO"" + }} + }} + }} + }} + }} + }} + }} + }}, + ""components"": {{ + ""schemas"": {{ + ""PathVariableDTO"": {{ + ""required"": [ + ""pathVariable"" + ], + ""type"": ""object"", + ""properties"": {{ + ""pathVariable"": {{ + ""type"": ""string"", + ""description"": ""The value of the path variable"", + ""nullable"": true + }} + }}, + ""additionalProperties"": false, + ""description"": ""A DTO containing a path variable"" + }} + }} + }} +}}"; + } + + /// + /// This string if statement is exactly the same as the if statement in method. + /// + private string generatedCode = +@"if (!System.Text.RegularExpressions.Regex.IsMatch(pathVariable, ""^[a-zA-Z0-9_]+$"")) + throw new System.ArgumentException(""Parameter 'pathVariable' does not match the required pattern '^[a-zA-Z0-9_]+$'."");"; + + /// + /// Used to mock execution of the generated code + /// + /// The value of the path variable. + /// A regular expression use to validate . + /// Thrown if + /// does not match the . + private static void ValidatePathPatternMock(string pathVariable, string regexPattern) + { + if (!System.Text.RegularExpressions.Regex.IsMatch(pathVariable, regexPattern)) + throw new System.ArgumentException( + $"Parameter 'pathVariable' does not match the required pattern '{regexPattern}'." + ); + } + + [Fact] + public async Task When_path_parameter_have_pattern_field() + { + // Arrange + var seetings = new CSharpClientGeneratorSettings(); + var document = await OpenApiDocument.FromJsonAsync(GenerateSpec(withPattern: true)); + var generator = new CSharpClientGenerator(document, seetings); + + // Act + var code = generator.GenerateFile(); + + // Assert + Assert.Contains(NormalizeWhitespace(generatedCode), NormalizeWhitespace(code)); + } + + [Fact] + public async Task When_path_parameter_not_have_pattern_field() + { + // Arrange + var seetings = new CSharpClientGeneratorSettings(); + var document = await OpenApiDocument.FromJsonAsync(GenerateSpec(withPattern: false)); + var generator = new CSharpClientGenerator(document, seetings); + + // Act + var code = generator.GenerateFile(); + + // Assert + Assert.DoesNotContain(generatedCode, code); + } + + [Theory] + [InlineData("MockValue123", "^[a-zA-Z0-9_]+$")] // Alphanumeric and underscores + [InlineData("MockValue-123", "^[a-zA-Z0-9_-]+$")] // Alphanumeric, underscores, and dashes + [InlineData("Mock.Value.123", "^[a-zA-Z0-9._]+$")] // Alphanumeric, dots, and underscores + [InlineData("123456", "^\\d+$")] // Digits only + public void ValidatePathVariable_ValidPathVariables_DoesNotThrow( + string pathVariable, + string regexPattern + ) + { + // Arrange & Act & Assert + var exception = Record.Exception( + () => ValidatePathPatternMock(pathVariable, regexPattern) + ); + Assert.Null(exception); + } + + [Theory] + [InlineData("Mock@123", "^[a-zA-Z0-9_]+$")] // Alphanumeric and underscores + [InlineData("Mock Value", "^[a-zA-Z0-9_-]+$")] // Alphanumeric, underscores, and dashes + [InlineData("Mock!Value", "^[a-zA-Z0-9._]+$")] // Alphanumeric, dots, and underscores + [InlineData("MockValue123", "^\\d+$")] // Digits only + public void ValidatePathVariable_InvalidPathVariables_ThrowsArgumentException( + string pathVariable, + string regexPattern + ) + { + // Arrange & Act & Assert + var exception = Assert.Throws( + () => ValidatePathPatternMock(pathVariable, regexPattern) + ); + Assert.Contains( + $"Parameter 'pathVariable' does not match the required pattern", + exception.Message + ); + } + + private static string NormalizeWhitespace(string input) + { + char[] separators = ['\r', '\n', '\t', ' ']; + return string.Join( + " ", + input.Split(separators, StringSplitOptions.RemoveEmptyEntries) + ); + } + } +} \ No newline at end of file diff --git a/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.liquid b/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.liquid index 52400fa632..05430fc5e3 100644 --- a/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.liquid +++ b/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.liquid @@ -156,6 +156,11 @@ if ({{ parameter.VariableName }} == null) throw new System.ArgumentNullException("{{ parameter.VariableName }}"); +{% endif -%} +{% if parameter.Schema.Pattern -%} + if (!System.Text.RegularExpressions.Regex.IsMatch({{ parameter.VariableName }}, "{{ parameter.Schema.Pattern }}")) + throw new System.ArgumentException("Parameter '{{ parameter.VariableName }}' does not match the required pattern '{{ parameter.Schema.Pattern }}'."); + {% endif -%} {% endfor -%} {% for parameter in operation.QueryParameters -%} @@ -479,4 +484,4 @@ {% template Client.Class.ConvertToString %} {% template Client.Class.Body %} -} +} \ No newline at end of file diff --git a/src/NSwag.Npm/bin/nswag.js b/src/NSwag.Npm/bin/nswag.js deleted file mode 100644 index c1446d91fe..0000000000 --- a/src/NSwag.Npm/bin/nswag.js +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -var defaultCoreVersion = "Net80"; -var supportedCoreVersions = [ - { ver: '8.0', dir: "Net80", }, - { ver: '9.0', dir: "Net90", }, -]; - -// Initialize -process.title = 'nswag'; -console.log("NSwag NPM CLI"); -var args = process.argv.splice(2, process.argv.length - 2).map(function (a) { return a.indexOf(" ") === -1 ? a : '"' + a + '"' }).join(" "); - -// Legacy support -args = args.replace("--x86", "/runtime:WinX86"); -args = args.replace("/runtime:x86", "/runtime:WinX86"); -args = args.replace("--core 8.0", "/runtime:Net80"); -args = args.replace("--core 9.0", "/runtime:Net90"); -args = args.replace("--core", "/runtime:" + defaultCoreVersion); - -// Search for full .NET installation -var hasFullDotNet = false; -var fs = require('fs'); -if (process.env["windir"]) { - try { - var stats = fs.lstatSync(process.env["windir"] + '/Microsoft.NET'); - if (stats.isDirectory()) - hasFullDotNet = true; - } - catch (e) { - console.log(e); - } -} - -var c = require('child_process'); -if (hasFullDotNet && args.toLowerCase().indexOf("/runtime:win") != -1) { - // Run full .NET version - if (args.toLowerCase().indexOf("/runtime:winx86") != -1) { - var cmd = '"' + __dirname + '/binaries/Win/nswag.x86.exe" ' + args; - var code = c.execSync(cmd, { stdio: [0, 1, 2] }); - } else { - var cmd = '"' + __dirname + '/binaries/Win/nswag.exe" ' + args; - var code = c.execSync(cmd, { stdio: [0, 1, 2] }); - } -} else { - // Run .NET Core version - var defaultCmd = 'dotnet "' + __dirname + '/binaries/' + defaultCoreVersion + '/dotnet-nswag.dll" ' + args; - var infoCmd = "dotnet --version"; - c.exec(infoCmd, (error, stdout, _stderr) => { - for (let version of supportedCoreVersions) { - var coreCmd = 'dotnet "' + __dirname + '/binaries/' + version.dir + '/dotnet-nswag.dll" ' + args; - - if (args.toLowerCase().indexOf("/runtime:" + version.dir.toLocaleLowerCase()) != -1) { - c.execSync(coreCmd, { stdio: [0, 1, 2] }); - return; - } else { - if (!error) { - var coreVersion = stdout; - - if (coreVersion.startsWith(version.ver)) { - c.execSync(coreCmd, { stdio: [0, 1, 2] }); - return; - } - } - } - } - c.execSync(defaultCmd, { stdio: [0, 1, 2] }); - return; - }); -} diff --git a/src/NSwag.Npm/package-lock.json b/src/NSwag.Npm/package-lock.json index 08a92acf45..9a1b8ee9d7 100644 --- a/src/NSwag.Npm/package-lock.json +++ b/src/NSwag.Npm/package-lock.json @@ -1,12 +1,12 @@ { "name": "nswag", - "version": "14.0.0", + "version": "14.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nswag", - "version": "14.0.0", + "version": "14.2.0", "license": "MIT", "bin": { "nswag": "bin/nswag.js"