More BoxedFilters & add openapi endpoint

This commit is contained in:
Xiphoseer
2021-10-23 10:57:32 +02:00
parent 8f91bb21ad
commit e5f24c4468
13 changed files with 436 additions and 29 deletions

View File

@@ -10,12 +10,15 @@ handlebars = "3.5"
pretty_env_logger = "0.4.0"
structopt = "0.3.21"
color-eyre = "0.5.10"
#indexmap = "1"
mapr = "0.8.0"
notify = "5.0.0-pre.13"
openapiv3 = "0.5"
paradox-typed-db = { git = "https://github.com/Xiphoseer/paradox-typed-db.git" }
percent-encoding = "2.1.0"
pin-project = "1.0"
regex = "1.4"
serde_yaml = "0.8"
toml = "0.5"
tracing = "0.1"
@@ -44,3 +47,7 @@ features = ["rt-multi-thread", "macros", "signal"]
[dependencies.serde]
version = "1"
features = ["derive"]
[build-dependencies]
openapiv3 = "0.5"
serde_yaml = "0.8"

10
build.rs Normal file
View File

@@ -0,0 +1,10 @@
use openapiv3::OpenAPI;
use serde_yaml::{from_str, Error};
fn main() {
let text = include_str!("res/api.yaml");
let result: Result<OpenAPI, Error> = from_str(text);
if let Err(e) = result {
panic!("{}", e);
}
}

58
res/api.html Normal file
View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>API | LU-Explorer</title>
<!--<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />-->
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3/swagger-ui.css"></script>
<!--<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />-->
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js" charset="UTF-8"></script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "v0/openapi.json",
dom_id: '#swagger-ui',
deepLinking: true,
tryItOutEnabled: true,
persistAuthorization: true,
withCredentials: true,
presets: [
SwaggerUIBundle.presets.apis,
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "BaseLayout"
});
window.ui = ui;
};
</script>
</body>
</html>

225
res/api.yaml Normal file
View File

@@ -0,0 +1,225 @@
---
openapi: 3.0.3
info:
title: LU-Explorer API
version: "0.1"
tags:
- name: db
description: Queries on database tables
- name: locale
description: Queries the translations
components:
securitySchemes:
basic_auth:
type: http
scheme: basic
schemas:
Behavior:
type: object
properties: {}
Tables:
type: array
items:
type: string
TableDef:
type: object
properties:
name:
type: string
columns:
type: array
items:
type: object
properties:
name:
type: string
data_type:
type: string
LocaleNode:
type: object
properties:
value:
type: string
int_keys:
type: array
items:
type: number
str_keys:
type: array
items:
type: string
ErrorModel:
type: number
paths:
"/v0/tables":
get:
tags:
- db
description: List all database table names
responses:
"200":
description: The tables of the database
content:
application/json:
schema:
$ref: "#/components/schemas/Tables"
default:
description: Unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorModel"
"/v0/tables/{name}/def":
get:
tags:
- db
description: Show the definiton of a database table
responses:
"200":
description: The request was successfull
content:
application/json:
schema:
$ref: "#/components/schemas/TableDef"
parameters:
- in: path
required: true
name: name
schema:
type: string
"/v0/tables/{name}/{key}":
get:
tags:
- db
description: Show data for a key in a table
responses:
"200":
description: The request was successfull
content:
application/json:
schema:
type: array
items:
type: object
parameters:
- in: path
required: true
name: name
schema:
type: string
- in: path
required: true
name: key
schema:
type: string
"/v0/locale/{path}":
get:
tags:
- locale
description: Get a single locale node
responses:
"200":
description: The request was successfull
content:
application/json:
schema:
$ref: "#/components/schemas/LocaleNode"
parameters:
- in: path
required: true
name: path
schema:
type: string
"/v0/locale/{path}/$all":
get:
tags:
- locale
description: Get a locale subtree
responses:
"200":
description: The request was successfull
content:
application/json:
schema:
type: object
properties: {}
parameters:
- in: path
required: true
name: path
schema:
type: string
"/v0/rev":
get:
description: List all supported reverse lookup scopes
responses:
"200":
description: The request was successfull
content:
application/json:
schema:
type: array
items:
type: string
"/v0/rev/component_types":
get:
description: List all component types in the database
responses:
"200":
description: The request was successfull
content:
application/json:
schema: { type: array, items: { type: string } }
"/v0/rev/component_types/{type}":
get:
description: List all component IDs and associated objects for a component type
This is a reverse lookup of the `ComponentsRegistry` table
responses:
"200":
description: The request was successfull
content:
application/json:
schema: { type: array, items: { type: string } }
parameters:
- in: path
required: true
name: type
schema:
type: number
"/v0/rev/component_types/{type}/{id}":
get:
description: List all component IDs and associated objects for a component type
This is a reverse lookup of the `ComponentsRegistry` table
responses:
"200":
description: The request was successfull
content:
application/json:
schema: { type: array, items: { type: string } }
parameters:
- in: path
required: true
name: type
schema:
type: number
- in: path
required: true
name: id
schema:
type: number
"/v0/rev/behaviors/{id}":
get:
description: Get all data for a specific behavior ID
responses:
"200":
description: The request was successfull
content:
application/json:
schema:
$ref: "#/components/schemas/Behavior"
parameters:
- in: path
required: true
name: id
schema:
type: number

