Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ bool ValidateMinValue(int value)
/// Converts schedule definition into cron expression
/// </summary>
/// <param name="scheduleDefinition">Schedule definition</param>
/// <returns>The cron expression<returns>
/// <returns>The cron expression</returns>
public static string ScheduleDefinitionToCron(ScheduleDefinition scheduleDefinition)
{
var second = ConvertToCronString(scheduleDefinition.second);
Expand Down
4 changes: 3 additions & 1 deletion cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@
/// Deserialize JSON into a concrete type. Unlike Unity JsonUtility, this supports
/// custom semantic wrapper types (ex: types implementing IBeamSemanticType<T>)
/// even when JSON contains only the primitive token (string/number).
/// </summary>

Check warning on line 85 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / testSourceGen (10.0.x)

XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'T'.'

Check warning on line 85 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / baseNuget (10.0.x)

XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'T'.'

Check warning on line 85 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / testMicroserviceBaseImage (10.0.x)

XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'T'.'

Check warning on line 85 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / testCli (10.0.x, ubuntu-latest)

XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'T'.'

Check warning on line 85 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / testCli (10.0.x, ubuntu-latest)

XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'T'.'

Check warning on line 85 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / Build for StandaloneOSX

XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'T'.'

Check warning on line 85 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / Tests 6000.3.0f1 editmode

XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'T'.'

Check warning on line 85 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / Tests 6000.0.37f1 playmode

XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'T'.'

Check warning on line 85 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / Tests 6000.0.37f1 editmode

XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'T'.'

Check warning on line 85 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / Tests 6000.2.8f1 editmode

XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'T'.'
public static T Deserialize<T>(string json)

Check warning on line 86 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / testSourceGen (10.0.x)

XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'

Check warning on line 86 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / baseNuget (10.0.x)

XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'

Check warning on line 86 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / testMicroserviceBaseImage (10.0.x)

XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'

Check warning on line 86 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / testCli (10.0.x, ubuntu-latest)

XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'

Check warning on line 86 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / testCli (10.0.x, ubuntu-latest)

XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'

Check warning on line 86 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / Build for StandaloneOSX

XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'

Check warning on line 86 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / Tests 6000.3.0f1 editmode

XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'

Check warning on line 86 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / Tests 6000.0.37f1 playmode

XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'

Check warning on line 86 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / Tests 6000.0.37f1 editmode

XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'

Check warning on line 86 in cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs

View workflow job for this annotation

GitHub Actions / Tests 6000.2.8f1 editmode

XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'
{
return (T)Deserialize(json, typeof(T));
}
Expand Down Expand Up @@ -1082,6 +1082,7 @@
private static class ObjectMapper
{
private static readonly Type SerializeFieldType = typeof(SerializeField);
private static readonly Type CompilerGeneratedType = typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute);

// NOTE: We cannot reference Beamable.Common.Semantics directly from this folder.
// We detect semantic types via reflection by interface full name instead
Expand Down Expand Up @@ -1182,7 +1183,8 @@
var field = fields[i];

var hasSerializeField = field.GetCustomAttribute(SerializeFieldType) != null;
var canBeSerialized = (hasSerializeField || !field.IsNotSerialized);
var isGeneratedByCompiler = field.GetCustomAttribute(CompilerGeneratedType) != null;
var canBeSerialized = (hasSerializeField || !field.IsNotSerialized) && !isGeneratedByCompiler;
if (!canBeSerialized) continue;

if (!objNode.TryGetValue(field.Name, out var rawValue)) continue;
Expand Down
1 change: 1 addition & 0 deletions cli/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Resolved issues in the token refresh flow where the CLI did not properly refresh, and persist the access token.
- Concurrency issue in `Promise` code that could lead to deadlock scenario in multi-threaded code
- Improve OpenAPI schema population

## [7.0.0] - 2026-02-19
### Added
Expand Down
10 changes: 8 additions & 2 deletions cli/cli/Commands/Config/DownloadOpenAPICommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,16 @@ public override async Task Handle(DownloadOpenAPICommandArgs args)
Log.Information(json);
return;
}


