FormData.vala 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. using Astralis;
  2. using Invercargill;
  3. using Invercargill.DataStructures;
  4. /**
  5. * FormData Example
  6. *
  7. * Demonstrates handling POST form data in Astralis using async RouteHandler.
  8. * Shows both application/x-www-form-urlencoded and multipart/form-data handling.
  9. */
  10. // HTML form page handler
  11. class FormPageHandler : Object, RouteHandler {
  12. public async HttpResult handle_route(HttpContext context, RouteContext route_context) throws Error {
  13. var headers = new Catalogue<string, string>();
  14. headers.add("Content-Type", "text/html");
  15. return new BufferedHttpResult.from_string(
  16. """<!DOCTYPE html>
  17. <html>
  18. <head>
  19. <title>Form Data Example</title>
  20. <style>
  21. body { font-family: Arial, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; }
  22. h1 { color: #333; }
  23. .form-section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
  24. label { display: block; margin: 10px 0 5px; font-weight: bold; }
  25. input, textarea, select { width: 100%; padding: 8px; margin: 5px 0; box-sizing: border-box; }
  26. button { background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; }
  27. button:hover { background: #0056b3; }
  28. a { color: #007bff; text-decoration: none; }
  29. a:hover { text-decoration: underline; }
  30. </style>
  31. </head>
  32. <body>
  33. <h1>Form Data Examples</h1>
  34. <div class="form-section">
  35. <h2>Simple Form (URL Encoded)</h2>
  36. <form action="/submit-simple" method="POST">
  37. <label for="name">Name:</label>
  38. <input type="text" id="name" name="name" required>
  39. <label for="email">Email:</label>
  40. <input type="email" id="email" name="email" required>
  41. <button type="submit">Submit</button>
  42. </form>
  43. </div>
  44. <div class="form-section">
  45. <h2>Registration Form (URL Encoded)</h2>
  46. <form action="/submit-register" method="POST">
  47. <label for="username">Username:</label>
  48. <input type="text" id="username" name="username" required>
  49. <label for="password">Password:</label>
  50. <input type="password" id="password" name="password" required>
  51. <label for="age">Age:</label>
  52. <input type="number" id="age" name="age" min="18" max="120">
  53. <label for="country">Country:</label>
  54. <select id="country" name="country">
  55. <option value="us">United States</option>
  56. <option value="uk">United Kingdom</option>
  57. <option value="nz">New Zealand</option>
  58. <option value="au">Australia</option>
  59. </select>
  60. <label for="bio">Bio:</label>
  61. <textarea id="bio" name="bio" rows="4"></textarea>
  62. <label>
  63. <input type="checkbox" name="newsletter" value="yes"> Subscribe to newsletter
  64. </label>
  65. <button type="submit">Register</button>
  66. </form>
  67. </div>
  68. <div class="form-section">
  69. <h2>Search Form (URL Encoded)</h2>
  70. <form action="/submit-search" method="POST">
  71. <label for="query">Search Query:</label>
  72. <input type="text" id="query" name="query" required>
  73. <label for="category">Category:</label>
  74. <select id="category" name="category">
  75. <option value="">All Categories</option>
  76. <option value="books">Books</option>
  77. <option value="electronics">Electronics</option>
  78. <option value="clothing">Clothing</option>
  79. </select>
  80. <label for="min_price">Min Price:</label>
  81. <input type="number" id="min_price" name="min_price" min="0" step="0.01">
  82. <label for="max_price">Max Price:</label>
  83. <input type="number" id="max_price" name="max_price" min="0" step="0.01">
  84. <button type="submit">Search</button>
  85. </form>
  86. </div>
  87. <div class="form-section">
  88. <h2>File Upload (Multipart)</h2>
  89. <form action="/submit-file" method="POST" enctype="multipart/form-data">
  90. <label for="description">Description:</label>
  91. <input type="text" id="description" name="description">
  92. <label for="file">File:</label>
  93. <input type="file" id="file" name="file">
  94. <button type="submit">Upload</button>
  95. </form>
  96. </div>
  97. <div class="form-section">
  98. <h2>Links</h2>
  99. <p><a href="/form-debug">Form Debug Tool</a></p>
  100. </div>
  101. </body>
  102. </html>""",
  103. StatusCode.OK,
  104. headers
  105. );
  106. }
  107. }
  108. // Simple form submission handler
  109. class SimpleFormHandler : Object, RouteHandler {
  110. public async HttpResult handle_route(HttpContext context, RouteContext route_context) throws Error {
  111. if (!context.request.is_post()) {
  112. return new BufferedHttpResult.from_string(
  113. "Please use POST method",
  114. StatusCode.METHOD_NOT_ALLOWED
  115. );
  116. }
  117. // Parse form data asynchronously from the request body
  118. FormData form_data = yield FormDataParser.parse(
  119. context.request.request_body,
  120. context.request.content_type
  121. );
  122. var name = form_data.get_field_or_default("name", "Anonymous");
  123. var email = form_data.get_field_or_default("email", "no-email@example.com");
  124. var parts = new Series<string>();
  125. parts.add("Form Submission Received!\n");
  126. parts.add("=========================\n\n");
  127. parts.add(@"Name: $name\n");
  128. parts.add(@"Email: $email\n");
  129. parts.add("\nAll form data:\n");
  130. form_data.fields.to_immutable_buffer()
  131. .iterate((grouping) => {
  132. grouping.iterate((value) => {
  133. parts.add(@" $(grouping.key): $value\n");
  134. });
  135. });
  136. var result = parts.to_immutable_buffer()
  137. .aggregate<string>("", (acc, s) => acc + s);
  138. return new BufferedHttpResult.from_string(result);
  139. }
  140. }
  141. // Registration form submission handler
  142. class RegisterFormHandler : Object, RouteHandler {
  143. public async HttpResult handle_route(HttpContext context, RouteContext route_context) throws Error {
  144. if (!context.request.is_post()) {
  145. return new BufferedHttpResult.from_string(
  146. "Please use POST method",
  147. StatusCode.METHOD_NOT_ALLOWED
  148. );
  149. }
  150. FormData form_data = yield FormDataParser.parse(
  151. context.request.request_body,
  152. context.request.content_type
  153. );
  154. var username = form_data.get_field("username");
  155. var password = form_data.get_field("password");
  156. var age_str = form_data.get_field_or_default("age", "0");
  157. var country = form_data.get_field_or_default("country", "us");
  158. var bio = form_data.get_field_or_default("bio", "");
  159. var newsletter = form_data.get_field("newsletter");
  160. // Validation
  161. if (username == null || username == "") {
  162. var headers = new Catalogue<string, string>();
  163. headers.add("Content-Type", "application/json");
  164. return new BufferedHttpResult.from_string(
  165. @"{ \"error\": \"Username is required\" }",
  166. StatusCode.BAD_REQUEST,
  167. headers
  168. );
  169. }
  170. if (password == null || password == "") {
  171. var headers = new Catalogue<string, string>();
  172. headers.add("Content-Type", "application/json");
  173. return new BufferedHttpResult.from_string(
  174. @"{ \"error\": \"Password is required\" }",
  175. StatusCode.BAD_REQUEST,
  176. headers
  177. );
  178. }
  179. var age = int.parse(age_str);
  180. if (age < 18) {
  181. var headers = new Catalogue<string, string>();
  182. headers.add("Content-Type", "application/json");
  183. return new BufferedHttpResult.from_string(
  184. @"{ \"error\": \"You must be at least 18 years old\" }",
  185. StatusCode.BAD_REQUEST,
  186. headers
  187. );
  188. }
  189. // Build JSON response using Series
  190. var json_parts = new Series<string>();
  191. json_parts.add(@"{ \"success\": true, \"user\": {");
  192. json_parts.add(@" \"username\": \"$username\",");
  193. json_parts.add(@" \"age\": $age,");
  194. json_parts.add(@" \"country\": \"$country\",");
  195. json_parts.add(@" \"bio\": \"$(bio.replace("\"", "\\\""))\",");
  196. json_parts.add(@" \"newsletter\": $(newsletter != null ? "true" : "false")");
  197. json_parts.add(@"} }");
  198. var json_string = json_parts.to_immutable_buffer()
  199. .aggregate<string>("", (acc, s) => acc + s);
  200. var headers = new Catalogue<string, string>();
  201. headers.add("Content-Type", "application/json");
  202. return new BufferedHttpResult.from_string(
  203. json_string,
  204. StatusCode.OK,
  205. headers
  206. );
  207. }
  208. }
  209. // Search form submission handler
  210. class SearchFormHandler : Object, RouteHandler {
  211. public async HttpResult handle_route(HttpContext context, RouteContext route_context) throws Error {
  212. if (!context.request.is_post()) {
  213. return new BufferedHttpResult.from_string(
  214. "Please use POST method",
  215. StatusCode.METHOD_NOT_ALLOWED
  216. );
  217. }
  218. FormData form_data = yield FormDataParser.parse(
  219. context.request.request_body,
  220. context.request.content_type
  221. );
  222. var query = form_data.get_field("query");
  223. var category = form_data.get_field_or_default("category", "");
  224. var min_price = double.parse(form_data.get_field_or_default("min_price", "0"));
  225. var max_price = double.parse(form_data.get_field_or_default("max_price", "999999"));
  226. if (query == null || query == "") {
  227. var headers = new Catalogue<string, string>();
  228. headers.add("Content-Type", "application/json");
  229. return new BufferedHttpResult.from_string(
  230. @"{ \"error\": \"Search query is required\" }",
  231. StatusCode.BAD_REQUEST,
  232. headers
  233. );
  234. }
  235. // Simulated search results using Enumerable operations
  236. var all_products = new Series<Product>();
  237. all_products.add(new Product(1, "Book A", "books", 15.99));
  238. all_products.add(new Product(2, "Book B", "books", 24.99));
  239. all_products.add(new Product(3, "Laptop", "electronics", 999.99));
  240. all_products.add(new Product(4, "Phone", "electronics", 699.99));
  241. all_products.add(new Product(5, "Shirt", "clothing", 29.99));
  242. all_products.add(new Product(6, "Pants", "clothing", 49.99));
  243. // Filter results using Enumerable operations
  244. var results = all_products.to_immutable_buffer()
  245. .where(p => {
  246. var matches_query = p.name.down().contains(query.down());
  247. var matches_category = category == "" || p.category == category;
  248. var matches_price = p.price >= min_price && p.price <= max_price;
  249. return matches_query && matches_category && matches_price;
  250. });
  251. var json_parts = new Series<string>();
  252. json_parts.add(@"{ \"query\": \"$query\", \"category\": \"$category\", \"min_price\": $min_price, \"max_price\": $max_price, \"results\": [");
  253. bool first = true;
  254. results.iterate((product) => {
  255. if (!first) json_parts.add(", ");
  256. json_parts.add(product.to_json());
  257. first = false;
  258. });
  259. json_parts.add("] }");
  260. var json_string = json_parts.to_immutable_buffer()
  261. .aggregate<string>("", (acc, s) => acc + s);
  262. var headers = new Catalogue<string, string>();
  263. headers.add("Content-Type", "application/json");
  264. return new BufferedHttpResult.from_string(
  265. json_string,
  266. StatusCode.OK,
  267. headers
  268. );
  269. }
  270. }
  271. // File upload handler (multipart/form-data)
  272. class FileUploadHandler : Object, RouteHandler {
  273. public async HttpResult handle_route(HttpContext context, RouteContext route_context) throws Error {
  274. if (!context.request.is_post()) {
  275. return new BufferedHttpResult.from_string(
  276. "Please use POST method",
  277. StatusCode.METHOD_NOT_ALLOWED
  278. );
  279. }
  280. FormData form_data = yield FormDataParser.parse(
  281. context.request.request_body,
  282. context.request.content_type
  283. );
  284. var description = form_data.get_field_or_default("description", "");
  285. var file = form_data.get_file("file");
  286. var parts = new Series<string>();
  287. parts.add("File Upload Result\n");
  288. parts.add("==================\n\n");
  289. parts.add(@"Description: $description\n");
  290. if (file != null) {
  291. parts.add(@"\nFile Information:\n");
  292. parts.add(@" Field Name: $(file.field_name)\n");
  293. parts.add(@" Filename: $(file.filename)\n");
  294. parts.add(@" Content-Type: $(file.content_type)\n");
  295. parts.add(@" Size: $(file.data.length) bytes\n");
  296. } else {
  297. parts.add("\nNo file uploaded.\n");
  298. }
  299. var result = parts.to_immutable_buffer()
  300. .aggregate<string>("", (acc, s) => acc + s);
  301. return new BufferedHttpResult.from_string(result);
  302. }
  303. }
  304. // Form debug tool handler
  305. class FormDebugHandler : Object, RouteHandler {
  306. public async HttpResult handle_route(HttpContext context, RouteContext route_context) throws Error {
  307. FormData? form_data = null;
  308. string? body_text = null;
  309. // Try to parse form data if this is a POST with form content
  310. if (context.request.is_post() &&
  311. (context.request.is_form_urlencoded() || context.request.is_multipart())) {
  312. try {
  313. form_data = yield FormDataParser.parse(
  314. context.request.request_body,
  315. context.request.content_type
  316. );
  317. } catch (Error e) {
  318. body_text = @"Error parsing form data: $(e.message)";
  319. }
  320. }
  321. // Get counts (uint type)
  322. uint field_count = form_data != null ? form_data.field_count() : 0;
  323. uint file_count = form_data != null ? form_data.file_count() : 0;
  324. var parts = new Series<string>();
  325. parts.add("""<!DOCTYPE html>
  326. <html>
  327. <head>
  328. <title>Form Debug Tool</title>
  329. <style>
  330. body { font-family: monospace; max-width: 800px; margin: 40px auto; padding: 20px; background: #f5f5f5; }
  331. .section { background: white; padding: 20px; margin: 20px 0; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
  332. h1 { color: #333; }
  333. h2 { color: #666; border-bottom: 2px solid #007bff; padding-bottom: 10px; }
  334. table { width: 100%; border-collapse: collapse; margin: 10px 0; }
  335. th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
  336. th { background: #007bff; color: white; }
  337. tr:hover { background: #f9f9f9; }
  338. .empty { color: #999; font-style: italic; }
  339. a { color: #007bff; text-decoration: none; }
  340. a:hover { text-decoration: underline; }
  341. form { margin: 20px 0; }
  342. input, textarea { width: 100%; padding: 8px; margin: 5px 0; box-sizing: border-box; }
  343. button { background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; }
  344. </style>
  345. </head>
  346. <body>
  347. <h1>Form Debug Tool</h1>
  348. <div class="section">
  349. <h2>Test Form</h2>
  350. <form action="/form-debug" method="POST" enctype="multipart/form-data">
  351. <label>Text Field: <input type="text" name="test_field" value="test value"></label>
  352. <label>File: <input type="file" name="test_file"></label>
  353. <button type="submit">Submit Test</button>
  354. </form>
  355. </div>
  356. <div class="section">
  357. <h2>Request Information</h2>
  358. <table>
  359. <tr><th>Method</th><td>$(context.request.method)</td></tr>
  360. <tr><th>Path</th><td>$(context.request.raw_path)</td></tr>
  361. <tr><th>Content Type</th><td>$(context.request.content_type)</td></tr>
  362. <tr><th>Content Length</th><td>$(context.request.content_length)</td></tr>
  363. <tr><th>Is Form URL Encoded</th><td>$(context.request.is_form_urlencoded())</td></tr>
  364. <tr><th>Is Multipart</th><td>$(context.request.is_multipart())</td></tr>
  365. </table>
  366. </div>
  367. """);
  368. if (form_data != null) {
  369. parts.add(@" <div class=\"section\">\n");
  370. parts.add(@" <h2>Form Fields ($field_count fields)</h2>\n");
  371. if (field_count == 0) {
  372. parts.add(@" <p class=\"empty\">No form fields received.</p>\n");
  373. } else {
  374. parts.add(@" <table>\n");
  375. parts.add(@" <tr><th>Field Name</th><th>Value</th></tr>\n");
  376. form_data.fields.to_immutable_buffer()
  377. .iterate((grouping) => {
  378. grouping.iterate((value) => {
  379. parts.add(@" <tr><td>$(grouping.key)</td><td>$(value.escape(""))</td></tr>\n");
  380. });
  381. });
  382. parts.add(@" </table>\n");
  383. }
  384. parts.add(@" </div>\n");
  385. parts.add(@" <div class=\"section\">\n");
  386. parts.add(@" <h2>File Uploads ($file_count files)</h2>\n");
  387. if (file_count == 0) {
  388. parts.add(@" <p class=\"empty\">No files uploaded.</p>\n");
  389. } else {
  390. parts.add(@" <table>\n");
  391. parts.add(@" <tr><th>Field Name</th><th>Filename</th><th>Content-Type</th><th>Size</th></tr>\n");
  392. form_data.files.to_immutable_buffer()
  393. .iterate((grouping) => {
  394. grouping.iterate((file) => {
  395. parts.add(@" <tr><td>$(file.field_name)</td><td>$(file.filename)</td><td>$(file.content_type)</td><td>$(file.data.length) bytes</td></tr>\n");
  396. });
  397. });
  398. parts.add(@" </table>\n");
  399. }
  400. parts.add(@" </div>\n");
  401. } else if (body_text != null) {
  402. parts.add(@" <div class=\"section\">\n");
  403. parts.add(@" <h2>Error</h2>\n");
  404. parts.add(@" <pre>$(body_text.escape(""))</pre>\n");
  405. parts.add(@" </div>\n");
  406. }
  407. parts.add(""" <div class="section">
  408. <h2>Query Parameters</h2>
  409. """);
  410. var query_count = context.request.query_params.to_immutable_buffer().count();
  411. if (query_count == 0) {
  412. parts.add(@" <p class=\"empty\">No query parameters.</p>\n");
  413. } else {
  414. parts.add(@" <table>\n");
  415. parts.add(@" <tr><th>Parameter</th><th>Value</th></tr>\n");
  416. context.request.query_params.to_immutable_buffer()
  417. .iterate((grouping) => {
  418. grouping.iterate((value) => {
  419. parts.add(@" <tr><td>$(grouping.key)</td><td>$(value.escape(""))</td></tr>\n");
  420. });
  421. });
  422. parts.add(@" </table>\n");
  423. }
  424. parts.add(""" </div>
  425. <p><a href="/">Back to Forms</a></p>
  426. </body>
  427. </html>""");
  428. var result = parts.to_immutable_buffer()
  429. .aggregate<string>("", (acc, s) => acc + s);
  430. var headers = new Catalogue<string, string>();
  431. headers.add("Content-Type", "text/html");
  432. return new BufferedHttpResult.from_string(
  433. result,
  434. StatusCode.OK,
  435. headers
  436. );
  437. }
  438. }
  439. // Helper class for product data
  440. class Product {
  441. public int id { get; private set; }
  442. public string name { get; private set; }
  443. public string category { get; private set; }
  444. public double price { get; private set; }
  445. public Product(int id, string name, string category, double price) {
  446. this.id = id;
  447. this.name = name;
  448. this.category = category;
  449. this.price = price;
  450. }
  451. public string to_json() {
  452. return @"{ \"id\": $id, \"name\": \"$name\", \"category\": \"$category\", \"price\": $price }";
  453. }
  454. }
  455. void main() {
  456. var router = new Router();
  457. var server = new Server(8084, router);
  458. // Register handlers
  459. router.get("/", new FormPageHandler());
  460. router.post("/submit-simple", new SimpleFormHandler());
  461. router.post("/submit-register", new RegisterFormHandler());
  462. router.post("/submit-search", new SearchFormHandler());
  463. router.post("/submit-file", new FileUploadHandler());
  464. router.map("/form-debug", new FormDebugHandler()); // Handles both GET and POST
  465. print("Form Data Example Server running on port 8084\n");
  466. print("Open http://localhost:8084/ in your browser to try the forms\n");
  467. server.run();
  468. }