diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/OutputTypes/ScmOutputLibrary.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/OutputTypes/ScmOutputLibrary.cs index 70bb87afa5..c9d21cec04 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/OutputTypes/ScmOutputLibrary.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/OutputTypes/ScmOutputLibrary.cs @@ -39,7 +39,8 @@ protected override TypeProvider[] BuildTypeProviders() new ModelSerializationExtensionsProvider(), new TypeFormattersProvider(), new ClientPipelineExtensionsProvider(), - new ErrorResultProvider() + new ErrorResultProvider(), + new ClientUriBuilderProvider(), ]; } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/ClientUriBuilderProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/ClientUriBuilderProvider.cs new file mode 100644 index 0000000000..c11ba66ba6 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/ClientUriBuilderProvider.cs @@ -0,0 +1,336 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Statements; +using Microsoft.Generator.CSharp.Snippets; +using static Microsoft.Generator.CSharp.Snippets.Snippet; +using static Microsoft.Generator.CSharp.ClientModel.Snippets.TypeFormattersSnippet; +using Microsoft.Generator.CSharp.Primitives; + +namespace Microsoft.Generator.CSharp.ClientModel.Providers +{ + internal sealed class ClientUriBuilderProvider : TypeProvider + { + protected override TypeSignatureModifiers GetDeclarationModifiers() + { + return TypeSignatureModifiers.Internal; + } + + public override string RelativeFilePath => Path.Combine("src", "Generated", "Internal", $"{Name}.cs"); + + public override string Name => "ClientUriBuilder"; + + private readonly FieldProvider _uriBuilderField = new(FieldModifiers.Private, typeof(UriBuilder), "_uriBuilder"); + private readonly FieldProvider _pathBuilderField = new(FieldModifiers.Private, typeof(StringBuilder), "_pathBuilder"); + private readonly FieldProvider _queryBuilderField = new(FieldModifiers.Private, typeof(StringBuilder), "_queryBuilder"); + + protected override FieldProvider[] BuildFields() + { + return [_uriBuilderField, _pathBuilderField, _queryBuilderField]; + } + + private PropertyProvider? _uriBuilderProperty; + private PropertyProvider UriBuilderProperty => _uriBuilderProperty ??= new( + modifiers: MethodSignatureModifiers.Private, + name: "UriBuilder", + type: typeof(UriBuilder), + body: new ExpressionPropertyBody(new BinaryOperatorExpression(" ??= ", _uriBuilderField, New.Instance(typeof(UriBuilder)))), + description: null); + + internal ValueExpression UriBuilderPath => new MemberExpression(UriBuilderProperty, "Path"); + internal ValueExpression UriBuilderQuery => new MemberExpression(UriBuilderProperty, "Query"); + + private PropertyProvider? _pathBuilderProperty; + private PropertyProvider PathBuilderProperty => _pathBuilderProperty ??= new( + modifiers: MethodSignatureModifiers.Private, + name: "PathBuilder", + type: typeof(StringBuilder), + body: new ExpressionPropertyBody(new BinaryOperatorExpression(" ??= ", _pathBuilderField, New.Instance(typeof(StringBuilder), UriBuilderPath))), + description: null); + + private PropertyProvider? _queryBuilderProperty; + private PropertyProvider QueryBuilderProperty => _queryBuilderProperty ??= new( + modifiers: MethodSignatureModifiers.Private, + name: "QueryBuilder", + type: typeof(StringBuilder), + body: new ExpressionPropertyBody(new BinaryOperatorExpression(" ??= ", _queryBuilderField, New.Instance(typeof(StringBuilder), UriBuilderQuery))), + description: null); + + protected override PropertyProvider[] BuildProperties() + { + return [UriBuilderProperty, PathBuilderProperty, QueryBuilderProperty]; + } + + protected override MethodProvider[] BuildConstructors() + { + var signature = new ConstructorSignature( + Type: Type, + Modifiers: MethodSignatureModifiers.Public, + Parameters: Array.Empty(), + Description: null); + return [new MethodProvider(signature, MethodBodyStatement.Empty, this)]; + } + + protected override MethodProvider[] BuildMethods() + { + var methods = new List(); + + methods.Add(BuildResetMethod()); + + methods.AddRange(BuildAppendPathMethods()); + methods.AddRange(BuildAppendQueryMethods()); + methods.AddRange(BuildAppendQueryDelimitedMethods()); + + methods.Add(BuildToUriMethod()); + + return methods.ToArray(); + } + + private const string _resetMethodName = "Reset"; + private MethodProvider BuildResetMethod() + { + var uriParameter = new ParameterProvider("uri", $"The uri.", typeof(Uri)); + var signature = new MethodSignature( + Name: _resetMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: new[] + { + uriParameter + }, + ReturnType: null, + Description: null, ReturnDescription: null); + + var body = new MethodBodyStatement[] + { + _uriBuilderField.Assign(New.Instance(_uriBuilderField.Type, uriParameter)).Terminate(), + _pathBuilderField.Assign(New.Instance(_pathBuilderField.Type, UriBuilderPath)).Terminate(), + _queryBuilderField.Assign(New.Instance(_queryBuilderField.Type, UriBuilderQuery)).Terminate() + }; + + return new(signature, body, this); + } + + private const string _appendPathMethodName = "AppendPath"; + private MethodProvider[] BuildAppendPathMethods() + { + var valueParameter = new ParameterProvider("value", $"The value.", typeof(string)); + var escapeParameter = new ParameterProvider("escape", $"The escape", typeof(bool)); + var signature = new MethodSignature( + Name: _appendPathMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: [valueParameter, escapeParameter], + ReturnType: null, + Description: null, ReturnDescription: null); + + var value = new StringSnippet(valueParameter); + var escape = new BoolSnippet(escapeParameter); + var pathBuilder = new StringBuilderSnippet(PathBuilderProperty); + MethodBodyStatement body = new MethodBodyStatement[] + { + MethodBodyStatement.Empty, + new IfStatement(escape) + { + value.Assign(new InvokeStaticMethodExpression(typeof(Uri), nameof(Uri.EscapeDataString), new[]{ value.Expression })).Terminate() + }, + MethodBodyStatement.Empty, + new IfStatement(pathBuilder.Length.Expression.GreaterThan(Int(0)).And(new IndexerExpression(pathBuilder, pathBuilder.Length - Int(1)).Equal(Literal('/'))).And(new IndexerExpression(value, Int(0)).Equal(Literal('/')))) + { + pathBuilder.Remove(pathBuilder.Length - Int(1), Int(1)).Terminate() + }, + MethodBodyStatement.Empty, + pathBuilder.Append(value).Terminate(), + UriBuilderPath.Assign(pathBuilder.Expression.InvokeToString()).Terminate() + }; + + return + [ + new(signature, body, this), + BuildAppendPathMethod(typeof(bool), false, false), + BuildAppendPathMethod(typeof(float), true, false), + BuildAppendPathMethod(typeof(double), true, false), + BuildAppendPathMethod(typeof(int), true, false), + BuildAppendPathMethod(typeof(byte[]), true, true), + BuildAppendPathMethod(typeof(IEnumerable), true, false), + BuildAppendPathMethod(typeof(DateTimeOffset), true, true), + BuildAppendPathMethod(typeof(TimeSpan), true, true), + BuildAppendPathMethod(typeof(Guid), true, false), + BuildAppendPathMethod(typeof(long), true, false) + ]; + } + + private MethodProvider BuildAppendPathMethod(CSharpType valueType, bool escapeDefaultValue, bool hasFormat) + { + var valueParameter = new ParameterProvider("value", $"The value.", valueType); + var escapeParameter = new ParameterProvider("escape", $"The escape.", typeof(bool), Bool(escapeDefaultValue)); + var formatParameter = new ParameterProvider("format", $"The format", typeof(string)); + var parameters = hasFormat + ? new[] { valueParameter, formatParameter, escapeParameter } + : new[] { valueParameter, escapeParameter }; + + var signature = new MethodSignature( + Name: _appendPathMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: parameters, + ReturnType: null, + Description: null, ReturnDescription: null); + var convertToStringExpression = ConvertToString(valueParameter, hasFormat ? (ValueExpression)formatParameter : null); + var body = new InvokeInstanceMethodExpression(null, _appendPathMethodName, new[] { convertToStringExpression.Expression, escapeParameter }, null, false); + + return new(signature, body, this); + } + + private const string _appendQueryMethodName = "AppendQuery"; + private MethodProvider[] BuildAppendQueryMethods() + { + var nameParameter = new ParameterProvider("name", $"The name.", typeof(string)); + var valueParameter = new ParameterProvider("value", $"The value.", typeof(string)); + var escapeParameter = new ParameterProvider("escape", $"The escape.", typeof(bool)); + + var signature = new MethodSignature( + Name: _appendQueryMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: new[] { nameParameter, valueParameter, escapeParameter }, + ReturnType: null, + Description: null, ReturnDescription: null); + + var name = new StringSnippet(nameParameter); + var value = new StringSnippet(valueParameter); + var escape = new BoolSnippet(escapeParameter); + var queryBuilder = new StringBuilderSnippet(QueryBuilderProperty); + var body = new MethodBodyStatement[] + { + MethodBodyStatement.Empty, + new IfStatement(queryBuilder.Length.Expression.GreaterThan(Int(0))) + { + queryBuilder.Append(Literal('&')).Terminate() + }, + MethodBodyStatement.Empty, + new IfStatement(escape) + { + value.Assign(new InvokeStaticMethodExpression(typeof(Uri), nameof(Uri.EscapeDataString), new[] { value.Expression })).Terminate() + }, + MethodBodyStatement.Empty, + queryBuilder.Append(name).Terminate(), + queryBuilder.Append(Literal('=')).Terminate(), + queryBuilder.Append(value).Terminate() + }; + + return + [ + new MethodProvider(signature, body, this), + BuildAppendQueryMethod(typeof(bool), false, false), + BuildAppendQueryMethod(typeof(float), true, false), + BuildAppendQueryMethod(typeof(DateTimeOffset), true, true), + BuildAppendQueryMethod(typeof(TimeSpan), true, true), + BuildAppendQueryMethod(typeof(double), true, false), + BuildAppendQueryMethod(typeof(decimal), true, false), + BuildAppendQueryMethod(typeof(int), true, false), + BuildAppendQueryMethod(typeof(long), true, false), + BuildAppendQueryMethod(typeof(TimeSpan), true, false), + BuildAppendQueryMethod(typeof(byte[]), true, true), + BuildAppendQueryMethod(typeof(Guid), true, false) + ]; + } + + private MethodProvider BuildAppendQueryMethod(CSharpType valueType, bool escapeDefaultValue, bool hasFormat) + { + var nameParameter = new ParameterProvider("name", $"The name.", typeof(string)); + var valueParameter = new ParameterProvider("value", $"The value.", valueType); + var escapeParameter = new ParameterProvider("escape", $"The escape.", typeof(bool), Bool(escapeDefaultValue)); + var formatParameter = new ParameterProvider("format", $"The format.", typeof(string)); + var parameters = hasFormat + ? new[] { nameParameter, valueParameter, formatParameter, escapeParameter } + : new[] { nameParameter, valueParameter, escapeParameter }; + + var signature = new MethodSignature( + Name: _appendQueryMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: parameters, + ReturnType: null, + Description: null, ReturnDescription: null); + var convertToStringExpression = ConvertToString(valueParameter, hasFormat ? (ValueExpression)formatParameter : null); + var body = new InvokeInstanceMethodExpression(null, _appendQueryMethodName, new[] { nameParameter, convertToStringExpression.Expression, escapeParameter }, null, false); + + return new(signature, body, this); + } + + private MethodProvider[] BuildAppendQueryDelimitedMethods() + { + return [ BuildAppendQueryDelimitedMethod(false), BuildAppendQueryDelimitedMethod(true)]; + } + + private readonly CSharpType _t = typeof(IEnumerable<>).GetGenericArguments()[0]; + + private const string _appendQueryDelimitedMethodName = "AppendQueryDelimited"; + private MethodProvider BuildAppendQueryDelimitedMethod(bool hasFormat) + { + var nameParameter = new ParameterProvider("name", $"The name.", typeof(string)); + var valueParameter = new ParameterProvider("value", $"The value.", new CSharpType(typeof(IEnumerable<>), _t)); + var delimiterParameter = new ParameterProvider("delimiter", $"The delimiter.", typeof(string)); + var formatParameter = new ParameterProvider("format", $"The format.", typeof(string)); + var escapeParameter = new ParameterProvider("escape", $"The escape.", typeof(bool), Bool(true)); + + var parameters = hasFormat + ? new[] { nameParameter, valueParameter, delimiterParameter, formatParameter, escapeParameter } + : new[] { nameParameter, valueParameter, delimiterParameter, escapeParameter }; + var signature = new MethodSignature( + Name: _appendQueryDelimitedMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: parameters, + ReturnType: null, + GenericArguments: new[] { _t }, + Description: null, ReturnDescription: null); + + var name = new StringSnippet(nameParameter); + var value = new EnumerableSnippet(_t, valueParameter); + var delimiter = new StringSnippet(delimiterParameter); + var escape = new BoolSnippet(escapeParameter); + + var v = new VariableExpression(_t, "v"); + var convertToStringExpression = ConvertToString(v, hasFormat ? new StringSnippet(formatParameter).Expression : null); + var body = new[] + { + Declare("stringValues", value.Select(new StringSnippet(new FuncExpression(new[] {v.Declaration}, convertToStringExpression))), out var stringValues), + new InvokeInstanceMethodExpression(null, _appendQueryMethodName, new[] { name.Expression, StringSnippet.Join(delimiter, stringValues), escape }, null, false).Terminate() + }; + + return new(signature, body, this); + } + + private const string _toUriMethodName = "ToUri"; + private MethodProvider BuildToUriMethod() + { + var signature = new MethodSignature( + Name: _toUriMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: Array.Empty(), + ReturnType: typeof(Uri), + Description: null, ReturnDescription: null); + + var pathBuilder = (ValueExpression)_pathBuilderField; + var queryBuilder = (ValueExpression)_queryBuilderField; + var body = new MethodBodyStatement[] + { + new IfStatement(pathBuilder.NotEqual(Null)) + { + UriBuilderPath.Assign(pathBuilder.InvokeToString()).Terminate() + }, + MethodBodyStatement.Empty, + new IfStatement(queryBuilder.NotEqual(Null)) + { + UriBuilderQuery.Assign(queryBuilder.InvokeToString()).Terminate() + }, + MethodBodyStatement.Empty, + Return(new MemberExpression(UriBuilderProperty, nameof(UriBuilder.Uri))) + }; + + return new(signature, body, this); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientUriBuilderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientUriBuilderTests.cs new file mode 100644 index 0000000000..b3ca5b3daf --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/ClientUriBuilderTests.cs @@ -0,0 +1,461 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using UnbrandedTypeSpec; + +namespace Microsoft.Generator.CSharp.ClientModel.Tests.Providers +{ + public class ClientUriBuilderTests + { + [TestCase("http://localhost", null, "http://localhost/")] + [TestCase("http://localhost/", null, "http://localhost/")] + [TestCase("http://localhost:12345", null, "http://localhost:12345/")] + [TestCase("http://localhost:12345/", null, "http://localhost:12345/")] + [TestCase("http://localhost/with", null, "http://localhost/with")] + [TestCase("http://localhost/with/", null, "http://localhost/with/")] + [TestCase("http://localhost:12345/with", null, "http://localhost:12345/with")] + [TestCase("http://localhost:12345/with/", null, "http://localhost:12345/with/")] + [TestCase("http://localhost", "path", "http://localhost/path")] + [TestCase("http://localhost/", "path", "http://localhost/path")] + [TestCase("http://localhost:12345", "path", "http://localhost:12345/path")] + [TestCase("http://localhost:12345/", "path", "http://localhost:12345/path")] + [TestCase("http://localhost/with", "path", "http://localhost/withpath")] + [TestCase("http://localhost/with/", "path", "http://localhost/with/path")] + [TestCase("http://localhost:12345/with", "path", "http://localhost:12345/withpath")] + [TestCase("http://localhost:12345/with/", "path", "http://localhost:12345/with/path")] + [TestCase("http://localhost", "/path", "http://localhost/path")] + [TestCase("http://localhost/", "/path", "http://localhost/path")] + [TestCase("http://localhost:12345", "/path", "http://localhost:12345/path")] + [TestCase("http://localhost:12345/", "/path", "http://localhost:12345/path")] + [TestCase("http://localhost/with", "/path", "http://localhost/with/path")] + [TestCase("http://localhost/with/", "/path", "http://localhost/with/path")] + [TestCase("http://localhost:12345/with", "/path", "http://localhost:12345/with/path")] + [TestCase("http://localhost:12345/with/", "/path", "http://localhost:12345/with/path")] + [TestCase("http://localhost", "/path/", "http://localhost/path/")] + [TestCase("http://localhost/", "/path/", "http://localhost/path/")] + [TestCase("http://localhost:12345", "/path/", "http://localhost:12345/path/")] + [TestCase("http://localhost:12345/", "/path/", "http://localhost:12345/path/")] + [TestCase("http://localhost/with", "/path/", "http://localhost/with/path/")] + [TestCase("http://localhost/with/", "/path/", "http://localhost/with/path/")] + [TestCase("http://localhost:12345/with", "/path/", "http://localhost:12345/with/path/")] + [TestCase("http://localhost:12345/with/", "/path/", "http://localhost:12345/with/path/")] + public void ResetThenAppendPath(string endpoint, string pathPart, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + if (pathPart != null) + { + builder.AppendPath(pathPart, false); + } + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", "one|1", "http://localhost/?one=1")] + [TestCase("http://localhost:12345", "one|1", "http://localhost:12345/?one=1")] + [TestCase("http://localhost/some", "one|1", "http://localhost/some?one=1")] + [TestCase("http://localhost:12345/some", "one|1", "http://localhost:12345/some?one=1")] + [TestCase("http://localhost/", "one|1", "http://localhost/?one=1")] + [TestCase("http://localhost:12345/", "one|1", "http://localhost:12345/?one=1")] + [TestCase("http://localhost/some/", "one|1", "http://localhost/some/?one=1")] + [TestCase("http://localhost:12345/some/", "one|1", "http://localhost:12345/some/?one=1")] + [TestCase("http://localhost", "one|1|two|2", "http://localhost/?one=1&two=2")] + [TestCase("http://localhost:12345", "one|1|two|2", "http://localhost:12345/?one=1&two=2")] + [TestCase("http://localhost/some", "one|1|two|2", "http://localhost/some?one=1&two=2")] + [TestCase("http://localhost:12345/some", "one|1|two|2", "http://localhost:12345/some?one=1&two=2")] + [TestCase("http://localhost/", "one|1|two|2", "http://localhost/?one=1&two=2")] + [TestCase("http://localhost:12345/", "one|1|two|2", "http://localhost:12345/?one=1&two=2")] + [TestCase("http://localhost/some/", "one|1|two|2", "http://localhost/some/?one=1&two=2")] + [TestCase("http://localhost:12345/some/", "one|1|two|2", "http://localhost:12345/some/?one=1&two=2")] + public void ResetThenAppendQuery(string endpoint, string queryPart, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + if (queryPart != null) + { + var parts = queryPart.Split('|'); + for (int i = 0; i < parts.Length; i += 2) + { + builder.AppendQuery(parts[i], parts[i + 1], false); + } + } + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", "some", "one|1", "http://localhost/some?one=1")] + [TestCase("http://localhost:12345", "some", "one|1", "http://localhost:12345/some?one=1")] + [TestCase("http://localhost/", "some/", "one|1", "http://localhost/some/?one=1")] + [TestCase("http://localhost:12345", "some/", "one|1", "http://localhost:12345/some/?one=1")] + [TestCase("http://localhost/", "/some", "one|1", "http://localhost/some?one=1")] + [TestCase("http://localhost:12345", "/some", "one|1", "http://localhost:12345/some?one=1")] + [TestCase("http://localhost/", "/some/", "one|1", "http://localhost/some/?one=1")] + [TestCase("http://localhost:12345", "/some/", "one|1", "http://localhost:12345/some/?one=1")] + [TestCase("http://localhost", "/some", "one|1|two|2", "http://localhost/some?one=1&two=2")] + [TestCase("http://localhost:12345", "/some", "one|1|two|2", "http://localhost:12345/some?one=1&two=2")] + [TestCase("http://localhost", "some/", "one|1|two|2", "http://localhost/some/?one=1&two=2")] + [TestCase("http://localhost:12345", "some/", "one|1|two|2", "http://localhost:12345/some/?one=1&two=2")] + [TestCase("http://localhost", "/some/", "one|1|two|2", "http://localhost/some/?one=1&two=2")] + [TestCase("http://localhost:12345", "/some/", "one|1|two|2", "http://localhost:12345/some/?one=1&two=2")] + public void ResetThenAppendPathThenAppendQuery(string endpoint, string pathPart, string queryPart, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + if (pathPart != null) + { + builder.AppendPath(pathPart, false); + } + + if (queryPart != null) + { + var parts = queryPart.Split('|'); + for (int i = 0; i < parts.Length; i += 2) + { + builder.AppendQuery(parts[i], parts[i + 1], false); + } + } + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", "some", "one|1", "http://localhost/some?one=1")] + [TestCase("http://localhost:12345", "some", "one|1", "http://localhost:12345/some?one=1")] + [TestCase("http://localhost/", "some/", "one|1", "http://localhost/some/?one=1")] + [TestCase("http://localhost:12345", "some/", "one|1", "http://localhost:12345/some/?one=1")] + [TestCase("http://localhost/", "/some", "one|1", "http://localhost/some?one=1")] + [TestCase("http://localhost:12345", "/some", "one|1", "http://localhost:12345/some?one=1")] + [TestCase("http://localhost/", "/some/", "one|1", "http://localhost/some/?one=1")] + [TestCase("http://localhost:12345", "/some/", "one|1", "http://localhost:12345/some/?one=1")] + [TestCase("http://localhost", "/some", "one|1|two|2", "http://localhost/some?one=1&two=2")] + [TestCase("http://localhost:12345", "/some", "one|1|two|2", "http://localhost:12345/some?one=1&two=2")] + [TestCase("http://localhost", "some/", "one|1|two|2", "http://localhost/some/?one=1&two=2")] + [TestCase("http://localhost:12345", "some/", "one|1|two|2", "http://localhost:12345/some/?one=1&two=2")] + [TestCase("http://localhost", "/some/", "one|1|two|2", "http://localhost/some/?one=1&two=2")] + [TestCase("http://localhost:12345", "/some/", "one|1|two|2", "http://localhost:12345/some/?one=1&two=2")] + public void ResetThenAppendQueryThenAppendPath(string endpoint, string pathPart, string queryPart, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + if (queryPart != null) + { + var parts = queryPart.Split('|'); + for (int i = 0; i < parts.Length; i += 2) + { + builder.AppendQuery(parts[i], parts[i + 1], false); + } + } + + if (pathPart != null) + { + builder.AppendPath(pathPart, false); + } + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", true, true, "http://localhost/true")] + [TestCase("http://localhost", true, false, "http://localhost/true")] + [TestCase("http://localhost", false, true, "http://localhost/false")] + [TestCase("http://localhost", false, false, "http://localhost/false")] + public void AppendPath_Bool(string endpoint, bool value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendPath(value, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", 3.14f, true, "http://localhost/3.14")] + [TestCase("http://localhost", 3.14f, false, "http://localhost/3.14")] + [TestCase("http://localhost", -3.14f, true, "http://localhost/-3.14")] + [TestCase("http://localhost", -3.14f, false, "http://localhost/-3.14")] + public void AppendPath_Float(string endpoint, float value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendPath(value, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", 3.14, true, "http://localhost/3.14")] + [TestCase("http://localhost", 3.14, false, "http://localhost/3.14")] + [TestCase("http://localhost", -3.14, true, "http://localhost/-3.14")] + [TestCase("http://localhost", -3.14, false, "http://localhost/-3.14")] + public void AppendPath_Double(string endpoint, double value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendPath(value, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", 299792458, true, "http://localhost/299792458")] + [TestCase("http://localhost", 299792458, false, "http://localhost/299792458")] + [TestCase("http://localhost", -299792458, true, "http://localhost/-299792458")] + [TestCase("http://localhost", -299792458, false, "http://localhost/-299792458")] + public void AppendPath_Int(string endpoint, int value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendPath(value, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", 299792458000000, true, "http://localhost/299792458000000")] + [TestCase("http://localhost", 299792458000000, false, "http://localhost/299792458000000")] + [TestCase("http://localhost", -299792458000000, true, "http://localhost/-299792458000000")] + [TestCase("http://localhost", -299792458000000, false, "http://localhost/-299792458000000")] + public void AppendPath_Long(string endpoint, long value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendPath(value, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", new byte[] { 104, 101, 108, 108, 111 }, "U", true, "http://localhost/aGVsbG8")] + [TestCase("http://localhost", new byte[] { 104, 101, 108, 108, 111 }, "D", true, "http://localhost/aGVsbG8%3D")] + [TestCase("http://localhost", new byte[] { 104, 101, 108, 108, 111 }, "U", false, "http://localhost/aGVsbG8")] + [TestCase("http://localhost", new byte[] { 104, 101, 108, 108, 111 }, "D", false, "http://localhost/aGVsbG8=")] + public void AppendPath_ByteArray(string endpoint, byte[] value, string format, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendPath(value, format, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", new[] { "hello", "world" }, true, "http://localhost/hello%2Cworld")] + [TestCase("http://localhost", new[] { "hello", "world" }, false, "http://localhost/hello,world")] + public void AppendPath_StringArray(string endpoint, IEnumerable value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendPath(value, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "D", true, "http://localhost/1905-06-30")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "U", true, "http://localhost/-2035622760")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "O", true, "http://localhost/1905-06-30T13%3A14%3A00.0000000Z")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "o", true, "http://localhost/1905-06-30T13%3A14%3A00.0000000Z")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "R", true, "http://localhost/Fri%2C 30 Jun 1905 13%3A14%3A00 GMT")] // TODO -- why spaces are not escaped? + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "D", false, "http://localhost/1905-06-30")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "U", false, "http://localhost/-2035622760")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "O", false, "http://localhost/1905-06-30T13:14:00.0000000Z")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "o", false, "http://localhost/1905-06-30T13:14:00.0000000Z")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "R", false, "http://localhost/Fri, 30 Jun 1905 13:14:00 GMT")] + public void AppendPath_DateTimeOffset(string endpoint, string value, string format, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + var dateTimeOffset = DateTimeOffset.Parse(value); + builder.AppendPath(dateTimeOffset, format, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", "02:15:00", "P", true, "http://localhost/PT2H15M")] + [TestCase("http://localhost", "02:15:00", "", true, "http://localhost/02%3A15%3A00")] + [TestCase("http://localhost", "02:15:00", null, true, "http://localhost/PT2H15M")] + [TestCase("http://localhost", "02:15:00", "P", false, "http://localhost/PT2H15M")] + [TestCase("http://localhost", "02:15:00", "", false, "http://localhost/02:15:00")] + [TestCase("http://localhost", "02:15:00", null, false, "http://localhost/PT2H15M")] + public void AppendPath_TimeSpan(string endpoint, string value, string format, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + var timeSpan = TimeSpan.Parse(value); + builder.AppendPath(timeSpan, format, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", "3f2504e0-4f89-11d3-9a0c-0305e82c3301", true, "http://localhost/3f2504e0-4f89-11d3-9a0c-0305e82c3301")] + [TestCase("http://localhost", "3f2504e0-4f89-11d3-9a0c-0305e82c3301", false, "http://localhost/3f2504e0-4f89-11d3-9a0c-0305e82c3301")] + public void AppendPath_Guid(string endpoint, string value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + var guid = Guid.Parse(value); + builder.AppendPath(guid, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", true, true, "http://localhost/?query=true")] + [TestCase("http://localhost", true, false, "http://localhost/?query=true")] + [TestCase("http://localhost", false, true, "http://localhost/?query=false")] + [TestCase("http://localhost", false, false, "http://localhost/?query=false")] + public void AppendQuery_Bool(string endpoint, bool value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendQuery("query", value, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", 3.14f, true, "http://localhost/?query=3.14")] + [TestCase("http://localhost", 3.14f, false, "http://localhost/?query=3.14")] + [TestCase("http://localhost", -3.14f, true, "http://localhost/?query=-3.14")] + [TestCase("http://localhost", -3.14f, false, "http://localhost/?query=-3.14")] + public void AppendQuery_Float(string endpoint, float value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendQuery("query", value, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", 3.14, true, "http://localhost/?query=3.14")] + [TestCase("http://localhost", 3.14, false, "http://localhost/?query=3.14")] + [TestCase("http://localhost", -3.14, true, "http://localhost/?query=-3.14")] + [TestCase("http://localhost", -3.14, false, "http://localhost/?query=-3.14")] + public void AppendQuery_Double(string endpoint, double value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendQuery("query", value, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", 299792458, true, "http://localhost/?query=299792458")] + [TestCase("http://localhost", 299792458, false, "http://localhost/?query=299792458")] + [TestCase("http://localhost", -299792458, true, "http://localhost/?query=-299792458")] + [TestCase("http://localhost", -299792458, false, "http://localhost/?query=-299792458")] + public void AppendQuery_Int(string endpoint, int value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendQuery("query", value, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", 299792458000000, true, "http://localhost/?query=299792458000000")] + [TestCase("http://localhost", 299792458000000, false, "http://localhost/?query=299792458000000")] + [TestCase("http://localhost", -299792458000000, true, "http://localhost/?query=-299792458000000")] + [TestCase("http://localhost", -299792458000000, false, "http://localhost/?query=-299792458000000")] + public void AppendQuery_Long(string endpoint, long value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendQuery("query", value, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", new byte[] { 104, 101, 108, 108, 111 }, "U", true, "http://localhost/?query=aGVsbG8")] + [TestCase("http://localhost", new byte[] { 104, 101, 108, 108, 111 }, "D", true, "http://localhost/?query=aGVsbG8%3D")] + [TestCase("http://localhost", new byte[] { 104, 101, 108, 108, 111 }, "U", false, "http://localhost/?query=aGVsbG8")] + [TestCase("http://localhost", new byte[] { 104, 101, 108, 108, 111 }, "D", false, "http://localhost/?query=aGVsbG8=")] + public void AppendQuery_ByteArray(string endpoint, byte[] value, string format, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendQuery("query", value, format, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "D", true, "http://localhost/?query=1905-06-30")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "U", true, "http://localhost/?query=-2035622760")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "O", true, "http://localhost/?query=1905-06-30T13%3A14%3A00.0000000Z")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "o", true, "http://localhost/?query=1905-06-30T13%3A14%3A00.0000000Z")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "R", true, "http://localhost/?query=Fri%2C 30 Jun 1905 13%3A14%3A00 GMT")] // TODO -- why spaces are not escaped? + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "D", false, "http://localhost/?query=1905-06-30")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "U", false, "http://localhost/?query=-2035622760")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "O", false, "http://localhost/?query=1905-06-30T13:14:00.0000000Z")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "o", false, "http://localhost/?query=1905-06-30T13:14:00.0000000Z")] + [TestCase("http://localhost", "6/30/1905 1:14:00 PM +00:00", "R", false, "http://localhost/?query=Fri, 30 Jun 1905 13:14:00 GMT")] + public void AppendQuery_DateTimeOffset(string endpoint, string value, string format, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + var dateTimeOffset = DateTimeOffset.Parse(value); + builder.AppendQuery("query", dateTimeOffset, format, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", "02:15:00", "P", true, "http://localhost/?query=PT2H15M")] + [TestCase("http://localhost", "02:15:00", "", true, "http://localhost/?query=02%3A15%3A00")] + [TestCase("http://localhost", "02:15:00", null, true, "http://localhost/?query=PT2H15M")] + [TestCase("http://localhost", "02:15:00", "P", false, "http://localhost/?query=PT2H15M")] + [TestCase("http://localhost", "02:15:00", "", false, "http://localhost/?query=02:15:00")] + [TestCase("http://localhost", "02:15:00", null, false, "http://localhost/?query=PT2H15M")] + public void AppendQuery_TimeSpan(string endpoint, string value, string format, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + var timeSpan = TimeSpan.Parse(value); + builder.AppendQuery("query", timeSpan, format, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", "3f2504e0-4f89-11d3-9a0c-0305e82c3301", true, "http://localhost/?query=3f2504e0-4f89-11d3-9a0c-0305e82c3301")] + [TestCase("http://localhost", "3f2504e0-4f89-11d3-9a0c-0305e82c3301", false, "http://localhost/?query=3f2504e0-4f89-11d3-9a0c-0305e82c3301")] + public void AppendQuery_Guid(string endpoint, string value, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + var guid = Guid.Parse(value); + builder.AppendQuery("query", guid, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + + [TestCase("http://localhost", new[] { "hello", "world" }, ",", true, "http://localhost/?query=hello%2Cworld")] + [TestCase("http://localhost", new[] { "hello", "world" }, ";", true, "http://localhost/?query=hello%3Bworld")] + [TestCase("http://localhost", new[] { "hello", "world" }, "|", true, "http://localhost/?query=hello|world")] + [TestCase("http://localhost", new[] { "hello", "world" }, ",", false, "http://localhost/?query=hello,world")] + [TestCase("http://localhost", new[] { "hello", "world" }, ";", false, "http://localhost/?query=hello;world")] + [TestCase("http://localhost", new[] { "hello", "world" }, "|", false, "http://localhost/?query=hello|world")] + public void AppendQueryDelimited(string endpoint, IEnumerable value, string delimiter, bool escape, string expected) + { + var builder = new ClientUriBuilder(); + builder.Reset(new Uri(endpoint)); + + builder.AppendQueryDelimited("query", value, delimiter, escape); + + Assert.AreEqual(expected, builder.ToUri().ToString()); + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/ClientUriBuilder.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/ClientUriBuilder.cs new file mode 100644 index 0000000000..8d913b556b --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/ClientUriBuilder.cs @@ -0,0 +1,134 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace UnbrandedTypeSpec +{ + internal partial class ClientUriBuilder + { + private UriBuilder _uriBuilder; + private StringBuilder _pathBuilder; + private StringBuilder _queryBuilder; + + public ClientUriBuilder() + { + } + + /// Gets the uribuilder. + private UriBuilder UriBuilder => _uriBuilder ??= new UriBuilder(); + + /// Gets the pathbuilder. + private StringBuilder PathBuilder => _pathBuilder ??= new StringBuilder(UriBuilder.Path); + + /// Gets the querybuilder. + private StringBuilder QueryBuilder => _queryBuilder ??= new StringBuilder(UriBuilder.Query); + + public void Reset(Uri uri) + { + _uriBuilder = new UriBuilder(uri); + _pathBuilder = new StringBuilder(UriBuilder.Path); + _queryBuilder = new StringBuilder(UriBuilder.Query); + } + + public void AppendPath(string value, bool escape) + { + if (escape) + { + value = Uri.EscapeDataString(value); + } + if (PathBuilder.Length > 0 && PathBuilder[PathBuilder.Length - 1] == '/' && value[0] == '/') + { + PathBuilder.Remove(PathBuilder.Length - 1, 1); + } + PathBuilder.Append(value); + UriBuilder.Path = PathBuilder.ToString(); + } + + public void AppendPath(bool value, bool escape = false) => AppendPath(TypeFormatters.ConvertToString(value), escape); + + public void AppendPath(float value, bool escape = true) => AppendPath(TypeFormatters.ConvertToString(value), escape); + + public void AppendPath(double value, bool escape = true) => AppendPath(TypeFormatters.ConvertToString(value), escape); + + public void AppendPath(int value, bool escape = true) => AppendPath(TypeFormatters.ConvertToString(value), escape); + + public void AppendPath(byte[] value, string format, bool escape = true) => AppendPath(TypeFormatters.ConvertToString(value, format), escape); + + public void AppendPath(IEnumerable value, bool escape = true) => AppendPath(TypeFormatters.ConvertToString(value), escape); + + public void AppendPath(DateTimeOffset value, string format, bool escape = true) => AppendPath(TypeFormatters.ConvertToString(value, format), escape); + + public void AppendPath(TimeSpan value, string format, bool escape = true) => AppendPath(TypeFormatters.ConvertToString(value, format), escape); + + public void AppendPath(Guid value, bool escape = true) => AppendPath(TypeFormatters.ConvertToString(value), escape); + + public void AppendPath(long value, bool escape = true) => AppendPath(TypeFormatters.ConvertToString(value), escape); + + public void AppendQuery(string name, string value, bool escape) + { + if (QueryBuilder.Length > 0) + { + QueryBuilder.Append('&'); + } + if (escape) + { + value = Uri.EscapeDataString(value); + } + QueryBuilder.Append(name); + QueryBuilder.Append('='); + QueryBuilder.Append(value); + } + + public void AppendQuery(string name, bool value, bool escape = false) => AppendQuery(name, TypeFormatters.ConvertToString(value), escape); + + public void AppendQuery(string name, float value, bool escape = true) => AppendQuery(name, TypeFormatters.ConvertToString(value), escape); + + public void AppendQuery(string name, DateTimeOffset value, string format, bool escape = true) => AppendQuery(name, TypeFormatters.ConvertToString(value, format), escape); + + public void AppendQuery(string name, TimeSpan value, string format, bool escape = true) => AppendQuery(name, TypeFormatters.ConvertToString(value, format), escape); + + public void AppendQuery(string name, double value, bool escape = true) => AppendQuery(name, TypeFormatters.ConvertToString(value), escape); + + public void AppendQuery(string name, decimal value, bool escape = true) => AppendQuery(name, TypeFormatters.ConvertToString(value), escape); + + public void AppendQuery(string name, int value, bool escape = true) => AppendQuery(name, TypeFormatters.ConvertToString(value), escape); + + public void AppendQuery(string name, long value, bool escape = true) => AppendQuery(name, TypeFormatters.ConvertToString(value), escape); + + public void AppendQuery(string name, TimeSpan value, bool escape = true) => AppendQuery(name, TypeFormatters.ConvertToString(value), escape); + + public void AppendQuery(string name, byte[] value, string format, bool escape = true) => AppendQuery(name, TypeFormatters.ConvertToString(value, format), escape); + + public void AppendQuery(string name, Guid value, bool escape = true) => AppendQuery(name, TypeFormatters.ConvertToString(value), escape); + + public void AppendQueryDelimited(string name, IEnumerable value, string delimiter, bool escape = true) + { + IEnumerable stringValues = value.Select(v => TypeFormatters.ConvertToString(v)); + AppendQuery(name, string.Join(delimiter, stringValues), escape); + } + + public void AppendQueryDelimited(string name, IEnumerable value, string delimiter, string format, bool escape = true) + { + IEnumerable stringValues = value.Select(v => TypeFormatters.ConvertToString(v, format)); + AppendQuery(name, string.Join(delimiter, stringValues), escape); + } + + public Uri ToUri() + { + if (_pathBuilder != null) + { + UriBuilder.Path = _pathBuilder.ToString(); + } + if (_queryBuilder != null) + { + UriBuilder.Query = _queryBuilder.ToString(); + } + return UriBuilder.Uri; + } + } +}