mirror of https://github.com/rwf2/Rocket.git
Rocket is almost operational! `routes!` macro complete.
Here's what works so far: * The `route` decorator checks its inputs correctly. There's a nice utility for doing this, and it's working quite well at the moment. * The `route` decorator emits a `route_fn` and a `route_struct`. The `routes` * macro prepends the path terminator with the route struct prefix. The * `Rocket` library can read mount information (though not act on it properly just yet) and launch a server using Hyper.
This commit is contained in:
parent
967fcd26b2
commit
2e2cc3c216
|
@ -8,7 +8,7 @@
|
|||
*.exe
|
||||
|
||||
# Generated by Cargo
|
||||
/target/
|
||||
target
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "hello"
|
||||
version = "0.0.1"
|
||||
authors = ["Sergio Benitez <sb@sergio.bz>"]
|
||||
|
||||
[dependencies]
|
||||
rocket = { path = "../../lib" }
|
||||
rocket_macros = { path = "../../macros" }
|
|
@ -0,0 +1,25 @@
|
|||
#![feature(plugin)]
|
||||
#![plugin(rocket_macros)]
|
||||
|
||||
extern crate rocket;
|
||||
use rocket::{Rocket, Request, Response, Method, Route};
|
||||
|
||||
#[route(GET, path = "/hello")]
|
||||
fn hello() -> &'static str {
|
||||
"Hello, world!"
|
||||
}
|
||||
|
||||
mod test {
|
||||
use rocket::{Request, Response, Method, Route};
|
||||
|
||||
#[route(GET, path = "")]
|
||||
pub fn hello() -> &'static str {
|
||||
"Hello, world!"
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut rocket = Rocket::new("localhost", 8000);
|
||||
rocket.mount("/test", routes![test::hello]);
|
||||
rocket.mount_and_launch("/", routes![hello]);
|
||||
}
|
|
@ -4,5 +4,4 @@ version = "0.0.1"
|
|||
authors = ["Sergio Benitez <sb@sergio.bz>"]
|
||||
|
||||
[dependencies]
|
||||
hyper = "> 0.7.0"
|
||||
rocket_macros = { path = "macros" }
|
||||
hyper = "*"
|
|
@ -0,0 +1,3 @@
|
|||
pub enum Error {
|
||||
BadMethod
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
extern crate hyper;
|
||||
|
||||
mod method;
|
||||
mod error;
|
||||
|
||||
pub use method::Method;
|
||||
pub use error::Error;
|
||||
|
||||
use hyper::server::Handler as HypHandler;
|
||||
use hyper::server::Request as HypRequest;
|
||||
use hyper::server::Response as HypResponse;
|
||||
use hyper::net::Fresh as HypFresh;
|
||||
use hyper::Server;
|
||||
|
||||
pub type Handler = fn(Request) -> Response;
|
||||
|
||||
pub struct Request;
|
||||
pub struct Response;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Route<'a> {
|
||||
pub method: Method,
|
||||
pub path: &'a str,
|
||||
pub handler: Handler
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Rocket {
|
||||
address: &'static str,
|
||||
port: isize,
|
||||
// mounts: HashMap<&'static str, Route<'a>>
|
||||
}
|
||||
|
||||
impl HypHandler for Rocket {
|
||||
fn handle<'a, 'k>(&'a self, req: HypRequest<'a, 'k>,
|
||||
res: HypResponse<'a, HypFresh>) {
|
||||
println!("Request: {:?}", req.uri);
|
||||
res.send(b"Hello World!").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Rocket {
|
||||
pub fn new(address: &'static str, port: isize) -> Rocket {
|
||||
Rocket {
|
||||
address: address,
|
||||
port: port
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount(&mut self, base: &str, routes: &[&Route]) -> &mut Self {
|
||||
println!("Mounting at {}", base);
|
||||
for route in routes {
|
||||
println!(" - Found {} route to {}", route.method, route.path);
|
||||
(route.handler)(Request);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mount_and_launch(mut self, base: &str, routes: &[&Route]) {
|
||||
self.mount(base, routes);
|
||||
self.launch();
|
||||
}
|
||||
|
||||
pub fn launch(self) {
|
||||
let full_addr = format!("{}:{}", self.address, self.port);
|
||||
println!("🚀 Rocket is launching ({})...", full_addr);
|
||||
let _ = Server::http(full_addr.as_str()).unwrap().handle(self);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
use self::Method::{Options, Get, Post, Put, Delete, Head, Trace, Connect, Patch};
|
||||
use std::str::FromStr;
|
||||
use std::fmt::{self, Display};
|
||||
use error::Error;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum Method {
|
||||
Get,
|
||||
Put,
|
||||
Post,
|
||||
Delete,
|
||||
Options,
|
||||
Head,
|
||||
Trace,
|
||||
Connect,
|
||||
Patch
|
||||
}
|
||||
|
||||
impl FromStr for Method {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Method, Error> {
|
||||
match s {
|
||||
"OPTIONS" => Ok(Options),
|
||||
"GET" => Ok(Get),
|
||||
"POST" => Ok(Post),
|
||||
"PUT" => Ok(Put),
|
||||
"DELETE" => Ok(Delete),
|
||||
"HEAD" => Ok(Head),
|
||||
"TRACE" => Ok(Trace),
|
||||
"CONNECT" => Ok(Connect),
|
||||
"PATCH" => Ok(Patch),
|
||||
_ => Err(Error::BadMethod)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Method {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.write_str(match *self {
|
||||
Options => "OPTIONS",
|
||||
Get => "GET",
|
||||
Post => "POST",
|
||||
Put => "PUT",
|
||||
Delete => "DELETE",
|
||||
Head => "HEAD",
|
||||
Trace => "TRACE",
|
||||
Connect => "CONNECT",
|
||||
Patch => "PATCH",
|
||||
})
|
||||
}
|
||||
}
|
|
@ -2,11 +2,9 @@
|
|||
name = "rocket_macros"
|
||||
version = "0.0.1"
|
||||
authors = ["Sergio Benitez <sb@sergio.bz>"]
|
||||
description = "Core Rocket Macros"
|
||||
|
||||
[dependencies]
|
||||
hyper = "> 0.0.0"
|
||||
|
||||
[lib]
|
||||
name = "rocket_macros"
|
||||
plugin = true
|
||||
|
||||
[dependencies]
|
||||
rocket = { path = "../lib/" }
|
||||
|
|
|
@ -1,116 +1,59 @@
|
|||
#![crate_type = "dylib"]
|
||||
#![feature(plugin_registrar, rustc_private)]
|
||||
#![feature(quote, concat_idents, plugin_registrar, rustc_private)]
|
||||
|
||||
extern crate syntax;
|
||||
#[macro_use] extern crate syntax;
|
||||
extern crate rustc;
|
||||
extern crate rustc_plugin;
|
||||
extern crate hyper;
|
||||
extern crate rocket;
|
||||
|
||||
#[macro_use] mod macro_utils;
|
||||
|
||||
use macro_utils::{prepend_ident, get_key_values};
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use rustc_plugin::Registry;
|
||||
|
||||
use syntax::parse::token::{intern};
|
||||
use syntax::ext::base::SyntaxExtension;
|
||||
use std::default::Default;
|
||||
|
||||
use syntax::ext::quote::rt::ToTokens;
|
||||
use syntax::codemap::Span;
|
||||
use syntax::ast::{Item, ItemKind, MetaItem, MetaItemKind, FnDecl, LitKind};
|
||||
use syntax::ext::base::{Annotatable, ExtCtxt};
|
||||
use syntax::ptr::P;
|
||||
|
||||
use hyper::method::Method;
|
||||
use syntax::ast::{Path, PathSegment, Expr, ExprKind, TokenTree};
|
||||
use syntax::ext::base::{DummyResult, MacResult, MacEager};
|
||||
use syntax::ext::build::AstBuilder; // trait for expr_usize
|
||||
use syntax::parse::parser::{Parser, PathParsingMode};
|
||||
use syntax::parse::PResult;
|
||||
use syntax::parse::token::Token;
|
||||
|
||||
fn bad_item_fatal(ecx: &mut ExtCtxt, dec_sp: Span, item_sp: Span) -> ! {
|
||||
ecx.span_err(dec_sp, "This decorator cannot be used on non-functions...");
|
||||
ecx.span_fatal(item_sp, "...but an attempt to use it on the item below was made.")
|
||||
}
|
||||
const DEBUG: bool = true;
|
||||
|
||||
fn bad_method_err(ecx: &mut ExtCtxt, dec_sp: Span, method: &str) {
|
||||
let message = format!("`{}` is not a valid method. Valid methods are: \
|
||||
[GET, POST, PUT, DELETE, HEAD, PATCH]", method);
|
||||
ecx.span_err(dec_sp, message.as_str());
|
||||
}
|
||||
const STRUCT_PREFIX: &'static str = "ROCKET_ROUTE_STRUCT_";
|
||||
const FN_PREFIX: &'static str = "rocket_route_fn_";
|
||||
|
||||
struct RouteParams {
|
||||
use rocket::Method;
|
||||
|
||||
struct Params {
|
||||
method: Method,
|
||||
path: String,
|
||||
path: String
|
||||
}
|
||||
|
||||
fn demo_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem,
|
||||
annotated: &Annotatable, _push: &mut FnMut(Annotatable)) {
|
||||
// Word: #[demo]
|
||||
// List: #[demo(one, two, ..)] or #[demo(one = "1", ...)] or mix both
|
||||
// NameValue: #[demo = "1"]
|
||||
let params: &Vec<P<MetaItem>> = match meta_item.node {
|
||||
MetaItemKind::List(_, ref params) => params,
|
||||
// Would almost certainly be better to use "DummyResult" here.
|
||||
_ => ecx.span_fatal(meta_item.span,
|
||||
"incorrect use of macro. correct form is: #[demo(...)]"),
|
||||
};
|
||||
fn bad_item_fatal(ecx: &mut ExtCtxt, dec_sp: Span, i_sp: Span) -> ! {
|
||||
ecx.span_err(dec_sp, "This decorator cannot be used on non-functions...");
|
||||
ecx.span_fatal(i_sp, "...but an attempt to use it on the item below was made.")
|
||||
}
|
||||
|
||||
if params.len() < 2 {
|
||||
ecx.span_fatal(meta_item.span, "Bad invocation. Need >= 2 arguments.");
|
||||
}
|
||||
|
||||
let (method_param, kv_params) = params.split_first().unwrap();
|
||||
let method = if let MetaItemKind::Word(ref word) = method_param.node {
|
||||
let method = Method::from_str(word).unwrap_or_else(|e| {
|
||||
Method::Extension(String::from(&**word))
|
||||
});
|
||||
|
||||
if let Method::Extension(ref name) = method {
|
||||
bad_method_err(ecx, meta_item.span, name.as_str());
|
||||
fn bad_method_err(ecx: &mut ExtCtxt, dec_sp: Span, message: &str) -> Method {
|
||||
let message = format!("{} Valid methods are: [GET, PUT, POST, DELETE, \
|
||||
OPTIONS, HEAD, TRACE, CONNECT, PATCH]", message);
|
||||
ecx.span_err(dec_sp, message.as_str());
|
||||
Method::Get
|
||||
} else {
|
||||
method
|
||||
}
|
||||
} else {
|
||||
Method::Get
|
||||
};
|
||||
|
||||
let mut route_params: RouteParams = RouteParams {
|
||||
method: method,
|
||||
path: String::new()
|
||||
};
|
||||
|
||||
let mut found_path = false;
|
||||
for param in kv_params {
|
||||
if let MetaItemKind::NameValue(ref name, ref value) = param.node {
|
||||
match &**name {
|
||||
"path" => {
|
||||
found_path = true;
|
||||
if let LitKind::Str(ref string, _) = value.node {
|
||||
route_params.path = String::from(&**string);
|
||||
} else {
|
||||
ecx.span_err(param.span, "Path value must be string.");
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
ecx.span_err(param.span, "Unrecognized parameter.");
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
ecx.span_err(param.span, "Invalid parameter. Must be key = value.");
|
||||
}
|
||||
}
|
||||
|
||||
if !found_path {
|
||||
ecx.span_err(meta_item.span, "`path` argument is missing.");
|
||||
}
|
||||
|
||||
// for param in params {
|
||||
// if let MetaItemKind::Word(ref word) = param.node {
|
||||
// if hyper::method::Method::from_str(word).is_ok() {
|
||||
// println!("METHOD! {}", word);
|
||||
// }
|
||||
|
||||
// println!("WORD Param: {:?}", param);
|
||||
// } else {
|
||||
// println!("NOT word Param: {:?}", param);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fn get_fn_decl<'a>(ecx: &mut ExtCtxt, sp: Span, annotated: &'a Annotatable)
|
||||
-> (&'a P<Item>, &'a P<FnDecl>) {
|
||||
// `annotated` is the AST object for the annotated item.
|
||||
let item: &P<Item> = match annotated {
|
||||
&Annotatable::Item(ref item) => item,
|
||||
|
@ -123,12 +66,121 @@ fn demo_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem,
|
|||
_ => bad_item_fatal(ecx, sp, item.span)
|
||||
};
|
||||
|
||||
println!("Function arguments: {:?}", fn_decl.inputs);
|
||||
(item, fn_decl)
|
||||
}
|
||||
|
||||
fn get_route_params(ecx: &mut ExtCtxt, meta_item: &MetaItem) -> Params {
|
||||
// First, check that the macro was used in the #[route(a, b, ..)] form.
|
||||
let params: &Vec<P<MetaItem>> = match meta_item.node {
|
||||
MetaItemKind::List(_, ref params) => params,
|
||||
_ => ecx.span_fatal(meta_item.span,
|
||||
"incorrect use of macro. correct form is: #[demo(...)]"),
|
||||
};
|
||||
|
||||
// Ensure we can unwrap the k = v params.
|
||||
if params.len() < 1 {
|
||||
bad_method_err(ecx, meta_item.span, "HTTP method parameter is missing.");
|
||||
ecx.span_fatal(meta_item.span, "At least 2 arguments are required.");
|
||||
}
|
||||
|
||||
// Get the method and the rest of the k = v params. Ensure method parameter
|
||||
// is valid. If it's not, issue an error but use "GET" to continue parsing.
|
||||
let (method_param, kv_params) = params.split_first().unwrap();
|
||||
let method = if let MetaItemKind::Word(ref word) = method_param.node {
|
||||
Method::from_str(word).unwrap_or_else(|_| {
|
||||
let message = format!("{} is not a valid method.", word);
|
||||
bad_method_err(ecx, method_param.span, message.as_str())
|
||||
})
|
||||
} else {
|
||||
bad_method_err(ecx, method_param.span, "Invalid parameter. Expected a
|
||||
valid HTTP method at this position.")
|
||||
};
|
||||
|
||||
// Now grab all of the required and optional parameters.
|
||||
let req: [&'static str; 1] = ["path"];
|
||||
let opt: [&'static str; 0] = [];
|
||||
let kv_pairs = get_key_values(ecx, meta_item.span, &req, &opt, kv_params);
|
||||
|
||||
// Ensure we have a path, just to keep parsing and generating errors.
|
||||
let path = kv_pairs.get("path").map_or(String::from("/"), |s| {
|
||||
String::from(*s)
|
||||
});
|
||||
|
||||
Params {
|
||||
method: method,
|
||||
path: path
|
||||
}
|
||||
}
|
||||
|
||||
fn route_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem,
|
||||
annotated: &Annotatable, push: &mut FnMut(Annotatable)) {
|
||||
let (item, fn_decl) = get_fn_decl(ecx, sp, annotated);
|
||||
let route_params = get_route_params(ecx, meta_item);
|
||||
|
||||
let route_fn_name = prepend_ident(FN_PREFIX, &item.ident);
|
||||
let fn_name = item.ident;
|
||||
push(Annotatable::Item(quote_item!(ecx,
|
||||
fn $route_fn_name(_req: Request) -> Response {
|
||||
let result = $fn_name();
|
||||
println!("Routing function. Result: {}", result);
|
||||
Response
|
||||
}
|
||||
).unwrap()));
|
||||
|
||||
let struct_name = prepend_ident(STRUCT_PREFIX, &item.ident);
|
||||
let path = route_params.path;
|
||||
let struct_item = quote_item!(ecx,
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub static $struct_name: Route<'static> = Route {
|
||||
method: Method::Get, // FIXME
|
||||
path: $path,
|
||||
handler: $route_fn_name
|
||||
};
|
||||
).unwrap();
|
||||
push(Annotatable::Item(struct_item));
|
||||
}
|
||||
|
||||
fn get_paths<'a>(parser: &mut Parser<'a>) -> PResult<'a, Vec<Path>> {
|
||||
if parser.eat(&Token::Eof) {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
loop {
|
||||
results.push(try!(parser.parse_path(PathParsingMode::NoTypesAllowed)));
|
||||
if !parser.eat(&Token::Comma) {
|
||||
try!(parser.expect(&Token::Eof));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn routes_macro(ecx: &mut ExtCtxt, sp: Span, args: &[TokenTree])
|
||||
-> Box<MacResult + 'static> {
|
||||
let mut parser = ecx.new_parser_from_tts(args);
|
||||
let mut paths = get_paths(&mut parser).unwrap_or_else(|mut e| {
|
||||
e.emit();
|
||||
vec![]
|
||||
});
|
||||
|
||||
// Prefix each path terminator with STRUCT_PREFIX.
|
||||
for p in &mut paths {
|
||||
let last = p.segments.len() - 1;
|
||||
let last_seg = &mut p.segments[last];
|
||||
let new_ident = prepend_ident(STRUCT_PREFIX, &last_seg.identifier);
|
||||
last_seg.identifier = new_ident;
|
||||
}
|
||||
|
||||
// Build up the P<Expr> for each &path.
|
||||
let path_exprs = paths.iter().map(|p| { quote_expr!(ecx, &$p) }).collect();
|
||||
MacEager::expr(ecx.expr_vec_slice(sp, path_exprs))
|
||||
}
|
||||
|
||||
#[plugin_registrar]
|
||||
pub fn plugin_registrar(reg: &mut Registry) {
|
||||
// reg.register_macro("rn", expand_rn);
|
||||
reg.register_syntax_extension(intern("route"),
|
||||
SyntaxExtension::MultiDecorator(Box::new(demo_decorator)));
|
||||
SyntaxExtension::MultiDecorator(Box::new(route_decorator)));
|
||||
reg.register_macro("routes", routes_macro);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
use syntax::parse::{token};
|
||||
use syntax::ast::{Ident, MetaItem, MetaItemKind, LitKind};
|
||||
use syntax::ext::base::{ExtCtxt};
|
||||
use syntax::codemap::Span;
|
||||
use syntax::ptr::P;
|
||||
|
||||
use std::collections::{HashSet, HashMap};
|
||||
|
||||
// macro_rules! debug {
|
||||
// ($session:expr, $span:expr, $($message:tt)*) => ({
|
||||
// if cfg!(debug) {
|
||||
// span_note!($session, $span, "{}:{}", file!(), line!());
|
||||
// span_note!($session, $span, $($message)*);
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
macro_rules! debug {
|
||||
($($message:tt)*) => ({
|
||||
if DEBUG {
|
||||
println!("{}:{}", file!(), line!());
|
||||
println!($($message)*);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prepend_ident<T: ToString>(other: T, ident: &Ident) -> Ident {
|
||||
let mut new_ident = other.to_string();
|
||||
new_ident.push_str(ident.name.to_string().as_str());
|
||||
token::str_to_ident(new_ident.as_str())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn append_ident<T: ToString>(ident: &Ident, other: T) -> Ident {
|
||||
let mut new_ident = ident.name.to_string();
|
||||
new_ident.push_str(other.to_string().as_str());
|
||||
token::str_to_ident(new_ident.as_str())
|
||||
}
|
||||
|
||||
pub fn get_key_values<'b>(ecx: &mut ExtCtxt, sp: Span, required: &[&str],
|
||||
optional: &[&str], kv_params: &'b [P<MetaItem>])
|
||||
-> HashMap<&'b str, &'b str> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut kv_pairs = HashMap::new();
|
||||
|
||||
// Collect all the kv pairs, keeping track of what we've seen.
|
||||
for param in kv_params {
|
||||
if let MetaItemKind::NameValue(ref name, ref value) = param.node {
|
||||
if required.contains(&&**name) || optional.contains(&&**name) {
|
||||
if seen.contains(&**name) {
|
||||
let msg = format!("'{}' cannot be set twice.", &**name);
|
||||
ecx.span_err(param.span, &msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
seen.insert(&**name);
|
||||
if let LitKind::Str(ref string, _) = value.node {
|
||||
kv_pairs.insert(&**name, &**string);
|
||||
} else {
|
||||
ecx.span_err(param.span, "Value must be a string.");
|
||||
}
|
||||
} else {
|
||||
let msg = format!("'{}' is not a valid parameter.", &**name);
|
||||
ecx.span_err(param.span, &msg);
|
||||
}
|
||||
} else {
|
||||
ecx.span_err(param.span, "Expected 'key = value', found:");
|
||||
}
|
||||
}
|
||||
|
||||
// Now, trigger an error for missing `required` params.
|
||||
for req in required {
|
||||
if !seen.contains(req) {
|
||||
let m = format!("'{}' parameter is required but is missing.", req);
|
||||
ecx.span_err(sp, &m);
|
||||
}
|
||||
}
|
||||
|
||||
kv_pairs
|
||||
}
|
||||
// pub fn find_value_for(key: &str, kv_params: &[P<MetaItem>]) -> Option<String> {
|
||||
// for param in kv_params {
|
||||
// if let MetaItemKind::NameValue(ref name, ref value) = param.node {
|
||||
// if &**name == key {
|
||||
// if let LitKind::Str(ref string, _) = value.node {
|
||||
// return Some(String::from(&**string));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// None
|
||||
// }
|
13
src/main.rs
13
src/main.rs
|
@ -1,13 +0,0 @@
|
|||
#![feature(plugin)]
|
||||
#![plugin(rocket_macros)]
|
||||
|
||||
#[route(POST, path = "/")]
|
||||
fn function(_x: usize, _y: isize) {
|
||||
|
||||
}
|
||||
|
||||
#[route(GET, path = "/")]
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
function(1, 2);
|
||||
}
|
Loading…
Reference in New Issue