Skip to content

Commit edc34aa

Browse files
authored
Merge pull request #6 from phel-lang/feat/json-parsed-body
ref: read JSON request bodies from :parsed-body
2 parents 4b50dfb + 77160d8 commit edc34aa

5 files changed

Lines changed: 58 additions & 36 deletions

File tree

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,19 @@ request data at the edge of a handler with `phel.schema` directly —
8989

9090
## Reading request bodies
9191

92-
`phel.http` exposes form fields on `:parsed-body` (`$_POST`) and query string on
93-
`:query-params` (`$_GET`). For JSON APIs, read the raw stream — see
94-
`json-body` in `controller/routes.phel`:
92+
`phel.http` decodes the request body into `:parsed-body` for you: form fields
93+
(`$_POST`) for `application/x-www-form-urlencoded` / `multipart/form-data`, and
94+
the decoded JSON for `application/json`. Query string lives on `:query-params`
95+
(`$_GET`). Handlers just read the map — no manual `php://input` plumbing:
9596

9697
```phel
97-
(json/decode (php/file_get_contents "php://input"))
98+
(defn greet-post-handler [req]
99+
(greet-response (or (:parsed-body req) {})))
98100
```
99101

102+
`:parsed-body` is `nil` for an empty or malformed body, so `(or … {})` gives a
103+
safe default and the schema reports the missing field.
104+
100105
## Routing
101106

102107
Routes live in `src/app.phel` and use the built-in `phel.router`:

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
"type": "project",
1313
"require": {
1414
"php": ">=8.4",
15-
"phel-lang/phel-lang": "^0.41"
15+
"phel-lang/phel-lang": "dev-main"
1616
},
1717
"require-dev": {
1818
"symfony/var-dumper": "^7.3"
1919
},
20+
"minimum-stability": "dev",
21+
"prefer-stable": true,
2022
"scripts": {
2123
"run:dev": [
2224
"Composer\\Config::disableProcessTimeout",

composer.lock

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

src/controller/routes.phel

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,41 +31,36 @@
3131
[:error (sc/human-readable-explain (sc/explain schema data))]
3232
[:ok result])))
3333

34-
(defn- json-body
35-
"Reads and decodes a JSON request body from the raw input stream.
36-
Returns an empty map when the body is empty or malformed."
37-
[_req]
38-
(let [raw (php/file_get_contents "php://input")]
39-
(if (and (string? raw) (not (php/empty raw)))
40-
(try (json/decode raw) (catch \Throwable _ {}))
41-
{})))
34+
(defn- greet-response
35+
"Validates `params` against the greet schema and answers with a greeting or
36+
a 400. Shared by the path-param and JSON-body greet routes."
37+
[params]
38+
(let [[status payload] (conform-or-error schema/greet-params params)]
39+
(if (= status :ok)
40+
(json-response 200 {:greeting (g/greet (:name payload))})
41+
(bad-request payload))))
4242

4343
(defn index-handler [_req]
4444
(html-response 200 index-html))
4545

4646
(defn ping-handler [req]
47-
;; Pattern-match on the HTTP method instead of nested conds.
47+
;; Pattern-match on the HTTP method instead of nested conds. The JSON body is
48+
;; decoded by phel.http into :parsed-body, so there is no manual php://input read.
4849
(match [(:method req)]
49-
["POST"] (json-response 200 {:message "pong" :echo (json-body req) :ts (php/time)})
50+
["POST"] (json-response 200 {:message "pong" :echo (or (:parsed-body req) {}) :ts (php/time)})
5051
["GET"] (json-response 200 {:message "pong" :ts (php/time)})
5152
:else (json-response 405 {:error "method not allowed"})))
5253

5354
(defn greet-handler
5455
"GET /greet/{name} — validates the path param against a schema before use."
5556
[req]
56-
(let [name (get-in req [:attributes :match :path-params :name] "world")
57-
[status result] (conform-or-error schema/greet-params {:name name})]
58-
(if (= status :ok)
59-
(json-response 200 {:greeting (g/greet (:name result))})
60-
(bad-request result))))
57+
(greet-response {:name (get-in req [:attributes :match :path-params :name] "world")}))
6158

6259
(defn greet-post-handler
63-
"POST /greet — reads a JSON body `{\"name\": \"...\"}`, validates, responds."
60+
"POST /greet — reads a JSON body `{\"name\": \"...\"}`, validates, responds.
61+
phel.http already decoded the body into `:parsed-body`."
6462
[req]
65-
(let [[status result] (conform-or-error schema/greet-params (json-body req))]
66-
(if (= status :ok)
67-
(json-response 200 {:greeting (g/greet (:name result))})
68-
(bad-request result))))
63+
(greet-response (or (:parsed-body req) {})))
6964

7065
(defn not-found-handler [_req]
7166
(html-response 404 "<h1>404</h1><p>Page not found.</p>"))

tests/controller/routes-test.phel

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,27 @@
3535
(is (= 400 (:status res)))))
3636

3737
(deftest greet-post-handler-rejects-empty-body
38-
;; No request body in the test environment -> schema validation fails.
38+
;; No request body -> phel.http leaves :parsed-body nil -> validation fails.
3939
(let [res (routes/greet-post-handler {})]
4040
(is (= 400 (:status res)))))
4141

42+
(deftest greet-post-handler-greets-from-parsed-body
43+
;; phel.http decodes the JSON body into :parsed-body; the handler just reads it.
44+
(let [res (routes/greet-post-handler {:parsed-body {:name "Phel"}})]
45+
(is (= 200 (:status res)))
46+
(is (= "Hello, Phel!" (:greeting (json/decode (:body res)))))))
47+
48+
(deftest greet-post-handler-rejects-too-long-name
49+
(let [long-name (apply str (repeat 60 "x"))
50+
res (routes/greet-post-handler {:parsed-body {:name long-name}})]
51+
(is (= 400 (:status res)))))
52+
53+
(deftest ping-handler-echoes-parsed-body-on-post
54+
(let [res (routes/ping-handler {:method "POST" :parsed-body {:hello "world"}})
55+
body (json/decode (:body res))]
56+
(is (= 200 (:status res)))
57+
(is (= {:hello "world"} (:echo body)))))
58+
4259
(deftest not-found-handler-returns-404
4360
(let [res (routes/not-found-handler {})]
4461
(is (= 404 (:status res)))))

0 commit comments

Comments
 (0)