Skip to content

Commit bfb0724

Browse files
All nuclia db tests pass
1 parent 190b420 commit bfb0724

File tree

11 files changed

+700
-2281
lines changed

11 files changed

+700
-2281
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
1313
- NEVER copy files. Only MOVE files
1414
- Don't use Git unless I explicitly ask you to
1515
- Promote code analysis warnings to errors
16+
- EXHAUSTION001 is a critical error and must be turned on everywhere
1617
- Nullable reference types are enabled and MUST be obeyed
1718
- Do not back files up
1819
- Aggressively pursue these aims, even when it means taking more time on a task

RestClient.Net.OpenApiGenerator/ExtensionMethodGenerator.cs

Lines changed: 52 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ public static (string ExtensionMethods, string TypeAliases) GenerateExtensionMet
5656
path.Key,
5757
operation.Key,
5858
operation.Value,
59-
basePath
59+
basePath,
60+
document.Components?.Schemas
6061
);
6162
if (!string.IsNullOrEmpty(publicMethod))
6263
{
@@ -155,6 +156,34 @@ private static async Task<string> DeserializeError(
155156
var content = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
156157
return string.IsNullOrEmpty(content) ? "Unknown error" : content;
157158
}
159+
160+
private static string BuildQueryString(params (string Key, object? Value)[] parameters)
161+
{
162+
var parts = new List<string>();
163+
foreach (var (key, value) in parameters)
164+
{
165+
if (value == null)
166+
{
167+
continue;
168+
}
169+
170+
if (value is System.Collections.IEnumerable enumerable and not string)
171+
{
172+
foreach (var item in enumerable)
173+
{
174+
if (item != null)
175+
{
176+
parts.Add($"{key}={Uri.EscapeDataString(item.ToString() ?? string.Empty)}");
177+
}
178+
}
179+
}
180+
else
181+
{
182+
parts.Add($"{key}={Uri.EscapeDataString(value.ToString() ?? string.Empty)}");
183+
}
184+
}
185+
return parts.Count > 0 ? "?" + string.Join("&", parts) : string.Empty;
186+
}
158187
}
159188
""";
160189

@@ -208,11 +237,12 @@ private static (string PublicMethod, string PrivateDelegate) GenerateMethod(
208237
string path,
209238
HttpMethod operationType,
210239
OpenApiOperation operation,
211-
string basePath
240+
string basePath,
241+
IDictionary<string, IOpenApiSchema>? schemas = null
212242
)
213243
{
214244
var methodName = GetMethodName(operation, operationType, path);
215-
var parameters = GetParameters(operation);
245+
var parameters = GetParameters(operation, schemas);
216246
var requestBodyType = GetRequestBodyType(operation);
217247
var responseType = GetResponseType(operation);
218248
var fullPath = $"{basePath}{path}";
@@ -331,28 +361,20 @@ string summary
331361
var paramInvocation = isSingleParam ? nonPathParamsNames : $"({nonPathParamsNames})";
332362
var deserializeMethod = isDelete ? "_deserializeUnit" : deserializer;
333363

334-
var queryString = hasQueryParams
364+
var queryStringExpression = hasQueryParams
335365
? (
336366
isSingleParam && queryParams.Count == 1
337-
? "?"
338-
+ string.Join(
339-
"&",
340-
queryParams.Select(q => $"{q.OriginalName}={{param}}")
341-
)
342-
: "?"
343-
+ string.Join(
344-
"&",
345-
queryParams.Select(q => $"{q.OriginalName}={{param.{q.Name}}}")
346-
)
367+
? $"BuildQueryString((\"{queryParams[0].OriginalName}\", param))"
368+
: $"BuildQueryString({string.Join(", ", queryParams.Select(q => $"(\"{q.OriginalName}\", param.{q.Name})"))})"
347369
)
348-
: string.Empty;
370+
: "string.Empty";
349371

350372
var headersExpression = hasHeaderParams
351373
? BuildHeadersDictionaryExpression(headerParams, "param", isSingleParam)
352374
: "null";
353375

354376
var buildRequestBody =
355-
$"static param => new HttpRequestParts(new RelativeUrl($\"{path}{queryString}\"), null, {headersExpression})";
377+
$"static param => new HttpRequestParts(new RelativeUrl($\"{path}{{{queryStringExpression}}}\"), null, {headersExpression})";
356378

357379
return BuildMethod(
358380
methodName,
@@ -389,21 +411,13 @@ string summary
389411
var paramInvocation = isSingleParam ? allParamsNames : $"({allParamsNames})";
390412
var deserializeMethod = isDelete ? "_deserializeUnit" : deserializer;
391413

392-
var queryString = hasQueryParams
414+
var queryStringExpression = hasQueryParams
393415
? (
394416
isSingleParam && queryParams.Count == 1
395-
? "?"
396-
+ string.Join(
397-
"&",
398-
queryParams.Select(q => $"{q.OriginalName}={{param}}")
399-
)
400-
: "?"
401-
+ string.Join(
402-
"&",
403-
queryParams.Select(q => $"{q.OriginalName}={{param.{q.Name}}}")
404-
)
417+
? $"BuildQueryString((\"{queryParams[0].OriginalName}\", param))"
418+
: $"BuildQueryString({string.Join(", ", queryParams.Select(q => $"(\"{q.OriginalName}\", param.{q.Name})"))})"
405419
)
406-
: string.Empty;
420+
: "string.Empty";
407421

408422
var headersExpression = hasHeaderParams
409423
? BuildHeadersDictionaryExpression(headerParams, "param", isSingleParam)
@@ -423,7 +437,7 @@ string summary
423437
: sanitizedPath.Replace("{", "{param.", StringComparison.Ordinal);
424438

425439
var buildRequestBody =
426-
$"static param => new HttpRequestParts(new RelativeUrl($\"{pathWithParam}{queryString}\"), null, {headersExpression})";
440+
$"static param => new HttpRequestParts(new RelativeUrl($\"{pathWithParam}{{{queryStringExpression}}}\"), null, {headersExpression})";
427441

428442
return BuildMethod(
429443
methodName,
@@ -506,20 +520,16 @@ string summary
506520
: sanitizedPath.Replace("{", "{param.Params.", StringComparison.Ordinal)
507521
);
508522

509-
var queryString = hasQueryParams
510-
? "?"
511-
+ string.Join(
512-
"&",
513-
queryParams.Select(q => $"{q.OriginalName}={{param.{q.Name}}}")
514-
)
515-
: string.Empty;
523+
var queryStringExpression = hasQueryParams
524+
? $"BuildQueryString({string.Join(", ", queryParams.Select(q => $"(\"{q.OriginalName}\", param.{q.Name})"))})"
525+
: "string.Empty";
516526

517527
var headersExpression = hasHeaderParams
518528
? BuildHeadersDictionaryExpression(headerParams, "param", false)
519529
: "null";
520530

521531
var buildRequestBody =
522-
$"static param => new HttpRequestParts(new RelativeUrl($\"{pathWithParamInterpolation}{queryString}\"), CreateJsonContent(param.Body), {headersExpression})";
532+
$"static param => new HttpRequestParts(new RelativeUrl($\"{pathWithParamInterpolation}{{{queryStringExpression}}}\"), CreateJsonContent(param.Body), {headersExpression})";
523533

524534
// Public methods ALWAYS have individual parameters, never tuples
525535
var publicMethodParams = hasNonPathNonBodyParams
@@ -577,7 +587,10 @@ string path
577587
return $"{methodName}{CodeGenerationHelpers.ToPascalCase(pathPart)}";
578588
}
579589

580-
private static List<ParameterInfo> GetParameters(OpenApiOperation operation)
590+
private static List<ParameterInfo> GetParameters(
591+
OpenApiOperation operation,
592+
IDictionary<string, IOpenApiSchema>? schemas = null
593+
)
581594
{
582595
var parameters = new List<ParameterInfo>();
583596

@@ -595,7 +608,7 @@ private static List<ParameterInfo> GetParameters(OpenApiOperation operation)
595608

596609
var isPath = param.In == ParameterLocation.Path;
597610
var isHeader = param.In == ParameterLocation.Header;
598-
var type = ModelGenerator.MapOpenApiType(param.Schema);
611+
var type = ModelGenerator.MapOpenApiType(param.Schema, schemas);
599612
var sanitizedName = CodeGenerationHelpers.ToCamelCase(param.Name);
600613
parameters.Add(new ParameterInfo(sanitizedName, type, isPath, isHeader, param.Name));
601614
}

RestClient.Net.OpenApiGenerator/ModelGenerator.cs

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ public static string GenerateModels(OpenApiDocument document, string @namespace)
1717
{
1818
if (schema.Value is OpenApiSchema schemaObj)
1919
{
20+
// Skip string enums - they'll be mapped to string type
21+
if (IsStringEnum(schemaObj))
22+
{
23+
continue;
24+
}
25+
2026
var className = CodeGenerationHelpers.ToPascalCase(schema.Key);
21-
var model = GenerateModel(className, schemaObj);
27+
var model = GenerateModel(className, schemaObj, document.Components.Schemas);
2228
models.Add(model);
2329
}
2430
}
@@ -33,11 +39,24 @@ namespace {{@namespace}};
3339
""";
3440
}
3541

