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

Use protobuf for serialization needs #107

Merged
merged 4 commits into from
Dec 29, 2023
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
2 changes: 1 addition & 1 deletion LiftLog.App/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public static MauiApp CreateMauiApp()
builder.Services.AddSingleton<IKeyValueStore, AppDataFileStorageKeyValueStore>();
builder.Services.AddSingleton<IPreferenceStore, SecureStoragePreferenceStore>();
builder.Services.AddScoped<INotificationService, MauiNotificationService>();
builder.Services.AddScoped<ITextExporter, MauiShareTextExporter>();
builder.Services.AddScoped<IExporter, MauiShareExporter>();

builder.Services.AddSingleton(new HttpClient());
builder.Services.AddScoped<IAiWorkoutPlanner, ApiBasedAiWorkoutPlanner>();
Expand Down
17 changes: 17 additions & 0 deletions LiftLog.App/Services/AppDataFileStorageKeyValueStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,28 @@ public class AppDataFileStorageKeyValueStore : IKeyValueStore
return null;
}

public async ValueTask<byte[]?> GetItemBytesAsync(string key)
{
var fileName = GetFileName(key);
var exists = File.Exists(fileName);
if (exists)
{
var content = await File.ReadAllBytesAsync(fileName);
return content;
}
return null;
}

public async ValueTask SetItemAsync(string key, string value)
{
await File.WriteAllTextAsync(GetFileName(key), value);
}

public async ValueTask SetItemAsync(string key, byte[] value)
{
await File.WriteAllBytesAsync(GetFileName(key), value);
}

private string GetFileName(string key)
{
var dataDir = FileSystem.Current.AppDataDirectory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,35 @@

namespace LiftLog.App.Services;

public class MauiShareTextExporter(IShare share, IFilePicker filePicker) : ITextExporter
public class MauiShareExporter(IShare share, IFilePicker filePicker) : IExporter
{
public async Task ExportTextAsync(string text)
public async Task ExportBytesAsync(byte[] bytes)
{
string fileName = "liftlog-export.json.gz";
string fileName = "export.liftlogbackup.gz";
string file = Path.Combine(FileSystem.CacheDirectory, fileName);

using (FileStream stream = File.Create(file))
using (GZipStream gzip = new(stream, CompressionMode.Compress))
using (StreamWriter writer = new(gzip))
{
await writer.WriteAsync(text);
}
await gzip.WriteAsync(bytes);

await share.RequestAsync(
new ShareFileRequest { Title = "Export Data", File = new ShareFile(file) }
);
}

public async Task<string> ImportTextAsync()
public async Task<byte[]> ImportBytesAsync()
{
var file = await filePicker.PickAsync();

if (file == null)
{
return "";
return Array.Empty<byte>();
}

using FileStream stream = File.OpenRead(file.FullPath);
using GZipStream gzip = new(stream, CompressionMode.Decompress);
using StreamReader reader = new(gzip);
return await reader.ReadToEndAsync();
using MemoryStream memoryStream = new();
await gzip.CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
}
21 changes: 21 additions & 0 deletions LiftLog.Tests/Serialization/SerializationTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json;
using FluentAssertions;
using Google.Protobuf;
using LiftLog.Lib.Models;
using LiftLog.Ui.Models.SessionHistoryDao;

Expand Down Expand Up @@ -32,4 +33,24 @@ public void SessionHistoryDaoV1_SerializeDeserialize_RoundTrip()
deserialized!.Should().Be(sessionHistoryDao);
deserialized!.ToModel().CompletedSessions.First().Value.Should().Be(session);
}

[Fact]
public void SessionHistoryDaoV2_SerializeDeserialize_RoundTrip()
{
var session = Sessions.CreateSession();
var sessionHistoryDao = SessionHistoryDaoV2.FromModel(
new SessionHistoryDaoContainer(
CompletedSessions: new Dictionary<Guid, Session>
{
{ session.Id, session }
}.ToImmutableDictionary()
)
);

var serialized = sessionHistoryDao.ToByteArray();
var deserialized = SessionHistoryDaoV2.Parser.ParseFrom(serialized);

deserialized!.Should().Be(sessionHistoryDao);
deserialized!.ToModel().CompletedSessions.First().Value.Should().Be(session);
}
}
15 changes: 15 additions & 0 deletions LiftLog.Ui/LiftLog.Ui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<!-- Protobuf generates code with lower case types -->
<NoWarn>CS8981;</NoWarn>
</PropertyGroup>

