diff --git a/Cargo.lock b/Cargo.lock
index 70a54258..defafbe8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1059,12 +1059,12 @@ dependencies = [
[[package]]
name = "darling"
-version = "0.20.3"
+version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
+checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391"
dependencies = [
- "darling_core 0.20.3",
- "darling_macro 0.20.3",
+ "darling_core 0.20.8",
+ "darling_macro 0.20.8",
]
[[package]]
@@ -1083,9 +1083,9 @@ dependencies = [
[[package]]
name = "darling_core"
-version = "0.20.3"
+version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
+checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f"
dependencies = [
"fnv",
"ident_case",
@@ -1108,11 +1108,11 @@ dependencies = [
[[package]]
name = "darling_macro"
-version = "0.20.3"
+version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
+checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
- "darling_core 0.20.3",
+ "darling_core 0.20.8",
"quote",
"syn 2.0.48",
]
@@ -1291,6 +1291,12 @@ dependencies = [
"dtoa",
]
+[[package]]
+name = "dyn-clone"
+version = "1.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d"
+
[[package]]
name = "dyn-stack"
version = "0.10.0"
@@ -1833,6 +1839,19 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+[[package]]
+name = "gloo-utils"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
[[package]]
name = "h2"
version = "0.3.24"
@@ -2321,6 +2340,15 @@ dependencies = [
"either",
]
+[[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 = "0.4.8"
@@ -2986,6 +3014,17 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+[[package]]
+name = "openapiv3"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc02deea53ffe807708244e5914f6b099ad7015a207ee24317c22112e17d9c5c"
+dependencies = [
+ "indexmap 2.2.3",
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "openssl"
version = "0.10.63"
@@ -3040,6 +3079,7 @@ dependencies = [
"logos",
"once_cell",
"serde",
+ "tapi",
"thiserror",
"utoipa",
]
@@ -4140,7 +4180,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb6085ff9c3fd7e5163826901d39164ab86f11bdca16b2f766a00c528ff9cef9"
dependencies = [
- "darling 0.20.3",
+ "darling 0.20.8",
"proc-macro2",
"quote",
"syn 2.0.48",
@@ -4203,18 +4243,40 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4"
[[package]]
name = "serde"
-version = "1.0.196"
+version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
+checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.196"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
+checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
@@ -4223,9 +4285,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.113"
+version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
+checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa 1.0.10",
"ryu",
@@ -4516,6 +4578,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"tantivy",
+ "tapi",
"thiserror",
"tikv-jemallocator",
"tokenizers",
@@ -4810,6 +4873,36 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+[[package]]
+name = "tapi"
+version = "0.1.0"
+dependencies = [
+ "axum",
+ "dyn-clone",
+ "futures-util",
+ "heck 0.4.1",
+ "indexmap 2.2.3",
+ "itertools 0.12.1",
+ "openapiv3",
+ "serde",
+ "serde_json",
+ "tapi-macro",
+ "tsify",
+]
+
+[[package]]
+name = "tapi-macro"
+version = "0.1.0"
+dependencies = [
+ "darling 0.20.8",
+ "heck 0.4.1",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals 0.29.0",
+ "syn 2.0.48",
+]
+
[[package]]
name = "tar"
version = "0.4.40"
@@ -5250,6 +5343,31 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+[[package]]
+name = "tsify"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0"
+dependencies = [
+ "gloo-utils",
+ "serde",
+ "serde_json",
+ "tsify-macros",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "tsify-macros"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals 0.28.0",
+ "syn 2.0.48",
+]
+
[[package]]
name = "twox-hash"
version = "1.6.3"
diff --git a/Cargo.toml b/Cargo.toml
index d5ca6e25..fb9bfba0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -94,6 +94,7 @@ serde = {version = "1.0.137", features = ["rc", "derive"]}
serde_json = "1.0.81"
serde_urlencoded = "0.7.1"
tantivy = {git = "https://github.com/quickwit-oss/tantivy", rev = "182f58cea"}
+tapi = {path = "../tapi/crates/tapi/", package = "tapi", features = ["openapi"]}
thiserror = "1.0.31"
tikv-jemallocator = "0.5"
tokenizers = "0.13.2"
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index 229f6456..172a1566 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -88,6 +88,7 @@ serde = {workspace = true}
serde_json = {workspace = true}
serde_urlencoded = {workspace = true}
tantivy = {workspace = true}
+tapi = {workspace = true}
thiserror = {workspace = true}
tokenizers = {workspace = true}
tokio = {workspace = true}
diff --git a/crates/core/src/api/autosuggest.rs b/crates/core/src/api/autosuggest.rs
index 9a1d973e..00b81719 100644
--- a/crates/core/src/api/autosuggest.rs
+++ b/crates/core/src/api/autosuggest.rs
@@ -14,13 +14,11 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-use std::{collections::HashMap, sync::Arc};
-
-use axum::{extract, response::IntoResponse, Json};
+use axum::{extract, Json};
use serde::Serialize;
use utoipa::{IntoParams, ToSchema};
-use super::State;
+use super::AppState;
const HIGHLIGHTED_PREFIX: &str = "";
const HIGHLIGHTED_POSTFIX: &str = "";
@@ -39,17 +37,17 @@ fn highlight(query: &str, suggestion: &str) -> String {
new_suggestion
}
-#[derive(Serialize, ToSchema)]
+#[derive(Serialize, ToSchema, tapi::Tapi)]
#[serde(rename_all = "camelCase")]
pub struct Suggestion {
highlighted: String,
raw: String,
}
-#[derive(Debug, serde::Serialize, serde::Deserialize, IntoParams)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, IntoParams, tapi::Tapi)]
#[serde(rename_all = "camelCase")]
pub struct AutosuggestQuery {
- q: String,
+ q: Option,
}
#[utoipa::path(
@@ -60,12 +58,12 @@ pub struct AutosuggestQuery {
(status = 200, description = "Autosuggest", body = Vec),
)
)]
-
+#[tapi::tapi(path = "/autosuggest", method = Post, state = AppState)]
pub async fn route(
- extract::State(state): extract::State>,
- extract::Query(params): extract::Query>,
-) -> impl IntoResponse {
- if let Some(query) = params.get("q") {
+ extract::State(state): extract::State,
+ extract::Query(params): extract::Query,
+) -> Json> {
+ if let Some(query) = ¶ms.q {
let mut suggestions = Vec::new();
for suggestion in state.autosuggest.suggestions(query).unwrap() {
@@ -82,11 +80,12 @@ pub async fn route(
}
}
+#[tapi::tapi(path = "/autosuggest/browser", method = Post, state = AppState)]
pub async fn browser(
- extract::State(state): extract::State>,
- extract::Query(params): extract::Query>,
-) -> impl IntoResponse {
- if let Some(query) = params.get("q") {
+ extract::State(state): extract::State,
+ extract::Query(params): extract::Query,
+) -> Json<(String, Vec)> {
+ if let Some(query) = ¶ms.q {
Json((query.clone(), state.autosuggest.suggestions(query).unwrap()))
} else {
Json((String::new(), Vec::new()))
diff --git a/crates/core/src/api/explore.rs b/crates/core/src/api/explore.rs
index 979e4516..2fdc941f 100644
--- a/crates/core/src/api/explore.rs
+++ b/crates/core/src/api/explore.rs
@@ -15,11 +15,10 @@
// along with this program. If not, see .
use axum::extract;
-use http::StatusCode;
use optics::{HostRankings, Optic};
use utoipa::ToSchema;
-#[derive(serde::Deserialize, ToSchema)]
+#[derive(serde::Deserialize, ToSchema, tapi::Tapi)]
#[serde(rename_all = "camelCase")]
pub struct ExploreExportOpticParams {
chosen_hosts: Vec,
@@ -34,12 +33,13 @@ pub struct ExploreExportOpticParams {
(status = 200, description = "Export explored sites as an optic", body = String),
)
)]
+#[tapi::tapi(path = "/expore/export", method = Post)]
pub async fn explore_export_optic(
extract::Json(ExploreExportOpticParams {
chosen_hosts,
similar_hosts,
}): extract::Json,
-) -> Result {
+) -> String {
let matches = similar_hosts
.into_iter()
.chain(chosen_hosts.clone().into_iter())
@@ -69,5 +69,5 @@ pub async fn explore_export_optic(
..Default::default()
};
- Ok(optic.to_string())
+ optic.to_string()
}
diff --git a/crates/core/src/api/hosts.rs b/crates/core/src/api/hosts.rs
index cf54d18c..8dd4ccad 100644
--- a/crates/core/src/api/hosts.rs
+++ b/crates/core/src/api/hosts.rs
@@ -15,11 +15,10 @@
// along with this program. If not, see .
use axum::{extract, Json};
-use http::StatusCode;
use optics::{HostRankings, Optic};
use utoipa::ToSchema;
-#[derive(serde::Deserialize, ToSchema)]
+#[derive(serde::Deserialize, ToSchema, tapi::Tapi)]
#[serde(rename_all = "camelCase")]
pub struct HostsExportOpticParams {
host_rankings: HostRankings,
@@ -32,13 +31,14 @@ pub struct HostsExportOpticParams {
(status = 200, description = "Export host rankings as an optic", body = String),
)
)]
+#[tapi::tapi(path = "/hosts/export", method = Post)]
pub async fn hosts_export_optic(
extract::Json(HostsExportOpticParams { host_rankings }): extract::Json,
-) -> Result, StatusCode> {
+) -> Json {
let optic = Optic {
host_rankings,
..Default::default()
};
- Ok(Json(optic.to_string()))
+ Json(optic.to_string())
}
diff --git a/crates/core/src/api/improvement.rs b/crates/core/src/api/improvement.rs
index fb8cd112..e676a6c7 100644
--- a/crates/core/src/api/improvement.rs
+++ b/crates/core/src/api/improvement.rs
@@ -23,7 +23,7 @@ use uuid::Uuid;
use crate::improvement::{ImprovementEvent, StoredQuery};
-use super::State;
+use super::AppState;
#[derive(Deserialize, Debug)]
pub struct ClickParams {
@@ -39,7 +39,7 @@ pub struct StoreParams {
pub async fn click(
extract::Query(params): extract::Query,
- extract::State(state): extract::State>,
+ extract::State(state): extract::State,
) {
if let Some(q) = state.improvement_queue.as_ref() {
q.lock().await.push(ImprovementEvent::Click {
@@ -62,7 +62,7 @@ impl TryFrom for StoredQuery {
}
pub async fn store(
- extract::State(state): extract::State>,
+ extract::State(state): extract::State,
extract::Json(params): extract::Json,
) -> impl IntoResponse {
match state.improvement_queue.as_ref() {
diff --git a/crates/core/src/api/mod.rs b/crates/core/src/api/mod.rs
index 37ba6b10..624f434b 100644
--- a/crates/core/src/api/mod.rs
+++ b/crates/core/src/api/mod.rs
@@ -70,6 +70,7 @@ pub struct Counters {
pub daily_active_users: user_count::UserCount,
}
+pub type AppState = Arc;
pub struct State {
pub config: ApiConfig,
pub searcher: ApiSearcher,
@@ -206,6 +207,32 @@ pub async fn router(config: &ApiConfig, counters: Counters) -> Result {
.with_state(state))
}
+pub fn endpoints() -> tapi::endpoints::Endpoints<'static, AppState> {
+ type E = &'static dyn tapi::endpoints::Endpoint;
+
+ tapi::endpoints::Endpoints::new([
+ &autosuggest::browser::endpoint as E,
+ &autosuggest::route::endpoint as E,
+ &explore::explore_export_optic::endpoint as E,
+ // &favicon::endpoint as E,
+ &hosts::hosts_export_optic::endpoint as E,
+ // &improvement::click::endpoint as E,
+ // &improvement::store::endpoint as E,
+ // &search::entity_image::endpoint as E,
+ &search::search::endpoint as E,
+ &search::sidebar::endpoint as E,
+ &search::spellcheck::endpoint as E,
+ &search::widget::endpoint as E,
+ // &summarize::summarize_route::endpoint as E,
+ &webgraph::host::ingoing_hosts::endpoint as E,
+ &webgraph::host::knows::endpoint as E,
+ &webgraph::host::outgoing_hosts::endpoint as E,
+ &webgraph::host::similar::endpoint as E,
+ &webgraph::page::ingoing_pages::endpoint as E,
+ &webgraph::page::outgoing_pages::endpoint as E,
+ ])
+}
+
/// Enables CORS for development where the API and frontend are on
/// different hosts.
fn cors_layer() -> tower_http::cors::CorsLayer {
@@ -222,7 +249,7 @@ pub fn metrics_router(registry: crate::metrics::PrometheusRegistry) -> Router {
}
async fn search_metric(
- extract::State(state): extract::State>,
+ extract::State(state): extract::State,
extract::ConnectInfo(addr): extract::ConnectInfo,
request: axum::extract::Request,
next: middleware::Next,
diff --git a/crates/core/src/api/search.rs b/crates/core/src/api/search.rs
index 94bf3c20..fa17f1db 100644
--- a/crates/core/src/api/search.rs
+++ b/crates/core/src/api/search.rs
@@ -17,7 +17,6 @@
use crate::config::defaults;
use http::StatusCode;
use optics::{HostRankings, Optic};
-use std::sync::Arc;
use utoipa::ToSchema;
use axum::Json;
@@ -29,11 +28,11 @@ use crate::{
webpage::region::Region,
};
-use super::State;
+use super::AppState;
use axum::{extract, response::IntoResponse};
-#[derive(Debug, serde::Serialize, serde::Deserialize, ToSchema)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, ToSchema, tapi::Tapi)]
#[serde(rename_all = "camelCase")]
#[schema(title = "SearchQuery", example = json!({"query": "hello world"}))]
pub struct ApiSearchQuery {
@@ -81,7 +80,7 @@ impl TryFrom for SearchQuery {
}
}
-#[derive(Debug, serde::Serialize, serde::Deserialize, ToSchema)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, ToSchema, tapi::Tapi)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ApiSearchResult {
Websites(WebsitesResult),
@@ -106,17 +105,24 @@ impl From for ApiSearchResult {
(status = 200, description = "Search results", body = ApiSearchResult),
)
)]
+#[tapi::tapi(path = "/search", method = Post, state = AppState)]
pub async fn search(
- extract::State(state): extract::State>,
+ extract::State(state): extract::State,
extract::Json(query): extract::Json,
-) -> Result {
+) -> tapi::endpoints::OneOf5<
+ extract::Json,
+ extract::Json,
+ String,
+ tapi::endpoints::Statused<400, ()>,
+ tapi::endpoints::Statused<500, ()>,
+> {
tracing::debug!(?query);
let flatten_result = query.flatten_response;
let query = SearchQuery::try_from(query);
if let Err(err) = query {
tracing::error!("{:?}", err);
- return Err(StatusCode::BAD_REQUEST);
+ return tapi::endpoints::OneOf5::D(().into());
}
let mut query = query.unwrap();
@@ -125,27 +131,25 @@ pub async fn search(
match state.searcher.search(&query).await {
Ok(result) => {
if flatten_result {
- Ok(Json(ApiSearchResult::from(result)).into_response())
+ tapi::endpoints::OneOf5::A(Json(ApiSearchResult::from(result)))
} else {
- Ok(Json(result).into_response())
+ tapi::endpoints::OneOf5::B(Json(result))
}
}
Err(err) => match err.downcast_ref() {
Some(searcher::distributed::Error::EmptyQuery) => {
- Ok(searcher::distributed::Error::EmptyQuery
- .to_string()
- .into_response())
+ tapi::endpoints::OneOf5::C(searcher::distributed::Error::EmptyQuery.to_string())
}
_ => {
tracing::error!("{:?}", err);
- Err(StatusCode::INTERNAL_SERVER_ERROR)
+ tapi::endpoints::OneOf5::E(().into())
}
},
}
}
-#[derive(Debug, serde::Serialize, serde::Deserialize, ToSchema)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, ToSchema, tapi::Tapi)]
pub struct WidgetQuery {
pub query: String,
}
@@ -159,14 +163,15 @@ pub struct WidgetQuery {
(status = 200, description = "The resulting widget if one matches the query", body = Option),
)
)]
+#[tapi::tapi(path = "/search/widget", method = Post, state = AppState)]
pub async fn widget(
- extract::State(state): extract::State>,
+ extract::State(state): extract::State,
extract::Json(req): extract::Json,
-) -> impl IntoResponse {
+) -> extract::Json