49
src/api/docs.rs Normal file
View File

@@ -0,0 +1,49 @@
use openapiv3::{OpenAPI, SecurityRequirement, Server};
use std::{convert::Infallible, future::Future, sync::Arc, task::Poll};
use warp::{
filters::BoxedFilter,
hyper::StatusCode,
reply::{json, with_status, Json, WithStatus},
Filter,
};
use crate::auth::AuthKind;
pub struct OpenApiFuture {
/// The openapi structure
inner: Arc<OpenAPI>,
}
impl Future for OpenApiFuture {
type Output = Result<WithStatus<Json>, Infallible>;
fn poll(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
Poll::Ready(Ok(with_status(json(self.inner.as_ref()), StatusCode::OK)))
}
}
/// Build the openapi endpoint
pub fn openapi(
url: String,
auth_kind: AuthKind,
) -> Result<BoxedFilter<(WithStatus<Json>,)>, serde_yaml::Error> {
let text = include_str!("../../res/api.yaml");
let mut data: OpenAPI = serde_yaml::from_str(text)?;
data.servers.push(Server {
url,
description: Some(String::from("The current server")),
..Default::default()
});
if auth_kind == AuthKind::Basic {
let mut req = SecurityRequirement::new();
req.insert("basic_auth".to_string(), vec![]);
data.security = Some(vec![req]);
}
let arc = Arc::new(data);
Ok(warp::path("openapi.json")
.and_then(move || OpenApiFuture { inner: arc.clone() })
.boxed())
}

View File

@@ -10,11 +10,14 @@ use assembly_data::{fdb::mem::Database, xml::localization::LocaleNode};
use paradox_typed_db::TypedDatabase;
use percent_encoding::percent_decode_str;
use warp::{
filters::BoxedFilter,
path::Tail,
reply::{Json, WithStatus},
Filter, Reply,
};
use crate::auth::AuthKind;
use self::{
adapter::{LocaleAll, LocalePod},
rev::{make_api_rev, ReverseLookup},
@@ -22,6 +25,7 @@ use self::{
};
pub mod adapter;
mod docs;
pub mod rev;
pub mod tables;
@@ -145,12 +149,14 @@ pub fn locale_api(lr: Arc<LocaleNode>) -> impl Fn(Tail) -> Option<warp::reply::J
}
}
pub(crate) fn make_api<'a>(
db: Database<'a>,
pub(crate) fn make_api(
url: String,
auth_kind: AuthKind,
db: Database<'static>,
tydb: &'static TypedDatabase<'static>,
rev: &'static ReverseLookup,
lr: Arc<LocaleNode>,
) -> impl Filter<Extract = (WithStatus<Json>,), Error = Infallible> + Clone + 'a {
) -> BoxedFilter<(WithStatus<Json>,)> {
let v0_base = warp::path("v0");
let v0_tables = warp::path("tables").and(make_api_tables(db));
let v0_locale = warp::path("locale")
@@ -159,7 +165,16 @@ pub(crate) fn make_api<'a>(
.map(map_opt);
let v0_rev = warp::path("rev").and(make_api_rev(tydb, rev));
let v0 = v0_base.and(v0_tables.or(v0_locale).unify().or(v0_rev).unify());
let v0_openapi = docs::openapi(url, auth_kind).unwrap();
let v0 = v0_base.and(
v0_tables
.or(v0_locale)
.unify()
.or(v0_rev)
.unify()
.or(v0_openapi)
.unify(),
);
// v1
let dbf = db_filter(db);
@@ -174,5 +189,5 @@ pub(crate) fn make_api<'a>(
// catch all
let catch_all = make_api_catch_all();
v0.or(v1).unify().or(catch_all).unify()
v0.or(v1).unify().or(catch_all).unify().boxed()
}

