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