Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/libs/AutoSDK.CLI/Commands/GenerateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ private async Task HandleAsync(ParseResult parseResult)
.Concat([Sources.Polyfills(settings)])
.Concat([Sources.Exceptions(settings)])
.Concat([Sources.PathBuilder(settings)])
.Concat(data.Methods.Any(static x => x.RawStream)
? [Sources.ResponseStream(data.Converters.Settings)]
: [])
.Concat([Sources.UnixTimestampJsonConverter(settings)])
.Where(x => !x.IsEmpty)
.ToArray();
Expand All @@ -290,4 +293,4 @@ private async Task HandleAsync(ParseResult parseResult)

Console.WriteLine("Done.");
}
}
}
217 changes: 207 additions & 10 deletions src/libs/AutoSDK/Extensions/OpenApiExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
return (decimal.MinValue, decimal.MaxValue);
}

return schema.Format?.ToLowerInvariant() switch

Check warning on line 157 in src/libs/AutoSDK/Extensions/OpenApiExtensions.cs

View workflow job for this annotation

GitHub Actions / Test / Build, test and publish

In method 'GetTypeRange', replace the call to 'ToLowerInvariant' with 'ToUpperInvariant' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1308)

Check warning on line 157 in src/libs/AutoSDK/Extensions/OpenApiExtensions.cs

View workflow job for this annotation

GitHub Actions / Test / Build, test and publish

