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

[2.x] Merge prop #23

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
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
6 changes: 5 additions & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 9.0.x
dotnet-version: |
6.0.x
7.0.x
8.0.x
9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand Down
6 changes: 6 additions & 0 deletions InertiaCore/Inertia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ public static class Inertia

public static AlwaysProp Always(Func<Task<object?>> callback) => _factory.Always(callback);

public static MergeProp Merge(object? value) => _factory.Merge(value);

public static MergeProp Merge(Func<object?> callback) => _factory.Merge(callback);

public static MergeProp Merge(Func<Task<object?>> callback) => _factory.Merge(callback);

public static LazyProp Lazy(Func<object?> callback) => _factory.Lazy(callback);

public static LazyProp Lazy(Func<Task<object?>> callback) => _factory.Lazy(callback);
Expand Down
5 changes: 5 additions & 0 deletions InertiaCore/Models/Page.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;

namespace InertiaCore.Models;

internal class Page
Expand All @@ -6,4 +8,7 @@ internal class Page
public string Component { get; set; } = default!;
public string? Version { get; set; }
public string Url { get; set; } = default!;

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<string>? MergeProps { get; set; }
}
25 changes: 25 additions & 0 deletions InertiaCore/Props/MergeProp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using InertiaCore.Props;

namespace InertiaCore.Utils;

public class MergeProp : InvokableProp, Mergeable
{
public bool merge { get; set; } = true;

public MergeProp(object? value) : base(value)
{
merge = true;
}

internal MergeProp(Func<object?> value) : base(value)
{
merge = true;
}

internal MergeProp(Func<Task<object?>> value) : base(value)
{
merge = true;
}
}


35 changes: 35 additions & 0 deletions InertiaCore/Response.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ protected internal async Task ProcessResponse()
.ToDictionary(o => o.Name.ToCamelCase(), o => o.GetValue(_props)))
};

page.MergeProps = ResolveMergeProps(page.Props);

var shared = _context!.HttpContext.Features.Get<InertiaSharedData>();
if (shared != null)
page.Props = shared.GetMerged(page.Props);
Expand All @@ -59,6 +61,7 @@ protected internal async Task ProcessResponse()
Func<object?> f => (pair.Key, f.Invoke()),
LazyProp l => (pair.Key, await l.Invoke()),
AlwaysProp l => (pair.Key, await l.Invoke()),
MergeProp m => (pair.Key, await m.Invoke()),
_ => (pair.Key, pair.Value)
}))).ToDictionary(pair => pair.Key, pair => pair.Item2);
}
Expand Down Expand Up @@ -155,4 +158,36 @@ public Response WithViewData(IDictionary<string, object> viewData)
.Where(kv => kv.Value is not AlwaysProp)
.Concat(alwaysProps).ToDictionary(kv => kv.Key, kv => kv.Value);
}

private List<string>? ResolveMergeProps(Dictionary<string, object?> props)
{
// Parse the "RESET" header into a collection of keys to reset
var resetProps = new HashSet<string>(
_context!.HttpContext.Request.Headers[InertiaHeader.Reset]
.ToString()
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim()),
StringComparer.OrdinalIgnoreCase
);

var resolvedProps = props
.Select(kv => kv.Key.ToCamelCase()) // Convert property name to camelCase
.ToList();

// Filter the props that are Mergeable and should be merged
var mergeProps = _props.GetType().GetProperties().ToDictionary(o => o.Name.ToCamelCase(), o => o.GetValue(_props))
.Where(kv => kv.Value is Mergeable mergeable && mergeable.ShouldMerge()) // Check if value is Mergeable and should merge
.Where(kv => !resetProps.Contains(kv.Key)) // Exclude reset keys
.Select(kv => kv.Key.ToCamelCase()) // Convert property name to camelCase
.Where(resolvedProps.Contains) // Filter only the props that are in the resolved props
.ToList();

if (mergeProps.Count == 0)
{
return null;
}

// Return the result
return mergeProps;
}
}
6 changes: 6 additions & 0 deletions InertiaCore/ResponseFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ internal interface IResponseFactory
public AlwaysProp Always(Func<Task<object?>> callback);
public LazyProp Lazy(Func<object?> callback);
public LazyProp Lazy(Func<Task<object?>> callback);
public MergeProp Merge(object? value);
public MergeProp Merge(Func<object?> callback);
public MergeProp Merge(Func<Task<object?>> callback);
}

internal class ResponseFactory : IResponseFactory
Expand Down Expand Up @@ -127,4 +130,7 @@ public void Share(IDictionary<string, object?> data)
public AlwaysProp Always(object? value) => new AlwaysProp(value);
public AlwaysProp Always(Func<object?> callback) => new AlwaysProp(callback);
public AlwaysProp Always(Func<Task<object?>> callback) => new AlwaysProp(callback);
public MergeProp Merge(object? value) => new MergeProp(value);
public MergeProp Merge(Func<object?> callback) => new MergeProp(callback);
public MergeProp Merge(Func<Task<object?>> callback) => new MergeProp(callback);
}
2 changes: 2 additions & 0 deletions InertiaCore/Utils/InertiaHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ public static class InertiaHeader
public const string PartialOnly = "X-Inertia-Partial-Data";

public const string PartialExcept = "X-Inertia-Partial-Except";

public const string Reset = "X-Inertia-Reset";
}
15 changes: 15 additions & 0 deletions InertiaCore/Utils/Mergeable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace InertiaCore.Utils;

