From 6a2a1fbe012274b3d18ee08f34b715a81d463699 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Sat, 17 Sep 2022 10:21:50 -0400 Subject: [PATCH 01/24] feat: initial impl ic-kit-http, supports only GET --- .gitignore | 3 +- Cargo.toml | 2 + dfx.json | 7 +- examples/counter/Cargo.toml | 4 +- examples/pastebin/Cargo.toml | 13 +++ examples/pastebin/candid.did | 1 + examples/pastebin/src/canister.rs | 66 +++++++++++++ examples/pastebin/src/lib.rs | 2 + examples/pastebin/src/main.rs | 3 + ic-kit-http/Cargo.toml | 22 +++++ ic-kit-http/README.md | 3 + ic-kit-http/src/lib.rs | 44 +++++++++ ic-kit-macros/Cargo.toml | 4 + ic-kit-macros/src/export_service.rs | 11 +++ ic-kit-macros/src/http.rs | 140 ++++++++++++++++++++++++++++ ic-kit-macros/src/lib.rs | 51 +++++++++- ic-kit-runtime/src/replica.rs | 7 +- ic-kit-stable/src/core/allocator.rs | 2 +- ic-kit-stable/src/core/global.rs | 1 - ic-kit/Cargo.toml | 5 +- ic-kit/src/lib.rs | 6 ++ rust-toolchain.toml | 4 - 22 files changed, 383 insertions(+), 18 deletions(-) create mode 100644 examples/pastebin/Cargo.toml create mode 100644 examples/pastebin/candid.did create mode 100644 examples/pastebin/src/canister.rs create mode 100644 examples/pastebin/src/lib.rs create mode 100644 examples/pastebin/src/main.rs create mode 100644 ic-kit-http/Cargo.toml create mode 100644 ic-kit-http/README.md create mode 100644 ic-kit-http/src/lib.rs create mode 100644 ic-kit-macros/src/http.rs delete mode 100644 rust-toolchain.toml diff --git a/.gitignore b/.gitignore index df88fc8..bac5453 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ #/target Cargo.lock -.dfx \ No newline at end of file +.dfx +*.old* \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index b50a21f..3939798 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "examples/fib", "examples/multi_counter", "examples/naming_system", + "examples/pastebin", "ic-kit", "ic-kit-certified", "ic-kit-macros", @@ -13,6 +14,7 @@ members = [ "ic-kit-runtime", "ic-kit-stable", "ic-kit-sys", + "ic-kit-http", ] [profile.canister-release] diff --git a/dfx.json b/dfx.json index 04f9a63..7ec8ff1 100644 --- a/dfx.json +++ b/dfx.json @@ -1,6 +1,6 @@ { "version": 1, - "dfx": "0.11.0", + "dfx": "0.11.1", "canisters": { "counter": { "candid": "examples/counter/candid.did", @@ -16,6 +16,11 @@ "candid": "examples/naming_system/candid.did", "package": "naming_system", "type": "rust" + }, + "pastebin": { + "candid": "examples/pastebin/candid.did", + "package": "ic_kit_example_pastebin", + "type": "rust" } }, "networks": { diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml index 7ad73c8..2f2cd06 100644 --- a/examples/counter/Cargo.toml +++ b/examples/counter/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ic-kit = {path="../../ic-kit"} +# With the `http` feature enabled, and no configured routes, the canister will provide status info at the index path. +ic-kit = {path="../../ic-kit", features = ["http"]} + [[bin]] name = "ic_kit_example_counter" diff --git a/examples/pastebin/Cargo.toml b/examples/pastebin/Cargo.toml new file mode 100644 index 0000000..5315d70 --- /dev/null +++ b/examples/pastebin/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "ic_kit_example_pastebin" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ic-kit = {path="../../ic-kit", features = ["http"]} + +[[bin]] +name = "ic_kit_example_pastebin" +path = "src/main.rs" diff --git a/examples/pastebin/candid.did b/examples/pastebin/candid.did new file mode 100644 index 0000000..6ce8364 --- /dev/null +++ b/examples/pastebin/candid.did @@ -0,0 +1 @@ +service : {} \ No newline at end of file diff --git a/examples/pastebin/src/canister.rs b/examples/pastebin/src/canister.rs new file mode 100644 index 0000000..65b0e8d --- /dev/null +++ b/examples/pastebin/src/canister.rs @@ -0,0 +1,66 @@ +use std::collections::HashMap; + +use ic_kit::prelude::*; + +const INDEX_HTML: &str = r#" + + + + + IC Pastebin + + +

IC Pastebin