In method 'GetTypeRange', replace the call to 'ToLowerInvariant' with 'ToUpperInvariant' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1308)
{
"int32" => (int.MinValue, int.MaxValue),
_ => (long.MinValue, long.MaxValue),
Expand Down Expand Up @@ -217,14 +217,14 @@

if (schemeType == SecuritySchemeType.Http)
{
schemeName = $"http_{namePart.ToLowerInvariant()}";

Check warning on line 220 in src/libs/AutoSDK/Extensions/OpenApiExtensions.cs

View workflow job for this annotation

GitHub Actions / Test / Build, test and publish

In method 'InjectSecuritySchemes', replace the call to 'ToLowerInvariant' with 'ToUpperInvariant' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1308)
securityScheme.Type = SecuritySchemeType.Http;
securityScheme.Scheme = namePart;
securityScheme.In = location;
}
else
{
schemeName = $"apikey_{namePart.ToLowerInvariant()}";

Check warning on line 227 in src/libs/AutoSDK/Extensions/OpenApiExtensions.cs

View workflow job for this annotation

GitHub Actions / Test / Build, test and publish

In method 'InjectSecuritySchemes', replace the call to 'ToLowerInvariant' with 'ToUpperInvariant' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1308)
securityScheme.Type = schemeType;
securityScheme.In = location;
securityScheme.Name = namePart;
Expand Down Expand Up @@ -769,29 +769,90 @@
{
operation = operation ?? throw new ArgumentNullException(nameof(operation));

// In Microsoft.OpenApi 3.0, extension values are IOpenApiExtension, not JsonNode directly
// Check for specific extension keys instead
if ((operation.Extensions?.TryGetValue("x-stage", out var stage) ?? false) &&
stage is JsonValue stageJsonValue && stageJsonValue.TryGetValue<string>(out var stageString))
if (TryGetExtensionString(operation.Extensions, "x-stage", out var stageString))
{
return stageString;
return NormalizeExperimentalStage(stageString);
}

if ((operation.Extensions?.TryGetValue("x-alpha", out var alpha) ?? false) &&
alpha is JsonValue alphaJsonValue && alphaJsonValue.TryGetValue<bool>(out var alphaBoolean) && alphaBoolean)
if (TryGetExtensionBoolean(operation.Extensions, "x-alpha"))
{
return "Alpha";
}

if ((operation.Extensions?.TryGetValue("x-beta", out var beta) ?? false) &&
beta is JsonValue betaJsonValue && betaJsonValue.TryGetValue<bool>(out var betaBoolean) && betaBoolean)
if (TryGetExtensionBoolean(operation.Extensions, "x-beta"))
{
return "Beta";
}

if (TryGetAvailability(operation.Extensions, out var availability))
{
return NormalizeExperimentalStage(availability);
}
Comment on lines +787 to +790
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix generally-available normalization mismatch (currently treated as experimental).

TryGetAvailability normalizes to GenerallyAvailable, but NormalizeExperimentalStage only suppresses GENERALLY-AVAILABLE (hyphenated). That returns a non-empty stage and can incorrectly mark GA operations as experimental.

💡 Proposed fix
 private static string NormalizeExperimentalStage(string? stage)
 {
     var normalized = stage?.Trim() ?? string.Empty;
     if (normalized.Length == 0)
     {
         return string.Empty;
     }

     return normalized.ToUpperInvariant() switch
     {
         "ALPHA" => "Alpha",
         "BETA" => "Beta",
         "EXPERIMENTAL" => "Experimental",
-        "GENERALLY-AVAILABLE" => string.Empty,
+        "GENERALLY-AVAILABLE" or "GENERALLYAVAILABLE" => string.Empty,
         "DEPRECATED" => string.Empty,
         _ => normalized,
     };
 }

Also applies to: 971-979, 990-996

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libs/AutoSDK/Extensions/OpenApiExtensions.cs` around lines 787 - 790,
TryGetAvailability returns a normalized availability value (e.g.,
GenerallyAvailable) but NormalizeExperimentalStage only checks for the
hyphenated string "GENERALLY-AVAILABLE", causing GA items to be treated as
experimental; update NormalizeExperimentalStage (and its callers) to do a
case-insensitive, punctuation-insensitive comparison (e.g., remove
hyphens/underscores and compare to "GENERALLYAVAILABLE") or accept the
normalized enum/value from TryGetAvailability directly and treat that as GA,
ensuring both "GenerallyAvailable" and "GENERALLY-AVAILABLE" map to
non-experimental; adjust the checks used in NormalizeExperimentalStage (and
similar logic at the other occurrences referenced) to use this normalized
comparison.


return GetExperimentalStageFromSummary(operation.Summary);
}

public static bool IsDeprecated(this OpenApiOperation operation)
{
operation = operation ?? throw new ArgumentNullException(nameof(operation));

return operation.Deprecated ||
TryGetAvailability(operation.Extensions, out var availability) &&
string.Equals(availability, "Deprecated", StringComparison.Ordinal);
}

public static bool IsDeprecated(this IOpenApiSchema schema)
{
schema = schema ?? throw new ArgumentNullException(nameof(schema));

return schema.Deprecated ||
TryGetAvailability(schema.Extensions, out var availability) &&
string.Equals(availability, "Deprecated", StringComparison.Ordinal);
}

public static string GetExperimentalStageFromSummary(this string? summary)
{
if (string.IsNullOrWhiteSpace(summary))
{
return string.Empty;
}

var trimmed = (summary ?? string.Empty).TrimStart();

Check warning on line 820 in src/libs/AutoSDK/Extensions/OpenApiExtensions.cs

View workflow job for this annotation

GitHub Actions / Test / Build, test and publish

'summary' is never 'null'. Remove or refactor the condition(s) to avoid dead code. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508)

foreach (var (prefix, stage) in ExperimentalStagePrefixes)
{
if (trimmed.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return stage;
}
Comment on lines +822 to +827
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use token-boundary checks for stage prefixes to avoid false positives.

Current StartsWith on plain prefixes (Alpha, Beta, Experimental) will match unrelated words (e.g., Alphabet), causing wrong stage detection/summary stripping.

💡 Proposed fix
 foreach (var (prefix, stage) in ExperimentalStagePrefixes)
 {
-    if (trimmed.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+    if (trimmed.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
+        (prefix.StartsWith("[", StringComparison.Ordinal) ||
+         trimmed.Length == prefix.Length ||
+         " \t:-_".Contains(trimmed[prefix.Length])))
     {
         return stage;
     }
 }
 foreach (var (prefix, _) in ExperimentalStagePrefixes)
 {
-    if (!trimmed.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+    if (!trimmed.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) ||
+        (!prefix.StartsWith("[", StringComparison.Ordinal) &&
+         trimmed.Length > prefix.Length &&
+         !" \t:-_".Contains(trimmed[prefix.Length])))
     {
         continue;
     }

     var remainder = trimmed.Substring(prefix.Length).TrimStart(' ', '\t', ':', '-', '_');
     return string.IsNullOrWhiteSpace(remainder) ? trimmed : remainder;
 }

Also applies to: 842-850, 953-961

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libs/AutoSDK/Extensions/OpenApiExtensions.cs` around lines 822 - 827, The
current detection using trimmed.StartsWith(prefix, ...) on
ExperimentalStagePrefixes can match inside words (e.g., "Alphabet"), so update
the check in OpenApiExtensions.cs where you iterate ExperimentalStagePrefixes to
ensure the prefix is a standalone token: after matching the prefix, verify
either the prefix reaches the end of the string or the next character is a
non-letter/non-digit (or use a word-boundary regex) before returning stage;
apply the same token-boundary logic to the other similar checks (the other loops
using ExperimentalStagePrefixes and StartsWith at the other occurrences) to
avoid false positives.

}