View File

@@ -35,7 +35,13 @@ pub struct Api<T, E> {
}
fn rev_api(_db: &TypedDatabase, _rev: Rev) -> Result<Json, CastError> {
Ok(warp::reply::json(&["skill_ids"]))
Ok(warp::reply::json(&[
"behaviors",
"component_types",
"mission_types",
"object_types",
"skill_ids",
]))
}
#[derive(Debug, Copy, Clone)]

View File

@@ -9,8 +9,9 @@ use assembly_data::fdb::{
use linked_hash_map::LinkedHashMap;
use serde::{ser::SerializeSeq, Serialize};
use warp::{
filters::BoxedFilter,
reply::{Json, WithStatus},
Filter, Rejection,
Filter,
};
use super::{db_filter, map_opt_res, map_res};
@@ -197,9 +198,7 @@ fn table_key_api(db: Database<'_>, name: String, key: String) -> Result<Option<J
Ok(Some(warp::reply::json(&RowIter { cols, to_rows })))
}
pub(super) fn make_api_tables(
db: Database<'_>,
) -> impl Filter<Extract = (WithStatus<Json>,), Error = Rejection> + Clone + Send + '_
pub(super) fn make_api_tables(db: Database<'static>) -> BoxedFilter<(WithStatus<Json>,)>
//where
//H: Filter<Extract = (ArcHandle<B, FDBHeader>,), Error = Infallible> + Clone + Send,
{
@@ -239,4 +238,5 @@ pub(super) fn make_api_tables(
.unify()
.or(table_get)
.unify()
.boxed()
}

View File

@@ -12,6 +12,14 @@ use warp::{
use crate::config::AuthConfig;
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum AuthKind {
/// No authentication
None,
/// HTTP Basic Auth
Basic,
}
#[derive(Clone)]
pub enum AuthImpl {
None,

View File

@@ -18,6 +18,7 @@ fn default_lu_json_cache() -> PathBuf {
#[derive(Deserialize)]
pub struct CorsOptions {
pub all: bool,
#[serde(default)]
pub domains: Vec<String>,
}

View File

@@ -1,11 +1,9 @@
use std::path::PathBuf;
use tracing::info;
use warp::{fs::File, Filter, Rejection};
use warp::{filters::BoxedFilter, fs::File, Filter};
pub fn make_fallback(
lu_json_path: PathBuf,
) -> impl Filter<Extract = (File,), Error = Rejection> + Clone {
pub fn make_fallback(lu_json_path: PathBuf) -> BoxedFilter<(File,)> {
let maps_dir = lu_json_path.join("maps");
info!("Maps on '{}'", maps_dir.display());
let maps = warp::path("maps").and(warp::fs::dir(maps_dir));

View File

@@ -1,5 +1,6 @@
use std::{
fs::File,
str::FromStr,
sync::{Arc, RwLock},
};
@@ -14,7 +15,7 @@ use paradox_typed_db::TypedDatabase;
use structopt::StructOpt;
use template::make_spa_dynamic;
use tokio::runtime::Handle;
use warp::{filters::BoxedFilter, Filter};
use warp::{filters::BoxedFilter, hyper::Uri, path::FullPath, Filter, Reply};
mod api;
mod auth;
@@ -26,6 +27,8 @@ mod template;
use crate::{
api::rev::ReverseLookup,
auth::AuthKind,
config::AuthConfig,
fallback::make_fallback,
redirect::{add_host_filters, add_redirect_filters, base_filter},
template::{load_meta_template, FsEventHandler, TemplateUpdateTask},
@@ -71,13 +74,17 @@ async fn main() -> color_eyre::Result<()> {
};
let cfg_g = &cfg.general;
let lu_res = cfg.data.lu_res_prefix.clone().unwrap_or_else(|| {
if let Some(b) = cfg_g.base.as_deref() {
format!("{}://{}/{}/lu-res", scheme, &cfg_g.domain, b)
} else {
format!("{}://{}/lu-res", scheme, &cfg_g.domain)
}
});
let canonical_base_url = if let Some(b) = cfg_g.base.as_deref() {
format!("{}://{}/{}", scheme, &cfg_g.domain, b)
} else {
format!("{}://{}", scheme, &cfg_g.domain)
};
let lu_res = cfg
.data
.lu_res_prefix
.clone()
.unwrap_or_else(|| format!("{}/lu-res", canonical_base_url));
let lu_res_prefix = Box::leak(lu_res.clone().into_boxed_str());
let tydb = TypedDatabase::new(lr.clone(), lu_res_prefix, tables)?;
@@ -88,8 +95,28 @@ async fn main() -> color_eyre::Result<()> {
let lu_json_path = cfg.data.lu_json_cache.clone();
let fallback_routes = make_fallback(lu_json_path);
let api_routes = make_api(db, data, rev, lr.clone());
let api = warp::path("api").and(fallback_routes.or(api_routes));
let auth_kind = if matches!(cfg.auth, Some(AuthConfig { basic: Some(_) })) {
AuthKind::Basic
} else {
AuthKind::None
};
let api_url = format!("{}/api/", canonical_base_url);
let api_uri = Uri::from_str(&api_url).unwrap();
let api_file = include_str!("../res/api.html");
let api_swagger = warp::path::end()
.and(warp::path::full())
.map(move |path: FullPath| {
if path.as_str().ends_with('/') {
warp::reply::html(api_file).into_response()
} else {
warp::redirect(api_uri.clone()).into_response()
}
})
.boxed();
let api_routes = make_api(api_url, auth_kind, db, data, rev, lr.clone());
let api = warp::path("api").and(fallback_routes.or(api_swagger).or(api_routes));
let spa_path = &cfg.data.explorer_spa;
let spa_index = spa_path.join("index.html");
@@ -159,7 +186,9 @@ async fn main() -> color_eyre::Result<()> {
cors = cors.allow_origin(key.as_ref());
}
}
cors = cors.allow_methods(vec!["GET"]);
cors = cors
.allow_methods(vec!["OPTIONS", "GET"])
.allow_headers(vec!["authorization"]);
let to_serve = routes.with(cors);
let server = warp::serve(to_serve);

View File

@@ -16,7 +16,7 @@ use regex::{Captures, Regex};
use serde::Serialize;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error, info};
use warp::{path::FullPath, Filter};
use warp::{filters::BoxedFilter, path::FullPath, Filter};
fn make_meta_template(text: &str) -> Cow<str> {
let re = Regex::new("<meta\\s+(name|property)=\"(.*?)\"\\s+content=\"(.*)\"\\s*/?>").unwrap();
@@ -352,12 +352,12 @@ fn meta<'r>(
}
#[allow(clippy::needless_lifetimes)] // false positive?
pub(crate) fn make_spa_dynamic<'r>(
pub(crate) fn make_spa_dynamic(
data: &'static TypedDatabase<'static>,
hb: Arc<RwLock<Handlebars<'r>>>,
hb: Arc<RwLock<Handlebars<'static>>>,
domain: &str,
// hnd: ArcHandle<B, FDBHeader>,
) -> impl Filter<Extract = (impl warp::Reply,), Error = Infallible> + Clone + 'r {
) -> BoxedFilter<(impl warp::Reply,)> {
let dom = {
let d = Box::leak(domain.to_string().into_boxed_str()) as &str;
warp::any().map(move || d)
@@ -393,4 +393,5 @@ pub(crate) fn make_spa_dynamic<'r>(
},
)
.map(handlebars)
.boxed()
}