├── examples ├── databases │ ├── postgres-diesel │ │ ├── migrations │ │ │ ├── .keep │ │ │ ├── 2023-10-25-125915_create_todos │ │ │ │ ├── down.sql │ │ │ │ └── up.sql │ │ │ └── 00000000000000_diesel_initial_setup │ │ │ │ ├── down.sql │ │ │ │ └── up.sql │ │ ├── schema.rs │ │ ├── diesel.toml │ │ ├── Cargo.toml │ │ ├── models.rs │ │ ├── README.md │ │ └── serve.rs │ ├── postgres-seaorm │ │ ├── entities │ │ │ ├── mod.rs │ │ │ ├── prelude.rs │ │ │ └── todos.rs │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── migrator.rs │ │ └── serve.rs │ ├── redis-driver │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src │ │ │ └── main.rs │ ├── sqlite-turbosql │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src │ │ │ └── main.rs │ ├── sqlite-sqlx │ │ ├── build.rs │ │ ├── migrations │ │ │ └── 20220718111257_todos.sql │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── serve.rs │ └── mongo-driver │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src │ │ └── main.rs ├── todo │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── todo-pwa │ ├── build.rs │ ├── Cargo.toml │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── README.md ├── todo-pwa-auth │ ├── build.rs │ ├── Cargo.toml │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── README.md ├── bench │ ├── README.md │ └── Cargo.toml ├── todo-pwa-auth-sync │ ├── build.rs │ ├── Cargo.toml │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── README.md ├── blog │ ├── build.rs │ ├── package.json │ ├── src │ │ ├── prism.ts │ │ ├── icons │ │ │ ├── medium.svg │ │ │ ├── email.svg │ │ │ ├── docs.svg │ │ │ ├── admin.svg │ │ │ ├── github.svg │ │ │ └── twitter.svg │ │ └── main.rs │ ├── README.md │ └── Cargo.toml ├── polkadot │ ├── runtime │ │ ├── build.rs │ │ └── Cargo.toml │ ├── host │ │ ├── build.rs │ │ ├── main.rs │ │ └── Cargo.toml │ └── README.md ├── scraping │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── llm-mistral │ ├── Cargo.toml │ ├── README.md │ ├── serve.rs │ └── llm.rs └── solana │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── .gitattributes ├── ui └── favicon.ico ├── cotton ├── src │ ├── main.rs │ ├── progress.rs │ ├── cache.rs │ ├── config.rs │ └── lib.rs └── Cargo.toml ├── .gitignore ├── serde_derive_fork ├── build.rs ├── src │ ├── internals │ │ ├── respan.rs │ │ ├── mod.rs │ │ ├── ctxt.rs │ │ ├── symbol.rs │ │ └── name.rs │ ├── dummy.rs │ ├── this.rs │ ├── fragment.rs │ └── lib.rs └── Cargo.toml ├── package.json ├── embed ├── utils │ └── Cargo.toml └── macro │ └── Cargo.toml ├── host ├── admin │ ├── assets │ │ ├── analytics.svg │ │ ├── logs.svg │ │ ├── admin.svg │ │ ├── db.svg │ │ └── loader.svg │ ├── logs.rs │ ├── analytics.rs │ ├── schedule.rs │ ├── mod.rs │ └── db.rs ├── release-builder.Dockerfile ├── webview.rs ├── auth │ ├── permissions.rs │ ├── session.rs │ ├── authn.rs │ └── google_openid.rs ├── state.rs ├── db │ ├── transaction.rs │ └── snapshot.rs ├── sse.rs ├── docker.rs └── server.rs ├── .env.example ├── init └── Cargo.toml ├── db ├── macro │ ├── Cargo.toml │ ├── analyze.rs │ ├── from_glue_value.rs │ └── into_glue_expr.rs └── key.rs ├── html ├── macro │ ├── Cargo.toml │ ├── escape.rs │ ├── lib.rs │ └── htmx.rs ├── escape.rs └── htmx.rs ├── service_worker └── state.rs ├── .cursor └── rules │ └── general.mdc ├── vals.rs ├── docs ├── TS_VS_RUST_FRONTEND.md └── WHATS_NEXT.md ├── config.rs └── lib.rs /examples/databases/postgres-diesel/migrations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ui/preset.css linguist-vendored 2 | ui/htmx/** linguist-vendored -------------------------------------------------------------------------------- /ui/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edezhic/prest/HEAD/ui/favicon.ico -------------------------------------------------------------------------------- /cotton/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | cotton_install::run().unwrap(); 3 | } 4 | -------------------------------------------------------------------------------- /examples/databases/postgres-diesel/migrations/2023-10-25-125915_create_todos/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE todos -------------------------------------------------------------------------------- /examples/todo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todo" 3 | edition = "2021" 4 | 5 | [dependencies] 6 | prest = { version = "0.5", path = "../../" } -------------------------------------------------------------------------------- /examples/todo-pwa/build.rs: -------------------------------------------------------------------------------- 1 | use prest_build::*; 2 | fn main() { 3 | default_cfg_aliases(); 4 | build_pwa(PWAOptions::new()).unwrap(); 5 | } 6 | -------------------------------------------------------------------------------- /examples/todo-pwa-auth/build.rs: -------------------------------------------------------------------------------- 1 | use prest_build::*; 2 | fn main() { 3 | default_cfg_aliases(); 4 | build_pwa(PWAOptions::new()).unwrap(); 5 | } 6 | -------------------------------------------------------------------------------- /examples/bench/README.md: -------------------------------------------------------------------------------- 1 | DB load testing script which runs tons of queries concurrently. Configured by a bunch of const values at the top: 2 | 3 | {src/main.rs} -------------------------------------------------------------------------------- /examples/databases/postgres-seaorm/entities/mod.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4 2 | 3 | pub mod prelude; 4 | 5 | pub mod todos; 6 | -------------------------------------------------------------------------------- /examples/databases/postgres-seaorm/entities/prelude.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4 2 | 3 | pub use super::todos::Entity as Todos; 4 | -------------------------------------------------------------------------------- /examples/databases/redis-driver/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redis-driver" 3 | edition = "2021" 4 | 5 | [dependencies] 6 | prest = "0.5" 7 | redis = "0.23.3" -------------------------------------------------------------------------------- /examples/todo-pwa-auth-sync/build.rs: -------------------------------------------------------------------------------- 1 | use prest_build::*; 2 | fn main() { 3 | default_cfg_aliases(); 4 | build_pwa(PWAOptions::new()).unwrap(); 5 | } 6 | -------------------------------------------------------------------------------- /examples/blog/build.rs: -------------------------------------------------------------------------------- 1 | use prest_build::*; 2 | fn main() { 3 | default_cfg_aliases(); 4 | bundle_ts(); 5 | build_pwa(PWAOptions::new()).unwrap(); 6 | } 7 | -------------------------------------------------------------------------------- /examples/databases/sqlite-turbosql/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sqlite-turbosql" 3 | edition = "2021" 4 | 5 | [dependencies] 6 | prest = "0.5" 7 | turbosql = "0.9" -------------------------------------------------------------------------------- /examples/blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "exports": { 3 | "prism": "./src/prism.ts" 4 | }, 5 | "dependencies": { 6 | "prismjs": "^1.29.0" 7 | } 8 | } -------------------------------------------------------------------------------- /examples/polkadot/runtime/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "std")] 3 | { 4 | polkadot_sdk::substrate_wasm_builder::WasmBuilder::build_using_defaults(); 5 | } 6 | } -------------------------------------------------------------------------------- /examples/blog/src/prism.ts: -------------------------------------------------------------------------------- 1 | import "prismjs"; 2 | import "prismjs/components/prism-rust"; 3 | import "prismjs/components/prism-toml"; 4 | import "prismjs/components/prism-bash"; 5 | -------------------------------------------------------------------------------- /examples/databases/sqlite-sqlx/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // https://docs.rs/sqlx/latest/sqlx/macro.migrate.html 3 | println!("cargo:rerun-if-changed=migrations"); 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | cert.pem 3 | key.pem 4 | Cargo.lock 5 | target 6 | prest_storage 7 | .DS_Store 8 | migrations.toml 9 | node_modules 10 | bun.lock 11 | bun.lockb 12 | cotton.lock 13 | .cotton -------------------------------------------------------------------------------- /examples/databases/postgres-diesel/migrations/2023-10-25-125915_create_todos/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE todos ( 2 | uuid uuid PRIMARY KEY, 3 | task TEXT NOT NULL, 4 | done BOOLEAN NOT NULL DEFAULT FALSE 5 | ) -------------------------------------------------------------------------------- /examples/databases/mongo-driver/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mongo-driver" 3 | edition = "2021" 4 | 5 | [dependencies] 6 | prest = "0.5" 7 | mongodb = { version = "2.7.0", features = ["bson-uuid-1"] } 8 | -------------------------------------------------------------------------------- /examples/polkadot/host/build.rs: -------------------------------------------------------------------------------- 1 | use polkadot_sdk::substrate_build_script_utils::{generate_cargo_keys, rerun_if_git_head_changed}; 2 | 3 | fn main() { 4 | generate_cargo_keys(); 5 | rerun_if_git_head_changed(); 6 | } -------------------------------------------------------------------------------- /examples/bench/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bench" 3 | edition = "2021" 4 | 5 | [dependencies] 6 | prest = { path = "../../", features = ["experimental"] } 7 | fastrand = "2" 8 | 9 | [package.metadata] 10 | domain = "prest.blog" 11 | -------------------------------------------------------------------------------- /examples/databases/postgres-diesel/schema.rs: -------------------------------------------------------------------------------- 1 | // @generated automatically by Diesel CLI. 2 | 3 | diesel::table! { 4 | todos (uuid) { 5 | uuid -> Uuid, 6 | task -> Text, 7 | done -> Bool, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/scraping/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scraping" 3 | edition = "2021" 4 | 5 | [dependencies] 6 | prest = { version = "0.5", path = "../../" } 7 | reqwest = { version = "0.12", default-features=false, features = ["rustls-tls"] } 8 | scraper = "0.22" 9 | -------------------------------------------------------------------------------- /examples/databases/sqlite-sqlx/migrations/20220718111257_todos.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS todos 2 | ( 3 | uuid TEXT PRIMARY KEY NOT NULL, 4 | task TEXT NOT NULL, 5 | done BOOLEAN NOT NULL DEFAULT 0 6 | ); -------------------------------------------------------------------------------- /examples/todo-pwa/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todo-pwa" 3 | edition = "2021" 4 | 5 | [dependencies] 6 | prest = { version = "0.5", path = "../../" } 7 | wasm-bindgen = "0.2" 8 | 9 | [build-dependencies] 10 | prest-build = { version = "0.4", path = "../../build" } 11 | 12 | -------------------------------------------------------------------------------- /examples/blog/src/icons/medium.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /examples/databases/sqlite-sqlx/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sqlite-sqlx" 3 | edition = "2021" 4 | 5 | [[bin]] 6 | name = "serve" 7 | path = "./serve.rs" 8 | 9 | [dependencies] 10 | prest = "0.5" 11 | sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-rustls", "sqlite", "macros" ] } 12 | -------------------------------------------------------------------------------- /examples/llm-mistral/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "llm-mistral" 3 | edition = "2021" 4 | 5 | [[bin]] 6 | name = "serve" 7 | path = "./serve.rs" 8 | 9 | [dependencies] 10 | prest = "0.5" 11 | 12 | hf-hub = "0.3" 13 | tokenizers = "0.15" 14 | candle-core = "0.3" 15 | candle-transformers = "0.3" 16 | -------------------------------------------------------------------------------- /examples/todo-pwa-auth/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todo-pwa-auth" 3 | edition = "2021" 4 | 5 | [dependencies] 6 | prest = { version = "0.5", features = ["auth"], path = "../../" } 7 | wasm-bindgen = "0.2" 8 | 9 | [build-dependencies] 10 | prest-build = { version = "0.4", path = "../../build" } 11 | -------------------------------------------------------------------------------- /examples/todo-pwa-auth-sync/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todo-pwa-auth-sync" 3 | edition = "2021" 4 | 5 | [dependencies] 6 | prest = { version = "0.5", features = ["auth"], path = "../../" } 7 | wasm-bindgen = "0.2" 8 | 9 | [build-dependencies] 10 | prest-build = { version = "0.4", path = "../../build" } 11 | -------------------------------------------------------------------------------- /serde_derive_fork/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // Warning: build.rs is not published to crates.io. 3 | 4 | println!("cargo:rerun-if-changed=build.rs"); 5 | println!("cargo:rustc-cfg=check_cfg"); 6 | println!("cargo:rustc-check-cfg=cfg(check_cfg)"); 7 | println!("cargo:rustc-check-cfg=cfg(exhaustive)"); 8 | } 9 | -------------------------------------------------------------------------------- /examples/blog/README.md: -------------------------------------------------------------------------------- 1 | Built from repo's markdown files as a proof-of-concept for prest and maintained to prettify documentation. Served at [prest.blog](https://prest.blog). Here goes all it's source code: 2 | 3 | {Cargo.toml} 4 | 5 | {build.rs} 6 | 7 | {src/main.rs} 8 | 9 | {src/lib.rs} 10 | 11 | {src/content.rs} 12 | -------------------------------------------------------------------------------- /examples/databases/postgres-diesel/diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see https://diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "./schema.rs" 6 | custom_type_derives = ["diesel::query_builder::QueryId"] 7 | 8 | [migrations_directory] 9 | dir = "./migrations" 10 | -------------------------------------------------------------------------------- /examples/databases/postgres-diesel/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "postgres-diesel" 3 | edition = "2021" 4 | 5 | [[bin]] 6 | name = "serve" 7 | path = "./serve.rs" 8 | 9 | [dependencies] 10 | prest = "0.5" 11 | diesel = { version = "2.1.0", features = ["uuid"] } 12 | diesel-async = { version = "0.3.1", features = ["deadpool", "postgres"] } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "exports": { 3 | "preset": "./ui/preset.ts", 4 | "stats": "./ui/stats.tsx", 5 | "traces": "./ui/traces.tsx", 6 | "db": "./ui/db.tsx" 7 | }, 8 | "dependencies": { 9 | "chart.js": "^4.4.7", 10 | "htmx.org": "2.0.6", 11 | "hyperscript.org": "^0.9.14", 12 | "preact": "^10.25.4" 13 | } 14 | } -------------------------------------------------------------------------------- /embed/utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "prest-embed-utils" 3 | version = "0.2.0" 4 | edition = "2021" 5 | description = "fork of embed utils from rust-embed" 6 | license = "MIT OR Apache-2.0" 7 | 8 | [lib] 9 | path = "lib.rs" 10 | 11 | [dependencies] 12 | walkdir = "2.3.1" 13 | sha2 = "0.10.5" 14 | mime_guess = "2.0.4" 15 | globset = "0.4.8" 16 | -------------------------------------------------------------------------------- /examples/databases/postgres-seaorm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "postgres-seaorm" 3 | edition = "2021" 4 | 5 | [[bin]] 6 | name = "serve" 7 | path = "./serve.rs" 8 | 9 | [dependencies] 10 | prest = "0.5" 11 | sea-orm = { version = "0.12", features = [ "sqlx-postgres", "runtime-tokio-rustls", "macros", "with-uuid"] } 12 | sea-orm-migration = "0.12" 13 | -------------------------------------------------------------------------------- /host/admin/assets/analytics.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /examples/blog/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blog" 3 | edition = "2021" 4 | 5 | [dependencies] 6 | prest = { version = "0.5.1", path = "../../" } 7 | wasm-bindgen = "0.2" 8 | markdown = "1.0.0-alpha.16" 9 | toml = "0.8.8" 10 | 11 | [build-dependencies] 12 | prest-build = { version = "0.4", path = "../../build", features = ["typescript", "sass"] } 13 | 14 | [package.metadata] 15 | domain = "prest.blog" 16 | -------------------------------------------------------------------------------- /examples/databases/postgres-diesel/migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # (optional) port that will be used to serve requests; defaults to 80 2 | PORT=80 3 | # (optional) for PWA builds in debug mode. Use "release" or no value to build only when cfg(not(debug_assertions)) 4 | PWA=debug 5 | # (optional) google openid auth 6 | GOOGLE_CLIENT_ID=XXX 7 | GOOGLE_CLIENT_SECRET=XXX 8 | # (optional) host credentials 9 | SSH_ADDR=123.232.111.222 10 | SSH_USER=root 11 | SSH_PASSWORD=verystrongpassword -------------------------------------------------------------------------------- /examples/databases/redis-driver/README.md: -------------------------------------------------------------------------------- 1 | Minimalistic todo app powered by the [redis client](https://github.com/redis-rs/redis-rs). Created just to showcase how to connect to a redis instance from rust and use that connection in handlers. But overall redis is not designed for this type of apps at all. To get it started locally you can use the official redis docker image: `docker run -p 6379:6379 -d redis:latest` 2 | 3 | {Cargo.toml} 4 | 5 | {src/main.rs} -------------------------------------------------------------------------------- /examples/databases/mongo-driver/README.md: -------------------------------------------------------------------------------- 1 | Minimalistic todo app powered by the [official rust mongo driver](https://github.com/mongodb/mongo-rust-driver). To get it running you can use the official mongo docker container: `docker run -p 27017:27017 -d mongo:latest`. In general working with mongo in rust is fairly straightforward thanks to the integration with serde's auto (de)serialization tools and driver's utility macros: 2 | 3 | {Cargo.toml} 4 | 5 | {src/main.rs} -------------------------------------------------------------------------------- /init/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "prest-init-macro" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "prest init macro" 6 | license = "MIT OR Apache-2.0" 7 | authors = ["Egor Dezhic "] 8 | 9 | [lib] 10 | path = "lib.rs" 11 | proc-macro = true 12 | 13 | [dependencies] 14 | syn = { version = "2", features = ["derive", "parsing", "proc-macro", "printing", "full"] } 15 | quote = "1" 16 | proc-macro2 = "1" 17 | toml = "0.8" 18 | -------------------------------------------------------------------------------- /examples/blog/src/icons/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/databases/sqlite-sqlx/README.md: -------------------------------------------------------------------------------- 1 | Minimalistic todo app powered by [SQLx](https://github.com/launchbadge/sqlx)-based connection to the [SQLite](https://www.sqlite.org/index.html) DB. The core feature of sqlx is that it's macros can run queries during the build time to test their overall correctness. Also, it's a pretty good choice if you prefer good old sql. 2 | 3 | {Cargo.toml} 4 | 5 | {build.rs} 6 | 7 | {migrations/20220718111257_todos.sql} 8 | 9 | {serve.rs} -------------------------------------------------------------------------------- /examples/databases/postgres-diesel/models.rs: -------------------------------------------------------------------------------- 1 | use diesel::{pg::Pg, prelude::*}; 2 | use prest::{Deserialize, Serialize, Uuid}; 3 | 4 | #[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)] 5 | #[diesel(table_name = crate::schema::todos)] 6 | #[diesel(check_for_backend(Pg))] 7 | pub struct Todo { 8 | #[serde(default = "Uuid::now_v7")] 9 | pub uuid: Uuid, 10 | #[serde(default)] 11 | pub task: String, 12 | #[serde(default)] 13 | pub done: bool, 14 | } 15 | -------------------------------------------------------------------------------- /db/macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "prest-db-macro" 3 | version = "0.4.0" 4 | edition = "2021" 5 | authors = ["edezhic@gmail.com"] 6 | description = "macro that derives Storage trait to work with gluesql db" 7 | license = "MIT OR Apache-2.0" 8 | 9 | [lib] 10 | path = "lib.rs" 11 | proc-macro = true 12 | 13 | [dependencies] 14 | syn = { version = "2", default-features = false, features = ["derive", "parsing", "proc-macro", "printing"] } 15 | quote = "1" 16 | proc-macro2 = "1" 17 | gluesql-core = "0.16.3" -------------------------------------------------------------------------------- /host/release-builder.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.88.0 2 | ARG ZIG_VERSION=0.10.1 3 | 4 | # Install Zig 5 | RUN curl -L "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-$(uname -m)-${ZIG_VERSION}.tar.xz" | tar -J -x -C /usr/local && \ 6 | ln -s "/usr/local/zig-linux-$(uname -m)-${ZIG_VERSION}/zig" /usr/local/bin/zig 7 | 8 | # Install Rust targets 9 | RUN rustup target add x86_64-unknown-linux-musl 10 | RUN rustup target add wasm32-unknown-unknown 11 | 12 | # Install cargo-zigbuild 13 | RUN cargo install cargo-zigbuild 14 | -------------------------------------------------------------------------------- /examples/databases/sqlite-turbosql/README.md: -------------------------------------------------------------------------------- 1 | Showcasing probably the easiest library that allows using local sqlite instance with just a couple of lines of code - [turbosql](https://github.com/trevyn/turbosql). Ideaologically similar to the GlueSQL integration and the `Storage` macro of prest. All you need to get started is to derive `Turbosql` on the struct that you want to use as a table and make sure that the struct's types are compatible (all columns are optional, first goes the rowid with i64 and then others): 2 | 3 | {Cargo.toml} 4 | 5 | {src/main.rs} -------------------------------------------------------------------------------- /serde_derive_fork/src/internals/respan.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Group, Span, TokenStream, TokenTree}; 2 | 3 | pub(crate) fn respan(stream: TokenStream, span: Span) -> TokenStream { 4 | stream 5 | .into_iter() 6 | .map(|token| respan_token(token, span)) 7 | .collect() 8 | } 9 | 10 | fn respan_token(mut token: TokenTree, span: Span) -> TokenTree { 11 | if let TokenTree::Group(g) = &mut token { 12 | *g = Group::new(g.delimiter(), respan(g.stream(), span)); 13 | } 14 | token.set_span(span); 15 | token 16 | } 17 | -------------------------------------------------------------------------------- /serde_derive_fork/src/internals/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ast; 2 | pub mod attr; 3 | pub mod name; 4 | 5 | mod case; 6 | mod check; 7 | mod ctxt; 8 | mod receiver; 9 | mod respan; 10 | mod symbol; 11 | 12 | use syn::Type; 13 | 14 | pub use self::ctxt::Ctxt; 15 | pub use self::receiver::replace_receiver; 16 | 17 | #[derive(Copy, Clone)] 18 | pub enum Derive { 19 | Serialize, 20 | Deserialize, 21 | } 22 | 23 | pub fn ungroup(mut ty: &Type) -> &Type { 24 | while let Type::Group(group) = ty { 25 | ty = &group.elem; 26 | } 27 | ty 28 | } 29 | -------------------------------------------------------------------------------- /host/admin/assets/logs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/databases/postgres-seaorm/entities/todos.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4 2 | 3 | use prest::*; 4 | use sea_orm::entity::prelude::*; 5 | 6 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] 7 | #[sea_orm(table_name = "todos")] 8 | pub struct Model { 9 | #[sea_orm(primary_key, auto_increment = false)] 10 | pub uuid: Uuid, 11 | pub task: String, 12 | pub done: bool, 13 | } 14 | 15 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 16 | pub enum Relation {} 17 | 18 | impl ActiveModelBehavior for ActiveModel {} 19 | -------------------------------------------------------------------------------- /embed/macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "prest-embed-macro" 3 | version = "0.3.0" 4 | edition = "2021" 5 | description = "fork of embed macro from rust-embed" 6 | license = "MIT OR Apache-2.0" 7 | authors = ["pyros2097 ", "Egor Dezhic "] 8 | 9 | [lib] 10 | path = "lib.rs" 11 | proc-macro = true 12 | 13 | [dependencies] 14 | prest-embed-utils = { path = "../utils", version = "0.2" } 15 | syn = { version = "2", default-features = false, features = ["derive", "parsing", "proc-macro", "printing"] } 16 | quote = "1" 17 | proc-macro2 = "1" 18 | walkdir = "2.3.1" 19 | shellexpand = "3" 20 | -------------------------------------------------------------------------------- /examples/scraping/README.md: -------------------------------------------------------------------------------- 1 | Simple [scraper](https://github.com/causal-agent/scraper-based)-based parser that collects posts from [AP News](https://apnews.com). Beside scraper it uses [reqwest](https://github.com/seanmonstar/reqwest) which is a standard option in tokio ecosystem to make requests: 2 | 3 | {Cargo.toml} 4 | 5 | This example spawns the `scrape` function which fetches the provided url, extracts links to other pages from it (but using only 5 of them later to limit the load), then using `join_all` function from the `futures` crate to get all these pages concurrently, then awaits their bodies, then extracts titles and contents and saves the results: 6 | 7 | {src/main.rs} -------------------------------------------------------------------------------- /examples/polkadot/README.md: -------------------------------------------------------------------------------- 1 | !Currently broken! 2 | 3 | This example is aimed to create a full-stack todo app with a built-in polkadot-based blockchain as a database. Such database can be easily customizable, contain logic written also in Rust, has strong consistency guarantees, easily replicable and optionally publicly verifiable. These features can be interesting for mission-critical data including finances etc. As of now even code copypasted from polkadot-sdk repo produces compilations errors and the amount of required apis for a minimal example is relatively huge so I've set it aside until things improve. Also, no real work on the client side has been done and subxt is probably the best option. -------------------------------------------------------------------------------- /serde_derive_fork/src/dummy.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | pub fn wrap_in_const(serde_path: Option<&syn::Path>, code: TokenStream) -> TokenStream { 5 | let use_serde = match serde_path { 6 | Some(path) => quote! { 7 | use #path as _serde; 8 | }, 9 | None => quote! { 10 | #[allow(unused_extern_crates, clippy::useless_attribute)] 11 | extern crate serde as _serde; 12 | }, 13 | }; 14 | 15 | quote! { 16 | #[doc(hidden)] 17 | #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)] 18 | const _: () = { 19 | #use_serde 20 | #code 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /html/macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "prest-html-macro" 3 | version = "0.3.0" 4 | authors = ["Chris Wong ", "Egor Dezhic "] 5 | license = "MIT/Apache-2.0" 6 | documentation = "https://docs.rs/maud_macros/" 7 | homepage = "https://maud.lambda.xyz/" 8 | repository = "https://github.com/lambda-fairy/maud" 9 | description = "Compile-time HTML templates." 10 | edition = "2021" 11 | keywords = ["maud", "prest", "tailwind"] 12 | 13 | #[workspace] 14 | 15 | [dependencies] 16 | syn = "2" 17 | quote = "1.0.7" 18 | proc-macro2 = "1.0.23" 19 | proc-macro-error = { version = "1.0.0", default-features = false } 20 | lazy_static = "1.4.0" 21 | rand = "0.8" 22 | hex = "0.4" 23 | regex = "1" 24 | 25 | [lib] 26 | path = "lib.rs" 27 | proc-macro = true 28 | -------------------------------------------------------------------------------- /examples/blog/src/icons/docs.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /service_worker/state.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! state { 3 | ($struct_name:ident: $type:ty = $init:block) => { 4 | pub static $struct_name: prest::Lazy<$type> = prest::Lazy::new(|| { 5 | fn init() -> Result<$type, Box> { 6 | let v = { $init }; 7 | Ok(v) 8 | } 9 | init().unwrap() 10 | }); 11 | }; 12 | ($struct_name:ident: $type:ty = async $init:block) => { 13 | pub static $struct_name: prest::Lazy<$type> = prest::Lazy::new(|| { 14 | async fn init() -> Result<$type, Box> { 15 | let v = { $init }; 16 | Ok(v) 17 | } 18 | prest::block_on(init()).unwrap() 19 | }); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /examples/blog/src/main.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | 3 | embed_build_output_as!(BuiltAssets); 4 | 5 | #[derive(Debug, Storage, Default, Serialize, Deserialize)] 6 | struct Todo { 7 | pub id: Uuid, 8 | pub custom: Inner, 9 | pub done: bool, 10 | } 11 | 12 | #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] 13 | struct Inner { 14 | a: String, 15 | b: NaiveDateTime, 16 | } 17 | 18 | #[init] 19 | async fn main() -> Result { 20 | // example table with data to showcase in the admin panel 21 | Todo { 22 | id: Uuid::now_v7(), 23 | custom: Inner { 24 | a: "v5 release".to_owned(), 25 | b: Default::default(), 26 | }, 27 | done: false, 28 | } 29 | .save() 30 | .await?; 31 | 32 | blog::routes().embed(BuiltAssets).run().await 33 | } 34 | -------------------------------------------------------------------------------- /examples/todo-pwa/src/lib.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | 3 | pub fn shared_routes() -> Router { 4 | route("/", get(home)) 5 | } 6 | 7 | async fn home() -> Markup { 8 | into_page(html!( 9 | a get="/todos" trigger="load" push-url 10 | after-request="if (!event.detail.successful) { document.getElementById('error').style.display = 'flex'; this.remove() }" {} 11 | div #"error" style="display: none;" {"Couldn't connect to the server :("} 12 | )) 13 | .await 14 | } 15 | 16 | pub async fn into_page(content: Markup) -> Markup { 17 | html! { html { (Head::with_title("Todo PWA app")) 18 | body $"max-w-screen-sm px-8 mx-auto mt-12 flex flex-col items-center" { 19 | (content) 20 | (Scripts::default()) 21 | } 22 | }} 23 | } 24 | 25 | #[cfg(wasm)] 26 | #[wasm_bindgen(start)] 27 | pub fn main() { 28 | shared_routes().handle_fetch_events() 29 | } 30 | -------------------------------------------------------------------------------- /examples/todo-pwa-auth/src/lib.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | 3 | pub fn shared_routes() -> Router { 4 | route("/", get(home)) 5 | } 6 | 7 | async fn home() -> Markup { 8 | into_page(html!( 9 | a get="/todos" trigger="load" push-url 10 | after-request="if (!event.detail.successful) { document.getElementById('error').style.display = 'flex'; this.remove() }" {} 11 | div #"error" style="display: none;" {"Couldn't connect to the server :("} 12 | )) 13 | .await 14 | } 15 | 16 | pub async fn into_page(content: Markup) -> Markup { 17 | html! { html { (Head::with_title("Todo PWA app with auth")) 18 | body $"max-w-screen-sm mx-auto px-8 mt-12 flex flex-col items-center" { 19 | (content) 20 | (Scripts::default()) 21 | } 22 | }} 23 | } 24 | 25 | #[cfg(sw)] 26 | #[wasm_bindgen(start)] 27 | pub fn main() { 28 | shared_routes().handle_fetch_events() 29 | } 30 | -------------------------------------------------------------------------------- /examples/todo-pwa-auth-sync/src/lib.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | 3 | pub fn shared_routes() -> Router { 4 | route("/", get(home)) 5 | } 6 | 7 | async fn home() -> Markup { 8 | into_page(html!( 9 | a get="/todos" trigger="load" push-url 10 | after-request="if (!event.detail.successful) { document.getElementById('error').style.display = 'flex'; this.remove() }" {} 11 | div #"error" style="display: none;" {"Couldn't connect to the server :("} 12 | )) 13 | .await 14 | } 15 | 16 | pub async fn into_page(content: Markup) -> Markup { 17 | html! { html { (Head::with_title("Todo PWA app with auth and sync")) 18 | body $"max-w-screen-sm mx-auto px-8 mt-12 flex flex-col items-center" { 19 | (content) 20 | (Scripts::default()) 21 | } 22 | }} 23 | } 24 | 25 | #[cfg(sw)] 26 | #[wasm_bindgen(start)] 27 | pub fn main() { 28 | shared_routes().handle_fetch_events() 29 | } 30 | -------------------------------------------------------------------------------- /host/admin/assets/admin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /cotton/src/progress.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use indicatif::{ProgressBar, ProgressStyle}; 4 | use once_cell::sync::Lazy; 5 | use owo_colors::OwoColorize; 6 | 7 | pub static PROGRESS_BAR: Lazy = Lazy::new(|| { 8 | let pb = ProgressBar::new(0).with_style( 9 | ProgressStyle::with_template("{spinner:.blue} {wide_msg} +{pos:.green} ~{len:.magenta}") 10 | .unwrap() 11 | .progress_chars("#>-") 12 | .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"), 13 | ); 14 | pb.enable_steady_tick(Duration::from_millis(200)); 15 | pb 16 | }); 17 | 18 | pub fn log_verbose(text: &str) { 19 | // if ARGS.verbose { 20 | PROGRESS_BAR.suspend(|| println!("{} {}", " VERBOSE ".on_white(), text)); 21 | // } 22 | } 23 | 24 | pub fn log_warning(text: &str) { 25 | PROGRESS_BAR.suspend(|| println!("{} {}", " WARNING ".on_yellow(), text)); 26 | } 27 | 28 | pub fn log_progress(text: &str) { 29 | PROGRESS_BAR.set_message(text.to_string()); 30 | log_verbose(text); 31 | } 32 | -------------------------------------------------------------------------------- /examples/blog/src/icons/admin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /host/webview.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | use tao::{ 4 | dpi::LogicalSize, 5 | event::{Event, WindowEvent}, 6 | event_loop::{ControlFlow, EventLoop}, 7 | platform::macos::WindowBuilderExtMacOS, 8 | window::WindowBuilder, 9 | }; 10 | use wry::WebViewBuilder; 11 | 12 | pub fn init_webview(url: &str) -> Result { 13 | let size: LogicalSize = LogicalSize::from((1280., 720.)); 14 | let event_loop = EventLoop::new(); 15 | let window = WindowBuilder::new() 16 | .with_title_hidden(true) 17 | .with_inner_size(size) 18 | .build(&event_loop)?; 19 | let _webview = WebViewBuilder::new(&window) 20 | .with_devtools(true) 21 | .with_url(url)? 22 | .build()?; 23 | 24 | event_loop.run(move |event, _, control_flow| { 25 | *control_flow = ControlFlow::Wait; 26 | if let Event::WindowEvent { 27 | event: WindowEvent::CloseRequested, 28 | .. 29 | } = event 30 | { 31 | *control_flow = ControlFlow::Exit 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /html/escape.rs: -------------------------------------------------------------------------------- 1 | // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 2 | // !!!!! PLEASE KEEP THIS IN SYNC WITH `macro/escape.rs` !!!!! 3 | // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 4 | 5 | extern crate alloc; 6 | 7 | use alloc::string::String; 8 | 9 | pub fn escape_to_string(input: &str, output: &mut String) { 10 | for b in input.bytes() { 11 | match b { 12 | b'&' => output.push_str("&"), 13 | b'<' => output.push_str("<"), 14 | b'>' => output.push_str(">"), 15 | b'"' => output.push_str("""), 16 | _ => unsafe { output.as_mut_vec().push(b) }, 17 | } 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod test { 23 | extern crate alloc; 24 | 25 | use super::escape_to_string; 26 | use alloc::string::String; 27 | 28 | #[test] 29 | fn it_works() { 30 | let mut s = String::new(); 31 | escape_to_string("", &mut s); 32 | assert_eq!(s, "<script>launchMissiles()</script>"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /html/macro/escape.rs: -------------------------------------------------------------------------------- 1 | // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 2 | // !!!!!!!! PLEASE KEEP THIS IN SYNC WITH `maud/src/escape.rs` !!!!!!!!! 3 | // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 4 | 5 | extern crate alloc; 6 | 7 | use alloc::string::String; 8 | 9 | pub fn escape_to_string(input: &str, output: &mut String) { 10 | for b in input.bytes() { 11 | match b { 12 | b'&' => output.push_str("&"), 13 | b'<' => output.push_str("<"), 14 | b'>' => output.push_str(">"), 15 | b'"' => output.push_str("""), 16 | _ => unsafe { output.as_mut_vec().push(b) }, 17 | } 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod test { 23 | extern crate alloc; 24 | 25 | use super::escape_to_string; 26 | use alloc::string::String; 27 | 28 | #[test] 29 | fn it_works() { 30 | let mut s = String::new(); 31 | escape_to_string("", &mut s); 32 | assert_eq!(s, "<script>launchMissiles()</script>"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /serde_derive_fork/src/this.rs: -------------------------------------------------------------------------------- 1 | use crate::internals::ast::Container; 2 | use syn::{Path, PathArguments, Token}; 3 | 4 | pub fn this_type(cont: &Container) -> Path { 5 | if let Some(remote) = cont.attrs.remote() { 6 | let mut this = remote.clone(); 7 | for segment in &mut this.segments { 8 | if let PathArguments::AngleBracketed(arguments) = &mut segment.arguments { 9 | arguments.colon2_token = None; 10 | } 11 | } 12 | this 13 | } else { 14 | Path::from(cont.ident.clone()) 15 | } 16 | } 17 | 18 | pub fn this_value(cont: &Container) -> Path { 19 | if let Some(remote) = cont.attrs.remote() { 20 | let mut this = remote.clone(); 21 | for segment in &mut this.segments { 22 | if let PathArguments::AngleBracketed(arguments) = &mut segment.arguments { 23 | if arguments.colon2_token.is_none() { 24 | arguments.colon2_token = Some(Token![::](arguments.lt_token.span)); 25 | } 26 | } 27 | } 28 | this 29 | } else { 30 | Path::from(cont.ident.clone()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /host/admin/assets/db.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/databases/postgres-seaorm/README.md: -------------------------------------------------------------------------------- 1 | Minimalistic todo app powered by [SeaORM](https://www.sea-ql.org/SeaORM/)-based connection to Postgres. Seaorm is async-first, dynamic and includes powerful tools for testing. Also it supports [Seaography](https://www.sea-ql.org/SeaORM/docs/seaography/seaography-intro/) - library that can automatically build graphql endpoints from seaorm entities. Overall [SeaQL](https://www.sea-ql.org/) provides pretty much everything necessary to work with postgres, mysql and sqlite, and currently it is the main competitor of [diesel](https://prest.blog/postgres-diesel). 2 | 3 | To work with it you'll need the [sea-orm-cli](https://www.sea-ql.org/SeaORM/docs/generate-entity/sea-orm-cli/), running postgres instance and a connection string defined in `.env` or environment variables. Usually entities are generated using the cli from the database schema - you write migrations, run them, invoke cli and get the entities. A command to do that: 4 | 5 | (`cd examples/databases/postgres-seaorm`) 6 | `sea-orm-cli generate entity -u postgres://postgres:password@localhost/prest -o ./entities --with-serde both` 7 | 8 | Source code of the example: 9 | 10 | {Cargo.toml} 11 | 12 | {migrator.rs} 13 | 14 | {serve.rs} -------------------------------------------------------------------------------- /serde_derive_fork/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "prest-serde-derive-fork" 3 | version = "1.0.216" 4 | authors = ["Erick Tryzelaar ", "David Tolnay "] 5 | categories = ["no-std", "no-std::no-alloc"] 6 | description = "Macros 1.1 implementation of #[derive(Serialize, Deserialize)]" 7 | documentation = "https://serde.rs/derive.html" 8 | edition = "2015" 9 | exclude = ["build.rs"] 10 | homepage = "https://serde.rs" 11 | keywords = ["serde", "serialization", "no_std", "derive"] 12 | license = "MIT OR Apache-2.0" 13 | repository = "https://github.com/serde-rs/serde" 14 | rust-version = "1.61" 15 | 16 | [features] 17 | default = [] 18 | deserialize_in_place = [] 19 | 20 | [lib] 21 | name = "serde_derive" 22 | proc-macro = true 23 | 24 | [dependencies] 25 | proc-macro2 = { version = "1.0.74", default-features = false, features = ["proc-macro"] } 26 | quote = { version = "1.0.35", default-features = false, features = ["proc-macro"] } 27 | syn = { version = "2.0.46", default-features = false, features = ["clone-impls", "derive", "parsing", "printing", "proc-macro"] } 28 | 29 | [package.metadata.docs.rs] 30 | targets = ["x86_64-unknown-linux-gnu"] 31 | rustdoc-args = ["--generate-link-to-definition"] 32 | -------------------------------------------------------------------------------- /.cursor/rules/general.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | This is the PREST (Progressive REST) framework. It's a full-stack project aiming for development of full-cycle app development in Rust. It's most distinctive feature is cross-compilation of some of the code into both regular server-side endpoints and service worker capabilities to render static pages on the client side. Another major focus is batteries-included simplicity of development - it comes with a built-in (sled + gluesql)-based DB (`./db/`), templating (`./html`) based on HTMX with special abbreviations to make code cleaner and compilation of tailwind classes directly into in-page styles, npm package manager (`./cotton`) for FE scripts, build system (`./build`) for service workers, TypeScript scripts and SASS styles, file embedding for both host and SW (`./embed`), host tools (`./host`) for serving, logging, auth, admin panel, deployment and monitoring utilities. As well as a whole bunch of other utilities. Instead of tests for now it's focused on providing runnable examples (`./examples`), and specifically `./examples/blog` serves as both live example and documentation website. 7 | 8 | After making changes run `cargo check` for modified projects to make sure they compile fine. -------------------------------------------------------------------------------- /host/auth/permissions.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use axum_login::AuthzBackend; 4 | 5 | use crate::*; 6 | 7 | pub type Permission = String; 8 | 9 | #[async_trait] 10 | impl AuthzBackend for Prest { 11 | type Permission = Permission; 12 | 13 | async fn get_user_permissions( 14 | &self, 15 | user: &Self::User, 16 | ) -> std::result::Result, Self::Error> { 17 | Ok(user.permissions.iter().map(|s| s.to_owned()).collect()) 18 | } 19 | 20 | async fn get_group_permissions( 21 | &self, 22 | user: &Self::User, 23 | ) -> std::result::Result, Self::Error> { 24 | Ok(user.permissions.iter().map(|s| s.to_owned()).collect()) 25 | } 26 | 27 | async fn get_all_permissions( 28 | &self, 29 | user: &Self::User, 30 | ) -> std::result::Result, Self::Error> { 31 | Ok(user.permissions.iter().map(|s| s.to_owned()).collect()) 32 | } 33 | 34 | async fn has_perm( 35 | &self, 36 | user: &Self::User, 37 | perm: Self::Permission, 38 | ) -> std::result::Result { 39 | Ok(user.permissions.iter().find(|p| **p == perm).is_some()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /vals.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// Utility that deserializes from either [`Query`] (GET) or [`Json`] (others) based on request method 4 | pub struct Vals(pub T); 5 | #[async_trait] 6 | impl FromRequest for Vals 7 | where 8 | T: serde::de::DeserializeOwned + Send, 9 | S: Send + Sync, 10 | { 11 | type Rejection = Error; 12 | 13 | async fn from_request(req: Request, state: &S) -> Result { 14 | if req.method() == Method::GET || req.method() == Method::HEAD { 15 | let (mut parts, _) = req.into_parts(); 16 | match axum::extract::Query::::from_request_parts(&mut parts, state).await { 17 | Ok(axum::extract::Query(params)) => Ok(Vals(params)), 18 | Err(e) => Err(e.into()), 19 | } 20 | } else { 21 | match Json::::from_request(req, state).await { 22 | Ok(Json(params)) => Ok(Vals(params)), 23 | Err(e) => Err(e.into()), 24 | } 25 | } 26 | } 27 | } 28 | 29 | impl std::ops::Deref for Vals { 30 | type Target = T; 31 | 32 | fn deref(&self) -> &Self::Target { 33 | &self.0 34 | } 35 | } 36 | 37 | impl std::ops::DerefMut for Vals { 38 | fn deref_mut(&mut self) -> &mut Self::Target { 39 | &mut self.0 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/databases/postgres-diesel/migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /examples/solana/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["."] 3 | resolver = "2" 4 | 5 | [package] 6 | name = "todo-solana" 7 | edition = "2021" 8 | 9 | [lib] 10 | crate-type = ["cdylib", "lib"] 11 | 12 | [target.'cfg(target_os = "solana")'.dependencies] 13 | anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } 14 | 15 | [target.'cfg(not(target_os = "solana"))'.dependencies] 16 | anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } 17 | anchor-client = { version = "0.30.1", features = ["async"] } 18 | prest = "0.5" 19 | 20 | [profile.release] 21 | overflow-checks = true 22 | lto = "fat" 23 | codegen-units = 1 24 | [profile.release.build-override] 25 | opt-level = 3 26 | incremental = false 27 | codegen-units = 1 28 | 29 | # until https://github.com/coral-xyz/anchor/pull/3057 is released 30 | [patch.crates-io.anchor-client] 31 | git = "https://github.com/coral-xyz/anchor.git" 32 | rev = "f677742a978ffdf7bc321746b4119394f6654b7c" 33 | [patch.crates-io.anchor-lang] 34 | git = "https://github.com/coral-xyz/anchor.git" 35 | rev = "f677742a978ffdf7bc321746b4119394f6654b7c" 36 | 37 | # dependencies conflicts, should be resolved with solana-program v2 38 | [patch.crates-io.curve25519-dalek] 39 | git = "https://github.com/solana-labs/curve25519-dalek.git" 40 | rev = "b500cdc2a920cd5bff9e2dd974d7b97349d61464" 41 | [patch.crates-io.aes-gcm-siv] 42 | git = "https://github.com/edezhic/AEADs" 43 | -------------------------------------------------------------------------------- /examples/blog/src/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /examples/databases/postgres-seaorm/migrator.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | pub struct Migrator; 4 | 5 | #[async_trait::async_trait] 6 | impl MigratorTrait for Migrator { 7 | fn migrations() -> Vec> { 8 | vec![Box::new(MigrationCreateTodos)] 9 | } 10 | } 11 | struct MigrationCreateTodos; 12 | impl MigrationName for MigrationCreateTodos { 13 | fn name(&self) -> &str { 14 | "m_20231106_000001_create_todos_table" 15 | } 16 | } 17 | #[async_trait::async_trait] 18 | impl MigrationTrait for MigrationCreateTodos { 19 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 20 | manager 21 | .create_table( 22 | Table::create() 23 | .table(Todos::Storage) 24 | .col(ColumnDef::new(Todos::Uuid).uuid().not_null().primary_key()) 25 | .col(ColumnDef::new(Todos::Task).string().not_null()) 26 | .col(ColumnDef::new(Todos::Done).boolean().not_null()) 27 | .to_owned(), 28 | ) 29 | .await 30 | } 31 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 32 | manager 33 | .drop_table(Table::drop().table(Todos::Storage).to_owned()) 34 | .await 35 | } 36 | } 37 | 38 | #[derive(Iden)] 39 | pub enum Todos { 40 | Storage, 41 | Uuid, 42 | Task, 43 | Done, 44 | } 45 | -------------------------------------------------------------------------------- /examples/blog/src/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /db/key.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | use sql::Key; 4 | 5 | pub trait IntoSqlKey { 6 | fn into_sql_key(self) -> Key; 7 | } 8 | 9 | macro_rules! into_key { 10 | ($type:tt, $variant:tt) => { 11 | impl IntoSqlKey for $type { 12 | fn into_sql_key(self) -> Key { 13 | Key::$variant(self) 14 | } 15 | } 16 | }; 17 | } 18 | 19 | into_key!(bool, Bool); 20 | into_key!(i8, I8); 21 | into_key!(i16, I16); 22 | into_key!(i32, I32); 23 | into_key!(i64, I64); 24 | into_key!(i128, I128); 25 | into_key!(u8, U8); 26 | into_key!(u16, U16); 27 | into_key!(u32, U32); 28 | into_key!(u64, U64); 29 | into_key!(u128, U128); 30 | into_key!(String, Str); 31 | into_key!(NaiveDateTime, Timestamp); 32 | into_key!(NaiveDate, Date); 33 | into_key!(NaiveTime, Time); 34 | 35 | impl IntoSqlKey for Uuid { 36 | fn into_sql_key(self) -> Key { 37 | Key::Uuid(self.as_u128()) 38 | } 39 | } 40 | 41 | impl IntoSqlKey for Vec { 42 | fn into_sql_key(self) -> Key { 43 | Key::Bytea(self) 44 | } 45 | } 46 | 47 | impl IntoSqlKey for f32 { 48 | fn into_sql_key(self) -> Key { 49 | Key::F32(sql::OrderedFloat(self)) 50 | } 51 | } 52 | 53 | impl IntoSqlKey for f64 { 54 | fn into_sql_key(self) -> Key { 55 | Key::F64(sql::OrderedFloat(self)) 56 | } 57 | } 58 | 59 | // TODO: remaining possible key types 60 | // Decimal(v) => Ok(Key::Decimal(v)), 61 | // Inet(v) => Ok(Key::Inet(v)), 62 | // Interval(v) => Ok(Key::Interval(v)), 63 | -------------------------------------------------------------------------------- /examples/solana/README.md: -------------------------------------------------------------------------------- 1 | Example of a todo application that is using Solana blockchain for storage. One of the cool things about Solana is that it supports writing onchain programs in Rust so we can reuse program's types in the offchain prest code to simplify interactions. Also, there is an [Anchor](https://www.anchor-lang.com/) framework that simplifies smart contract development by abstracting onchain accounts so that we don't have to worry all technical details. To get started we'll need to add anchor dependencies and some patches to make it compatible with prest: 2 | 3 | {Cargo.toml} 4 | 5 | You might also notice profile overrides which are needed to make program's code as small as possible because onchain storage is quite expensive. 6 | 7 | Next comes the program's code: 8 | 9 | {src/lib.rs} 10 | 11 | Here we have definitions for the onchain data and available instructions. Each instruction requires a context which defines accounts that are needed for the execution. Some of them like `Signer` and `System` are built-in, but `TodoList` is defined by this program. 12 | 13 | Last piece is the application which will prepare and deploy the program to the local network and allow us to interact with it: 14 | 15 | {src/main.rs} 16 | 17 | This example is hardcoded for an easy local setup and demo purposes, but overall solana interactions aren't much different. However, in a real project you'll probably want to run transactions from the frontend signed by users' keys, and current solana sdks do not support doing that in rust, so you'll probably need to add some javascript. -------------------------------------------------------------------------------- /examples/databases/postgres-diesel/README.md: -------------------------------------------------------------------------------- 1 | Minimalistic todo app with storage based on [PostgreSQL](https://www.postgresql.org/) DB with [Diesel](https://github.com/launchbadge/sqlx) ORM - probably the first mature rust orm, currently used in [crates.io](https://crates.io/) and many other projects. 2 | 3 | It has a number of advantages - stability, feature-completeness, plenty of configs and utility crates, and easy to use once you've set it up. High-performance and low-risk choice for lots of projects. However, the initial setup might be tricky because diesel crates link to host-provided db client libraries. 4 | 5 | This example is using [diesel-async](https://github.com/weiznich/diesel_async) because the rest of the server is async, and it's intended to showcase basic apis with a UI similar to other DB examples to easily compare their usage. To get started with this one you'll need: 6 | 7 | 1. `cargo install diesel_cli --no-default-features --features postgres` - install diesel CLI that you'll need for common diesel-related ops 8 | 2. `docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -d postgres` - start a postgres instance in a docker container 9 | 3. `cd examples/databases/postgres-diesel && diesel setup --migration-dir="./migrations/"` - setup database & migrations 10 | 4. `cargo run -p postgres-diesel` - to start the example 11 | 12 | It's powered by a few additional dependencies in the manifest: 13 | 14 | {Cargo.toml} 15 | 16 | A separate manifest for the diesel configuration: 17 | 18 | {diesel.toml} 19 | 20 | A model that defines the auto-generated schema: 21 | 22 | {models.rs} 23 | 24 | And a prest app that uses all of the above to manage todos: 25 | 26 | {serve.rs} 27 | -------------------------------------------------------------------------------- /examples/llm-mistral/README.md: -------------------------------------------------------------------------------- 1 | Simple example that runs [Mistral](https://mistral.ai/news/announcing-mistral-7b/) model using [candle](https://github.com/huggingface/candle) framework. Adopted from [candle's mistral example](https://github.com/huggingface/candle/tree/main/candle-examples/examples/mistral), but without platform-dependent optimizations and with UI for the easiest start. 2 | 3 | Candle is a framework developed by [Hugging Face](https://huggingface.co/) - leading AI platform in the world. They started to build it because Python, common choice for AI development, introduces significant performance and devops overhead, while rust solves these problems, enchances reliability and provides direct access to WASM and WebGPU ecosystems to easily run models on the client side. As of now it's not as easy to use as PyTorch and missing some important features, but the future is bright and it already supports a lot of modern models like the one used in this example. 4 | 5 | As always let's start with the manifest: 6 | 7 | {Cargo.toml} 8 | 9 | It includes `hf-hub` that simplifies model loading, `tokenizers` - another hugging face utility for efficient text pre- and postprocessing for LLMs, and `candle-*` crates which run calculations of the models. 10 | 11 | The core example's code is in: 12 | 13 | {llm.rs} 14 | 15 | It defines how the model is initialized, encodes, performs inference and decodes. Prest-based service that works with this model is defined here: 16 | 17 | {serve.rs} 18 | 19 | Beware that it's a simple and naive implementation designed to check it out locally. For real-world SaaS or other types of services model should be managed differently, but this example is enough to demonstrate core building blocks. -------------------------------------------------------------------------------- /cotton/src/cache.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, hash::Hash, sync::Arc}; 2 | 3 | use dashmap::DashMap; 4 | use futures::{ 5 | future::{BoxFuture, Shared}, 6 | Future, FutureExt, 7 | }; 8 | 9 | use crate::progress::PROGRESS_BAR; 10 | 11 | type SharedBoxFuture = Shared>; 12 | 13 | pub struct Cache { 14 | loader: Box BoxFuture<'static, V> + Send + Sync + 'static>, 15 | map: DashMap>, 16 | } 17 | 18 | impl Cache { 19 | pub fn new(loader: T) -> Self 20 | where 21 | F: Future + Sized + Send + 'static, 22 | T: Fn(K) -> F + Send + Sync + Clone + 'static, 23 | { 24 | let loader = Arc::new(loader); 25 | 26 | Self { 27 | loader: Box::new({ 28 | move |key| { 29 | let loader = loader.clone(); 30 | Box::pin({ 31 | async move { 32 | PROGRESS_BAR.inc_length(1); 33 | let v = loader(key).await; 34 | PROGRESS_BAR.inc(1); 35 | v 36 | } 37 | }) 38 | } 39 | }), 40 | map: DashMap::new(), 41 | } 42 | } 43 | 44 | pub async fn get(&self, key: K) -> V { 45 | let f = self 46 | .map 47 | .entry(key.clone()) 48 | .or_insert_with(|| (self.loader)(key).boxed().shared()) 49 | .clone(); 50 | 51 | f.await 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /html/macro/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc(html_root_url = "https://docs.rs/maud_macros/0.25.0")] 2 | // TokenStream values are reference counted, and the mental overhead of tracking 3 | // lifetimes outweighs the marginal gains from explicit borrowing 4 | #![allow(clippy::needless_pass_by_value)] 5 | 6 | extern crate proc_macro; 7 | 8 | mod ast; 9 | mod escape; 10 | mod generate; 11 | mod htmx; 12 | mod parse; 13 | mod tailwind; 14 | 15 | use proc_macro2::{Ident, Span, TokenStream, TokenTree}; 16 | use proc_macro_error::proc_macro_error; 17 | use quote::quote; 18 | 19 | /// Compose HTML templates right inside of Rust code with ease. 20 | /// 21 | /// As of now it is almost identical to the original so check out the [maud book](https://maud.lambda.xyz/) for details 22 | /// but it also supports $"..." notation for styles based on tailwind classes and some htmx aliases 23 | #[proc_macro] 24 | #[proc_macro_error] 25 | pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 26 | expand(input.into()).into() 27 | } 28 | 29 | fn expand(input: TokenStream) -> TokenStream { 30 | let output_ident = TokenTree::Ident(Ident::new("__maud_output", Span::mixed_site())); 31 | // Heuristic: the size of the resulting markup tends to correlate with the 32 | // code size of the template itself, but also styles, loops and htmx aliases so multiply it 33 | let size_hint = input.to_string().len() * 3; 34 | let markups = parse::parse(input); 35 | let stmts = generate::generate(markups, output_ident.clone()); 36 | quote!({ 37 | extern crate alloc; 38 | //extern crate prest; 39 | let mut #output_ident = alloc::string::String::with_capacity(#size_hint); 40 | #stmts 41 | prest::PreEscaped(#output_ident) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /examples/todo/src/main.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | 3 | #[derive(Storage, Serialize, Deserialize)] 4 | struct Todo { 5 | #[serde(default = "Uuid::now_v7")] 6 | pub id: Uuid, 7 | pub task: String, 8 | #[serde(default)] 9 | pub done: bool, 10 | } 11 | 12 | impl Render for Todo { 13 | fn render(&self) -> Markup { 14 | html! { 15 | $"flex justify-between items-center" swap-this vals=(json!(self)) { 16 | input type="checkbox" patch="/" checked[self.done] {} 17 | label $"ml-4 text-lg" {(self.task)} 18 | button $"ml-auto" delete="/" {"Delete"} 19 | } 20 | } 21 | } 22 | } 23 | 24 | async fn into_page(content: Markup) -> Markup { 25 | html! {(DOCTYPE) html {(Head::with_title("Todo app")) 26 | body $"max-w-screen-sm px-8 mx-auto mt-12 flex flex-col items-center" { 27 | form put="/" into-end-of="#list" after-request="this.reset()" { 28 | input $"border rounded-md" type="text" name="task" {} 29 | button $"ml-4" type="submit" {"Add"} 30 | } 31 | div #list $"w-full" {(content)} 32 | (Scripts::default()) 33 | } 34 | }} 35 | } 36 | 37 | #[init] 38 | async fn main() -> Result { 39 | route( 40 | "/", 41 | get(|| async { ok(Todo::get_all().await?.render()) }) 42 | .put(|todo: Vals| async move { ok(todo.save().await?.render()) }) 43 | .delete(|todo: Vals| async move { ok(todo.remove().await?) }) 44 | .patch(|Vals(mut todo): Vals| async move { 45 | ok(todo.update_done(!todo.done).await?.render()) 46 | }), 47 | ) 48 | .wrap_non_htmx(into_page) 49 | .run() 50 | .await 51 | } 52 | -------------------------------------------------------------------------------- /examples/todo-pwa/src/main.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | use todo_pwa::{into_page, shared_routes}; 3 | 4 | embed_build_output_as!(BuiltAssets); 5 | 6 | #[derive(Storage, Default, Serialize, Deserialize)] 7 | #[serde(default)] 8 | struct Todo { 9 | #[serde(default = "Uuid::now_v7")] 10 | pub id: Uuid, 11 | pub task: String, 12 | pub done: bool, 13 | } 14 | 15 | impl Render for Todo { 16 | fn render(&self) -> Markup { 17 | html! { 18 | $"flex justify-between items-center" swap-this vals=(json!(self)) { 19 | input type="checkbox" patch="/todos" checked[self.done] {} 20 | label $"ml-4 text-lg" {(self.task)} 21 | button $"ml-auto" delete="/todos" {"Delete"} 22 | } 23 | } 24 | } 25 | } 26 | 27 | #[init] 28 | async fn main() -> Result { 29 | shared_routes() 30 | .route( 31 | "/todos", 32 | get(|| async { 33 | ok(html!( 34 | form put="/todos" into-end-of="#list" after-request="this.reset()" { 35 | input $"border rounded-md" type="text" name="task" {} 36 | button $"ml-4" type="submit" {"Add"} 37 | } 38 | div #list $"w-full" {(Todo::get_all().await?)} 39 | )) 40 | }) 41 | .put(|todo: Vals| async move { ok(todo.save().await?.render()) }) 42 | .delete(|todo: Vals| async move { ok(todo.remove().await?) }) 43 | .patch(|Vals(mut todo): Vals| async move { 44 | ok(todo.update_done(!todo.done).await?.render()) 45 | }), 46 | ) 47 | .wrap_non_htmx(into_page) 48 | .embed(BuiltAssets) 49 | .run() 50 | .await 51 | } 52 | -------------------------------------------------------------------------------- /host/state.rs: -------------------------------------------------------------------------------- 1 | /// Macro that simplifies lazy globals by reducing boilerplate, allowing `?` operator and async init 2 | #[macro_export] 3 | macro_rules! state { 4 | ($(($v:tt))? $struct_name:ident: $type:ty = $init:block) => { 5 | pub$(($v))? static $struct_name: prest::Lazy<$type> = prest::Lazy::new(|| { 6 | fn init() -> prest::Somehow<$type> { 7 | let v = { $init }; 8 | Ok(v) 9 | } 10 | init().expect("Prest initialization must finish successfully") 11 | }); 12 | }; 13 | 14 | ($(($v:tt))? $struct_name:ident: $type:ty = async $init:block) => { 15 | pub$(($v))? static $struct_name: prest::Lazy<$type> = prest::Lazy::new(|| { 16 | async fn init() -> prest::Somehow<$type> { 17 | let v = { $init }; 18 | Ok(v) 19 | } 20 | if let Ok(handle) = prest::_host::RuntimeHandle::try_current() { 21 | if handle.runtime_flavor() != prest::_host::RuntimeFlavor::CurrentThread { 22 | prest::block_in_place(move || handle.block_on(init()) 23 | .expect("Prest initialization must finish successfully")) 24 | } else { 25 | panic!("Prest doesn't support async state inside of the tokio's current_thread runtime") 26 | } 27 | } else { 28 | prest::_host::RuntimeBuilder::new_current_thread() 29 | .enable_all() 30 | .build() 31 | .expect("Runtime spawn should be fine outside of another runtime") 32 | .block_on(init()) 33 | .expect("Prest initialization must finish successfully") 34 | } 35 | }); 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /cotton/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cotton-install" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | path = "src/lib.rs" 8 | 9 | [[bin]] 10 | name = "cotton-install" 11 | path = "src/main.rs" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | async-compression = { version = "0.4.9", features = ["tokio", "gzip"] } 17 | async-recursion = "1.1.1" 18 | cached = "0.44.0" 19 | color-eyre = "0.6.3" 20 | compact_str = { version = "0.8.0", features = ["serde"] } 21 | dashmap = { version = "6.0.0", features = ["serde"] } 22 | flume = "0.11.0" 23 | futures = "0.3.30" 24 | futures-lite = "2.3.0" 25 | indexmap = { version = "2.2.6", features = ["serde"] } 26 | indicatif = "0.17.8" 27 | itertools = "0.14.0" 28 | node-semver = { git = "https://github.com/danielhuang/node-semver-rs", rev = "bf4b103dc88b310c9dc049433aff1a14716e1e68" } 29 | notify = "=8.0.0" 30 | once_cell = "1.19.0" 31 | owo-colors = "4.1.0" 32 | reqwest = { version = "0.12.4", features = [ 33 | "json", 34 | "stream", 35 | "rustls-tls", 36 | "trust-dns", 37 | "brotli", 38 | "gzip", 39 | "deflate", 40 | "http2", 41 | ], default-features = false } 42 | rustc-hash = "2.0.0" 43 | serde = { version = "1.0.200", features = ["derive", "rc"] } 44 | serde_json = { version = "1.0.116", features = ["preserve_order"] } 45 | serde_path_to_error = "0.1.16" 46 | tokio = { version = "1.37.0", features = ["full"] } 47 | tokio-tar = { git = "https://github.com/danielhuang/tokio-tar", rev = "ac063a10224a9dcb16967e792c3075e0ee8bb1a7" } 48 | tokio-util = { version = "0.7.10", features = ["compat"] } 49 | tracing = "0.1.40" 50 | tracing-error = "0.2.0" 51 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 52 | toml = "0.8.12" 53 | tap = "1.0.1" 54 | url = { version = "2.5.0", features = ["serde"] } 55 | -------------------------------------------------------------------------------- /host/admin/assets/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/TS_VS_RUST_FRONTEND.md: -------------------------------------------------------------------------------- 1 | While prest is mostly focused on rust, I've made a significant effort to make it work well with typescript for the interfaces. Why not for example dioxus which is really awesome? Or some other rust -> wasm/native UI solution? Overall, as much as I'd love to write everything in Rust I have to consider smth: 2 | 3 | First of all - ecosystem. Since the death of Flash UI development has been almost exclusively JS-based. Not because ppl love JS that much. Not because it's fast or reliable (because it's neither). But it is the most widely supported one, somewhat like Java with it's "installed on X billion devices" - cancer that we have to deal with. This ecosystem has pretty much everything you might want or need. No other UI dev tooling comes even close (except, again, gaming-related stuff, but it's a whole different story). 4 | 5 | Second - there is no second. That's it. There are a whole bunch of reasons to pick other frontend tools, especially rust-based ones for full-stack in a single language experience, but none of them comes close in the development speed to JS. Especially if you're sprinkling some TypeScript on top to keep some sanity in the codebase. 6 | 7 | Native interfaces are cool, but for 99% of the projects (excluding games) performance gains just aren't worth the effort. Especially considering how hard browser engine developers are optimizing them. 8 | 9 | Service worker part is already compiled into wasm and runs on the client side. However, it's fairly simple and requires just a few outside intefaces to integrate, and it's easily cross-compiled from the existing server code. It's closer to server-side development than client-side. 10 | 11 | Being able to write everything in rust sounds amazing to me. Yet reinventing all the libraries I could use with JS does not. 12 | 13 | As awesome as dioxus is, it's will be hard to compete with JS ecosystem in the foreseeable future. But maybe prest can adopt dioxus's hot reloading? Definitely worth a try. -------------------------------------------------------------------------------- /host/auth/session.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use tower_sessions::{ 3 | session::{Id, Record}, 4 | session_store::{Error as SessionError, Result as SessionResult}, 5 | Expiry, SessionManagerLayer, SessionStore, 6 | }; 7 | 8 | #[derive(Storage, Debug, Serialize, Deserialize)] 9 | pub struct SessionRow { 10 | pub id: i128, 11 | pub record: Vec, 12 | } 13 | 14 | #[async_trait] 15 | impl SessionStore for Prest { 16 | async fn save(&self, record: &Record) -> SessionResult<()> { 17 | let id = record.id.0; 18 | let record = match bitcode::serialize(record) { 19 | Ok(s) => s, 20 | Err(e) => return Err(SessionError::Encode(format!("{e}"))), 21 | }; 22 | match (SessionRow { id, record }).save().await { 23 | Ok(_) => Ok(()), 24 | Err(e) => Err(SessionError::Backend(format!("Session save error: {e}"))), 25 | } 26 | } 27 | 28 | async fn load(&self, session_id: &Id) -> SessionResult> { 29 | let search = match SessionRow::get_by_pkey(session_id.0).await { 30 | Ok(v) => v, 31 | Err(e) => { 32 | return Err(SessionError::Backend(format!( 33 | "Failed to load session: {e}" 34 | ))) 35 | } 36 | }; 37 | 38 | let Some(session_row) = search else { 39 | return Ok(None); 40 | }; 41 | match bitcode::deserialize(&session_row.record) { 42 | Ok(record) => Ok(Some(record)), 43 | Err(e) => Err(SessionError::Decode(format!("Session load error: {e}"))), 44 | } 45 | } 46 | 47 | async fn delete(&self, session_id: &Id) -> SessionResult<()> { 48 | match SessionRow::delete_by_pkey(session_id.0).await { 49 | Ok(_) => Ok(()), 50 | Err(e) => Err(SessionError::Backend(format!( 51 | "Session deletion error: {e}" 52 | ))), 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /host/admin/logs.rs: -------------------------------------------------------------------------------- 1 | use chrono::NaiveDate; 2 | use host::{admin::LOADER_SVG, TRACES_DATE_FORMAT}; 3 | 4 | use crate::{host::LOGS, *}; 5 | 6 | pub(crate) async fn info_explorer() -> Markup { 7 | html! { 8 | $"w-full"{ 9 | $"font-bold text-lg" {"Latest info"} 10 | $"font-mono text-[0.5rem] md:text-sm leading-snug" { 11 | $"w-8 mx-auto" get="/admin/latest_info/0" trigger="revealed" swap-this {(LOADER_SVG)} 12 | } 13 | } 14 | } 15 | } 16 | 17 | pub(crate) async fn info(Path(offset): Path) -> Markup { 18 | const PER_PAGE: usize = 20; 19 | let logs: Vec<_> = LOGS 20 | .latest_info(offset, PER_PAGE) 21 | .into_iter() 22 | .map(|log| PreEscaped(log)) 23 | .collect(); 24 | 25 | let maybe_more = logs.len() > 0; 26 | 27 | html! { 28 | @for log in logs {p style="margin:0 !important"{(log)}} 29 | @if maybe_more { 30 | $"w-8 mx-auto" 31 | get={"/admin/latest_info/"(offset + PER_PAGE)} 32 | trigger="revealed" 33 | target="this" 34 | swap="outerHTML transition:false" 35 | {(LOADER_SVG)} 36 | } 37 | } 38 | } 39 | 40 | pub(crate) async fn traces_explorer() -> Markup { 41 | let today = Utc::now().format(TRACES_DATE_FORMAT); 42 | let mut available_dates = LOGS.recorded_traces_dates(); 43 | available_dates.sort_by(|a, b| b.cmp(a)); 44 | 45 | html! { 46 | a _=(format!("on load call loadTraces('{today}') then remove me")) {} 47 | select $"bg-stone-900 accent-stone-600 px-2 py-1" _="on every change call loadTraces(event.target.value)" { 48 | @for date in available_dates { 49 | option value=(date) {(date)} 50 | } 51 | } 52 | #"traces-container" $"font-mono" {} 53 | } 54 | } 55 | 56 | pub(crate) async fn traces(Path(date): Path) -> impl IntoResponse { 57 | LOGS.traces(date) 58 | } 59 | -------------------------------------------------------------------------------- /config.rs: -------------------------------------------------------------------------------- 1 | use std::{ops::Deref, sync::OnceLock}; 2 | 3 | use crate::*; 4 | 5 | /// Holds initialized [`AppConfig`], requires arguments from the init macro to initialize so cant be Lazy 6 | pub static APP_CONFIG: AppConfig = AppConfig::new(); 7 | 8 | /// Holds basic information about the app 9 | pub struct AppConfig { 10 | info: OnceLock, 11 | } 12 | 13 | #[derive(Debug)] 14 | pub struct AppConfigInfo { 15 | pub name: &'static str, 16 | pub version: semver::Version, 17 | pub persistent: bool, 18 | pub domain: Option<&'static str>, 19 | pub manifest_dir: &'static str, 20 | #[cfg(host)] 21 | pub data_dir: std::path::PathBuf, 22 | } 23 | 24 | impl AppConfig { 25 | const fn new() -> Self { 26 | Self { 27 | info: OnceLock::new(), 28 | } 29 | } 30 | 31 | pub fn _init( 32 | &self, 33 | manifest_dir: &'static str, 34 | name: &'static str, 35 | version: &str, 36 | persistent: bool, 37 | domain: Option<&'static str>, 38 | ) { 39 | let version = version.parse::().unwrap(); 40 | 41 | #[cfg(host)] 42 | let data_dir = { 43 | let project_dirs = prest::ProjectDirs::from("", "", name).unwrap(); 44 | let path = project_dirs.data_dir().to_path_buf(); 45 | std::fs::create_dir_all(&path).unwrap(); 46 | path 47 | }; 48 | 49 | self.info 50 | .set(AppConfigInfo { 51 | name, 52 | version, 53 | persistent, 54 | domain, 55 | manifest_dir, 56 | #[cfg(host)] 57 | data_dir, 58 | }) 59 | .expect("App config should initialize"); 60 | } 61 | } 62 | 63 | impl Deref for AppConfig { 64 | type Target = AppConfigInfo; 65 | 66 | fn deref(&self) -> &Self::Target { 67 | self.info.get().expect("App config should be initialized. Did you forget to add `#[init]` macro to the main function?") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/todo-pwa-auth-sync/README.md: -------------------------------------------------------------------------------- 1 | In the [previous example](https://prest.blog/todo-pwa-auth) we've added auth to the [todo PWA](https://prest.blog/todo-pwa). In this one we'll make all the todos public and provide real-time updates to the clients about every change in them. We'll add a new dependency here - [async-broadcast](https://docs.rs/async-broadcast/latest/async_broadcast/) which provides a simple mechanism to share changes with multiple streams: 2 | 3 | {Cargo.toml:8} 4 | 5 | Besides this manifest remains the same as well as build script and library so their contents are at the bottom. 6 | 7 | Until now we've changed the state of the clients only based on their requests and it made sense, but now we'll update the todo list based on [server sent events](https://en.wikipedia.org/wiki/Server-sent_events) initiated by other users adding or modifying their todos. Render method of our todo won't be based on the `Render` trait anymore because it will need additional user data to disable controls for non-owners. Also, we won't be returning markup from `add`, `toggle` and `delete` handlers anymore but instead use them to modify the data accordingly and broadcast the changes to all active clients: 8 | 9 | {src/main.rs} 10 | 11 | We're using the htmx's `sse` extension which allows us to easily swap events payloads into the right places based on their names. It comes with the default prest bundle so all you need is to set `sse="/todos/subscribe"` attribute which connects to the specified route to listen for events. Then `sse-swap="EVENT_NAME"` attributes can be used on it and its children to listen to events with specified names and swap if got any. In this case we're using `add` to append and todo's `id` as names to make sure events reach the right places. 12 | 13 | Now we have an installable collaborative real-time full-stack app! No react or another frontend framework involved and without even writing js. This is the end(for now) of the tutorials series, but you can also check out other examples from the menu. 14 | 15 | Remaining code used in this example: 16 | 17 | {Cargo.toml} 18 | 19 | {build.rs} 20 | 21 | {src/lib.rs} -------------------------------------------------------------------------------- /examples/todo-pwa-auth/README.md: -------------------------------------------------------------------------------- 1 | In the [previous example](https://prest.blog/todo-pwa) we've made our [todo app](https://prest.blog/todo) installable and now we'll provide authentication mechanisms. As always we're starting with the manifest and now we'll need to activate the `auth` feature of prest: 2 | 3 | {Cargo.toml:6} 4 | 5 | Everything else remains just like in the previous one, and you can find it's full content at the end of this tutorial. Same with the build script and the library as they remain untouched. Since authentication is the server-side business we'll only need to modify our binary: 6 | 7 | {src/main.rs} 8 | 9 | Now our handlers will need to consider whether requests are authorized to read/write specific todos and their code becomes significantly larger, so we'll be moving away from closures and use functions. Also, here we introduced two new extractors: 10 | 11 | * `Auth` - struct that provides auth-related methods to authenticate and an optional user field 12 | * `User` - little utility that returns user data if there is some `auth.user` and returns `UNAUTHORIZED` if none 13 | 14 | Also, our `todos` handler got new templates with sign in / sign up forms, and an optional login with google button that renders depending on whether `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` env variables required for the auth are provided. By default after the auth flow they will redirect back to the `/`, but this behaviour can be customized by sending `redirect` field with the forms or by adding `next` query param to the google auth route. 15 | 16 | Handlers for the routes specified in these forms and the button are automatically appended to the router in the `.run()` function. It will also set up the session and user management middleware, storage for them and other required utils. 17 | 18 | That's it! Now users can install the app and handle their own todos without spying on each other. But maybe you actually want todos to be public? Let's make it happen in the [next example](https://prest.blog/todo-pwa-auth-sync). 19 | 20 | Remaining code used in this example: 21 | 22 | {Cargo.toml} 23 | 24 | {build.rs} 25 | 26 | {src/lib.rs} -------------------------------------------------------------------------------- /host/admin/analytics.rs: -------------------------------------------------------------------------------- 1 | use crate::{analytics::RouteStat, *}; 2 | 3 | pub(crate) async fn full() -> Result { 4 | let analytics = RouteStat::get_all().await?; 5 | let mut total_path_hits = 0; 6 | 7 | type Stats = Vec<(Markup, Markup, u64, Markup)>; 8 | 9 | let mut path_stats: Stats = vec![]; 10 | let mut asset_stats: Stats = vec![]; 11 | 12 | for route in analytics { 13 | for (method, (hits, latency)) in route.method_hits_and_latency { 14 | let method = PreEscaped(method); 15 | let path = PreEscaped(route.path.clone()); 16 | let latency = PreEscaped(format!("{:.3}ms", latency)); 17 | 18 | let view = (method, path, hits, latency); 19 | 20 | if route.is_asset { 21 | asset_stats.push(view); 22 | } else { 23 | path_stats.push(view); 24 | total_path_hits += hits; 25 | } 26 | } 27 | } 28 | 29 | Ok(html! { 30 | a get="/admin/schedule" trigger="load" swap-this {} 31 | $"font-bold text-lg" {"Routes stats (total hits: "(total_path_hits)"*)"} 32 | $"hidden md:block italic text-xs" {"*only counts requests to the server, static pages like blog's are served primarily by the Service Worker and aren't reflected here"} 33 | table $"w-full text-xs md:text-sm font-mono" { 34 | @for route in path_stats { 35 | tr { 36 | td $"w-[17%]"{(route.0)} 37 | td $"w-[53%]"{(route.1)} 38 | td $"w-[10%]"{(route.2)} 39 | td $"w-[20%]"{(route.3)} 40 | } 41 | } 42 | } 43 | $"font-bold text-lg" {"Assets"} 44 | table $"w-full text-xs md:text-sm font-mono" { 45 | @for route in asset_stats { 46 | tr { 47 | td $"w-[17%]"{(route.0)} 48 | td $"w-[53%]"{(route.1)} 49 | td $"w-[10%]"{(route.2)} 50 | td $"w-[20%]"{(route.3)} 51 | } 52 | } 53 | } 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /html/macro/htmx.rs: -------------------------------------------------------------------------------- 1 | pub fn check_attr_name_alias(name: &str) -> Option<&str> { 2 | match name { 3 | "get" => Some("hx-get"), 4 | "put" => Some("hx-put"), 5 | "post" => Some("hx-post"), 6 | "patch" => Some("hx-patch"), 7 | "delete" => Some("hx-delete"), 8 | "target" => Some("hx-target"), 9 | "vals" => Some("hx-vals"), 10 | "swap" => Some("hx-swap"), 11 | "trigger" => Some("hx-trigger"), 12 | "include" => Some("hx-include"), 13 | "boost" => Some("hx-boost"), 14 | "history-elt" => Some("hx-history-elt"), 15 | "before-request" => Some("hx-on--before-request"), 16 | "after-request" => Some("hx-on--after-request"), 17 | "sse-msg" => Some("sse-swap"), 18 | _ => None, 19 | } 20 | } 21 | 22 | pub fn check_attr_shorthand(name: &str) -> Option<&str> { 23 | match name { 24 | "swap-this" => Some(r#"hx-target="this""#), 25 | "swap-inner" => Some(r#"hx-swap="innerHTML""#), 26 | "swap-full" => Some(r#"hx-swap="outerHTML""#), 27 | "swap-textContent" => Some(r#"hx-swap="textContent""#), 28 | "swap-before-begin" => Some(r#"hx-swap="beforebegin""#), 29 | "swap-after-begin" => Some(r#"hx-swap="afterbegin""#), 30 | "swap-before-end" => Some(r#"hx-swap="beforeend""#), 31 | "swap-after-end" => Some(r#"hx-swap="afterend""#), 32 | "swap-delete" => Some(r#"hx-swap="delete""#), 33 | "swap-none" => Some(r#"hx-swap="none""#), 34 | "replace-url" => Some(r#"hx-replace-url="true""#), 35 | "push-url" => Some(r#"hx-push-url="true""#), 36 | "no-replace-url" => Some(r#"hx-replace-url="false""#), 37 | "no-push-url" => Some(r#"hx-push-url="false""#), 38 | // complex ones 39 | "sse" => Some(r#"hx-ext="sse" sse-connect"#), 40 | "into" => Some(r#"hx-swap="innerHTML" hx-target"#), 41 | "put-before" => Some(r#"hx-swap="beforebegin" hx-target"#), 42 | "into-end-of" => Some(r#"hx-swap="beforeend" hx-target"#), 43 | "put-after" => Some(r#"hx-swap="afterend" hx-target"#), 44 | "swap-this-no-transition" => Some(r#"hx-target="this" hx-swap="transition:false""#), 45 | _ => None, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/databases/sqlite-turbosql/src/main.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | use turbosql::{execute, select, Turbosql}; 3 | 4 | #[derive(Default, Turbosql, Serialize, Deserialize)] 5 | struct Todo { 6 | pub rowid: Option, 7 | pub task: Option, 8 | pub done: Option, 9 | } 10 | 11 | #[init] 12 | async fn main() -> Result { 13 | route( 14 | "/", 15 | get(|| async { select!(Vec).unwrap().render() }) 16 | .put(|Vals(mut todo): Vals| async move { 17 | todo.done = Some(false); 18 | todo.insert().unwrap(); 19 | select!(Vec).unwrap().render() 20 | }) 21 | .patch(|Vals(mut todo): Vals| async move { 22 | todo.done = Some(!todo.done.unwrap()); 23 | todo.update().unwrap(); 24 | todo.render() 25 | }) 26 | .delete(|Vals(Todo { rowid, .. }): Vals| async move { 27 | execute!("DELETE FROM todo WHERE rowid = " rowid.unwrap()).unwrap(); 28 | }), 29 | ) 30 | .wrap_non_htmx(page) 31 | .run() 32 | .await 33 | } 34 | 35 | impl Render for Todo { 36 | fn render(&self) -> Markup { 37 | let rowid = self.rowid.clone().unwrap(); 38 | let task = self.task.clone().unwrap(); 39 | let done = self.done.clone().unwrap(); 40 | html! { 41 | $"flex items-center" swap-this vals=(json!({ "rowid": rowid, "task": task, "done": done})) { 42 | input type="checkbox" patch="/" checked[done] {} 43 | label $"ml-4 text-lg" {(task)} 44 | button $"ml-auto" detele="/" {"Delete"} 45 | } 46 | } 47 | } 48 | } 49 | 50 | async fn page(content: Markup) -> Markup { 51 | html! { html { (Head::with_title("With Turbosql SQLite")) 52 | body $"max-w-screen-sm mx-auto mt-12" { 53 | form $"flex gap-4 justify-center" put="/" into-end-of="#list" after-request="this.reset()" { 54 | input $"border rounded-md" type="text" name="task" {} 55 | button type="submit" {"Add"} 56 | } 57 | div #list $"w-full" {(content)} 58 | (Scripts::default()) 59 | } 60 | }} 61 | } 62 | -------------------------------------------------------------------------------- /serde_derive_fork/src/internals/ctxt.rs: -------------------------------------------------------------------------------- 1 | use quote::ToTokens; 2 | use std::cell::RefCell; 3 | use std::fmt::Display; 4 | use std::thread; 5 | 6 | /// A type to collect errors together and format them. 7 | /// 8 | /// Dropping this object will cause a panic. It must be consumed using `check`. 9 | /// 10 | /// References can be shared since this type uses run-time exclusive mut checking. 11 | #[derive(Default)] 12 | pub struct Ctxt { 13 | // The contents will be set to `None` during checking. This is so that checking can be 14 | // enforced. 15 | errors: RefCell>>, 16 | } 17 | 18 | impl Ctxt { 19 | /// Create a new context object. 20 | /// 21 | /// This object contains no errors, but will still trigger a panic if it is not `check`ed. 22 | pub fn new() -> Self { 23 | Ctxt { 24 | errors: RefCell::new(Some(Vec::new())), 25 | } 26 | } 27 | 28 | /// Add an error to the context object with a tokenenizable object. 29 | /// 30 | /// The object is used for spanning in error messages. 31 | pub fn error_spanned_by(&self, obj: A, msg: T) { 32 | self.errors 33 | .borrow_mut() 34 | .as_mut() 35 | .unwrap() 36 | // Curb monomorphization from generating too many identical methods. 37 | .push(syn::Error::new_spanned(obj.into_token_stream(), msg)); 38 | } 39 | 40 | /// Add one of Syn's parse errors. 41 | pub fn syn_error(&self, err: syn::Error) { 42 | self.errors.borrow_mut().as_mut().unwrap().push(err); 43 | } 44 | 45 | /// Consume this object, producing a formatted error string if there are errors. 46 | pub fn check(self) -> syn::Result<()> { 47 | let mut errors = self.errors.borrow_mut().take().unwrap().into_iter(); 48 | 49 | let mut combined = match errors.next() { 50 | Some(first) => first, 51 | None => return Ok(()), 52 | }; 53 | 54 | for rest in errors { 55 | combined.combine(rest); 56 | } 57 | 58 | Err(combined) 59 | } 60 | } 61 | 62 | impl Drop for Ctxt { 63 | fn drop(&mut self) { 64 | if !thread::panicking() && self.errors.borrow().is_some() { 65 | panic!("forgot to check for errors"); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /host/admin/schedule.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::collections::HashMap; 3 | 4 | pub(crate) async fn full() -> Result { 5 | let jobs_records = ScheduledJobRecord::get_all().await?; 6 | 7 | #[derive(Default)] 8 | struct ScheduledJobStat { 9 | finished_successfully: u32, 10 | in_progress: u32, 11 | avg_duration: f64, 12 | errors: Vec<(NaiveDateTime, String)>, 13 | } 14 | 15 | let jobs_stats: HashMap = jobs_records.into_iter().fold( 16 | HashMap::new(), 17 | |mut map, 18 | ScheduledJobRecord { 19 | name, 20 | start, 21 | end, 22 | error, 23 | .. 24 | }| { 25 | let entry = map.entry(name).or_default(); 26 | 27 | if end.is_none() { 28 | entry.in_progress += 1; 29 | } else if end.is_some() && error.is_none() { 30 | let updated_successes = entry.finished_successfully + 1; 31 | let duration = (end.unwrap() - start).num_milliseconds().abs() as f64; 32 | 33 | let updated_avg_duration = 34 | (entry.finished_successfully as f64 * entry.avg_duration + duration) 35 | / (updated_successes as f64); 36 | 37 | entry.finished_successfully = updated_successes; 38 | entry.avg_duration = updated_avg_duration; 39 | } else if end.is_some() && error.is_some() { 40 | entry.errors.push((end.unwrap(), error.unwrap())); 41 | } 42 | 43 | map 44 | }, 45 | ); 46 | 47 | Ok(html! { 48 | $"w-full" get="/admin/schedule" trigger="load delay:10s" swap-this-no-transition { 49 | $"font-bold text-lg" {"Scheduled jobs stats"} 50 | $"w-full text-xs md:text-sm font-mono" { 51 | @for (name, stats) in jobs_stats { 52 | @let duration = format!("{:.1}ms", stats.avg_duration); 53 | $"w-full" {b{(name)}": in progress = "(stats.in_progress)", finished = "(stats.finished_successfully)", avg duration = "(duration)} 54 | @for (end, error) in stats.errors { 55 | p{(end)" - "(error)} 56 | } 57 | } 58 | } 59 | } 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /serde_derive_fork/src/fragment.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::ToTokens; 3 | use syn::{token, Token}; 4 | 5 | pub enum Fragment { 6 | /// Tokens that can be used as an expression. 7 | Expr(TokenStream), 8 | /// Tokens that can be used inside a block. The surrounding curly braces are 9 | /// not part of these tokens. 10 | Block(TokenStream), 11 | } 12 | 13 | macro_rules! quote_expr { 14 | ($($tt:tt)*) => { 15 | $crate::fragment::Fragment::Expr(quote!($($tt)*)) 16 | } 17 | } 18 | 19 | macro_rules! quote_block { 20 | ($($tt:tt)*) => { 21 | $crate::fragment::Fragment::Block(quote!($($tt)*)) 22 | } 23 | } 24 | 25 | /// Interpolate a fragment in place of an expression. This involves surrounding 26 | /// Block fragments in curly braces. 27 | pub struct Expr(pub Fragment); 28 | impl ToTokens for Expr { 29 | fn to_tokens(&self, out: &mut TokenStream) { 30 | match &self.0 { 31 | Fragment::Expr(expr) => expr.to_tokens(out), 32 | Fragment::Block(block) => { 33 | token::Brace::default().surround(out, |out| block.to_tokens(out)); 34 | } 35 | } 36 | } 37 | } 38 | 39 | /// Interpolate a fragment as the statements of a block. 40 | pub struct Stmts(pub Fragment); 41 | impl ToTokens for Stmts { 42 | fn to_tokens(&self, out: &mut TokenStream) { 43 | match &self.0 { 44 | Fragment::Expr(expr) => expr.to_tokens(out), 45 | Fragment::Block(block) => block.to_tokens(out), 46 | } 47 | } 48 | } 49 | 50 | /// Interpolate a fragment as the value part of a `match` expression. This 51 | /// involves putting a comma after expressions and curly braces around blocks. 52 | pub struct Match(pub Fragment); 53 | impl ToTokens for Match { 54 | fn to_tokens(&self, out: &mut TokenStream) { 55 | match &self.0 { 56 | Fragment::Expr(expr) => { 57 | expr.to_tokens(out); 58 | ::default().to_tokens(out); 59 | } 60 | Fragment::Block(block) => { 61 | token::Brace::default().surround(out, |out| block.to_tokens(out)); 62 | } 63 | } 64 | } 65 | } 66 | 67 | impl AsRef for Fragment { 68 | fn as_ref(&self) -> &TokenStream { 69 | match self { 70 | Fragment::Expr(expr) => expr, 71 | Fragment::Block(block) => block, 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/polkadot/runtime/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "runtime" 5 | edition = "2021" 6 | 7 | [lib] 8 | path = "./lib.rs" 9 | crate-type = ["cdylib", "rlib"] 10 | 11 | [features] 12 | default = ["std"] 13 | std = [ 14 | "codec/std", 15 | "polkadot-sdk/std", 16 | "scale-info/std", 17 | "frame-support/std", 18 | "frame-system/std", 19 | "serde_json/std", 20 | ] 21 | 22 | [dependencies] 23 | polkadot-sdk = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409", features = ["experimental", 24 | "pallet-balances", 25 | "pallet-sudo", 26 | "pallet-timestamp", 27 | "pallet-transaction-payment", 28 | "pallet-transaction-payment-rpc-runtime-api", 29 | "runtime", 30 | ]} 31 | codec = { package = "parity-scale-codec", version = "3.6", features = ["derive"] } 32 | scale-info = { version = "2.11.1", default-features = false, features = ["derive"] } 33 | frame-support = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409" } 34 | frame-system = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409" } 35 | getrandom = { version = "0.2", features = ["js"] } 36 | serde_json = { version = "1", default-features = false, features = ["alloc"] } 37 | 38 | #frame-executive = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409" } 39 | #sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409" } 40 | #sp-core = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409" } 41 | #sp-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409" } 42 | #sp-version = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409" } 43 | #sp-block-builder = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409" } 44 | ##sp-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409" } 45 | #sp-inherents = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409" } 46 | #pallet-transaction-payment = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409", default-features = false } 47 | #pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409", default-features = false } 48 | 49 | [build-dependencies] 50 | polkadot-sdk = { optional = true, git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409", features = [ 51 | "substrate-wasm-builder", 52 | ] } -------------------------------------------------------------------------------- /cotton/src/config.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::eyre::Result; 2 | use reqwest::RequestBuilder; 3 | use serde::{Deserialize, Serialize}; 4 | use std::env; 5 | use tokio::fs::read_to_string; 6 | 7 | #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Debug, Default)] 8 | #[serde(deny_unknown_fields)] 9 | pub struct Config { 10 | #[serde(default)] 11 | pub registry: Vec, 12 | #[serde(default)] 13 | pub allow_install_scripts: bool, 14 | } 15 | 16 | #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Debug)] 17 | #[serde(deny_unknown_fields)] 18 | pub struct Registry { 19 | pub url: String, 20 | pub scope: Option, 21 | pub auth: Option, 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Debug)] 25 | #[serde(rename_all = "snake_case")] 26 | #[serde(untagged)] 27 | #[serde(deny_unknown_fields)] 28 | pub enum RegistryAuth { 29 | Token { 30 | token: AuthSource, 31 | }, 32 | Basic { 33 | username: AuthSource, 34 | #[serde(default)] 35 | password: Option, 36 | }, 37 | } 38 | 39 | pub fn client_auth(req: RequestBuilder, auth: Option<&RegistryAuth>) -> Result { 40 | Ok(match auth { 41 | Some(RegistryAuth::Token { token }) => { 42 | let token = token.read_token()?; 43 | req.bearer_auth(token) 44 | } 45 | Some(RegistryAuth::Basic { username, password }) => { 46 | let username = username.read_token()?; 47 | let password = password.as_ref().map(|x| x.read_token()).transpose()?; 48 | req.basic_auth(username, password) 49 | } 50 | None => req, 51 | }) 52 | } 53 | 54 | #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Debug)] 55 | #[serde(rename_all = "snake_case")] 56 | #[serde(untagged)] 57 | #[serde(deny_unknown_fields)] 58 | pub enum AuthSource { 59 | Inline(String), 60 | FromEnv { from_env: String }, 61 | } 62 | 63 | impl AuthSource { 64 | #[tracing::instrument] 65 | pub fn read_token(&self) -> Result { 66 | match self { 67 | AuthSource::Inline(x) => Ok(x.clone()), 68 | AuthSource::FromEnv { from_env } => Ok(env::var(from_env)?), 69 | } 70 | } 71 | } 72 | 73 | pub async fn read_config() -> Result { 74 | let config = read_to_string("cotton.toml").await; 75 | if let Ok(config) = config { 76 | Ok(toml::from_str(&config)?) 77 | } else { 78 | Ok(Config::default()) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/llm-mistral/serve.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | 3 | mod llm; 4 | 5 | state!(LLM: Mutex = { Mutex::new(llm::init()?) }); 6 | 7 | #[derive(Deserialize)] 8 | struct Prompt { 9 | pub content: String, 10 | } 11 | 12 | #[init] 13 | async fn main() -> Result { 14 | info!("Initializing LLM..."); 15 | let _ = *LLM; 16 | 17 | route("/", get(page)) 18 | .route( 19 | "/prompt", 20 | post(|Vals(prompt): Vals| async move { 21 | { 22 | let mut llm = LLM.lock().await; 23 | if llm.history.len() == 0 { 24 | llm.prompt(&prompt.content).unwrap() 25 | } else { 26 | let prompt = "\n".to_owned() + &prompt.content; 27 | llm.prompt(&prompt).unwrap() 28 | } 29 | } 30 | history(true).await 31 | }), 32 | ) 33 | .route( 34 | "/more", 35 | get(|| async { 36 | let in_progress = LLM.lock().await.more(); 37 | history(in_progress).await 38 | }), 39 | ) 40 | .route( 41 | "/reset", 42 | get(|| async { 43 | let mut llm = LLM.lock().await; 44 | *llm = llm::init().unwrap(); 45 | Redirect::to("/") 46 | }), 47 | ) 48 | .run() 49 | .await 50 | } 51 | 52 | async fn page() -> Markup { 53 | html!( html { (Head::with_title("With Mistral LLM")) 54 | body $"max-w-screen-sm mx-auto mt-8" { 55 | div {(history(false).await)} 56 | (Scripts::default()) 57 | } 58 | }) 59 | } 60 | 61 | async fn history(in_progress: bool) -> Markup { 62 | let content = LLM.lock().await.history.clone(); 63 | let btn = if content.len() == 0 { 64 | "Start generating" 65 | } else { 66 | "Append and continue" 67 | }; 68 | html!( 69 | (PreEscaped(content)) 70 | @if in_progress { 71 | ins get="/more" target="div" trigger="load"{} 72 | span {"loading..."} 73 | br{} 74 | button get="/" target="body" {"Pause"} 75 | } 76 | @else { 77 | form post="/prompt" target="div" { 78 | input type="text" name="content" placeholder="Prompt" required {} 79 | button type="submit" {(btn)} 80 | } 81 | } 82 | button get="/reset" target="body" {"Reset"} 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /examples/solana/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | declare_id!("S7SsyD8YqKtPzZS6pRButF658jCDnX5KvoU6kFQwKWH"); 4 | 5 | #[account] 6 | #[derive(InitSpace)] 7 | pub struct TodoList { 8 | #[max_len(5)] 9 | pub items: Vec, 10 | } 11 | 12 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)] 13 | pub struct Todo { 14 | #[max_len(20)] 15 | pub task: String, 16 | pub done: bool, 17 | } 18 | 19 | #[program] 20 | pub mod todo_solana { 21 | use super::*; 22 | 23 | #[derive(Accounts)] 24 | #[instruction(task: String)] 25 | pub struct AddTodo<'info> { 26 | #[account( 27 | init_if_needed, 28 | seeds = [owner.key().as_ref()], 29 | bump, 30 | payer = owner, 31 | space = 8 + TodoList::INIT_SPACE, 32 | )] 33 | pub list: Account<'info, TodoList>, 34 | #[account(mut)] 35 | pub owner: Signer<'info>, 36 | pub system_program: Program<'info, System>, 37 | } 38 | pub fn add_todo(ctx: Context, task: String) -> Result<()> { 39 | ctx.accounts.list.items.push(Todo { task, done: false }); 40 | Ok(()) 41 | } 42 | 43 | #[derive(Accounts)] 44 | #[instruction(index: u32)] 45 | pub struct ToggleTodo<'info> { 46 | #[account( 47 | mut, 48 | seeds = [owner.key().as_ref()], 49 | bump, 50 | )] 51 | pub list: Account<'info, TodoList>, 52 | #[account(mut)] 53 | pub owner: Signer<'info>, 54 | } 55 | pub fn toggle_todo(ctx: Context, index: u32) -> Result<()> { 56 | let index = index as usize; 57 | require!(ctx.accounts.list.items.get(index).is_some(), TodoError::NotFound); 58 | let todo = ctx.accounts.list.items.get_mut(index).unwrap(); 59 | todo.done = !todo.done; 60 | Ok(()) 61 | } 62 | 63 | #[derive(Accounts)] 64 | #[instruction(index: u32)] 65 | pub struct DeleteTodo<'info> { 66 | #[account( 67 | mut, 68 | seeds = [owner.key().as_ref()], 69 | bump, 70 | )] 71 | pub list: Account<'info, TodoList>, 72 | #[account(mut)] 73 | pub owner: Signer<'info>, 74 | } 75 | pub fn delete_todo(ctx: Context, index: u32) -> Result<()> { 76 | let index = index as usize; 77 | require!(ctx.accounts.list.items.get(index).is_some(), TodoError::NotFound); 78 | ctx.accounts.list.items.remove(index); 79 | Ok(()) 80 | } 81 | 82 | #[error_code] 83 | pub enum TodoError { 84 | NotFound 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /host/db/transaction.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{AsStorageError, DbConn, Snapshot, WRITE_STATE_KEY}, 3 | async_trait::async_trait, 4 | gluesql_core::{ 5 | error::{Error, Result}, 6 | store::{DataRow, Transaction}, 7 | }, 8 | }; 9 | 10 | #[async_trait(?Send)] 11 | impl<'a> Transaction for DbConn<'a> { 12 | async fn begin(&mut self, autocommit: bool) -> Result { 13 | if !autocommit { 14 | return Err(Error::StorageMsg( 15 | "nested and non-autocommitted transactions are not supported".to_owned(), 16 | )); 17 | } 18 | 19 | if !self.readonly { 20 | self.state.in_progress = true; 21 | if let Err(e) = bitcode::serialize(&self.state) 22 | .as_storage_err() 23 | .map(|state| self.tree.insert(WRITE_STATE_KEY, state)) 24 | { 25 | crate::error!("failed to update latest transaction id: {e}"); 26 | return Err(Error::StorageMsg( 27 | "failed to record tx beginning".to_owned(), 28 | )); 29 | } 30 | } 31 | 32 | Ok(true) 33 | } 34 | 35 | async fn rollback(&mut self) -> Result<()> { 36 | if !self.readonly { 37 | self.rollback_self()?; 38 | // CURRENT_TXID.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); 39 | } 40 | Ok(()) 41 | } 42 | 43 | async fn commit(&mut self) -> Result<()> { 44 | if !self.readonly { 45 | self.state.in_progress = false; 46 | if let Err(e) = bitcode::serialize(&self.state) 47 | .as_storage_err() 48 | .map(|state| self.tree.insert(WRITE_STATE_KEY, state)) 49 | { 50 | crate::error!("failed to update latest transaction id: {e}") 51 | } 52 | } 53 | Ok(()) 54 | } 55 | } 56 | 57 | impl<'a> DbConn<'a> { 58 | pub fn rollback_self(&self) -> Result<()> { 59 | let mut affected = 0; 60 | for item in self.tree.scan_prefix(b"data/") { 61 | let (key, value) = item.as_storage_err()?; 62 | let snapshot = bitcode::deserialize::>(&value).as_storage_err()?; 63 | if let Some(v) = snapshot.rollback(self.state) { 64 | affected += 1; 65 | if let Some(restored) = v { 66 | let restored = bitcode::serialize(&restored).as_storage_err()?; 67 | self.tree.insert(key, restored).as_storage_err()?; 68 | } else { 69 | self.tree.remove(key).as_storage_err()?; 70 | } 71 | } 72 | } 73 | crate::warn!("rolled back {affected} affected rows"); 74 | Ok(()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /serde_derive_fork/src/internals/symbol.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | use syn::{Ident, Path}; 3 | 4 | #[derive(Copy, Clone)] 5 | pub struct Symbol(&'static str); 6 | 7 | pub const ALIAS: Symbol = Symbol("alias"); 8 | pub const BORROW: Symbol = Symbol("borrow"); 9 | pub const BOUND: Symbol = Symbol("bound"); 10 | pub const CONTENT: Symbol = Symbol("content"); 11 | pub const CRATE: Symbol = Symbol("crate"); 12 | pub const DEFAULT: Symbol = Symbol("default"); 13 | pub const DENY_UNKNOWN_FIELDS: Symbol = Symbol("deny_unknown_fields"); 14 | pub const DESERIALIZE: Symbol = Symbol("deserialize"); 15 | pub const DESERIALIZE_WITH: Symbol = Symbol("deserialize_with"); 16 | pub const EXPECTING: Symbol = Symbol("expecting"); 17 | pub const FIELD_IDENTIFIER: Symbol = Symbol("field_identifier"); 18 | pub const FLATTEN: Symbol = Symbol("flatten"); 19 | pub const FROM: Symbol = Symbol("from"); 20 | pub const GETTER: Symbol = Symbol("getter"); 21 | pub const INTO: Symbol = Symbol("into"); 22 | pub const NON_EXHAUSTIVE: Symbol = Symbol("non_exhaustive"); 23 | pub const OTHER: Symbol = Symbol("other"); 24 | pub const REMOTE: Symbol = Symbol("remote"); 25 | pub const RENAME: Symbol = Symbol("rename"); 26 | pub const RENAME_ALL: Symbol = Symbol("rename_all"); 27 | pub const RENAME_ALL_FIELDS: Symbol = Symbol("rename_all_fields"); 28 | pub const REPR: Symbol = Symbol("repr"); 29 | pub const SERDE: Symbol = Symbol("serde"); 30 | pub const SERIALIZE: Symbol = Symbol("serialize"); 31 | pub const SERIALIZE_WITH: Symbol = Symbol("serialize_with"); 32 | pub const SKIP: Symbol = Symbol("skip"); 33 | pub const SKIP_DESERIALIZING: Symbol = Symbol("skip_deserializing"); 34 | pub const SKIP_SERIALIZING: Symbol = Symbol("skip_serializing"); 35 | pub const SKIP_SERIALIZING_IF: Symbol = Symbol("skip_serializing_if"); 36 | pub const TAG: Symbol = Symbol("tag"); 37 | pub const TRANSPARENT: Symbol = Symbol("transparent"); 38 | pub const TRY_FROM: Symbol = Symbol("try_from"); 39 | pub const UNTAGGED: Symbol = Symbol("untagged"); 40 | pub const VARIANT_IDENTIFIER: Symbol = Symbol("variant_identifier"); 41 | pub const WITH: Symbol = Symbol("with"); 42 | 43 | impl PartialEq for Ident { 44 | fn eq(&self, word: &Symbol) -> bool { 45 | self == word.0 46 | } 47 | } 48 | 49 | impl PartialEq for &Ident { 50 | fn eq(&self, word: &Symbol) -> bool { 51 | *self == word.0 52 | } 53 | } 54 | 55 | impl PartialEq for Path { 56 | fn eq(&self, word: &Symbol) -> bool { 57 | self.is_ident(word.0) 58 | } 59 | } 60 | 61 | impl PartialEq for &Path { 62 | fn eq(&self, word: &Symbol) -> bool { 63 | self.is_ident(word.0) 64 | } 65 | } 66 | 67 | impl Display for Symbol { 68 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 69 | formatter.write_str(self.0) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /db/macro/analyze.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub fn from_field(field: Field) -> Column { 4 | let pkey = field 5 | .attrs 6 | .iter() 7 | .find(|a| a.path().to_token_stream().to_string() == "pkey") 8 | .is_some(); 9 | 10 | let unique = field 11 | .attrs 12 | .iter() 13 | .find(|a| a.path().to_token_stream().to_string() == "unique") 14 | .is_some() 15 | || pkey; 16 | 17 | let field_name = field.ident.expect("only named structs"); 18 | let field_name_str = field_name.to_string(); 19 | let full_type = field.ty; 20 | let type_str = full_type.to_token_stream().to_string(); 21 | let type_str = type_str.as_str(); 22 | 23 | let (inner_type_str, optional, list) = 24 | if type_str.starts_with("Option < ") && type_str.ends_with(" >") { 25 | let inner_type_str = type_str 26 | .trim_start_matches("Option < ") 27 | .trim_end_matches(" >"); 28 | (inner_type_str, true, false) 29 | } else if type_str.starts_with("Vec < ") && type_str.ends_with(" >") { 30 | let inner_type_str = type_str.trim_start_matches("Vec < ").trim_end_matches(" >"); 31 | (inner_type_str, false, true) 32 | } else { 33 | (type_str, false, false) 34 | }; 35 | 36 | if pkey && optional || pkey && list { 37 | panic!("Primary Key (first attribute by default) cannot be Option<...> or Vec<...>") 38 | } 39 | 40 | let inner_type: syn::Type = syn::parse_str(inner_type_str).unwrap(); 41 | 42 | let serialized = match inner_type_str { 43 | "Uuid" | "String" | "NaiveDateTime" | "bool" | "u128" | "u64" | "u32" | "u16" | "u8" 44 | | "i128" | "i64" | "i32" | "i16" | "i8" | "f64" | "f32" => false, 45 | _ => true, 46 | }; 47 | 48 | use SqlType::*; 49 | let sql_type = match inner_type_str { 50 | "Uuid" => Uuid, 51 | "NaiveDateTime" => Timestamp, 52 | "bool" => Boolean, 53 | "u128" => Uint128, 54 | "u64" => Uint64, 55 | "u32" => Uint32, 56 | "u16" => Uint16, 57 | "u8" => Uint8, 58 | "i128" => Int128, 59 | "i64" => Int, 60 | "i32" => Int32, 61 | "i16" => Int16, 62 | "i8" => Int8, 63 | "f32" => Float32, 64 | "f64" => Float, 65 | "String" => Text, 66 | _ if serialized => Bytea, 67 | _ => panic!("Unsupported inner type str = {inner_type_str}"), 68 | // _ => Text, // fallback? 69 | }; 70 | 71 | Column { 72 | full_type_str: type_str.replace(' ', ""), 73 | sql_type, 74 | field_name, 75 | field_name_str, 76 | full_type, 77 | inner_type, 78 | pkey, 79 | optional, 80 | list, 81 | unique, 82 | serialized, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/databases/sqlite-sqlx/serve.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | use sqlx::{migrate, query, query_as, FromRow, Sqlite, SqlitePool}; 3 | 4 | state!(DB: SqlitePool = async { 5 | let conn = SqlitePool::connect("sqlite::memory:").await?; 6 | migrate!().run(&conn).await?; 7 | conn 8 | }); 9 | 10 | #[derive(Debug, FromRow, Serialize, Deserialize)] 11 | struct Todo { 12 | #[serde(default = "new_uuid")] 13 | pub uuid: String, 14 | #[serde(default)] 15 | pub task: String, 16 | #[serde(default)] 17 | pub done: bool, 18 | } 19 | 20 | fn new_uuid() -> String { 21 | Uuid::now_v7().to_string() 22 | } 23 | 24 | #[init] 25 | async fn main() -> Result { 26 | route( 27 | "/", 28 | get(get_todos) 29 | .patch(toggle_todo) 30 | .put(add_todo) 31 | .delete(delete_todo), 32 | ) 33 | .wrap_non_htmx(page) 34 | .run() 35 | .await 36 | } 37 | 38 | async fn get_todos() -> Markup { 39 | let q = "select * from todos"; 40 | let todos = query_as::(q).fetch_all(&*DB).await.unwrap(); 41 | todos.render() 42 | } 43 | 44 | async fn add_todo(Vals(todo): Vals) -> Markup { 45 | let q = "insert into todos (uuid, task) values (?, ?) returning *"; 46 | query_as::(q) 47 | .bind(todo.uuid) 48 | .bind(todo.task) 49 | .fetch_one(&*DB) 50 | .await 51 | .unwrap() 52 | .render() 53 | } 54 | 55 | async fn toggle_todo(Vals(todo): Vals) -> Markup { 56 | let q = "update todos set done = ? where uuid = ? returning *"; 57 | query_as::(q) 58 | .bind(!todo.done) 59 | .bind(todo.uuid) 60 | .fetch_one(&*DB) 61 | .await 62 | .unwrap() 63 | .render() 64 | } 65 | async fn delete_todo(Vals(todo): Vals) { 66 | let q = "delete from todos where uuid = ?"; 67 | query(q).bind(todo.uuid).execute(&*DB).await.unwrap(); 68 | } 69 | 70 | impl Render for Todo { 71 | fn render(&self) -> Markup { 72 | html! { 73 | $"flex items-center" swap-this vals=(json!(self)) { 74 | input type="checkbox" patch="/" checked[self.done] {} 75 | label $"ml-4 text-lg" {(self.task)} 76 | button $"ml-auto" detele="/" {"Delete"} 77 | } 78 | } 79 | } 80 | } 81 | 82 | async fn page(content: Markup) -> Markup { 83 | html! { html { (Head::with_title("With SQLx SQLite")) 84 | body $"max-w-screen-sm mx-auto mt-12" { 85 | form $"flex gap-4 justify-center" put="/" into-end-of="#list" after-request="this.reset()" { 86 | input $"border rounded-md" type="text" name="task" {} 87 | button type="submit" {"Add"} 88 | } 89 | div #list $"w-full" {(content)} 90 | (Scripts::default()) 91 | } 92 | }} 93 | } 94 | -------------------------------------------------------------------------------- /host/sse.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use async_broadcast::{broadcast, Receiver, Sender}; 3 | pub use axum::response::sse::{Event as SseEvent, KeepAlive as SseKeepAlive, Sse}; 4 | /// Alias for Server Sent Events event 5 | pub type SseItem = Result; 6 | 7 | // use stream::{Map, TryStream}; 8 | 9 | /// SseEvent wrapper which holds 10 | #[derive(Clone)] 11 | pub(crate) struct SseEventWrapper { 12 | pub event_name: String, 13 | pub data: T, 14 | } 15 | 16 | // unsafe impl Send for SseEventWrapper {} 17 | /// Broadcasting singleton for SSE (check out todo sync example) 18 | pub struct SseBroadcast { 19 | sender: Sender>, 20 | receiver: Receiver>, 21 | } 22 | 23 | impl Default for SseBroadcast { 24 | fn default() -> Self { 25 | let (sender, receiver) = broadcast(1000); 26 | Self { sender, receiver } 27 | } 28 | } 29 | 30 | impl SseBroadcast { 31 | // pub(crate) fn stream(&self) -> Receiver> { 32 | // self.receiver.new_receiver() 33 | // } 34 | 35 | pub async fn send>(&self, event_name: E, data: T) -> Result { 36 | self.sender 37 | .broadcast_direct(SseEventWrapper { 38 | event_name: event_name.into(), 39 | data, 40 | }) 41 | .await 42 | .somehow()?; 43 | Ok(()) 44 | } 45 | } 46 | 47 | /// Utility to `stream_and_render` [`SseBroadcast`]s 48 | pub trait SseBroadcastExt { 49 | fn stream_and_render(&self, f: F) -> Response 50 | where 51 | F: FnMut(&String, T) -> Markup + std::marker::Send + 'static; 52 | 53 | // fn subscribe(&self, f: F) -> MethodRouter where 54 | // // S: 55 | // F: FnMut(&String, T) -> Markup + std::marker::Send + 'static; 56 | } 57 | 58 | impl SseBroadcastExt for SseBroadcast { 59 | fn stream_and_render(&self, mut f: F) -> Response 60 | where 61 | F: (FnMut(&String, T) -> Markup) + std::marker::Send + 'static, 62 | { 63 | let stream = self.receiver.new_receiver().map(move |event| { 64 | let event_name = event.event_name; 65 | let data = event.data; 66 | let rendered = f(&event_name, data); 67 | SseEvent::default().event(event_name).data(rendered.0) 68 | }); 69 | 70 | Sse::new(stream.map(Ok::)) 71 | .keep_alive(SseKeepAlive::default()) 72 | .into_response() 73 | } 74 | 75 | // fn subscribe(&self, f: F) -> MethodRouter where 76 | // // S: 77 | // F: FnMut(&String, T) -> Markup + std::marker::Send + 'static { 78 | // get(|| async {}) 79 | // } 80 | } 81 | -------------------------------------------------------------------------------- /host/admin/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | mod analytics; 4 | mod db; 5 | mod logs; 6 | mod monitoring; 7 | mod remote; 8 | mod schedule; 9 | 10 | const ADMIN_SVG: PreEscaped<&str> = PreEscaped(include_str!("assets/admin.svg")); 11 | const DB_SVG: PreEscaped<&str> = PreEscaped(include_str!("assets/db.svg")); 12 | const LOGS_SVG: PreEscaped<&str> = PreEscaped(include_str!("assets/logs.svg")); 13 | const ANALYTICS_SVG: PreEscaped<&str> = PreEscaped(include_str!("assets/analytics.svg")); 14 | const LOADER_SVG: PreEscaped<&str> = PreEscaped(include_str!("assets/loader.svg")); 15 | 16 | pub(crate) async fn routes() -> Router { 17 | route( 18 | "/", 19 | get(|| async { 20 | ok(html!( 21 | (monitoring::container().await?) 22 | a get="/admin/remote/state" trigger="load" swap-this {} 23 | (logs::info_explorer().await) 24 | )) 25 | }), 26 | ) 27 | .route("/monitoring", get(monitoring::container)) 28 | .route("/latest_info", get(logs::info_explorer)) 29 | .route("/latest_info/:offset", get(logs::info)) 30 | .route("/traces", get(logs::traces_explorer)) 31 | .route("/schedule", get(schedule::full)) 32 | .route("/analytics", get(analytics::full)) 33 | .route("/db", get(db::db_page)) 34 | .nest("/remote", remote::routes()) 35 | .wrap_non_htmx(into_page) 36 | .nest("/db", db::table_routes()) 37 | .route("/db/schema", get(db::schema)) 38 | .route("/traces/:period", get(logs::traces)) 39 | .route("/monitoring/data", get(monitoring::data)) 40 | } 41 | 42 | async fn into_page(content: Markup) -> impl IntoResponse { 43 | html! {(DOCTYPE) html $"bg-stone-800 font-sans text-gray-300" { 44 | (Head::with_title("Prest Admin")) 45 | body $"max-w-screen-md lg:max-w-screen-lg md:mx-auto" { 46 | nav replace-url into="main" $"bg-stone-900 my-4 p-5 shadow-lg rounded-full items-center flex gap-6 w-min mx-auto" { 47 | a href="/" boost="false" {(home_svg())} 48 | button get="/admin" {$"w-6" {(ADMIN_SVG)}} 49 | button get="/admin/analytics" {$"w-6" {(ANALYTICS_SVG)}} 50 | button get="/admin/traces" {$"w-6" {(LOGS_SVG)}} 51 | @if DB.custom_schemas().len() > 0 { 52 | button get="/admin/db" {$"w-6" {(DB_SVG)}} 53 | } 54 | } 55 | main $"opacity-80 mx-auto p-4 gap-4 flex flex-col text-sm lg:text-base leading-loose" { 56 | (content) 57 | } 58 | (Scripts::default().include("/traces.js").include("/db.js").module("/stats.js").css("/admin.css")) 59 | } 60 | }} 61 | } 62 | 63 | fn home_svg() -> Markup { 64 | html!( 65 | svg $"w-6" viewBox="0 0 16 16" fill="none" { 66 | path d="M1 6V15H6V11C6 9.89543 6.89543 9 8 9C9.10457 9 10 9.89543 10 11V15H15V6L8 0L1 6Z" fill="currentColor" {} 67 | } 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /examples/polkadot/host/main.rs: -------------------------------------------------------------------------------- 1 | 2 | use runtime::*; 3 | use clap::Parser; 4 | use sc_cli::SubstrateCli; 5 | use sc_executor::WasmExecutor; 6 | 7 | #[derive(Debug, clap::Parser)] 8 | pub struct Cli { 9 | #[clap(flatten)] 10 | pub run: sc_cli::RunCmd, 11 | } 12 | impl SubstrateCli for Cli { 13 | fn impl_name() -> String { 14 | "Minimal Polkadot SDK Node".into() 15 | } 16 | fn impl_version() -> String { 17 | env!("CARGO_PKG_VERSION").into() 18 | } 19 | fn description() -> String { 20 | env!("CARGO_PKG_DESCRIPTION").into() 21 | } 22 | fn author() -> String { 23 | env!("CARGO_PKG_AUTHORS").into() 24 | } 25 | fn support_url() -> String { 26 | "https://github.com/your-username/minimal-polkadot-sdk-node/issues".into() 27 | } 28 | fn copyright_start_year() -> i32 { 29 | 2024 30 | } 31 | fn load_spec(&self, _: &str) -> Result, String> { 32 | Ok(Box::new(crate::chain_spec::development_config()?)) 33 | } 34 | } 35 | 36 | pub mod chain_spec { 37 | use super::*; 38 | use sc_service::{ChainType, GenericChainSpec}; 39 | use serde::Serialize; 40 | use sp_runtime::BuildStorage; 41 | use sc_chain_spec::GetExtension; 42 | use std::any::{Any, TypeId}; 43 | 44 | #[derive(Default, Serialize, Clone)] 45 | pub struct RuntimeGenesisConfig; 46 | 47 | impl GetExtension for RuntimeGenesisConfig { 48 | fn get_any(&self, _: TypeId) -> &dyn Any { 49 | &() 50 | } 51 | fn get_any_mut(&mut self, _: TypeId) -> &mut dyn Any { 52 | todo!() 53 | } 54 | } 55 | 56 | impl BuildStorage for RuntimeGenesisConfig { 57 | fn assimilate_storage( 58 | &self, 59 | storage: &mut sp_core::storage::Storage, 60 | ) -> Result<(), String> { 61 | frame_system::GenesisConfig::::default().assimilate_storage(storage) 62 | } 63 | } 64 | 65 | pub fn development_config() -> Result, String> { 66 | const WASM_BINARY: &[u8] = &[];//include_bytes!("../target/wasm32-unknown-unknown/release/polkadot.wasm"); 67 | let chain_spec = GenericChainSpec::builder(WASM_BINARY, Default::default()) 68 | .with_name("Development") 69 | .with_id("dev") 70 | .with_chain_type(ChainType::Development) 71 | .build(); 72 | Ok(chain_spec) 73 | } 74 | } 75 | 76 | fn main() -> sc_cli::Result<()> { 77 | let cli = Cli::parse(); 78 | let runner = cli.create_runner(&cli.run)?; 79 | runner.run_node_until_exit(|config| async move { 80 | let executor: WasmExecutor = 81 | sc_executor::WasmExecutor::builder() 82 | .build(); 83 | let (_, _, _, task_manager) = 84 | sc_service::new_full_parts::(&config, None, executor)?; 85 | Ok(task_manager) 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /examples/databases/mongo-driver/src/main.rs: -------------------------------------------------------------------------------- 1 | use mongodb::{ 2 | bson::{doc, Uuid}, 3 | options::ClientOptions, 4 | Client, Collection, 5 | }; 6 | use prest::*; 7 | 8 | state!(TODOS: Collection = async { 9 | let opts = ClientOptions::parse("mongodb://localhost:27017").await?; 10 | let client = Client::with_options(opts)?; 11 | let db = client.database("todosdb"); 12 | db.collection::("todos") 13 | }); 14 | 15 | #[derive(Clone, Serialize, Deserialize)] 16 | pub struct Todo { 17 | #[serde(default)] 18 | pub uuid: Uuid, 19 | #[serde(default)] 20 | pub task: String, 21 | #[serde(default)] 22 | pub done: bool, 23 | } 24 | 25 | #[init] 26 | async fn main() -> Result { 27 | route( 28 | "/", 29 | get(|| async { 30 | let todos: Vec = TODOS 31 | .find(None, None) 32 | .await 33 | .unwrap() 34 | .try_collect() 35 | .await 36 | .unwrap(); 37 | todos.render() 38 | }) 39 | .put(|Vals(Todo { task, .. }): Vals| async move { 40 | let new_todo = Todo { 41 | uuid: Uuid::new(), 42 | task, 43 | done: false, 44 | }; 45 | TODOS.insert_one(&new_todo, None).await.unwrap(); 46 | new_todo.render() 47 | }) 48 | .patch(|Vals(Todo { uuid, done, .. }): Vals| async move { 49 | TODOS 50 | .update_one(doc! {"uuid": uuid}, doc! {"$set": {"done": !done}}, None) 51 | .await 52 | .unwrap(); 53 | TODOS 54 | .find_one(doc! {"uuid": uuid}, None) 55 | .await 56 | .unwrap() 57 | .unwrap() 58 | .render() 59 | }) 60 | .delete(|Vals(Todo { uuid, .. }): Vals| async move { 61 | TODOS.delete_one(doc! {"uuid": uuid}, None).await.unwrap(); 62 | }), 63 | ) 64 | .wrap_non_htmx(page) 65 | .run() 66 | .await 67 | } 68 | 69 | impl Render for Todo { 70 | fn render(&self) -> Markup { 71 | html!( 72 | $"flex items-center" swap-this vals=(json!(self)) { 73 | input type="checkbox" patch="/" checked[self.done] {} 74 | label $"ml-4 text-lg" {(self.task)} 75 | button $"ml-auto" detele="/" {"Delete"} 76 | } 77 | ) 78 | } 79 | } 80 | 81 | async fn page(content: Markup) -> Markup { 82 | html! { html { (Head::with_title("With Mongo")) 83 | body $"max-w-screen-sm mx-auto mt-12" { 84 | form $"flex gap-4 justify-center" put="/" into-end-of="#list" after-request="this.reset()" { 85 | input $"border rounded-md" type="text" name="task" {} 86 | button type="submit" {"Add"} 87 | } 88 | div #list $"w-full" {(content)} 89 | (Scripts::default()) 90 | } 91 | }} 92 | } 93 | -------------------------------------------------------------------------------- /db/macro/from_glue_value.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use proc_macro2::TokenStream; 3 | 4 | pub fn from_glue_value((index, col): (usize, &Column)) -> TokenStream { 5 | let Column { 6 | field_name, 7 | inner_type, 8 | sql_type, 9 | optional, 10 | list, 11 | .. 12 | } = col; 13 | 14 | let value_variant = ident(col.value_variant()); 15 | 16 | let transform = match col.value_transform() { 17 | ValueTransform::UuidU128 => q!(let v = prest::Uuid::from_u128(v)), 18 | ValueTransform::SerDe => q!(let v = prest::from_bitcode(&v)?), 19 | ValueTransform::None => q!(), 20 | }; 21 | 22 | let error_arms = q!( 23 | Some(other) => { 24 | let column = &Self::FIELD_SCHEMAS[#index]; 25 | return Err(prest::e!("unexpected value {other:?} for {column:?}")) 26 | } 27 | None => { 28 | let column = &Self::FIELD_SCHEMAS[#index]; 29 | return Err(prest::e!("row too short, missing {column:?}")) 30 | } 31 | ); 32 | 33 | if *list { 34 | let err = q!( 35 | let column = &Self::FIELD_SCHEMAS[#index]; 36 | return Err(prest::e!("unexpected list item value {item:?} in {column:?}")) 37 | ); 38 | let match_and_push = if sql_type.int_or_smaller() { 39 | q!( 40 | use prest::sql::Value::*; 41 | let v = match item { 42 | I8(v) => v as #inner_type, 43 | I16(v) => v as #inner_type, 44 | I32(v) => v as #inner_type, 45 | I64(v) => v as #inner_type, 46 | U8(v) => v as #inner_type, 47 | U16(v) => v as #inner_type, 48 | U32(v) => v as #inner_type, 49 | item => { #err } 50 | }; 51 | list.push(v); 52 | ) 53 | } else { 54 | q!( 55 | if let prest::sql::Value::#value_variant(v) = item { 56 | #transform; 57 | list.push(v); 58 | } else { #err } 59 | ) 60 | }; 61 | q!( 62 | let #field_name = match row.pop() { 63 | Some(prest::sql::Value::List(values)) => { 64 | let mut list = vec![]; 65 | for item in values.into_iter() { #match_and_push } 66 | list 67 | } 68 | #error_arms 69 | }; 70 | ) 71 | } else { 72 | let res = if *optional { q!(Some(v)) } else { q!(v) }; 73 | let null_arm = match optional { 74 | true => q!( Some(prest::sql::Value::Null) => None, ), 75 | false => q!(), 76 | }; 77 | q! { 78 | let #field_name = match row.pop() { 79 | Some(prest::sql::Value::#value_variant(v)) => {#transform; #res} 80 | #null_arm 81 | #error_arms 82 | }; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/scraping/src/main.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | use reqwest::get as fetch; 3 | use scraper::{Html, Selector}; 4 | 5 | #[derive(Storage, Serialize, Deserialize)] 6 | struct Story { 7 | pub title: String, 8 | pub content: String, 9 | } 10 | 11 | #[init(log_filters=[("html5ever", "info"), ("selectors", "info")])] 12 | async fn main() -> Result { 13 | spawn(scrape( 14 | "https://apnews.com", 15 | Selector::parse(".Page-content .PageList-items-item a").unwrap(), 16 | Selector::parse("h1.Page-headline").unwrap(), 17 | Selector::parse(".RichTextStoryBody > p").unwrap(), 18 | )); 19 | 20 | route( 21 | "/", 22 | get(|| async { 23 | ok(html!(html {(Head::with_title("With scraping")) 24 | body { @for story in Story::get_all().await? { 25 | div $"my-2" { 26 | h3 {(story.title)} 27 | div $"text-sm" {(format!("{:.150}...", story.content))} 28 | } 29 | }} 30 | })) 31 | }), 32 | ) 33 | .run() 34 | .await 35 | } 36 | 37 | async fn scrape( 38 | url: &str, 39 | links_selector: Selector, 40 | title_selector: Selector, 41 | content_selector: Selector, 42 | ) -> Somehow { 43 | let text = fetch(url).await?.text().await?; 44 | 45 | let links = get_links(text, &links_selector); 46 | 47 | // restricting amount of parsed pages 48 | let links = &links[0..5]; 49 | 50 | let responses = join_all(links.into_iter().map(|link| fetch(link))) 51 | .await 52 | .into_iter() 53 | .filter_map(|resp| resp.ok()); 54 | 55 | let stories: Vec = join_all(responses.map(|resp| resp.text())) 56 | .await 57 | .into_iter() 58 | .filter_map(|text| text.ok()) 59 | .map(|text| get_content(text, &title_selector, &content_selector)) 60 | .collect(); 61 | 62 | for story in stories { 63 | story.save().await?; 64 | } 65 | 66 | Ok(()) 67 | } 68 | 69 | fn get_content(text: String, title_selector: &Selector, content_selector: &Selector) -> Story { 70 | let document = Html::parse_document(&text); 71 | 72 | let title = document 73 | .select(title_selector) 74 | .map(|t| t.inner_html()) 75 | .next() 76 | .unwrap(); 77 | 78 | let content = document 79 | .select(content_selector) 80 | .fold(String::new(), |full, p| { 81 | p.text().fold(full, |full, text| full + text) + "\n" 82 | }); 83 | 84 | Story { title, content } 85 | } 86 | 87 | fn get_links(text: String, selector: &Selector) -> Vec { 88 | let document = Html::parse_document(&text); 89 | 90 | let mut links = document 91 | .select(&selector) 92 | .filter_map(|x| x.value().attr("href")) 93 | .map(ToOwned::to_owned) 94 | .collect::>(); 95 | 96 | links.sort_unstable(); 97 | links.dedup(); 98 | links 99 | } 100 | -------------------------------------------------------------------------------- /examples/databases/postgres-diesel/serve.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | pub mod schema; 3 | 4 | use prest::*; 5 | 6 | use diesel::prelude::*; 7 | use diesel_async::{ 8 | pooled_connection::{deadpool::Pool, AsyncDieselConnectionManager}, 9 | AsyncPgConnection, RunQueryDsl, 10 | }; 11 | use models::Todo; 12 | use schema::todos::dsl::*; 13 | 14 | state!(DB_POOL: Pool = { 15 | let database_url = "postgres://postgres:password@localhost/prest"; 16 | let config = AsyncDieselConnectionManager::::new(database_url); 17 | Pool::builder(config).build()? 18 | }); 19 | 20 | #[init] 21 | async fn main() -> Result { 22 | route( 23 | "/", 24 | get(|| async { get_todos().await.render() }) 25 | .patch(toggle_todo) 26 | .put(add_todo) 27 | .delete(delete_todo), 28 | ) 29 | .wrap_non_htmx(page) 30 | .run() 31 | .await 32 | } 33 | 34 | async fn get_todos() -> Vec { 35 | let mut con = DB_POOL.get().await.unwrap(); 36 | todos 37 | .select(Todo::as_select()) 38 | .load(&mut con) 39 | .await 40 | .expect("successful select query") 41 | } 42 | 43 | async fn toggle_todo(Vals(todo): Vals) -> Markup { 44 | let mut con = DB_POOL.get().await.unwrap(); 45 | diesel::update(todos.find(todo.uuid)) 46 | .set(done.eq(!todo.done)) 47 | .returning(Todo::as_returning()) 48 | .get_result(&mut con) 49 | .await 50 | .expect("successful update query") 51 | .render() 52 | } 53 | 54 | async fn add_todo(Vals(todo): Vals) -> Markup { 55 | let mut con = DB_POOL.get().await.unwrap(); 56 | diesel::insert_into(todos) 57 | .values(&todo) 58 | .returning(Todo::as_returning()) 59 | .get_result(&mut con) 60 | .await 61 | .expect("successful insert query") 62 | .render() 63 | } 64 | 65 | async fn delete_todo(Vals(todo): Vals) { 66 | let mut con = DB_POOL.get().await.unwrap(); 67 | diesel::delete(todos.filter(uuid.eq(todo.uuid))) 68 | .execute(&mut con) 69 | .await 70 | .expect("successful delete query"); 71 | } 72 | 73 | impl Render for Todo { 74 | fn render(&self) -> Markup { 75 | html! { 76 | $"flex items-center" swap-this vals=(json!(self)) { 77 | input type="checkbox" patch="/" checked[self.done] {} 78 | label $"ml-4 text-lg" {(self.task)} 79 | button $"ml-auto" detele="/" {"Delete"} 80 | } 81 | } 82 | } 83 | } 84 | 85 | async fn page(content: Markup) -> Markup { 86 | html! { html { (Head::with_title("With Diesel Postgres")) 87 | body $"max-w-screen-sm mx-auto mt-12" { 88 | form $"flex gap-4 justify-center" put="/" into-end-of="#list" after-request="this.reset()" { 89 | input $"border rounded-md" type="text" name="task" {} 90 | button type="submit" {"Add"} 91 | } 92 | div #list $"w-full" {(content)} 93 | (Scripts::default()) 94 | } 95 | }} 96 | } 97 | -------------------------------------------------------------------------------- /examples/polkadot/host/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "host" 5 | edition = "2021" 6 | 7 | [[bin]] 8 | name = "host" 9 | path = "./main.rs" 10 | 11 | [features] 12 | default = ["std"] 13 | try-runtime = [] 14 | std = [ 15 | "codec/std", 16 | "frame-support/std", 17 | "frame-system/std", 18 | "sp-core/std", 19 | "sp-runtime/std", 20 | "scale-info/std", 21 | "pallet-transaction-payment/std", 22 | "pallet-balances/std", 23 | "sp-api/std", 24 | "sp-block-builder/std", 25 | "sp-transaction-pool/std", 26 | "sp-inherents/std", 27 | ] 28 | 29 | [dependencies] 30 | runtime = { path = "../runtime" } 31 | #prest = "0.4" 32 | 33 | getrandom = { version = "0.2", features = ["js"] } 34 | 35 | codec = { package = "parity-scale-codec", version = "3.6", features = ["derive"] } 36 | scale-info = { version = "2.11.1", default-features = false, features = ["derive"] } 37 | frame-support = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 38 | frame-system = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 39 | frame-executive = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 40 | sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 41 | sp-core = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 42 | sp-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 43 | sp-version = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 44 | sp-block-builder = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 45 | sp-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 46 | sp-inherents = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 47 | pallet-transaction-payment = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } 48 | pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } 49 | 50 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 51 | clap = { version = "4.0", features = ["derive"] } 52 | 53 | sp-io = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 54 | sc-service = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 55 | sc-executor = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 56 | sc-chain-spec = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 57 | sc-cli = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 58 | sp-inherents = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 59 | sp-std = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } 60 | serde = { version = "1.0", features = ["derive"] } 61 | serde_json = "*" 62 | 63 | [build-dependencies] 64 | polkadot-sdk = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", features = ["substrate-build-script-utils"] } 65 | -------------------------------------------------------------------------------- /examples/todo-pwa-auth/src/main.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | use todo_pwa_auth::{into_page, shared_routes}; 3 | 4 | embed_build_output_as!(BuiltAssets); 5 | 6 | #[derive(Storage, Default, Serialize, Deserialize)] 7 | #[serde(default)] 8 | struct Todo { 9 | #[serde(default = "Uuid::now_v7")] 10 | pub id: Uuid, 11 | #[serde(default)] 12 | pub owner: Uuid, 13 | pub task: String, 14 | pub done: bool, 15 | } 16 | 17 | impl Render for Todo { 18 | fn render(&self) -> Markup { 19 | html! { 20 | $"flex justify-between items-center" swap-this vals=(json!(self)) { 21 | input type="checkbox" patch="/todos" checked[self.done] {} 22 | label $"ml-4 text-lg" {(self.task)} 23 | button $"ml-auto" delete="/todos" {"Delete"} 24 | } 25 | } 26 | } 27 | } 28 | 29 | #[init] 30 | async fn main() -> Result { 31 | shared_routes() 32 | .route( 33 | "/todos", 34 | get(|auth: Auth| async move { 35 | ok(html!( 36 | @if let Some(user) = auth.user { 37 | form put="/todos" into-end-of="#list" after-request="this.reset()" { 38 | input $"border rounded-md" type="text" name="task" {} 39 | button $"ml-4" type="submit" {"Add"} 40 | } 41 | div #list $"w-full" {(Todo::select_by_owner(&user.id).await?)} 42 | } @else { 43 | @if *WITH_GOOGLE_AUTH { 44 | a $"p-4 border rounded-md" href=(GOOGLE_LOGIN_ROUTE) {"Login with Google"} 45 | div {"OR"} 46 | } 47 | form $"flex flex-col gap-4 items-center" method="POST" action=(LOGIN_ROUTE) { 48 | input $"border rounded-md mx-4" type="text" name="username" placeholder="username" {} 49 | input $"border rounded-md mx-4" type="password" name="password" placeholder="password" {} 50 | input type="hidden" name="signup" value="true" {} 51 | button $"ml-4" type="submit" {"Sign in / Sign up"} 52 | } 53 | } 54 | )) 55 | }) 56 | .put(|user: User, Vals(mut todo): Vals| async move { 57 | todo.owner = user.id; 58 | ok(todo.save().await?.render()) 59 | }) 60 | .patch(|user: User, Vals(mut todo): Vals| async move { 61 | if !todo.check_owner(user.id).await? { 62 | return Err(Error::Unauthorized); 63 | } 64 | Ok(todo.update_done(!todo.done).await?.render()) 65 | }) 66 | .delete(|user: User, Vals(todo): Vals| async move { 67 | if !todo.check_owner(user.id).await? { 68 | return Err(Error::Unauthorized); 69 | } 70 | Ok(todo.remove().await?) 71 | }), 72 | ) 73 | .wrap_non_htmx(into_page) 74 | .embed(BuiltAssets) 75 | .run() 76 | .await 77 | } 78 | -------------------------------------------------------------------------------- /serde_derive_fork/src/internals/name.rs: -------------------------------------------------------------------------------- 1 | use crate::internals::attr::{Attr, VecAttr}; 2 | use proc_macro2::{Ident, Span, TokenStream}; 3 | use quote::ToTokens; 4 | use std::cmp::Ordering; 5 | use std::collections::BTreeSet; 6 | use std::fmt::{self, Display}; 7 | use syn::LitStr; 8 | 9 | pub struct MultiName { 10 | pub(crate) serialize: Name, 11 | pub(crate) serialize_renamed: bool, 12 | pub(crate) deserialize: Name, 13 | pub(crate) deserialize_renamed: bool, 14 | pub(crate) deserialize_aliases: BTreeSet, 15 | } 16 | 17 | impl MultiName { 18 | pub(crate) fn from_attrs( 19 | source_name: Name, 20 | ser_name: Attr, 21 | de_name: Attr, 22 | de_aliases: Option>, 23 | ) -> Self { 24 | let mut alias_set = BTreeSet::new(); 25 | if let Some(de_aliases) = de_aliases { 26 | for alias_name in de_aliases.get() { 27 | alias_set.insert(alias_name); 28 | } 29 | } 30 | 31 | let ser_name = ser_name.get(); 32 | let ser_renamed = ser_name.is_some(); 33 | let de_name = de_name.get(); 34 | let de_renamed = de_name.is_some(); 35 | MultiName { 36 | serialize: ser_name.unwrap_or_else(|| source_name.clone()), 37 | serialize_renamed: ser_renamed, 38 | deserialize: de_name.unwrap_or(source_name), 39 | deserialize_renamed: de_renamed, 40 | deserialize_aliases: alias_set, 41 | } 42 | } 43 | 44 | /// Return the container name for the container when serializing. 45 | pub fn serialize_name(&self) -> &Name { 46 | &self.serialize 47 | } 48 | 49 | /// Return the container name for the container when deserializing. 50 | pub fn deserialize_name(&self) -> &Name { 51 | &self.deserialize 52 | } 53 | 54 | pub(crate) fn deserialize_aliases(&self) -> &BTreeSet { 55 | &self.deserialize_aliases 56 | } 57 | } 58 | 59 | #[derive(Clone)] 60 | pub struct Name { 61 | pub value: String, 62 | pub span: Span, 63 | } 64 | 65 | impl ToTokens for Name { 66 | fn to_tokens(&self, tokens: &mut TokenStream) { 67 | LitStr::new(&self.value, self.span).to_tokens(tokens); 68 | } 69 | } 70 | 71 | impl Ord for Name { 72 | fn cmp(&self, other: &Self) -> Ordering { 73 | Ord::cmp(&self.value, &other.value) 74 | } 75 | } 76 | 77 | impl PartialOrd for Name { 78 | fn partial_cmp(&self, other: &Self) -> Option { 79 | Some(Ord::cmp(self, other)) 80 | } 81 | } 82 | 83 | impl Eq for Name {} 84 | 85 | impl PartialEq for Name { 86 | fn eq(&self, other: &Self) -> bool { 87 | self.value == other.value 88 | } 89 | } 90 | 91 | impl From<&Ident> for Name { 92 | fn from(ident: &Ident) -> Self { 93 | Name { 94 | value: ident.to_string(), 95 | span: ident.span(), 96 | } 97 | } 98 | } 99 | 100 | impl From<&LitStr> for Name { 101 | fn from(lit: &LitStr) -> Self { 102 | Name { 103 | value: lit.value(), 104 | span: lit.span(), 105 | } 106 | } 107 | } 108 | 109 | impl Display for Name { 110 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 111 | Display::fmt(&self.value, formatter) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /examples/databases/postgres-seaorm/serve.rs: -------------------------------------------------------------------------------- 1 | mod entities; 2 | mod migrator; 3 | 4 | use prest::*; 5 | 6 | use entities::{prelude::*, *}; 7 | use sea_orm::{ActiveModelTrait, ActiveValue, Database, DatabaseConnection, EntityTrait}; 8 | use sea_orm_migration::migrator::MigratorTrait; 9 | 10 | state!(DB: DatabaseConnection = async { 11 | let db = Database::connect("postgres://postgres:password@localhost/prest").await?; 12 | migrator::Migrator::refresh(&db).await?; 13 | db 14 | }); 15 | 16 | #[derive(Deserialize)] 17 | struct NewTodo { 18 | task: String, 19 | } 20 | 21 | #[derive(Deserialize)] 22 | struct ToggleTodo { 23 | uuid: Uuid, 24 | done: bool, 25 | } 26 | 27 | #[derive(Deserialize)] 28 | struct DeleteTodo { 29 | uuid: Uuid, 30 | } 31 | 32 | #[init] 33 | async fn main() -> Result { 34 | route( 35 | "/", 36 | get(|| async { Todos::find().all(&*DB).await.unwrap().render() }) 37 | .put(|Vals(NewTodo { task }): Vals| async move { 38 | todos::ActiveModel { 39 | uuid: ActiveValue::Set(Uuid::now_v7()), 40 | task: ActiveValue::Set(task), 41 | done: ActiveValue::Set(false), 42 | } 43 | .insert(&*DB) 44 | .await 45 | .unwrap(); 46 | Redirect::to("/") 47 | }) 48 | .patch( 49 | |Vals(ToggleTodo { uuid, done }): Vals| async move { 50 | todos::ActiveModel { 51 | uuid: ActiveValue::Set(uuid), 52 | done: ActiveValue::Set(!done), 53 | ..Default::default() 54 | } 55 | .update(&*DB) 56 | .await 57 | .unwrap(); 58 | Redirect::to("/") 59 | }, 60 | ) 61 | .delete(|Vals(DeleteTodo { uuid }): Vals| async move { 62 | todos::ActiveModel { 63 | uuid: ActiveValue::Set(uuid), 64 | ..Default::default() 65 | } 66 | .delete(&*DB) 67 | .await 68 | .unwrap(); 69 | Redirect::to("/") 70 | }), 71 | ) 72 | .wrap_non_htmx(page) 73 | .run() 74 | .await 75 | } 76 | 77 | impl Render for todos::Model { 78 | fn render(&self) -> Markup { 79 | html!( 80 | $"flex items-center" vals=(json!(self)) { 81 | input type="checkbox" patch="/" checked[self.done] {} 82 | label $"ml-4 text-lg" {(self.task)} 83 | button $"ml-auto" detele="/" {"Delete"} 84 | } 85 | ) 86 | } 87 | } 88 | 89 | async fn page(content: Markup) -> Markup { 90 | html! { html data-theme="dark" { 91 | (Head::with_title("With SeaORM Postgres")) 92 | body $"max-w-screen-sm mx-auto mt-12" target="div" { 93 | form $"flex gap-4 justify-center" put="/" into-end-of="#list" after-request="this.reset()" { 94 | input $"border rounded-md" type="text" name="task" {} 95 | button type="submit" {"Add"} 96 | } 97 | div #list $"w-full" {(content)} 98 | (Scripts::default()) 99 | } 100 | }} 101 | } 102 | -------------------------------------------------------------------------------- /db/macro/into_glue_expr.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use proc_macro2::TokenStream; 3 | 4 | pub fn into_glue_expr(column: &Column, path: TokenStream, deref: bool, inner: bool) -> TokenStream { 5 | let Column { 6 | sql_type, 7 | list, 8 | optional, 9 | serialized, 10 | .. 11 | } = column; 12 | let optional = *optional && !inner; 13 | match (list, optional, serialized) { 14 | (true, false, _) => { 15 | let literal = |ts: TokenStream| q!(sql::Expr::Literal(sql::AstLiteral::#ts)); 16 | 17 | let item_into_expr = match sql_type { 18 | _ if *serialized => literal(q!(HexString(prest::hex::encode(prest::into_bitcode(item)?)))), 19 | _ if sql_type.integer() => literal(q!(Number(item.into()))), 20 | SqlType::Boolean => literal(q!(Boolean(*item))), 21 | SqlType::Text => literal(q!(QuotedString(item.to_string()))), 22 | _ => unimplemented!("Vec of this type is not currently supported due to complexities related to the untyped nature of gluesql lists"), 23 | }; 24 | 25 | q!({ 26 | let mut elem = vec![]; 27 | for item in #path.iter() { 28 | elem.push(#item_into_expr) 29 | } 30 | sql::ExprNode::Expr(std::borrow::Cow::Owned(sql::Expr::Array { elem })) 31 | }) 32 | } 33 | (false, false, true) => q!(sql::bytea(prest::into_bitcode(&#path)?)), 34 | (false, true, true) => q!( 35 | if let Some(v) = &#path { sql::bytea(prest::into_bitcode(v)?)} 36 | else { sql::null() } 37 | ), 38 | (false, true, false) => { 39 | let inner = match sql_type { 40 | SqlType::Text => q!(sql::text(v.clone())), 41 | SqlType::Uuid => q!(sql::uuid(v.to_string())), 42 | SqlType::Boolean => node_literal(q!(Boolean(*v))), 43 | // these do not implement Into 44 | SqlType::Int128 | SqlType::Uint128 => q!(sql::num(v.to_string())), 45 | _ if sql_type.numeric() => q!(sql::num(*v)), 46 | _ => q!(sql::expr(v.to_string())), 47 | }; 48 | q!( if let Some(v) = &#path { #inner } else { sql::null() } ) 49 | } 50 | (false, false, false) => { 51 | match sql_type { 52 | SqlType::Boolean => node_literal(q!(Boolean(#path.clone()))), 53 | SqlType::Text => q!(sql::text(#path.clone())), 54 | SqlType::Uuid => q!(sql::uuid(#path.to_string())), 55 | // these do not implement Into 56 | SqlType::Int128 | SqlType::Uint128 => q!(sql::num(#path.to_string())), 57 | _ if sql_type.numeric() && deref => q!(sql::num(*#path)), 58 | _ if sql_type.numeric() => q!(sql::num(#path)), 59 | _ => q!(sql::expr(format!("'{}'", &#path))), 60 | } 61 | } 62 | (true, true, _) => { 63 | unreachable!("doesn't support combinations of Vec<> and Option<> in the analyzer") 64 | } 65 | } 66 | } 67 | 68 | fn node_literal(variant: TokenStream) -> TokenStream { 69 | let expr = expr_literal(variant); 70 | q!(prest::sql::ExprNode::Expr(std::borrow::Cow::Owned(#expr))) 71 | } 72 | 73 | fn expr_literal(variant: TokenStream) -> TokenStream { 74 | q!(prest::sql::Expr::Literal(prest::sql::AstLiteral::#variant)) 75 | } 76 | -------------------------------------------------------------------------------- /cotton/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod cache; 2 | mod config; 3 | mod npm; 4 | mod package; 5 | mod plan; 6 | mod progress; 7 | mod resolve; 8 | mod scoped_path; 9 | mod util; 10 | 11 | use color_eyre::eyre::Result; 12 | use color_eyre::owo_colors::OwoColorize; 13 | use compact_str::ToCompactString; 14 | use itertools::Itertools; 15 | use package::PackageMetadata; 16 | use plan::tree_size; 17 | use progress::{log_progress, log_verbose}; 18 | use resolve::Lockfile; 19 | use std::collections::HashMap; 20 | use std::time::Instant; 21 | use tokio::fs::create_dir_all; 22 | use tokio::fs::read_to_string; 23 | use util::{read_package, write_json}; 24 | 25 | use crate::util::load_graph_from_lockfile; 26 | use crate::{ 27 | plan::{execute_plan, Plan}, 28 | progress::PROGRESS_BAR, 29 | }; 30 | 31 | pub const STORE_PATH: &str = "./target/.cotton/store"; 32 | pub const NM_COTTON_PATH: &str = "./node_modules/.cotton"; 33 | pub const NM_COTTON_PLAN_PATH: &str = "./node_modules/.cotton/plan.json"; 34 | 35 | #[tokio::main] 36 | pub async fn run() -> Result> { 37 | let package = read_package().await?; 38 | 39 | init_storage().await?; 40 | 41 | let start = Instant::now(); 42 | 43 | let plan = prepare_plan(&package).await?; 44 | let size = tree_size(&plan.trees); 45 | 46 | if matches!(verify_installation(&package, &plan).await, Ok(true)) { 47 | log_verbose("Packages already installed") 48 | } else { 49 | execute_plan(plan.clone()).await?; 50 | 51 | PROGRESS_BAR.suspend(|| { 52 | if size > 0 { 53 | println!( 54 | "Installed {} packages in {}ms", 55 | size.yellow(), 56 | start.elapsed().as_millis().yellow() 57 | ) 58 | } 59 | }); 60 | write_json(NM_COTTON_PLAN_PATH, &plan).await?; 61 | } 62 | 63 | PROGRESS_BAR.finish_and_clear(); 64 | 65 | Ok(package.exports) 66 | } 67 | 68 | async fn prepare_plan(package: &PackageMetadata) -> Result { 69 | log_progress("Preparing"); 70 | 71 | let mut graph = load_graph_from_lockfile().await; 72 | 73 | graph.append(package.iter_all(), true).await?; 74 | write_json("cotton.lock", Lockfile::new(graph.clone())).await?; 75 | 76 | log_progress("Retrieved dependency graph"); 77 | 78 | let trees = graph.build_trees(&package.iter_all().collect_vec())?; 79 | log_progress(&format!("Fetched {} root deps", trees.len().yellow())); 80 | 81 | let plan = Plan::new( 82 | trees 83 | .iter() 84 | .map(|x| (x.root.name.to_compact_string(), x.clone())) 85 | .collect(), 86 | ); 87 | 88 | log_progress(&format!( 89 | "Planned {} dependencies", 90 | plan.trees.len().yellow() 91 | )); 92 | 93 | Ok(plan) 94 | } 95 | 96 | async fn read_plan(path: &str) -> Result { 97 | let plan = read_to_string(path).await?; 98 | Ok(serde_json::from_str(&plan)?) 99 | } 100 | 101 | async fn verify_installation(package: &PackageMetadata, plan: &Plan) -> Result { 102 | let installed = read_plan(NM_COTTON_PLAN_PATH).await?; 103 | 104 | if &installed != plan { 105 | return Ok(false); 106 | } 107 | 108 | Ok(installed.satisfies(package)) 109 | } 110 | 111 | async fn init_storage() -> Result<()> { 112 | create_dir_all(STORE_PATH).await?; 113 | create_dir_all(NM_COTTON_PATH).await?; 114 | Ok(()) 115 | } 116 | -------------------------------------------------------------------------------- /serde_derive_fork/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides Serde's two derive macros. 2 | //! 3 | //! ```edition2021 4 | //! # use serde_derive::{Deserialize, Serialize}; 5 | //! # 6 | //! #[derive(Serialize, Deserialize)] 7 | //! # struct S; 8 | //! # 9 | //! # fn main() {} 10 | //! ``` 11 | //! 12 | //! Please refer to [https://serde.rs/derive.html] for how to set this up. 13 | //! 14 | //! [https://serde.rs/derive.html]: https://serde.rs/derive.html 15 | 16 | #![doc(html_root_url = "https://docs.rs/serde_derive/1.0.216")] 17 | #![cfg_attr(not(check_cfg), allow(unexpected_cfgs))] 18 | // Ignored clippy lints 19 | #![allow( 20 | // clippy false positive: https://github.com/rust-lang/rust-clippy/issues/7054 21 | clippy::branches_sharing_code, 22 | clippy::cognitive_complexity, 23 | // clippy bug: https://github.com/rust-lang/rust-clippy/issues/7575 24 | clippy::collapsible_match, 25 | clippy::derive_partial_eq_without_eq, 26 | clippy::enum_variant_names, 27 | // clippy bug: https://github.com/rust-lang/rust-clippy/issues/6797 28 | clippy::manual_map, 29 | clippy::match_like_matches_macro, 30 | clippy::needless_lifetimes, 31 | clippy::needless_pass_by_value, 32 | clippy::too_many_arguments, 33 | clippy::trivially_copy_pass_by_ref, 34 | clippy::used_underscore_binding, 35 | clippy::wildcard_in_or_patterns, 36 | // clippy bug: https://github.com/rust-lang/rust-clippy/issues/5704 37 | clippy::unnested_or_patterns, 38 | )] 39 | // Ignored clippy_pedantic lints 40 | #![allow( 41 | clippy::cast_possible_truncation, 42 | clippy::checked_conversions, 43 | clippy::doc_markdown, 44 | clippy::enum_glob_use, 45 | clippy::indexing_slicing, 46 | clippy::items_after_statements, 47 | clippy::let_underscore_untyped, 48 | clippy::manual_assert, 49 | clippy::map_err_ignore, 50 | clippy::match_same_arms, 51 | // clippy bug: https://github.com/rust-lang/rust-clippy/issues/6984 52 | clippy::match_wildcard_for_single_variants, 53 | clippy::module_name_repetitions, 54 | clippy::must_use_candidate, 55 | clippy::similar_names, 56 | clippy::single_match_else, 57 | clippy::struct_excessive_bools, 58 | clippy::too_many_lines, 59 | clippy::uninlined_format_args, 60 | clippy::unseparated_literal_suffix, 61 | clippy::unused_self, 62 | clippy::use_self, 63 | clippy::wildcard_imports 64 | )] 65 | #![cfg_attr(all(test, exhaustive), feature(non_exhaustive_omitted_patterns_lint))] 66 | 67 | extern crate proc_macro2; 68 | extern crate quote; 69 | extern crate syn; 70 | 71 | extern crate proc_macro; 72 | 73 | mod internals; 74 | 75 | use proc_macro::TokenStream; 76 | use syn::parse_macro_input; 77 | use syn::DeriveInput; 78 | 79 | #[macro_use] 80 | mod bound; 81 | #[macro_use] 82 | mod fragment; 83 | 84 | mod de; 85 | mod dummy; 86 | mod pretend; 87 | mod ser; 88 | mod this; 89 | 90 | #[proc_macro_derive(Serialize, attributes(serde))] 91 | pub fn derive_serialize(input: TokenStream) -> TokenStream { 92 | let mut input = parse_macro_input!(input as DeriveInput); 93 | ser::expand_derive_serialize(&mut input) 94 | .unwrap_or_else(syn::Error::into_compile_error) 95 | .into() 96 | } 97 | 98 | #[proc_macro_derive(Deserialize, attributes(serde))] 99 | pub fn derive_deserialize(input: TokenStream) -> TokenStream { 100 | let mut input = parse_macro_input!(input as DeriveInput); 101 | de::expand_derive_deserialize(&mut input) 102 | .unwrap_or_else(syn::Error::into_compile_error) 103 | .into() 104 | } 105 | -------------------------------------------------------------------------------- /host/docker.rs: -------------------------------------------------------------------------------- 1 | use std::process::Stdio; 2 | 3 | use crate::*; 4 | 5 | static BUILDER_DOCKERFILE: &str = include_str!("release-builder.Dockerfile"); 6 | 7 | const DOCKER_BUILDER_IMAGE: &str = "prest-builder"; 8 | const DOCKER_CARGO_CACHE_DIR: &str = "docker_cargo_cache"; 9 | const RELEASE_TARGET: &str = "x86_64-unknown-linux-musl"; 10 | 11 | pub(crate) fn build_linux_binary() -> Result { 12 | let name = APP_CONFIG.name; 13 | let mut workspace_path = APP_CONFIG.manifest_dir.to_owned(); 14 | 15 | // checking higher-level workspace path required for local dependencies 16 | let mut pb = std::path::PathBuf::from(&workspace_path); 17 | while pb.pop() { 18 | let mut potential_manifest = pb.clone(); 19 | potential_manifest.push("Cargo.toml"); 20 | if let Ok(manifest) = std::fs::read_to_string(&potential_manifest) { 21 | if manifest.contains("[workspace]") && manifest.contains(name) { 22 | let Some(path) = pb.to_str() else { 23 | break; 24 | }; 25 | workspace_path = path.to_owned(); 26 | } 27 | } 28 | } 29 | 30 | let target_path = format!("{workspace_path}/target"); 31 | 32 | prepare_docker_builder(&target_path)?; 33 | 34 | info!(target: "builder", "starting release build for deployment"); 35 | match std::process::Command::new("docker") 36 | .current_dir(workspace_path) 37 | .arg("run") 38 | .arg("--rm") 39 | .args(["--volume", &format!(".:/usr/src/")]) 40 | .args(["--workdir", &format!("/usr/src/")]) 41 | .args([ 42 | "-e", 43 | &format!("CARGO_HOME=/usr/src/target/{DOCKER_CARGO_CACHE_DIR}"), 44 | ]) 45 | .arg(DOCKER_BUILDER_IMAGE) 46 | .args([ 47 | "cargo", 48 | "zigbuild", 49 | "-p", 50 | name, 51 | "--release", 52 | "--target", 53 | RELEASE_TARGET, 54 | "--target-dir", 55 | &format!("./target/{name}"), 56 | ]) 57 | .stdout(std::io::stdout()) 58 | .status() 59 | { 60 | Ok(s) if s.code().filter(|c| *c == 0).is_some() => Ok(format!( 61 | "{target_path}/{name}/{RELEASE_TARGET}/release/{name}" 62 | )), 63 | Ok(s) => Err(e!("Failed to build the linux binary: {s}")), 64 | Err(e) => { 65 | error!(target:"builder", "{e}"); 66 | Err(e!("Failed to start the docker builder image")) 67 | } 68 | } 69 | } 70 | 71 | fn prepare_docker_builder(target_dir: &str) -> Result { 72 | let dockerfile_path = &format!("{target_dir}/Dockerfile"); 73 | 74 | if !std::process::Command::new("docker") 75 | .arg("image") 76 | .arg("inspect") 77 | .arg(DOCKER_BUILDER_IMAGE) 78 | .stdout(Stdio::null()) 79 | .status() 80 | .map_err(|e| e!("failed to check docker builder image: {e}"))? 81 | .success() 82 | { 83 | std::fs::write(dockerfile_path, BUILDER_DOCKERFILE)?; 84 | 85 | if let Err(e) = std::process::Command::new("docker") 86 | .current_dir(target_dir) 87 | .env("DOCKER_CLI_HINTS", "false") 88 | .arg("build") 89 | .args(["-t", DOCKER_BUILDER_IMAGE]) 90 | .args(["-f", dockerfile_path]) 91 | .arg(".") 92 | .stdout(std::io::stdout()) 93 | .status() 94 | { 95 | error!(target:"builder", "{e}"); 96 | return Err(e!("Failed to build the linux binary")); 97 | } 98 | } 99 | OK 100 | } 101 | -------------------------------------------------------------------------------- /host/auth/authn.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | use axum_login::AuthnBackend; 4 | use password_auth::verify_password; 5 | 6 | use thiserror::Error; 7 | #[derive(Error, Debug)] 8 | pub enum AuthError { 9 | #[error("User not found: {0}")] 10 | UserNotFound(String), 11 | #[error("Failed to load: {0}")] 12 | DbError(String), 13 | } 14 | 15 | #[async_trait] 16 | impl AuthnBackend for Prest { 17 | type User = User; 18 | type Credentials = Credentials; 19 | type Error = AuthError; 20 | 21 | async fn authenticate( 22 | &self, 23 | creds: Self::Credentials, 24 | ) -> std::result::Result, Self::Error> { 25 | match creds { 26 | Credentials::GoogleOpenID { code, nonce } => { 27 | if !*WITH_GOOGLE_AUTH { 28 | warn!("Attempted to authenticate with google credentials without google credentials!"); 29 | return Ok(None); // TODO an error here 30 | } 31 | let Ok(email) = GOOGLE_CLIENT.get_email(code, nonce).await else { 32 | return Ok(None); // TODO an error here 33 | }; 34 | let maybe_user = match User::select_by_email(&email).await { 35 | Ok(v) => v, 36 | Err(e) => return Err(AuthError::DbError(format!("User load error: {e}"))), 37 | }; 38 | match maybe_user { 39 | Some(user) => Ok(Some(user)), 40 | None => { 41 | let user = User::from_email(email); 42 | user.save() 43 | .await 44 | .map_err(|e| AuthError::UserNotFound(e.to_string()))?; 45 | Ok(Some(user)) 46 | } 47 | } 48 | } 49 | Credentials::UsernamePassword { username, password } => { 50 | let maybe_user = match User::select_by_username(&username).await { 51 | Ok(v) => v, 52 | Err(e) => return Err(AuthError::DbError(format!("User load error: {e}"))), 53 | }; 54 | 55 | let Some(user) = maybe_user else { 56 | return Ok(None); // TODO an error here 57 | }; 58 | let Some(pw_hash) = &user.password_hash else { 59 | return Ok(None); // TODO an error here 60 | }; 61 | let Ok(()) = verify_password(password, pw_hash) else { 62 | return Ok(None); // TODO an error here 63 | }; 64 | Ok(Some(user)) 65 | } 66 | Credentials::EmailPassword { email, password } => { 67 | let maybe_user = match User::select_by_email(&email).await { 68 | Ok(v) => v, 69 | Err(e) => return Err(AuthError::DbError(format!("User load error: {e}"))), 70 | }; 71 | 72 | let Some(user) = maybe_user else { 73 | return Ok(None); // TODO an error here 74 | }; 75 | let Some(pw_hash) = &user.password_hash else { 76 | return Ok(None); // TODO an error here 77 | }; 78 | let Ok(()) = verify_password(password, pw_hash) else { 79 | return Ok(None); // TODO an error here 80 | }; 81 | Ok(Some(user)) 82 | } 83 | } 84 | } 85 | 86 | async fn get_user( 87 | &self, 88 | user_id: &axum_login::UserId, 89 | ) -> std::result::Result, Self::Error> { 90 | let maybe_user = match User::select_by_id(user_id).await { 91 | Ok(v) => v, 92 | Err(e) => return Err(AuthError::DbError(format!("User load error: {e}"))), 93 | }; 94 | Ok(maybe_user) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /docs/WHATS_NEXT.md: -------------------------------------------------------------------------------- 1 | This is a hobby project and plans change on the fly, but there are things I'd likely work on or consider next: 2 | 3 | + add runtimes(main and db) stats and db/sled storage_stats to system stats ([tokio](https://docs.rs/tokio/latest/tokio/runtime/struct.RuntimeMetrics.html)). Improve current reporting (RAM usage in mbs, ...) - add hover with mbs info. 4 | + finish custom storage impl: indexes, add/drop column methods & validations & automigrations 5 | + subdomains and multiple-services on single machine support. Maybe run bench on a subdomain? 6 | 7 | + generate Default + Serialize method into_mock and maybe generate TS types? For Rust<->TS glue 8 | 9 | + logs + metrics into db with 0x0(shared/localhost) owner? 10 | 11 | + DB and rest spawned in different processes and DB restarted only on migrations? How long DB restart takes with gigs of data? 12 | 13 | + how distributed scan might work? Need to keep all owners in the contact book which shared access? Mapping from any storage field type to CryptoAddress. Needs to implement smthlike `IntoOwnerAddress` like `IntoSqlKey`? 14 | + each `User` must have some kind of address? Roles/permissions for send/sync per table/row based on address/id/pk? 15 | + solana's programmatic delegation to programmatic addresses is kinda nice 16 | 17 | + host contact book = mapping CryptoAddress(or alias) -> NetworkAddress. Incoming requests can be from any network address as long as signature matches address. Should allow P2P etc, so no hard dependency on TLS. Separate current_thread rt sending/receiving blocks of txs and queries around? 18 | 19 | + `Send` and `Sync` have similar semantics but with threads instead of hosts. Attributes `web(Send)`(single owner) and `web(Sync)`(distributed) to derive required stuff. Sendable monitoring tables for remote debugging 20 | + `web(Send)` and `web(Sync)`, `owner` : owner is optional struct/field attr to send tx (and queries) to another host (otherwise 0x0/localhost), and owner is required field attr to sync txs with other hosts. 21 | 22 | + `owner` attribute that defines sharding? 23 | + single ownership(fast path): only owner can write into **^^^** and must sign to send, 0x0 (localhost/root) owner by default, but host can have a keypair(lock?) to write data? => tables can be split between owners into shards, owners can share read and/or delegate write access to other owners for replication and other stuff 24 | + distributed ownership(slow path): no wide blockchain standard for address - use which or make custom? Any indexed value is ok? Has to be relevant to signatures etc? BIP44 and Hierarchical Deterministic (HD) wallets seem relevant for management of this stuff. Use most of the key derivation scheme but custom paths like `/{table_name}/{index_key}`? 25 | 26 | + auth upgrades - simpler UX + DX, support more providers/methods 27 | + [rust-i18n](https://github.com/longbridgeapp/rust-i18n) or another i18n solution 28 | + `axum-valid`-like integration for `Vals` or smth alike 29 | 30 | Some ideas are more complex/crazy but interesting: 31 | + prest blogging about its own system monitoring stats etc using small llm 32 | + example with a built-in minimalistic polkadot chain - customizable + optionally distributed + optionally public DB 33 | + web3 tooling for the frontend, either with the above polkadot idea or for solana, with as little JS as possible 34 | + GlueSQL-based persistent DB in the SW that syncs with the host (meteor-like) 35 | 36 | There are also longer term things which will be needed or nice to have before the stable release of prest: 37 | * stabilization of async iterator and other basic concurrent std apis 38 | * stable releases of most important dependencies like axum and sled 39 | * parallel frontend and cranelift backend of the rust compiler for faster builds 40 | * more optional configs all around for flexibility 41 | * find a way to re-export wasm-bindgen into the prest to avoid need for other deps 42 | * better Service Worker DX in Rust 43 | * wider range of examples: interactive UIs, mobile builds, webgpu-based LLMs, ...? 44 | -------------------------------------------------------------------------------- /host/auth/google_openid.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use openidconnect::{core::*, reqwest::async_http_client, *}; 3 | 4 | state!(WITH_GOOGLE_AUTH: bool = { 5 | env_var("GOOGLE_CLIENT_ID").is_ok() && env_var("GOOGLE_CLIENT_SECRET").is_ok() 6 | }); 7 | 8 | state!(GOOGLE_CLIENT: GoogleClient = async { 9 | if !*WITH_GOOGLE_AUTH { 10 | panic!("Attempted to use google client without credentials!") 11 | } 12 | let client_id = env_var("GOOGLE_CLIENT_ID")?; 13 | let client_secret = env_var("GOOGLE_CLIENT_SECRET")?; 14 | 15 | let domain = if let Some(domain) = &APP_CONFIG.domain { 16 | format!("https://{domain}") 17 | } else { 18 | format!("http://localhost") 19 | }; 20 | let callback_url = format!("{domain}{GOOGLE_CALLBACK_ROUTE}"); 21 | GoogleClient::init(callback_url, client_id, client_secret).await 22 | }); 23 | 24 | pub struct GoogleClient(GoogleOAuthClient); 25 | 26 | impl GoogleClient { 27 | pub async fn init(callback_url: String, client_id: String, client_secret: String) -> Self { 28 | let redirect_url = RedirectUrl::new(callback_url).unwrap(); 29 | let client_id = ClientId::new(client_id); 30 | let client_secret = ClientSecret::new(client_secret); 31 | let issuer_url = IssuerUrl::new("https://accounts.google.com".to_string()).unwrap(); 32 | let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, async_http_client) 33 | .await 34 | .unwrap(); 35 | 36 | let client = 37 | CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) 38 | .set_redirect_uri(redirect_url); 39 | 40 | Self(client) 41 | } 42 | pub fn authz_request(&self) -> (url::Url, OAuthCSRF, OAuthNonce) { 43 | let mut authz_req = self.0.authorize_url( 44 | AuthenticationFlow::::AuthorizationCode, 45 | OAuthCSRF::new_random, 46 | OAuthNonce::new_random, 47 | ); 48 | let scopes = ["email"]; 49 | for scope in scopes { 50 | authz_req = authz_req.add_scope(Scope::new(scope.to_string())); 51 | } 52 | authz_req.url() 53 | } 54 | 55 | pub async fn get_email(&self, code: OAuthCode, nonce: OAuthNonce) -> Result { 56 | let token = self.get_token(code).await?; 57 | Ok(token 58 | .extra_fields() 59 | .id_token() 60 | .ok_or(e!("server did not return an ID token"))? 61 | .claims(&self.0.id_token_verifier(), &nonce)? 62 | .email() 63 | .ok_or(e!("email not found in openID claims"))? 64 | .to_string()) 65 | } 66 | 67 | pub async fn get_token(&self, code: OAuthCode) -> Result { 68 | Ok(self 69 | .0 70 | .exchange_code(AuthorizationCode::new(code)) 71 | .request_async(async_http_client) 72 | .await 73 | .somehow()?) 74 | } 75 | } 76 | 77 | pub type GoogleTokenResponse = StandardTokenResponse< 78 | IdTokenFields< 79 | EmptyAdditionalClaims, 80 | EmptyExtraTokenFields, 81 | CoreGenderClaim, 82 | CoreJweContentEncryptionAlgorithm, 83 | CoreJwsSigningAlgorithm, 84 | CoreJsonWebKeyType, 85 | >, 86 | CoreTokenType, 87 | >; 88 | 89 | pub type GoogleOAuthClient = Client< 90 | EmptyAdditionalClaims, 91 | CoreAuthDisplay, 92 | CoreGenderClaim, 93 | CoreJweContentEncryptionAlgorithm, 94 | CoreJwsSigningAlgorithm, 95 | CoreJsonWebKeyType, 96 | CoreJsonWebKeyUse, 97 | CoreJsonWebKey, 98 | CoreAuthPrompt, 99 | StandardErrorResponse, 100 | GoogleTokenResponse, 101 | CoreTokenType, 102 | StandardTokenIntrospectionResponse, 103 | CoreRevocableToken, 104 | StandardErrorResponse, 105 | >; 106 | -------------------------------------------------------------------------------- /examples/todo-pwa-auth-sync/src/main.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | use todo_pwa_auth_sync::{into_page, shared_routes}; 3 | 4 | embed_build_output_as!(BuiltAssets); 5 | 6 | state!(TODO_UPDATES: SseBroadcast> = { SseBroadcast::default() }); 7 | 8 | #[derive(Storage, Debug, Clone, Default, Serialize, Deserialize)] 9 | #[serde(default)] 10 | struct Todo { 11 | #[serde(default = "Uuid::now_v7")] 12 | pub id: Uuid, 13 | #[serde(default)] 14 | pub owner: Uuid, 15 | pub task: String, 16 | pub done: bool, 17 | } 18 | 19 | impl Todo { 20 | fn render_for(&self, maybe_user: &Option) -> Markup { 21 | let owned = maybe_user 22 | .as_ref() 23 | .map(|u| u.id == self.owner) 24 | .unwrap_or(false); 25 | 26 | html! { 27 | $"flex justify-between items-center" sse-swap=(self.id) vals=(json!(self)) { 28 | input type="checkbox" patch="/todos" disabled[!owned] checked[self.done] {} 29 | label $"ml-4 text-lg" {(self.task)} 30 | button $"ml-auto" delete="/todos" disabled[!owned] {"Delete"} 31 | } 32 | } 33 | } 34 | } 35 | 36 | #[init] 37 | async fn main() -> Result { 38 | shared_routes() 39 | .route( 40 | "/todos", 41 | get(|auth: Auth| async move { 42 | ok(html!( 43 | @if auth.user.is_some() { 44 | form put="/todos" swap-none after-request="this.reset()" { 45 | input $"border rounded-md" type="text" name="task" {} 46 | button $"ml-4" type="submit" {"Add"} 47 | } 48 | } @else { 49 | form $"flex flex-col gap-4 items-center" method="POST" action=(LOGIN_ROUTE) { 50 | input $"border rounded-md mx-4" type="text" name="username" placeholder="username" {} 51 | input $"border rounded-md mx-4" type="password" name="password" placeholder="password" {} 52 | input type="hidden" name="signup" value="true" {} 53 | button $"ml-4" type="submit" {"Sign in / Sign up"} 54 | } 55 | } 56 | div #"todos" $"w-full" sse="/todos/subscribe" sse-msg="add" swap-beforeend { 57 | @for item in Todo::get_all().await? {(item.render_for(&auth.user))} 58 | } 59 | )) 60 | }) 61 | .put(|user: User, Vals(mut todo): Vals| async move { 62 | todo.owner = user.id; 63 | todo.save().await?; 64 | TODO_UPDATES.send("add", Some(todo)).await?; 65 | OK 66 | }) 67 | .patch(|user: User, Vals(mut todo): Vals| async move { 68 | if !todo.check_owner(user.id).await? { 69 | return Err(Error::Unauthorized); 70 | } 71 | todo.update_done(!todo.done).await?; 72 | TODO_UPDATES.send(todo.id.to_string(), Some(todo)).await?; 73 | OK 74 | }) 75 | .delete(|user: User, Vals(todo): Vals| async move { 76 | if !todo.check_owner(user.id).await? { 77 | return Err(Error::Unauthorized); 78 | } 79 | todo.remove().await?; 80 | TODO_UPDATES.send(todo.id.to_string(), None).await?; 81 | OK 82 | }), 83 | ) 84 | .wrap_non_htmx(into_page) 85 | .route( 86 | "/todos/subscribe", 87 | get(|auth: Auth| async { 88 | TODO_UPDATES.stream_and_render(move |_event, todo| { 89 | todo.map(|t| t.render_for(&auth.user)).unwrap_or_default() 90 | }) 91 | }), 92 | ) 93 | .embed(BuiltAssets) 94 | .run() 95 | .await 96 | } 97 | -------------------------------------------------------------------------------- /host/db/snapshot.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::WriteState, 3 | crate::{Deserialize, Serialize}, 4 | std::fmt::Debug, 5 | }; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct Snapshot { 9 | pub data_txid: u64, 10 | pub data: Option, 11 | pub backup_txid: Option, 12 | pub backup: Option, 13 | } 14 | 15 | impl Snapshot { 16 | pub fn new(txid: u64, data: T) -> Self { 17 | Self { 18 | data: Some(data), 19 | data_txid: txid, 20 | backup: None, 21 | backup_txid: None, 22 | } 23 | } 24 | 25 | pub fn update(&mut self, state: WriteState, data: T) { 26 | // if last modification was in a previous tx then backup current data 27 | if self.data_txid < state.tx_id { 28 | self.backup = self.data.take(); 29 | self.backup_txid = Some(self.data_txid); 30 | } 31 | self.data = Some(data); 32 | self.data_txid = state.tx_id; 33 | } 34 | 35 | // pub fn update_cell(&mut self, state: WriteState, value: sql::Value) { 36 | // // if last modification was in a previous tx then backup current data 37 | // if self.data_txid < state.tx_id { 38 | // self.backup = self.data.take(); 39 | // self.backup_txid = Some(self.data_txid); 40 | // } 41 | // self.data = Some(data); 42 | // self.data_txid = state.tx_id; 43 | // } 44 | 45 | pub fn delete(mut self, state: WriteState) -> Option { 46 | // if last modification was in a previous tx 47 | if self.data_txid < state.tx_id { 48 | // and it has some data - back it up 49 | if let Some(data) = self.data.take() { 50 | self.backup = Some(data); 51 | self.backup_txid = Some(self.data_txid); 52 | return Some(self); 53 | } 54 | // and there is no data - remove 55 | else { 56 | return None; 57 | } 58 | } 59 | // if last modification was by current tx 60 | else { 61 | // and it has a backup 62 | if self.backup.is_some() { 63 | self.data = None; 64 | self.data_txid = state.tx_id; 65 | return Some(self); 66 | } 67 | // and no backup (just created) 68 | else { 69 | return None; 70 | } 71 | } 72 | } 73 | 74 | // -> Option(should update)> 75 | pub fn rollback(mut self, state: WriteState) -> Option> { 76 | // nothing if modified by previous tx 77 | if self.data_txid < state.tx_id { 78 | return None; 79 | } 80 | 81 | // if there is a backup 82 | if let Some(backup) = self.backup.take() { 83 | self.data = Some(backup); 84 | self.data_txid = self 85 | .backup_txid 86 | .take() 87 | .expect("backups must be stored with txid"); 88 | Some(Some(self)) 89 | } 90 | // just created - remove 91 | else { 92 | Some(None) 93 | } 94 | } 95 | 96 | pub fn take(mut self, state: WriteState) -> Option { 97 | if state.in_progress && state.tx_id == self.data_txid { 98 | self.backup.take() 99 | } else { 100 | self.data.take() 101 | } 102 | } 103 | 104 | pub fn get(&self, state: WriteState) -> Option { 105 | if state.in_progress && state.tx_id == self.data_txid { 106 | self.backup.clone().take() 107 | } else { 108 | self.data.clone().take() 109 | } 110 | } 111 | 112 | pub fn get_mut(&mut self, state: WriteState) -> Option<&mut T> { 113 | if let Some(data) = &mut self.data { 114 | Some(data) 115 | } else { 116 | None 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /examples/databases/redis-driver/src/main.rs: -------------------------------------------------------------------------------- 1 | use prest::*; 2 | use redis::{Client, Commands}; 3 | use std::collections::HashMap; 4 | 5 | state!(CLIENT: Client = { Client::open("redis://127.0.0.1")? }); 6 | 7 | #[derive(Serialize, Deserialize)] 8 | pub struct Todo { 9 | #[serde(default)] 10 | pub task: String, 11 | #[serde(default)] 12 | pub done: bool, 13 | } 14 | 15 | #[derive(Deserialize)] 16 | pub struct TodoForm { 17 | #[serde(default)] 18 | pub uuid: String, 19 | #[serde(default)] 20 | pub task: String, 21 | #[serde(default)] 22 | pub done: bool, 23 | } 24 | 25 | #[init] 26 | async fn main() -> Result { 27 | route( 28 | "/", 29 | get(|| async { 30 | let todos = get_todos(); 31 | html!(@for todo in todos {(render_item(todo.0, todo.1))}) 32 | }) 33 | .put(|Vals(TodoForm { task, .. }): Vals| async move { 34 | add_todo(task); 35 | Redirect::to("/") 36 | }) 37 | .patch( 38 | |Vals(TodoForm { uuid, done, .. }): Vals| async move { 39 | toggle_todo(uuid, done); 40 | Redirect::to("/") 41 | }, 42 | ) 43 | .delete(|Vals(TodoForm { uuid, .. }): Vals| async move { 44 | delete_todo(uuid); 45 | Redirect::to("/") 46 | }), 47 | ) 48 | .wrap_non_htmx(page) 49 | .run() 50 | .await 51 | } 52 | 53 | fn get_todos() -> Vec<(String, Todo)> { 54 | let mut con = CLIENT.get_connection().unwrap(); 55 | let map: HashMap = con.hgetall("todos").unwrap(); 56 | map.into_iter() 57 | .map(|(uuid, todo)| { 58 | let todo = from_json_str::(&todo).unwrap(); 59 | (uuid, todo) 60 | }) 61 | .collect() 62 | } 63 | 64 | fn add_todo(task: String) { 65 | let mut con = CLIENT.get_connection().unwrap(); 66 | let uuid = Uuid::now_v7().to_string(); 67 | con.hset_nx( 68 | "todos", 69 | uuid, 70 | to_json_string(&Todo { task, done: false }).unwrap(), 71 | ) 72 | .unwrap() 73 | } 74 | 75 | fn toggle_todo(uuid: String, done: bool) { 76 | let mut con = CLIENT.get_connection().unwrap(); 77 | let todo: String = con.hget("todos", &uuid).unwrap(); 78 | let mut todo: Todo = from_json_str(&todo).unwrap(); 79 | todo.done = !done; 80 | con.hset("todos", uuid, to_json_string(&todo).unwrap()) 81 | .unwrap() 82 | } 83 | 84 | fn delete_todo(uuid: String) { 85 | let mut con = CLIENT.get_connection().unwrap(); 86 | con.hdel("todos", uuid).unwrap() 87 | } 88 | 89 | fn render_item(uuid: String, todo: Todo) -> Markup { 90 | let id = format!("uuid-{}", uuid); 91 | html!( 92 | div style="height: 64px; display: flex; justify-content: space-between; align-items: center;" { 93 | form #(id) patch="/" style="margin-bottom: 0px;" { 94 | input type="hidden" name="uuid" value={(uuid)} {} 95 | input type="hidden" name="done" value={(todo.done)} {} 96 | label { 97 | input .(id) type="checkbox" onchange="this.form.submit()" checked[todo.done] {} 98 | {(todo.task)} 99 | } 100 | } 101 | form detele="/" style="margin-bottom: 0px;" { 102 | input type="hidden" name="uuid" value={(uuid)} {} 103 | input type="submit" value="Delete" style="margin-bottom: 0px;" {} 104 | } 105 | } 106 | ) 107 | } 108 | 109 | async fn page(content: Markup) -> Markup { 110 | html! { html { (Head::with_title("With Redis")) 111 | body $"container" target="div" style="margin-top: 16px;" { 112 | form put="/" after-request="this.reset()" { 113 | label for="task" {"Task description:"} 114 | input type="text" name="task" {} 115 | button type="submit" {"Add"} 116 | } 117 | $"w-full" {(content)} 118 | (Scripts::default()) 119 | } 120 | }} 121 | } 122 | -------------------------------------------------------------------------------- /examples/todo-pwa/README.md: -------------------------------------------------------------------------------- 1 | In the [first example](https://prest.blog/todo) we created a simple todo app and in this one we'll enchance it with [Progressive Web App](https://web.dev/what-are-pwas/) capabilities to make it installable and provide some offline UX. Beware that compilation will require [WebAssembly](https://webassembly.org/) rust target. It can be added with `rustup target add wasm32-unknown-unknown`. 2 | 3 | First, there will be a few additions to the app's dependencies: 4 | 5 | {Cargo.toml} 6 | 7 | `wasm-bindgen` will generate bindings necessary to interact with the JS runtime from our rust code while `prest-build` will help us compile our app's code into a service worker and bundle other required PWA assets like webmanifest. 8 | 9 | We'll move some of the code from the `main.rs` into a separate shared `lib.rs` file that will be used by the server and also compiled into a wasm library. It's possible to keep everything in a single file and use conditional compilation to select the right functions depending on the compilation target, but compiler will warn us that it's not a good idea because they are semantically different: binary target, which defaults to `main.rs`, is supposed to execute everything on it's own once started, while library target, which defaults to `lib.rs`, is supposed to provide utility to other running executables. In our case library will be used as a service worker by the browser to provide a local offline server of our app, while binary will be the server-side host of the app. 10 | 11 | Once the build is started and all the dependencies are resolved cargo automatically detects a `build.rs`, compiles and runs it before the compilation of the library and binaries. 12 | 13 | {build.rs} 14 | 15 | This script just invokes a single function `build_pwa` from prest build utils. By default this fn will check the `PWA` env variable and whether it is compiled with `debug_assertions` - if the assertions are present and `PWA` value is not `debug` then it will skip the PWA bundling process entirely to speed up the overall build process and development. However, if you'll build with the `--release` profile or provide the `PWA=debug` then it will run. 16 | 17 | This function builds our app as a library (`src/lib.rs`) into WASM target to be used in the service worker. It will use the same router produced by the `shared_routes` function as the host (`src/main.rs`) to handle [fetch events](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent) on the client side. Also, `build_pwa` runs wasm-bindgen on the resulting webassembly, injects SW event listeners into the js bindings, generates `.webmanifest` with PWA metadata and includes the default logo (if no other was provided). All the assets are moved to the special folder deep inside `target` dir that cargo creates for the build artifacts. 18 | 19 | {src/lib.rs} 20 | 21 | At this point `build.rs` is done and compilation proceeds to the `src/main.rs` binary which will import the same shared service, embed PWA build outputs and start the server just like the usual host: 22 | 23 | {src/main.rs} 24 | 25 | That's it! `Head`, `Scripts`, `build_pwa` and other utils are already adding everything necessary with default configs to get started. There are many ways how you can split app's handlers between shared and host-only, but the general rule of thumb should be - static content into the shared, dynamic into the host. While it's possible to use DB on the client just like on the host, their synchronization is a complex feature that should be avoided if possible. 26 | 27 | To verify that it's working in chrome you can open the `/` page, then go to the `application` tab in the dev tools, check that the service worker is installed and toggle the `offline` mode to see what it will look like for a user that doesn't an internet connection at the moment. By the way, you can do the same with this blog and continue browsing the site since all the content is static and is compiled into the service worker. You can check out it's source code on the [about](https://prest.blog/about) page. 28 | 29 | Now we have an installable app, but as of now it's just the same thing for every user. Quite likely that you'll want to distinguish them so let's [add authentication](https://prest.blog/todo-pwa-auth) to the mix. -------------------------------------------------------------------------------- /html/htmx.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// Convenience trait to easily add [`HtmxLayer`] to the [`Router`] 4 | pub trait HtmxRouting { 5 | fn wrap_non_htmx(self, wrapper: F) -> Self; 6 | } 7 | impl HtmxRouting for Router 8 | where 9 | F: Fn(Markup) -> Fut + Clone + Send + 'static, 10 | Fut: Future + Send, 11 | R: IntoResponse, 12 | { 13 | fn wrap_non_htmx(self, wrapper: F) -> Self { 14 | self.route_layer(HtmxLayer::wrap(wrapper)) 15 | } 16 | } 17 | 18 | /// Layer that modifies non-HTMX requests with the provided [`Fn`] 19 | /// 20 | /// Function or closure must take a single [`Markup`] argument and return [`Markup`] 21 | /// 22 | /// Can be used like this: `router.layer(HtmxLayer::wrap(|content| html!{body {(content)}}))` 23 | /// 24 | /// It also sets a proper html content type header and disables caching for htmx responses 25 | #[derive(Clone)] 26 | #[doc(hidden)] 27 | pub struct HtmxLayer { 28 | pub wrapper: F, 29 | } 30 | 31 | impl HtmxLayer { 32 | pub fn wrap(wrapper: F) -> Self { 33 | Self { wrapper } 34 | } 35 | } 36 | 37 | impl tower::Layer for HtmxLayer 38 | where 39 | F: Clone, 40 | { 41 | type Service = HtmxMiddleware; 42 | 43 | fn layer(&self, inner: S) -> Self::Service { 44 | HtmxMiddleware { 45 | wrapper: self.wrapper.clone(), 46 | inner, 47 | } 48 | } 49 | } 50 | 51 | /// Underlying middleware that powers [`HtmxLayer`] layer 52 | #[doc(hidden)] 53 | #[derive(Clone)] 54 | pub struct HtmxMiddleware { 55 | wrapper: F, 56 | inner: S, 57 | } 58 | 59 | use core::{ 60 | future::Future, 61 | pin::Pin, 62 | task::{Context, Poll}, 63 | }; 64 | use std::boxed::Box; 65 | 66 | impl tower::Service> for HtmxMiddleware 67 | where 68 | S: tower::Service, Response = Response> + Send + 'static, 69 | S::Future: Send + 'static, 70 | F: Fn(Markup) -> Fut + Send + Clone + 'static, 71 | Fut: Future + Send, 72 | R: IntoResponse, 73 | { 74 | type Response = S::Response; 75 | type Error = S::Error; 76 | type Future = Pin< 77 | Box> + Send + 'static>, 78 | >; 79 | 80 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 81 | self.inner.poll_ready(cx) 82 | } 83 | 84 | fn call(&mut self, request: Request) -> Self::Future { 85 | let not_htmx_request = request.headers().get(axum_htmx::HX_REQUEST).is_none(); 86 | let future = self.inner.call(request); 87 | let wrapper = self.wrapper.clone(); 88 | Box::pin(async move { 89 | let (mut parts, body) = future.await?.into_parts(); 90 | 91 | if parts.status.as_u16() != 200 { 92 | return Ok(Response::from_parts(parts, body)); 93 | } 94 | 95 | parts 96 | .headers 97 | .insert(header::CONTENT_TYPE, HeaderValue::from_static("text/html")); 98 | 99 | if not_htmx_request { 100 | let body = axum::body::to_bytes(body, 10000000).await.unwrap(); 101 | let content = std::string::String::from_utf8(body.to_vec()).unwrap(); 102 | let response_future = wrapper(PreEscaped(content)); 103 | let response = response_future.await.into_response(); 104 | // let body = Body::from(content.0); 105 | // let length = body.size_hint().lower(); 106 | // parts.headers.insert(header::CONTENT_LENGTH, length.into()); 107 | // let response = Response::from_parts(parts, body); 108 | Ok(response) 109 | } else { 110 | parts.headers.insert( 111 | header::CACHE_CONTROL, 112 | HeaderValue::from_static( 113 | "max-age=0, no-cache, must-revalidate, proxy-revalidate", 114 | ), 115 | ); 116 | Ok(Response::from_parts(parts, body)) 117 | } 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /host/admin/db.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Serialize)] 4 | struct TableDescription { 5 | name: String, 6 | fields: Vec, 7 | } 8 | 9 | #[derive(Deserialize)] 10 | struct TableQueryParams { 11 | offset: Option, 12 | limit: Option, 13 | } 14 | 15 | pub(crate) async fn schema() -> impl IntoResponse { 16 | let descriptions = DB 17 | .custom_schemas() 18 | .iter() 19 | .map(|s| TableDescription { 20 | name: s.name().to_owned(), 21 | fields: s.fields().to_vec(), 22 | }) 23 | .collect::>(); 24 | Json(descriptions) 25 | } 26 | 27 | #[derive(Serialize)] 28 | struct TableData { 29 | name: String, 30 | fields: Vec, 31 | rows: Vec>, 32 | has_more: bool, 33 | total_pages: Option, 34 | } 35 | 36 | // table_data_json function removed - now handled within table_routes() 37 | 38 | pub(crate) async fn db_page() -> Markup { 39 | html! { 40 | a _="on load call loadSchema() then remove me" {} 41 | div #db-container { 42 | // React component will be rendered here 43 | } 44 | } 45 | } 46 | 47 | pub(crate) fn table_routes() -> Router { 48 | let mut router = Router::new(); 49 | for table in DB.custom_schemas() { 50 | let table_name = table.name().to_owned(); 51 | router = router.route( 52 | table.relative_path(), 53 | get({ 54 | let table_name = table_name.clone(); 55 | move |Vals(params): Vals| async move { 56 | let table = DB 57 | .custom_schemas() 58 | .into_iter() 59 | .find(|t| t.name() == table_name) 60 | .ok_or_else(|| e!("Table not found: {}", table_name))?; 61 | 62 | let offset = params.offset.unwrap_or(0); 63 | let limit = params.limit.unwrap_or(20); 64 | 65 | let (rows, has_more) = table.get_as_strings_paginated(offset, limit).await?; 66 | 67 | ok(Json(TableData { 68 | name: table.name().to_owned(), 69 | fields: table.fields().to_vec(), 70 | rows, 71 | has_more, 72 | total_pages: None, // We don't calculate total pages for performance 73 | })) 74 | } 75 | }) 76 | .put({ 77 | let table_name = table_name.clone(); 78 | move |req: Request| async move { 79 | let table = DB 80 | .custom_schemas() 81 | .into_iter() 82 | .find(|t| t.name() == table_name) 83 | .ok_or_else(|| e!("Table not found: {}", table_name))?; 84 | let id = table.save(req).await?; 85 | ok(Json(serde_json::json!({ "success": true, "id": id }))) 86 | } 87 | }) 88 | .patch({ 89 | let table_name = table_name.clone(); 90 | move |req: Request| async move { 91 | let table = DB 92 | .custom_schemas() 93 | .into_iter() 94 | .find(|t| t.name() == table_name) 95 | .ok_or_else(|| e!("Table not found: {}", table_name))?; 96 | let id = table.save(req).await?; 97 | ok(Json(serde_json::json!({ "success": true, "id": id }))) 98 | } 99 | }) 100 | .delete({ 101 | move |req: Request| async move { 102 | let table = DB 103 | .custom_schemas() 104 | .into_iter() 105 | .find(|t| t.name() == table_name) 106 | .ok_or_else(|| e!("Table not found: {}", table_name))?; 107 | table.remove(req).await?; 108 | ok(Json(serde_json::json!({ "success": true }))) 109 | } 110 | }), 111 | ); 112 | } 113 | router 114 | } 115 | -------------------------------------------------------------------------------- /host/server.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | use axum::handler::HandlerWithoutStateExt; 4 | use axum_server::Handle; 5 | use http::uri::Authority; 6 | use rustls_acme::{caches::DirCache, AcmeConfig}; 7 | use std::net::{Ipv6Addr, SocketAddr}; 8 | 9 | pub async fn start(router: Router) -> Result<(), Error> { 10 | let name = APP_CONFIG.name; 11 | let domain = APP_CONFIG.domain; 12 | let data_dir = APP_CONFIG.data_dir.clone(); 13 | 14 | let handle = RT.new_server_handle(); 15 | 16 | if *IS_REMOTE && domain.is_some() { 17 | let domain = domain.expect("Already validated is_some"); 18 | let mut certs_path = data_dir.clone(); 19 | certs_path.push("certs"); 20 | 21 | let mut state = AcmeConfig::new(vec![domain]) 22 | .cache_option(Some(DirCache::new(certs_path))) 23 | .directory_lets_encrypt(true) 24 | .state(); 25 | let acceptor = state.axum_acceptor(state.default_rustls_config()); 26 | 27 | tokio::spawn(async move { 28 | loop { 29 | match state.next().await { 30 | Some(Ok(ok)) => trace!(target: "server", "TLS acme event: {:?}", ok), 31 | Some(Err(err)) => error!(target: "server", "TLS acme error: {:?}", err), 32 | None => tokio::time::sleep(std::time::Duration::from_millis(100)).await, 33 | } 34 | } 35 | }); 36 | 37 | let redirect_handle = RT.new_server_handle(); 38 | tokio::spawn(redirect_http_to_https(redirect_handle)); 39 | 40 | info!(target: "server", "Starting serving {name} at https://{domain}"); 41 | axum_server::bind(SocketAddr::from((Ipv6Addr::UNSPECIFIED, 443))) 42 | .acceptor(acceptor) 43 | .handle(handle) 44 | .serve(router.into_make_service_with_connect_info::()) 45 | .await?; 46 | } else { 47 | #[cfg(debug_assertions)] 48 | info!(target: "server", "Starting serving {name} at http://localhost"); 49 | 50 | axum_server::bind(SocketAddr::from((Ipv6Addr::UNSPECIFIED, check_port()))) 51 | .handle(handle) 52 | .serve(router.into_make_service_with_connect_info::()) 53 | .await?; 54 | } 55 | OK 56 | } 57 | 58 | async fn redirect_http_to_https(handle: Handle) { 59 | fn make_https(host: &str, uri: Uri, https_port: u16) -> Result { 60 | let mut parts = uri.into_parts(); 61 | 62 | parts.scheme = Some(axum::http::uri::Scheme::HTTPS); 63 | 64 | if parts.path_and_query.is_none() { 65 | parts.path_and_query = Some("/".parse().expect("Valid root path")); 66 | } 67 | 68 | let authority: Authority = host.parse()?; 69 | let bare_host = match authority.port() { 70 | Some(port_struct) => authority 71 | .as_str() 72 | .strip_suffix(port_struct.as_str()) 73 | .map(|a| a.strip_suffix(':')) 74 | .flatten() 75 | .expect("Authority.port() is Some(port) then we can be sure authority ends with :{port}"), 76 | None => authority.as_str(), 77 | }; 78 | 79 | parts.authority = Some(format!("{bare_host}:{https_port}").parse()?); 80 | 81 | Ok(Uri::from_parts(parts)?) 82 | } 83 | 84 | let redirect = move |Host(host): Host, uri: Uri| async move { 85 | match make_https(&host, uri, 443) { 86 | Ok(uri) => Ok(Redirect::permanent(&uri.to_string())), 87 | Err(error) => { 88 | tracing::warn!(target: "https redirect", %error, "failed to convert URI to HTTPS"); 89 | Err(StatusCode::BAD_REQUEST) 90 | } 91 | } 92 | }; 93 | 94 | let addr = SocketAddr::from(([127, 0, 0, 1], 80)); 95 | 96 | axum_server::bind(addr) 97 | .handle(handle) 98 | .serve(redirect.into_make_service_with_connect_info::()) 99 | .await 100 | .expect("HTTP -> HTTPS redirection service should start and end gracefully"); 101 | } 102 | 103 | fn check_port() -> u16 { 104 | if let Ok(v) = env_var("PORT") { 105 | v.parse::().unwrap_or(80) 106 | } else { 107 | 80 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib.rs: -------------------------------------------------------------------------------- 1 | //! These docs are focused on technical details. For tutorials check out [prest.blog](https://prest.blog) 2 | #![doc(html_favicon_url = "https://prest.blog/favicon.ico")] 3 | #![allow(warnings)] 4 | 5 | // for macro-generated code inside prest itself 6 | pub(crate) use crate as prest; 7 | 8 | #[derive(Debug, Clone, Copy)] 9 | pub struct Prest; 10 | 11 | pub use prest_init_macro::init; 12 | 13 | #[doc(hidden)] 14 | pub use serde; 15 | pub use serde_derive::{Deserialize, Serialize}; 16 | 17 | pub use std::future::Future; 18 | 19 | // pub use anyhow::{anyhow, bail, Result as AnyhowResult}; 20 | pub use async_trait::async_trait; 21 | pub use axum::{ 22 | self, 23 | body::{Body, HttpBody}, 24 | error_handling::{HandleError, HandleErrorLayer}, 25 | extract::{ 26 | self, Extension, FromRequest, FromRequestParts, Host, MatchedPath, NestedPath, OriginalUri, 27 | Path, Request, 28 | }, 29 | http::{self, header, HeaderMap, HeaderValue, Method, StatusCode, Uri}, 30 | middleware::{from_fn, Next}, 31 | response::{ErrorResponse, Html, IntoResponse, Json, Redirect, Response}, 32 | routing::{any, delete, get, patch, post, put}, 33 | Router, 34 | }; 35 | // TODO: either do smth with it or get rid of 36 | // pub use axum_htmx::{ 37 | // HxBoosted, HxCurrentUrl, HxEvent, HxHistoryRestoreRequest, HxLocation, HxPrompt, HxPushUrl, 38 | // HxRedirect, HxRefresh, HxReplaceUrl, HxRequest, HxReselect, HxResponseTrigger, HxReswap, 39 | // HxRetarget, HxTarget, HxTrigger, HxTriggerName, SwapOption, 40 | // }; 41 | 42 | pub use bitcode::{deserialize as from_bitcode, serialize as into_bitcode}; 43 | pub use chrono::{NaiveDate, NaiveDateTime, NaiveTime, Utc}; 44 | pub use futures::{ 45 | future::{join_all, FutureExt}, 46 | stream::{self, Stream, StreamExt, TryStreamExt}, 47 | }; 48 | pub use serde_json::{ 49 | from_slice as from_json_slice, from_str as from_json_str, json, to_string as to_json_string, 50 | to_vec as to_json_vec, 51 | }; 52 | pub use std::sync::LazyLock as Lazy; 53 | pub use std::{env::var as env_var, sync::Arc}; 54 | // pub use tower::{self, BoxError, Layer, Service, ServiceBuilder}; 55 | pub use hex; 56 | pub use tracing::{debug, error, info, trace, warn}; 57 | pub use uuid::Uuid; 58 | 59 | #[doc(hidden)] 60 | pub mod config; 61 | pub use config::APP_CONFIG; 62 | 63 | mod result; 64 | #[doc(hidden)] 65 | pub use result::_Somehow; 66 | pub use result::{ok, AnyError, Error, Result, Somehow, OK}; 67 | 68 | mod vals; 69 | pub use vals::Vals; 70 | 71 | #[cfg(feature = "db")] 72 | mod db; 73 | #[cfg(feature = "db")] 74 | pub use db::*; 75 | #[cfg(feature = "embed")] 76 | #[doc(hidden)] 77 | pub mod embed; 78 | #[cfg(feature = "embed")] 79 | pub use embed::Embed; 80 | #[cfg(feature = "embed")] 81 | #[doc(hidden)] 82 | pub use embed::{EmbedRoutes, EmbeddedStruct}; 83 | #[cfg(feature = "html")] 84 | mod html; 85 | #[cfg(feature = "html")] 86 | pub use html::*; 87 | #[cfg(feature = "html")] 88 | 89 | /// Default doctype for HTML 90 | pub const DOCTYPE: PreEscaped<&'static str> = PreEscaped(""); 91 | /// Default favicon 92 | pub(crate) static FAVICON: &[u8] = include_bytes!("ui/favicon.ico"); 93 | 94 | #[cfg(host)] 95 | mod host; 96 | #[cfg(host)] 97 | pub use host::*; 98 | 99 | #[cfg(sw)] 100 | mod service_worker; 101 | #[cfg(sw)] 102 | pub use service_worker::*; 103 | 104 | // --- GENERAL UTILS --- 105 | 106 | /// A little helper to init router and route in a single call to improve formatting 107 | pub fn route( 108 | path: &str, 109 | method_router: axum::routing::method_routing::MethodRouter, 110 | ) -> Router { 111 | Router::::new().route(path, method_router) 112 | } 113 | 114 | /// Default javascript code that registers a service worker from `/sw.js` 115 | pub const REGISTER_SW_SNIPPET: &str = 116 | "if ('serviceWorker' in navigator) navigator.serviceWorker.register('sw.js', {type: 'module'});"; 117 | /// Returns whether PWA will be built with current configs 118 | pub fn is_pwa() -> bool { 119 | #[cfg(sw)] 120 | return true; 121 | #[cfg(host)] 122 | match cfg!(debug) { 123 | true => std::env::var("PWA").map_or(false, |v| v == "debug"), 124 | false => std::env::var("PWA").map_or(true, |v| v == "release"), 125 | } 126 | } 127 | 128 | /// Shorthand for `PreEscaped(include_str!(...))`` 129 | #[macro_export] 130 | macro_rules! include_html { 131 | ($path: tt) => { 132 | PreEscaped(include_str!($path)) 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /examples/llm-mistral/llm.rs: -------------------------------------------------------------------------------- 1 | use candle_core::{DType, Device, Tensor}; 2 | use candle_transformers::{ 3 | generation::LogitsProcessor, 4 | models::quantized_mistral::{Config as QMistralCfg, Model as QMistral}, 5 | quantized_var_builder::VarBuilder, 6 | utils::apply_repeat_penalty, 7 | }; 8 | use hf_hub::{api::sync::Api, Repo}; 9 | use prest::*; 10 | use tokenizers::Tokenizer; 11 | 12 | pub fn init() -> Somehow { 13 | let cfg = MistralConfig::default(); 14 | let start = std::time::Instant::now(); 15 | info!("started initializing the model..."); 16 | let repo = Repo::model("lmz/candle-mistral".to_owned()); 17 | let repo_api = Api::new().unwrap().repo(repo); 18 | let tokenizer_filename = repo_api.get("tokenizer.json").unwrap(); 19 | let tokenizer = Tokenizer::from_file(tokenizer_filename).unwrap(); 20 | let eos_token = *tokenizer.get_vocab(true).get("").unwrap(); 21 | let logits_processor = LogitsProcessor::new(cfg.seed, cfg.temperature, cfg.top_p); 22 | let weights_filename = repo_api.get("model-q4k.gguf").unwrap(); 23 | let mistral_cfg = QMistralCfg::config_7b_v0_1(true); 24 | let weights = VarBuilder::from_gguf(&weights_filename, &Device::Cpu)?; 25 | let model = QMistral::new(&mistral_cfg, weights)?; 26 | info!("initialized the model in {:?}", start.elapsed()); 27 | Ok(Mistral { 28 | model, 29 | logits_processor, 30 | cfg, 31 | tokenizer, 32 | eos_token, 33 | history: String::new(), 34 | tokens: vec![], 35 | current_ctx: 0, 36 | processed: 0, 37 | }) 38 | } 39 | 40 | pub struct MistralConfig { 41 | pub seed: u64, 42 | pub repeat_penalty: f32, 43 | pub repeat_last_n: usize, 44 | pub temperature: Option, 45 | pub top_p: Option, 46 | } 47 | impl Default for MistralConfig { 48 | fn default() -> Self { 49 | Self { 50 | seed: 123456789, 51 | repeat_penalty: 1.1, 52 | repeat_last_n: 64, 53 | temperature: None, 54 | top_p: None, 55 | } 56 | } 57 | } 58 | 59 | pub struct Mistral { 60 | model: QMistral, 61 | logits_processor: LogitsProcessor, 62 | tokenizer: Tokenizer, 63 | cfg: MistralConfig, 64 | pub history: String, 65 | tokens: Vec, 66 | eos_token: u32, 67 | pub current_ctx: usize, 68 | processed: usize, 69 | } 70 | 71 | impl Mistral { 72 | pub fn prompt(&mut self, text: &str) -> Result<(), Error> { 73 | self.history += text; 74 | self.tokens.append(&mut self.encode(text)); 75 | self.processed = self.tokens.len(); 76 | Ok(()) 77 | } 78 | pub fn more(&mut self) -> bool { 79 | let next_token = self.predict().unwrap(); 80 | self.current_ctx = self.tokens.len(); 81 | self.tokens.push(next_token); 82 | self.try_decode(); 83 | return next_token != self.eos_token; 84 | } 85 | fn predict(&mut self) -> Somehow { 86 | let Mistral { 87 | tokens, 88 | current_ctx, 89 | cfg, 90 | .. 91 | } = self; 92 | let input = Tensor::new(&tokens[*current_ctx..], &Device::Cpu)?.unsqueeze(0)?; 93 | let logits = self.model.forward(&input, *current_ctx)?; 94 | let logits = logits.squeeze(0)?.squeeze(0)?.to_dtype(DType::F32)?; 95 | let penalty_pos = tokens.len().saturating_sub(cfg.repeat_last_n); 96 | let logits = apply_repeat_penalty(&logits, cfg.repeat_penalty, &tokens[penalty_pos..])?; 97 | let next_token = self.logits_processor.sample(&logits)?; 98 | Ok(next_token) 99 | } 100 | fn encode(&self, input: &str) -> Vec { 101 | self.tokenizer 102 | .encode(input, true) 103 | .unwrap() 104 | .get_ids() 105 | .to_vec() 106 | } 107 | fn try_decode(&mut self) { 108 | let Mistral { 109 | tokens, 110 | processed, 111 | current_ctx, 112 | .. 113 | } = self; 114 | let processed_text = self 115 | .tokenizer 116 | .decode(&tokens[*processed..*current_ctx], true) 117 | .unwrap(); 118 | let updated_text = self.tokenizer.decode(&tokens[*processed..], true).unwrap(); 119 | if updated_text.len() > processed_text.len() 120 | && updated_text.chars().last().unwrap().is_ascii() 121 | { 122 | self.processed = self.current_ctx; 123 | let new_text = updated_text.split_at(processed_text.len()).1.to_string(); 124 | self.history += &new_text; 125 | } 126 | } 127 | } 128 | --------------------------------------------------------------------------------