+
+ +
+ +
+ + +"#; + +#[get(route = "/")] +fn index_handler(_: HttpRequest, _: Params) -> HttpResponse { + HttpResponse { + status_code: 200, + headers: vec![], + body: INDEX_HTML.into(), + streaming_strategy: None, + upgrade: false, + } +} + +#[get(route = "/:file")] +fn get_file(_: HttpRequest, p: Params) -> HttpResponse { + let file = p.get("file").unwrap(); + + let res = format!("reading file: {}", file); + + HttpResponse { + status_code: 200, + headers: vec![], + body: res.into(), + streaming_strategy: None, + upgrade: false, + } +} + +// #[put(route = "/:file")] +// fn put_file(req: HttpRequest, p: Params) -> HttpResponse { +// let file = p.get("file").unwrap(); + +// let res = format!("recieved file: {} ({} bytes)", file, req.body.len()); + +// HttpResponse { +// status_code: 200, +// headers: vec![], +// body: res.into_bytes(), +// streaming_strategy: None, +// upgrade: false, +// } +// } + +#[derive(KitCanister)] +#[candid_path("candid.did")] +pub struct PastebinCanister; diff --git a/examples/pastebin/src/lib.rs b/examples/pastebin/src/lib.rs new file mode 100644 index 0000000..000fadf --- /dev/null +++ b/examples/pastebin/src/lib.rs @@ -0,0 +1,2 @@ +pub mod canister; +pub use canister::PastebinCanister; diff --git a/examples/pastebin/src/main.rs b/examples/pastebin/src/main.rs new file mode 100644 index 0000000..c53437a --- /dev/null +++ b/examples/pastebin/src/main.rs @@ -0,0 +1,3 @@ +mod canister; + +fn main() {} diff --git a/ic-kit-http/Cargo.toml b/ic-kit-http/Cargo.toml new file mode 100644 index 0000000..8cd12c6 --- /dev/null +++ b/ic-kit-http/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ic-kit-http" +version = "0.1.0-alpha.0" +edition = "2021" +authors = ["Ossian Mapes ", "Parsa Ghadimi "] +description = "IC-Kit's macros for canister development" +license = "MIT" +readme = "README.md" +repository = "https://github.com/Psychedelic/ic-kit" +documentation = "https://docs.rs/ic-kit-http" +homepage = "https://sly.ooo" +categories = ["api-bindings"] +keywords = ["internet-computer", "canister", "fleek", "psychedelic"] +include = ["src", "Cargo.toml", "README.md"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ic-kit-macros = { path = "../ic-kit-macros", features = ["http"] } +candid = "0.7" +serde = "1.0" +matchit = "0.6.0" \ No newline at end of file diff --git a/ic-kit-http/README.md b/ic-kit-http/README.md new file mode 100644 index 0000000..01b1bba --- /dev/null +++ b/ic-kit-http/README.md @@ -0,0 +1,3 @@ +# Ic Kit Http + +Types and macro export for canister http routing \ No newline at end of file diff --git a/ic-kit-http/src/lib.rs b/ic-kit-http/src/lib.rs new file mode 100644 index 0000000..6fb3d0d --- /dev/null +++ b/ic-kit-http/src/lib.rs @@ -0,0 +1,44 @@ +use candid::{CandidType, Deserialize, Func, Nat}; + +pub use ic_kit_macros::{delete, get, post, put}; +pub use matchit::{Match, MatchError, Params, Router as BasicRouter}; + +pub type HeaderField = (String, String); + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct HttpRequest { + pub method: String, + pub url: String, + pub headers: Vec<(String, String)>, + pub body: Vec, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct HttpResponse { + pub status_code: u16, + pub headers: Vec, + pub body: Vec, + pub streaming_strategy: Option, + pub upgrade: bool, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct StreamingCallbackToken { + pub key: String, + pub content_encoding: String, + pub index: Nat, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum StreamingStrategy { + Callback { + callback: Func, + token: StreamingCallbackToken, + }, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct StreamingCallbackHttpResponse { + pub body: Vec, + pub token: Option, +} diff --git a/ic-kit-macros/Cargo.toml b/ic-kit-macros/Cargo.toml index d18f36a..c8b1f89 100644 --- a/ic-kit-macros/Cargo.toml +++ b/ic-kit-macros/Cargo.toml @@ -23,5 +23,9 @@ serde = "1.0" serde_tokenstream = "0.1" lazy_static = "1.4" +[features] +default = [] +http = [] + [lib] proc-macro = true diff --git a/ic-kit-macros/src/export_service.rs b/ic-kit-macros/src/export_service.rs index 90871b8..b7e4e5b 100644 --- a/ic-kit-macros/src/export_service.rs +++ b/ic-kit-macros/src/export_service.rs @@ -220,6 +220,15 @@ pub fn export_service(input: DeriveInput, save_candid_path: Option) quote! {} }; + let http_request: TokenStream; + + http_request = match () { + #[cfg(not(feature = "http"))] + () => quote! {}, + #[cfg(feature = "http")] + () => crate::http::gen_http_request_code(), + }; + quote! { impl ic_kit::KitCanister for #name { #[cfg(not(target_family = "wasm"))] @@ -249,6 +258,8 @@ pub fn export_service(input: DeriveInput, save_candid_path: Option) } #save_candid + + #http_request } } diff --git a/ic-kit-macros/src/http.rs b/ic-kit-macros/src/http.rs new file mode 100644 index 0000000..5843e44 --- /dev/null +++ b/ic-kit-macros/src/http.rs @@ -0,0 +1,140 @@ +use lazy_static::lazy_static; +use proc_macro2::TokenStream; +use quote::quote; +use serde::Deserialize; +use serde_tokenstream::from_tokenstream; +use std::sync::Mutex; +use syn::{spanned::Spanned, Error}; + +struct Method { + name: String, + route: String, + method: String, +} + +lazy_static! { + static ref GETS: Mutex> = Mutex::new(Vec::new()); +} + +#[derive(Deserialize)] +struct Config { + route: String, +} + +/// Process a rust syntax and generate the code for processing it. +pub fn gen_handler_code( + method: &str, + attr: TokenStream, + item: TokenStream, +) -> Result { + let attrs = from_tokenstream::(&attr)?; + let fun: syn::ItemFn = syn::parse2::(item.clone()).map_err(|e| { + Error::new( + item.span(), + format!("#[{0}] must be above a function. \n{1}", method, e), + ) + })?; + + GETS.lock().unwrap().push(Method { + name: fun.sig.ident.to_string(), + route: attrs.route, + method: method.into(), + }); + + Ok(quote! { + #item + }) +} + +pub fn gen_http_request_code() -> TokenStream { + let routes = GETS.lock().unwrap(); + + let mut routes_insert = TokenStream::new(); + + if routes.is_empty() { + // if no routes, provide a basic index displaying canister stats + routes_insert = quote! { + fn index(_: ic_kit::http::HttpRequest, _: ic_kit::http::Params) -> ic_kit::http::HttpResponse { + let res = format!("{{\"cycles\": {}}}", ic_kit::ic::balance()); + + ic_kit::http::HttpResponse { + status_code: 200, + headers: vec![], + body: res.into_bytes(), + streaming_strategy: None, + upgrade: false, + } + } + + router.insert("/", &index); + }; + } else { + for Method { + method, + name, + route, + } in routes.iter() + { + let name = syn::Ident::new(name, proc_macro2::Span::call_site()); + + routes_insert.extend(quote! { + router.0.insert(#route, &#name); + }); + } + } + quote! { + pub type HandlerFn = dyn Fn(ic_kit::http::HttpRequest, ic_kit::http::Params) -> ic_kit::http::HttpResponse; + + + #[derive(Clone)] + pub struct Router<'a>(ic_kit::http::BasicRouter<&'a HandlerFn>); + impl<'a> Router<'a> { + pub fn insert(&mut self, path: &str, handler: &'a HandlerFn) { + self.0 + .insert(path, handler) + .unwrap_or_else(|e| ic::trap(&format!("{}", e))); + } + } + + + impl<'a> Default for Router<'a> { + fn default() -> Self { + let mut router = Self(ic_kit::http::BasicRouter::new()); + #routes_insert + router + } + } + + #[doc(hidden)] + #[export_name = "canister_query http_request"] + fn _ic_kit_canister_query_http_request() { + let bytes = ic_kit::utils::arg_data_raw(); + let args: (ic_kit::http::HttpRequest,) = match ic_kit::candid::decode_args(&bytes) { + Ok(v) => v, + Err(_) => { + ic_kit::utils::reject("Could not decode arguments."); + return; + } + }; + let (req,) = args; + ic_kit::ic::with(|router: &Router| { + // let certificate = ic::data_certificate().unwrap_or_else(|| ic::trap("no data certificate available")); + // ic::print(format!("{:?} {:?}", req, certificate)); + let result = match router.0.at(req.url.clone().as_str()) { + Ok(m) => (m.value)(req, m.params), + Err(e) => ic_kit::http::HttpResponse { + status_code: 404, + headers: vec![], + body: e.to_string().as_bytes().to_vec(), + streaming_strategy: None, + upgrade: false, + }, + }; + let bytes = + ic_kit::candid::encode_one(result).expect("Could not encode canister's response."); + ic_kit::utils::reply(&bytes); + }); + } + + } +} diff --git a/ic-kit-macros/src/lib.rs b/ic-kit-macros/src/lib.rs index 17f1afe..2a69af8 100644 --- a/ic-kit-macros/src/lib.rs +++ b/ic-kit-macros/src/lib.rs @@ -1,12 +1,15 @@ -mod entry; -mod export_service; -mod test; - use entry::{gen_entry_point_code, EntryPoint}; use proc_macro::TokenStream; use syn::parse_macro_input; use test::gen_test_code; +mod entry; +mod export_service; +mod test; + +#[cfg(feature = "http")] +mod http; + fn process_entry_point( entry_point: EntryPoint, attr: TokenStream, @@ -92,3 +95,43 @@ fn get_save_candid_path(input: &syn::DeriveInput) -> syn::Result Ok(None), } } + +/// Export a function as a HTTP GET handler. +#[cfg(feature = "http")] +#[proc_macro_attribute] +pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream { + http::gen_handler_code("GET", attr.into(), item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +/// Export a function as a HTTP POST handler. +#[cfg(feature = "http")] +#[proc_macro_attribute] +pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream { + http::gen_handler_code("POST", attr.into(), item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +/// Export a function as a HTTP PUT handler. +#[cfg(feature = "http")] +#[proc_macro_attribute] +pub fn put(_attr: TokenStream, _item: TokenStream) -> TokenStream { + // http::gen_handler_code(http::HttpMethod::PUT, attr.into(), item.into()) + // .unwrap_or_else(|error| error.to_compile_error()) + // .into() + + todo!() +} + +/// Export a function as a HTTP DELETE handler. +#[cfg(feature = "http")] +#[proc_macro_attribute] +pub fn delete(_attr: TokenStream, _item: TokenStream) -> TokenStream { + // http::gen_handler_code(http::HttpMethod::DELETE, attr.into(), item.into()) + // .unwrap_or_else(|error| error.to_compile_error()) + // .into() + + todo!() +} diff --git a/ic-kit-runtime/src/replica.rs b/ic-kit-runtime/src/replica.rs index 4c7913d..7f599d7 100644 --- a/ic-kit-runtime/src/replica.rs +++ b/ic-kit-runtime/src/replica.rs @@ -21,7 +21,6 @@ use ic_kit_sys::types::RejectionCode; use ic_types::Principal; use std::collections::HashMap; use std::future::Future; -use std::panic::{RefUnwindSafe, UnwindSafe}; use tokio::sync::{mpsc, oneshot}; /// A local replica that contains one or several canisters. @@ -178,9 +177,9 @@ async fn replica_worker(mut rx: mpsc::UnboundedReceiver) { /// Start a dedicated event loop for a canister, this will get CanisterMessage messages from a tokio /// channel and perform async fn canister_worker( - mut rx: mpsc::UnboundedReceiver, - mut replica: mpsc::UnboundedSender, - mut canister: Canister, + rx: mpsc::UnboundedReceiver, + replica: mpsc::UnboundedSender, + canister: Canister, ) { let canister_id = canister.id(); diff --git a/ic-kit-stable/src/core/allocator.rs b/ic-kit-stable/src/core/allocator.rs index aa609d0..1e66643 100644 --- a/ic-kit-stable/src/core/allocator.rs +++ b/ic-kit-stable/src/core/allocator.rs @@ -1,6 +1,6 @@ use crate::core::checksum::CheckedU40; use crate::core::hole::HoleList; -use crate::core::memory::{DefaultMemory, IcMemory, Memory}; +use crate::core::memory::{DefaultMemory, Memory}; use crate::core::utils::read_struct; use ic_kit::stable::StableMemoryError; diff --git a/ic-kit-stable/src/core/global.rs b/ic-kit-stable/src/core/global.rs index ea0a1ab..466150e 100644 --- a/ic-kit-stable/src/core/global.rs +++ b/ic-kit-stable/src/core/global.rs @@ -1,7 +1,6 @@ use crate::core::allocator::{BlockAddress, BlockSize, StableAllocator}; use crate::core::lru::LruCache; use ic_kit::stable::StableMemoryError; -use std::borrow::BorrowMut; use std::cell::RefCell; thread_local! { diff --git a/ic-kit/Cargo.toml b/ic-kit/Cargo.toml index e1fba49..9ddbf92 100644 --- a/ic-kit/Cargo.toml +++ b/ic-kit/Cargo.toml @@ -16,6 +16,7 @@ include = ["src", "Cargo.toml", "README.md"] [dependencies] ic-kit-sys = {path="../ic-kit-sys", version="0.1.3"} ic-kit-macros = {path="../ic-kit-macros", version="0.1.1-alpha.0"} +ic-kit-http = {path="../ic-kit-http", version="0.1.0-alpha.0", optional=true} candid="0.7" serde = "1.0" @@ -23,5 +24,7 @@ serde = "1.0" ic-kit-runtime = {path="../ic-kit-runtime", version="0.1.0-alpha.1"} [features] +default = [] experimental-stable64 = [] -experimental-cycles128 = [] \ No newline at end of file +experimental-cycles128 = [] +http = ["dep:ic-kit-http", "ic-kit-macros/http"] \ No newline at end of file diff --git a/ic-kit/src/lib.rs b/ic-kit/src/lib.rs index ebe518d..741206e 100644 --- a/ic-kit/src/lib.rs +++ b/ic-kit/src/lib.rs @@ -25,6 +25,9 @@ pub use ic_kit_macros::KitCanister; #[cfg(not(target_family = "wasm"))] pub use ic_kit_runtime as rt; +#[cfg(feature = "http")] +pub use ic_kit_http as http; + /// The famous prelude module which re exports the most useful methods. pub mod prelude { pub use super::canister::KitCanister; @@ -38,6 +41,9 @@ pub mod prelude { pub use ic_kit_macros::*; + #[cfg(feature = "http")] + pub use ic_kit_http::*; + #[cfg(not(target_family = "wasm"))] pub use ic_kit_runtime as rt; diff --git a/rust-toolchain.toml b/rust-toolchain.toml deleted file mode 100644 index 9b31b19..0000000 --- a/rust-toolchain.toml +++ /dev/null @@ -1,4 +0,0 @@ -[toolchain] -channel = "1.60.0" -targets = ["wasm32-unknown-unknown"] -components = ["rustfmt", "clippy"] From 4c0bc291b5f2f408f2e805d9958d1728dfdbf3f3 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Sat, 17 Sep 2022 13:20:02 -0400 Subject: [PATCH 02/24] feat: update call upgrade, [post, put, delete, option, head] macros --- Cargo.toml | 1 - examples/pastebin/src/canister.rs | 46 ++++++---- ic-kit-http/Cargo.toml | 3 +- ic-kit-http/src/lib.rs | 81 +++++++++++++++++ ic-kit-macros/src/http.rs | 145 ++++++++++++++++++++++++++---- ic-kit-macros/src/lib.rs | 45 +++++++--- 6 files changed, 274 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3939798..d14afe1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ [workspace] members = [ - "e2e", "examples/counter", "examples/factory_counter", "examples/fib", diff --git a/examples/pastebin/src/canister.rs b/examples/pastebin/src/canister.rs index 65b0e8d..c891a24 100644 --- a/examples/pastebin/src/canister.rs +++ b/examples/pastebin/src/canister.rs @@ -1,6 +1,7 @@ +use ic_kit::prelude::*; use std::collections::HashMap; -use ic_kit::prelude::*; +pub type Data = HashMap>; const INDEX_HTML: &str = r#" @@ -34,33 +35,42 @@ fn index_handler(_: HttpRequest, _: Params) -> HttpResponse { #[get(route = "/:file")] fn get_file(_: HttpRequest, p: Params) -> HttpResponse { let file = p.get("file").unwrap(); + ic::with(|data: &Data| match data.get(file) { + Some(content) => HttpResponse { + status_code: 200, + headers: vec![], + body: content.clone(), + streaming_strategy: None, + upgrade: false, + }, + None => HttpResponse { + status_code: 404, + headers: vec![], + body: format!("404: file not found `{}`", file).into(), + streaming_strategy: None, + upgrade: false, + }, + }) +} + +#[put(route = "/:file", upgrade = true)] +fn put_file(req: HttpRequest, p: Params) -> HttpResponse { + let filename = p.get("file").unwrap(); + let res = format!("recieved file: {} ({} bytes)", filename, req.body.len(),); - let res = format!("reading file: {}", file); + ic::with_mut(|d: &mut Data| { + d.insert(filename.to_string(), req.body); + }); HttpResponse { status_code: 200, headers: vec![], - body: res.into(), + body: res.into_bytes(), streaming_strategy: None, upgrade: false, } } -// #[put(route = "/:file")] -// fn put_file(req: HttpRequest, p: Params) -> HttpResponse { -// let file = p.get("file").unwrap(); - -// let res = format!("recieved file: {} ({} bytes)", file, req.body.len()); - -// HttpResponse { -// status_code: 200, -// headers: vec![], -// body: res.into_bytes(), -// streaming_strategy: None, -// upgrade: false, -// } -// } - #[derive(KitCanister)] #[candid_path("candid.did")] pub struct PastebinCanister; diff --git a/ic-kit-http/Cargo.toml b/ic-kit-http/Cargo.toml index 8cd12c6..fe64e9d 100644 --- a/ic-kit-http/Cargo.toml +++ b/ic-kit-http/Cargo.toml @@ -19,4 +19,5 @@ include = ["src", "Cargo.toml", "README.md"] ic-kit-macros = { path = "../ic-kit-macros", features = ["http"] } candid = "0.7" serde = "1.0" -matchit = "0.6.0" \ No newline at end of file +matchit = "0.6.0" +lazy_static = "1.4" \ No newline at end of file diff --git a/ic-kit-http/src/lib.rs b/ic-kit-http/src/lib.rs index 6fb3d0d..91ba7d2 100644 --- a/ic-kit-http/src/lib.rs +++ b/ic-kit-http/src/lib.rs @@ -42,3 +42,84 @@ pub struct StreamingCallbackHttpResponse { pub body: Vec, pub token: Option, } + +/// A [`Method`](https://developer.mozilla.org/en-US/docs/Web/API/Request/method) representation +/// used on Request objects. +#[derive(Debug, Clone, PartialEq, Hash, Eq)] +pub enum Method { + Head = 0, + Get, + Post, + Put, + Patch, + Delete, + Options, + Connect, + Trace, +} + +impl Method { + pub fn all() -> Vec { + vec![ + Method::Head, + Method::Get, + Method::Post, + Method::Put, + Method::Patch, + Method::Delete, + Method::Options, + Method::Connect, + Method::Trace, + ] + } +} + +impl From for Method { + fn from(m: String) -> Self { + match m.to_ascii_uppercase().as_str() { + "HEAD" => Method::Head, + "POST" => Method::Post, + "PUT" => Method::Put, + "PATCH" => Method::Patch, + "DELETE" => Method::Delete, + "OPTIONS" => Method::Options, + "CONNECT" => Method::Connect, + "TRACE" => Method::Trace, + _ => Method::Get, + } + } +} + +impl From for String { + fn from(val: Method) -> Self { + val.as_ref().to_string() + } +} + +impl AsRef for Method { + fn as_ref(&self) -> &'static str { + match self { + Method::Head => "HEAD", + Method::Post => "POST", + Method::Put => "PUT", + Method::Patch => "PATCH", + Method::Delete => "DELETE", + Method::Options => "OPTIONS", + Method::Connect => "CONNECT", + Method::Trace => "TRACE", + Method::Get => "GET", + } + } +} + +impl ToString for Method { + fn to_string(&self) -> String { + (*self).clone().into() + } +} + +impl Default for Method { + fn default() -> Self { + Method::Get + } +} diff --git a/ic-kit-macros/src/http.rs b/ic-kit-macros/src/http.rs index 5843e44..2d3d895 100644 --- a/ic-kit-macros/src/http.rs +++ b/ic-kit-macros/src/http.rs @@ -10,6 +10,7 @@ struct Method { name: String, route: String, method: String, + upgrade: bool, } lazy_static! { @@ -19,6 +20,7 @@ lazy_static! { #[derive(Deserialize)] struct Config { route: String, + upgrade: Option, } /// Process a rust syntax and generate the code for processing it. @@ -39,6 +41,7 @@ pub fn gen_handler_code( name: fun.sig.ident.to_string(), route: attrs.route, method: method.into(), + upgrade: attrs.upgrade.unwrap_or(false), }); Ok(quote! { @@ -50,6 +53,7 @@ pub fn gen_http_request_code() -> TokenStream { let routes = GETS.lock().unwrap(); let mut routes_insert = TokenStream::new(); + let mut upgradable = false; if routes.is_empty() { // if no routes, provide a basic index displaying canister stats @@ -66,45 +70,151 @@ pub fn gen_http_request_code() -> TokenStream { } } - router.insert("/", &index); + router.insert("GET", "/", (index, false)); }; } else { for Method { method, name, route, + upgrade, } in routes.iter() { let name = syn::Ident::new(name, proc_macro2::Span::call_site()); + if *upgrade { + upgradable = true; + } + routes_insert.extend(quote! { - router.0.insert(#route, &#name); + router.insert(#method, #route, (#name, #upgrade)); }); } } - quote! { - pub type HandlerFn = dyn Fn(ic_kit::http::HttpRequest, ic_kit::http::Params) -> ic_kit::http::HttpResponse; + let mut upgrade_code = TokenStream::new(); + let mut query_code = TokenStream::new(); - #[derive(Clone)] - pub struct Router<'a>(ic_kit::http::BasicRouter<&'a HandlerFn>); - impl<'a> Router<'a> { - pub fn insert(&mut self, path: &str, handler: &'a HandlerFn) { - self.0 - .insert(path, handler) - .unwrap_or_else(|e| ic::trap(&format!("{}", e))); + if upgradable { + query_code.extend(quote! { + let (handler, upgrade) = m.value; + if *upgrade { + ic_kit::http::HttpResponse { + status_code: 100, + headers: vec![], + body: vec![], + streaming_strategy: None, + upgrade: true, + } + } else { + handler(req, m.params) } - } + }); + + upgrade_code.extend(quote! { + #[doc(hidden)] + #[export_name = "canister_update http_request_update"] + fn _ic_kit_canister_update_http_request_update() { + let bytes = ic_kit::utils::arg_data_raw(); + let args: (ic_kit::http::HttpRequest,) = match ic_kit::candid::decode_args(&bytes) { + Ok(v) => v, + Err(_) => { + ic_kit::utils::reject("Could not decode arguments."); + return; + } + }; + let (req,) = args; + ic_kit::ic::with(|router: &Router| { + // let certificate = ic::data_certificate().unwrap_or_else(|| ic::trap("no data certificate available")); + // ic::print(format!("{:?} {:?}", req, certificate)); + let result = match router.at(&req.method.clone(), &req.url.clone()) { + Ok(m) => { + let (handler, _) = m.value; + handler(req, m.params) + }, + Err(e) => ic_kit::http::HttpResponse { + status_code: 404, + headers: vec![], + body: e.to_string().as_bytes().to_vec(), + streaming_strategy: None, + upgrade: false, + }, + }; + let bytes = + ic_kit::candid::encode_one(result).expect("Could not encode canister's response."); + ic_kit::utils::reply(&bytes); + }); + } + }); + } else { + query_code.extend(quote! { + let (handler, _) = m.value; + handler(req, m.params) + }); + }; + quote! { + pub type HandlerFn = (fn(ic_kit::http::HttpRequest, ic_kit::http::Params) -> ic_kit::http::HttpResponse, bool); - impl<'a> Default for Router<'a> { + #[derive(Clone)] + pub struct Router { + get: ic_kit::http::BasicRouter, + post: ic_kit::http::BasicRouter, + put: ic_kit::http::BasicRouter, + delete: ic_kit::http::BasicRouter, + patch: ic_kit::http::BasicRouter, + head: ic_kit::http::BasicRouter, + options: ic_kit::http::BasicRouter, + } + + impl Default for Router { fn default() -> Self { - let mut router = Self(ic_kit::http::BasicRouter::new()); + let mut router = Self { + get: ic_kit::http::BasicRouter::new(), + post: ic_kit::http::BasicRouter::new(), + put: ic_kit::http::BasicRouter::new(), + delete: ic_kit::http::BasicRouter::new(), + patch: ic_kit::http::BasicRouter::new(), + head: ic_kit::http::BasicRouter::new(), + options: ic_kit::http::BasicRouter::new(), + }; #routes_insert router } } + impl Router { + pub fn insert(&mut self, method: &str, path: &str, handler: HandlerFn) { + match method { + "GET" => self.get.insert(path, handler).unwrap(), + "POST" => self.post.insert(path, handler).unwrap(), + "PUT" => self.put.insert(path, handler).unwrap(), + "DELETE" => self.delete.insert(path, handler).unwrap(), + "PATCH" => self.patch.insert(path, handler).unwrap(), + "HEAD" => self.head.insert(path, handler).unwrap(), + "OPTIONS" => self.options.insert(path, handler).unwrap(), + _ => panic!("unsupported method: {}", method), + }; + } + + pub fn at<'s: 'p, 'p>( + &'s self, + method: &str, + path: &'p str, + ) -> Result, MatchError> { + match method { + "GET" => self.get.at(path), + "POST" => self.post.at(path), + "PUT" => self.put.at(path), + "DELETE" => self.delete.at(path), + "PATCH" => self.patch.at(path), + "HEAD" => self.head.at(path), + "OPTIONS" => self.options.at(path), + _ => Err(MatchError::NotFound), + } + } + } + #[doc(hidden)] #[export_name = "canister_query http_request"] fn _ic_kit_canister_query_http_request() { @@ -120,8 +230,10 @@ pub fn gen_http_request_code() -> TokenStream { ic_kit::ic::with(|router: &Router| { // let certificate = ic::data_certificate().unwrap_or_else(|| ic::trap("no data certificate available")); // ic::print(format!("{:?} {:?}", req, certificate)); - let result = match router.0.at(req.url.clone().as_str()) { - Ok(m) => (m.value)(req, m.params), + let result = match router.at(&req.method.clone(), &req.url.clone()) { + Ok(m) => { + #query_code + }, Err(e) => ic_kit::http::HttpResponse { status_code: 404, headers: vec![], @@ -136,5 +248,6 @@ pub fn gen_http_request_code() -> TokenStream { }); } + #upgrade_code } } diff --git a/ic-kit-macros/src/lib.rs b/ic-kit-macros/src/lib.rs index 2a69af8..032f97f 100644 --- a/ic-kit-macros/src/lib.rs +++ b/ic-kit-macros/src/lib.rs @@ -117,21 +117,44 @@ pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream { /// Export a function as a HTTP PUT handler. #[cfg(feature = "http")] #[proc_macro_attribute] -pub fn put(_attr: TokenStream, _item: TokenStream) -> TokenStream { - // http::gen_handler_code(http::HttpMethod::PUT, attr.into(), item.into()) - // .unwrap_or_else(|error| error.to_compile_error()) - // .into() - - todo!() +pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream { + http::gen_handler_code("PUT", attr.into(), item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() } /// Export a function as a HTTP DELETE handler. #[cfg(feature = "http")] #[proc_macro_attribute] -pub fn delete(_attr: TokenStream, _item: TokenStream) -> TokenStream { - // http::gen_handler_code(http::HttpMethod::DELETE, attr.into(), item.into()) - // .unwrap_or_else(|error| error.to_compile_error()) - // .into() +pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream { + http::gen_handler_code("DELETE", attr.into(), item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +/// Export a function as a HTTP PATCH handler. +#[cfg(feature = "http")] +#[proc_macro_attribute] +pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream { + http::gen_handler_code("PATCH", attr.into(), item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +/// Export a function as a HTTP OPTIONS handler. +#[cfg(feature = "http")] +#[proc_macro_attribute] +pub fn options(attr: TokenStream, item: TokenStream) -> TokenStream { + http::gen_handler_code("OPTIONS", attr.into(), item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} - todo!() +/// Export a function as a HTTP HEAD handler. +#[cfg(feature = "http")] +#[proc_macro_attribute] +pub fn head(attr: TokenStream, item: TokenStream) -> TokenStream { + http::gen_handler_code("HEAD", attr.into(), item.into()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() } From d4d6b8c9ab69225f8f399ac700b9e8d46aed3572 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Sat, 17 Sep 2022 13:38:13 -0400 Subject: [PATCH 03/24] feat: HttpRequest header helper, HttpResponse builder, pastebin returns different index for curl --- examples/pastebin/src/canister.rs | 62 +++++++++++++++++-------------- ic-kit-http/src/lib.rs | 61 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 28 deletions(-) diff --git a/examples/pastebin/src/canister.rs b/examples/pastebin/src/canister.rs index c891a24..1c3a7ee 100644 --- a/examples/pastebin/src/canister.rs +++ b/examples/pastebin/src/canister.rs @@ -21,38 +21,50 @@ const INDEX_HTML: &str = r#" "#; +const INDEX_MANPAGE: &str = r#" +IC PASTEBIN(1) IC PASTEBIN IC PASTEBIN(1) + +NAME + + ic-pastebin - HTTP pastebin canister for the Internet Computer + +DESCRIPTION + + The ic-pastebin canister is a simple pastebin canister that allows users to + upload text and get a URL to share it with others. + + The canister is written in Rust and uses the ic-kit library to provide + access to the Internet Computer. + +USAGE + + curl -T file.txt https://rrkah-fqaaa-aaaaa-aaaaq-cai.raw.ic0.app + curl https://rrkah-fqaaa-aaaaa-aaaaq-cai.raw.ic0.app/file.txt +"#; + +/// Index handler #[get(route = "/")] -fn index_handler(_: HttpRequest, _: Params) -> HttpResponse { - HttpResponse { - status_code: 200, - headers: vec![], - body: INDEX_HTML.into(), - streaming_strategy: None, - upgrade: false, +fn index_handler(r: HttpRequest, _: Params) -> HttpResponse { + if let Some(ua) = r.header("User-Agent") { + if ua.contains("curl") { + return HttpResponse::ok().with_body(INDEX_MANPAGE.into()); + } } + + HttpResponse::ok().with_body(INDEX_HTML.into()) } +/// Get paste handler #[get(route = "/:file")] fn get_file(_: HttpRequest, p: Params) -> HttpResponse { let file = p.get("file").unwrap(); ic::with(|data: &Data| match data.get(file) { - Some(content) => HttpResponse { - status_code: 200, - headers: vec![], - body: content.clone(), - streaming_strategy: None, - upgrade: false, - }, - None => HttpResponse { - status_code: 404, - headers: vec![], - body: format!("404: file not found `{}`", file).into(), - streaming_strategy: None, - upgrade: false, - }, + Some(content) => HttpResponse::ok().with_body(content.clone()), + None => HttpResponse::new(404).with_body(format!("404: file not found `{}`", file).into()), }) } +/// Upload paste handler #[put(route = "/:file", upgrade = true)] fn put_file(req: HttpRequest, p: Params) -> HttpResponse { let filename = p.get("file").unwrap(); @@ -62,13 +74,7 @@ fn put_file(req: HttpRequest, p: Params) -> HttpResponse { d.insert(filename.to_string(), req.body); }); - HttpResponse { - status_code: 200, - headers: vec![], - body: res.into_bytes(), - streaming_strategy: None, - upgrade: false, - } + HttpResponse::ok().with_body(res.into()) } #[derive(KitCanister)] diff --git a/ic-kit-http/src/lib.rs b/ic-kit-http/src/lib.rs index 91ba7d2..a2c362c 100644 --- a/ic-kit-http/src/lib.rs +++ b/ic-kit-http/src/lib.rs @@ -13,6 +13,15 @@ pub struct HttpRequest { pub body: Vec, } +impl HttpRequest { + pub fn header(&self, name: &str) -> Option<&str> { + self.headers + .iter() + .find(|(key, _)| key.to_lowercase() == name.to_lowercase()) + .map(|(_, value)| value.as_str()) + } +} + #[derive(Clone, Debug, CandidType, Deserialize)] pub struct HttpResponse { pub status_code: u16, @@ -22,6 +31,58 @@ pub struct HttpResponse { pub upgrade: bool, } +impl HttpResponse { + pub fn ok() -> Self { + Self { + status_code: 200, + headers: vec![], + body: vec![], + streaming_strategy: None, + upgrade: false, + } + } + + pub fn new(status_code: u16) -> Self { + Self { + status_code, + headers: vec![], + body: vec![], + streaming_strategy: None, + upgrade: false, + } + } + + #[inline(always)] + pub fn with_body(mut self, body: Vec) -> Self { + self.body = body; + self + } + + #[inline(always)] + pub fn with_header(mut self, name: &str, value: &str) -> Self { + self.headers.push((name.to_string(), value.to_string())); + self + } + + #[inline(always)] + pub fn with_headers(mut self, headers: Vec) -> Self { + self.headers.extend(headers); + self + } + + #[inline(always)] + pub fn with_streaming_strategy(mut self, streaming_strategy: StreamingStrategy) -> Self { + self.streaming_strategy = Some(streaming_strategy); + self + } + + #[inline(always)] + pub fn with_upgrade(mut self) -> Self { + self.upgrade = true; + self + } +} + #[derive(Clone, Debug, CandidType, Deserialize)] pub struct StreamingCallbackToken { pub key: String, From 31f822c78a4b81534220091c377db71fadc2fa71 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Sat, 17 Sep 2022 14:17:02 -0400 Subject: [PATCH 04/24] feat: add basic index templating to pastebin example (canister side rendering!) --- examples/pastebin/Cargo.toml | 2 + examples/pastebin/src/canister.rs | 66 +++++++++++++++++++++++-------- ic-kit-http/src/lib.rs | 4 +- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/examples/pastebin/Cargo.toml b/examples/pastebin/Cargo.toml index 5315d70..6bc9212 100644 --- a/examples/pastebin/Cargo.toml +++ b/examples/pastebin/Cargo.toml @@ -7,6 +7,8 @@ edition = "2021" [dependencies] ic-kit = {path="../../ic-kit", features = ["http"]} +tinytemplate = "1.1" +serde = { version = "1.0", features = ["derive"] } [[bin]] name = "ic_kit_example_pastebin" diff --git a/examples/pastebin/src/canister.rs b/examples/pastebin/src/canister.rs index 1c3a7ee..10155bc 100644 --- a/examples/pastebin/src/canister.rs +++ b/examples/pastebin/src/canister.rs @@ -3,20 +3,39 @@ use std::collections::HashMap; pub type Data = HashMap>; -const INDEX_HTML: &str = r#" +use serde::Serialize; + +use std::error::Error; +use tinytemplate::TinyTemplate; + +#[derive(Serialize)] +struct IndexContext { + manpage: String, +} + +#[derive(Serialize)] +struct ManpageContext { + canister_id: String, +} + +static INDEX_HTML: &'static str = r#" - IC Pastebin + -

IC Pastebin

-
- -
- -
+
+            
+{manpage}
+            
+        
"#; @@ -38,20 +57,35 @@ DESCRIPTION USAGE - curl -T file.txt https://rrkah-fqaaa-aaaaa-aaaaq-cai.raw.ic0.app - curl https://rrkah-fqaaa-aaaaa-aaaaq-cai.raw.ic0.app/file.txt -"#; + curl -T file.txt https://{canister_id}.raw.ic0.app + curl https://{canister_id}.raw.ic0.app/file.txt"#; /// Index handler #[get(route = "/")] fn index_handler(r: HttpRequest, _: Params) -> HttpResponse { + let mut tt = TinyTemplate::new(); + + tt.add_template("manpage", INDEX_MANPAGE).unwrap(); + let manpage = tt + .render( + "manpage", + &ManpageContext { + canister_id: ic::id().to_text(), + }, + ) + .unwrap(); + + // Just return the manpage if client is a terminal (curl or wget) if let Some(ua) = r.header("User-Agent") { - if ua.contains("curl") { - return HttpResponse::ok().with_body(INDEX_MANPAGE.into()); + if ua.starts_with("curl") || ua.starts_with("wget") { + return HttpResponse::ok().with_body(manpage); } } - HttpResponse::ok().with_body(INDEX_HTML.into()) + tt.add_template("html", INDEX_HTML).unwrap(); + let html = tt.render("html", &IndexContext { manpage }).unwrap(); + + HttpResponse::ok().with_body(html) } /// Get paste handler @@ -60,7 +94,7 @@ fn get_file(_: HttpRequest, p: Params) -> HttpResponse { let file = p.get("file").unwrap(); ic::with(|data: &Data| match data.get(file) { Some(content) => HttpResponse::ok().with_body(content.clone()), - None => HttpResponse::new(404).with_body(format!("404: file not found `{}`", file).into()), + None => HttpResponse::new(404).with_body(format!("file not found `{}`", file)), }) } @@ -74,7 +108,7 @@ fn put_file(req: HttpRequest, p: Params) -> HttpResponse { d.insert(filename.to_string(), req.body); }); - HttpResponse::ok().with_body(res.into()) + HttpResponse::ok().with_body(res) } #[derive(KitCanister)] diff --git a/ic-kit-http/src/lib.rs b/ic-kit-http/src/lib.rs index a2c362c..f62bbf8 100644 --- a/ic-kit-http/src/lib.rs +++ b/ic-kit-http/src/lib.rs @@ -53,8 +53,8 @@ impl HttpResponse { } #[inline(always)] - pub fn with_body(mut self, body: Vec) -> Self { - self.body = body; + pub fn with_body>>(mut self, body: T) -> Self { + self.body = body.into(); self } From 0ca7480f2cde800f53673eeb5dae7f68bb0a97c8 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Sat, 17 Sep 2022 14:31:56 -0400 Subject: [PATCH 05/24] docs: pastebin readme --- examples/pastebin/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 examples/pastebin/README.md diff --git a/examples/pastebin/README.md b/examples/pastebin/README.md new file mode 100644 index 0000000..121f7b3 --- /dev/null +++ b/examples/pastebin/README.md @@ -0,0 +1,23 @@ +# IC Pastebin + +This directory contains an example HTTP canister implemented using the IC Kit. It is a simple HTTP pastebin that allows you to store and retrieve text. + +## How to use + +1. Build and deploy the canister: + ```bash + dfx deploy pastebin + ``` +2. View the canister's HTML UI at `http://rrkah-fqaaa-aaaaa-aaaaq-cai.localhost:8000/` +3. View the canister's manpage: + ```bash + curl rrkah-fqaaa-aaaaa-aaaaq-cai.localhost:8000 + ``` +5. upload some text: + ```bash + curl -T file.txt rrkah-fqaaa-aaaaa-aaaaq-cai.localhost:8000 + ``` +5. download some text: + ```bash + curl rrkah-fqaaa-aaaaa-aaaaq-cai.localhost:8000/file.txt + ``` \ No newline at end of file From 66010ec37b3d095c374c95bd579897ecdecf4f99 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Sat, 17 Sep 2022 14:35:11 -0400 Subject: [PATCH 06/24] docs: add more to examples readme --- examples/README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index 964300b..13bb8eb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,4 +5,13 @@ to be as simple as possible to demonstrate one aspect of the IC Kit and a possib you can also use to develop canisters. Simple State Manipulation: -- Counter \ No newline at end of file +- Counter + +Inter Canister Communication: +- Multi Counter + +Child Canister Creation: +- Factory Counter + +Inbound HTTP Server: +- Pastebin \ No newline at end of file From ddab4426ca831acc6bc03572782dbfeb181c86ad Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Sun, 2 Oct 2022 09:12:20 -0400 Subject: [PATCH 07/24] chore: update dependencies to latest candid 0.8 and ic-types 0.6 --- ic-kit-certified/Cargo.toml | 8 ++++---- ic-kit-http/Cargo.toml | 4 ++-- ic-kit-management/Cargo.toml | 6 +++--- ic-kit-runtime/Cargo.toml | 10 ++++----- ic-kit-runtime/src/call.rs | 11 +++++----- ic-kit-runtime/src/canister.rs | 37 ++++++++++++++++++---------------- ic-kit-runtime/src/handle.rs | 8 +++++--- ic-kit-runtime/src/replica.rs | 2 +- ic-kit-runtime/src/types.rs | 8 ++++---- ic-kit-runtime/src/users.rs | 2 +- ic-kit-stable/Cargo.toml | 2 +- ic-kit/Cargo.toml | 14 ++++++------- 12 files changed, 59 insertions(+), 53 deletions(-) diff --git a/ic-kit-certified/Cargo.toml b/ic-kit-certified/Cargo.toml index 1fc194c..adfe8d2 100644 --- a/ic-kit-certified/Cargo.toml +++ b/ic-kit-certified/Cargo.toml @@ -14,11 +14,11 @@ keywords = ["internet-computer", "canister", "fleek", "psychedelic"] include = ["src", "Cargo.toml", "README.md"] [dependencies] -ic-kit-stable = {path="../ic-kit-stable"} -ic-types = "0.4.1" -candid = "0.7" +ic-kit-stable = { path = "../ic-kit-stable" } +ic-types = "0.6" +candid = "0.8" sha2 = "0.10.2" -serde = { version="1.0.116", features = ["derive"] } +serde = { version = "1.0.116", features = ["derive"] } serde_bytes = "0.11.5" serde_cbor = "0.11.2" hex = "0.4.3" diff --git a/ic-kit-http/Cargo.toml b/ic-kit-http/Cargo.toml index fe64e9d..576365c 100644 --- a/ic-kit-http/Cargo.toml +++ b/ic-kit-http/Cargo.toml @@ -17,7 +17,7 @@ include = ["src", "Cargo.toml", "README.md"] [dependencies] ic-kit-macros = { path = "../ic-kit-macros", features = ["http"] } -candid = "0.7" +candid = "0.8" serde = "1.0" matchit = "0.6.0" -lazy_static = "1.4" \ No newline at end of file +lazy_static = "1.4" diff --git a/ic-kit-management/Cargo.toml b/ic-kit-management/Cargo.toml index a149c96..e7b8920 100644 --- a/ic-kit-management/Cargo.toml +++ b/ic-kit-management/Cargo.toml @@ -6,6 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ic-kit = {path="../ic-kit", version="0.5.0-alpha.4"} -candid="0.7" -serde="1.0" +ic-kit = { path = "../ic-kit", version = "0.5.0-alpha.5" } +candid = "0.8" +serde = "1.0" diff --git a/ic-kit-runtime/Cargo.toml b/ic-kit-runtime/Cargo.toml index 2abdf2c..edc97cc 100644 --- a/ic-kit-runtime/Cargo.toml +++ b/ic-kit-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ic-kit-runtime" -version = "0.1.0-alpha.1" +version = "0.1.0-alpha.2" edition = "2021" authors = ["Parsa Ghadimi ", "Ossian Mapes "] description = "IC-Kit's Canister Simulator" @@ -19,13 +19,13 @@ include = ["src", "Cargo.toml", "README.md"] cfg-if = "1.0.0" [target.'cfg(not(target_family = "wasm"))'.dependencies] -ic-kit-sys = {path="../ic-kit-sys", version="0.1.3"} -ic-types = "0.4.1" -tokio = {version="1.20", features=["sync", "macros", "rt"]} +ic-kit-sys = { path = "../ic-kit-sys", version = "0.1.3" } +ic-types = "0.6.0" +tokio = { version = "1.20", features = ["sync", "macros", "rt"] } thread-local-panic-hook = "0.1.0" lazy_static = "1.4" memmap = "0.7.0" futures = "0.3" actix = "0.13" -candid = "0.7" +candid = "0.8" serde = "1.0" diff --git a/ic-kit-runtime/src/call.rs b/ic-kit-runtime/src/call.rs index c691f3a..0415f6b 100644 --- a/ic-kit-runtime/src/call.rs +++ b/ic-kit-runtime/src/call.rs @@ -1,11 +1,12 @@ -use crate::types::*; -use crate::Replica; use candid::utils::{ArgumentDecoder, ArgumentEncoder}; -use candid::{decode_args, decode_one, encode_args, encode_one, CandidType}; -use ic_kit_sys::types::{CallError, RejectionCode, CANDID_EMPTY_ARG}; -use ic_types::Principal; +use candid::{decode_args, decode_one, encode_args, encode_one, CandidType, Principal}; use serde::de::DeserializeOwned; +use ic_kit_sys::types::{CallError, RejectionCode, CANDID_EMPTY_ARG}; + +use crate::types::*; +use crate::Replica; + /// A CallBuilder for a replica. #[derive(Clone)] pub struct CallBuilder<'a> { diff --git a/ic-kit-runtime/src/canister.rs b/ic-kit-runtime/src/canister.rs index 20bb8b3..a17528b 100644 --- a/ic-kit-runtime/src/canister.rs +++ b/ic-kit-runtime/src/canister.rs @@ -1,21 +1,24 @@ -use crate::call::CallReply; -use crate::stable::{HeapStableMemory, StableMemoryBackend}; -use crate::types::*; -use futures::executor::block_on; -use ic_kit_sys::ic0; -use ic_kit_sys::ic0::runtime; -use ic_kit_sys::ic0::runtime::Ic0CallHandlerProxy; -use ic_kit_sys::types::RejectionCode; -use ic_types::Principal; use std::any::Any; use std::collections::{HashMap, HashSet}; use std::panic::catch_unwind; use std::thread::JoinHandle; + +use candid::Principal; +use futures::executor::block_on; use thread_local_panic_hook::set_hook; use tokio::select; use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::sync::oneshot; +use ic_kit_sys::ic0; +use ic_kit_sys::ic0::runtime; +use ic_kit_sys::ic0::runtime::Ic0CallHandlerProxy; +use ic_kit_sys::types::RejectionCode; + +use crate::call::CallReply; +use crate::stable::{HeapStableMemory, StableMemoryBackend}; +use crate::types::*; + const MAX_CYCLES_PER_RESPONSE: u128 = 12; /// A canister that is being executed. @@ -585,7 +588,7 @@ impl Ic0CallHandlerProxy for Canister { return Err(format!( "msg_reply_data_append can not be called from '{}'", self.env.get_entry_point_name() - )) + )); } }; @@ -616,7 +619,7 @@ impl Ic0CallHandlerProxy for Canister { return Err(format!( "msg_reply can not be called from '{}'", self.env.get_entry_point_name() - )) + )); } }; @@ -654,7 +657,7 @@ impl Ic0CallHandlerProxy for Canister { return Err(format!( "msg_reject can not be called from '{}'", self.env.get_entry_point_name() - )) + )); } }; @@ -755,7 +758,7 @@ impl Ic0CallHandlerProxy for Canister { return Err(format!( "msg_cycles_accept can not be called from '{}'", self.env.get_entry_point_name() - )) + )); } }; @@ -785,7 +788,7 @@ impl Ic0CallHandlerProxy for Canister { return Err(format!( "msg_cycles_accept128 can not be called from '{}'", self.env.get_entry_point_name() - )) + )); } }; @@ -846,7 +849,7 @@ impl Ic0CallHandlerProxy for Canister { return Err(format!( "msg_method_name_size can not be called from '{}'", self.env.get_entry_point_name() - )) + )); } }; @@ -870,7 +873,7 @@ impl Ic0CallHandlerProxy for Canister { return Err(format!( "msg_method_name_copy can not be called from '{}'", self.env.get_entry_point_name() - )) + )); } }; @@ -904,7 +907,7 @@ impl Ic0CallHandlerProxy for Canister { return Err(format!( "call_new can not be called from '{}'", self.env.get_entry_point_name() - )) + )); } } diff --git a/ic-kit-runtime/src/handle.rs b/ic-kit-runtime/src/handle.rs index 08b75df..80c731c 100644 --- a/ic-kit-runtime/src/handle.rs +++ b/ic-kit-runtime/src/handle.rs @@ -1,9 +1,11 @@ +use std::panic::{RefUnwindSafe, UnwindSafe}; + +use candid::Principal; +use tokio::sync::oneshot; + use crate::call::{CallBuilder, CallReply}; use crate::types::{Env, Message, RequestId}; use crate::Replica; -use ic_types::Principal; -use std::panic::{RefUnwindSafe, UnwindSafe}; -use tokio::sync::oneshot; pub struct CanisterHandle<'a> { pub(crate) replica: &'a Replica, diff --git a/ic-kit-runtime/src/replica.rs b/ic-kit-runtime/src/replica.rs index 7f599d7..d51601b 100644 --- a/ic-kit-runtime/src/replica.rs +++ b/ic-kit-runtime/src/replica.rs @@ -18,7 +18,7 @@ use crate::canister::Canister; use crate::handle::CanisterHandle; use crate::types::*; use ic_kit_sys::types::RejectionCode; -use ic_types::Principal; +use candid::Principal; use std::collections::HashMap; use std::future::Future; use tokio::sync::{mpsc, oneshot}; diff --git a/ic-kit-runtime/src/types.rs b/ic-kit-runtime/src/types.rs index c41278f..0804deb 100644 --- a/ic-kit-runtime/src/types.rs +++ b/ic-kit-runtime/src/types.rs @@ -1,11 +1,11 @@ -use candid::utils::ArgumentEncoder; -use candid::{encode_args, encode_one, CandidType}; -use ic_kit_sys::types::{RejectionCode, CANDID_EMPTY_ARG}; -use ic_types::Principal; use std::panic::{RefUnwindSafe, UnwindSafe}; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; +use candid::{encode_args, encode_one, utils::ArgumentEncoder, CandidType, Principal}; + +use ic_kit_sys::types::{RejectionCode, CANDID_EMPTY_ARG}; + static REQUEST_ID: AtomicU64 = AtomicU64::new(0); /// A request ID for a request that is coming to this canister from the outside. diff --git a/ic-kit-runtime/src/users.rs b/ic-kit-runtime/src/users.rs index f3a43c7..ab1025e 100644 --- a/ic-kit-runtime/src/users.rs +++ b/ic-kit-runtime/src/users.rs @@ -1,6 +1,6 @@ //! A set of mock principal ids. -use ic_types::Principal; +use candid::Principal; use lazy_static::lazy_static; lazy_static! { diff --git a/ic-kit-stable/Cargo.toml b/ic-kit-stable/Cargo.toml index 58cb34e..10adbdd 100644 --- a/ic-kit-stable/Cargo.toml +++ b/ic-kit-stable/Cargo.toml @@ -16,7 +16,7 @@ include = ["src", "Cargo.toml", "README.md"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ic-kit = {path="../ic-kit", version="0.5.0-alpha.3"} +ic-kit = { path = "../ic-kit", version = "0.5.0-alpha.5" } [features] experimental-stable64 = ["ic-kit/experimental-stable64"] diff --git a/ic-kit/Cargo.toml b/ic-kit/Cargo.toml index 9ddbf92..5441093 100644 --- a/ic-kit/Cargo.toml +++ b/ic-kit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ic-kit" -version = "0.5.0-alpha.4" +version = "0.5.0-alpha.5" description = "Testable Canister Developer Kit for the Internet Computer." authors = ["Parsa Ghadimi ", "Ossian Mapes "] edition = "2018" @@ -14,17 +14,17 @@ keywords = ["internet-computer", "canister", "fleek", "psychedelic"] include = ["src", "Cargo.toml", "README.md"] [dependencies] -ic-kit-sys = {path="../ic-kit-sys", version="0.1.3"} -ic-kit-macros = {path="../ic-kit-macros", version="0.1.1-alpha.0"} -ic-kit-http = {path="../ic-kit-http", version="0.1.0-alpha.0", optional=true} -candid="0.7" +ic-kit-sys = { path = "../ic-kit-sys", version = "0.1.3" } +ic-kit-macros = { path = "../ic-kit-macros", version = "0.1.1-alpha.0" } +ic-kit-http = { path = "../ic-kit-http", version = "0.1.0-alpha.0", optional = true } +candid = "0.8" serde = "1.0" [target.'cfg(not(target_family = "wasm"))'.dependencies] -ic-kit-runtime = {path="../ic-kit-runtime", version="0.1.0-alpha.1"} +ic-kit-runtime = { path = "../ic-kit-runtime", version = "0.1.0-alpha.2" } [features] default = [] experimental-stable64 = [] experimental-cycles128 = [] -http = ["dep:ic-kit-http", "ic-kit-macros/http"] \ No newline at end of file +http = ["dep:ic-kit-http", "ic-kit-macros/http"] From c9951a6f451039ad3259f62ba23067eab57a2312 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Sun, 2 Oct 2022 09:12:51 -0400 Subject: [PATCH 08/24] chore: minor tweaks on http stuff --- ic-kit-macros/src/http.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ic-kit-macros/src/http.rs b/ic-kit-macros/src/http.rs index 2d3d895..98bd52f 100644 --- a/ic-kit-macros/src/http.rs +++ b/ic-kit-macros/src/http.rs @@ -1,12 +1,13 @@ +use std::sync::Mutex; + use lazy_static::lazy_static; use proc_macro2::TokenStream; use quote::quote; use serde::Deserialize; use serde_tokenstream::from_tokenstream; -use std::sync::Mutex; -use syn::{spanned::Spanned, Error}; +use syn::{Error, spanned::Spanned}; -struct Method { +struct Handler { name: String, route: String, method: String, @@ -14,7 +15,7 @@ struct Method { } lazy_static! { - static ref GETS: Mutex> = Mutex::new(Vec::new()); + static ref HANDLERS: Mutex> = Mutex::new(Vec::new()); } #[derive(Deserialize)] @@ -37,7 +38,7 @@ pub fn gen_handler_code( ) })?; - GETS.lock().unwrap().push(Method { + HANDLERS.lock().unwrap().push(Handler { name: fun.sig.ident.to_string(), route: attrs.route, method: method.into(), @@ -50,7 +51,7 @@ pub fn gen_handler_code( } pub fn gen_http_request_code() -> TokenStream { - let routes = GETS.lock().unwrap(); + let routes = HANDLERS.lock().unwrap(); let mut routes_insert = TokenStream::new(); let mut upgradable = false; @@ -70,10 +71,10 @@ pub fn gen_http_request_code() -> TokenStream { } } - router.insert("GET", "/", (index, false)); + router.insert("GET", "/*p", (index, false)); }; } else { - for Method { + for Handler { method, name, route, From c3c8ce67c2d4b2e055319bf4e571aa2d7239c167 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Sun, 2 Oct 2022 09:13:25 -0400 Subject: [PATCH 09/24] feat: dynamic url on pastebin example --- .gitignore | 12 ++----- examples/pastebin/src/canister.rs | 53 ++++++++++++++++--------------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index bac5453..9e834ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,5 @@ -/target - - -# Added by cargo -# -# already existing elements were commented out - -#/target +target/ Cargo.lock .dfx -*.old* \ No newline at end of file +*.old* +.idea/ \ No newline at end of file diff --git a/examples/pastebin/src/canister.rs b/examples/pastebin/src/canister.rs index 10155bc..343c201 100644 --- a/examples/pastebin/src/canister.rs +++ b/examples/pastebin/src/canister.rs @@ -1,24 +1,23 @@ -use ic_kit::prelude::*; use std::collections::HashMap; -pub type Data = HashMap>; - use serde::Serialize; - -use std::error::Error; use tinytemplate::TinyTemplate; +use ic_kit::prelude::*; + +pub type Data = HashMap>; + #[derive(Serialize)] -struct IndexContext { +struct HtmlContext { manpage: String, } #[derive(Serialize)] struct ManpageContext { - canister_id: String, + url: String, } -static INDEX_HTML: &'static str = r#" +static HTML_TEMPLATE: &str = r#" @@ -40,7 +39,7 @@ static INDEX_HTML: &'static str = r#" "#; -const INDEX_MANPAGE: &str = r#" +static MANPAGE_TEMPLATE: &str = r#" IC PASTEBIN(1) IC PASTEBIN IC PASTEBIN(1) NAME @@ -57,23 +56,23 @@ DESCRIPTION USAGE - curl -T file.txt https://{canister_id}.raw.ic0.app - curl https://{canister_id}.raw.ic0.app/file.txt"#; + curl -T file.txt {url} + curl {url}/file.txt +"#; /// Index handler #[get(route = "/")] fn index_handler(r: HttpRequest, _: Params) -> HttpResponse { + ic::print(format!("{:?}", r)); + let url = match r.header("host") { + Some(host) => format!("http://{}", host), + None => format!("https://{}.raw.ic0.app", id()), + }; + let mut tt = TinyTemplate::new(); - tt.add_template("manpage", INDEX_MANPAGE).unwrap(); - let manpage = tt - .render( - "manpage", - &ManpageContext { - canister_id: ic::id().to_text(), - }, - ) - .unwrap(); + tt.add_template("manpage", MANPAGE_TEMPLATE).unwrap(); + let manpage = tt.render("manpage", &ManpageContext { url }).unwrap(); // Just return the manpage if client is a terminal (curl or wget) if let Some(ua) = r.header("User-Agent") { @@ -82,8 +81,8 @@ fn index_handler(r: HttpRequest, _: Params) -> HttpResponse { } } - tt.add_template("html", INDEX_HTML).unwrap(); - let html = tt.render("html", &IndexContext { manpage }).unwrap(); + tt.add_template("html", HTML_TEMPLATE).unwrap(); + let html = tt.render("html", &HtmlContext { manpage }).unwrap(); HttpResponse::ok().with_body(html) } @@ -92,9 +91,9 @@ fn index_handler(r: HttpRequest, _: Params) -> HttpResponse { #[get(route = "/:file")] fn get_file(_: HttpRequest, p: Params) -> HttpResponse { let file = p.get("file").unwrap(); - ic::with(|data: &Data| match data.get(file) { + with(|data: &Data| match data.get(file) { Some(content) => HttpResponse::ok().with_body(content.clone()), - None => HttpResponse::new(404).with_body(format!("file not found `{}`", file)), + None => HttpResponse::new(404).with_body(format!("file not found `{}`\n", file)), }) } @@ -102,9 +101,11 @@ fn get_file(_: HttpRequest, p: Params) -> HttpResponse { #[put(route = "/:file", upgrade = true)] fn put_file(req: HttpRequest, p: Params) -> HttpResponse { let filename = p.get("file").unwrap(); - let res = format!("recieved file: {} ({} bytes)", filename, req.body.len(),); + let url = req.header("host").unwrap_or("unknown"); + + let res = format!("{}.{}/{}", id().to_text(), "localhost:8000", filename); - ic::with_mut(|d: &mut Data| { + with_mut(|d: &mut Data| { d.insert(filename.to_string(), req.body); }); From fbb012d396411b7e8d7194d855c22c4f32be2d02 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Wed, 12 Oct 2022 12:03:51 -0400 Subject: [PATCH 10/24] fix: duplicate imports --- ic-kit-macros/src/lib.rs | 2 -- ic-kit-runtime/src/replica.rs | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/ic-kit-macros/src/lib.rs b/ic-kit-macros/src/lib.rs index caaac72..520fb73 100644 --- a/ic-kit-macros/src/lib.rs +++ b/ic-kit-macros/src/lib.rs @@ -1,5 +1,3 @@ - -use entry::{gen_entry_point_code, EntryPoint}; use proc_macro::TokenStream; use syn::parse_macro_input; diff --git a/ic-kit-runtime/src/replica.rs b/ic-kit-runtime/src/replica.rs index 4a616fa..3d756d7 100644 --- a/ic-kit-runtime/src/replica.rs +++ b/ic-kit-runtime/src/replica.rs @@ -13,15 +13,10 @@ //! This also allows the canister event loops to have accesses to the replica without any borrows by //! just sending their request to the same channel, causing the replica to process the messages. -use crate::call::{CallBuilder, CallReply}; -use crate::canister::Canister; -use crate::handle::CanisterHandle; -use crate::types::*; -use ic_kit_sys::types::RejectionCode; -use candid::Principal; use std::collections::HashMap; use std::future::Future; +use candid::Principal; use tokio::sync::{mpsc, oneshot}; use ic_kit_sys::types::RejectionCode; From 9375d7663b03807d0f1be2141ff650cde3af7d40 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Wed, 12 Oct 2022 20:01:00 -0400 Subject: [PATCH 11/24] chore: move dep injection helpers to their own module --- ic-kit-macros/src/di.rs | 128 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 ic-kit-macros/src/di.rs diff --git a/ic-kit-macros/src/di.rs b/ic-kit-macros/src/di.rs new file mode 100644 index 0000000..4cca58b --- /dev/null +++ b/ic-kit-macros/src/di.rs @@ -0,0 +1,128 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::{spanned::Spanned, Error}; + +#[derive(Default)] +pub struct ProcessedArgs { + pub(crate) args: Vec, + pub(crate) mut_args: Vec<(Ident, syn::Type)>, + pub(crate) imu_args: Vec<(Ident, syn::Type)>, + pub(crate) can_args: Vec<(Ident, syn::Type)>, + injected: Vec, +} + +pub fn di(args: Vec<(Ident, syn::Type)>, is_async: bool) -> Result { + let mut result = ProcessedArgs::default(); + + for (ident, ty) in args { + result.args.push(ident.clone()); + + match ty { + syn::Type::Reference(ty_ref) if is_async => { + return Err(Error::new( + ty_ref.span(), + "IC-Kit's dependency injection can only work on sync methods.".to_string(), + )); + } + syn::Type::Reference(ty_ref) if !result.can_args.is_empty() => { + return Err(Error::new( + ty_ref.span(), + "An IC-kit dependency injected reference could only come before canister arguments.".to_string(), + )); + } + syn::Type::Reference(ty_ref) if result.injected.contains(&ty_ref.elem) => { + return Err(Error::new( + ty_ref.span(), + "IC-Kit's dependency injection can only inject one instance of each type." + .to_string(), + )); + } + syn::Type::Reference(ty_ref) if ty_ref.mutability.is_some() => { + result.mut_args.push((ident, *ty_ref.elem.clone())); + result.injected.push(*ty_ref.elem); + } + syn::Type::Reference(ty_ref) => { + result.imu_args.push((ident, *ty_ref.elem.clone())); + result.injected.push(*ty_ref.elem); + } + ty => { + result.can_args.push((ident, ty)); + } + } + } + + Ok(result) +} + +pub fn collect_args( + entry_point: &str, + signature: &syn::Signature, +) -> Result, Error> { + let mut args = Vec::new(); + + for (id, arg) in signature.inputs.iter().enumerate() { + let (ident, ty) = match arg { + syn::FnArg::Receiver(r) => { + return Err(Error::new( + r.span(), + format!( + "#[{}] macro can not be used on a function with `self` as a parameter.", + entry_point + ), + )); + } + syn::FnArg::Typed(syn::PatType { pat, ty, .. }) => { + if let syn::Pat::Ident(syn::PatIdent { ident, .. }) = pat.as_ref() { + (ident.clone(), *ty.clone()) + } else { + ( + Ident::new(&format!("_di_arg_{}", id), pat.span()), + *ty.clone(), + ) + } + } + }; + + args.push((ident, ty)); + } + + Ok(args) +} + +pub fn wrap(inner: TokenStream, args: ProcessedArgs) -> TokenStream { + let mut result = inner; + let (imu_args, imu_types): (Vec<_>, Vec<_>) = args.imu_args.into_iter().unzip(); + let (mut_args, mut_types): (Vec<_>, Vec<_>) = args.mut_args.into_iter().unzip(); + + result = match imu_args.len() { + 0 => result, + 1 => { + quote! { + ic_kit::ic::with(|#(#imu_args: &#imu_types),*| { + #result + }) + } + } + _ => quote! { + ic_kit::ic::with_many(|(#(#imu_args),*) : (#(&#imu_types),*)| { + #result + }) + }, + }; + + result = match mut_args.len() { + 0 => result, + 1 => quote! { + ic_kit::ic::with_mut(|#(#mut_args: &mut #mut_types),*| { + #result + }) + }, + _ => quote! { + ic_kit::ic::with_many_mut(|(#(#mut_args),*) : (#(&mut #mut_types),*)| { + #result + }) + }, + }; + + result +} From 3c75abd1e202b3fa9f6cdae16afe2285e4378e30 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Wed, 12 Oct 2022 20:02:33 -0400 Subject: [PATCH 12/24] feat: http method dependency injection refactor: dynamic Router type; only include the method types used chore: update entry to new di module --- ic-kit-macros/src/entry.rs | 99 +++-------------------------- ic-kit-macros/src/export_service.rs | 1 + ic-kit-macros/src/http.rs | 85 +++++++++++++++---------- ic-kit-macros/src/lib.rs | 1 + 4 files changed, 65 insertions(+), 121 deletions(-) diff --git a/ic-kit-macros/src/entry.rs b/ic-kit-macros/src/entry.rs index 93921cc..7256f1e 100644 --- a/ic-kit-macros/src/entry.rs +++ b/ic-kit-macros/src/entry.rs @@ -2,14 +2,17 @@ //! //! [1]: -use crate::export_service::declare; +use std::fmt::Formatter; + use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use serde::Deserialize; use serde_tokenstream::from_tokenstream; -use std::fmt::Formatter; use syn::{spanned::Spanned, Error}; +use crate::di::{collect_args, di}; +use crate::export_service::declare; + #[derive(Copy, Clone, PartialOrd, PartialEq, Ord, Eq)] pub enum EntryPoint { Init, @@ -170,7 +173,10 @@ pub fn gen_entry_point_code( }; // Build the outer function's body. - let tmp = di(collect_args(entry_point, signature)?, is_async)?; + let tmp = di( + collect_args(entry_point.to_string().as_str(), signature)?, + is_async, + )?; let args = tmp.args; let (can_args, can_types): (Vec<_>, Vec<_>) = tmp.can_args.into_iter().unzip(); let (imu_args, imu_types): (Vec<_>, Vec<_>) = tmp.imu_args.into_iter().unzip(); @@ -178,7 +184,7 @@ pub fn gen_entry_point_code( // If the method does not accept any arguments, don't even read the msg_data, and if the // deserialization fails, just reject the message, which is cheaper than trap. - let arg_decode = if can_args.len() == 0 { + let arg_decode = if can_args.is_empty() { quote! {} } else { quote! { @@ -324,88 +330,3 @@ pub fn gen_entry_point_code( #item }) } - -#[derive(Default)] -struct ProcessedArgs { - args: Vec, - mut_args: Vec<(Ident, syn::Type)>, - imu_args: Vec<(Ident, syn::Type)>, - can_args: Vec<(Ident, syn::Type)>, - injected: Vec, -} - -fn di(args: Vec<(Ident, syn::Type)>, is_async: bool) -> Result { - let mut result = ProcessedArgs::default(); - - for (ident, ty) in args { - result.args.push(ident.clone()); - - match ty { - syn::Type::Reference(ty_ref) if is_async => { - return Err(Error::new( - ty_ref.span(), - format!("IC-Kit's dependency injection can only work on sync methods."), - )); - } - syn::Type::Reference(ty_ref) if !result.can_args.is_empty() => { - return Err(Error::new( - ty_ref.span(), - format!("An IC-kit dependency injected reference could only come before canister arguments."), - )); - } - syn::Type::Reference(ty_ref) if result.injected.contains(&ty_ref.elem) => { - return Err(Error::new( - ty_ref.span(), - format!( - "IC-Kit's dependency injection can only inject one instance of each type." - ), - )); - } - syn::Type::Reference(ty_ref) if ty_ref.mutability.is_some() => { - result.mut_args.push((ident, *ty_ref.elem.clone())); - result.injected.push(*ty_ref.elem); - } - syn::Type::Reference(ty_ref) => { - result.imu_args.push((ident, *ty_ref.elem.clone())); - result.injected.push(*ty_ref.elem); - } - ty => { - result.can_args.push((ident, ty)); - } - } - } - - Ok(result) -} - -fn collect_args( - entry_point: EntryPoint, - signature: &syn::Signature, -) -> Result, Error> { - let mut args = Vec::new(); - - for (id, arg) in signature.inputs.iter().enumerate() { - let (ident, ty) = match arg { - syn::FnArg::Receiver(r) => { - return Err(Error::new( - r.span(), - format!( - "#[{}] macro can not be used on a function with `self` as a parameter.", - entry_point - ), - )) - } - syn::FnArg::Typed(syn::PatType { pat, ty, .. }) => { - if let syn::Pat::Ident(syn::PatIdent { ident, .. }) = pat.as_ref() { - (ident.clone(), *ty.clone()) - } else { - (Ident::new(&format!("arg_{}", id), pat.span()), *ty.clone()) - } - } - }; - - args.push((ident, ty)); - } - - Ok(args) -} diff --git a/ic-kit-macros/src/export_service.rs b/ic-kit-macros/src/export_service.rs index db55fe0..2ce1776 100644 --- a/ic-kit-macros/src/export_service.rs +++ b/ic-kit-macros/src/export_service.rs @@ -21,6 +21,7 @@ struct Method { lazy_static! { static ref METHODS: Mutex> = Mutex::new(Default::default()); static ref LIFE_CYCLES: Mutex> = Mutex::new(Default::default()); + } pub(crate) fn declare( diff --git a/ic-kit-macros/src/http.rs b/ic-kit-macros/src/http.rs index 98bd52f..13d7e7a 100644 --- a/ic-kit-macros/src/http.rs +++ b/ic-kit-macros/src/http.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::sync::Mutex; use lazy_static::lazy_static; @@ -5,7 +6,9 @@ use proc_macro2::TokenStream; use quote::quote; use serde::Deserialize; use serde_tokenstream::from_tokenstream; -use syn::{Error, spanned::Spanned}; +use syn::{spanned::Spanned, Error}; + +use crate::di::{collect_args, di}; struct Handler { name: String, @@ -31,22 +34,42 @@ pub fn gen_handler_code( item: TokenStream, ) -> Result { let attrs = from_tokenstream::(&attr)?; - let fun: syn::ItemFn = syn::parse2::(item.clone()).map_err(|e| { + let fun = syn::parse2::(item.clone()).map_err(|e| { Error::new( item.span(), format!("#[{0}] must be above a function. \n{1}", method, e), ) })?; + let sig = fun.sig; + let output = sig.output.clone(); + let ident = sig.ident.clone(); + let name = sig.ident.to_string(); + let is_async = sig.asyncness.is_some(); + let stmts = fun.block.stmts; HANDLERS.lock().unwrap().push(Handler { - name: fun.sig.ident.to_string(), + name, route: attrs.route, method: method.into(), upgrade: attrs.upgrade.unwrap_or(false), }); + // Build the outer function's body. + let args = di(collect_args(method, &sig)?, is_async)?; + let (can_args, can_types): (Vec<_>, Vec<_>) = args.can_args.clone().into_iter().unzip(); + + // Because DI doesn't work on an async method. + let mut inner = TokenStream::new(); + for stmt in stmts { + inner.extend(quote!(#stmt)); + } + + let result = crate::di::wrap(inner, args); + Ok(quote! { - #item + fn #ident(#(#can_args: #can_types),*) #output { + #result + } }) } @@ -55,6 +78,7 @@ pub fn gen_http_request_code() -> TokenStream { let mut routes_insert = TokenStream::new(); let mut upgradable = false; + let mut router_types: HashSet<&str> = HashSet::new(); if routes.is_empty() { // if no routes, provide a basic index displaying canister stats @@ -73,6 +97,8 @@ pub fn gen_http_request_code() -> TokenStream { router.insert("GET", "/*p", (index, false)); }; + + router_types.insert("get"); } else { for Handler { method, @@ -90,6 +116,8 @@ pub fn gen_http_request_code() -> TokenStream { routes_insert.extend(quote! { router.insert(#method, #route, (#name, #upgrade)); }); + + router_types.insert(method.as_str()); } } @@ -154,30 +182,35 @@ pub fn gen_http_request_code() -> TokenStream { }); }; + let mut router_fields = TokenStream::new(); + let mut router_default = TokenStream::new(); + let mut router_insert = TokenStream::new(); + let mut router_ats = TokenStream::new(); + + for method in router_types { + let method = method; + let ident = syn::Ident::new(&method.to_lowercase(), proc_macro2::Span::call_site()); + router_fields.extend(quote!(#ident: ic_kit::http::BasicRouter,)); + + router_default.extend(quote!(#ident: ic_kit::http::BasicRouter::new(),)); + + router_insert.extend(quote!(#method => self.#ident.insert(path, handler).unwrap(),)); + + router_ats.extend(quote!(#method => self.#ident.at(path),)); + } + quote! { pub type HandlerFn = (fn(ic_kit::http::HttpRequest, ic_kit::http::Params) -> ic_kit::http::HttpResponse, bool); #[derive(Clone)] pub struct Router { - get: ic_kit::http::BasicRouter, - post: ic_kit::http::BasicRouter, - put: ic_kit::http::BasicRouter, - delete: ic_kit::http::BasicRouter, - patch: ic_kit::http::BasicRouter, - head: ic_kit::http::BasicRouter, - options: ic_kit::http::BasicRouter, + #router_fields } impl Default for Router { fn default() -> Self { let mut router = Self { - get: ic_kit::http::BasicRouter::new(), - post: ic_kit::http::BasicRouter::new(), - put: ic_kit::http::BasicRouter::new(), - delete: ic_kit::http::BasicRouter::new(), - patch: ic_kit::http::BasicRouter::new(), - head: ic_kit::http::BasicRouter::new(), - options: ic_kit::http::BasicRouter::new(), + #router_default }; #routes_insert router @@ -187,13 +220,7 @@ pub fn gen_http_request_code() -> TokenStream { impl Router { pub fn insert(&mut self, method: &str, path: &str, handler: HandlerFn) { match method { - "GET" => self.get.insert(path, handler).unwrap(), - "POST" => self.post.insert(path, handler).unwrap(), - "PUT" => self.put.insert(path, handler).unwrap(), - "DELETE" => self.delete.insert(path, handler).unwrap(), - "PATCH" => self.patch.insert(path, handler).unwrap(), - "HEAD" => self.head.insert(path, handler).unwrap(), - "OPTIONS" => self.options.insert(path, handler).unwrap(), + #router_insert _ => panic!("unsupported method: {}", method), }; } @@ -204,13 +231,7 @@ pub fn gen_http_request_code() -> TokenStream { path: &'p str, ) -> Result, MatchError> { match method { - "GET" => self.get.at(path), - "POST" => self.post.at(path), - "PUT" => self.put.at(path), - "DELETE" => self.delete.at(path), - "PATCH" => self.patch.at(path), - "HEAD" => self.head.at(path), - "OPTIONS" => self.options.at(path), + #router_ats _ => Err(MatchError::NotFound), } } diff --git a/ic-kit-macros/src/lib.rs b/ic-kit-macros/src/lib.rs index 520fb73..ccc5ae1 100644 --- a/ic-kit-macros/src/lib.rs +++ b/ic-kit-macros/src/lib.rs @@ -11,6 +11,7 @@ mod export_service; #[cfg(feature = "http")] mod http; +mod di; mod metadata; mod test; From 124c135152f9653e59bd7a98f40a9169d8eda98f Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Wed, 12 Oct 2022 20:07:09 -0400 Subject: [PATCH 13/24] feat: use dependency injection in the http pastebin example! --- examples/counter/Cargo.toml | 2 +- examples/pastebin/src/canister.rs | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml index 2f2cd06..4e2de7c 100644 --- a/examples/counter/Cargo.toml +++ b/examples/counter/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] # With the `http` feature enabled, and no configured routes, the canister will provide status info at the index path. -ic-kit = {path="../../ic-kit", features = ["http"]} +ic-kit = { path = "../../ic-kit" } [[bin]] diff --git a/examples/pastebin/src/canister.rs b/examples/pastebin/src/canister.rs index 343c201..bb0a25c 100644 --- a/examples/pastebin/src/canister.rs +++ b/examples/pastebin/src/canister.rs @@ -89,25 +89,23 @@ fn index_handler(r: HttpRequest, _: Params) -> HttpResponse { /// Get paste handler #[get(route = "/:file")] -fn get_file(_: HttpRequest, p: Params) -> HttpResponse { +fn get_file_handler(data: &Data, _: HttpRequest, p: Params) -> HttpResponse { let file = p.get("file").unwrap(); - with(|data: &Data| match data.get(file) { + match data.get(file) { Some(content) => HttpResponse::ok().with_body(content.clone()), None => HttpResponse::new(404).with_body(format!("file not found `{}`\n", file)), - }) + } } /// Upload paste handler #[put(route = "/:file", upgrade = true)] -fn put_file(req: HttpRequest, p: Params) -> HttpResponse { +fn put_file_handler(data: &mut Data, req: HttpRequest, p: Params) -> HttpResponse { let filename = p.get("file").unwrap(); - let url = req.header("host").unwrap_or("unknown"); + let host = req.header("host").unwrap_or("unknown"); - let res = format!("{}.{}/{}", id().to_text(), "localhost:8000", filename); + let res = format!("{}/{}", host, filename); - with_mut(|d: &mut Data| { - d.insert(filename.to_string(), req.body); - }); + data.insert(filename.to_string(), req.body); HttpResponse::ok().with_body(res) } From 2cbe62777da33e9e68595feec263e38b072efc5d Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Wed, 12 Oct 2022 20:50:14 -0400 Subject: [PATCH 14/24] feat: http mutable DI error message when not upgraded to update call --- ic-kit-macros/src/http.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/ic-kit-macros/src/http.rs b/ic-kit-macros/src/http.rs index 13d7e7a..2703d41 100644 --- a/ic-kit-macros/src/http.rs +++ b/ic-kit-macros/src/http.rs @@ -34,6 +34,7 @@ pub fn gen_handler_code( item: TokenStream, ) -> Result { let attrs = from_tokenstream::(&attr)?; + let upgrade = attrs.upgrade.unwrap_or(false); let fun = syn::parse2::(item.clone()).map_err(|e| { Error::new( item.span(), @@ -43,21 +44,37 @@ pub fn gen_handler_code( let sig = fun.sig; let output = sig.output.clone(); let ident = sig.ident.clone(); - let name = sig.ident.to_string(); let is_async = sig.asyncness.is_some(); let stmts = fun.block.stmts; HANDLERS.lock().unwrap().push(Handler { - name, + name: sig.ident.to_string(), route: attrs.route, method: method.into(), - upgrade: attrs.upgrade.unwrap_or(false), + upgrade, }); // Build the outer function's body. let args = di(collect_args(method, &sig)?, is_async)?; let (can_args, can_types): (Vec<_>, Vec<_>) = args.can_args.clone().into_iter().unzip(); + if !upgrade && !args.mut_args.is_empty() { + // TODO: This is much better as a warning, but the feature is not yet stable. + // #![feature()] + /*sig.span().unwrap().warning(format!( + "HTTP mutable dependencies will rollback unless the call is in update mode. To make changes persist, specify `upgrade = true` in the #[{}] attribute.", + method.to_lowercase() + )).emit();*/ + + return Err(Error::new( + sig.span(), + format!( + "HTTP mutable dependencies will rollback unless the call is in update mode. To make changes persist, specify `upgrade = true` in the #[{}] attribute.", + method.to_lowercase() + ), + )); + } + // Because DI doesn't work on an async method. let mut inner = TokenStream::new(); for stmt in stmts { From ceda29602405aada9c80bfe775264cbef825c535 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Wed, 12 Oct 2022 23:25:53 -0400 Subject: [PATCH 15/24] chore: bump all versions --- Cargo.toml | 1 + ic-kit-certified/Cargo.toml | 2 +- ic-kit-http/Cargo.toml | 4 ++-- ic-kit-macros/Cargo.toml | 2 +- ic-kit-runtime/Cargo.toml | 2 +- ic-kit-stable/Cargo.toml | 2 +- ic-kit-sys/Cargo.toml | 6 +++--- ic-kit/Cargo.toml | 10 +++++----- 8 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1328997..2d106b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "examples/hello", "examples/counter", "examples/factory_counter", "examples/fib", diff --git a/ic-kit-certified/Cargo.toml b/ic-kit-certified/Cargo.toml index adfe8d2..6dcb6ee 100644 --- a/ic-kit-certified/Cargo.toml +++ b/ic-kit-certified/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ic-kit-certified" -version = "0.1.0-alpha.0" +version = "0.1.0-alpha.1" edition = "2018" description = "Certified variable friendly data structures for the Internet Computer." authors = ["Parsa Ghadimi ", "Ossian Mapes "] diff --git a/ic-kit-http/Cargo.toml b/ic-kit-http/Cargo.toml index 576365c..c2be680 100644 --- a/ic-kit-http/Cargo.toml +++ b/ic-kit-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ic-kit-http" -version = "0.1.0-alpha.0" +version = "0.1.0-alpha.2" edition = "2021" authors = ["Ossian Mapes ", "Parsa Ghadimi "] description = "IC-Kit's macros for canister development" @@ -16,7 +16,7 @@ include = ["src", "Cargo.toml", "README.md"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ic-kit-macros = { path = "../ic-kit-macros", features = ["http"] } +ic-kit-macros = { path = "../ic-kit-macros", features = ["http"], version = "0.2.0-alpha.0" } candid = "0.8" serde = "1.0" matchit = "0.6.0" diff --git a/ic-kit-macros/Cargo.toml b/ic-kit-macros/Cargo.toml index 7542ddf..8b0d1bc 100644 --- a/ic-kit-macros/Cargo.toml +++ b/ic-kit-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ic-kit-macros" -version = "0.1.1-alpha.0" +version = "0.2.0-alpha.0" edition = "2021" authors = ["Parsa Ghadimi ", "Ossian Mapes "] description = "IC-Kit's macros for canister development" diff --git a/ic-kit-runtime/Cargo.toml b/ic-kit-runtime/Cargo.toml index 61804f5..34b77be 100644 --- a/ic-kit-runtime/Cargo.toml +++ b/ic-kit-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ic-kit-runtime" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" edition = "2021" authors = ["Parsa Ghadimi ", "Ossian Mapes "] description = "IC-Kit's Canister Simulator" diff --git a/ic-kit-stable/Cargo.toml b/ic-kit-stable/Cargo.toml index 10adbdd..80d11cc 100644 --- a/ic-kit-stable/Cargo.toml +++ b/ic-kit-stable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ic-kit-stable" -version = "0.1.0-alpha.0" +version = "0.1.0-alpha.1" edition = "2021" authors = ["Parsa Ghadimi ", "Ossian Mapes "] description = "IC-Kit's data structures living on the stable storage." diff --git a/ic-kit-sys/Cargo.toml b/ic-kit-sys/Cargo.toml index 1639491..0331bfb 100644 --- a/ic-kit-sys/Cargo.toml +++ b/ic-kit-sys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ic-kit-sys" -version = "0.1.3" +version = "0.1.4" edition = "2021" authors = ["Parsa Ghadimi ", "Ossian Mapes "] description = "IC-Kit's API bindings to the Internet Computer's WASM runtime." @@ -16,5 +16,5 @@ include = ["src", "Cargo.toml", "README.md"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [target.'cfg(not(target_family = "wasm"))'.dependencies] -tokio = {version="1.20", features=["sync"]} -futures = {version="0.3"} \ No newline at end of file +tokio = { version = "1.20", features = ["sync"] } +futures = { version = "0.3" } diff --git a/ic-kit/Cargo.toml b/ic-kit/Cargo.toml index 204e810..48f33ea 100644 --- a/ic-kit/Cargo.toml +++ b/ic-kit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ic-kit" -version = "0.5.0-alpha.5" +version = "0.5.0-alpha.6" description = "Testable Canister Developer Kit for the Internet Computer." authors = ["Parsa Ghadimi ", "Ossian Mapes "] edition = "2018" @@ -14,9 +14,9 @@ keywords = ["internet-computer", "canister", "fleek", "psychedelic"] include = ["src", "Cargo.toml", "README.md"] [dependencies] -ic-kit-sys = { path = "../ic-kit-sys", version = "0.1.3" } -ic-kit-macros = { path = "../ic-kit-macros", version = "0.1.1-alpha.0" } -ic-kit-http = { path = "../ic-kit-http", version = "0.1.0-alpha.0", optional = true } +ic-kit-sys = { path = "../ic-kit-sys", version = "0.1.4" } +ic-kit-macros = { path = "../ic-kit-macros", version = "0.2.0-alpha.0" } +ic-kit-http = { path = "../ic-kit-http", version = "0.1.0-alpha.2", optional = true } candid = "0.8" serde = "1.0" @@ -27,4 +27,4 @@ ic-kit-runtime = { path = "../ic-kit-runtime", version = "0.1.0-alpha.2" } default = [] experimental-stable64 = [] experimental-cycles128 = [] -http = ["dep:ic-kit-http", "ic-kit-macros/http"] \ No newline at end of file +http = ["dep:ic-kit-http", "ic-kit-macros/http"] From 61e4cdbb1d3c18650e208d2c455f61dc2e3d350b Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Thu, 13 Oct 2022 13:21:04 -0400 Subject: [PATCH 16/24] refactor: export service now reuses new di::wrap, entry let's use take --- ic-kit-macros/src/entry.rs | 64 ++++++----------------------- ic-kit-macros/src/export_service.rs | 7 ++-- 2 files changed, 16 insertions(+), 55 deletions(-) diff --git a/ic-kit-macros/src/entry.rs b/ic-kit-macros/src/entry.rs index 7256f1e..0f76347 100644 --- a/ic-kit-macros/src/entry.rs +++ b/ic-kit-macros/src/entry.rs @@ -10,7 +10,7 @@ use serde::Deserialize; use serde_tokenstream::from_tokenstream; use syn::{spanned::Spanned, Error}; -use crate::di::{collect_args, di}; +use crate::di::{collect_args, di, wrap}; use crate::export_service::declare; #[derive(Copy, Clone, PartialOrd, PartialEq, Ord, Eq)] @@ -40,17 +40,11 @@ impl std::fmt::Display for EntryPoint { impl EntryPoint { pub fn is_lifecycle(&self) -> bool { - match &self { - EntryPoint::Update | EntryPoint::Query => false, - _ => true, - } + !matches!(self, EntryPoint::Update | EntryPoint::Query) } pub fn is_inspect_message(&self) -> bool { - match &self { - EntryPoint::InspectMessage => true, - _ => false, - } + matches!(self, EntryPoint::InspectMessage) } } @@ -173,14 +167,12 @@ pub fn gen_entry_point_code( }; // Build the outer function's body. - let tmp = di( + let p_args = di( collect_args(entry_point.to_string().as_str(), signature)?, is_async, )?; - let args = tmp.args; - let (can_args, can_types): (Vec<_>, Vec<_>) = tmp.can_args.into_iter().unzip(); - let (imu_args, imu_types): (Vec<_>, Vec<_>) = tmp.imu_args.into_iter().unzip(); - let (mut_args, mut_types): (Vec<_>, Vec<_>) = tmp.mut_args.into_iter().unzip(); + let args = p_args.args.clone(); + let (can_args, can_types): (Vec<_>, Vec<_>) = p_args.can_args.clone().into_iter().unzip(); // If the method does not accept any arguments, don't even read the msg_data, and if the // deserialization fails, just reject the message, which is cheaper than trap. @@ -229,39 +221,14 @@ pub fn gen_entry_point_code( } }; - // Because DI doesn't work on an async method. - let mut sync_result = quote! { - let result = #name ( #(#args),* ); - #return_encode - }; - - sync_result = match imu_args.len() { - 0 => sync_result, - 1 => quote! { - ic_kit::ic::with(|#(#imu_args: &#imu_types),*| { - #sync_result - }); - }, - _ => quote! { - ic_kit::ic::with_many(|(#(#imu_args),*) : (#(&#imu_types),*)| { - #sync_result - }); - }, - }; - - sync_result = match mut_args.len() { - 0 => sync_result, - 1 => quote! { - ic_kit::ic::with_mut(|#(#mut_args: &mut #mut_types),*| { - #sync_result - }); - }, - _ => quote! { - ic_kit::ic::with_many_mut(|(#(#mut_args),*) : (#(&mut #mut_types),*)| { - #sync_result - }); + // DI doesn't work on async methods. + let sync_result = wrap( + quote! { + let result = #name ( #(#args),* ); + #return_encode }, - }; + p_args, + ); // only spawn for async methods. let body = if is_async { @@ -309,9 +276,7 @@ pub fn gen_entry_point_code( #[doc(hidden)] #[export_name = #export_name] fn #outer_function_ident() { - #[cfg(target_family = "wasm")] ic_kit::setup_hooks(); - #guard #body } @@ -319,9 +284,6 @@ pub fn gen_entry_point_code( #[cfg(not(target_family = "wasm"))] #[doc(hidden)] fn #outer_function_ident() { - #[cfg(target_family = "wasm")] - ic_kit::setup_hooks(); - #guard #body } diff --git a/ic-kit-macros/src/export_service.rs b/ic-kit-macros/src/export_service.rs index 2ce1776..c5d3107 100644 --- a/ic-kit-macros/src/export_service.rs +++ b/ic-kit-macros/src/export_service.rs @@ -21,7 +21,6 @@ struct Method { lazy_static! { static ref METHODS: Mutex> = Mutex::new(Default::default()); static ref LIFE_CYCLES: Mutex> = Mutex::new(Default::default()); - } pub(crate) fn declare( @@ -91,12 +90,12 @@ pub(crate) fn declare( pub fn export_service(input: DeriveInput, save_candid_path: Option) -> TokenStream { let methods = { let mut map = METHODS.lock().unwrap(); - std::mem::replace(&mut *map, BTreeMap::new()) + std::mem::take(&mut *map) }; let mut life_cycles = { let mut map = LIFE_CYCLES.lock().unwrap(); - std::mem::replace(&mut *map, BTreeMap::new()) + std::mem::take(&mut *map) }; let mut rust_methods = Vec::new(); @@ -230,7 +229,7 @@ pub fn export_service(input: DeriveInput, save_candid_path: Option) #[cfg(feature = "http")] () => crate::http::gen_http_request_code(), }; - + let metadata = generate_metadata(); quote! { From c6e77e5ef88ae3b9815b05a3b85e7b787673dc14 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Thu, 13 Oct 2022 13:23:48 -0400 Subject: [PATCH 17/24] chore: remove unused Method implementation from ic-kit-http --- ic-kit-http/src/lib.rs | 83 +----------------------------------------- 1 file changed, 1 insertion(+), 82 deletions(-) diff --git a/ic-kit-http/src/lib.rs b/ic-kit-http/src/lib.rs index f62bbf8..8fb4221 100644 --- a/ic-kit-http/src/lib.rs +++ b/ic-kit-http/src/lib.rs @@ -1,7 +1,7 @@ use candid::{CandidType, Deserialize, Func, Nat}; +pub use matchit::{Match, MatchError, Params, Router as BasicRouter}; pub use ic_kit_macros::{delete, get, post, put}; -pub use matchit::{Match, MatchError, Params, Router as BasicRouter}; pub type HeaderField = (String, String); @@ -103,84 +103,3 @@ pub struct StreamingCallbackHttpResponse { pub body: Vec, pub token: Option, } - -/// A [`Method`](https://developer.mozilla.org/en-US/docs/Web/API/Request/method) representation -/// used on Request objects. -#[derive(Debug, Clone, PartialEq, Hash, Eq)] -pub enum Method { - Head = 0, - Get, - Post, - Put, - Patch, - Delete, - Options, - Connect, - Trace, -} - -impl Method { - pub fn all() -> Vec { - vec![ - Method::Head, - Method::Get, - Method::Post, - Method::Put, - Method::Patch, - Method::Delete, - Method::Options, - Method::Connect, - Method::Trace, - ] - } -} - -impl From for Method { - fn from(m: String) -> Self { - match m.to_ascii_uppercase().as_str() { - "HEAD" => Method::Head, - "POST" => Method::Post, - "PUT" => Method::Put, - "PATCH" => Method::Patch, - "DELETE" => Method::Delete, - "OPTIONS" => Method::Options, - "CONNECT" => Method::Connect, - "TRACE" => Method::Trace, - _ => Method::Get, - } - } -} - -impl From for String { - fn from(val: Method) -> Self { - val.as_ref().to_string() - } -} - -impl AsRef for Method { - fn as_ref(&self) -> &'static str { - match self { - Method::Head => "HEAD", - Method::Post => "POST", - Method::Put => "PUT", - Method::Patch => "PATCH", - Method::Delete => "DELETE", - Method::Options => "OPTIONS", - Method::Connect => "CONNECT", - Method::Trace => "TRACE", - Method::Get => "GET", - } - } -} - -impl ToString for Method { - fn to_string(&self) -> String { - (*self).clone().into() - } -} - -impl Default for Method { - fn default() -> Self { - Method::Get - } -} From e729c20690079887556ddea1aec0d225b52d4e0a Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Thu, 13 Oct 2022 13:30:06 -0400 Subject: [PATCH 18/24] feat: hello example --- dfx.json | 5 +++++ examples/hello/Cargo.toml | 14 ++++++++++++++ examples/hello/candid.did | 1 + examples/hello/src/canister.rs | 13 +++++++++++++ examples/hello/src/lib.rs | 1 + examples/hello/src/main.rs | 3 +++ 6 files changed, 37 insertions(+) create mode 100644 examples/hello/Cargo.toml create mode 100644 examples/hello/candid.did create mode 100644 examples/hello/src/canister.rs create mode 100644 examples/hello/src/lib.rs create mode 100644 examples/hello/src/main.rs diff --git a/dfx.json b/dfx.json index 7ec8ff1..4936d07 100644 --- a/dfx.json +++ b/dfx.json @@ -2,6 +2,11 @@ "version": 1, "dfx": "0.11.1", "canisters": { + "hello": { + "candid": "examples/hello/candid.did", + "package": "ic_kit_example_hello", + "type": "rust" + }, "counter": { "candid": "examples/counter/candid.did", "package": "ic_kit_example_counter", diff --git a/examples/hello/Cargo.toml b/examples/hello/Cargo.toml new file mode 100644 index 0000000..83f2d87 --- /dev/null +++ b/examples/hello/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ic_kit_example_hello" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# With the `http` feature enabled, and no configured routes, the canister will provide status info at the index path. +ic-kit = { path = "../../ic-kit", features = ["http"] } + +[[bin]] +name = "ic_kit_example_hello" +path = "src/main.rs" diff --git a/examples/hello/candid.did b/examples/hello/candid.did new file mode 100644 index 0000000..40e5d2f --- /dev/null +++ b/examples/hello/candid.did @@ -0,0 +1 @@ +service : { hello : () -> (text) query } \ No newline at end of file diff --git a/examples/hello/src/canister.rs b/examples/hello/src/canister.rs new file mode 100644 index 0000000..d6f09cf --- /dev/null +++ b/examples/hello/src/canister.rs @@ -0,0 +1,13 @@ +use ic_kit::prelude::*; + +#[query] +fn hello() -> String { + "Hello, World!".to_string() +} + +// When the http feature is enabled, the `http_request` function is generated, with a single index route. +// This index route returns the balance of the canister in cycles, in JSON. + +#[derive(KitCanister)] +#[candid_path("candid.did")] +struct HelloCanister; diff --git a/examples/hello/src/lib.rs b/examples/hello/src/lib.rs new file mode 100644 index 0000000..1d28d24 --- /dev/null +++ b/examples/hello/src/lib.rs @@ -0,0 +1 @@ +pub mod canister; diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs new file mode 100644 index 0000000..c53437a --- /dev/null +++ b/examples/hello/src/main.rs @@ -0,0 +1,3 @@ +mod canister; + +fn main() {} From 9cd689fa22c87e249f22e57687eb3e726b8240ba Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Thu, 13 Oct 2022 13:34:54 -0400 Subject: [PATCH 19/24] docs: update examples README --- examples/README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/README.md b/examples/README.md index 13bb8eb..5f600d5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,14 +4,25 @@ This directory contains some example canister's implemented using the IC Kit. Ea to be as simple as possible to demonstrate one aspect of the IC Kit and a possible design pattern you can also use to develop canisters. +Hello World: + +- [Hello](hello/): A simple canister that returns a greeting. + Simple State Manipulation: -- Counter + +- [Counter](counter/): A simple canister that allows incrementing and decrementing a counter. Inter Canister Communication: -- Multi Counter + +- [Multi Counter](multi_counter/): A canister that allows incrementing and decrementing a counter + that is stored in separate canisters. Child Canister Creation: -- Factory Counter + +- [Factory Counter](factory_counter/): A canister that allows incrementing and decrementing a counter + that is stored in separate canisters that are created on demand. Inbound HTTP Server: -- Pastebin \ No newline at end of file + +- [Pastebin](pastebin/): A canister that allows storing and retrieving unencrypted text snippets through http routing. + Also features a simple canister generated frontend, that serves plaintext or html depending on the request client. From 2347558cd38d2c00c6c249379a99f0122b00806f Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Thu, 13 Oct 2022 17:23:38 -0400 Subject: [PATCH 20/24] docs: ic-kit-http cargo doc --- examples/pastebin/src/canister.rs | 10 +- ic-kit-http/src/lib.rs | 225 ++++++++++++++++++++++++++++-- ic-kit-macros/src/lib.rs | 89 ++++++++++++ ic-kit-macros/src/test.rs | 2 +- ic-kit/src/lib.rs | 2 +- 5 files changed, 308 insertions(+), 20 deletions(-) diff --git a/examples/pastebin/src/canister.rs b/examples/pastebin/src/canister.rs index bb0a25c..5ee2e3f 100644 --- a/examples/pastebin/src/canister.rs +++ b/examples/pastebin/src/canister.rs @@ -77,14 +77,14 @@ fn index_handler(r: HttpRequest, _: Params) -> HttpResponse { // Just return the manpage if client is a terminal (curl or wget) if let Some(ua) = r.header("User-Agent") { if ua.starts_with("curl") || ua.starts_with("wget") { - return HttpResponse::ok().with_body(manpage); + return HttpResponse::ok().body(manpage); } } tt.add_template("html", HTML_TEMPLATE).unwrap(); let html = tt.render("html", &HtmlContext { manpage }).unwrap(); - HttpResponse::ok().with_body(html) + HttpResponse::ok().body(html) } /// Get paste handler @@ -92,8 +92,8 @@ fn index_handler(r: HttpRequest, _: Params) -> HttpResponse { fn get_file_handler(data: &Data, _: HttpRequest, p: Params) -> HttpResponse { let file = p.get("file").unwrap(); match data.get(file) { - Some(content) => HttpResponse::ok().with_body(content.clone()), - None => HttpResponse::new(404).with_body(format!("file not found `{}`\n", file)), + Some(content) => HttpResponse::ok().body(content.clone()), + None => HttpResponse::new(404).body(format!("file not found `{}`\n", file)), } } @@ -107,7 +107,7 @@ fn put_file_handler(data: &mut Data, req: HttpRequest, p: Params) -> HttpRespons data.insert(filename.to_string(), req.body); - HttpResponse::ok().with_body(res) + HttpResponse::ok().body(res) } #[derive(KitCanister)] diff --git a/ic-kit-http/src/lib.rs b/ic-kit-http/src/lib.rs index 8fb4221..b806f8c 100644 --- a/ic-kit-http/src/lib.rs +++ b/ic-kit-http/src/lib.rs @@ -1,19 +1,102 @@ +//! This crate seeks to provide an easy to use framework for writing canisters that accept HTTP requests +//! on the Internet Computer. +//! +//! For the router, we use [`matchit`], a *blazing* fast URL router. +//! +//! It is built in conjunction with the [ic_kit_macros] crate. The macros +//! provided by this crate are used to generate a canister method `http_request` and `http_request_update` +//! that routes the HTTP requests to handlers. A `Router` struct and implementation will be generated +//! and used that will dispatch the HTTP requests to the appropriate handler. +//! +//! ## Example +//! +//! For a complete example, see the [`pastebin`]. +//! +//! ## Macro Generated Router +//! The macro generated router will have a field for each HTTP method, as well as a generic `insert` +//! and `at` method. These accept the same arguments as the `insert` and `at` methods of the [`BasicRouter`], +//! but with an additional argument for the HTTP method. +//! A router can look like this: +//! ``` +//! use ic_kit_http::*; +//! +//! pub struct Router { +//! pub get: BasicRouter, +//! // if there are no handlers for the method, the router will not have a field or implementation for it +//! pub post: BasicRouter, +//! pub put: BasicRouter, +//! pub delete: BasicRouter, +//! } +//! +//! impl Router { +//! pub fn insert(&mut self, method: &str, path: &str, handler: HandlerFn) { +//! match method { +//! "get" => self.get.insert(path, handler).unwrap(), +//! "post" => self.post.insert(path, handler).unwrap(), +//! "put" => self.put.insert(path, handler).unwrap(), +//! "delete" => self.delete.insert(path, handler).unwrap(), +//! _ => panic!("unsupported method: " + method), +//! }; +//! } +//! pub fn at<'s: 'p, 'p>( +//! &'s self, +//! method: &str, +//! path: &'p str, +//! ) -> Result, MatchError> { +//! match method { +//! "get" => self.get.at(path), +//! "post" => self.post.at(path), +//! "put" => self.put.at(path), +//! "delete" => self.delete.at(path), +//! _ => Err(MatchError::NotFound), +//! } +//! } +//! } +//! ``` +//! +//! ## Macro Generated http_request update and query calls +//! The macros will also generate a `http_request` query call handler. They will utilize +//! the generated router to dispatch the request to the appropriate handler. It also performs the +//! necessary path to upgrade to an additional update method (`http_request_update`) if the handler +//! is marked as upgraded. +//! +//! [`pastebin`]: https://github.com/Psychedelic/ic-kit/tree/main/examples/pastebin + +use std::collections::HashMap; + use candid::{CandidType, Deserialize, Func, Nat}; -pub use matchit::{Match, MatchError, Params, Router as BasicRouter}; +/// The macro generated `Router` struct will return this type when a route is matched. +/// +pub use matchit::Match; +pub use matchit::{MatchError, Params, Router as BasicRouter}; pub use ic_kit_macros::{delete, get, post, put}; +/// Alias for a key/value header tuple pub type HeaderField = (String, String); +/// # HttpRequest +/// The IC data type for a request. +/// * `method` - the HTTP method of the request. +/// * `url` - the URL of the request. +/// * `headers` - a list of key-value pairs. +/// * `body` - a raw byte array for the request body. #[derive(Clone, Debug, CandidType, Deserialize)] pub struct HttpRequest { pub method: String, pub url: String, - pub headers: Vec<(String, String)>, + pub headers: Vec, pub body: Vec, } impl HttpRequest { + /// Returns the value of the header with the given name. Case Insensitive. + /// If the header is not present, returns `None`. + /// + /// # Example + /// ``` + /// let host = request.header("host").expect("header not found"); + /// ``` pub fn header(&self, name: &str) -> Option<&str> { self.headers .iter() @@ -22,6 +105,27 @@ impl HttpRequest { } } +/// # HttpResponse +/// The IC data type for a response. +/// - `status_code`: an HTTP status code. +/// - `headers`: a list of key-value pairs. +/// - `streaming_strategy`: a streaming strategy for chunking the response. +/// - `body`: a raw byte array for the response body. +/// - `upgrade`: a flag to indicate whether the response should be upgraded to an update call. +/// This adds time (consensus) to the call, but allows for state changes to be commited. +/// Otherwise, the call is read-only and any changes done will be dropped +/// +/// # Example +/// +/// ```rs +/// // create a new ok response with a body +/// let res = HttpResponse::ok().body("Hello World"); +/// +/// // create a new custom response with headers +/// let res = HttpResponse::new(404) +/// .headers(vec![("Content-Type", "text/html")]) +/// .body("

Not Found

"); +/// ``` #[derive(Clone, Debug, CandidType, Deserialize)] pub struct HttpResponse { pub status_code: u16, @@ -32,6 +136,13 @@ pub struct HttpResponse { } impl HttpResponse { + /// Create a new empty [`HttpResponse`] with a 200 status code. Generally used in conjunction with + /// [`HttpResponse::body`] and [`HttpResponse::headers`] + /// + /// ``` + /// use ic_kit_http::HttpResponse; + /// let res = HttpResponse::ok().body("Hello World"); + /// ``` pub fn ok() -> Self { Self { status_code: 200, @@ -42,6 +153,12 @@ impl HttpResponse { } } + /// Create a new empty [`HttpResponse`] with a given status code. + /// + /// ``` + /// use ic_kit_http::HttpResponse; + /// let res = HttpResponse::new(404); + /// ``` pub fn new(status_code: u16) -> Self { Self { status_code, @@ -52,37 +169,104 @@ impl HttpResponse { } } - #[inline(always)] - pub fn with_body>>(mut self, body: T) -> Self { + /// Set the body of the [`HttpResponse`]. + /// ``` + /// use ic_kit_http::HttpResponse; + /// let res = HttpResponse::ok(); + /// + /// res.body("Hello World"); + /// ``` + pub fn body>>(mut self, body: T) -> Self { self.body = body.into(); self } - #[inline(always)] - pub fn with_header(mut self, name: &str, value: &str) -> Self { - self.headers.push((name.to_string(), value.to_string())); + /// Extend the existing body of the [`HttpResponse`]. + /// ``` + /// use ic_kit_http::HttpResponse; + /// let res = HttpResponse::ok(); + /// + /// res.body("Hello"); + /// res.extend_body(" World"); + /// ``` + /// + pub fn extend_body>>(mut self, body: T) -> Self { + self.body.extend(body.into()); self } - #[inline(always)] - pub fn with_headers(mut self, headers: Vec) -> Self { - self.headers.extend(headers); + /// Insert a header into the [`HttpResponse`]. If the header already exists, it will be replaced. + /// If the value is empty, the header will be removed. + pub fn header>(mut self, name: T, value: T) -> Self { + let (name, value) = (name.into(), value.into()); + let mut map = self + .headers + .into_iter() + .collect::>(); + if value.is_empty() { + map.remove(&name); + } else { + map.insert(name, value); + } + self.headers = map.into_iter().collect(); + self } - #[inline(always)] - pub fn with_streaming_strategy(mut self, streaming_strategy: StreamingStrategy) -> Self { + /// Set the headers of the [`HttpResponse`]. + /// + /// To remove a header, set it to an empty string. + /// + /// ``` + /// use ic_kit_http::HttpResponse; + /// let res = HttpResponse::ok(); + /// + /// // set some headers + /// res.headers(vec![ + /// ("Content-Type".into(), "text/html".into()), + /// ("X-Foo".into(), "Bar".into()), + /// ]); + /// + /// // remove a header + /// res.headers(vec![ + /// ("X-Foo".into(), "".into()), + /// ]); + /// + /// ``` + pub fn headers(mut self, headers: Vec) -> Self { + let mut map = self + .headers + .into_iter() + .collect::>(); + for (name, value) in headers { + if value.is_empty() { + map.remove(&name); + } else { + map.insert(name, value); + } + } + self.headers = map.into_iter().collect(); + + self + } + + pub fn streaming_strategy(mut self, streaming_strategy: StreamingStrategy) -> Self { self.streaming_strategy = Some(streaming_strategy); self } #[inline(always)] - pub fn with_upgrade(mut self) -> Self { + pub fn upgrade(mut self) -> Self { self.upgrade = true; self } } +/// # StreamingCallbackToken +/// The IC data type for a streaming callback token. +/// - `key`: the key of the resource to stream. +/// - `content_encoding`: the content encoding of the resource. +/// - `index`: the index to be used to identify the chunk or byte offset of the resource. #[derive(Clone, Debug, CandidType, Deserialize)] pub struct StreamingCallbackToken { pub key: String, @@ -90,6 +274,17 @@ pub struct StreamingCallbackToken { pub index: Nat, } +/// # StreamingStrategy +/// The IC data type for a streaming strategy. +/// A streaming strategy is used to chunk the response body into multiple chunks. +/// +/// ## Chunking Strategies +/// +/// ### Callback +/// The `StreamingCallbackToken` is used to retrieve the next chunk of the response body. +/// The callback [`Func`] is a candid method that accepts a [`StreamingCallbackToken`] +/// and returns another [`StreamingCallbackHttpResponse`], for the next chunk of the response body. +/// These chunks are then streamed to the client by the gateway until there is no more callback tokens. #[derive(Clone, Debug, CandidType, Deserialize)] pub enum StreamingStrategy { Callback { @@ -98,6 +293,10 @@ pub enum StreamingStrategy { }, } +/// # StreamingCallbackHttpResponse +/// The IC data type for a streaming callback response. +/// - `body`: a raw byte array for the chunk of the response body. +/// - `token`: a [`StreamingCallbackToken`] for the next chunk of the response body. If the token is `None`, then there are no more chunks. #[derive(Clone, Debug, CandidType, Deserialize)] pub struct StreamingCallbackHttpResponse { pub body: Vec, diff --git a/ic-kit-macros/src/lib.rs b/ic-kit-macros/src/lib.rs index ccc5ae1..a6fac4b 100644 --- a/ic-kit-macros/src/lib.rs +++ b/ic-kit-macros/src/lib.rs @@ -102,6 +102,26 @@ fn get_save_candid_path(input: &syn::DeriveInput) -> syn::Result fn([HttpRequest](../ic_kit_http/struct.HttpRequest.html), [Params](../ic_kit_http/struct.Params.html)) -> [HttpResponse](../ic_kit_http/struct.HttpResponse.html) +/// +/// HTTP macros will remove dependency injected reference args from the function signature, so you can use DI in your handlers. +/// +/// # Example +/// ```rs +/// // set a route for GET / that has no params +/// #[get(route = "/")] +/// fn index_handler(r: HttpRequest, _: Params) -> HttpResponse { +/// ic::print(format!("{:?}", r)); +/// +/// // grab a header +/// let header = r.headers.get("host").unwrap(); +/// +/// // Build an Ok (200) response with a body containing the host header +/// HttpResponse::ok().body(header) +/// } +/// ``` #[cfg(feature = "http")] #[proc_macro_attribute] pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -111,6 +131,30 @@ pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream { } /// Export a function as a HTTP POST handler. +/// +/// The function must have the signature +/// > fn([HttpRequest](../ic_kit_http/struct.HttpRequest.html), [Params](../ic_kit_http/struct.Params.html)) -> [HttpResponse](../ic_kit_http/struct.HttpResponse.html) +/// +/// HTTP macros will remove dependency injected reference args from the function signature, so you can use DI in your handlers. +/// +/// # Example +/// Store a value in the canister's memory, with the key being a path parameter. +/// +/// ```rs +/// pub type Data = HashMap>; +/// +/// // set a route for POST /data/ that has a single path param, and upgrades to an update call +/// #[post(route = "/set/:key", upgrade = true)] +/// fn set_handler(r: HttpRequest, p: Params) -> HttpResponse { +/// let key = p.get("key").unwrap(); +/// let value = r.body; +/// +/// ic_kit::with_mut(|data: &mut Data| { +/// data.insert(key, value); +/// }); +/// +/// HttpResponse::ok().body("stored value") +/// } #[cfg(feature = "http")] #[proc_macro_attribute] pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -120,6 +164,30 @@ pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream { } /// Export a function as a HTTP PUT handler. +/// +/// The function must have the signature +/// > fn([HttpRequest](../ic_kit_http/struct.HttpRequest.html), [Params](../ic_kit_http/struct.Params.html)) -> [HttpResponse](../ic_kit_http/struct.HttpResponse.html) +/// +/// HTTP macros will remove dependency injected reference args from the function signature, so you can use DI in your handlers. +/// +/// # Example +/// +/// ```rs +/// pub type Data = HashMap>; +/// +/// // set a route for PUT / that has a single path param, and upgrades to an update call +/// #[put(route = ":filename", upgrade = true)] +/// fn put_handler(data: &mut Data, r: HttpRequest, p: Params) -> HttpResponse { +/// // get the filename param +/// let file = p.get("filename").unwrap(); +/// let value = r.body; +/// +/// data.insert(file, value); +/// +/// // Build an Ok (200) response with a body +/// HttpResponse::ok().body("stored file") +/// } +/// ``` #[cfg(feature = "http")] #[proc_macro_attribute] pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -129,6 +197,27 @@ pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream { } /// Export a function as a HTTP DELETE handler. +/// +/// The function must have the signature +/// > fn([HttpRequest](../ic_kit_http/struct.HttpRequest.html), [Params](../ic_kit_http/struct.Params.html)) -> [HttpResponse](../ic_kit_http/struct.HttpResponse.html) +/// +/// HTTP macros will remove dependency injected reference args from the function signature, so you can use DI in your handlers. +/// +/// # Example +/// +/// ```rs +/// pub type Data = HashMap>; +/// +/// // set a route for DELETE / that has a single path param, and upgrades to an update call +/// #[delete(route = ":file", upgrade = true)] +/// fn delete_handler(data: &mut Data, r: HttpRequest, p: Params) -> HttpResponse { +/// let file = p.get("file").unwrap(); +/// +/// data.remove(file); +/// +/// // Build an Ok (200) response with a body +/// HttpResponse::ok().body("deleted file") +/// } #[cfg(feature = "http")] #[proc_macro_attribute] pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream { diff --git a/ic-kit-macros/src/test.rs b/ic-kit-macros/src/test.rs index 6124d9b..2189a95 100644 --- a/ic-kit-macros/src/test.rs +++ b/ic-kit-macros/src/test.rs @@ -22,7 +22,7 @@ pub fn gen_test_code(_: TokenStream, item: TokenStream) -> Result Date: Thu, 13 Oct 2022 20:32:49 -0400 Subject: [PATCH 21/24] refactor: simplify and fmt exports --- ic-kit/src/lib.rs | 58 ++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/ic-kit/src/lib.rs b/ic-kit/src/lib.rs index 6b10b16..4529704 100644 --- a/ic-kit/src/lib.rs +++ b/ic-kit/src/lib.rs @@ -1,3 +1,20 @@ +#![warn(missing_docs)] +#![doc = include_str ! ("../../README.md")] + +// re-exports. +pub use candid::{self, CandidType, Nat, Principal}; + +// The KitCanister derive macro. +pub use canister::KitCanister; +#[cfg(feature = "http")] +pub use ic_kit_http as http; +pub use ic_kit_macros as macros; +/// The IC-kit runtime, which can be used for testing the canister in non-wasm environments. +#[cfg(not(target_family = "wasm"))] +pub use ic_kit_runtime as rt; +pub use macros::KitCanister; +pub use setup::setup_hooks; + mod canister; mod futures; mod setup; @@ -12,41 +29,20 @@ pub mod stable; /// Internal utility methods to deal with reading data. pub mod utils; -// re-exports. -pub use candid::{self, CandidType, Nat, Principal}; -pub use ic_kit_macros as macros; -pub use setup::setup_hooks; - -// The KitCanister derive macro. -pub use canister::KitCanister; -pub use ic_kit_macros::KitCanister; - -/// The IC-kit runtime, which can be used for testing the canister in non-wasm environments. -#[cfg(not(target_family = "wasm"))] -pub use ic_kit_runtime as rt; - -#[cfg(feature = "http")] -pub use ic_kit_http as http; - /// The famous prelude module which re exports the most useful methods. pub mod prelude { - pub use super::canister::KitCanister; - pub use super::ic; - pub use super::ic::CallBuilder; - pub use super::ic::{balance, caller, id, spawn}; - pub use super::ic::{maybe_with, maybe_with_mut, swap, take, with, with_mut}; - pub use super::ic::{Cycles, StableSize}; - pub use candid::{CandidType, Nat, Principal}; pub use serde::{Deserialize, Serialize}; - pub use ic_kit_macros::{query, init, update, pre_upgrade, post_upgrade}; - + pub use super::candid::{CandidType, Nat, Principal}; + pub use super::canister::KitCanister; + /// Enabled with the `http` feature. This re-exports the http module and macros #[cfg(feature = "http")] - pub use ic_kit_http::*; - - #[cfg(not(target_family = "wasm"))] - pub use ic_kit_runtime as rt; - + pub use super::http::*; + pub use super::ic::{ + self, balance, caller, id, maybe_with, maybe_with_mut, spawn, swap, take, with, with_mut, + CallBuilder, Cycles, StableSize, + }; + pub use super::macros::*; #[cfg(not(target_family = "wasm"))] - pub use ic_kit_runtime::prelude::*; + pub use super::rt::{self, prelude::*}; } From 5a23ae663d0723d48f2bc90e9907f29279af1196 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Thu, 13 Oct 2022 20:33:52 -0400 Subject: [PATCH 22/24] refactor: experimental-http feature; patch, path, head, options --- ic-kit-macros/Cargo.toml | 1 + ic-kit-macros/src/lib.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ic-kit-macros/Cargo.toml b/ic-kit-macros/Cargo.toml index 8b0d1bc..222a51c 100644 --- a/ic-kit-macros/Cargo.toml +++ b/ic-kit-macros/Cargo.toml @@ -27,6 +27,7 @@ compile-time-run = "0.2.12" [features] default = [] http = [] +experimental-http = [] [lib] proc-macro = true diff --git a/ic-kit-macros/src/lib.rs b/ic-kit-macros/src/lib.rs index a6fac4b..0911da5 100644 --- a/ic-kit-macros/src/lib.rs +++ b/ic-kit-macros/src/lib.rs @@ -226,8 +226,8 @@ pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream { .into() } -/// Export a function as a HTTP PATCH handler. -#[cfg(feature = "http")] +/// Export a function as a HTTP PATCH handler. Enabled with the `experimental-http` feature. +#[cfg(feature = "experimental-http")] #[proc_macro_attribute] pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream { http::gen_handler_code("PATCH", attr.into(), item.into()) @@ -235,8 +235,8 @@ pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream { .into() } -/// Export a function as a HTTP OPTIONS handler. -#[cfg(feature = "http")] +/// Export a function as a HTTP OPTIONS handler. Enabled with the `experimental-http` feature. +#[cfg(feature = "experimental-http")] #[proc_macro_attribute] pub fn options(attr: TokenStream, item: TokenStream) -> TokenStream { http::gen_handler_code("OPTIONS", attr.into(), item.into()) @@ -244,8 +244,8 @@ pub fn options(attr: TokenStream, item: TokenStream) -> TokenStream { .into() } -/// Export a function as a HTTP HEAD handler. -#[cfg(feature = "http")] +/// Export a function as a HTTP HEAD handler. Enabled with the `experimental-http` feature. +#[cfg(feature = "experimental-http")] #[proc_macro_attribute] pub fn head(attr: TokenStream, item: TokenStream) -> TokenStream { http::gen_handler_code("HEAD", attr.into(), item.into()) From e35a024b139a7eba503883bd236fd3a2ade810fe Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Thu, 3 Nov 2022 20:44:56 -0400 Subject: [PATCH 23/24] chore: fix cdk version metadata macro --- ic-kit-macros/src/metadata.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ic-kit-macros/src/metadata.rs b/ic-kit-macros/src/metadata.rs index 78d9457..ade4e93 100644 --- a/ic-kit-macros/src/metadata.rs +++ b/ic-kit-macros/src/metadata.rs @@ -22,7 +22,7 @@ pub fn generate_metadata() -> TokenStream { let cdk = generate_static_string( "CDK_VERSION", - run_command_str!("cargo", "tree", "-i", "ic-kit", "-e", "build"), + run_command_str!("cargo", "tree", "-i", "ic-kit"), ); let compiler = generate_static_string( From 4ad4b0c766b97919e1f7892112029f2467c6d5a1 Mon Sep 17 00:00:00 2001 From: ozwaldorf Date: Thu, 3 Nov 2022 20:48:19 -0400 Subject: [PATCH 24/24] chore: hardcode cdk version metadata (for now) --- ic-kit-macros/src/metadata.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ic-kit-macros/src/metadata.rs b/ic-kit-macros/src/metadata.rs index ade4e93..82391a6 100644 --- a/ic-kit-macros/src/metadata.rs +++ b/ic-kit-macros/src/metadata.rs @@ -20,10 +20,7 @@ pub fn generate_metadata() -> TokenStream { run_command_str!("git", "config", "--get", "remote.origin.url"), ); - let cdk = generate_static_string( - "CDK_VERSION", - run_command_str!("cargo", "tree", "-i", "ic-kit"), - ); + let cdk = generate_static_string("CDK_VERSION", "0.5.0-alpha"); let compiler = generate_static_string( "COMPILER",