diff --git a/.gitignore b/.gitignore index df88fc8..9e834ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,5 @@ -/target - - -# Added by cargo -# -# already existing elements were commented out - -#/target +target/ Cargo.lock -.dfx \ No newline at end of file +.dfx +*.old* +.idea/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 3b0103f..2d106b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,12 @@ [workspace] members = [ - "e2e", + "examples/hello", "examples/counter", "examples/factory_counter", "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 4050482..4936d07 100644 --- a/dfx.json +++ b/dfx.json @@ -1,6 +1,12 @@ { "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", @@ -15,6 +21,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/README.md b/examples/README.md index 964300b..5f600d5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,5 +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 \ No newline at end of file + +- [Counter](counter/): A simple canister that allows incrementing and decrementing a counter. + +Inter Canister Communication: + +- [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/): A canister that allows incrementing and decrementing a counter + that is stored in separate canisters that are created on demand. + +Inbound HTTP Server: + +- [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. diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml index 7ad73c8..4e2de7c 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" } + [[bin]] name = "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() {} diff --git a/examples/pastebin/Cargo.toml b/examples/pastebin/Cargo.toml new file mode 100644 index 0000000..6bc9212 --- /dev/null +++ b/examples/pastebin/Cargo.toml @@ -0,0 +1,15 @@ +[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"]} +tinytemplate = "1.1" +serde = { version = "1.0", features = ["derive"] } + +[[bin]] +name = "ic_kit_example_pastebin" +path = "src/main.rs" 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 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..5ee2e3f --- /dev/null +++ b/examples/pastebin/src/canister.rs @@ -0,0 +1,115 @@ +use std::collections::HashMap; + +use serde::Serialize; +use tinytemplate::TinyTemplate; + +use ic_kit::prelude::*; + +pub type Data = HashMap>; + +#[derive(Serialize)] +struct HtmlContext { + manpage: String, +} + +#[derive(Serialize)] +struct ManpageContext { + url: String, +} + +static HTML_TEMPLATE: &str = r#" + + + + IC Pastebin + + + +
+            
+{manpage}
+            
+        
+ + +"#; + +static MANPAGE_TEMPLATE: &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 {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", 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") { + if ua.starts_with("curl") || ua.starts_with("wget") { + return HttpResponse::ok().body(manpage); + } + } + + tt.add_template("html", HTML_TEMPLATE).unwrap(); + let html = tt.render("html", &HtmlContext { manpage }).unwrap(); + + HttpResponse::ok().body(html) +} + +/// Get paste handler +#[get(route = "/:file")] +fn get_file_handler(data: &Data, _: HttpRequest, p: Params) -> HttpResponse { + let file = p.get("file").unwrap(); + match data.get(file) { + Some(content) => HttpResponse::ok().body(content.clone()), + None => HttpResponse::new(404).body(format!("file not found `{}`\n", file)), + } +} + +/// Upload paste handler +#[put(route = "/:file", upgrade = true)] +fn put_file_handler(data: &mut Data, req: HttpRequest, p: Params) -> HttpResponse { + let filename = p.get("file").unwrap(); + let host = req.header("host").unwrap_or("unknown"); + + let res = format!("{}/{}", host, filename); + + data.insert(filename.to_string(), req.body); + + HttpResponse::ok().body(res) +} + +#[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-certified/Cargo.toml b/ic-kit-certified/Cargo.toml index 1fc194c..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 "] @@ -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 new file mode 100644 index 0000000..c2be680 --- /dev/null +++ b/ic-kit-http/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ic-kit-http" +version = "0.1.0-alpha.2" +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"], version = "0.2.0-alpha.0" } +candid = "0.8" +serde = "1.0" +matchit = "0.6.0" +lazy_static = "1.4" 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..b806f8c --- /dev/null +++ b/ic-kit-http/src/lib.rs @@ -0,0 +1,304 @@ +//! 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}; +/// 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, + 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() + .find(|(key, _)| key.to_lowercase() == name.to_lowercase()) + .map(|(_, value)| value.as_str()) + } +} + +/// # 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, + pub headers: Vec, + pub body: Vec, + pub streaming_strategy: Option, + pub upgrade: bool, +} + +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, + headers: vec![], + body: vec![], + streaming_strategy: None, + upgrade: false, + } + } + + /// 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, + headers: vec![], + body: vec![], + streaming_strategy: None, + upgrade: false, + } + } + + /// 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 + } + + /// 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 + } + + /// 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 + } + + /// 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 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, + pub content_encoding: String, + 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 { + callback: Func, + token: StreamingCallbackToken, + }, +} + +/// # 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, + pub token: Option, +} diff --git a/ic-kit-macros/Cargo.toml b/ic-kit-macros/Cargo.toml index b6de7aa..222a51c 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" @@ -24,5 +24,10 @@ serde_tokenstream = "0.1" lazy_static = "1.4" compile-time-run = "0.2.12" +[features] +default = [] +http = [] +experimental-http = [] + [lib] proc-macro = true 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 +} diff --git a/ic-kit-macros/src/entry.rs b/ic-kit-macros/src/entry.rs index 93921cc..0f76347 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, wrap}; +use crate::export_service::declare; + #[derive(Copy, Clone, PartialOrd, PartialEq, Ord, Eq)] pub enum EntryPoint { Init, @@ -37,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) } } @@ -170,15 +167,16 @@ pub fn gen_entry_point_code( }; // Build the outer function's body. - let tmp = di(collect_args(entry_point, 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 p_args = di( + collect_args(entry_point.to_string().as_str(), signature)?, + is_async, + )?; + 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. - let arg_decode = if can_args.len() == 0 { + let arg_decode = if can_args.is_empty() { quote! {} } else { quote! { @@ -223,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 { @@ -303,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 } @@ -313,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 } @@ -324,88 +292,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 83d3c7a..c5d3107 100644 --- a/ic-kit-macros/src/export_service.rs +++ b/ic-kit-macros/src/export_service.rs @@ -90,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(); @@ -221,6 +221,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(), + }; + let metadata = generate_metadata(); quote! { @@ -254,6 +263,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..2703d41 --- /dev/null +++ b/ic-kit-macros/src/http.rs @@ -0,0 +1,292 @@ +use std::collections::HashSet; +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 syn::{spanned::Spanned, Error}; + +use crate::di::{collect_args, di}; + +struct Handler { + name: String, + route: String, + method: String, + upgrade: bool, +} + +lazy_static! { + static ref HANDLERS: Mutex> = Mutex::new(Vec::new()); +} + +#[derive(Deserialize)] +struct Config { + route: String, + upgrade: Option, +} + +/// 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 upgrade = attrs.upgrade.unwrap_or(false); + 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 is_async = sig.asyncness.is_some(); + let stmts = fun.block.stmts; + + HANDLERS.lock().unwrap().push(Handler { + name: sig.ident.to_string(), + route: attrs.route, + method: method.into(), + 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 { + inner.extend(quote!(#stmt)); + } + + let result = crate::di::wrap(inner, args); + + Ok(quote! { + fn #ident(#(#can_args: #can_types),*) #output { + #result + } + }) +} + +pub fn gen_http_request_code() -> TokenStream { + let routes = HANDLERS.lock().unwrap(); + + 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 + 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("GET", "/*p", (index, false)); + }; + + router_types.insert("get"); + } else { + for Handler { + 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.insert(#method, #route, (#name, #upgrade)); + }); + + router_types.insert(method.as_str()); + } + } + + let mut upgrade_code = TokenStream::new(); + let mut query_code = TokenStream::new(); + + 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) + }); + }; + + 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 { + #router_fields + } + + impl Default for Router { + fn default() -> Self { + let mut router = Self { + #router_default + }; + #routes_insert + router + } + } + + impl Router { + pub fn insert(&mut self, method: &str, path: &str, handler: HandlerFn) { + match method { + #router_insert + _ => panic!("unsupported method: {}", method), + }; + } + + pub fn at<'s: 'p, 'p>( + &'s self, + method: &str, + path: &'p str, + ) -> Result, MatchError> { + match method { + #router_ats + _ => Err(MatchError::NotFound), + } + } + } + + #[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.at(&req.method.clone(), &req.url.clone()) { + Ok(m) => { + #query_code + }, + 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); + }); + } + + #upgrade_code + } +} diff --git a/ic-kit-macros/src/lib.rs b/ic-kit-macros/src/lib.rs index d52db7b..0911da5 100644 --- a/ic-kit-macros/src/lib.rs +++ b/ic-kit-macros/src/lib.rs @@ -7,6 +7,11 @@ use test::gen_test_code; mod entry; mod export_service; + +#[cfg(feature = "http")] +mod http; + +mod di; mod metadata; mod test; @@ -95,3 +100,155 @@ fn get_save_candid_path(input: &syn::DeriveInput) -> syn::Result Ok(None), } } + +/// Export a function as a HTTP GET 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 +/// // 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 { + 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. +/// +/// 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 { + 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. +/// +/// 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 { + 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. +/// +/// 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 { + 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. 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()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +/// 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()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} + +/// 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()) + .unwrap_or_else(|error| error.to_compile_error()) + .into() +} diff --git a/ic-kit-macros/src/metadata.rs b/ic-kit-macros/src/metadata.rs index 78d9457..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", "-e", "build"), - ); + let cdk = generate_static_string("CDK_VERSION", "0.5.0-alpha"); let compiler = generate_static_string( "COMPILER", 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", "Ossian Mapes "] description = "IC-Kit's Canister Simulator" diff --git a/ic-kit-runtime/src/replica.rs b/ic-kit-runtime/src/replica.rs index 021912c..3d756d7 100644 --- a/ic-kit-runtime/src/replica.rs +++ b/ic-kit-runtime/src/replica.rs @@ -15,7 +15,6 @@ use std::collections::HashMap; use std::future::Future; -use std::panic::{RefUnwindSafe, UnwindSafe}; use candid::Principal; use tokio::sync::{mpsc, oneshot}; @@ -181,9 +180,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-runtime/src/types.rs b/ic-kit-runtime/src/types.rs index 772d7b9..0804deb 100644 --- a/ic-kit-runtime/src/types.rs +++ b/ic-kit-runtime/src/types.rs @@ -2,9 +2,7 @@ use std::panic::{RefUnwindSafe, UnwindSafe}; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; -use candid::utils::ArgumentEncoder; -use candid::Principal; -use candid::{encode_args, encode_one, CandidType}; +use candid::{encode_args, encode_one, utils::ArgumentEncoder, CandidType, Principal}; use ic_kit_sys::types::{RejectionCode, CANDID_EMPTY_ARG}; diff --git a/ic-kit-stable/Cargo.toml b/ic-kit-stable/Cargo.toml index 58cb34e..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." @@ -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-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-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 1a4fcc6..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.4" +version = "0.5.0-alpha.6" description = "Testable Canister Developer Kit for the Internet Computer." authors = ["Parsa Ghadimi ", "Ossian Mapes "] edition = "2018" @@ -14,14 +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-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" [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"] diff --git a/ic-kit/src/lib.rs b/ic-kit/src/lib.rs index ebe518d..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,35 +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; - /// 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::*; - - #[cfg(not(target_family = "wasm"))] - pub use ic_kit_runtime as rt; - + 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 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::*}; } 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"]