-
Notifications
You must be signed in to change notification settings - Fork 29
Basic layout persistence implementation. #755
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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
|
||
} | ||
} |
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
|
||
} | ||
|
||
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
|
||
} | ||
|
||
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
|
||
} | ||
|
||
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
|
||
} | ||
|
||
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)); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.