Skip to content

Commit

Permalink
[1.x] Properly resolve nested properties (#32)
Browse files Browse the repository at this point in the history
* Resolve nested dictionaries

* Revert changing the shared props mechanic, rename shared data to props

* Fix a typo

* Update dotnet.yml
  • Loading branch information
kapi2289 authored Feb 1, 2025
1 parent fb027eb commit cfc5968
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 145 deletions.
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
44 changes: 19 additions & 25 deletions InertiaCore/Extensions/InertiaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,6 @@ namespace InertiaCore.Extensions;

internal static class InertiaExtensions
{
internal static Dictionary<string, object?> OnlyProps(this ActionContext context, Dictionary<string, object?> props)
{
var onlyKeys = context.HttpContext.Request.Headers[InertiaHeader.PartialOnly]
.ToString().Split(',')
.Select(k => k.Trim())
.Where(k => !string.IsNullOrEmpty(k))
.ToList();

return props.Where(kv => onlyKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase))
.ToDictionary(kv => kv.Key, kv => kv.Value);
}

internal static Dictionary<string, object?> ExceptProps(this ActionContext context,
Dictionary<string, object?> props)
{
var exceptKeys = context.HttpContext.Request.Headers[InertiaHeader.PartialExcept]
.ToString().Split(',')
.Select(k => k.Trim())
.Where(k => !string.IsNullOrEmpty(k))
.ToList();

return props.Where(kv => exceptKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase) == false)
.ToDictionary(kv => kv.Key, kv => kv.Value);
}

internal static bool IsInertiaPartialComponent(this ActionContext context, string component) =>
context.HttpContext.Request.Headers[InertiaHeader.PartialComponent] == component;

Expand All @@ -55,4 +30,23 @@ internal static bool Override<TKey, TValue>(this IDictionary<TKey, TValue> dicti

return true;
}

internal static Task<object?> ResolveAsync(this Func<object?> func)
{
var rt = func.Method.ReturnType;

if (!rt.IsGenericType || rt.GetGenericTypeDefinition() != typeof(Task<>))
return Task.Run(func.Invoke);

var task = func.DynamicInvoke() as Task;
return task!.ResolveResult();
}

internal static async Task<object?> ResolveResult(this Task task)
{
await task.ConfigureAwait(false);
var result = task.GetType().GetProperty("Result");

return result?.GetValue(task);
}
}
8 changes: 5 additions & 3 deletions InertiaCore/Props/InvokableProp.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using InertiaCore.Extensions;

namespace InertiaCore.Props;

