diff --git a/CREDITS.md b/CREDITS.md index 782f40a8..238a4d8b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,58 +1,53 @@ # 🔃 Dependencies -PageTop is developed in the [Rust programming language](https://www.rust-lang.org/) and stands on -the shoulders of true giants, using some of the most stable and renowned libraries (*crates*) from -the [Rust ecosystem](https://lib.rs), such as: - -* [Actix Web](https://actix.rs/) for web services and server management. -* [Tracing](https://github.com/tokio-rs/tracing) for the diagnostic system and structured logging. -* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates) that incorporate - [Fluent](https://projectfluent.org/) for project internationalization. -* Among others, which you can review in the PageTop - [`Cargo.toml`](https://github.com/manuelcillero/pagetop/blob/main/Cargo.toml) file. +PageTop is developed using the [Rust programming language](https://www.rust-lang.org/) and stands on +the shoulders of giants, leveraging some of the most stable and renowned libraries (*crates*) from +the [Rust ecosystem](https://lib.rs), including: + * [Actix Web](https://actix.rs/) for web services and server management. + * [Tracing](https://github.com/tokio-rs/tracing) for diagnostics and structured logging. + * [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), which integrate + [Fluent](https://projectfluent.org/) for internationalization. + * Additional crates, which you can explore in the `Cargo.toml` files of PageTop and its packages. # ⌨️ Code -PageTop integrates code from various renowned crates to enhance functionality: +PageTop incorporates code from several well-regarded crates to enhance its functionality: -* [**Config (v0.11.0)**](https://github.com/mehcode/config-rs/tree/0.11.0): Includes code from - [config-rs](https://crates.io/crates/config) by [Ryan Leckey](https://crates.io/users/mehcode), - chosen for its advantages in reading configuration settings and delegating assignment to safe - types, tailored to the specific needs of each package, theme, or application. + * **[Config (v0.11.0)](https://github.com/mehcode/config-rs/tree/0.11.0)**: Includes code from + [config-rs](https://crates.io/crates/config) by [Ryan Leckey](https://crates.io/users/mehcode), + chosen for its advantages in reading configuration settings and delegating assignment to safe + types, tailored to the specific needs of each package, theme, or application. -* [**Maud (v0.25.0)**](https://github.com/lambda-fairy/maud/tree/v0.25.0/maud): An adapted version - of the excellent [maud](https://crates.io/crates/maud) crate by - [Chris Wong](https://crates.io/users/lambda-fairy) is incorporated to leverage its functionalities without requiring a reference to `maud` in the `Cargo.toml` files. - -* **SmartDefault (v0.7.1)**: Embedded [SmartDefault](https://crates.io/crates/smart_default) by - [Jane Doe](https://crates.io/users/jane-doe) as `AutoDefault`to simplify the documentation of - Default implementations and also removes the need to explicitly list `smart_default` in the - `Cargo.toml` files. + * **[Maud (v0.25.0)](https://github.com/lambda-fairy/maud/tree/v0.25.0/maud)**: An adapted version + of the excellent [maud](https://crates.io/crates/maud) crate by + [Chris Wong](https://crates.io/users/lambda-fairy) is integrated, enabling its functionalities + without requiring a direct dependency in the `Cargo.toml` files. + * **SmartDefault (v0.7.1)**: The [SmartDefault](https://crates.io/crates/smart_default) crate by + [Jane Doe](https://crates.io/users/jane-doe) has been embedded as `AutoDefault`, simplifying + `Default` implementations and eliminating the need to explicitly reference `smart_default` in + the `Cargo.toml` files. # 🗚 FIGfonts PageTop uses the [figlet-rs](https://crates.io/crates/figlet-rs) package by *yuanbohan* to display a -presentation banner in the terminal with the application's name using -[FIGlet](http://www.figlet.org) characters. The fonts included in `src/app` are: +presentation banner in the terminal featuring the application's name in +[FIGlet](http://www.figlet.org) characters. The fonts included in `pagetop/src/app` are: * [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) by *Glenn Chappell* * [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) by *Glenn Chappell* (default) * [speed.flf](http://www.figlet.org/fontdb_example.cgi?font=speed.flf) by *Claude Martins* * [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) by *Ryan Youck* - # 📰 Templates -* The default welcome homepage design is based on the - [Zinc](https://themewagon.com/themes/free-bootstrap-5-html5-business-website-template-zinc) - template created by [inovatik](https://inovatik.com/) and distributed by - [ThemeWagon](https://themewagon.com). - +The default welcome homepage design is inspired by a tutorial for creating a unique +[Neobrutalism](https://www.codewithfaraz.com/content/109/creating-a-unique-neobrutalism-portfolio-page-with-html-css-and-javascript) +portfolio page by [Faraz](https://www.codewithfaraz.com/). # 🎨 Icon -"The creature" smiling is a fun creation by [Webalys](https://www.iconfinder.com/webalys). It can be -found in their [Nasty Icons](https://www.iconfinder.com/iconsets/nasty) collection available on +"The Creature" smiling is a playful creation by [Webalys](https://www.iconfinder.com/webalys). It is +part of their [Nasty Icons](https://www.iconfinder.com/iconsets/nasty) collection, available on [ICONFINDER](https://www.iconfinder.com). diff --git a/Cargo.lock b/Cargo.lock index 5eca0cae..a668ac4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bytes", "futures-core", "futures-sink", @@ -29,7 +29,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web", - "bitflags", + "bitflags 2.6.0", "bytes", "derive_more 0.99.18", "futures-core", @@ -54,7 +54,7 @@ dependencies = [ "actix-utils", "ahash", "base64 0.22.1", - "bitflags", + "bitflags 2.6.0", "brotli", "bytes", "bytestring", @@ -128,7 +128,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2", + "socket2 0.5.7", "tokio", "tracing", ] @@ -208,7 +208,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2", + "socket2 0.5.7", "time", "url", ] @@ -309,6 +309,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -400,6 +406,196 @@ version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.2.0", + "futures-lite 2.5.0", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io 2.4.0", + "async-lock 3.4.0", + "blocking", + "futures-lite 2.5.0", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock 3.4.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.5.0", + "parking", + "polling 3.7.4", + "rustix 0.38.40", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io 2.4.0", + "async-lock 3.4.0", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 2.5.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" @@ -433,11 +629,26 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -448,6 +659,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite 2.5.0", + "piper", +] + [[package]] name = "brotli" version = "6.0.0" @@ -632,6 +856,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7439becb5fafc780b6f4de382b1a7a3e70234afe783854a4702ee8adbb838609" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.4.0" @@ -656,6 +895,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -671,6 +920,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -708,6 +972,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -734,6 +1007,51 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -790,6 +1108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -805,6 +1124,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "drust" version = "0.0.3" @@ -813,6 +1138,15 @@ dependencies = [ "pagetop-bootsier", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -838,6 +1172,59 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" + [[package]] name = "figlet-rs" version = "0.1.5" @@ -928,6 +1315,8 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ + "futures-core", + "futures-sink", "spin", ] @@ -937,6 +1326,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -946,12 +1350,104 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +dependencies = [ + "fastrand 2.2.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -970,8 +1466,13 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1041,11 +1542,23 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags", + "bitflags 2.6.0", "ignore", "walkdir", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "grass" version = "0.13.4" @@ -1106,12 +1619,45 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -1130,6 +1676,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.12" @@ -1309,6 +1864,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1381,6 +1942,17 @@ dependencies = [ "hashbrown 0.15.1", ] +[[package]] +name = "inherent" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "inout" version = "0.1.3" @@ -1390,6 +1962,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "intl-memoizer" version = "0.5.2" @@ -1409,12 +1990,32 @@ dependencies = [ "unic-langid", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1439,6 +2040,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1459,6 +2069,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1472,6 +2085,23 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1516,6 +2146,9 @@ name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "value-bag", +] [[package]] name = "matchers" @@ -1526,6 +2159,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1569,7 +2212,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "log", "wasi", @@ -1582,6 +2225,23 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -1602,12 +2262,49 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1615,6 +2312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1638,6 +2336,84 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "3.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ouroboros" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944fa20996a25aded6b4795c6d63f10014a7a83f8be9828a11860b08c5fc4a67" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39b0deead1528fd0e5947a8546a9642a9777c25f6e1e26f34c97b204bbb465bd" +dependencies = [ + "heck 0.4.1", + "itertools", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.87", +] + [[package]] name = "overload" version = "0.1.1" @@ -1659,6 +2435,7 @@ dependencies = [ "fluent-templates", "itoa", "nom", + "pagetop-build", "pagetop-macros", "paste", "serde", @@ -1713,6 +2490,26 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "pagetop-seaorm" +version = "0.0.1" +dependencies = [ + "async-trait", + "futures", + "pagetop", + "sea-orm", + "sea-schema", + "serde", + "static-files", + "url", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1766,6 +2563,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498a099351efa4becc6a19c72aa9270598e8fd274ca47052e37455241c88b696" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1901,12 +2707,75 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand 2.2.0", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.40", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "polyval" version = "0.6.2" @@ -1967,6 +2836,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -1982,6 +2873,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "version_check", + "yansi", +] + [[package]] name = "quote" version = "1.0.37" @@ -2027,7 +2931,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -2080,6 +2984,26 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2101,16 +3025,30 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + [[package]] name = "rustix" version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] @@ -2129,12 +3067,151 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "sea-orm" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5680a8b686985116607ef5f5af2b1f9e1cc2c228330e93101816a0baa279afa" +dependencies = [ + "async-stream", + "async-trait", + "futures", + "log", + "ouroboros", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "sqlx", + "strum", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a239e3bb1b566ad4ec2654d0d193d6ceddfd733487edc9c21a64d214c773910" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.87", + "unicode-ident", +] + +[[package]] +name = "sea-query" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff504d13b5e4b52fffcf2fb203d0352a5722fa5151696db768933e41e1e591bb" +dependencies = [ + "inherent", + "ordered-float", + "sea-query-derive", +] + +[[package]] +name = "sea-query-binder" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" +dependencies = [ + "sea-query", + "sqlx", +] + +[[package]] +name = "sea-query-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9834af2c4bd8c5162f00c89f1701fb6886119a88062cf76fe842ea9e232b9839" +dependencies = [ + "darling", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.87", + "thiserror", +] + +[[package]] +name = "sea-schema" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab1592d17860a9a8584d9b549aebcd06f7bdc3ff615f71752486ba0b05b1e6e" +dependencies = [ + "futures", + "sea-query", + "sea-schema-derive", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "0.10.3" @@ -2255,6 +3332,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2285,6 +3372,19 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] [[package]] name = "socket2" @@ -2305,6 +3405,221 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +dependencies = [ + "async-io 1.13.0", + "async-std", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener 5.3.1", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.14.5", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "native-tls", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.87", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +dependencies = [ + "async-std", + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.87", + "tempfile", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.6.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.6.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "tracing", + "url", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2322,12 +3637,35 @@ dependencies = [ "path-slash", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + [[package]] name = "substring" version = "1.4.5" @@ -2375,6 +3713,19 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand 2.2.0", + "once_cell", + "rustix 0.38.40", + "windows-sys 0.59.0", +] + [[package]] name = "tera" version = "1.20.0" @@ -2403,7 +3754,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" dependencies = [ - "rustix", + "rustix 0.38.40", "windows-sys 0.59.0", ] @@ -2478,6 +3829,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.41.1" @@ -2491,7 +3857,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.7", "windows-sys 0.52.0", ] @@ -2762,18 +4128,45 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "universal-hash" version = "0.5.1" @@ -2786,9 +4179,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -2834,12 +4227,30 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "value-bag" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "walkdir" version = "2.5.0" @@ -2856,6 +4267,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.95" @@ -2882,6 +4299,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.95" @@ -2911,6 +4340,26 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3120,6 +4569,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.7.4" @@ -3186,6 +4641,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerovec" version = "0.10.4" diff --git a/Cargo.toml b/Cargo.toml index effe459c..e37f712e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,18 +5,16 @@ members = [ "helpers/pagetop-build", "helpers/pagetop-macros", + # PageTop + "pagetop", + # Packages - "packages/pagetop", "packages/pagetop-aliner", "packages/pagetop-bootsier", + "packages/pagetop-seaorm", # App - "packages/drust", - - # Examples -# "examples/app-basic", -# "examples/hello-world", -# "examples/hello-name", + "drust", ] [workspace.package] @@ -34,7 +32,9 @@ static-files = "0.2.4" pagetop-build = { version = "0.0", path = "helpers/pagetop-build" } pagetop-macros = { version = "0.0", path = "helpers/pagetop-macros" } +# PageTop +pagetop = { version = "0.0", path = "pagetop" } + # Packages -pagetop = { version = "0.0", path = "packages/pagetop" } pagetop-aliner = { version = "0.0", path = "packages/pagetop-aliner" } pagetop-bootsier = { version = "0.0", path = "packages/pagetop-bootsier" } diff --git a/README.md b/README.md index df5b2429..96049b9d 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ impl PackageTrait for HelloWorld { async fn hello_world(request: HttpRequest) -> ResultPage { Page::new(request) - .with_component(Html::with(html! { h1 { "Hello World!" } })) + .with_body(PrepareMarkup::With(html! { h1 { "Hello World!" } })) .render() } diff --git a/config/common.toml b/config/common.toml index 900872d6..2b018a16 100644 --- a/config/common.toml +++ b/config/common.toml @@ -1,5 +1,6 @@ [app] -name = "Samples" +name = "Drust" +description = "A modern web Content Management System to share your world." -[log] -tracing = "Debug" +[database] +db_type = "mysql" diff --git a/packages/drust/config/default.toml b/config/default.toml similarity index 100% rename from packages/drust/config/default.toml rename to config/default.toml diff --git a/packages/drust/config/local.default.toml b/config/local.default.toml similarity index 100% rename from packages/drust/config/local.default.toml rename to config/local.default.toml diff --git a/packages/drust/Cargo.toml b/drust/Cargo.toml similarity index 100% rename from packages/drust/Cargo.toml rename to drust/Cargo.toml diff --git a/packages/drust/src/main.rs b/drust/src/main.rs similarity index 100% rename from packages/drust/src/main.rs rename to drust/src/main.rs diff --git a/helpers/pagetop-build/README.md b/helpers/pagetop-build/README.md deleted file mode 100644 index 0e80b363..00000000 --- a/helpers/pagetop-build/README.md +++ /dev/null @@ -1,37 +0,0 @@ -
- -

PageTop Build

- -

Simplifies the process of embedding resources in PageTop app binaries.

