Skip to content

Commit 7c89122

Browse files
authored
Basic layout persistence implementation (#755)
1 parent de6689a commit 7c89122

File tree

13 files changed

+605
-0
lines changed

13 files changed

+605
-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<T>
18+
{
19+
Task SaveLayoutAsync(string layoutName, T layoutData, CancellationToken cancellationToken = default);
20+
Task<T?> 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<T>
18+
{
19+
Task<string> SerializeAsync(T layoutObject, CancellationToken cancellationToken = default);
20+
Task<T?> DeserializeAsync(string layoutData, CancellationToken cancellationToken = default);
21+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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<T> : ILayoutPersistence<T>
20+
{
21+
private readonly string _basePath;
22+
private readonly ILayoutSerializer<T> _serializer;
23+
private readonly SemaphoreSlim _semaphore = new(1,1);
24+
25+
public FileLayoutPersistence(string basePath, ILayoutSerializer<T> serializer)
26+
{
27+
_basePath = NormalizeFilePath(basePath);
28+
_serializer = serializer;
29+
30+
if (!Directory.Exists(_basePath))
31+
{
32+
Directory.CreateDirectory(_basePath);
33+
}
34+
}
35+
36+
public async Task SaveLayoutAsync(string layoutName, T layoutData, CancellationToken cancellationToken = default)
37+
{
38+
var filePath = GetFilePath(layoutName);
39+
40+
await _semaphore.WaitAsync(cancellationToken);
41+
42+
try
43+
{
44+
var serializedData = await _serializer.SerializeAsync(layoutData, cancellationToken);
45+
await File.WriteAllTextAsync(filePath, serializedData, cancellationToken);
46+
}
47+
finally
48+
{
49+
_semaphore.Release();
50+
}
51+
}
52+
53+
public async Task<T?> LoadLayoutAsync(string layoutName, CancellationToken cancellationToken = default)
54+
{
55+
var filePath = GetFilePath(layoutName);
56+
57+
if (!File.Exists(filePath))
58+
{
59+
throw new FileNotFoundException($"Layout file not found: {filePath}");
60+
}
61+
62+
await _semaphore.WaitAsync(cancellationToken);
63+
64+
try
65+
{
66+
var serializedData = await File.ReadAllTextAsync(filePath, cancellationToken);
67+
return await _serializer.DeserializeAsync(serializedData, cancellationToken);
68+
}
69+
finally
70+
{
71+
_semaphore.Release();
72+
}
73+
}
74+
75+
private string GetFilePath(string layoutName)
76+
{
77+
var combinedPath = Path.Combine(_basePath, $"{layoutName}.layout");
78+
var fullPath = Path.GetFullPath(combinedPath);
79+
80+
if (!fullPath.StartsWith(_basePath, StringComparison.OrdinalIgnoreCase))
81+
{
82+
throw new ArgumentException("Invalid layoutName argument. File cannot be saved outside of the base directory.", layoutName);
83+
}
84+
85+
return fullPath;
86+
}
87+
88+
private static string NormalizeFilePath(string path)
89+
{
90+
if (path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
91+
{
92+
var normalizedPath = Uri.UnescapeDataString(path[7..]);
93+
return Path.GetFullPath(normalizedPath);
94+
}
95+
96+
return Path.GetFullPath(path);
97+
}
98+
}
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<T> : ILayoutSerializer<T>
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 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(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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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.Xml.Serialization;
18+
19+
namespace MorganStanley.ComposeUI.LayoutPersistence.Serializers;
20+
21+
public class XmlLayoutSerializer<T> : ILayoutSerializer<T>
22+
{
23+
public async Task<string> SerializeAsync(T layoutObject, CancellationToken cancellationToken = default)
24+
{
25+
if (layoutObject == null)
26+
{
27+
throw new ArgumentNullException(nameof(layoutObject), "The layout object to serialize cannot be null.");
28+
}
29+
30+
using var stream = new MemoryStream();
31+
var serializer = new XmlSerializer(typeof(T));
32+
33+
using (var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true))
34+
{
35+
serializer.Serialize(writer, layoutObject);
36+
await writer.FlushAsync();
37+
}
38+
39+
cancellationToken.ThrowIfCancellationRequested();
40+
41+
return Encoding.UTF8.GetString(stream.ToArray());
42+
}
43+
44+
public async Task<T?> DeserializeAsync(string layoutData, CancellationToken cancellationToken = default)
45+
{
46+
if (string.IsNullOrWhiteSpace(layoutData))
47+
{
48+
throw new ArgumentException("The layout data cannot be null or empty.", nameof(layoutData));
49+
}
50+
51+
var dataBytes = Encoding.UTF8.GetBytes(layoutData);
52+
53+
using var stream = new MemoryStream(dataBytes);
54+
55+
cancellationToken.ThrowIfCancellationRequested();
56+
57+
var serializer = new XmlSerializer(typeof(T));
58+
59+
return await Task.FromResult((T?)serializer.Deserialize(stream));
60+
}
61+
}

0 commit comments

Comments
 (0)