Skip to content

Commit

Permalink
release 4.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
drache42 committed Jul 16, 2024
1 parent e092870 commit 0fa4824
Show file tree
Hide file tree
Showing 36 changed files with 2,915 additions and 0 deletions.
158 changes: 158 additions & 0 deletions src/Tableau.Migration/Engine/Manifest/MigrationManifestSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//
// Copyright (c) 2024, Salesforce, Inc.
// SPDX-License-Identifier: Apache-2
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

using System;
using System.Collections.Immutable;
using System.IO;
using System.IO.Abstractions;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Tableau.Migration.JsonConverters;
using Tableau.Migration.JsonConverters.SerializableObjects;
using Tableau.Migration.Resources;


namespace Tableau.Migration.Engine.Manifest
{
/// <summary>
/// Provides functionality to serialize and deserialize migration manifests in JSON format.
/// </summary>
public class MigrationManifestSerializer
{
private readonly IFileSystem _fileSystem;
private readonly ISharedResourcesLocalizer _localizer;
private readonly ILoggerFactory _loggerFactory;

private readonly ImmutableArray<JsonConverter> _converters;

/// <summary>
/// Initializes a new instance of the <see cref="MigrationManifestSerializer"/> class.
/// </summary>
public MigrationManifestSerializer(IFileSystem fileSystem, ISharedResourcesLocalizer localizer, ILoggerFactory loggerFactory)
{
_fileSystem = fileSystem;
_localizer = localizer;
_loggerFactory = loggerFactory;

_converters = CreateConverters();
}

/// <summary>
/// This is the current MigrationManifest.ManifestVersion that this serializer supports.
/// </summary>
public const uint SupportedManifestVersion = MigrationManifest.LatestManifestVersion;


/// <summary>
/// Creates the list of JSON converters used by the MigrationManifestSerializer.
/// </summary>
/// <remarks>This is a static method so the tests can use the same list converters.</remarks>
/// <returns>An immutable array of JSON converters.</returns>
internal static ImmutableArray<JsonConverter> CreateConverters()
{
return new JsonConverter[]
{
new PythonExceptionConverter(),
new SerializedExceptionJsonConverter(),
new BuildResponseExceptionJsonConverter(),
new JobJsonConverter(),
new TimeoutJobExceptionJsonConverter(),
new RestExceptionJsonConverter(),
new FailedJobExceptionJsonConverter(),
new ExceptionJsonConverterFactory(), // This needs to be at the end. This list is ordered.
}.ToImmutableArray();
}

private JsonSerializerOptions MergeJsonOptions(JsonSerializerOptions? jsonOptions)
{
jsonOptions ??= new() { WriteIndented = true };

foreach (var converter in _converters)
{
jsonOptions.Converters.Add(converter);
}

return jsonOptions;
}

/// <summary>
/// Saves a manifest in JSON format.
/// </summary>
/// <remarks>This async function does not take a cancellation token. This is because the saving should happen,
/// no matter what the status of the cancellation token is. Otherwise the manifest is not saved if the migration is cancelled.</remarks>
/// <param name="manifest">The manifest to save.</param>
/// <param name="path">The file path to save the manifest to.</param>
/// <param name="jsonOptions">Optional JSON options to use.</param>
public async Task SaveAsync(IMigrationManifest manifest, string path, JsonSerializerOptions? jsonOptions = null)
{
jsonOptions = MergeJsonOptions(jsonOptions);

var dir = Path.GetDirectoryName(path);
if (dir is not null && !_fileSystem.Directory.Exists(dir))
{
_fileSystem.Directory.CreateDirectory(dir);
}

var serializableManifest = new SerializableMigrationManifest(manifest);

var file = _fileSystem.File.Create(path);
await using (file.ConfigureAwait(false))
{
// If cancellation was requested, we still need to save the file, so use the default token.
await JsonSerializer.SerializeAsync(file, serializableManifest, jsonOptions, default)
.ConfigureAwait(false);
}
}

/// <summary>
/// Loads a manifest from JSON format.
/// </summary>
/// <param name="path">The file path to load the manifest from.</param>
/// <param name="cancel">The cancellation token to obey.</param>
/// <param name="jsonOptions">Optional JSON options to use.</param>
/// <returns>The loaded <see cref="MigrationManifest"/>, or null if the manifest could not be loaded.</returns>
public async Task<MigrationManifest?> LoadAsync(string path, CancellationToken cancel, JsonSerializerOptions? jsonOptions = null)
{
if (!_fileSystem.File.Exists(path))
{
return null;
}

jsonOptions = MergeJsonOptions(jsonOptions);

var file = _fileSystem.File.OpenRead(path);
await using (file.ConfigureAwait(false))
{
var manifest = await JsonSerializer.DeserializeAsync<SerializableMigrationManifest>(file, jsonOptions, cancel)
.ConfigureAwait(false);

if (manifest is not null)
{
if (manifest.ManifestVersion is not SupportedManifestVersion)
throw new NotSupportedException($"This {nameof(MigrationManifestSerializer)} only supports Manifest version {SupportedManifestVersion}. The manifest being loaded is version {manifest.ManifestVersion}");

return manifest.ToMigrationManifest(_localizer, _loggerFactory) as MigrationManifest;
}

return null;
}
}
}
}
74 changes: 74 additions & 0 deletions src/Tableau.Migration/ExceptionComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// Copyright (c) 2024, Salesforce, Inc.
// SPDX-License-Identifier: Apache-2
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;

