Skip to content

Commit b4c7692

Browse files
authored
[codex] generate polling helpers from vendor metadata (#269)
* feat(csharp): generate polling helpers from vendor metadata * fix(csharp): gate polling runtime support generation * fix(ci): stabilize generator support and snapshots * fix(ci): unify generated cookie header handling * fix(ci): remove duplicate webhook source entry
1 parent aa55036 commit b4c7692

File tree

13 files changed

+1118
-5
lines changed

13 files changed

+1118
-5
lines changed

src/libs/AutoSDK.CSharp/Operations/CSharpEndPointFactory.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ public static EndPoint CreateEndPoint(
165165
streamFormat = streamFormatValue;
166166
}
167167

168+
var pollingOperations = streamFormat == StreamFormat.None
169+
? operation.Operation.GetPollingOperations()
170+
: ImmutableArray<PollingOperation>.Empty.AsEquatableArray();
171+
168172
var endPointId = string.IsNullOrWhiteSpace(methodNameSuffix)
169173
? operation.MethodName
170174
: operation.MethodName + methodNameSuffix;
@@ -174,6 +178,7 @@ public static EndPoint CreateEndPoint(
174178
var notAsyncMethodName = endPointId.ToPropertyName();
175179
var generateResponseWrapper =
176180
responses.Any(x => x.HasHeaders && (x.Is2XX || x.IsDefault)) ||
181+
!pollingOperations.IsEmpty ||
177182
OpenApiExtensions.GetExtensionBooleanValue(
178183
operation.Operation.Extensions,
179184
"x-autosdk-response-wrapper");
@@ -229,6 +234,7 @@ public static EndPoint CreateEndPoint(
229234
: string.Empty,
230235
Remarks: GetCodeSamplesRemarks(operation.Operation),
231236
GenerateResponseWrapper: generateResponseWrapper,
237+
PollingOperations: pollingOperations,
232238
Servers: servers,
233239
HasServerOverride: operation.HasServerOverride);
234240
}

src/libs/AutoSDK.CSharp/Pipeline/CSharpPipeline.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,10 @@ public static IReadOnlyList<FileWithName> GenerateFiles(
158158
data.Methods.Any(static x => x.ClientUsesServerSelectionSupport)
159159
? [Sources.ServerSelectionSupport(settings, cancellationToken)]
160160
: [])
161-
.Concat([Sources.OptionsSupport(settings, cancellationToken)])
161+
.Concat([Sources.OptionsSupport(
162+
settings,
163+
includePollingSupport: data.Methods.Any(static x => !x.PollingOperations.IsEmpty),
164+
cancellationToken: cancellationToken)])
162165
.Concat(!data.Authorizations.IsEmpty
163166
? [Sources.SecuritySupport(settings, cancellationToken)]
164167
: [])

src/libs/AutoSDK.CSharp/Sources/Sources.Methods.cs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public partial class {endPoint.ClassName}
9797
9898
{GenerateMethod(endPoint)}
9999
{(ShouldGenerateResponseWrapperMethod(endPoint) ? GenerateMethod(endPoint, returnResponseWrapper: true) : TrimmedLine)}
100+
{GeneratePollingMethods(endPoint)}
100101
{GenerateExtensionMethod(endPoint)}
101102
}}
102103
}}".RemoveBlankLinesWhereOnlyWhitespaces();
@@ -121,6 +122,7 @@ public partial interface I{endPoint.ClassName}
121122
{{
122123
{GenerateMethod(endPoint, isInterface: true)}
123124
{(ShouldGenerateResponseWrapperMethod(endPoint) ? GenerateMethod(endPoint, isInterface: true, returnResponseWrapper: true) : TrimmedLine)}
125+
{GeneratePollingMethods(endPoint, isInterface: true)}
124126
{GenerateExtensionMethod(endPoint, isInterface: true)}
125127
}}
126128
}}".RemoveBlankLinesWhereOnlyWhitespaces();
@@ -272,6 +274,170 @@ private static string GetResponseWrapperMethodName(EndPoint endPoint)
272274
return $"{endPoint.NotAsyncMethodName}AsResponseAsync";
273275
}
274276