const string defaultFilename = "beam-oapi.json";
if (string.IsNullOrEmpty(args.OutputPath))
{
args.OutputPath = "beam-oapi.json";
args.OutputPath = defaultFilename;
}
// If OutputPath is an existing directory without a filename, append the default filename
if (Directory.Exists(args.OutputPath))
{
args.OutputPath = Path.Combine(args.OutputPath, defaultFilename);
}
var dir = Path.GetDirectoryName(args.OutputPath);
if (!string.IsNullOrEmpty(dir))
Expand Down
196 changes: 171 additions & 25 deletions cli/cli/Services/SwaggerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Exceptions;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
using Microsoft.OpenApi.Writers;
Expand Down Expand Up @@ -433,19 +434,45 @@ public OpenApiDocument GetCombinedDocument(List<NamedOpenApiSchema> apis)
var combinedDocument = new OpenApiDocument(apis.FirstOrDefault()!.Document);
foreach (var documentResult in apis.Skip(1))
{
if (documentResult.Document.Info.Extensions != null)
{
foreach (var extension in documentResult.Document.Info.Extensions)
{
if (!combinedDocument.Info.Extensions.ContainsKey(extension.Key))
{
combinedDocument.Info.Extensions.Add(extension);
}
}
}
foreach (var path in documentResult.Document.Paths)
{
if(combinedDocument.Paths.ContainsKey(path.Key))
{
var existingPath = combinedDocument.Paths[path.Key];
var areEqual = ArePathItemsEqual(existingPath, path.Value, out var pathDifferences);
if (areEqual)
{
Log.Verbose($"Skipping duplicate path [{path.Key}] - path items are identical");
}
else
{
Log.Information($"Cannot merge path [{path.Key}] - different operations exist. Keeping original. Differences: {string.Join(", ", pathDifferences)}");
}
continue;
}
combinedDocument.Paths.Add(path.Key, path.Value);
}

foreach (var component in documentResult.Document.Components.Schemas)
{
if(combinedDocument.Components.Schemas.TryGetValue(component.Value.Reference.Id, out var schema))
{
if(NamedOpenApiSchema.AreEqual(schema, component.Value, out _))
if(NamedOpenApiSchema.AreEqual(schema, component.Value, out var schemaDifferences))
continue;

var mergedSchema = MergeSchemasWithExtensionMerge(schema, component.Value, schemaDifferences);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

Unused variable. The variable 'mergedSchema' is assigned the result of MergeSchemasWithExtensionMerge but is never used. The method modifies the original schema in-place (as seen in the implementation), but the assignment suggests the result should be used. Either remove the assignment or clarify the intent.

Suggested change
var mergedSchema = MergeSchemasWithExtensionMerge(schema, component.Value, schemaDifferences);
MergeSchemasWithExtensionMerge(schema, component.Value, schemaDifferences);

Copilot uses AI. Check for mistakes.
Log.Verbose($"Merged schema [{component.Value.Reference.Id}] with extension merge - schema differences: {string.Join(", ", schemaDifferences)}");
continue;
}

if (combinedDocument.Components.Schemas.ContainsKey(component.Value.Reference.Id))
Expand Down Expand Up @@ -524,38 +551,111 @@ public OpenApiDocument GetCombinedDocument(List<NamedOpenApiSchema> apis)

return combinedDocument;
}

public static async Task<(string url, string content)> GetOapiStringReader(
IAppContext context,
ISwaggerStreamDownloader downloader,
BeamableApiDescriptor api)


public static async Task<(string url, string content)> GetOapiStringReader(
IAppContext context,
ISwaggerStreamDownloader downloader,
BeamableApiDescriptor api)
{
switch (api.Location)
{
case BeamableApiLocation.Web:
{
var url = $"{context.Host}/{api.RelativeUrl}";
Log.Information("Downloading OAPI: {url}", url);
var stream = await downloader.GetStreamAsync(url);
var sr = new StreamReader(stream);
var content = await sr.ReadToEndAsync();
return new(url, content);
}
case BeamableApiLocation.Embedded:
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = "cli.openapi." + api.RelativeUrl;
Log.Information("Unpacking OAPI: {url}", resourceName);
using var stream = assembly.GetManifestResourceStream(resourceName);
using var reader = new StreamReader(stream!);
var content = await reader.ReadToEndAsync();
return (resourceName, content);
}
default:
throw new NotImplementedException();
}
}
private static bool ArePathItemsEqual(OpenApiPathItem a, OpenApiPathItem b, out List<string> differences)
{
switch (api.Location)
differences = new List<string>();

if (a.Operations.Count != b.Operations.Count)
{
case BeamableApiLocation.Web:
differences.Add($"operation count differs: {a.Operations.Count} vs {b.Operations.Count}");
}

var aMethods = new HashSet<OperationType>(a.Operations.Keys);
var bMethods = new HashSet<OperationType>(b.Operations.Keys);

if (!aMethods.SetEquals(bMethods))
{
differences.Add($"HTTP methods differ");
return false;
}

foreach (var method in aMethods)
{
var aOp = a.Operations[method];
var bOp = b.Operations[method];

if (aOp.RequestBody == null && bOp.RequestBody != null)
{
differences.Add($"[{method}] request body exists only in second document");
}
else if (aOp.RequestBody != null && bOp.RequestBody == null)
{
var url = $"{context.Host}/{api.RelativeUrl}";
Log.Information("Downloading OAPI: {url}", url);
var stream = await downloader.GetStreamAsync(url);
var sr = new StreamReader(stream);
var content = await sr.ReadToEndAsync();
return new(url, content);
differences.Add($"[{method}] request body exists only in first document");
}
case BeamableApiLocation.Embedded:
else if (aOp.RequestBody != null && bOp.RequestBody != null)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = "cli.openapi." + api.RelativeUrl;
Log.Information("Unpacking OAPI: {url}", resourceName);
using var stream = assembly.GetManifestResourceStream(resourceName);
using var reader = new StreamReader(stream!);
var content = await reader.ReadToEndAsync();
return (resourceName, content);
var aContent = aOp.RequestBody.Content;
var bContent = bOp.RequestBody.Content;

if (aContent.Count != bContent.Count)
{
differences.Add($"[{method}] content type count differs");
}
}

if (aOp.Responses.Count != bOp.Responses.Count)
{
differences.Add($"[{method}] response count differs: {aOp.Responses.Count} vs {bOp.Responses.Count}");
}
default:
throw new NotImplementedException();
}

