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