Skip to content

Commit ef9cd4d

Browse files
authored
Merge pull request cipherunits#2 from cipherunits/dev
Add pagination APIs and C# handler parameter binding.
2 parents d3443aa + a98d633 commit ef9cd4d

27 files changed

Lines changed: 1208 additions & 18 deletions

‎Cargo.lock‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ members = [
88
]
99

1010
[workspace.package]
11-
version = "1.2.5"
11+
version = "1.2.6"
1212
edition = "2024"
1313

1414
[workspace.dependencies]

‎bindings/csharp/FusionFramework/FusionApp.cs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public void Mount()
4545
Path = mountSlot.Path,
4646
Method = mountSlot.HttpMethod.ToUpperInvariant(),
4747
Handler = mountSlot.Handler,
48+
Specs = mountSlot.Specs,
4849
Global = _middleware.ToList(),
4950
};
5051

@@ -73,8 +74,9 @@ public void Mount()
7374
{
7475
var instance = (FusionBaseApi)Activator.CreateInstance(s.Entry.ApiClass)!;
7576
instance.Request = r;
77+
var args = HandlerBinding.BindArgs(s.Handler, r, s.Specs);
7678
// Async methods return Task/ValueTask — RunChain unwraps them.
77-
return s.Handler.Invoke(instance, null);
79+
return s.Handler.Invoke(instance, args);
7880
});
7981
}
8082
catch (TargetInvocationException tie) when (tie.InnerException is HttpException hex)

‎bindings/csharp/FusionFramework/FusionBaseApi.cs‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,34 @@ public object Response(object? body = null, int status = 200, IDictionary<string
4444
envelope["headers"] = headers.ToDictionary(kv => kv.Key, kv => (object)kv.Value);
4545
return envelope;
4646
}
47+
48+
/// <summary>Read pagination from query (explicit args override query values).</summary>
49+
public PaginationParams PageParams(
50+
ulong? page = null,
51+
ulong? pageSize = null,
52+
ulong? offset = null,
53+
ulong defaultPageSize = 20,
54+
ulong maxPageSize = 100) =>
55+
Pagination.Parse(
56+
Request.Query,
57+
page,
58+
pageSize,
59+
offset,
60+
defaultPageSize,
61+
maxPageSize);
62+
63+
/// <summary>Return a paginated list response envelope.</summary>
64+
public object Paginated(
65+
object items,
66+
ulong total,
67+
PaginationParams? parameters = null,
68+
ulong? page = null,
69+
ulong? pageSize = null,
70+
int status = 200,
71+
IDictionary<string, string>? headers = null)
72+
{
73+
var p = parameters ?? PageParams(page: page, pageSize: pageSize);
74+
var body = Pagination.Body(items, total, p);
75+
return Response(body, status, headers);
76+
}
4777
}

