forked from vieter-v/vieter
				
			Started own mod of vweb library; first version of PUT request
							parent
							
								
									1967f395f1
								
							
						
					
					
						commit
						5edbaf9b4b
					
				
							
								
								
									
										4
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										4
									
								
								Makefile
								
								
								
								
							|  | @ -1,5 +1,9 @@ | |||
| .PHONY: run | ||||
| run: | ||||
| 	API_KEY=test REPO_DIR=data LOG_LEVEL=DEBUG v run vieter | ||||
| 
 | ||||
| .PHONY: watch | ||||
| watch: | ||||
| 	API_KEY=test REPO_DIR=data LOG_LEVEL=DEBUG v watch run vieter | ||||
| 
 | ||||
| .PHONY: fmt | ||||
|  |  | |||
							
								
								
									
										104
									
								
								vieter/main.v
								
								
								
								
							
							
						
						
									
										104
									
								
								vieter/main.v
								
								
								
								
							|  | @ -1,16 +1,18 @@ | |||
| module main | ||||
| 
 | ||||
| import vweb | ||||
| import web | ||||
| import os | ||||
| import log | ||||
| import io | ||||
| 
 | ||||
| const port = 8000 | ||||
| const buf_size = 1_000_000 | ||||
| 
 | ||||
| struct App { | ||||
| 	vweb.Context | ||||
| 	api_key  string [required; vweb_global] | ||||
| 	repo_dir string [required; vweb_global] | ||||
| 	logger   log.Log [required; vweb_global] | ||||
| 	web.Context | ||||
| 	api_key  string  [required; web_global] | ||||
| 	repo_dir string  [required; web_global] | ||||
| 	logger   log.Log [required; web_global] | ||||
| } | ||||
| 
 | ||||
| [noreturn] | ||||
|  | @ -19,33 +21,83 @@ fn exit_with_message(code int, msg string) { | |||
| 	exit(code) | ||||
| } | ||||
| 
 | ||||
