Skip to content

Commit

Permalink
Basic layout persistence implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
fhubi committed Sep 3, 2024
1 parent dd3817e commit dbc32c7
Show file tree
Hide file tree
Showing 13 changed files with 612 additions and 0 deletions.
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,105 @@
/*
* 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;

try
{
if (!Directory.Exists(_basePath))
{
Directory.CreateDirectory(_basePath);
}
}
catch (Exception ex)

Check warning on line 37 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#L37

Added line #L37 was not covered by tests
{
throw new IOException($"Failed to create or access the directory: {_basePath}.", ex);

Check warning on line 39 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#L39

Added line #L39 was not covered by tests
}
}

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 103 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#L103

Added line #L103 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());
}

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

0 comments on commit dbc32c7

Please sign in to comment.