64a06e1e44fee67a62654ba66f843c0a283e6e49
[nit.git] / lib / popcorn / pop_json.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2017 Alexandre Terrasa <alexandre@moz-code.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Introduce useful services for JSON REST API handlers.
18 #
19 # Validation and Deserialization of request bodies:
20 #
21 # ~~~nit
22 # class MyJsonHandler
23 # super Handler
24 #
25 # # Validator used do validate the body
26 # redef var validator = new MyFormValidator
27 #
28 # # Define the kind of objects expected by the deserialization process
29 # redef type BODY: MyForm
30 #
31 # redef fun post(req, res) do
32 # var post = validate_body(req, res)
33 # if post == null then return # Validation error: let popcorn return a HTTP 400
34 # var form = deserialize_body(req, res)
35 # if form == null then return # Deserialization error: let popcorn return a HTTP 400
36 #
37 # # TODO do something with the input
38 # print form.name
39 # end
40 # end
41 #
42 # class MyForm
43 # serialize
44 #
45 # var name: String
46 # end
47 #
48 # class MyFormValidator
49 # super ObjectValidator
50 #
51 # init do
52 # add new StringField("name", min_size=1, max_size=255)
53 # end
54 # end
55 # ~~~
56 module pop_json
57
58 import json
59 import pop_handlers
60 import pop_validation
61
62 redef class Handler
63
64 # Validator used to check body input
65 #
66 # Here we use the `pop_validation` module to validate JSON document from the request body.
67 var validator: nullable DocumentValidator = null
68
69 # Validate body input with `validator`
70 #
71 # Try to validate the request body as a json document using `validator`:
72 # * Returns the validated string input if the result of the validation is ok.
73 # * Answers a json error and returns `null` if something went wrong.
74 # * If no `validator` is set, returns the body without validation.
75 #
76 # Example:
77 #
78 # ~~~nit
79 # class ValidatedHandler
80 # super Handler
81 #
82 # redef var validator = new MyObjectValidator
83 #
84 # redef fun post(req, res) do
85 # var body = validate_body(req, res)
86 # if body == null then return # Validation error
87 # # At this point popcorn returned a HTTP 400 code with the validation error
88 # # if the validation failed.
89 #
90 # # TODO do something with the input
91 # print body
92 # end
93 # end
94 #
95 # class MyObjectValidator
96 # super ObjectValidator
97 #
98 # init do
99 # add new StringField("name", min_size=1, max_size=255)
100 # end
101 # end
102 # ~~~
103 fun validate_body(req: HttpRequest, res: HttpResponse): nullable String do
104 var body = req.body
105
106 var validator = self.validator
107 if validator == null then return body
108
109 if not validator.validate(body) then
110 res.json(validator.validation, 400)
111 return null
112 end
113 return body
114 end
115
116 # Deserialize the request body
117 #
118 # Returns the deserialized request body body or `null` if something went wrong.
119 # If the object cannot be deserialized, answers with a HTTP 400.
120 #
121 # See `BODY` and `new_body_object`.
122 #
123 # Example:
124 # ~~~nit
125 # class MyDeserializedHandler
126 # super Handler
127 #
128 # redef type BODY: MyObject
129 #
130 # redef fun post(req, res) do
131 # var form = deserialize_body(req, res)
132 # if form == null then return # Deserialization error
133 # # At this point popcorn returned a HTTP 400 code if something was wrong with
134 # # the deserialization process
135 #
136 # # TODO do something with the input
137 # print form.name
138 # end
139 # end
140 #
141 # class MyObject
142 # serialize
143 #
144 # var name: String
145 # end
146 # ~~~
147 fun deserialize_body(req: HttpRequest, res: HttpResponse): nullable BODY do
148 var body = req.body
149 var deserializer = new JsonDeserializer(body)
150 var form = deserializer.deserialize(body)
151 if not form isa BODY or deserializer.errors.not_empty then
152 res.json_error("Bad input", 400)
153 return null
154 end
155 return form
156 end
157
158 # Kind of objects returned by `deserialize_body`
159 #
160 # Define it in each sub handlers depending on the kind of objects sent in request bodies.
161 type BODY: Serializable
162 end
163
164 redef class HttpResponse
165
166 # Write data as JSON and set the right content type header.
167 fun json(json: nullable Serializable, status: nullable Int) do
168 header["Content-Type"] = media_types["json"].as(not null)
169 if json == null then
170 send(null, status)
171 else
172 send(json.to_json, status)
173 end
174 end
175
176 # Write error as JSON.
177 #
178 # Format: `{"message": message, "status": status}`
179 fun json_error(message: String, status: Int) do
180 var obj = new JsonObject
181 obj["status"] = status
182 obj["message"] = message
183 json(obj, status)
184 end
185 end