diff --git a/vlib/v/fmt/fmt.v b/vlib/v/fmt/fmt.v index 72c29377f1..c87b1a5003 100644 --- a/vlib/v/fmt/fmt.v +++ b/vlib/v/fmt/fmt.v @@ -821,6 +821,8 @@ pub fn (mut f Fmt) expr(node ast.Expr) { ast.ComptimeCall { if node.is_vweb { f.write('$' + 'vweb.html()') + } else { + f.write('${node.left}.\$${node.method_name}($node.args_var)') } } ast.ConcatExpr { diff --git a/vlib/v/fmt/tests/comptime_keep.vv b/vlib/v/fmt/tests/comptime_keep.vv new file mode 100644 index 0000000000..ae14a6e6ee --- /dev/null +++ b/vlib/v/fmt/tests/comptime_keep.vv @@ -0,0 +1,89 @@ +struct App { + a string + b string +mut: + c int + d f32 +pub: + e f32 + f u64 +pub mut: + g string + h byte +} + +fn comptime_for() { + println(@FN) + $for method in App.methods { + println(' method: $method.name | $method') + } +} + +fn comptime_for_with_if() { + println(@FN) + $for method in App.methods { + println(' method: $method') + $if method.typ is fn () { + assert method.name in ['run', 'method2'] + } + $if method.return_type is int { + assert method.name in ['int_method1', 'int_method2'] + } + $if method.args[0].typ is string { + assert method.name == 'my_method' + } + } +} + +fn comptime_for_fields() { + println(@FN) + $for field in App.fields { + println(' field: $field.name | $field') + $if field.typ is string { + assert field.name in ['a', 'b', 'g'] + } + $if field.typ is f32 { + assert field.name in ['d', 'e'] + } + if field.is_mut { + assert field.name in ['c', 'd', 'g', 'h'] + } + if field.is_pub { + assert field.name in ['e', 'f', 'g', 'h'] + } + if field.is_pub && field.is_mut { + assert field.name in ['g', 'h'] + } + } +} + +struct Result { +} + +fn (mut a App) my_method(p string) Result { + println('>>>> ${@FN} | p: $p') + return Result{} +} + +fn handle_conn(mut app T) { + mut vars := []string{cap: 123} + vars << 'abc' + vars << 'def' + $for method in T.methods { + $if method.return_type is Result { + app.$method(vars) + } + } +} + +fn comptime_call_dollar_method() { + mut app := App{} + handle_conn(mut app) +} + +fn main() { + comptime_for() + comptime_for_with_if() + comptime_for_fields() + comptime_call_dollar_method() +} diff --git a/vlib/vweb/vweb.v b/vlib/vweb/vweb.v index fe5e4efd70..fbae29d214 100644 --- a/vlib/vweb/vweb.v +++ b/vlib/vweb/vweb.v @@ -1,7 +1,6 @@ // Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. - module vweb import os @@ -12,70 +11,74 @@ import strings import time pub const ( - methods_with_form = [http.Method.post, .put, .patch] - header_server = 'Server: VWeb\r\n' + methods_with_form = [http.Method.post, .put, .patch] + header_server = 'Server: VWeb\r\n' header_connection_close = 'Connection: close\r\n' - headers_close = '${header_server}${header_connection_close}\r\n' - http_404 = 'HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n${headers_close}404 Not Found' - http_500 = 'HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\n${headers_close}500 Internal Server Error' - mime_types = { - '.css': 'text/css; charset=utf-8', - '.gif': 'image/gif', - '.htm': 'text/html; charset=utf-8', - '.html': 'text/html; charset=utf-8', - '.jpg': 'image/jpeg', - '.js': 'application/javascript', - '.json': 'application/json', - '.md': 'text/markdown; charset=utf-8', - '.pdf': 'application/pdf', - '.png': 'image/png', - '.svg': 'image/svg+xml', - '.txt': 'text/plain; charset=utf-8', - '.wasm': 'application/wasm', + headers_close = '$header_server$header_connection_close\r\n' + http_404 = 'HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n${headers_close}404 Not Found' + http_500 = 'HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\n${headers_close}500 Internal Server Error' + mime_types = { + '.css': 'text/css; charset=utf-8' + '.gif': 'image/gif' + '.htm': 'text/html; charset=utf-8' + '.html': 'text/html; charset=utf-8' + '.jpg': 'image/jpeg' + '.js': 'application/javascript' + '.json': 'application/json' + '.md': 'text/markdown; charset=utf-8' + '.pdf': 'application/pdf' + '.png': 'image/png' + '.svg': 'image/svg+xml' + '.txt': 'text/plain; charset=utf-8' + '.wasm': 'application/wasm' '.xml': 'text/xml; charset=utf-8' } - max_http_post_size = 1024 * 1024 - default_port = 8080 + max_http_post_size = 1024 * 1024 + default_port = 8080 ) pub struct Context { mut: - static_files map[string]string + static_files map[string]string static_mime_types map[string]string - content_type string = 'text/plain' - status string = '200 OK' + content_type string = 'text/plain' + status string = '200 OK' pub: - req http.Request - conn net.Socket + req http.Request + conn net.Socket // TODO Response pub mut: - form map[string]string - query map[string]string - headers string // response headers - done bool - page_gen_start i64 - form_error string - + form map[string]string + query map[string]string + headers string // response headers + done bool + page_gen_start i64 + form_error string } pub struct Cookie { - name string - value string - expires time.Time - secure bool + name string + value string + expires time.Time + secure bool http_only bool } -pub struct Result {} +pub struct Result { +} fn (mut ctx Context) send_response_to_client(mimetype string, res string) bool { - if ctx.done { return false } + if ctx.done { + return false + } ctx.done = true mut sb := strings.new_builder(1024) - defer { sb.free() } - sb.write('HTTP/1.1 ${ctx.status}') - sb.write('\r\nContent-Type: ${mimetype}') - sb.write('\r\nContent-Length: ${res.len}') + defer { + sb.free() + } + sb.write('HTTP/1.1 $ctx.status') + sb.write('\r\nContent-Type: $mimetype') + sb.write('\r\nContent-Length: $res.len') sb.write(ctx.headers) sb.write('\r\n') sb.write(headers_close) @@ -84,7 +87,9 @@ fn (mut ctx Context) send_response_to_client(mimetype string, res string) bool { defer { s.free() } - ctx.conn.send_string(s) or { return false } + ctx.conn.send_string(s) or { + return false + } return true } @@ -108,26 +113,32 @@ pub fn (mut ctx Context) ok(s string) Result { } pub fn (mut ctx Context) redirect(url string) Result { - if ctx.done { return Result{} } + if ctx.done { + return Result{} + } ctx.done = true - ctx.conn.send_string('HTTP/1.1 302 Found\r\nLocation: ${url}${ctx.headers}\r\n${headers_close}') or { return Result{} } + ctx.conn.send_string('HTTP/1.1 302 Found\r\nLocation: $url$ctx.headers\r\n$headers_close') or { + return Result{} + } return Result{} } pub fn (mut ctx Context) not_found() Result { - if ctx.done { return vweb.Result{} } + if ctx.done { + return Result{} + } ctx.done = true - ctx.conn.send_string(http_404) or {} - return vweb.Result{} + ctx.conn.send_string(http_404) or { } + return Result{} } pub fn (mut ctx Context) set_cookie(cookie Cookie) { mut cookie_data := []string{} - mut secure := if cookie.secure { "Secure;" } else { "" } - secure += if cookie.http_only { " HttpOnly" } else { " " } + mut secure := if cookie.secure { 'Secure;' } else { '' } + secure += if cookie.http_only { ' HttpOnly' } else { ' ' } cookie_data << secure if cookie.expires.unix > 0 { - cookie_data << 'expires=${cookie.expires.utc_string()}' + cookie_data << 'expires=$cookie.expires.utc_string()' } data := cookie_data.join(' ') ctx.add_header('Set-Cookie', '$cookie.name=$cookie.value; $data') @@ -135,8 +146,8 @@ pub fn (mut ctx Context) set_cookie(cookie Cookie) { pub fn (mut ctx Context) set_cookie_old(key string, val string) { // TODO support directives, escape cookie value (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) - //ctx.add_header('Set-Cookie', '${key}=${val}; Secure; HttpOnly') - ctx.add_header('Set-Cookie', '${key}=${val}; HttpOnly') + // ctx.add_header('Set-Cookie', '${key}=${val}; Secure; HttpOnly') + ctx.add_header('Set-Cookie', '$key=$val; HttpOnly') } pub fn (mut ctx Context) set_content_type(typ string) { @@ -144,7 +155,7 @@ pub fn (mut ctx Context) set_content_type(typ string) { } pub fn (mut ctx Context) set_cookie_with_expire_date(key string, val string, expire_date time.Time) { - ctx.add_header('Set-Cookie', '$key=$val; Secure; HttpOnly; expires=${expire_date.utc_string()}') + ctx.add_header('Set-Cookie', '$key=$val; Secure; HttpOnly; expires=$expire_date.utc_string()') } pub fn (ctx &Context) get_cookie(key string) ?string { // TODO refactor @@ -153,13 +164,10 @@ pub fn (ctx &Context) get_cookie(key string) ?string { // TODO refactor cookie_header = ctx.get_header('Cookie') } cookie_header = ' ' + cookie_header - //println('cookie_header="$cookie_header"') - //println(ctx.req.headers) - cookie := if cookie_header.contains(';') { - cookie_header.find_between(' $key=', ';') - } else { - cookie_header.find_between(' $key=', '\r') - } + // println('cookie_header="$cookie_header"') + // println(ctx.req.headers) + cookie := if cookie_header.contains(';') { cookie_header.find_between(' $key=', ';') } else { cookie_header.find_between(' $key=', + '\r') } if cookie != '' { return cookie.trim_space() } @@ -175,20 +183,18 @@ pub fn (mut ctx Context) set_status(code int, desc string) { } pub fn (mut ctx Context) add_header(key string, val string) { - //println('add_header($key, $val)') + // println('add_header($key, $val)') ctx.headers = ctx.headers + '\r\n$key: $val' - //println(ctx.headers) + // println(ctx.headers) } pub fn (ctx &Context) get_header(key string) string { return ctx.req.headers[key] } -//fn handle_conn(conn net.Socket) { - //println('handle') - -//} - +// fn handle_conn(conn net.Socket) { +// println('handle') +// } pub fn run(port int) { mut app := T{} run_app(mut app, port) @@ -196,7 +202,9 @@ pub fn run(port int) { pub fn run_app(mut app T, port int) { println('Running a Vweb app on http://localhost:$port') - l := net.listen(port) or { panic('failed to listen') } + l := net.listen(port) or { + panic('failed to listen') + } app.vweb = Context{} app.init_once() $for method in T.methods { @@ -204,16 +212,18 @@ pub fn run_app(mut app T, port int) { // check routes for validity } } - //app.reset() + // app.reset() for { - conn := l.accept() or { panic('accept() failed') } - //handle_conn(conn, mut app) + conn := l.accept() or { + panic('accept() failed') + } + // handle_conn(conn, mut app) handle_conn(conn, mut app) - //app.vweb.page_gen_time = time.ticks() - t - //eprintln('handle conn() took ${time.ticks()-t}ms') - //message := readall(conn) - //println(message) -/* + // app.vweb.page_gen_time = time.ticks() - t + // eprintln('handle conn() took ${time.ticks()-t}ms') + // message := readall(conn) + // println(message) + /* if message.len > max_http_post_size { println('message.len = $message.len > max_http_post_size') conn.send_string(http_500) or {} @@ -221,11 +231,9 @@ pub fn run_app(mut app T, port int) { continue } */ - - //lines := message.split_into_lines() - //println(lines) - -/* + // lines := message.split_into_lines() + // println(lines) + /* if lines.len < 2 { conn.send_string(http_500) or {} conn.close() or {} @@ -236,65 +244,64 @@ pub fn run_app(mut app T, port int) { } fn handle_conn(conn net.Socket, mut app T) { - defer { conn.close() or {} } -//fn handle_conn(conn net.Socket, app_ T) T { - //mut app := app_ - //first_line := strip(lines[0]) + defer { + conn.close() or { } + } + // fn handle_conn(conn net.Socket, app_ T) T { + // mut app := app_ + // first_line := strip(lines[0]) page_gen_start := time.ticks() first_line := conn.read_line() $if debug { println('firstline="$first_line"') } - // Parse the first line // "GET / HTTP/1.1" - //first_line := s.all_before('\n') + // first_line := s.all_before('\n') vals := first_line.split(' ') if vals.len < 2 { println('no vals for http') - conn.send_string(http_500) or {} + conn.send_string(http_500) or { } return - //continue } mut headers := []string{} mut body := '' mut in_headers := true mut len := 0 - //for line in lines[1..] { - for _ in 0..100 { - //println(j) + // for line in lines[1..] { + for _ in 0 .. 100 { + // println(j) line := conn.read_line() sline := strip(line) if sline == '' { - //if in_headers { - // End of headers, no body => exit - if len == 0 { - break - } - //} //else { - // End of body - //break - //} + // if in_headers { + // End of headers, no body => exit + if len == 0 { + break + } + // } //else { + // End of body + // break + // } in_headers = false } if in_headers { - //println(sline) + // println(sline) headers << sline if sline.starts_with('Content-Length') { len = sline.all_after(': ').int() - //println('GOT CL=$len') + // println('GOT CL=$len') } } else { body += line.trim_left('\r\n') if body.len >= len { break } - //println('body:$body') + // println('body:$body') } } - req := http.Request{ - headers: http.parse_headers(headers) //s.split_into_lines()) + headers: http.parse_headers(headers) // s.split_into_lines()) data: strip(body) ws_func: 0 user_ptr: 0 @@ -304,19 +311,19 @@ fn handle_conn(conn net.Socket, mut app T) { $if debug { println('req.headers = ') println(req.headers) - println('req.data="$req.data"' ) - //println('vweb action = "$action"') + println('req.data="$req.data"') + // println('vweb action = "$action"') } - //mut app := T{ + // mut app := T{ app.vweb = Context{ req: req conn: conn - form: map[string]string + form: map[string]string{} static_files: app.vweb.static_files static_mime_types: app.vweb.static_mime_types page_gen_start: page_gen_start } - //} + // } if req.method in methods_with_form { app.vweb.parse_form(req.data) } @@ -325,9 +332,7 @@ fn handle_conn(conn net.Socket, mut app T) { println('no vals for http') } return - //continue } - // Serve a static file if it is one // TODO: handle url parameters properly - for now, ignore them mut static_file_name := app.vweb.req.url @@ -336,10 +341,9 @@ fn handle_conn(conn net.Socket, mut app T) { } static_file := app.vweb.static_files[static_file_name] mime_type := app.vweb.static_mime_types[static_file_name] - if static_file != '' && mime_type != '' { data := os.read_file(static_file) or { - conn.send_string(http_404) or {} + conn.send_string(http_404) or { } return } app.vweb.send_response_to_client(mime_type, data) @@ -347,19 +351,16 @@ fn handle_conn(conn net.Socket, mut app T) { return } app.init() - // Call the right action $if debug { println('route matching...') } - //t := time.ticks() - //mut action := '' + // t := time.ticks() + // mut action := '' mut route_words_a := [][]string{} - //mut url_words := vals[1][1..].split('/').filter(it != '') + // mut url_words := vals[1][1..].split('/').filter(it != '') x := vals[1][1..].split('/') mut url_words := x.filter(it != '') - - if url_words.len == 0 { app.index() return @@ -376,7 +377,6 @@ fn handle_conn(conn net.Socket, mut app T) { } } } - mut vars := []string{cap: route_words_a.len} mut action := '' $for method in T.methods { @@ -388,7 +388,9 @@ fn handle_conn(conn net.Socket, mut app T) { // since such methods have a priority. // For example URL `/register` matches route `/:user`, but `fn register()` // should be called first. - if (req.method == .get && url_words[0] == method.name && url_words.len == 1) || (req.method == .post && url_words[0] + '_post' == method.name) { + if (req.method == .get && + url_words[0] == method.name && url_words.len == 1) || + (req.method == .post && url_words[0] + '_post' == method.name) { $if debug { println('easy match method=$method.name') } @@ -427,12 +429,13 @@ fn handle_conn(conn net.Socket, mut app T) { } if route_words_a.len > 0 { for route_words in route_words_a { - if url_words.len == route_words.len || (url_words.len >= route_words.len - 1 && route_words.last().ends_with('...')) { + if url_words.len == route_words.len || + (url_words.len >= route_words.len - 1 && route_words.last().ends_with('...')) { // match `/:user/:repo/tree` to `/vlang/v/tree` mut matching := false mut unknown := false mut variables := []string{cap: route_words.len} - for i in 0..route_words.len { + for i in 0 .. route_words.len { if url_words.len == i { variables << '' matching = true @@ -477,7 +480,7 @@ fn handle_conn(conn net.Socket, mut app T) { } if action == '' { // site not found - conn.send_string(http_404) or {} + conn.send_string(http_404) or { } return } $for method in T.methods { @@ -488,7 +491,7 @@ fn handle_conn(conn net.Socket, mut app T) { if method.args.len == vars.len { app.$method(vars) } else { - eprintln('warning: uneven parameters count (${method.args.len}) in `$method.name`, compared to the vweb route `$method.attrs` (${vars.len})') + eprintln('warning: uneven parameters count ($method.args.len) in `$method.name`, compared to the vweb route `$method.attrs` ($vars.len)') } } } @@ -499,9 +502,9 @@ fn (mut ctx Context) parse_form(s string) { if ctx.req.method !in methods_with_form { return } - //pos := s.index('\r\n\r\n') - //if pos > -1 { - mut str_form := s//[pos..s.len] + // pos := s.index('\r\n\r\n') + // if pos > -1 { + mut str_form := s // [pos..s.len] str_form = str_form.replace('+', ' ') words := str_form.split('&') for word in words { @@ -509,7 +512,9 @@ fn (mut ctx Context) parse_form(s string) { println('parse form keyval="$word"') } keyval := word.trim_space().split('=') - if keyval.len != 2 { continue } + if keyval.len != 2 { + continue + } key := urllib.query_unescape(keyval[0]) or { continue } @@ -521,26 +526,26 @@ fn (mut ctx Context) parse_form(s string) { } ctx.form[key] = val } - //} + // } // todo: parse form-data and application/json // ... } fn (mut ctx Context) scan_static_directory(directory_path string, mount_path string) { - files := os.ls(directory_path) or { panic(err) } - + files := os.ls(directory_path) or { + panic(err) + } if files.len > 0 { for file in files { - if os.is_dir(file) { ctx.scan_static_directory(directory_path + '/' + file, mount_path + '/' + file) - } else if file.contains('.') && ! file.starts_with('.') && ! file.ends_with('.') { + } else if file.contains('.') && !file.starts_with('.') && !file.ends_with('.') { ext := os.file_ext(file) - // Rudimentary guard against adding files not in mime_types. // Use serve_static directly to add non-standard mime types. if ext in mime_types { - ctx.serve_static(mount_path + '/' + file, directory_path + '/' + file, mime_types[ext]) + ctx.serve_static(mount_path + '/' + file, directory_path + '/' + file, + mime_types[ext]) } } } @@ -548,24 +553,19 @@ fn (mut ctx Context) scan_static_directory(directory_path string, mount_path str } pub fn (mut ctx Context) handle_static(directory_path string) bool { - if ctx.done || ! os.exists(directory_path) { + if ctx.done || !os.exists(directory_path) { return false } - dir_path := directory_path.trim_space().trim_right('/') mut mount_path := '' - if dir_path != '.' && os.is_dir(dir_path) { // Mount point hygene, "./assets" => "/assets". mount_path = '/' + dir_path.trim_left('.').trim('/') } - ctx.scan_static_directory(dir_path, mount_path) - return true } - pub fn (mut ctx Context) serve_static(url string, file_path string, mime_type string) { ctx.static_files[url] = file_path ctx.static_mime_types[url] = mime_type @@ -580,7 +580,9 @@ pub fn (ctx &Context) ip() string { ip = ip.all_before(',') } if ip == '' { - ip = ctx.conn.peer_ip() or { '' } + ip = ctx.conn.peer_ip() or { + '' + } } return ip } @@ -589,7 +591,6 @@ pub fn (mut ctx Context) error(s string) { ctx.form_error = s } - /* fn readall(conn net.Socket) string { // read all message from socket @@ -606,7 +607,6 @@ fn readall(conn net.Socket) string { return message } */ - fn strip(s string) string { // strip('\nabc\r\n') => 'abc' return s.trim('\r\n') @@ -618,11 +618,13 @@ pub fn not_found() Result { fn filter(s string) string { return s.replace_each([ - '<', '<', - '"', '"', - '&', '&', + '<', + '<', + '"', + '"', + '&', + '&', ]) - } pub type RawHtml = string