Add the ntex integration

This commit is contained in:
photino 2024-04-23 00:55:18 +08:00
parent 132ef1a62b
commit ed55703978
63 changed files with 1546 additions and 47 deletions

View File

@ -18,11 +18,11 @@ which emphasizes **simplicity**, **extensibility** and **productivity**.
- 📅 Lightweight scheduler for sync and async cron jobs.
- 💠 Unified access to storage services, data sources and chatbots.
- 📊 Built-in support for [`tracing`], [`metrics`] and logging.
- 💖 Full integrations with [`actix-web`], [`axum`] and [`dioxus`].
- 💖 Full integrations with [`actix-web`], [`axum`], [`dioxus`] and [`ntex`].
## Getting started
You can start with the example [`actix-app`], [`axum-app`] or [`dioxus-desktop`].
You can start with the example [`actix-app`], [`axum-app`], [`dioxus-desktop`] or [`ntex-app`].
It requires **Rust 1.75+** to build the project.
```shell
@ -38,7 +38,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
zino = { version = "0.20", features = ["axum"] }
zino = { version = "0.21", features = ["axum"] }
```
```rust
@ -94,8 +94,10 @@ If you have any problems or ideas, please don't hesitate to [open an issue][zino
[`actix-web`]: https://crates.io/crates/actix-web
[`axum`]: https://crates.io/crates/axum
[`dioxus`]: https://crates.io/crates/dioxus
[`ntex`]: https://crates.io/crates/ntex
[`actix-app`]: https://github.com/zino-rs/zino/tree/main/examples/actix-app
[`axum-app`]: https://github.com/zino-rs/zino/tree/main/examples/axum-app
[`dioxus-desktop`]: https://github.com/zino-rs/zino/tree/main/examples/dioxus-desktop
[`ntex-app`]: https://github.com/zino-rs/zino/tree/main/examples/ntex-app
[license]: https://github.com/zino-rs/zino/blob/main/LICENSE
[zino-issue]: https://github.com/zino-rs/zino/issues/new

View File

@ -5,6 +5,7 @@ members = [
"axum-app",
"dioxus-desktop",
"minimal-app",
"ntex-app",
]
[profile.release]

View File

@ -1,7 +1,7 @@
[package]
name = "actix-app"
description = "An example for actix-web integration."
version = "0.7.1"
version = "0.7.2"
rust-version = "1.75"
edition = "2021"
publish = false
@ -19,7 +19,7 @@ features = ["derive"]
[dependencies.zino]
path = "../../zino"
version = "0.20.3"
version = "0.21.0"
features = [
"actix",
"i18n",
@ -29,7 +29,7 @@ features = [
[dependencies.zino-core]
path = "../../zino-core"
version = "0.21.3"
version = "0.22.0"
features = [
"cookie",
"env-filter",
@ -39,8 +39,8 @@ features = [
[dependencies.zino-derive]
path = "../../zino-derive"
version = "0.18.3"
version = "0.19.0"
[dependencies.zino-model]
path = "../../zino-model"
version = "0.18.3"
version = "0.19.0"

View File

@ -1,7 +1,7 @@
[package]
name = "axum-app"
description = "An example for axum integration."
version = "0.13.1"
version = "0.13.2"
rust-version = "1.75"
edition = "2021"
publish = false
@ -19,7 +19,7 @@ features = ["derive"]
[dependencies.zino]
path = "../../zino"
version = "0.20.3"
version = "0.21.0"
features = [
"axum",
"i18n",
@ -29,7 +29,7 @@ features = [
[dependencies.zino-core]
path = "../../zino-core"
version = "0.21.3"
version = "0.22.0"
features = [
"cookie",
"env-filter",
@ -40,8 +40,8 @@ features = [
[dependencies.zino-derive]
path = "../../zino-derive"
version = "0.18.3"
version = "0.19.0"
[dependencies.zino-model]
path = "../../zino-model"
version = "0.18.3"
version = "0.19.0"

View File

@ -1,7 +1,7 @@
[package]
name = "dioxus-desktop"
description = "An example for Dioxus desktop integration."
version = "0.4.1"
version = "0.4.2"
rust-version = "1.75"
edition = "2021"
publish = false
@ -26,18 +26,18 @@ features = ["derive"]
[dependencies.zino]
path = "../../zino"
version = "0.20.3"
version = "0.21.0"
features = ["dioxus-desktop", "orm"]
[dependencies.zino-core]
path = "../../zino-core"
version = "0.21.3"
version = "0.22.0"
features = ["env-filter", "orm-sqlite", "tls-rustls"]
[dependencies.zino-model]
path = "../../zino-model"
version = "0.18.3"
version = "0.19.0"
[dependencies.zino-dioxus]
path = "../../zino-dioxus"
version = "0.4.1"
version = "0.4.2"

View File

@ -1,13 +1,13 @@
[package]
name = "minimal-app"
description = "A minimal example to run a server."
version = "0.1.3"
version = "0.2.0"
rust-version = "1.75"
edition = "2021"
publish = false
[dependencies.zino]
path = "../../zino"
version = "0.20.3"
version = "0.21.0"
features = ["axum"]

View File

@ -0,0 +1,46 @@
[package]
name = "ntex-app"
description = "An example for ntex integration."
version = "0.1.0"
rust-version = "1.75"
edition = "2021"
publish = false
[dependencies]
tracing = "0.1.40"
[dependencies.ntex]
version = "1.2.1"
default-features = false
[dependencies.serde]
version = "1.0.198"
features = ["derive"]
[dependencies.zino]
path = "../../zino"
version = "0.21.0"
features = [
"ntex",
"i18n",
"jwt",
"orm",
]
[dependencies.zino-core]
path = "../../zino-core"
version = "0.22.0"
features = [
"cookie",
"env-filter",
"orm-postgres",
"view-minijinja",
]
[dependencies.zino-derive]
path = "../../zino-derive"
version = "0.19.0"
[dependencies.zino-model]
path = "../../zino-model"
version = "0.19.0"

View File

@ -0,0 +1,5 @@
# ntex-app
This folder provides an example for the integration with [`ntex`].
[`ntex`]: https://crates.io/crates/ntex

View File

@ -0,0 +1,54 @@
# --env=dev
name = "data-cube"
version = "0.6.4"
[dirs]
uploads = "local/uploads"
[debug]
host = "127.0.0.1"
port = 6070
[main]
host = "127.0.0.1"
port = 6080
[[standby]]
host = "127.0.0.1"
port = 6081
tag = "portal"
[[standby]]
host = "127.0.0.1"
port = 6082
tag = "admin"
[database]
namespace = "dc"
max-rows = 10000
[[postgres]]
host = "127.0.0.1"
port = 5432
database = "data_cube"
username = "postgres"
password = "smcddNr2mrpwgYvO6ICRLPFfLFd27WySGN9a7a9JrsYP3tIP"
[[sqlite]]
database = "local/data/main.db"
[tracing]
filter = "info,sqlx=info,zino=trace,zino_core=trace"
[metrics]
exporter = "prometheus"
host = "127.0.0.1"
port = 9000
[jwt]
max-age = "20m"
refresh-interval = "7d"
[openapi]
custom-html = "local/docs/rapidoc.html"

View File

@ -0,0 +1,51 @@
# --env=prod
name = "data-cube"
version = "0.6.4"
[dirs]
uploads = "local/uploads"
[debug]
host = "127.0.0.1"
port = 6070
[main]
host = "127.0.0.1"
port = 6080
[[standby]]
host = "127.0.0.1"
port = 6081
tag = "portal"
[[standby]]
host = "127.0.0.1"
port = 6082
tag = "admin"
[database]
namespace = "dc"
[[postgres]]
host = "127.0.0.1"
port = 5432
database = "data_cube"
username = "postgres"
password = "uzU7eGgSw6HLp63qbjpMQMnl4Kk0SrDDp9bIXkRmcjqwRFRf"
[[sqlite]]
database = "local/data/main.db"
[tracing]
level = "warn"
[metrics]
exporter = "prometheus"
host = "127.0.0.1"
port = 9000
[openapi]
show-docs = true
rapidoc-route = "/rapidoc"
custom-html = "local/docs/rapidoc.html"

View File

@ -0,0 +1,3 @@
## User
user-intro = Welcome, { $name }!

View File

@ -0,0 +1,3 @@
## User
user-intro = 欢迎{ $name }

View File

@ -0,0 +1,18 @@
[info]
title = "DataCube API"
description = """
An example for [actix-web] integration.
[actix-web]: https://crates.io/crates/actix-web
"""
contact = { url = "https://github.com/zino-rs/zino" }
license = "MIT"
version = "1.0"
[security_schemes.jwt_auth]
type = "http"
scheme = "bearer"
bearer_format = "JWT"
[[securities]]
name = "jwt_auth"

View File

@ -0,0 +1,22 @@
name = "Authentication"
[[endpoints]]
path = "/auth/login"
method = "POST"
summary = "Logins the user account"
securities = []
[endpoints.body]
type = "object"
account = { type = "string", description = "User account" }
password = { type = "string", format = "password", description = "User password" }
[[endpoints]]
path = "/auth/refresh"
method = "GET"
summary = "Refreshes the access token"
[[endpoints]]
path = "/auth/logout"
method = "POST"
summary = "Logouts the user account"

View File

@ -0,0 +1,22 @@
name = "Files"
[[endpoints]]
path = "/file/upload"
method = "POST"
summary = "Uploads the file"
[endpoints.body]
content_type = "multipart/form-data"
required = ["file"]
file = { type = "string", format = "binary", description = "File content" }
[[endpoints]]
path = "/file/decrypt"
method = "GET"
summary = "Decrypts the file"
[endpoints.query]
required = ["file_name", "access_key_id", "security_token"]
file_name = { type = "string", description = "File name" }
access_key_id = { type = "string", description = "Access key ID" }
security_token = { type = "string", description = "Security token" }

View File

@ -0,0 +1,56 @@
name = "Tags"
securities = []
[[endpoints]]
path = "/tag/new"
method = "POST"
summary = "Creates a new tag"
[endpoints.body]
schema = "/tag/definition"
[[endpoints]]
path = "/tag/{tag_id}/delete"
method = "POST"
summary = "Deletes a tag by ID"
[[endpoints]]
path = "/tag/{tag_id}/update"
method = "POST"
summary = "Updates a tag by ID"
[endpoints.body]
schema = "/tag/definition?action=update"
[[endpoints]]
path = "/tag/{tag_id}/view"
method = "GET"
summary = "Gets a tag by ID"
[[endpoints]]
path = "/tag/list"
method = "GET"
summary = "Finds a list of tags"
[endpoints.query]
category = { type = "string", description = "Tag category" }
parent_id = { schema = "tagId" }
[schemas.tagId]
type = "string"
format = "uuid"
description = "Tag ID"
[models.tag.visibility]
translations = [
["Public", "🌐"],
["Internal", "🔵"],
["Proteched", "⛔"],
["Private", "🔴"],
]
[models.tag.status]
translations = [
["Active", "😄"],
["Inactive", "😴"],
]

View File

@ -0,0 +1,113 @@
name = "Users"
securities = []
[[endpoints]]
path = "/user/new"
method = "POST"
summary = "Creates a new user"
[endpoints.body]
schema = "newUser"
[[endpoints]]
path = "/user/{user_id}/delete"
method = "POST"
summary = "Deletes a user by ID"
[[endpoints]]
path = "/user/{user_id}/update"
method = "POST"
summary = "Updates a user by ID"
[endpoints.body]
schema = "userInfo"
[[endpoints]]
path = "/user/{user_id}/view"
method = "GET"
summary = "Gets a user by ID"
[[endpoints]]
path = "/user/list"
method = "GET"
summary = "Finds a list of users"
[endpoints.query]
roles = { type = "string", description = "User roles" }
tags = { type = "string", description = "User tags" }
[[endpoints]]
path = "/user/import"
method = "POST"
summary = "Imports the user data"
[endpoints.body]
schema = "userData"
[[endpoints]]
path = "/user/export"
method = "GET"
summary = "Exports the user data"
[endpoints.query]
format = { type = "string", enum = ["csv", "json", "jsonlines"], default = "json", description = "File format" }
roles = { type = "string", description = "User roles" }
tags = { type = "string", description = "User tags" }
[schemas.userId]
type = "string"
format = "uuid"
description = "User ID"
[schemas.newUser]
type = "object"
required = ["name", "roles", "account", "password"]
name = { type = "string", description = "User name" }
roles = { type = "array", items = "string", example = ["admin"], description = "User roles" }
account = { type = "string", description = "User account" }
password = { type = "string", format = "password", description = "User password" }
tags = { type = "array", items = { type = "string", format = "uuid" }, description = "User tags" }
[schemas.userInfo]
type = "object"
name = { type = "string", description = "User name" }
status = { type = "string", enum = ["Active", "Inactive", "Locked", "Deleted"], description = "User status" }
roles = { type = "array", items = "string", description = "User roles" }
tags = { type = "array", items = { type = "string", format = "uuid" }, description = "User tags" }
[schemas.userData]
type = "array"
items = "object"
required = ["name", "roles", "account", "password"]
name = { type = "string", description = "User name" }
roles = { type = "array", items = "string", example = ["admin"], description = "User roles" }
account = { type = "string", description = "User account" }
password = { type = "string", format = "password", description = "User password" }
tags = { type = "array", items = { type = "string", format = "uuid" }, description = "User tags" }
[models.user.visibility]
translations = [
["Public", "🌐"],
["Internal", "🔵"],
["Proteched", "⛔"],
["Private", "🔴"],
]
[models.user.status]
translations = [
["Active", "😄"],
["Inactive", "😴"],
]
[models.user.roles]
translations = [
["admin", "👮"],
["worker", "👷"],
]
[models.user.updated_at]
translations = [
["$span:24h", "Updated within 1 day"],
["$span:7d", "Updated within 1 week"],
]

View File

@ -0,0 +1,10 @@
{"timestamp":"2023-02-22T13:30:37.2454056+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:9000","exporter":"prometheus"},"target":"zino_core::application::metrics_exporter"}
{"timestamp":"2023-02-22T13:30:37.4556548+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:6080","env":"dev"},"target":"zino::cluster::axum_cluster"}
{"timestamp":"2023-02-22T13:30:37.4573445+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:6081","env":"dev"},"target":"zino::cluster::axum_cluster"}
{"timestamp":"2023-02-22T13:30:37.4589684+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:6082","env":"dev"},"target":"zino::cluster::axum_cluster"}
{"timestamp":"2023-02-22T13:30:44.3227123+08:00","level":"DEBUG","fields":{"message":"started processing request"},"target":"zino::middleware::tower_tracing","span":{"context.span_id":"1","http.method":"GET","http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
{"timestamp":"2023-02-22T13:30:44.7291153+08:00","level":"INFO","fields":{"message":"finished processing request"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"7c73a4af-c256-4e96-a335-84bf72b0e0a9","context.span_id":"1","context.trace_id":"4278ab19-a395-45a5-8122-941319f2039b","http.method":"GET","http.response.header.traceparent":"00-4278ab19a39545a58122941319f2039b-0000000000000001-03","http.response.header.tracestate":"zino=1","http.server.duration":406,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
{"timestamp":"2023-02-22T13:30:44.7295366+08:00","level":"DEBUG","fields":{"message":"flushed 1944 bytes"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"7c73a4af-c256-4e96-a335-84bf72b0e0a9","context.span_id":"1","context.trace_id":"4278ab19-a395-45a5-8122-941319f2039b","http.method":"GET","http.response.header.traceparent":"00-4278ab19a39545a58122941319f2039b-0000000000000001-03","http.response.header.tracestate":"zino=1","http.server.duration":406,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
{"timestamp":"2023-02-22T13:31:43.8217876+08:00","level":"DEBUG","fields":{"message":"started processing request"},"target":"zino::middleware::tower_tracing","span":{"context.span_id":"8000000001","http.method":"GET","http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
{"timestamp":"2023-02-22T13:31:43.8331486+08:00","level":"INFO","fields":{"message":"finished processing request"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"da7d4993-0ec6-44e2-b42e-70ff3ac781d0","context.span_id":"8000000001","context.trace_id":"beb53cc8-33c5-4dcc-9a50-3c855136774a","http.method":"GET","http.response.header.traceparent":"00-beb53cc833c54dcc9a503c855136774a-0000008000000001-03","http.response.header.tracestate":"zino=8000000001","http.server.duration":11,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
{"timestamp":"2023-02-22T13:31:43.8336681+08:00","level":"DEBUG","fields":{"message":"flushed 1944 bytes"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"da7d4993-0ec6-44e2-b42e-70ff3ac781d0","context.span_id":"8000000001","context.trace_id":"beb53cc8-33c5-4dcc-9a50-3c855136774a","http.method":"GET","http.response.header.traceparent":"00-beb53cc833c54dcc9a503c855136774a-0000008000000001-03","http.response.header.tracestate":"zino=8000000001","http.server.duration":11,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}

View File

@ -0,0 +1,14 @@
"id","name","namespace","visibility","status","description","access_key_id","account","password","mobile","email","avatar","roles","tags","content","metrics","extra","owner_id","maintainer_id","created_at","updated_at","version","edition"
"b3ba6584-3e22-47fc-bd47-770c1d1c55c0","alice","dc:user","internal","active","","SsuJAujkjQ8pr1ntIfQY","","","","","","{admin}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 18:34:01.4012+08","2022-12-16 18:34:01.401206+08",0,0
"2fc3c930-0d99-46e5-a1b2-8f3d6b61771e","bob","dc:user","internal","active","","uiaTnFjGwY9uJc5RbPZZ","","","","","","{worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 18:34:34.316352+08","2022-12-16 18:34:34.316372+08",0,0
"67c96d3f-e020-4ed5-8608-6234abd29063","carol","dc:user","internal","active","","kHUze4mBkaQH4mwIfRBT","","","","","","{admin,worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 18:46:28.686262+08","2022-12-16 18:46:28.68628+08",0,0
"06fdaacd-3d02-48eb-9f7c-64349451dce4","dave","dc:user","internal","active","","d1l2hlKrOA1twJKWep6X","","","","","","{worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 18:46:43.224728+08","2022-12-16 18:46:43.224736+08",0,0
"316de34b-88ba-4f08-bf50-27449de28dd5","eve","dc:user","internal","active","","MqtTxBQq28QcK6cNxZaA","","","","","","{auditor}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 18:47:05.391224+08","2022-12-16 18:47:05.391273+08",0,0
"620ac06f-b2b3-45b6-8509-bc6692b9d22d","frid","dc:user","internal","active","","LwgWVfdW59oyYCHEjJkQ","","","","","","{auditor,worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 19:29:57.196914+08","2022-12-16 19:29:57.196921+08",0,0
"cad561d5-ff6e-4323-bdb1-ce304cf7fd08","grace","dc:user","internal","active","","kQ1vvrdnAK5gQI0rJxtD","","","","","","{worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 19:30:11.000058+08","2022-12-16 19:30:11.000065+08",0,0
"329000bb-f36d-477b-9fcc-4d728e6b63b9","tom","dc:user","internal","active","","nSIUIMMNHtCBr2Bd1v5f","","","","","","{worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2023-01-27 15:34:37.770765+08","2023-01-27 15:34:37.770777+08",0,0
"0b8b59c3-fe72-45a8-b9e9-f670d2cfb2ca","peter","dc:user","internal","active","","b3VJCpJWH3kpo9hc1MNZ","","","","","","{worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2023-01-27 15:52:14.224398+08","2023-01-27 15:52:14.224408+08",0,0
"dbb56a21-7e6b-4702-b069-1f337645a8e6","peter","dc:user","internal","active","","qsOHvYfJQALId5QEazzh","","","","","","{worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2023-01-27 16:59:25.898107+08","2023-01-27 16:59:25.898155+08",0,0
"0eb846c2-7c09-4a7b-b7c8-78092f1a85e1","zino","dc:user","internal","active","","6kimxTOU0f3ysxqUKDgW","","","","","","{worker,auditor}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2023-02-18 21:09:42.466218+08","2023-02-18 21:09:42.466256+08",0,0
"f944a630-3677-4559-9de2-8723e9c26d08","zino","dc:user","internal","active","","6DI5DP8dbuNvTHnMA07r","","","","","","{worker,auditor}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2023-02-18 21:38:23.270831+08","2023-02-18 21:38:23.270865+08",0,0
"b5b261ed-b1c1-46a3-88c7-a22b9eb76b40","bob","dc:user","internal","active","","XXcwVoahGQrsDdlG7s73","","","","","","{admin,worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2023-02-18 21:42:13.0111+08","2023-02-18 21:42:13.011111+08",0,0
1 id name namespace visibility status description access_key_id account password mobile email avatar roles tags content metrics extra owner_id maintainer_id created_at updated_at version edition
2 b3ba6584-3e22-47fc-bd47-770c1d1c55c0 alice dc:user internal active SsuJAujkjQ8pr1ntIfQY {admin} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2022-12-16 18:34:01.4012+08 2022-12-16 18:34:01.401206+08 0 0
3 2fc3c930-0d99-46e5-a1b2-8f3d6b61771e bob dc:user internal active uiaTnFjGwY9uJc5RbPZZ {worker} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2022-12-16 18:34:34.316352+08 2022-12-16 18:34:34.316372+08 0 0
4 67c96d3f-e020-4ed5-8608-6234abd29063 carol dc:user internal active kHUze4mBkaQH4mwIfRBT {admin,worker} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2022-12-16 18:46:28.686262+08 2022-12-16 18:46:28.68628+08 0 0
5 06fdaacd-3d02-48eb-9f7c-64349451dce4 dave dc:user internal active d1l2hlKrOA1twJKWep6X {worker} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2022-12-16 18:46:43.224728+08 2022-12-16 18:46:43.224736+08 0 0
6 316de34b-88ba-4f08-bf50-27449de28dd5 eve dc:user internal active MqtTxBQq28QcK6cNxZaA {auditor} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2022-12-16 18:47:05.391224+08 2022-12-16 18:47:05.391273+08 0 0
7 620ac06f-b2b3-45b6-8509-bc6692b9d22d frid dc:user internal active LwgWVfdW59oyYCHEjJkQ {auditor,worker} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2022-12-16 19:29:57.196914+08 2022-12-16 19:29:57.196921+08 0 0
8 cad561d5-ff6e-4323-bdb1-ce304cf7fd08 grace dc:user internal active kQ1vvrdnAK5gQI0rJxtD {worker} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2022-12-16 19:30:11.000058+08 2022-12-16 19:30:11.000065+08 0 0
9 329000bb-f36d-477b-9fcc-4d728e6b63b9 tom dc:user internal active nSIUIMMNHtCBr2Bd1v5f {worker} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2023-01-27 15:34:37.770765+08 2023-01-27 15:34:37.770777+08 0 0
10 0b8b59c3-fe72-45a8-b9e9-f670d2cfb2ca peter dc:user internal active b3VJCpJWH3kpo9hc1MNZ {worker} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2023-01-27 15:52:14.224398+08 2023-01-27 15:52:14.224408+08 0 0
11 dbb56a21-7e6b-4702-b069-1f337645a8e6 peter dc:user internal active qsOHvYfJQALId5QEazzh {worker} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2023-01-27 16:59:25.898107+08 2023-01-27 16:59:25.898155+08 0 0
12 0eb846c2-7c09-4a7b-b7c8-78092f1a85e1 zino dc:user internal active 6kimxTOU0f3ysxqUKDgW {worker,auditor} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2023-02-18 21:09:42.466218+08 2023-02-18 21:09:42.466256+08 0 0
13 f944a630-3677-4559-9de2-8723e9c26d08 zino dc:user internal active 6DI5DP8dbuNvTHnMA07r {worker,auditor} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2023-02-18 21:38:23.270831+08 2023-02-18 21:38:23.270865+08 0 0
14 b5b261ed-b1c1-46a3-88c7-a22b9eb76b40 bob dc:user internal active XXcwVoahGQrsDdlG7s73 {admin,worker} {} {} {} {} 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 2023-02-18 21:42:13.0111+08 2023-02-18 21:42:13.011111+08 0 0

View File

@ -0,0 +1,21 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>RapiDoc</title>
<script type="module" src="https://s4.zstatic.net/npm/rapidoc/dist/rapidoc-min.js"></script>
</head>
<body>
<rapi-doc spec-url="$specUrl"
heading-text="RapiDoc"
schema-style="table"
schema-description-expanded="true"
default-schema-tab="schema"
show-method-in-nav-bar="as-colored-block"
sort-endpoints-by="method"
persist-auth="true"
allow-spec-file-download="true"
primary-color="#2d87e2">
</rapi-doc>
</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404 Not Found</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h3>404 Not Found</h3>
</body>
</html>

View File

@ -0,0 +1,10 @@
{"timestamp":"2023-02-22T13:30:37.2454056+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:9000","exporter":"prometheus"},"target":"zino_core::application::metrics_exporter"}
{"timestamp":"2023-02-22T13:30:37.4556548+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:6080","env":"dev"},"target":"zino::cluster::axum_cluster"}
{"timestamp":"2023-02-22T13:30:37.4573445+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:6081","env":"dev"},"target":"zino::cluster::axum_cluster"}
{"timestamp":"2023-02-22T13:30:37.4589684+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:6082","env":"dev"},"target":"zino::cluster::axum_cluster"}
{"timestamp":"2023-02-22T13:30:44.3227123+08:00","level":"DEBUG","fields":{"message":"started processing request"},"target":"zino::middleware::tower_tracing","span":{"context.span_id":"1","http.method":"GET","http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
{"timestamp":"2023-02-22T13:30:44.7291153+08:00","level":"INFO","fields":{"message":"finished processing request"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"7c73a4af-c256-4e96-a335-84bf72b0e0a9","context.span_id":"1","context.trace_id":"4278ab19-a395-45a5-8122-941319f2039b","http.method":"GET","http.response.header.traceparent":"00-4278ab19a39545a58122941319f2039b-0000000000000001-03","http.response.header.tracestate":"zino=1","http.server.duration":406,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
{"timestamp":"2023-02-22T13:30:44.7295366+08:00","level":"DEBUG","fields":{"message":"flushed 1944 bytes"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"7c73a4af-c256-4e96-a335-84bf72b0e0a9","context.span_id":"1","context.trace_id":"4278ab19-a395-45a5-8122-941319f2039b","http.method":"GET","http.response.header.traceparent":"00-4278ab19a39545a58122941319f2039b-0000000000000001-03","http.response.header.tracestate":"zino=1","http.server.duration":406,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
{"timestamp":"2023-02-22T13:31:43.8217876+08:00","level":"DEBUG","fields":{"message":"started processing request"},"target":"zino::middleware::tower_tracing","span":{"context.span_id":"8000000001","http.method":"GET","http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
{"timestamp":"2023-02-22T13:31:43.8331486+08:00","level":"INFO","fields":{"message":"finished processing request"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"da7d4993-0ec6-44e2-b42e-70ff3ac781d0","context.span_id":"8000000001","context.trace_id":"beb53cc8-33c5-4dcc-9a50-3c855136774a","http.method":"GET","http.response.header.traceparent":"00-beb53cc833c54dcc9a503c855136774a-0000008000000001-03","http.response.header.tracestate":"zino=8000000001","http.server.duration":11,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
{"timestamp":"2023-02-22T13:31:43.8336681+08:00","level":"DEBUG","fields":{"message":"flushed 1944 bytes"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"da7d4993-0ec6-44e2-b42e-70ff3ac781d0","context.span_id":"8000000001","context.trace_id":"beb53cc8-33c5-4dcc-9a50-3c855136774a","http.method":"GET","http.response.header.traceparent":"00-beb53cc833c54dcc9a503c855136774a-0000008000000001-03","http.response.header.tracestate":"zino=8000000001","http.server.duration":11,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h3>Congratulations, you have booted your application successfully!</h3>
</body>
</html>

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>File Uploader</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<form method="POST" action="/file/upload" enctype="multipart/form-data">
<div>Title: <input name="title"/></div>
<div>File: <input type="file" name="files" multiple/></div>
<button type="submit">Upload</button>
</form>
</body>
</html>

View File

@ -0,0 +1,59 @@
use zino::{prelude::*, Request, Response, Result};
use zino_model::user::{JwtAuthService, User};
pub async fn login(mut req: Request) -> Result {
let current_time = DateTime::now();
let body: Map = req.parse_body().await?;
let (user_id, mut data) = User::generate_token(body).await.extract(&req)?;
let user_updates = json!({
"status": "Active",
"last_login_at": data.remove("current_login_at").and_then(|v| v.as_datetime()),
"last_login_ip": data.remove("current_login_ip"),
"current_login_at": current_time,
"current_login_ip": req.client_ip(),
"$inc": { "login_count": 1 },
});
let mut user_mutations = user_updates.into_map_opt().unwrap_or_default();
let (validation, user) = User::update_by_id(&user_id, &mut user_mutations, None)
.await
.extract(&req)?;
if !validation.is_success() {
reject!(req, validation);
}
data.upsert("entry", user.snapshot());
let mut res = Response::default().context(&req);
res.set_json_data(data);
Ok(res.into())
}
pub async fn refresh(req: Request) -> Result {
let claims = req.parse_jwt_claims(JwtClaims::shared_key())?;
let data = User::refresh_token(&claims).await.extract(&req)?;
let mut res = Response::default().context(&req);
res.set_json_data(data);
Ok(res.into())
}
pub async fn logout(req: Request) -> Result {
let user_session = req
.get_data::<UserSession<_>>()
.ok_or_else(|| warn!("401 Unauthorized: the user session is invalid"))
.extract(&req)?;
let mut mutations = Map::from_entry("status", "SignedOut");
let user_id = user_session.user_id();
let (validation, user) = User::update_by_id(user_id, &mut mutations, None)
.await
.extract(&req)?;
if !validation.is_success() {
reject!(req, validation);
}
let data = Map::data_entry(user.snapshot());
let mut res = Response::default().context(&req);
res.set_json_data(data);
Ok(res.into())
}

View File

@ -0,0 +1,68 @@
use std::time::{Duration, Instant};
use zino::{prelude::*, Cluster, Request, Response, Result};
pub async fn upload(mut req: Request) -> Result {
let (mut body, files) = req.parse_form_data::<Map>().await?;
let dir = Cluster::shared_dir("uploads");
let expires = DateTime::now() + Duration::from_secs(600);
let mut encryption_duration = Duration::ZERO;
let mut uploads = Vec::new();
for mut file in files {
let mut query = Map::new();
let access_key_id = AccessKeyId::new();
query.upsert("access_key_id", access_key_id.to_string());
let secret_key = SecretAccessKey::new(&access_key_id);
let security_token =
SecurityToken::try_new(access_key_id, expires, &secret_key).extract(&req)?;
query.upsert("security_token", security_token.to_string());
let encryption_start_time = Instant::now();
file.encrypt_with(secret_key.as_ref()).extract(&req)?;
encryption_duration += encryption_start_time.elapsed();
if let Some(file_name) = file.file_name() {
file.write(dir.join(file_name)).extract(&req)?;
query.upsert("file_name", file_name);
let mut map = Map::new();
map.upsert("field_name", file.field_name());
map.upsert("file_name", file_name);
map.upsert("content_type", file.content_type().map(|m| m.as_ref()));
map.upsert("url", format!("/file/decrypt?{}", query.to_query_string()));
uploads.push(map);
}
}
body.upsert("files", uploads);
let mut res = Response::default().context(&req);
res.record_server_timing("enc", None, Some(encryption_duration));
res.set_json_data(Map::data_entry(body));
Ok(res.into())
}
pub async fn decrypt(req: Request) -> Result {
let query = req.parse_query::<Map>()?;
let access_key_id = req.parse_access_key_id()?;
let secret_key = SecretAccessKey::new(&access_key_id);
let security_token = req.parse_security_token(secret_key.as_ref())?;
if security_token.is_expired() {
reject!(req, forbidden, "the security token has expired");
}
let Some(file_name) = query.get_str("file_name") else {
reject!(req, "file_name", "it should be specified");
};
let file_path = Cluster::shared_dir("uploads").join(file_name);
let mut file = NamedFile::try_from_local(file_path).extract(&req)?;
let decryption_start_time = Instant::now();
file.decrypt_with(secret_key).extract(&req)?;
let decryption_duration = decryption_start_time.elapsed();
let mut res = Response::default().context(&req);
res.record_server_timing("dec", None, Some(decryption_duration));
res.send_file(file);
Ok(res.into())
}

View File

@ -0,0 +1,4 @@
pub(crate) mod auth;
pub(crate) mod file;
pub(crate) mod stats;
pub(crate) mod user;

View File

@ -0,0 +1,15 @@
use zino::{prelude::*, Cluster, Request, Response, Result};
pub async fn index(req: Request) -> Result {
let res = Response::default().context(&req);
let stats = json!({
"method": "GET",
"path": "/stats",
"app_state_data": Cluster::state_data(),
});
let data = json!({
"title": "Stats",
"output": stats.to_string_pretty(),
});
Ok(res.render("output.html", data).into())
}

View File

@ -0,0 +1,44 @@
use std::time::Instant;
use zino::{prelude::*, Request, Response, Result};
use zino_model::user::User;
pub async fn new(mut req: Request) -> Result {
let mut user = User::new();
let mut res = req.model_validation(&mut user).await?;
let validation = user.check_constraints().await.extract(&req)?;
if !validation.is_success() {
reject!(req, validation);
}
let user_name = user.name().to_owned();
user.insert().await.extract(&req)?;
let args = fluent_args![
"name" => user_name
];
let user_intro = req.translate("user-intro", Some(args)).extract(&req)?;
let data = json!({
"method": req.request_method().as_ref(),
"path": req.request_path(),
"user_intro": user_intro,
});
let locale = req.new_cookie("locale".into(), "en-US".into(), None);
res.set_cookie(&locale);
res.set_code(StatusCode::CREATED);
res.set_json_data(data);
Ok(res.into())
}
pub async fn view(req: Request) -> Result {
let user_id = req.parse_param("id")?;
let db_query_start_time = Instant::now();
let user = User::fetch_by_id(&user_id).await.extract(&req)?;
let db_query_duration = db_query_start_time.elapsed();
let data = Map::data_entry(user);
let mut res = Response::default().context(&req);
res.record_server_timing("db", None, Some(db_query_duration));
res.set_json_data(data);
Ok(res.into())
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,19 @@
mod controller;
mod domain;
mod extension;
mod logic;
mod middleware;
mod model;
mod router;
mod schedule;
mod service;
use zino::prelude::*;
fn main() {
zino::Cluster::boot()
.register(router::routes())
.register_debug(router::debug_routes())
.spawn(schedule::job_scheduler())
.run_with(schedule::async_job_scheduler())
}

View File

@ -0,0 +1,62 @@
use ntex::{
service::{Middleware, Service, ServiceCtx},
web::{Error, ErrorRenderer, WebRequest, WebResponse},
};
use zino::{prelude::*, Request};
#[derive(Default)]
pub struct UserSessionInitializer;
pub struct UserSessionMiddleware<S> {
service: S,
}
impl<S> Middleware<S> for UserSessionInitializer {
type Service = UserSessionMiddleware<S>;
fn create(&self, service: S) -> Self::Service {
UserSessionMiddleware { service }
}
}
impl<S, Err> Service<WebRequest<Err>> for UserSessionMiddleware<S>
where
S: Service<WebRequest<Err>, Response = WebResponse, Error = Error>,
Err: ErrorRenderer,
{
type Response = WebResponse;
type Error = Error;
ntex::forward_poll_ready!(service);
async fn call(
&self,
req: WebRequest<Err>,
ctx: ServiceCtx<'_, Self>,
) -> Result<Self::Response, Self::Error> {
let mut req = Request::from(req);
match req.parse_jwt_claims(JwtClaims::shared_key()) {
Ok(claims) => {
if let Ok(mut user_session) = UserSession::<Uuid>::try_from_jwt_claims(claims) {
if let Ok(session_id) = req.parse_session_id() {
user_session.set_session_id(session_id);
}
req.set_data(user_session);
} else {
let message = "401 Unauthorized: invalid JWT claims";
let rejection = Rejection::with_message(message).context(&req).into();
let result: zino::Result<Self::Response> = Err(rejection);
return result.map_err(|err| err.into());
}
}
Err(rejection) => {
let result: zino::Result<Self::Response> = Err(rejection.into());
return result.map_err(|err| err.into());
}
}
let req = WebRequest::try_from(req)?;
let res = ctx.call(&self.service, req).await?;
Ok(res)
}
}

View File

@ -0,0 +1,3 @@
mod access;
pub(crate) use access::UserSessionInitializer;

View File

@ -0,0 +1,3 @@
mod tag;
pub(crate) use tag::Tag;

View File

@ -0,0 +1,50 @@
use serde::{Deserialize, Serialize};
use zino::prelude::*;
use zino_derive::{DecodeRow, Model, ModelAccessor, ModelHooks, Schema};
/// The `tag` model.
#[derive(
Debug,
Clone,
Default,
Serialize,
Deserialize,
DecodeRow,
Schema,
ModelAccessor,
ModelHooks,
Model,
)]
#[serde(default)]
pub struct Tag {
// Basic fields.
#[schema(primary_key, read_only, constructor = "Uuid::now_v7")]
id: Uuid,
#[schema(not_null, comment = "Tag name")]
name: String,
#[schema(default_value = "Active", index_type = "hash")]
status: String,
description: String,
// Info fields.
#[schema(not_null, index_type = "hash", comment = "Tag category")]
category: String,
#[schema(
snapshot,
reference = "Tag",
fetch_as = "parent_tag",
comment = "Optional parent tag"
)]
parent_id: Option<Uuid>,
// Extensions.
#[schema(reserved)]
extra: Map,
// Revisions.
#[schema(read_only, default_value = "now", index_type = "btree")]
created_at: DateTime,
#[schema(default_value = "now", index_type = "btree")]
updated_at: DateTime,
version: u64,
}

View File

@ -0,0 +1,79 @@
use crate::{
controller::{auth, file, stats, user},
middleware,
model::Tag,
};
use ntex::web::{get, post, scope, ServiceConfig};
use zino::{DefaultController, RouterConfigure};
use zino_model::User;
pub fn routes() -> Vec<RouterConfigure> {
vec![
auth_router as RouterConfigure,
file_router as RouterConfigure,
user_router as RouterConfigure,
tag_router as RouterConfigure,
]
}
pub fn debug_routes() -> Vec<RouterConfigure> {
vec![
stats_router as RouterConfigure,
user_debug_router as RouterConfigure,
tag_debug_router as RouterConfigure,
]
}
fn auth_router(cfg: &mut ServiceConfig) {
cfg.route("/auth/login", post().to(auth::login));
cfg.service(
scope("/auth")
.route("/refresh", get().to(auth::refresh))
.route("/logout", post().to(auth::logout))
.wrap(middleware::UserSessionInitializer),
);
}
fn file_router(cfg: &mut ServiceConfig) {
cfg.service(
scope("/file")
.route("/upload", post().to(file::upload))
.route("/decrypt", get().to(file::decrypt))
.wrap(middleware::UserSessionInitializer),
);
}
fn user_router(cfg: &mut ServiceConfig) {
cfg.route("/user/new", post().to(user::new))
.route("/user/{id}/delete", post().to(User::soft_delete))
.route("/user/{id}/update", post().to(User::update))
.route("/user/{id}/view", get().to(user::view))
.route("/user/list", get().to(User::list))
.route("/user/import", post().to(User::import))
.route("/user/export", get().to(User::export));
}
fn tag_router(cfg: &mut ServiceConfig) {
cfg.route("/tag/new", post().to(Tag::new))
.route("/tag/{id}/delete", post().to(Tag::soft_delete))
.route("/tag/{id}/update", post().to(Tag::update))
.route("/tag/{id}/view", get().to(Tag::view))
.route("/tag/list", get().to(Tag::list))
.route("/tag/tree", get().to(Tag::tree));
}
fn stats_router(cfg: &mut ServiceConfig) {
cfg.route("/stats", get().to(stats::index));
}
fn user_debug_router(cfg: &mut ServiceConfig) {
cfg.route("/user/schema", get().to(User::schema))
.route("/user/definition", get().to(User::definition))
.route("/user/mock", get().to(User::mock));
}
fn tag_debug_router(cfg: &mut ServiceConfig) {
cfg.route("/tag/schema", get().to(Tag::schema))
.route("/tag/definition", get().to(Tag::definition))
.route("/tag/mock", get().to(Tag::mock));
}

View File

@ -0,0 +1,42 @@
use zino::prelude::*;
use zino_model::User;
pub fn every_15s(job_id: Uuid, job_data: &mut Map, last_tick: DateTime) {
let counter = job_data
.get("counter")
.map(|c| c.as_u64().unwrap_or_default() + 1)
.unwrap_or_default();
job_data.upsert("counter", counter);
job_data.upsert("current", DateTime::now());
job_data.upsert("last_tick", last_tick);
job_data.upsert("job_id", job_id.to_string());
}
pub fn every_20s(job_id: Uuid, job_data: &mut Map, last_tick: DateTime) {
let counter = job_data
.get("counter")
.map(|c| c.as_u64().unwrap_or_default() + 1)
.unwrap_or_default();
job_data.upsert("counter", counter);
job_data.upsert("current", DateTime::now());
job_data.upsert("last_tick", last_tick);
job_data.upsert("job_id", job_id.to_string());
}
pub fn every_hour(job_id: Uuid, job_data: &mut Map, last_tick: DateTime) -> BoxFuture {
let counter = job_data
.get("counter")
.map(|c| c.as_u64().unwrap_or_default() + 1)
.unwrap_or_default();
job_data.upsert("counter", counter);
job_data.upsert("current", DateTime::now());
job_data.upsert("last_tick", last_tick);
job_data.upsert("job_id", job_id.to_string());
Box::pin(async {
let query = Query::default();
let columns = [("*", true), ("roles", true)];
if let Ok(mut map) = User::count_many(&query, &columns).await {
job_data.append(&mut map);
}
})
}

View File

@ -0,0 +1,24 @@
use zino::prelude::*;
mod job;
pub fn job_scheduler() -> JobScheduler {
let mut scheduler = JobScheduler::new();
let job = Job::new("0/15 * * * * *", job::every_15s as CronJob).disable(true);
scheduler.add(job);
let job = Job::new("0/20 * * * * *", job::every_20s as CronJob).max_ticks(3);
scheduler.add(job);
scheduler
}
pub fn async_job_scheduler() -> AsyncJobScheduler {
let mut scheduler = AsyncJobScheduler::new();
let job = AsyncJob::new("0/25 * * * * *", job::every_hour as AsyncCronJob).immediate(true);
scheduler.add(job);
scheduler
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>{{ title }} | {{ APP_NAME }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
</head>
<body>
<div class="container">
{% block content %}{% endblock content %}
</div>
</body>
</html>

View File

@ -0,0 +1,6 @@
{% extends "layout.html" %}
{% block content %}
<div class="output">
<code><pre>{{ output }}</pre></code>
</div>
{% endblock content %}

View File

@ -1,7 +1,7 @@
[package]
name = "zino-cli"
description = "CLI tools for zino."
version = "0.2.4"
version = "0.3.0"
rust-version = "1.75"
edition = "2021"
license = "MIT"
@ -25,4 +25,4 @@ features = ["color", "derive"]
[dependencies.zino-core]
path = "../zino-core"
version = "0.21.3"
version = "0.22.0"

View File

@ -1,7 +1,7 @@
[package]
name = "zino-core"
description = "Core types and traits for zino."
version = "0.21.3"
version = "0.22.0"
rust-version = "1.75"
edition = "2021"
license = "MIT"

View File

@ -15,7 +15,6 @@ use crate::{
validation::Validation,
warn, JsonValue, Map, SharedString, Uuid,
};
use bytes::Bytes;
use multer::Multipart;
use serde::de::DeserializeOwned;
use std::{borrow::Cow, net::IpAddr, str::FromStr, time::Instant};
@ -84,7 +83,7 @@ pub trait RequestContext {
fn client_ip(&self) -> Option<IpAddr>;
/// Reads the entire request body into a byte buffer.
async fn read_body_bytes(&mut self) -> Result<Bytes, Error>;
async fn read_body_bytes(&mut self) -> Result<Vec<u8>, Error>;
/// Returns the request path regardless of nesting.
#[inline]

View File

@ -1,7 +1,7 @@
[package]
name = "zino-derive"
description = "Derived traits for zino."
version = "0.18.3"
version = "0.19.0"
rust-version = "1.75"
edition = "2021"
license = "MIT"
@ -21,5 +21,5 @@ syn = "2.0.60"
[dependencies.zino-core]
path = "../zino-core"
version = "0.21.3"
version = "0.22.0"
features = ["orm"]

View File

@ -1,7 +1,7 @@
[package]
name = "zino-dioxus"
description = "Dioxus components for zino."
version = "0.4.1"
version = "0.4.2"
rust-version = "1.75"
edition = "2021"
license = "MIT"
@ -38,4 +38,4 @@ features = [
[dependencies.zino-core]
path = "../zino-core"
version = "0.21.3"
version = "0.22.0"

View File

@ -1,7 +1,7 @@
[package]
name = "zino-extra"
description = "Extra utilities for zino."
version = "0.3.1"
version = "0.3.2"
rust-version = "1.75"
edition = "2021"
license = "MIT"
@ -42,4 +42,4 @@ optional = true
[dependencies.zino-core]
path = "../zino-core"
version = "0.21.3"
version = "0.22.0"

View File

@ -1,7 +1,7 @@
[package]
name = "zino-model"
description = "Domain models for zino."
version = "0.18.3"
version = "0.19.0"
rust-version = "1.75"
edition = "2021"
license = "MIT"
@ -49,7 +49,7 @@ features = ["derive"]
[dependencies.zino-core]
path = "../zino-core"
version = "0.21.3"
version = "0.22.0"
features = [
"jwt",
"orm",
@ -59,4 +59,4 @@ features = [
[dependencies.zino-derive]
path = "../zino-derive"
version = "0.18.3"
version = "0.19.0"

View File

@ -1,7 +1,7 @@
[package]
name = "zino"
description = "Next-generation framework for composable applications in Rust."
version = "0.20.3"
version = "0.21.0"
rust-version = "1.75"
edition = "2021"
license = "MIT"
@ -52,6 +52,12 @@ dioxus-desktop = [
default = []
i18n = ["zino-core/i18n"]
jwt = ["zino-core/jwt"]
ntex = [
"dep:futures",
"dep:ntex",
"dep:ntex-files",
"zino-core/runtime-tokio",
]
orm = ["zino-core/orm"]
[dependencies]
@ -103,6 +109,16 @@ optional = true
version = "0.25.1"
optional = true
[dependencies.ntex]
version = "1.2.1"
optional = true
default-features = false
features = ["compress", "tokio"]
[dependencies.ntex-files]
version = "1.0.0"
optional = true
[dependencies.tokio]
version = "1.37.0"
optional = true
@ -119,7 +135,7 @@ optional = true
features = ["timeout"]
[dependencies.tower-http]
version = "0.5.1"
version = "0.5.2"
optional = true
features = [
"add-extension",
@ -146,4 +162,4 @@ optional = true
[dependencies.zino-core]
path = "../zino-core"
version = "0.21.3"
version = "0.22.0"

View File

@ -20,11 +20,11 @@ which emphasizes **simplicity**, **extensibility** and **productivity**.
- 📅 Lightweight scheduler for sync and async cron jobs.
- 💠 Unified access to storage services, data sources and chatbots.
- 📊 Built-in support for [`tracing`], [`metrics`] and logging.
- 💖 Full integrations with [`actix-web`], [`axum`] and [`dioxus`].
- 💖 Full integrations with [`actix-web`], [`axum`], [`dioxus`] and [`ntex`].
## Getting started
You can start with the example [`actix-app`], [`axum-app`] or [`dioxus-desktop`].
You can start with the example [`actix-app`], [`axum-app`], [`dioxus-desktop`] or [`ntex-app`].
Here is the simplest application to run a server:
```toml
@ -34,7 +34,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
zino = { version = "0.20", features = ["axum"] }
zino = { version = "0.21", features = ["axum"] }
```
```rust,ignore
@ -56,6 +56,7 @@ The following optional features are available:
| `dioxus` | Enables the integration with [`dioxus`]. | No |
| `i18n` | Enables the support for internationalization. | No |
| `jwt` | Enables the support for JSON Web Token. | No |
| `ntex` | Enables the integration with [`ntex`]. | No |
| `orm` | Enables the ORM for MySQL, PostgreSQL or **SQLite**. | No |
[`zino`]: https://github.com/zino-rs/zino
@ -65,6 +66,8 @@ The following optional features are available:
[`actix-web`]: https://crates.io/crates/actix-web
[`axum`]: https://crates.io/crates/axum
[`dioxus`]: https://crates.io/crates/dioxus
[`ntex`]: https://crates.io/crates/ntex
[`actix-app`]: https://github.com/zino-rs/zino/tree/main/examples/actix-app
[`axum-app`]: https://github.com/zino-rs/zino/tree/main/examples/axum-app
[`dioxus-desktop`]: https://github.com/zino-rs/zino/tree/main/examples/dioxus-desktop
[`ntex-app`]: https://github.com/zino-rs/zino/tree/main/examples/ntex-app