return string.Empty;
}

public static string StripExperimentalStagePrefix(this string? summary)
{
if (string.IsNullOrWhiteSpace(summary))
{
return string.Empty;
}

var trimmed = (summary ?? string.Empty).Trim();

Check warning on line 840 in src/libs/AutoSDK/Extensions/OpenApiExtensions.cs

View workflow job for this annotation

GitHub Actions / Test / Build, test and publish

'summary' is never 'null'. Remove or refactor the condition(s) to avoid dead code. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508)

foreach (var (prefix, _) in ExperimentalStagePrefixes)
{
if (!trimmed.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
continue;
}

var remainder = trimmed.Substring(prefix.Length).TrimStart(' ', '\t', ':', '-', '_');
return string.IsNullOrWhiteSpace(remainder) ? trimmed : remainder;
}

return trimmed;
}

public static string ClearForXml(this string text)
{
text = text ?? throw new ArgumentNullException(nameof(text));
Expand Down Expand Up @@ -888,6 +949,142 @@

return @enum;
}

private static readonly (string Prefix, string Stage)[] ExperimentalStagePrefixes =
[
("[Alpha]", "Alpha"),
("[Beta]", "Beta"),
("[Experimental]", "Experimental"),
("Alpha", "Alpha"),
("Beta", "Beta"),
("Experimental", "Experimental"),
];

private static string NormalizeExperimentalStage(string? stage)
{
var normalized = stage?.Trim() ?? string.Empty;
if (normalized.Length == 0)
{
return string.Empty;
}

return normalized.ToUpperInvariant() switch
{
"ALPHA" => "Alpha",
"BETA" => "Beta",
"EXPERIMENTAL" => "Experimental",
"GENERALLY-AVAILABLE" => string.Empty,
"DEPRECATED" => string.Empty,
_ => normalized,
};
}

private static string NormalizeAvailability(string? availability)
{
var normalized = availability?.Trim() ?? string.Empty;
if (normalized.Length == 0)
{
return string.Empty;
}

return normalized.ToUpperInvariant() switch
{
"ALPHA" => "Alpha",
"BETA" => "Beta",
"DEPRECATED" => "Deprecated",
"GENERALLY-AVAILABLE" => "GenerallyAvailable",
_ => normalized,
};
}

