Skip to content

Commit eb55a76

Browse files
Stuff
1 parent 2477e31 commit eb55a76

File tree

18 files changed

+855
-394
lines changed

18 files changed

+855
-394
lines changed

RestClient.Net.OpenApiGenerator/CodeGenerationHelpers.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,29 @@ public static string Indent(string text, int level)
4848
/// <returns>The path expression.</returns>
4949
public static string BuildPathExpression(string path) => path;
5050

51+
/// <summary>
52+
/// Replaces path parameter names with their sanitized C# equivalents.
53+
/// </summary>
54+
/// <param name="path">The path template with original parameter names.</param>
55+
/// <param name="parameters">List of parameters with original and sanitized names.</param>
56+
/// <returns>The path with sanitized parameter names.</returns>
57+
public static string SanitizePathParameters(
58+
string path,
59+
List<(string Name, string Type, bool IsPath, string OriginalName)> parameters
60+
)
61+
{
62+
var result = path;
63+
foreach (var param in parameters.Where(p => p.IsPath))
64+
{
65+
result = result.Replace(
66+
"{" + param.OriginalName + "}",
67+
"{" + param.Name + "}",
68+
StringComparison.Ordinal
69+
);
70+
}
71+
return result;
72+
}
73+
5174
/// <summary>Regular expression for matching path parameters.</summary>
5275
[GeneratedRegex(@"\{[^}]+\}")]
5376
public static partial Regex PathParameterRegex();