‎bindings/csharp/FusionFramework/FusionFramework.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<RootNamespace>FusionFramework</RootNamespace>
88
<AssemblyName>FusionFramework</AssemblyName>
99
<PackageId>Fusion-Framework</PackageId>
10-
<Version>1.2.5</Version>
10+
<Version>1.2.6</Version>
1111
<Authors>CipherUnits</Authors>
1212
<Company>CipherUnits</Company>
1313
<Description>Fusion Framework managed bindings (C#) over fusion-ffi / fusion-core</Description>
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
using System.Reflection;
2+
using System.Text.Json;
3+
using System.Text.Json.Nodes;
4+
5+
namespace FusionFramework;
6+
7+
internal enum HandlerParamKind
8+
{
9+
String,
10+
Int,
11+
Float,
12+
Bool,
13+
Auto,
14+
}
15+
16+
internal sealed class HandlerParamSpec
17+
{
18+
public required string Name { get; init; }
19+
public HandlerParamKind Kind { get; init; }
20+
public bool Optional { get; init; }
21+
public bool HasDefault { get; init; }
22+
public object? DefaultValue { get; init; }
23+
public required Type ParameterType { get; init; }
24+
}
25+
26+
/// <summary>Bind handler parameters from path/query/body (fusion-core dispatch parity).</summary>
27+
internal static class HandlerBinding
28+
{
29+
static readonly string[] BodyMethods = ["POST", "PUT", "PATCH"];
30+
31+
public static List<HandlerParamSpec> ExtractSpecs(MethodInfo method)
32+
{
33+
var specs = new List<HandlerParamSpec>();
34+
foreach (var p in method.GetParameters())
35+
{
36+
if (p.ParameterType == typeof(CancellationToken))
37+
continue;
38+
39+
var (kind, optional, underlying) = ClassifyType(p.ParameterType);
40+
specs.Add(new HandlerParamSpec
41+
{
42+
Name = p.Name ?? "",
43+
Kind = kind,
44+
Optional = optional || p.HasDefaultValue,
45+
HasDefault = p.HasDefaultValue,
46+
DefaultValue = p.HasDefaultValue ? p.DefaultValue : null,
47+
ParameterType = underlying ?? p.ParameterType,
48+
});
49+
}
50+
return specs;
51+
}
52+
53+
public static object?[] BindArgs(MethodInfo method, FusionRequest req, IReadOnlyList<HandlerParamSpec> specs)
54+
{
55+
var parameters = method.GetParameters();
56+
if (parameters.Length == 0)
57+
return Array.Empty<object?>();
58+
59+
var specByName = specs.ToDictionary(s => s.Name, StringComparer.Ordinal);
60+
var httpMethod = (req.Method ?? "").ToUpperInvariant();
61+
var bodyFields = BodyMethods.Contains(httpMethod)
62+
? ParseJsonObject(req.Body)
63+
: null;
64+
65+
var args = new object?[parameters.Length];
66+
for (var i = 0; i < parameters.Length; i++)
67+
{
68+
var p = parameters[i];
69+
if (p.ParameterType == typeof(CancellationToken))
70+
{
71+
args[i] = CancellationToken.None;
72+
continue;
73+
}
74+
75+
if (!specByName.TryGetValue(p.Name ?? "", out var spec))
76+
{
77+
args[i] = p.HasDefaultValue ? p.DefaultValue : DefaultFor(p.ParameterType);
78+
continue;
79+
}
80+
81+
string? source = null;
82+
JsonNode? raw = null;
83+
84+
if (req.Params.TryGetValue(spec.Name, out var pathVal))
85+
{
86+
source = "path";
87+
raw = JsonValue.Create(pathVal);
88+
}
89+
else if (bodyFields is not null && bodyFields.TryGetValue(spec.Name, out var bodyVal))
90+
{
91+
source = "body";
92+
raw = bodyVal;
93+
}
94+
else if (req.Query.TryGetValue(spec.Name, out var queryVal))
95+
{
96+
source = "query";
97+
raw = JsonValue.Create(queryVal);
98+
}
99+
100+
if (source is null)
101+
{
102+
if (spec.HasDefault)
103+
{
104+
args[i] = spec.DefaultValue;
105+
continue;
106+
}
107+
args[i] = spec.Optional ? null : DefaultFor(p.ParameterType);
108+
continue;
109+
}
110+
111+
args[i] = CoerceToType(raw, p.ParameterType, spec);
112+
}
113+
114+
return args;
115+
}
116+
117+
public static JsonObject OpenApiSchema(HandlerParamKind kind, bool nullable)
118+
{
119+
JsonObject schema = kind switch
120+
{
121+
HandlerParamKind.Int => new JsonObject { ["type"] = "integer", ["format"] = "int64" },
122+
HandlerParamKind.Float => new JsonObject { ["type"] = "number" },
123+
HandlerParamKind.Bool => new JsonObject { ["type"] = "boolean" },
124+
_ => new JsonObject { ["type"] = "string" },
125+
};
126+
if (nullable)
127+
schema["nullable"] = true;
128+
return schema;
129+
}
130+
131+
static (HandlerParamKind Kind, bool Optional, Type? Underlying) ClassifyType(Type type)
132+
{
133+
var underlying = Nullable.GetUnderlyingType(type);
134+
if (underlying is not null)
135+
return (MapKind(underlying), true, underlying);
136+
137+
return (MapKind(type), false, type);
138+
}
139+
140+
static HandlerParamKind MapKind(Type type)
141+
{
142+
if (type == typeof(int) || type == typeof(long) || type == typeof(short) ||
143+
type == typeof(byte) || type == typeof(uint) || type == typeof(ulong) ||
144+
type == typeof(ushort) || type == typeof(sbyte))
145+
return HandlerParamKind.Int;
146+
if (type == typeof(float) || type == typeof(double) || type == typeof(decimal))
147+
return HandlerParamKind.Float;
148+
if (type == typeof(bool))
149+
return HandlerParamKind.Bool;
150+
if (type == typeof(string))
151+
return HandlerParamKind.String;
152+
return HandlerParamKind.Auto;
153+
}
154+
155+
static Dictionary<string, JsonNode?>? ParseJsonObject(string? body)
156+
{
157+
if (string.IsNullOrWhiteSpace(body))
158+
return null;
159+
try
160+
{
161+
var node = JsonNode.Parse(body);
162+
if (node is not JsonObject obj)
163+
return null;
164+
return obj.ToDictionary(kv => kv.Key, kv => kv.Value);
165+
}
166+
catch
167+
{
168+
return null;
169+
}
170+
}
171+
172+
static object? CoerceToType(JsonNode? raw, Type targetType, HandlerParamSpec spec)
173+
{
174+
var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType;
175+
if (raw is null || raw.GetValueKind() == JsonValueKind.Null)
176+
return spec.Optional ? null : DefaultFor(targetType);
177+
178+
if (underlying == typeof(string))
179+
return raw.GetValueKind() == JsonValueKind.String ? raw.GetValue<string>() : raw.ToJsonString();
180+
181+
if (underlying == typeof(bool))
182+
{
183+
if (raw is JsonValue v && v.TryGetValue<bool>(out var b)) return b;
184+
var s = raw.ToString().Trim().ToLowerInvariant();
185+
return s is "1" or "true" or "yes" or "on";
186+
}
187+
188+
if (underlying == typeof(int))
189+
{
190+
if (raw is JsonValue v && v.TryGetValue<int>(out var parsed)) return parsed;
191+
return int.TryParse(raw.ToString(), out var parsedInt) ? parsedInt : 0;
192+
}
193+
194+
if (underlying == typeof(long))
195+
{
196+
if (raw is JsonValue v && v.TryGetValue<long>(out var parsed)) return parsed;
197+
return long.TryParse(raw.ToString(), out var parsedLong) ? parsedLong : 0L;
198+
}
199+
200+
if (underlying == typeof(float))
201+
{
202+
if (raw is JsonValue v && v.TryGetValue<float>(out var parsed)) return parsed;
203+
return float.TryParse(raw.ToString(), out var parsedFloat) ? parsedFloat : 0f;
204+
}
205+
206+
if (underlying == typeof(double))
207+
{
208+
if (raw is JsonValue v && v.TryGetValue<double>(out var parsed)) return parsed;
209+
return double.TryParse(raw.ToString(), out var parsedDouble) ? parsedDouble : 0d;
210+
}
211+
212+
if (underlying == typeof(decimal))
213+
{
214+
if (raw is JsonValue v && v.TryGetValue<decimal>(out var parsed)) return parsed;
215+
return decimal.TryParse(raw.ToString(), out var parsedDecimal) ? parsedDecimal : 0m;
216+
}
217+
218+
return raw.Deserialize(targetType);
219+
}
220+
221+
static object? DefaultFor(Type type) =>
222+
type.IsValueType ? Activator.CreateInstance(type) : null;
223+
}

‎bindings/csharp/FusionFramework/Header.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public static Dictionary<string, string> Fingerprint()
9090
{
9191
["X-Powered-By"] = "Fusion Framework",
9292
["X-Framework"] = "Fusion",
93-
["X-Fusion-Version"] = "1.2.5",
93+
["X-Fusion-Version"] = "1.2.6",
9494
};
9595
}
9696
}

0 commit comments

Comments
 (0)