View File

@ -13,6 +13,11 @@ cfg_if::cfg_if! {
mod plugin_loader;
pub(crate) mod dioxus_desktop;
use plugin_loader::load_plugins;
} else if #[cfg(feature = "ntex")] {
mod plugin_loader;
pub(crate) mod ntex_cluster;
use plugin_loader::load_plugins;
}
}

View File

@ -0,0 +1,185 @@
use crate::RouterConfigure;
use ntex::{
rt::System,
time::{self, Seconds},
web::{
self,
middleware::Compress,
types::{FormConfig, JsonConfig, PayloadConfig},
App, HttpServer,
},
};
use ntex_files::{Files, NamedFile};
use std::path::PathBuf;
use zino_core::{
application::{Application, Plugin, ServerTag},
extension::TomlTableExt,
schedule::AsyncScheduler,
};
/// An HTTP server cluster for `ntex`.
#[derive(Default)]
pub struct NtexCluster {
/// Custom plugins.
custom_plugins: Vec<Plugin>,
/// Default routes.
default_routes: Vec<RouterConfigure>,
/// Tagged routes.
tagged_routes: Vec<(ServerTag, Vec<RouterConfigure>)>,
}
impl Application for NtexCluster {
type Routes = Vec<RouterConfigure>;
fn register(mut self, routes: Self::Routes) -> Self {
self.default_routes = routes;
self
}
fn register_with(mut self, server_tag: ServerTag, routes: Self::Routes) -> Self {
self.tagged_routes.push((server_tag, routes));
self
}
fn add_plugin(mut self, plugin: Plugin) -> Self {
self.custom_plugins.push(plugin);
self
}
fn run_with<T: AsyncScheduler + Send + 'static>(self, mut scheduler: T) {
let app_env = Self::env();
System::new("prelude").block_on(async {
Self::load().await;
super::load_plugins(self.custom_plugins, app_env).await;
});
if scheduler.is_ready() {
System::new("scheduler")
.system()
.arbiter()
.spawn(Box::pin(async move {
loop {
scheduler.tick().await;
// Cannot use `std::thread::sleep` because it blocks the Tokio runtime.
time::sleep(scheduler.time_till_next_job()).await;
}
}));
}
System::new("main").block_on(async {
let default_routes = self.default_routes.leak() as &'static [_];
let tagged_routes = self.tagged_routes.leak() as &'static [_];
let app_state = Self::shared_state();
let app_name = Self::name();
let app_version = Self::version();
let app_domain = Self::domain();
let listeners = app_state.listeners();
let servers = listeners.into_iter().map(|listener| {
let server_tag = listener.0;
let addr = listener.1;
tracing::warn!(
server_tag = server_tag.as_str(),
app_env = app_env.as_str(),
app_name,
app_version,
"listen on `{addr}`",
);
// Server config
let project_dir = Self::project_dir();
let default_public_dir = project_dir.join("public");
let mut public_route_prefix = "/public";
let mut public_dir = PathBuf::new();
let mut backlog = 2048; // Maximum number of pending connections
let mut max_connections = 25000; // Maximum number of concurrent connections
let mut body_limit = 128 * 1024 * 1024; // 128MB
let mut request_timeout = 60; // 60 seconds
if let Some(config) = app_state.get_config("server") {
if let Some(dir) = config.get_str("page-dir") {
public_route_prefix = "/page";
public_dir.push(dir);
} else if let Some(dir) = config.get_str("public-dir") {
public_dir.push(dir);
} else {
public_dir = default_public_dir;
}
if let Some(route_prefix) = config.get_str("public-route-prefix") {
public_route_prefix = route_prefix;
}
if let Some(value) = config.get_i32("backlog") {
backlog = value;
}
if let Some(value) = config.get_usize("max-connections") {
max_connections = value;
}
if let Some(limit) = config.get_usize("body-limit") {
body_limit = limit;
}
if let Some(timeout) = config
.get_duration("request-timeout")
.and_then(|d| d.as_secs().try_into().ok())
{
request_timeout = timeout;
}
} else {
public_dir = default_public_dir;
}
HttpServer::new(move || {
let mut app = App::new();
if public_dir.exists() {
let index_file = public_dir.join("index.html");
let favicon_file = public_dir.join("favicon.ico");
if index_file.exists() {
let index_file_handler = web::get()
.to(move || async { NamedFile::open("./public/index.html") });
app = app.route("/", index_file_handler);
}
if favicon_file.exists() {
let favicon_file_handler =
web::get().to(|| async { NamedFile::open("./public/favicon.ico") });
app = app.route("/favicon.ico", favicon_file_handler);
}
let static_files = Files::new(public_route_prefix, public_dir.clone())
.show_files_listing()
.index_file("index.html");
app = app.service(static_files);
tracing::info!(
"Static pages `{public_route_prefix}/**` are registered for `{addr}`"
);
}
for route in default_routes {
app = app.configure(route);
}
for (tag, routes) in tagged_routes {
if tag == &server_tag || server_tag.is_debug() {
for route in routes {
app = app.configure(route);
}
}
}
app.state(FormConfig::default().limit(body_limit))
.state(JsonConfig::default().limit(body_limit))
.state(PayloadConfig::default().limit(body_limit))
.wrap(Compress::default())
})
.stop_runtime()
.disable_signals()
.server_hostname(app_domain)
.backlog(backlog)
.maxconn(max_connections)
.client_timeout(Seconds(request_timeout))
.bind(addr)
.unwrap_or_else(|err| panic!("fail to create an HTTP server: {err}"))
.run()
});
for result in futures::future::join_all(servers).await {
if let Err(err) = result {
tracing::error!("ntex server error: {err}");
}
}
});
}
}

