diff --git a/cli/beamable.common/Runtime/CronExpression/ExpressionParser.cs b/cli/beamable.common/Runtime/CronExpression/ExpressionParser.cs
index 8de9e489eb..0673b571e9 100644
--- a/cli/beamable.common/Runtime/CronExpression/ExpressionParser.cs
+++ b/cli/beamable.common/Runtime/CronExpression/ExpressionParser.cs
@@ -311,7 +311,7 @@ bool ValidateMinValue(int value)
/// Converts schedule definition into cron expression
///
/// Schedule definition
- /// The cron expression
+ /// The cron expression
public static string ScheduleDefinitionToCron(ScheduleDefinition scheduleDefinition)
{
var second = ConvertToCronString(scheduleDefinition.second);
diff --git a/cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs b/cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs
index bb2ebb6210..e2333a92e8 100644
--- a/cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs
+++ b/cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs
@@ -1082,6 +1082,7 @@ private static void SerializeOther(object value, StringBuilder builder)
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
@@ -1182,7 +1183,8 @@ public static object ConvertToType(object node, Type targetType)
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;
diff --git a/cli/cli/CHANGELOG.md b/cli/cli/CHANGELOG.md
index 8c4ae907b3..ee0a3b34e9 100644
--- a/cli/cli/CHANGELOG.md
+++ b/cli/cli/CHANGELOG.md
@@ -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
diff --git a/cli/cli/Commands/Config/DownloadOpenAPICommand.cs b/cli/cli/Commands/Config/DownloadOpenAPICommand.cs
index 1fde78d91d..267fc0e9b6 100644
--- a/cli/cli/Commands/Config/DownloadOpenAPICommand.cs
+++ b/cli/cli/Commands/Config/DownloadOpenAPICommand.cs
@@ -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))
diff --git a/cli/cli/Services/SwaggerService.cs b/cli/cli/Services/SwaggerService.cs
index 281b270fa7..87a2d53ed8 100644
--- a/cli/cli/Services/SwaggerService.cs
+++ b/cli/cli/Services/SwaggerService.cs
@@ -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;
@@ -433,10 +434,32 @@ public OpenApiDocument GetCombinedDocument(List 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);
}
@@ -444,8 +467,12 @@ public OpenApiDocument GetCombinedDocument(List apis)
{
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);
+ 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))
@@ -524,38 +551,111 @@ public OpenApiDocument GetCombinedDocument(List 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 differences)
{
- switch (api.Location)
+ differences = new List();
+
+ 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(a.Operations.Keys);
+ var bMethods = new HashSet(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 schemaDifferences)
+ {
+ var mergedExtensions = new Dictionary(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;
+ }
+
///
/// Download a set of open api documents given the
///
@@ -1748,6 +1848,52 @@ public static bool AreEqual(OpenApiSchema a, OpenApiSchema b, out List 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 a,
+ IDictionary b,
+ out List differences)
+ {
+ differences = new List();
+
+ if (a.Count != b.Count)
+ {
+ differences.Add($"extension count differs: {a.Count} vs {b.Count}");
+ }
+
+ var allKeys = new HashSet(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;
}
diff --git a/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs b/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs
index 09ba6cb6cd..fc3ccbd95e 100644
--- a/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs
+++ b/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs
@@ -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;
diff --git a/cli/tests/ExtraTests.cs b/cli/tests/ExtraTests.cs
new file mode 100644
index 0000000000..bc3e0a4c28
--- /dev/null
+++ b/cli/tests/ExtraTests.cs
@@ -0,0 +1,705 @@
+using Beamable;
+using System;
+using Beamable.Common.Content;
+using System.Collections.Generic;
+using Beamable.Api.Autogenerated.Models;
+using Beamable.Common;
+using Beamable.Common.Dependencies;
+using Beamable.Server;
+using Beamable.Tooling.Common.OpenAPI;
+using cli;
+using cli.Unreal;
+using Microsoft.Extensions.Logging;
+using Microsoft.OpenApi;
+using Microsoft.OpenApi.Extensions;
+using Microsoft.OpenApi.Models;
+using NUnit.Framework;
+using System.Linq;
+using System.Threading.Tasks;
+using tests;
+using Unity.Beamable.Customer.Common;
+using UnityEngine;
+using ZLogger;
+
+namespace Unity.Beamable.Customer.Common
+{
+ [Serializable]
+ public class ItemReward
+ {
+ public string contentID;
+ public long instanceID = 0;
+ public long amount = 0;
+ public Dictionary properties;
+ }
+
+ [Serializable]
+ public class ListSubtype : List { }
+
+ [Serializable]
+ public class DictSubtype : Dictionary { }
+
+ [Serializable]
+ public class ContentObjectSubType : ContentObject { }
+
+ [Serializable]
+ public class SerializedClass {}
+
+ [Serializable]
+ public struct SerializedStruct {}
+
+ public class NonSerializedClass {}
+
+ public struct NonSerializedStruct {}
+
+
+ [Serializable]
+ public class ValidClass_NonBeamGenerate
+ {
+ public InvalidClass_MissingBeamGenerate MissingBeamGenerate;
+ public InvalidClass_MissingSerializable MissingSerializable;
+ public InvalidClass_MissingBeamGenerateAndSerializable MissingBeamGenerateAndSerializable;
+ public InvalidEnum_MissingSerializable EnumMissingSerializable;
+ }
+
+ [Serializable, BeamGenerateSchema]
+ public class ValidClass_BeamGenerate
+ {
+ public InvalidClass_MissingBeamGenerate MissingBeamGenerate;
+ public InvalidClass_MissingSerializable MissingSerializable;
+ public InvalidClass_MissingBeamGenerateAndSerializable MissingBeamGenerateAndSerializable;
+ public InvalidEnum_MissingSerializable EnumMissingSerializable;
+ }
+
+ [BeamGenerateSchema]
+ public class InvalidClass_MissingSerializable {}
+
+ [Serializable]
+ public class InvalidClass_MissingBeamGenerate {}
+
+ public class InvalidClass_MissingBeamGenerateAndSerializable {}
+
+ public enum InvalidEnum_MissingSerializable { None = 0}
+
+ [Serializable]
+ public class ValidClass_WarningProperties
+ {
+ public int Count { get; set; }
+ }
+
+ [Serializable]
+ public class ValidClass_InvalidFields
+ {
+ private ListSubtype _listSubtype;
+ private DictSubtype _dictSubtype;
+ private Dictionary _invalidDict;
+ private ContentObject _contentObject;
+ private ContentObjectSubType _contentObjectSubType;
+ }
+}
+namespace Beamable.SourceGenTest
+{
+ ///
+ /// this class is not meant to be used. It's sole purpose is to stand in
+ /// when something in the outer class needs to access a method with nameof()
+ ///
+ [FederationId("dummyThirdParty")]
+ class DummyThirdParty : IFederationId
+ {
+ public string UniqueName => "__temp__";
+ }
+ [Microservice("SourceGenTest")]
+ public partial class SourceGenTest : Microservice, IFederatedLogin, IFederatedPlayerInit, IFederatedGameServer
+ {
+ private static string StaticStringValue = "Hello World";
+
+ [ClientCallable]
+ public short Test_ReturnType_Short()
+ {
+ return short.MaxValue;
+ }
+
+ [ClientCallable]
+ public int Test_ReturnType_Int()
+ {
+ return int.MaxValue;
+ }
+
+ [ClientCallable]
+ public long Test_ReturnType_Long()
+ {
+ return long.MaxValue;
+ }
+
+ [ClientCallable]
+ public float Test_ReturnType_Float()
+ {
+ return float.MaxValue;
+ }
+
+ [ClientCallable]
+ public double Test_ReturnType_Double()
+ {
+ return double.MaxValue;
+ }
+
+ [ClientCallable]
+ public char Test_ReturnType_Char()
+ {
+ return 'A';
+ }
+
+ [ClientCallable]
+ public string Test_ReturnType_String()
+ {
+ return "TestString";
+ }
+
+ [ClientCallable]
+ public byte Test_ReturnType_Byte()
+ {
+ return byte.MaxValue;
+ }
+
+ [ClientCallable]
+ public uint Test_ReturnType_UInt()
+ {
+ return uint.MaxValue;
+ }
+
+ [ClientCallable]
+ public sbyte Test_ReturnType_SByte()
+ {
+ return sbyte.MaxValue;
+ }
+
+ [ClientCallable]
+ public ulong Test_ReturnType_ULong()
+ {
+ return ulong.MaxValue;
+ }
+
+ [ClientCallable]
+ public ushort Test_ReturnType_UShort()
+ {
+ return ushort.MaxValue;
+ }
+
+ [ClientCallable]
+ public DateTime Test_ReturnType_DateTime()
+ {
+ return DateTime.Now;
+ }
+
+ [ClientCallable]
+ public Guid Test_ReturnType_Guid()
+ {
+ return Guid.NewGuid();
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_Short_Async()
+ {
+ await Task.Delay(1);
+ return short.MaxValue;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_Int_Async()
+ {
+ await Task.Delay(1);
+ return int.MaxValue;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_Long_Async()
+ {
+ await Task.Delay(1);
+ return long.MaxValue;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_Float_Async()
+ {
+ await Task.Delay(1);
+ return float.MaxValue;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_Double_Async()
+ {
+ await Task.Delay(1);
+ return double.MaxValue;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_Char_Async()
+ {
+ await Task.Delay(1);
+ return 'A';
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_String_Async()
+ {
+ await Task.Delay(1);
+ return "TestString";
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_Byte_Async()
+ {
+ await Task.Delay(1);
+ return byte.MaxValue;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_UInt_Async()
+ {
+ await Task.Delay(1);
+ return uint.MaxValue;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_SByte_Async()
+ {
+ await Task.Delay(1);
+ return sbyte.MaxValue;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_ULong_Async()
+ {
+ await Task.Delay(1);
+ return ulong.MaxValue;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_UShort_Async()
+ {
+ await Task.Delay(1);
+ return ushort.MaxValue;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_DateTime_Async()
+ {
+ await Task.Delay(1);
+ return DateTime.Now;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_Guid_Async()
+ {
+ await Task.Delay(1);
+ return Guid.NewGuid();
+ }
+
+ [ClientCallable]
+ public ItemReward[] Test_ReturnType_ItemRewardArray()
+ {
+ return new ItemReward[]{};
+ }
+
+ [ClientCallable]
+ public List Test_ReturnType_ItemRewardList()
+ {
+ return new List();
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_ItemRewardArray_Async()
+ {
+ await Task.Delay(1);
+ return Array.Empty();
+ }
+
+ [ClientCallable]
+ public async Task> Test_ReturnType_ItemRewardList_Async()
+ {
+ await Task.Delay(1);
+ return new List();
+ }
+
+
+ [ClientCallable]
+ public ContentObject Test_ReturnType_ContentObject()
+ {
+ return ScriptableObject.CreateInstance();
+ }
+
+ [ClientCallable]
+ public ContentObject[] Test_ReturnType_ContentObjectArray()
+ {
+ return new ContentObject[]{};
+ }
+
+ [ClientCallable]
+ public List Test_ReturnType_ContentObjectList_Error()
+ {
+ return new List();
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_ContentObject_Async_Error()
+ {
+ await Task.Delay(1);
+ return ScriptableObject.CreateInstance();
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_ContentObjectArray_Async()
+ {
+ await Task.Delay(1);
+ return new ContentObject[]{};
+ }
+
+ [ClientCallable]
+ public async Task> Test_ReturnType_ContentObjectList_Async_Error()
+ {
+ await Task.Delay(1);
+ return new List();
+ }
+
+ [ClientCallable]
+ public ContentRef Test_ReturnType_ContentRef()
+ {
+ return new ContentRef();
+ }
+
+ [ClientCallable]
+ public ContentRef[] Test_ReturnType_ContentRefArray()
+ {
+ return new ContentRef[]{};
+ }
+
+ [ClientCallable]
+ public List> Test_ReturnType_ContentRefList()
+ {
+ return new List>();
+ }
+
+ [ClientCallable]
+ public async Task> Test_ReturnType_ContentRef_Async()
+ {
+ await Task.Delay(1);
+ return new ContentRef();
+ }
+
+ [ClientCallable]
+ public async Task[]> Test_ReturnType_ContentRefArray_Async()
+ {
+ await Task.Delay(1);
+ return new ContentRef[]{};
+ }
+
+ [ClientCallable]
+ public async Task>> Test_ReturnType_ContentRefList_Async()
+ {
+ await Task.Delay(1);
+ return new List>();
+ }
+
+ [ClientCallable]
+ public ContentObjectSubType Test_ReturnType_ContentObjectSubType_Error()
+ {
+ return ScriptableObject.CreateInstance();
+ }
+
+ [ClientCallable]
+ public ContentObjectSubType[] Test_ReturnType_ContentObjectSubTypeArray_Error()
+ {
+ return new ContentObjectSubType[]{};
+ }
+
+ [ClientCallable]
+ public List Test_ReturnType_ContentObjectSubTypeList_Error()
+ {
+ return new List();
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_ContentObjectSubType_Async_Error()
+ {
+ await Task.Delay(1);
+ return ScriptableObject.CreateInstance();
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_ContentObjectSubTypeArray_Async_Error()
+ {
+ await Task.Delay(1);
+ return new ContentObjectSubType[]{};
+ }
+
+ [ClientCallable]
+ public async Task> Test_ReturnType_ContentObjectSubTypeList_Async_Error()
+ {
+ await Task.Delay(1);
+ return new List();
+ }
+
+ [ClientCallable]
+ public Dictionary Test_ReturnType_DictionaryStringInt()
+ {
+ return new Dictionary();
+ }
+
+ [ClientCallable]
+ public Dictionary Test_ReturnType_DictionaryIntInt_Error()
+ {
+ return new Dictionary();
+ }
+
+ [ClientCallable]
+ public DictSubtype Test_ReturnType_DictSubtypeStringString_Error()
+ {
+ return new DictSubtype();
+ }
+
+ [ClientCallable]
+ public async Task> Test_ReturnType_DictionaryStringInt_Async_Error()
+ {
+ await Task.Delay(1);
+ return new Dictionary();
+ }
+
+ [ClientCallable]
+ public async Task> Test_ReturnType_DictionaryIntInt_Async_Error()
+ {
+ await Task.Delay(1);
+ return new Dictionary();
+ }
+
+ [ClientCallable]
+ public async Task> Test_ReturnType_DictSubtypeStringString_Async_Error()
+ {
+ await Task.Delay(1);
+ return new DictSubtype();
+ }
+
+ [ClientCallable]
+ public ListSubtype Test_ReturnType_ListSubtypeString_Error()
+ {
+ return new ListSubtype();
+ }
+
+ [ClientCallable]
+ public async Task> Test_ReturnType_ListSubtypeString_Async_Error()
+ {
+ await Task.Delay(1);
+ return new ListSubtype();
+ }
+
+ [ClientCallable]
+ public SerializedClass Test_ReturnType_SerializedClass()
+ {
+ return new SerializedClass();
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_SerializedClass_Async()
+ {
+ await Task.Delay(1);
+ return new SerializedClass();
+ }
+
+ [ClientCallable]
+ public SerializedStruct Test_ReturnType_SerializedStruct()
+ {
+ return default;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_SerializedStruct_Async()
+ {
+ await Task.Delay(1);
+ return default;
+ }
+
+ [ClientCallable]
+ public NonSerializedClass Test_ReturnType_NonSerializedClass_Error()
+ {
+ return new NonSerializedClass();
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_NonSerializedClass_Async_Error()
+ {
+ await Task.Delay(1);
+ return new NonSerializedClass();
+ }
+
+ [ClientCallable]
+ public NonSerializedStruct Test_ReturnType_NonSerializedStruct_Error()
+ {
+ return default;
+ }
+
+ [ClientCallable]
+ public async Task Test_ReturnType_NonSerializedStruct_Async_Error()
+ {
+ await Task.Delay(1);
+ return default;
+ }
+
+ [ClientCallable]
+ public void Test_Param_Short(short value) { }
+
+ [ClientCallable]
+ public void Test_Param_Int(int value) { }
+
+ [ClientCallable]
+ public void Test_Param_Long(long value) { }
+
+ [ClientCallable]
+ public void Test_Param_Float(float value) { }
+
+ [ClientCallable]
+ public void Test_Param_Double(double value) { }
+
+ [ClientCallable]
+ public void Test_Param_Char(char value) { }
+
+ [ClientCallable]
+ public void Test_Param_String(string value) { }
+
+ [ClientCallable]
+ public void Test_Param_Byte(byte value) { }
+
+ [ClientCallable]
+ public void Test_Param_SByte(sbyte value) { }
+
+ [ClientCallable]
+ public void Test_Param_UInt(uint value) { }
+
+ [ClientCallable]
+ public void Test_Param_ULong(ulong value) { }
+
+ [ClientCallable]
+ public void Test_Param_UShort(ushort value) { }
+
+ [ClientCallable]
+ public void Test_Param_DateTime(DateTime value) { }
+
+ [ClientCallable]
+ public void Test_Param_Guid(Guid value) { }
+
+ [ClientCallable]
+ public void Test_Param_ItemReward(ItemReward value) { }
+
+ [ClientCallable]
+ public void Test_Param_ItemRewardArray(ItemReward[] value) { }
+
+ [ClientCallable]
+ public void Test_Param_ItemRewardList(List value) { }
+
+ [ClientCallable]
+ public void Test_Param_ContentObject_Error(ContentObject value) { }
+
+ [ClientCallable]
+ public void Test_Param_ContentRef(ContentRef value) { }
+
+ [ClientCallable]
+ public void Test_Param_ContentObjectSubType_Error(ContentObjectSubType value) { }
+
+ [ClientCallable]
+ public void Test_Param_DictionaryStringInt(Dictionary value) { }
+
+ [ClientCallable]
+ public void Test_Param_DictionaryIntInt_Error(Dictionary value) { }
+
+ [ClientCallable]
+ public void Test_Param_DictSubtypeStringString_Error(DictSubtype value) { }
+
+ [ClientCallable]
+ public void Test_Param_ListSubtypeString_Error(ListSubtype value) { }
+
+ [ClientCallable]
+ public void Test_Param_SerializedClass(SerializedClass value) { }
+
+ [ClientCallable]
+ public void Test_Param_SerializedStruct(SerializedStruct value) { }
+
+ [ClientCallable]
+ public void Test_Param_NonSerializedClass_Error(NonSerializedClass value) { }
+
+ [ClientCallable]
+ public void Test_Param_NonSerializedStruct_Error(NonSerializedStruct value) { }
+
+ [ClientCallable]
+ public void Test_InvalidFieldsInClass(ValidClass_NonBeamGenerate value) { }
+
+ [ClientCallable]
+ public void Test_ClassWithProperties(ValidClass_WarningProperties value) { }
+
+ [ClientCallable]
+ public void Test_ClassWithInvalidFields(ValidClass_InvalidFields value) { }
+
+ public Promise Authenticate(string token, string challenge, string solution)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Promise CreatePlayer(Account account, Dictionary properties)
+ {
+ return new PlayerInitResult();
+ }
+
+ public async Promise CreateGameServer(Lobby lobby)
+ {
+ return new ServerInfo();
+ }
+
+ [ServerCallable]
+ public async Task CreateMatchResult(long userId, string lobbyId)
+ {
+ }
+
+ [Test]
+ public void TestUnreal2MicroserviceGen()
+ {
+ BeamableZLoggerProvider.Provider = new BeamableZLoggerProvider();
+ BeamableZLoggerProvider.LogContext.Value = LoggerFactory.Create(builder =>
+ {
+ builder.AddZLoggerConsole();
+ }).CreateLogger();
+
+ var gen = new ServiceDocGenerator();
+
+ var builder = new DependencyBuilder();
+
+ builder.AddSingleton();
+ builder.AddSingleton>();
+ builder.AddSingleton(new MicroserviceArgs());
+ var provider = builder.Build();
+ var doc = gen.Generate(provider);
+ string json = doc.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0);
+
+ Console.WriteLine(json);
+ UnrealSourceGenerator.exportMacro = "TROUBLESOMEPROJECT_API";
+ UnrealSourceGenerator.blueprintExportMacro = "TROUBLESOMEPROJECTBLUEPRINTNODES_API";
+ UnrealSourceGenerator.headerFileOutputPath = "/";
+ UnrealSourceGenerator.cppFileOutputPath = "/";
+ UnrealSourceGenerator.blueprintHeaderFileOutputPath = "/Public/";
+ UnrealSourceGenerator.blueprintCppFileOutputPath = "/Private/";
+ UnrealSourceGenerator.genType = UnrealSourceGenerator.GenerationType.Microservice;
+ var generator = new UnrealSourceGenerator();
+ var docs = new List() { doc };
+ var orderedSchemas = SwaggerService.ExtractAllSchemas(docs,
+ GenerateSdkConflictResolutionStrategy.RenameUncommonConflicts);
+ var ctx = new SwaggerService.DefaultGenerationContext
+ {
+ Documents = docs,
+ OrderedSchemas = orderedSchemas,
+ ReplacementTypes = new Dictionary()
+ };
+ var descriptors = generator.Generate(ctx);
+
+ Console.WriteLine("----- OUTPUT ----");
+ Console.WriteLine(string.Join("\n", descriptors.Select(d => $"{d.FileName}\n\n{d.Content}\n")));
+ Console.WriteLine($"Descriptor counts: {descriptors.Count}");
+ // Assert.AreEqual(15, descriptors.Count);
+ }
+ }
+
+}
diff --git a/client/Packages/com.beamable/Common/Runtime/CronExpression/ExpressionParser.cs b/client/Packages/com.beamable/Common/Runtime/CronExpression/ExpressionParser.cs
index f25d415f1e..35ff4716b4 100644
--- a/client/Packages/com.beamable/Common/Runtime/CronExpression/ExpressionParser.cs
+++ b/client/Packages/com.beamable/Common/Runtime/CronExpression/ExpressionParser.cs
@@ -314,7 +314,7 @@ bool ValidateMinValue(int value)
/// Converts schedule definition into cron expression
///
/// Schedule definition
- /// The cron expression
+ /// The cron expression
public static string ScheduleDefinitionToCron(ScheduleDefinition scheduleDefinition)
{
var second = ConvertToCronString(scheduleDefinition.second);
diff --git a/client/Packages/com.beamable/Common/Runtime/SmallerJSON/SmallerJSON.cs b/client/Packages/com.beamable/Common/Runtime/SmallerJSON/SmallerJSON.cs
index db4482f8f4..0ae2690a93 100644
--- a/client/Packages/com.beamable/Common/Runtime/SmallerJSON/SmallerJSON.cs
+++ b/client/Packages/com.beamable/Common/Runtime/SmallerJSON/SmallerJSON.cs
@@ -1,6 +1,6 @@
-// This file generated by a copy-operation from another project.
-// Edits to this file will be overwritten by the build process.
-
+// This file generated by a copy-operation from another project.
+// Edits to this file will be overwritten by the build process.
+
/*
* Simplified fork of MiniJSON optimized to minimize heap allocations
*
@@ -1186,7 +1186,8 @@ public static object ConvertToType(object node, Type targetType)
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;
diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs
index 035cb3bb8c..cb08954f11 100644
--- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs
+++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs
@@ -4,12 +4,14 @@
using Beamable.Server.Common;
using Beamable.Server.Common.XmlDocs;
using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using System.Collections;
using System.Reflection;
using Beamable.Common.Semantics;
using UnityEngine;
+using ZLogger;
using static Beamable.Common.Constants.Features.Services;
namespace Beamable.Tooling.Common.OpenAPI;
@@ -68,7 +70,7 @@ public bool IsIncluded()
// We don't want to emit generic types for documentation, because the generic aspect will be covered by the openAPI schema itself
// No arrays, dictionaries and collections because the OAPI has its own definitions for those (so we don't need to include them as Schemas)
- shouldEmit &= !Type.IsGenericType;
+ shouldEmit &= !Type.IsGenericType || Type.IsAssignableTo(typeof(IContentRef));
shouldEmit &= !Type.IsArray;
// Nullables are not supported, use optional instead
@@ -178,6 +180,51 @@ public static IEnumerable FindAllTypesForOAPI(IEnumerable
+ /// Generates a dictionary of schemas that can be used to populate the OpenAPI docs.
+ ///
+ /// The list of OAPI types whose underlying CLR types will be converted into OpenAPI schemas.
+ /// A set, passed by reference, that will be populated with additional CLR types referenced by the generated schemas but not yet converted.
+ /// Dictionary of OpenApiSchemas
+ public static Dictionary ToOpenApiSchemasDictionary(IList oapiTypes, ref HashSet requiredTypes)
+ {
+ var result = new Dictionary(oapiTypes.Count);
+ var toSkip = new HashSet(oapiTypes.Count);
+ for (int i = 0; i < oapiTypes.Count; i++)
+ {
+ if(toSkip.Contains(i))
+ continue;
+ var shouldGenerateClientCode = !oapiTypes[i].ShouldSkipClientCodeGeneration();
+
+ for (int j = i + 1; j < oapiTypes.Count; j++)
+ {
+ if (oapiTypes[j].Type != oapiTypes[i].Type)
+ {
+ continue;
+ }
+
+ toSkip.Add(j);
+
+ if (!shouldGenerateClientCode && !oapiTypes[j].ShouldSkipClientCodeGeneration())
+ {
+ shouldGenerateClientCode = true;
+ }
+ }
+
+ // We check because the same type can both be an extra type (declared via BeamGenerateSchema) AND be used in a signature; so we de-duplicate the concatenated lists.
+ // If all usages of this type (within a sub-graph of types starting from a ServiceMethod) is set to NOT generate the client code, we won't.
+ // Otherwise, even if just a single usage of the type wants the client code to be generated, we do generate it.
+ // That's what this thing does.
+ var type = oapiTypes[i].Type;
+ var key = GetQualifiedReferenceName(type);
+ var schema = Convert(type, ref requiredTypes);
+ schema.AddExtension(METHOD_SKIP_CLIENT_GENERATION_KEY, new OpenApiBoolean(shouldGenerateClientCode));
+ BeamableZLoggerProvider.LogContext.Value.ZLogDebug($"Adding Schema to Microservice OAPI docs. Type={type.FullName}, WillGenClient={shouldGenerateClientCode}");
+ result.Add(key, schema);
+ }
+ return result;
+ }
+
///
/// Traverses the type hierarchy starting from the specified type .
///
@@ -195,23 +242,65 @@ public static IEnumerable Traverse(Type runtimeType)
yield return runtimeType;
}
+ ///
+ /// Recursively adds OpenAPI schema definitions for all types in the specified
+ /// set and any additional dependent types
+ /// discovered during schema generation to the provided .
+ ///
+ ///
+ /// The whose
+ /// collection will be populated with schema definitions for the required types.
+ ///
+ ///
+ /// A set of types that must have schema definitions present in the OpenAPI document.
+ /// Any additional types identified as dependencies while generating schemas are also
+ /// processed until no new schema types are required.
+ ///
+ ///
+ /// true when all required types and their dependencies have been processed
+ /// and corresponding schema definitions have been added to the OpenAPI document.
+ ///
+ public static bool TryAddMissingSchemaTypes(ref OpenApiDocument oapiDoc, HashSet requiredTypes)
+ {
+ var newRequiredTypes = new HashSet();
+ foreach (Type requiredType in requiredTypes)
+ {
+ if (requiredType.IsBasicType())
+ {
+ continue;
+ }
+ var key = requiredType.GetSanitizedFullName();
+ if(oapiDoc.Components.Schemas.ContainsKey(key))
+ continue;
+ var schema = Convert(requiredType, ref newRequiredTypes);
+ oapiDoc.Components.Schemas.Add(key, schema);
+ }
+
+ if (newRequiredTypes.Count > 0)
+ {
+ return TryAddMissingSchemaTypes(ref oapiDoc, newRequiredTypes);
+ }
+
+ return true;
+ }
+
///
/// Converts a runtime type into an OpenAPI schema.
///
- public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool sanitizeGenericType = false)
+ public static OpenApiSchema Convert(Type runtimeType, ref HashSet requiredTypes, int depth = 1, bool sanitizeGenericType = false)
{
switch (runtimeType)
{
case { } x when x.IsAssignableTo(typeof(Optional)):
var instance = Activator.CreateInstance(runtimeType) as Optional;
- return Convert(instance.GetOptionalType(), depth - 1);
+ return Convert(instance.GetOptionalType(), ref requiredTypes,depth - 1, sanitizeGenericType);
case { } x when x.IsGenericType && x.GetGenericTypeDefinition() == typeof(Optional<>):
- return Convert(x.GetGenericArguments()[0], depth - 1);
+ return Convert(x.GetGenericArguments()[0], ref requiredTypes,depth - 1, sanitizeGenericType);
case { } x when x.IsGenericType && x.GetGenericTypeDefinition() == typeof(Nullable<>):
- return Convert(x.GetGenericArguments()[0], depth - 1);
+ return Convert(x.GetGenericArguments()[0], ref requiredTypes, depth - 1);
case { } x when x.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBeamSemanticType<>)):
var semanticType = x.GetInterfaces().First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBeamSemanticType<>));
- var semanticTypeSchema = Convert(semanticType.GetGenericArguments()[0], depth - 1);
+ var semanticTypeSchema = Convert(semanticType.GetGenericArguments()[0], ref requiredTypes, depth - 1);
if (Activator.CreateInstance(runtimeType) is IBeamSemanticType semanticTypeInstance)
{
semanticTypeSchema.Extensions[SCHEMA_SEMANTIC_TYPE_NAME_KEY] =
@@ -225,12 +314,21 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti
return new OpenApiSchema { Type = "number", Format = "float" };
case { } x when x == typeof(short):
- return new OpenApiSchema { Type = "integer", Format = "int16" };
+ return new OpenApiSchema { Type = "integer", Format = "int16", Minimum = short.MinValue, Maximum = short.MaxValue };
+ case { } x when x == typeof(ushort):
+ return new OpenApiSchema { Type = "integer", Format = "int16", Minimum = ushort.MinValue, Maximum = ushort.MaxValue };
case { } x when x == typeof(int):
return new OpenApiSchema { Type = "integer", Format = "int32" };
+ case { } x when x == typeof(uint):
+ return new OpenApiSchema { Type = "integer", Format = "int32", Minimum = uint.MinValue, Maximum = uint.MaxValue };
case { } x when x == typeof(long):
return new OpenApiSchema { Type = "integer", Format = "int64" };
+ case { } x when x == typeof(ulong):
+ return new OpenApiSchema { Type = "integer", Format = "int64", Minimum = ulong.MinValue, Maximum = ulong.MaxValue };
+ case { } x when x == typeof(short):
+ return new OpenApiSchema { Type = "integer", Format = "int32" };
+
case { } x when x == typeof(bool):
return new OpenApiSchema { Type = "boolean" };
case { } x when x == typeof(decimal):
@@ -238,19 +336,23 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti
case { } x when x == typeof(string):
return new OpenApiSchema { Type = "string" };
+ case { } x when x == typeof(char):
+ return new OpenApiSchema { Type = "string", MaxLength = 1, MinLength = 1};
case { } x when x == typeof(byte):
- return new OpenApiSchema { Type = "string", Format = "byte" };
+ return new OpenApiSchema { Type = "string", Format = "byte", Minimum = byte.MinValue, Maximum = byte.MaxValue};
+ case { } x when x == typeof(sbyte):
+ return new OpenApiSchema { Type = "string", Format = "byte", Minimum = sbyte.MinValue, Maximum = sbyte.MaxValue };
case { } x when x == typeof(Guid):
return new OpenApiSchema { Type = "string", Format = "uuid" };
// handle arrays
case Type x when x.IsArray:
var elemType = x.GetElementType();
- OpenApiSchema arrayOpenApiSchema = elemType is { IsGenericType: true } ? Convert(elemType, 1, true) : Convert(elemType, depth - 1);
+ OpenApiSchema arrayOpenApiSchema = elemType is { IsGenericType: true } ? Convert(elemType, ref requiredTypes, depth, true) : Convert(elemType, ref requiredTypes,depth - 1);
return new OpenApiSchema { Type = "array", Items = arrayOpenApiSchema };
case Type x when x.IsAssignableTo(typeof(IList)) && x.IsGenericType:
elemType = x.GetGenericArguments()[0];
- OpenApiSchema listOpenApiSchema = elemType is { IsGenericType: true } ? Convert(elemType, 1, true) : Convert(elemType, depth - 1);
+ OpenApiSchema listOpenApiSchema = elemType is { IsGenericType: true } ? Convert(elemType, ref requiredTypes, depth, true) : Convert(elemType, ref requiredTypes,depth - 1);
return new OpenApiSchema { Type = "array", Items = listOpenApiSchema };
// handle maps
@@ -259,21 +361,74 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti
{
Type = "object",
AdditionalPropertiesAllowed = true,
- AdditionalProperties = Convert(x.GetGenericArguments()[1], depth - 1),
+ AdditionalProperties = Convert(x.GetGenericArguments()[1], ref requiredTypes,depth - 1, sanitizeGenericType),
Extensions = new Dictionary
{
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_NAMESPACE] = new OpenApiString(runtimeType.Namespace),
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_NAME] = new OpenApiString(runtimeType.Name),
- [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_ASSEMBLY_QUALIFIED_NAME] = new OpenApiString(runtimeType.GetGenericQualifiedTypeName()),
+ [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_ASSEMBLY_QUALIFIED_NAME] = new OpenApiString(runtimeType.GetSanitizedFullName()),
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_OWNER_ASSEMBLY] = new OpenApiString(runtimeType.Assembly.GetName().Name),
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_OWNER_ASSEMBLY_VERSION] = new OpenApiString(runtimeType.Assembly.GetName().Version.ToString()),
- [MICROSERVICE_EXTENSION_BEAMABLE_FORCE_TYPE_NAME] = new OpenApiString(runtimeType.GetGenericSanitizedFullName())
+ [MICROSERVICE_EXTENSION_BEAMABLE_FORCE_TYPE_NAME] = new OpenApiString(runtimeType.GetSanitizedFullName())
+ }
+ };
+ case Type x when IsDictionary(x):
+ var das= GetDictionaryTypes(x);
+ return new OpenApiSchema
+ {
+ Type = "object",
+ AdditionalPropertiesAllowed = true,
+
+ AdditionalProperties = Convert(das.Value.ValueType, ref requiredTypes,depth - 1, sanitizeGenericType),
+ Extensions = new Dictionary
+ {
+ [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_NAMESPACE] = new OpenApiString(runtimeType.Namespace),
+ [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_NAME] = new OpenApiString(runtimeType.Name),
+ [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_ASSEMBLY_QUALIFIED_NAME] = new OpenApiString(runtimeType.GetSanitizedFullName()),
+ [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_OWNER_ASSEMBLY] = new OpenApiString(runtimeType.Assembly.GetName().Name),
+ [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_OWNER_ASSEMBLY_VERSION] = new OpenApiString(runtimeType.Assembly.GetName().Version.ToString()),
+ [MICROSERVICE_EXTENSION_BEAMABLE_FORCE_TYPE_NAME] = new OpenApiString(runtimeType.GetSanitizedFullName())
}
};
+ case Type x when x.IsAssignableTo(typeof(IContentRef)):
+ {
+ var c = DocsLoader.GetTypeComments(runtimeType);
+ string t = runtimeType.GetSanitizedFullName();
+ var idSchema = Convert(typeof(string), ref requiredTypes, 0);
+ idSchema.Description = "id of the content";
+ return new OpenApiSchema
+ {
+ Description = c.Summary,
+ Type = "object",
+ AdditionalPropertiesAllowed = false,
+ Title = t,
+ Properties = new Dictionary()
+ {
+ {"id", idSchema}
+ },
+ Required = new SortedSet { "id" },
+ Extensions = new Dictionary
+ {
+ [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_NAMESPACE] = new OpenApiString(runtimeType.Namespace),
+ [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_NAME] = new OpenApiString(t),
+ [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_ASSEMBLY_QUALIFIED_NAME] = new OpenApiString(sanitizeGenericType ? runtimeType.GetSanitizedFullName() : GetQualifiedReferenceName(runtimeType)),
+ [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_OWNER_ASSEMBLY] = new OpenApiString(runtimeType.Assembly.GetName().Name),
+ [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_OWNER_ASSEMBLY_VERSION] = new OpenApiString(runtimeType.Assembly.GetName().Version.ToString())
+ }
+ };
+ }
case Type _ when depth <= 0:
- return new OpenApiSchema { Type = "object", Reference = new OpenApiReference { Id = GetQualifiedReferenceName(runtimeType), Type = ReferenceType.Schema } };
+ if (!ServiceDocGenerator.IsEmptyResponseType(runtimeType))
+ {
+ requiredTypes.Add(runtimeType);
+ return new OpenApiSchema { Type = "object", Reference = new OpenApiReference { Id = GetQualifiedReferenceName(runtimeType), Type = ReferenceType.Schema } };
+ }
+ else
+ {
+ return new OpenApiSchema { };
+ }
case { IsEnum: true }:
var enumNames = Enum.GetNames(runtimeType);
@@ -295,7 +450,7 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti
var schema = new OpenApiSchema { };
var comments = DocsLoader.GetTypeComments(runtimeType);
- string typeName = sanitizeGenericType ? runtimeType.GetGenericSanitizedFullName() : runtimeType.Name;
+ string typeName = sanitizeGenericType ? runtimeType.GetSanitizedFullName() : runtimeType.Name;
schema.Description = comments.Summary;
schema.Properties = new Dictionary();
@@ -307,23 +462,30 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti
{
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_NAMESPACE] = new OpenApiString(runtimeType.Namespace),
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_NAME] = new OpenApiString(typeName),
- [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_ASSEMBLY_QUALIFIED_NAME] = new OpenApiString(sanitizeGenericType ? runtimeType.GetGenericQualifiedTypeName() : GetQualifiedReferenceName(runtimeType)),
+ [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_ASSEMBLY_QUALIFIED_NAME] = new OpenApiString(sanitizeGenericType ? runtimeType.GetSanitizedFullName() : GetQualifiedReferenceName(runtimeType)),
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_OWNER_ASSEMBLY] = new OpenApiString(runtimeType.Assembly.GetName().Name),
[MICROSERVICE_EXTENSION_BEAMABLE_TYPE_OWNER_ASSEMBLY_VERSION] = new OpenApiString(runtimeType.Assembly.GetName().Version.ToString())
};
+ if (runtimeType.GetCustomAttribute() is { } contentTypeAttribute)
+ {
+ schema.Extensions["x-beamable-content-type-name"] = new OpenApiString(contentTypeAttribute.TypeName);
+ }
if (sanitizeGenericType)
{
schema.Extensions[MICROSERVICE_EXTENSION_BEAMABLE_FORCE_TYPE_NAME] =
- new OpenApiString(runtimeType.GetGenericSanitizedFullName());
+ new OpenApiString(runtimeType.GetSanitizedFullName());
}
- if (depth == 0) return schema;
+ if (depth == 0) {
+ requiredTypes.Add(runtimeType);
+ return schema;
+ }
var members = UnityJsonContractResolver.GetSerializedFields(runtimeType);
foreach (var member in members)
{
var name = member.Name;
- var fieldSchema = Convert(member.FieldType, depth - 1);
+ var fieldSchema = Convert(member.FieldType,ref requiredTypes, depth - 1, sanitizeGenericType);
var comment = DocsLoader.GetMemberComments(member);
fieldSchema.Description = comment?.Summary;
@@ -354,6 +516,40 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti
///
public static string GetQualifiedReferenceName(Type runtimeType)
{
- return runtimeType.FullName.Replace("+", ".");
+ return Uri.EscapeDataString(runtimeType.GetSanitizedFullName());
+ }
+ static bool IsDictionary(Type type)
+ {
+ if (type == null) return false;
+
+ return type.GetInterfaces().Any(i =>
+ i.IsGenericType &&
+ i.GetGenericTypeDefinition() == typeof(IDictionary<,>))
+ || IsSubclassOfRawGeneric(typeof(Dictionary<,>), type);
+ }
+ static bool IsSubclassOfRawGeneric(Type generic, Type toCheck) {
+ while (toCheck != null && toCheck != typeof(object)) {
+ var cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck;
+ if (generic == cur) {
+ return true;
+ }
+ toCheck = toCheck.BaseType;
+ }
+ return false;
+ }
+ static (Type KeyType, Type ValueType)? GetDictionaryTypes(Type type)
+ {
+ // Look for the IDictionary interface
+ var dictionaryIntf = type.GetInterfaces()
+ .FirstOrDefault(i => i.IsGenericType &&
+ i.GetGenericTypeDefinition() == typeof(IDictionary<,>));
+
+ if (dictionaryIntf != null)
+ {
+ var args = dictionaryIntf.GetGenericArguments();
+ return (args[0], args[1]);
+ }
+
+ return null;
}
}
diff --git a/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs
index 3582581389..612b4a9999 100644
--- a/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs
+++ b/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs
@@ -197,30 +197,11 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r
// in any serializable type here that follows microservice serialization rules).
var allTypesFromRoutes = SchemaGenerator.FindAllTypesForOAPI(methods).ToList();
allTypesFromRoutes.AddRange(extraSchemas.Select(ex => new SchemaGenerator.OAPIType(null, ex)));
- foreach (var oapiType in allTypesFromRoutes)
+ var requiredTypes = new HashSet();
+ var routesFromSchemas = SchemaGenerator.ToOpenApiSchemasDictionary(allTypesFromRoutes, ref requiredTypes);
+ foreach (var (key, schema) in routesFromSchemas)
{
- // We check because the same type can both be an extra type (declared via BeamGenerateSchema) AND be used in a signature; so we de-duplicate the concatenated lists.
- // If all usages of this type (within a sub-graph of types starting from a ServiceMethod) is set to NOT generate the client code, we won't.
- // Otherwise, even if just a single usage of the type wants the client code to be generated, we do generate it.
- // That's what this thing does.
- var type = oapiType.Type;
- var key = SchemaGenerator.GetQualifiedReferenceName(type);
- bool shouldSkipCodeGen = oapiType.ShouldSkipClientCodeGeneration();
- if (doc.Components.Schemas.TryGetValue(key, out var existingSchema))
- {
- existingSchema.AddExtension(Constants.Features.Services.METHOD_SKIP_CLIENT_GENERATION_KEY, new OpenApiBoolean(shouldSkipCodeGen));
-
- BeamableZLoggerProvider.LogContext.Value.ZLogDebug($"Tried to add Schema more than once. Type={type.FullName}, SchemaKey={key}, WillSGenClient={!shouldSkipCodeGen}");
- }
- else
- {
- // Convert the type into a schema, then set this schema's client-code generation extension based on whether the OAPI type so our code-gen pipelines can decide whether to output it.
- var schema = SchemaGenerator.Convert(type);
- schema.AddExtension(Constants.Features.Services.METHOD_SKIP_CLIENT_GENERATION_KEY, new OpenApiBoolean(shouldSkipCodeGen));
-
- BeamableZLoggerProvider.LogContext.Value.ZLogDebug($"Adding Schema to Microservice OAPI docs. Type={type.FullName}, WillGenClient={!shouldSkipCodeGen}");
- doc.Components.Schemas.Add(key, schema);
- }
+ doc.Components.Schemas.Add(key, schema);
}
var methodsSkippedForClientCodeGen = new List();
@@ -233,13 +214,12 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r
var parameterNameToComment = comments.Parameters.ToDictionary(kvp => kvp.Name, kvp => kvp.Text);
var returnType = GetTypeFromPromiseOrTask(method.Method.ReturnType);
-
- OpenApiSchema openApiSchema = SchemaGenerator.Convert(returnType, 0);
-
+
+ OpenApiSchema openApiSchema = SchemaGenerator.Convert(returnType, ref requiredTypes, 0);
var returnJson = new OpenApiMediaType { Schema = openApiSchema };
if (openApiSchema.Reference != null && !doc.Components.Schemas.ContainsKey(openApiSchema.Reference.Id))
{
- returnJson.Extensions.Add(Constants.Features.Services.MICROSERVICE_EXTENSION_BEAMABLE_TYPE_ASSEMBLY_QUALIFIED_NAME, new OpenApiString(returnType.GetGenericSanitizedFullName()));
+ returnJson.Extensions.Add(Constants.Features.Services.MICROSERVICE_EXTENSION_BEAMABLE_TYPE_ASSEMBLY_QUALIFIED_NAME, new OpenApiString(returnType.GetSanitizedFullName()));
}
@@ -271,7 +251,7 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r
for (var i = 0; i < method.ParameterInfos.Count; i++)
{
Type parameterType = method.ParameterInfos[i].ParameterType;
- var parameterSchema = SchemaGenerator.Convert(parameterType, 0);
+ var parameterSchema = SchemaGenerator.Convert(parameterType, ref requiredTypes,0);
var parameterName = method.ParameterNames[i];
var parameterSource = method.ParameterSources[parameterName];
@@ -290,7 +270,7 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r
case ParameterSource.Body:
if (parameterSchema.Reference != null && !doc.Components.Schemas.ContainsKey(parameterSchema.Reference.Id))
{
- requestSchema.Properties[parameterName] = SchemaGenerator.Convert(parameterType, 1, true);
+ requestSchema.Properties[parameterName] = SchemaGenerator.Convert(parameterType, ref requiredTypes,1, true);
}
else
{
@@ -363,6 +343,8 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r
doc.Paths.Add("/" + method.Path, pathItem);
}
+ SchemaGenerator.TryAddMissingSchemaTypes(ref doc, requiredTypes);
+
var skippedForClientCodeGenArray = new OpenApiArray();
skippedForClientCodeGenArray.AddRange(methodsSkippedForClientCodeGen.Select(item => new OpenApiString(item)));
doc.Extensions.Add(Constants.Features.Services.MICROSERVICE_METHODS_TO_SKIP_GENERATION_KEY, skippedForClientCodeGenArray);
@@ -389,11 +371,11 @@ public OpenApiDocument Generate(Assembly ownerAssembly, IEnumerable schema
}
};
doc.Extensions = new Dictionary();
-
+ var requiredTypes = new HashSet();
// Generate the list of schemas
foreach (var type in schemas)
{
- var schema = SchemaGenerator.Convert(type);
+ var schema = SchemaGenerator.Convert(type, ref requiredTypes);
BeamableZLoggerProvider.LogContext.Value.ZLogDebug($"Adding Schema to Microservice OAPI docs. Type={type.FullName}");
doc.Components.Schemas.Add(SchemaGenerator.GetQualifiedReferenceName(type), schema);
}
diff --git a/microservice/beamable.tooling.common/SharedRuntime/MicroserviceRuntimeMetadata.cs b/microservice/beamable.tooling.common/SharedRuntime/MicroserviceRuntimeMetadata.cs
index e75fcf0a6a..d6fdef667e 100644
--- a/microservice/beamable.tooling.common/SharedRuntime/MicroserviceRuntimeMetadata.cs
+++ b/microservice/beamable.tooling.common/SharedRuntime/MicroserviceRuntimeMetadata.cs
@@ -3,25 +3,80 @@
namespace beamable.server
{
+ ///
+ /// Contains runtime metadata information for a Beamable microservice instance.
+ /// This class holds configuration and identification data used during microservice execution.
+ ///
[Serializable]
public class MicroserviceRuntimeMetadata
{
+ ///
+ /// The name of the microservice.
+ ///
public string serviceName;
+
+ ///
+ /// The version of the Beamable SDK being used.
+ ///
public string sdkVersion;
+
+ ///
+ /// The base build version of the Beamable SDK.
+ ///
public string sdkBaseBuildVersion;
+
+ ///
+ /// The execution version of the Beamable SDK.
+ ///
public string sdkExecutionVersion;
+
+ ///
+ /// Indicates whether the microservice should use legacy serialization methods.
+ ///
public bool useLegacySerialization;
+
+ ///
+ /// When true, disables all Beamable events for this microservice instance.
+ ///
public bool disableAllBeamableEvents;
+
+ ///
+ /// When true, enables eager loading of content at startup rather than lazy loading.
+ ///
public bool enableEagerContentLoading;
+
+ ///
+ /// Unique identifier for this specific microservice instance.
+ ///
public string instanceId;
+
+ ///
+ /// The routing key used for message routing to this microservice instance.
+ ///
public string routingKey;
+ ///
+ /// Collection of federated components associated with this microservice.
+ ///
public List federatedComponents = new List();
}
+ ///
+ /// Contains metadata information for a federated component within a microservice.
+ /// Federated components allow microservices to participate in distributed system architectures.
+ ///
+ [Serializable]
public class FederationComponentMetadata
{
+ ///
+ /// The namespace identifier for the federation component.
+ ///
public string federationNamespace;
+
+ ///
+ /// The type identifier for the federation component.
+ ///
public string federationType;
}
}
+
diff --git a/microservice/beamable.tooling.common/TypeExtensions.cs b/microservice/beamable.tooling.common/TypeExtensions.cs
index 1508b2ad6e..69c08ec0d0 100644
--- a/microservice/beamable.tooling.common/TypeExtensions.cs
+++ b/microservice/beamable.tooling.common/TypeExtensions.cs
@@ -20,42 +20,62 @@ public static bool IsAssignableTo(this Type type, Type other)
public static string GetSanitizedFullName(this Type type)
{
- if (type.FullName != null && type.FullName.Contains("`"))
- return type.FullName.Split('`')[0];
- return type.FullName;
- }
-
- public static string GetGenericSanitizedFullName(this Type type)
- {
- if (!type.IsGenericType)
+ if (type.IsGenericType)
{
- if(type.IsPrimitive && OpenApiUtils.OpenApiCSharpNameMap.TryGetValue(type.Name.ToLower(), out string shortName))
- return shortName;
- return type.FullName ?? type.Name;
- }
-
- string typeName = type.FullName?.Split('`')[0] ?? type.Name.Split('`')[0];
+ string typeName = type.FullName?.Split('`')[0] ?? type.Name.Split('`')[0];
- var genericArgs = type.GetGenericArguments();
- string args = string.Join(", ", genericArgs.Select(GetGenericSanitizedFullName));
+ var genericArgs = type.GetGenericArguments();
+ string args = string.Join(", ", genericArgs.Select(GetSanitizedFullName));
- return $"{typeName}<{args}>";
- }
+ return $"{typeName}<{args}>".Replace("+",".");
+ }
+ if(type.IsBasicType() && OpenApiUtils.OpenApiCSharpNameMap.TryGetValue(type.Name.ToLower(), out string shortName))
+ {
+ return shortName;
+ }
- public static string GetGenericQualifiedTypeName(this Type type)
- {
- if (!type.IsGenericType)
+ if (type.FullName == null)
{
- return type.FullName ?? type.Name;
+ return type.Name.Replace("+",".");
}
-
- string typeName = type.FullName?.Split('`')[0] ?? type.Name.Split('`')[0];
-
- var genericArgs = type.GetGenericArguments();
- string args = string.Join(", ", genericArgs.Select(GetGenericQualifiedTypeName));
- return $"{typeName}.{args}";
+ if(type.FullName.Contains("`"))
+ return type.FullName.Split('`')[0].Replace("+",".");
+ return type.FullName.Replace("+",".");
}
+ ///
+ /// Determines whether the specified represents a basic or primitive value type,
+ /// including common types such as , , ,
+ /// numeric types, and characters.
+ ///
+ /// The to evaluate.
+ ///
+ /// true if the given is considered a basic or primitive type; otherwise, false.
+ ///
+ public static bool IsBasicType(this Type t)
+ {
+ switch (Type.GetTypeCode(t))
+ {
+ case TypeCode.Boolean:
+ case TypeCode.Byte:
+ case TypeCode.SByte:
+ case TypeCode.Int16:
+ case TypeCode.UInt16:
+ case TypeCode.Int32:
+ case TypeCode.UInt32:
+ case TypeCode.Int64:
+ case TypeCode.UInt64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ case TypeCode.String:
+ case TypeCode.Char:
+ return true;
+
+ default:
+ return t.IsPrimitive;
+ }
+ }
}
diff --git a/microservice/microserviceTests/OpenAPITests/TypeTests.cs b/microservice/microserviceTests/OpenAPITests/TypeTests.cs
index 6967947076..69a68c119d 100644
--- a/microservice/microserviceTests/OpenAPITests/TypeTests.cs
+++ b/microservice/microserviceTests/OpenAPITests/TypeTests.cs
@@ -1,151 +1,227 @@
-using Beamable.Common.Reflection;
-using Beamable.Tooling.Common.OpenAPI;
-using NUnit.Framework;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Beamable.Common.Semantics;
-using UnityEngine;
-
-namespace microserviceTests.OpenAPITests;
-
-public class TypeTests
-{
- [Test]
- public void CheckRelatedTypes()
- {
- var types = SchemaGenerator.Traverse().ToList();
-
- Assert.AreEqual(1, types.Count);
- Assert.AreEqual(typeof(int), types[0]);
- }
-
- [TestCase(typeof(float), "number", "float")]
- [TestCase(typeof(double), "number", "double")]
- [TestCase(typeof(short), "integer", "int16")]
- [TestCase(typeof(int), "integer", "int32")]
- [TestCase(typeof(long), "integer", "int64")]
- [TestCase(typeof(bool), "boolean", null)]
- [TestCase(typeof(string), "string", null)]
- [TestCase(typeof(byte), "string", "byte")]
- [TestCase(typeof(Guid), "string", "uuid")]
- public void CheckPrimitives(Type runtimeType, string typeName, string format)
- {
- var schema = SchemaGenerator.Convert(runtimeType);
- Assert.AreEqual(typeName, schema.Type);
- Assert.AreEqual(format, schema.Format);
- }
-
- [TestCase(typeof(float[]), "number", "float")]
- [TestCase(typeof(List), "number", "float")]
- public void CheckPrimitiveArrays(Type runtimeType, string typeName, string format)
- {
- var schema = SchemaGenerator.Convert(runtimeType);
- Assert.AreEqual(typeName, schema.Items.Type);
- Assert.AreEqual(format, schema.Items.Format);
- }
-
- [TestCase(typeof(Dictionary), "integer", "int32")]
- public void CheckMapTypes(Type runtimeType, string typeName, string format)
- {
- var schema = SchemaGenerator.Convert(runtimeType);
- Assert.AreEqual(true, schema.AdditionalPropertiesAllowed);
- Assert.AreEqual(typeName, schema.AdditionalProperties.Type);
- Assert.AreEqual(format, schema.AdditionalProperties.Format);
- }
-
- [TestCase(typeof(Sample[]))]
- [TestCase(typeof(List))]
- public void CheckListOfObjects(Type runtimeType)
- {
- var schema = SchemaGenerator.Convert(runtimeType);
- Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Sample", schema.Items.Reference.Id);
- }
-
-
- [Test]
- public void CheckObject()
- {
- var schema = SchemaGenerator.Convert(typeof(Vector2));
-
- Assert.AreEqual(2, schema.Properties.Count);
-
- Assert.AreEqual("number", schema.Properties["x"].Type);
- Assert.AreEqual("float", schema.Properties["x"].Format);
-
- Assert.AreEqual("number", schema.Properties["y"].Type);
- Assert.AreEqual("float", schema.Properties["y"].Format);
- }
-
- [Test]
- public void CheckObjectWithReference()
- {
- var schema = SchemaGenerator.Convert(typeof(Sample));
-
- Assert.AreEqual("this is a sample", schema.Description);
- Assert.AreEqual(1, schema.Properties.Count);
-
- Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Tuna", schema.Properties[nameof(Sample.fish)].Reference.Id);
- Assert.AreEqual("a fish", schema.Properties[nameof(Sample.fish)].Description);
- }
-
- [Test]
- public void CheckEnums()
- {
- var schema = SchemaGenerator.Convert(typeof(Fish));
- Assert.AreEqual(2, schema.Enum.Count);
- }
-
-
- [Test]
- public void CheckEnumsOnObject()
- {
- var schema = SchemaGenerator.Convert(typeof(FishThing));
- Assert.AreEqual(1, schema.Properties.Count);
-
- var prop = schema.Properties[nameof(FishThing.type)];
- Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Fish", prop.Reference.Id);
- }
-
- [TestCase(typeof(BeamAccountId), "integer", "int64")]
- [TestCase(typeof(BeamCid), "integer", "int64")]
- [TestCase(typeof(BeamContentId), "string", null)]
- [TestCase(typeof(BeamContentManifestId), "string", null)]
- [TestCase(typeof(BeamGamerTag), "integer", "int64")]
- [TestCase(typeof(BeamPid), "string", null)]
- [TestCase(typeof(BeamStats), "string", null)]
- [TestCase(typeof(ServiceName), "string", null)]
- public void CheckSemanticTypes(Type type, string typeName, string format)
- {
- var schema = SchemaGenerator.Convert(type);
- Assert.AreEqual(typeName, schema.Type);
- Assert.AreEqual(format, schema.Format);
- }
-
-
- ///
- /// this is a sample
- ///
- public class Sample
- {
- ///
- /// a fish
- ///
- public Tuna fish;
- }
-
- ///
- /// the fish
- ///
- public class Tuna
- {
- public int smelly;
- }
-
- public enum Fish { Tuna, Salmon }
-
- public class FishThing
- {
- public Fish type;
- }
-}
+using beamable.server;
+using Beamable.Server.Common;
+using Beamable.Tooling.Common.OpenAPI;
+using Microsoft.OpenApi.Models;
+using NUnit.Framework;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Beamable.Common.Semantics;
+using System.Text.Json;
+using UnityEngine;
+
+namespace microserviceTests.OpenAPITests;
+
+public class TypeTests
+{
+ [Test]
+ public void CheckRelatedTypes()
+ {
+ var types = SchemaGenerator.Traverse().ToList();
+
+ Assert.AreEqual(1, types.Count);
+ Assert.AreEqual(typeof(int), types[0]);
+ }
+
+ [TestCase(typeof(float), "number", "float")]
+ [TestCase(typeof(double), "number", "double")]
+ [TestCase(typeof(short), "integer", "int16")]
+ [TestCase(typeof(int), "integer", "int32")]
+ [TestCase(typeof(long), "integer", "int64")]
+ [TestCase(typeof(bool), "boolean", null)]
+ [TestCase(typeof(string), "string", null)]
+ [TestCase(typeof(byte), "string", "byte")]
+ [TestCase(typeof(Guid), "string", "uuid")]
+ public void CheckPrimitives(Type runtimeType, string typeName, string format)
+ {
+ var requiredField = new HashSet();
+ var schema = SchemaGenerator.Convert(runtimeType, ref requiredField);
+ Assert.AreEqual(typeName, schema.Type);
+ Assert.AreEqual(format, schema.Format);
+ }
+
+ [TestCase(typeof(float[]), "number", "float")]
+ [TestCase(typeof(List), "number", "float")]
+ public void CheckPrimitiveArrays(Type runtimeType, string typeName, string format)
+ {
+ var requiredField = new HashSet();
+ var schema = SchemaGenerator.Convert(runtimeType, ref requiredField);
+ Assert.AreEqual(typeName, schema.Items.Type);
+ Assert.AreEqual(format, schema.Items.Format);
+ }
+
+ [TestCase(typeof(Dictionary), "integer", "int32")]
+ public void CheckMapTypes(Type runtimeType, string typeName, string format)
+ {
+ var requiredField = new HashSet();
+ var schema = SchemaGenerator.Convert(runtimeType, ref requiredField);
+ Assert.AreEqual(true, schema.AdditionalPropertiesAllowed);
+ Assert.AreEqual(typeName, schema.AdditionalProperties.Type);
+ Assert.AreEqual(format, schema.AdditionalProperties.Format);
+ }
+
+ [TestCase(typeof(Sample[]))]
+ [TestCase(typeof(List))]
+ public void CheckListOfObjects(Type runtimeType)
+ {
+ var requiredField = new HashSet();
+ var schema = SchemaGenerator.Convert(runtimeType, ref requiredField);
+ Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Sample", Uri.UnescapeDataString(schema.Items.Reference.Id));
+ }
+
+ [Test]
+ public void CheckMicroserviceRuntimeMetadata()
+ {
+ var requiredFields = new HashSet();
+ var schema = SchemaGenerator.Convert(typeof(MicroserviceRuntimeMetadata),ref requiredFields);
+ Assert.AreEqual("beamable.server.FederationComponentMetadata", schema.Properties["federatedComponents"].Items.Reference.Id);
+ var doc = new OpenApiDocument
+ {
+ Info = new OpenApiInfo { Title = "Test", Version = "0.0.0" },
+ Paths = new OpenApiPaths(),
+ Components = new OpenApiComponents
+ {
+ Schemas = new Dictionary()
+ }
+ };
+ doc.Components.Schemas.Add(typeof(MicroserviceRuntimeMetadata).GetSanitizedFullName(), schema);
+ SchemaGenerator.TryAddMissingSchemaTypes(ref doc, requiredFields);
+ Assert.AreEqual(2, doc.Components.Schemas[typeof(FederationComponentMetadata).GetSanitizedFullName()].Properties.Count);
+ }
+
+ [Test]
+ public void CheckObject()
+ {
+ var requiredField = new HashSet();
+ var schema = SchemaGenerator.Convert(typeof(Vector2), ref requiredField);
+
+ Assert.AreEqual(2, schema.Properties.Count);
+
+ Assert.AreEqual("number", schema.Properties["x"].Type);
+ Assert.AreEqual("float", schema.Properties["x"].Format);
+
+ Assert.AreEqual("number", schema.Properties["y"].Type);
+ Assert.AreEqual("float", schema.Properties["y"].Format);
+ }
+
+ [Test]
+ public void CheckObjectWithReference()
+ {
+ var requiredField = new HashSet();
+ var schema = SchemaGenerator.Convert(typeof(Sample), ref requiredField);
+
+ Assert.AreEqual("this is a sample", schema.Description);
+ Assert.AreEqual(1, schema.Properties.Count);
+
+ Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Tuna", Uri.UnescapeDataString(schema.Properties[nameof(Sample.fish)].Reference.Id));
+ Assert.AreEqual("a fish", schema.Properties[nameof(Sample.fish)].Description);
+ Assert.AreEqual(requiredField.Count, 1, "It should be missing Sample type definition");
+ }
+ [Test]
+ public void CheckObjectWithGeneric()
+ {
+ var requiredFields = new HashSet();
+ var schema = SchemaGenerator.Convert(typeof(SampleGenericField), ref requiredFields, 1, true);
+ var doc = new OpenApiDocument
+ {
+ Info = new OpenApiInfo { Title = "Test", Version = "0.0.0" },
+
+ Paths = new OpenApiPaths(),
+ Components = new OpenApiComponents
+ {
+ Schemas = new Dictionary()
+ }
+ };
+ doc.Components.Schemas.Add(SchemaGenerator.GetQualifiedReferenceName(typeof(SampleGenericField)), schema);
+ SchemaGenerator.TryAddMissingSchemaTypes(ref doc, requiredFields);
+
+ Assert.AreEqual("this is a sample", schema.Description);
+ Assert.AreEqual(1, schema.Properties.Count);
+ Assert.AreEqual(doc.Components.Schemas[typeof(Result).GetSanitizedFullName()].Properties[nameof(Result.Field)].Type, "string");
+ }
+
+ [Test]
+ public void CheckEnums()
+ {
+ var requiredField = new HashSet();
+ var schema = SchemaGenerator.Convert(typeof(Fish), ref requiredField);
+ Assert.AreEqual(2, schema.Enum.Count);
+ }
+
+
+ [Test]
+ public void CheckEnumsOnObject()
+ {
+ var requiredField = new HashSet();
+ var schema = SchemaGenerator.Convert(typeof(FishThing), ref requiredField);
+ Assert.AreEqual(1, schema.Properties.Count);
+
+ var prop = schema.Properties[nameof(FishThing.type)];
+ Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Fish", Uri.UnescapeDataString(prop.Reference.Id));
+ }
+
+ [TestCase(typeof(BeamAccountId), "integer", "int64")]
+ [TestCase(typeof(BeamCid), "integer", "int64")]
+ [TestCase(typeof(BeamContentId), "string", null)]
+ [TestCase(typeof(BeamContentManifestId), "string", null)]
+ [TestCase(typeof(BeamGamerTag), "integer", "int64")]
+ [TestCase(typeof(BeamPid), "string", null)]
+ [TestCase(typeof(BeamStats), "string", null)]
+ [TestCase(typeof(ServiceName), "string", null)]
+ public void CheckSemanticTypes(Type type, string typeName, string format)
+ {
+ var requiredFields = new HashSet();
+ var schema = SchemaGenerator.Convert(type, ref requiredFields);
+ Assert.AreEqual(typeName, schema.Type);
+ Assert.AreEqual(format, schema.Format);
+ }
+
+
+ ///
+ /// this is a sample
+ ///
+ public class Sample
+ {
+ ///
+ /// a fish
+ ///
+ public Tuna fish;
+ }
+ ///
+ /// this is a sample
+ ///
+ public class SampleGenericField
+ {
+ ///
+ /// This is a field description
+ ///
+ public Result theOnlyField;
+ }
+
+ ///
+ /// A generic result class
+ ///
+ /// Type of the field
+ public class Result
+ {
+ ///
+ /// Description of the generic field
+ ///
+ public T Field;
+ }
+
+ ///
+ /// the fish
+ ///
+ public class Tuna
+ {
+ public int smelly;
+ }
+
+ public enum Fish { Tuna, Salmon }
+
+ public class FishThing
+ {
+ public Fish type;
+ }
+}