Skip to content

Commit aff08f2

Browse files
committed
Basic layout persistence implementation.
1 parent dd3817e commit aff08f2

15 files changed

+798
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.9.34723.18
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.LayoutPersistence", "src\MorganStanley.ComposeUI.LayoutPersistence\MorganStanley.ComposeUI.LayoutPersistence.csproj", "{BC4F5C92-F894-45A7-BBEA-A956F89C617B}"
7+
EndProject
8+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{15375C6D-AEF5-42DA-B809-36C55A5133B0}"
9+
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{387B5C73-2D89-45B1-92E0-71CDBB825F46}"
11+
EndProject
12+
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}"
13+
EndProject
14+
Global
15+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
16+
Debug|Any CPU = Debug|Any CPU
17+
Release|Any CPU = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
20+
{BC4F5C92-F894-45A7-BBEA-A956F89C617B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{BC4F5C92-F894-45A7-BBEA-A956F89C617B}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{BC4F5C92-F894-45A7-BBEA-A956F89C617B}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{BC4F5C92-F894-45A7-BBEA-A956F89C617B}.Release|Any CPU.Build.0 = Release|Any CPU
24+
{9727D812-2697-4BB5-8C65-80774842FE99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25+
{9727D812-2697-4BB5-8C65-80774842FE99}.Debug|Any CPU.Build.0 = Debug|Any CPU
26+
{9727D812-2697-4BB5-8C65-80774842FE99}.Release|Any CPU.ActiveCfg = Release|Any CPU
27+
{9727D812-2697-4BB5-8C65-80774842FE99}.Release|Any CPU.Build.0 = Release|Any CPU
28+
EndGlobalSection
29+
GlobalSection(SolutionProperties) = preSolution
30+
HideSolutionNode = FALSE
31+
EndGlobalSection
32+
GlobalSection(NestedProjects) = preSolution
33+
{BC4F5C92-F894-45A7-BBEA-A956F89C617B} = {15375C6D-AEF5-42DA-B809-36C55A5133B0}
34+
{9727D812-2697-4BB5-8C65-80774842FE99} = {387B5C73-2D89-45B1-92E0-71CDBB825F46}
35+
EndGlobalSection
36+
GlobalSection(ExtensibilityGlobals) = postSolution
37+
SolutionGuid = {5AC4D360-3DCE-47E1-8128-C76C86113476}
38+
EndGlobalSection
39+
EndGlobal
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Morgan Stanley makes this available to you under the Apache License,
3+
* Version 2.0 (the "License"). You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0.
6+
*
7+
* See the NOTICE file distributed with this work for additional information
8+
* regarding copyright ownership. Unless required by applicable law or agreed
9+
* to in writing, software distributed under the License is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions
12+
* and limitations under the License.
13+
*/
14+
15+
namespace MorganStanley.ComposeUI.LayoutPersistence.Abstractions;
16+
17+
public interface ILayoutPersistence
18+
{
19+
Task SaveLayoutAsync(string layoutData, string layoutName, CancellationToken cancellationToken = default);
20+
Task<string> LoadLayoutAsync(string layoutName, CancellationToken cancellationToken = default);
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Morgan Stanley makes this available to you under the Apache License,
3+
* Version 2.0 (the "License"). You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0.
6+
*
7+
* See the NOTICE file distributed with this work for additional information
8+
* regarding copyright ownership. Unless required by applicable law or agreed
9+
* to in writing, software distributed under the License is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions
12+
* and limitations under the License.
13+
*/
14+
15+
namespace MorganStanley.ComposeUI.LayoutPersistence.Abstractions;
16+
17+
public interface ILayoutSerializer
18+
{
19+
Task<string> SerializeAsync<T>(T layoutObject, CancellationToken cancellationToken = default);
20+
Task<T?> DeserializeAsync<T>(string layoutData, CancellationToken cancellationToken = default);
21+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Morgan Stanley makes this available to you under the Apache License,
3+
* Version 2.0 (the "License"). You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0.
6+
*
7+
* See the NOTICE file distributed with this work for additional information
8+
* regarding copyright ownership. Unless required by applicable law or agreed
9+
* to in writing, software distributed under the License is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions
12+
* and limitations under the License.
13+
*/
14+
15+
using MorganStanley.ComposeUI.LayoutPersistence.Abstractions;
16+
17+
namespace MorganStanley.ComposeUI.LayoutPersistence;
18+
19+
public class FileLayoutPersistence : ILayoutPersistence
20+
{
21+
private readonly string _basePath;
22+
private readonly SemaphoreSlim _semaphore = new(1,1);
23+
24+
public FileLayoutPersistence(string basePath)
25+
{
26+
_basePath = NormalizeFilePath(basePath);
27+
28+
try
29+
{
30+
if (!Directory.Exists(_basePath))
31+
{
32+
Directory.CreateDirectory(_basePath);
33+
}
34+
}
35+
catch (Exception ex)
36+
{
37+
throw new IOException($"Failed to create or access the directory: {_basePath}.", ex);
38+
}
39+
}
40+
41+
public async Task SaveLayoutAsync(string layoutData, string layoutName, CancellationToken cancellationToken = default)
42+
{
43+
var filePath = GetFilePath(layoutName);
44+
45+
await _semaphore.WaitAsync(cancellationToken);
46+
47+
try
48+
{
49+
await File.WriteAllTextAsync(filePath, layoutData, cancellationToken);
50+
}
51+
catch (Exception ex)
52+
{
53+
throw new IOException($"Failed to save layout to file: {filePath}.", ex);
54+
}
55+
finally
56+
{
57+
_semaphore.Release();
58+
}
59+
}
60+
61+
public async Task<string> LoadLayoutAsync(string layoutName, CancellationToken cancellationToken = default)
62+
{
63+
var filePath = GetFilePath(layoutName);
64+
65+
await _semaphore.WaitAsync(cancellationToken);
66+
67+
if (!File.Exists(filePath))
68+
{
69+
throw new FileNotFoundException($"Layout file not found: {filePath}");
70+
}
71+
72+
try
73+
{
74+
return await File.ReadAllTextAsync(filePath, cancellationToken);
75+
}
76+
catch (Exception ex)
77+
{
78+
throw new IOException($"Failed to load layout from file: {filePath}.", ex);
79+
}
80+
finally
81+
{
82+
_semaphore.Release();
83+
}
84+
}
85+
86+
private string GetFilePath(string layoutName)
87+
{
88+
return Path.Combine(_basePath, $"{layoutName}.layout");
89+
}
90+
91+
private static string NormalizeFilePath(string path)
92+
{
93+
if (path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
94+
{
95+
var normalizedPath = Uri.UnescapeDataString(path[7..]);
96+
return Path.GetFullPath(normalizedPath);
97+
}
98+
99+
return Path.GetFullPath(path);
100+
}
101+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Morgan Stanley makes this available to you under the Apache License,
3+
* Version 2.0 (the "License"). You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0.
6+
*
7+
* See the NOTICE file distributed with this work for additional information
8+
* regarding copyright ownership. Unless required by applicable law or agreed
9+
* to in writing, software distributed under the License is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions
12+
* and limitations under the License.
13+
*/
14+
15+
using MorganStanley.ComposeUI.LayoutPersistence.Abstractions;
16+
17+
namespace MorganStanley.ComposeUI.LayoutPersistence;
18+
19+
public class LayoutManager
20+
{
21+
private readonly ILayoutPersistence _layoutPersistence;
22+
private readonly ILayoutSerializer _serializer;
23+
24+
public LayoutManager(ILayoutPersistence layoutPersistence, ILayoutSerializer serializer)
25+
{
26+
_layoutPersistence = layoutPersistence;
27+
_serializer = serializer;
28+
}
29+
30+
public async Task SaveLayoutAsync<T>(T layoutObject, string layoutName)
31+
{
32+
var serializedData = await _serializer.SerializeAsync(layoutObject);
33+
await _layoutPersistence.SaveLayoutAsync(serializedData, layoutName);
34+
}
35+
36+
public async Task<T?> LoadLayoutAsync<T>(string layoutName)
37+
{
38+
var serializedData = await _layoutPersistence.LoadLayoutAsync(layoutName);
39+
return await _serializer.DeserializeAsync<T>(serializedData);
40+
}
41+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Morgan Stanley makes this available to you under the Apache License,
3+
* Version 2.0 (the "License"). You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0.
6+
*
7+
* See the NOTICE file distributed with this work for additional information
8+
* regarding copyright ownership. Unless required by applicable law or agreed
9+
* to in writing, software distributed under the License is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions
12+
* and limitations under the License.
13+
*/
14+
15+
using System.Text;
16+
using System.Text.Json;
17+
using MorganStanley.ComposeUI.LayoutPersistence.Abstractions;
18+
19+
namespace MorganStanley.ComposeUI.LayoutPersistence.Serializers;
20+
21+
public class JsonLayoutSerializer : ILayoutSerializer
22+
{
23+
private readonly JsonSerializerOptions? _jsonSerializerOptions;
24+
25+
public JsonLayoutSerializer(JsonSerializerOptions? jsonSerializerOptions = null)
26+
{
27+
_jsonSerializerOptions = jsonSerializerOptions;
28+
}
29+
30+
public async Task<string> SerializeAsync<T>(T layoutObject, CancellationToken cancellationToken = default)
31+
{
32+
if (layoutObject == null)
33+
{
34+
throw new ArgumentNullException(nameof(layoutObject), "The layout object to serialize cannot be null.");
35+
}
36+
37+
using var stream = new MemoryStream();
38+
await JsonSerializer.SerializeAsync(stream, layoutObject, _jsonSerializerOptions, cancellationToken);
39+
40+
return Encoding.UTF8.GetString(stream.ToArray());
41+
}
42+
43+
public async Task<T?> DeserializeAsync<T>(string layoutData, CancellationToken cancellationToken = default)
44+
{
45+
if (string.IsNullOrWhiteSpace(layoutData))
46+
{
47+
throw new ArgumentException("The layout data cannot be null or empty.", nameof(layoutData));
48+
}
49+
50+
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(layoutData));
51+
return await JsonSerializer.DeserializeAsync<T>(stream, _jsonSerializerOptions, cancellationToken);
52+
}
53+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Morgan Stanley makes this available to you under the Apache License,
3+
* Version 2.0 (the "License"). You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0.
6+
*
7+
* See the NOTICE file distributed with this work for additional information
8+
* regarding copyright ownership. Unless required by applicable law or agreed
9+
* to in writing, software distributed under the License is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions
12+
* and limitations under the License.
13+
*/
14+
15+
using MorganStanley.ComposeUI.LayoutPersistence.Abstractions;
16+
using System.Text;
17+
using System.Text.Json;
18+
using System.Xml.Serialization;
19+
20+
namespace MorganStanley.ComposeUI.LayoutPersistence.Serializers;
21+
22+
public class XmlLayoutSerializer : ILayoutSerializer
23+
{
24+
public async Task<string> SerializeAsync<T>(T layoutObject, CancellationToken cancellationToken = default)
25+
{
26+
if (layoutObject == null)
27+
{
28+
throw new ArgumentNullException(nameof(layoutObject), "The layout object to serialize cannot be null.");
29+
}
30+
31+
using var stream = new MemoryStream();
32+
var serializer = new XmlSerializer(typeof(T));
33+
34+
using (var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true))
35+
{
36+
serializer.Serialize(writer, layoutObject);
37+
await writer.FlushAsync();
38+
}
39+
40+
cancellationToken.ThrowIfCancellationRequested();
41+
42+
return Encoding.UTF8.GetString(stream.ToArray());
43+
}
44+
45+
public async Task<T?> DeserializeAsync<T>(string layoutData, CancellationToken cancellationToken = default)
46+
{
47+
if (string.IsNullOrWhiteSpace(layoutData))
48+
{
49+
throw new ArgumentException("The layout data cannot be null or empty.", nameof(layoutData));
50+
}
51+
52+
byte[] dataBytes = Encoding.UTF8.GetBytes(layoutData);
53+
54+
using var stream = new MemoryStream(dataBytes);
55+
56+
cancellationToken.ThrowIfCancellationRequested();
57+
58+
var serializer = new XmlSerializer(typeof(T));
59+
60+
return await Task.Run(() =>
61+
{
62+
return (T?) serializer.Deserialize(stream);
63+
}, cancellationToken);
64+
}
65+
}

0 commit comments

Comments
 (0)