View File

@ -52,7 +52,7 @@ pub trait DefaultController<K, U> {
async fn mock(req: Self::Request) -> Self::Result;
}
#[cfg(any(feature = "actix", feature = "axum"))]
#[cfg(any(feature = "actix", feature = "axum", feature = "ntex"))]
#[cfg(feature = "orm")]
use zino_core::{
extension::JsonObjectExt,
@ -63,7 +63,7 @@ use zino_core::{
JsonValue, Map,
};
#[cfg(any(feature = "actix", feature = "axum"))]
#[cfg(any(feature = "actix", feature = "axum", feature = "ntex"))]
#[cfg(feature = "orm")]
impl<K, U, M> DefaultController<K, U> for M
where

View File

@ -56,5 +56,24 @@ cfg_if::cfg_if! {
/// Desktop applications for `dioxus`.
pub type Desktop<R> = DioxusDesktop<R>;
} else if #[cfg(feature = "ntex")] {
use crate::application::ntex_cluster::NtexCluster;
use crate::request::ntex_request::NtexExtractor;
use crate::response::ntex_response::{NtexRejection, NtexResponse};
/// HTTP server cluster for `ntex`.
pub type Cluster = NtexCluster;
/// Router configure for `ntex`.
pub type RouterConfigure = fn(cfg: &mut ntex::web::ServiceConfig);
/// A specialized request extractor for `ntex`.
pub type Request = NtexExtractor<ntex::web::HttpRequest>;
/// A specialized response for `ntex`.
pub type Response = zino_core::response::Response<ntex::http::StatusCode>;
/// A specialized `Result` type for `ntex`.
pub type Result<T = NtexResponse> = std::result::Result<T, NtexRejection>;
}
}