<PropertyGroup>
Expand All @@ -24,6 +26,12 @@
<PackageReference Include="Fluxor" Version="5.9.1" />
<PackageReference Include="Fluxor.Blazor.Web" Version="5.9.1" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

<PackageReference Include="Google.Protobuf" Version="3.18.0" />
<PackageReference Include="Grpc.Tools" Version="2.40.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>


Expand All @@ -47,4 +55,11 @@
<ProjectReference Include="..\LiftLog.Lib\LiftLog.Lib.csproj" />
</ItemGroup>

<ItemGroup>
<Protobuf Include="Models\Utils.proto" GrpcServices="None" AdditionalProtocArguments="--csharp_opt=internal_access=true"/>
<Protobuf Include="Models\SessionHistoryDao\SessionHistoryDaoV2.proto" GrpcServices="None" AdditionalProtocArguments="--csharp_opt=internal_access=true"/>
<Protobuf Include="Models\SessionBlueprintDao\SessionBlueprintDaoV2.proto" GrpcServices="None" AdditionalProtocArguments="--csharp_opt=internal_access=true"/>
<Protobuf Include="Models\SettingStorageDao\SettingsStorageDaoV2.proto" GrpcServices="None" AdditionalProtocArguments="--csharp_opt=internal_access=true"/>
<Protobuf Include="Models\CurrentSessionStateDao\CurrentSessionStateDaoV2.proto" GrpcServices="None" AdditionalProtocArguments="--csharp_opt=internal_access=true"/>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace LiftLog.Ui.Models.CurrentSessionStateDao;

using LiftLog.Ui.Models.SessionHistoryDao;
using LiftLog.Ui.Store.CurrentSession;