public interface Mergeable
{
public bool merge { get; set; }

public Mergeable Merge()
{
merge = true;

return this;
}

public bool ShouldMerge() => merge;
}
207 changes: 207 additions & 0 deletions InertiaCoreTests/UnitTestMergeData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using InertiaCore.Models;
using Microsoft.AspNetCore.Http;

namespace InertiaCoreTests;

public partial class Tests
{
[Test]
[Description("Test if the merge data is fetched properly.")]
public async Task TestMergeData()
{
var response = _factory.Render("Test/Page", new
{
Test = "Test",
TestFunc = new Func<string>(() => "Func"),
TestMerge = _factory.Merge(() =>
{
return "Merge";
})
});

var context = PrepareContext();

response.SetContext(context);
await response.ProcessResponse();

var page = response.GetJson().Value as Page;

Assert.That(page?.Props, Is.EqualTo(new Dictionary<string, object?>
{
{ "test", "Test" },
{ "testFunc", "Func" },
{ "testMerge", "Merge" },
{ "errors", new Dictionary<string, string>(0) }
}));
Assert.That(page?.MergeProps, Is.EqualTo(new List<string> { "testMerge" }));
}

[Test]
[Description("Test if the merge data is fetched properly with specified partial props.")]
public async Task TestMergePartialData()
{
var response = _factory.Render("Test/Page", new
{
TestFunc = new Func<string>(() => "Func"),
TestMerge = _factory.Merge(() => "Merge")
});

var headers = new HeaderDictionary
{
{ "X-Inertia-Partial-Data", "testFunc,testMerge" },
{ "X-Inertia-Partial-Component", "Test/Page" }
};

var context = PrepareContext(headers);

response.SetContext(context);
await response.ProcessResponse();

var page = response.GetJson().Value as Page;

Assert.That(page?.Props, Is.EqualTo(new Dictionary<string, object?>
{
{ "testFunc", "Func" },
{ "testMerge", "Merge" },
{ "errors", new Dictionary<string, string>(0) }
}));

Assert.That(page?.MergeProps, Is.EqualTo(new List<string> { "testMerge" }));
}

[Test]
[Description("Test if the merge async data is fetched properly.")]
public async Task TestMergeAsyncData()
{
var testFunction = new Func<Task<object?>>(async () =>
{
await Task.Delay(100);
return "Merge Async";
});

var response = _factory.Render("Test/Page", new
{
Test = "Test",
TestFunc = new Func<string>(() => "Func"),
TestMerge = _factory.Merge(testFunction)
});

var context = PrepareContext();

response.SetContext(context);
await response.ProcessResponse();

var page = response.GetJson().Value as Page;

Assert.That(page?.Props, Is.EqualTo(new Dictionary<string, object?>
{
{ "test", "Test" },
{ "testFunc", "Func" },
{ "testMerge", "Merge Async" },
{ "errors", new Dictionary<string, string>(0) }
}));
Assert.That(page?.MergeProps, Is.EqualTo(new List<string> { "testMerge" }));
}

[Test]
[Description("Test if the merge async data is fetched properly with specified partial props.")]
public async Task TestMergeAsyncPartialData()
{
var testFunction = new Func<Task<string>>(async () =>
{
await Task.Delay(100);
return "Merge Async";
});

var response = _factory.Render("Test/Page", new
{
TestFunc = new Func<string>(() => "Func"),
TestMerge = _factory.Merge(async () => await testFunction())
});

var headers = new HeaderDictionary
{
{ "X-Inertia-Partial-Data", "testFunc,testMerge" },
{ "X-Inertia-Partial-Component", "Test/Page" }
};

var context = PrepareContext(headers);

response.SetContext(context);
await response.ProcessResponse();

var page = response.GetJson().Value as Page;

Assert.That(page?.Props, Is.EqualTo(new Dictionary<string, object?>
{
{ "testFunc", "Func" },
{ "testMerge", "Merge Async" },
{ "errors", new Dictionary<string, string>(0) }
}));

Assert.That(page?.MergeProps, Is.EqualTo(new List<string> { "testMerge" }));
}

[Test]
[Description("Test if the merge async data is fetched properly without specified partial props.")]
public async Task TestMergeAsyncPartialDataOmitted()
{
var testFunction = new Func<Task<string>>(async () =>
{
await Task.Delay(100);
return "Merge Async";
});

var response = _factory.Render("Test/Page", new
{
TestFunc = new Func<string>(() => "Func"),
TestMerge = _factory.Merge(async () => await testFunction())
});

var headers = new HeaderDictionary
{
{ "X-Inertia-Partial-Data", "testFunc" },
{ "X-Inertia-Partial-Component", "Test/Page" }
};

var context = PrepareContext(headers);

response.SetContext(context);
await response.ProcessResponse();

var page = response.GetJson().Value as Page;

Assert.That(page?.Props, Is.EqualTo(new Dictionary<string, object?>
{
{ "testFunc", "Func" },
{ "errors", new Dictionary<string, string>(0) }
}));

Assert.That(page?.MergeProps, Is.EqualTo(null));
}

public async Task TestNoMergeProps()
{
var response = _factory.Render("Test/Page", new
{
Test = "Test",
TestFunc = new Func<string>(() => "Func"),
});

var context = PrepareContext();

response.SetContext(context);
await response.ProcessResponse();

var page = response.GetJson().Value as Page;

Assert.That(page?.Props, Is.EqualTo(new Dictionary<string, object?>
{
{ "test", "Test" },
{ "testFunc", "Func" },
{ "errors", new Dictionary<string, string>(0) }
}));
Assert.That(page?.MergeProps, Is.EqualTo(null));
}

}
Loading
Loading