├── .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 |
7 |

8 | name: 9 | 10 |

11 |

12 | age: 13 | 14 |

15 | 16 |
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 | logo 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 | 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\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 | --------------------------------------------------------------------------------