private static bool TryGetAvailability(IDictionary<string, IOpenApiExtension>? extensions, out string availability)
{
availability = string.Empty;

if (!TryGetExtensionString(extensions, "x-fern-availability", out var rawAvailability))
{
return false;
}

availability = NormalizeAvailability(rawAvailability);
return !string.IsNullOrWhiteSpace(availability);
}

private static bool TryGetExtensionString(
IDictionary<string, IOpenApiExtension>? extensions,
string name,
out string value)
{
value = string.Empty;

if (!(extensions?.TryGetValue(name, out var extension) ?? false))
{
return false;
}

if (TryGetJsonString(extension, out var stringValue))
{
value = stringValue;
return true;
}

return false;
}

private static bool TryGetExtensionBoolean(
IDictionary<string, IOpenApiExtension>? extensions,
string name)
{
if (!(extensions?.TryGetValue(name, out var extension) ?? false))
{
return false;
}

return TryGetJsonBoolean(extension, out var booleanValue) && booleanValue;
}

private static bool TryGetJsonString(IOpenApiExtension extension, out string value)
{
value = string.Empty;

var node = extension switch
{
JsonValue jsonValue => jsonValue,
JsonNodeExtension jsonNodeExtension => jsonNodeExtension.Node,
_ => null,
};

if (node is JsonValue stringNode &&
stringNode.TryGetValue<string>(out var stringValue) &&
!string.IsNullOrWhiteSpace(stringValue))
{
value = stringValue;
return true;
}

return false;
}

private static bool TryGetJsonBoolean(IOpenApiExtension extension, out bool value)
{
value = false;

var node = extension switch
{
JsonValue jsonValue => jsonValue,
JsonNodeExtension jsonNodeExtension => jsonNodeExtension.Node,
_ => null,
};

if (node is JsonValue booleanNode &&
booleanNode.TryGetValue<bool>(out var booleanValue))
{
value = booleanValue;
return true;
}

return false;
}

public static Dictionary<string, PropertyData> ComputeEnum(
this IList<JsonNode> @enum,
Expand Down Expand Up @@ -994,4 +1191,4 @@
.Select(x => x.Value.OperationId!)
.ToArray();
}
}
}
2 changes: 1 addition & 1 deletion src/libs/AutoSDK/Models/EndPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public static EndPoint FromSchema(OperationContext operation, string? preferredM
operation.MethodName.FirstWord().ToLowerInvariant(),
Settings: operation.Settings,
GlobalSettings: operation.GlobalSettings,
IsDeprecated: operation.Operation.Deprecated,
IsDeprecated: operation.Operation.IsDeprecated(),
ExperimentalStage: operation.Operation.GetExperimentalStage(),
RequestType: requestType ?? TypeData.Default);

