@@ -10,13 +10,71 @@ public abstract class FusionBaseTemplate : FusionBaseApi
1010 public static string TemplateAddress { get ; set ; } = "" ;
1111 public static string TemplatesDir { get ; set ; } = "" ;
1212
13- /// <summary>Sync template variables (override in subclasses).</summary>
13+ /// <summary>
14+ /// Template variables (not an HTTP verb). Override in subclasses.
15+ /// <see cref="Get"/> renders this as HTML; POST handlers use <see cref="Form"/> / <see cref="Ok"/> / <see cref="Fail"/>.
16+ /// </summary>
1417 public virtual Dictionary < string , JsonNode ? > Context ( ) => new ( ) ;
1518
1619 /// <summary>Async template variables; default wraps <see cref="Context"/>.</summary>
1720 public virtual Task < Dictionary < string , JsonNode ? > > ContextAsync ( ) =>
1821 Task . FromResult ( Context ( ) ) ;
1922
23+ /// <summary>Parsed POST body (urlencoded or JSON) as flat string fields.</summary>
24+ public Dictionary < string , string > Form => ParseFormBody ( Body , ContentType ( ) ) ;
25+
26+ /// <summary>Parse urlencoded or JSON body into flat string fields.</summary>
27+ public static Dictionary < string , string > ParseFormBody ( string ? body , string ? contentType )
28+ {
29+ var raw = body ?? "" ;
30+ var ct = ( contentType ?? "" ) . ToLowerInvariant ( ) ;
31+ var outDict = new Dictionary < string , string > ( StringComparer . Ordinal ) ;
32+ if ( ct . Contains ( "application/json" , StringComparison . Ordinal )
33+ || ( raw . TrimStart ( ) . StartsWith ( '{' ) && ! ct . Contains ( "urlencoded" , StringComparison . Ordinal ) ) )
34+ {
35+ try
36+ {
37+ var node = string . IsNullOrWhiteSpace ( raw ) ? null : JsonNode . Parse ( raw ) ;
38+ if ( node is JsonObject obj )
39+ {
40+ foreach ( var kv in obj )
41+ outDict [ kv . Key ] = kv . Value is null || kv . Value . GetValueKind ( ) == JsonValueKind . Null
42+ ? ""
43+ : kv . Value . ToString ( ) ?? "" ;
44+ }
45+ }
46+ catch ( JsonException )
47+ {
48+ // ignore invalid JSON
49+ }
50+ return outDict ;
51+ }
52+
53+ if ( string . IsNullOrEmpty ( raw ) )
54+ return outDict ;
55+
56+ foreach ( var pair in raw . Split ( '&' , StringSplitOptions . RemoveEmptyEntries ) )
57+ {
58+ var parts = pair . Split ( '=' , 2 ) ;
59+ var key = Uri . UnescapeDataString ( parts [ 0 ] . Replace ( '+' , ' ' ) ) ;
60+ var value = parts . Length > 1
61+ ? Uri . UnescapeDataString ( parts [ 1 ] . Replace ( '+' , ' ' ) )
62+ : "" ;
63+ outDict [ key ] = value ;
64+ }
65+ return outDict ;
66+ }
67+
68+ string ? ContentType ( )
69+ {
70+ foreach ( var kv in Request . Headers )
71+ {
72+ if ( string . Equals ( kv . Key , "Content-Type" , StringComparison . OrdinalIgnoreCase ) )
73+ return kv . Value ;
74+ }
75+ return null ;
76+ }
77+
2078 /// <summary>
2179 /// Default GET — HTML or JSON context. Uses <see cref="ContextAsync"/> so
2280 /// subclasses can override that for DB/API-backed pages (Python async context parity).
@@ -42,6 +100,103 @@ object FinishGet(Dictionary<string, JsonNode?> ctx)
42100 return HtmlResponse ( ctx ) ;
43101 }
44102
103+ /// <summary>Validation failure — JSON for SPA fetch, else same template with errors.</summary>
104+ public object Fail (
105+ IDictionary < string , string > ? errors = null ,
106+ string ? message = null ,
107+ IDictionary < string , string > ? fields = null )
108+ {
109+ var err = errors ? . ToDictionary ( kv => kv . Key , kv => kv . Value , StringComparer . Ordinal )
110+ ?? new Dictionary < string , string > ( StringComparer . Ordinal ) ;
111+ var flat = fields ? . ToDictionary (
112+ kv => kv . Key ,
113+ kv => kv . Value ?? "" ,
114+ StringComparer . Ordinal )
115+ ?? new Dictionary < string , string > ( StringComparer . Ordinal ) ;
116+ var msg = message ?? "Validation failed" ;
117+ if ( WantsJson ( ) )
118+ {
119+ return Response ( new Dictionary < string , object ? >
120+ {
121+ [ "ok" ] = false ,
122+ [ "message" ] = msg ,
123+ [ "errors" ] = err ,
124+ [ "fields" ] = flat ,
125+ } , 400 ) ;
126+ }
127+ return FormHtmlResult ( ok : false , message : msg , errors : err , fields : flat , status : 400 ) ;
128+ }
129+
130+ /// <summary>Success — JSON for SPA fetch, else same template with ok=true.</summary>
131+ public object Ok ( string ? message = null , IDictionary < string , string > ? fields = null )
132+ {
133+ var flat = fields ? . ToDictionary (
134+ kv => kv . Key ,
135+ kv => kv . Value ?? "" ,
136+ StringComparer . Ordinal )
137+ ?? new Dictionary < string , string > ( StringComparer . Ordinal ) ;
138+ var msg = message ?? "OK" ;
139+ if ( WantsJson ( ) )
140+ {
141+ return Response ( new Dictionary < string , object ? >
142+ {
143+ [ "ok" ] = true ,
144+ [ "message" ] = msg ,
145+ [ "errors" ] = new Dictionary < string , string > ( ) ,
146+ [ "fields" ] = flat ,
147+ } , 200 ) ;
148+ }
149+ return FormHtmlResult (
150+ ok : true ,
151+ message : msg ,
152+ errors : new Dictionary < string , string > ( ) ,
153+ fields : flat ,
154+ status : 200 ) ;
155+ }
156+
157+ object FormHtmlResult (
158+ bool ok ,
159+ string message ,
160+ IDictionary < string , string > errors ,
161+ IDictionary < string , string > fields ,
162+ int status )
163+ {
164+ var task = ContextAsync ( ) ;
165+ if ( ! task . IsCompletedSuccessfully )
166+ return FormHtmlResultAsync ( task , ok , message , errors , fields , status ) ;
167+ return FinishFormHtml ( task . Result , ok , message , errors , fields , status ) ;
168+ }
169+
170+ async Task < object > FormHtmlResultAsync (
171+ Task < Dictionary < string , JsonNode ? > > task ,
172+ bool ok ,
173+ string message ,
174+ IDictionary < string , string > errors ,
175+ IDictionary < string , string > fields ,
176+ int status )
177+ {
178+ var ctx = await task . ConfigureAwait ( false ) ;
179+ return FinishFormHtml ( ctx , ok , message , errors , fields , status ) ;
180+ }
181+
182+ object FinishFormHtml (
183+ Dictionary < string , JsonNode ? > ctx ,
184+ bool ok ,
185+ string message ,
186+ IDictionary < string , string > errors ,
187+ IDictionary < string , string > fields ,
188+ int status )
189+ {
190+ var data = new Dictionary < string , JsonNode ? > ( ctx , StringComparer . Ordinal ) ;
191+ foreach ( var kv in fields )
192+ data [ kv . Key ] = JsonValue . Create ( kv . Value ) ;
193+ data [ "ok" ] = JsonValue . Create ( ok ) ;
194+ data [ "message" ] = JsonValue . Create ( message ) ;
195+ data [ "errors" ] = JsonSerializer . SerializeToNode ( errors ) ;
196+ data [ "fields" ] = JsonSerializer . SerializeToNode ( fields ) ;
197+ return HtmlResponse ( data , status ) ;
198+ }
199+
45200 public virtual string TemplateName ( )
46201 {
47202 var name = ! string . IsNullOrEmpty ( Template ) ? Template : TemplateAddress ;
0 commit comments