public class InvokableProp
Expand All @@ -10,9 +12,9 @@ public class InvokableProp
{
return _value switch
{
Func<Task<object?>> asyncCallable => asyncCallable.Invoke(),
Func<object?> callable => Task.Run(() => callable.Invoke()),
Task<object?> value => value,
Func<object?> f => f.ResolveAsync(),
Task t => t.ResolveResult(),
InvokableProp p => p.Invoke(),
_ => Task.FromResult(_value)
};
}
Expand Down
181 changes: 120 additions & 61 deletions InertiaCore/Response.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,54 +13,157 @@ namespace InertiaCore;
public class Response : IActionResult
{
private readonly string _component;
private readonly object _props;
private readonly Dictionary<string, object?> _props;
private readonly string _rootView;
private readonly string? _version;

private ActionContext? _context;
private Page? _page;
private IDictionary<string, object>? _viewData;

public Response(string component, object props, string rootView, string? version)
internal Response(string component, Dictionary<string, object?> props, string rootView, string? version)
=> (_component, _props, _rootView, _version) = (component, props, rootView, version);

public async Task ExecuteResultAsync(ActionContext context)
{
SetContext(context);
await ProcessResponse();

await GetResult().ExecuteResultAsync(_context!);
}

protected internal async Task ProcessResponse()
{
var props = await ResolveProperties();

var page = new Page
{
Component = _component,
Version = _version,
Url = _context!.RequestedUri(),
Props = await ResolveProperties(_props.GetType().GetProperties()
.ToDictionary(o => o.Name.ToCamelCase(), o => o.GetValue(_props)))
Props = props
};

var shared = _context!.HttpContext.Features.Get<InertiaSharedData>();
if (shared != null)
page.Props = shared.GetMerged(page.Props);

page.Props["errors"] = GetErrors();

SetPage(page);
}

private static async Task<Dictionary<string, object?>> PrepareProps(Dictionary<string, object?> props)
/// <summary>
/// Resolve the properties for the response.
/// </summary>
private async Task<Dictionary<string, object?>> ResolveProperties()
{
var props = _props;

props = ResolveSharedProps(props);
props = ResolvePartialProperties(props);
props = ResolveAlways(props);
props = await ResolvePropertyInstances(props);

return props;
}

/// <summary>
/// Resolve `shared` props stored in the current request context.
/// </summary>
private Dictionary<string, object?> ResolveSharedProps(Dictionary<string, object?> props)
{
var shared = _context!.HttpContext.Features.Get<InertiaSharedProps>();
if (shared != null)
props = shared.GetMerged(props);

return props;
}

/// <summary>
/// Resolve the `only` and `except` partial request props.
/// </summary>
private Dictionary<string, object?> ResolvePartialProperties(Dictionary<string, object?> props)
{
var isPartial = _context!.IsInertiaPartialComponent(_component);

if (!isPartial)
return props
.Where(kv => kv.Value is not LazyProp)
.ToDictionary(kv => kv.Key, kv => kv.Value);

props = props.ToDictionary(kv => kv.Key, kv => kv.Value);

if (_context!.HttpContext.Request.Headers.ContainsKey(InertiaHeader.PartialOnly))
props = ResolveOnly(props);

if (_context!.HttpContext.Request.Headers.ContainsKey(InertiaHeader.PartialExcept))
props = ResolveExcept(props);

return props;
}

/// <summary>
/// Resolve the `only` partial request props.
/// </summary>
private Dictionary<string, object?> ResolveOnly(Dictionary<string, object?> props)
{
var onlyKeys = _context!.HttpContext.Request.Headers[InertiaHeader.PartialOnly]
.ToString().Split(',')
.Select(k => k.Trim())
.Where(k => !string.IsNullOrEmpty(k))
.ToList();

return props.Where(kv => onlyKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase))
.ToDictionary(kv => kv.Key, kv => kv.Value);
}

/// <summary>
/// Resolve the `except` partial request props.
/// </summary>
private Dictionary<string, object?> ResolveExcept(Dictionary<string, object?> props)
{
var exceptKeys = _context!.HttpContext.Request.Headers[InertiaHeader.PartialExcept]
.ToString().Split(',')
.Select(k => k.Trim())
.Where(k => !string.IsNullOrEmpty(k))
.ToList();

return props.Where(kv => exceptKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase) == false)
.ToDictionary(kv => kv.Key, kv => kv.Value);
}

/// <summary>
/// Resolve `always` properties that should always be included on all visits, regardless of "only" or "except" requests.
/// </summary>
private Dictionary<string, object?> ResolveAlways(Dictionary<string, object?> props)
{
var alwaysProps = _props.Where(o => o.Value is AlwaysProp);

return props
.Where(kv => kv.Value is not AlwaysProp)
.Concat(alwaysProps).ToDictionary(kv => kv.Key, kv => kv.Value);
}

/// <summary>
/// Resolve all necessary class instances in the given props.
/// </summary>
private static async Task<Dictionary<string, object?>> ResolvePropertyInstances(Dictionary<string, object?> props)
{
return (await Task.WhenAll(props.Select(async pair => pair.Value switch
return (await Task.WhenAll(props.Select(async pair =>
{
Func<object?> f => (pair.Key, f.Invoke()),
LazyProp l => (pair.Key, await l.Invoke()),
AlwaysProp l => (pair.Key, await l.Invoke()),
_ => (pair.Key, pair.Value)
}))).ToDictionary(pair => pair.Key, pair => pair.Item2);
var key = pair.Key.ToCamelCase();

var value = pair.Value switch
{
Func<object?> f => (key, await f.ResolveAsync()),
Task t => (key, await t.ResolveResult()),
InvokableProp p => (key, await p.Invoke()),
_ => (key, pair.Value)
};

if (value.Item2 is Dictionary<string, object?> dict)
{
value = (key, await ResolvePropertyInstances(dict));
}

return value;
}))).ToDictionary(pair => pair.key, pair => pair.Item2);
}

protected internal JsonResult GetJson()
Expand Down Expand Up @@ -93,7 +196,7 @@ private ViewResult GetView()