View File

@ -95,9 +95,9 @@ impl RequestContext for ActixExtractor<HttpRequest> {
}
#[inline]
async fn read_body_bytes(&mut self) -> Result<Bytes, Error> {
async fn read_body_bytes(&mut self) -> Result<Vec<u8>, Error> {
let bytes = Bytes::from_request(&self.0, &mut self.1).await?;
Ok(bytes)
Ok(bytes.to_vec())
}
}

View File

@ -1,6 +1,5 @@
use async_trait::async_trait;
use axum::{
body::Bytes,
extract::{ConnectInfo, FromRequest, MatchedPath, OriginalUri, Request},
http::{HeaderMap, Method},
};
@ -117,10 +116,10 @@ impl RequestContext for AxumExtractor<Request> {
}
#[inline]
async fn read_body_bytes(&mut self) -> Result<Bytes, Error> {
async fn read_body_bytes(&mut self) -> Result<Vec<u8>, Error> {
let body = mem::take(self.body_mut());
let bytes = axum::body::to_bytes(body, usize::MAX).await?;
Ok(bytes)
Ok(bytes.to_vec())
}
}

View File

@ -3,5 +3,7 @@ cfg_if::cfg_if! {
pub(crate) mod actix_request;
} else if #[cfg(feature = "axum")] {
pub(crate) mod axum_request;
} else if #[cfg(feature = "ntex")] {
pub(crate) mod ntex_request;
}
}

