Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic layout persistence implementation. #755

Merged
merged 1 commit into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/layout-persistence/dotnet/LayoutPersistence.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34723.18
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.LayoutPersistence", "src\MorganStanley.ComposeUI.LayoutPersistence\MorganStanley.ComposeUI.LayoutPersistence.csproj", "{BC4F5C92-F894-45A7-BBEA-A956F89C617B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{15375C6D-AEF5-42DA-B809-36C55A5133B0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{387B5C73-2D89-45B1-92E0-71CDBB825F46}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MorganStanley.ComposeUI.LayoutPersistence.Tests", "tests\MorganStanley.ComposeUI.LayoutPersistence.Tests\MorganStanley.ComposeUI.LayoutPersistence.Tests.csproj", "{9727D812-2697-4BB5-8C65-80774842FE99}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BC4F5C92-F894-45A7-BBEA-A956F89C617B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC4F5C92-F894-45A7-BBEA-A956F89C617B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC4F5C92-F894-45A7-BBEA-A956F89C617B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC4F5C92-F894-45A7-BBEA-A956F89C617B}.Release|Any CPU.Build.0 = Release|Any CPU
{9727D812-2697-4BB5-8C65-80774842FE99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9727D812-2697-4BB5-8C65-80774842FE99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9727D812-2697-4BB5-8C65-80774842FE99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9727D812-2697-4BB5-8C65-80774842FE99}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{BC4F5C92-F894-45A7-BBEA-A956F89C617B} = {15375C6D-AEF5-42DA-B809-36C55A5133B0}
{9727D812-2697-4BB5-8C65-80774842FE99} = {387B5C73-2D89-45B1-92E0-71CDBB825F46}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5AC4D360-3DCE-47E1-8128-C76C86113476}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Morgan Stanley makes this available to you under the Apache License,
* Version 2.0 (the "License"). You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0.
*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership. 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 MorganStanley.ComposeUI.LayoutPersistence.Abstractions;