internal partial class CurrentSessionStateDaoV2
{
public CurrentSessionStateDaoV2(
SessionDaoV2? workoutSession,
SessionDaoV2? historySession,
UUIDDao? latestSetTimerNotificationId
)
{
WorkoutSession = workoutSession;
HistorySession = historySession;
LatestSetTimerNotificationId = latestSetTimerNotificationId;
}

public static CurrentSessionStateDaoV2 FromModel(CurrentSessionState model) =>
new(
model.WorkoutSession is null ? null : SessionDaoV2.FromModel(model.WorkoutSession),
model.HistorySession is null ? null : SessionDaoV2.FromModel(model.HistorySession),
model.LatestSetTimerNotificationId
);

public CurrentSessionState ToModel() =>
new(WorkoutSession?.ToModel(), HistorySession?.ToModel(), LatestSetTimerNotificationId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
syntax = "proto3";
package LiftLog.Ui.Models.CurrentSessionStateDao;

import "Models/Utils.proto";
import "Models/SessionHistoryDao/SessionHistoryDaoV2.proto";

message CurrentSessionStateDaoV2 {
optional LiftLog.Ui.Models.SessionHistoryDao.SessionDaoV2 workout_session = 1;
optional LiftLog.Ui.Models.SessionHistoryDao.SessionDaoV2 history_session = 2;
optional LiftLog.Ui.Models.UUIDDao latest_set_timer_notification_id = 3;
}
102 changes: 102 additions & 0 deletions LiftLog.Ui/Models/SessionBlueprintDao/SessionBlueprintDaoV2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System.Collections.Immutable;
using Google.Protobuf.WellKnownTypes;
using LiftLog.Lib;

namespace LiftLog.Ui.Models.SessionBlueprintDao;

internal partial class SessionBlueprintContainerDaoV2
{
public SessionBlueprintContainerDaoV2(IEnumerable<SessionBlueprintDaoV2> sessions)
{
SessionBlueprints.AddRange(sessions);
}

public static SessionBlueprintContainerDaoV2 FromModel(
ImmutableList<Lib.Models.SessionBlueprint> model
) => new(model.Select(SessionBlueprintDaoV2.FromModel));

public ImmutableListValue<Lib.Models.SessionBlueprint> ToModel() =>
SessionBlueprints.Select(x => x.ToModel()).ToImmutableList();
}

internal partial class SessionBlueprintDaoV2
{
public SessionBlueprintDaoV2(
string name,
IEnumerable<ExerciseBlueprintDaoV2> exerciseBlueprints
)
{
Name = name;
ExerciseBlueprints.AddRange(exerciseBlueprints);
}

public static SessionBlueprintDaoV2 FromModel(Lib.Models.SessionBlueprint model) =>
new(model.Name, model.Exercises.Select(ExerciseBlueprintDaoV2.FromModel).ToImmutableList());

public Lib.Models.SessionBlueprint ToModel() =>
new(Name, ExerciseBlueprints.Select(x => x.ToModel()).ToImmutableList());
}

internal partial class ExerciseBlueprintDaoV2
{
public ExerciseBlueprintDaoV2(
string name,
int sets,
int repsPerSet,
DecimalValue initialWeight,
DecimalValue weightIncreaseOnSuccess,
RestDaoV2 restBetweenSets,
bool supersetWithNext
)
{
Name = name;
Sets = sets;
RepsPerSet = repsPerSet;
InitialWeight = initialWeight;
WeightIncreaseOnSuccess = weightIncreaseOnSuccess;
RestBetweenSets = restBetweenSets;
SupersetWithNext = supersetWithNext;
}

public static ExerciseBlueprintDaoV2 FromModel(Lib.Models.ExerciseBlueprint model) =>
new(
model.Name,
model.Sets,
model.RepsPerSet,
model.InitialWeight,
model.WeightIncreaseOnSuccess,
RestDaoV2.FromModel(model.RestBetweenSets),
model.SupersetWithNext
);

public Lib.Models.ExerciseBlueprint ToModel() =>
new(
Name,
Sets,
RepsPerSet,
InitialWeight,
WeightIncreaseOnSuccess,
RestBetweenSets.ToModel(),
SupersetWithNext
);
}

internal partial class RestDaoV2
{
public RestDaoV2(Duration minRest, Duration maxRest, Duration failureRest)
{
MinRest = minRest;
MaxRest = maxRest;
FailureRest = failureRest;
}

public static RestDaoV2 FromModel(Lib.Models.Rest model) =>
new(
Duration.FromTimeSpan(model.MinRest),
Duration.FromTimeSpan(model.MaxRest),
Duration.FromTimeSpan(model.FailureRest)
);

public Lib.Models.Rest ToModel() =>
new(MinRest.ToTimeSpan(), MaxRest.ToTimeSpan(), FailureRest.ToTimeSpan());
}
31 changes: 31 additions & 0 deletions LiftLog.Ui/Models/SessionBlueprintDao/SessionBlueprintDaoV2.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
syntax = "proto3";
package LiftLog.Ui.Models.SessionBlueprintDao;

import "Models/Utils.proto";
import "google/protobuf/duration.proto";

message SessionBlueprintContainerDaoV2 {
repeated SessionBlueprintDaoV2 session_blueprints = 1;
}

message SessionBlueprintDaoV2 {
string name = 1;
repeated ExerciseBlueprintDaoV2 exercise_blueprints = 2;
}


message ExerciseBlueprintDaoV2 {
string name = 1;
int32 sets = 2;
int32 reps_per_set = 3;
LiftLog.Ui.Models.DecimalValue initial_weight = 4;
LiftLog.Ui.Models.DecimalValue weight_increase_on_success = 5;
RestDaoV2 rest_between_sets = 6;
bool superset_with_next = 7;
}

message RestDaoV2 {
google.protobuf.Duration min_rest = 1;
google.protobuf.Duration max_rest = 2;
google.protobuf.Duration failure_rest = 3;
}
Loading
Loading