namespace Tableau.Migration
{
/// <summary>
/// Provides methods for comparing exceptions
/// </summary>
public class ExceptionComparer : IEqualityComparer<Exception>
{
///<inheritdoc/>
public bool Equals(Exception? x, Exception? y)
{
if (x is null && y is null) return true;
if (x is null || y is null) return false;

// Check if x and y are of the same type
if (x.GetType() != y.GetType())
{
return false;
}

// Check if they implement IEquatable<T> for their specific type
Type equatableType = typeof(IEquatable<>).MakeGenericType(x.GetType());
if (equatableType.IsAssignableFrom(x.GetType()))
{
return (bool?)equatableType.GetMethod("Equals")?.Invoke(x, new object?[] { y }) ?? false;
}
else
{
return x.Message == y.Message;
}
}

///<inheritdoc/>
public int GetHashCode(Exception obj)
{
// Check if the object's type overrides GetHashCode
MethodInfo? getHashCodeMethod = obj.GetType().GetMethod("GetHashCode", Type.EmptyTypes);
if (getHashCodeMethod != null && getHashCodeMethod.DeclaringType != typeof(object))
{
return obj.GetHashCode();
}
else if (obj is IEquatable<Exception>)
{
return obj.GetHashCode();
}
else
{
return obj.Message?.GetHashCode() ?? 0;
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// Copyright (c) 2024, Salesforce, Inc.
// SPDX-License-Identifier: Apache-2
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

using System;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using Tableau.Migration.Api.Simulation.Rest.Net.Responses;

namespace Tableau.Migration.JsonConverters
{
/// <summary>
/// JsonConverter that serializes a <see cref="BuildResponseException"/>. It does not support reading exceptions back in.
/// </summary>
internal class BuildResponseExceptionJsonConverter : JsonConverter<BuildResponseException>
{
public override void Write(Utf8JsonWriter writer, BuildResponseException value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString(nameof(BuildResponseException.StatusCode), value.StatusCode.ToString());
writer.WriteNumber(nameof(BuildResponseException.SubCode), value.SubCode);
writer.WriteString(nameof(BuildResponseException.Summary), value.Summary);
writer.WriteString(nameof(BuildResponseException.Detail), value.Detail);
JsonWriterUtils.WriteExceptionProperties(ref writer, value);
writer.WriteEndObject();
}


public override BuildResponseException? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
JsonReaderUtils.AssertStartObject(ref reader);

HttpStatusCode? statusCode = null;
int? subCode = null;
string? summary = null;
string? detail = null;

while (reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
string? propertyName = reader.GetString();
Guard.AgainstNullOrEmpty(propertyName, nameof(propertyName));

reader.Read(); // Move to the property value.

switch(propertyName)
{
case nameof(BuildResponseException.StatusCode):
var statusCodeStr = reader.GetString();
Guard.AgainstNull(statusCodeStr, nameof(statusCodeStr));
statusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), statusCodeStr);
break;

case nameof(BuildResponseException.SubCode):
subCode = reader.GetInt32();
break;

case nameof(BuildResponseException.Summary):
summary = reader.GetString();
Guard.AgainstNull(summary, nameof(summary));
break;

case nameof(BuildResponseException.Detail):
detail = reader.GetString();
Guard.AgainstNull(detail, nameof(detail));
break;

default:
break;
}
}
else if (reader.TokenType == JsonTokenType.EndObject)
{
break; // End of the object.
}
}

return new BuildResponseException(statusCode!.Value, subCode!.Value, summary!, detail!);
}

}
}
27 changes: 27 additions & 0 deletions src/Tableau.Migration/JsonConverters/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Copyright (c) 2024, Salesforce, Inc.
// SPDX-License-Identifier: Apache-2
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

namespace Tableau.Migration.JsonConverters
{
internal static class Constants
{
public const string PARTITION = "Partition";
public const string ENTRIES = "Entries";
public const string CLASS_NAME = "ClassName";
public const string EXCEPTION = "Exception";
}
}
Loading

0 comments on commit 0fa4824

Please sign in to comment.