View File

@ -0,0 +1,144 @@
use crate::response::ntex_response::NtexRejection;
use ntex::{
http::{header::HeaderMap, Method, Payload},
util::Bytes,
web::{
error::{DefaultError, ErrorRenderer},
FromRequest, HttpRequest, WebRequest,
},
};
use std::{
borrow::Cow,
convert::Infallible,
net::IpAddr,
ops::{Deref, DerefMut},
};
use zino_core::{
error::Error,
request::{Context, RequestContext, Uri},
response::Rejection,
state::Data,
};
/// An HTTP request extractor for `ntex`.
pub struct NtexExtractor<T>(T, Payload);
impl<T> Deref for NtexExtractor<T> {
type Target = T;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for NtexExtractor<T> {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl RequestContext for NtexExtractor<HttpRequest> {
type Method = Method;
type Headers = HeaderMap;
#[inline]
fn request_method(&self) -> &Self::Method {
self.method()
}
#[inline]
fn original_uri(&self) -> &Uri {
self.uri()
}
#[inline]
fn matched_route(&self) -> Cow<'_, str> {
self.match_info().path().into()
}
#[inline]
fn header_map(&self) -> &Self::Headers {
self.headers()
}
#[inline]
fn get_header(&self, name: &str) -> Option<&str> {
self.headers().get(name)?.to_str().ok()
}
#[inline]
fn get_context(&self) -> Option<Context> {
let extensions = self.extensions();
extensions.get::<Context>().cloned()
}
#[inline]
fn get_data<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
self.extensions().get::<Data<T>>().map(|data| data.get())
}
#[inline]
fn set_data<T: Clone + Send + Sync + 'static>(&mut self, value: T) -> Option<T> {
let mut ext = self.extensions_mut();
let old_data = ext.remove::<Data<T>>().map(|data| data.into_inner());
ext.insert(Data::new(value));
old_data
}
#[inline]
fn client_ip(&self) -> Option<IpAddr> {
self.connection_info().remote().and_then(|s| s.parse().ok())
}
#[inline]
async fn read_body_bytes(&mut self) -> Result<Vec<u8>, Error> {
let bytes =
<Bytes as FromRequest<DefaultError>>::from_request(&self.0, &mut self.1).await?;
Ok(bytes.to_vec())
}
}
impl<Err: ErrorRenderer> From<WebRequest<Err>> for NtexExtractor<HttpRequest> {
#[inline]
fn from(request: WebRequest<Err>) -> Self {
let (request, payload) = request.into_parts();
Self(request, payload)
}
}
impl<Err: ErrorRenderer> TryFrom<NtexExtractor<HttpRequest>> for WebRequest<Err> {
type Error = NtexRejection;
#[inline]
fn try_from(extractor: NtexExtractor<HttpRequest>) -> Result<Self, Self::Error> {
Self::from_parts(extractor.0, extractor.1).map_err(|_| {
let error = Error::new("fail to re-constructed `WebRequest`");
Rejection::internal_server_error(error).into()
})
}
}
impl From<HttpRequest> for NtexExtractor<HttpRequest> {
#[inline]
fn from(request: HttpRequest) -> Self {
Self(request, Payload::None)
}
}
impl From<NtexExtractor<HttpRequest>> for HttpRequest {
#[inline]
fn from(extractor: NtexExtractor<HttpRequest>) -> Self {
extractor.0
}
}
impl FromRequest<DefaultError> for NtexExtractor<HttpRequest> {
type Error = Infallible;
#[inline]
async fn from_request(req: &HttpRequest, payload: &mut Payload) -> Result<Self, Self::Error> {
Ok(NtexExtractor(req.clone(), payload.take()))
}
}