RestClient.Net.OpenApiGenerator/ExtensionMethodGenerator.cs

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ internal static class ExtensionMethodGenerator
77
/// <param name="document">The OpenAPI document.</param>
88
/// <param name="namespace">The namespace for generated code.</param>
99
/// <param name="className">The class name for extension methods.</param>
10-
/// <param name="baseUrl">The base URL for API requests.</param>
10+
/// <param name="baseUrl">The base URL for API requests (not used, kept for API compatibility).</param>
1111
/// <param name="basePath">The base path for API requests.</param>
1212
/// <param name="jsonNamingPolicy">JSON naming policy (camelCase, PascalCase, snake_case).</param>
1313
/// <param name="caseInsensitive">Enable case-insensitive JSON deserialization.</param>
1414
/// <returns>Tuple containing the extension methods code and type aliases code.</returns>
15+
#pragma warning disable IDE0060 // Remove unused parameter
1516
public static (string ExtensionMethods, string TypeAliases) GenerateExtensionMethods(
1617
OpenApiDocument document,
1718
string @namespace,
@@ -21,6 +22,7 @@ public static (string ExtensionMethods, string TypeAliases) GenerateExtensionMet
2122
string jsonNamingPolicy = "camelCase",
2223
bool caseInsensitive = true
2324
)
25+
#pragma warning restore IDE0060 // Remove unused parameter
2426
{
2527
var groupedMethods =
2628
new Dictionary<string, List<(string PublicMethod, string PrivateDelegate)>>();
@@ -77,6 +79,7 @@ when s.Equals("snake_case", StringComparison.OrdinalIgnoreCase)
7779
var caseInsensitiveCode = caseInsensitive ? "true" : "false";
7880

7981
var extensionMethodsCode = $$"""
82+
#nullable enable
8083
using System;
8184
using System.Collections.Generic;
8285
using System.Net.Http;
@@ -251,6 +254,8 @@ string summary
251254
: operationType == HttpMethod.Put ? "Put"
252255
: operationType == HttpMethod.Delete ? "Delete"
253256
: operationType == HttpMethod.Patch ? "Patch"
257+
: operationType == HttpMethod.Head ? "Head"
258+
: operationType == HttpMethod.Options ? "Options"
254259
: operationType.Method;
255260
var createMethod = $"Create{verb}";
256261
var delegateType = $"{verb}Async";
@@ -265,7 +270,7 @@ string summary
265270
var deserializeMethod = isDelete ? "_deserializeUnit" : deserializer;
266271

267272
var privateDelegate = $$"""
268-
private static {{delegateType}}<{{resultResponseType}}, string, Unit> {{privateFunctionName}} { get; } =
273+
private static {{delegateType}}<{{resultResponseType}}, string, Unit> {{privateFunctionName}}() =>
269274
RestClient.Net.HttpClientFactoryExtensions.{{createMethod}}<{{resultResponseType}}, string, Unit>(
270275
url: BaseUrl,
271276
buildRequest: static _ => new HttpRequestParts(new RelativeUrl("{{path}}"), null, null),
@@ -289,7 +294,7 @@ string summary
289294
var deserializeMethod = isDelete ? "_deserializeUnit" : deserializer;
290295

291296
var privateDelegate = $$"""
292-
private static {{delegateType}}<{{resultResponseType}}, string, {{bodyType}}> {{privateFunctionName}} { get; } =
297+
private static {{delegateType}}<{{resultResponseType}}, string, {{bodyType}}> {{privateFunctionName}}() =>
293298
RestClient.Net.HttpClientFactoryExtensions.{{createMethod}}<{{resultResponseType}}, string, {{bodyType}}>(
294299
url: BaseUrl,
295300
buildRequest: static body => new HttpRequestParts(new RelativeUrl("{{path}}"), CreateJsonContent(body), null),
@@ -304,7 +309,7 @@ string summary
304309
this HttpClient httpClient,
305310
{{bodyType}} body,
306311
CancellationToken ct = default
307-
) => {{privateFunctionName}}(httpClient, body, ct);
312+
) => {{privateFunctionName}}()(httpClient, body, ct);
308313
""";
309314

310315
return (publicMethod, privateDelegate);
@@ -330,7 +335,7 @@ string summary
330335
);
331336

332337
var privateDelegate = $$"""
333-
private static {{delegateType}}<{{resultResponseType}}, string, {{queryParamsType}}> {{privateFunctionName}} { get; } =
338+
private static {{delegateType}}<{{resultResponseType}}, string, {{queryParamsType}}> {{privateFunctionName}}() =>
334339
RestClient.Net.HttpClientFactoryExtensions.{{createMethod}}<{{resultResponseType}}, string, {{queryParamsType}}>(
335340
url: BaseUrl,
336341
buildRequest: static param => new HttpRequestParts(new RelativeUrl($"{{path}}{{queryStringWithParam}}"), null, null),
@@ -345,7 +350,7 @@ string summary
345350
this HttpClient httpClient,
346351
{{string.Join(", ", queryParams.Select(q => $"{q.Type} {q.Name}"))}},
347352
CancellationToken ct = default
348-
) => {{privateFunctionName}}(httpClient, {{paramInvocation}}, ct);
353+
) => {{privateFunctionName}}()(httpClient, {{paramInvocation}}, ct);
349354
""";
350355

351356
return (publicMethod, privateDelegate);
@@ -358,7 +363,7 @@ string summary
358363
? pathParams[0].Type
359364
: $"({string.Join(", ", pathParams.Select(p => $"{p.Type} {p.Name}"))})";
360365
var pathParamsNames = string.Join(", ", pathParams.Select(p => p.Name));
361-
var lambda = isSinglePathParam ? pathParams[0].Name : $"({pathParamsNames})";
366+
var lambda = isSinglePathParam ? pathParams[0].Name : "param";
362367
var publicMethodParams = string.Join(", ", pathParams.Select(p => $"{p.Type} {p.Name}"));
363368
var publicMethodInvocation = isSinglePathParam ? pathParamsNames : $"({pathParamsNames})";
364369

@@ -381,17 +386,21 @@ string summary
381386
"&",
382387
queryParams.Select(q => $"{q.OriginalName}={{param.{q.Name}}}")
383388
);
389+
var sanitizedPath = CodeGenerationHelpers.SanitizePathParameters(
390+
pathExpression,
391+
parameters
392+
);
384393
var pathWithParam =
385394
isSingleParam && hasPathParams
386-
? pathExpression.Replace(
395+
? sanitizedPath.Replace(
387396
"{" + parameters.First(p => p.IsPath).Name + "}",
388397
"{param}",
389398
StringComparison.Ordinal
390399
)
391-
: pathExpression.Replace("{", "{param.", StringComparison.Ordinal);
400+
: sanitizedPath.Replace("{", "{param.", StringComparison.Ordinal);
392401

393402
var privateDelegate = $$"""
394-
private static {{delegateType}}<{{resultResponseType}}, string, {{allParamsType}}> {{privateFunctionName}} { get; } =
403+
private static {{delegateType}}<{{resultResponseType}}, string, {{allParamsType}}> {{privateFunctionName}} =>
395404
RestClient.Net.HttpClientFactoryExtensions.{{createMethod}}<{{resultResponseType}}, string, {{allParamsType}}>(
396405
url: BaseUrl,
397406
buildRequest: static param => new HttpRequestParts(new RelativeUrl($"{{pathWithParam}}{{queryStringWithParam}}"), null, null),
@@ -415,12 +424,19 @@ string summary
415424
if (!hasBody)
416425
{
417426
var deserializeMethod = isDelete ? "_deserializeUnit" : deserializer;
427+
var sanitizedPath = CodeGenerationHelpers.SanitizePathParameters(
428+
pathExpression,
429+
parameters
430+
);
431+
var pathWithParam = isSinglePathParam
432+
? sanitizedPath
433+
: sanitizedPath.Replace("{", "{param.", StringComparison.Ordinal);
418434

419435
var privateDelegate = $$"""
420-
private static {{delegateType}}<{{resultResponseType}}, string, {{pathParamsType}}> {{privateFunctionName}} { get; } =
436+
private static {{delegateType}}<{{resultResponseType}}, string, {{pathParamsType}}> {{privateFunctionName}}() =>
421437
RestClient.Net.HttpClientFactoryExtensions.{{createMethod}}<{{resultResponseType}}, string, {{pathParamsType}}>(
422438
url: BaseUrl,
423-
buildRequest: static {{lambda}} => new HttpRequestParts(new RelativeUrl($"{{pathExpression}}"), null, null),
439+
buildRequest: static {{lambda}} => new HttpRequestParts(new RelativeUrl($"{{pathWithParam}}"), null, null),
424440
deserializeSuccess: {{deserializeMethod}},
425441
deserializeError: DeserializeError
426442
);
@@ -432,20 +448,28 @@ string summary
432448
this HttpClient httpClient,
433449
{{publicMethodParams}},
434450
CancellationToken ct = default
435-
) => {{privateFunctionName}}(httpClient, {{publicMethodInvocation}}, ct);
451+
) => {{privateFunctionName}}()(httpClient, {{publicMethodInvocation}}, ct);
436452
""";
437453

438454
return (publicMethod, privateDelegate);
439455
}
440456
else
441457
{
442458
var compositeType = $"({pathParamsType} Params, {bodyType} Body)";
443-
var pathWithParamInterpolation = CodeGenerationHelpers
444-
.PathParameterRegex()
445-
.Replace(pathExpression, "{param.Params}");
459+
var sanitizedPath = CodeGenerationHelpers.SanitizePathParameters(
460+
pathExpression,
461+
parameters
462+
);
463+
var pathWithParamInterpolation = isSinglePathParam
464+
? sanitizedPath.Replace(
465+
"{" + pathParams[0].Name + "}",
466+
"{param.Params}",
467+
StringComparison.Ordinal
468+
)
469+
: sanitizedPath.Replace("{", "{param.Params.", StringComparison.Ordinal);
446470

447471
var privateDelegate = $$"""
448-
private static {{delegateType}}<{{resultResponseType}}, string, {{compositeType}}> {{privateFunctionName}} { get; } =
472+
private static {{delegateType}}<{{resultResponseType}}, string, {{compositeType}}> {{privateFunctionName}}() =>
449473
RestClient.Net.HttpClientFactoryExtensions.{{createMethod}}<{{resultResponseType}}, string, {{compositeType}}>(
450474
url: BaseUrl,
451475
buildRequest: static param => new HttpRequestParts(new RelativeUrl($"{{pathWithParamInterpolation}}"), CreateJsonContent(param.Body), null),
@@ -460,7 +484,7 @@ string summary
460484
this HttpClient httpClient,
461485
{{compositeType}} param,
462486
CancellationToken ct = default
463-
) => {{privateFunctionName}}(httpClient, param, ct);
487+
) => {{privateFunctionName}}()(httpClient, param, ct);
464488
""";
465489

466490
return (publicMethod, privateDelegate);
@@ -488,6 +512,8 @@ string path
488512
: operationType == HttpMethod.Put ? "Update"
489513
: operationType == HttpMethod.Delete ? "Delete"
490514
: operationType == HttpMethod.Patch ? "Patch"
515+
: operationType == HttpMethod.Head ? "Head"
516+
: operationType == HttpMethod.Options ? "Options"
491517
: operationType.Method;
492518

493519
return $"{methodName}{CodeGenerationHelpers.ToPascalCase(pathPart)}";
@@ -591,14 +617,14 @@ _ when responseType.StartsWith("List<", StringComparison.Ordinal) =>
591617
// Generate Ok alias
592618
aliases.Add(
593619
$$"""
594-
using Ok{{aliasName}} = Outcome.Result<{{qualifiedType}}, Outcome.HttpError<string>>.Ok<{{qualifiedType}}, Outcome.HttpError<string>>;
620+
global using Ok{{aliasName}} = Outcome.Result<{{qualifiedType}}, Outcome.HttpError<string>>.Ok<{{qualifiedType}}, Outcome.HttpError<string>>;
595621
"""
596622
);
597623

598624
// Generate Error alias
599625
aliases.Add(
600626
$$"""
601-
using Error{{aliasName}} = Outcome.Result<{{qualifiedType}}, Outcome.HttpError<string>>.Error<{{qualifiedType}}, Outcome.HttpError<string>>;
627+
global using Error{{aliasName}} = Outcome.Result<{{qualifiedType}}, Outcome.HttpError<string>>.Error<{{qualifiedType}}, Outcome.HttpError<string>>;
602628
"""
603629
);
604630
}

RestClient.Net.OpenApiGenerator/ModelGenerator.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ private static string GenerateModel(string name, OpenApiSchema schema)
4343
.Select(p =>
4444
{
4545
var propName = CodeGenerationHelpers.ToPascalCase(p.Key);
46+
47+
// Avoid property name conflict with class name
48+
if (propName.Equals(name, StringComparison.Ordinal))
49+
{
50+
propName += "Value";
51+
}
52+
4653
var propType = MapOpenApiType(p.Value);
4754
var propDesc = SanitizeDescription(
4855
(p.Value as OpenApiSchema)?.Description ?? propName

RestClient.Net/Delegates.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,39 @@ public delegate Task<Result<TSuccess, HttpError<TError>>> PatchAsync<TSuccess, T
143143
CancellationToken cancellationToken = default
144144
);
145145
#pragma warning restore CA1005 // Avoid excessive parameters on generic types
146+
147+
/// <summary>
148+
/// Delegate for executing HEAD requests that return a Result with typed success and error responses.
149+
/// </summary>
150+
/// <typeparam name="TSuccess">The type of the success response body.</typeparam>
151+
/// <typeparam name="TError">The type of the error response body.</typeparam>
152+
/// <typeparam name="TParam">The type of the parameter used to construct the request URL.</typeparam>
153+
/// <param name="httpClient">The HttpClient to use for the request.</param>
154+
/// <param name="parameters">The parameters used to construct the request URL.</param>
155+
/// <param name="cancellationToken">Cancellation token to cancel the request.</param>
156+
/// <returns>A Result containing either the success response or an HttpError with the error response.</returns>
157+
#pragma warning disable CA1005 // Avoid excessive parameters on generic types
158+
public delegate Task<Result<TSuccess, HttpError<TError>>> HeadAsync<TSuccess, TError, TParam>(
159+
HttpClient httpClient,
160+
TParam parameters,
161+
CancellationToken cancellationToken = default
162+
);
163+
#pragma warning restore CA1005 // Avoid excessive parameters on generic types
164+
165+
/// <summary>
166+
/// Delegate for executing OPTIONS requests that return a Result with typed success and error responses.
167+
/// </summary>
168+
/// <typeparam name="TSuccess">The type of the success response body.</typeparam>
169+
/// <typeparam name="TError">The type of the error response body.</typeparam>
170+
/// <typeparam name="TParam">The type of the parameter used to construct the request URL.</typeparam>
171+
/// <param name="httpClient">The HttpClient to use for the request.</param>
172+
/// <param name="parameters">The parameters used to construct the request URL.</param>
173+
/// <param name="cancellationToken">Cancellation token to cancel the request.</param>
174+
/// <returns>A Result containing either the success response or an HttpError with the error response.</returns>
175+
#pragma warning disable CA1005 // Avoid excessive parameters on generic types
176+
public delegate Task<Result<TSuccess, HttpError<TError>>> OptionsAsync<TSuccess, TError, TParam>(
177+
HttpClient httpClient,
178+
TParam parameters,
179+
CancellationToken cancellationToken = default
180+
);
181+
#pragma warning restore CA1005 // Avoid excessive parameters on generic types

RestClient.Net/HttpClientExtensions.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,66 @@ public static Task<Result<TSuccess, HttpError<TError>>> PatchAsync<TSuccess, TEr
242242
cancellationToken: cancellationToken
243243
);
244244

245+
/// <summary>
246+
/// Performs a HEAD request.
247+
/// </summary>
248+
/// <typeparam name="TSuccess">The type representing a successful response.</typeparam>
249+
/// <typeparam name="TError">The type representing an error response.</typeparam>
250+
/// <param name="httpClient">The HTTP client to use.</param>
251+
/// <param name="url">The URL to send the request to.</param>
252+
/// <param name="deserializeSuccess">Function to deserialize a successful response.</param>
253+
/// <param name="deserializeError">Function to deserialize an error response.</param>
254+
/// <param name="headers">The headers to include in the request (optional).</param>
255+
/// <param name="cancellationToken">A token to cancel the operation.</param>
256+
/// <returns>A Result containing either the successful response or an HTTP error.</returns>
257+
public static Task<Result<TSuccess, HttpError<TError>>> HeadAsync<TSuccess, TError>(
258+
this HttpClient httpClient,
259+
AbsoluteUrl url,
260+
Deserialize<TSuccess> deserializeSuccess,
261+
Deserialize<TError> deserializeError,
262+
IReadOnlyDictionary<string, string>? headers = null,
263+
CancellationToken cancellationToken = default
264+
) =>
265+
httpClient.SendAsync(
266+
url: url,
267+
httpMethod: HttpMethod.Head,
268+
deserializeSuccess: deserializeSuccess,
269+
deserializeError: deserializeError,
270+
requestBody: null,
271+
headers: headers,
272+
cancellationToken: cancellationToken
273+
);
274+
275+
/// <summary>
276+
/// Performs an OPTIONS request.
277+
/// </summary>
278+
/// <typeparam name="TSuccess">The type representing a successful response.</typeparam>
279+
/// <typeparam name="TError">The type representing an error response.</typeparam>
280+
/// <param name="httpClient">The HTTP client to use.</param>
281+
/// <param name="url">The URL to send the request to.</param>
282+
/// <param name="deserializeSuccess">Function to deserialize a successful response.</param>
283+
/// <param name="deserializeError">Function to deserialize an error response.</param>
284+
/// <param name="headers">The headers to include in the request (optional).</param>
285+
/// <param name="cancellationToken">A token to cancel the operation.</param>
286+
/// <returns>A Result containing either the successful response or an HTTP error.</returns>
287+
public static Task<Result<TSuccess, HttpError<TError>>> OptionsAsync<TSuccess, TError>(
288+
this HttpClient httpClient,
289+
AbsoluteUrl url,
290+
Deserialize<TSuccess> deserializeSuccess,
291+
Deserialize<TError> deserializeError,
292+
IReadOnlyDictionary<string, string>? headers = null,
293+
CancellationToken cancellationToken = default
294+
) =>
295+
httpClient.SendAsync(
296+
url: url,
297+
httpMethod: HttpMethod.Options,
298+
deserializeSuccess: deserializeSuccess,
299+
deserializeError: deserializeError,
300+
requestBody: null,
301+
headers: headers,
302+
cancellationToken: cancellationToken
303+
);
304+
245305
/// <summary>
246306
/// Downloads a file from the specified URL to a stream.
247307
/// </summary>

0 commit comments

Comments
 (0)