├── .editorconfig
├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── config.v
├── constant.v
├── cookie.v
├── examples
├── basic.v
├── json.v
├── send_file.v
└── static_folder
│ ├── index.html
│ ├── main.v
│ └── static
│ ├── script.js
│ └── styles.css
├── route.v
├── spaceship.v
└── v.mod
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset = utf-8
3 | end_of_line = lf
4 | insert_final_newline = true
5 | trim_trailing_whitespace = true
6 |
7 | [*.v]
8 | indent_style = tab
9 | indent_size = 4
10 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.v linguist-language=V text=auto eol=lf
2 | *.vv linguist-language=V text=auto eol=lf
3 | *.vsh linguist-language=V text=auto eol=lf
4 | **/v.mod linguist-language=V text=auto eol=lf
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | main
3 | spaceship
4 | *.exe
5 | *.exe~
6 | *.so
7 | *.dylib
8 | *.dll
9 | vls.log
10 | .vscode
11 | notes.txt
12 | tools
13 | .DS_Store
14 | *_test.v
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 cookie bacon
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 | # Spaceship
2 |
3 | ## Installation
4 |
5 | ```bash
6 | v up
7 | v install cookieforpres.spaceship
8 | ```
9 |
10 | ## Example
11 |
12 | ```v
13 | module main
14 |
15 | import cookieforpres.spaceship
16 |
17 | fn handler(mut req spaceship.Request, mut res spaceship.Response) {
18 | res.set_body('Welcome to Spaceship 🚀. Get ready for blast off!')
19 | }
20 |
21 | fn main() {
22 | mut sp := spaceship.new('0.0.0.0', 8080)
23 |
24 | mut route := spaceship.new_route('/', ['GET', 'POST'], handler)
25 | sp.add_route(route)
26 |
27 | sp.listen() or { panic(err) }
28 | }
29 | ```
30 |
31 | ## Upcoming Features / Already Implemented
32 |
33 | * [X] sending files (e.g. images, html file, json file, etc.)
34 | * [X] having a static folder for css and js files
35 | * [ ] middleware
36 |
37 | if you have any suggestions or want to contribute, please feel free to open an issue or make a pull request on [GitHub](https://github.com/cookieforpres/spaceship)
38 |
--------------------------------------------------------------------------------
/config.v:
--------------------------------------------------------------------------------
1 | module spaceship
2 |
3 | pub struct SpaceshipConfig {
4 | pub mut:
5 | verbose bool
6 | show_favicon_request bool
7 | show_server_header bool
8 | }
9 |
10 | pub fn (mut sp Spaceship) edit_config(id string, toggle bool) {
11 | match id {
12 | 'verbose' { sp.config.verbose = toggle }
13 | 'show_favicon_request' { sp.config.show_favicon_request = toggle }
14 | 'show_server_header' { sp.config.show_server_header = toggle }
15 | else {}
16 | }
17 | }
--------------------------------------------------------------------------------
/constant.v:
--------------------------------------------------------------------------------
1 | module spaceship
2 |
3 | pub fn get_status_codes() map[int]string {
4 | mut codes := map[int]string{}
5 | codes[100] = "Continue"
6 | codes[101] = "Switching Protocols"
7 | codes[102] = "Processing"
8 | codes[200] = "OK"
9 | codes[201] = "Created"
10 | codes[202] = "Accepted"
11 | codes[203] = "Non-Authoritative Information"
12 | codes[204] = "No Content"
13 | codes[205] = "Reset Content"
14 | codes[206] = "Partial Content"
15 | codes[207] = "Multi-Status"
16 | codes[300] = "Multiple Choices"
17 | codes[301] = "Moved Permanently"
18 | codes[302] = "Found"
19 | codes[303] = "See Other"
20 | codes[304] = "Not Modified"
21 | codes[305] = "Use Proxy"
22 | codes[307] = "Temporary Redirect"
23 | codes[400] = "Bad Request"
24 | codes[401] = "Unauthorized"
25 | codes[402] = "Payment Required"
26 | codes[403] = "Forbidden"
27 | codes[404] = "Not Found"
28 | codes[405] = "Method Not Allowed"
29 | codes[406] = "Not Acceptable"
30 | codes[407] = "Proxy Authentication Required"
31 | codes[408] = "Request Timeout"
32 | codes[409] = "Conflict"
33 | codes[410] = "Gone"
34 | codes[411] = "Length Required"
35 | codes[412] = "Precondition Failed"
36 | codes[413] = "Request Entity Too Large"
37 | codes[414] = "Request-URI Too Long"
38 | codes[415] = "Unsupported Media Type"
39 | codes[416] = "Request Range Not Satisfiable"
40 | codes[417] = "Expectation Failed"
41 | codes[418] = "I'm a teapot"
42 | codes[422] = "Unprocessable Entity"
43 | codes[423] = "Locked"
44 | codes[424] = "Failed Dependency"
45 | codes[425] = "Unordered Collection"
46 | codes[426] = "Upgrade Required"
47 | codes[449] = "Retry With"
48 | codes[500] = "Internal Server Error"
49 | codes[501] = "Not Implemented"
50 | codes[502] = "Bad Gateway"
51 | codes[503] = "Service Unavailable"
52 | codes[504] = "Gateway Timeout"
53 | codes[505] = "HTTP Version Not Supported"
54 | codes[506] = "Variant Also Negotiates"
55 | codes[507] = "Insufficient Storage"
56 | codes[509] = "Bandwidth Limit Exceeded"
57 | codes[510] = "Not Extended"
58 | return codes
59 | }
--------------------------------------------------------------------------------
/cookie.v:
--------------------------------------------------------------------------------
1 | module spaceship
2 |
3 | pub struct Cookie {
4 | pub mut:
5 | name string
6 | value string
7 | path string
8 | domain string
9 | expires string
10 | http_only bool
11 | secure bool
12 | max_age int
13 | same_site string
14 | }
15 |
16 | pub fn new_cookie() Cookie {
17 | return Cookie{}
18 | }
19 |
20 | pub fn (mut resp Response) add_cookie(cookie Cookie) {
21 | resp.cookies << &cookie
22 | }
23 |
24 | fn (mut resp Response) format_cookie(cookie Cookie) string {
25 | mut coo := '$cookie.name=$cookie.value; '
26 | if cookie.path != '' {
27 | coo += 'Path=$cookie.path; '
28 | }
29 |
30 | if cookie.domain != '' {
31 | coo += 'Domain=$cookie.domain; '
32 | }
33 |
34 | if cookie.expires != '' {
35 | coo += 'Expires=$cookie.expires; '
36 | }
37 |
38 | if cookie.http_only {
39 | coo += 'HttpOnly; '
40 | }
41 |
42 | if cookie.secure {
43 | coo += 'Secure; '
44 | }
45 |
46 | if cookie.max_age != 0 {
47 | coo += 'Max-Age=$cookie.max_age; '
48 | }
49 |
50 | if cookie.same_site != '' {
51 | coo += 'SameSite=$cookie.same_site; '
52 | }
53 |
54 | return coo
55 | }
--------------------------------------------------------------------------------
/examples/basic.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import cookieforpres.spaceship
4 |
5 | fn handler(mut req spaceship.Request, mut res spaceship.Response) {
6 | res.set_body('Welcome to Spaceship 🚀. Get ready for blast off!')
7 | }
8 |
9 | fn main() {
10 | mut sp := spaceship.new('0.0.0.0', 8080)
11 |
12 | mut route := spaceship.new_route('/', ['GET', 'POST'], handler)
13 | sp.add_route(route)
14 |
15 | sp.listen() or { panic(err) }
16 | }
--------------------------------------------------------------------------------
/examples/json.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import cookieforpres.spaceship
4 | import json
5 |
6 | fn handler(mut req spaceship.Request, mut res spaceship.Response) {
7 | res.add_header('Content-Type', 'application/json')
8 |
9 | json_data := json.encode({'message': 'Welcome to Spaceship 🚀. Get ready for blast off!'})
10 | res.set_body(json_data)
11 | }
12 |
13 | fn main() {
14 | mut sp := spaceship.new('0.0.0.0', 8080)
15 |
16 | mut route := spaceship.new_route('/', ['GET', 'POST'], handler)
17 | sp.add_route(route)
18 |
19 | sp.listen() or { panic(err) }
20 | }
--------------------------------------------------------------------------------
/examples/send_file.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import cookieforpres.spaceship
4 |
5 | fn handler(mut req spaceship.Request, mut res spaceship.Response) {
6 | res.add_header('Content-Type', 'application/json')
7 | res.send_file('index.json')
8 | }
9 |
10 | fn main() {
11 | mut sp := spaceship.new('0.0.0.0', 8080)
12 |
13 | mut route := spaceship.new_route('/', ['GET', 'POST'], handler)
14 | sp.add_route(route)
15 |
16 | sp.listen() or { panic(err) }
17 | }
--------------------------------------------------------------------------------
/examples/static_folder/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Spaceship 🚀
9 |
10 |
11 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/examples/static_folder/main.v:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | import cookieforpres.spaceship
4 |
5 | fn handler(mut req spaceship.Request, mut res spaceship.Response) {
6 | res.add_header('Content-Type', 'text/html')
7 | res.send_file('index.html')
8 | }
9 |
10 | fn main() {
11 | mut sp := spaceship.new('0.0.0.0', 8080)
12 | sp.static_folder('static/') ?
13 |
14 | mut route := spaceship.new_route('/', ['GET', 'POST'], handler)
15 | sp.add_route(route)
16 |
17 | sp.listen() or { panic(err) }
18 | }
--------------------------------------------------------------------------------
/examples/static_folder/static/script.js:
--------------------------------------------------------------------------------
1 | function helloWorld() {
2 | console.log("hello world!")
3 | }
--------------------------------------------------------------------------------
/examples/static_folder/static/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | color: #fff;
3 | background-color: #333;
4 | }
--------------------------------------------------------------------------------
/route.v:
--------------------------------------------------------------------------------
1 | module spaceship
2 |
3 | import os
4 |
5 | pub struct Response {
6 | mut:
7 | status_code_name string
8 |
9 | pub mut:
10 | status_code int
11 | headers map[string]string
12 | cookies []&Cookie
13 | body string
14 | }
15 |
16 | pub struct Route {
17 | pub mut:
18 | path string
19 | methods []string
20 | handler fn(mut req Request, mut res Response)
21 | }
22 |
23 | pub fn new_response() Response {
24 | mut res := Response{}
25 | res.add_header('Content-Type', 'text/plain;charset=UTF-8')
26 | res.add_header('Server', 'spaceship')
27 |
28 | return res
29 | }
30 |
31 | pub fn (mut sp Spaceship) add_route(route Route) {
32 | sp.routes << &route
33 | }
34 |
35 | pub fn (mut resp Response) set_status_code(status_code int) {
36 | codes := get_status_codes()
37 | for key, value in codes {
38 | if key == status_code {
39 | resp.status_code_name = value
40 | }
41 | }
42 |
43 | resp.status_code = status_code
44 | }
45 |
46 | pub fn (mut resp Response) add_header(key string, value string) {
47 | if key == 'Content-Type' {
48 | if value.contains('charset=') {
49 | resp.headers[key] = value
50 | } else {
51 | resp.headers[key] = value + ';charset=UTF-8'
52 | }
53 | } else {
54 | resp.headers[key] = value
55 | }
56 | }
57 |
58 | pub fn (mut resp Response) remove_header(key string) {
59 | resp.headers.delete(key)
60 | }
61 |
62 | pub fn (mut resp Response) set_body(body string) {
63 | resp.body = body
64 | }
65 |
66 | pub fn (mut resp Response) send_file(path string) {
67 | data := os.read_file(path) or {
68 | eprintln('[\x1b[31;1merror\x1b[0m] $err')
69 | exit(1)
70 | }
71 |
72 | resp.set_body(data)
73 | }
74 |
75 | pub fn new_route(path string, methods []string, handler fn(mut req Request, mut res Response)) Route {
76 | if methods.len == 0 {
77 | return Route {
78 | path: path,
79 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH'],
80 | handler: handler
81 | }
82 | } else {
83 | return Route {
84 | path: path,
85 | methods: methods,
86 | handler: handler
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/spaceship.v:
--------------------------------------------------------------------------------
1 | module spaceship
2 |
3 | import net
4 | import time
5 | import os
6 |
7 | pub struct DefaultResponse {
8 | pub mut:
9 | status_code int
10 | status_code_name string
11 | message string
12 | }
13 |
14 | pub struct Spaceship {
15 | pub mut:
16 | host string
17 | port int
18 |
19 | routes []&Route
20 | default_responses []&DefaultResponse
21 |
22 | static_files []&StaticFile
23 | static_path string
24 |
25 | config SpaceshipConfig
26 | }
27 |
28 | pub struct Request {
29 | pub mut:
30 | method string
31 | path string
32 | body string
33 | headers map[string]string
34 | start_time time.Time
35 | conn net.TcpConn
36 | }
37 |
38 | pub enum StaticFileType {
39 | css
40 | js
41 | image
42 | other
43 | }
44 |
45 | pub struct StaticFile {
46 | pub mut:
47 | file_path string
48 | file_type StaticFileType
49 | }
50 |
51 | pub enum ErrorMessage {
52 | no_error
53 | not_found
54 | method_not_allowed
55 | internal_server_error
56 | }
57 |
58 | pub fn (mut sp Spaceship) static_folder(path string) ? {
59 | mut path_ := path
60 | for {
61 | if !path_.ends_with('/') {
62 | break
63 | }
64 |
65 | path_ = path_.trim_right('/')
66 | }
67 |
68 | if !os.is_dir(path_) {
69 | eprintln('[\x1b[31;1merror\x1b[0m] $path_ is not a directory or does not exist')
70 | exit(1)
71 | }
72 |
73 | mut files := []&StaticFile{}
74 | mut pfiles := &files
75 |
76 | os.walk(path_, fn [mut pfiles] (f string) {
77 | if f.ends_with('.css') {
78 | mut file := &StaticFile{file_path: f, file_type: StaticFileType.css}
79 | pfiles << file
80 | } else if f.ends_with('.js') {
81 | mut file := &StaticFile{file_path: f, file_type: StaticFileType.js}
82 | pfiles << file
83 | } else if f.ends_with('.png') {
84 | mut file := &StaticFile{file_path: f, file_type: StaticFileType.image}
85 | pfiles << file
86 | } else {
87 | mut file := &StaticFile{file_path: f, file_type: StaticFileType.other}
88 | pfiles << file
89 | }
90 | })
91 |
92 | sp.static_files = files
93 | sp.static_path = path_
94 | }
95 |
96 | fn get_path(str string) string {
97 | return str.split(' ')[1]
98 | }
99 |
100 | fn get_method(str string) string {
101 | return str.split(' ')[0]
102 | }
103 |
104 | fn get_body(str string) string {
105 | parts := str.split('\r\n\r\n')
106 | if parts.len > 1 {
107 | return parts[1]
108 | } else {
109 | return ''
110 | }
111 | }
112 |
113 | fn get_headers(strs []string) map[string]string {
114 | mut headers := map[string]string{}
115 | for str in strs {
116 | parts := str.split(':')
117 | if parts.len == 2 {
118 | headers[parts[0]] = parts[1]
119 | }
120 | }
121 | return headers
122 | }
123 |
124 | fn log_request(mut sp Spaceship, host string, status_code int, method string, path string, response_time string) {
125 | if !sp.config.show_favicon_request && path.contains('favicon.ico') {
126 | return
127 | } else {
128 | if status_code >= 200 && status_code < 300 {
129 | println('[\x1b[35;1mrequest\x1b[0m] $host - $method \x1b[32;1m$status_code\x1b[0m $path - $response_time')
130 | } else if status_code >= 300 && status_code < 400 {
131 | println('[\x1b[35;1mrequest\x1b[0m] $host - $method \x1b[33;1m$status_code\x1b[0m $path - $response_time')
132 | } else if status_code >= 400 && status_code < 500 {
133 | println('[\x1b[35;1mrequest\x1b[0m] $host - $method \x1b[33;1m$status_code\x1b[0m $path - $response_time')
134 | } else {
135 | println('[\x1b[35;1mrequest\x1b[0m] $host - $method \x1b[31;1m$status_code\x1b[0m $path - $response_time')
136 | }
137 | }
138 | }
139 |
140 | fn run_response(mut sp Spaceship, mut conn net.TcpConn, mut request Request, mut response Response) ? {
141 | if response.status_code == 0 {
142 | response.set_status_code(200)
143 | }
144 |
145 | mut resp := 'HTTP/1.1 $response.status_code $response.status_code_name\r\n'
146 | for key, value in response.headers {
147 | resp += '$key: $value\r\n'
148 | }
149 |
150 | for cookie in response.cookies {
151 | formated_cookie := response.format_cookie(cookie)
152 | resp += 'Set-Cookie: $formated_cookie\r\n'
153 | }
154 |
155 | resp += '\r\n'
156 | resp += response.body
157 |
158 |
159 | conn.write_string(resp) ?
160 |
161 | diff := time.since(request.start_time)
162 |
163 | if sp.config.verbose {
164 | log_request(mut sp, conn.peer_ip() ?, response.status_code, request.method, request.path, '$diff')
165 | }
166 | }
167 |
168 | fn (mut sp Spaceship) handle_connection(mut conn net.TcpConn) ? {
169 | mut message := ''
170 | for {
171 | mut line := conn.read_line()
172 | bytes := line.bytes()
173 |
174 | if bytes.len < 2 {
175 | break
176 | }
177 |
178 | if bytes[0] == `\r` && bytes[1] == `\n` {
179 | break
180 | }
181 |
182 | message += line
183 | }
184 |
185 | start := time.now()
186 |
187 | if message.len == 0 {
188 | conn.close() ?
189 | return
190 | }
191 |
192 | message_parts := message.split('\r\n')
193 |
194 | method := get_method(message_parts[0])
195 | path := get_path(message_parts[0])
196 | headers := get_headers(message_parts[1..])
197 | body := get_body(message)
198 |
199 | mut request := Request{method: method, path: path, body: body, headers: headers, start_time: start, conn: conn}
200 | mut response := new_response()
201 |
202 | mut error_message := ErrorMessage.no_error
203 | mut found := false
204 | for route in sp.routes {
205 | if route.path == request.path {
206 | if method in route.methods {
207 | route.handler(mut request, mut response)
208 | found = true
209 | break
210 | } else if route.methods.len == 0 {
211 | route.handler(mut request, mut response)
212 | found = true
213 | break
214 | } else {
215 | error_message = ErrorMessage.method_not_allowed
216 | break
217 | }
218 | }
219 |
220 | if sp.static_path != '' {
221 | sp.static_folder(sp.static_path) ?
222 | }
223 |
224 | for file in sp.static_files {
225 | static_path := file.file_path.substr(sp.static_path.len, file.file_path.len)
226 |
227 | if path == static_path {
228 | match file.file_type {
229 | .css {
230 | response.set_status_code(200)
231 | response.add_header('Content-Type', 'text/css')
232 |
233 | contents := os.read_file(file.file_path) or {
234 | eprintln('[\x1b[31;1merror\x1b[0m] $err')
235 | exit(1)
236 | }
237 |
238 | response.set_body(contents)
239 | found = true
240 | break
241 | }
242 | .js {
243 | response.set_status_code(200)
244 | response.add_header('Content-Type', 'text/javascript')
245 |
246 | contents := os.read_file(file.file_path) or {
247 | eprintln('[\x1b[31;1merror\x1b[0m] $err')
248 | exit(1)
249 | }
250 |
251 | response.set_body(contents)
252 | found = true
253 | break
254 | }
255 | .image {
256 | response.set_status_code(200)
257 | response.add_header('Content-Type', 'image/png')
258 |
259 | contents := os.read_file(file.file_path) or {
260 | eprintln('[\x1b[31;1merror\x1b[0m] $err')
261 | exit(1)
262 | }
263 |
264 | response.set_body(contents)
265 | found = true
266 | break
267 | }
268 | .other {
269 | response.set_status_code(200)
270 | response.add_header('Content-Type', 'text/plain')
271 |
272 | contents := os.read_file(file.file_path) or {
273 | eprintln('[\x1b[31;1merror\x1b[0m] $err')
274 | exit(1)
275 | }
276 |
277 | response.set_body(contents)
278 | found = true
279 | break
280 | }
281 | }
282 | }
283 | }
284 | }
285 |
286 | if !found && error_message != ErrorMessage.method_not_allowed {
287 | error_message = ErrorMessage.not_found
288 | }
289 |
290 | if !sp.config.show_server_header {
291 | response.remove_header('Server')
292 | }
293 |
294 | match error_message {
295 | .method_not_allowed {
296 | for dr in sp.default_responses {
297 | if dr.status_code == 405 {
298 | response.set_status_code(dr.status_code)
299 | response.set_body(dr.message)
300 | break
301 | }
302 | }
303 |
304 | run_response(mut sp, mut conn, mut request, mut response) ?
305 | conn.close() ?
306 | }
307 | .not_found {
308 | for dr in sp.default_responses {
309 | if dr.status_code == 404 {
310 | response.set_status_code(dr.status_code)
311 | response.set_body(dr.message)
312 | break
313 | }
314 | }
315 |
316 | run_response(mut sp, mut conn, mut request, mut response) ?
317 | conn.close() ?
318 | }
319 | else {
320 | run_response(mut sp, mut conn, mut request, mut response) ?
321 | conn.close() ?
322 | }
323 | }
324 | }
325 |
326 | pub fn new(host string, port int) Spaceship {
327 | mut default_responses := []&DefaultResponse{}
328 | default_responses << &DefaultResponse{
329 | status_code: 404,
330 | status_code_name: 'Not Found',
331 | message: 'Not Found',
332 | }
333 |
334 | default_responses << &DefaultResponse{
335 | status_code: 500,
336 | status_code_name: 'Internal Server Error',
337 | message: 'Internal Server Error',
338 | }
339 |
340 | default_responses << &DefaultResponse{
341 | status_code: 405,
342 | status_code_name: 'Method Not Allowed',
343 | message: 'Method Not Allowed',
344 | }
345 |
346 | return Spaceship{
347 | host: host,
348 | port: port,
349 | default_responses: default_responses,
350 | static_path: '',
351 | config: SpaceshipConfig {
352 | verbose: true
353 | show_favicon_request: true
354 | show_server_header: true
355 | }
356 | }
357 | }
358 |
359 | pub fn (mut sp Spaceship) listen() ? {
360 | mut ln := net.listen_tcp(net.AddrFamily.ip, '$sp.host:$sp.port') or {
361 | eprintln('[\x1b[31;1merror\x1b[0m] $err')
362 | return
363 | }
364 |
365 | if sp.config.verbose {
366 | print('\x1b[2J\x1b[1;1H')
367 | println('[\x1b[32;1mspaceship\x1b[0m] ready for take off 🚀 (http://$sp.host:$sp.port/)\n')
368 | }
369 |
370 | for {
371 | mut conn := ln.accept() or {
372 | eprintln('[\x1b[31;1merror\x1b[0m] $err')
373 | continue
374 | }
375 |
376 | go sp.handle_connection(mut conn)
377 | }
378 | }
--------------------------------------------------------------------------------
/v.mod:
--------------------------------------------------------------------------------
1 | Module {
2 | name: 'spaceship'
3 | description: 'Spaceship is a web framework made in V'
4 | version: '0.1.0'
5 | license: 'MIT'
6 | dependencies: []
7 | }
8 |
--------------------------------------------------------------------------------