View File

@ -3,5 +3,7 @@ cfg_if::cfg_if! {
pub(crate) mod actix_response;
} else if #[cfg(feature = "axum")] {
pub(crate) mod axum_response;
} else if #[cfg(feature = "ntex")] {
pub(crate) mod ntex_response;
}
}

View File

@ -0,0 +1,142 @@
use ntex::{
http::{
body::Body,
header::{self, HeaderName, HeaderValue},
ResponseError, StatusCode,
},
web::{HttpRequest, HttpResponse, Responder, WebResponseError},
};
use std::fmt;
use zino_core::{
response::{Rejection, Response, ResponseCode},
trace::TimingMetric,
};
/// An HTTP response for `ntex`.
pub struct NtexResponse<S: ResponseCode = StatusCode>(Response<S>);
impl<S: ResponseCode> From<Response<S>> for NtexResponse<S> {
#[inline]
fn from(response: Response<S>) -> Self {
Self(response)
}
}
impl<S: ResponseCode> Responder for NtexResponse<S> {
async fn respond_to(self, req: &HttpRequest) -> HttpResponse {
let mut response = self.0;
if !response.has_context() {
let req = crate::Request::from(req.clone());
response = response.context(&req);
}
let mut res = build_http_response(&mut response);
for (key, value) in response.finalize() {
if let Ok(header_name) = HeaderName::try_from(key.as_ref()) {
if let Ok(header_value) = HeaderValue::try_from(value) {
res.headers_mut().insert(header_name, header_value);
}
}
}
res
}
}
/// An HTTP rejection response for `ntex`.
pub struct NtexRejection(Response<StatusCode>);
impl fmt::Debug for NtexRejection {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0.message().unwrap_or("OK"))
}
}
impl fmt::Display for NtexRejection {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0.status_code())
}
}
impl From<Rejection> for NtexRejection {
#[inline]
fn from(rejection: Rejection) -> Self {
Self(Response::from(rejection))
}
}
impl ResponseError for NtexRejection {
fn error_response(&self) -> HttpResponse {
let mut response = self.0.clone();
let mut res = build_http_response(&mut response);
let request_id = response.request_id();
if !request_id.is_nil() {
if let Ok(header_value) = HeaderValue::try_from(request_id.to_string()) {
let header_name = HeaderName::from_static("x-request-id");
res.headers_mut().insert(header_name, header_value);
}
}
let (traceparent, tracestate) = response.trace_context();
if let Ok(header_value) = HeaderValue::try_from(traceparent) {
let header_name = HeaderName::from_static("traceparent");
res.headers_mut().insert(header_name, header_value);
}
if let Ok(header_value) = HeaderValue::try_from(tracestate) {
let header_name = HeaderName::from_static("tracestate");
res.headers_mut().insert(header_name, header_value);
}
let response_time = response.response_time();
let timing = TimingMetric::new("total".into(), None, response_time.into());
if let Ok(header_value) = HeaderValue::try_from(timing.to_string()) {
let header_name = HeaderName::from_static("server-timing");
res.headers_mut().insert(header_name, header_value);
}
for (key, value) in response.headers() {
if let Ok(header_name) = HeaderName::try_from(key.as_ref()) {
if let Ok(header_value) = HeaderValue::try_from(value) {
res.headers_mut().insert(header_name, header_value);
}
}
}
res
}
}
impl WebResponseError for NtexRejection {
#[inline]
fn error_response(&self, _: &HttpRequest) -> HttpResponse {
ResponseError::error_response(&self)
}
}
/// Build http response from `zino_core::response::Response`.
fn build_http_response<S: ResponseCode>(response: &mut Response<S>) -> HttpResponse {
match response.read_bytes() {
Ok(data) => {
let status_code = response
.status_code()
.try_into()
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let body = Body::from(data.to_vec());
let mut res = HttpResponse::with_body(status_code, body);
if let Ok(header_value) = HeaderValue::try_from(response.content_type()) {
res.headers_mut().insert(header::CONTENT_TYPE, header_value);
}
res
}
Err(err) => {
let status_code = StatusCode::INTERNAL_SERVER_ERROR;
let body = Body::from(err.to_string());
let mut res = HttpResponse::with_body(status_code, body);
res.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
);
res
}
}
}