- -[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?style=for-the-badge)](#-license) -[![API Docs](https://img.shields.io/docsrs/pagetop-build?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-build) -[![Crates.io](https://img.shields.io/crates/v/pagetop-build.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-build) -[![Downloads](https://img.shields.io/crates/d/pagetop-build.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-build) - -
- -# 📦 About PageTop - -[PageTop](https://docs.rs/pagetop) is an opinionated web framework to build modular *Server-Side -Rendering* web solutions. - - -# 🚧 Warning - -**PageTop** framework is currently in active development. The API is unstable and subject to -frequent changes. Production use is not recommended until version **0.1.0**. - - -# 📜 License - -All code in this crate is dual-licensed under either: - - * MIT License - ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) - - * Apache License, Version 2.0, - ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) - -at your option. This means you can select the license you prefer! This dual-licensing approach is -the de-facto standard in the Rust ecosystem. diff --git a/helpers/pagetop-build/src/lib.rs b/helpers/pagetop-build/src/lib.rs index 0e36f2dc..1b2fb39a 100644 --- a/helpers/pagetop-build/src/lib.rs +++ b/helpers/pagetop-build/src/lib.rs @@ -1,5 +1,4 @@ -//! **`StaticFilesBundle`** uses [static_files](https://docs.rs/static-files/latest/static_files/) -//! to provide an easy way to embed static files or compiled SCSS files into your binary at compile +//! Provide an easy way to embed static files or compiled SCSS files into your binary at compile //! time. //! //! ## Adding to your project @@ -24,9 +23,9 @@ //! use pagetop_build::StaticFilesBundle; //! //! fn main() -> std::io::Result<()> { -//! StaticFilesBundle::from_dir("./static", None) // Include all files. -//! .with_name("guides") // Name the generated module. -//! .build() // Build the bundle. +//! StaticFilesBundle::from_dir("./static", None) +//! .with_name("guides") +//! .build() //! } //! ``` //! @@ -67,7 +66,7 @@ //! //! ## Generated module //! -//! `StaticFilesBundle` generates a file in the standard directory +//! [`StaticFilesBundle`] generates a file in the standard directory //! [OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html) where all //! intermediate and output artifacts are placed during compilation. For example, if you use //! `with_name("guides")`, it generates a file named `guides.rs`: @@ -78,15 +77,15 @@ //! ```rust#ignore //! use pagetop::prelude::*; //! -//! static_files!(guides); +//! include_files!(guides); //! ``` //! -//! Or, access the entire bundle as a static `HashMap`: +//! Or, access the entire bundle as a global static `HashMap`: //! //! ```rust#ignore //! use pagetop::prelude::*; //! -//! static_files!(guides => BUNDLE_GUIDES); +//! include_files!(guides => BUNDLE_GUIDES); //! ``` //! //! You can build more than one resources file to compile with your project. @@ -98,6 +97,8 @@ use std::fs::{create_dir_all, remove_dir_all, File}; use std::io::Write; use std::path::Path; +/// Generates the resources to embed at compile time using +/// [static_files](https://docs.rs/static-files/latest/static_files/). pub struct StaticFilesBundle { resource_dir: ResourceDir, } diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 006b92d4..947327e7 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -3,8 +3,110 @@ mod smart_default; use proc_macro::TokenStream; use proc_macro_error::proc_macro_error; -use quote::quote; -use syn::{parse_macro_input, DeriveInput}; +use quote::{quote, quote_spanned, ToTokens}; +use syn::{parse_macro_input, parse_str, DeriveInput, ItemFn}; + +/// Macro attribute to generate builder methods from `set_` methods. +/// +/// This macro takes a method with the `set_` prefix and generates a corresponding method with the +/// `with_` prefix to use in the builder pattern. +/// +/// # Panics +/// +/// This function will panic if a parameter identifier is not found in the argument list. +/// +/// # Examples +/// +/// ``` +/// #[fn_builder] +/// pub fn set_example(&mut self) -> &mut Self { +/// // implementation +/// } +/// ``` +/// +/// Will generate: +/// +/// ``` +/// pub fn with_example(mut self) -> Self { +/// self.set_example(); +/// self +/// } +/// ``` +#[proc_macro_attribute] +pub fn fn_builder(_: TokenStream, item: TokenStream) -> TokenStream { + let fn_set = parse_macro_input!(item as ItemFn); + let fn_set_name = fn_set.sig.ident.to_string(); + + if !fn_set_name.starts_with("set_") { + let expanded = quote_spanned! { + fn_set.sig.ident.span() => + compile_error!("expected a \"pub fn set_...() -> &mut Self\" method"); + }; + return expanded.into(); + } + + let fn_with_name = fn_set_name.replace("set_", "with_"); + let fn_with_generics = if fn_set.sig.generics.params.is_empty() { + fn_with_name.clone() + } else { + let g = &fn_set.sig.generics; + format!("{fn_with_name}{}", quote! { #g }.to_string()) + }; + + let where_clause = fn_set + .sig + .generics + .where_clause + .as_ref() + .map_or(String::new(), |where_clause| { + format!("{} ", quote! { #where_clause }.to_string()) + }); + + let args: Vec = fn_set + .sig + .inputs + .iter() + .skip(1) + .map(|arg| arg.to_token_stream().to_string()) + .collect(); + + let params: Vec = args + .iter() + .map(|arg| { + arg.split_whitespace() + .next() + .unwrap() + .trim_end_matches(':') + .to_string() + }) + .collect(); + + #[rustfmt::skip] + let fn_with = parse_str::(format!(r#" + pub fn {fn_with_generics}(mut self, {}) -> Self {where_clause} {{ + self.{fn_set_name}({}); + self + }} + "#, args.join(", "), params.join(", ") + ).as_str()).unwrap(); + + #[rustfmt::skip] + let fn_set_doc = format!(r##" +

Use + pub fn {fn_with_name}(self, …) -> Self + for the builder pattern. +

+ "##); + + let expanded = quote! { + #[doc(hidden)] + #fn_with + #[inline] + #[doc = #fn_set_doc] + #fn_set + }; + expanded.into() +} #[proc_macro] #[proc_macro_error] diff --git a/packages/drust/config/common.toml b/packages/drust/config/common.toml deleted file mode 100644 index 2b018a16..00000000 --- a/packages/drust/config/common.toml +++ /dev/null @@ -1,6 +0,0 @@ -[app] -name = "Drust" -description = "A modern web Content Management System to share your world." - -[database] -db_type = "mysql" diff --git a/packages/pagetop-aliner/Cargo.toml b/packages/pagetop-aliner/Cargo.toml index c25be723..dc57f59f 100644 --- a/packages/pagetop-aliner/Cargo.toml +++ b/packages/pagetop-aliner/Cargo.toml @@ -19,7 +19,6 @@ pagetop.workspace = true include_dir.workspace = true static-files.workspace = true - tera = "1.20.0" [build-dependencies] diff --git a/packages/pagetop-aliner/src/lib.rs b/packages/pagetop-aliner/src/lib.rs index 414e1a5a..166a9c23 100644 --- a/packages/pagetop-aliner/src/lib.rs +++ b/packages/pagetop-aliner/src/lib.rs @@ -5,9 +5,9 @@ use tera::Tera; use std::sync::LazyLock; -static_locales!(LOCALES_ALINER); +include_locales!(LOCALES_ALINER); -static_files!(aliner); +include_files!(aliner); // ALINER THEME ************************************************************************************ @@ -50,7 +50,7 @@ impl PackageTrait for Aliner { } fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - static_files_service!(scfg, aliner => "/aliner"); + include_files_service!(scfg, aliner => "/aliner"); } } diff --git a/packages/pagetop-bootsier/src/lib.rs b/packages/pagetop-bootsier/src/lib.rs index 7c78c2b1..bdab71d4 100644 --- a/packages/pagetop-bootsier/src/lib.rs +++ b/packages/pagetop-bootsier/src/lib.rs @@ -1,8 +1,8 @@ use pagetop::prelude::*; -static_locales!(LOCALES_BOOTSIER); +include_locales!(LOCALES_BOOTSIER); -//static_files!(bootsier); +//include_files!(bootsier); pub struct Bootsier; @@ -26,7 +26,7 @@ impl PackageTrait for Bootsier { } fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - static_files_service!(scfg, bootsier => "/bootsier"); + include_files_service!(scfg, bootsier => "/bootsier"); } */ } diff --git a/packages/pagetop-seaorm/Cargo.toml b/packages/pagetop-seaorm/Cargo.toml new file mode 100644 index 00000000..9ee0a2f3 --- /dev/null +++ b/packages/pagetop-seaorm/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "pagetop-seaorm" +version = "0.0.1" +edition = "2021" + +description = """\ + Integrate SeaORM as the database framework for PageTop applications.\ +""" +categories = ["web-programming", "database"] +keywords = ["pagetop", "database", "sql", "orm"] + +homepage = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[dependencies] +pagetop.workspace = true + +async-trait = "0.1.83" +futures = "0.3.31" +serde.workspace = true +static-files.workspace = true +url = "2.5.4" + +[dependencies.sea-orm] +version = "1.1.1" +features = [ + "debug-print", "macros", "runtime-async-std-native-tls", + "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", +] +default-features = false + +[dependencies.sea-schema] +version = "0.16.0" diff --git a/helpers/pagetop-macros/README.md b/packages/pagetop-seaorm/README.md similarity index 53% rename from helpers/pagetop-macros/README.md rename to packages/pagetop-seaorm/README.md index 6dd80331..189012e8 100644 --- a/helpers/pagetop-macros/README.md +++ b/packages/pagetop-seaorm/README.md @@ -1,16 +1,22 @@
-

PageTop Macros

+

PageTop SeaORM

-

A collection of macros that boost PageTop development.

+

Integrate SeaORM as the database framework for PageTop applications.

[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?style=for-the-badge)](#-license) -[![API Docs](https://img.shields.io/docsrs/pagetop-macros?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-macros) -[![Crates.io](https://img.shields.io/crates/v/pagetop-macros.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-macros) -[![Downloads](https://img.shields.io/crates/d/pagetop-macros.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-macros) +[![API Docs](https://img.shields.io/docsrs/pagetop-seaorm?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-seaorm) +[![Crates.io](https://img.shields.io/crates/v/pagetop-seaorm.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-seaorm) +[![Downloads](https://img.shields.io/crates/d/pagetop-seaorm.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-seaorm)
+PageTop SeaORM employs [SQLx](https://crates.io/crates/sqlx) and +[SeaQuery](https://crates.io/crates/sea-query), complemented by a custom version of +[SeaORM Migration](https://github.com/SeaQL/sea-orm/tree/1.1.1/sea-orm-migration/src) (v1.1.1). The +modified SeaORM Migration ensures migrations are scoped per package, providing greater control and +reducing coupling between database components. + # 📦 About PageTop [PageTop](https://docs.rs/pagetop) is an opinionated web framework to build modular *Server-Side @@ -23,20 +29,6 @@ Rendering* web solutions. frequent changes. Production use is not recommended until version **0.1.0**. -# 🔖 Credits - -This crate includes an adapted version of [maud-macros](https://crates.io/crates/maud_macros), -version [0.25.0](https://github.com/lambda-fairy/maud/tree/v0.25.0/maud_macros), by -[Chris Wong](https://crates.io/users/lambda-fairy). - -It also includes an adapted version of [SmartDefault](https://crates.io/crates/smart_default) -(version 0.7.1) by [Jane Doe](https://crates.io/users/jane-doe), renamed as `AutoDefault`, to -streamline the implementation of `Default` in **PageTop** projects. - -Both adaptations eliminate the need to explicitly add `maud` or `smart_default` as dependencies in -`Cargo.toml` files. - - # 📜 License All code in this crate is dual-licensed under either: diff --git a/packages/pagetop-seaorm/src/config.rs b/packages/pagetop-seaorm/src/config.rs new file mode 100644 index 00000000..f423cb7e --- /dev/null +++ b/packages/pagetop-seaorm/src/config.rs @@ -0,0 +1,71 @@ +//! Configuration settings for the SeaORM PageTop package. +//! +//! Example: +//! +//! ```toml +//! [database] +//! db_type = "mysql" +//! db_name = "db" +//! db_user = "user" +//! db_pass = "password" +//! db_host = "localhost" +//! db_port = 3306 +//! max_pool_size = 5 +//! ``` +//! +//! Usage: +//! +//! ```rust +//! use pagetop_seaorm::config; +//! +//! assert_eq!(config::SETTINGS.database.db_host, "localhost"); +//! ``` +//! See [`pagetop::include_config`] to learn how **PageTop** read configuration files and use +//! settings. + +use pagetop::prelude::*; + +use serde::Deserialize; + +include_config!(SETTINGS: Settings => [ + // [database] + "database.db_type" => "", + "database.db_name" => "", + "database.db_user" => "", + "database.db_pass" => "", + "database.db_host" => "localhost", + "database.db_port" => 0, + "database.max_pool_size" => 5, +]); + +#[derive(Debug, Deserialize)] +/// Represents configuration settings, specifically the [`[database]`](Database) section (used by +/// [`SETTINGS`]). +pub struct Settings { + pub database: Database, +} +#[derive(Debug, Deserialize)] +/// Represents the `[database]` section in the [`Settings`] type. +pub struct Database { + /// Type of database: *"mysql"*, *"postgres"*, or *"sqlite"*. + /// Default: *""*. + pub db_type: String, + /// Name (for MySQL/Postgres) or reference (for SQLite) of the database. + /// Default: *""*. + pub db_name: String, + /// Username for database connection (for MySQL/Postgres). + /// Default: *""*. + pub db_user: String, + /// Password for database connection (for MySQL/Postgres). + /// Default: *""*. + pub db_pass: String, + /// Hostname for database connection (for MySQL/Postgres). + /// Default: *"localhost"*. + pub db_host: String, + /// Port number for database connection, typically 3306 (MySQL) or 5432 (Postgres). + /// Default: *0*. + pub db_port: u16, + /// Maximum number of allowed connections. + /// Default: *5*. + pub max_pool_size: u32, +} diff --git a/packages/pagetop-seaorm/src/db.rs b/packages/pagetop-seaorm/src/db.rs new file mode 100644 index 00000000..cece9a8f --- /dev/null +++ b/packages/pagetop-seaorm/src/db.rs @@ -0,0 +1,132 @@ +use pagetop::trace; +use pagetop::util::TypeInfo; + +pub use url::Url as DbUri; + +pub use sea_orm::error::{DbErr, RuntimeErr}; +pub use sea_orm::{DatabaseConnection as DbConn, ExecResult, QueryResult}; + +use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + +mod dbconn; +pub(crate) use dbconn::{run_now, DBCONN}; + +// The migration module is a customized version of the sea_orm_migration module (v1.0.0) +// https://github.com/SeaQL/sea-orm/tree/1.0.0/sea-orm-migration to avoid errors caused by the +// package paradigm of PageTop. Files integrated from original: +// +// lib.rs => db/migration.rs . . . . . . . . . . . . . . (excluding some modules and exports) +// connection.rs => db/migration/connection.rs . . . . . . . . . . . . . . (full integration) +// manager.rs => db/migration/manager.rs . . . . . . . . . . . . . . . . . (full integration) +// migrator.rs => db/migration/migrator.rs . . . . . . . . . . . .(omitting error management) +// prelude.rs => db/migration/prelude.rs . . . . . . . . . . . . . . . . . . . (avoiding CLI) +// seaql_migrations.rs => db/migration/seaql_migrations.rs . . . . . . . . (full integration) +// +mod migration; +pub use migration::prelude::*; +pub use migration::schema::*; + +pub async fn query(stmt: &mut Q) -> Result, DbErr> { + let dbconn = &*DBCONN; + let dbbackend = dbconn.get_database_backend(); + dbconn + .query_all(Statement::from_string( + dbbackend, + match dbbackend { + DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), + DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), + DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), + }, + )) + .await +} + +pub async fn exec(stmt: &mut Q) -> Result, DbErr> { + let dbconn = &*DBCONN; + let dbbackend = dbconn.get_database_backend(); + dbconn + .query_one(Statement::from_string( + dbbackend, + match dbbackend { + DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), + DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), + DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), + }, + )) + .await +} + +pub async fn exec_raw(stmt: String) -> Result { + let dbconn = &*DBCONN; + let dbbackend = dbconn.get_database_backend(); + dbconn + .execute(Statement::from_string(dbbackend, stmt)) + .await +} + +pub trait MigratorBase { + fn run_up(); + + fn run_down(); +} + +#[rustfmt::skip] +impl MigratorBase for M { + fn run_up() { + if let Err(e) = run_now(Self::up(SchemaManagerConnection::Connection(&DBCONN), None)) { + trace::error!("Migration upgrade failed ({})", e); + }; + } + + fn run_down() { + if let Err(e) = run_now(Self::down(SchemaManagerConnection::Connection(&DBCONN), None)) { + trace::error!("Migration downgrade failed ({})", e); + }; + } +} + +impl MigrationName for M { + fn name(&self) -> &str { + TypeInfo::NameTo(-2).of::() + } +} + +pub type MigrationItem = Box; + +#[macro_export] +macro_rules! install_migrations { + ( $($migration_module:ident),+ $(,)? ) => {{ + use $crate::db::{MigrationItem, MigratorBase, MigratorTrait}; + + struct Migrator; + impl MigratorTrait for Migrator { + fn migrations() -> Vec { + let mut m = Vec::::new(); + $( + m.push(Box::new(migration::$migration_module::Migration)); + )* + m + } + } + Migrator::run_up(); + }}; +} + +#[macro_export] +macro_rules! uninstall_migrations { + ( $($migration_module:ident),+ $(,)? ) => {{ + use $crate::db::{MigrationItem, MigratorBase, MigratorTrait}; + + struct Migrator; + impl MigratorTrait for Migrator { + fn migrations() -> Vec { + let mut m = Vec::::new(); + $( + m.push(Box::new(migration::$migration_module::Migration)); + )* + m + } + } + Migrator::run_down(); + }}; +} diff --git a/packages/pagetop-seaorm/src/db/dbconn.rs b/packages/pagetop-seaorm/src/db/dbconn.rs new file mode 100644 index 00000000..bd227956 --- /dev/null +++ b/packages/pagetop-seaorm/src/db/dbconn.rs @@ -0,0 +1,69 @@ +use pagetop::trace; + +use crate::config; +use crate::db::{DbConn, DbUri}; + +use std::sync::LazyLock; + +use sea_orm::{ConnectOptions, Database}; + +pub use futures::executor::block_on as run_now; + +pub static DBCONN: LazyLock = LazyLock::new(|| { + trace::info!( + "Connecting to database \"{}\" using a pool of {} connections", + &config::SETTINGS.database.db_name, + &config::SETTINGS.database.max_pool_size + ); + + let db_uri = match config::SETTINGS.database.db_type.as_str() { + "mysql" | "postgres" => { + let mut tmp_uri = DbUri::parse( + format!( + "{}://{}/{}", + &config::SETTINGS.database.db_type, + &config::SETTINGS.database.db_host, + &config::SETTINGS.database.db_name + ) + .as_str(), + ) + .unwrap(); + tmp_uri + .set_username(config::SETTINGS.database.db_user.as_str()) + .unwrap(); + // https://github.com/launchbadge/sqlx/issues/1624 + tmp_uri + .set_password(Some(config::SETTINGS.database.db_pass.as_str())) + .unwrap(); + if config::SETTINGS.database.db_port != 0 { + tmp_uri + .set_port(Some(config::SETTINGS.database.db_port)) + .unwrap(); + } + tmp_uri + } + "sqlite" => DbUri::parse( + format!( + "{}://{}", + &config::SETTINGS.database.db_type, + &config::SETTINGS.database.db_name + ) + .as_str(), + ) + .unwrap(), + _ => { + trace::error!( + "Unrecognized database type \"{}\"", + &config::SETTINGS.database.db_type + ); + DbUri::parse("").unwrap() + } + }; + + run_now(Database::connect::({ + let mut db_opt = ConnectOptions::new(db_uri.to_string()); + db_opt.max_connections(config::SETTINGS.database.max_pool_size); + db_opt + })) + .unwrap_or_else(|_| panic!("Failed to connect to database")) +}); diff --git a/packages/pagetop-seaorm/src/db/migration.rs b/packages/pagetop-seaorm/src/db/migration.rs new file mode 100644 index 00000000..29314bf6 --- /dev/null +++ b/packages/pagetop-seaorm/src/db/migration.rs @@ -0,0 +1,33 @@ +//pub mod cli; +pub mod connection; +pub mod manager; +pub mod migrator; +pub mod prelude; +pub mod schema; +pub mod seaql_migrations; +//pub mod util; + +pub use connection::*; +pub use manager::*; +//pub use migrator::*; + +pub use async_trait; +//pub use sea_orm; +//pub use sea_orm::sea_query; +use sea_orm::DbErr; + +pub trait MigrationName { + fn name(&self) -> &str; +} + +/// The migration definition +#[async_trait::async_trait] +pub trait MigrationTrait: MigrationName + Send + Sync { + /// Define actions to perform when applying the migration + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>; + + /// Define actions to perform when rolling back the migration + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + Err(DbErr::Migration("We Don't Do That Here".to_owned())) + } +} diff --git a/packages/pagetop-seaorm/src/db/migration/connection.rs b/packages/pagetop-seaorm/src/db/migration/connection.rs new file mode 100644 index 00000000..116185e4 --- /dev/null +++ b/packages/pagetop-seaorm/src/db/migration/connection.rs @@ -0,0 +1,148 @@ +use futures::Future; +use sea_orm::{ + AccessMode, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr, + ExecResult, IsolationLevel, QueryResult, Statement, TransactionError, TransactionTrait, +}; +use std::pin::Pin; + +pub enum SchemaManagerConnection<'c> { + Connection(&'c DatabaseConnection), + Transaction(&'c DatabaseTransaction), +} + +#[async_trait::async_trait] +impl<'c> ConnectionTrait for SchemaManagerConnection<'c> { + fn get_database_backend(&self) -> DbBackend { + match self { + SchemaManagerConnection::Connection(conn) => conn.get_database_backend(), + SchemaManagerConnection::Transaction(trans) => trans.get_database_backend(), + } + } + + async fn execute(&self, stmt: Statement) -> Result { + match self { + SchemaManagerConnection::Connection(conn) => conn.execute(stmt).await, + SchemaManagerConnection::Transaction(trans) => trans.execute(stmt).await, + } + } + + async fn execute_unprepared(&self, sql: &str) -> Result { + match self { + SchemaManagerConnection::Connection(conn) => conn.execute_unprepared(sql).await, + SchemaManagerConnection::Transaction(trans) => trans.execute_unprepared(sql).await, + } + } + + async fn query_one(&self, stmt: Statement) -> Result, DbErr> { + match self { + SchemaManagerConnection::Connection(conn) => conn.query_one(stmt).await, + SchemaManagerConnection::Transaction(trans) => trans.query_one(stmt).await, + } + } + + async fn query_all(&self, stmt: Statement) -> Result, DbErr> { + match self { + SchemaManagerConnection::Connection(conn) => conn.query_all(stmt).await, + SchemaManagerConnection::Transaction(trans) => trans.query_all(stmt).await, + } + } + + fn is_mock_connection(&self) -> bool { + match self { + SchemaManagerConnection::Connection(conn) => conn.is_mock_connection(), + SchemaManagerConnection::Transaction(trans) => trans.is_mock_connection(), + } + } +} + +#[async_trait::async_trait] +impl<'c> TransactionTrait for SchemaManagerConnection<'c> { + async fn begin(&self) -> Result { + match self { + SchemaManagerConnection::Connection(conn) => conn.begin().await, + SchemaManagerConnection::Transaction(trans) => trans.begin().await, + } + } + + async fn begin_with_config( + &self, + isolation_level: Option, + access_mode: Option, + ) -> Result { + match self { + SchemaManagerConnection::Connection(conn) => { + conn.begin_with_config(isolation_level, access_mode).await + } + SchemaManagerConnection::Transaction(trans) => { + trans.begin_with_config(isolation_level, access_mode).await + } + } + } + + async fn transaction(&self, callback: F) -> Result> + where + F: for<'a> FnOnce( + &'a DatabaseTransaction, + ) -> Pin> + Send + 'a>> + + Send, + T: Send, + E: std::error::Error + Send, + { + match self { + SchemaManagerConnection::Connection(conn) => conn.transaction(callback).await, + SchemaManagerConnection::Transaction(trans) => trans.transaction(callback).await, + } + } + + async fn transaction_with_config( + &self, + callback: F, + isolation_level: Option, + access_mode: Option, + ) -> Result> + where + F: for<'a> FnOnce( + &'a DatabaseTransaction, + ) -> Pin> + Send + 'a>> + + Send, + T: Send, + E: std::error::Error + Send, + { + match self { + SchemaManagerConnection::Connection(conn) => { + conn.transaction_with_config(callback, isolation_level, access_mode) + .await + } + SchemaManagerConnection::Transaction(trans) => { + trans + .transaction_with_config(callback, isolation_level, access_mode) + .await + } + } + } +} + +pub trait IntoSchemaManagerConnection<'c>: Send +where + Self: 'c, +{ + fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c>; +} + +impl<'c> IntoSchemaManagerConnection<'c> for SchemaManagerConnection<'c> { + fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> { + self + } +} + +impl<'c> IntoSchemaManagerConnection<'c> for &'c DatabaseConnection { + fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> { + SchemaManagerConnection::Connection(self) + } +} + +impl<'c> IntoSchemaManagerConnection<'c> for &'c DatabaseTransaction { + fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> { + SchemaManagerConnection::Transaction(self) + } +} diff --git a/packages/pagetop-seaorm/src/db/migration/manager.rs b/packages/pagetop-seaorm/src/db/migration/manager.rs new file mode 100644 index 00000000..d1cc3b6a --- /dev/null +++ b/packages/pagetop-seaorm/src/db/migration/manager.rs @@ -0,0 +1,167 @@ +use super::{IntoSchemaManagerConnection, SchemaManagerConnection}; +use sea_orm::sea_query::{ + extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement}, + ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement, + TableAlterStatement, TableCreateStatement, TableDropStatement, TableRenameStatement, + TableTruncateStatement, +}; +use sea_orm::{ConnectionTrait, DbBackend, DbErr, StatementBuilder}; +use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite}; + +/// Helper struct for writing migration scripts in migration file +pub struct SchemaManager<'c> { + conn: SchemaManagerConnection<'c>, +} + +impl<'c> SchemaManager<'c> { + pub fn new(conn: T) -> Self + where + T: IntoSchemaManagerConnection<'c>, + { + Self { + conn: conn.into_schema_manager_connection(), + } + } + + pub async fn exec_stmt(&self, stmt: S) -> Result<(), DbErr> + where + S: StatementBuilder, + { + let builder = self.conn.get_database_backend(); + self.conn.execute(builder.build(&stmt)).await.map(|_| ()) + } + + pub fn get_database_backend(&self) -> DbBackend { + self.conn.get_database_backend() + } + + pub fn get_connection(&self) -> &SchemaManagerConnection<'c> { + &self.conn + } +} + +/// Schema Creation +impl<'c> SchemaManager<'c> { + pub async fn create_table(&self, stmt: TableCreateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn create_index(&self, stmt: IndexCreateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn create_foreign_key(&self, stmt: ForeignKeyCreateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn create_type(&self, stmt: TypeCreateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } +} + +/// Schema Mutation +impl<'c> SchemaManager<'c> { + pub async fn alter_table(&self, stmt: TableAlterStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn drop_table(&self, stmt: TableDropStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn rename_table(&self, stmt: TableRenameStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn truncate_table(&self, stmt: TableTruncateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn drop_index(&self, stmt: IndexDropStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn drop_foreign_key(&self, stmt: ForeignKeyDropStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn alter_type(&self, stmt: TypeAlterStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn drop_type(&self, stmt: TypeDropStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } +} + +/// Schema Inspection. +impl<'c> SchemaManager<'c> { + pub async fn has_table(&self, table: T) -> Result + where + T: AsRef, + { + has_table(&self.conn, table).await + } + + pub async fn has_column(&self, table: T, column: C) -> Result + where + T: AsRef, + C: AsRef, + { + let stmt = match self.conn.get_database_backend() { + DbBackend::MySql => MySql.has_column(table, column), + DbBackend::Postgres => Postgres.has_column(table, column), + DbBackend::Sqlite => Sqlite.has_column(table, column), + }; + + let builder = self.conn.get_database_backend(); + let res = self + .conn + .query_one(builder.build(&stmt)) + .await? + .ok_or_else(|| DbErr::Custom("Failed to check column exists".to_owned()))?; + + res.try_get("", "has_column") + } + + pub async fn has_index(&self, table: T, index: I) -> Result + where + T: AsRef, + I: AsRef, + { + let stmt = match self.conn.get_database_backend() { + DbBackend::MySql => MySql.has_index(table, index), + DbBackend::Postgres => Postgres.has_index(table, index), + DbBackend::Sqlite => Sqlite.has_index(table, index), + }; + + let builder = self.conn.get_database_backend(); + let res = self + .conn + .query_one(builder.build(&stmt)) + .await? + .ok_or_else(|| DbErr::Custom("Failed to check index exists".to_owned()))?; + + res.try_get("", "has_index") + } +} + +pub(crate) async fn has_table(conn: &C, table: T) -> Result +where + C: ConnectionTrait, + T: AsRef, +{ + let stmt = match conn.get_database_backend() { + DbBackend::MySql => MySql.has_table(table), + DbBackend::Postgres => Postgres.has_table(table), + DbBackend::Sqlite => Sqlite.has_table(table), + }; + + let builder = conn.get_database_backend(); + let res = conn + .query_one(builder.build(&stmt)) + .await? + .ok_or_else(|| DbErr::Custom("Failed to check table exists".to_owned()))?; + + res.try_get("", "has_table") +} diff --git a/packages/pagetop-seaorm/src/db/migration/migrator.rs b/packages/pagetop-seaorm/src/db/migration/migrator.rs new file mode 100644 index 00000000..06611412 --- /dev/null +++ b/packages/pagetop-seaorm/src/db/migration/migrator.rs @@ -0,0 +1,593 @@ +use futures::Future; +use std::collections::HashSet; +use std::fmt::Display; +use std::pin::Pin; +use std::time::SystemTime; + +use pagetop::trace::info; + +use sea_orm::sea_query::{ + self, extension::postgres::Type, Alias, Expr, ForeignKey, IntoIden, JoinType, Order, Query, + SelectStatement, SimpleExpr, Table, +}; +use sea_orm::{ + ActiveModelTrait, ActiveValue, Condition, ConnectionTrait, DbBackend, DbErr, DeriveIden, + DynIden, EntityTrait, FromQueryResult, Iterable, QueryFilter, Schema, Statement, + TransactionTrait, +}; +use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite}; + +use super::{seaql_migrations, IntoSchemaManagerConnection, MigrationTrait, SchemaManager}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Status of migration +pub enum MigrationStatus { + /// Not yet applied + Pending, + /// Applied + Applied, +} + +impl Display for MigrationStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let status = match self { + MigrationStatus::Pending => "Pending", + MigrationStatus::Applied => "Applied", + }; + write!(f, "{status}") + } +} + +pub struct Migration { + migration: Box, + status: MigrationStatus, +} + +impl Migration { + /// Get migration name from MigrationName trait implementation + pub fn name(&self) -> &str { + self.migration.name() + } + + /// Get migration status + pub fn status(&self) -> MigrationStatus { + self.status + } +} + +/// Performing migrations on a database +#[async_trait::async_trait] +pub trait MigratorTrait: Send { + /// Vector of migrations in time sequence + fn migrations() -> Vec>; + + /// Name of the migration table, it is `seaql_migrations` by default + fn migration_table_name() -> DynIden { + seaql_migrations::Entity.into_iden() + } + + /// Get list of migrations wrapped in `Migration` struct + fn get_migration_files() -> Vec { + Self::migrations() + .into_iter() + .map(|migration| Migration { + migration, + status: MigrationStatus::Pending, + }) + .collect() + } + + /// Get list of applied migrations from database + async fn get_migration_models(db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + Self::install(db).await?; + let stmt = Query::select() + .table_name(Self::migration_table_name()) + .columns(seaql_migrations::Column::iter().map(IntoIden::into_iden)) + .order_by(seaql_migrations::Column::Version, Order::Asc) + .to_owned(); + let builder = db.get_database_backend(); + seaql_migrations::Model::find_by_statement(builder.build(&stmt)) + .all(db) + .await + } + + /// Get list of migrations with status + async fn get_migration_with_status(db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + Self::install(db).await?; + let mut migration_files = Self::get_migration_files(); + let migration_models = Self::get_migration_models(db).await?; + + let migration_in_db: HashSet = migration_models + .into_iter() + .map(|model| model.version) + .collect(); + let migration_in_fs: HashSet = migration_files + .iter() + .map(|file| file.migration.name().to_string()) + .collect(); + + let pending_migrations = &migration_in_fs - &migration_in_db; + for migration_file in migration_files.iter_mut() { + if !pending_migrations.contains(migration_file.migration.name()) { + migration_file.status = MigrationStatus::Applied; + } + } + /* + let missing_migrations_in_fs = &migration_in_db - &migration_in_fs; + let errors: Vec = missing_migrations_in_fs + .iter() + .map(|missing_migration| { + format!("Migration file of version '{missing_migration}' is missing, this migration has been applied but its file is missing") + }).collect(); + + if !errors.is_empty() { + Err(DbErr::Custom(errors.join("\n"))) + } else { */ + Ok(migration_files) + /* } */ + } + + /// Get list of pending migrations + async fn get_pending_migrations(db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + Self::install(db).await?; + Ok(Self::get_migration_with_status(db) + .await? + .into_iter() + .filter(|file| file.status == MigrationStatus::Pending) + .collect()) + } + + /// Get list of applied migrations + async fn get_applied_migrations(db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + Self::install(db).await?; + Ok(Self::get_migration_with_status(db) + .await? + .into_iter() + .filter(|file| file.status == MigrationStatus::Applied) + .collect()) + } + + /// Create migration table `seaql_migrations` in the database + async fn install(db: &C) -> Result<(), DbErr> + where + C: ConnectionTrait, + { + let builder = db.get_database_backend(); + let table_name = Self::migration_table_name(); + let schema = Schema::new(builder); + let mut stmt = schema + .create_table_from_entity(seaql_migrations::Entity) + .table_name(table_name); + stmt.if_not_exists(); + db.execute(builder.build(&stmt)).await.map(|_| ()) + } + + /// Check the status of all migrations + async fn status(db: &C) -> Result<(), DbErr> + where + C: ConnectionTrait, + { + Self::install(db).await?; + + info!("Checking migration status"); + + for Migration { migration, status } in Self::get_migration_with_status(db).await? { + info!("Migration '{}'... {}", migration.name(), status); + } + + Ok(()) + } + + /// Drop all tables from the database, then reapply all migrations + async fn fresh<'c, C>(db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection::<'_, _, _>(db, move |manager| { + Box::pin(async move { exec_fresh::(manager).await }) + }) + .await + } + + /// Rollback all applied migrations, then reapply all migrations + async fn refresh<'c, C>(db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection::<'_, _, _>(db, move |manager| { + Box::pin(async move { + exec_down::(manager, None).await?; + exec_up::(manager, None).await + }) + }) + .await + } + + /// Rollback all applied migrations + async fn reset<'c, C>(db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection::<'_, _, _>(db, move |manager| { + Box::pin(async move { exec_down::(manager, None).await }) + }) + .await + } + + /// Apply pending migrations + async fn up<'c, C>(db: C, steps: Option) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection::<'_, _, _>(db, move |manager| { + Box::pin(async move { exec_up::(manager, steps).await }) + }) + .await + } + + /// Rollback applied migrations + async fn down<'c, C>(db: C, steps: Option) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection::<'_, _, _>(db, move |manager| { + Box::pin(async move { exec_down::(manager, steps).await }) + }) + .await + } +} + +async fn exec_with_connection<'c, C, F>(db: C, f: F) -> Result<(), DbErr> +where + C: IntoSchemaManagerConnection<'c>, + F: for<'b> Fn( + &'b SchemaManager<'_>, + ) -> Pin> + Send + 'b>>, +{ + let db = db.into_schema_manager_connection(); + + match db.get_database_backend() { + DbBackend::Postgres => { + let transaction = db.begin().await?; + let manager = SchemaManager::new(&transaction); + f(&manager).await?; + transaction.commit().await + } + DbBackend::MySql | DbBackend::Sqlite => { + let manager = SchemaManager::new(db); + f(&manager).await + } + } +} + +async fn exec_fresh(manager: &SchemaManager<'_>) -> Result<(), DbErr> +where + M: MigratorTrait + ?Sized, +{ + let db = manager.get_connection(); + + M::install(db).await?; + let db_backend = db.get_database_backend(); + + // Temporarily disable the foreign key check + if db_backend == DbBackend::Sqlite { + info!("Disabling foreign key check"); + db.execute(Statement::from_string( + db_backend, + "PRAGMA foreign_keys = OFF".to_owned(), + )) + .await?; + info!("Foreign key check disabled"); + } + + // Drop all foreign keys + if db_backend == DbBackend::MySql { + info!("Dropping all foreign keys"); + let stmt = query_mysql_foreign_keys(db); + let rows = db.query_all(db_backend.build(&stmt)).await?; + for row in rows.into_iter() { + let constraint_name: String = row.try_get("", "CONSTRAINT_NAME")?; + let table_name: String = row.try_get("", "TABLE_NAME")?; + info!( + "Dropping foreign key '{}' from table '{}'", + constraint_name, table_name + ); + let mut stmt = ForeignKey::drop(); + stmt.table(Alias::new(table_name.as_str())) + .name(constraint_name.as_str()); + db.execute(db_backend.build(&stmt)).await?; + info!("Foreign key '{}' has been dropped", constraint_name); + } + info!("All foreign keys dropped"); + } + + // Drop all tables + let stmt = query_tables(db).await; + let rows = db.query_all(db_backend.build(&stmt)).await?; + for row in rows.into_iter() { + let table_name: String = row.try_get("", "table_name")?; + info!("Dropping table '{}'", table_name); + let mut stmt = Table::drop(); + stmt.table(Alias::new(table_name.as_str())) + .if_exists() + .cascade(); + db.execute(db_backend.build(&stmt)).await?; + info!("Table '{}' has been dropped", table_name); + } + + // Drop all types + if db_backend == DbBackend::Postgres { + info!("Dropping all types"); + let stmt = query_pg_types(db); + let rows = db.query_all(db_backend.build(&stmt)).await?; + for row in rows { + let type_name: String = row.try_get("", "typname")?; + info!("Dropping type '{}'", type_name); + let mut stmt = Type::drop(); + stmt.name(Alias::new(&type_name)); + db.execute(db_backend.build(&stmt)).await?; + info!("Type '{}' has been dropped", type_name); + } + } + + // Restore the foreign key check + if db_backend == DbBackend::Sqlite { + info!("Restoring foreign key check"); + db.execute(Statement::from_string( + db_backend, + "PRAGMA foreign_keys = ON".to_owned(), + )) + .await?; + info!("Foreign key check restored"); + } + + // Reapply all migrations + exec_up::(manager, None).await +} + +async fn exec_up(manager: &SchemaManager<'_>, mut steps: Option) -> Result<(), DbErr> +where + M: MigratorTrait + ?Sized, +{ + let db = manager.get_connection(); + + M::install(db).await?; + /* + if let Some(steps) = steps { + info!("Applying {} pending migrations", steps); + } else { + info!("Applying all pending migrations"); + } + */ + let migrations = M::get_pending_migrations(db).await?.into_iter(); + /* + if migrations.len() == 0 { + info!("No pending migrations"); + } + */ + for Migration { migration, .. } in migrations { + if let Some(steps) = steps.as_mut() { + if steps == &0 { + break; + } + *steps -= 1; + } + info!("Applying migration '{}'", migration.name()); + migration.up(manager).await?; + info!("Migration '{}' has been applied", migration.name()); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("SystemTime before UNIX EPOCH!"); + seaql_migrations::Entity::insert(seaql_migrations::ActiveModel { + version: ActiveValue::Set(migration.name().to_owned()), + applied_at: ActiveValue::Set(now.as_secs() as i64), + }) + .table_name(M::migration_table_name()) + .exec(db) + .await?; + } + + Ok(()) +} + +async fn exec_down(manager: &SchemaManager<'_>, mut steps: Option) -> Result<(), DbErr> +where + M: MigratorTrait + ?Sized, +{ + let db = manager.get_connection(); + + M::install(db).await?; + + if let Some(steps) = steps { + info!("Rolling back {} applied migrations", steps); + } else { + info!("Rolling back all applied migrations"); + } + + let migrations = M::get_applied_migrations(db).await?.into_iter().rev(); + if migrations.len() == 0 { + info!("No applied migrations"); + } + for Migration { migration, .. } in migrations { + if let Some(steps) = steps.as_mut() { + if steps == &0 { + break; + } + *steps -= 1; + } + info!("Rolling back migration '{}'", migration.name()); + migration.down(manager).await?; + info!("Migration '{}' has been rollbacked", migration.name()); + seaql_migrations::Entity::delete_many() + .filter(Expr::col(seaql_migrations::Column::Version).eq(migration.name())) + .table_name(M::migration_table_name()) + .exec(db) + .await?; + } + + Ok(()) +} + +async fn query_tables(db: &C) -> SelectStatement +where + C: ConnectionTrait, +{ + match db.get_database_backend() { + DbBackend::MySql => MySql.query_tables(), + DbBackend::Postgres => Postgres.query_tables(), + DbBackend::Sqlite => Sqlite.query_tables(), + } +} + +fn get_current_schema(db: &C) -> SimpleExpr +where + C: ConnectionTrait, +{ + match db.get_database_backend() { + DbBackend::MySql => MySql::get_current_schema(), + DbBackend::Postgres => Postgres::get_current_schema(), + DbBackend::Sqlite => unimplemented!(), + } +} + +#[derive(DeriveIden)] +enum InformationSchema { + #[sea_orm(iden = "information_schema")] + Schema, + #[sea_orm(iden = "TABLE_NAME")] + TableName, + #[sea_orm(iden = "CONSTRAINT_NAME")] + ConstraintName, + TableConstraints, + TableSchema, + ConstraintType, +} + +fn query_mysql_foreign_keys(db: &C) -> SelectStatement +where + C: ConnectionTrait, +{ + let mut stmt = Query::select(); + stmt.columns([ + InformationSchema::TableName, + InformationSchema::ConstraintName, + ]) + .from(( + InformationSchema::Schema, + InformationSchema::TableConstraints, + )) + .cond_where( + Condition::all() + .add(Expr::expr(get_current_schema(db)).equals(( + InformationSchema::TableConstraints, + InformationSchema::TableSchema, + ))) + .add( + Expr::col(( + InformationSchema::TableConstraints, + InformationSchema::ConstraintType, + )) + .eq("FOREIGN KEY"), + ), + ); + stmt +} + +#[derive(DeriveIden)] +enum PgType { + Table, + Typname, + Typnamespace, + Typelem, +} + +#[derive(DeriveIden)] +enum PgNamespace { + Table, + Oid, + Nspname, +} + +fn query_pg_types(db: &C) -> SelectStatement +where + C: ConnectionTrait, +{ + let mut stmt = Query::select(); + stmt.column(PgType::Typname) + .from(PgType::Table) + .join( + JoinType::LeftJoin, + PgNamespace::Table, + Expr::col((PgNamespace::Table, PgNamespace::Oid)) + .equals((PgType::Table, PgType::Typnamespace)), + ) + .cond_where( + Condition::all() + .add( + Expr::expr(get_current_schema(db)) + .equals((PgNamespace::Table, PgNamespace::Nspname)), + ) + .add(Expr::col((PgType::Table, PgType::Typelem)).eq(0)), + ); + stmt +} + +trait QueryTable { + type Statement; + + fn table_name(self, table_name: DynIden) -> Self::Statement; +} + +impl QueryTable for SelectStatement { + type Statement = SelectStatement; + + fn table_name(mut self, table_name: DynIden) -> SelectStatement { + self.from(table_name); + self + } +} + +impl QueryTable for sea_query::TableCreateStatement { + type Statement = sea_query::TableCreateStatement; + + fn table_name(mut self, table_name: DynIden) -> sea_query::TableCreateStatement { + self.table(table_name); + self + } +} + +impl QueryTable for sea_orm::Insert +where + A: ActiveModelTrait, +{ + type Statement = sea_orm::Insert; + + fn table_name(mut self, table_name: DynIden) -> sea_orm::Insert { + sea_orm::QueryTrait::query(&mut self).into_table(table_name); + self + } +} + +impl QueryTable for sea_orm::DeleteMany +where + E: EntityTrait, +{ + type Statement = sea_orm::DeleteMany; + + fn table_name(mut self, table_name: DynIden) -> sea_orm::DeleteMany { + sea_orm::QueryTrait::query(&mut self).from_table(table_name); + self + } +} diff --git a/packages/pagetop-seaorm/src/db/migration/prelude.rs b/packages/pagetop-seaorm/src/db/migration/prelude.rs new file mode 100644 index 00000000..5556a094 --- /dev/null +++ b/packages/pagetop-seaorm/src/db/migration/prelude.rs @@ -0,0 +1,13 @@ +//pub use super::cli; + +pub use super::connection::IntoSchemaManagerConnection; +pub use super::connection::SchemaManagerConnection; +pub use super::manager::SchemaManager; +pub use super::migrator::MigratorTrait; +pub use super::{MigrationName, MigrationTrait}; +pub use async_trait; +pub use sea_orm; +pub use sea_orm::sea_query; +pub use sea_orm::sea_query::*; +pub use sea_orm::DeriveIden; +pub use sea_orm::DeriveMigrationName; diff --git a/packages/pagetop-seaorm/src/db/migration/schema.rs b/packages/pagetop-seaorm/src/db/migration/schema.rs new file mode 100644 index 00000000..cb013c59 --- /dev/null +++ b/packages/pagetop-seaorm/src/db/migration/schema.rs @@ -0,0 +1,613 @@ +//! > Adapted from +//! +//! # Database Table Schema Helpers +//! +//! This module defines functions and helpers for creating database table +//! schemas using the `sea-orm` and `sea-query` libraries. +//! +//! # Example +//! +//! The following example shows how the user migration file should be and using +//! the schema helpers to create the Db fields. +//! +//! ```rust +//! use sea_orm_migration::{prelude::*, schema::*}; +//! +//! #[derive(DeriveMigrationName)] +//! pub struct Migration; +//! +//! #[async_trait::async_trait] +//! impl MigrationTrait for Migration { +//! async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { +//! let table = table_auto(Users::Table) +//! .col(pk_auto(Users::Id)) +//! .col(uuid(Users::Pid)) +//! .col(string_uniq(Users::Email)) +//! .col(string(Users::Password)) +//! .col(string(Users::Name)) +//! .col(string_null(Users::ResetToken)) +//! .col(timestamp_null(Users::ResetSentAt)) +//! .to_owned(); +//! manager.create_table(table).await?; +//! Ok(()) +//! } +//! +//! async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { +//! manager +//! .drop_table(Table::drop().table(Users::Table).to_owned()) +//! .await +//! } +//! } +//! +//! #[derive(Iden)] +//! pub enum Users { +//! Table, +//! Id, +//! Pid, +//! Email, +//! Name, +//! Password, +//! ResetToken, +//! ResetSentAt, +//! } +//! ``` + +use crate::prelude::Iden; +use sea_orm::sea_query::{ + self, Alias, ColumnDef, ColumnType, Expr, IntoIden, PgInterval, Table, TableCreateStatement, +}; + +#[derive(Iden)] +enum GeneralIds { + CreatedAt, + UpdatedAt, +} + +/// Wrapping table schema creation. +pub fn table_auto(name: T) -> TableCreateStatement { + timestamps(Table::create().table(name).if_not_exists().take()) +} + +/// Create a primary key column with auto-increment feature. +pub fn pk_auto(name: T) -> ColumnDef { + integer(name).auto_increment().primary_key().take() +} + +/// Create a UUID primary key +pub fn pk_uuid(name: T) -> ColumnDef { + uuid(name).primary_key().take() +} + +pub fn char_len(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).char_len(length).not_null().take() +} + +pub fn char_len_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).char_len(length).null().take() +} + +pub fn char_len_uniq(col: T, length: u32) -> ColumnDef { + char_len(col, length).unique_key().take() +} + +pub fn char(col: T) -> ColumnDef { + ColumnDef::new(col).char().not_null().take() +} + +pub fn char_null(col: T) -> ColumnDef { + ColumnDef::new(col).char().null().take() +} + +pub fn char_uniq(col: T) -> ColumnDef { + char(col).unique_key().take() +} + +pub fn string_len(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).string_len(length).not_null().take() +} + +pub fn string_len_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).string_len(length).null().take() +} + +pub fn string_len_uniq(col: T, length: u32) -> ColumnDef { + string_len(col, length).unique_key().take() +} + +pub fn string(col: T) -> ColumnDef { + ColumnDef::new(col).string().not_null().take() +} + +pub fn string_null(col: T) -> ColumnDef { + ColumnDef::new(col).string().null().take() +} + +pub fn string_uniq(col: T) -> ColumnDef { + string(col).unique_key().take() +} + +pub fn text(col: T) -> ColumnDef { + ColumnDef::new(col).text().not_null().take() +} + +pub fn text_null(col: T) -> ColumnDef { + ColumnDef::new(col).text().null().take() +} + +pub fn text_uniq(col: T) -> ColumnDef { + text(col).unique_key().take() +} + +pub fn tiny_integer(col: T) -> ColumnDef { + ColumnDef::new(col).tiny_integer().not_null().take() +} + +pub fn tiny_integer_null(col: T) -> ColumnDef { + ColumnDef::new(col).tiny_integer().null().take() +} + +pub fn tiny_integer_uniq(col: T) -> ColumnDef { + tiny_integer(col).unique_key().take() +} + +pub fn small_integer(col: T) -> ColumnDef { + ColumnDef::new(col).small_integer().not_null().take() +} + +pub fn small_integer_null(col: T) -> ColumnDef { + ColumnDef::new(col).small_integer().null().take() +} + +pub fn small_integer_uniq(col: T) -> ColumnDef { + small_integer(col).unique_key().take() +} + +pub fn integer(col: T) -> ColumnDef { + ColumnDef::new(col).integer().not_null().take() +} + +pub fn integer_null(col: T) -> ColumnDef { + ColumnDef::new(col).integer().null().take() +} + +pub fn integer_uniq(col: T) -> ColumnDef { + integer(col).unique_key().take() +} + +pub fn big_integer(col: T) -> ColumnDef { + ColumnDef::new(col).big_integer().not_null().take() +} + +pub fn big_integer_null(col: T) -> ColumnDef { + ColumnDef::new(col).big_integer().null().take() +} + +pub fn big_integer_uniq(col: T) -> ColumnDef { + big_integer(col).unique_key().take() +} + +pub fn tiny_unsigned(col: T) -> ColumnDef { + ColumnDef::new(col).tiny_unsigned().not_null().take() +} + +pub fn tiny_unsigned_null(col: T) -> ColumnDef { + ColumnDef::new(col).tiny_unsigned().null().take() +} + +pub fn tiny_unsigned_uniq(col: T) -> ColumnDef { + tiny_unsigned(col).unique_key().take() +} + +pub fn small_unsigned(col: T) -> ColumnDef { + ColumnDef::new(col).small_unsigned().not_null().take() +} + +pub fn small_unsigned_null(col: T) -> ColumnDef { + ColumnDef::new(col).small_unsigned().null().take() +} + +pub fn small_unsigned_uniq(col: T) -> ColumnDef { + small_unsigned(col).unique_key().take() +} + +pub fn unsigned(col: T) -> ColumnDef { + ColumnDef::new(col).unsigned().not_null().take() +} + +pub fn unsigned_null(col: T) -> ColumnDef { + ColumnDef::new(col).unsigned().null().take() +} + +pub fn unsigned_uniq(col: T) -> ColumnDef { + unsigned(col).unique_key().take() +} + +pub fn big_unsigned(col: T) -> ColumnDef { + ColumnDef::new(col).big_unsigned().not_null().take() +} + +pub fn big_unsigned_null(col: T) -> ColumnDef { + ColumnDef::new(col).big_unsigned().null().take() +} + +pub fn big_unsigned_uniq(col: T) -> ColumnDef { + big_unsigned(col).unique_key().take() +} + +pub fn float(col: T) -> ColumnDef { + ColumnDef::new(col).float().not_null().take() +} + +pub fn float_null(col: T) -> ColumnDef { + ColumnDef::new(col).float().null().take() +} + +pub fn float_uniq(col: T) -> ColumnDef { + float(col).unique_key().take() +} + +pub fn double(col: T) -> ColumnDef { + ColumnDef::new(col).double().not_null().take() +} + +pub fn double_null(col: T) -> ColumnDef { + ColumnDef::new(col).double().null().take() +} + +pub fn double_uniq(col: T) -> ColumnDef { + double(col).unique_key().take() +} + +pub fn decimal_len(col: T, precision: u32, scale: u32) -> ColumnDef { + ColumnDef::new(col) + .decimal_len(precision, scale) + .not_null() + .take() +} + +pub fn decimal_len_null(col: T, precision: u32, scale: u32) -> ColumnDef { + ColumnDef::new(col) + .decimal_len(precision, scale) + .null() + .take() +} + +pub fn decimal_len_uniq(col: T, precision: u32, scale: u32) -> ColumnDef { + decimal_len(col, precision, scale).unique_key().take() +} + +pub fn decimal(col: T) -> ColumnDef { + ColumnDef::new(col).decimal().not_null().take() +} + +pub fn decimal_null(col: T) -> ColumnDef { + ColumnDef::new(col).decimal().null().take() +} + +pub fn decimal_uniq(col: T) -> ColumnDef { + decimal(col).unique_key().take() +} + +pub fn date_time(col: T) -> ColumnDef { + ColumnDef::new(col).date_time().not_null().take() +} + +pub fn date_time_null(col: T) -> ColumnDef { + ColumnDef::new(col).date_time().null().take() +} + +pub fn date_time_uniq(col: T) -> ColumnDef { + date_time(col).unique_key().take() +} + +pub fn interval( + col: T, + fields: Option, + precision: Option, +) -> ColumnDef { + ColumnDef::new(col) + .interval(fields, precision) + .not_null() + .take() +} + +pub fn interval_null( + col: T, + fields: Option, + precision: Option, +) -> ColumnDef { + ColumnDef::new(col) + .interval(fields, precision) + .null() + .take() +} + +pub fn interval_uniq( + col: T, + fields: Option, + precision: Option, +) -> ColumnDef { + interval(col, fields, precision).unique_key().take() +} + +pub fn timestamp(col: T) -> ColumnDef { + ColumnDef::new(col).timestamp().not_null().take() +} + +pub fn timestamp_null(col: T) -> ColumnDef { + ColumnDef::new(col).timestamp().null().take() +} + +pub fn timestamp_uniq(col: T) -> ColumnDef { + timestamp(col).unique_key().take() +} + +pub fn timestamp_with_time_zone(col: T) -> ColumnDef { + ColumnDef::new(col) + .timestamp_with_time_zone() + .not_null() + .take() +} + +pub fn timestamp_with_time_zone_null(col: T) -> ColumnDef { + ColumnDef::new(col).timestamp_with_time_zone().null().take() +} + +pub fn timestamp_with_time_zone_uniq(col: T) -> ColumnDef { + timestamp_with_time_zone(col).unique_key().take() +} + +pub fn time(col: T) -> ColumnDef { + ColumnDef::new(col).time().not_null().take() +} + +pub fn time_null(col: T) -> ColumnDef { + ColumnDef::new(col).time().null().take() +} + +pub fn time_uniq(col: T) -> ColumnDef { + time(col).unique_key().take() +} + +pub fn date(col: T) -> ColumnDef { + ColumnDef::new(col).date().not_null().take() +} + +pub fn date_null(col: T) -> ColumnDef { + ColumnDef::new(col).date().null().take() +} + +pub fn date_uniq(col: T) -> ColumnDef { + date(col).unique_key().take() +} + +pub fn year(col: T) -> ColumnDef { + ColumnDef::new(col).year().not_null().take() +} + +pub fn year_null(col: T) -> ColumnDef { + ColumnDef::new(col).year().null().take() +} + +pub fn year_uniq(col: T) -> ColumnDef { + year(col).unique_key().take() +} + +pub fn binary_len(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).binary_len(length).not_null().take() +} + +pub fn binary_len_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).binary_len(length).null().take() +} + +pub fn binary_len_uniq(col: T, length: u32) -> ColumnDef { + binary_len(col, length).unique_key().take() +} + +pub fn binary(col: T) -> ColumnDef { + ColumnDef::new(col).binary().not_null().take() +} + +pub fn binary_null(col: T) -> ColumnDef { + ColumnDef::new(col).binary().null().take() +} + +pub fn binary_uniq(col: T) -> ColumnDef { + binary(col).unique_key().take() +} + +pub fn var_binary(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).var_binary(length).not_null().take() +} + +pub fn var_binary_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).var_binary(length).null().take() +} + +pub fn var_binary_uniq(col: T, length: u32) -> ColumnDef { + var_binary(col, length).unique_key().take() +} + +pub fn bit(col: T, length: Option) -> ColumnDef { + ColumnDef::new(col).bit(length).not_null().take() +} + +pub fn bit_null(col: T, length: Option) -> ColumnDef { + ColumnDef::new(col).bit(length).null().take() +} + +pub fn bit_uniq(col: T, length: Option) -> ColumnDef { + bit(col, length).unique_key().take() +} + +pub fn varbit(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).varbit(length).not_null().take() +} + +pub fn varbit_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).varbit(length).null().take() +} + +pub fn varbit_uniq(col: T, length: u32) -> ColumnDef { + varbit(col, length).unique_key().take() +} + +pub fn blob(col: T) -> ColumnDef { + ColumnDef::new(col).blob().not_null().take() +} + +pub fn blob_null(col: T) -> ColumnDef { + ColumnDef::new(col).blob().null().take() +} + +pub fn blob_uniq(col: T) -> ColumnDef { + blob(col).unique_key().take() +} + +pub fn boolean(col: T) -> ColumnDef { + ColumnDef::new(col).boolean().not_null().take() +} + +pub fn boolean_null(col: T) -> ColumnDef { + ColumnDef::new(col).boolean().null().take() +} + +pub fn boolean_uniq(col: T) -> ColumnDef { + boolean(col).unique_key().take() +} + +pub fn money_len(col: T, precision: u32, scale: u32) -> ColumnDef { + ColumnDef::new(col) + .money_len(precision, scale) + .not_null() + .take() +} + +pub fn money_len_null(col: T, precision: u32, scale: u32) -> ColumnDef { + ColumnDef::new(col) + .money_len(precision, scale) + .null() + .take() +} + +pub fn money_len_uniq(col: T, precision: u32, scale: u32) -> ColumnDef { + money_len(col, precision, scale).unique_key().take() +} + +pub fn money(col: T) -> ColumnDef { + ColumnDef::new(col).money().not_null().take() +} + +pub fn money_null(col: T) -> ColumnDef { + ColumnDef::new(col).money().null().take() +} + +pub fn money_uniq(col: T) -> ColumnDef { + money(col).unique_key().take() +} + +pub fn json(col: T) -> ColumnDef { + ColumnDef::new(col).json().not_null().take() +} + +pub fn json_null(col: T) -> ColumnDef { + ColumnDef::new(col).json().null().take() +} + +pub fn json_uniq(col: T) -> ColumnDef { + json(col).unique_key().take() +} + +pub fn json_binary(col: T) -> ColumnDef { + ColumnDef::new(col).json_binary().not_null().take() +} + +pub fn json_binary_null(col: T) -> ColumnDef { + ColumnDef::new(col).json_binary().null().take() +} + +pub fn json_binary_uniq(col: T) -> ColumnDef { + json_binary(col).unique_key().take() +} + +pub fn uuid(col: T) -> ColumnDef { + ColumnDef::new(col).uuid().not_null().take() +} + +pub fn uuid_null(col: T) -> ColumnDef { + ColumnDef::new(col).uuid().null().take() +} + +pub fn uuid_uniq(col: T) -> ColumnDef { + uuid(col).unique_key().take() +} + +pub fn custom(col: T, name: N) -> ColumnDef { + ColumnDef::new(col).custom(name).not_null().take() +} + +pub fn custom_null(col: T, name: N) -> ColumnDef { + ColumnDef::new(col).custom(name).null().take() +} + +pub fn enumeration(col: T, name: N, variants: V) -> ColumnDef +where + T: IntoIden, + N: IntoIden, + S: IntoIden, + V: IntoIterator, +{ + ColumnDef::new(col) + .enumeration(name, variants) + .not_null() + .take() +} + +pub fn enumeration_null(col: T, name: N, variants: V) -> ColumnDef +where + T: IntoIden, + N: IntoIden, + S: IntoIden, + V: IntoIterator, +{ + ColumnDef::new(col) + .enumeration(name, variants) + .null() + .take() +} + +pub fn enumeration_uniq(col: T, name: N, variants: V) -> ColumnDef +where + T: IntoIden, + N: IntoIden, + S: IntoIden, + V: IntoIterator, +{ + enumeration(col, name, variants).unique_key().take() +} + +pub fn array(col: T, elem_type: ColumnType) -> ColumnDef { + ColumnDef::new(col).array(elem_type).not_null().take() +} + +pub fn array_null(col: T, elem_type: ColumnType) -> ColumnDef { + ColumnDef::new(col).array(elem_type).null().take() +} + +pub fn array_uniq(col: T, elem_type: ColumnType) -> ColumnDef { + array(col, elem_type).unique_key().take() +} + +/// Add timestamp columns (`CreatedAt` and `UpdatedAt`) to an existing table. +pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement { + let mut t = t; + t.col(timestamp(GeneralIds::CreatedAt).default(Expr::current_timestamp())) + .col(timestamp(GeneralIds::UpdatedAt).default(Expr::current_timestamp())) + .take() +} + +/// Create an Alias. +pub fn name>(name: T) -> Alias { + Alias::new(name) +} diff --git a/packages/pagetop-seaorm/src/db/migration/seaql_migrations.rs b/packages/pagetop-seaorm/src/db/migration/seaql_migrations.rs new file mode 100644 index 00000000..51da9300 --- /dev/null +++ b/packages/pagetop-seaorm/src/db/migration/seaql_migrations.rs @@ -0,0 +1,15 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +// One should override the name of migration table via `MigratorTrait::migration_table_name` method +#[sea_orm(table_name = "seaql_migrations")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub version: String, + pub applied_at: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/packages/pagetop-seaorm/src/lib.rs b/packages/pagetop-seaorm/src/lib.rs new file mode 100644 index 00000000..75c524a7 --- /dev/null +++ b/packages/pagetop-seaorm/src/lib.rs @@ -0,0 +1,31 @@ +use pagetop::prelude::*; + +use std::sync::LazyLock; + +pub mod config; +pub mod db; + +/// The package Prelude. +pub mod prelude { + pub use crate::db::*; + pub use crate::install_migrations; +} + +include_locales!(LOCALES_SEAORM); + +/// Implements [`PackageTrait`] and specific package API. +pub struct SeaORM; + +impl PackageTrait for SeaORM { + fn name(&self) -> L10n { + L10n::t("package_name", &LOCALES_SEAORM) + } + + fn description(&self) -> L10n { + L10n::t("package_description", &LOCALES_SEAORM) + } + + fn init(&self) { + LazyLock::force(&db::DBCONN); + } +} diff --git a/packages/pagetop-seaorm/src/locale/en-US/package.ftl b/packages/pagetop-seaorm/src/locale/en-US/package.ftl new file mode 100644 index 00000000..d1c53cc1 --- /dev/null +++ b/packages/pagetop-seaorm/src/locale/en-US/package.ftl @@ -0,0 +1,2 @@ +package_name = SeaORM support +package_description = Integrate SeaORM as the database framework for PageTop applications. diff --git a/packages/pagetop-seaorm/src/locale/es-ES/package.ftl b/packages/pagetop-seaorm/src/locale/es-ES/package.ftl new file mode 100644 index 00000000..bd7c004b --- /dev/null +++ b/packages/pagetop-seaorm/src/locale/es-ES/package.ftl @@ -0,0 +1,2 @@ +package_name = Soporte a SeaORM +package_description = Integra SeaORM como framework de base de datos para aplicaciones PageTop. diff --git a/packages/pagetop/src/core/theme/definition.rs b/packages/pagetop/src/core/theme/definition.rs deleted file mode 100644 index 70c178f2..00000000 --- a/packages/pagetop/src/core/theme/definition.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::core::package::PackageTrait; - -pub type ThemeRef = &'static dyn ThemeTrait; - -/// Los temas deben implementar este "trait". -pub trait ThemeTrait: PackageTrait + Send + Sync { - /* - #[rustfmt::skip] - fn regions(&self) -> Vec<(&'static str, L10n)> { - vec![ - ("header", L10n::l("header")), - ("pagetop", L10n::l("pagetop")), - ("sidebar_left", L10n::l("sidebar_left")), - ("content", L10n::l("content")), - ("sidebar_right", L10n::l("sidebar_right")), - ("footer", L10n::l("footer")), - ] - } - - #[allow(unused_variables)] - fn before_prepare_body(&self, page: &mut Page) {} - - fn prepare_body(&self, page: &mut Page) -> PrepareMarkup { - let skip_to_id = page.body_skip_to().get().unwrap_or("content".to_owned()); - - PrepareMarkup::With(html! { - body id=[page.body_id().get()] class=[page.body_classes().get()] { - @if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) { - div class="skip__to_content" { - a href=(concat_string!("#", skip_to_id)) { (skip) } - } - } - (flex::Container::new() - .with_id("body__wrapper") - .with_direction(flex::Direction::Column(BreakPoint::None)) - .with_align(flex::Align::Center) - .add_item(flex::Item::region().with_id("header")) - .add_item(flex::Item::region().with_id("pagetop")) - .add_item( - flex::Item::with( - flex::Container::new() - .with_direction(flex::Direction::Row(BreakPoint::None)) - .add_item( - flex::Item::region() - .with_id("sidebar_left") - .with_grow(flex::Grow::Is1), - ) - .add_item( - flex::Item::region() - .with_id("content") - .with_grow(flex::Grow::Is3), - ) - .add_item( - flex::Item::region() - .with_id("sidebar_right") - .with_grow(flex::Grow::Is1), - ), - ) - .with_id("flex__wrapper"), - ) - .add_item(flex::Item::region().with_id("footer")) - .render(page.context())) - } - }) - } - - fn after_prepare_body(&self, page: &mut Page) { - page.set_assets(AssetsOp::SetFaviconIfNone( - Favicon::new().with_icon("/base/favicon.ico"), - )); - } - - fn prepare_head(&self, page: &mut Page) -> PrepareMarkup { - let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; - PrepareMarkup::With(html! { - head { - meta charset="utf-8"; - - @if let Some(title) = page.title() { - title { (global::SETTINGS.app.name) (" - ") (title) } - } @else { - title { (global::SETTINGS.app.name) } - } - - @if let Some(description) = page.description() { - meta name="description" content=(description); - } - - meta name="viewport" content=(viewport); - @for (name, content) in page.metadata() { - meta name=(name) content=(content) {} - } - - meta http-equiv="X-UA-Compatible" content="IE=edge"; - @for (property, content) in page.properties() { - meta property=(property) content=(content) {} - } - - (page.context().prepare_assets()) - } - }) - } - */ -} diff --git a/packages/pagetop/src/html.rs b/packages/pagetop/src/html.rs deleted file mode 100644 index f81273bf..00000000 --- a/packages/pagetop/src/html.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! HTML in code. - -mod maud; -pub use maud::{html, html_private, Markup, PreEscaped, DOCTYPE}; diff --git a/packages/pagetop/src/locale/en-US/base.ftl b/packages/pagetop/src/locale/en-US/base.ftl deleted file mode 100644 index 9ec4803b..00000000 --- a/packages/pagetop/src/locale/en-US/base.ftl +++ /dev/null @@ -1,13 +0,0 @@ -# Branding component. -site_home = Home - -# PoweredBy component. -poweredby_pagetop = Powered by {$pagetop_link} -pagetop_logo = PageTop logo - -# Menu component. -menu_toggle = Toggle menu visibility - -# Form components. -button_submit = Submit -button_reset = Reset diff --git a/packages/pagetop/src/locale/en-US/theme.ftl b/packages/pagetop/src/locale/en-US/theme.ftl deleted file mode 100644 index 6b3cb0e8..00000000 --- a/packages/pagetop/src/locale/en-US/theme.ftl +++ /dev/null @@ -1,8 +0,0 @@ -header = Header -pagetop = Page Top -content = Content -sidebar_left = Sidebar Left -sidebar_right = Sidebar Right -footer = Footer - -skip_to_content = Skip to main content (Press Enter) diff --git a/packages/pagetop/src/locale/en-US/welcome.ftl b/packages/pagetop/src/locale/en-US/welcome.ftl deleted file mode 100644 index 566691e4..00000000 --- a/packages/pagetop/src/locale/en-US/welcome.ftl +++ /dev/null @@ -1,26 +0,0 @@ -welcome_package_name = Default homepage -welcome_package_description = Displays a demo homepage when none is configured. - -welcome_title = Hello world! - -welcome_intro = This page is used to check the proper operation of the { $app } installation. -welcome_powered = This web solution is powered by { $pagetop }. -welcome_code = Code -welcome = Welcome - -welcome_page = Welcome page -welcome_subtitle = Are you user of { $app }? -welcome_text1 = If you don't know what this page is about, this probably means that the site is either experiencing problems or is undergoing routine maintenance. -welcome_text2 = If the problem persists, please contact your system administrator. - -welcome_pagetop_title = About PageTop -welcome_pagetop_text1 = If you can read this page, it means that the PageTop server is working properly, but has not yet been configured. -welcome_pagetop_text2 = PageTop is a Rust-based web development framework to build modular, extensible, and configurable web solutions. -welcome_pagetop_text3 = For more information on PageTop please visit the technical documentation. - -welcome_promo_title = Promoting PageTop -welcome_promo_text1 = You are free to use the image below on applications powered by { $pagetop }. Thanks for using PageTop! - -welcome_issues_title = Reporting problems -welcome_issues_text1 = Please use GitHub to report any issues with PageTop. However, check the existing error reports before submitting a new issue. -welcome_issues_text2 = If the issues are specific to { $app }, please refer to its official repository or support channel, rather than directly to PageTop. diff --git a/packages/pagetop/src/locale/es-ES/base.ftl b/packages/pagetop/src/locale/es-ES/base.ftl deleted file mode 100644 index 953d891c..00000000 --- a/packages/pagetop/src/locale/es-ES/base.ftl +++ /dev/null @@ -1,13 +0,0 @@ -# Branding component. -site_home = Inicio - -# PoweredBy component. -poweredby_pagetop = Funciona con {$pagetop_link} -pagetop_logo = Logotipo de PageTop - -# Menu component. -menu_toggle = Alternar visibilidad del menú - -# Form components. -button_submit = Enviar -button_reset = Reiniciar diff --git a/packages/pagetop/src/locale/es-ES/theme.ftl b/packages/pagetop/src/locale/es-ES/theme.ftl deleted file mode 100644 index fb5caacd..00000000 --- a/packages/pagetop/src/locale/es-ES/theme.ftl +++ /dev/null @@ -1,8 +0,0 @@ -header = Cabecera -pagetop = Superior -content = Contenido -sidebar_left = Barra lateral izquierda -sidebar_right = Barra lateral derecha -footer = Pie - -skip_to_content = Ir al contenido principal (Pulsar Intro) diff --git a/packages/pagetop/src/locale/es-ES/welcome.ftl b/packages/pagetop/src/locale/es-ES/welcome.ftl deleted file mode 100644 index 5034f155..00000000 --- a/packages/pagetop/src/locale/es-ES/welcome.ftl +++ /dev/null @@ -1,26 +0,0 @@ -welcome_package_name = Página de inicio predeterminada -welcome_package_description = Muestra una página de demostración predeterminada cuando no hay ninguna configurada. - -welcome_title = ¡Hola mundo! - -welcome_intro = Esta página se utiliza para verificar el correcto funcionamiento de la instalación de { $app }. -welcome_powered = Esta solución web funciona con { $pagetop }. -welcome_code = Código -welcome = Bienvenida - -welcome_page = Página de bienvenida -welcome_subtitle = ¿Eres usuario de { $app }? -welcome_text1 = Si no sabes de qué trata esta página, probablemente significa que el sitio está experimentando problemas o está pasando por un mantenimiento de rutina. -welcome_text2 = Si el problema persiste, póngase en contacto con el administrador del sistema. - -welcome_pagetop_title = Sobre PageTop -welcome_pagetop_text1 = Si puedes leer esta página, significa que el servidor PageTop funciona correctamente, pero aún no se ha configurado. -welcome_pagetop_text2 = PageTop es un entorno de desarrollo web basado en Rust para construir soluciones web modulares, extensibles y configurables. -welcome_pagetop_text3 = Para más información sobre PageTop, por favor visita la documentación técnica. - -welcome_promo_title = Promociona PageTop -welcome_promo_text1 = Eres libre de usar la siguiente imagen en aplicaciones desarrolladas con { $pagetop }. ¡Gracias por usar PageTop! - -welcome_issues_title = Informando problemas -welcome_issues_text1 = Por favor, utiliza GitHub para reportar cualquier problema con PageTop. No obstante, comprueba los informes de errores existentes antes de enviar uno nuevo. -welcome_issues_text2 = Si son fallos específicos de { $app }, por favor acude a su repositorio o canal de soporte oficial y no al de PageTop directamente. diff --git a/packages/pagetop/src/util.rs b/packages/pagetop/src/util.rs deleted file mode 100644 index ed743491..00000000 --- a/packages/pagetop/src/util.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! Useful functions and macros. - -pub mod config; - -mod data; -mod de; -mod error; -mod file; -mod path; -mod source; -mod value; - -use crate::trace; - -use std::io; -use std::path::PathBuf; - -// USEFUL FUNCTIONS ******************************************************************************** - -pub enum TypeInfo { - FullName, - ShortName, - NameFrom(isize), - NameTo(isize), - PartialName(isize, isize), -} - -impl TypeInfo { - pub fn of(&self) -> &'static str { - let type_name = std::any::type_name::(); - match self { - TypeInfo::FullName => type_name, - TypeInfo::ShortName => Self::partial(type_name, -1, None), - TypeInfo::NameFrom(start) => Self::partial(type_name, *start, None), - TypeInfo::NameTo(end) => Self::partial(type_name, 0, Some(*end)), - TypeInfo::PartialName(start, end) => Self::partial(type_name, *start, Some(*end)), - } - } - - fn partial(type_name: &'static str, start: isize, end: Option) -> &'static str { - let maxlen = type_name.len(); - let mut segments = Vec::new(); - let mut segment_start = 0; // Start position of the current segment. - let mut angle_brackets = 0; // Counter for tracking '<' and '>'. - let mut previous_char = '\0'; // Initializes to a null character, no previous character. - - for (idx, c) in type_name.char_indices() { - match c { - ':' if angle_brackets == 0 => { - if previous_char == ':' { - if segment_start < idx - 1 { - segments.push((segment_start, idx - 1)); // Do not include last '::'. - } - segment_start = idx + 1; // Next segment starts after '::'. - } - } - '<' => angle_brackets += 1, - '>' => angle_brackets -= 1, - _ => {} - } - previous_char = c; - } - - // Include the last segment if there's any. - if segment_start < maxlen { - segments.push((segment_start, maxlen)); - } - - // Calculates the start position. - let start_pos = segments - .get(if start >= 0 { - start as usize - } else { - segments.len() - start.unsigned_abs() - }) - .map_or(0, |&(s, _)| s); - - // Calculates the end position. - let end_pos = segments - .get(if let Some(end) = end { - if end >= 0 { - end as usize - } else { - segments.len() - end.unsigned_abs() - } - } else { - segments.len() - 1 - }) - .map_or(maxlen, |&(_, e)| e); - - // Returns the partial string based on the calculated positions. - &type_name[start_pos..end_pos] - } -} - -/// Calculates the absolute directory given a root path and a relative path. -/// -/// # Arguments -/// -/// * `root_path` - A string slice that holds the root path. -/// * `relative_path` - A string slice that holds the relative path. -/// -/// # Returns -/// -/// * `Ok` - If the operation is successful, returns the absolute directory as a `String`. -/// * `Err` - If an I/O error occurs, returns an `io::Error`. -/// -/// # Errors -/// -/// This function will return an error if: -/// - The root path or relative path are invalid. -/// - There is an issue with file system operations, such as reading the directory. -/// -/// # Examples -/// -/// ``` -/// let root = "/home/user"; -/// let relative = "documents"; -/// let abs_dir = absolute_dir(root, relative).unwrap(); -/// println!("{}", abs_dir); -/// ``` -pub fn absolute_dir( - root_path: impl Into, - relative_path: impl Into, -) -> Result { - let root_path = PathBuf::from(root_path.into()); - let full_path = root_path.join(relative_path.into()); - let absolute_dir = full_path.to_string_lossy().into(); - - if !full_path.is_absolute() { - let message = format!("Path \"{absolute_dir}\" is not absolute"); - trace::warn!(message); - return Err(io::Error::new(io::ErrorKind::InvalidInput, message)); - } - - if !full_path.exists() { - let message = format!("Path \"{absolute_dir}\" does not exist"); - trace::warn!(message); - return Err(io::Error::new(io::ErrorKind::NotFound, message)); - } - - if !full_path.is_dir() { - let message = format!("Path \"{absolute_dir}\" is not a directory"); - trace::warn!(message); - return Err(io::Error::new(io::ErrorKind::InvalidInput, message)); - } - - Ok(absolute_dir) -} - -// USEFUL MACROS *********************************************************************************** - -#[macro_export] -/// Macro para construir grupos de pares clave-valor. -/// -/// ```rust#ignore -/// let args = kv![ -/// "userName" => "Roberto", -/// "photoCount" => 3, -/// "userGender" => "male", -/// ]; -/// ``` -macro_rules! kv { - ( $($key:expr => $value:expr),* $(,)? ) => {{ - let mut a = std::collections::HashMap::new(); - $( - a.insert($key.into(), $value.into()); - )* - a - }}; -} - -#[macro_export] -/// Define un conjunto de ajustes de configuración usando tipos seguros y valores predefinidos. -/// -/// Detiene la aplicación con un panic! si no pueden asignarse los ajustes de configuración. -/// -/// Carga la configuración de la aplicación en forma de pares `clave = valor` recogidos en archivos -/// [TOML](https://toml.io). -/// -/// La metodología [The Twelve-Factor App](https://12factor.net/es/) define **la configuración de -/// una aplicación como todo lo que puede variar entre despliegues**, diferenciando entre entornos -/// de desarrollo, pre-producción, producción, etc. -/// -/// A veces las aplicaciones guardan configuraciones como constantes en el código, lo que implica -/// una violación de esta metodología. `PageTop` recomienda una **estricta separación entre código y -/// configuración**. La configuración variará en cada tipo de despliegue, y el código no. -/// -/// -/// # Cómo cargar los ajustes de configuración -/// -/// Si tu aplicación requiere archivos de configuración debes crear un directorio *config* al mismo -/// nivel del archivo *Cargo.toml* de tu proyecto (o del ejecutable binario de la aplicación). -/// -/// `PageTop` se encargará de cargar todos los ajustes de configuración de tu aplicación leyendo los -/// siguientes archivos TOML en este orden (todos los archivos son opcionales): -/// -/// 1. **config/common.toml**, útil para los ajustes comunes a cualquier entorno. Estos valores -/// podrán ser sobrescritos al fusionar los archivos de configuración restantes. -/// -/// 2. **config/{file}.toml**, donde *{file}* se define con la variable de entorno -/// `PAGETOP_RUN_MODE`: -/// -/// * Si no está definida se asumirá *default* por defecto y `PageTop` intentará cargar el -/// archivo *config/default.toml* si existe. -/// -/// * De esta manera podrás tener diferentes ajustes de configuración para diferentes entornos -/// de ejecución. Por ejemplo, para *devel.toml*, *staging.toml* o *production.toml*. O -/// también para *server1.toml* o *server2.toml*. Sólo uno será cargado. -/// -/// * Normalmente estos archivos suelen ser idóneos para incluir contraseñas o configuración -/// sensible asociada al entorno correspondiente. Estos archivos no deberían ser publicados en -/// el repositorio Git por razones de seguridad. -/// -/// 3. **config/local.toml**, para añadir o sobrescribir ajustes de los archivos anteriores. -/// -/// -/// # Cómo añadir ajustes de configuración -/// -/// Para proporcionar a tu **módulo** sus propios ajustes de configuración, añade -/// [*serde*](https://docs.rs/serde) en las dependencias de tu archivo *Cargo.toml* habilitando la -/// característica `derive`: -/// -/// ```toml -/// [dependencies] -/// serde = { version = "1.0", features = ["derive"] } -/// ``` -/// -/// Y luego inicializa con la macro [`static_config!`](crate::static_config) tus ajustes, usando -/// tipos seguros y asignando los valores predefinidos para la estructura asociada: -/// -/// ``` -/// use pagetop::prelude::*; -/// use serde::Deserialize; -/// -/// #[derive(Debug, Deserialize)] -/// pub struct Settings { -/// pub myapp: MyApp, -/// } -/// -/// #[derive(Debug, Deserialize)] -/// pub struct MyApp { -/// pub name: String, -/// pub description: Option, -/// pub width: u16, -/// pub height: u16, -/// } -/// -/// static_config!(SETTINGS: Settings => [ -/// // [myapp] -/// "myapp.name" => "Value Name", -/// "myapp.width" => 900, -/// "myapp.height" => 320, -/// ]); -/// ``` -/// -/// De hecho, así se declaran los ajustes globales de la configuración (ver [`SETTINGS`]). -/// -/// Puedes usar la [sintaxis TOML](https://toml.io/en/v1.0.0#table) para añadir tu nueva sección -/// `[myapp]` en los archivos de configuración, del mismo modo que se añaden `[log]` o `[server]` en -/// los ajustes globales (ver [`Settings`]). -/// -/// Se recomienda inicializar todos los ajustes con valores predefinidos, o utilizar la notación -/// `Option` si van a ser tratados en el código como opcionales. -/// -/// Si no pueden inicializarse correctamente los ajustes de configuración, entonces la aplicación -/// ejecutará un panic! y detendrá la ejecución. -/// -/// Los ajustes de configuración siempre serán de sólo lectura. -/// -/// -/// # Cómo usar tus nuevos ajustes de configuración -/// -/// ``` -/// use pagetop::prelude::*; -/// use crate::config; -/// -/// fn global_settings() { -/// println!("App name: {}", &global::SETTINGS.app.name); -/// println!("App description: {}", &global::SETTINGS.app.description); -/// println!("Value of PAGETOP_RUN_MODE: {}", &global::SETTINGS.app.run_mode); -/// } -/// -/// fn package_settings() { -/// println!("{} - {:?}", &config::SETTINGS.myapp.name, &config::SETTINGS.myapp.description); -/// println!("{}", &config::SETTINGS.myapp.width); -/// } -/// ``` -macro_rules! static_config { - ( $SETTINGS:ident: $Settings:ty => [ $($key:literal => $value:literal),* $(,)? ] ) => { - #[doc = concat!( - "Assigned or predefined values for configuration settings associated to the ", - "[`", stringify!($Settings), "`] type." - )] - pub static $SETTINGS: std::sync::LazyLock<$Settings> = std::sync::LazyLock::new(|| { - let mut settings = $crate::util::config::CONFIG_DATA.clone(); - $( - settings.set_default($key, $value).unwrap(); - )* - match settings.try_into() { - Ok(s) => s, - Err(e) => panic!("Error parsing settings: {}", e), - } - }); - }; -} diff --git a/packages/pagetop/src/util/config.rs b/packages/pagetop/src/util/config.rs deleted file mode 100644 index ad628ed3..00000000 --- a/packages/pagetop/src/util/config.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Retrieve settings values from configuration files. - -use crate::concat_string; -use crate::util::data::ConfigData; -use crate::util::file::File; - -use std::sync::LazyLock; - -use std::env; -use std::path::Path; - -/// Original configuration values in `key = value` pairs gathered from configuration files. -pub static CONFIG_DATA: LazyLock = LazyLock::new(|| { - // Identify the configuration directory. - let config_dir = env::var("CARGO_MANIFEST_DIR") - .map(|manifest_dir| { - let manifest_config = Path::new(&manifest_dir).join("config"); - if manifest_config.exists() { - manifest_config.to_string_lossy().to_string() - } else { - "config".to_string() - } - }) - .unwrap_or_else(|_| "config".to_string()); - - // Execution mode based on the environment variable PAGETOP_RUN_MODE, defaults to 'default'. - let rm = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| "default".into()); - - // Initialize settings. - let mut settings = ConfigData::default(); - - // Merge (optional) configuration files and set the execution mode. - settings - // First, add the common configuration for all environments. Defaults to 'common.toml'. - .merge(File::with_name(&concat_string!(config_dir, "/common.toml")).required(false)) - .expect("Failed to merge common configuration (common.toml)") - // Add the environment-specific configuration. Defaults to 'default.toml'. - .merge(File::with_name(&concat_string!(config_dir, "/", rm, ".toml")).required(false)) - .expect(&format!("Failed to merge {rm}.toml configuration")) - // Add reserved local configuration for the environment. Defaults to 'local.default.toml'. - .merge(File::with_name(&concat_string!(config_dir, "/local.", rm, ".toml")).required(false)) - .expect("Failed to merge reserved local environment configuration") - // Add the general reserved local configuration. Defaults to 'local.toml'. - .merge(File::with_name(&concat_string!(config_dir, "/local.toml")).required(false)) - .expect("Failed to merge general reserved local configuration") - // Save the execution mode. - .set("app.run_mode", rm) - .expect("Failed to set application run mode"); - - settings -}); diff --git a/packages/pagetop/Cargo.toml b/pagetop/Cargo.toml similarity index 62% rename from packages/pagetop/Cargo.toml rename to pagetop/Cargo.toml index 67d8f12c..d5fed213 100644 --- a/packages/pagetop/Cargo.toml +++ b/pagetop/Cargo.toml @@ -8,7 +8,7 @@ description = """\ """ categories = ["web-programming", "gui", "development-tools", "asynchronous"] keywords = ["pagetop", "web", "framework", "frontend", "ssr"] -readme = "../../README.md" +readme = "../README.md" homepage = { workspace = true } repository = { workspace = true } @@ -19,22 +19,24 @@ license = { workspace = true } name = "pagetop" [dependencies] -colored = "2.1.0" -concat-string = "1.0.1" -figlet-rs = "0.1.5" -fluent-bundle = "0.15.3" -fluent-templates = "0.11.0" -itoa = "1.0.11" -nom = "7.1.3" -paste = "1.0.15" -substring = "1.4.5" -terminal_size = "0.4.0" -toml = "0.8.19" -tracing = "0.1.40" -tracing-appender = "0.2.3" +colored = "2.1.0" +concat-string = "1.0.1" +figlet-rs = "0.1.5" +itoa = "1.0.11" +nom = "7.1.3" +paste = "1.0.15" +substring = "1.4.5" +terminal_size = "0.4.0" +toml = "0.8.19" + +tracing = "0.1.40" +tracing-appender = "0.2.3" tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] } -tracing-actix-web = "0.7.15" -unic-langid = { version = "0.9.5", features = ["macros"] } +tracing-actix-web = "0.7.15" + +fluent-bundle = "0.15.3" +fluent-templates = "0.11.0" +unic-langid = { version = "0.9.5", features = ["macros"] } actix-web = "4.9.0" actix-web-files = { package = "actix-files", version = "0.6.6" } @@ -45,3 +47,6 @@ serde.workspace = true static-files.workspace = true pagetop-macros.workspace = true + +[build-dependencies] +pagetop-build.workspace = true diff --git a/build.rs b/pagetop/build.rs similarity index 53% rename from build.rs rename to pagetop/build.rs index 6e9dc40b..1450422c 100644 --- a/build.rs +++ b/pagetop/build.rs @@ -1,7 +1,7 @@ use pagetop_build::StaticFilesBundle; fn main() -> std::io::Result<()> { - StaticFilesBundle::from_dir("./static/base") - .with_name("base") + StaticFilesBundle::from_dir("../static", None) + .with_name("assets") .build() } diff --git a/pagetop/config/common.toml b/pagetop/config/common.toml new file mode 100644 index 00000000..900872d6 --- /dev/null +++ b/pagetop/config/common.toml @@ -0,0 +1,5 @@ +[app] +name = "Samples" + +[log] +tracing = "Debug" diff --git a/examples/app-basic.rs b/pagetop/examples/app-basic.rs similarity index 100% rename from examples/app-basic.rs rename to pagetop/examples/app-basic.rs diff --git a/examples/hello-name.rs b/pagetop/examples/hello-name.rs similarity index 87% rename from examples/hello-name.rs rename to pagetop/examples/hello-name.rs index 3a03e8b1..8dba5cec 100644 --- a/examples/hello-name.rs +++ b/pagetop/examples/hello-name.rs @@ -15,7 +15,7 @@ async fn hello_name( ) -> ResultPage { let name = path.into_inner(); Page::new(request) - .with_component(Html::with(html! { h1 { "Hello " (name) "!" } })) + .with_body(PrepareMarkup::With(html! { h1 { "Hello " (name) "!" } })) .render() } diff --git a/examples/hello-world.rs b/pagetop/examples/hello-world.rs similarity index 86% rename from examples/hello-world.rs rename to pagetop/examples/hello-world.rs index 17c1e9d3..c904eb07 100644 --- a/examples/hello-world.rs +++ b/pagetop/examples/hello-world.rs @@ -10,7 +10,7 @@ impl PackageTrait for HelloWorld { async fn hello_world(request: HttpRequest) -> ResultPage { Page::new(request) - .with_component(Html::with(html! { h1 { "Hello World!" } })) + .with_body(PrepareMarkup::With(html! { h1 { "Hello World!" } })) .render() } diff --git a/packages/pagetop/src/app.rs b/pagetop/src/app.rs similarity index 93% rename from packages/pagetop/src/app.rs rename to pagetop/src/app.rs index 34e1c373..aaac3234 100644 --- a/packages/pagetop/src/app.rs +++ b/pagetop/src/app.rs @@ -3,6 +3,8 @@ mod figfont; use crate::core::{package, package::PackageRef}; +use crate::html::Markup; +use crate::response::page::{ErrorPage, ResultPage}; use crate::{global, locale, service, trace}; use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle}; @@ -42,7 +44,7 @@ impl Application { LazyLock::force(&trace::TRACING); // Validates the default language identifier. - LazyLock::force(&locale::LANGID_DEFAULT); + LazyLock::force(&locale::DEFAULT_LANGID); // Registers the application's packages. package::all::register_packages(root_package); @@ -154,12 +156,12 @@ impl Application { InitError = (), >, > { - service::App::new().configure(package::all::configure_services) - // .default_service(service::web::route().to(service_not_found)) + service::App::new() + .configure(package::all::configure_services) + .default_service(service::web::route().to(service_not_found)) } } -/* -async fn service_not_found(request: HttpRequest) -> ResultPage { + +async fn service_not_found(request: service::HttpRequest) -> ResultPage { Err(ErrorPage::NotFound(request)) } -*/ diff --git a/packages/pagetop/src/app/figfont.rs b/pagetop/src/app/figfont.rs similarity index 100% rename from packages/pagetop/src/app/figfont.rs rename to pagetop/src/app/figfont.rs diff --git a/packages/pagetop/src/app/slant.flf b/pagetop/src/app/slant.flf similarity index 100% rename from packages/pagetop/src/app/slant.flf rename to pagetop/src/app/slant.flf diff --git a/packages/pagetop/src/app/small.flf b/pagetop/src/app/small.flf similarity index 100% rename from packages/pagetop/src/app/small.flf rename to pagetop/src/app/small.flf diff --git a/packages/pagetop/src/app/speed.flf b/pagetop/src/app/speed.flf similarity index 100% rename from packages/pagetop/src/app/speed.flf rename to pagetop/src/app/speed.flf diff --git a/packages/pagetop/src/app/starwars.flf b/pagetop/src/app/starwars.flf similarity index 100% rename from packages/pagetop/src/app/starwars.flf rename to pagetop/src/app/starwars.flf diff --git a/pagetop/src/config.rs b/pagetop/src/config.rs new file mode 100644 index 00000000..07924057 --- /dev/null +++ b/pagetop/src/config.rs @@ -0,0 +1,196 @@ +//! Load configuration settings. +//! +//! These settings are loaded from [TOML](https://toml.io) files as `key = value` pairs and mapped +//! into type-safe structures with predefined values. +//! +//! Following the [Twelve-Factor App](https://12factor.net/config) methodology, `PageTop` separates +//! code from configuration. This approach allows configurations to vary across deployments, such as +//! development, staging, or production, without changing the codebase. +//! +//! +//! # Loading configuration settings +//! +//! If your application requires configuration files, create a `config` directory in the root of +//! your project, at the same level as the *Cargo.toml* file or the application's binary. +//! +//! `PageTop` automatically loads configuration settings by reading the following TOML files in +//! order (all files are optional): +//! +//! 1. **config/common.toml**, for settings shared across all environments. This approach simplifies +//! maintenance by centralizing common configuration values. +//! +//! 2. **config/{rm}.toml**, where `{rm}` corresponds to the environment variable +//! `PAGETOP_RUN_MODE`: +//! +//! * If `PAGETOP_RUN_MODE` is not set, it defaults to `default`, and `PageTop` attempts to load +//! *config/default.toml* if available. +//! +//! * Useful for environment-specific configurations, ensuring that each environment +//! (e.g., development, staging, production) has its own settings without affecting others, +//! such as API keys, URLs, or performance-related adjustments. +//! +//! 3. **config/local.{rm}.toml**, useful for local machine-specific configurations: +//! +//! * This file allows you to add or override settings specific to the environment. For example, +//! `local.devel.toml` for development or `local.production.toml` for production tweaks. +//! +//! * It enables developers to tailor settings for their machines within a given environment and +//! is typically not shared or committed to version control systems. +//! +//! 4. **config/local.toml**, for general local settings across all environments, ideal for quick +//! adjustments or temporary values not tied to any specific environment. +//! +//! The configuration settings are merged in the order listed above, with later files overriding +//! earlier ones if there are conflicts. +//! +//! +//! # Adding configuration settings +//! +//! To give your **module** its own configuration settings, add [*serde*](https://docs.rs/serde) as +//! a dependency in your *Cargo.toml* file with the `derive` feature enabled: +//! +//! ```toml +//! [dependencies] +//! serde = { version = "1.0", features = ["derive"] } +//! ``` +//! +//! Then, use the [`include_config!`](crate::include_config) macro to initialize your settings with +//! type-safe structures and predefined values: +//! +//! ``` +//! use pagetop::prelude::*; +//! use serde::Deserialize; +//! +//! include_config!(SETTINGS: Settings => [ +//! // [myapp] +//! "myapp.name" => "Value Name", +//! "myapp.width" => 900, +//! "myapp.height" => 320, +//! ]); +//! +//! #[derive(Debug, Deserialize)] +//! pub struct Settings { +//! pub myapp: MyApp, +//! } +//! +//! #[derive(Debug, Deserialize)] +//! pub struct MyApp { +//! pub name: String, +//! pub description: Option, +//! pub width: u16, +//! pub height: u16, +//! } +//! ``` +//! +//! This is how global configuration settings are declared (see [`SETTINGS`](crate::global::SETTINGS)). +//! +//! You can add a new `[myapp]` section in the configuration files using the +//! [TOML syntax](https://toml.io/en/v1.0.0#table), just like the `[log]` or `[server]` sections in +//! the global settings (see [`Settings`](crate::global::Settings)). +//! +//! It is recommended to initialize all settings with predefined values or use `Option` for +//! optional settings handled within the code. +//! +//! If configuration settings fail to initialize correctly, the application will panic and stop +//! execution. +//! +//! Configuration settings are always read-only. +//! +//! +//! # Using your new configuration settings +//! +//! Access the settings directly in your code: +//! +//! ``` +//! use pagetop::prelude::*; +//! use crate::config; +//! +//! fn global_settings() { +//! println!("App name: {}", &global::SETTINGS.app.name); +//! println!("App description: {}", &global::SETTINGS.app.description); +//! println!("Value of PAGETOP_RUN_MODE: {}", &global::SETTINGS.app.run_mode); +//! } +//! +//! fn package_settings() { +//! println!("{} - {:?}", &config::SETTINGS.myapp.name, &config::SETTINGS.myapp.description); +//! println!("{}", &config::SETTINGS.myapp.width); +//! } +//! ``` + +mod data; +mod de; +mod error; +mod file; +mod path; +mod source; +mod value; + +use crate::concat_string; +use crate::config::data::ConfigData; +use crate::config::file::File; + +use std::sync::LazyLock; + +use std::env; +use std::path::Path; + +/// Original values read from configuration files in `key = value` pairs. +pub static CONFIG_VALUES: LazyLock = LazyLock::new(|| { + // Identify the configuration directory. + let config_dir = env::var("CARGO_MANIFEST_DIR") + .map(|manifest_dir| { + let manifest_config = Path::new(&manifest_dir).join("config"); + if manifest_config.exists() { + manifest_config.to_string_lossy().to_string() + } else { + "config".to_string() + } + }) + .unwrap_or_else(|_| "config".to_string()); + + // Execution mode based on the environment variable PAGETOP_RUN_MODE, defaults to 'default'. + let rm = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| "default".into()); + + // Initialize config values. + let mut values = ConfigData::default(); + + // Merge (optional) configuration files and set the execution mode. + values + // First, add the common configuration for all environments. Defaults to 'common.toml'. + .merge(File::with_name(&concat_string!(config_dir, "/common.toml")).required(false)) + .expect("Failed to merge common configuration (common.toml)") + // Add the environment-specific configuration. Defaults to 'default.toml'. + .merge(File::with_name(&concat_string!(config_dir, "/", rm, ".toml")).required(false)) + .expect(&format!("Failed to merge {rm}.toml configuration")) + // Add reserved local configuration for the environment. Defaults to 'local.default.toml'. + .merge(File::with_name(&concat_string!(config_dir, "/local.", rm, ".toml")).required(false)) + .expect("Failed to merge reserved local environment configuration") + // Add common reserved local configuration. Defaults to 'local.toml'. + .merge(File::with_name(&concat_string!(config_dir, "/local.toml")).required(false)) + .expect("Failed to merge general reserved local configuration") + // Save the execution mode. + .set("app.run_mode", rm) + .expect("Failed to set application run mode"); + + values +}); + +#[macro_export] +macro_rules! include_config { + ( $SETTINGS:ident: $Settings:ty => [ $($key:literal => $value:literal),* $(,)? ] ) => { + #[doc = concat!( + "Assigned or predefined values for configuration settings associated to the ", + "[`", stringify!($Settings), "`] type." + )] + pub static $SETTINGS: std::sync::LazyLock<$Settings> = std::sync::LazyLock::new(|| { + let mut settings = $crate::config::CONFIG_VALUES.clone(); + $( + settings.set_default($key, $value).unwrap(); + )* + match settings.try_into() { + Ok(s) => s, + Err(e) => panic!("Error parsing settings: {}", e), + } + }); + }; +} diff --git a/packages/pagetop/src/util/data.rs b/pagetop/src/config/data.rs similarity index 96% rename from packages/pagetop/src/util/data.rs rename to pagetop/src/config/data.rs index b62803fb..22fe8359 100644 --- a/packages/pagetop/src/util/data.rs +++ b/pagetop/src/config/data.rs @@ -1,7 +1,7 @@ -use crate::util::error::*; -use crate::util::path; -use crate::util::source::Source; -use crate::util::value::Value; +use crate::config::error::*; +use crate::config::path; +use crate::config::source::Source; +use crate::config::value::Value; use serde::de::Deserialize; diff --git a/packages/pagetop/src/util/de.rs b/pagetop/src/config/de.rs similarity index 99% rename from packages/pagetop/src/util/de.rs rename to pagetop/src/config/de.rs index 55a58037..875219af 100644 --- a/packages/pagetop/src/util/de.rs +++ b/pagetop/src/config/de.rs @@ -1,6 +1,6 @@ -use crate::util::data::ConfigData; -use crate::util::error::*; -use crate::util::value::{Table, Value, ValueKind}; +use crate::config::data::ConfigData; +use crate::config::error::*; +use crate::config::value::{Table, Value, ValueKind}; use serde::de; use serde::forward_to_deserialize_any; diff --git a/packages/pagetop/src/util/error.rs b/pagetop/src/config/error.rs similarity index 100% rename from packages/pagetop/src/util/error.rs rename to pagetop/src/config/error.rs diff --git a/packages/pagetop/src/util/file.rs b/pagetop/src/config/file.rs similarity index 95% rename from packages/pagetop/src/util/file.rs rename to pagetop/src/config/file.rs index 643f4ea1..00f0c34d 100644 --- a/packages/pagetop/src/util/file.rs +++ b/pagetop/src/config/file.rs @@ -1,9 +1,9 @@ mod source; mod toml; -use crate::util::error::*; -use crate::util::source::Source; -use crate::util::value::Value; +use crate::config::error::*; +use crate::config::source::Source; +use crate::config::value::Value; use std::collections::HashMap; use std::path::{Path, PathBuf}; diff --git a/packages/pagetop/src/util/file/source.rs b/pagetop/src/config/file/source.rs similarity index 100% rename from packages/pagetop/src/util/file/source.rs rename to pagetop/src/config/file/source.rs diff --git a/packages/pagetop/src/util/file/toml.rs b/pagetop/src/config/file/toml.rs similarity index 96% rename from packages/pagetop/src/util/file/toml.rs rename to pagetop/src/config/file/toml.rs index 88e8230f..e8fa06c6 100644 --- a/packages/pagetop/src/util/file/toml.rs +++ b/pagetop/src/config/file/toml.rs @@ -1,4 +1,4 @@ -use crate::util::value::{Value, ValueKind}; +use crate::config::value::{Value, ValueKind}; use toml; diff --git a/packages/pagetop/src/util/path.rs b/pagetop/src/config/path.rs similarity index 98% rename from packages/pagetop/src/util/path.rs rename to pagetop/src/config/path.rs index 8b365d6a..72376a95 100644 --- a/packages/pagetop/src/util/path.rs +++ b/pagetop/src/config/path.rs @@ -1,5 +1,5 @@ -use crate::util::error::*; -use crate::util::value::{Value, ValueKind}; +use crate::config::error::*; +use crate::config::value::{Value, ValueKind}; use std::collections::HashMap; use std::str::FromStr; diff --git a/packages/pagetop/src/util/path/parser.rs b/pagetop/src/config/path/parser.rs similarity index 100% rename from packages/pagetop/src/util/path/parser.rs rename to pagetop/src/config/path/parser.rs diff --git a/packages/pagetop/src/util/source.rs b/pagetop/src/config/source.rs similarity index 95% rename from packages/pagetop/src/util/source.rs rename to pagetop/src/config/source.rs index 5b1ae11d..5e693b68 100644 --- a/packages/pagetop/src/util/source.rs +++ b/pagetop/src/config/source.rs @@ -1,6 +1,6 @@ -use crate::util::error::*; -use crate::util::path; -use crate::util::value::{Value, ValueKind}; +use crate::config::error::*; +use crate::config::path; +use crate::config::value::{Value, ValueKind}; use std::collections::HashMap; use std::fmt::Debug; diff --git a/packages/pagetop/src/util/value.rs b/pagetop/src/config/value.rs similarity index 99% rename from packages/pagetop/src/util/value.rs rename to pagetop/src/config/value.rs index cee2dc32..29d62cfe 100644 --- a/packages/pagetop/src/util/value.rs +++ b/pagetop/src/config/value.rs @@ -1,4 +1,4 @@ -use crate::util::error::*; +use crate::config::error::*; use serde::de::{Deserialize, Deserializer, Visitor}; diff --git a/packages/pagetop/src/core.rs b/pagetop/src/core.rs similarity index 100% rename from packages/pagetop/src/core.rs rename to pagetop/src/core.rs diff --git a/packages/pagetop/src/core/action.rs b/pagetop/src/core/action.rs similarity index 100% rename from packages/pagetop/src/core/action.rs rename to pagetop/src/core/action.rs diff --git a/packages/pagetop/src/core/action/all.rs b/pagetop/src/core/action/all.rs similarity index 100% rename from packages/pagetop/src/core/action/all.rs rename to pagetop/src/core/action/all.rs diff --git a/packages/pagetop/src/core/action/definition.rs b/pagetop/src/core/action/definition.rs similarity index 100% rename from packages/pagetop/src/core/action/definition.rs rename to pagetop/src/core/action/definition.rs diff --git a/packages/pagetop/src/core/action/list.rs b/pagetop/src/core/action/list.rs similarity index 100% rename from packages/pagetop/src/core/action/list.rs rename to pagetop/src/core/action/list.rs diff --git a/packages/pagetop/src/core/package.rs b/pagetop/src/core/package.rs similarity index 77% rename from packages/pagetop/src/core/package.rs rename to pagetop/src/core/package.rs index e0c6a12e..69be7b7b 100644 --- a/packages/pagetop/src/core/package.rs +++ b/pagetop/src/core/package.rs @@ -2,3 +2,4 @@ mod definition; pub use definition::{PackageRef, PackageTrait}; pub(crate) mod all; +pub(crate) mod welcome; diff --git a/packages/pagetop/src/core/package/all.rs b/pagetop/src/core/package/all.rs similarity index 90% rename from packages/pagetop/src/core/package/all.rs rename to pagetop/src/core/package/all.rs index fd177c66..c21fed8f 100644 --- a/packages/pagetop/src/core/package/all.rs +++ b/pagetop/src/core/package/all.rs @@ -1,7 +1,7 @@ use crate::core::action::add_action; -use crate::core::package::PackageRef; +use crate::core::package::{welcome, PackageRef}; use crate::core::theme::all::THEMES; -use crate::{service, trace}; +use crate::{include_files, include_files_service, service, trace}; use std::sync::{LazyLock, RwLock}; @@ -23,8 +23,6 @@ pub fn register_packages(root_package: Option) { if let Some(package) = root_package { add_to_enabled(&mut enabled_list, package); } - // Reverse the order to ensure packages are sorted from none to most dependencies. - enabled_list.reverse(); // Save the final list of enabled packages. ENABLED_PACKAGES.write().unwrap().append(&mut enabled_list); @@ -41,16 +39,14 @@ pub fn register_packages(root_package: Option) { fn add_to_enabled(list: &mut Vec, package: PackageRef) { // Check if the package is not already in the enabled list to avoid duplicates. if !list.iter().any(|p| p.type_id() == package.type_id()) { - // Add the package to the enabled list. - list.push(package); - - // Reverse dependencies to add them in correct order (dependencies first). - let mut dependencies = package.dependencies(); - dependencies.reverse(); - for d in &dependencies { + // Add the package dependencies in reverse order first. + for d in package.dependencies().iter().rev() { add_to_enabled(list, *d); } + // Add the package itself to the enabled list. + list.push(package); + // Check if the package has an associated theme to register. if let Some(theme) = package.theme() { let mut registered_themes = THEMES.write().unwrap(); @@ -119,8 +115,14 @@ pub fn init_packages() { // CONFIGURE SERVICES ****************************************************************************** +include_files!(assets); + pub fn configure_services(scfg: &mut service::web::ServiceConfig) { for m in ENABLED_PACKAGES.read().unwrap().iter() { m.configure_service(scfg); } + // Default welcome homepage. + scfg.route("/", service::web::get().to(welcome::homepage)); + // Default assets. + include_files_service!(scfg, assets => "/"); } diff --git a/packages/pagetop/src/core/package/definition.rs b/pagetop/src/core/package/definition.rs similarity index 97% rename from packages/pagetop/src/core/package/definition.rs rename to pagetop/src/core/package/definition.rs index 3506a929..b1d0a8c9 100644 --- a/packages/pagetop/src/core/package/definition.rs +++ b/pagetop/src/core/package/definition.rs @@ -13,7 +13,7 @@ pub trait PackageTrait: AnyBase + Send + Sync { } fn description(&self) -> L10n { - L10n::none() + L10n::default() } fn theme(&self) -> Option { diff --git a/pagetop/src/core/package/welcome.rs b/pagetop/src/core/package/welcome.rs new file mode 100644 index 00000000..1c3d41c6 --- /dev/null +++ b/pagetop/src/core/package/welcome.rs @@ -0,0 +1,116 @@ +use crate::html::{html, Markup, PrepareMarkup, StyleSheet}; +use crate::locale::L10n; +use crate::response::page::{AssetsOp, ErrorPage, Page, ResultPage}; +use crate::{global, service}; + +pub async fn homepage(request: service::HttpRequest) -> ResultPage { + Page::new(request) + .with_title(L10n::l("welcome_page")) + .with_assets(AssetsOp::AddStyleSheet(StyleSheet::inline("styles", r#" + body { + background-color: #f3d060; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 20px; + } + .wrapper { + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 0; + } + .container { + padding: 0 16px; + } + .title { + font-size: clamp(3rem, 10vw, 10rem); + letter-spacing: -0.05em; + line-height: 1.2; + margin: 0; + } + .subtitle { + font-size: clamp(1.8rem, 2vw, 3rem); + letter-spacing: -0.02em; + line-height: 1.2; + margin: 0; + } + .powered { + margin: .5em 0 1em; + } + .box-container { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: stretch; + gap: 1.5em; + } + .box { + flex: 1 1 280px; + border: 3px solid #25282a; + box-shadow: 5px 5px 0px #25282a; + box-sizing: border-box; + padding: 0 16px; + } + footer { + margin-top: 5em; + font-size: 14px; + font-weight: 500; + color: #a5282c; + } + "#))) + .with_body(PrepareMarkup::With(html! { + div class="wrapper" { + div class="container" { + h1 class="title" { (L10n::l("welcome_title").markup()) } + + p class="subtitle" { + (L10n::l("welcome_intro").with_arg("app", format!( + "{}", + &global::SETTINGS.app.name + )).markup()) + } + p class="powered" { + (L10n::l("welcome_powered").with_arg("pagetop", format!( + "{}", + "https://crates.io/crates/pagetop", "PageTop" + )).markup()) + } + + h2 { (L10n::l("welcome_page").markup()) } + + div class="box-container" { + section class="box" style="background-color: #5eb0e5;" { + h3 { + (L10n::l("welcome_subtitle") + .with_arg("app", &global::SETTINGS.app.name) + .markup()) + } + p { (L10n::l("welcome_text1").markup()) } + p { (L10n::l("welcome_text2").markup()) } + } + section class="box" style="background-color: #aee1cd;" { + h3 { + (L10n::l("welcome_pagetop_title").markup()) + } + p { (L10n::l("welcome_pagetop_text1").markup()) } + p { (L10n::l("welcome_pagetop_text2").markup()) } + p { (L10n::l("welcome_pagetop_text3").markup()) } + } + section class="box" style="background-color: #ebebe3;" { + h3 { + (L10n::l("welcome_issues_title").markup()) + } + p { (L10n::l("welcome_issues_text1").markup()) } + p { + (L10n::l("welcome_issues_text2") + .with_arg("app", &global::SETTINGS.app.name) + .markup()) + } + } + } + + footer { "[ " (L10n::l("welcome_have_fun").markup()) " ]" } + } + } + })) + .render() +} diff --git a/packages/pagetop/src/core/theme.rs b/pagetop/src/core/theme.rs similarity index 100% rename from packages/pagetop/src/core/theme.rs rename to pagetop/src/core/theme.rs diff --git a/packages/pagetop/src/core/theme/all.rs b/pagetop/src/core/theme/all.rs similarity index 79% rename from packages/pagetop/src/core/theme/all.rs rename to pagetop/src/core/theme/all.rs index 7aab35a7..b27bf1e5 100644 --- a/packages/pagetop/src/core/theme/all.rs +++ b/pagetop/src/core/theme/all.rs @@ -1,4 +1,6 @@ -use crate::core::theme::ThemeRef; +use crate::core::package::PackageTrait; +use crate::core::theme::{ThemeRef, ThemeTrait}; +use crate::global; use std::sync::{LazyLock, RwLock}; @@ -6,7 +8,7 @@ use std::sync::{LazyLock, RwLock}; pub static THEMES: LazyLock>> = LazyLock::new(|| RwLock::new(Vec::new())); -/* DEFAULT THEME *********************************************************************************** +// DEFAULT THEME *********************************************************************************** pub struct NoTheme; @@ -16,10 +18,9 @@ impl PackageTrait for NoTheme { } } -impl ThemeTrait for NoTheme { -} +impl ThemeTrait for NoTheme {} -pub static THEME_DEFAULT: LazyLock = +pub static DEFAULT_THEME: LazyLock = LazyLock::new(|| match theme_by_short_name(&global::SETTINGS.app.theme) { Some(theme) => theme, None => &NoTheme, @@ -39,4 +40,3 @@ pub fn theme_by_short_name(short_name: &str) -> Option { _ => None, } } -*/ diff --git a/pagetop/src/core/theme/definition.rs b/pagetop/src/core/theme/definition.rs new file mode 100644 index 00000000..126e7731 --- /dev/null +++ b/pagetop/src/core/theme/definition.rs @@ -0,0 +1,82 @@ +use crate::core::package::PackageTrait; +use crate::html::{html, PrepareMarkup}; +use crate::locale::L10n; +use crate::response::page::Page; +use crate::{global, service}; + +pub type ThemeRef = &'static dyn ThemeTrait; + +/// Los temas deben implementar este "trait". +pub trait ThemeTrait: PackageTrait + Send + Sync { + /* + #[rustfmt::skip] + fn regions(&self) -> Vec<(&'static str, L10n)> { + vec![ + ("header", L10n::l("header")), + ("pagetop", L10n::l("pagetop")), + ("sidebar_left", L10n::l("sidebar_left")), + ("content", L10n::l("content")), + ("sidebar_right", L10n::l("sidebar_right")), + ("footer", L10n::l("footer")), + ] + } */ + + fn prepare_page_head(&self, page: &mut Page) -> PrepareMarkup { + let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; + PrepareMarkup::With(html! { + head { + meta charset="utf-8"; + + @if let Some(title) = page.title() { + title { (global::SETTINGS.app.name) (" | ") (title) } + } @else { + title { (global::SETTINGS.app.name) } + } + + @if let Some(description) = page.description() { + meta name="description" content=(description); + } + + meta name="viewport" content=(viewport); + @for (name, content) in page.metadata() { + meta name=(name) content=(content) {} + } + + meta http-equiv="X-UA-Compatible" content="IE=edge"; + @for (property, content) in page.properties() { + meta property=(property) content=(content) {} + } + + (page.context().prepare_assets()) + } + }) + } + + fn prepare_page_body(&self, page: &mut Page) -> PrepareMarkup { + PrepareMarkup::With(html! { + body id=[page.body_id().get()] class=[page.body_classes().get()] { + (page.body_content().render()) + } + }) + } + + fn error_403(&self, request: service::HttpRequest) -> Page { + Page::new(request) + .with_title(L10n::n("Error FORBIDDEN")) + .with_body(PrepareMarkup::With(html! { + div { + h1 { ("FORBIDDEN ACCESS") } + } + })) + } + + fn error_404(&self, request: service::HttpRequest) -> Page { + Page::new(request) + .with_title(L10n::n("Error RESOURCE NOT FOUND")) + .with_body(PrepareMarkup::With(html! { + div { + h1 { ("RESOURCE NOT FOUND") } + } + })) + } +} diff --git a/packages/pagetop/src/global.rs b/pagetop/src/global.rs similarity index 98% rename from packages/pagetop/src/global.rs rename to pagetop/src/global.rs index 49c0cdba..e538958b 100644 --- a/packages/pagetop/src/global.rs +++ b/pagetop/src/global.rs @@ -1,10 +1,10 @@ //! Global settings. -use crate::static_config; +use crate::include_config; use serde::Deserialize; -static_config!(SETTINGS: Settings => [ +include_config!(SETTINGS: Settings => [ // [app] "app.name" => "My App", "app.description" => "Developed with the amazing PageTop framework.", diff --git a/pagetop/src/html.rs b/pagetop/src/html.rs new file mode 100644 index 00000000..f9e70cd2 --- /dev/null +++ b/pagetop/src/html.rs @@ -0,0 +1,47 @@ +//! HTML in code. + +mod maud; +pub use maud::{html, html_private, Markup, PreEscaped, DOCTYPE}; + +mod assets; +pub use assets::favicon::Favicon; +pub use assets::javascript::JavaScript; +pub use assets::stylesheet::{StyleSheet, TargetMedia}; +pub(crate) use assets::Assets; + +mod opt_id; +pub use opt_id::OptionId; + +mod opt_name; +pub use opt_name::OptionName; + +mod opt_string; +pub use opt_string::OptionString; + +mod opt_translated; +pub use opt_translated::OptionTranslated; + +mod opt_classes; +pub use opt_classes::{ClassesOp, OptionClasses}; + +pub mod unit; + +use crate::AutoDefault; + +#[derive(AutoDefault)] +pub enum PrepareMarkup { + #[default] + None, + Escaped(String), + With(Markup), +} + +impl PrepareMarkup { + pub fn render(&self) -> Markup { + match self { + PrepareMarkup::None => html! {}, + PrepareMarkup::Escaped(string) => html! { (PreEscaped(string)) }, + PrepareMarkup::With(markup) => html! { (markup) }, + } + } +} diff --git a/pagetop/src/html/assets.rs b/pagetop/src/html/assets.rs new file mode 100644 index 00000000..4c8f27ce --- /dev/null +++ b/pagetop/src/html/assets.rs @@ -0,0 +1,53 @@ +pub mod favicon; +pub mod javascript; +pub mod stylesheet; + +use crate::html::{html, Markup}; +use crate::{AutoDefault, Weight}; + +pub trait AssetsTrait { + fn name(&self) -> &String; + + fn weight(&self) -> Weight; + + fn prepare(&self) -> Markup; +} + +#[derive(AutoDefault)] +pub(crate) struct Assets(Vec); + +impl Assets { + pub fn new() -> Self { + Assets::(Vec::::new()) + } + + pub fn add(&mut self, asset: T) -> &mut Self { + match self.0.iter().position(|x| x.name() == asset.name()) { + Some(index) => { + if self.0[index].weight() > asset.weight() { + self.0.remove(index); + self.0.push(asset); + } + } + _ => self.0.push(asset), + }; + self + } + + pub fn remove(&mut self, name: &'static str) -> &mut Self { + if let Some(index) = self.0.iter().position(|x| x.name() == name) { + self.0.remove(index); + }; + self + } + + pub fn prepare(&mut self) -> Markup { + let assets = &mut self.0; + assets.sort_by_key(AssetsTrait::weight); + html! { + @for a in assets { + (a.prepare()) + } + } + } +} diff --git a/pagetop/src/html/assets/favicon.rs b/pagetop/src/html/assets/favicon.rs new file mode 100644 index 00000000..068efcb4 --- /dev/null +++ b/pagetop/src/html/assets/favicon.rs @@ -0,0 +1,93 @@ +use crate::html::{html, Markup}; +use crate::AutoDefault; + +#[derive(AutoDefault)] +pub struct Favicon(Vec); + +impl Favicon { + pub fn new() -> Self { + Favicon::default() + } + + // Favicon BUILDER. + + pub fn with_icon(self, image: &str) -> Self { + self.add_icon_item("icon", image, None, None) + } + + pub fn with_icon_for_sizes(self, image: &str, sizes: &str) -> Self { + self.add_icon_item("icon", image, Some(sizes), None) + } + + pub fn with_apple_touch_icon(self, image: &str, sizes: &str) -> Self { + self.add_icon_item("apple-touch-icon", image, Some(sizes), None) + } + + pub fn with_mask_icon(self, image: &str, color: &str) -> Self { + self.add_icon_item("mask-icon", image, None, Some(color)) + } + + pub fn with_manifest(self, file: &str) -> Self { + self.add_icon_item("manifest", file, None, None) + } + + pub fn with_theme_color(mut self, color: &str) -> Self { + self.0.push(html! { + meta name="theme-color" content=(color); + }); + self + } + + pub fn with_ms_tile_color(mut self, color: &str) -> Self { + self.0.push(html! { + meta name="msapplication-TileColor" content=(color); + }); + self + } + + pub fn with_ms_tile_image(mut self, image: &str) -> Self { + self.0.push(html! { + meta name="msapplication-TileImage" content=(image); + }); + self + } + + fn add_icon_item( + mut self, + icon_rel: &str, + icon_source: &str, + icon_sizes: Option<&str>, + icon_color: Option<&str>, + ) -> Self { + let icon_type = match icon_source.rfind('.') { + Some(i) => match icon_source[i..].to_owned().to_lowercase().as_str() { + ".gif" => Some("image/gif"), + ".ico" => Some("image/x-icon"), + ".jpg" => Some("image/jpg"), + ".png" => Some("image/png"), + ".svg" => Some("image/svg+xml"), + _ => None, + }, + _ => None, + }; + self.0.push(html! { + link + rel=(icon_rel) + type=[(icon_type)] + sizes=[(icon_sizes)] + color=[(icon_color)] + href=(icon_source); + }); + self + } + + // Favicon PREPARE. + + pub(crate) fn prepare(&self) -> Markup { + html! { + @for item in &self.0 { + (item) + } + } + } +} diff --git a/pagetop/src/html/assets/javascript.rs b/pagetop/src/html/assets/javascript.rs new file mode 100644 index 00000000..672ab3e0 --- /dev/null +++ b/pagetop/src/html/assets/javascript.rs @@ -0,0 +1,111 @@ +use crate::html::assets::AssetsTrait; +use crate::html::{html, Markup}; +use crate::{concat_string, AutoDefault, Weight}; + +#[derive(AutoDefault)] +enum Source { + #[default] + From(String), + Defer(String), + Async(String), + Inline(String, String), + OnLoad(String, String), +} + +#[rustfmt::skip] +#[derive(AutoDefault)] +pub struct JavaScript { + source : Source, + prefix : &'static str, + version: &'static str, + weight : Weight, +} + +impl AssetsTrait for JavaScript { + fn name(&self) -> &String { + match &self.source { + Source::From(path) => path, + Source::Defer(path) => path, + Source::Async(path) => path, + Source::Inline(name, _) => name, + Source::OnLoad(name, _) => name, + } + } + + fn weight(&self) -> Weight { + self.weight + } + + fn prepare(&self) -> Markup { + match &self.source { + Source::From(path) => html! { + script src=(concat_string!(path, self.prefix, self.version)) {}; + }, + Source::Defer(path) => html! { + script src=(concat_string!(path, self.prefix, self.version)) defer {}; + }, + Source::Async(path) => html! { + script src=(concat_string!(path, self.prefix, self.version)) async {}; + }, + Source::Inline(_, code) => html! { + script { (code) }; + }, + Source::OnLoad(_, code) => html! { (concat_string!( + "document.addEventListener('DOMContentLoaded',function(){", + code, + "});" + )) }, + } + } +} + +impl JavaScript { + pub fn from(path: impl Into) -> Self { + JavaScript { + source: Source::From(path.into()), + ..Default::default() + } + } + + pub fn defer(path: impl Into) -> Self { + JavaScript { + source: Source::Defer(path.into()), + ..Default::default() + } + } + + pub fn asynchronous(path: impl Into) -> Self { + JavaScript { + source: Source::Async(path.into()), + ..Default::default() + } + } + + pub fn inline(name: impl Into, script: impl Into) -> Self { + JavaScript { + source: Source::Inline(name.into(), script.into()), + ..Default::default() + } + } + + pub fn on_load(name: impl Into, script: impl Into) -> Self { + JavaScript { + source: Source::OnLoad(name.into(), script.into()), + ..Default::default() + } + } + + pub fn with_version(mut self, version: &'static str) -> Self { + (self.prefix, self.version) = if version.is_empty() { + ("", "") + } else { + ("?v=", version) + }; + self + } + + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } +} diff --git a/pagetop/src/html/assets/stylesheet.rs b/pagetop/src/html/assets/stylesheet.rs new file mode 100644 index 00000000..11dde4ef --- /dev/null +++ b/pagetop/src/html/assets/stylesheet.rs @@ -0,0 +1,95 @@ +use crate::html::assets::AssetsTrait; +use crate::html::{html, Markup, PreEscaped}; +use crate::{concat_string, AutoDefault, Weight}; + +#[derive(AutoDefault)] +enum Source { + #[default] + From(String), + Inline(String, String), +} + +pub enum TargetMedia { + Default, + Print, + Screen, + Speech, +} + +#[rustfmt::skip] +#[derive(AutoDefault)] +pub struct StyleSheet { + source : Source, + prefix : &'static str, + version: &'static str, + media : Option<&'static str>, + weight : Weight, +} + +impl AssetsTrait for StyleSheet { + fn name(&self) -> &String { + match &self.source { + Source::From(path) => path, + Source::Inline(name, _) => name, + } + } + + fn weight(&self) -> Weight { + self.weight + } + + fn prepare(&self) -> Markup { + match &self.source { + Source::From(path) => html! { + link + rel="stylesheet" + href=(concat_string!(path, self.prefix, self.version)) + media=[self.media]; + }, + Source::Inline(_, code) => html! { + style { (PreEscaped(code)) }; + }, + } + } +} + +impl StyleSheet { + pub fn from(path: impl Into) -> Self { + StyleSheet { + source: Source::From(path.into()), + ..Default::default() + } + } + + pub fn inline(name: impl Into, styles: impl Into) -> Self { + StyleSheet { + source: Source::Inline(name.into(), styles.into()), + ..Default::default() + } + } + + pub fn with_version(mut self, version: &'static str) -> Self { + (self.prefix, self.version) = if version.is_empty() { + ("", "") + } else { + ("?v=", version) + }; + self + } + + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } + + #[rustfmt::skip] + pub fn for_media(mut self, media: &TargetMedia) -> Self { + self.media = match media { + TargetMedia::Default => None, + TargetMedia::Print => Some("print"), + TargetMedia::Screen => Some("screen"), + TargetMedia::Speech => Some("speech"), + }; + self + } +} diff --git a/packages/pagetop/src/html/maud.rs b/pagetop/src/html/maud.rs similarity index 100% rename from packages/pagetop/src/html/maud.rs rename to pagetop/src/html/maud.rs diff --git a/packages/pagetop/src/html/maud/escape.rs b/pagetop/src/html/maud/escape.rs similarity index 100% rename from packages/pagetop/src/html/maud/escape.rs rename to pagetop/src/html/maud/escape.rs diff --git a/pagetop/src/html/opt_classes.rs b/pagetop/src/html/opt_classes.rs new file mode 100644 index 00000000..453991cd --- /dev/null +++ b/pagetop/src/html/opt_classes.rs @@ -0,0 +1,111 @@ +//! **OptionClasses** implements a *helper* for dynamically adding class names to components. +//! +//! This *helper* differentiates between default classes (generally associated with styles provided +//! by the theme) and user classes (for customizing components based on application styles). +//! +//! Classes can be added using [Add]. Operations to [Remove], [Replace] or [Toggle] a class, as well +//! as [Clear] all classes, are also provided. +//! +//! **OptionClasses** assumes that the order of the classes is irrelevant +//! (), and duplicate classes will not be allowed. + +use crate::{fn_builder, AutoDefault}; + +pub enum ClassesOp { + Add, + Prepend, + Remove, + Replace(String), + Toggle, + Set, +} + +#[derive(AutoDefault)] +pub struct OptionClasses(Vec); + +impl OptionClasses { + pub fn new(classes: impl Into) -> Self { + OptionClasses::default().with_value(ClassesOp::Prepend, classes) + } + + // OptionClasses BUILDER. + + #[fn_builder] + pub fn set_value(&mut self, op: ClassesOp, classes: impl Into) -> &mut Self { + let classes: String = classes.into(); + let classes: Vec<&str> = classes.split_ascii_whitespace().collect(); + + if classes.is_empty() { + return self; + } + + match op { + ClassesOp::Add => { + self.add(&classes, self.0.len()); + } + ClassesOp::Prepend => { + self.add(&classes, 0); + } + ClassesOp::Remove => { + for class in classes { + self.0.retain(|c| c.ne(&class.to_string())); + } + } + ClassesOp::Replace(classes_to_replace) => { + let mut pos = self.0.len(); + let replace: Vec<&str> = classes_to_replace.split_ascii_whitespace().collect(); + for class in replace { + if let Some(replace_pos) = self.0.iter().position(|c| c.eq(class)) { + self.0.remove(replace_pos); + if pos > replace_pos { + pos = replace_pos; + } + } + } + self.add(&classes, pos); + } + ClassesOp::Toggle => { + for class in classes { + if !class.is_empty() { + if let Some(pos) = self.0.iter().position(|c| c.eq(class)) { + self.0.remove(pos); + } else { + self.0.push(class.to_string()); + } + } + } + } + ClassesOp::Set => { + self.0.clear(); + self.add(&classes, 0); + } + } + + self + } + + #[inline] + fn add(&mut self, classes: &[&str], mut pos: usize) { + for &class in classes { + if !class.is_empty() && !self.0.iter().any(|c| c == class) { + self.0.insert(pos, class.to_string()); + pos += 1; + } + } + } + + // OptionClasses GETTERS. + + pub fn get(&self) -> Option { + if self.0.is_empty() { + None + } else { + Some(self.0.join(" ")) + } + } + + pub fn contains(&self, class: impl Into) -> bool { + let class: String = class.into(); + self.0.iter().any(|c| c.eq(&class)) + } +} diff --git a/pagetop/src/html/opt_id.rs b/pagetop/src/html/opt_id.rs new file mode 100644 index 00000000..80e98325 --- /dev/null +++ b/pagetop/src/html/opt_id.rs @@ -0,0 +1,29 @@ +use crate::{fn_builder, AutoDefault}; + +#[derive(AutoDefault)] +pub struct OptionId(Option); + +impl OptionId { + pub fn new(value: impl Into) -> Self { + OptionId::default().with_value(value) + } + + // OptionId BUILDER. + + #[fn_builder] + pub fn set_value(&mut self, value: impl Into) -> &mut Self { + self.0 = Some(value.into().trim().replace(' ', "_")); + self + } + + // OptionId GETTERS. + + pub fn get(&self) -> Option { + if let Some(value) = &self.0 { + if !value.is_empty() { + return Some(value.to_owned()); + } + } + None + } +} diff --git a/pagetop/src/html/opt_name.rs b/pagetop/src/html/opt_name.rs new file mode 100644 index 00000000..5ba0c486 --- /dev/null +++ b/pagetop/src/html/opt_name.rs @@ -0,0 +1,29 @@ +use crate::{fn_builder, AutoDefault}; + +#[derive(AutoDefault)] +pub struct OptionName(Option); + +impl OptionName { + pub fn new(value: impl Into) -> Self { + OptionName::default().with_value(value) + } + + // OptionName BUILDER. + + #[fn_builder] + pub fn set_value(&mut self, value: impl Into) -> &mut Self { + self.0 = Some(value.into().trim().replace(' ', "_")); + self + } + + // OptionName GETTERS. + + pub fn get(&self) -> Option { + if let Some(value) = &self.0 { + if !value.is_empty() { + return Some(value.to_owned()); + } + } + None + } +} diff --git a/pagetop/src/html/opt_string.rs b/pagetop/src/html/opt_string.rs new file mode 100644 index 00000000..7de22486 --- /dev/null +++ b/pagetop/src/html/opt_string.rs @@ -0,0 +1,29 @@ +use crate::{fn_builder, AutoDefault}; + +#[derive(AutoDefault)] +pub struct OptionString(Option); + +impl OptionString { + pub fn new(value: impl Into) -> Self { + OptionString::default().with_value(value) + } + + // OptionString BUILDER. + + #[fn_builder] + pub fn set_value(&mut self, value: impl Into) -> &mut Self { + self.0 = Some(value.into().trim().to_owned()); + self + } + + // OptionString GETTERS. + + pub fn get(&self) -> Option { + if let Some(value) = &self.0 { + if !value.is_empty() { + return Some(value.to_owned()); + } + } + None + } +} diff --git a/pagetop/src/html/opt_translated.rs b/pagetop/src/html/opt_translated.rs new file mode 100644 index 00000000..e50a073f --- /dev/null +++ b/pagetop/src/html/opt_translated.rs @@ -0,0 +1,30 @@ +use crate::html::Markup; +use crate::locale::{L10n, LanguageIdentifier}; +use crate::{fn_builder, AutoDefault}; + +#[derive(AutoDefault)] +pub struct OptionTranslated(L10n); + +impl OptionTranslated { + pub fn new(value: L10n) -> Self { + OptionTranslated(value) + } + + // OptionTranslated BUILDER. + + #[fn_builder] + pub fn set_value(&mut self, value: L10n) -> &mut Self { + self.0 = value; + self + } + + // OptionTranslated GETTERS. + + pub fn using(&self, langid: &LanguageIdentifier) -> Option { + self.0.using(langid) + } + + pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup { + self.0.escaped(langid) + } +} diff --git a/pagetop/src/html/unit.rs b/pagetop/src/html/unit.rs new file mode 100644 index 00000000..5a153c55 --- /dev/null +++ b/pagetop/src/html/unit.rs @@ -0,0 +1,56 @@ +use crate::AutoDefault; + +use std::fmt; + +// About pixels: Pixels (px) are relative to the viewing device. For low-dpi devices, 1px is one +// device pixel (dot) of the display. For printers and high resolution screens 1px implies multiple +// device pixels. + +// About em: 2em means 2 times the size of the current font. The em and rem units are practical in +// creating perfectly scalable layout! + +// About viewport: If the browser window size is 50cm wide, 1vw = 0.5cm. + +#[rustfmt::skip] +#[derive(AutoDefault)] +pub enum Value { + #[default] + None, + Auto, + + Cm(isize), // Centimeters. + In(isize), // Inches (1in = 96px = 2.54cm). + Mm(isize), // Millimeters. + Pc(isize), // Picas (1pc = 12pt). + Pt(isize), // Points (1pt = 1/72 of 1in). + Px(isize), // Pixels (1px = 1/96th of 1in). + + RelEm(f32), // Relative to the font-size of the element. + RelPct(f32), // Percentage relative to the parent element. + RelRem(f32), // Relative to font-size of the root element. + RelVh(f32), // Relative to 1% of the height of the viewport. + RelVw(f32), // Relative to 1% of the value of the viewport. +} + +#[rustfmt::skip] +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Value::None => write!(f, ""), + Value::Auto => write!(f, "auto"), + // Absolute value. + Value::Cm(av) => write!(f, "{av}cm"), + Value::In(av) => write!(f, "{av}in"), + Value::Mm(av) => write!(f, "{av}mm"), + Value::Pc(av) => write!(f, "{av}pc"), + Value::Pt(av) => write!(f, "{av}pt"), + Value::Px(av) => write!(f, "{av}px"), + // Relative value. + Value::RelEm(rv) => write!(f, "{rv}em"), + Value::RelPct(rv) => write!(f, "{rv}%"), + Value::RelRem(rv) => write!(f, "{rv}rem"), + Value::RelVh(rv) => write!(f, "{rv}vh"), + Value::RelVw(rv) => write!(f, "{rv}vw"), + } + } +} diff --git a/packages/pagetop/src/lib.rs b/pagetop/src/lib.rs similarity index 94% rename from packages/pagetop/src/lib.rs rename to pagetop/src/lib.rs index cbda48c5..4076b92f 100644 --- a/packages/pagetop/src/lib.rs +++ b/pagetop/src/lib.rs @@ -42,7 +42,7 @@ //! //! async fn hello_world(request: HttpRequest) -> ResultPage { //! Page::new(request) -//! .with_component(Html::with(html! { h1 { "Hello World!" } })) +//! .with_body(PrepareMarkup::With(html! { h1 { "Hello World!" } })) //! .render() //! } //! @@ -53,7 +53,7 @@ //! ``` //! This program implements a package named `HelloWorld` with one service that returns a web page //! that greets the world whenever it is accessed from the browser at `http://localhost:8088` (using -//! the [default configuration settings](`config::Server`)). You can find this code in the `PageTop` +//! the [default configuration settings](`global::Server`)). You can find this code in the `PageTop` //! [examples repository](https://github.com/manuelcillero/pagetop/tree/latest/examples). //! //! # 🧩 Dependency Management @@ -79,7 +79,7 @@ pub use concat_string::concat_string; /// Enables flexible identifier concatenation in macros, allowing new items with pasted identifiers. pub use paste::paste; -pub use pagetop_macros::{main, test, AutoDefault}; +pub use pagetop_macros::{fn_builder, html, main, test, AutoDefault}; pub type StaticResources = std::collections::HashMap<&'static str, static_files::Resource>; @@ -91,6 +91,8 @@ pub type Weight = i8; // Useful functions and macros. pub mod util; +// Load configuration settings. +pub mod config; // Application tracing and event logging. pub mod trace; // HTML in code. diff --git a/packages/pagetop/src/locale.rs b/pagetop/src/locale.rs similarity index 64% rename from packages/pagetop/src/locale.rs rename to pagetop/src/locale.rs index 80b366b3..ef9c1924 100644 --- a/packages/pagetop/src/locale.rs +++ b/pagetop/src/locale.rs @@ -67,13 +67,13 @@ //! # How to apply localization in your code //! //! Once you have created your FTL resource directory, use the -//! [`static_locales!`](crate::static_locales) macro to integrate them into your module or +//! [`include_locales!`](crate::include_locales) macro to integrate them into your module or //! application. If your resources are located in the `"src/locale"` directory, simply declare: //! //! ``` //! use pagetop::prelude::*; //! -//! static_locales!(LOCALES_SAMPLE); +//! include_locales!(LOCALES_SAMPLE); //! ``` //! //! But if they are in another directory, then you can use: @@ -81,14 +81,15 @@ //! ``` //! use pagetop::prelude::*; //! -//! static_locales!(LOCALES_SAMPLE in "path/to/locale"); +//! include_locales!(LOCALES_SAMPLE from "path/to/locale"); //! ``` +use crate::html::{Markup, PreEscaped}; use crate::{global, kv, AutoDefault}; pub use fluent_bundle::FluentValue; pub use fluent_templates; -pub use unic_langid::LanguageIdentifier; +pub use unic_langid::{CharacterDirection, LanguageIdentifier}; use fluent_templates::Loader; use fluent_templates::StaticLoader as Locales; @@ -100,43 +101,62 @@ use std::sync::LazyLock; use std::fmt; -const LANGUAGE_SET_FAILURE: &str = "language_set_failure"; - /// A mapping between language codes (e.g., "en-US") and their corresponding [`LanguageIdentifier`] -/// and human-readable names. +/// and locale key names. static LANGUAGES: LazyLock> = LazyLock::new(|| { kv![ - "en" => (langid!("en-US"), "English"), - "en-GB" => (langid!("en-GB"), "English (British)"), - "en-US" => (langid!("en-US"), "English (United States)"), - "es" => (langid!("es-ES"), "Spanish"), - "es-ES" => (langid!("es-ES"), "Spanish (Spain)"), + "en" => ( langid!("en-US"), "english" ), + "en-GB" => ( langid!("en-GB"), "english_british" ), + "en-US" => ( langid!("en-US"), "english_united_states" ), + "es" => ( langid!("es-ES"), "spanish" ), + "es-ES" => ( langid!("es-ES"), "spanish_spain" ), ] }); -pub static LANGID_FALLBACK: LazyLock = LazyLock::new(|| langid!("en-US")); +static FALLBACK: LazyLock = LazyLock::new(|| langid!("en-US")); /// Sets the application's default /// [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier) /// through `SETTINGS.app.language`. -pub static LANGID_DEFAULT: LazyLock<&LanguageIdentifier> = - LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&LANGID_FALLBACK)); +pub static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> = + LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&FALLBACK)); -pub fn langid_for(language: impl Into) -> Result<&'static LanguageIdentifier, String> { +pub enum LangError { + EmptyLang, + UnknownLang(String), +} + +impl fmt::Display for LangError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LangError::EmptyLang => write!(f, "The language identifier is empty."), + LangError::UnknownLang(lang) => write!(f, "Unknown language identifier: {lang}"), + } + } +} + +pub fn langid_for(language: impl Into) -> Result<&'static LanguageIdentifier, LangError> { let language = language.into(); if language.is_empty() { - return Ok(&LANGID_FALLBACK); + return Err(LangError::EmptyLang); } - LANGUAGES - .get(&language) - .map(|(langid, _)| langid) - .ok_or_else(|| format!("No langid for Unicode Language Identifier \"{language}\".")) + // Attempt to match the full language code (e.g., "es-MX"). + if let Some(langid) = LANGUAGES.get(&language).map(|(langid, _)| langid) { + return Ok(langid); + } + // Fallback to the base language if no sublocale is found (e.g., "es"). + if let Some((base_lang, _)) = language.split_once('-') { + if let Some(langid) = LANGUAGES.get(base_lang).map(|(langid, _)| langid) { + return Ok(langid); + } + } + Err(LangError::UnknownLang(language)) } #[macro_export] /// Defines a set of localization elements and local translation texts, removing Unicode isolating /// marks around arguments to improve readability and compatibility in certain rendering contexts. -macro_rules! static_locales { +macro_rules! include_locales { ( $LOCALES:ident $(, $core_locales:literal)? ) => { $crate::locale::fluent_templates::static_loader! { static $LOCALES = { @@ -148,7 +168,7 @@ macro_rules! static_locales { }; } }; - ( $LOCALES:ident in $dir_locales:literal $(, $core_locales:literal)? ) => { + ( $LOCALES:ident from $dir_locales:literal $(, $core_locales:literal)? ) => { $crate::locale::fluent_templates::static_loader! { static $LOCALES = { locales: $dir_locales, @@ -161,7 +181,7 @@ macro_rules! static_locales { }; } -static_locales!(LOCALES_PAGETOP); +include_locales!(LOCALES_PAGETOP); #[derive(AutoDefault)] enum L10nOp { @@ -174,18 +194,15 @@ enum L10nOp { #[derive(AutoDefault)] pub struct L10n { op: L10nOp, - locales: Option<&'static Locales>, + #[default(&LOCALES_PAGETOP)] + locales: &'static Locales, args: HashMap>, } impl L10n { - pub fn none() -> Self { - L10n::default() - } - pub fn n(text: impl Into) -> Self { L10n { - op: L10nOp::Text(text.into()), + op: L10nOp::Text(text.into().to_string()), ..Default::default() } } @@ -193,7 +210,6 @@ impl L10n { pub fn l(key: impl Into) -> Self { L10n { op: L10nOp::Translate(key.into()), - locales: Some(&LOCALES_PAGETOP), ..Default::default() } } @@ -201,68 +217,60 @@ impl L10n { pub fn t(key: impl Into, locales: &'static Locales) -> Self { L10n { op: L10nOp::Translate(key.into()), - locales: Some(locales), + locales, ..Default::default() } } pub fn with_arg(mut self, arg: impl Into, value: impl Into) -> Self { - self.args - .insert(arg.into(), FluentValue::from(value.into())); + let value = FluentValue::from(value.into()); + self.args.insert(arg.into(), value); self } + pub fn with_args(mut self, args: HashMap) -> Self { + for (k, v) in args { + self.args.insert(k, FluentValue::from(v)); + } + self + } + + pub fn get(&self) -> Option { + self.using(&DEFAULT_LANGID) + } + pub fn using(&self, langid: &LanguageIdentifier) -> Option { match &self.op { L10nOp::None => None, L10nOp::Text(text) => Some(text.to_owned()), - L10nOp::Translate(key) => match self.locales { - Some(locales) => { - if self.args.is_empty() { - locales.try_lookup(langid, key) - } else { - locales.try_lookup_with_args(langid, key, &self.args) - } + L10nOp::Translate(key) => { + if self.args.is_empty() { + self.locales.try_lookup(langid, key) + } else { + self.locales.try_lookup_with_args(langid, key, &self.args) } - None => None, - }, + } } } + + /// Escapes translated text using the default language identifier. + pub fn markup(&self) -> Markup { + PreEscaped(self.get().unwrap_or_default()) + } + + /// Escapes translated text using the specified language identifier. + pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup { + PreEscaped(self.using(langid).unwrap_or_default()) + } } impl fmt::Display for L10n { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.op { - L10nOp::None => write!(f, ""), - L10nOp::Text(text) => write!(f, "{text}"), - L10nOp::Translate(key) => { - if let Some(locales) = self.locales { - write!( - f, - "{}", - if self.args.is_empty() { - locales.lookup( - match key.as_str() { - LANGUAGE_SET_FAILURE => &LANGID_FALLBACK, - _ => &LANGID_DEFAULT, - }, - key, - ) - } else { - locales.lookup_with_args( - match key.as_str() { - LANGUAGE_SET_FAILURE => &LANGID_FALLBACK, - _ => &LANGID_DEFAULT, - }, - key, - &self.args, - ) - } - ) - } else { - write!(f, "Unknown localization {key}") - } - } - } + let content = match &self.op { + L10nOp::None => "".to_string(), + L10nOp::Text(text) => text.clone(), + L10nOp::Translate(key) => self.get().unwrap_or_else(|| format!("No <{}>", key)), + }; + write!(f, "{content}") } } diff --git a/pagetop/src/locale/en-US/languages.ftl b/pagetop/src/locale/en-US/languages.ftl new file mode 100644 index 00000000..1e816605 --- /dev/null +++ b/pagetop/src/locale/en-US/languages.ftl @@ -0,0 +1,5 @@ +english = English +english_british = English (British) +english_united_states = English (United States) +spanish = Spanish +spanish_spain = Spanish (Spain) diff --git a/pagetop/src/locale/en-US/welcome.ftl b/pagetop/src/locale/en-US/welcome.ftl new file mode 100644 index 00000000..d117f462 --- /dev/null +++ b/pagetop/src/locale/en-US/welcome.ftl @@ -0,0 +1,20 @@ +welcome_title = Hello world! + +welcome_intro = Verifying the installation of { $app }. +welcome_powered = A web solution powered by { $pagetop }. + +welcome_page = Welcome Page +welcome_subtitle = Are you a { $app } user? +welcome_text1 = If you don't know what this page is about, this probably means that the site is either experiencing problems or is undergoing routine maintenance. +welcome_text2 = If the issue persists, please contact your system administrator for assistance. + +welcome_pagetop_title = About PageTop +welcome_pagetop_text1 = If you can read this page, it means that the PageTop server is working properly, but has not yet been configured. +welcome_pagetop_text2 = PageTop is a Rust-based web development framework designed to create modular, extensible, and configurable web solutions. +welcome_pagetop_text3 = For detailed information, please visit the official technical documentation. + +welcome_issues_title = Reporting Issues +welcome_issues_text1 = To report any issues with PageTop, please use GitHub. However, check the existing error reports to avoid duplicates. +welcome_issues_text2 = For issues specific to { $app }, please refer to its official repository or support channel, rather than directly to PageTop. + +welcome_have_fun = Coding is creating diff --git a/pagetop/src/locale/es-ES/languages.ftl b/pagetop/src/locale/es-ES/languages.ftl new file mode 100644 index 00000000..ee74ec26 --- /dev/null +++ b/pagetop/src/locale/es-ES/languages.ftl @@ -0,0 +1,5 @@ +english = Inglés +english_british = Inglés (Gran Bretaña) +english_united_states = Inglés (Estados Unidos) +spanish = Español +spanish_spain = Español (España) diff --git a/pagetop/src/locale/es-ES/welcome.ftl b/pagetop/src/locale/es-ES/welcome.ftl new file mode 100644 index 00000000..b351bfc9 --- /dev/null +++ b/pagetop/src/locale/es-ES/welcome.ftl @@ -0,0 +1,20 @@ +welcome_title = ¡Hola mundo! + +welcome_intro = Verificando la instalación de { $app }. +welcome_powered = Una solución web creada con { $pagetop }. + +welcome_page = Página de Bienvenida +welcome_subtitle = ¿Eres usuario de { $app }? +welcome_text1 = Si no sabes por qué se muestra esta página probablemente significa que el sitio está experimentando problemas o está pasando por un mantenimiento de rutina. +welcome_text2 = Si el problema persiste, por favor póngase en contacto con el administrador del sistema. + +welcome_pagetop_title = Sobre PageTop +welcome_pagetop_text1 = Si puedes leer esta página significa que el servidor PageTop funciona correctamente, pero aún no se ha configurado. +welcome_pagetop_text2 = PageTop es un entorno de desarrollo web basado en Rust, diseñado para crear soluciones web modulares, extensibles y configurables. +welcome_pagetop_text3 = Para más información visita la documentación técnica oficial. + +welcome_issues_title = Informando Problemas +welcome_issues_text1 = Para comunicar cualquier problema con PageTop utiliza GitHub. No obstante, comprueba los informes de errores ya existentes para evitar duplicados. +welcome_issues_text2 = Si son fallos específicos de { $app }, por favor acude a su repositorio oficial o canal de soporte, y no al de PageTop directamente. + +welcome_have_fun = Programar es crear diff --git a/packages/pagetop/src/prelude.rs b/pagetop/src/prelude.rs similarity index 66% rename from packages/pagetop/src/prelude.rs rename to pagetop/src/prelude.rs index 76c29b03..c1489152 100644 --- a/packages/pagetop/src/prelude.rs +++ b/pagetop/src/prelude.rs @@ -2,18 +2,20 @@ // RE-EXPORTED. -pub use crate::{concat_string, main, paste, test}; +pub use crate::{concat_string, fn_builder, html, main, paste, test}; pub use crate::{AutoDefault, StaticResources, TypeId, Weight}; // MACROS. // crate::util -pub use crate::{kv, static_config}; +pub use crate::kv; +// crate::config +pub use crate::include_config; // crate::locale -pub use crate::static_locales; +pub use crate::include_locales; // crate::service -pub use crate::{static_files, static_files_service}; +pub use crate::{include_files, include_files_service}; // crate::core::action pub use crate::actions; @@ -36,7 +38,7 @@ pub use crate::core::action::*; pub use crate::core::package::*; pub use crate::core::theme::*; -pub use crate::response::{json::*, redirect::*, ResponseError}; +pub use crate::response::{json::*, page::*, redirect::*, ResponseError}; pub use crate::global; diff --git a/packages/pagetop/src/response.rs b/pagetop/src/response.rs similarity index 87% rename from packages/pagetop/src/response.rs rename to pagetop/src/response.rs index ecbcd954..e51974b1 100644 --- a/packages/pagetop/src/response.rs +++ b/pagetop/src/response.rs @@ -2,6 +2,8 @@ pub use actix_web::ResponseError; +pub mod page; + pub mod json; pub mod redirect; diff --git a/packages/pagetop/src/response/json.rs b/pagetop/src/response/json.rs similarity index 100% rename from packages/pagetop/src/response/json.rs rename to pagetop/src/response/json.rs diff --git a/pagetop/src/response/page.rs b/pagetop/src/response/page.rs new file mode 100644 index 00000000..66ff300c --- /dev/null +++ b/pagetop/src/response/page.rs @@ -0,0 +1,195 @@ +mod error; +pub use error::ErrorPage; + +mod context; +pub use context::{AssetsOp, ContextPage /*, ParamError*/}; +/* +pub type FnContextualPath = fn(cx: &Context) -> &str; +*/ + +use crate::fn_builder; +use crate::html::{html, Markup, PrepareMarkup, DOCTYPE}; +use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated}; +use crate::locale::L10n; +use crate::service::HttpRequest; + +pub use actix_web::Result as ResultPage; + +use unic_langid::CharacterDirection; + +#[rustfmt::skip] +pub struct Page { + title : OptionTranslated, + description : OptionTranslated, + metadata : Vec<(&'static str, &'static str)>, + properties : Vec<(&'static str, &'static str)>, + context : ContextPage, + body_id : OptionId, + body_classes: OptionClasses, + body_content: PrepareMarkup, +} + +impl Page { + #[rustfmt::skip] + pub fn new(request: HttpRequest) -> Self { + Page { + title : OptionTranslated::default(), + description : OptionTranslated::default(), + metadata : Vec::default(), + properties : Vec::default(), + context : ContextPage::new(request), + body_id : OptionId::default(), + body_classes: OptionClasses::default(), + body_content: PrepareMarkup::default(), + } + } + + // Page BUILDER. + + #[fn_builder] + pub fn set_title(&mut self, title: L10n) -> &mut Self { + self.title.set_value(title); + self + } + + #[fn_builder] + pub fn set_description(&mut self, description: L10n) -> &mut Self { + self.description.set_value(description); + self + } + + #[fn_builder] + pub fn set_metadata(&mut self, name: &'static str, content: &'static str) -> &mut Self { + self.metadata.push((name, content)); + self + } + + #[fn_builder] + pub fn set_property(&mut self, property: &'static str, content: &'static str) -> &mut Self { + self.metadata.push((property, content)); + self + } + + #[fn_builder] + pub fn set_assets(&mut self, op: AssetsOp) -> &mut Self { + self.context.set_assets(op); + self + } + + #[fn_builder] + pub fn set_body_id(&mut self, id: impl Into) -> &mut Self { + self.body_id.set_value(id); + self + } + + #[fn_builder] + pub fn set_body_classes(&mut self, op: ClassesOp, classes: impl Into) -> &mut Self { + self.body_classes.set_value(op, classes); + self + } + + #[fn_builder] + pub fn set_body(&mut self, content: PrepareMarkup) -> &mut Self { + self.body_content = content; + self + } + /* + #[fn_builder] + pub fn set_layout(&mut self, layout: &'static str) -> &mut Self { + self.context.set_assets(AssetsOp::Layout(layout)); + self + } + + #[fn_builder] + pub fn set_regions(&mut self, region: &'static str, op: AnyOp) -> &mut Self { + self.context.set_regions(region, op); + self + } + + pub fn with_component(mut self, component: impl ComponentTrait) -> Self { + self.context + .set_regions("content", AnyOp::Add(AnyComponent::with(component))); + self + } + + pub fn with_component_in( + mut self, + region: &'static str, + component: impl ComponentTrait, + ) -> Self { + self.context + .set_regions(region, AnyOp::Add(AnyComponent::with(component))); + self + } + */ + // Page GETTERS. + + pub fn title(&mut self) -> Option { + self.title.using(self.context.langid()) + } + + pub fn description(&mut self) -> Option { + self.description.using(self.context.langid()) + } + + pub fn metadata(&self) -> &Vec<(&str, &str)> { + &self.metadata + } + + pub fn properties(&self) -> &Vec<(&str, &str)> { + &self.properties + } + + pub fn context(&mut self) -> &mut ContextPage { + &mut self.context + } + + pub fn body_id(&self) -> &OptionId { + &self.body_id + } + + pub fn body_classes(&self) -> &OptionClasses { + &self.body_classes + } + + pub fn body_content(&self) -> &PrepareMarkup { + &self.body_content + } + + // Page RENDER. + + pub fn render(&mut self) -> ResultPage { + // Theme operations before preparing the page body. + //self.context.theme().before_prepare_body(self); + + // Packages actions before preparing the page body. + //action::page::BeforePrepareBody::dispatch(self); + + // Prepare page body. + let body = self.context.theme().prepare_page_body(self); + + // Theme operations after preparing the page body. + //self.context.theme().after_prepare_body(self); + + // Packages actions after preparing the page body. + //action::page::AfterPrepareBody::dispatch(self); + + // Prepare page head. + let head = self.context.theme().prepare_page_head(self); + + // Render the page. + let lang = self.context.langid().language.as_str(); + let dir = match self.context.langid().character_direction() { + CharacterDirection::LTR => "ltr", + CharacterDirection::RTL => "rtl", + CharacterDirection::TTB => "auto", + }; + Ok(html! { + (DOCTYPE) + html lang=(lang) dir=(dir) { + (head.render()) + (body.render()) + } + }) + } +} diff --git a/pagetop/src/response/page/context.rs b/pagetop/src/response/page/context.rs new file mode 100644 index 00000000..8c4e9078 --- /dev/null +++ b/pagetop/src/response/page/context.rs @@ -0,0 +1,208 @@ +/* +use crate::base::component::add_base_assets; +use crate::concat_string; +use crate::core::component::AnyOp; */ +use crate::core::theme::all::{theme_by_short_name, DEFAULT_THEME}; +use crate::core::theme::{/*ComponentsInRegions,*/ ThemeRef}; +/* use crate::global::TypeInfo; */ +use crate::html::{html, Markup}; +use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; +use crate::locale::{LanguageIdentifier, DEFAULT_LANGID}; +use crate::service::HttpRequest; +/* +use std::collections::HashMap; +use std::error::Error; +use std::str::FromStr; + +use std::fmt; +*/ + +pub enum AssetsOp { + LangId(&'static LanguageIdentifier), + Theme(&'static str), + //Layout(&'static str), + // Favicon. + SetFavicon(Option), + SetFaviconIfNone(Favicon), + // Stylesheets. + AddStyleSheet(StyleSheet), + RemoveStyleSheet(&'static str), + // JavaScripts. + AddJavaScript(JavaScript), + RemoveJavaScript(&'static str), + // Add assets to properly use base components. + //AddBaseAssets, +} +/* +#[derive(Debug)] +pub enum ParamError { + NotFound, + ParseError(String), +} + +impl fmt::Display for ParamError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParamError::NotFound => write!(f, "Parameter not found"), + ParamError::ParseError(e) => write!(f, "Parse error: {e}"), + } + } +} + +impl Error for ParamError {} +*/ +#[rustfmt::skip] +pub struct ContextPage { + request : HttpRequest, + langid : &'static LanguageIdentifier, + theme : ThemeRef, /* + layout : &'static str, */ + favicon : Option, + stylesheet: Assets, + javascript: Assets, /* + regions : ComponentsInRegions, + params : HashMap<&'static str, String>, + id_counter: usize, */ +} + +impl ContextPage { + #[rustfmt::skip] + pub(crate) fn new(request: HttpRequest) -> Self { + ContextPage { + request, + langid : &DEFAULT_LANGID, + theme : *DEFAULT_THEME, /* + layout : "default", */ + favicon : None, + stylesheet: Assets::::new(), + javascript: Assets::::new(), /* + regions : ComponentsInRegions::default(), + params : HashMap::<&str, String>::new(), + id_counter: 0,*/ + } + } + + pub fn set_assets(&mut self, op: AssetsOp) -> &mut Self { + match op { + AssetsOp::LangId(langid) => { + self.langid = langid; + } + AssetsOp::Theme(theme_name) => { + self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME); + } /* + AssetsOp::Layout(layout) => { + self.layout = layout; + } */ + // Favicon. + AssetsOp::SetFavicon(favicon) => { + self.favicon = favicon; + } + AssetsOp::SetFaviconIfNone(icon) => { + if self.favicon.is_none() { + self.favicon = Some(icon); + } + } + // Stylesheets. + AssetsOp::AddStyleSheet(css) => { + self.stylesheet.add(css); + } + AssetsOp::RemoveStyleSheet(path) => { + self.stylesheet.remove(path); + } + // JavaScripts. + AssetsOp::AddJavaScript(js) => { + self.javascript.add(js); + } + AssetsOp::RemoveJavaScript(path) => { + self.javascript.remove(path); + } /* + // Add assets to properly use base components. + AssetsOp::AddBaseAssets => { + add_base_assets(self); + } */ + } + self + } + /* + pub fn set_regions(&mut self, region: &'static str, op: AnyOp) -> &mut Self { + self.regions.set_components(region, op); + self + } + + pub fn set_param(&mut self, key: &'static str, value: &T) -> &mut Self { + self.params.insert(key, value.to_string()); + self + } + */ + // Context GETTERS. + + pub fn request(&self) -> &HttpRequest { + &self.request + } + + pub fn langid(&self) -> &LanguageIdentifier { + self.langid + } + + pub fn theme(&self) -> ThemeRef { + self.theme + } + /* + pub fn layout(&self) -> &str { + self.layout + } + + pub fn regions(&self) -> &ComponentsInRegions { + &self.regions + } + + pub fn get_param(&self, key: &'static str) -> Result { + self.params + .get(key) + .ok_or(ParamError::NotFound) + .and_then(|v| T::from_str(v).map_err(|_| ParamError::ParseError(v.clone()))) + } + */ + // Context PREPARE. + + pub(crate) fn prepare_assets(&mut self) -> Markup { + html! { + @if let Some(favicon) = &self.favicon { + (favicon.prepare()) + } + (self.stylesheet.prepare()) + (self.javascript.prepare()) + } + } + /* + pub(crate) fn prepare_region(&mut self, region: impl Into) -> Markup { + self.regions + .all_components(self.theme, region.into().as_str()) + .render(self) + } + + // Context EXTRAS. + + pub fn remove_param(&mut self, key: &'static str) -> bool { + self.params.remove(key).is_some() + } + + pub fn required_id(&mut self, id: Option) -> String { + if let Some(id) = id { + id + } else { + let prefix = TypeInfo::ShortName + .of::() + .trim() + .replace(' ', "_") + .to_lowercase(); + let prefix = if prefix.is_empty() { + "prefix".to_owned() + } else { + prefix + }; + self.id_counter += 1; + concat_string!(prefix, "-", self.id_counter.to_string()) + } + } */ +} diff --git a/pagetop/src/response/page/error.rs b/pagetop/src/response/page/error.rs new file mode 100644 index 00000000..2dfa93ff --- /dev/null +++ b/pagetop/src/response/page/error.rs @@ -0,0 +1,71 @@ +use crate::core::theme::all::DEFAULT_THEME; +use crate::response::ResponseError; +use crate::service::http::{header::ContentType, StatusCode}; +use crate::service::{HttpRequest, HttpResponse}; + +use std::fmt; + +#[derive(Debug)] +pub enum ErrorPage { + NotModified(HttpRequest), + BadRequest(HttpRequest), + AccessDenied(HttpRequest), + NotFound(HttpRequest), + PreconditionFailed(HttpRequest), + InternalError(HttpRequest), + Timeout(HttpRequest), +} + +impl fmt::Display for ErrorPage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + // Error 304. + ErrorPage::NotModified(_) => write!(f, "Not Modified"), + // Error 400. + ErrorPage::BadRequest(_) => write!(f, "Bad Client Data"), + // Error 403. + ErrorPage::AccessDenied(request) => { + if let Ok(page) = DEFAULT_THEME.error_403(request.clone()).render() { + write!(f, "{}", page.into_string()) + } else { + write!(f, "Access Denied") + } + } + // Error 404. + ErrorPage::NotFound(request) => { + if let Ok(page) = DEFAULT_THEME.error_404(request.clone()).render() { + write!(f, "{}", page.into_string()) + } else { + write!(f, "Not Found") + } + } + // Error 412. + ErrorPage::PreconditionFailed(_) => write!(f, "Precondition Failed"), + // Error 500. + ErrorPage::InternalError(_) => write!(f, "Internal Error"), + // Error 504. + ErrorPage::Timeout(_) => write!(f, "Timeout"), + } + } +} + +impl ResponseError for ErrorPage { + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()) + .insert_header(ContentType::html()) + .body(self.to_string()) + } + + #[rustfmt::skip] + fn status_code(&self) -> StatusCode { + match self { + ErrorPage::NotModified(_) => StatusCode::NOT_MODIFIED, + ErrorPage::BadRequest(_) => StatusCode::BAD_REQUEST, + ErrorPage::AccessDenied(_) => StatusCode::FORBIDDEN, + ErrorPage::NotFound(_) => StatusCode::NOT_FOUND, + ErrorPage::PreconditionFailed(_) => StatusCode::PRECONDITION_FAILED, + ErrorPage::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::Timeout(_) => StatusCode::GATEWAY_TIMEOUT, + } + } +} diff --git a/packages/pagetop/src/response/redirect.rs b/pagetop/src/response/redirect.rs similarity index 100% rename from packages/pagetop/src/response/redirect.rs rename to pagetop/src/response/redirect.rs diff --git a/packages/pagetop/src/service.rs b/pagetop/src/service.rs similarity index 95% rename from packages/pagetop/src/service.rs rename to pagetop/src/service.rs index e28be6e9..e6bc6373 100644 --- a/packages/pagetop/src/service.rs +++ b/pagetop/src/service.rs @@ -13,7 +13,7 @@ pub use actix_web_files::Files as ActixFiles; pub use actix_web_static_files::ResourceFiles; #[macro_export] -macro_rules! static_files { +macro_rules! include_files { ( $bundle:ident ) => { $crate::paste! { mod [] { @@ -34,11 +34,12 @@ macro_rules! static_files { } #[macro_export] -macro_rules! static_files_service { +macro_rules! include_files_service { ( $scfg:ident, $bundle:ident => $path:expr $(, [$root:expr, $relative:expr])? ) => {{ $crate::paste! { let span = $crate::trace::debug_span!("Configuring static files ", path = $path); let _ = span.in_scope(|| { + #[allow(unused_mut)] let mut serve_embedded:bool = true; $( if !$root.is_empty() && !$relative.is_empty() { diff --git a/packages/pagetop/src/trace.rs b/pagetop/src/trace.rs similarity index 100% rename from packages/pagetop/src/trace.rs rename to pagetop/src/trace.rs diff --git a/pagetop/src/util.rs b/pagetop/src/util.rs new file mode 100644 index 00000000..a34ee599 --- /dev/null +++ b/pagetop/src/util.rs @@ -0,0 +1,161 @@ +//! Useful functions and macros. + +use crate::trace; + +use std::io; +use std::path::PathBuf; + +// USEFUL FUNCTIONS ******************************************************************************** + +pub enum TypeInfo { + FullName, + ShortName, + NameFrom(isize), + NameTo(isize), + PartialName(isize, isize), +} + +impl TypeInfo { + pub fn of(&self) -> &'static str { + let type_name = std::any::type_name::(); + match self { + TypeInfo::FullName => type_name, + TypeInfo::ShortName => Self::partial(type_name, -1, None), + TypeInfo::NameFrom(start) => Self::partial(type_name, *start, None), + TypeInfo::NameTo(end) => Self::partial(type_name, 0, Some(*end)), + TypeInfo::PartialName(start, end) => Self::partial(type_name, *start, Some(*end)), + } + } + + fn partial(type_name: &'static str, start: isize, end: Option) -> &'static str { + let maxlen = type_name.len(); + let mut segments = Vec::new(); + let mut segment_start = 0; // Start position of the current segment. + let mut angle_brackets = 0; // Counter for tracking '<' and '>'. + let mut previous_char = '\0'; // Initializes to a null character, no previous character. + + for (idx, c) in type_name.char_indices() { + match c { + ':' if angle_brackets == 0 => { + if previous_char == ':' { + if segment_start < idx - 1 { + segments.push((segment_start, idx - 1)); // Do not include last '::'. + } + segment_start = idx + 1; // Next segment starts after '::'. + } + } + '<' => angle_brackets += 1, + '>' => angle_brackets -= 1, + _ => {} + } + previous_char = c; + } + + // Include the last segment if there's any. + if segment_start < maxlen { + segments.push((segment_start, maxlen)); + } + + // Calculates the start position. + let start_pos = segments + .get(if start >= 0 { + start as usize + } else { + segments.len() - start.unsigned_abs() + }) + .map_or(0, |&(s, _)| s); + + // Calculates the end position. + let end_pos = segments + .get(if let Some(end) = end { + if end >= 0 { + end as usize + } else { + segments.len() - end.unsigned_abs() + } + } else { + segments.len() - 1 + }) + .map_or(maxlen, |&(_, e)| e); + + // Returns the partial string based on the calculated positions. + &type_name[start_pos..end_pos] + } +} + +/// Calculates the absolute directory given a root path and a relative path. +/// +/// # Arguments +/// +/// * `root_path` - A string slice that holds the root path. +/// * `relative_path` - A string slice that holds the relative path. +/// +/// # Returns +/// +/// * `Ok` - If the operation is successful, returns the absolute directory as a `String`. +/// * `Err` - If an I/O error occurs, returns an `io::Error`. +/// +/// # Errors +/// +/// This function will return an error if: +/// - The root path or relative path are invalid. +/// - There is an issue with file system operations, such as reading the directory. +/// +/// # Examples +/// +/// ``` +/// let root = "/home/user"; +/// let relative = "documents"; +/// let abs_dir = absolute_dir(root, relative).unwrap(); +/// println!("{}", abs_dir); +/// ``` +pub fn absolute_dir( + root_path: impl Into, + relative_path: impl Into, +) -> Result { + let root_path = PathBuf::from(root_path.into()); + let full_path = root_path.join(relative_path.into()); + let absolute_dir = full_path.to_string_lossy().into(); + + if !full_path.is_absolute() { + let message = format!("Path \"{absolute_dir}\" is not absolute"); + trace::warn!(message); + return Err(io::Error::new(io::ErrorKind::InvalidInput, message)); + } + + if !full_path.exists() { + let message = format!("Path \"{absolute_dir}\" does not exist"); + trace::warn!(message); + return Err(io::Error::new(io::ErrorKind::NotFound, message)); + } + + if !full_path.is_dir() { + let message = format!("Path \"{absolute_dir}\" is not a directory"); + trace::warn!(message); + return Err(io::Error::new(io::ErrorKind::InvalidInput, message)); + } + + Ok(absolute_dir) +} + +// USEFUL MACROS *********************************************************************************** + +#[macro_export] +/// Macro para construir grupos de pares clave-valor. +/// +/// ```rust#ignore +/// let args = kv![ +/// "userName" => "Roberto", +/// "photoCount" => 3, +/// "userGender" => "male", +/// ]; +/// ``` +macro_rules! kv { + ( $($key:expr => $value:expr),* $(,)? ) => {{ + let mut a = std::collections::HashMap::new(); + $( + a.insert($key.into(), $value.into()); + )* + a + }}; +} diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 00000000..95e1affa Binary files /dev/null and b/static/favicon.ico differ