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/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs b/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs
index b78f0dcca4..d859d18baf 100644
--- a/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs
+++ b/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs
@@ -2396,7 +2396,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/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs
index acf8e86a6b..46e6b2f045 100644
--- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs
+++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs
@@ -4,11 +4,13 @@
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 UnityEngine;
+using ZLogger;
using static Beamable.Common.Constants.Features.Services;
namespace Beamable.Tooling.Common.OpenAPI;
@@ -63,7 +65,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
@@ -173,6 +175,51 @@ public static IEnumerable FindAllTypesForOAPI(IEnumerable
+ /// Generates a dictionary of schemas that can be used to populate the OpenAPI docs.
+ ///
+ ///
+ ///
+ /// 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].ShouldNotGenerateClientCode();
+
+ for (int j = i + 1; j < oapiTypes.Count; j++)
+ {
+ if (oapiTypes[j].Type != oapiTypes[i].Type)
+ {
+ continue;
+ }
+
+ toSkip.Add(j);
+
+ if (!shouldGenerateClientCode && !oapiTypes[j].ShouldNotGenerateClientCode())
+ {
+ 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 .
///
@@ -190,32 +237,65 @@ public static IEnumerable Traverse(Type runtimeType)
yield return runtimeType;
}
+ 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, sanitizeGenericType);
case { } x when x == typeof(double):
return new OpenApiSchema { Type = "number", Format = "double" };
case { } x when x == typeof(float):
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):
@@ -223,19 +303,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
@@ -244,21 +328,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);
@@ -280,7 +417,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();
@@ -292,23 +429,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;
@@ -329,6 +473,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 884d0e5c1f..a2253e0d5b 100644
--- a/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs
+++ b/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs
@@ -195,30 +195,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);
- if (doc.Components.Schemas.TryGetValue(key, out var existingSchema))
- {
- var shouldGenerate = !oapiType.ShouldNotGenerateClientCode();
- if (shouldGenerate) existingSchema.AddExtension(Constants.Features.Services.METHOD_SKIP_CLIENT_GENERATION_KEY, new OpenApiBoolean(false));
-
- BeamableZLoggerProvider.LogContext.Value.ZLogDebug($"Tried to add Schema more than once. Type={type.FullName}, SchemaKey={key}, WillGenClient={oapiType.ShouldNotGenerateClientCode()}");
- }
- 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(oapiType.ShouldNotGenerateClientCode()));
-
- BeamableZLoggerProvider.LogContext.Value.ZLogDebug($"Adding Schema to Microservice OAPI docs. Type={type.FullName}, WillGenClient={oapiType.ShouldNotGenerateClientCode()}");
- doc.Components.Schemas.Add(key, schema);
- }
+ doc.Components.Schemas.Add(key, schema);
}
var methodsSkippedForClientCodeGen = new List();
@@ -232,11 +213,11 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r
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()));
}
var response = new OpenApiResponse() { Description = comments.Returns ?? "", };
@@ -266,7 +247,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];
@@ -285,7 +266,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
{
@@ -358,6 +339,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);
@@ -384,11 +367,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);
}
@@ -489,4 +472,4 @@ public static OpenApiDocument Generate(this ServiceDocGenerator g
};
return generator.Generate(startupContext, provider);
}
-}
\ No newline at end of file
+}
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..16197dfb4b 100644
--- a/microservice/beamable.tooling.common/TypeExtensions.cs
+++ b/microservice/beamable.tooling.common/TypeExtensions.cs
@@ -20,42 +20,53 @@ 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("+",".");
}
+ 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 daba283d61..9be6203d99 100644
--- a/microservice/microserviceTests/OpenAPITests/TypeTests.cs
+++ b/microservice/microserviceTests/OpenAPITests/TypeTests.cs
@@ -1,9 +1,12 @@
-using Beamable.Common.Reflection;
+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 System.Text.Json;
using UnityEngine;
namespace microserviceTests.OpenAPITests;
@@ -30,7 +33,8 @@ public void CheckRelatedTypes()
[TestCase(typeof(Guid), "string", "uuid")]
public void CheckPrimitives(Type runtimeType, string typeName, string format)
{
- var schema = SchemaGenerator.Convert(runtimeType);
+ var requiredField = new HashSet();
+ var schema = SchemaGenerator.Convert(runtimeType, ref requiredField);
Assert.AreEqual(typeName, schema.Type);
Assert.AreEqual(format, schema.Format);
}
@@ -39,7 +43,8 @@ public void CheckPrimitives(Type runtimeType, string typeName, string format)
[TestCase(typeof(List), "number", "float")]
public void CheckPrimitiveArrays(Type runtimeType, string typeName, string format)
{
- var schema = SchemaGenerator.Convert(runtimeType);
+ var requiredField = new HashSet();
+ var schema = SchemaGenerator.Convert(runtimeType, ref requiredField);
Assert.AreEqual(typeName, schema.Items.Type);
Assert.AreEqual(format, schema.Items.Format);
}
@@ -47,7 +52,8 @@ public void CheckPrimitiveArrays(Type runtimeType, string typeName, string forma
[TestCase(typeof(Dictionary), "integer", "int32")]
public void CheckMapTypes(Type runtimeType, string typeName, string format)
{
- var schema = SchemaGenerator.Convert(runtimeType);
+ 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);
@@ -57,15 +63,36 @@ public void CheckMapTypes(Type runtimeType, string typeName, string format)
[TestCase(typeof(List))]
public void CheckListOfObjects(Type runtimeType)
{
- var schema = SchemaGenerator.Convert(runtimeType);
- Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Sample", schema.Items.Reference.Id);
+ 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 schema = SchemaGenerator.Convert(typeof(Vector2));
+ var requiredField = new HashSet();
+ var schema = SchemaGenerator.Convert(typeof(Vector2), ref requiredField);
Assert.AreEqual(2, schema.Properties.Count);
@@ -79,19 +106,44 @@ public void CheckObject()
[Test]
public void CheckObjectWithReference()
{
- var schema = SchemaGenerator.Convert(typeof(Sample));
+ 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", schema.Properties[nameof(Sample.fish)].Reference.Id);
+ 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 schema = SchemaGenerator.Convert(typeof(Fish));
+ var requiredField = new HashSet();
+ var schema = SchemaGenerator.Convert(typeof(Fish), ref requiredField);
Assert.AreEqual(2, schema.Enum.Count);
}
@@ -99,11 +151,12 @@ public void CheckEnums()
[Test]
public void CheckEnumsOnObject()
{
- var schema = SchemaGenerator.Convert(typeof(FishThing));
+ 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", prop.Reference.Id);
+ Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Fish", Uri.UnescapeDataString(prop.Reference.Id));
}
@@ -117,6 +170,28 @@ public class Sample
///
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