| [post; '/publish'] | ||||
| fn (mut app App) put_package(filename string) vweb.Result { | ||||
| 	for _, files in app.files { | ||||
| 		for file in files { | ||||
| 			filepath := os.join_path_single(app.repo_dir, file.filename) | ||||
| fn reader_to_file(mut reader io.BufferedReader, path string) ? { | ||||
| 	// Open up a file for writing to | ||||
| 	mut file := os.create(path) ? | ||||
| 	defer { | ||||
| 		file.close() | ||||
| 	} | ||||
| 
 | ||||
| 			if os.exists(filepath) { | ||||
| 				return app.text('File already exists.') | ||||
| 	mut buf := []byte{len: buf_size} | ||||
| 
 | ||||
| 	// Repeat as long as the stream still has data | ||||
| 	for { | ||||
| 		// TODO don't just endlessly loop if reading keeps failing | ||||
| 		println('heey') | ||||
| 		// TODO check if just breaking here is safe | ||||
| 		bytes_read := reader.read(mut &buf) or { | ||||
| 			println('youre here') | ||||
| 			break | ||||
| 		} | ||||
| 		println(bytes_read) | ||||
| 
 | ||||
| 		mut to_write := bytes_read | ||||
| 
 | ||||
| 		for to_write > 0 { | ||||
| 			// TODO don't just loop infinitely here | ||||
| 			bytes_written := file.write(buf[bytes_read - to_write..bytes_read]) or { | ||||
| 				println("$err.msg") | ||||
| 				continue | ||||
| 			} | ||||
| 			println(bytes_written) | ||||
| 
 | ||||
| 			os.write_file(filepath, file.data) or { | ||||
| 				return app.text('Failed to upload file.') | ||||
| 			} | ||||
| 
 | ||||
| 			return app.text('yeet') | ||||
| 
 | ||||
| 			to_write = to_write - bytes_written | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return app.text('done') | ||||
| 	println('File complete!') | ||||
| } | ||||
| 
 | ||||
| [put; '/pkgs/:pkg'] | ||||
| fn (mut app App) put_package(pkg string) web.Result { | ||||
| 	full_path := os.join_path_single(app.repo_dir, pkg) | ||||
| 
 | ||||
| 	if os.exists(full_path) { | ||||
| 		return app.text('File already exists.') | ||||
| 	} | ||||
| 
 | ||||
| 	reader_to_file(mut app.reader, full_path) or { | ||||
| 		return app.text('Failed to upload file.') | ||||
| 	} | ||||
| 
 | ||||
| 	return app.text('just stop') | ||||
| } | ||||
| 
 | ||||
| // ['/publish'; post] | ||||
| // fn (mut app App) put_package(filename string) web.Result { | ||||
| // 	for _, files in app.files { | ||||
| // 		for file in files { | ||||
| // 			filepath := os.join_path_single(app.repo_dir, file.filename) | ||||
| 
 | ||||
| // 			if os.exists(filepath) { | ||||
| // 				return app.text('File already exists.') | ||||
| // 			} | ||||
| 
 | ||||
| // 			os.write_file(filepath, file.data) or { return app.text('Failed to upload file.') } | ||||
| 
 | ||||
| // 			return app.text('yeet') | ||||
| // 		} | ||||
| // 	} | ||||
| 
 | ||||
| // 	return app.text('done') | ||||
| // } | ||||
| 
 | ||||
| fn main() { | ||||
| 	// Configure logger | ||||
| 	log_level_str := os.getenv_opt('LOG_LEVEL') or { 'WARN' } | ||||
| 	log_level := log.level_from_tag(log_level_str) or { | ||||
| 		exit_with_message(1, 'Invalid log level. The allowed values are FATAL, ERROR, WARN, INFO & DEBUG.') } | ||||
| 		exit_with_message(1, 'Invalid log level. The allowed values are FATAL, ERROR, WARN, INFO & DEBUG.') | ||||
| 	} | ||||
| 	log_file := os.getenv_opt('LOG_FILE') or { 'vieter.log' } | ||||
| 
 | ||||
| 	mut logger := log.Log{ | ||||
|  | @ -62,7 +114,7 @@ fn main() { | |||
| 	logger.info('Logger set up.') | ||||
| 	logger.flush() | ||||
| 
 | ||||
| 	// Configure vweb server | ||||
| 	// Configure web server | ||||
| 	key := os.getenv_opt('API_KEY') or { exit_with_message(1, 'No API key was provided.') } | ||||
| 	repo_dir := os.getenv_opt('REPO_DIR') or { | ||||
| 		exit_with_message(1, 'No repo directory was configured.') | ||||
|  | @ -70,14 +122,16 @@ fn main() { | |||
| 
 | ||||
| 	// We create the upload directory during startup | ||||
| 	if !os.is_dir(repo_dir) { | ||||
| 		os.mkdir_all(repo_dir) or { exit_with_message(2, "Failed to create repo directory '$repo_dir'.") } | ||||
| 		os.mkdir_all(repo_dir) or { | ||||
| 			exit_with_message(2, "Failed to create repo directory '$repo_dir'.") | ||||
| 		} | ||||
| 
 | ||||
| 		println("Repo directory '$repo_dir' created.") | ||||
| 	} | ||||
| 
 | ||||
| 	vweb.run(&App{ | ||||
| 		api_key: key, | ||||
| 		repo_dir: repo_dir, | ||||
| 	web.run(&App{ | ||||
| 		api_key: key | ||||
| 		repo_dir: repo_dir | ||||
| 		logger: logger | ||||
| 	}, port) | ||||
| } | ||||
|  |  | |||
|  | @ -1,141 +0,0 @@ | |||
| # vweb - the V Web Server # | ||||
| 
 | ||||
| A simple yet powerful web server with built-in routing, parameter handling, | ||||
| templating, and other features. | ||||
| 
 | ||||
| ## Alpha level software ## | ||||
| 
 | ||||
| Some features may not be complete, and there may still be bugs.  However, it is | ||||
| still a very useful tool.  The [gitly](https://gitly.org/) site is based on vweb. | ||||
| 
 | ||||
| ## Features ## | ||||
| 
 | ||||
| - **Very fast** performance of C on the web. | ||||
| - **Small binary** hello world website is <100 KB. | ||||
| - **Easy to deploy** just one binary file that also includes all templates. | ||||
|   No need to install any dependencies. | ||||
| - **Templates are precompiled** all errors are visible at compilation time, | ||||
|   not at runtime. | ||||
| 
 | ||||
| There is no formal documentation yet - here is a simple | ||||
| [example](https://github.com/vlang/v/tree/master/examples/vweb/vweb_example.v) | ||||
| 
 | ||||
| There's also the V forum, [vorum](https://github.com/vlang/vorum) | ||||
| 
 | ||||
| `vorum.v` contains all GET and POST actions. | ||||
| 
 | ||||
| ```v ignore | ||||
| pub fn (app mut App) index() { | ||||
| 	posts := app.find_all_posts() | ||||
| 	$vweb.html() | ||||
| } | ||||
| 
 | ||||
| // TODO ['/post/:id/:title'] | ||||
| // TODO `fn (app App) post(id int)` | ||||
| pub fn (app App) post() { | ||||
| 	id := app.get_post_id() | ||||
| 	post := app.retrieve_post(id) or { | ||||
| 		app.redirect('/') | ||||
| 		return | ||||
| 	} | ||||
| 	comments := app.find_comments(id) | ||||
| 	show_form := true | ||||
| 	$vweb.html() | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| `index.html` is an example of the V template language: | ||||
| 
 | ||||
| ```html | ||||
| @for post in posts | ||||
| 	<div class=post> | ||||
| 		<a class=topic href="@post.url">@post.title</a> | ||||
| 		<img class=comment-img> | ||||
| 		<span class=nr-comments>@post.nr_comments</span> | ||||
| 		<span class=time>@post.time</span> | ||||
| 	</div> | ||||
| @end | ||||
| ``` | ||||
| 
 | ||||
| `$vweb.html()` compiles an HTML template into V during compilation, | ||||
| and embeds the resulting code into the current action. | ||||
| 
 | ||||
| That means that the template automatically has access to that action's entire environment. | ||||
| 
 | ||||
| ## Deploying vweb apps ## | ||||
| 
 | ||||
| Everything, including HTML templates, is in one binary file. That's all you need to deploy. | ||||
| 
 | ||||
| ## Getting Started ## | ||||
| 
 | ||||
| To start with vweb, you have to import the module `vweb`.  After the import, | ||||
| define a struct to hold vweb.Context (and any other variables your program will | ||||
| need). | ||||
| 
 | ||||
| The web server can be started by calling `vweb.run(&App{}, port)`. | ||||
| 
 | ||||
| **Example:** | ||||
| 
 | ||||
| ```v ignore | ||||
| import vweb | ||||
| 
 | ||||
| struct App { | ||||
|     vweb.Context | ||||
| } | ||||
| 
 | ||||
| fn main() { | ||||
| 	vweb.run(&App{}, 8080) | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Defining endpoints ### | ||||
| 
 | ||||
| To add endpoints to your web server, you have to extend the `App` struct. | ||||
| For routing you can either use auto-mapping of function names or specify the path as an attribute. | ||||
| The function expects a response of the type `vweb.Result`. | ||||
| 
 | ||||
| **Example:** | ||||
| 
 | ||||
| ```v ignore | ||||
| // This endpoint can be accessed via http://localhost:port/hello | ||||
| fn (mut app App) hello() vweb.Result { | ||||
| 	return app.text('Hello') | ||||
| } | ||||
| 
 | ||||
| // This endpoint can be accessed via http://localhost:port/foo | ||||
| ["/foo"] | ||||
| fn (mut app App) world() vweb.Result { | ||||
| 	return app.text('World') | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| To create an HTTP POST endpoint, you simply add a `[post]` attribute before the function definition. | ||||
| 
 | ||||
| **Example:** | ||||
| 
 | ||||
| ```v ignore | ||||
| [post] | ||||
| fn (mut app App) world() vweb.Result { | ||||
| 	return app.text('World') | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| To pass a parameter to an endpoint, you simply define it inside | ||||
| an attribute, e. g. `['/hello/:user]`. | ||||
| After it is defined in the attribute, you have to add it as a function parameter. | ||||
| 
 | ||||
| **Example:** | ||||
| 
 | ||||
| ```v ignore | ||||
| ['/hello/:user'] | ||||
| fn (mut app App) hello_user(user string) vweb.Result { | ||||
| 	return app.text('Hello $user') | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| You have access to the raw request data such as headers | ||||
| or the request body by accessing `app` (which is `vweb.Context`). | ||||
| If you want to read the request body, you can do that by calling `app.req.data`. | ||||
| To read the request headers, you just call `app.req.header` and access the | ||||
| header you want, e.g. `app.req.header.get(.content_type)`. See `struct Header` | ||||
| for all available methods (`v doc net.http Header`). | ||||
|  | @ -1,201 +0,0 @@ | |||
| module assets | ||||
| 
 | ||||
| // this module provides an AssetManager for combining | ||||
| // and caching javascript & css. | ||||
| import os | ||||
| import time | ||||
| import crypto.md5 | ||||
| 
 | ||||
| const ( | ||||
| 	unknown_asset_type_error = 'vweb.assets: unknown asset type' | ||||
| ) | ||||
| 
 | ||||
| struct AssetManager { | ||||
| mut: | ||||
| 	css []Asset | ||||
| 	js  []Asset | ||||
| pub mut: | ||||
| 	// when true assets will be minified | ||||
| 	minify bool | ||||
| 	// the directory to store the cached/combined files | ||||
| 	cache_dir string | ||||
| } | ||||
| 
 | ||||
| struct Asset { | ||||
| 	file_path     string | ||||
| 	last_modified time.Time | ||||
| } | ||||
| 
 | ||||
| // new_manager returns a new AssetManager | ||||
| pub fn new_manager() &AssetManager { | ||||
| 	return &AssetManager{} | ||||
| } | ||||
| 
 | ||||
| // add_css adds a css asset | ||||
| pub fn (mut am AssetManager) add_css(file string) bool { | ||||
| 	return am.add('css', file) | ||||
| } | ||||
| 
 | ||||
| // add_js adds a js asset | ||||
| pub fn (mut am AssetManager) add_js(file string) bool { | ||||
| 	return am.add('js', file) | ||||
| } | ||||
| 
 | ||||
| // combine_css returns the combined css as a string when to_file is false | ||||
| // when to_file is true it combines the css to disk and returns the path of the file | ||||
| pub fn (am AssetManager) combine_css(to_file bool) string { | ||||
| 	return am.combine('css', to_file) | ||||
| } | ||||
| 
 | ||||
| // combine_js returns the combined js as a string when to_file is false | ||||
| // when to_file is true it combines the css to disk and returns the path of the file | ||||
| pub fn (am AssetManager) combine_js(to_file bool) string { | ||||
| 	return am.combine('js', to_file) | ||||
| } | ||||
| 
 | ||||
| // include_css returns the html <link> tag(s) for including the css files in a page. | ||||
| // when combine is true the files are combined. | ||||
| pub fn (am AssetManager) include_css(combine bool) string { | ||||
| 	return am.include('css', combine) | ||||
| } | ||||
| 
 | ||||
| // include_js returns the html <script> tag(s) for including the js files in a page. | ||||
| // when combine is true the files are combined. | ||||
| pub fn (am AssetManager) include_js(combine bool) string { | ||||
| 	return am.include('js', combine) | ||||
| } | ||||
| 
 | ||||
| fn (am AssetManager) combine(asset_type string, to_file bool) string { | ||||
| 	if am.cache_dir == '' { | ||||
| 		panic('vweb.assets: you must set a cache dir.') | ||||
| 	} | ||||
| 	cache_key := am.get_cache_key(asset_type) | ||||
| 	out_file := '$am.cache_dir/${cache_key}.$asset_type' | ||||
| 	mut out := '' | ||||
| 	// use cache | ||||
| 	if os.exists(out_file) { | ||||
| 		if to_file { | ||||
| 			return out_file | ||||
| 		} | ||||
| 		cached := os.read_file(out_file) or { return '' } | ||||
| 		return cached | ||||
| 	} | ||||
| 	// rebuild | ||||
| 	for asset in am.get_assets(asset_type) { | ||||
| 		data := os.read_file(asset.file_path) or { return '' } | ||||
| 		out += data | ||||
| 	} | ||||
| 	if am.minify { | ||||
| 		if asset_type == 'css' { | ||||
| 			out = minify_css(out) | ||||
| 		} else { | ||||
| 			out = minify_js(out) | ||||
| 		} | ||||
| 	} | ||||
| 	if !to_file { | ||||
| 		return out | ||||
| 	} | ||||
| 	if !os.is_dir(am.cache_dir) { | ||||
| 		os.mkdir(am.cache_dir) or { panic(err) } | ||||
| 	} | ||||
| 	mut file := os.create(out_file) or { panic(err) } | ||||
| 	file.write(out.bytes()) or { panic(err) } | ||||
| 	file.close() | ||||
| 	return out_file | ||||
| } | ||||
| 
 | ||||
| fn (am AssetManager) get_cache_key(asset_type string) string { | ||||
| 	mut files_salt := '' | ||||
| 	mut latest_modified := i64(0) | ||||
| 	for asset in am.get_assets(asset_type) { | ||||
| 		files_salt += asset.file_path | ||||
| 		if asset.last_modified.unix > latest_modified { | ||||
| 			latest_modified = asset.last_modified.unix | ||||
| 		} | ||||
| 	} | ||||
| 	hash := md5.sum(files_salt.bytes()).hex() | ||||
| 	return '$hash-$latest_modified' | ||||
| } | ||||
| 
 | ||||
| fn (am AssetManager) include(asset_type string, combine bool) string { | ||||
| 	assets := am.get_assets(asset_type) | ||||
| 	mut out := '' | ||||
| 	if asset_type == 'css' { | ||||
| 		if combine { | ||||
| 			file := am.combine(asset_type, true) | ||||
| 			return '<link rel="stylesheet" href="$file">\n' | ||||
| 		} | ||||
| 		for asset in assets { | ||||
| 			out += '<link rel="stylesheet" href="$asset.file_path">\n' | ||||
| 		} | ||||
| 	} | ||||
| 	if asset_type == 'js' { | ||||
| 		if combine { | ||||
| 			file := am.combine(asset_type, true) | ||||
| 			return '<script type="text/javascript" src="$file"></script>\n' | ||||
| 		} | ||||
| 		for asset in assets { | ||||
| 			out += '<script type="text/javascript" src="$asset.file_path"></script>\n' | ||||
| 		} | ||||
| 	} | ||||
| 	return out | ||||
| } | ||||
| 
 | ||||
| // dont return option until size limit is removed | ||||
| // fn (mut am AssetManager) add(asset_type, file string) ?bool { | ||||
| fn (mut am AssetManager) add(asset_type string, file string) bool { | ||||
| 	if !os.exists(file) { | ||||
| 		// return error('vweb.assets: cannot add asset $file, it does not exist') | ||||
| 		return false | ||||
| 	} | ||||
| 	asset := Asset{ | ||||
| 		file_path: file | ||||
| 		last_modified: time.Time{ | ||||
| 			unix: os.file_last_mod_unix(file) | ||||
| 		} | ||||
| 	} | ||||
| 	if asset_type == 'css' { | ||||
| 		am.css << asset | ||||
| 	} else if asset_type == 'js' { | ||||
| 		am.js << asset | ||||
| 	} else { | ||||
| 		panic('$assets.unknown_asset_type_error ($asset_type).') | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| fn (am AssetManager) exists(asset_type string, file string) bool { | ||||
| 	assets := am.get_assets(asset_type) | ||||
| 	for asset in assets { | ||||
| 		if asset.file_path == file { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| fn (am AssetManager) get_assets(asset_type string) []Asset { | ||||
| 	if asset_type != 'css' && asset_type != 'js' { | ||||
| 		panic('$assets.unknown_asset_type_error ($asset_type).') | ||||
| 	} | ||||
| 	assets := if asset_type == 'css' { am.css } else { am.js } | ||||
| 	return assets | ||||
| } | ||||
| 
 | ||||
| // todo: implement proper minification | ||||
| pub fn minify_css(css string) string { | ||||
| 	mut lines := css.split('\n') | ||||
| 	for i, _ in lines { | ||||
| 		lines[i] = lines[i].trim_space() | ||||
| 	} | ||||
| 	return lines.join(' ') | ||||
| } | ||||
| 
 | ||||
| // todo: implement proper minification | ||||
| pub fn minify_js(js string) string { | ||||
| 	mut lines := js.split('\n') | ||||
| 	for i, _ in lines { | ||||
| 		lines[i] = lines[i].trim_space() | ||||
| 	} | ||||
| 	return lines.join(' ') | ||||
| } | ||||
|  | @ -1,179 +0,0 @@ | |||
| import vweb.assets | ||||
| import os | ||||
| 
 | ||||
| // clean_cache_dir used before and after tests that write to a cache directory. | ||||
| // Because of parallel compilation and therefore test running, | ||||
| // unique cache dirs are needed per test function. | ||||
| fn clean_cache_dir(dir string) { | ||||
| 	if os.is_dir(dir) { | ||||
| 		os.rmdir_all(dir) or { panic(err) } | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| fn base_cache_dir() string { | ||||
| 	return os.join_path(os.temp_dir(), 'assets_test_cache') | ||||
| } | ||||
| 
 | ||||
| fn cache_dir(test_name string) string { | ||||
| 	return os.join_path(base_cache_dir(), test_name) | ||||
| } | ||||
| 
 | ||||
| fn get_test_file_path(file string) string { | ||||
| 	path := os.join_path(base_cache_dir(), file) | ||||
| 	if !os.is_dir(base_cache_dir()) { | ||||
| 		os.mkdir_all(base_cache_dir()) or { panic(err) } | ||||
| 	} | ||||
| 	if !os.exists(path) { | ||||
| 		os.write_file(path, get_test_file_contents(file)) or { panic(err) } | ||||
| 	} | ||||
| 	return path | ||||
| } | ||||
| 
 | ||||
| fn get_test_file_contents(file string) string { | ||||
| 	contents := match file { | ||||
| 		'test1.js' { '{"one": 1}\n' } | ||||
| 		'test2.js' { '{"two": 2}\n' } | ||||
| 		'test1.css' { '.one {\n\tcolor: #336699;\n}\n' } | ||||
| 		'test2.css' { '.two {\n\tcolor: #996633;\n}\n' } | ||||
| 		else { 'wibble\n' } | ||||
| 	} | ||||
| 	return contents | ||||
| } | ||||
| 
 | ||||
| fn test_set_cache() { | ||||
| 	mut am := assets.new_manager() | ||||
| 	am.cache_dir = 'cache' | ||||
| } | ||||
| 
 | ||||
| fn test_set_minify() { | ||||
| 	mut am := assets.new_manager() | ||||
| 	am.minify = true | ||||
| } | ||||
| 
 | ||||
| fn test_add() { | ||||
| 	mut am := assets.new_manager() | ||||
| 	assert am.add('css', 'testx.css') == false | ||||
| 	assert am.add('css', get_test_file_path('test1.css')) == true | ||||
| 	assert am.add('js', get_test_file_path('test1.js')) == true | ||||
| 	// assert am.add('css', get_test_file_path('test2.js')) == false // TODO: test extension on add | ||||
| } | ||||
| 
 | ||||
| fn test_add_css() { | ||||
| 	mut am := assets.new_manager() | ||||
| 	assert am.add_css('testx.css') == false | ||||
| 	assert am.add_css(get_test_file_path('test1.css')) == true | ||||
| 	// assert am.add_css(get_test_file_path('test1.js')) == false // TODO: test extension on add | ||||
| } | ||||
| 
 | ||||
| fn test_add_js() { | ||||
| 	mut am := assets.new_manager() | ||||
| 	assert am.add_js('testx.js') == false | ||||
| 	assert am.add_css(get_test_file_path('test1.js')) == true | ||||
| 	// assert am.add_css(get_test_file_path('test1.css')) == false // TODO: test extension on add | ||||
| } | ||||
| 
 | ||||
| fn test_combine_css() { | ||||
| 	mut am := assets.new_manager() | ||||
| 	am.cache_dir = cache_dir('test_combine_css') | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| 	am.add_css(get_test_file_path('test1.css')) | ||||
| 	am.add_css(get_test_file_path('test2.css')) | ||||
| 	// TODO: How do I test non-minified, is there a "here doc" format that keeps formatting? | ||||
| 	am.minify = true | ||||
| 	expected := '.one { color: #336699; } .two { color: #996633; } ' | ||||
| 	actual := am.combine_css(false) | ||||
| 	assert actual == expected | ||||
| 	assert actual.contains(expected) | ||||
| 	// Test cache path doesn't change when input files and minify setting do not. | ||||
| 	path1 := am.combine_css(true) | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| 	path2 := am.combine_css(true) | ||||
| 	assert path1 == path2 | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| } | ||||
| 
 | ||||
| fn test_combine_js() { | ||||
| 	mut am := assets.new_manager() | ||||
| 	am.cache_dir = cache_dir('test_combine_js') | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| 	am.add_js(get_test_file_path('test1.js')) | ||||
| 	am.add_js(get_test_file_path('test2.js')) | ||||
| 	expected1 := '{"one": 1}' | ||||
| 	expected2 := '{"two": 2}' | ||||
| 	expected := expected1 + '\n' + expected2 + '\n' | ||||
| 	actual := am.combine_js(false) | ||||
| 	assert actual == expected | ||||
| 	assert actual.contains(expected) | ||||
| 	assert actual.contains(expected1) | ||||
| 	assert actual.contains(expected2) | ||||
| 	am.minify = true | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| 	expected3 := expected1 + ' ' + expected2 + ' ' | ||||
| 	actual2 := am.combine_js(false) | ||||
| 	assert actual2 == expected3 | ||||
| 	assert actual2.contains(expected3) | ||||
| 	// Test cache path doesn't change when input files and minify setting do not. | ||||
| 	path1 := am.combine_js(true) | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| 	path2 := am.combine_js(true) | ||||
| 	assert path1 == path2 | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| } | ||||
| 
 | ||||
| fn test_include_css() { | ||||
| 	mut am := assets.new_manager() | ||||
| 	file1 := get_test_file_path('test1.css') | ||||
| 	am.add_css(file1) | ||||
| 	expected := '<link rel="stylesheet" href="$file1">\n' | ||||
| 	actual := am.include_css(false) | ||||
| 	assert actual == expected | ||||
| 	assert actual.contains(expected) | ||||
| 	// Two lines of output. | ||||
| 	file2 := get_test_file_path('test2.css') | ||||
| 	am.add_css(file2) | ||||
| 	am.cache_dir = cache_dir('test_include_css') | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| 	expected2 := expected + '<link rel="stylesheet" href="$file2">\n' | ||||
| 	actual2 := am.include_css(false) | ||||
| 	assert actual2 == expected2 | ||||
| 	assert actual2.contains(expected2) | ||||
| 	// Combined output. | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| 	actual3 := am.include_css(true) | ||||
| 	assert actual3.contains(expected2) == false | ||||
| 	assert actual3.starts_with('<link rel="stylesheet" href="$am.cache_dir/') == true | ||||
| 	// Test cache path doesn't change when input files and minify setting do not. | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| 	actual4 := am.include_css(true) | ||||
| 	assert actual4 == actual3 | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| } | ||||
| 
 | ||||
| fn test_include_js() { | ||||
| 	mut am := assets.new_manager() | ||||
| 	file1 := get_test_file_path('test1.js') | ||||
| 	am.add_js(file1) | ||||
| 	expected := '<script type="text/javascript" src="$file1"></script>\n' | ||||
| 	actual := am.include_js(false) | ||||
| 	assert actual == expected | ||||
| 	assert actual.contains(expected) | ||||
| 	// Two lines of output. | ||||
| 	file2 := get_test_file_path('test2.js') | ||||
| 	am.add_js(file2) | ||||
| 	am.cache_dir = cache_dir('test_include_js') | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| 	expected2 := expected + '<script type="text/javascript" src="$file2"></script>\n' | ||||
| 	actual2 := am.include_js(false) | ||||
| 	assert actual2 == expected2 | ||||
| 	assert actual2.contains(expected2) | ||||
| 	// Combined output. | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| 	actual3 := am.include_js(true) | ||||
| 	assert actual3.contains(expected2) == false | ||||
| 	assert actual3.starts_with('<script type="text/javascript" src="$am.cache_dir/') | ||||
| 	// Test cache path doesn't change when input files and minify setting do not. | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| 	actual4 := am.include_js(true) | ||||
| 	assert actual4 == actual3 | ||||
| 	clean_cache_dir(am.cache_dir) | ||||
| } | ||||
|  | @ -1,282 +0,0 @@ | |||
| module vweb | ||||
| 
 | ||||
| struct RoutePair { | ||||
| 	url   string | ||||
| 	route string | ||||
| } | ||||
| 
 | ||||
| fn (rp RoutePair) test() ?[]string { | ||||
| 	url := rp.url.split('/').filter(it != '') | ||||
| 	route := rp.route.split('/').filter(it != '') | ||||
| 	return route_matches(url, route) | ||||
| } | ||||
| 
 | ||||
| fn (rp RoutePair) test_match() { | ||||
| 	rp.test() or { panic('should match: $rp') } | ||||
| } | ||||
| 
 | ||||
| fn (rp RoutePair) test_no_match() { | ||||
| 	rp.test() or { return } | ||||
| 	panic('should not match: $rp') | ||||
| } | ||||
| 
 | ||||
| fn (rp RoutePair) test_param(expected []string) { | ||||
| 	res := rp.test() or { panic('should match: $rp') } | ||||
| 	assert res == expected | ||||
| } | ||||
| 
 | ||||
| fn test_route_no_match() { | ||||
| 	tests := [ | ||||
| 		RoutePair{ | ||||
| 			url: '/a' | ||||
| 			route: '/a/b/c' | ||||
| 		}, | ||||
| 		RoutePair{ | ||||
| 			url: '/a/' | ||||
| 			route: '/a/b/c' | ||||
| 		}, | ||||
| 		RoutePair{ | ||||
| 			url: '/a/b' | ||||
| 			route: '/a/b/c' | ||||
| 		}, | ||||
| 		RoutePair{ | ||||
| 			url: '/a/b/' | ||||
| 			route: '/a/b/c' | ||||
| 		}, | ||||
| 		RoutePair{ | ||||
| 			url: '/a/c/b' | ||||
| 			route: '/a/b/c' | ||||
| 		}, | ||||
| 		RoutePair{ | ||||
| 			url: '/a/c/b/' | ||||
| 			route: '/a/b/c' | ||||
| 		}, | ||||
| 		RoutePair{ | ||||
| 			url: '/a/b/c/d' | ||||
| 			route: '/a/b/c' | ||||
| 		}, | ||||
| 		RoutePair{ | ||||
| 			url: '/a/b/c' | ||||
| 			route: '/' | ||||
| 		}, | ||||
| 	] | ||||
| 	for test in tests { | ||||
| 		test.test_no_match() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| fn test_route_exact_match() { | ||||
| 	tests := [ | ||||
| 		RoutePair{ | ||||
| 			url: '/a/b/c' | ||||
| 			route: '/a/b/c' | ||||
| 		}, | ||||
| 		RoutePair{ | ||||
| 			url: '/a/b/c/' | ||||
| 			route: '/a/b/c' | ||||
| 		}, | ||||
| 		RoutePair{ | ||||
| 			url: '/a' | ||||
| 			route: '/a' | ||||
| 		}, | ||||
| 		RoutePair{ | ||||
| 			url: '/' | ||||
| 			route: '/' | ||||
| 		}, | ||||
| 	] | ||||
| 	for test in tests { | ||||
| 		test.test_match() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| fn test_route_params_match() { | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c' | ||||
| 		route: '/:a/b/c' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c' | ||||
| 		route: '/a/:b/c' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c' | ||||
| 		route: '/a/b/:c' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c' | ||||
| 		route: '/:a/b/:c' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c' | ||||
| 		route: '/:a/:b/:c' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/two/three' | ||||
| 		route: '/:a/:b/:c' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/b/c' | ||||
| 		route: '/:a/b/c' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/two/three' | ||||
| 		route: '/:a/b/c' | ||||
| 	}.test_no_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/two/three' | ||||
| 		route: '/:a/:b/c' | ||||
| 	}.test_no_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/two/three' | ||||
| 		route: '/:a/b/:c' | ||||
| 	}.test_no_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c/d' | ||||
| 		route: '/:a/:b/:c' | ||||
| 	}.test_no_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/1/2/3/4' | ||||
| 		route: '/:a/:b/:c' | ||||
| 	}.test_no_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b' | ||||
| 		route: '/:a/:b/:c' | ||||
| 	}.test_no_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/1/2' | ||||
| 		route: '/:a/:b/:c' | ||||
| 	}.test_no_match() | ||||
| } | ||||
| 
 | ||||
| fn test_route_params() { | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c' | ||||
| 		route: '/:a/b/c' | ||||
| 	}.test_param(['a']) | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/b/c' | ||||
| 		route: '/:a/b/c' | ||||
| 	}.test_param(['one']) | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/two/c' | ||||
| 		route: '/:a/:b/c' | ||||
| 	}.test_param(['one', 'two']) | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/two/three' | ||||
| 		route: '/:a/:b/:c' | ||||
| 	}.test_param(['one', 'two', 'three']) | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/b/three' | ||||
| 		route: '/:a/b/:c' | ||||
| 	}.test_param(['one', 'three']) | ||||
| } | ||||
| 
 | ||||
| fn test_route_params_array_match() { | ||||
| 	// array can only be used on the last word (TODO: add parsing / tests to ensure this) | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c' | ||||
| 		route: '/a/b/:c...' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c/d' | ||||
| 		route: '/a/b/:c...' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c/d/e' | ||||
| 		route: '/a/b/:c...' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/b/c/d/e' | ||||
| 		route: '/:a/b/:c...' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/two/c/d/e' | ||||
| 		route: '/:a/:b/:c...' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/two/three/four/five' | ||||
| 		route: '/:a/:b/:c...' | ||||
| 	}.test_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b' | ||||
| 		route: '/:a/:b/:c...' | ||||
| 	}.test_no_match() | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/' | ||||
| 		route: '/:a/:b/:c...' | ||||
| 	}.test_no_match() | ||||
| } | ||||
| 
 | ||||
| fn test_route_params_array() { | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c' | ||||
| 		route: '/a/b/:c...' | ||||
| 	}.test_param(['c']) | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c/d' | ||||
| 		route: '/a/b/:c...' | ||||
| 	}.test_param(['c/d']) | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c/d/' | ||||
| 		route: '/a/b/:c...' | ||||
| 	}.test_param(['c/d']) | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/a/b/c/d/e' | ||||
| 		route: '/a/b/:c...' | ||||
| 	}.test_param(['c/d/e']) | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/b/c/d/e' | ||||
| 		route: '/:a/b/:c...' | ||||
| 	}.test_param(['one', 'c/d/e']) | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/two/c/d/e' | ||||
| 		route: '/:a/:b/:c...' | ||||
| 	}.test_param(['one', 'two', 'c/d/e']) | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/one/two/three/d/e' | ||||
| 		route: '/:a/:b/:c...' | ||||
| 	}.test_param(['one', 'two', 'three/d/e']) | ||||
| } | ||||
| 
 | ||||
| fn test_route_index_path() { | ||||
| 	RoutePair{ | ||||
| 		url: '/' | ||||
| 		route: '/:path...' | ||||
| 	}.test_param(['/']) | ||||
| 
 | ||||
| 	RoutePair{ | ||||
| 		url: '/foo/bar' | ||||
| 		route: '/:path...' | ||||
| 	}.test_param(['/foo/bar']) | ||||
| } | ||||
|  | @ -1,77 +0,0 @@ | |||
| module sse | ||||
| 
 | ||||
| import net | ||||
| import time | ||||
| import strings | ||||
| 
 | ||||
| // This module implements the server side of `Server Sent Events`. | ||||
| // See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format | ||||
| // as well as https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events | ||||
| // for detailed description of the protocol, and a simple web browser client example. | ||||
| // | ||||
| // > Event stream format | ||||
| // > The event stream is a simple stream of text data which must be encoded using UTF-8. | ||||
| // > Messages in the event stream are separated by a pair of newline characters. | ||||
| // > A colon as the first character of a line is in essence a comment, and is ignored. | ||||
| // > Note: The comment line can be used to prevent connections from timing out; | ||||
| // > a server can send a comment periodically to keep the connection alive. | ||||
| // > | ||||
| // > Each message consists of one or more lines of text listing the fields for that message. | ||||
| // > Each field is represented by the field name, followed by a colon, followed by the text | ||||
| // > data for that field's value. | ||||
| 
 | ||||
| [heap] | ||||
| pub struct SSEConnection { | ||||
| pub mut: | ||||
| 	headers       map[string]string | ||||
| 	conn          &net.TcpConn | ||||
| 	write_timeout time.Duration = 600 * time.second | ||||
| } | ||||
| 
 | ||||
| pub struct SSEMessage { | ||||
| 	id    string | ||||
| 	event string | ||||
| 	data  string | ||||
| 	retry int | ||||
| } | ||||
| 
 | ||||
| pub fn new_connection(conn &net.TcpConn) &SSEConnection { | ||||
| 	return &SSEConnection{ | ||||
| 		conn: unsafe { conn } | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // sse_start is used to send the start of a Server Side Event response. | ||||
| pub fn (mut sse SSEConnection) start() ? { | ||||
| 	sse.conn.set_write_timeout(sse.write_timeout) | ||||
| 	mut start_sb := strings.new_builder(512) | ||||
| 	start_sb.write_string('HTTP/1.1 200') | ||||
| 	start_sb.write_string('\r\nConnection: keep-alive') | ||||
| 	start_sb.write_string('\r\nCache-Control: no-cache') | ||||
| 	start_sb.write_string('\r\nContent-Type: text/event-stream') | ||||
| 	for k, v in sse.headers { | ||||
| 		start_sb.write_string('\r\n$k: $v') | ||||
| 	} | ||||
| 	start_sb.write_string('\r\n') | ||||
| 	sse.conn.write(start_sb) or { return error('could not start sse response') } | ||||
| } | ||||
| 
 | ||||
| // send_message sends a single message to the http client that listens for SSE. | ||||
| // It does not close the connection, so you can use it many times in a loop. | ||||
| pub fn (mut sse SSEConnection) send_message(message SSEMessage) ? { | ||||
| 	mut sb := strings.new_builder(512) | ||||
| 	if message.id != '' { | ||||
| 		sb.write_string('id: $message.id\n') | ||||
| 	} | ||||
| 	if message.event != '' { | ||||
| 		sb.write_string('event: $message.event\n') | ||||
| 	} | ||||
| 	if message.data != '' { | ||||
| 		sb.write_string('data: $message.data\n') | ||||
| 	} | ||||
| 	if message.retry != 0 { | ||||
| 		sb.write_string('retry: $message.retry\n') | ||||
| 	} | ||||
| 	sb.write_string('\n') | ||||
| 	sse.conn.write(sb) ? | ||||
| } | ||||
|  | @ -1,300 +0,0 @@ | |||
| import os | ||||
| import time | ||||
| import json | ||||
| import net | ||||
| import net.http | ||||
| import io | ||||
| 
 | ||||
| const ( | ||||
| 	sport           = 12380 | ||||
| 	exit_after_time = 12000 // milliseconds | ||||
| 	vexe            = os.getenv('VEXE') | ||||
| 	vweb_logfile    = os.getenv('VWEB_LOGFILE') | ||||
| 	vroot           = os.dir(vexe) | ||||
| 	serverexe       = os.join_path(os.cache_dir(), 'vweb_test_server.exe') | ||||
| 	tcp_r_timeout   = 30 * time.second | ||||
| 	tcp_w_timeout   = 30 * time.second | ||||
| ) | ||||
| 
 | ||||
| // setup of vweb webserver | ||||
| fn testsuite_begin() { | ||||
| 	os.chdir(vroot) or {} | ||||
| 	if os.exists(serverexe) { | ||||
| 		os.rm(serverexe) or {} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| fn test_a_simple_vweb_app_can_be_compiled() { | ||||
| 	// did_server_compile := os.system('$vexe -g -o $serverexe vlib/vweb/tests/vweb_test_server.v') | ||||
| 	// TODO: find out why it does not compile with -usecache and -g | ||||
| 	did_server_compile := os.system('$vexe -o $serverexe vlib/vweb/tests/vweb_test_server.v') | ||||
| 	assert did_server_compile == 0 | ||||
| 	assert os.exists(serverexe) | ||||
| } | ||||
| 
 | ||||
| fn test_a_simple_vweb_app_runs_in_the_background() { | ||||
| 	mut suffix := '' | ||||
| 	$if !windows { | ||||
| 		suffix = ' > /dev/null &' | ||||
| 	} | ||||
| 	if vweb_logfile != '' { | ||||
| 		suffix = ' 2>> $vweb_logfile >> $vweb_logfile &' | ||||
| 	} | ||||
| 	server_exec_cmd := '$serverexe $sport $exit_after_time $suffix' | ||||
| 	$if debug_net_socket_client ? { | ||||
| 		eprintln('running:\n$server_exec_cmd') | ||||
| 	} | ||||
| 	$if windows { | ||||
| 		go os.system(server_exec_cmd) | ||||
| 	} $else { | ||||
| 		res := os.system(server_exec_cmd) | ||||
| 		assert res == 0 | ||||
| 	} | ||||
| 	$if macos { | ||||
| 		time.sleep(1000 * time.millisecond) | ||||
| 	} $else { | ||||
| 		time.sleep(100 * time.millisecond) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // web client tests follow | ||||
| fn assert_common_headers(received string) { | ||||
| 	assert received.starts_with('HTTP/1.1 200 OK\r\n') | ||||
| 	assert received.contains('Server: VWeb\r\n') | ||||
| 	assert received.contains('Content-Length:') | ||||
| 	assert received.contains('Connection: close\r\n') | ||||
| } | ||||
| 
 | ||||
| fn test_a_simple_tcp_client_can_connect_to_the_vweb_server() { | ||||
| 	received := simple_tcp_client(path: '/') or { | ||||
| 		assert err.msg == '' | ||||
| 		return | ||||
| 	} | ||||
| 	assert_common_headers(received) | ||||
| 	assert received.contains('Content-Type: text/plain') | ||||
| 	assert received.contains('Content-Length: 15') | ||||
| 	assert received.ends_with('Welcome to VWeb') | ||||
| } | ||||
| 
 | ||||
| fn test_a_simple_tcp_client_simple_route() { | ||||
| 	received := simple_tcp_client(path: '/simple') or { | ||||
| 		assert err.msg == '' | ||||
| 		return | ||||
| 	} | ||||
| 	assert_common_headers(received) | ||||
| 	assert received.contains('Content-Type: text/plain') | ||||
| 	assert received.contains('Content-Length: 15') | ||||
| 	assert received.ends_with('A simple result') | ||||
| } | ||||
| 
 | ||||
| fn test_a_simple_tcp_client_zero_content_length() { | ||||
| 	// tests that sending a content-length header of 0 doesn't hang on a read timeout | ||||
| 	watch := time.new_stopwatch(auto_start: true) | ||||
| 	simple_tcp_client(path: '/', headers: 'Content-Length: 0\r\n\r\n') or { | ||||
| 		assert err.msg == '' | ||||
| 		return | ||||
| 	} | ||||
| 	assert watch.elapsed() < 1 * time.second | ||||
| } | ||||
| 
 | ||||
| fn test_a_simple_tcp_client_html_page() { | ||||
| 	received := simple_tcp_client(path: '/html_page') or { | ||||
| 		assert err.msg == '' | ||||
| 		return | ||||
| 	} | ||||
| 	assert_common_headers(received) | ||||
| 	assert received.contains('Content-Type: text/html') | ||||
| 	assert received.ends_with('<h1>ok</h1>') | ||||
| } | ||||
| 
 | ||||
| // net.http client based tests follow: | ||||
| fn assert_common_http_headers(x http.Response) ? { | ||||
| 	assert x.status() == .ok | ||||
| 	assert x.header.get(.server) ? == 'VWeb' | ||||
| 	assert x.header.get(.content_length) ?.int() > 0 | ||||
| 	assert x.header.get(.connection) ? == 'close' | ||||
| } | ||||
| 
 | ||||
| fn test_http_client_index() ? { | ||||
| 	x := http.get('http://127.0.0.1:$sport/') or { panic(err) } | ||||
| 	assert_common_http_headers(x) ? | ||||
| 	assert x.header.get(.content_type) ? == 'text/plain' | ||||
| 	assert x.text == 'Welcome to VWeb' | ||||
| } | ||||
| 
 | ||||
| fn test_http_client_404() ? { | ||||
| 	url_404_list := [ | ||||
| 		'http://127.0.0.1:$sport/zxcnbnm', | ||||
| 		'http://127.0.0.1:$sport/JHKAJA', | ||||
| 		'http://127.0.0.1:$sport/unknown', | ||||
| 	] | ||||
| 	for url in url_404_list { | ||||
| 		res := http.get(url) or { panic(err) } | ||||
| 		assert res.status() == .not_found | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| fn test_http_client_simple() ? { | ||||
| 	x := http.get('http://127.0.0.1:$sport/simple') or { panic(err) } | ||||
| 	assert_common_http_headers(x) ? | ||||
| 	assert x.header.get(.content_type) ? == 'text/plain' | ||||
| 	assert x.text == 'A simple result' | ||||
| } | ||||
| 
 | ||||
| fn test_http_client_html_page() ? { | ||||
| 	x := http.get('http://127.0.0.1:$sport/html_page') or { panic(err) } | ||||
| 	assert_common_http_headers(x) ? | ||||
| 	assert x.header.get(.content_type) ? == 'text/html' | ||||
| 	assert x.text == '<h1>ok</h1>' | ||||
| } | ||||
| 
 | ||||
| fn test_http_client_settings_page() ? { | ||||
| 	x := http.get('http://127.0.0.1:$sport/bilbo/settings') or { panic(err) } | ||||
| 	assert_common_http_headers(x) ? | ||||
| 	assert x.text == 'username: bilbo' | ||||
| 	// | ||||
| 	y := http.get('http://127.0.0.1:$sport/kent/settings') or { panic(err) } | ||||
| 	assert_common_http_headers(y) ? | ||||
| 	assert y.text == 'username: kent' | ||||
| } | ||||
| 
 | ||||
| fn test_http_client_user_repo_settings_page() ? { | ||||
| 	x := http.get('http://127.0.0.1:$sport/bilbo/gostamp/settings') or { panic(err) } | ||||
| 	assert_common_http_headers(x) ? | ||||
| 	assert x.text == 'username: bilbo | repository: gostamp' | ||||
| 	// | ||||
| 	y := http.get('http://127.0.0.1:$sport/kent/golang/settings') or { panic(err) } | ||||
| 	assert_common_http_headers(y) ? | ||||
| 	assert y.text == 'username: kent | repository: golang' | ||||
| 	// | ||||
| 	z := http.get('http://127.0.0.1:$sport/missing/golang/settings') or { panic(err) } | ||||
| 	assert z.status() == .not_found | ||||
| } | ||||
| 
 | ||||
| struct User { | ||||
| 	name string | ||||
| 	age  int | ||||
| } | ||||
| 
 | ||||
| fn test_http_client_json_post() ? { | ||||
| 	ouser := User{ | ||||
| 		name: 'Bilbo' | ||||
| 		age: 123 | ||||
| 	} | ||||
| 	json_for_ouser := json.encode(ouser) | ||||
| 	mut x := http.post_json('http://127.0.0.1:$sport/json_echo', json_for_ouser) or { panic(err) } | ||||
| 	$if debug_net_socket_client ? { | ||||
| 		eprintln('/json_echo endpoint response: $x') | ||||
| 	} | ||||
| 	assert x.header.get(.content_type) ? == 'application/json' | ||||
| 	assert x.text == json_for_ouser | ||||
| 	nuser := json.decode(User, x.text) or { User{} } | ||||
| 	assert '$ouser' == '$nuser' | ||||
| 	// | ||||
| 	x = http.post_json('http://127.0.0.1:$sport/json', json_for_ouser) or { panic(err) } | ||||
| 	$if debug_net_socket_client ? { | ||||
| 		eprintln('/json endpoint response: $x') | ||||
| 	} | ||||
| 	assert x.header.get(.content_type) ? == 'application/json' | ||||
| 	assert x.text == json_for_ouser | ||||
| 	nuser2 := json.decode(User, x.text) or { User{} } | ||||
| 	assert '$ouser' == '$nuser2' | ||||
| } | ||||
| 
 | ||||
| fn test_http_client_multipart_form_data() ? { | ||||
| 	boundary := '6844a625b1f0b299' | ||||
| 	name := 'foo' | ||||
| 	ct := 'multipart/form-data; boundary=$boundary' | ||||
| 	contents := 'baz buzz' | ||||
| 	data := '--$boundary\r | ||||
| Content-Disposition: form-data; name="$name"\r | ||||
| \r | ||||
| $contents\r | ||||
| --$boundary--\r | ||||
| ' | ||||
| 	mut x := http.fetch( | ||||
| 		url: 'http://127.0.0.1:$sport/form_echo' | ||||
| 		method: .post | ||||
| 		header: http.new_header( | ||||
| 			key: .content_type | ||||
| 			value: ct | ||||
| 		) | ||||
| 		data: data | ||||
| 	) ? | ||||
| 	$if debug_net_socket_client ? { | ||||
| 		eprintln('/form_echo endpoint response: $x') | ||||
| 	} | ||||
| 	assert x.text == contents | ||||
| } | ||||
| 
 | ||||
| fn test_http_client_shutdown_does_not_work_without_a_cookie() { | ||||
| 	x := http.get('http://127.0.0.1:$sport/shutdown') or { | ||||
| 		assert err.msg == '' | ||||
| 		return | ||||
| 	} | ||||
| 	assert x.status() == .not_found | ||||
| 	assert x.text == '404 Not Found' | ||||
| } | ||||
| 
 | ||||
| fn testsuite_end() { | ||||
| 	// This test is guaranteed to be called last. | ||||
| 	// It sends a request to the server to shutdown. | ||||
| 	x := http.fetch( | ||||
| 		url: 'http://127.0.0.1:$sport/shutdown' | ||||
| 		method: .get | ||||
| 		cookies: { | ||||
| 			'skey': 'superman' | ||||
| 		} | ||||
| 	) or { | ||||
| 		assert err.msg == '' | ||||
| 		return | ||||
| 	} | ||||
| 	assert x.status() == .ok | ||||
| 	assert x.text == 'good bye' | ||||
| } | ||||
| 
 | ||||
| // utility code: | ||||
| struct SimpleTcpClientConfig { | ||||
| 	retries int    = 20 | ||||
| 	host    string = 'static.dev' | ||||
| 	path    string = '/' | ||||
| 	agent   string = 'v/net.tcp.v' | ||||
| 	headers string = '\r\n' | ||||
| 	content string | ||||
| } | ||||
| 
 | ||||
| fn simple_tcp_client(config SimpleTcpClientConfig) ?string { | ||||
| 	mut client := &net.TcpConn(0) | ||||
| 	mut tries := 0 | ||||
| 	for tries < config.retries { | ||||
| 		tries++ | ||||
| 		client = net.dial_tcp('127.0.0.1:$sport') or { | ||||
| 			if tries > config.retries { | ||||
| 				return err | ||||
| 			} | ||||
| 			time.sleep(100 * time.millisecond) | ||||
| 			continue | ||||
| 		} | ||||
| 		break | ||||
| 	} | ||||
| 	client.set_read_timeout(tcp_r_timeout) | ||||
| 	client.set_write_timeout(tcp_w_timeout) | ||||
| 	defer { | ||||
| 		client.close() or {} | ||||
| 	} | ||||
| 	message := 'GET $config.path HTTP/1.1 | ||||
| Host: $config.host | ||||
| User-Agent: $config.agent | ||||
| Accept: */* | ||||
| $config.headers | ||||
| $config.content' | ||||
| 	$if debug_net_socket_client ? { | ||||
| 		eprintln('sending:\n$message') | ||||
| 	} | ||||
| 	client.write(message.bytes()) ? | ||||
| 	read := io.read_all(reader: client) ? | ||||
| 	$if debug_net_socket_client ? { | ||||
| 		eprintln('received:\n$read') | ||||
| 	} | ||||
| 	return read.bytestr() | ||||
| } | ||||
|  | @ -1,118 +0,0 @@ | |||
| module main | ||||
| 
 | ||||
| import os | ||||
| import vweb | ||||
| import time | ||||
| 
 | ||||
| const ( | ||||
| 	known_users = ['bilbo', 'kent'] | ||||
| ) | ||||
| 
 | ||||
| struct App { | ||||
| 	vweb.Context | ||||
| 	port          int | ||||
| 	timeout       int | ||||
| 	global_config shared Config | ||||
| } | ||||
| 
 | ||||
| struct Config { | ||||
| 	max_ping int | ||||
| } | ||||
| 
 | ||||
| fn exit_after_timeout(timeout_in_ms int) { | ||||
| 	time.sleep(timeout_in_ms * time.millisecond) | ||||
| 	// eprintln('webserver is exiting ...') | ||||
| 	exit(0) | ||||
| } | ||||
| 
 | ||||
| fn main() { | ||||
| 	if os.args.len != 3 { | ||||
| 		panic('Usage: `vweb_test_server.exe PORT TIMEOUT_IN_MILLISECONDS`') | ||||
| 	} | ||||
| 	http_port := os.args[1].int() | ||||
| 	assert http_port > 0 | ||||
| 	timeout := os.args[2].int() | ||||
| 	assert timeout > 0 | ||||
| 	go exit_after_timeout(timeout) | ||||
| 	// | ||||
| 	shared config := &Config{ | ||||
| 		max_ping: 50 | ||||
| 	} | ||||
| 	app := &App{ | ||||
| 		port: http_port | ||||
| 		timeout: timeout | ||||
| 		global_config: config | ||||
| 	} | ||||
| 	eprintln('>> webserver: started on http://127.0.0.1:$app.port/ , with maximum runtime of $app.timeout milliseconds.') | ||||
| 	// vweb.run<App>(mut app, http_port) | ||||
| 	vweb.run(app, http_port) | ||||
| } | ||||
| 
 | ||||
| // pub fn (mut app App) init_server() { | ||||
| //} | ||||
| 
 | ||||
| pub fn (mut app App) index() vweb.Result { | ||||
| 	assert app.global_config.max_ping == 50 | ||||
| 	return app.text('Welcome to VWeb') | ||||
| } | ||||
| 
 | ||||
| pub fn (mut app App) simple() vweb.Result { | ||||
| 	return app.text('A simple result') | ||||
| } | ||||
| 
 | ||||
| pub fn (mut app App) html_page() vweb.Result { | ||||
| 	return app.html('<h1>ok</h1>') | ||||
| } | ||||
| 
 | ||||
| // the following serve custom routes | ||||
| ['/:user/settings'] | ||||
| pub fn (mut app App) settings(username string) vweb.Result { | ||||
| 	if username !in known_users { | ||||
| 		return app.not_found() | ||||
| 	} | ||||
| 	return app.html('username: $username') | ||||
| } | ||||
| 
 | ||||
| ['/:user/:repo/settings'] | ||||
| pub fn (mut app App) user_repo_settings(username string, repository string) vweb.Result { | ||||
| 	if username !in known_users { | ||||
| 		return app.not_found() | ||||
| 	} | ||||
| 	return app.html('username: $username | repository: $repository') | ||||
| } | ||||
| 
 | ||||
| ['/json_echo'; post] | ||||
| pub fn (mut app App) json_echo() vweb.Result { | ||||
| 	// eprintln('>>>>> received http request at /json_echo is: $app.req') | ||||
| 	app.set_content_type(app.req.header.get(.content_type) or { '' }) | ||||
| 	return app.ok(app.req.data) | ||||
| } | ||||
| 
 | ||||
| ['/form_echo'; post] | ||||
| pub fn (mut app App) form_echo() vweb.Result { | ||||
| 	app.set_content_type(app.req.header.get(.content_type) or { '' }) | ||||
| 	return app.ok(app.form['foo']) | ||||
| } | ||||
| 
 | ||||
| // Make sure [post] works without the path | ||||
| [post] | ||||
| pub fn (mut app App) json() vweb.Result { | ||||
| 	// eprintln('>>>>> received http request at /json is: $app.req') | ||||
| 	app.set_content_type(app.req.header.get(.content_type) or { '' }) | ||||
| 	return app.ok(app.req.data) | ||||
| } | ||||
| 
 | ||||
| pub fn (mut app App) shutdown() vweb.Result { | ||||
| 	session_key := app.get_cookie('skey') or { return app.not_found() } | ||||
| 	if session_key != 'superman' { | ||||
| 		return app.not_found() | ||||
| 	} | ||||
| 	go app.gracefull_exit() | ||||
| 	return app.ok('good bye') | ||||
| } | ||||
| 
 | ||||
| fn (mut app App) gracefull_exit() { | ||||
| 	eprintln('>> webserver: gracefull_exit') | ||||
| 	time.sleep(100 * time.millisecond) | ||||
| 	exit(0) | ||||
| } | ||||
|  | @ -1,78 +0,0 @@ | |||
| module main | ||||
| 
 | ||||
| import vweb | ||||
| import time | ||||
| import sqlite | ||||
| 
 | ||||
| struct App { | ||||
| 	vweb.Context | ||||
| pub mut: | ||||
| 	db      sqlite.DB | ||||
| 	user_id string | ||||
| } | ||||
| 
 | ||||
| struct Article { | ||||
| 	id    int | ||||
| 	title string | ||||
| 	text  string | ||||
| } | ||||
| 
 | ||||
| fn test_a_vweb_application_compiles() { | ||||
| 	go fn () { | ||||
| 		time.sleep(2 * time.second) | ||||
| 		exit(0) | ||||
| 	}() | ||||
| 	vweb.run(&App{}, 18081) | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| /TODO | ||||
| pub fn (mut app App) init_server_old() { | ||||
| 	app.db = sqlite.connect('blog.db') or { panic(err) } | ||||
| 	app.db.create_table('article', [ | ||||
| 		'id integer primary key', | ||||
| 		"title text default ''", | ||||
| 		"text text default ''", | ||||
| 	]) | ||||
| } | ||||
| */ | ||||
| 
 | ||||
| pub fn (mut app App) before_request() { | ||||
| 	app.user_id = app.get_cookie('id') or { '0' } | ||||
| } | ||||
| 
 | ||||
| ['/new_article'; post] | ||||
| pub fn (mut app App) new_article() vweb.Result { | ||||
| 	title := app.form['title'] | ||||
| 	text := app.form['text'] | ||||
| 	if title == '' || text == '' { | ||||
| 		return app.text('Empty text/title') | ||||
| 	} | ||||
| 	article := Article{ | ||||
| 		title: title | ||||
| 		text: text | ||||
| 	} | ||||
| 	println('posting article') | ||||
| 	println(article) | ||||
| 	sql app.db { | ||||
| 		insert article into Article | ||||
| 	} | ||||
| 
 | ||||
| 	return app.redirect('/') | ||||
| } | ||||
| 
 | ||||
| fn (mut app App) time() { | ||||
| 	app.text(time.now().format()) | ||||
| } | ||||
| 
 | ||||
| fn (mut app App) time_json() { | ||||
| 	app.json({ | ||||
| 		'time': time.now().format() | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| fn (mut app App) time_json_pretty() { | ||||
| 	app.json_pretty({ | ||||
| 		'time': time.now().format() | ||||
| 	}) | ||||
| } | ||||
|  | @ -1,4 +1,4 @@ | |||
| module vweb | ||||
| module web | ||||
| 
 | ||||
| import net.urllib | ||||
| import net.http | ||||
|  | @ -1,7 +1,7 @@ | |||
| // Copyright (c) 2019-2022 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 | ||||
| module web | ||||
| 
 | ||||
| import os | ||||
| import io | ||||
|  | @ -148,11 +148,11 @@ pub: | |||
| 	// TODO Response | ||||
| pub mut: | ||||
| 	done bool | ||||
| 	// time.ticks() from start of vweb connection handle. | ||||
| 	// time.ticks() from start of web connection handle. | ||||
| 	// You can use it to determine how much time is spent on your request. | ||||
| 	page_gen_start i64 | ||||
| 	// TCP connection to client. | ||||
| 	// But beware, do not store it for further use, after request processing vweb will close connection. | ||||
| 	// But beware, do not store it for further use, after request processing web will close connection. | ||||
| 	conn              &net.TcpConn | ||||
| 	static_files      map[string]string | ||||
| 	static_mime_types map[string]string | ||||
|  | @ -167,6 +167,8 @@ pub mut: | |||
| 	header http.Header // response headers | ||||
| 	// ? It doesn't seem to be used anywhere | ||||
| 	form_error string | ||||
| 	// Allows reading the request body | ||||
| 	reader io.BufferedReader | ||||
| } | ||||
| 
 | ||||
| struct FileData { | ||||
|  | @ -201,7 +203,7 @@ pub struct Cookie { | |||
| 	http_only bool | ||||
| } | ||||
| 
 | ||||
| // vweb intern function | ||||
| // web intern function | ||||
| [manualfree] | ||||
| pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bool { | ||||
| 	if ctx.done { | ||||
|  | @ -216,7 +218,7 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bo | |||
| 	}).join(ctx.header) | ||||
| 
 | ||||
| 	mut resp := http.Response{ | ||||
| 		header: header.join(vweb.headers_close) | ||||
| 		header: header.join(web.headers_close) | ||||
| 		text: res | ||||
| 	} | ||||
| 	resp.set_version(.v1_1) | ||||
|  | @ -259,7 +261,7 @@ pub fn (mut ctx Context) file(f_path string) Result { | |||
| 		ctx.server_error(500) | ||||
| 		return Result{} | ||||
| 	} | ||||
| 	content_type := vweb.mime_types[ext] | ||||
| 	content_type := web.mime_types[ext] | ||||
| 	if content_type == '' { | ||||
| 		eprintln('no MIME type found for extension $ext') | ||||
| 		ctx.server_error(500) | ||||
|  | @ -283,7 +285,7 @@ pub fn (mut ctx Context) server_error(ecode int) Result { | |||
| 	if ctx.done { | ||||
| 		return Result{} | ||||
| 	} | ||||
| 	send_string(mut ctx.conn, vweb.http_500.bytestr()) or {} | ||||
| 	send_string(mut ctx.conn, web.http_500.bytestr()) or {} | ||||
| 	return Result{} | ||||
| } | ||||
| 
 | ||||
|  | @ -293,7 +295,7 @@ pub fn (mut ctx Context) redirect(url string) Result { | |||
| 		return Result{} | ||||
| 	} | ||||
| 	ctx.done = true | ||||
| 	mut resp := vweb.http_302 | ||||
| 	mut resp := web.http_302 | ||||
| 	resp.header = resp.header.join(ctx.header) | ||||
| 	resp.header.add(.location, url) | ||||
| 	send_string(mut ctx.conn, resp.bytestr()) or { return Result{} } | ||||
|  | @ -306,7 +308,7 @@ pub fn (mut ctx Context) not_found() Result { | |||
| 		return Result{} | ||||
| 	} | ||||
| 	ctx.done = true | ||||
| 	send_string(mut ctx.conn, vweb.http_404.bytestr()) or {} | ||||
| 	send_string(mut ctx.conn, web.http_404.bytestr()) or {} | ||||
| 	return Result{} | ||||
| } | ||||
| 
 | ||||
|  | @ -379,7 +381,6 @@ interface DbInterface { | |||
| // run_app | ||||
| [manualfree] | ||||
| pub fn run<T>(global_app &T, port int) { | ||||
| 	println("sup neef") | ||||
| 	mut l := net.listen_tcp(.ip6, ':$port') or { panic('failed to listen $err.code $err') } | ||||
| 
 | ||||
| 	// Parsing methods attributes | ||||
|  | @ -402,10 +403,10 @@ pub fn run<T>(global_app &T, port int) { | |||
| 		$if T is DbInterface { | ||||
| 			request_app.db = global_app.db | ||||
| 		} $else { | ||||
| 			// println('vweb no db') | ||||
| 			// println('web no db') | ||||
| 		} | ||||
| 		$for field in T.fields { | ||||
| 			if 'vweb_global' in field.attrs || field.is_shared { | ||||
| 			if 'web_global' in field.attrs || field.is_shared { | ||||
| 				request_app.$(field.name) = global_app.$(field.name) | ||||
| 			} | ||||
| 		} | ||||
|  | @ -438,16 +439,24 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) { | |||
| 	page_gen_start := time.ticks() | ||||
| 
 | ||||
| 	// Request parse | ||||
| 	req := http.parse_request(mut reader) or { | ||||
| 	head := http.parse_request_head(mut reader) or { | ||||
| 		// Prevents errors from being thrown when BufferedReader is empty | ||||
| 		if '$err' != 'none' { | ||||
| 			eprintln('error parsing request: $err') | ||||
| 			eprintln('error parsing request head: $err') | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| // 	req := http.parse_request(mut reader) or { | ||||
| // 		// Prevents errors from being thrown when BufferedReader is empty | ||||
| // 		if '$err' != 'none' { | ||||
| // 			eprintln('error parsing request: $err') | ||||
| // 		} | ||||
| // 		return | ||||
| // 	} | ||||
| 
 | ||||
| 	// URL Parse | ||||
| 	url := urllib.parse(req.url) or { | ||||
| 	url := urllib.parse(head.url) or { | ||||
| 		eprintln('error parsing path: $err') | ||||
| 		return | ||||
| 	} | ||||
|  | @ -456,22 +465,24 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) { | |||
| 	query := parse_query_from_url(url) | ||||
| 	url_words := url.path.split('/').filter(it != '') | ||||
| 
 | ||||
| 	// TODO re-add form parsing | ||||
| 	// Form parse | ||||
| 	form, files := parse_form_from_request(req) or { | ||||
| 		// Bad request | ||||
| 		conn.write(vweb.http_400.bytes()) or {} | ||||
| 		return | ||||
| 	} | ||||
| 	// form, files := parse_form_from_request(req) or { | ||||
| 	// 	// Bad request | ||||
| 	// 	conn.write(web.http_400.bytes()) or {} | ||||
| 	// 	return | ||||
| 	// } | ||||
| 
 | ||||
| 	app.Context = Context{ | ||||
| 		req: req | ||||
| 		req: head | ||||
| 		page_gen_start: page_gen_start | ||||
| 		conn: conn | ||||
| 		query: query | ||||
| 		form: form | ||||
| 		files: files | ||||
| 		// form: form | ||||
| 		// files: files | ||||
| 		static_files: app.static_files | ||||
| 		static_mime_types: app.static_mime_types | ||||
| 		reader: reader | ||||
| 	} | ||||
| 
 | ||||
| 	// Calling middleware... | ||||
|  | @ -492,7 +503,7 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) { | |||
| 			} | ||||
| 
 | ||||
| 			// Skip if the HTTP request method does not match the attributes | ||||
| 			if req.method in route.methods { | ||||
| 			if head.method in route.methods { | ||||
| 				// Used for route matching | ||||
| 				route_words := route.path.split('/').filter(it != '') | ||||
| 
 | ||||
|  | @ -501,13 +512,14 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) { | |||
| 				// should be called first. | ||||
| 				if !route.path.contains('/:') && url_words == route_words { | ||||
| 					// We found a match | ||||
| 					if req.method == .post && method.args.len > 0 { | ||||
| 					if head.method == .post && method.args.len > 0 { | ||||
| 						// TODO implement POST requests | ||||
| 						// Populate method args with form values | ||||
| 						mut args := []string{cap: method.args.len} | ||||
| 						for param in method.args { | ||||
| 							args << form[param.name] | ||||
| 						} | ||||
| 						app.$method(args) | ||||
| 						// mut args := []string{cap: method.args.len} | ||||
| 						// for param in method.args { | ||||
| 						// 	args << form[param.name] | ||||
| 						// } | ||||
| 						// app.$method(args) | ||||
| 					} else { | ||||
| 						app.$method() | ||||
| 					} | ||||
|  | @ -522,7 +534,7 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) { | |||
| 				if params := route_matches(url_words, route_words) { | ||||
| 					method_args := params.clone() | ||||
| 					if method_args.len != method.args.len { | ||||
| 						eprintln('warning: uneven parameters count ($method.args.len) in `$method.name`, compared to the vweb route `$method.attrs` ($method_args.len)') | ||||
| 						eprintln('warning: uneven parameters count ($method.args.len) in `$method.name`, compared to the web route `$method.attrs` ($method_args.len)') | ||||
| 					} | ||||
| 					app.$method(method_args) | ||||
| 					return | ||||
|  | @ -531,7 +543,7 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) { | |||
| 		} | ||||
| 	} | ||||
| 	// Route not found | ||||
| 	conn.write(vweb.http_404.bytes()) or {} | ||||
| 	conn.write(web.http_404.bytes()) or {} | ||||
| } | ||||
| 
 | ||||
| fn route_matches(url_words []string, route_words []string) ?[]string { | ||||
|  | @ -587,7 +599,7 @@ fn serve_if_static<T>(mut app T, url urllib.URL) bool { | |||
| 		return false | ||||
| 	} | ||||
| 	data := os.read_file(static_file) or { | ||||
| 		send_string(mut app.conn, vweb.http_404.bytestr()) or {} | ||||
| 		send_string(mut app.conn, web.http_404.bytestr()) or {} | ||||
| 		return true | ||||
| 	} | ||||
| 	app.send_response_to_client(mime_type, data) | ||||
|  | @ -606,7 +618,7 @@ fn (mut ctx Context) scan_static_directory(directory_path string, mount_path str | |||
| 				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 vweb.mime_types { | ||||
| 				if ext in web.mime_types { | ||||
| 					ctx.serve_static(mount_path + '/' + file, full_path) | ||||
| 				} | ||||
| 			} | ||||
|  | @ -649,7 +661,7 @@ pub fn (mut ctx Context) serve_static(url string, file_path string) { | |||
| 	ctx.static_files[url] = file_path | ||||
| 	// ctx.static_mime_types[url] = mime_type | ||||
| 	ext := os.file_ext(file_path) | ||||
| 	ctx.static_mime_types[url] = vweb.mime_types[ext] | ||||
| 	ctx.static_mime_types[url] = web.mime_types[ext] | ||||
| } | ||||
| 
 | ||||
| // Returns the ip address from the current user | ||||
|  | @ -670,7 +682,7 @@ pub fn (ctx &Context) ip() string { | |||
| 
 | ||||
| // Set s to the form error | ||||
| pub fn (mut ctx Context) error(s string) { | ||||
| 	println('vweb error: $s') | ||||
| 	println('web error: $s') | ||||
| 	ctx.form_error = s | ||||
| } | ||||
| 
 | ||||
|  | @ -684,7 +696,7 @@ fn send_string(mut conn net.TcpConn, s string) ? { | |||
| } | ||||
| 
 | ||||
| // Do not delete. | ||||
| // It used by `vlib/v/gen/c/str_intp.v:130` for string interpolation inside vweb templates | ||||
| // It used by `vlib/v/gen/c/str_intp.v:130` for string interpolation inside web templates | ||||
| // TODO: move it to template render | ||||
| fn filter(s string) string { | ||||
| 	return s.replace_each([ | ||||
		Loading…
	
		Reference in New Issue