protected internal IActionResult GetResult() => _context!.IsInertiaRequest() ? GetJson() : GetView();

private IDictionary<string, string> GetErrors()
private Dictionary<string, string> GetErrors()
{
if (!_context!.ModelState.IsValid)
return _context!.ModelState.ToDictionary(o => o.Key.ToCamelCase(),
Expand All @@ -111,48 +214,4 @@ public Response WithViewData(IDictionary<string, object> viewData)
_viewData = viewData;
return this;
}

private async Task<Dictionary<string, object?>> ResolveProperties(Dictionary<string, object?> props)
{
var isPartial = _context!.IsInertiaPartialComponent(_component);

if (!isPartial)
{
props = props
.Where(kv => kv.Value is not LazyProp)
.ToDictionary(kv => kv.Key, kv => kv.Value);
}
else
{
props = props.ToDictionary(kv => kv.Key, kv => kv.Value);

if (_context!.HttpContext.Request.Headers.ContainsKey(InertiaHeader.PartialOnly))
props = ResolveOnly(props);

if (_context!.HttpContext.Request.Headers.ContainsKey(InertiaHeader.PartialExcept))
props = ResolveExcept(props);
}

props = ResolveAlways(props);
props = await PrepareProps(props);

return props;
}

private Dictionary<string, object?> ResolveOnly(Dictionary<string, object?> props)
=> _context!.OnlyProps(props);

private Dictionary<string, object?> ResolveExcept(Dictionary<string, object?> props)
=> _context!.ExceptProps(props);

private Dictionary<string, object?> ResolveAlways(Dictionary<string, object?> props)
{
var alwaysProps = _props.GetType().GetProperties()
.Where(o => o.PropertyType == typeof(AlwaysProp))
.ToDictionary(o => o.Name.ToCamelCase(), o => o.GetValue(_props));

return props
.Where(kv => kv.Value is not AlwaysProp)
.Concat(alwaysProps).ToDictionary(kv => kv.Key, kv => kv.Value);
}
}
26 changes: 16 additions & 10 deletions InertiaCore/ResponseFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ public ResponseFactory(IHttpContextAccessor contextAccessor, IGateway gateway, I
public Response Render(string component, object? props = null)
{
props ??= new { };
var dictProps = props switch
{
Dictionary<string, object?> dict => dict,
_ => props.GetType().GetProperties()
.ToDictionary(o => o.Name, o => o.GetValue(props))
};

return new Response(component, props, _options.Value.RootView, GetVersion());
return new Response(component, dictProps, _options.Value.RootView, GetVersion());
}

public async Task<IHtmlContent> Head(dynamic model)
Expand Down Expand Up @@ -104,8 +110,8 @@ public void Share(string key, object? value)
{
var context = _contextAccessor.HttpContext!;

var sharedData = context.Features.Get<InertiaSharedData>();
sharedData ??= new InertiaSharedData();
var sharedData = context.Features.Get<InertiaSharedProps>();
sharedData ??= new InertiaSharedProps();
sharedData.Set(key, value);

context.Features.Set(sharedData);
Expand All @@ -115,16 +121,16 @@ public void Share(IDictionary<string, object?> data)
{
var context = _contextAccessor.HttpContext!;

var sharedData = context.Features.Get<InertiaSharedData>();
sharedData ??= new InertiaSharedData();
var sharedData = context.Features.Get<InertiaSharedProps>();
sharedData ??= new InertiaSharedProps();
sharedData.Merge(data);

context.Features.Set(sharedData);
}

public LazyProp Lazy(Func<object?> callback) => new LazyProp(callback);
public LazyProp Lazy(Func<Task<object?>> callback) => new LazyProp(callback);
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 LazyProp Lazy(Func<object?> callback) => new(callback);
public LazyProp Lazy(Func<Task<object?>> callback) => new(callback);
public AlwaysProp Always(object? value) => new(value);
public AlwaysProp Always(Func<object?> callback) => new(callback);
public AlwaysProp Always(Func<Task<object?>> callback) => new(callback);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace InertiaCore.Utils;

internal class InertiaSharedData
internal class InertiaSharedProps
{
private IDictionary<string, object?>? Data { get; set; }

Expand Down
Loading

0 comments on commit cfc5968

Please sign in to comment.