277+
private static string GetPollingMethodName(
278+
EndPoint endPoint,
279+
PollingOperation pollingOperation)
280+
{
281+
return $"{endPoint.NotAsyncMethodName}{pollingOperation.Name.ToPropertyName()}Async";
282+
}
283+
284+
private static bool SupportsPollingOperation(
285+
EndPoint endPoint,
286+
PollingOperation pollingOperation)
287+
{
288+
if (string.IsNullOrWhiteSpace(pollingOperation.Name) ||
289+
pollingOperation.SuccessCriteria.IsEmpty ||
290+
endPoint.RawStream ||
291+
endPoint.EnumerableStream)
292+
{
293+
return false;
294+
}
295+
296+
if (!string.IsNullOrWhiteSpace(endPoint.SuccessResponse.Type.CSharpType))
297+
{
298+
return true;
299+
}
300+
301+
return !pollingOperation.SuccessCriteria.Any(static x => x.ContextType == PollingCriterionContextType.ResponseBody) &&
302+
!pollingOperation.FailureCriteria.Any(static x => x.ContextType == PollingCriterionContextType.ResponseBody);
303+
}
304+
305+
private static string GeneratePollingMethods(
306+
EndPoint endPoint,
307+
bool isInterface = false)
308+
{
309+
return endPoint.PollingOperations
310+
.Where(x => SupportsPollingOperation(endPoint, x))
311+
.Select(x => GeneratePollingMethod(endPoint, x, isInterface))
312+
.Inject();
313+
}
314+
315+
private static string GeneratePollingMethod(
316+
EndPoint endPoint,
317+
PollingOperation pollingOperation,
318+
bool isInterface = false)
319+
{
320+
var methodName = GetPollingMethodName(endPoint, pollingOperation);
321+
var hasBody = !string.IsNullOrWhiteSpace(endPoint.SuccessResponse.Type.CSharpType);
322+
var bodyReturnType = hasBody
323+
? $"global::System.Threading.Tasks.Task<{endPoint.SuccessResponse.Type.CSharpTypeWithoutNullability}>"
324+
: "global::System.Threading.Tasks.Task";
325+
var successExpression = GeneratePollingCriteriaExpression(endPoint, pollingOperation.SuccessCriteria, "__pollingResponse");
326+
var failureExpression = GeneratePollingCriteriaExpression(endPoint, pollingOperation.FailureCriteria, "__pollingResponse");
327+
var body = isInterface
328+
? ";"
329+
: $@"
330+
{{
331+
var __pollingOptions = global::{endPoint.Settings.Namespace}.AutoSDKPollingSupport.ResolvePollingOptions(
332+
pollingOptions: pollingOptions,
333+
defaultInitialDelay: global::System.TimeSpan.FromSeconds({pollingOperation.DelaySeconds}),
334+
defaultInterval: global::System.TimeSpan.FromSeconds({pollingOperation.IntervalSeconds}),
335+
defaultMaxAttempts: {pollingOperation.LimitCount});
336+
global::{endPoint.Settings.Namespace}.AutoSDKHttpResponse? __lastResponse = null;
337+
338+
if (__pollingOptions.InitialDelay > global::System.TimeSpan.Zero)
339+
{{
340+
await global::{endPoint.Settings.Namespace}.AutoSDKPollingSupport.DelayAsync(
341+
delay: __pollingOptions.InitialDelay,
342+
cancellationToken: cancellationToken).ConfigureAwait(false);
343+
}}
344+
345+
for (var __pollAttempt = 1; __pollAttempt <= __pollingOptions.MaxAttempts; __pollAttempt++)
346+
{{
347+
var __pollingResponse = await {GetResponseWrapperMethodName(endPoint)}(
348+
{GenerateMethodInvocationArguments(endPoint)}
349+
).ConfigureAwait(false);
350+
__lastResponse = __pollingResponse;
351+
352+
{(!pollingOperation.FailureCriteria.IsEmpty ? $@" if ({failureExpression})
353+
{{
354+
throw new global::{endPoint.Settings.Namespace}.AutoSDKPollingException(
355+
message: $""Polling helper '{methodName}' matched a configured failure criterion on attempt {{__pollAttempt}}."",
356+
response: __pollingResponse);
357+
}}
358+
" : TrimmedLine)}
359+
if ({successExpression})
360+
{{
361+
{(hasBody ? " return __pollingResponse.Body;" : " return;")}
362+
}}
363+
364+
if (__pollAttempt == __pollingOptions.MaxAttempts)
365+
{{
366+
break;
367+
}}
368+
369+
await global::{endPoint.Settings.Namespace}.AutoSDKPollingSupport.DelayAsync(
370+
delay: __pollingOptions.Interval,
371+
cancellationToken: cancellationToken).ConfigureAwait(false);
372+
}}
373+
374+
throw new global::{endPoint.Settings.Namespace}.AutoSDKPollingException(
375+
message: $""Polling helper '{methodName}' did not satisfy its success criteria after {{__pollingOptions.MaxAttempts}} attempts."",
376+
response: __lastResponse);
377+
}}";
378+
379+
return $@"
380+
{"Polls the endpoint until the configured polling criteria are satisfied.".ToXmlDocumentationSummary(level: 8)}
381+
{endPoint.Parameters.Where(x => x.Location != null).Select(x => $@"
382+
{x.Summary.ToXmlDocumentationForParam(x.ParameterName, level: 8)}").Inject()}
383+
{(string.IsNullOrWhiteSpace(endPoint.RequestType.CSharpType) ? TrimmedLine : @"
384+
/// <param name=""request""></param>")}
385+
/// <param name=""pollingOptions"">Overrides the generated polling delay, interval, and attempt limit.</param>
386+
/// <param name=""requestOptions"">Per-request overrides applied to each poll attempt.</param>
387+
/// <param name=""cancellationToken"">The token to cancel the polling operation with</param>
388+
/// <exception cref=""global::{endPoint.Settings.Namespace}.AutoSDKPollingException""></exception>
389+
{(isInterface ? "" : "public async ")}{bodyReturnType} {methodName}(
390+
{endPoint.Parameters.Where(x => x is { Location: not null, IsRequired: true } && !x.HasSchemaDefault).Select(x => $@"
391+
{x.Type.CSharpType} {x.ParameterName},").Inject()}
392+
{(string.IsNullOrWhiteSpace(endPoint.RequestType.CSharpType) ? TrimmedLine : $@"
393+
{endPoint.RequestType.CSharpTypeWithoutNullability} request,")}
394+
{endPoint.Parameters.Where(x => x is { Location: not null } && (!x.IsRequired || x.HasSchemaDefault)).Select(x => $@"
395+
{x.Type.CSharpType} {x.ParameterName} = {x.ParameterDefaultValue},").Inject()}
396+
global::{endPoint.Settings.Namespace}.AutoSDKPollingOptions? pollingOptions = default,
397+
global::{endPoint.Settings.Namespace}.AutoSDKRequestOptions? requestOptions = default,
398+
global::System.Threading.CancellationToken cancellationToken = default){body}
399+
".RemoveBlankLinesWhereOnlyWhitespaces();
400+
}
401+
402+
private static string GeneratePollingCriteriaExpression(
403+
EndPoint endPoint,
404+
EquatableArray<PollingCriterion> criteria,
405+
string responseVariableName)
406+
{
407+
return criteria.IsEmpty
408+
? "false"
409+
: string.Join(
410+
" && ",
411+
criteria.Select(x => GeneratePollingCriterionExpression(endPoint, x, responseVariableName)));
412+
}
413+
414+
private static string GeneratePollingCriterionExpression(
415+
EndPoint endPoint,
416+
PollingCriterion criterion,
417+
string responseVariableName)
418+
{
419+
return criterion switch
420+
{
421+
{ Type: PollingCriterionType.Simple, ContextType: PollingCriterionContextType.StatusCode } => $@"global::{endPoint.Settings.Namespace}.AutoSDKPollingSupport.MatchesStatusCode(
422+
statusCode: {responseVariableName}.StatusCode,
423+
@operator: {criterion.Operator.ToCSharpStringLiteral()},
424+
expectedValue: {criterion.ExpectedValue.ToCSharpStringLiteral()})",
425+
{ Type: PollingCriterionType.Regex, ContextType: PollingCriterionContextType.StatusCode } => $@"global::{endPoint.Settings.Namespace}.AutoSDKPollingSupport.MatchesRegexValue(
426+
value: global::{endPoint.Settings.Namespace}.AutoSDKPollingSupport.GetStatusCodeValue({responseVariableName}.StatusCode),
427+
pattern: {criterion.Pattern.ToCSharpStringLiteral()})",
428+
{ Type: PollingCriterionType.Simple, ContextType: PollingCriterionContextType.ResponseBody } => $@"global::{endPoint.Settings.Namespace}.AutoSDKPollingSupport.MatchesSimpleCondition(
429+
body: {responseVariableName}{(string.IsNullOrWhiteSpace(endPoint.SuccessResponse.Type.CSharpType) ? string.Empty : ".Body")},
430+
jsonPointer: {criterion.JsonPointer.ToCSharpStringLiteral()},
431+
@operator: {criterion.Operator.ToCSharpStringLiteral()},
432+
expectedValue: {criterion.ExpectedValue.ToCSharpStringLiteral()})",
433+
{ Type: PollingCriterionType.Regex, ContextType: PollingCriterionContextType.ResponseBody } => $@"global::{endPoint.Settings.Namespace}.AutoSDKPollingSupport.MatchesRegexCondition(
434+
body: {responseVariableName}{(string.IsNullOrWhiteSpace(endPoint.SuccessResponse.Type.CSharpType) ? string.Empty : ".Body")},
435+
jsonPointer: {criterion.JsonPointer.ToCSharpStringLiteral()},
436+
pattern: {criterion.Pattern.ToCSharpStringLiteral()})",
437+
_ => "false",
438+
};
439+
}
440+
275441
private static string GetSendExpression(
276442
EndPoint endPoint,
277443
string requestVariableName,

0 commit comments

Comments
 (0)