return differences.Count == 0;
}


private static OpenApiSchema MergeSchemasWithExtensionMerge(OpenApiSchema original, OpenApiSchema incoming, List<string> schemaDifferences)
{
var mergedExtensions = new Dictionary<string, IOpenApiExtension>(original.Extensions);

foreach (var ext in incoming.Extensions)
{
if (!mergedExtensions.ContainsKey(ext.Key))
{
mergedExtensions.Add(ext.Key, ext.Value);
Log.Verbose($"Added extension [{ext.Key}] to schema [{original.Reference?.Id ?? "inline"}]");
}
}

original.Extensions.Clear();
foreach (var ext in mergedExtensions)
{
original.Extensions.Add(ext.Key, ext.Value);
}

return original;
}

/// <summary>
/// Download a set of open api documents given the <see cref="openApiUrls"/>
/// </summary>
Expand Down Expand Up @@ -1748,6 +1848,52 @@ public static bool AreEqual(OpenApiSchema a, OpenApiSchema b, out List<string> d
differences.Add("a has properties, but b doesn't");
}

if (!AreExtensionsEqual(a.Extensions, b.Extensions, out var extDifferences))
{
differences.AddRange(extDifferences);
}

return differences.Count == 0;
}

private static bool AreExtensionsEqual(
IDictionary<string, IOpenApiExtension> a,
IDictionary<string, IOpenApiExtension> b,
out List<string> differences)
{
differences = new List<string>();

if (a.Count != b.Count)
{
differences.Add($"extension count differs: {a.Count} vs {b.Count}");
}

var allKeys = new HashSet<string>(a.Keys);
allKeys.UnionWith(b.Keys);

foreach (var key in allKeys)
{
var aHasKey = a.TryGetValue(key, out var aExt);
var bHasKey = b.TryGetValue(key, out var bExt);

if (!aHasKey && bHasKey)
{
differences.Add($"extension [{key}] missing in first schema");
}
else if (aHasKey && !bHasKey)
{
differences.Add($"extension [{key}] missing in second schema");
}
else if (aHasKey && bHasKey)
{
var aVal = aExt?.ToString() ?? "";
var bVal = bExt?.ToString() ?? "";
if (aVal != bVal)
{
differences.Add($"extension [{key}] values differ: [{aVal}] vs [{bVal}]");
}
}
}

return differences.Count == 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2508,7 +2508,12 @@ private static UnrealType GetUnrealTypeForField(out UnrealType nonOverridenType,
: UNREAL_MAP + $"<{UNREAL_STRING}, {dataType}>");
}
case ("object", _, _, _) when schema.Reference == null && !schema.AdditionalPropertiesAllowed:
throw new Exception("Object fields must either reference some other schema or must be a map/dictionary!");
if (parentDoc.Components.Schemas.TryGetValue(schema.Title, out var innerSchema) || parentDoc.Components.Schemas.TryGetValue( Uri.EscapeDataString(schema.Title), out innerSchema))
{
return GetUnrealTypeForField(out nonOverridenType, context, parentDoc, innerSchema, fieldDeclarationHandle, flags);
}
throw new Exception(
"Object fields must either reference some other schema or must be a map/dictionary!");
case ("array", _, _, _):
{
var isReference = schema.Items.Reference != null;
Expand Down
Loading
Loading