diff --git a/src/Speckle.Core/Api/GraphQL/Legacy/LegacyGraphQLModels.cs b/src/Speckle.Core/Api/GraphQL/Legacy/LegacyGraphQLModels.cs index 180c728..b65af82 100644 --- a/src/Speckle.Core/Api/GraphQL/Legacy/LegacyGraphQLModels.cs +++ b/src/Speckle.Core/Api/GraphQL/Legacy/LegacyGraphQLModels.cs @@ -90,6 +90,8 @@ public class CommitCreateInput public string sourceApplication { get; set; } = ".net"; public int totalChildrenCount { get; set; } public List parents { get; set; } + + public string SchemaVersion { get; set; } [Obsolete("Please use the parents property. This property will be removed in later versions")] public List previousCommitIds { get; set; } @@ -218,6 +220,10 @@ public class Commit public int totalChildrenCount { get; set; } public List parents { get; set; } + // should be nullable but is currently disabled here + // null schema == this is old AF + public string SchemaVersion { get; set; } + public override string ToString() { return $"Commit ({message} | {id})"; diff --git a/src/Speckle.Core/Api/GraphQL/Models/Version.cs b/src/Speckle.Core/Api/GraphQL/Models/Version.cs index 1aa46b0..0ebadc7 100644 --- a/src/Speckle.Core/Api/GraphQL/Models/Version.cs +++ b/src/Speckle.Core/Api/GraphQL/Models/Version.cs @@ -15,4 +15,9 @@ public sealed class Version public Uri previewUrl { get; init; } public string referencedObject { get; init; } public string sourceApplication { get; init; } + + public System.Version SchemaVersion { get; init; } + + // POC: is this the right place for a const? + public const string EARLIEST_SCHEMA_VERSION_STRING = "0.0.0"; } diff --git a/src/Speckle.Core/Api/Helpers.cs b/src/Speckle.Core/Api/Helpers.cs index 04d6201..31b4857 100644 --- a/src/Speckle.Core/Api/Helpers.cs +++ b/src/Speckle.Core/Api/Helpers.cs @@ -17,6 +17,8 @@ using Speckle.Core.Kits; using Speckle.Core.Logging; using Speckle.Core.Models; +using Speckle.Core.SchemaVersioning; +using Speckle.Core.Serialisation.TypeCache; using Speckle.Core.Transports; using Speckle.Newtonsoft.Json; @@ -37,6 +39,9 @@ public static class Helpers /// public static async Task Receive( string stream, + ITypeCache typeCache, + ISchemaObjectUpgradeManager objectUpgradeManager, + System.Version schemaVersion, Account account = null, Action> onProgressAction = null, Action onTotalChildrenCountKnown = null @@ -109,6 +114,9 @@ public static class Helpers var receiveRes = await Operations .Receive( objectId, + typeCache, + objectUpgradeManager, + schemaVersion, transport, onProgressAction: onProgressAction, onTotalChildrenCountKnown: onTotalChildrenCountKnown diff --git a/src/Speckle.Core/Api/Operations/Operations.Receive.Obsolete.cs b/src/Speckle.Core/Api/Operations/Operations.Receive.Obsolete.cs deleted file mode 100644 index 5c486aa..0000000 --- a/src/Speckle.Core/Api/Operations/Operations.Receive.Obsolete.cs +++ /dev/null @@ -1,533 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Serilog.Context; -using Speckle.Core.Logging; -using Speckle.Core.Models; -using Speckle.Core.Serialisation; -using Speckle.Core.Transports; -using Speckle.Newtonsoft.Json; - -namespace Speckle.Core.Api; - -#pragma warning disable CA1068, IDE1006 - -[Obsolete("Serializer v1 is deprecated")] -public enum SerializerVersion -{ - V1, - V2 -} - -public static partial class Operations -{ - private const string RECEIVE_DEPRECATION_MESSAGE = """ - This method overload is obsolete, consider using a non-obsolete overload. - 1.SerializerVersion selection will no longer be supported going foward (serializer v1 is now deprecated). - 2.Use of disposeTransports will no longer be supported going forward (you should dispose your own transports). - 3 OnErrorAction is no longer used (instead functions with throw exceptions for consistancy and clear stack trace) - """; - - /// - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static Task Receive( - string objectId, - CancellationToken cancellationToken, - ITransport? remoteTransport, - ITransport? localTransport, - Action>? onProgressAction, - Action? onErrorAction, - Action? onTotalChildrenCountKnown, - bool disposeTransports - ) - { - return Receive( - objectId, - cancellationToken, - remoteTransport, - localTransport, - onProgressAction, - onErrorAction, - onTotalChildrenCountKnown, - disposeTransports, - SerializerVersion.V2 - ); - } - - /// - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static Task Receive( - string objectId, - CancellationToken cancellationToken, - ITransport? remoteTransport, - Action>? onProgressAction, - Action? onErrorAction, - Action? onTotalChildrenCountKnown, - bool disposeTransports, - SerializerVersion serializerVersion - ) - { - return Receive( - objectId, - cancellationToken, - remoteTransport, - null, - onProgressAction, - onErrorAction, - onTotalChildrenCountKnown, - disposeTransports, - serializerVersion - ); - } - - /// - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static Task Receive( - string objectId, - CancellationToken cancellationToken, - ITransport? remoteTransport, - Action>? onProgressAction, - Action? onErrorAction, - Action? onTotalChildrenCountKnown, - bool disposeTransports - ) - { - return Receive( - objectId, - cancellationToken, - remoteTransport, - null, - onProgressAction, - onErrorAction, - onTotalChildrenCountKnown, - disposeTransports, - SerializerVersion.V2 - ); - } - - /// - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static Task Receive( - string objectId, - CancellationToken cancellationToken, - ITransport? remoteTransport, - bool disposeTransports - ) - { - return Receive( - objectId, - cancellationToken, - remoteTransport, - null, - null, - null, - null, - disposeTransports, - SerializerVersion.V2 - ); - } - - /// - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static Task Receive( - string objectId, - ITransport? remoteTransport, - ITransport? localTransport, - Action>? onProgressAction, - Action? onErrorAction, - Action? onTotalChildrenCountKnown, - bool disposeTransports, - SerializerVersion serializerVersion - ) - { - return Receive( - objectId, - CancellationToken.None, - remoteTransport, - localTransport, - onProgressAction, - onErrorAction, - onTotalChildrenCountKnown, - disposeTransports, - serializerVersion - ); - } - - /// - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static Task Receive( - string objectId, - ITransport? remoteTransport, - ITransport? localTransport, - Action>? onProgressAction, - Action? onErrorAction, - Action? onTotalChildrenCountKnown, - bool disposeTransports - ) - { - return Receive( - objectId, - CancellationToken.None, - remoteTransport, - localTransport, - onProgressAction, - onErrorAction, - onTotalChildrenCountKnown, - disposeTransports, - SerializerVersion.V2 - ); - } - - /// - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static Task Receive( - string objectId, - ITransport? remoteTransport, - Action>? onProgressAction, - Action? onErrorAction, - Action? onTotalChildrenCountKnown, - bool disposeTransports - ) - { - return Receive( - objectId, - CancellationToken.None, - remoteTransport, - null, - onProgressAction, - onErrorAction, - onTotalChildrenCountKnown, - disposeTransports, - SerializerVersion.V2 - ); - } - - /// - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static Task Receive( - string objectId, - ITransport? remoteTransport, - ITransport? localTransport, - bool disposeTransports - ) - { - return Receive( - objectId, - CancellationToken.None, - remoteTransport, - localTransport, - null, - null, - null, - disposeTransports, - SerializerVersion.V2 - ); - } - - /// - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static Task Receive(string objectId, ITransport? remoteTransport, bool disposeTransports) - { - return Receive( - objectId, - CancellationToken.None, - remoteTransport, - null, - null, - null, - null, - disposeTransports, - SerializerVersion.V2 - ); - } - - /// - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static Task Receive(string objectId, bool disposeTransports) - { - return Receive( - objectId, - CancellationToken.None, - null, - null, - null, - null, - null, - disposeTransports, - SerializerVersion.V2 - ); - } - - /// - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static Task Receive( - string objectId, - ITransport? remoteTransport, - ITransport? localTransport, - Action? onErrorAction - ) - { - return Receive( - objectId, - default, - remoteTransport, - localTransport, - null, - onErrorAction, - null, - false, - SerializerVersion.V2 - ); - } - - /// - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static Task Receive( - string objectId, - ITransport? remoteTransport, - Action? onErrorAction - ) - { - return Receive(objectId, default, remoteTransport, null, null, onErrorAction, null, false, SerializerVersion.V2); - } - - /// - /// Receives an object from a transport. - /// - /// - /// This overload is deprecated. You should consider using - /// - ///
- /// The new overload no longer support switching as v1 is now deprecated. - ///
- /// We also no longer offer the option to . - /// You should instead handle disposal yourself - /// using conventional mechanisms like the using keyword or try finally block
- ///
- /// This function overload will be kept around for several releases, but will eventually be removed. - ///
- /// - /// The transport to receive from. - /// Leave null to use the default cache. - /// Action invoked on progress iterations. - /// Action invoked on internal errors. - /// Action invoked once the total count of objects is known. - /// - [Obsolete(RECEIVE_DEPRECATION_MESSAGE)] - public static async Task Receive( - string objectId, - CancellationToken cancellationToken, - ITransport? remoteTransport, - ITransport? localTransport, - Action>? onProgressAction, - Action? onErrorAction, - Action? onTotalChildrenCountKnown, - bool disposeTransports, - SerializerVersion serializerVersion - ) - { - var hasUserProvidedLocalTransport = localTransport != null; - localTransport ??= new SQLiteTransport(); - using (LogContext.PushProperty("remoteTransportContext", remoteTransport?.TransportContext)) - using (LogContext.PushProperty("localTransportContext", localTransport.TransportContext)) - using (LogContext.PushProperty("objectId", objectId)) - { - var timer = Stopwatch.StartNew(); - SpeckleLog.Logger.Information( - "Starting receive {objectId} from transports {localTransport} / {remoteTransport}", - objectId, - localTransport.TransportName, - remoteTransport?.TransportName - ); - - BaseObjectSerializer? serializer = null; - JsonSerializerSettings? settings = null; - BaseObjectDeserializerV2? serializerV2 = null; - if (serializerVersion == SerializerVersion.V1) - { - (serializer, settings) = GetSerializerInstance(); - } - else - { - serializerV2 = new BaseObjectDeserializerV2(); - } - - var internalProgressAction = GetInternalProgressAction(onProgressAction); - - localTransport.OnProgressAction = internalProgressAction; - localTransport.CancellationToken = cancellationToken; - - if (serializerVersion == SerializerVersion.V1) - { - serializer!.ReadTransport = localTransport; - serializer.OnProgressAction = internalProgressAction; - serializer.OnErrorAction = onErrorAction; - serializer.CancellationToken = cancellationToken; - } - else - { - serializerV2!.ReadTransport = localTransport; - serializerV2.OnProgressAction = internalProgressAction; - serializerV2.OnErrorAction = onErrorAction; - serializerV2.CancellationToken = cancellationToken; - if (remoteTransport is IBlobCapableTransport t) - { - serializerV2.BlobStorageFolder = t.BlobStorageFolder; - } - } - - // First we try and get the object from the local transport. If it's there, we assume all its children are there, and proceed with deserialisation. - // This assumption is hard-wired into the SDK. Read below. - var objString = localTransport.GetObject(objectId); - - if (objString != null) - { - // Shoot out the total children count - var partial = JsonConvert.DeserializeObject(objString); - if (partial == null) - { - throw new SpeckleDeserializeException( - $"Failed to deserialize {nameof(objString)} into {nameof(Placeholder)}" - ); - } - - if (partial.__closure != null) - { - onTotalChildrenCountKnown?.Invoke(partial.__closure.Count); - } - - Base? localRes = DeserializeStringToBase(serializerVersion, objString, settings, serializerV2); - - if ((disposeTransports || !hasUserProvidedLocalTransport) && localTransport is IDisposable dispLocal) - { - dispLocal.Dispose(); - } - - if (disposeTransports && remoteTransport != null && remoteTransport is IDisposable dispRemote) - { - dispRemote.Dispose(); - } - - timer.Stop(); - SpeckleLog - .Logger.ForContext("deserializerElapsed", serializerV2?.Elapsed) - .ForContext( - "transportElapsedBreakdown", - new[] { localTransport, remoteTransport } - .Where(t => t != null) - .ToDictionary(t => t!.TransportName, t => t!.Elapsed) - ) - .Information( - "Finished receiving {objectId} from {source} in {elapsed} seconds", - objectId, - localTransport.TransportName, - timer.Elapsed.TotalSeconds - ); - return localRes; - } - - if (remoteTransport == null) - { - var ex = new SpeckleException( - $"Could not find specified object using the local transport {localTransport.TransportName}, and you didn't provide a fallback remote from which to pull it." - ); - - SpeckleLog.Logger.Error(ex, "Cannot receive object from the given transports {exceptionMessage}", ex.Message); - throw ex; - } - - // If we've reached this stage, it means that we didn't get a local transport hit on our object, so we will proceed to get it from the provided remote transport. - // This is done by copying itself and all its children from the remote transport into the local one. - remoteTransport.OnProgressAction = internalProgressAction; - remoteTransport.CancellationToken = cancellationToken; - - SpeckleLog.Logger.Debug( - "Cannot find object {objectId} in the local transport, hitting remote {transportName}", - remoteTransport.TransportName - ); - objString = await remoteTransport - .CopyObjectAndChildren(objectId, localTransport, onTotalChildrenCountKnown) - .ConfigureAwait(false); - - // Wait for the local transport to finish "writing" - in this case, it signifies that the remote transport has done pushing copying objects into it. (TODO: I can see some scenarios where latency can screw things up, and we should rather wait on the remote transport). - await localTransport.WriteComplete().ConfigureAwait(false); - - // Proceed to deserialise the object, now safely knowing that all its children are present in the local (fast) transport. - - Base? res = DeserializeStringToBase(serializerVersion, objString, settings, serializerV2); - if ((disposeTransports || !hasUserProvidedLocalTransport) && localTransport is IDisposable dl) - { - dl.Dispose(); - } - - if (disposeTransports && remoteTransport is IDisposable dr) - { - dr.Dispose(); - } - - SpeckleLog - .Logger.ForContext("deserializerElapsed", serializerV2?.Elapsed) - .ForContext( - "transportElapsedBreakdown", - new[] { localTransport, remoteTransport } - .Where(t => t != null) - .ToDictionary(t => t.TransportName, t => t.Elapsed) - ) - .Information( - "Finished receiving {objectId} from {source} in {elapsed} seconds", - objectId, - remoteTransport.TransportName, - timer.Elapsed.TotalSeconds - ); - return res; - - // Summary: - // Basically, receiving an object (and all its subchildren) operates with two transports, one that is potentially slow, and one that is fast. - // The fast transport ("localTransport") is used syncronously inside the deserialisation routine to get the value of nested references and set them. The slow transport ("remoteTransport") is used to get the raw data and populate the local transport with all necessary data for a successful deserialisation of the object. - // Note: if properly implemented, there is no hard distinction between what is a local or remote transport; it's still just a transport. So, for example, if you want to receive an object without actually writing it first to a local transport, you can just pass a Server/S3 transport as a local transport. - // This is not reccommended, but shows what you can do. Another tidbit: the local transport does not need to be disk-bound; it can easily be an in memory transport. In memory transports are the fastest ones, but they're of limited use for more - } - } - - [Obsolete("Serializer v1 is deprecated, use other overload(s)")] - private static Base? DeserializeStringToBase( - SerializerVersion serializerVersion, - string objString, - JsonSerializerSettings? settings, - BaseObjectDeserializerV2? serializerV2 - ) - { - if (serializerVersion == SerializerVersion.V1) - { - return JsonConvert.DeserializeObject(objString, settings); - } - else - { - return serializerV2!.Deserialize(objString); - } - } -} - -[Obsolete("Use " + nameof(TransportHelpers.Placeholder))] -internal sealed class Placeholder -{ - public Dictionary? __closure { get; set; } = new(); -} - -#pragma warning restore CA1068, IDE1006 diff --git a/src/Speckle.Core/Api/Operations/Operations.Receive.cs b/src/Speckle.Core/Api/Operations/Operations.Receive.cs index 2502b6d..bdfa183 100644 --- a/src/Speckle.Core/Api/Operations/Operations.Receive.cs +++ b/src/Speckle.Core/Api/Operations/Operations.Receive.cs @@ -8,7 +8,9 @@ using Serilog.Context; using Speckle.Core.Logging; using Speckle.Core.Models; +using Speckle.Core.SchemaVersioning; using Speckle.Core.Serialisation; +using Speckle.Core.Serialisation.TypeCache; using Speckle.Core.Transports; namespace Speckle.Core.Api; @@ -38,6 +40,9 @@ public static partial class Operations /// The requested Speckle Object public static async Task Receive( string objectId, + ITypeCache typeCache, + ISchemaObjectUpgradeManager objectUpgradeManager, + System.Version payloadSchemaVersion, ITransport? remoteTransport = null, ITransport? localTransport = null, Action>? onProgressAction = null, @@ -62,7 +67,7 @@ public static partial class Operations // Setup Serializer BaseObjectDeserializerV2 serializerV2 = - new() + new(typeCache, objectUpgradeManager, payloadSchemaVersion) { ReadTransport = localTransport, OnProgressAction = internalProgressAction, diff --git a/src/Speckle.Core/Api/Operations/Operations.Send.Obsolete.cs b/src/Speckle.Core/Api/Operations/Operations.Send.Obsolete.cs deleted file mode 100644 index 24cfd31..0000000 --- a/src/Speckle.Core/Api/Operations/Operations.Send.Obsolete.cs +++ /dev/null @@ -1,241 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Serilog.Context; -using Speckle.Core.Logging; -using Speckle.Core.Models; -using Speckle.Core.Serialisation; -using Speckle.Core.Transports; -using Speckle.Newtonsoft.Json; -using Speckle.Newtonsoft.Json.Linq; - -namespace Speckle.Core.Api; - -public static partial class Operations -{ - private const string DEPRECATION_NOTICE = """ - This Send overload has been replaced by an overload with fewer function arguments. - We are no longer supporting SerializerV1, OnErrorAction, or handling disposal of transports. - Consider switching one of the other send overloads instead. - This function will be kept around for several releases, but will eventually be removed. - """; - - /// - [Obsolete("This overload has been deprecated along with serializer v1. Use other Send overloads instead.")] - [SuppressMessage("Naming", "CA1720:Identifier contains type name")] - public static Task Send(Base @object) => Send(@object, CancellationToken.None); - - /// - [Obsolete("This overload has been deprecated along with serializer v1. Use other Send overloads instead.")] - [SuppressMessage("Naming", "CA1720:Identifier contains type name")] - public static Task Send( - Base @object, - List? transports, - bool useDefaultCache, - Action>? onProgressAction, - Action? onErrorAction, - bool disposeTransports, - SerializerVersion serializerVersion = SerializerVersion.V2 - ) => - Send( - @object, - CancellationToken.None, - transports, - useDefaultCache, - onProgressAction, - onErrorAction, - disposeTransports, - serializerVersion - ); - - /// - [Obsolete("This overload has been deprecated along with serializer v1. Use other Send overloads instead.")] - [SuppressMessage("Naming", "CA1720:Identifier contains type name")] - public static Task Send( - Base @object, - List? transports, - bool useDefaultCache, - bool disposeTransports, - SerializerVersion serializerVersion = SerializerVersion.V2 - ) => - Send( - @object, - CancellationToken.None, - transports, - useDefaultCache, - null, - null, - disposeTransports, - serializerVersion - ); - - /// - [Obsolete("This overload has been deprecated along with serializer v1. Use other Send overloads instead.")] - [SuppressMessage("Naming", "CA1720:Identifier contains type name")] - public static Task Send( - Base @object, - bool disposeTransports, - SerializerVersion serializerVersion = SerializerVersion.V2 - ) => Send(@object, CancellationToken.None, null, true, null, null, disposeTransports, serializerVersion); - - /// - /// Sends an object via the provided transports. Defaults to the local cache. - /// - /// - /// This overload is deprecated. You should consider using - ///
- ///
or - ///
- ///
- /// These new overloads no longer support switching as v1 is now deprecated. - ///
- /// We also no longer offer the option to . - /// You should instead handle disposal yourself - /// using conventional mechanisms like the using keyword.
- ///
- /// This function overload will be kept around for several releases, but will eventually be removed. - ///
- /// The object you want to send. - /// A cancellation token that can be used by other objects or threads to send notice of cancellation. - /// Where you want to send them. - /// Toggle for the default cache. If set to false, it will only send to the provided transports. - /// Action that gets triggered on every progress tick (keeps track of all transports). - /// Use this to capture and handle any errors from within the transports. - /// - /// - /// The id (hash) of the object. - [SuppressMessage("Naming", "CA1720:Identifier contains type name")] - [Obsolete(DEPRECATION_NOTICE)] - public static async Task Send( - Base @object, - CancellationToken cancellationToken, - List? transports = null, - bool useDefaultCache = true, - Action>? onProgressAction = null, - Action? onErrorAction = null, - bool disposeTransports = false, - SerializerVersion serializerVersion = SerializerVersion.V2 - ) - { - transports ??= new List(); - using var sqLiteTransport = new SQLiteTransport { TransportName = "LC" }; - - if (transports.Count == 0 && useDefaultCache == false) - { - throw new ArgumentException( - "You need to provide at least one transport: cannot send with an empty transport list and no default cache.", - nameof(transports) - ); - } - - if (useDefaultCache) - { - transports.Insert(0, sqLiteTransport); - } - - var transportContext = transports.ToDictionary(t => t.TransportName, t => t.TransportContext); - - // make sure all logs in the operation have the proper context - using (LogContext.PushProperty("transportContext", transportContext)) - using (LogContext.PushProperty("correlationId", Guid.NewGuid().ToString())) - { - var sendTimer = Stopwatch.StartNew(); - SpeckleLog.Logger.Information("Starting send operation"); - - var internalProgressAction = GetInternalProgressAction(onProgressAction); - - BaseObjectSerializer? serializer = null; - JsonSerializerSettings? settings = null; - BaseObjectSerializerV2? serializerV2 = null; - if (serializerVersion == SerializerVersion.V1) - { - (serializer, settings) = GetSerializerInstance(); - serializer.WriteTransports = transports; - serializer!.OnProgressAction = internalProgressAction; - serializer.CancellationToken = cancellationToken; - serializer.OnErrorAction = onErrorAction; - } - else - { - serializerV2 = new BaseObjectSerializerV2(transports, internalProgressAction, false, cancellationToken); - } - - foreach (var t in transports) - { - t.OnProgressAction = internalProgressAction; - t.CancellationToken = cancellationToken; - t.BeginWrite(); - } - - string obj; - List transportAwaits; - if (serializerVersion == SerializerVersion.V1) - { - obj = JsonConvert.SerializeObject(@object, settings); - transportAwaits = serializer!.WriteTransports.Select(t => t.WriteComplete()).ToList(); - } - else - { - obj = serializerV2!.Serialize(@object); - transportAwaits = serializerV2.WriteTransports.Select(t => t.WriteComplete()).ToList(); - } - - if (cancellationToken.IsCancellationRequested) - { - SpeckleLog.Logger.Information( - "Send operation cancelled after {elapsed} seconds", - sendTimer.Elapsed.TotalSeconds - ); - cancellationToken.ThrowIfCancellationRequested(); - } - - await Task.WhenAll(transportAwaits).ConfigureAwait(false); - - foreach (var t in transports) - { - t.EndWrite(); - if (useDefaultCache && t is SQLiteTransport lc && lc.TransportName == "LC") - { - lc.Dispose(); - continue; - } - if (disposeTransports && t is IDisposable disp) - { - disp.Dispose(); - } - } - - if (cancellationToken.IsCancellationRequested) - { - SpeckleLog.Logger.Information("Send operation cancelled after {elapsed}", sendTimer.Elapsed.TotalSeconds); - cancellationToken.ThrowIfCancellationRequested(); - } - - var idToken = JObject.Parse(obj).GetValue("id"); - if (idToken == null) - { - throw new SpeckleException("Failed to get id of serialized object"); - } - - var hash = idToken.ToString(); - - sendTimer.Stop(); - SpeckleLog - .Logger.ForContext("transportElapsedBreakdown", transports.ToDictionary(t => t.TransportName, t => t.Elapsed)) - .ForContext("note", "the elapsed summary doesn't need to add up to the total elapsed... Threading magic...") - .ForContext("serializerElapsed", serializerV2?.Elapsed) - .Information( - "Finished sending {objectCount} objects after {elapsed}, result {objectId}", - transports.Max(t => t.SavedObjectCount), - sendTimer.Elapsed.TotalSeconds, - hash - ); - return hash; - } - } -} diff --git a/src/Speckle.Core/Api/Operations/Operations.Serialize.cs b/src/Speckle.Core/Api/Operations/Operations.Serialize.cs index bd3bea9..ef6fe27 100644 --- a/src/Speckle.Core/Api/Operations/Operations.Serialize.cs +++ b/src/Speckle.Core/Api/Operations/Operations.Serialize.cs @@ -1,10 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading; using Speckle.Core.Logging; using Speckle.Core.Models; +using Speckle.Core.SchemaVersioning; using Speckle.Core.Serialisation; +using Speckle.Core.Serialisation.TypeCache; using Speckle.Newtonsoft.Json; namespace Speckle.Core.Api; @@ -40,112 +38,9 @@ public static string Serialize(Base value, CancellationToken cancellationToken = /// was not valid JSON /// cannot be deserialised to type /// contains closure references (see Remarks) - public static Base Deserialize(string value, CancellationToken cancellationToken = default) + public static Base Deserialize(string value, ITypeCache typeCache, ISchemaObjectUpgradeManager objectUpgradeManager, System.Version payloadSchemaVersion, CancellationToken cancellationToken = default) { - var deserializer = new BaseObjectDeserializerV2 { CancellationToken = cancellationToken }; + var deserializer = new BaseObjectDeserializerV2(typeCache, objectUpgradeManager, payloadSchemaVersion) { CancellationToken = cancellationToken }; return deserializer.Deserialize(value); } - - #region obsolete - - [Obsolete("Serializer v1 is deprecated, use other overload(s)")] - public static string Serialize( - Base value, - SerializerVersion serializerVersion, - CancellationToken cancellationToken = default - ) - { - if (serializerVersion == SerializerVersion.V1) - { - var (serializer, settings) = GetSerializerInstance(); - serializer.CancellationToken = cancellationToken; - - return JsonConvert.SerializeObject(value, settings); - } - else - { - return Serialize(value, cancellationToken); - } - } - - [Obsolete("Serializer v1 is deprecated, use other overload(s)")] - public static Base Deserialize( - string value, - SerializerVersion serializerVersion, - CancellationToken cancellationToken = default - ) - { - if (serializerVersion == SerializerVersion.V1) - { - var (serializer, settings) = GetSerializerInstance(); - serializer.CancellationToken = cancellationToken; - var ret = JsonConvert.DeserializeObject(value, settings); - return ret ?? throw new SpeckleException($"{nameof(value)} failed to deserialize to a {nameof(Base)} object"); - } - - return Deserialize(value, cancellationToken); - } - - [Obsolete("Please use the Deserialize(string value) function.", true)] - public static List DeserializeArray( - string objectArr, - SerializerVersion serializerVersion = SerializerVersion.V2 - ) - { - throw new NotImplementedException(); - } - - [Obsolete( - "Please use the Deserialize(Base @object) function. This function will be removed in later versions.", - true - )] - public static Dictionary DeserializeDictionary(string dictionary) - { - throw new NotImplementedException(); - } - - [Obsolete("Use overload that takes cancellation token last")] - [SuppressMessage("Naming", "CA1720:Identifier contains type name")] - public static Base Deserialize( - string @object, - CancellationToken cancellationToken, - SerializerVersion serializerVersion = SerializerVersion.V2 - ) - { - return Deserialize(@object, serializerVersion, cancellationToken); - } - - [Obsolete("Use overload that takes cancellation token last")] - [SuppressMessage("Naming", "CA1720:Identifier contains type name")] - public static string Serialize( - Base @object, - CancellationToken cancellationToken, - SerializerVersion serializerVersion = SerializerVersion.V2 - ) - { - return Serialize(@object, serializerVersion, cancellationToken); - } - - /// - /// Serializes a list of objects. Note: if you want to save and persist objects to speckle, please use any of the "Send" methods. - /// - /// - /// - [Obsolete("Please use the Serialize(Base value) function. This function will be removed in later versions.", true)] - public static string Serialize(List objects) - { - throw new NotImplementedException(); - } - - /// - /// Serializes a list of objects. Note: if you want to save and persist objects to speckle, please use any of the "Send" methods. - /// - /// - /// - [Obsolete("Please use the Serialize(Base value) function. This function will be removed in later versions.")] - public static string Serialize(Dictionary objects) - { - throw new NotImplementedException(); - } - #endregion } diff --git a/src/Speckle.Core/Api/Operations/Operations.cs b/src/Speckle.Core/Api/Operations/Operations.cs index 83e153b..67e61b7 100644 --- a/src/Speckle.Core/Api/Operations/Operations.cs +++ b/src/Speckle.Core/Api/Operations/Operations.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using Speckle.Core.Serialisation; using Speckle.Newtonsoft.Json; using Speckle.Newtonsoft.Json.Serialization; @@ -14,25 +12,6 @@ namespace Speckle.Core.Api; /// public static partial class Operations { - /// - /// Convenience method to instantiate an instance of the default object serializer and settings pre-populated with it. - /// - [Obsolete("V1 Serializer is deprecated. Use " + nameof(BaseObjectSerializerV2))] - public static (BaseObjectSerializer, JsonSerializerSettings) GetSerializerInstance() - { - var serializer = new BaseObjectSerializer(); - var settings = new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore, - ContractResolver = new CamelCasePropertyNamesContractResolver(), - Formatting = Formatting.None, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - Converters = new List { serializer } - }; - - return (serializer, settings); - } - /// /// Factory for progress actions used internally inside send and receive methods. /// diff --git a/src/Speckle.Core/Kits/Attributes.cs b/src/Speckle.Core/Kits/Attributes.cs index f706859..b46a6e1 100644 --- a/src/Speckle.Core/Kits/Attributes.cs +++ b/src/Speckle.Core/Kits/Attributes.cs @@ -1,6 +1,4 @@ #nullable disable -using System; - namespace Speckle.Core.Kits; [AttributeUsage(AttributeTargets.Constructor)] diff --git a/src/Speckle.Core/Kits/ConverterInterfaces/IFinalizable.cs b/src/Speckle.Core/Kits/ConverterInterfaces/IFinalizable.cs deleted file mode 100644 index 7f10c2f..0000000 --- a/src/Speckle.Core/Kits/ConverterInterfaces/IFinalizable.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Speckle.Core.Kits.ConverterInterfaces; - -public interface IFinalizable -{ - void FinalizeConversion(); -} diff --git a/src/Speckle.Core/Kits/Exceptions.cs b/src/Speckle.Core/Kits/Exceptions.cs index 6e3c4d4..98c81c9 100644 --- a/src/Speckle.Core/Kits/Exceptions.cs +++ b/src/Speckle.Core/Kits/Exceptions.cs @@ -1,37 +1,7 @@ -using System; using Speckle.Core.Logging; namespace Speckle.Core.Kits; -/// -/// Exception thrown when an fails to load/initialise -/// -/// -/// Does NOT inherit from , because this usage of this exception is not dependent on Speckle Data (user data) -/// Ideally, this exception should contain a meaningful message, and a reference to the -/// -public class KitException : Exception -{ - /// - /// A reference to the that failed to perform - /// - public ISpeckleKit? Kit { get; } - - public KitException(string? message, ISpeckleKit? kit, Exception? innerException = null) - : base(message, innerException) - { - Kit = kit; - } - - public KitException() { } - - public KitException(string? message) - : base(message) { } - - public KitException(string? message, Exception? innerException) - : base(message, innerException) { } -} - /// /// Exception thrown when conversion of an object was not successful /// diff --git a/src/Speckle.Core/Kits/ISpeckleConverter.cs b/src/Speckle.Core/Kits/ISpeckleConverter.cs deleted file mode 100644 index c7ba36c..0000000 --- a/src/Speckle.Core/Kits/ISpeckleConverter.cs +++ /dev/null @@ -1,152 +0,0 @@ -#nullable disable -using System.Collections.Generic; -using Speckle.Core.Models; - -namespace Speckle.Core.Kits; - -public interface ISpeckleConverter -{ - string Description { get; } - string Name { get; } - string Author { get; } - string WebsiteOrEmail { get; } - - /// - /// Keeps track of the conversion process - /// - public ProgressReport Report { get; } - - /// - /// Decides what to do when an element being received already exists - /// - public ReceiveMode ReceiveMode { get; set; } - - /// - /// Converts a native object to a Speckle one - /// - /// Native object to convert - /// - /// - public Base ConvertToSpeckle(object value); - - /// - /// Converts a list of objects to Speckle. - /// - /// - /// - /// - public List ConvertToSpeckle(List values); - - /// - /// Checks if it can convert a native object to a Speckle one - /// - /// Native object to convert - /// - public bool CanConvertToSpeckle(object value); - - /// - /// Converts a Speckle object to a native one - /// - /// Speckle object to convert - /// - /// - public object ConvertToNative(Base value); - - /// - /// Converts a list of Speckle objects to a native ones. - /// - /// - /// - /// - public List ConvertToNative(List values); - - /// - /// Converts a given speckle objects as a generic native object. - /// This should assume has been called and returned True, - /// or call it within this method's implementation to ensure non-displayable objects are gracefully handled. - /// - /// - /// This method should not try to convert an object to it's native representation (i.e Speckle Wall -> Wall), - /// but rather use the 'displayValue' of that wall to create a geometrically correct representation of that object - /// in the native application. - /// An object may be able to be converted both with and . - /// In this case, deciding which to use is dependent on each connector developer. - /// Preferably, should be used as a fallback to the logic. - /// - /// Speckle object to convert - /// The native object that resulted after converting the input - public object ConvertToNativeDisplayable(Base value); - - /// - /// Checks if it can convert a Speckle object to a native one - /// - /// Speckle object to convert - /// - public bool CanConvertToNative(Base value); - - /// - /// Checks to verify if a given object is: 1) displayable and 2) can be supported for conversion to the native application. - /// An object is considered "displayable" if it has a 'displayValue' property (defined in its class or dynamically attached to it, detached or not). - /// - /// - /// An object may return "True" for both and - /// In this case, deciding which to use is dependent on each connector developer. - /// Preferably, should be used as a fallback to the logic. - /// Objects found in the 'displayValue' property are assumed to be universally convertible by all converters and the viewer, but are not guaranteed to be so. - /// - /// Speckle object to convert - /// True if the object is "displayable" and the converter supports native conversion of the given speckle object in particular. - public bool CanConvertToNativeDisplayable(Base value); - - /// - /// Returns a list of applications serviced by this converter - /// - /// - public IEnumerable GetServicedApplications(); - - /// - /// Sets the application document that the converter is targeting - /// - /// The current application document - public void SetContextDocument(object doc); - - /// - /// Some converters need to know which other objects are being converted, in order to sort relationships between them (ie, Revit). Use this method to set them. - /// - /// - public void SetContextObjects(List objects); - - /// - /// Some converters need to know which objects have been converted before in order to update them (ie, Revit). Use this method to set them. - /// - /// - public void SetPreviousContextObjects(List objects); - - /// - /// Some converters need to be able to receive some settings to modify their internal behaviour (i.e. Rhino's Brep Meshing options). Use this method to set them. - /// - /// The object representing the settings for your converter. - public void SetConverterSettings(object settings); -} - -// NOTE: Do not change the order of the existing ones -/// -/// Receive modes indicate what to do and not do when receiving objects -/// -public enum ReceiveMode -{ - /// - /// Attemts updating previously received objects by ID, deletes previously received objects that do not exist anymore and creates new ones - /// - Update, - - /// - /// Always creates new objects - /// - Create, - - /// - /// Ignores updating previously received objects and does not attempt updating or deleting them, creates new objects - /// - Ignore -} diff --git a/src/Speckle.Core/Kits/ISpeckleKit.cs b/src/Speckle.Core/Kits/ISpeckleKit.cs deleted file mode 100644 index c784ab0..0000000 --- a/src/Speckle.Core/Kits/ISpeckleKit.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Speckle.Core.Kits; - -/// -/// Defines the basic interface for creating a "Speckle Kit" -/// -public interface ISpeckleKit -{ - /// - /// Gets all the object types (the object model) provided by this kit. - /// - IEnumerable Types { get; } - - /// - /// Gets all available converters for this Kit. - /// - IEnumerable Converters { get; } - - /// - /// Gets this Kit's description. - /// - string Description { get; } - - /// - /// Gets this Kit's name. - /// - string Name { get; } - - /// - /// Gets this Kit's author. - /// - string Author { get; } - - /// - /// Gets the website (or email) to contact the Kit's author. - /// - string WebsiteOrEmail { get; } - - /// - /// Tries to load a converter for a specific . - /// - /// The host app string for which a is desired. see - /// The converter for the specific - /// Thrown if the requested converter failed to load - public ISpeckleConverter LoadConverter(string app); -} diff --git a/src/Speckle.Core/Kits/KitDeclaration.cs b/src/Speckle.Core/Kits/KitDeclaration.cs deleted file mode 100644 index abf8bc3..0000000 --- a/src/Speckle.Core/Kits/KitDeclaration.cs +++ /dev/null @@ -1,30 +0,0 @@ -#nullable disable -using System; -using System.Collections.Generic; -using System.Linq; -using Speckle.Core.Models; - -namespace Speckle.Core.Kits; - -/// -/// Needed so we can properly deserialize all the Base-derived objects from Speckle.Core itself. -/// -public sealed class CoreKit : ISpeckleKit -{ - public IEnumerable Types => GetType().Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(Base))); - - public string Description => "Base Speckle models for revisions, streams, etc."; - - public string Name => nameof(CoreKit); - - public string Author => "Dimitrie"; - - public string WebsiteOrEmail => "hello@speckle.systems"; - - public IEnumerable Converters => new List(); - - public ISpeckleConverter LoadConverter(string app) - { - return null; - } -} diff --git a/src/Speckle.Core/Kits/KitManager.cs b/src/Speckle.Core/Kits/KitManager.cs deleted file mode 100644 index 0517445..0000000 --- a/src/Speckle.Core/Kits/KitManager.cs +++ /dev/null @@ -1,339 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using Speckle.Core.Helpers; -using Speckle.Core.Logging; -using Speckle.Core.Models; - -namespace Speckle.Core.Kits; - -public static class KitManager -{ - private static string? s_kitsFolder; - - public static readonly AssemblyName SpeckleAssemblyName = typeof(Base).GetTypeInfo().Assembly.GetName(); - - private static readonly Dictionary s_speckleKits = new(); - - private static List s_availableTypes = new(); - - private static bool s_initialized; - - /// - /// Local installations store kits in C:\Users\USERNAME\AppData\Roaming\Speckle\Kits - /// Admin/System-wide installations in C:\ProgramData\Speckle\Kits - /// - public static string KitsFolder - { - get => s_kitsFolder ??= SpecklePathProvider.KitsFolderPath; - set => s_kitsFolder = value; - } - - /// - /// Returns a list of all the kits found on this user's device. - /// - public static IEnumerable Kits - { - get - { - Initialize(); - return s_speckleKits.Values.Where(v => v != null); //NOTE: null check here should be unnecessary - } - } - - /// - /// Returns a list of all the types found in all the kits on this user's device. - /// - public static IEnumerable Types - { - get - { - Initialize(); - return s_availableTypes; - } - } - - /// - /// Checks whether a specific kit exists. - /// - /// - /// - public static bool HasKit(string assemblyFullName) - { - Initialize(); - return s_speckleKits.ContainsKey(assemblyFullName); - } - - /// - /// Gets a specific kit. - /// - /// - /// - public static ISpeckleKit GetKit(string assemblyFullName) - { - Initialize(); - return s_speckleKits[assemblyFullName]; - } - - /// - /// Gets the default Speckle provided kit, "Objects". - /// - /// - public static ISpeckleKit GetDefaultKit() - { - Initialize(); - return s_speckleKits.First(kvp => kvp.Value.Name == "Objects").Value; - } - - /// - /// Returns all the kits with potential converters for the software app. - /// - /// - /// - public static IEnumerable GetKitsWithConvertersForApp(string app) - { - foreach (var kit in Kits) - { - if (kit.Converters.Contains(app)) - { - yield return kit; - } - } - } - - /// - /// Tells the kit manager to initialise from a specific location. - /// - /// - public static void Initialize(string kitFolderLocation) - { - if (s_initialized) - { - SpeckleLog.Logger.Error("{objectType} is already initialised", typeof(KitManager)); - throw new SpeckleException( - "The kit manager has already been initialised. Make sure you call this method earlier in your code!" - ); - } - - KitsFolder = kitFolderLocation; - Load(); - s_initialized = true; - } - - #region Private Methods - - private static void Initialize() - { - if (!s_initialized) - { - Load(); - s_initialized = true; - } - } - - private static void Load() - { - SpeckleLog.Logger.Information("Initializing Kit Manager in {KitsFolder}", SpecklePathProvider.KitsFolderPath); - - GetLoadedSpeckleReferencingAssemblies(); - LoadSpeckleReferencingAssemblies(); - - s_availableTypes = s_speckleKits - .Where(kit => kit.Value != null) //Null check should be unnecessary - .SelectMany(kit => kit.Value.Types) - .ToList(); - } - - // recursive search for referenced assemblies - public static List GetReferencedAssemblies() - { - var returnAssemblies = new List(); - var loadedAssemblies = new HashSet(); - var assembliesToCheck = new Queue(); - - assembliesToCheck.Enqueue(Assembly.GetEntryAssembly()); - - while (assembliesToCheck.Count > 0) - { - var assemblyToCheck = assembliesToCheck.Dequeue(); - - if (assemblyToCheck == null) - { - continue; - } - - foreach (var reference in assemblyToCheck.GetReferencedAssemblies()) - { - // filtering out system dlls - if (reference.FullName.StartsWith("System.")) - { - continue; - } - - if (reference.FullName.StartsWith("Microsoft.")) - { - continue; - } - - if (loadedAssemblies.Contains(reference.FullName)) - { - continue; - } - - Assembly assembly; - try - { - assembly = Assembly.Load(reference); - } - catch (SystemException ex) when (ex is IOException or BadImageFormatException) - { - continue; - } - - assembliesToCheck.Enqueue(assembly); - loadedAssemblies.Add(reference.FullName); - returnAssemblies.Add(assembly); - } - } - - return returnAssemblies; - } - - private static void GetLoadedSpeckleReferencingAssemblies() - { - List assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList(); - assemblies.AddRange(GetReferencedAssemblies()); - - foreach (var assembly in assemblies) - { - if (assembly.IsDynamic || assembly.ReflectionOnly) - { - continue; - } - - if (!assembly.IsReferencing(SpeckleAssemblyName)) - { - continue; - } - - if (s_speckleKits.ContainsKey(assembly.FullName)) - { - continue; - } - - var kitClass = GetKitClass(assembly); - if (kitClass == null) - { - continue; - } - - if (Activator.CreateInstance(kitClass) is ISpeckleKit speckleKit) - { - s_speckleKits.Add(assembly.FullName, speckleKit); - } - } - } - - private static void LoadSpeckleReferencingAssemblies() - { - if (!Directory.Exists(KitsFolder)) - { - return; - } - - var directories = Directory.GetDirectories(KitsFolder); - - foreach (var directory in directories) - { - foreach (var assemblyPath in Directory.EnumerateFiles(directory, "*.dll")) - { - var unloadedAssemblyName = SafeGetAssemblyName(assemblyPath); - - if (unloadedAssemblyName == null) - { - continue; - } - - try - { - var assembly = Assembly.LoadFrom(assemblyPath); - var kitClass = GetKitClass(assembly); - if (assembly.IsReferencing(SpeckleAssemblyName) && kitClass != null) - { - if (!s_speckleKits.ContainsKey(assembly.FullName)) - { - if (Activator.CreateInstance(kitClass) is ISpeckleKit speckleKit) - { - s_speckleKits.Add(assembly.FullName, speckleKit); - } - } - } - } - catch (FileLoadException) { } - catch (BadImageFormatException) { } - } - } - } - - private static Type? GetKitClass(Assembly assembly) - { - try - { - var kitClass = assembly - .GetTypes() - .FirstOrDefault(type => - { - return type.GetInterfaces().Any(iface => iface.Name == nameof(ISpeckleKit)); - }); - - return kitClass; - } - catch (ReflectionTypeLoadException) - { - return null; - } - } - - private static AssemblyName? SafeGetAssemblyName(string? assemblyPath) - { - try - { - return AssemblyName.GetAssemblyName(assemblyPath); - } - catch (Exception ex) when (ex is ArgumentException or IOException or BadImageFormatException) - { - return null; - } - } - - #endregion -} - -internal static class AssemblyExtensions -{ - /// - /// Indicates if a given assembly references another which is identified by its name. - /// - /// The assembly which will be probed. - /// The reference assembly name. - /// A boolean value indicating if there is a reference. - public static bool IsReferencing(this Assembly assembly, AssemblyName referenceName) - { - if (AssemblyName.ReferenceMatchesDefinition(assembly.GetName(), referenceName)) - { - return true; - } - - foreach (var referencedAssemblyName in assembly.GetReferencedAssemblies()) - { - if (AssemblyName.ReferenceMatchesDefinition(referencedAssemblyName, referenceName)) - { - return true; - } - } - - return false; - } -} diff --git a/src/Speckle.Core/Kits/Units.cs b/src/Speckle.Core/Kits/Units.cs index e3fec56..b939b53 100644 --- a/src/Speckle.Core/Kits/Units.cs +++ b/src/Speckle.Core/Kits/Units.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.Contracts; using Speckle.Core.Common; diff --git a/src/Speckle.Core/Models/Base.cs b/src/Speckle.Core/Models/Base.cs index b5da1ed..7c139a3 100644 --- a/src/Speckle.Core/Models/Base.cs +++ b/src/Speckle.Core/Models/Base.cs @@ -287,23 +287,4 @@ public Base ShallowCopy() return myDuplicate; } - - #region Obsolete - /// - [Obsolete("Serializer v1 is deprecated, use other overload(s)", true)] - public string GetId(SerializerVersion serializerVersion) - { - return GetId(false, serializerVersion); - } - - /// - [Obsolete("Serializer v1 is deprecated, use other overload(s)", true)] - public string GetId(bool decompose, SerializerVersion serializerVersion) - { - throw new NotImplementedException( - "Overload has been deprecated along with SerializerV1, use other overload (uses SerializerV2)" - ); - } - - #endregion } diff --git a/src/Speckle.Core/Models/GraphTraversal/DefaultTraversal.cs b/src/Speckle.Core/Models/GraphTraversal/DefaultTraversal.cs index 885b031..4e87e19 100644 --- a/src/Speckle.Core/Models/GraphTraversal/DefaultTraversal.cs +++ b/src/Speckle.Core/Models/GraphTraversal/DefaultTraversal.cs @@ -111,102 +111,4 @@ public static IEnumerable DisplayValueAndElementsAliases(Base _) } #endregion - - #region Legacy function varients - - [Obsolete("Renamed to " + nameof(ElementsPropAliases))] - [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Obsolete")] - public static IReadOnlyList elementsPropAliases => ElementsPropAliases; - - [Obsolete("Renamed to " + nameof(DisplayValuePropAliases))] - [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Obsolete")] - public static IReadOnlyList displayValuePropAliases => DisplayValuePropAliases; - - [Obsolete("Renamed to " + nameof(DefinitionPropAliases))] - [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Obsolete")] - public static IReadOnlyList definitionPropAliases => DefinitionPropAliases; - - [Obsolete("Renamed to " + nameof(GeometryPropAliases))] - [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Obsolete")] - public static IReadOnlyList geometryPropAliases => GeometryPropAliases; - - [Obsolete("Renamed to " + nameof(DisplayValueAndElementsPropAliases))] - [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Obsolete")] - [SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Obsolete")] - public static string[] displayValueAndElementsPropAliases => DisplayValueAndElementsPropAliases; - - /// - /// - /// - /// - [Obsolete($"Consider using {nameof(CreateTraversalFunc)}")] - public static GraphTraversal CreateTraverseFunc(ISpeckleConverter converter) - { - return CreateLegacyTraverseFunc(converter.CanConvertToNative); - } - - /// - /// Legacy traversal rule that was dependent on the converter - /// - /// - /// Treats convertable objects and objects with displayValues as "convertable" such that only elements and dynamic props will be traversed - /// New code should use instead. - /// - /// - /// - [Obsolete($"Consider using {nameof(CreateTraversalFunc)}")] - public static GraphTraversal CreateLegacyTraverseFunc(Func canConvertToNative) - { - var convertableRule = TraversalRule - .NewTraversalRule() - .When(b => canConvertToNative(b)) - .When(HasDisplayValue) - .ContinueTraversing(_ => ElementsPropAliases); - - return new GraphTraversal(convertableRule, s_ignoreResultsRule, DefaultRule); - } - - /// - /// Traverses until finds a convertable object then HALTS deeper traversal - /// - /// - /// The DUI2 Revit connector does traversal, - /// so this traversal is a shallow traversal for directly convertable objects, - /// and a deep traversal for all other types - /// New code should use instead. - /// - /// - /// - [Obsolete($"Consider using {nameof(CreateTraversalFunc)}")] - public static GraphTraversal CreateRevitTraversalFunc(ISpeckleConverter converter) - { - var convertableRule = TraversalRule - .NewTraversalRule() - .When(converter.CanConvertToNative) - .When(HasDisplayValue) - .ContinueTraversing(None); - - return new GraphTraversal(convertableRule, s_ignoreResultsRule, DefaultRule); - } - - /// - /// Traverses until finds a convertable object (or fallback) then traverses members - /// - /// - /// New code should use instead. - /// - /// - /// - [Obsolete($"Consider using {nameof(CreateTraversalFunc)}")] - public static GraphTraversal CreateBIMTraverseFunc(ISpeckleConverter converter) - { - var bimElementRule = TraversalRule - .NewTraversalRule() - .When(converter.CanConvertToNative) - .ContinueTraversing(ElementsAliases); - - return new GraphTraversal(bimElementRule, s_ignoreResultsRule, DefaultRule); - } - - #endregion } diff --git a/src/Speckle.Core/Reflection/ITypeFinder.cs b/src/Speckle.Core/Reflection/ITypeFinder.cs new file mode 100644 index 0000000..53ad935 --- /dev/null +++ b/src/Speckle.Core/Reflection/ITypeFinder.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace Speckle.Core.Reflection; + +public interface ITypeFinder +{ + IList GetTypesWhereSubclassOf(IEnumerable assemblies, Type subclassOf); + IList GetTypesWhereImplementing(IEnumerable assemblies, Type subclassOf); +} diff --git a/src/Speckle.Core/Reflection/ITypeInstanceResolver.cs b/src/Speckle.Core/Reflection/ITypeInstanceResolver.cs new file mode 100644 index 0000000..834e83a --- /dev/null +++ b/src/Speckle.Core/Reflection/ITypeInstanceResolver.cs @@ -0,0 +1,7 @@ +namespace Speckle.Core.Reflection; + +public interface ITypeInstanceResolver where TType : class +{ + TType Resolve(string typeNameWithSuffix); + bool TryResolve(string typeNameWithSuffix, out TType instance); +} diff --git a/src/Speckle.Core/Reflection/NamedTypeAttribute.cs b/src/Speckle.Core/Reflection/NamedTypeAttribute.cs new file mode 100644 index 0000000..a0b92ed --- /dev/null +++ b/src/Speckle.Core/Reflection/NamedTypeAttribute.cs @@ -0,0 +1,23 @@ +using Speckle.Core.Common; + +namespace Speckle.Core.Reflection; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class NamedTypeAttribute : Attribute +{ + public string TypeName { get; private set; } + public string TypeNameWithKeySuffix { get; private set; } + + public NamedTypeAttribute( + Type type, + string keySuffix) + { + TypeName = type.FullName.NotNull(); + + // creating this key here needs to be consistent with where it is created + TypeNameWithKeySuffix = CreateTypeNameWithKeySuffix(TypeName, keySuffix); + } + + // POC: we can absolutely decide how we wish to construct this + public static string CreateTypeNameWithKeySuffix(string typeName, string keySuffix) => $"{typeName}+{keySuffix}"; +} diff --git a/src/Speckle.Core/Reflection/SingletonTypeInstanceResolver.cs b/src/Speckle.Core/Reflection/SingletonTypeInstanceResolver.cs new file mode 100644 index 0000000..276b344 --- /dev/null +++ b/src/Speckle.Core/Reflection/SingletonTypeInstanceResolver.cs @@ -0,0 +1,42 @@ +namespace Speckle.Core.Reflection; + +public class SingletonTypeInstanceResolver : ITypeInstanceResolver + where TType : class +{ + private readonly ITypeFinder _typeFinder; + private readonly Dictionary _typeInstances = new(); + + public SingletonTypeInstanceResolver(ITypeFinder typeFinder) + { + _typeFinder = typeFinder; + + // POC: not wild about evaluating this during construction but... is that still a thing? + // could be done on the fly... it will require some locking semantics if used during deserialisation + var foundTypes = _typeFinder.GetTypesWhereImplementing(AppDomain.CurrentDomain.GetAssemblies(), typeof(TType)); + + // let's make an instance of each of these + // could also be done on the fly + foreach (var type in foundTypes) + { + // the type must have the attribute so we know what we can upgrade *from* and upgrade *to* and + // also what the Name and suffix should be + + // POC: we may want something other than default exception when casting... + // maybe but there should always be one ONLY one + var namedType = (NamedTypeAttribute) type.GetCustomAttributes(typeof(NamedTypeAttribute), false).Single(); + _typeInstances[namedType.TypeNameWithKeySuffix] = (TType) Activator.CreateInstance(type); + } + } + + public TType Resolve(string typeNameWithSuffix) => _typeInstances[typeNameWithSuffix]; + + public bool TryResolve(string typeNameWithSuffix, out TType instance) + { + if (_typeInstances.TryGetValue(typeNameWithSuffix, out instance)) + { + return true; + } + + return false; + } +} diff --git a/src/Speckle.Core/Reflection/TypeFinder.cs b/src/Speckle.Core/Reflection/TypeFinder.cs new file mode 100644 index 0000000..0279f3b --- /dev/null +++ b/src/Speckle.Core/Reflection/TypeFinder.cs @@ -0,0 +1,48 @@ +using System.Reflection; + +namespace Speckle.Core.Reflection; + +public class TypeFinder : ITypeFinder +{ + public IList GetTypesWhereSubclassOf(IEnumerable assemblies, Type subclassOf) + { + List types = new(); + + // this assumes the DUI2 objects are not already loaded + foreach (var assembly in assemblies) + { + try + { + types.AddRange(assembly.GetTypes().Where(x => x.IsSubclassOf(subclassOf) && !x.IsAbstract)); + } + // POC: right one? more? + catch (ReflectionTypeLoadException) + { + // POC: guard against loading things that cause explosions due to not being able to load assemblies but are not real issues + } + } + + return types; + } + + public IList GetTypesWhereImplementing(IEnumerable assemblies, Type subclassOf) + { + List types = new(); + + // this assumes the DUI2 objects are not already loaded + foreach (var assembly in assemblies) + { + try + { + types.AddRange(assembly.GetTypes().Where(x => x.GetInterfaces().Contains(subclassOf) && !x.IsAbstract)); + } + // POC: right one? more? + catch (ReflectionTypeLoadException) + { + // POC: guard against loading things that cause explosions due to not being able to load assemblies but are not real issues + } + } + + return types; + } +} diff --git a/src/Speckle.Core/SchemaVersioning/AbstractSchemaObjectUpgrader.cs b/src/Speckle.Core/SchemaVersioning/AbstractSchemaObjectUpgrader.cs new file mode 100644 index 0000000..48344c5 --- /dev/null +++ b/src/Speckle.Core/SchemaVersioning/AbstractSchemaObjectUpgrader.cs @@ -0,0 +1,24 @@ +using Speckle.Core.Models; + +namespace Speckle.Core.SchemaVersioning; + +public abstract class AbstractSchemaObjectBaseUpgrader : ISchemaObjectUpgrader + where TInputType : Base + where TOutputType : Base +{ + public AbstractSchemaObjectBaseUpgrader(Version from, Version to) + { + UpgradeFrom = from; + UpgradeTo = to; + } + + public Base Upgrade(Base input) + { + return Upgrade((TInputType) input); + } + + public abstract TOutputType Upgrade(TInputType input); + + public Version UpgradeFrom { get; private set; } + public Version UpgradeTo { get; private set; } +} diff --git a/src/Speckle.Core/SchemaVersioning/ISchemaObjectUpgradeManager.cs b/src/Speckle.Core/SchemaVersioning/ISchemaObjectUpgradeManager.cs new file mode 100644 index 0000000..8b05634 --- /dev/null +++ b/src/Speckle.Core/SchemaVersioning/ISchemaObjectUpgradeManager.cs @@ -0,0 +1,7 @@ +namespace Speckle.Core.SchemaVersioning; + +public interface ISchemaObjectUpgradeManager + where TInputType : class where TOutputType : class +{ + TOutputType UpgradeObject(TInputType input, string typeName, Version inputSchemaVersion, Version loadedSchemaVersion); +} diff --git a/src/Speckle.Core/SchemaVersioning/ISchemaObjectUpgrader.cs b/src/Speckle.Core/SchemaVersioning/ISchemaObjectUpgrader.cs new file mode 100644 index 0000000..3e2265d --- /dev/null +++ b/src/Speckle.Core/SchemaVersioning/ISchemaObjectUpgrader.cs @@ -0,0 +1,10 @@ +namespace Speckle.Core.SchemaVersioning; + +public interface ISchemaObjectUpgrader + where TInputType : class where TOutputType : class +{ + TOutputType Upgrade(TInputType input); + + Version UpgradeFrom { get; } + Version UpgradeTo { get; } +} diff --git a/src/Speckle.Core/SchemaVersioning/SchemaObjectUpgradeManager.cs b/src/Speckle.Core/SchemaVersioning/SchemaObjectUpgradeManager.cs new file mode 100644 index 0000000..1b38b67 --- /dev/null +++ b/src/Speckle.Core/SchemaVersioning/SchemaObjectUpgradeManager.cs @@ -0,0 +1,57 @@ +using Speckle.Core.Logging; +using Speckle.Core.Reflection; + +namespace Speckle.Core.SchemaVersioning; + +public class SchemaObjectUpgradeManager : ISchemaObjectUpgradeManager + where TInputType : class where TOutputType : class +{ + // POC: use manual activation, newing the interface and caching the upgrader but... + // we will need to inject this later most likely + private readonly ITypeInstanceResolver> _typeInstanceResolver; + + public SchemaObjectUpgradeManager(ITypeInstanceResolver> typeInstanceResolver) + { + _typeInstanceResolver = typeInstanceResolver; + } + + public TOutputType UpgradeObject(TInputType input, string typeName, Version inputSchemaVersion, Version loadedSchemaVersion) + { + TOutputType? upgraded = null; + + // we try and upgrade while-ever the versions don't match + while (inputSchemaVersion < loadedSchemaVersion) + { + // building this must be done consistently + string typeKey = NamedTypeAttribute.CreateTypeNameWithKeySuffix(typeName, inputSchemaVersion.ToString()); + + // POC: do we expect there must always be types? + if (!_typeInstanceResolver.TryResolve(typeKey, out ISchemaObjectUpgrader upgrader)) + { + // there's no upgrader for this + break; + } + + upgraded = upgrader.Upgrade(input); + inputSchemaVersion = upgrader.UpgradeTo; + } + + // if we didn't do any upgrading, then we should be pass the input directly to the output + // BUT in cases where there is a conversion + if (upgraded is null) + { + upgraded = input as TOutputType; + if (upgraded is null) + { + // POC: we'll want some exception type here because we probably want this to explode always + // even if we change from Base to an IBase, it will be easy to do in a big bang, because Base can IBase cam implement + // I *think* this means we can neatly use this same class with different input and output types + // to nicely migrate to a different base if we wished to + throw new SpeckleException( + ($"Failed to convert '{input.GetType().FullName}' to '{typeof(TOutputType).FullName}'")); + } + } + + return upgraded; + } +} diff --git a/src/Speckle.Core/Serialisation/BaseObjectDeserializerV2.cs b/src/Speckle.Core/Serialisation/BaseObjectDeserializerV2.cs index 5213c3f..19a5e8b 100644 --- a/src/Speckle.Core/Serialisation/BaseObjectDeserializerV2.cs +++ b/src/Speckle.Core/Serialisation/BaseObjectDeserializerV2.cs @@ -1,21 +1,18 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Reflection; -using System.Threading; -using System.Threading.Tasks; using Speckle.Core.Common; using Speckle.Core.Logging; using Speckle.Core.Models; +using Speckle.Core.SchemaVersioning; using Speckle.Core.Serialisation.SerializationUtilities; +using Speckle.Core.Serialisation.TypeCache; using Speckle.Core.Transports; using Speckle.Newtonsoft.Json; using Speckle.Newtonsoft.Json.Linq; namespace Speckle.Core.Serialisation; -public sealed class BaseObjectDeserializerV2 +public sealed class BaseObjectDeserializerV2 : ISpeckleDeserializer { private bool _isBusy; private readonly object _callbackLock = new(); @@ -23,11 +20,6 @@ public sealed class BaseObjectDeserializerV2 // id -> Base if already deserialized or id -> Task if was handled by a bg thread private Dictionary? _deserializedObjects; - /// - /// Property that describes the type of the object. - /// - private const string TYPE_DISCRIMINATOR = nameof(Base.speckle_type); - private DeserializationWorkerThreads? _workerThreads; public CancellationToken CancellationToken { get; set; } @@ -45,6 +37,20 @@ public sealed class BaseObjectDeserializerV2 public static int DefaultNumberThreads => Math.Min(Environment.ProcessorCount, 6); //6 threads seems the sweet spot, see performance test project public int WorkerThreadCount { get; set; } = DefaultNumberThreads; + private readonly ITypeCache _typeCache; + private readonly Version _payloadSchemaVersion; + private readonly ISchemaObjectUpgradeManager _objectUpgradeManager; + + // POC: inject the TypeCacheManager, and interface out + public BaseObjectDeserializerV2(ITypeCache typeCache, ISchemaObjectUpgradeManager objectUpgradeManager, Version payloadSchemaVersion) + { + _typeCache = typeCache; + _objectUpgradeManager = objectUpgradeManager; + _payloadSchemaVersion = payloadSchemaVersion; + + _typeCache.EnsureCacheIsBuilt(); + } + /// The JSON string of the object to be deserialized /// A typed object deserialized from the /// Thrown when @@ -125,12 +131,12 @@ private List<(string, int)> GetClosures(string rootObjectJson) List<(string, int)> closureList = new(); JObject doc1 = JObject.Parse(rootObjectJson); - if (!doc1.ContainsKey("__closure")) + if (!doc1.ContainsKey(SerializationConstants.CLOSURE_PROPERTY_NAME)) { return new List<(string, int)>(); } - foreach (JToken prop in doc1["__closure"].NotNull()) + foreach (JToken prop in doc1[SerializationConstants.CLOSURE_PROPERTY_NAME].NotNull()) { string childId = ((JProperty)prop).Name; int childMinDepth = (int)((JProperty)prop).Value; @@ -269,7 +275,7 @@ private List<(string, int)> GetClosures(string rootObjectJson) foreach (JToken propJToken in jObject) { JProperty prop = (JProperty)propJToken; - if (prop.Name == "__closure") + if (prop.Name == SerializationConstants.CLOSURE_PROPERTY_NAME) { continue; } @@ -277,7 +283,7 @@ private List<(string, int)> GetClosures(string rootObjectJson) dict[prop.Name] = ConvertJsonElement(prop.Value); } - if (!dict.TryGetValue(TYPE_DISCRIMINATOR, out object? speckleType)) + if (!dict.TryGetValue(SerializationConstants.TYPE_DISCRIMINATOR, out object? speckleType)) { return dict; } @@ -340,22 +346,26 @@ private List<(string, int)> GetClosures(string rootObjectJson) private Base Dict2Base(Dictionary dictObj) { - string typeName = (string)dictObj[TYPE_DISCRIMINATOR].NotNull(); - Type type = BaseObjectSerializationUtilities.GetType(typeName); - Base baseObj = (Base)Activator.CreateInstance(type); - - dictObj.Remove(TYPE_DISCRIMINATOR); - dictObj.Remove("__closure"); - - Dictionary staticProperties = BaseObjectSerializationUtilities.GetTypeProperties(typeName); - List onDeserializedCallbacks = BaseObjectSerializationUtilities.GetOnDeserializedCallbacks(typeName); - + string typeName = (string) dictObj[SerializationConstants.TYPE_DISCRIMINATOR]!; + + // here we're getting the actual type to deserialise into, this won't be the type we return + // POC: is there any guarantee this can't happen? probably not ATM + // it's almost certainly a bug if the type returned is a version and not the latest! + (Version objectVersion, CachedTypeInfo cachedTypeInfo) = _typeCache.GetMatchedTypeOrLater(typeName, _payloadSchemaVersion); + + Base baseObj = (Base) Activator.CreateInstance(cachedTypeInfo.Type); + var props = cachedTypeInfo.Props; + var onDeserializedCallbacks = cachedTypeInfo.Callbacks; + + dictObj.Remove(SerializationConstants.TYPE_DISCRIMINATOR); + dictObj.Remove(SerializationConstants.CLOSURE_PROPERTY_NAME); + foreach (var entry in dictObj) { string lowerPropertyName = entry.Key.ToLower(); - if (staticProperties.TryGetValue(lowerPropertyName, out PropertyInfo? value) && value.CanWrite) + if (props.TryGetValue(lowerPropertyName, out PropertyInfo? value) && value.CanWrite) { - PropertyInfo property = staticProperties[lowerPropertyName]; + PropertyInfo property = props[lowerPropertyName]; if (entry.Value == null) { // Check for JsonProperty(NullValueHandling = NullValueHandling.Ignore) attribute @@ -391,18 +401,23 @@ private Base Dict2Base(Dictionary dictObj) { bb.filePath = bb.GetLocalDestinationPath(BlobStorageFolder); } - + + // version the object + // POC: we need to cache the right name here, because reflecting to get the mame is meh + baseObj = _objectUpgradeManager.UpgradeObject( + baseObj, + cachedTypeInfo.Type.FullName.NotNull(), + _payloadSchemaVersion, + _typeCache.LoadedSchemaVersion); + + // POC: what are these callback methods? are they used? + // Do they need calling on the object BEFORE upgrading it? Do they need calling AGAIN after upgrading it? foreach (MethodInfo onDeserialized in onDeserializedCallbacks) { onDeserialized.Invoke(baseObj, new object?[] { null }); } + // POC: we need to be returning the latest version return baseObj; } - - [Obsolete("Use nameof(Base.speckle_type)")] - public string TypeDiscriminator => TYPE_DISCRIMINATOR; - - [Obsolete("OnErrorAction unused, deserializer will throw exceptions instead")] - public Action? OnErrorAction { get; set; } } diff --git a/src/Speckle.Core/Serialisation/BaseObjectSerializer.cs b/src/Speckle.Core/Serialisation/BaseObjectSerializer.cs deleted file mode 100644 index 418df0b..0000000 --- a/src/Speckle.Core/Serialisation/BaseObjectSerializer.cs +++ /dev/null @@ -1,719 +0,0 @@ -#nullable disable -using System; -using System.Collections; -using System.Collections.Generic; -using System.Runtime.Serialization; -using System.Threading; -using Speckle.Core.Helpers; -using Speckle.Core.Logging; -using Speckle.Core.Models; -using Speckle.Core.Serialisation.SerializationUtilities; -using Speckle.Core.Transports; -using Speckle.Newtonsoft.Json; -using Speckle.Newtonsoft.Json.Linq; -using Speckle.Newtonsoft.Json.Serialization; -using Utilities = Speckle.Core.Models.Utilities; - -// ReSharper disable InconsistentNaming -// ReSharper disable UseNegatedPatternInIsExpression -#pragma warning disable IDE0075, IDE1006, IDE0083, CA1051, CA1502, CA1854 - -namespace Speckle.Core.Serialisation; - -/// -/// Json converter that handles base speckle objects. Enables detachment and -/// simultaneous transport (persistence) of objects. -/// -[Obsolete("Use " + nameof(BaseObjectSerializerV2))] -public class BaseObjectSerializer : JsonConverter -{ - /// - /// Property that describes the type of the object. - /// - public string TypeDiscriminator = "speckle_type"; - - public BaseObjectSerializer() - { - ResetAndInitialize(); - } - - public CancellationToken CancellationToken { get; set; } - - /// - /// The sync transport. This transport will be used synchronously. - /// - public ITransport ReadTransport { get; set; } - - /// - /// List of transports to write to. - /// - public List WriteTransports { get; set; } = new(); - - public override bool CanWrite => true; - - public override bool CanRead => true; - - public Action OnProgressAction { get; set; } - - public Action OnErrorAction { get; set; } - - /// - /// Reinitializes the lineage, and other variables that get used during the - /// json writing process. - /// - public void ResetAndInitialize() - { - DetachLineage = new List(); - Lineage = new List(); - RefMinDepthTracker = new Dictionary>(); - OnProgressAction = null; - TotalProcessedCount = 0; - } - - public override bool CanConvert(Type objectType) - { - return true; - } - - #region Read Json - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (CancellationToken.IsCancellationRequested) - { - return null; // Check for cancellation - } - - if (reader.TokenType == JsonToken.Null) - { - return null; - } - - // Check if we passed in an array, rather than an object. - // TODO: Test the following branch. It's not used anywhere at the moment, and the default serializer prevents it from - // ever being used (only allows single object serialization) - if (reader.TokenType == JsonToken.StartArray) - { - var list = new List(); - var jarr = JArray.Load(reader); - - foreach (var val in jarr) - { - if (CancellationToken.IsCancellationRequested) - { - return null; // Check for cancellation - } - - var whatever = BaseObjectSerializationUtilities.HandleValue(val, serializer, CancellationToken); - list.Add(whatever as Base); - } - return list; - } - - if (CancellationToken.IsCancellationRequested) - { - return null; // Check for cancellation - } - - var jObject = JObject.Load(reader); - - if (jObject == null) - { - return null; - } - - var objType = jObject.GetValue(TypeDiscriminator); - - // Assume dictionary! - if (objType == null) - { - var dict = new Dictionary(); - - foreach (var val in jObject) - { - if (CancellationToken.IsCancellationRequested) - { - return null; // Check for cancellation - } - - dict[val.Key] = BaseObjectSerializationUtilities.HandleValue(val.Value, serializer, CancellationToken); - } - return dict; - } - - if (CancellationToken.IsCancellationRequested) - { - return null; // Check for cancellation - } - - var discriminator = objType.Value(); - - // Check for references. - if (discriminator == "reference") - { - var id = jObject.GetValue("referencedId").Value(); - - string str = - ReadTransport != null - ? ReadTransport.GetObject(id) - : throw new SpeckleException("Cannot resolve reference, no transport is defined."); - - if (str != null && !string.IsNullOrEmpty(str)) - { - jObject = JObject.Parse(str); - discriminator = jObject.GetValue(TypeDiscriminator).Value(); - } - else - { - throw new SpeckleException("Cannot resolve reference. The provided transport could not find it."); - } - } - - var type = BaseObjectSerializationUtilities.GetType(discriminator); - var obj = existingValue ?? Activator.CreateInstance(type); - - var contract = (JsonDynamicContract)serializer.ContractResolver.ResolveContract(type); - var used = new HashSet(); - - // remove unsettable properties - jObject.Remove(TypeDiscriminator); - jObject.Remove("__closure"); - - if (CancellationToken.IsCancellationRequested) - { - return null; // Check for cancellation - } - - foreach (var jProperty in jObject.Properties()) - { - if (CancellationToken.IsCancellationRequested) - { - return null; // Check for cancellation - } - - if (used.Contains(jProperty.Name)) - { - continue; - } - - used.Add(jProperty.Name); - - // first attempt to find a settable property, otherwise fall back to a dynamic set without type - JsonProperty property = contract.Properties.GetClosestMatchProperty(jProperty.Name); - - if (property != null && property.Writable) - { - if (type == typeof(Abstract) && property.PropertyName == "base") - { - var propertyValue = BaseObjectSerializationUtilities.HandleAbstractOriginalValue( - jProperty.Value, - ((JValue)jObject.GetValue("assemblyQualifiedName")).Value as string - ); - property.ValueProvider.SetValue(obj, propertyValue); - } - else - { - var val = BaseObjectSerializationUtilities.HandleValue( - jProperty.Value, - serializer, - CancellationToken, - property - ); - property.ValueProvider.SetValue(obj, val); - } - } - else - { - // dynamic properties - CallSiteCache.SetValue( - jProperty.Name, - obj, - BaseObjectSerializationUtilities.HandleValue(jProperty.Value, serializer, CancellationToken) - ); - } - } - - if (CancellationToken.IsCancellationRequested) - { - return null; // Check for cancellation - } - - TotalProcessedCount++; - OnProgressAction?.Invoke("DS", 1); - - foreach (var callback in contract.OnDeserializedCallbacks) - { - callback(obj, serializer.Context); - } - - return obj; - } - - #endregion - - #region Write Json Helper Properties - - /// - /// Keeps track of wether current property pointer is marked for detachment. - /// - private List DetachLineage { get; set; } - - /// - /// Keeps track of the hash chain through the object tree. - /// - private List Lineage { get; set; } - - /// - /// Dictionary of object if and its subsequent closure table (a dictionary of hashes and min depth at which they are found). - /// - private Dictionary> RefMinDepthTracker { get; set; } - - public int TotalProcessedCount; - - #endregion - - #region Write Json - - // Keeps track of the actual tree structure of the objects being serialised. - // These tree references will thereafter be stored in the __tree prop. - private void TrackReferenceInTree(string refId) - { - // Help with creating closure table entries. - for (int i = 0; i < Lineage.Count; i++) - { - var parent = Lineage[i]; - - if (!RefMinDepthTracker.ContainsKey(parent)) - { - RefMinDepthTracker[parent] = new Dictionary(); - } - - if (!RefMinDepthTracker[parent].ContainsKey(refId)) - { - RefMinDepthTracker[parent][refId] = Lineage.Count - i; - } - else if (RefMinDepthTracker[parent][refId] > Lineage.Count - i) - { - RefMinDepthTracker[parent][refId] = Lineage.Count - i; - } - } - } - - private bool FirstEntry = true, - FirstEntryWasListOrDict; - - // While this function looks complicated, it's actually quite smooth: - // The important things to remember is that serialization goes depth first: - // The first object to get fully serialised is the first nested one, with - // the parent object being last. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.Formatting = serializer.Formatting; - if (CancellationToken.IsCancellationRequested) - { - return; // Check for cancellation - } - - ///////////////////////////////////// - // Path one: nulls - ///////////////////////////////////// - - if (value == null) - { - return; - } - - ///////////////////////////////////// - // Path two: primitives (string, bool, int, etc) - ///////////////////////////////////// - - if (value.GetType().IsPrimitive || value is string) - { - FirstEntry = false; - writer.WriteValue(value); - //var t = JToken.FromObject(value); // bypasses this converter as we do not pass in the serializer - //t.WriteTo(writer); - return; - } - - ///////////////////////////////////// - // Path three: Bases - ///////////////////////////////////// - - if (value is Base && !(value is ObjectReference)) - { - if (CancellationToken.IsCancellationRequested) - { - return; // Check for cancellation - } - - var obj = value as Base; - - FirstEntry = false; - //TotalProcessedCount++; - - // Append to lineage tracker - Lineage.Add(Guid.NewGuid().ToString()); - - var jo = new JObject(); - var propertyNames = obj.GetDynamicMemberNames(); - - var contract = (JsonDynamicContract)serializer.ContractResolver.ResolveContract(value.GetType()); - - // Iterate through the object's properties, one by one, checking for ignored ones - foreach (var prop in propertyNames) - { - if (CancellationToken.IsCancellationRequested) - { - return; // Check for cancellation - } - // Ignore properties starting with a double underscore. - if (prop.StartsWith("__")) - { - continue; - } - - if (prop == "id") - { - continue; - } - - var property = contract.Properties.GetClosestMatchProperty(prop); - - // Ignore properties decorated with [JsonIgnore]. - if (property != null && property.Ignored) - { - continue; - } - - // Ignore nulls - object propValue = obj[prop]; - if (propValue == null) - { - continue; - } - - // Check if this property is marked for detachment: either by the presence of "@" at the beginning of the name, or by the presence of a DetachProperty attribute on a typed property. - if (property != null) - { - var detachableAttributes = property.AttributeProvider.GetAttributes(typeof(DetachProperty), true); - if (detachableAttributes.Count > 0) - { - DetachLineage.Add(((DetachProperty)detachableAttributes[0]).Detachable); - } - else - { - DetachLineage.Add(false); - } - - var chunkableAttributes = property.AttributeProvider.GetAttributes(typeof(Chunkable), true); - if (chunkableAttributes.Count > 0) - { - //DetachLineage.Add(true); // NOOPE - serializer.Context = new StreamingContext(StreamingContextStates.Other, chunkableAttributes[0]); - } - else - { - //DetachLineage.Add(false); - serializer.Context = new StreamingContext(); - } - } - else if (prop.StartsWith("@")) // Convention check for dynamically added properties. - { - DetachLineage.Add(true); - - var chunkSyntax = Constants.ChunkPropertyNameRegex; - - if (chunkSyntax.IsMatch(prop)) - { - var match = chunkSyntax.Match(prop); - _ = int.TryParse(match.Groups[match.Groups.Count - 1].Value, out int chunkSize); - serializer.Context = new StreamingContext( - StreamingContextStates.Other, - chunkSize > 0 ? new Chunkable(chunkSize) : new Chunkable() - ); - } - else - { - serializer.Context = new StreamingContext(); - } - } - else - { - DetachLineage.Add(false); - } - - // Set and store a reference, if it is marked as detachable and the transport is not null. - if ( - WriteTransports != null - && WriteTransports.Count != 0 - && propValue is Base - && DetachLineage[DetachLineage.Count - 1] - ) - { - var what = JToken.FromObject(propValue, serializer); // Trigger next. - - if (CancellationToken.IsCancellationRequested) - { - return; // Check for cancellation - } - - if (what == null) - { - return; // HACK: Prevent nulls from borking our serialization on nested schema object refs. (i.e. Line has @SchemaObject, that has ref to line) - } - - var refHash = ((JObject)what).GetValue("id").ToString(); - - var reference = new ObjectReference { referencedId = refHash }; - TrackReferenceInTree(refHash); - jo.Add(prop, JToken.FromObject(reference)); - } - else - { - jo.Add(prop, JToken.FromObject(propValue, serializer)); // Default route - } - - // Pop detach lineage. If you don't get this, remember this thing moves ONLY FORWARD, DEPTH FIRST - DetachLineage.RemoveAt(DetachLineage.Count - 1); - // Refresh the streaming context to remove chunking flag - serializer.Context = new StreamingContext(); - } - - // Check if we actually have any transports present that would warrant a - if ( - WriteTransports != null - && WriteTransports.Count != 0 - && RefMinDepthTracker.ContainsKey(Lineage[Lineage.Count - 1]) - ) - { - jo.Add("__closure", JToken.FromObject(RefMinDepthTracker[Lineage[Lineage.Count - 1]])); - } - - var hash = Utilities.HashString(jo.ToString()); - if (!jo.ContainsKey("id")) - { - jo.Add("id", JToken.FromObject(hash)); - } - - jo.WriteTo(writer); - - if ( - (DetachLineage.Count == 0 || DetachLineage[DetachLineage.Count - 1]) - && WriteTransports != null - && WriteTransports.Count != 0 - ) - { - var objString = jo.ToString(writer.Formatting); - var objId = jo["id"].Value(); - - OnProgressAction?.Invoke("S", 1); - - foreach (var transport in WriteTransports) - { - if (CancellationToken.IsCancellationRequested) - { - continue; // Check for cancellation - } - - transport.SaveObject(objId, objString); - } - } - - // Pop lineage tracker - Lineage.RemoveAt(Lineage.Count - 1); - return; - } - - ///////////////////////////////////// - // Path four: lists/arrays & dicts - ///////////////////////////////////// - - if (CancellationToken.IsCancellationRequested) - { - return; // Check for cancellation - } - - var type = value.GetType(); - - // TODO: List handling and dictionary serialisation handling can be sped up significantly if we first check by their inner type. - // This handles a broader case in which we are, essentially, checking only for object[] or List / Dictionary cases. - // A much faster approach is to check for List, where primitive = string, number, etc. and directly serialize it in full. - // Same goes for dictionaries. - if ( - typeof(IEnumerable).IsAssignableFrom(type) - && !typeof(IDictionary).IsAssignableFrom(type) - && type != typeof(string) - ) - { - if (TotalProcessedCount == 0 && FirstEntry) - { - FirstEntry = false; - FirstEntryWasListOrDict = true; - TotalProcessedCount += 1; - DetachLineage.Add(WriteTransports != null && WriteTransports.Count != 0 ? true : false); - } - - JArray arr = new(); - - // Chunking large lists into manageable parts. - if (DetachLineage[DetachLineage.Count - 1] && serializer.Context.Context is Chunkable chunkInfo) - { - var maxCount = chunkInfo.MaxObjCountPerChunk; - var i = 0; - var chunkList = new List(); - var currChunk = new DataChunk(); - - foreach (var arrValue in (IEnumerable)value) - { - if (i == maxCount) - { - if (currChunk.data.Count != 0) - { - chunkList.Add(currChunk); - } - - currChunk = new DataChunk(); - i = 0; - } - currChunk.data.Add(arrValue); - i++; - } - - if (currChunk.data.Count != 0) - { - chunkList.Add(currChunk); - } - - value = chunkList; - } - - foreach (var arrValue in (IEnumerable)value) - { - if (CancellationToken.IsCancellationRequested) - { - return; // Check for cancellation - } - - if (arrValue == null) - { - continue; - } - - if ( - WriteTransports != null - && WriteTransports.Count != 0 - && arrValue is Base - && DetachLineage[DetachLineage.Count - 1] - ) - { - var what = JToken.FromObject(arrValue, serializer); // Trigger next - - var refHash = ((JObject)what).GetValue("id").ToString(); - - var reference = new ObjectReference { referencedId = refHash }; - TrackReferenceInTree(refHash); - arr.Add(JToken.FromObject(reference)); - } - else - { - arr.Add(JToken.FromObject(arrValue, serializer)); // Default route - } - } - - if (CancellationToken.IsCancellationRequested) - { - return; // Check for cancellation - } - - arr.WriteTo(writer); - - if (DetachLineage.Count == 1 && FirstEntryWasListOrDict) // are we in a list entry point case? - { - DetachLineage.RemoveAt(0); - } - - return; - } - - if (CancellationToken.IsCancellationRequested) - { - return; // Check for cancellation - } - - if (typeof(IDictionary).IsAssignableFrom(type)) - { - if (TotalProcessedCount == 0 && FirstEntry) - { - FirstEntry = false; - FirstEntryWasListOrDict = true; - TotalProcessedCount += 1; - DetachLineage.Add(WriteTransports != null && WriteTransports.Count != 0 ? true : false); - } - var dict = value as IDictionary; - var dictJo = new JObject(); - foreach (DictionaryEntry kvp in dict) - { - if (CancellationToken.IsCancellationRequested) - { - return; // Check for cancellation - } - - if (kvp.Value == null) - { - continue; - } - - JToken jToken; - if ( - WriteTransports != null - && WriteTransports.Count != 0 - && kvp.Value is Base - && DetachLineage[DetachLineage.Count - 1] - ) - { - var what = JToken.FromObject(kvp.Value, serializer); // Trigger next - var refHash = ((JObject)what).GetValue("id").ToString(); - - var reference = new ObjectReference { referencedId = refHash }; - TrackReferenceInTree(refHash); - jToken = JToken.FromObject(reference); - } - else - { - jToken = JToken.FromObject(kvp.Value, serializer); // Default route - } - dictJo.Add(kvp.Key.ToString(), jToken); - } - dictJo.WriteTo(writer); - - if (CancellationToken.IsCancellationRequested) - { - return; // Check for cancellation - } - - if (DetachLineage.Count == 1 && FirstEntryWasListOrDict) // are we in a dictionary entry point case? - { - DetachLineage.RemoveAt(0); - } - - return; - } - - ///////////////////////////////////// - // Path five: everything else (enums?) - ///////////////////////////////////// - - if (CancellationToken.IsCancellationRequested) - { - return; // Check for cancellation - } - - FirstEntry = false; - var lastCall = JToken.FromObject(value); // bypasses this converter as we do not pass in the serializer - lastCall.WriteTo(writer); - } - - #endregion -} -#pragma warning restore IDE0075, IDE1006, IDE0083, CA1051, CA1502, CA1854 diff --git a/src/Speckle.Core/Serialisation/BaseObjectSerializerV2.cs b/src/Speckle.Core/Serialisation/BaseObjectSerializerV2.cs index fb4efe4..f234acd 100644 --- a/src/Speckle.Core/Serialisation/BaseObjectSerializerV2.cs +++ b/src/Speckle.Core/Serialisation/BaseObjectSerializerV2.cs @@ -145,7 +145,7 @@ public string Serialize(Base baseObj) { ["speckle_type"] = r.speckle_type, ["referencedId"] = r.referencedId, - ["__closure"] = r.closure + [SerializationConstants.CLOSURE_PROPERTY_NAME] = r.closure }; if (r.closure is not null) { @@ -320,7 +320,7 @@ public string Serialize(Base baseObj) if (closure.Count > 0) { - convertedBase["__closure"] = closure; + convertedBase[SerializationConstants.CLOSURE_PROPERTY_NAME] = closure; } if (computeClosures || inheritedDetachInfo.IsDetachable || baseObj is Blob) diff --git a/src/Speckle.Core/Serialisation/ISpeckleDeserializer.cs b/src/Speckle.Core/Serialisation/ISpeckleDeserializer.cs new file mode 100644 index 0000000..15ff888 --- /dev/null +++ b/src/Speckle.Core/Serialisation/ISpeckleDeserializer.cs @@ -0,0 +1,39 @@ +using Speckle.Core.Models; +using Speckle.Core.Transports; +using Speckle.Newtonsoft.Json; +using Speckle.Newtonsoft.Json.Linq; + +namespace Speckle.Core.Serialisation; + +public interface ISpeckleDeserializer + where RootType : class +{ + CancellationToken CancellationToken { get; set; } + + /// + /// The sync transport. This transport will be used synchronously. + /// + ITransport ReadTransport { get; set; } + + Action? OnProgressAction { get; set; } + string? BlobStorageFolder { get; set; } + TimeSpan Elapsed { get; } + int WorkerThreadCount { get; set; } + + /// The JSON string of the object to be deserialized + /// A typed object deserialized from the + /// Thrown when + /// was null + /// cannot be deserialised to type + // /// did not contain the required json objects (closures) + RootType Deserialize(string rootObjectJson); + + /// + /// The deserialized object + /// was null + /// was not valid JSON + /// Failed to deserialize to the target type + object? DeserializeTransportObject(string objectJson); + + object? ConvertJsonElement(JToken doc); +} diff --git a/src/Speckle.Core/Serialisation/SerializationConstants.cs b/src/Speckle.Core/Serialisation/SerializationConstants.cs new file mode 100644 index 0000000..c8a134d --- /dev/null +++ b/src/Speckle.Core/Serialisation/SerializationConstants.cs @@ -0,0 +1,9 @@ +using Speckle.Core.Models; + +namespace Speckle.Core.Serialisation; + +public class SerializationConstants +{ + public const string TYPE_DISCRIMINATOR = nameof(Base.speckle_type); + public const string CLOSURE_PROPERTY_NAME = "__closure"; +} diff --git a/src/Speckle.Core/Serialisation/SerializationUtilities/BaseObjectSerializationUtilities.cs b/src/Speckle.Core/Serialisation/SerializationUtilities/BaseObjectSerializationUtilities.cs deleted file mode 100644 index e8f00ca..0000000 --- a/src/Speckle.Core/Serialisation/SerializationUtilities/BaseObjectSerializationUtilities.cs +++ /dev/null @@ -1,370 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.Serialization; -using System.Threading; -using Speckle.Core.Kits; -using Speckle.Core.Logging; -using Speckle.Core.Models; -using Speckle.Newtonsoft.Json; -using Speckle.Newtonsoft.Json.Linq; -using Speckle.Newtonsoft.Json.Serialization; - -namespace Speckle.Core.Serialisation.SerializationUtilities; - -internal static class BaseObjectSerializationUtilities -{ - #region Getting Types - - private static Dictionary s_cachedTypes = new(); - - private static readonly Dictionary> s_typeProperties = new(); - - private static readonly Dictionary> s_onDeserializedCallbacks = new(); - - internal static Type GetType(string objFullType) - { - lock (s_cachedTypes) - { - if (s_cachedTypes.TryGetValue(objFullType, out Type? type1)) - { - return type1; - } - - var type = GetAtomicType(objFullType); - s_cachedTypes[objFullType] = type; - return type; - } - } - - internal static Type GetAtomicType(string objFullType) - { - var objectTypes = objFullType.Split(':').Reverse(); - foreach (var typeName in objectTypes) - { - //TODO: rather than getting the type from the first loaded kit that has it, maybe - //we get it from a specific Kit - var type = KitManager.Types.FirstOrDefault(tp => tp.FullName == typeName); - if (type != null) - { - return type; - } - - //To allow for backwards compatibility saving deserialization target types. - //We also check a ".Deprecated" prefixed namespace - string deprecatedTypeName = GetDeprecatedTypeName(typeName); - - var deprecatedType = KitManager.Types.FirstOrDefault(tp => tp.FullName == deprecatedTypeName); - if (deprecatedType != null) - { - return deprecatedType; - } - } - - return typeof(Base); - } - - internal static string GetDeprecatedTypeName(string typeName, string deprecatedSubstring = "Deprecated.") - { - int lastDotIndex = typeName.LastIndexOf('.'); - return typeName.Insert(lastDotIndex + 1, deprecatedSubstring); - } - - internal static Dictionary GetTypeProperties(string objFullType) - { - lock (s_typeProperties) - { - if (s_typeProperties.TryGetValue(objFullType, out Dictionary? value)) - { - return value; - } - - Dictionary ret = new(); - Type type = GetType(objFullType); - PropertyInfo[] properties = type.GetProperties(); - foreach (PropertyInfo prop in properties) - { - ret[prop.Name.ToLower()] = prop; - } - - value = ret; - s_typeProperties[objFullType] = value; - return value; - } - } - - internal static List GetOnDeserializedCallbacks(string objFullType) - { - // return new List(); - lock (s_onDeserializedCallbacks) - { - // System.Runtime.Serialization.Ca - if (s_onDeserializedCallbacks.TryGetValue(objFullType, out List? value)) - { - return value; - } - - List ret = new(); - Type type = GetType(objFullType); - MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - foreach (MethodInfo method in methods) - { - List onDeserializedAttributes = method - .GetCustomAttributes(true) - .ToList(); - if (onDeserializedAttributes.Count > 0) - { - ret.Add(method); - } - } - - value = ret; - s_onDeserializedCallbacks[objFullType] = value; - return value; - } - } - - internal static Type GetSystemOrSpeckleType(string typeName) - { - var systemType = Type.GetType(typeName); - if (systemType != null) - { - return systemType; - } - - return GetAtomicType(typeName); - } - - /// - /// Flushes kit's (discriminator, type) cache. Useful if you're dynamically loading more kits at runtime, that provide better coverage of what you're deserialising, and it's now somehow poisoned because the higher level types were not originally available. - /// - public static void FlushCachedTypes() - { - lock (s_cachedTypes) - { - s_cachedTypes = new Dictionary(); - } - } - - #endregion - - #region Obsolete -#pragma warning disable CS8602, CA1502 - - private static readonly Dictionary s_cachedAbstractTypes = new(); - - [Obsolete("Only Used by Serializer V1")] - internal static object? HandleAbstractOriginalValue(JToken jToken, string assemblyQualifiedName) - { - if (s_cachedAbstractTypes.TryGetValue(assemblyQualifiedName, out Type? type)) - { - return jToken.ToObject(type); - } - - var pieces = assemblyQualifiedName.Split(',').Select(s => s.Trim()).ToArray(); - - var myAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(ass => ass.GetName().Name == pieces[1]); - if (myAssembly == null) - { - throw new SpeckleException("Could not load abstract object's assembly."); - } - - var myType = myAssembly.GetType(pieces[0]); - if (myType == null) - { - throw new SpeckleException("Could not load abstract object's assembly."); - } - - s_cachedAbstractTypes[assemblyQualifiedName] = myType; - - return jToken.ToObject(myType); - } - - [Obsolete("Only used by serializer v1")] - internal static object? HandleValue( - JToken? value, - JsonSerializer serializer, - CancellationToken cancellationToken, - JsonProperty? jsonProperty = null, - string typeDiscriminator = "speckle_type" - ) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (jsonProperty is { PropertyType: null }) - { - throw new ArgumentException($"Expected {nameof(JsonProperty.PropertyType)} to be non-null", nameof(jsonProperty)); - } - - switch (value) - { - case JValue jValue when jsonProperty != null: - return jValue.ToObject(jsonProperty.PropertyType); - case JValue jValue: - return jValue.Value; - // Lists - case JArray array when jsonProperty != null && jsonProperty.PropertyType.GetConstructor(Type.EmptyTypes) != null: - { - var arr = Activator.CreateInstance(jsonProperty.PropertyType); - - var addMethod = arr.GetType().GetMethod(nameof(IList.Add))!; - var hasGenericType = jsonProperty.PropertyType.GenericTypeArguments.Length != 0; - - foreach (var val in array) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (val == null) - { - continue; - } - - var item = HandleValue(val, serializer, cancellationToken); - - if (item is DataChunk chunk) - { - foreach (var dataItem in chunk.data) - { - if (hasGenericType && !jsonProperty.PropertyType.GenericTypeArguments[0].IsInterface) - { - if (jsonProperty.PropertyType.GenericTypeArguments[0].IsAssignableFrom(dataItem.GetType())) - { - addMethod.Invoke(arr, new[] { dataItem }); - } - else - { - addMethod.Invoke( - arr, - new[] { Convert.ChangeType(dataItem, jsonProperty.PropertyType.GenericTypeArguments[0]) } - ); - } - } - else - { - addMethod.Invoke(arr, new[] { dataItem }); - } - } - } - else if (hasGenericType && !jsonProperty.PropertyType.GenericTypeArguments[0].IsInterface) - { - if (jsonProperty.PropertyType.GenericTypeArguments[0].IsAssignableFrom(item.GetType())) - { - addMethod.Invoke(arr, new[] { item }); - } - else - { - addMethod.Invoke( - arr, - new[] { Convert.ChangeType(item, jsonProperty.PropertyType.GenericTypeArguments[0]) } - ); - } - } - else - { - addMethod.Invoke(arr, new[] { item }); - } - } - return arr; - } - case JArray array when jsonProperty != null: - { - var arr = (IList) - Activator.CreateInstance(typeof(List<>).MakeGenericType(jsonProperty.PropertyType.GetElementType())); - - foreach (var val in array) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (val == null) - { - continue; - } - - var item = HandleValue(val, serializer, cancellationToken); - if (item is DataChunk chunk) - { - foreach (var dataItem in chunk.data) - { - if (!jsonProperty.PropertyType.GetElementType()!.IsInterface) - { - arr.Add(Convert.ChangeType(dataItem, jsonProperty.PropertyType.GetElementType()!)); - } - else - { - arr.Add(dataItem); - } - } - } - else - { - if (!jsonProperty.PropertyType.GetElementType()!.IsInterface) - { - arr.Add(Convert.ChangeType(item, jsonProperty.PropertyType.GetElementType()!)); - } - else - { - arr.Add(item); - } - } - } - var actualArr = Array.CreateInstance(jsonProperty.PropertyType.GetElementType()!, arr.Count); - arr.CopyTo(actualArr, 0); - return actualArr; - } - case JArray array: - { - var arr = new List(); - foreach (var val in array) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (val == null) - { - continue; - } - - var item = HandleValue(val, serializer, cancellationToken); - - if (item is DataChunk chunk) - { - arr.AddRange(chunk.data); - } - else - { - arr.Add(item); - } - } - return arr; - } - case JObject jObject when jObject.Property(typeDiscriminator) != null: - return jObject.ToObject(serializer); - case JObject jObject: - { - var dict = - jsonProperty != null - ? Activator.CreateInstance(jsonProperty.PropertyType) as IDictionary - : new Dictionary(); - foreach (var prop in jObject) - { - cancellationToken.ThrowIfCancellationRequested(); - - object key = prop.Key; - if (jsonProperty != null) - { - key = Convert.ChangeType(prop.Key, jsonProperty.PropertyType.GetGenericArguments()[0]); - } - - dict[key] = HandleValue(prop.Value, serializer, cancellationToken); - } - return dict; - } - default: - return null; - } - } -#pragma warning restore CS8602, CA1502 // Dereference of a possibly null reference. - - #endregion -} diff --git a/src/Speckle.Core/Serialisation/TypeCache/AbstractTypeCache.cs b/src/Speckle.Core/Serialisation/TypeCache/AbstractTypeCache.cs new file mode 100644 index 0000000..af39332 --- /dev/null +++ b/src/Speckle.Core/Serialisation/TypeCache/AbstractTypeCache.cs @@ -0,0 +1,272 @@ +using System.Reflection; +using System.Runtime.Serialization; +using Speckle.Core.Common; +using Speckle.Core.Reflection; + +namespace Speckle.Core.Serialisation.TypeCache; + +// POC: could move +internal class VersionCache +{ + public string Type { get; private set; } + public List<(Version, CachedTypeInfo)> Versions { get; private set; } = new(); + public CachedTypeInfo? LatestVersion; + + public VersionCache(string type) + { + Type = type; + } + + public void SortVersions() + { + // for some reason I can't get the tuple deconstructed (but it IS rather late) + Versions = Versions.OrderBy(v => v.Item1).ToList(); + } +} + +public abstract class AbstractTypeCache : ITypeCache +{ + private readonly IList _assemblies; + private readonly Type _baseType; + private readonly Dictionary _cachedTypes = new(); + + private bool _cacheBuilt = false; + + public Version LoadedSchemaVersion { get; private set; } + + private readonly ITypeFinder _typeFinder; + private readonly string _namespacePrefix; + private readonly string _versionNamespacePrefix; + + public CachedTypeInfo? FallbackType { get; private set; } + + protected AbstractTypeCache( + IEnumerable assemblies, + Type baseType, + Version loadedSchemaVersion, + string namespacePrefix, + ITypeFinder typeFinder) + { + _assemblies = assemblies.ToList(); + _baseType = baseType; + LoadedSchemaVersion = loadedSchemaVersion; + + // FUTURE: we may need to associate the prefix with each assembly as the prefix may change + // better yet detach the namespaces & assemblies from the type naming + _namespacePrefix = namespacePrefix; + _versionNamespacePrefix = $"{_namespacePrefix}.Versions."; + _typeFinder = typeFinder; + + // POC: manually including core, not sure of the wisdom of this... + _assemblies.Add(typeof(AbstractTypeCache).Assembly); + } + + // could build in constructor.. but throwing from constructors generally frowned upon + public void EnsureCacheIsBuilt() + { + if (_cacheBuilt) + { + return; + } + + _cacheBuilt = true; + + foreach (Type type in _typeFinder.GetTypesWhereSubclassOf(_assemblies, _baseType)) + { + try + { + (string typeName, Version? version) = GetTypeNameAndVersion(type.FullName.NotNull()); + + var typeCacheInfo = new CachedTypeInfo( + typeName.NotNull(), + type, + GetPropertyInfo(type), + GetCallbacks(type)); + + CacheType(typeName, version, typeCacheInfo); + } + // POC: right one? more? + catch (ReflectionTypeLoadException) + { + // POC: guard against loading things that cause explosions + } + } + + // future incarnations may permit mutating the type + // so the return type is something other than base, they may also allow moving namespaces + // this is not currently possible because of the way the namespace has been used historically and how we are using it here + // we can probably tweak this to allow for namespace remapping - it would be nice if things began with Speckle :) + // object version may also be able to retype things... + foreach (var typeVersions in _cachedTypes.Values) + { + if (typeVersions.LatestVersion == null) + { + // we cannot have non-matching types atm + // .i.e. a versioned Objects.Versions.V_1_2.0.Wall must have a corresponding Objects.Wall + // I imagine it would be possible to add some annotation somewhere to allow for this but now is not the time... + var versionNames = string.Join(",", typeVersions.Versions + .Select((v, typeCache) => v.Item1.ToString())).ToList(); + throw new ArgumentException( + $"The type {typeVersions.Type} has no latest - we have the following versions of this type: '{versionNames}'"); + } + + // sort the versions + typeVersions.SortVersions(); + } + + FallbackType = new CachedTypeInfo( + _baseType.FullName.NotNull(), + _baseType, + GetPropertyInfo(_baseType), + GetCallbacks(_baseType)); + } + + public (Version version, CachedTypeInfo cachedTypeInfo) GetMatchedTypeOrLater(string speckleType, Version versionToMatch) + { + int length = speckleType.Length; + int end = length - 1; + int start; + + VersionCache? cachedVersions; + + do + { + string typeName; + + start = speckleType.LastIndexOf(':', end); + if (start < 0) + { + // we didn't find a : + typeName = speckleType.Substring(0, 1 + end); + } + else + { + typeName = speckleType.Substring(start + 1, end - start); + end = start - 1; + } + + if (_cachedTypes.TryGetValue(typeName, out cachedVersions)) + { + return MatchOrLater(versionToMatch, cachedVersions); + } + + var lastPeriod = typeName.LastIndexOf('.'); + typeName = typeName.Insert(1 + lastPeriod, "Deprecated."); + if (_cachedTypes.TryGetValue(typeName, out cachedVersions)) + { + return MatchOrLater(versionToMatch, cachedVersions); + } + } while (start >= 0); + + // FallbackType should exist + return (LoadedSchemaVersion, FallbackType.NotNull()); + } + + private (Version version, CachedTypeInfo cachedTypeInfo) MatchOrLater(Version versionToMatch, VersionCache versionCache) + { + if (versionToMatch == LoadedSchemaVersion) + { + return (LoadedSchemaVersion, versionCache.LatestVersion.NotNull()); + } + + // we could search or we can walk. We might be able to optomise this + foreach ((Version version, CachedTypeInfo cachedTypeInfo) cachedVersion in versionCache.Versions) + { + // if it's a match or comes later, then use this + if (cachedVersion.version >= versionToMatch) + { + return (cachedVersion.version, cachedVersion.cachedTypeInfo); + } + } + + // if we get here just use the latest version + return (LoadedSchemaVersion, versionCache.LatestVersion.NotNull()); + } + + private void CacheType(string typeName, Version version, CachedTypeInfo typeCacheInfo) + { + if (!_cachedTypes.TryGetValue(typeName, out VersionCache versionCache)) + { + versionCache = new VersionCache(typeName); + _cachedTypes[typeName] = versionCache; + } + + // is this the latest? + if (version == LoadedSchemaVersion) + { + // POC: we could be checking for prior existence + versionCache.LatestVersion = typeCacheInfo; + } + else + { + // we should NOT already have one of these + if (versionCache.Versions.Any<(Version version, CachedTypeInfo cache)>(item => item.version == version)) + { + throw new ArgumentException($"Version '{version}' already exists for cached type '{typeName}'"); + } + + versionCache.Versions.Add((version, typeCacheInfo)); + } + } + + private (string name, Version version) GetTypeNameAndVersion(string typeName) + { + // is this versioned at all? + if (typeName.StartsWith(_versionNamespacePrefix)) + { + // this is a version, the next portion is the version number + var nextPeriod = typeName.IndexOf('.', _versionNamespacePrefix.Length); + if (nextPeriod == -1 || nextPeriod == typeName.Length - 1) + { + // period should be found but should not be the last character + throw new ArgumentException($"Loaded type {typeName} appears to be a version but is malformed, expected a '.' and subsequent type name"); + } + + // POC: this is a sort of magic string with structure, I don't mind it because it is coming from the actual code but maybe worth considering + // we assume the version starts V_ hence shunting things 2 chars and replacing the _ with periods + int versionLength = nextPeriod - (_versionNamespacePrefix.Length + 2); + string versionString = + typeName.Substring(_versionNamespacePrefix.Length + 2, versionLength) + .Replace('_', '.'); + + string unversionedTypeName = $"{_namespacePrefix}{typeName.Substring(nextPeriod)}"; + + // we have to remove the Versionsless prefix + return (unversionedTypeName, new Version(versionString)); + } + + return (typeName, LoadedSchemaVersion); + } + + private Dictionary GetPropertyInfo(Type type) + { + Dictionary propertyMap = new(); + + PropertyInfo[] properties = type.GetProperties(); + foreach (PropertyInfo prop in properties) + { + propertyMap[prop.Name.ToLower()] = prop; + } + + return propertyMap; + } + + private List GetCallbacks(Type type) + { + List callbacks = new(); + + MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + foreach (MethodInfo method in methods) + { + List onDeserializedAttributes = method + .GetCustomAttributes(true) + .ToList(); + if (onDeserializedAttributes.Count > 0) + { + callbacks.Add(method); + } + } + + return callbacks; + } +} diff --git a/src/Speckle.Core/Serialisation/TypeCache/CachedTypeInfo.cs b/src/Speckle.Core/Serialisation/TypeCache/CachedTypeInfo.cs new file mode 100644 index 0000000..59a4996 --- /dev/null +++ b/src/Speckle.Core/Serialisation/TypeCache/CachedTypeInfo.cs @@ -0,0 +1,24 @@ +using System.Collections.Immutable; +using System.Reflection; + +namespace Speckle.Core.Serialisation.TypeCache; + +public sealed class CachedTypeInfo +{ + public string UnversionedTypeName { get; private set; } + public Type Type { get; private set; } + public ImmutableDictionary Props { get; private set; } + public ImmutableList Callbacks { get; private set; } + + public CachedTypeInfo( + string unversionedTypeName, + Type type, + Dictionary props, + List callbacks) + { + UnversionedTypeName = unversionedTypeName; + Type = type; + Props = props.ToImmutableDictionary(); + Callbacks = callbacks.ToImmutableList(); + } +} diff --git a/src/Speckle.Core/Serialisation/TypeCache/ITypeCache.cs b/src/Speckle.Core/Serialisation/TypeCache/ITypeCache.cs new file mode 100644 index 0000000..1b02cd9 --- /dev/null +++ b/src/Speckle.Core/Serialisation/TypeCache/ITypeCache.cs @@ -0,0 +1,10 @@ +namespace Speckle.Core.Serialisation.TypeCache; + +public interface ITypeCache +{ + void EnsureCacheIsBuilt(); + + (Version version, CachedTypeInfo cachedTypeInfo) GetMatchedTypeOrLater(string speckleType, Version versionToMatch); + + Version LoadedSchemaVersion { get; } +} diff --git a/src/Speckle.Core/SpeckleObjectSchema.cs b/src/Speckle.Core/SpeckleObjectSchema.cs new file mode 100644 index 0000000..dc27c15 --- /dev/null +++ b/src/Speckle.Core/SpeckleObjectSchema.cs @@ -0,0 +1,13 @@ +using Speckle.Core.Serialisation; + +namespace Speckle.Core; + +// POC: core needs to know the version of the schema we have baked into the Objects DLL +// you'll notice this version isn't IN the objects DLL, that's because Objects is dependent upon core +// but core needs to know the current schema version we have loaded. Core should depend on objects and NOT +// the other way around. This needs some consideration but for POC of object versioning, this is where it lives. +public class SpeckleObjectSchema +{ + // POC: I'm not sure about using the Version object ATM, strings may be just as a straight-forward... + public static readonly Version Version = new Version(0, 2, 0); +} diff --git a/src/Speckle.Core/Transports/ServerV2.cs b/src/Speckle.Core/Transports/ServerV2.cs index ff1baa2..4bfcd3a 100644 --- a/src/Speckle.Core/Transports/ServerV2.cs +++ b/src/Speckle.Core/Transports/ServerV2.cs @@ -9,6 +9,7 @@ using Speckle.Core.Helpers; using Speckle.Core.Logging; using Speckle.Core.Models; +using Speckle.Core.Serialisation; using Speckle.Core.Transports.ServerUtils; using Speckle.Newtonsoft.Json.Linq; @@ -326,7 +327,7 @@ private static IList ParseChildrenIds(string json) List childrenIds = new(); JObject doc1 = JObject.Parse(json); - JToken? closures = doc1["__closure"]; + JToken? closures = doc1[SerializationConstants.CLOSURE_PROPERTY_NAME]; if (closures == null) { return Array.Empty(); diff --git a/src/Speckle.Objects/Geometry/Point.cs b/src/Speckle.Objects/Geometry/Point.cs index 244ece4..caf5c60 100644 --- a/src/Speckle.Objects/Geometry/Point.cs +++ b/src/Speckle.Objects/Geometry/Point.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Objects.Other; using Speckle.Core.Common; using Speckle.Core.Kits; @@ -27,11 +25,12 @@ public class Point : Base, ITransformable /// The z coordinate /// The units of the point's coordinates. Defaults to Meters. /// The object's unique application ID - public Point(double x, double y, double z = 0d, string units = Units.Meters, string? applicationId = null) + public Point(double x, double y, double z = 0d, string units = Units.Meters, string? applicationId = null, double w = 1d) { this.x = x; this.y = y; this.z = z; + this.w = w; this.applicationId = applicationId; this.units = units; } @@ -72,6 +71,11 @@ public List value /// The z coordinate of the point. /// public double z { get; set; } + + /// + /// The w coordinate of the point. + /// + public double w { get; set; } /// /// The units this is in. diff --git a/src/Speckle.Objects/ObjectsKit.cs b/src/Speckle.Objects/ObjectsKit.cs deleted file mode 100644 index c09ed19..0000000 --- a/src/Speckle.Objects/ObjectsKit.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using Speckle.Core.Common; -using Speckle.Core.Helpers; -using Speckle.Core.Kits; -using Speckle.Core.Logging; -using Speckle.Core.Models; - -namespace Objects; - -/// -/// The default Speckle Kit -/// -public class ObjectsKit : ISpeckleKit -{ - private static string? s_objectsFolder; - - private readonly Dictionary _loadedConverters = new(); - - private List? _converters; - - /// - /// Local installations store objects in C:\Users\USERNAME\AppData\Roaming\Speckle\Kits\Objects - /// Admin/System-wide installations in C:\ProgramData\Speckle\Kits\Objects - /// - public static string ObjectsFolder - { - get => s_objectsFolder ??= SpecklePathProvider.ObjectsFolderPath; - [Obsolete("Use " + nameof(SpecklePathProvider.OverrideObjectsFolderName), true)] - set => s_objectsFolder = value; - } - - /// - public string Description => "The default Speckle Kit."; - - /// - public string Name => "Objects"; - - /// - public string Author => "Speckle"; - - /// - public string WebsiteOrEmail => "https://speckle.systems"; - - /// - public IEnumerable Types => - Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(Base)) && !t.IsAbstract); - - /// - public IEnumerable Converters => _converters ??= GetAvailableConverters(); - - /// - public ISpeckleConverter LoadConverter(string app) - { - try - { - _converters = GetAvailableConverters(); - if (_loadedConverters.TryGetValue(app, out Type t)) - { - return (ISpeckleConverter)Activator.CreateInstance(t); - } - - var converterInstance = LoadConverterFromDisk(app); - _loadedConverters[app] = converterInstance.GetType(); - - return converterInstance; - } - catch (Exception ex) - { - SpeckleLog.Logger.Fatal(ex, "Failed to load converter for app {app}", app); - throw new KitException($"Failed to load converter for app {app}:\n\n{ex.Message}", this, ex); - } - } - - private static ISpeckleConverter LoadConverterFromDisk(string app) - { - var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location).NotNull(); - - var path = Path.Combine(basePath, $"Objects.Converter.{app}.dll"); - - //fallback to the default folder, in case the Objects.dll was loaded in the app domain for other reasons - if (!File.Exists(path)) - { - path = Path.Combine(ObjectsFolder, $"Objects.Converter.{app}.dll"); - } - - if (!File.Exists(path)) - { - throw new FileNotFoundException($"Converter for {app} was not found in kit {basePath}", path); - } - - AssemblyName assemblyToLoad = AssemblyName.GetAssemblyName(path); - var objects = Assembly.GetExecutingAssembly().GetName(); - - //only get assemblies matching the Major and Minor version of Objects - if (assemblyToLoad.Version.Major != objects.Version.Major || assemblyToLoad.Version.Minor != objects.Version.Minor) - { - throw new SpeckleException( - $"Mismatch between Objects library v{objects.Version} Converter v{assemblyToLoad.Version}.\nEnsure the same 2.x version of Speckle connectors is installed." - ); - } - - var assembly = Assembly.LoadFrom(path); - - var converterInstance = assembly - .GetTypes() - .Where(type => typeof(ISpeckleConverter).IsAssignableFrom(type)) - .Select(type => (ISpeckleConverter)Activator.CreateInstance(type)) - .FirstOrDefault(converter => converter.GetServicedApplications().Contains(app)); - - if (converterInstance == null) - { - throw new SpeckleException($"No suitable converter instance found for {app}"); - } - - SpeckleLog - .Logger.ForContext() - .ForContext("basePath", basePath) - .ForContext("app", app) - .Information("Converter {converterName} successfully loaded from {path}", converterInstance.Name, path); - - return converterInstance; - } - - public List GetAvailableConverters() - { - var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location).NotNull(); - var allConverters = Directory.EnumerateFiles(basePath, "Objects.Converter.*.dll").ToArray(); - - //fallback to the default folder, in case the Objects.dll was loaded in the app domain for other reasons - if (allConverters.Length == 0) - { - allConverters = Directory.EnumerateFiles(ObjectsFolder, "Objects.Converter.*.dll").ToArray(); - } - - //only get assemblies matching the Major and Minor version of Objects - var objects = Assembly.GetExecutingAssembly().GetName(); - var availableConverters = new List(); - foreach (var converter in allConverters) - { - AssemblyName assemblyName = AssemblyName.GetAssemblyName(converter); - if (assemblyName.Version.Major == objects.Version.Major && assemblyName.Version.Minor == objects.Version.Minor) - { - availableConverters.Add(converter); - } - } - - return availableConverters.Select(dllPath => dllPath.Split('.').Reverse().ElementAt(1)).ToList(); - } -} diff --git a/src/Speckle.Objects/ObjectsTypeCache.cs b/src/Speckle.Objects/ObjectsTypeCache.cs new file mode 100644 index 0000000..48d031b --- /dev/null +++ b/src/Speckle.Objects/ObjectsTypeCache.cs @@ -0,0 +1,23 @@ +using System.Diagnostics; +using System.Reflection; +using Objects; +using Speckle.Core.Models; +using Speckle.Core.Reflection; +using Speckle.Core.Serialisation.TypeCache; + +namespace Speckle.Objects; + +public sealed class ObjectsTypeCache : AbstractTypeCache +{ + private readonly ITypeFinder _typeFinder; + + public ObjectsTypeCache(ITypeFinder typeFinder, Version? version = null) + : base(new[] { typeof(ObjectsTypeCache).Assembly }, + typeof(Base), + version ?? SpeckleSchemaInfo.Version, + "Objects", + typeFinder) + { + _typeFinder = typeFinder; + } +} diff --git a/src/Speckle.Objects/Speckle.Objects.csproj b/src/Speckle.Objects/Speckle.Objects.csproj index e4ff519..55b2773 100644 --- a/src/Speckle.Objects/Speckle.Objects.csproj +++ b/src/Speckle.Objects/Speckle.Objects.csproj @@ -8,12 +8,17 @@ Objects is the default object model for Speckle $(PackageTags), objects System.Runtime.CompilerServices.IsExternalInit;System.Runtime.CompilerServices.RequiresLocationAttribute + true + + + + diff --git a/src/Speckle.Objects/SpeckleSchemaInfo.cs b/src/Speckle.Objects/SpeckleSchemaInfo.cs new file mode 100644 index 0000000..ed930ac --- /dev/null +++ b/src/Speckle.Objects/SpeckleSchemaInfo.cs @@ -0,0 +1,6 @@ +namespace Objects; + +public static class SpeckleSchemaInfo +{ + public static readonly Version Version = new(0, 2, 0); +} diff --git a/src/Speckle.Objects/Versions/V_0_0_0/Geometry/Point.cs b/src/Speckle.Objects/Versions/V_0_0_0/Geometry/Point.cs new file mode 100644 index 0000000..c0868ef --- /dev/null +++ b/src/Speckle.Objects/Versions/V_0_0_0/Geometry/Point.cs @@ -0,0 +1,244 @@ +using Objects.Geometry; +using Objects.Other; +using Speckle.Core.Common; +using Speckle.Core.Kits; +using Speckle.Core.Models; +using Speckle.Newtonsoft.Json; + +namespace Objects.Versions.V_0_0_0.Geometry; + +/// +/// A 3-dimensional point +/// +/// +/// TODO: The Point class does not override the Equality operator, which means that there may be cases where `Equals` is used instead of `==`, as the comparison will be done by reference, not value. +/// +public class Point : Base, ITransformable +{ + /// + public Point() { } + + /// + /// Constructs a new from a set of coordinates and it's units. + /// + /// The x coordinate + /// The y coordinate + /// The z coordinate + /// The units of the point's coordinates. Defaults to Meters. + /// The object's unique application ID + public Point(double x, double y, double z = 0d, string units = Units.Meters, string? applicationId = null) + { + this.x = x; + this.y = y; + this.z = z; + this.applicationId = applicationId; + this.units = units; + } + + /// + /// Constructs a new from a + /// + /// The Vector whose coordinates will be used for the Point + public Point(Vector vector) + : this(vector.x, vector.y, vector.z, vector.units, vector.applicationId) { } + + /// + /// Gets or sets the coordinates of the + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore), Obsolete("Use x,y,z properties instead", true)] + public List value + { + get => null!; + set + { + x = value[0]; + y = value[1]; + z = value.Count > 2 ? value[2] : 0; + } + } + + /// + /// The x coordinate of the point. + /// + public double x { get; set; } + + /// + /// The y coordinate of the point. + /// + public double y { get; set; } + + /// + /// The z coordinate of the point. + /// + public double z { get; set; } + + /// + /// The units this is in. + /// This should be one of the units specified in + /// + public string units { get; set; } = Units.None; + + [JsonIgnore, Obsolete("Bounding box no longer applicable to point as of 2.18", true)] + public Box? bbox { get; set; } + + /// + public bool TransformTo(Transform transform, out Point transformed) + { + var matrix = transform.matrix; + + var unitFactor = Units.GetConversionFactor(transform.units, units); // applied to translation vector + var divisor = matrix.M41 + matrix.M42 + matrix.M43 + unitFactor * matrix.M44; + var x = (this.x * matrix.M11 + this.y * matrix.M12 + this.z * matrix.M13 + unitFactor * matrix.M14) / divisor; + var y = (this.x * matrix.M21 + this.y * matrix.M22 + this.z * matrix.M23 + unitFactor * matrix.M24) / divisor; + var z = (this.x * matrix.M31 + this.y * matrix.M32 + this.z * matrix.M33 + unitFactor * matrix.M34) / divisor; + + transformed = new Point(x, y, z) { units = units, applicationId = applicationId }; + return true; + } + + /// + public bool TransformTo(Transform transform, out ITransformable transformed) + { + var res = TransformTo(transform, out Point pt); + transformed = pt; + return res; + } + + /// + /// Returns the coordinates of this as a list of numbers + /// + /// A list of coordinates {x, y, z} + public List ToList() + { + return new List { x, y, z }; + } + + /// + /// Creates a new based on a list of coordinates and the unit they're drawn in. + /// + /// The list of coordinates {x, y, z} + /// The units the coordinates are in + /// A new with the provided coordinates. + public static Point FromList(IList list, string units) + { + return new Point(list[0], list[1], list[2], units); + } + + /// + /// Deconstructs a into it's coordinates and units + /// + /// The x coordinate + /// The y coordinate + /// The z coordinate + /// The units the point's coordinates are in. + public void Deconstruct(out double x, out double y, out double z, out string? units) + { + Deconstruct(out x, out y, out z); + units = this.units; + } + + /// + /// Deconstructs a into it's coordinates and units + /// + /// The x coordinate + /// The y coordinate + /// The z coordinate + public void Deconstruct(out double x, out double y, out double z) + { + x = this.x; + y = this.y; + z = this.z; + } + + public static Point operator +(Point point1, Point point2) => + new(point1.x + point2.x, point1.y + point2.y, point1.z + point2.z, point1.units); + + public static Point operator -(Point point1, Point point2) => + new(point1.x - point2.x, point1.y - point2.y, point1.z - point2.z, point1.units); + + public static Point operator *(Point point1, Point point2) => + new(point1.x * point2.x, point1.y * point2.y, point1.z * point2.z, point1.units); + + public static Point operator *(Point point, double val) => + new(point.x * val, point.y * val, point.z * val, point.units); + + public static Point operator /(Point point, double val) => + new(point.x / val, point.y / val, point.z / val, point.units); + + public static bool operator ==(Point? point1, Point? point2) + { + if (point1 is null && point2 is null) + { + return true; + } + else if (point1 is null || point2 is null) + { + return false; + } + + return point1.units == point2.units && point1.x == point2.x && point1.y == point2.y && point1.z == point2.z; + } + + public static bool operator !=(Point? point1, Point? point2) => !(point1 == point2); + + /// + /// Computes a point equidistant from two points. + /// + /// First point. + /// Second point. + /// A point at the same distance from and + public static Point Midpoint(Point point1, Point point2) + { + return new Point( + 0.5 * (point1.x + point2.x), + 0.5 * (point1.y + point2.y), + 0.5 * (point1.z + point2.z), + point1.units + ); + } + + /// + /// Computes the distance between two points + /// + /// First point. + /// Second point. + /// The distance from to + public static double Distance(Point point1, Point point2) + { + return Math.Sqrt( + Math.Pow(point1.x - point2.x, 2) + Math.Pow(point1.y - point2.y, 2) + Math.Pow(point1.z - point2.z, 2) + ); + } + + /// + /// Computes the distance between two points. + /// + /// point for distance measurement + /// The length of the line between this and the other point + public double DistanceTo(Point point) + { + return Math.Sqrt(Math.Pow(x - point.x, 2) + Math.Pow(y - point.y, 2) + Math.Pow(z - point.z, 2)); + } + + public static Point Add(Point left, Point right) + { + throw new NotImplementedException(); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (ReferenceEquals(obj, null)) + { + return false; + } + + return this == (Point)obj; + } + + public override int GetHashCode() => HashCode.Of(units).And(x).And(y).And(y); +} diff --git a/src/Speckle.Objects/Versions/V_0_0_0/Upgraders/PointUpgrader.cs b/src/Speckle.Objects/Versions/V_0_0_0/Upgraders/PointUpgrader.cs new file mode 100644 index 0000000..67bcefb --- /dev/null +++ b/src/Speckle.Objects/Versions/V_0_0_0/Upgraders/PointUpgrader.cs @@ -0,0 +1,26 @@ +using Speckle.Core.Reflection; +using Speckle.Core.SchemaVersioning; + +using Source = Objects.Versions.V_0_0_0.Geometry.Point; +using Destination = Objects.Geometry.Point; + +namespace Speckle.Objects.Versions.V_0_0_0.Upgraders; + +// POC: consider weakness of strings here +// the type passed in is a bit janky, if we want to move from say RevitWall to Wall, +// then we would need to have some care about where the type name came from, as in this example +// POC: the version could come from some constant tbh and then it won't be wrong... +// is the typename off the source or destination :pained face: +[NamedType(typeof(Source), "0.0.0")] +public sealed class PointUpgrader : AbstractSchemaObjectBaseUpgrader +{ + public PointUpgrader() : base(new Version(0,0,0), new Version(0,1, 0)) + { + + } + + public override Destination Upgrade(Source input) + { + return new Destination(input.x, input.y, input.z); + } +} diff --git a/tests/Speckle.Core.Tests.Unit/Api/Operations/SendReceiveLocal.cs b/tests/Speckle.Core.Tests.Unit/Api/Operations/SendReceiveLocal.cs index 2bb9e62..86d2ba9 100644 --- a/tests/Speckle.Core.Tests.Unit/Api/Operations/SendReceiveLocal.cs +++ b/tests/Speckle.Core.Tests.Unit/Api/Operations/SendReceiveLocal.cs @@ -214,36 +214,7 @@ public async Task DownloadProgressReports() progress.NotNull(); Assert.That(progress.Keys, Has.Count.GreaterThanOrEqualTo(1)); } - - [Test(Description = "Should dispose of transports after a send or receive operation if so specified.")] - [Obsolete("Send overloads that perform disposal are deprecated")] - public async Task ShouldDisposeTransports() - { - var @base = new Base(); - @base["test"] = "the best"; - - var myLocalTransport = new SQLiteTransport(); - var id = await Core.Api.Operations.Send( - @base, - new List { myLocalTransport }, - false, - disposeTransports: true - ); - - // Send - Assert.ThrowsAsync( - async () => - await Core.Api.Operations.Send(@base, new List { myLocalTransport }, false, disposeTransports: true) - ); - - myLocalTransport = myLocalTransport.Clone() as SQLiteTransport; - _ = await Core.Api.Operations.Receive(id, null, myLocalTransport, disposeTransports: true); - - Assert.ThrowsAsync( - async () => await Core.Api.Operations.Receive(id, null, myLocalTransport) - ); - } - + [Test(Description = "Should not dispose of transports if so specified.")] public async Task ShouldNotDisposeTransports() { @@ -258,30 +229,6 @@ public async Task ShouldNotDisposeTransports() await Core.Api.Operations.Receive(id, null, myLocalTransport); } - //[Test] - //public async Task DiskTransportTest() - //{ - // var myObject = new Base(); - // myObject["@items"] = new List(); - // myObject["test"] = "random"; - - // var rand = new Random(); - - // for (int i = 0; i < 100; i++) - // { - // ((List)myObject["@items"]).Add(new Point(i, i, i) { applicationId = i + "-___/---" }); - // } - - // var dt = new Speckle.Core.Transports.Speckle.Speckle.Core.Transports(); - // var id = await Operations.Send(myObject, new List() { dt }, false); - - // Assert.IsNotNull(id); - - // var rebase = await Operations.Receive(id, dt); - - // Assert.AreEqual(rebase.GetId(true), id); - //} - public void Dispose() { _sut.Dispose();