Expand Down
4 changes: 2 additions & 2 deletions src/libs/AutoSDK/Models/MethodParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public static MethodParameter FromSchemaContext(SchemaContext context)
Style: parameter.Style,
Explode: parameter.Explode,
Settings: context.Settings,
IsDeprecated: context.Schema.Deprecated,
IsDeprecated: context.Schema.IsDeprecated(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve parameter-level deprecation when building MethodParameter.

This currently reads only schema deprecation and can miss OpenApiParameter.Deprecated.

💡 Proposed fix
-            IsDeprecated: context.Schema.IsDeprecated(),
+            IsDeprecated: parameter.Deprecated || context.Schema.IsDeprecated(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
IsDeprecated: context.Schema.IsDeprecated(),
IsDeprecated: parameter.Deprecated || context.Schema.IsDeprecated(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libs/AutoSDK/Models/MethodParameter.cs` at line 97, The current
MethodParameter construction only uses context.Schema.IsDeprecated() and
therefore misses parameter-level deprecation flags; update the IsDeprecated
assignment in the MethodParameter builder to consider both the
OpenApiParameter.Deprecated flag and the schema deprecation (e.g., check
context.Parameter?.Deprecated or equivalent and OR it with
context.Schema.IsDeprecated()), so MethodParameter.IsDeprecated reflects either
source of deprecation.

DefaultValue: context.GetDefaultValue(),
Summary: context.Schema.GetSummary(),
Description:
Expand Down Expand Up @@ -228,4 +228,4 @@ public string ArgumentName
ProducesDeprecationWarning
? "#pragma warning disable CS0618 // Type or member is obsolete"
: " ";
}
}
4 changes: 2 additions & 2 deletions src/libs/AutoSDK/Models/ModelData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public static ModelData FromSchemaContext(
: [],
Summary: context.Schema.GetSummary(),
Description: context.Schema.Description ?? string.Empty,
IsDeprecated: context.Schema.Deprecated,
IsDeprecated: context.Schema.IsDeprecated(),
BaseClass: context.IsDerivedClass
? context.BaseClassContext.Id
: string.Empty,
Expand Down Expand Up @@ -96,4 +96,4 @@ public static ModelData FromSchemaContext(
};

public string FileNameWithoutExtension => $"{Namespace}.Models.{ExternalClassName}";
}
}
4 changes: 2 additions & 2 deletions src/libs/AutoSDK/Models/PropertyData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public static PropertyData FromSchemaContext(SchemaContext context)
IsWriteOnly: context.Schema.WriteOnly,
IsMultiPartFormDataFilename: false,
Settings: context.Settings,
IsDeprecated: context.Schema.Deprecated,
IsDeprecated: context.Schema.IsDeprecated(),
DefaultValue: context.Schema is { ReadOnly: true } && !type.CSharpTypeNullability
? "default!"
: context.GetDefaultValue(),
Expand Down Expand Up @@ -187,4 +187,4 @@ public static PropertyData FromSchemaContext(SchemaContext context)
.ReplaceIfEquals("void", "@void")
.ReplaceIfEquals("volatile", "@volatile")
.ReplaceIfEquals("while", "@while");
}
}
4 changes: 2 additions & 2 deletions src/libs/AutoSDK/Models/TypeData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ Default with
Namespace: type.StartsWith("global::System.", StringComparison.Ordinal)
? "System"
: context.Settings.Namespace,
IsDeprecated: context.Schema.Deprecated,
IsDeprecated: context.Schema.IsDeprecated(),
Settings: context.Settings);
}

Expand Down Expand Up @@ -373,4 +373,4 @@ public static bool GetCSharpNullability(
context.Schema.IsNullableAnyOf() || // anyOf: [X, {type: null}] is nullable
!context.IsRequired && additionalContext?.IsRequired != true;
}
}
}
1 change: 1 addition & 0 deletions src/libs/AutoSDK/Naming/Methods/SummaryGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class SummaryGenerator : IMethodNameGenerator
operation = operation ?? throw new ArgumentNullException(nameof(operation));

var methodName = operation.Operation.Summary?
.StripExperimentalStagePrefix()
.Replace("'", string.Empty)
.Replace("’", string.Empty)
.ToPropertyName()
Expand Down
2 changes: 1 addition & 1 deletion src/libs/AutoSDK/Sources/Data.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public static Models.Data Prepare(
.Where(operation =>
{
if (settings.ExcludeDeprecatedOperations &&
operation.Operation.Deprecated)
operation.Operation.IsDeprecated())
{
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion src/libs/AutoSDK/Sources/Sources.Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public static string GenerateHttpRequest(OperationContext operation)
var summary = !string.IsNullOrWhiteSpace(op.Summary)
? op.Summary
: op.Description;
var deprecated = op.Deprecated ? "[DEPRECATED] " : "";
var deprecated = op.IsDeprecated() ? "[DEPRECATED] " : "";
var title = !string.IsNullOrWhiteSpace(summary)
? string.Concat(deprecated, summary)
: string.Concat(deprecated, method, " ", operation.OperationPath);
Expand Down
Loading
Loading