public interface ILayoutPersistence<T>
{
Task SaveLayoutAsync(string layoutName, T layoutData, CancellationToken cancellationToken = default);
Task<T?> LoadLayoutAsync(string layoutName, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Morgan Stanley makes this available to you under the Apache License,
* Version 2.0 (the "License"). You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0.
*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership. 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 MorganStanley.ComposeUI.LayoutPersistence.Abstractions;

public interface ILayoutSerializer<T>
{
Task<string> SerializeAsync(T layoutObject, CancellationToken cancellationToken = default);
Task<T?> DeserializeAsync(string layoutData, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Morgan Stanley makes this available to you under the Apache License,
* Version 2.0 (the "License"). You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0.
*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership. 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 MorganStanley.ComposeUI.LayoutPersistence.Abstractions;

namespace MorganStanley.ComposeUI.LayoutPersistence;

public class FileLayoutPersistence<T> : ILayoutPersistence<T>
{
private readonly string _basePath;
private readonly ILayoutSerializer<T> _serializer;
private readonly SemaphoreSlim _semaphore = new(1,1);

public FileLayoutPersistence(string basePath, ILayoutSerializer<T> serializer)
{
_basePath = NormalizeFilePath(basePath);
_serializer = serializer;

if (!Directory.Exists(_basePath))
{
Directory.CreateDirectory(_basePath);
}
}

public async Task SaveLayoutAsync(string layoutName, T layoutData, CancellationToken cancellationToken = default)
{
var filePath = GetFilePath(layoutName);

await _semaphore.WaitAsync(cancellationToken);

try
{
var serializedData = await _serializer.SerializeAsync(layoutData, cancellationToken);
await File.WriteAllTextAsync(filePath, serializedData, cancellationToken);
}
finally
{
_semaphore.Release();
}
}

public async Task<T?> LoadLayoutAsync(string layoutName, CancellationToken cancellationToken = default)
{
var filePath = GetFilePath(layoutName);

if (!File.Exists(filePath))
{
throw new FileNotFoundException($"Layout file not found: {filePath}");
}

await _semaphore.WaitAsync(cancellationToken);

try
{
var serializedData = await File.ReadAllTextAsync(filePath, cancellationToken);
return await _serializer.DeserializeAsync(serializedData, cancellationToken);
}
finally
{
_semaphore.Release();
}
}

private string GetFilePath(string layoutName)
{
var combinedPath = Path.Combine(_basePath, $"{layoutName}.layout");
var fullPath = Path.GetFullPath(combinedPath);

if (!fullPath.StartsWith(_basePath, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Invalid layoutName argument. File cannot be saved outside of the base directory.", layoutName);
}

return fullPath;
}

private static string NormalizeFilePath(string path)
{
if (path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
var normalizedPath = Uri.UnescapeDataString(path[7..]);
return Path.GetFullPath(normalizedPath);
}

return Path.GetFullPath(path);

Check warning on line 96 in src/layout-persistence/dotnet/src/MorganStanley.ComposeUI.LayoutPersistence/FileLayoutPersistence.cs

View check run for this annotation

Codecov / codecov/patch

src/layout-persistence/dotnet/src/MorganStanley.ComposeUI.LayoutPersistence/FileLayoutPersistence.cs#L96

Added line #L96 was not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Morgan Stanley makes this available to you under the Apache License,
* Version 2.0 (the "License"). You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0.
*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership. 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.Text;
using System.Text.Json;
using MorganStanley.ComposeUI.LayoutPersistence.Abstractions;

namespace MorganStanley.ComposeUI.LayoutPersistence.Serializers;

public class JsonLayoutSerializer<T> : ILayoutSerializer<T>
{
private readonly JsonSerializerOptions? _jsonSerializerOptions;

public JsonLayoutSerializer(JsonSerializerOptions? jsonSerializerOptions = null)
{
_jsonSerializerOptions = jsonSerializerOptions;
}

public async Task<string> SerializeAsync(T layoutObject, CancellationToken cancellationToken = default)
{
if (layoutObject == null)
{
throw new ArgumentNullException(nameof(layoutObject), "The layout object to serialize cannot be null.");

Check warning on line 34 in src/layout-persistence/dotnet/src/MorganStanley.ComposeUI.LayoutPersistence/Serializers/JsonLayoutSerializer.cs

View check run for this annotation

Codecov / codecov/patch

src/layout-persistence/dotnet/src/MorganStanley.ComposeUI.LayoutPersistence/Serializers/JsonLayoutSerializer.cs#L34

Added line #L34 was not covered by tests
}

using var stream = new MemoryStream();
await JsonSerializer.SerializeAsync(stream, layoutObject, _jsonSerializerOptions, cancellationToken);

return Encoding.UTF8.GetString(stream.ToArray());
fhubi marked this conversation as resolved.
Show resolved Hide resolved
}

public async Task<T?> DeserializeAsync(string layoutData, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(layoutData))
{
throw new ArgumentException("The layout data cannot be null or empty.", nameof(layoutData));

Check warning on line 47 in src/layout-persistence/dotnet/src/MorganStanley.ComposeUI.LayoutPersistence/Serializers/JsonLayoutSerializer.cs

View check run for this annotation

Codecov / codecov/patch

src/layout-persistence/dotnet/src/MorganStanley.ComposeUI.LayoutPersistence/Serializers/JsonLayoutSerializer.cs#L47

Added line #L47 was not covered by tests
}

using var stream = new MemoryStream(Encoding.UTF8.GetBytes(layoutData));
return await JsonSerializer.DeserializeAsync<T>(stream, _jsonSerializerOptions, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Morgan Stanley makes this available to you under the Apache License,
* Version 2.0 (the "License"). You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0.
*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership. 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 MorganStanley.ComposeUI.LayoutPersistence.Abstractions;
using System.Text;
using System.Xml.Serialization;

namespace MorganStanley.ComposeUI.LayoutPersistence.Serializers;

public class XmlLayoutSerializer<T> : ILayoutSerializer<T>
{
public async Task<string> SerializeAsync(T layoutObject, CancellationToken cancellationToken = default)
{
if (layoutObject == null)
{
throw new ArgumentNullException(nameof(layoutObject), "The layout object to serialize cannot be null.");

Check warning on line 27 in src/layout-persistence/dotnet/src/MorganStanley.ComposeUI.LayoutPersistence/Serializers/XmlLayoutSerializer.cs

View check run for this annotation

Codecov / codecov/patch

src/layout-persistence/dotnet/src/MorganStanley.ComposeUI.LayoutPersistence/Serializers/XmlLayoutSerializer.cs#L27

Added line #L27 was not covered by tests
}

using var stream = new MemoryStream();
var serializer = new XmlSerializer(typeof(T));

using (var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true))
{
serializer.Serialize(writer, layoutObject);
await writer.FlushAsync();
}

cancellationToken.ThrowIfCancellationRequested();

return Encoding.UTF8.GetString(stream.ToArray());
}

public async Task<T?> DeserializeAsync(string layoutData, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(layoutData))
{
throw new ArgumentException("The layout data cannot be null or empty.", nameof(layoutData));

Check warning on line 48 in src/layout-persistence/dotnet/src/MorganStanley.ComposeUI.LayoutPersistence/Serializers/XmlLayoutSerializer.cs

View check run for this annotation

Codecov / codecov/patch

src/layout-persistence/dotnet/src/MorganStanley.ComposeUI.LayoutPersistence/Serializers/XmlLayoutSerializer.cs#L48

Added line #L48 was not covered by tests
}

var dataBytes = Encoding.UTF8.GetBytes(layoutData);

using var stream = new MemoryStream(dataBytes);

cancellationToken.ThrowIfCancellationRequested();

var serializer = new XmlSerializer(typeof(T));

return await Task.FromResult((T?)serializer.Deserialize(stream));
}
}
Loading
Loading