From 886c6ddc978b883398738e8cf9f8a5478fcb59eb Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Tue, 9 Dec 2025 14:30:59 +0100 Subject: [PATCH 01/16] OpenAPI- improve schema generation --- .../OpenAPI/SchemaGenerator.cs | 96 +++++++++++++++++-- .../OpenAPI/ServiceDocGenerator.cs | 41 +++----- .../MicroserviceRuntimeMetadata.cs | 55 +++++++++++ .../OpenAPITests/TypeTests.cs | 47 +++++++-- 4 files changed, 192 insertions(+), 47 deletions(-) diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs index acf8e86a6b..74be1af4e8 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; @@ -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,20 +237,40 @@ 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) + { + var key = GetQualifiedReferenceName(requiredType); + 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); 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); 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 == typeof(double): return new OpenApiSchema { Type = "number", Format = "double" }; case { } x when x == typeof(float): @@ -231,11 +298,11 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti // 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,7 +311,7 @@ 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), Extensions = new Dictionary { [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_NAMESPACE] = new OpenApiString(runtimeType.Namespace), @@ -258,6 +325,7 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti case Type _ when depth <= 0: + requiredTypes.Add(runtimeType); return new OpenApiSchema { Type = "object", Reference = new OpenApiReference { Id = GetQualifiedReferenceName(runtimeType), Type = ReferenceType.Schema } }; case { IsEnum: true }: @@ -303,12 +371,15 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti new OpenApiString(runtimeType.GetGenericSanitizedFullName()); } - 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); var comment = DocsLoader.GetMemberComments(member); fieldSchema.Description = comment?.Summary; @@ -331,4 +402,11 @@ public static string GetQualifiedReferenceName(Type runtimeType) { return runtimeType.FullName.Replace("+", "."); } + /// + /// Gets the fully qualified reference name for a runtime type. + /// + public static string GetQualifiedReferenceName() + { + return GetQualifiedReferenceName(typeof(T)); + } } diff --git a/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs index 884d0e5c1f..a6e3ccf997 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,7 +213,7 @@ 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)) { @@ -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/microserviceTests/OpenAPITests/TypeTests.cs b/microservice/microserviceTests/OpenAPITests/TypeTests.cs index daba283d61..3beaa75a5d 100644 --- a/microservice/microserviceTests/OpenAPITests/TypeTests.cs +++ b/microservice/microserviceTests/OpenAPITests/TypeTests.cs @@ -1,5 +1,6 @@ -using Beamable.Common.Reflection; +using beamable.server; using Beamable.Tooling.Common.OpenAPI; +using Microsoft.OpenApi.Models; using NUnit.Framework; using System; using System.Collections.Generic; @@ -30,7 +31,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 +41,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 +50,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 +61,36 @@ public void CheckMapTypes(Type runtimeType, string typeName, string format) [TestCase(typeof(List))] public void CheckListOfObjects(Type runtimeType) { - var schema = SchemaGenerator.Convert(runtimeType); + var requiredField = new HashSet(); + var schema = SchemaGenerator.Convert(runtimeType, ref requiredField); Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Sample", 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(SchemaGenerator.GetQualifiedReferenceName(typeof(MicroserviceRuntimeMetadata)), schema); + SchemaGenerator.TryAddMissingSchemaTypes(ref doc, requiredFields); + Assert.AreEqual(doc.Components.Schemas[SchemaGenerator.GetQualifiedReferenceName()].Properties.Count, 2); + } [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 +104,22 @@ 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("a fish", schema.Properties[nameof(Sample.fish)].Description); + Assert.AreEqual(requiredField.Count, 1, "It should be missing Sample type definition"); } [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,7 +127,8 @@ 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)]; From 2b412c7ed78a4d268cb62c793236f75f26b75d3b Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Tue, 9 Dec 2025 14:59:33 +0100 Subject: [PATCH 02/16] Improved OpenAPI components schema gen --- .../OpenAPI/SchemaGenerator.cs | 16 +++++---- .../beamable.tooling.common/TypeExtensions.cs | 29 ++++++++++++++- .../OpenAPITests/TypeTests.cs | 36 +++++++++++++++++++ 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs index 74be1af4e8..6791264d30 100644 --- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs @@ -242,7 +242,11 @@ public static bool TryAddMissingSchemaTypes(ref OpenApiDocument oapiDoc, HashSet var newRequiredTypes = new HashSet(); foreach (Type requiredType in requiredTypes) { - var key = GetQualifiedReferenceName(requiredType); + if (requiredType.IsBasicType()) + { + continue; + } + var key = requiredType.GetGenericSanitizedFullName(); if(oapiDoc.Components.Schemas.ContainsKey(key)) continue; var schema = Convert(requiredType, ref newRequiredTypes); @@ -266,11 +270,11 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required { case { } x when x.IsAssignableTo(typeof(Optional)): var instance = Activator.CreateInstance(runtimeType) as Optional; - return Convert(instance.GetOptionalType(), ref requiredTypes,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], ref requiredTypes,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], ref requiredTypes,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): @@ -311,7 +315,7 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required { Type = "object", AdditionalPropertiesAllowed = true, - AdditionalProperties = Convert(x.GetGenericArguments()[1], ref requiredTypes,depth - 1), + AdditionalProperties = Convert(x.GetGenericArguments()[1], ref requiredTypes,depth - 1, sanitizeGenericType), Extensions = new Dictionary { [MICROSERVICE_EXTENSION_BEAMABLE_TYPE_NAMESPACE] = new OpenApiString(runtimeType.Namespace), @@ -379,7 +383,7 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required foreach (var member in members) { var name = member.Name; - var fieldSchema = Convert(member.FieldType,ref requiredTypes, depth - 1); + var fieldSchema = Convert(member.FieldType,ref requiredTypes, depth - 1, sanitizeGenericType); var comment = DocsLoader.GetMemberComments(member); fieldSchema.Description = comment?.Summary; diff --git a/microservice/beamable.tooling.common/TypeExtensions.cs b/microservice/beamable.tooling.common/TypeExtensions.cs index 1508b2ad6e..268d0e54a1 100644 --- a/microservice/beamable.tooling.common/TypeExtensions.cs +++ b/microservice/beamable.tooling.common/TypeExtensions.cs @@ -25,12 +25,39 @@ public static string GetSanitizedFullName(this Type type) return type.FullName; } + 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 false; + } + } + public static string GetGenericSanitizedFullName(this Type type) { if (!type.IsGenericType) { - if(type.IsPrimitive && OpenApiUtils.OpenApiCSharpNameMap.TryGetValue(type.Name.ToLower(), out string shortName)) + if(type.IsBasicType() && OpenApiUtils.OpenApiCSharpNameMap.TryGetValue(type.Name.ToLower(), out string shortName)) + { return shortName; + } return type.FullName ?? type.Name; } diff --git a/microservice/microserviceTests/OpenAPITests/TypeTests.cs b/microservice/microserviceTests/OpenAPITests/TypeTests.cs index 3beaa75a5d..790fb69f1d 100644 --- a/microservice/microserviceTests/OpenAPITests/TypeTests.cs +++ b/microservice/microserviceTests/OpenAPITests/TypeTests.cs @@ -1,10 +1,12 @@ 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; @@ -114,6 +116,28 @@ public void CheckObjectWithReference() 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).GetGenericSanitizedFullName()].Properties["Field"].Type, "string"); + } [Test] public void CheckEnums() @@ -146,6 +170,18 @@ public class Sample /// public Tuna fish; } + /// + /// this is a sample + /// + public class SampleGenericField + { + public Result theOnlyField; + } + + public class Result + { + public T Field; + } /// /// the fish From 7a583eaab73da41e7008bb2549b38eff456a7786 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Tue, 9 Dec 2025 16:04:33 +0100 Subject: [PATCH 03/16] Reduce code repetition --- .../OpenAPI/SchemaGenerator.cs | 16 +++-- .../OpenAPI/ServiceDocGenerator.cs | 2 +- .../beamable.tooling.common/TypeExtensions.cs | 58 +++++++------------ .../OpenAPITests/TypeTests.cs | 6 +- 4 files changed, 35 insertions(+), 47 deletions(-) diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs index 6791264d30..525e250edf 100644 --- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs @@ -246,7 +246,7 @@ public static bool TryAddMissingSchemaTypes(ref OpenApiDocument oapiDoc, HashSet { continue; } - var key = requiredType.GetGenericSanitizedFullName(); + var key = requiredType.GetSanitizedFullName(); if(oapiDoc.Components.Schemas.ContainsKey(key)) continue; var schema = Convert(requiredType, ref newRequiredTypes); @@ -320,10 +320,10 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required { [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()) } }; @@ -352,7 +352,7 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required 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(); @@ -364,15 +364,19 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required { [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) { diff --git a/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs index a6e3ccf997..a2253e0d5b 100644 --- a/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs @@ -217,7 +217,7 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r 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 ?? "", }; diff --git a/microservice/beamable.tooling.common/TypeExtensions.cs b/microservice/beamable.tooling.common/TypeExtensions.cs index 268d0e54a1..29820427f7 100644 --- a/microservice/beamable.tooling.common/TypeExtensions.cs +++ b/microservice/beamable.tooling.common/TypeExtensions.cs @@ -20,7 +20,26 @@ public static bool IsAssignableTo(this Type type, Type other) public static string GetSanitizedFullName(this Type type) { - if (type.FullName != null && type.FullName.Contains("`")) + if (type.IsGenericType) + { + string typeName = type.FullName?.Split('`')[0] ?? type.Name.Split('`')[0]; + + var genericArgs = type.GetGenericArguments(); + string args = string.Join(", ", genericArgs.Select(GetSanitizedFullName)); + + return $"{typeName}<{args}>"; + } + if(type.IsBasicType() && OpenApiUtils.OpenApiCSharpNameMap.TryGetValue(type.Name.ToLower(), out string shortName)) + { + return shortName; + } + + if (type.FullName == null) + { + return type.Name; + } + + if(type.FullName.Contains("`")) return type.FullName.Split('`')[0]; return type.FullName; } @@ -46,43 +65,8 @@ public static bool IsBasicType(this Type t) return true; default: - return false; + return t.IsPrimitive; } } - public static string GetGenericSanitizedFullName(this Type type) - { - if (!type.IsGenericType) - { - if(type.IsBasicType() && 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]; - - var genericArgs = type.GetGenericArguments(); - string args = string.Join(", ", genericArgs.Select(GetGenericSanitizedFullName)); - - return $"{typeName}<{args}>"; - } - - public static string GetGenericQualifiedTypeName(this Type type) - { - if (!type.IsGenericType) - { - return type.FullName ?? type.Name; - } - - string typeName = type.FullName?.Split('`')[0] ?? type.Name.Split('`')[0]; - - var genericArgs = type.GetGenericArguments(); - string args = string.Join(", ", genericArgs.Select(GetGenericQualifiedTypeName)); - - return $"{typeName}.{args}"; - } - - } diff --git a/microservice/microserviceTests/OpenAPITests/TypeTests.cs b/microservice/microserviceTests/OpenAPITests/TypeTests.cs index 790fb69f1d..b599e11485 100644 --- a/microservice/microserviceTests/OpenAPITests/TypeTests.cs +++ b/microservice/microserviceTests/OpenAPITests/TypeTests.cs @@ -83,9 +83,9 @@ public void CheckMicroserviceRuntimeMetadata() Schemas = new Dictionary() } }; - doc.Components.Schemas.Add(SchemaGenerator.GetQualifiedReferenceName(typeof(MicroserviceRuntimeMetadata)), schema); + doc.Components.Schemas.Add(typeof(MicroserviceRuntimeMetadata).GetSanitizedFullName(), schema); SchemaGenerator.TryAddMissingSchemaTypes(ref doc, requiredFields); - Assert.AreEqual(doc.Components.Schemas[SchemaGenerator.GetQualifiedReferenceName()].Properties.Count, 2); + Assert.AreEqual(2, doc.Components.Schemas[typeof(FederationComponentMetadata).GetSanitizedFullName()].Properties.Count); } [Test] @@ -136,7 +136,7 @@ public void CheckObjectWithGeneric() Assert.AreEqual("this is a sample", schema.Description); Assert.AreEqual(1, schema.Properties.Count); - Assert.AreEqual(doc.Components.Schemas[typeof(Result).GetGenericSanitizedFullName()].Properties["Field"].Type, "string"); + Assert.AreEqual(doc.Components.Schemas[typeof(Result).GetSanitizedFullName()].Properties[nameof(Result.Field)].Type, "string"); } [Test] From a8e5a2048a8acfdf2782d126d43c60d583f82b48 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Tue, 9 Dec 2025 16:07:06 +0100 Subject: [PATCH 04/16] Simplify code --- .../beamable.tooling.common/OpenAPI/SchemaGenerator.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs index 525e250edf..0076b0fbd8 100644 --- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs @@ -408,13 +408,6 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required /// public static string GetQualifiedReferenceName(Type runtimeType) { - return runtimeType.FullName.Replace("+", "."); - } - /// - /// Gets the fully qualified reference name for a runtime type. - /// - public static string GetQualifiedReferenceName() - { - return GetQualifiedReferenceName(typeof(T)); + return runtimeType.GetSanitizedFullName().Replace("+", "."); } } From 81d9ceec3143a00d30c80687172bbe73b4c3a4a0 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Tue, 9 Dec 2025 16:09:01 +0100 Subject: [PATCH 05/16] Fix code doc --- cli/beamable.common/Runtime/CronExpression/ExpressionParser.cs | 2 +- .../Common/Runtime/CronExpression/ExpressionParser.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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); From 2c349e00b24f191167f14a799c0ca749b15b28e7 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Thu, 15 Jan 2026 09:29:22 +0100 Subject: [PATCH 06/16] improvements, more tests --- .../UnrealSourceGenerator.cs | 7 +- cli/tests/ExtraTests.cs | 705 ++++++++++++++++++ 2 files changed, 711 insertions(+), 1 deletion(-) create mode 100644 cli/tests/ExtraTests.cs diff --git a/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs b/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs index b78f0dcca4..f7060a5be9 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)) + { + 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); + } + } + +} From 5fbd62d36c0c3309269e2e1bdab87d87dcc5c24b Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Thu, 15 Jan 2026 09:29:58 +0100 Subject: [PATCH 07/16] Improvements, part 2 --- .../OpenAPI/SchemaGenerator.cs | 71 ++++++++++++++++++- .../OpenAPITests/TypeTests.cs | 10 +++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs index 0076b0fbd8..56daaee5ab 100644 --- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs @@ -281,12 +281,21 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required 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): @@ -294,8 +303,12 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required 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" }; @@ -326,6 +339,24 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required [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 _ when depth <= 0: @@ -408,6 +439,40 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required /// public static string GetQualifiedReferenceName(Type runtimeType) { - return runtimeType.GetSanitizedFullName().Replace("+", "."); + return Uri.EscapeUriString(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/microserviceTests/OpenAPITests/TypeTests.cs b/microservice/microserviceTests/OpenAPITests/TypeTests.cs index b599e11485..554ec698dc 100644 --- a/microservice/microserviceTests/OpenAPITests/TypeTests.cs +++ b/microservice/microserviceTests/OpenAPITests/TypeTests.cs @@ -175,11 +175,21 @@ public class 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; } From bd542a735cde93646251534115f4c0bb3ae03bc2 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Thu, 15 Jan 2026 15:23:01 +0100 Subject: [PATCH 08/16] Generate schemas for content references --- .../UnrealSourceGenerator.cs | 2 +- .../OpenAPI/SchemaGenerator.cs | 42 +++++++++++++++++-- .../OpenAPITests/TypeTests.cs | 6 +-- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs b/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs index f7060a5be9..d859d18baf 100644 --- a/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs +++ b/cli/cli/Services/UnrealSourceGenerator/UnrealSourceGenerator.cs @@ -2396,7 +2396,7 @@ private static UnrealType GetUnrealTypeForField(out UnrealType nonOverridenType, : UNREAL_MAP + $"<{UNREAL_STRING}, {dataType}>"); } case ("object", _, _, _) when schema.Reference == null && !schema.AdditionalPropertiesAllowed: - if (parentDoc.Components.Schemas.TryGetValue(schema.Title, out var innerSchema)) + 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); } diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs index 56daaee5ab..46e6b2f045 100644 --- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs @@ -65,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 @@ -358,10 +358,44 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required } }; + 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: - requiredTypes.Add(runtimeType); - 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); @@ -439,7 +473,7 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required /// public static string GetQualifiedReferenceName(Type runtimeType) { - return Uri.EscapeUriString(runtimeType.GetSanitizedFullName()); + return Uri.EscapeDataString(runtimeType.GetSanitizedFullName()); } static bool IsDictionary(Type type) { diff --git a/microservice/microserviceTests/OpenAPITests/TypeTests.cs b/microservice/microserviceTests/OpenAPITests/TypeTests.cs index 554ec698dc..b51a398852 100644 --- a/microservice/microserviceTests/OpenAPITests/TypeTests.cs +++ b/microservice/microserviceTests/OpenAPITests/TypeTests.cs @@ -65,7 +65,7 @@ public void CheckListOfObjects(Type runtimeType) { var requiredField = new HashSet(); var schema = SchemaGenerator.Convert(runtimeType, ref requiredField); - Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Sample", schema.Items.Reference.Id); + Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests+Sample", Uri.UnescapeDataString(schema.Items.Reference.Id)); } [Test] @@ -112,7 +112,7 @@ public void CheckObjectWithReference() 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"); } @@ -156,7 +156,7 @@ public void CheckEnumsOnObject() 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)); } From b14634f7a4392e1a1505d655fb56274cceac09a9 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Thu, 15 Jan 2026 15:33:29 +0100 Subject: [PATCH 09/16] Working version --- microservice/beamable.tooling.common/TypeExtensions.cs | 8 ++++---- microservice/microserviceTests/OpenAPITests/TypeTests.cs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/microservice/beamable.tooling.common/TypeExtensions.cs b/microservice/beamable.tooling.common/TypeExtensions.cs index 29820427f7..16197dfb4b 100644 --- a/microservice/beamable.tooling.common/TypeExtensions.cs +++ b/microservice/beamable.tooling.common/TypeExtensions.cs @@ -27,7 +27,7 @@ public static string GetSanitizedFullName(this Type type) 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)) { @@ -36,12 +36,12 @@ public static string GetSanitizedFullName(this Type type) if (type.FullName == null) { - return type.Name; + return type.Name.Replace("+","."); } if(type.FullName.Contains("`")) - return type.FullName.Split('`')[0]; - return type.FullName; + return type.FullName.Split('`')[0].Replace("+","."); + return type.FullName.Replace("+","."); } public static bool IsBasicType(this Type t) diff --git a/microservice/microserviceTests/OpenAPITests/TypeTests.cs b/microservice/microserviceTests/OpenAPITests/TypeTests.cs index b51a398852..9be6203d99 100644 --- a/microservice/microserviceTests/OpenAPITests/TypeTests.cs +++ b/microservice/microserviceTests/OpenAPITests/TypeTests.cs @@ -65,7 +65,7 @@ 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)); + Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Sample", Uri.UnescapeDataString(schema.Items.Reference.Id)); } [Test] @@ -112,7 +112,7 @@ public void CheckObjectWithReference() 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("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"); } @@ -156,7 +156,7 @@ public void CheckEnumsOnObject() Assert.AreEqual(1, schema.Properties.Count); var prop = schema.Properties[nameof(FishThing.type)]; - Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests+Fish", Uri.UnescapeDataString(prop.Reference.Id)); + Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Fish", Uri.UnescapeDataString(prop.Reference.Id)); } From 36c1e71928f3626b43820d42f48d6bd57c9c6647 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Fri, 16 Jan 2026 13:55:30 +0100 Subject: [PATCH 10/16] Improve OpenAPI document merging --- cli/cli/Services/SwaggerService.cs | 148 ++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/cli/cli/Services/SwaggerService.cs b/cli/cli/Services/SwaggerService.cs index aeb3ca9f70..6aaa6d8795 100644 --- a/cli/cli/Services/SwaggerService.cs +++ b/cli/cli/Services/SwaggerService.cs @@ -5,6 +5,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; @@ -431,10 +432,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); } @@ -442,8 +465,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)) @@ -523,6 +550,79 @@ public OpenApiDocument GetCombinedDocument(List apis) return combinedDocument; } + private static bool ArePathItemsEqual(OpenApiPathItem a, OpenApiPathItem b, out List differences) + { + differences = new List(); + + if (a.Operations.Count != b.Operations.Count) + { + 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) + { + differences.Add($"[{method}] request body exists only in first document"); + } + else if (aOp.RequestBody != null && bOp.RequestBody != null) + { + 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}"); + } + } + + 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 /// @@ -1683,6 +1783,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; } From d90ae65a8ac24249603e80a5acf959cecd50edac Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Fri, 16 Jan 2026 13:56:58 +0100 Subject: [PATCH 11/16] Fix calling `oapi download --combine-into-one-document` when `--output` is directory path --- cli/cli/Commands/Config/DownloadOpenAPICommand.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cli/cli/Commands/Config/DownloadOpenAPICommand.cs b/cli/cli/Commands/Config/DownloadOpenAPICommand.cs index f4478aa341..7b66a106c1 100644 --- a/cli/cli/Commands/Config/DownloadOpenAPICommand.cs +++ b/cli/cli/Commands/Config/DownloadOpenAPICommand.cs @@ -63,10 +63,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)) From 0a6ced44617de1360dbaf2986195b09bc4d3a5e7 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Wed, 25 Feb 2026 12:17:34 +0100 Subject: [PATCH 12/16] Squashed commit of the following: commit 5481294a03f2154d0657c44698d29a089a8f82cd Merge: 884c4aad9 312e1cebc Author: DiasAtBeamable Date: Tue Feb 24 13:53:26 2026 -0300 Merge pull request #4450 from beamable/feature/code-gen-type-mappings New - Beamable Semantic Types and Custom Replacement Types commit 312e1cebcab55c972fe193c0a6e6601adb372c83 Merge: 92b385115 884c4aad9 Author: DiasAtBeamable Date: Tue Feb 24 12:40:49 2026 -0300 Merge branch 'main' into feature/code-gen-type-mappings commit 92b3851154b969568ecc0e44fce595a3f8ca7384 Author: Gabriel Moraes Date: Wed Jan 7 16:06:25 2026 -0300 new: Added support to CustomReplacementTypes module on Unreal. commit aaa28ae63b960326a5afdd32a6af9dfc31c17df9 Author: Gabriel Moraes Date: Wed Jan 7 11:33:14 2026 -0300 change: Updated CHANGELOG.md commit b6acf133c7dbe46341e11494193c8918ebede4ff Author: Gabriel Moraes Date: Wed Jan 7 10:31:35 2026 -0300 fix: Issue when adding or removing replacement types when the field is null commit 39df7b0205845fb98341587b78b08ff309362127 Author: Gabriel Moraes Date: Mon Jan 5 15:31:31 2026 -0300 new: Commands to manage replacement types commit ee1c49f6ff664ef19a26cfc803133042f7736efd Author: Gabriel Moraes Date: Tue Dec 16 15:13:49 2025 -0300 change: merge conflict solve commit 4e3bd5055410d8aa334afb0489cadf52d2959f14 Author: Gabriel Moraes Date: Tue Dec 16 14:31:21 2025 -0300 change: Commit autogen files commit 79a52d904e8d925a7d53cdb28c7f3c8801af6c7b Merge: 66517d816 84e4184f6 Author: Gabriel Moraes Date: Tue Dec 16 14:05:41 2025 -0300 Merge remote-tracking branch 'origin/main' into feature/code-gen-type-mappings commit 66517d816032319c677fde01b1d4317f2b26fcf4 Author: Gabriel Moraes Date: Mon Dec 15 10:01:50 2025 -0300 change: Added support to Equatable Long, string and struct to each SemanticType commit 74dcc190e3fabb224cc2c67761614406ffa19064 Author: Gabriel Moraes Date: Fri Dec 12 16:57:31 2025 -0300 change: Updated Semantic Type to use types inherits by IBeamSemanticType instead of using an attribute. commit c43b7ab1419e40186465f43439ef61aa4e591421 Author: Gabriel Moraes Date: Thu Dec 11 10:27:01 2025 -0300 new: Introduced BeamSemanticTypeAttribute and IBeamSemanticType.cs (with a few implementations) change: Updated OpenAPI gen to use it correctly and add the extensions needed for each case. --- .../Implementations/ServiceConstants.cs | 5 + .../Runtime/Semantics/BeamAccountId.cs | 88 ++++++ .../Runtime/Semantics/BeamCid.cs | 86 ++++++ .../Runtime/Semantics/BeamContentId.cs | 52 ++++ .../Semantics/BeamContentManifestId.cs | 46 +++ .../Runtime/Semantics/BeamGamerTag.cs | 87 ++++++ .../Runtime/Semantics/BeamPid.cs | 46 +++ .../Runtime/Semantics/BeamStats.cs | 59 ++++ .../Runtime/Semantics/IBeamSemanticType.cs | 13 + .../Runtime/Semantics/ServiceName.cs | 25 +- .../Runtime/SmallerJSON/SmallerJSON.cs | 226 +++++++++++++++ cli/cli/App.cs | 4 + cli/cli/CHANGELOG.md | 5 + .../Project/AddReplacementTypeCommand.cs | 141 +++++++++ .../Project/GenerateClientFileCommand.cs | 111 +++++--- .../Project/ListReplacementTypeCommand.cs | 65 +++++ .../Project/RemoveReplacementTypeCommand.cs | 93 ++++++ cli/cli/Contants.cs | 2 +- cli/cli/Services/ConfigService.cs | 16 ++ .../OpenApiClientCodeGenerator.cs | 84 ++++++ .../Implementations/ServiceConstants.cs | 5 + .../Runtime/Environment/PackageVersion.cs | 5 + .../JsonSerializable/MessagePackStream.cs | 212 ++++++++++++++ .../MessagePackStream.cs.meta | 11 + .../Common/Runtime/Semantics/BeamAccountId.cs | 91 ++++++ .../Runtime/Semantics/BeamAccountId.cs.meta | 11 + .../Common/Runtime/Semantics/BeamCid.cs | 89 ++++++ .../Common/Runtime/Semantics/BeamCid.cs.meta | 11 + .../Common/Runtime/Semantics/BeamContentId.cs | 55 ++++ .../Runtime/Semantics/BeamContentId.cs.meta | 11 + .../Semantics/BeamContentManifestId.cs | 49 ++++ .../Semantics/BeamContentManifestId.cs.meta | 11 + .../Common/Runtime/Semantics/BeamGamerTag.cs | 90 ++++++ .../Runtime/Semantics/BeamGamerTag.cs.meta | 11 + .../Common/Runtime/Semantics/BeamPid.cs | 49 ++++ .../Common/Runtime/Semantics/BeamPid.cs.meta | 11 + .../Common/Runtime/Semantics/BeamStats.cs | 62 ++++ .../Runtime/Semantics/BeamStats.cs.meta | 11 + .../Runtime/Semantics/IBeamSemanticType.cs | 16 ++ .../Semantics/IBeamSemanticType.cs.meta | 11 + .../Common/Runtime/Semantics/ServiceName.cs | 25 +- .../Common/Runtime/SmallerJSON/SmallerJSON.cs | 226 +++++++++++++++ .../BeamListReplacementTypeCommandOutput.cs | 12 + ...amListReplacementTypeCommandOutput.cs.meta | 11 + .../Commands/BeamProjectAddReplacementType.cs | 81 ++++++ .../BeamProjectAddReplacementType.cs.meta | 11 + .../Commands/BeamProjectGenerateProperties.cs | 128 ++++----- .../BeamProjectListReplacementType.cs | 58 ++++ .../BeamProjectListReplacementType.cs.meta | 11 + .../BeamProjectRemoveReplacementType.cs | 60 ++++ .../BeamProjectRemoveReplacementType.cs.meta | 11 + .../Commands/BeamReplacementTypeInfo.cs | 15 + .../Commands/BeamReplacementTypeInfo.cs.meta | 11 + .../Runtime/Server/MicroserviceClient.cs | 7 +- .../Microservice/ServiceMethodHelper.cs | 10 +- .../OpenAPI/SchemaGenerator.cs | 29 +- .../OpenAPI/ServiceDocGenerator.cs | 4 + .../UnityJsonContractResolver.cs | 269 ++++++++++++++++++ .../OpenAPITests/TypeTests.cs | 19 +- 59 files changed, 2952 insertions(+), 122 deletions(-) create mode 100644 cli/beamable.common/Runtime/Semantics/BeamAccountId.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamCid.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamContentId.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamPid.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamStats.cs create mode 100644 cli/beamable.common/Runtime/Semantics/IBeamSemanticType.cs create mode 100644 cli/cli/Commands/Project/AddReplacementTypeCommand.cs create mode 100644 cli/cli/Commands/Project/ListReplacementTypeCommand.cs create mode 100644 cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs.meta create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs.meta create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs.meta create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs.meta create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs.meta create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs.meta diff --git a/cli/beamable.common/Runtime/Constants/Implementations/ServiceConstants.cs b/cli/beamable.common/Runtime/Constants/Implementations/ServiceConstants.cs index c0abb4a5cf..bb4894144f 100644 --- a/cli/beamable.common/Runtime/Constants/Implementations/ServiceConstants.cs +++ b/cli/beamable.common/Runtime/Constants/Implementations/ServiceConstants.cs @@ -79,6 +79,11 @@ public static partial class Services public const string PATH_CALLABLE_METHOD_NAME_KEY = "x-beamable-callable-method-name"; public const string PATH_CALLABLE_METHOD_CLIENT_PREFIX_KEY = "x-beamable-route-source-client-prefix"; + /// + /// OpenAPI extension that describes the semantic type of a primitive field. + /// + public const string SCHEMA_SEMANTIC_TYPE_NAME_KEY = "x-beamable-semantic-type"; + public const string MICROSERVICE_FEDERATED_COMPONENTS_V2_INTERFACE_KEY = "interface"; public const string MICROSERVICE_FEDERATED_COMPONENTS_V2_FEDERATION_ID_KEY = "federationId"; public const string MICROSERVICE_FEDERATED_COMPONENTS_V2_FEDERATION_CLASS_NAME_KEY = "federationClassName"; diff --git a/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs b/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs new file mode 100644 index 0000000000..a9eb934051 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs @@ -0,0 +1,88 @@ +using Beamable.Common.BeamCli; +using System; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamAccountId : IBeamSemanticType, IEquatable, IEquatable, IEquatable + { + private long _longValue; + private string _stringValue; + + public string SemanticName => "AccountId"; + + public long AsLong + { + get => _longValue; + set + { + _longValue = value; + _stringValue = value.ToString(); + } + } + + public string AsString + { + get => string.IsNullOrEmpty(_stringValue) ? _longValue.ToString() : _stringValue; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + } + + public BeamAccountId(long value) + { + _longValue = value; + _stringValue = value.ToString(); + } + + public BeamAccountId(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + + public static implicit operator string(BeamAccountId id) => id.AsString; + public static implicit operator long(BeamAccountId id) => id.AsLong; + + public static implicit operator BeamAccountId(string value) => new BeamAccountId(value); + public static implicit operator BeamAccountId(long value) => new BeamAccountId(value); + public string ToJson() + { + return AsString; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(long other) + { + return other == AsLong; + } + + public bool Equals(BeamAccountId other) + { + return other.AsLong == AsLong; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/cli/beamable.common/Runtime/Semantics/BeamCid.cs b/cli/beamable.common/Runtime/Semantics/BeamCid.cs new file mode 100644 index 0000000000..a9a1f3e99d --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamCid.cs @@ -0,0 +1,86 @@ +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamCid : IBeamSemanticType, IEquatable, IEquatable, IEquatable + { + private long _longValue; + private string _stringValue; + + public string SemanticName => "Cid"; + + public long AsLong { + get => _longValue; + set { + _longValue = value; + _stringValue = value.ToString(); + } + } + + public string AsString + { + get => string.IsNullOrEmpty(_stringValue) ? _longValue.ToString() : _stringValue; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + } + + public BeamCid(long value) + { + _longValue = value; + _stringValue = value.ToString(); + } + + public BeamCid(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + + public static implicit operator string(BeamCid cid) => cid.AsString; + public static implicit operator long(BeamCid cid) => cid.AsLong; + + public static implicit operator BeamCid(string value) => new BeamCid(value); + public static implicit operator BeamCid(long value) => new BeamCid(value); + public string ToJson() + { + return AsString; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(long other) + { + return other == AsLong; + } + + public bool Equals(BeamCid other) + { + return other.AsLong == AsLong; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/cli/beamable.common/Runtime/Semantics/BeamContentId.cs b/cli/beamable.common/Runtime/Semantics/BeamContentId.cs new file mode 100644 index 0000000000..46d62b8f14 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamContentId.cs @@ -0,0 +1,52 @@ +using System; +using Beamable.Common.BeamCli; +using Beamable.Common.Content; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamContentId : IBeamSemanticType, IEquatable, IEquatable + { + private string _value; + + public string SemanticName => "ContentId"; + + public string AsString + { + get => _value ?? string.Empty; + set => _value = value; + } + + public BeamContentId(string value) + { + _value = value; + } + + public BeamContentId(ContentRef contentRef) : this(contentRef.GetId()) { } + public BeamContentId(ContentObject contentObject) : this(contentObject.Id) { } + + public static implicit operator string(BeamContentId contentId) => contentId.AsString; + public static implicit operator BeamContentId(ContentRef contentRef) => new BeamContentId(contentRef); + public static implicit operator BeamContentId(ContentObject contentObject) => new BeamContentId(contentObject); + public static implicit operator BeamContentId(string contentId) => new BeamContentId(contentId); + public string ToJson() + { + return $"\"{AsString}\""; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamContentId other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs b/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs new file mode 100644 index 0000000000..a5b06a8c42 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs @@ -0,0 +1,46 @@ +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamContentManifestId : IBeamSemanticType, IEquatable, IEquatable + { + private string _value; + + public string SemanticName => "ContentManifestId"; + + public string AsString + { + get => _value ?? string.Empty; + set => _value = value; + } + + public BeamContentManifestId(string value) + { + _value = value; + } + + public static implicit operator string(BeamContentManifestId id) => id.AsString; + public static implicit operator BeamContentManifestId(string value) => new BeamContentManifestId(value); + public string ToJson() + { + return $"\"{AsString}\""; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamContentManifestId other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs b/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs new file mode 100644 index 0000000000..87e9b66402 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs @@ -0,0 +1,87 @@ +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamGamerTag : IBeamSemanticType, IEquatable, IEquatable, IEquatable + { + private long _longValue; + private string _stringValue; + + public string SemanticName => "GamerTag"; + + public long AsLong + { + get => _longValue; + set { + _longValue = value; + _stringValue = value.ToString(); + } + } + + public string AsString + { + get => string.IsNullOrEmpty(_stringValue) ? _longValue.ToString() : _stringValue; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + } + + public BeamGamerTag(long value) + { + _longValue = value; + _stringValue = value.ToString(); + } + + public BeamGamerTag(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + + public static implicit operator string(BeamGamerTag tag) => tag.AsString; + public static implicit operator long(BeamGamerTag tag) => tag.AsLong; + + public static implicit operator BeamGamerTag(string value) => new BeamGamerTag(value); + public static implicit operator BeamGamerTag(long value) => new BeamGamerTag(value); + public string ToJson() + { + return AsString; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(long other) + { + return other == AsLong; + } + + public bool Equals(BeamGamerTag other) + { + return other.AsLong == AsLong; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/cli/beamable.common/Runtime/Semantics/BeamPid.cs b/cli/beamable.common/Runtime/Semantics/BeamPid.cs new file mode 100644 index 0000000000..57c6fef4df --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamPid.cs @@ -0,0 +1,46 @@ +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamPid : IBeamSemanticType, IEquatable, IEquatable + { + private string _stringValue; + + public string SemanticName => "Pid"; + + public string AsString + { + get => _stringValue ?? string.Empty; + set => _stringValue = value; + } + + public BeamPid(string value) + { + _stringValue = value; + } + + public static implicit operator string(BeamPid id) => id.AsString; + public static implicit operator BeamPid(string value) => new BeamPid(value); + public string ToJson() + { + return $"\"{AsString}\""; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamPid other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/cli/beamable.common/Runtime/Semantics/BeamStats.cs b/cli/beamable.common/Runtime/Semantics/BeamStats.cs new file mode 100644 index 0000000000..fa007d2208 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamStats.cs @@ -0,0 +1,59 @@ +using System; +using Beamable.Common.Api.Stats; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamStats : IBeamSemanticType, IEquatable, IEquatable + { + private string _value; + + public string SemanticName => "StatsType"; + + public string AsString + { + get => _value ?? string.Empty; + set => _value = value; + } + + public BeamStats(StatsDomainType domainType, StatsAccessType accessType, long userId) + { + _value = StatsApiHelper.GeneratePrefix(domainType, accessType, userId); + } + + public BeamStats(string value) + { + _value = value; + } + + public static implicit operator string(BeamStats stats) => stats.AsString; + + public static implicit operator BeamStats((StatsDomainType, StatsAccessType, long) tuple) + { + return new BeamStats(StatsApiHelper.GeneratePrefix(tuple.Item1, tuple.Item2, tuple.Item3)); + } + + public static implicit operator BeamStats(string value) => new BeamStats(value); + + public string ToJson() + { + return $"\"{AsString}\""; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamStats other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/cli/beamable.common/Runtime/Semantics/IBeamSemanticType.cs b/cli/beamable.common/Runtime/Semantics/IBeamSemanticType.cs new file mode 100644 index 0000000000..e362105323 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/IBeamSemanticType.cs @@ -0,0 +1,13 @@ +using Beamable.Serialization.SmallerJSON; + +namespace Beamable.Common.Semantics +{ + public interface IBeamSemanticType : IRawJsonProvider + { + string SemanticName { get; } + } + + public interface IBeamSemanticType : IBeamSemanticType + { + } +} diff --git a/cli/beamable.common/Runtime/Semantics/ServiceName.cs b/cli/beamable.common/Runtime/Semantics/ServiceName.cs index 8767357e31..5e6b27ffe6 100644 --- a/cli/beamable.common/Runtime/Semantics/ServiceName.cs +++ b/cli/beamable.common/Runtime/Semantics/ServiceName.cs @@ -4,10 +4,13 @@ namespace Beamable.Common.Semantics { - [CliContractType] - public struct ServiceName + [CliContractType, Serializable] + public struct ServiceName : IBeamSemanticType, IEquatable, IEquatable { public string Value { get; } + + public string SemanticName => "ServiceName"; + public ServiceName(string value) { // if we do not set the value skip checks @@ -27,10 +30,28 @@ public ServiceName(string value) } public static implicit operator string(ServiceName d) => d.Value; + public static implicit operator ServiceName(string s) => new ServiceName(s); + + public bool Equals(string other) + { + return other == Value; + } + + public bool Equals(ServiceName other) + { + return other.Value == Value; + } public override string ToString() { return Value; } + + public string ToJson() + { + return $"\"{Value}\""; + } + + public Type OpenApiType => typeof(string); } } diff --git a/cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs b/cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs index d02732cf4d..3d49d79a14 100644 --- a/cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs +++ b/cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs @@ -77,6 +77,35 @@ public static object Deserialize(string json) return obj; } } + + /// + /// Deserialize JSON into a concrete type. Unlike Unity JsonUtility, this supports + /// custom semantic wrapper types (ex: types implementing IBeamSemanticType) + /// even when JSON contains only the primitive token (string/number). + /// + public static T Deserialize(string json) + { + return (T)Deserialize(json, typeof(T)); + } + + /// + /// Deserialize JSON into a concrete type (runtime type overload). + /// + public static object Deserialize(string json, Type targetType) + { + if (json == null) return null; + + // First parse into SmallerJSON's object model (ArrayDict/List/primitives). + object root; + using (var parser = new StringBasedParser(json)) + { + root = parser.ParseValue(); + } + + // Then materialize into the requested type. + return ObjectMapper.ConvertToType(root, targetType); + } + public static bool IsValidJson(string strInput) { @@ -1049,5 +1078,202 @@ 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 + private const string BeamSemanticInterfaceFullName = "Beamable.Common.Semantics.IBeamSemanticType`1"; + + public static object ConvertToType(object node, Type targetType) + { + if (targetType == null) throw new ArgumentNullException(nameof(targetType)); + + // Null handling + if (node == null) + { + targetType = Nullable.GetUnderlyingType(targetType) ?? targetType; + return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; + } + + // Unwrap nullable + targetType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + // Semantic types: construct from primitive JSON tokens + if (TryGetSemanticPrimitiveType(targetType, out var primitiveType)) + { + return ConstructSemantic(targetType, primitiveType, node); + } + + // Direct assign + if (targetType.IsInstanceOfType(node)) + { + return node; + } + + // Enums + if (targetType.IsEnum) + { + if (node is string s) return Enum.Parse(targetType, s, ignoreCase: true); + if (node is long l) return Enum.ToObject(targetType, (int)l); + } + + // Common scalars + if (targetType == typeof(string)) return node.ToString(); + if (targetType == typeof(bool)) return Convert.ToBoolean(node); + if (targetType == typeof(long)) return Convert.ToInt64(node); + if (targetType == typeof(int)) return Convert.ToInt32(node); + if (targetType == typeof(double)) return Convert.ToDouble(node); + if (targetType == typeof(float)) return Convert.ToSingle(node); + if (targetType == typeof(decimal)) return Convert.ToDecimal(node); + + // Dictionaries (only generic Dictionary supported here) + if (typeof(IDictionary).IsAssignableFrom(targetType) && targetType.IsGenericType) + { + var genArgs = targetType.GetGenericArguments(); + if (genArgs.Length == 2 && genArgs[0] == typeof(string) && node is ArrayDict dictNode) + { + var valueType = genArgs[1]; + var dict = (IDictionary)Activator.CreateInstance(targetType); + foreach (var kvp in dictNode) + { + dict[kvp.Key] = ConvertToType(kvp.Value, valueType); + } + return dict; + } + } + + // Arrays + if (targetType.IsArray && node is IList listNodeForArray) + { + var elemType = targetType.GetElementType(); + var arr = Array.CreateInstance(elemType!, listNodeForArray.Count); + for (int i = 0; i < listNodeForArray.Count; i++) + { + arr.SetValue(ConvertToType(listNodeForArray[i], elemType!), i); + } + return arr; + } + + // Lists + if (typeof(IList).IsAssignableFrom(targetType) && node is IList listNode) + { + var elemType = targetType.IsGenericType ? targetType.GetGenericArguments()[0] : typeof(object); + var list = (IList)Activator.CreateInstance(targetType); + foreach (var elem in listNode) + { + list.Add(ConvertToType(elem, elemType)); + } + return list; + } + + // POCO object + if (node is ArrayDict objNode) + { + var instance = Activator.CreateInstance(targetType); + + // Mirror existing Serializer rules: fields only, allow [SerializeField] privates, + // skip [NonSerialized] and skip compiler generated backing fields. + var fields = targetType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + for (int i = 0; i < fields.Length; i++) + { + var field = fields[i]; + + var hasSerializeField = field.GetCustomAttribute(SerializeFieldType) != null; + var isGeneratedByCompiler = field.GetCustomAttribute(CompilerGeneratedType) != null; + var canBeSerialized = (hasSerializeField || !field.IsNotSerialized) && !isGeneratedByCompiler; + if (!canBeSerialized) continue; + + if (!objNode.TryGetValue(field.Name, out var rawValue)) continue; + + var converted = ConvertToType(rawValue, field.FieldType); + field.SetValue(instance, converted); + } + + // If the target uses Unity callbacks, respect them (parity with existing Serializer behavior) + if (instance is ISerializationCallbackReceiver receiver) + { + receiver.OnAfterDeserialize(); + } + + return instance; + } + + throw new InvalidOperationException( + $"SmallerJSON cannot convert node type [{node.GetType().FullName}] to [{targetType.FullName}]."); + } + + private static bool TryGetSemanticPrimitiveType(Type semanticType, out Type primitiveType) + { + var iface = semanticType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition().FullName == BeamSemanticInterfaceFullName); + + if (iface == null) + { + primitiveType = null; + return false; + } + + primitiveType = iface.GetGenericArguments()[0]; + return true; + } + + private static object ConstructSemantic(Type semanticType, Type primitiveType, object node) + { + // Prefer ctor(string) when JSON is a string. Many long-backed semantic types accept string too + // and preserve formatting (leading zeros etc) or validate (ServiceName). + if (node is string s) + { + var stringCtor = semanticType.GetConstructor(new[] { typeof(string) }); + if (stringCtor != null) return stringCtor.Invoke(new object[] { s }); + + if (primitiveType == typeof(long)) + { + if (!long.TryParse(s, System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, out var parsed)) + { + throw new FormatException($"Cannot parse '{s}' as long for semantic type [{semanticType.FullName}]."); + } + + var longCtor = semanticType.GetConstructor(new[] { typeof(long) }); + if (longCtor != null) return longCtor.Invoke(new object[] { parsed }); + } + + throw new MissingMethodException( + $"Semantic type [{semanticType.FullName}] has no public ctor(string) and cannot be built from JSON string."); + } + + // If JSON is numeric and semantic primitive is long + if (primitiveType == typeof(long)) + { + var l = Convert.ToInt64(node, System.Globalization.CultureInfo.InvariantCulture); + var longCtor = semanticType.GetConstructor(new[] { typeof(long) }); + if (longCtor != null) return longCtor.Invoke(new object[] { l }); + + // Some semantic types might only expose ctor(string); allow numeric -> string as fallback. + var stringCtor = semanticType.GetConstructor(new[] { typeof(string) }); + if (stringCtor != null) return stringCtor.Invoke(new object[] { l.ToString(System.Globalization.CultureInfo.InvariantCulture) }); + + throw new MissingMethodException($"Semantic type [{semanticType.FullName}] must have ctor(long) or ctor(string)."); + } + + // If semantic primitive is string, coerce to string + if (primitiveType == typeof(string)) + { + var stringCtor = semanticType.GetConstructor(new[] { typeof(string) }); + if (stringCtor == null) + throw new MissingMethodException($"Semantic type [{semanticType.FullName}] must have ctor(string)."); + + return stringCtor.Invoke(new object[] { node.ToString() }); + } + + throw new NotSupportedException( + $"Semantic type [{semanticType.FullName}] uses unsupported primitive [{primitiveType.FullName}]."); + } + } + } } diff --git a/cli/cli/App.cs b/cli/cli/App.cs index f98e7cbae2..b67a3775f9 100644 --- a/cli/cli/App.cs +++ b/cli/cli/App.cs @@ -595,6 +595,10 @@ public virtual void Configure( Commands.AddSubCommand(); Commands.AddSubCommand(); + Commands.AddSubCommand(); + Commands.AddSubCommand(); + Commands.AddSubCommand(); + Commands.AddSubCommand(); Commands.AddRootCommand(); diff --git a/cli/cli/CHANGELOG.md b/cli/cli/CHANGELOG.md index c9b3e5baeb..69cb7286d5 100644 --- a/cli/cli/CHANGELOG.md +++ b/cli/cli/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased +### Added +- New Commands `project add-replacement-type`, `project list-replacement-type`, `project remove-replacement-type` to manage Unreal replacement types. +- Semantic Types for Beamable Classes with custom serialization and deserialization + ## [7.0.0] - 2026-02-19 ### Added - `net10` support diff --git a/cli/cli/Commands/Project/AddReplacementTypeCommand.cs b/cli/cli/Commands/Project/AddReplacementTypeCommand.cs new file mode 100644 index 0000000000..daebc046cb --- /dev/null +++ b/cli/cli/Commands/Project/AddReplacementTypeCommand.cs @@ -0,0 +1,141 @@ +using System.CommandLine; +using cli.Services; +using cli.Utils; +using Markdig.Extensions.TaskLists; +using Spectre.Console; + +namespace cli.Commands.Project; + + +public class AddReplacementTypeCommandArgs : CommandArgs +{ + public string ReferenceId; + public string EngineReplacementType; + public string EngineOptionalReplacementType; + public string EngineImport; + public string UnrealProjectName; +} +public class AddReplacementTypeCommand : AppCommand, IEmptyResult +{ + public AddReplacementTypeCommand() : base("add-replacement-type", "Add a replacement type for all Unreal linked projects") + { + } + + public override void Configure() + { + AddOption(new Option("--reference-id", "The reference Id (C# class/struct name) for the replacement"), (args, i) => args.ReferenceId = i); + AddOption(new Option("--replacement-type", "The name of the Type to replaced with in Unreal auto-gen"), (args, i) => args.EngineReplacementType = i); + AddOption(new Option("--optional-replacement-type", "The name of the Optional Type to replaced with in Unreal auto-gen"), (args, i) => args.EngineOptionalReplacementType = i); + AddOption(new Option("--engine-import", "The full import for the replacement type to be used in Unreal auto-gen"), (args, i) => args.EngineImport = i); + AddOption(new Option("--project-name", "The Unreal project name"), (args, i) => args.UnrealProjectName = i); + } + + public override async Task Handle(AddReplacementTypeCommandArgs args) + { + var replacementType = new ReplacementTypeInfo() + { + ReferenceId = await GetReferenceId(args), + EngineReplacementType = await GetEngineReplacementType(args), + EngineOptionalReplacementType = await GetEngineOptionalReplacementType(args), + EngineImport = await GetEngineImport(args) + }; + + var linkedEngineProjects = args.ConfigService.GetLinkedEngineProjects(); + var unrealProjects = linkedEngineProjects.unrealProjectsPaths; + + if (unrealProjects.Count == 0) + { + throw new CliException("No Unreal project linked, please link a project first with `beam project add-unreal-project `"); + } + + // Check if it has specific project name + if (!string.IsNullOrEmpty(args.UnrealProjectName)) + { + if (unrealProjects.Any(item => item.GetProjectName() == args.UnrealProjectName)) + { + var projectData = unrealProjects.FirstOrDefault(item => item.GetProjectName() == args.UnrealProjectName); + AddReplacementAndSetLinkedEngineProjects(args, linkedEngineProjects, projectData, replacementType); + } + else + { + throw new CliException($"Project {args.UnrealProjectName} not found"); + } + return; + } + + // If not, we need to check which options we have + if (unrealProjects.Count == 1 || args.Quiet) + { + var onlyItem = unrealProjects.First(); + AddReplacementAndSetLinkedEngineProjects(args, linkedEngineProjects, onlyItem, replacementType); + return; + } + + string projectSelection = GetProjectPrompt(unrealProjects); + + var selectedProject = unrealProjects.First(item => item.GetProjectName() == projectSelection); + AddReplacementAndSetLinkedEngineProjects(args, linkedEngineProjects, selectedProject, replacementType); + } + + private static void AddReplacementAndSetLinkedEngineProjects(AddReplacementTypeCommandArgs args, + EngineProjectData linkedEngineProjects, EngineProjectData.Unreal onlyItem, ReplacementTypeInfo replacementType) + { + var array = onlyItem.ReplacementTypeInfos ?? Array.Empty(); + linkedEngineProjects.unrealProjectsPaths.Remove(onlyItem); + onlyItem.ReplacementTypeInfos = array.Append(replacementType).ToArray(); + linkedEngineProjects.unrealProjectsPaths.Add(onlyItem); + args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + AnsiConsole.MarkupLine($"Added replacement type [green]{replacementType.ReferenceId}[/] for [green]{onlyItem.GetProjectName()}[/]. Make sure to add the Replacement Type Under your [green]Plugins/BeamableUnrealMicroserviceClients/Source/CustomReplacementTypes[/] folder"); + } + + private static string GetProjectPrompt(HashSet unrealProjects) + { + var projectSelection = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Which [green]project[/] do you want to add the Replacement Type?") + .AddChoices(unrealProjects.Select(item => item.GetProjectName())) + .AddBeamHightlight() + ); + return projectSelection; + } + + private Task GetReferenceId(AddReplacementTypeCommandArgs args) + { + if (string.IsNullOrEmpty(args.ReferenceId)) + { + return Task.FromResult(AnsiConsole.Prompt( + new TextPrompt("Please enter the Replacement [green]Reference Id[/] (Use the same name as in the OpenApi Specs):").PromptStyle("green"))); + } + return Task.FromResult(args.ReferenceId); + } + + private Task GetEngineReplacementType(AddReplacementTypeCommandArgs args) + { + if (string.IsNullOrEmpty(args.EngineReplacementType)) + { + return Task.FromResult(AnsiConsole.Prompt( + new TextPrompt("Please enter the [green]Engine Replacement Type[/]:").PromptStyle("green"))); + } + return Task.FromResult(args.EngineReplacementType); + } + + private Task GetEngineOptionalReplacementType(AddReplacementTypeCommandArgs args) + { + if (string.IsNullOrEmpty(args.EngineOptionalReplacementType)) + { + return Task.FromResult(AnsiConsole.Prompt( + new TextPrompt("Please enter the [green]Engine Optional Replacement Type[/]:").PromptStyle("green"))); + } + return Task.FromResult(args.EngineOptionalReplacementType); + } + + private Task GetEngineImport(AddReplacementTypeCommandArgs args) + { + if (string.IsNullOrEmpty(args.EngineImport)) + { + return Task.FromResult(AnsiConsole.Prompt( + new TextPrompt("Please enter the [green]Engine Import[/]:").PromptStyle("green"))); + } + return Task.FromResult(args.EngineImport); + } +} diff --git a/cli/cli/Commands/Project/GenerateClientFileCommand.cs b/cli/cli/Commands/Project/GenerateClientFileCommand.cs index 2439af2cd0..2d48f5aeb3 100644 --- a/cli/cli/Commands/Project/GenerateClientFileCommand.cs +++ b/cli/cli/Commands/Project/GenerateClientFileCommand.cs @@ -43,6 +43,50 @@ public class GenerateClientFileCommand : AppCommand , IResultSteam { + private static readonly Dictionary BaseReplacementTypeInfos = new() + { + { + "ClientPermission", new ReplacementTypeInfo + { + ReferenceId = "ClientPermission", + EngineReplacementType = "FBeamClientPermission", + EngineOptionalReplacementType = + $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamClientPermission", + EngineImport = @"#include ""BeamBackend/ReplacementTypes/BeamClientPermission.h""", + } + }, + { + "ExternalIdentity", new ReplacementTypeInfo + { + ReferenceId = "ExternalIdentity", + EngineReplacementType = "FBeamExternalIdentity", + EngineOptionalReplacementType = + $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamExternalIdentity", + EngineImport = @"#include ""BeamBackend/ReplacementTypes/BeamExternalIdentity.h""", + } + }, + { + "Tag", new ReplacementTypeInfo + { + ReferenceId = "Tag", + EngineReplacementType = "FBeamTag", + EngineOptionalReplacementType = $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamTag", + EngineImport = @"#include ""BeamBackend/ReplacementTypes/BeamTag.h""", + } + }, + { + "ClientContentInfoJson", new ReplacementTypeInfo() + { + ReferenceId = "ClientContentInfoJson", + EngineReplacementType = "FBeamRemoteContentManifestEntry", + EngineOptionalReplacementType = + $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamRemoteContentManifestEntry", + EngineImport = + @"#include ""BeamBackend/ReplacementTypes/BeamRemoteContentManifestEntry.h""", + } + } + }; + public override bool IsForInternalUse => true; public GenerateClientFileCommand() : base("generate-client", @@ -263,6 +307,7 @@ public override async Task Handle(GenerateClientFileCommandArgs args) // Handle Unreal code-gen var hasUnrealLinkedProjects = args.outputToLinkedProjects && args.ProjectService.GetLinkedUnrealProjects().Count > 0; + const string customReplacementTypesFolderName = "CustomReplacementTypes"; if (hasUnrealLinkedProjects) { // Check if the microservice projects are built and that the OAPI exists. @@ -313,53 +358,19 @@ public override async Task Handle(GenerateClientFileCommandArgs args) UnrealSourceGenerator.previousGenerationPassesData = JsonConvert.DeserializeObject(File.ReadAllText(previousGenerationFilePath)); UnrealSourceGenerator.currentGenerationPassDataFilePath = $"{unrealProjectData.CoreProjectName}_GenerationPass"; + + var replacementTypes = new Dictionary(BaseReplacementTypeInfos); + + foreach (ReplacementTypeInfo replacementTypeInfo in unrealProjectData.ReplacementTypeInfos) + { + replacementTypes.TryAdd(replacementTypeInfo.ReferenceId, replacementTypeInfo); + } + var unrealFileDescriptors = unrealGenerator.Generate(new SwaggerService.DefaultGenerationContext { Documents = docs, OrderedSchemas = orderedSchemas, - ReplacementTypes = new Dictionary - { - { - "ClientPermission", new ReplacementTypeInfo - { - ReferenceId = "ClientPermission", - EngineReplacementType = "FBeamClientPermission", - EngineOptionalReplacementType = - $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamClientPermission", - EngineImport = @"#include ""BeamBackend/ReplacementTypes/BeamClientPermission.h""", - } - }, - { - "ExternalIdentity", new ReplacementTypeInfo - { - ReferenceId = "ExternalIdentity", - EngineReplacementType = "FBeamExternalIdentity", - EngineOptionalReplacementType = - $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamExternalIdentity", - EngineImport = @"#include ""BeamBackend/ReplacementTypes/BeamExternalIdentity.h""", - } - }, - { - "Tag", new ReplacementTypeInfo - { - ReferenceId = "Tag", - EngineReplacementType = "FBeamTag", - EngineOptionalReplacementType = $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamTag", - EngineImport = @"#include ""BeamBackend/ReplacementTypes/BeamTag.h""", - } - }, - { - "ClientContentInfoJson", new ReplacementTypeInfo() - { - ReferenceId = "ClientContentInfoJson", - EngineReplacementType = "FBeamRemoteContentManifestEntry", - EngineOptionalReplacementType = - $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamRemoteContentManifestEntry", - EngineImport = - @"#include ""BeamBackend/ReplacementTypes/BeamRemoteContentManifestEntry.h""", - } - } - } + ReplacementTypes = replacementTypes, }); Log.Verbose($"completed in-memory generation of clients for project {unrealProjectData.CoreProjectName} path=[{unrealProjectData.Path}], total ms {sw.ElapsedMilliseconds}"); @@ -397,7 +408,12 @@ public override async Task Handle(GenerateClientFileCommandArgs args) ""Name"": ""{unrealProjectData.BlueprintNodesProjectName}"", ""Type"": ""UncookedOnly"", ""LoadingPhase"": ""Default"" - }} + }}, + {{ + ""Name"": ""{customReplacementTypesFolderName}"", + ""Type"": ""Runtime"", + ""LoadingPhase"": ""Default"" + }}, ], ""Plugins"": [ {{ @@ -700,6 +716,13 @@ class F{unrealProjectData.BlueprintNodesProjectName}Module : public IModuleInter } await Task.WhenAll(writeFiles); + + var replacementTypeFolder = new DirectoryInfo(Path.Join(outputDir, "Source", customReplacementTypesFolderName)); + if (!replacementTypeFolder.Exists) + { + Directory.CreateDirectory(replacementTypeFolder.FullName); + } + Log.Verbose($"completed writing auto-generated files to disk {unrealProjectData.CoreProjectName} path=[{unrealProjectData.Path}], total ms {sw.ElapsedMilliseconds}"); // Run the Regenerate Project Files utility for the project (so that create files are automatically updated in IDEs). diff --git a/cli/cli/Commands/Project/ListReplacementTypeCommand.cs b/cli/cli/Commands/Project/ListReplacementTypeCommand.cs new file mode 100644 index 0000000000..15ef8266bf --- /dev/null +++ b/cli/cli/Commands/Project/ListReplacementTypeCommand.cs @@ -0,0 +1,65 @@ +using System.CommandLine; +using cli.Services; +using cli.Utils; +using Markdig.Extensions.TaskLists; +using Spectre.Console; + +namespace cli.Commands.Project; + + +public class ListReplacementTypeCommandArgs : CommandArgs +{ + public string UnrealProjectName; +} + +public class ListReplacementTypeCommandOutput +{ + public List ReplacementsTypes; +} +public class ListReplacementTypeCommand : AtomicCommand +{ + public ListReplacementTypeCommand() : base("list-replacement-type", "Add a replacement type for all Unreal linked projects") + { + } + + public override void Configure() + { + AddOption(new Option("--project-name", "The Unreal project name"), (args, i) => args.UnrealProjectName = i); + } + + public override async Task GetResult(ListReplacementTypeCommandArgs args) + { + var linkedEngineProjects = args.ConfigService.GetLinkedEngineProjects(); + var unrealProjects = linkedEngineProjects.unrealProjectsPaths; + + if (unrealProjects.Count == 0) + { + throw new CliException("No Unreal project linked, please link a project first with `beam project add-unreal-project `"); + } + if (unrealProjects.Count == 1 || args.Quiet) + { + var onlyItem = unrealProjects.First(); + return new ListReplacementTypeCommandOutput { ReplacementsTypes = onlyItem.ReplacementTypeInfos.ToList() }; + } + + var projectName = GetProjectName(args, unrealProjects); + var selectedProject = unrealProjects.First(item => item.GetProjectName() == projectName); + return new ListReplacementTypeCommandOutput { ReplacementsTypes = selectedProject.ReplacementTypeInfos.ToList() }; + } + + private static string GetProjectName(ListReplacementTypeCommandArgs args, HashSet unrealProjects) + { + if (!string.IsNullOrEmpty(args.UnrealProjectName)) + { + return args.UnrealProjectName; + } + + string projectSelection = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Which [green]project[/] do you want to add the Replacement Type?") + .AddChoices(unrealProjects.Select(item => item.GetProjectName())) + .AddBeamHightlight() + ); + return projectSelection; + } +} diff --git a/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs b/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs new file mode 100644 index 0000000000..d591ae62d5 --- /dev/null +++ b/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs @@ -0,0 +1,93 @@ +using System.CommandLine; +using cli.Services; +using cli.Utils; +using Markdig.Extensions.TaskLists; +using Spectre.Console; + +namespace cli.Commands.Project; + + +public class RemoveReplacementTypeCommandArgs : CommandArgs +{ + public string ReferenceId; + public string UnrealProjectName; +} +public class RemoveReplacementTypeCommand : AppCommand, IEmptyResult +{ + public RemoveReplacementTypeCommand() : base("remove-replacement-type", "Add a replacement type for all Unreal linked projects") + { + } + + public override void Configure() + { + AddOption(new Option("--reference-id", "The reference Id (C# class/struct name) for the replacement"), (args, i) => args.ReferenceId = i); + AddOption(new Option("--project-name", "The Unreal project name"), (args, i) => args.UnrealProjectName = i); + } + + public override async Task Handle(RemoveReplacementTypeCommandArgs args) + { + + var referenceId = await GetReferenceId(args); + + var linkedEngineProjects = args.ConfigService.GetLinkedEngineProjects(); + var unrealProjects = linkedEngineProjects.unrealProjectsPaths; + + if (unrealProjects.Count == 0) + { + throw new CliException("No Unreal project linked, please link a project first with `beam project add-unreal-project `"); + } + + // Check if it has specific project name + if (!string.IsNullOrEmpty(args.UnrealProjectName)) + { + if (unrealProjects.Any(item => item.GetProjectName() == args.UnrealProjectName)) + { + var projectData = unrealProjects.FirstOrDefault(item => item.GetProjectName() == args.UnrealProjectName); + RemoveReferenceAndSetLinkedEngineProjects(args, linkedEngineProjects, projectData, referenceId); + } + else + { + throw new CliException($"Project {args.UnrealProjectName} not found"); + } + return; + } + + // If not, we need to check which options we have + if (unrealProjects.Count == 1 || args.Quiet) + { + var onlyItem = unrealProjects.First(); + RemoveReferenceAndSetLinkedEngineProjects(args, linkedEngineProjects, onlyItem, referenceId); + return; + } + + var projectSelection = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Which [green]project[/] do you want to add the Replacement Type?") + .AddChoices(unrealProjects.Select(item => item.GetProjectName())) + .AddBeamHightlight() + ); + + var selectedProject = unrealProjects.First(item => item.GetProjectName() == projectSelection); + RemoveReferenceAndSetLinkedEngineProjects(args, linkedEngineProjects, selectedProject, referenceId); + } + + private static void RemoveReferenceAndSetLinkedEngineProjects(RemoveReplacementTypeCommandArgs args, + EngineProjectData linkedEngineProjects, EngineProjectData.Unreal projectData, string referenceId) + { + var array = projectData.ReplacementTypeInfos ?? Array.Empty(); + linkedEngineProjects.unrealProjectsPaths.Remove(projectData); + projectData.ReplacementTypeInfos = array.Where(item => item.ReferenceId != referenceId).ToArray(); + linkedEngineProjects.unrealProjectsPaths.Add(projectData); + args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + } + + private Task GetReferenceId(RemoveReplacementTypeCommandArgs args) + { + if (string.IsNullOrEmpty(args.ReferenceId)) + { + return Task.FromResult(AnsiConsole.Prompt( + new TextPrompt("Please enter the Replacement [green]Reference Id[/]:").PromptStyle("green"))); + } + return Task.FromResult(args.ReferenceId); + } +} diff --git a/cli/cli/Contants.cs b/cli/cli/Contants.cs index a9cf3a6daf..48dccf66eb 100644 --- a/cli/cli/Contants.cs +++ b/cli/cli/Contants.cs @@ -19,7 +19,7 @@ public static class Constants /// /// OpenAPI extension that describes the semantic type of a primitive field. /// - public const string EXTENSION_BEAMABLE_SEMANTIC_TYPE = "x-beamable-semantic-type"; + public const string EXTENSION_BEAMABLE_SEMANTIC_TYPE = Beamable.Common.Constants.Features.Services.SCHEMA_SEMANTIC_TYPE_NAME_KEY; /// /// OpenAPI extension, added here as a , diff --git a/cli/cli/Services/ConfigService.cs b/cli/cli/Services/ConfigService.cs index bde1060212..c0cafdee3f 100644 --- a/cli/cli/Services/ConfigService.cs +++ b/cli/cli/Services/ConfigService.cs @@ -1797,6 +1797,10 @@ public override bool Equals(object obj) => (obj is Unreal unity && Equals(unity) [Serializable] public struct Unreal : IEquatable, IEquatable { + + public const string CORE_NAME_SUFFIX = "MicroserviceClients"; + public const string BP_CORE_NAME_SUFFIX = "MicroserviceClientsBp"; + /// /// Name for the project's core module (the module every other module has access to). /// This will be used to generate the ______API UE Macros for the generated types. @@ -1846,6 +1850,13 @@ public struct Unreal : IEquatable, IEquatable /// public string BeamableBackendGenerationPassFile; + public ReplacementTypeInfo[] ReplacementTypeInfos; + + public string GetProjectName() + { + return CoreProjectName.Remove(CoreProjectName.Length - CORE_NAME_SUFFIX.Length); + } + public bool Equals(string other) => Path.Equals(other); public bool Equals(Unreal other) => Path == other.Path; @@ -1856,5 +1867,10 @@ public override bool Equals(object obj) => (obj is Unreal unreal && Equals(unrea public static bool operator ==(Unreal left, Unreal right) => left.Equals(right); public static bool operator !=(Unreal left, Unreal right) => !(left == right); + + public static string GetCoreName(string projectName) => $"{projectName}{CORE_NAME_SUFFIX}"; + public static string GetBlueprintNodesProjectName(string projectName) => $"{projectName}{BP_CORE_NAME_SUFFIX}"; + + } } diff --git a/cli/cli/Services/UnityOpenApiSourceGenerator/OpenApiClientCodeGenerator.cs b/cli/cli/Services/UnityOpenApiSourceGenerator/OpenApiClientCodeGenerator.cs index cad39b49f8..0b315a68f6 100644 --- a/cli/cli/Services/UnityOpenApiSourceGenerator/OpenApiClientCodeGenerator.cs +++ b/cli/cli/Services/UnityOpenApiSourceGenerator/OpenApiClientCodeGenerator.cs @@ -1,13 +1,19 @@ using Beamable.Common; using Beamable.Common.Dependencies; +using Beamable.Common.Semantics; using Beamable.Server.Common; +using Beamable.Tooling.Common.OpenAPI; using Beamable.Tooling.Common.OpenAPI.Utils; using cli; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; +using System; using System.CodeDom; using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Text; using TypeAttributes = System.Reflection.TypeAttributes; using ServiceConstants = Beamable.Common.Constants.Features.Services; @@ -434,9 +440,44 @@ private void AddCallableMethod(CodeTypeDeclaration targetClass, string methodNam targetClass.Members.Add(genMethod); } + private bool IsSemanticType(OpenApiSchema schema, out string semanticType) + { + semanticType = string.Empty; + if (!schema.Extensions.TryGetValue(ServiceConstants.SCHEMA_SEMANTIC_TYPE_NAME_KEY, + out var semanticTypeName) || semanticTypeName is not OpenApiString semanticTypeNameStr) + { + return false; + } + semanticType = semanticTypeNameStr.Value; + return true; + } + private string GetParsedType(OpenApiSchema schema, bool useFullName = false) { + if (IsSemanticType(schema, out var semanticType)) + { + switch (semanticType) + { + case "Cid": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamCid)); + case "Pid": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamPid)); + case "AccountId": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamAccountId)); + case "GamerTag": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamGamerTag)); + case "ContentManifestId": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamContentManifestId)); + case "ContentId": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamContentId)); + case "StatsType": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamStats)); + case "ServiceName": + return SchemaGenerator.GetQualifiedReferenceName(typeof(ServiceName)); + } + } + if (schema.Extensions.TryGetValue(ServiceConstants.MICROSERVICE_EXTENSION_BEAMABLE_FORCE_TYPE_NAME, out var extensionType) && extensionType is OpenApiString forcedTypeName) { @@ -554,6 +595,49 @@ private string GetParameterTypeNameBase(OpenApiSchema schema, bool isNullable) private string GetParameterTypeName(OpenApiSchema schema) { + if (!IsSemanticType(schema, out var semanticType)) + { + return (schema.Type, schema.Format) switch + { + ("integer", "int16") => typeof(short).GetTypeString(), + ("integer", "int32") => typeof(int).GetTypeString(), + ("integer", "int64") => typeof(long).GetTypeString(), + ("integer", _) => typeof(int).GetTypeString(), + ("number", "float") => typeof(float).GetTypeString(), + ("number", "double") => typeof(double).GetTypeString(), + ("number", "decimal") => typeof(decimal).GetTypeString(), + ("number", _) => typeof(decimal).GetTypeString(), + ("string", "date") => typeof(DateTime).GetTypeString(), + ("string", "date-time") => typeof(DateTime).GetTypeString(), + ("string", "uuid") => typeof(Guid).GetTypeString(), + ("string", "byte") => typeof(byte).GetTypeString(), + ("string", _) => typeof(string).GetTypeString(), + ("boolean", _) => typeof(bool).GetTypeString(), + ("array", _) => GetParameterArrayTypeName(schema), + _ => GetObjectType(schema) + }; + } + + switch (semanticType) + { + case "Cid": + return nameof(BeamCid); + case "Pid": + return nameof(BeamPid); + case "AccountId": + return nameof(BeamAccountId); + case "GamerTag": + return nameof(BeamGamerTag); + case "ContentManifestId": + return nameof(BeamContentManifestId); + case "ContentId": + return nameof(BeamContentId); + case "StatsType": + return nameof(BeamStats); + case "ServiceName": + return nameof(ServiceName); + } + return (schema.Type, schema.Format) switch { ("integer", "int16") => typeof(short).GetTypeString(), diff --git a/client/Packages/com.beamable/Common/Runtime/Constants/Implementations/ServiceConstants.cs b/client/Packages/com.beamable/Common/Runtime/Constants/Implementations/ServiceConstants.cs index 531cc17816..5657c5c2c6 100644 --- a/client/Packages/com.beamable/Common/Runtime/Constants/Implementations/ServiceConstants.cs +++ b/client/Packages/com.beamable/Common/Runtime/Constants/Implementations/ServiceConstants.cs @@ -82,6 +82,11 @@ public static partial class Services public const string PATH_CALLABLE_METHOD_NAME_KEY = "x-beamable-callable-method-name"; public const string PATH_CALLABLE_METHOD_CLIENT_PREFIX_KEY = "x-beamable-route-source-client-prefix"; + /// + /// OpenAPI extension that describes the semantic type of a primitive field. + /// + public const string SCHEMA_SEMANTIC_TYPE_NAME_KEY = "x-beamable-semantic-type"; + public const string MICROSERVICE_FEDERATED_COMPONENTS_V2_INTERFACE_KEY = "interface"; public const string MICROSERVICE_FEDERATED_COMPONENTS_V2_FEDERATION_ID_KEY = "federationId"; public const string MICROSERVICE_FEDERATED_COMPONENTS_V2_FEDERATION_CLASS_NAME_KEY = "federationClassName"; diff --git a/client/Packages/com.beamable/Common/Runtime/Environment/PackageVersion.cs b/client/Packages/com.beamable/Common/Runtime/Environment/PackageVersion.cs index 3178356495..c704d4ef31 100644 --- a/client/Packages/com.beamable/Common/Runtime/Environment/PackageVersion.cs +++ b/client/Packages/com.beamable/Common/Runtime/Environment/PackageVersion.cs @@ -81,6 +81,11 @@ public class PackageVersion /// public int? RC => IsReleaseCandidate ? _rc : default; + /// + /// Whether this version is one of our locally built versions relative to our internal workflow. + /// + public bool IsLocalDev => ToString().StartsWith("0.0.123"); + public PackageVersion(int major, int minor, int patch, int rc = -1, long nightlyTime = -1, bool isPreview = false, bool isExperimental = false) { _major = major; diff --git a/client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs b/client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs new file mode 100644 index 0000000000..90050afcf7 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs @@ -0,0 +1,212 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Beamable.Common.Pooling; +using UnityEngine; + +namespace Beamable.Serialization +{ + public static class BeamMessagePack + { + public static byte[] Serialize(JsonSerializable.ISerializable serializable) + { + var stream = new MessagePackSerializerStream(); + serializable.Serialize(stream); + + return stream._bytes.ToArray(); + } + } + + public class MessagePackSerializerStream : JsonSerializable.IStreamSerializer + { + public List _bytes = new List(1000); + + public bool isSaving { get; } + public bool isLoading { get; } + public object GetValue(string key) + { + throw new NotImplementedException(); + } + + public void SetValue(string key, object value) + { + throw new NotImplementedException(); + } + + public bool HasKey(string key) + { + throw new NotImplementedException(); + } + + public JsonSerializable.ListMode Mode { get; } + public bool SerializeNestedJson(string key, ref JsonString jsonString) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref IDictionary target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref bool target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref bool? target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref int target) + { + // throw new NotImplementedException(); + return true; + } + + public bool Serialize(string key, ref int? target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref long target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref long? target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref ulong target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref ulong? target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref float target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref float? target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref double target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref double? target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref string target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Guid target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref StringBuilder target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref DateTime target, params string[] formats) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Rect target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Vector2 target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Vector3 target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Vector4 target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Color target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Quaternion target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Gradient target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref T value) where T : JsonSerializable.ISerializable + { + throw new NotImplementedException(); + } + + public bool SerializeInline(string key, ref T value) where T : JsonSerializable.ISerializable + { + throw new NotImplementedException(); + } + + public bool SerializeList(string key, ref TList value) where TList : IList, new() + { + throw new NotImplementedException(); + } + + public bool SerializeKnownList(string key, ref List value) where TElem : JsonSerializable.ISerializable, new() + { + throw new NotImplementedException(); + } + + public bool SerializeArray(string key, ref T[] value) + { + throw new NotImplementedException(); + } + + public bool SerializeDictionary(string key, ref Dictionary target) + { + throw new NotImplementedException(); + } + + public bool SerializeDictionary(string key, ref TDict target) where TDict : IDictionary, new() + { + throw new NotImplementedException(); + } + + public bool SerializeILL(string key, ref LinkedList list) where T : ClassPool, new() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs.meta b/client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs.meta new file mode 100644 index 0000000000..aa9cb8115c --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 80b70a8446bc7d3a881cedef86cb9300 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs new file mode 100644 index 0000000000..f63a647cb5 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs @@ -0,0 +1,91 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using Beamable.Common.BeamCli; +using System; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamAccountId : IBeamSemanticType, IEquatable, IEquatable, IEquatable + { + private long _longValue; + private string _stringValue; + + public string SemanticName => "AccountId"; + + public long AsLong + { + get => _longValue; + set + { + _longValue = value; + _stringValue = value.ToString(); + } + } + + public string AsString + { + get => string.IsNullOrEmpty(_stringValue) ? _longValue.ToString() : _stringValue; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + } + + public BeamAccountId(long value) + { + _longValue = value; + _stringValue = value.ToString(); + } + + public BeamAccountId(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + + public static implicit operator string(BeamAccountId id) => id.AsString; + public static implicit operator long(BeamAccountId id) => id.AsLong; + + public static implicit operator BeamAccountId(string value) => new BeamAccountId(value); + public static implicit operator BeamAccountId(long value) => new BeamAccountId(value); + public string ToJson() + { + return AsString; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(long other) + { + return other == AsLong; + } + + public bool Equals(BeamAccountId other) + { + return other.AsLong == AsLong; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs.meta new file mode 100644 index 0000000000..ec5fc38126 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ce2f2d73de9b7005aba6633a37c270e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs new file mode 100644 index 0000000000..6dbf8aa59e --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs @@ -0,0 +1,89 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamCid : IBeamSemanticType, IEquatable, IEquatable, IEquatable + { + private long _longValue; + private string _stringValue; + + public string SemanticName => "Cid"; + + public long AsLong { + get => _longValue; + set { + _longValue = value; + _stringValue = value.ToString(); + } + } + + public string AsString + { + get => string.IsNullOrEmpty(_stringValue) ? _longValue.ToString() : _stringValue; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + } + + public BeamCid(long value) + { + _longValue = value; + _stringValue = value.ToString(); + } + + public BeamCid(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + + public static implicit operator string(BeamCid cid) => cid.AsString; + public static implicit operator long(BeamCid cid) => cid.AsLong; + + public static implicit operator BeamCid(string value) => new BeamCid(value); + public static implicit operator BeamCid(long value) => new BeamCid(value); + public string ToJson() + { + return AsString; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(long other) + { + return other == AsLong; + } + + public bool Equals(BeamCid other) + { + return other.AsLong == AsLong; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs.meta new file mode 100644 index 0000000000..6e1afd393d --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3cfc98afee87b127c6b2f03388554840 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs new file mode 100644 index 0000000000..4bfcbef9f4 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs @@ -0,0 +1,55 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using Beamable.Common.BeamCli; +using Beamable.Common.Content; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamContentId : IBeamSemanticType, IEquatable, IEquatable + { + private string _value; + + public string SemanticName => "ContentId"; + + public string AsString + { + get => _value ?? string.Empty; + set => _value = value; + } + + public BeamContentId(string value) + { + _value = value; + } + + public BeamContentId(ContentRef contentRef) : this(contentRef.GetId()) { } + public BeamContentId(ContentObject contentObject) : this(contentObject.Id) { } + + public static implicit operator string(BeamContentId contentId) => contentId.AsString; + public static implicit operator BeamContentId(ContentRef contentRef) => new BeamContentId(contentRef); + public static implicit operator BeamContentId(ContentObject contentObject) => new BeamContentId(contentObject); + public static implicit operator BeamContentId(string contentId) => new BeamContentId(contentId); + public string ToJson() + { + return $"\"{AsString}\""; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamContentId other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs.meta new file mode 100644 index 0000000000..7eae704b9f --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 85f785bd8a78971ca7bcb5a9658917a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs new file mode 100644 index 0000000000..f81ecc3121 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs @@ -0,0 +1,49 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamContentManifestId : IBeamSemanticType, IEquatable, IEquatable + { + private string _value; + + public string SemanticName => "ContentManifestId"; + + public string AsString + { + get => _value ?? string.Empty; + set => _value = value; + } + + public BeamContentManifestId(string value) + { + _value = value; + } + + public static implicit operator string(BeamContentManifestId id) => id.AsString; + public static implicit operator BeamContentManifestId(string value) => new BeamContentManifestId(value); + public string ToJson() + { + return $"\"{AsString}\""; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamContentManifestId other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs.meta new file mode 100644 index 0000000000..efaa8acce9 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f0402b24d33d592f9720aa3eae4ffafc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs new file mode 100644 index 0000000000..b3325e9201 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs @@ -0,0 +1,90 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamGamerTag : IBeamSemanticType, IEquatable, IEquatable, IEquatable + { + private long _longValue; + private string _stringValue; + + public string SemanticName => "GamerTag"; + + public long AsLong + { + get => _longValue; + set { + _longValue = value; + _stringValue = value.ToString(); + } + } + + public string AsString + { + get => string.IsNullOrEmpty(_stringValue) ? _longValue.ToString() : _stringValue; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + } + + public BeamGamerTag(long value) + { + _longValue = value; + _stringValue = value.ToString(); + } + + public BeamGamerTag(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + + public static implicit operator string(BeamGamerTag tag) => tag.AsString; + public static implicit operator long(BeamGamerTag tag) => tag.AsLong; + + public static implicit operator BeamGamerTag(string value) => new BeamGamerTag(value); + public static implicit operator BeamGamerTag(long value) => new BeamGamerTag(value); + public string ToJson() + { + return AsString; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(long other) + { + return other == AsLong; + } + + public bool Equals(BeamGamerTag other) + { + return other.AsLong == AsLong; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs.meta new file mode 100644 index 0000000000..700fa21ec6 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 68b6c25ddc6a884b0a5924d3e0b1137b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs new file mode 100644 index 0000000000..4a849af8d6 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs @@ -0,0 +1,49 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamPid : IBeamSemanticType, IEquatable, IEquatable + { + private string _stringValue; + + public string SemanticName => "Pid"; + + public string AsString + { + get => _stringValue ?? string.Empty; + set => _stringValue = value; + } + + public BeamPid(string value) + { + _stringValue = value; + } + + public static implicit operator string(BeamPid id) => id.AsString; + public static implicit operator BeamPid(string value) => new BeamPid(value); + public string ToJson() + { + return $"\"{AsString}\""; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamPid other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs.meta new file mode 100644 index 0000000000..f6b378a560 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 697b5c66f91cf6c780e86f3125a244a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs new file mode 100644 index 0000000000..783030e472 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs @@ -0,0 +1,62 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using Beamable.Common.Api.Stats; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamStats : IBeamSemanticType, IEquatable, IEquatable + { + private string _value; + + public string SemanticName => "StatsType"; + + public string AsString + { + get => _value ?? string.Empty; + set => _value = value; + } + + public BeamStats(StatsDomainType domainType, StatsAccessType accessType, long userId) + { + _value = StatsApiHelper.GeneratePrefix(domainType, accessType, userId); + } + + public BeamStats(string value) + { + _value = value; + } + + public static implicit operator string(BeamStats stats) => stats.AsString; + + public static implicit operator BeamStats((StatsDomainType, StatsAccessType, long) tuple) + { + return new BeamStats(StatsApiHelper.GeneratePrefix(tuple.Item1, tuple.Item2, tuple.Item3)); + } + + public static implicit operator BeamStats(string value) => new BeamStats(value); + + public string ToJson() + { + return $"\"{AsString}\""; + } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamStats other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs.meta new file mode 100644 index 0000000000..20e7eacdea --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 276d4bec5854f140d7d336ea74f90bcc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs new file mode 100644 index 0000000000..befa54be04 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs @@ -0,0 +1,16 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using Beamable.Serialization.SmallerJSON; + +namespace Beamable.Common.Semantics +{ + public interface IBeamSemanticType : IRawJsonProvider + { + string SemanticName { get; } + } + + public interface IBeamSemanticType : IBeamSemanticType + { + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs.meta new file mode 100644 index 0000000000..fffc8d3a67 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3e7bf0d59ed76ceb5837ec008a785c9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/ServiceName.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/ServiceName.cs index 1c8c122d79..c18ceb102d 100644 --- a/client/Packages/com.beamable/Common/Runtime/Semantics/ServiceName.cs +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/ServiceName.cs @@ -7,10 +7,13 @@ namespace Beamable.Common.Semantics { - [CliContractType] - public struct ServiceName + [CliContractType, Serializable] + public struct ServiceName : IBeamSemanticType, IEquatable, IEquatable { public string Value { get; } + + public string SemanticName => "ServiceName"; + public ServiceName(string value) { // if we do not set the value skip checks @@ -30,10 +33,28 @@ public ServiceName(string value) } public static implicit operator string(ServiceName d) => d.Value; + public static implicit operator ServiceName(string s) => new ServiceName(s); + + public bool Equals(string other) + { + return other == Value; + } + + public bool Equals(ServiceName other) + { + return other.Value == Value; + } public override string ToString() { return Value; } + + public string ToJson() + { + return $"\"{Value}\""; + } + + public Type OpenApiType => typeof(string); } } diff --git a/client/Packages/com.beamable/Common/Runtime/SmallerJSON/SmallerJSON.cs b/client/Packages/com.beamable/Common/Runtime/SmallerJSON/SmallerJSON.cs index a1b609d00f..176f253779 100644 --- a/client/Packages/com.beamable/Common/Runtime/SmallerJSON/SmallerJSON.cs +++ b/client/Packages/com.beamable/Common/Runtime/SmallerJSON/SmallerJSON.cs @@ -80,6 +80,35 @@ public static object Deserialize(string json) return obj; } } + + /// + /// Deserialize JSON into a concrete type. Unlike Unity JsonUtility, this supports + /// custom semantic wrapper types (ex: types implementing IBeamSemanticType) + /// even when JSON contains only the primitive token (string/number). + /// + public static T Deserialize(string json) + { + return (T)Deserialize(json, typeof(T)); + } + + /// + /// Deserialize JSON into a concrete type (runtime type overload). + /// + public static object Deserialize(string json, Type targetType) + { + if (json == null) return null; + + // First parse into SmallerJSON's object model (ArrayDict/List/primitives). + object root; + using (var parser = new StringBasedParser(json)) + { + root = parser.ParseValue(); + } + + // Then materialize into the requested type. + return ObjectMapper.ConvertToType(root, targetType); + } + public static bool IsValidJson(string strInput) { @@ -1052,5 +1081,202 @@ 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 + private const string BeamSemanticInterfaceFullName = "Beamable.Common.Semantics.IBeamSemanticType`1"; + + public static object ConvertToType(object node, Type targetType) + { + if (targetType == null) throw new ArgumentNullException(nameof(targetType)); + + // Null handling + if (node == null) + { + targetType = Nullable.GetUnderlyingType(targetType) ?? targetType; + return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; + } + + // Unwrap nullable + targetType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + // Semantic types: construct from primitive JSON tokens + if (TryGetSemanticPrimitiveType(targetType, out var primitiveType)) + { + return ConstructSemantic(targetType, primitiveType, node); + } + + // Direct assign + if (targetType.IsInstanceOfType(node)) + { + return node; + } + + // Enums + if (targetType.IsEnum) + { + if (node is string s) return Enum.Parse(targetType, s, ignoreCase: true); + if (node is long l) return Enum.ToObject(targetType, (int)l); + } + + // Common scalars + if (targetType == typeof(string)) return node.ToString(); + if (targetType == typeof(bool)) return Convert.ToBoolean(node); + if (targetType == typeof(long)) return Convert.ToInt64(node); + if (targetType == typeof(int)) return Convert.ToInt32(node); + if (targetType == typeof(double)) return Convert.ToDouble(node); + if (targetType == typeof(float)) return Convert.ToSingle(node); + if (targetType == typeof(decimal)) return Convert.ToDecimal(node); + + // Dictionaries (only generic Dictionary supported here) + if (typeof(IDictionary).IsAssignableFrom(targetType) && targetType.IsGenericType) + { + var genArgs = targetType.GetGenericArguments(); + if (genArgs.Length == 2 && genArgs[0] == typeof(string) && node is ArrayDict dictNode) + { + var valueType = genArgs[1]; + var dict = (IDictionary)Activator.CreateInstance(targetType); + foreach (var kvp in dictNode) + { + dict[kvp.Key] = ConvertToType(kvp.Value, valueType); + } + return dict; + } + } + + // Arrays + if (targetType.IsArray && node is IList listNodeForArray) + { + var elemType = targetType.GetElementType(); + var arr = Array.CreateInstance(elemType!, listNodeForArray.Count); + for (int i = 0; i < listNodeForArray.Count; i++) + { + arr.SetValue(ConvertToType(listNodeForArray[i], elemType!), i); + } + return arr; + } + + // Lists + if (typeof(IList).IsAssignableFrom(targetType) && node is IList listNode) + { + var elemType = targetType.IsGenericType ? targetType.GetGenericArguments()[0] : typeof(object); + var list = (IList)Activator.CreateInstance(targetType); + foreach (var elem in listNode) + { + list.Add(ConvertToType(elem, elemType)); + } + return list; + } + + // POCO object + if (node is ArrayDict objNode) + { + var instance = Activator.CreateInstance(targetType); + + // Mirror existing Serializer rules: fields only, allow [SerializeField] privates, + // skip [NonSerialized] and skip compiler generated backing fields. + var fields = targetType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + for (int i = 0; i < fields.Length; i++) + { + var field = fields[i]; + + var hasSerializeField = field.GetCustomAttribute(SerializeFieldType) != null; + var isGeneratedByCompiler = field.GetCustomAttribute(CompilerGeneratedType) != null; + var canBeSerialized = (hasSerializeField || !field.IsNotSerialized) && !isGeneratedByCompiler; + if (!canBeSerialized) continue; + + if (!objNode.TryGetValue(field.Name, out var rawValue)) continue; + + var converted = ConvertToType(rawValue, field.FieldType); + field.SetValue(instance, converted); + } + + // If the target uses Unity callbacks, respect them (parity with existing Serializer behavior) + if (instance is ISerializationCallbackReceiver receiver) + { + receiver.OnAfterDeserialize(); + } + + return instance; + } + + throw new InvalidOperationException( + $"SmallerJSON cannot convert node type [{node.GetType().FullName}] to [{targetType.FullName}]."); + } + + private static bool TryGetSemanticPrimitiveType(Type semanticType, out Type primitiveType) + { + var iface = semanticType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition().FullName == BeamSemanticInterfaceFullName); + + if (iface == null) + { + primitiveType = null; + return false; + } + + primitiveType = iface.GetGenericArguments()[0]; + return true; + } + + private static object ConstructSemantic(Type semanticType, Type primitiveType, object node) + { + // Prefer ctor(string) when JSON is a string. Many long-backed semantic types accept string too + // and preserve formatting (leading zeros etc) or validate (ServiceName). + if (node is string s) + { + var stringCtor = semanticType.GetConstructor(new[] { typeof(string) }); + if (stringCtor != null) return stringCtor.Invoke(new object[] { s }); + + if (primitiveType == typeof(long)) + { + if (!long.TryParse(s, System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, out var parsed)) + { + throw new FormatException($"Cannot parse '{s}' as long for semantic type [{semanticType.FullName}]."); + } + + var longCtor = semanticType.GetConstructor(new[] { typeof(long) }); + if (longCtor != null) return longCtor.Invoke(new object[] { parsed }); + } + + throw new MissingMethodException( + $"Semantic type [{semanticType.FullName}] has no public ctor(string) and cannot be built from JSON string."); + } + + // If JSON is numeric and semantic primitive is long + if (primitiveType == typeof(long)) + { + var l = Convert.ToInt64(node, System.Globalization.CultureInfo.InvariantCulture); + var longCtor = semanticType.GetConstructor(new[] { typeof(long) }); + if (longCtor != null) return longCtor.Invoke(new object[] { l }); + + // Some semantic types might only expose ctor(string); allow numeric -> string as fallback. + var stringCtor = semanticType.GetConstructor(new[] { typeof(string) }); + if (stringCtor != null) return stringCtor.Invoke(new object[] { l.ToString(System.Globalization.CultureInfo.InvariantCulture) }); + + throw new MissingMethodException($"Semantic type [{semanticType.FullName}] must have ctor(long) or ctor(string)."); + } + + // If semantic primitive is string, coerce to string + if (primitiveType == typeof(string)) + { + var stringCtor = semanticType.GetConstructor(new[] { typeof(string) }); + if (stringCtor == null) + throw new MissingMethodException($"Semantic type [{semanticType.FullName}] must have ctor(string)."); + + return stringCtor.Invoke(new object[] { node.ToString() }); + } + + throw new NotSupportedException( + $"Semantic type [{semanticType.FullName}] uses unsupported primitive [{primitiveType.FullName}]."); + } + } + } } diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs new file mode 100644 index 0000000000..be6ec29cd6 --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs @@ -0,0 +1,12 @@ + +namespace Beamable.Editor.BeamCli.Commands +{ + using Beamable.Common; + using Beamable.Common.BeamCli; + + [System.SerializableAttribute()] + public partial class BeamListReplacementTypeCommandOutput + { + public System.Collections.Generic.List ReplacementsTypes; + } +} diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs.meta b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs.meta new file mode 100644 index 0000000000..5551283cfb --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 10aa27a2bd2c15d7f1bf5b9d6db3c1ac +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs new file mode 100644 index 0000000000..d5b907c1e4 --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs @@ -0,0 +1,81 @@ + +namespace Beamable.Editor.BeamCli.Commands +{ + using Beamable.Common; + using Beamable.Common.BeamCli; + + public partial class ProjectAddReplacementTypeArgs : Beamable.Common.BeamCli.IBeamCommandArgs + { + /// The reference Id (C# class/struct name) for the replacement + public string referenceId; + /// The name of the Type to replaced with in Unreal auto-gen + public string replacementType; + /// The name of the Optional Type to replaced with in Unreal auto-gen + public string optionalReplacementType; + /// The full import for the replacement type to be used in Unreal auto-gen + public string engineImport; + /// The Unreal project name + public string projectName; + /// Serializes the arguments for command line usage. + public virtual string Serialize() + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + // If the referenceId value was not default, then add it to the list of args. + if ((this.referenceId != default(string))) + { + genBeamCommandArgs.Add(("--reference-id=" + this.referenceId)); + } + // If the replacementType value was not default, then add it to the list of args. + if ((this.replacementType != default(string))) + { + genBeamCommandArgs.Add(("--replacement-type=" + this.replacementType)); + } + // If the optionalReplacementType value was not default, then add it to the list of args. + if ((this.optionalReplacementType != default(string))) + { + genBeamCommandArgs.Add(("--optional-replacement-type=" + this.optionalReplacementType)); + } + // If the engineImport value was not default, then add it to the list of args. + if ((this.engineImport != default(string))) + { + genBeamCommandArgs.Add(("--engine-import=" + this.engineImport)); + } + // If the projectName value was not default, then add it to the list of args. + if ((this.projectName != default(string))) + { + genBeamCommandArgs.Add(("--project-name=" + this.projectName)); + } + string genBeamCommandStr = ""; + // Join all the args with spaces + genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + return genBeamCommandStr; + } + } + public partial class BeamCommands + { + public virtual ProjectAddReplacementTypeWrapper ProjectAddReplacementType(ProjectAddReplacementTypeArgs addReplacementTypeArgs) + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + genBeamCommandArgs.Add("beam"); + genBeamCommandArgs.Add(defaultBeamArgs.Serialize()); + genBeamCommandArgs.Add("project"); + genBeamCommandArgs.Add("add-replacement-type"); + genBeamCommandArgs.Add(addReplacementTypeArgs.Serialize()); + // Create an instance of an IBeamCommand + Beamable.Common.BeamCli.IBeamCommand command = this._factory.Create(); + // Join all the command paths and args into one string + string genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + // Configure the command with the command string + command.SetCommand(genBeamCommandStr); + ProjectAddReplacementTypeWrapper genBeamCommandWrapper = new ProjectAddReplacementTypeWrapper(); + genBeamCommandWrapper.Command = command; + // Return the command! + return genBeamCommandWrapper; + } + } + public partial class ProjectAddReplacementTypeWrapper : Beamable.Common.BeamCli.BeamCommandWrapper + { + } +} diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs.meta b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs.meta new file mode 100644 index 0000000000..fc0bfd3590 --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 53ecf17ad873c9afb1a32a2a2d1bea03 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectGenerateProperties.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectGenerateProperties.cs index 06eb9efa85..55fecc3e44 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectGenerateProperties.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectGenerateProperties.cs @@ -1,67 +1,67 @@ -namespace Beamable.Editor.BeamCli.Commands -{ - using Beamable.Common; - using Beamable.Common.BeamCli; - - public partial class ProjectGeneratePropertiesArgs : Beamable.Common.BeamCli.IBeamCommandArgs - { - /// Where the file will be created - public string output; - /// Beam path to be used. Use BEAM_SOLUTION_DIR to template in $(SolutionDir) - public string beamPath; +namespace Beamable.Editor.BeamCli.Commands +{ + using Beamable.Common; + using Beamable.Common.BeamCli; + + public partial class ProjectGeneratePropertiesArgs : Beamable.Common.BeamCli.IBeamCommandArgs + { + /// Where the file will be created + public string output; + /// Beam path to be used. Use BEAM_SOLUTION_DIR to template in $(SolutionDir) + public string beamPath; /// The solution path to be used. ///The following values have special meaning and are not treated as paths... - ///- "DIR.PROPS" = $([System.IO.Path]::GetDirectoryName(`$(DirectoryBuildPropsPath)`)) - public string solutionDir; - /// A path relative to the given solution directory, that will be used to store the projects /bin and /obj directories. Note: the given path will have the project's assembly name and the bin or obj folder appended - public string buildDir; - /// Serializes the arguments for command line usage. - public virtual string Serialize() - { - // Create a list of arguments for the command - System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); - // Add the output value to the list of args. - genBeamCommandArgs.Add(this.output.ToString()); - // Add the beamPath value to the list of args. - genBeamCommandArgs.Add(this.beamPath.ToString()); - // Add the solutionDir value to the list of args. - genBeamCommandArgs.Add(this.solutionDir.ToString()); - // If the buildDir value was not default, then add it to the list of args. - if ((this.buildDir != default(string))) - { - genBeamCommandArgs.Add(("--build-dir=" + this.buildDir)); - } - string genBeamCommandStr = ""; - // Join all the args with spaces - genBeamCommandStr = string.Join(" ", genBeamCommandArgs); - return genBeamCommandStr; - } - } - public partial class BeamCommands - { - public virtual ProjectGeneratePropertiesWrapper ProjectGenerateProperties(ProjectGeneratePropertiesArgs generatePropertiesArgs) - { - // Create a list of arguments for the command - System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); - genBeamCommandArgs.Add("beam"); - genBeamCommandArgs.Add(defaultBeamArgs.Serialize()); - genBeamCommandArgs.Add("project"); - genBeamCommandArgs.Add("generate-properties"); - genBeamCommandArgs.Add(generatePropertiesArgs.Serialize()); - // Create an instance of an IBeamCommand - Beamable.Common.BeamCli.IBeamCommand command = this._factory.Create(); - // Join all the command paths and args into one string - string genBeamCommandStr = string.Join(" ", genBeamCommandArgs); - // Configure the command with the command string - command.SetCommand(genBeamCommandStr); - ProjectGeneratePropertiesWrapper genBeamCommandWrapper = new ProjectGeneratePropertiesWrapper(); - genBeamCommandWrapper.Command = command; - // Return the command! - return genBeamCommandWrapper; - } - } - public partial class ProjectGeneratePropertiesWrapper : Beamable.Common.BeamCli.BeamCommandWrapper - { - } -} + ///- "DIR.PROPS" = $([System.IO.Path]::GetDirectoryName(`$(DirectoryBuildPropsPath)`)) + public string solutionDir; + /// A path relative to the given solution directory, that will be used to store the projects /bin and /obj directories. Note: the given path will have the project's assembly name and the bin or obj folder appended + public string buildDir; + /// Serializes the arguments for command line usage. + public virtual string Serialize() + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + // Add the output value to the list of args. + genBeamCommandArgs.Add(this.output.ToString()); + // Add the beamPath value to the list of args. + genBeamCommandArgs.Add(this.beamPath.ToString()); + // Add the solutionDir value to the list of args. + genBeamCommandArgs.Add(this.solutionDir.ToString()); + // If the buildDir value was not default, then add it to the list of args. + if ((this.buildDir != default(string))) + { + genBeamCommandArgs.Add(("--build-dir=" + this.buildDir)); + } + string genBeamCommandStr = ""; + // Join all the args with spaces + genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + return genBeamCommandStr; + } + } + public partial class BeamCommands + { + public virtual ProjectGeneratePropertiesWrapper ProjectGenerateProperties(ProjectGeneratePropertiesArgs generatePropertiesArgs) + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + genBeamCommandArgs.Add("beam"); + genBeamCommandArgs.Add(defaultBeamArgs.Serialize()); + genBeamCommandArgs.Add("project"); + genBeamCommandArgs.Add("generate-properties"); + genBeamCommandArgs.Add(generatePropertiesArgs.Serialize()); + // Create an instance of an IBeamCommand + Beamable.Common.BeamCli.IBeamCommand command = this._factory.Create(); + // Join all the command paths and args into one string + string genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + // Configure the command with the command string + command.SetCommand(genBeamCommandStr); + ProjectGeneratePropertiesWrapper genBeamCommandWrapper = new ProjectGeneratePropertiesWrapper(); + genBeamCommandWrapper.Command = command; + // Return the command! + return genBeamCommandWrapper; + } + } + public partial class ProjectGeneratePropertiesWrapper : Beamable.Common.BeamCli.BeamCommandWrapper + { + } +} diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs new file mode 100644 index 0000000000..dd9ea9120d --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs @@ -0,0 +1,58 @@ + +namespace Beamable.Editor.BeamCli.Commands +{ + using Beamable.Common; + using Beamable.Common.BeamCli; + + public partial class ProjectListReplacementTypeArgs : Beamable.Common.BeamCli.IBeamCommandArgs + { + /// The Unreal project name + public string projectName; + /// Serializes the arguments for command line usage. + public virtual string Serialize() + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + // If the projectName value was not default, then add it to the list of args. + if ((this.projectName != default(string))) + { + genBeamCommandArgs.Add(("--project-name=" + this.projectName)); + } + string genBeamCommandStr = ""; + // Join all the args with spaces + genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + return genBeamCommandStr; + } + } + public partial class BeamCommands + { + public virtual ProjectListReplacementTypeWrapper ProjectListReplacementType(ProjectListReplacementTypeArgs listReplacementTypeArgs) + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + genBeamCommandArgs.Add("beam"); + genBeamCommandArgs.Add(defaultBeamArgs.Serialize()); + genBeamCommandArgs.Add("project"); + genBeamCommandArgs.Add("list-replacement-type"); + genBeamCommandArgs.Add(listReplacementTypeArgs.Serialize()); + // Create an instance of an IBeamCommand + Beamable.Common.BeamCli.IBeamCommand command = this._factory.Create(); + // Join all the command paths and args into one string + string genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + // Configure the command with the command string + command.SetCommand(genBeamCommandStr); + ProjectListReplacementTypeWrapper genBeamCommandWrapper = new ProjectListReplacementTypeWrapper(); + genBeamCommandWrapper.Command = command; + // Return the command! + return genBeamCommandWrapper; + } + } + public partial class ProjectListReplacementTypeWrapper : Beamable.Common.BeamCli.BeamCommandWrapper + { + public virtual ProjectListReplacementTypeWrapper OnStreamListReplacementTypeCommandOutput(System.Action> cb) + { + this.Command.On("stream", cb); + return this; + } + } +} diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs.meta b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs.meta new file mode 100644 index 0000000000..dca4323a0e --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e37ef1f024919fd96ed29153ef07b580 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs new file mode 100644 index 0000000000..9cef525cce --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs @@ -0,0 +1,60 @@ + +namespace Beamable.Editor.BeamCli.Commands +{ + using Beamable.Common; + using Beamable.Common.BeamCli; + + public partial class ProjectRemoveReplacementTypeArgs : Beamable.Common.BeamCli.IBeamCommandArgs + { + /// The reference Id (C# class/struct name) for the replacement + public string referenceId; + /// The Unreal project name + public string projectName; + /// Serializes the arguments for command line usage. + public virtual string Serialize() + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + // If the referenceId value was not default, then add it to the list of args. + if ((this.referenceId != default(string))) + { + genBeamCommandArgs.Add(("--reference-id=" + this.referenceId)); + } + // If the projectName value was not default, then add it to the list of args. + if ((this.projectName != default(string))) + { + genBeamCommandArgs.Add(("--project-name=" + this.projectName)); + } + string genBeamCommandStr = ""; + // Join all the args with spaces + genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + return genBeamCommandStr; + } + } + public partial class BeamCommands + { + public virtual ProjectRemoveReplacementTypeWrapper ProjectRemoveReplacementType(ProjectRemoveReplacementTypeArgs removeReplacementTypeArgs) + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + genBeamCommandArgs.Add("beam"); + genBeamCommandArgs.Add(defaultBeamArgs.Serialize()); + genBeamCommandArgs.Add("project"); + genBeamCommandArgs.Add("remove-replacement-type"); + genBeamCommandArgs.Add(removeReplacementTypeArgs.Serialize()); + // Create an instance of an IBeamCommand + Beamable.Common.BeamCli.IBeamCommand command = this._factory.Create(); + // Join all the command paths and args into one string + string genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + // Configure the command with the command string + command.SetCommand(genBeamCommandStr); + ProjectRemoveReplacementTypeWrapper genBeamCommandWrapper = new ProjectRemoveReplacementTypeWrapper(); + genBeamCommandWrapper.Command = command; + // Return the command! + return genBeamCommandWrapper; + } + } + public partial class ProjectRemoveReplacementTypeWrapper : Beamable.Common.BeamCli.BeamCommandWrapper + { + } +} diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs.meta b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs.meta new file mode 100644 index 0000000000..4ff624fe8d --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d9a99992c11188eb60e8887c4c17631 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs new file mode 100644 index 0000000000..002743914b --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs @@ -0,0 +1,15 @@ + +namespace Beamable.Editor.BeamCli.Commands +{ + using Beamable.Common; + using Beamable.Common.BeamCli; + + [System.SerializableAttribute()] + public partial class BeamReplacementTypeInfo + { + public string ReferenceId; + public string EngineReplacementType; + public string EngineOptionalReplacementType; + public string EngineImport; + } +} diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs.meta b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs.meta new file mode 100644 index 0000000000..2ef99b86e6 --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f7ebfca2c0f0f8a4cf406e9257590d78 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Runtime/Server/MicroserviceClient.cs b/client/Packages/com.beamable/Runtime/Server/MicroserviceClient.cs index 9fd06f6450..91c6da685e 100644 --- a/client/Packages/com.beamable/Runtime/Server/MicroserviceClient.cs +++ b/client/Packages/com.beamable/Runtime/Server/MicroserviceClient.cs @@ -3,12 +3,15 @@ using Beamable.Common.Api; using Beamable.Common.Api.Inventory; using Beamable.Common.Dependencies; +using Beamable.Common.Semantics; using Beamable.Serialization.SmallerJSON; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; +using System.Linq; +using System.Reflection; using System.Text; using UnityEngine; using Debug = UnityEngine.Debug; @@ -274,8 +277,8 @@ public static T DeserializeResult(string json) } return wrapped.items; } - - return JsonUtility.FromJson(json); + + return Json.Deserialize(json); } public static Dictionary ConvertArrayDictToDictionary(ArrayDict arrayDict) diff --git a/microservice/beamable.tooling.common/Microservice/ServiceMethodHelper.cs b/microservice/beamable.tooling.common/Microservice/ServiceMethodHelper.cs index 1139891316..53ebb0ed09 100644 --- a/microservice/beamable.tooling.common/Microservice/ServiceMethodHelper.cs +++ b/microservice/beamable.tooling.common/Microservice/ServiceMethodHelper.cs @@ -8,7 +8,6 @@ using System.Text; using Beamable.Common.Dependencies; -using beamable.tooling.common.Microservice; using ZLogger; using static Beamable.Common.Constants.Features.Services; @@ -310,8 +309,13 @@ public static object DeserializeStringParameter(string json) return Serialization.SmallerJSON.Json.Serialize(dict, new StringBuilder()); } - // or just peel off the quotes - return json.Substring(1, json.Length - 2); + // or just peel off the quotes if it starts of ends with them + if (json.StartsWith("\"") && json.EndsWith("\"")) + { + return json.Substring(1, json.Length - 2); + } + // if not just throw back the original string + return json; } private static List ScanType(IMicroserviceAttributes serviceAttribute, ServiceMethodProvider provider) diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs index 46e6b2f045..76f1de1f18 100644 --- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs @@ -9,6 +9,7 @@ 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; @@ -40,7 +41,11 @@ public OAPIType(ServiceMethod method, Type type) public bool IsFromBeamGenerateSchema() => SourceCallable == null && Type.GetCustomAttribute() != null; public bool IsFromCallableWithNoClientGen() => IsFromCallable() && SourceCallable.Method.GetCustomAttribute(true).Flags.HasFlag(CallableFlags.SkipGenerateClientFiles); - public bool ShouldNotGenerateClientCode() => (IsFromFederation() || IsFromCallableWithNoClientGen()) && !IsFromBeamGenerateSchema(); + + public bool IsBeamSemanticType() => Type.GetInterfaces() + .Any(i => i.GetGenericTypeDefinition() == typeof(IBeamSemanticType<>)); + + public bool ShouldSkipClientCodeGeneration() => (IsFromFederation() || IsFromCallableWithNoClientGen() || IsBeamSemanticType()) && !IsFromBeamGenerateSchema(); public bool IsPrimitive() { @@ -274,7 +279,17 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required case { } x when x.IsGenericType && x.GetGenericTypeDefinition() == typeof(Optional<>): return Convert(x.GetGenericArguments()[0], ref requiredTypes,depth - 1, sanitizeGenericType); case { } x when x.IsGenericType && x.GetGenericTypeDefinition() == typeof(Nullable<>): - return Convert(x.GetGenericArguments()[0], ref requiredTypes,depth - 1, sanitizeGenericType); + return Convert(x.GetGenericArguments()[0], 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); + if (Activator.CreateInstance(runtimeType) is IBeamSemanticType semanticTypeInstance) + { + semanticTypeSchema.Extensions[SCHEMA_SEMANTIC_TYPE_NAME_KEY] = + new OpenApiString(semanticTypeInstance.SemanticName); + } + + return semanticTypeSchema; case { } x when x == typeof(double): return new OpenApiSchema { Type = "number", Format = "double" }; case { } x when x == typeof(float): @@ -456,6 +471,16 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required var comment = DocsLoader.GetMemberComments(member); fieldSchema.Description = comment?.Summary; + + + Type classSemanticType = member.FieldType.GetInterfaces().FirstOrDefault(i => + i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBeamSemanticType<>)); + if (classSemanticType != null && Activator.CreateInstance(member.FieldType) is IBeamSemanticType memberSemanticTypeInstance) + { + fieldSchema.Extensions[SCHEMA_SEMANTIC_TYPE_NAME_KEY] = + new OpenApiString(memberSemanticTypeInstance.SemanticName); + } + schema.Properties[name] = fieldSchema; if (!member.FieldType.IsAssignableTo(typeof(Optional))) diff --git a/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs index 40bac92070..fe38aa3528 100644 --- a/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs @@ -12,6 +12,7 @@ using Microsoft.OpenApi.Readers; using System.Reflection; using Beamable.Common.Dependencies; +using Beamable.Common.Semantics; using ZLogger; namespace Beamable.Tooling.Common.OpenAPI; @@ -30,6 +31,7 @@ public class ServiceDocGenerator private const string V2_COMPONENT_FEDERATION_CLASS_NAME = Constants.Features.Services.MICROSERVICE_FEDERATED_COMPONENTS_V2_FEDERATION_CLASS_NAME_KEY; private const string SCHEMA_IS_OPTIONAL_KEY = Constants.Features.Services.SCHEMA_IS_OPTIONAL_KEY; private const string SCHEMA_OPTIONAL_TYPE_KEY = Constants.Features.Services.SCHEMA_OPTIONAL_TYPE_NAME_KEY; + private const string SCHEMA_SEMANTIC_TYPE_NAME_KEY = Constants.Features.Services.SCHEMA_SEMANTIC_TYPE_NAME_KEY; private static OpenApiSecurityScheme _userSecurityScheme = new OpenApiSecurityScheme @@ -219,6 +221,8 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r { returnJson.Extensions.Add(Constants.Features.Services.MICROSERVICE_EXTENSION_BEAMABLE_TYPE_ASSEMBLY_QUALIFIED_NAME, new OpenApiString(returnType.GetSanitizedFullName())); } + + var response = new OpenApiResponse() { Description = comments.Returns ?? "", }; if (!IsEmptyResponseType(returnType)) diff --git a/microservice/beamable.tooling.common/UnityJsonContractResolver.cs b/microservice/beamable.tooling.common/UnityJsonContractResolver.cs index 672b735757..8fd8e4da1e 100644 --- a/microservice/beamable.tooling.common/UnityJsonContractResolver.cs +++ b/microservice/beamable.tooling.common/UnityJsonContractResolver.cs @@ -1,6 +1,7 @@ using Beamable.Common; using Beamable.Common.Api.Inventory; using Beamable.Common.Content; +using Beamable.Common.Semantics; using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -30,6 +31,14 @@ public class UnitySerializationSettings : JsonSerializerSettings new StringToSomethingDictionaryConverter(), new StringToSomethingDictionaryConverter(), new StringToSomethingDictionaryConverter>(), + new BeamAccountIdConverter(), + new BeamCidConverter(), + new BeamContentIdConverter(), + new BeamContentManifestIdConverter(), + new BeamGamerTagConverter(), + new BeamPidConverter(), + new BeamStatsConverter(), + new ServiceNameConverter(), new OptionalConverter(), // THIS MUST BE LAST, because it is hacky, and falls back onto other converts as its _normal_ behaviour. If its not last, then other converts can run twice, which causes newtonsoft to explode. new UnitySerializationCallbackInvoker(), @@ -320,6 +329,266 @@ public void Dispose() _skip.Dispose(); } } + + + /// + /// Custom JSON converter for to serialize as long and deserialize it from string and long + /// + public class BeamAccountIdConverter : JsonConverter + { + + public override BeamAccountId ReadJson(JsonReader reader, Type objectType, BeamAccountId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.Integer: + { + var longValue = Convert.ToInt64(reader.Value); + return new BeamAccountId(longValue); + } + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamAccountId(stringValue); + } + case JsonToken.Null: + { + return new BeamAccountId(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamAccountId"); + } + } + + public override void WriteJson(JsonWriter writer, BeamAccountId value, JsonSerializer serializer) + { + writer.WriteValue(value.AsLong); + } + + + } + + /// + /// Custom JSON converter for to serialize as long and deserialize it from string and long + /// + public class BeamCidConverter : JsonConverter + { + public override BeamCid ReadJson(JsonReader reader, Type objectType, BeamCid existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.Integer: + { + var longValue = Convert.ToInt64(reader.Value); + return new BeamCid(longValue); + } + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamCid(stringValue); + } + case JsonToken.Null: + { + return new BeamCid(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamCid value, JsonSerializer serializer) + { + writer.WriteValue(value.AsLong); + } + } + + /// + /// Custom JSON converter for to serialize and deserialize it from string + /// + public class BeamContentIdConverter : JsonConverter + { + public override BeamContentId ReadJson(JsonReader reader, Type objectType, BeamContentId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamContentId(stringValue); + } + case JsonToken.Null: + { + return new BeamContentId(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamContentId value, JsonSerializer serializer) + { + writer.WriteValue(value.AsString); + } + } + + /// + /// Custom JSON converter for to serialize and deserialize it from string + /// + public class BeamContentManifestIdConverter : JsonConverter + { + public override BeamContentManifestId ReadJson(JsonReader reader, Type objectType, BeamContentManifestId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamContentManifestId(stringValue); + } + case JsonToken.Null: + { + return new BeamContentManifestId(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamContentManifestId value, JsonSerializer serializer) + { + writer.WriteValue(value.AsString); + } + } + + + /// + /// Custom JSON converter for to serialize as long and deserialize it from string and long + /// + public class BeamGamerTagConverter : JsonConverter + { + public override BeamGamerTag ReadJson(JsonReader reader, Type objectType, BeamGamerTag existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.Integer: + { + var longValue = Convert.ToInt64(reader.Value); + return new BeamGamerTag(longValue); + } + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamGamerTag(stringValue); + } + case JsonToken.Null: + { + return new BeamGamerTag(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamGamerTag value, JsonSerializer serializer) + { + writer.WriteValue(value.AsLong); + } + } + + /// + /// Custom JSON converter for to serialize and deserialize it from string + /// + public class BeamPidConverter : JsonConverter + { + public override BeamPid ReadJson(JsonReader reader, Type objectType, BeamPid existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamPid(stringValue); + } + case JsonToken.Null: + { + return new BeamPid(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamPid value, JsonSerializer serializer) + { + writer.WriteValue(value.AsString); + } + } + + /// + /// Custom JSON converter for to serialize and deserialize it from string + /// + public class BeamStatsConverter : JsonConverter + { + public override BeamStats ReadJson(JsonReader reader, Type objectType, BeamStats existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamStats(stringValue); + } + case JsonToken.Null: + { + return new BeamStats(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamStats value, JsonSerializer serializer) + { + writer.WriteValue(value.AsString); + } + } + + /// + /// Custom JSON converter for to serialize and deserialize it from string + /// + public class ServiceNameConverter : JsonConverter + { + public override ServiceName ReadJson(JsonReader reader, Type objectType, ServiceName existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new ServiceName(stringValue); + } + case JsonToken.Null: + { + return new ServiceName(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, ServiceName value, JsonSerializer serializer) + { + writer.WriteValue(value.Value); + } + } /// /// Custom contract resolver for JSON serialization in Unity, allowing for proper handling of SerializeField attributes. diff --git a/microservice/microserviceTests/OpenAPITests/TypeTests.cs b/microservice/microserviceTests/OpenAPITests/TypeTests.cs index 9be6203d99..f35c768f32 100644 --- a/microservice/microserviceTests/OpenAPITests/TypeTests.cs +++ b/microservice/microserviceTests/OpenAPITests/TypeTests.cs @@ -6,8 +6,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; -using UnityEngine; +using Beamable.Common.Semantics; +using System.Text.Json; using UnityEngine; namespace microserviceTests.OpenAPITests; @@ -158,6 +158,21 @@ public void CheckEnumsOnObject() 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 schema = SchemaGenerator.Convert(type); + Assert.AreEqual(typeName, schema.Type); + Assert.AreEqual(format, schema.Format); + } /// From b621cea9404d5f07132a8f1d44c311a1c3ede4f3 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Wed, 25 Feb 2026 12:21:09 +0100 Subject: [PATCH 13/16] Fix compilation --- .../OpenAPI/SchemaGenerator.cs | 8 +- .../OpenAPITests/TypeTests.cs | 450 +++++++++--------- 2 files changed, 230 insertions(+), 228 deletions(-) diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs index 76f1de1f18..7a3798f50f 100644 --- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs @@ -194,7 +194,7 @@ public static Dictionary ToOpenApiSchemasDictionary(IList< { if(toSkip.Contains(i)) continue; - var shouldGenerateClientCode = !oapiTypes[i].ShouldNotGenerateClientCode(); + var shouldGenerateClientCode = !oapiTypes[i].ShouldSkipClientCodeGeneration(); for (int j = i + 1; j < oapiTypes.Count; j++) { @@ -205,7 +205,7 @@ public static Dictionary ToOpenApiSchemasDictionary(IList< toSkip.Add(j); - if (!shouldGenerateClientCode && !oapiTypes[j].ShouldNotGenerateClientCode()) + if (!shouldGenerateClientCode && !oapiTypes[j].ShouldSkipClientCodeGeneration()) { shouldGenerateClientCode = true; } @@ -279,10 +279,10 @@ public static OpenApiSchema Convert(Type runtimeType, ref HashSet required case { } x when x.IsGenericType && x.GetGenericTypeDefinition() == typeof(Optional<>): 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] = diff --git a/microservice/microserviceTests/OpenAPITests/TypeTests.cs b/microservice/microserviceTests/OpenAPITests/TypeTests.cs index f35c768f32..69a68c119d 100644 --- a/microservice/microserviceTests/OpenAPITests/TypeTests.cs +++ b/microservice/microserviceTests/OpenAPITests/TypeTests.cs @@ -1,225 +1,227 @@ -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.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 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; - } - /// - /// 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; - } -} +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; + } +} From 5fd6e3fb4087f0fa63332bf9688bfb1f11876efd Mon Sep 17 00:00:00 2001 From: MevLyshkin Date: Mon, 16 Mar 2026 15:37:44 +0100 Subject: [PATCH 14/16] Update microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../beamable.tooling.common/OpenAPI/SchemaGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs index 02505051ba..b2d8ba47e1 100644 --- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs @@ -183,8 +183,8 @@ 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) { From f4ce611d55464a8807f206fd92d8487e71e7d184 Mon Sep 17 00:00:00 2001 From: MevLyshkin Date: Mon, 16 Mar 2026 15:38:04 +0100 Subject: [PATCH 15/16] Update microservice/beamable.tooling.common/TypeExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- microservice/beamable.tooling.common/TypeExtensions.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/microservice/beamable.tooling.common/TypeExtensions.cs b/microservice/beamable.tooling.common/TypeExtensions.cs index 16197dfb4b..69c08ec0d0 100644 --- a/microservice/beamable.tooling.common/TypeExtensions.cs +++ b/microservice/beamable.tooling.common/TypeExtensions.cs @@ -44,6 +44,15 @@ public static string GetSanitizedFullName(this Type type) 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)) From 9d277387a2a9b2135f85ef7c1d7565002b1c5c24 Mon Sep 17 00:00:00 2001 From: MevLyshkin Date: Mon, 16 Mar 2026 15:38:25 +0100 Subject: [PATCH 16/16] Update microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../OpenAPI/SchemaGenerator.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs index b2d8ba47e1..cb08954f11 100644 --- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs @@ -242,6 +242,24 @@ 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();