42+
/// <summary>Checks if a schema is a string enum.</summary>
43+
/// <param name="schema">The schema to check.</param>
44+
/// <returns>True if the schema is a string enum.</returns>
45+
private static bool IsStringEnum(OpenApiSchema schema) =>
46+
schema.Type == JsonSchemaType.String &&
47+
schema.Enum != null &&
48+
schema.Enum.Count > 0;
49+
3650
/// <summary>Generates a single C# model class from an OpenAPI schema.</summary>
3751
/// <param name="name">The name of the model.</param>
3852
/// <param name="schema">The OpenAPI schema.</param>
53+
/// <param name="schemas">Optional schemas dictionary to check for string enums.</param>
3954
/// <returns>The generated model code.</returns>
40-
private static string GenerateModel(string name, OpenApiSchema schema)
55+
private static string GenerateModel(
56+
string name,
57+
OpenApiSchema schema,
58+
IDictionary<string, IOpenApiSchema>? schemas = null
59+
)
4160
{
4261
var properties = (schema.Properties ?? new Dictionary<string, IOpenApiSchema>())
4362
.Select(p =>
@@ -50,7 +69,7 @@ private static string GenerateModel(string name, OpenApiSchema schema)
5069
propName += "Value";
5170
}
5271

53-
var propType = MapOpenApiType(p.Value);
72+
var propType = MapOpenApiType(p.Value, schemas);
5473
var propDesc = SanitizeDescription(
5574
(p.Value as OpenApiSchema)?.Description ?? propName
5675
);
@@ -82,8 +101,12 @@ private static string SanitizeDescription(string description) =>
82101

83102
/// <summary>Maps an OpenAPI schema to a C# type.</summary>
84103
/// <param name="schema">The OpenAPI schema.</param>
104+
/// <param name="schemas">Optional schemas dictionary to check for string enums.</param>
85105
/// <returns>The C# type name.</returns>
86-
public static string MapOpenApiType(IOpenApiSchema? schema)
106+
public static string MapOpenApiType(
107+
IOpenApiSchema? schema,
108+
IDictionary<string, IOpenApiSchema>? schemas = null
109+
)
87110
{
88111
if (schema == null)
89112
{
@@ -93,9 +116,15 @@ public static string MapOpenApiType(IOpenApiSchema? schema)
93116
// Check for schema reference first
94117
if (schema is OpenApiSchemaReference schemaRef)
95118
{
96-
return schemaRef.Reference.Id != null
97-
? CodeGenerationHelpers.ToPascalCase(schemaRef.Reference.Id)
98-
: "object";
119+
// Return "string" if this is a reference to a string enum, otherwise return the class name
120+
return schemaRef.Reference.Id == null
121+
? "object"
122+
: schemas != null &&
123+
schemas.TryGetValue(schemaRef.Reference.Id, out var referencedSchema) &&
124+
referencedSchema is OpenApiSchema refSchemaObj &&
125+
IsStringEnum(refSchemaObj)
126+
? "string"
127+
: CodeGenerationHelpers.ToPascalCase(schemaRef.Reference.Id);
99128
}
100129

101130
if (schema is not OpenApiSchema schemaObj)
@@ -107,15 +136,22 @@ public static string MapOpenApiType(IOpenApiSchema? schema)
107136
if (schemaObj.Type == JsonSchemaType.Array)
108137
{
109138
return schemaObj.Items is OpenApiSchemaReference itemsRef
110-
? $"List<{(itemsRef.Reference.Id != null ? CodeGenerationHelpers.ToPascalCase(itemsRef.Reference.Id) : "object")}>"
139+
? itemsRef.Reference.Id == null
140+
? "List<object>"
141+
: schemas != null &&
142+
schemas.TryGetValue(itemsRef.Reference.Id, out var itemsSchema) &&
143+
itemsSchema is OpenApiSchema itemsSchemaObj &&
144+
IsStringEnum(itemsSchemaObj)
145+
? "List<string>"
146+
: $"List<{CodeGenerationHelpers.ToPascalCase(itemsRef.Reference.Id)}>"
111147
: schemaObj.Items is OpenApiSchema items
112-
? items.Type switch
113-
{
114-
JsonSchemaType.String => "List<string>",
115-
JsonSchemaType.Integer => "List<int>",
116-
JsonSchemaType.Number => "List<double>",
117-
_ => "List<object>",
118-
}
148+
? items.Type switch
149+
{
150+
JsonSchemaType.String => "List<string>",
151+
JsonSchemaType.Integer => "List<int>",
152+
JsonSchemaType.Number => "List<double>",
153+
_ => "List<object>",
154+
}
119155
: "List<object>";
120156
}
121157

0 commit comments

Comments
 (0)