├── .gitignore
├── LICENSE
├── README.md
├── _config.yml
├── example
├── example.v
├── static
│ ├── demo.js
│ └── valval_logo.png
└── template
│ ├── test4.html
│ ├── test5.html
│ └── test6.html
├── todo.md
├── valval.v
└── valval_test.v
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # .v4p backup files
3 | *~.xml
4 |
5 | # Dynamic plugins .dll
6 | bin/
7 |
8 | *.val.html
9 | *.out
10 | example/example
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 thome
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Valval
2 |
3 | Valval is the fastest web framework in V language.
4 |
5 | This means you can __develop__ a website ___quickly___ and __run__ it ___even faster___!
6 |
7 | ##### A simple demo:
8 |
9 | ```v
10 | import valval
11 |
12 | fn hello(req valval.Request) valval.Response {
13 | return valval.response_ok('hello world')
14 | }
15 |
16 | fn main() {
17 | mut app := valval.new_app(true)
18 | app.route('/', hello)
19 | valval.runserver(app, 8012)
20 | }
21 | ```
22 |
23 |
24 | ## Installation
25 |
26 | ### Using Git
27 | ```
28 | $ git clone https://github.com/taojy123/valval
29 | $ mkdir -p ~/.vmodules
30 | $ ln -s $(pwd)/valval ~/.vmodules/valval
31 | ```
32 |
33 | ### Using VPM
34 | Watchmen123456 has registered the module with vpm.
35 | Simply use the following if you have v on your PATH variable:
36 | ``` bash
37 | $ v install watchmen123456.valval
38 | ```
39 |
40 | ***Note***: If you use vpm; you'll have to change the import to:
41 | ```
42 | import watchmen123456.valval
43 | ```
44 |
45 | ## Quickstart
46 |
47 | ### A Minimal Application
48 |
49 | A minimal Valval application looks something like this:
50 | ```v
51 | // demo.v
52 |
53 | module main
54 |
55 | import valval
56 |
57 | fn hello(req valval.Request) valval.Response {
58 | return valval.response_ok('hello world')
59 | }
60 |
61 | fn main() {
62 | mut app := valval.new_app(true)
63 | app.route('/', hello)
64 | valval.runserver(app, 8012)
65 | }
66 | ```
67 |
68 | Run server
69 | ```
70 | $ v run demo.v
71 | ```
72 |
73 | Then you can visit `http://127.0.0.1:8012/` to see the website
74 | ```
75 | $ curl http://127.0.0.1:8012/
76 | hello world
77 | ```
78 |
79 | ### Debug Mode
80 |
81 | You can decide whether to use debug mode when calling `valval.new_app()`
82 | ```v
83 | mut app := valval.new_app(true) // debug mode
84 | mut app := valval.new_app(false) // production mode
85 | ```
86 | debug mode will print out more infomation while app running
87 |
88 | ### Service Port
89 |
90 | You can decide the service port number when calling the `valval.runserver()`
91 | ```v
92 | valval.runserver(app, 8012) // listening 8012 port
93 | valval.runserver(app, 80) // listening 80 port
94 | ```
95 | The valval server will bind `0.0.0.0` address, so you can visit the website by `127.0.0.1:Port` or `ServerIp:Port`
96 |
97 | ### Routing
98 |
99 | Use the `App.route()` function to band a handler function to request path
100 |
101 | The handler function should have a parameter of type `Request` and return a `Response`
102 |
103 | ```v
104 | mut app := valval.new_app(true)
105 |
106 | app.route('/', hello) // http://127.0.0.1
107 |
108 | app.route('/users', function1) // http://127.0.0.1/users
109 | app.route('/user/info', function2) // http://127.0.0.1/user/info
110 |
111 | app.route('POST:/book', function3) // http://127.0.0.1/book by POST
112 | app.route('DELETE:/book', function4) // http://127.0.0.1/book by DELETE
113 | app.route('/book', function5) // http://127.0.0.1/book by other methods
114 |
115 | app.route('*', function6) // all remain
116 |
117 | valval.runserver(app, 80)
118 | ```
119 |
120 | ### Accessing Request Data
121 |
122 | Currently, only the following data can be parsed:
123 |
124 | - query parameters by GET request; by `valval.Request.query[xxx]`
125 | - `x-www-form-urlencoded` parameters by POST / PUT / PATCH request; by `valval.Request.form[xxx]`
126 |
127 | ```v
128 | fn hello(req valval.Request) valval.Response {
129 | mut name = request.query['name']
130 | if name == '' {
131 | name = 'world'
132 | }
133 | return valval.response_ok('hello $name')
134 | }
135 |
136 | fn post_hello(req valval.Request) valval.Response {
137 | mut name = request.form['name']
138 | if name == '' {
139 | name = 'world'
140 | }
141 | return valval.response_ok('hello $name')
142 | }
143 |
144 | app.route('GET:/hello', hello)
145 | app.route('POST:/hello', post_hello)
146 | ```
147 |
148 | `valval.Request.get()` provides a quick way to get data whether it is from `query` or `form`.
149 |
150 | ```v
151 | fn hello(req valval.Request) valval.Response {
152 | name = request.get('name', 'world') // default: 'world'
153 | return valval.response_ok('hello $name')
154 | }
155 |
156 | app.route('/hello', hello)
157 | ```
158 |
159 | More types of request data will be supported in the future:
160 | - parameters in url
161 | - `multipart/form-data` by POST request
162 | - `application/json` by POST request
163 | - uploaded files
164 |
165 | ### Static Files
166 |
167 | Use `valval.App.serve_static` to serve local files
168 |
169 | ```v
170 | mut app := valval.new_app(true)
171 |
172 | app.serve_static('/static/', './relative/path/to/static/')
173 | // visit http://127.0.0.1/static/xxx.js ...
174 |
175 | app.serve_static('/static2/', '/absolute/path/to/static2/')
176 | // visit http://127.0.0.1/static2/yyy.css ...
177 |
178 | valval.runserver(app, 80)
179 | ```
180 |
181 | ### Rendering Templates
182 |
183 | Valval used a whole new idea to implement the template function; inspired by [Vue's](https://github.com/vuejs/vue) system.
184 |
185 | Has the following advantages:
186 |
187 | - You don't need to spend time learning how to use templates, if you have used `Vue` before.
188 | - If you haven't used `Vue`, you also can [learn](https://vuejs.org/v2/guide/syntax.html) it fast, because it's so easy.
189 | - It can integrate some commonly used UI frameworks, such as: `element`, `mint`, `vant`, `antd`, `bootstrap`...
190 | - I don't need to spend time developing built-in templates 😁.
191 |
192 | An example for template:
193 |
194 | `server.v`:
195 |
196 | ```v
197 | import valval
198 | import json
199 |
200 | struct User {
201 | name string
202 | age int
203 | sex bool
204 | }
205 |
206 | fn users(req valval.Request) valval.Response {
207 |
208 | // create a view by template file (`test6.html` can be a relative or absolute path)
209 | // use `element` (https://github.com/ElemeFE/element) as ui framework
210 | mut view := valval.new_view(req, 'users.html', 'element') or {
211 | return valval.response_bad(err)
212 | }
213 |
214 | users := [
215 | User{'Lucy', 13, false},
216 | User{'Lily', 13, false},
217 | User{'Jim', 12, true},
218 | ]
219 | msg := 'This is a page of three user'
220 |
221 | // use view.set to bind data for rendering template
222 | // the second parameter must be a json string
223 | view.set('users', json.encode(users))
224 | view.set('msg', json.encode(msg))
225 |
226 | return valval.response_view(view)
227 | }
228 | ```
229 |
230 | `users.html`:
231 |
232 | ```html
233 |
234 |
235 | Users Page
236 |
237 |
238 |
239 | {{msg}}
240 |
241 | {{u.name}} ,
242 | {{u.age}} ,
243 | Male
244 | Female
245 |
246 |
247 |
248 | ```
249 |
250 | ### Redirects
251 |
252 | Use `valval.response_redirect()` to generate a redirect response
253 |
254 | ```v
255 | fn test1(req valval.Request) valval.Response {
256 | name = req.get('name', '')
257 | if name == '' {
258 | return valval.response_redirect('/not_found')
259 | }
260 | return valval.response_ok('hello $name')
261 | }
262 | ```
263 |
264 | ### Responses
265 |
266 | In addition to the responses mentioned above (`response_ok`, `response_view`, `response_redirect`)
267 |
268 | Valval also provides other response types, as follows:
269 |
270 | ```v
271 | struct User {
272 | name string
273 | age int
274 | sex bool
275 | }
276 |
277 | fn text(req valval.Request) valval.Response {
278 | return valval.response_text('this is plain text response')
279 | }
280 |
281 | fn json(req valval.Request) valval.Response {
282 | user = User{'Tom', 12, true}
283 | return valval.response_json(user)
284 | // -> {"name": "Tom", "age": 12, "sex": true}
285 | }
286 |
287 | fn json_str(req valval.Request) valval.Response {
288 | user = User{'Tom', 12, true}
289 | user_str = json.encode(user)
290 | return valval.response_json_str(user_str)
291 | // -> {"name": "Tom", "age": 12, "sex": true}
292 | }
293 |
294 | fn file(req valval.Request) valval.Response {
295 | return valval.response_file('path/to/local/file')
296 | }
297 |
298 | fn bad(req valval.Request) valval.Response {
299 | return valval.response_bad('Parameter error!')
300 | // response with statu code 400
301 | }
302 |
303 | ```
304 |
305 |
306 |
307 | ## Complete Example
308 |
309 | - You can visit https://github.com/taojy123/valval/tree/master/example to see the complete example.
310 | - And the official website of valval (https://valval.cool) is also written with the valval framework: https://github.com/taojy123/valval_website
311 |
312 |
313 |
314 | ## Install V Language
315 |
316 | Valval framework currently supports the `V language` version is `0.1.24`
317 |
318 | Here are some ways to install V:
319 |
320 | ### 1. Download a prebuilt V package
321 |
322 | Visit official home page https://vlang.io/ to download
323 |
324 |
325 | ### 2. Run V in docker [recommand]
326 |
327 | ```
328 | docker run -it -p 8012:8012 --name vlang taojy123/vlang bash
329 | ```
330 | It includes OpenSSL
331 |
332 | ### 3. Install V from source
333 | ```
334 | $ git clone https://github.com/vlang/v
335 | $ cd v
336 | $ make
337 | ```
338 |
339 | Install OpenSSL
340 | ```
341 | macOS:
342 | $ brew install openssl
343 |
344 | Debian/Ubuntu:
345 | $ sudo apt install libssl-dev openssl ca-certificates
346 | ```
347 | Windows (Win10 Verified):
348 | Source can be downloaded from:
349 | * https://www.openssl.org/source/
350 | * https://github.com/openssl/
351 |
352 | You can find a [Graphic installer](https://slproweb.com/products/Win32OpenSSL.html "32 and 64 bit available") if that's more to you're liking.
353 |
354 |
355 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-slate
--------------------------------------------------------------------------------
/example/example.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | // git clone https://github.com/toajy123/valval
4 | // ln -s $(pwd)/valval ~/.vmodules/valval [or v build module ./valval]
5 | // cd valval/example && v run example.v
6 | //
7 | // curl http://127.0.0.1:8012
8 |
9 | import valval
10 | import json
11 |
12 |
13 | struct User {
14 | name string
15 | age int
16 | sex bool
17 | }
18 |
19 | struct Book {
20 | name string
21 | author string
22 | }
23 |
24 |
25 | fn index(req valval.Request) valval.Response {
26 | return valval.response_redirect('/test6')
27 | }
28 |
29 | fn hello(req valval.Request) valval.Response {
30 | return valval.response_ok('hello world')
31 | }
32 |
33 | fn test1(req valval.Request) valval.Response {
34 | name := req.query['name']
35 | content := 'test1: name = $name'
36 | res := valval.response_text(content)
37 | return res
38 | }
39 |
40 | fn test2(req valval.Request) valval.Response {
41 | method := req.method
42 | if method == 'DELETE' {
43 | return valval.response_bad('can not delete data!')
44 | }
45 | name := req.get('name', 'jim')
46 | content := '$method: name = $name'
47 | mut res := valval.response_text(content)
48 | res.set_header('x-test-key', 'test-value')
49 | return res
50 | }
51 |
52 | fn test3(req valval.Request) valval.Response {
53 | name := req.get('name', 'lily')
54 | age := req.get('age', '18')
55 | sex_str := req.get('sex', '0')
56 | mut sex := true
57 | if sex_str in ['0', ''] {
58 | sex = false
59 | }
60 | user := User{name, age.int(), sex}
61 | res := valval.response_json(user)
62 | return res
63 | }
64 |
65 | fn test4(req valval.Request) valval.Response {
66 | res := valval.response_file('template/test4.html')
67 | return res
68 | }
69 |
70 | fn post_test4(req valval.Request) valval.Response {
71 | name := req.form['name']
72 | age := req.form['age']
73 | url := '/test3/?name=$name&age=$age'
74 | return valval.response_redirect(url)
75 | }
76 |
77 | fn test5(req valval.Request) valval.Response {
78 | return valval.response_file('template/test5.html')
79 | }
80 |
81 | fn test6(req valval.Request) valval.Response {
82 | mut view := valval.new_view(req, 'template/test6.html', 'element')
83 | if view.error != '' {
84 | return valval.response_bad(view.error)
85 | }
86 | if req.is_page() {
87 | println('a user is viewing the test6 page')
88 | } else {
89 | println('api request by vue')
90 | user := User{'lilei', 14, true}
91 | view.set('user', json.encode(user))
92 | users := [
93 | User{'Lucy', 13, false},
94 | User{'Lily', 13, false},
95 | User{'Jim', 12, true},
96 | ]
97 | total_count := users.len + 1
98 | view.set('users', json.encode(users))
99 | view.set('total_count', json.encode(total_count))
100 | }
101 | return valval.response_view(view)
102 | }
103 |
104 |
105 | fn main() {
106 |
107 | mut app := valval.new_app(true)
108 |
109 | app.serve_static('/static/', './static/')
110 |
111 | app.route('/', index) // as same as: ('', index)
112 | app.route('/hello/world', hello)
113 | app.route('/test1', test1)
114 | app.route('/test2', test2)
115 | app.route('/test3', test3)
116 | app.route('/test4', test4)
117 | app.route('POST:/test4', post_test4)
118 | app.route('/test5', test5)
119 | app.route('/test6', test6)
120 |
121 | // app.route('*', index)
122 |
123 | valval.runserver(app, 8012)
124 |
125 | }
126 |
127 | // http://127.0.0.1:8012
128 | // http://127.0.0.1:8012/test1?name=hello
129 | // http://127.0.0.1:8012/test2
130 | // http://127.0.0.1:8012/test3
131 | // http://127.0.0.1:8012/test4
132 | // http://127.0.0.1:8012/test5
133 | // http://127.0.0.1:8012/test6
134 |
135 |
--------------------------------------------------------------------------------
/example/static/demo.js:
--------------------------------------------------------------------------------
1 | // test js file
2 |
3 | alert('gogogo')
4 |
5 |
--------------------------------------------------------------------------------
/example/static/valval_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/valvalio/valval/d649fd0a53a04269b8b99185803303617c435016/example/static/valval_logo.png
--------------------------------------------------------------------------------
/example/template/test4.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | valval example test4
4 |
5 |
6 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/example/template/test5.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | valval
4 |
5 |
6 |
7 | Hello world
8 |
9 |
10 |
--------------------------------------------------------------------------------
/example/template/test6.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | valval template power by vue
4 |
5 |
6 |
7 | {{ user.name }}
8 | {{ user.age }}
9 | male
10 | female
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Total {{ total_count }} Users.
19 |
20 |
21 |
22 | {{u.name}}:
23 | Male
24 | Female
25 |
26 |
27 |
28 |
Element
29 |
Mint
30 |
Vant
31 |
Antd
32 |
Bootstrap
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/todo.md:
--------------------------------------------------------------------------------
1 | # todo
2 |
3 | - homepage / documention ok
4 | - publish to vpm ok
5 | - add to awesome-v ok
6 | - test
7 | - comment
8 |
9 | - router improve ok
10 | - request body ok
11 | - request params
12 | - request multipart-form
13 | - request files
14 | - request json
15 | - request cookies
16 |
17 | - redirect ok
18 | - statics ok
19 | - html ok
20 | - vue template [+ element / mint / vant / antd / bootstrap] ok
21 | - improve vue: data / mounted / methods blocks
22 | - template include
23 | - storage / orm
24 | - reponse set_cookie
25 |
26 | - auth
27 |
28 | - multi / async
29 |
30 |
31 |
--------------------------------------------------------------------------------
/valval.v:
--------------------------------------------------------------------------------
1 |
2 | module valval
3 |
4 | import net
5 | import net.urllib
6 | import json
7 | import os
8 | import time
9 | import strings
10 |
11 |
12 | const (
13 | VERSION = '0.1.4'
14 | V_VERSION = '0.1.24'
15 | HTTP_404 = 'HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\n\r\n404 Not Found'
16 | HTTP_413 = 'HTTP/1.1 413 Request Entity Too Large\r\nContent-Type: text/plain\r\n\r\n413 Request Entity Too Large'
17 | HTTP_500 = 'HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\n\r\n500 Internal Server Error'
18 | POST_BODY_LIMIT = 1024 * 1024 * 20 // 20MB
19 | API_KEY_FLAG = 'valvalapikey' // the param name won't be used anywhere else
20 | )
21 |
22 | // ===== structs ======
23 |
24 | pub struct Request {
25 | pub:
26 | app App
27 | method string
28 | path string
29 | query map[string]string
30 | form map[string]string
31 | body string
32 | headers map[string]string
33 | }
34 |
35 | pub fn (req Request) get(key string, default_value string) string {
36 | if key in req.form {
37 | return req.form[key]
38 | }
39 | if key in req.query {
40 | return req.query[key]
41 | }
42 | return default_value
43 | }
44 |
45 | pub fn (req Request) is_api() bool {
46 | // api request by axios
47 | return API_KEY_FLAG in req.query
48 | }
49 |
50 | pub fn (req Request) is_page() bool {
51 | // the first page view
52 | return !req.is_api()
53 | }
54 |
55 |
56 | pub struct Response {
57 | pub mut:
58 | status int = 200
59 | body string = ''
60 | content_type string = 'text/html; charset=utf-8'
61 | headers map[string]string
62 | }
63 |
64 | pub fn (res mut Response) set_header(key string, value string) {
65 | res.headers[key] = value
66 | }
67 |
68 | fn (res Response) header_text() string {
69 | // res.header_text() => '// Content-Encoding: UTF-8\r\nContent-Length: 138\r\n'
70 | mut text := ''
71 | keys := res.headers.keys()
72 | for key in keys {
73 | value := res.headers[key]
74 | text += '$key: $value\r\n'
75 | }
76 | return text
77 | }
78 |
79 | fn (res Response) status_msg() string {
80 | // res.status_msg() => 'OK'
81 | msg := match res.status {
82 | 100 { 'Continue' }
83 | 101 { 'Switching Protocols' }
84 | 200 { 'OK' }
85 | 201 { 'Created' }
86 | 202 { 'Accepted' }
87 | 203 { 'Non-Authoritive Information' }
88 | 204 { 'No Content' }
89 | 205 { 'Reset Content' }
90 | 206 { 'Partial Content' }
91 | 300 { 'Multiple Choices' }
92 | 301 { 'Moved Permanently' }
93 | 400 { 'Bad Request' }
94 | 401 { 'Unauthorized' }
95 | 403 { 'Forbidden' }
96 | 404 { 'Not Found' }
97 | 405 { 'Method Not Allowed' }
98 | 408 { 'Request Timeout' }
99 | 500 { 'Internal Server Error' }
100 | 501 { 'Not Implemented' }
101 | 502 { 'Bad Gateway' }
102 | else { '-' }
103 | }
104 | return msg
105 | }
106 |
107 |
108 | pub struct View {
109 | req Request
110 | template string
111 | ui string = 'element'
112 | mut:
113 | context map[string]string
114 | pub:
115 | content string // html after template compiled
116 | error string // because of https://github.com/vlang/v/issues/1709, new_view function could return option, so put it here.
117 | }
118 |
119 | pub fn (view mut View) set(key string, data string) {
120 | // data should be a json str of obj / str / int / bool / list ..
121 | view.context[key.trim_space()] = data
122 | }
123 |
124 | fn (view View) get(key string) string {
125 | if key in view.context {
126 | return view.context[key]
127 | }
128 | return '{}'
129 | }
130 |
131 |
132 | pub struct App {
133 | pub:
134 | name string = 'ValvalApp'
135 | debug bool = true
136 | run_ts int = 0
137 | mut:
138 | router map[string]Handler
139 | static_map map[string]string
140 | }
141 |
142 | pub fn (app mut App) route(path string, func fn(Request) Response) {
143 | // route path should not be ends with /
144 | rpath := path.trim_right('/')
145 | app.router[rpath] = Handler{func}
146 | }
147 |
148 | pub fn (app mut App) register(path string, func fn(Request) Response) {
149 | // as same as route
150 | app.route(path, func)
151 | }
152 |
153 | pub fn (app mut App) serve_static(static_prefix string, static_root string) {
154 | // app.serve_static('/static/', './static/')
155 | mut prefix := static_prefix
156 | mut root := static_root
157 | if !prefix.ends_with('/') {
158 | prefix += '/'
159 | }
160 | if !root.ends_with('/') {
161 | root += '/'
162 | }
163 | app.static_map[prefix] = root
164 | }
165 |
166 | fn (app App) handle(method string, path string, query_str string, body string, headers map[string]string) Response {
167 |
168 | for static_prefix in app.static_map.keys() {
169 | if path.starts_with(static_prefix) {
170 | static_root := app.static_map[static_prefix]
171 | fpath := path.replace_once(static_prefix, static_root)
172 | return response_file(fpath)
173 | }
174 | }
175 | query := urldecode(query_str)
176 | mut form := map[string]string
177 | if headers['content-type'] in ['application/x-www-form-urlencoded', ''] {
178 | form = urldecode(body)
179 | }
180 | req := Request{
181 | app: app
182 | method: method
183 | path: path
184 | query: query
185 | form: form
186 | body: body
187 | headers: headers
188 | }
189 | handler := app.find_handler(req)
190 | func := handler.func
191 | res := func(req)
192 | return res
193 | }
194 |
195 | fn (app App) find_handler(req Request) Handler {
196 | router := app.router
197 | path := req.path.trim_right('/')
198 | method_path := '${req.method}:$path'
199 | // first match `method:path`
200 | if (method_path in router) {
201 | return router[method_path]
202 | }
203 | // then math path
204 | if (path in router) {
205 | return router[path]
206 | }
207 | // then use common handler if it exists
208 | if '*' in router {
209 | return router['*']
210 | }
211 | // last return default handler
212 | return Handler{default_handler_func}
213 | }
214 |
215 |
216 | struct Server {
217 | pub:
218 | address string = '0.0.0.0'
219 | port int = 8012
220 | mut:
221 | app App
222 | }
223 |
224 | pub fn (server Server) run() {
225 | app := server.app
226 | println('${app.name} running on http://$server.address:$server.port ...')
227 | println('Working in: ${os.getwd()}')
228 | println('Server OS: ${os.user_os()}, Debug: ${app.debug}')
229 | println('Valval version: $VERSION, support V version: $V_VERSION')
230 |
231 | // listener := net.listen(server.port) or { panic('failed to listen') }
232 | for {
233 | listener := net.listen(server.port) or { panic('failed to listen') }
234 | conn := listener.accept() or { panic('accept failed') }
235 | listener.close() or {} // todo: do not close listener and recreate it everytime
236 | if app.debug {
237 | println('===============')
238 | println(conn)
239 | }
240 | message := readall(conn, app.debug) or {
241 | println(err)
242 | if err == '413' {
243 | conn.write(HTTP_413) or {}
244 | } else {
245 | conn.write(HTTP_500) or {}
246 | }
247 | conn.close() or {}
248 | continue
249 | }
250 | if app.debug {
251 | println('------------')
252 | println(message)
253 | }
254 | lines := message.split_into_lines()
255 | if lines.len < 2 {
256 | println('invalid message for http')
257 | conn.write(HTTP_500) or {}
258 | conn.close() or {}
259 | continue
260 | }
261 | first_line := lines[0].trim_space()
262 | items := first_line.split(' ')
263 | if items.len < 2 {
264 | println('invalid data for http')
265 | conn.write(HTTP_500) or {}
266 | conn.close() or {}
267 | continue
268 | }
269 | method := items[0].to_upper()
270 | // url => :///;?#
271 | url := items[1]
272 | path := url.all_before('?')
273 | mut query := ''
274 | if url.contains('?') {
275 | query = url.all_after('?').all_before('#')
276 | }
277 | println(first_line)
278 | if app.debug {
279 | println('------------')
280 | println(items)
281 | println('$method, $url, $path, $query')
282 | }
283 | mut headers := map[string]string
284 | mut body := ''
285 | mut flag := true
286 | // length of lines must more than 2
287 | for line in lines[1..] {
288 | sline := line.trim_space()
289 | if sline == '' {
290 | flag = false
291 | }
292 | if flag {
293 | header_name, header_value := split2(sline, ':')
294 | headers[header_name.to_lower()] = header_value.trim_space()
295 | } else {
296 | body += sline + '\r\n'
297 | }
298 | }
299 | body = body.trim_space()
300 | if app.debug {
301 | println('------ request headers ------')
302 | println(headers)
303 | if body.len > 2000 {
304 | println(body[..2000] + ' ...')
305 | } else {
306 | println(body)
307 | }
308 | }
309 |
310 | res := app.handle(method, path, query, body, headers)
311 |
312 | mut builder := strings.new_builder(1024)
313 | builder.write('HTTP/1.1 $res.status ${res.status_msg()}\r\n')
314 | builder.write('Content-Type: $res.content_type\r\n')
315 | builder.write('Content-Length: $res.body.len\r\n')
316 | builder.write('Connection: close\r\n')
317 | builder.write('${res.header_text()}')
318 | builder.write('\r\n')
319 |
320 | result := builder.str()
321 | conn.send_string(result) or {}
322 | if app.debug {
323 | println('------ response headers ------')
324 | if result.len > 500 {
325 | println(result[..500] + ' ...')
326 | } else {
327 | println(result)
328 | }
329 | }
330 | builder.free()
331 |
332 | conn.send_string(res.body) or {}
333 | if app.debug {
334 | println('------- response body -----')
335 | if res.body.len > 2000 {
336 | println(res.body[..2000] + ' ...')
337 | } else {
338 | println(res.body)
339 | }
340 | }
341 |
342 | conn.close() or {}
343 |
344 | if app.debug {
345 | println('======================')
346 | }
347 | }
348 | }
349 |
350 |
351 | struct Handler {
352 | func fn(Request) Response
353 | }
354 |
355 |
356 | // ===== functions ======
357 |
358 | fn split2(s string, flag string) (string, string) {
359 | // split2('abc:def:xyz', ':') => 'abc', 'def:xyz'
360 | // split2('abc', ':') => 'abc', ''
361 | mut items := s.split(flag)
362 | if items.len == 1 {
363 | items << ''
364 | }
365 | // length of items must more than 2
366 | return items[0], items[1..].join(flag)
367 | }
368 |
369 | fn default_handler_func(req Request) Response {
370 | res := Response{
371 | status: 404
372 | body: '$req.path not found!'
373 | }
374 | return res
375 | }
376 |
377 | fn urldecode(query_str string) map[string]string {
378 | mut query := map[string]string
379 | mut s := query_str
380 | s = s.replace('+', ' ')
381 | items := s.split('&')
382 | for item in items {
383 | if item.len == 0 {
384 | continue
385 | }
386 | key, value := split2(item.trim_space(), '=')
387 | val := urllib.query_unescape(value) or {
388 | continue
389 | }
390 | query[key] = val
391 | }
392 | return query
393 | }
394 |
395 | fn readall(conn net.Socket, debug bool) ?string {
396 | mut message := ''
397 | mut total_size := 0
398 | for {
399 | buf := [1024]byte
400 | if debug {
401 | println('recv..')
402 | }
403 | n := C.recv(conn.sockfd, buf, 1024, 2)
404 | if debug {
405 | println('n: $n')
406 | }
407 | if n == 0 {
408 | break
409 | }
410 | bs, m := conn.recv(1024 - 1)
411 | total_size += m
412 | if debug {
413 | println('m: $m, total: $total_size')
414 | }
415 | if total_size > POST_BODY_LIMIT {
416 | return error('413')
417 | }
418 | ss := tos_clone(bs)
419 | message += ss
420 | if n == m {
421 | break
422 | }
423 | }
424 | return message
425 | }
426 |
427 | pub fn new_app(debug bool) App {
428 | run_ts := time.now().unix
429 | return App{debug: debug, run_ts: run_ts}
430 | }
431 |
432 | pub fn runserver(app App, port int) {
433 | mut p := port
434 | if port <= 0 || port > 65536 {
435 | p = 8012
436 | }
437 | server := Server{
438 | port: p
439 | app: app
440 | }
441 | server.run()
442 | }
443 |
444 | pub fn new_view(req Request, template string, ui string) View{
445 | if !(ui in ['element', 'mint', 'vant', 'antd', 'bootstrap', '', 'none']) {
446 | return View{error: 'ui just support `element, mint, vant, antd, bootstrap, none` now'}
447 | }
448 | if req.method != 'GET' {
449 | return View{error: 'view template only support GET method'}
450 | }
451 | if !template.ends_with('.html') {
452 | return View{error: 'template must be a .html file'}
453 | }
454 | if !os.exists(template) {
455 | return View{error: '$template template not found'}
456 | }
457 |
458 | mut content := ''
459 | template2 := template[..template.len-5] + '.val.html'
460 | if os.exists(template2) {
461 | ts0 := req.app.run_ts
462 | ts1 := os.file_last_mod_unix(template)
463 | ts2 := os.file_last_mod_unix(template2)
464 | if ts2 > ts1 && ts2 > ts0 {
465 | // use the cache file if available
466 | file_content := os.read_file(template2) or {
467 | return View{error: err}
468 | }
469 | content = file_content
470 | }
471 | }
472 |
473 | if content == '' {
474 | // compile the template
475 | file_content := os.read_file(template) or {
476 | return View{error: err}
477 | }
478 | content = file_content
479 |
480 | mut top := '\n\n\n'
481 | top += '
Load failed, please check the network.
loading...
'
482 | top += '
'
483 | content = content.replace_once('', top)
484 |
485 | mut bottom := '\n
\n\n'
486 | bottom += '\n'
487 | // bottom += '\n'
488 | bottom += '\n'
489 |
490 | if ui == 'element' {
491 | bottom += ' \n'
492 | bottom += '\n'
493 | } else if ui == 'mint' {
494 | bottom += ' \n'
495 | bottom += '\n'
496 | } else if ui == 'vant' {
497 | bottom += ' \n'
498 | bottom += '\n'
499 | } else if ui == 'antd' {
500 | bottom += ' \n'
501 | bottom += '\n'
502 | } else if ui == 'bootstrap' {
503 | bottom += ' \n'
504 | bottom += '\n'
505 | }
506 |
507 | bottom += '\n'
530 | bottom += ''
531 | content = content.replace_once('', bottom)
532 | os.write_file(template2, content)
533 | }
534 | view := View{
535 | req: req
536 | template: template
537 | ui: ui
538 | content: content
539 | }
540 | return view
541 | }
542 |
543 | pub fn response_ok(content string) Response {
544 | res := Response {
545 | status: 200
546 | body: content
547 | }
548 | return res
549 | }
550 |
551 | pub fn response_text(content string) Response {
552 | res := Response {
553 | status: 200
554 | body: content
555 | content_type: 'text/plain; charset=utf-8'
556 | }
557 | return res
558 | }
559 |
560 | pub fn response_json(obj T) Response {
561 | str := json.encode(obj)
562 | res := Response {
563 | status: 200
564 | body: str
565 | content_type: 'application/json'
566 | }
567 | return res
568 | }
569 |
570 | pub fn response_json_str(data string) Response {
571 | res := Response {
572 | status: 200
573 | body: data
574 | content_type: 'application/json'
575 | }
576 | return res
577 | }
578 |
579 | pub fn response_file(path string) Response {
580 | // path := '${os.getwd()}/$path'
581 | if !os.exists(path) {
582 | return response_bad('$path file not found')
583 | }
584 | content := os.read_file(path) or {
585 | println(err)
586 | return response_bad('$path read_file failed')
587 | }
588 | ext := os.ext(path)
589 | mime_map := {
590 | '.css': 'text/css; charset=utf-8',
591 | '.gif': 'image/gif',
592 | '.htm': 'text/html; charset=utf-8',
593 | '.html': 'text/html; charset=utf-8',
594 | '.jpg': 'image/jpeg',
595 | '.js': 'application/javascript',
596 | '.wasm': 'application/wasm',
597 | '.pdf': 'application/pdf',
598 | '.png': 'image/png',
599 | '.svg': 'image/svg+xml',
600 | '.xml': 'text/xml; charset=utf-8'
601 | }
602 | content_type := mime_map[ext]
603 | res := Response {
604 | status: 200
605 | body: content
606 | content_type: content_type
607 | }
608 | return res
609 | }
610 |
611 | pub fn response_redirect(url string) Response {
612 | res := Response {
613 | status: 301
614 | headers: {'Location': url}
615 | }
616 | return res
617 | }
618 |
619 | pub fn response_bad(msg string) Response {
620 | res := Response {
621 | status: 400
622 | body: msg
623 | }
624 | return res
625 | }
626 |
627 | pub fn response_view(view View) Response {
628 | req := view.req
629 | if req.is_page() {
630 | // first get request, return html page
631 | return response_ok(view.content)
632 | }
633 | // api request, return json data
634 | mut r := '{\n'
635 | for key in view.context.keys() {
636 | json_str := view.context[key]
637 | r += ' "$key" : $json_str ,\n'
638 | }
639 | r = r.trim_right(',\n')
640 | r += '\n}'
641 | return response_json_str(r)
642 | }
643 |
644 |
645 | // ========= Request Message Example =========
646 | // POST /search HTTP/1.1
647 | // Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint,
648 | // application/msword, application/x-silverlight, application/x-shockwave-flash, */*
649 | // Referer: http://www.google.cn/
650 | // Accept-Language: zh-cn
651 | // Accept-Encoding: gzip, deflate
652 | // User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; TheWorld)
653 | // Host: www.google.cn
654 | // Connection: Keep-Alive
655 | // Cookie: PREF=ID=80a06da87be9ae3c:U=f7167333e2c3b714:NW=1:TM=1261551909:LM=1261551917:S=ybYcq2wpfefs4V9g;
656 | // NID=31=ojj8d-IygaEtSxLgaJmqSjVhCspkviJrB6omjamNrSm8lZhKy_yMfO2M4QMRKcH1g0iQv9u-2hfBW7bUFwVh7pGaRUb0RnHcJU37y-
657 | // FxlRugatx63JLv7CWMD6UB_O_r
658 | //
659 | // hl=zh-CN&source=hp&q=domety
660 | //
661 | //
662 | // ======== Respose Message Example ==========
663 | //
664 | // HTTP/1.1 200 OK
665 | // Date: Mon, 23 May 2005 22:38:34 GMT
666 | // Content-Type: text/html; charset=UTF-8
667 | // Content-Encoding: UTF-8
668 | // Content-Length: 138
669 | // Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
670 | // Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
671 | // ETag: "3f80f-1b6-3e1cb03b"
672 | // Accept-Ranges: bytes
673 | // Connection: close
674 |
675 | //
676 | //
677 | // An Example Page
678 | //
679 | //
680 | // Hello World, this is a very simple HTML document.
681 | //
682 | //
683 | // ============================================
684 |
685 |
686 |
--------------------------------------------------------------------------------
/valval_test.v:
--------------------------------------------------------------------------------
1 | import valval
2 |
3 |
4 | // todo add test case
5 |
6 | fn test_server() {
7 | assert 1 == 1
8 | }
9 |
--------------------------------------------------------------------------------