Skip to content

Commit

Permalink
[server] Add openapi endpoint #769 (#793)
Browse files Browse the repository at this point in the history
* Update .gitignore

* exclude api.rs from coverage

* add openapi
  • Loading branch information
michaelvlach authored Nov 23, 2023
1 parent bfe53f4 commit add9133
Show file tree
Hide file tree
Showing 13 changed files with 168 additions and 28 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v3
- uses: taiki-e/install-action@cargo-llvm-cov
- run: rustup component add llvm-tools-preview
- run: cargo llvm-cov --package agdb --package agdb_server --ignore-filename-regex "agdb(_derive|_benchmarks)" --lcov --output-path lcov.info
- run: cargo llvm-cov --package agdb --package agdb_server --ignore-filename-regex "agdb(_derive|_benchmarks)|api.rs" --lcov --output-path lcov.info
- uses: codecov/codecov-action@v3
with:
files: lcov.info
2 changes: 1 addition & 1 deletion .github/workflows/pr_rust.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- uses: actions/checkout@v3
- uses: taiki-e/install-action@cargo-llvm-cov
- run: rustup component add llvm-tools-preview
- run: cargo llvm-cov --package agdb --package agdb_server --ignore-filename-regex "agdb(_derive|_benchmarks)" --fail-uncovered-functions 0 --fail-uncovered-lines 0 --show-missing-lines
- run: cargo llvm-cov --package agdb --package agdb_server --ignore-filename-regex "agdb(_derive|_benchmarks)|api.rs" --fail-uncovered-functions 0 --fail-uncovered-lines 0 --show-missing-lines

format:
runs-on: ubuntu-latest
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ db.agdb
.db.agdb

lcov.info
agdb_benchmarks.yaml
agdb_benchmarks.yaml
.agdb_server.agdb
agdb_server.agdb
agdb_server.yaml
4 changes: 2 additions & 2 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@
"label": "Coverage Server",
"type": "cargo",
"command": "llvm-cov",
"args": ["--package", "agdb_server", "--ignore-filename-regex", "agdb(\\src|\\\\src|/src|_derive|_benchmarks)", "--show-missing-lines"],
"args": ["--package", "agdb_server", "--ignore-filename-regex", "agdb(\\src|\\\\src|/src|_derive|_benchmarks)|api.rs", "--show-missing-lines"],
"problemMatcher": ["$rustc"],
"group": "build"
},
{
"label": "Coverage Server HTML",
"type": "cargo",
"command": "llvm-cov",
"args": ["--package", "agdb_server", "--ignore-filename-regex", "agdb(\\src|\\\\src|/src|_derive|_benchmarks)", "--html", "--open"],
"args": ["--package", "agdb_server", "--ignore-filename-regex", "agdb(\\src|\\\\src|/src|_derive|_benchmarks)|api.rs", "--html", "--open"],
"problemMatcher": ["$rustc"],
"group": "build"
},
Expand Down
2 changes: 2 additions & 0 deletions agdb_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ tower = "0.4.13"
tower-http = { version = "0.4.4", features = ["map-request-body"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
utoipa = "4.1.0"
utoipa-swagger-ui = { version = "4.0.0", features = ["axum"] }

[dev-dependencies]
assert_cmd = "2.0.12"
Expand Down
71 changes: 71 additions & 0 deletions agdb_server/openapi/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"openapi": "3.0.3",
"info": {
"title": "agdb_server",
"description": "",
"license": {
"name": "Apache-2.0"
},
"version": "0.1.0"
},
"paths": {
"/create_db": {
"get": {
"tags": [
"crate::app"
],
"operationId": "create_db",
"parameters": [
{
"name": "name",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "db_type",
"in": "path",
"required": true,
"schema": {
"$ref": "#/components/schemas/CreateDbType"
}
}
],
"responses": {
"201": {
"description": "Created"
}
}
}
}
},
"components": {
"schemas": {
"CreateDb": {
"type": "object",
"required": [
"name",
"db_type"
],
"properties": {
"db_type": {
"$ref": "#/components/schemas/CreateDbType"
},
"name": {
"type": "string"
}
}
},
"CreateDbType": {
"type": "string",
"enum": [
"file",
"memory",
"memory_mapped"
]
}
}
}
}
8 changes: 8 additions & 0 deletions agdb_server/src/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(
paths(crate::app::create_db),
components(schemas(crate::app::CreateDb, crate::app::CreateDbType))
)]
pub(crate) struct Api;
37 changes: 32 additions & 5 deletions agdb_server/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::api::Api;
use crate::db::DbPool;
use crate::logger;
use axum::body;
Expand All @@ -12,17 +13,21 @@ use serde::Deserialize;
use tokio::sync::broadcast::Sender;
use tower::ServiceBuilder;
use tower_http::map_request_body::MapRequestBodyLayer;
use utoipa::IntoParams;
use utoipa::OpenApi;
use utoipa::ToSchema;
use utoipa_swagger_ui::SwaggerUi;

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
enum CreateDbType {
pub(crate) enum CreateDbType {
File,
Memory,
MemoryMapped,
}

#[derive(Deserialize)]
struct CreateDb {
#[derive(Deserialize, IntoParams, ToSchema)]
pub(crate) struct CreateDb {
name: String,
db_type: CreateDbType,
}
Expand Down Expand Up @@ -56,6 +61,7 @@ pub(crate) fn app(shutdown_sender: Sender<()>, db_pool: DbPool) -> Router {
};

Router::new()
.merge(SwaggerUi::new("/openapi").url("/openapi/openapi.json", Api::openapi()))
.route("/", routing::get(root))
.route("/error", routing::get(error))
.route("/shutdown", routing::get(shutdown))
Expand All @@ -64,7 +70,19 @@ pub(crate) fn app(shutdown_sender: Sender<()>, db_pool: DbPool) -> Router {
.with_state(state)
}

async fn create_db(State(_state): State<DbPool>, Query(request): Query<CreateDb>) -> StatusCode {
#[utoipa::path(get,
path = "/create_db",
responses(
(status = 201, description = "Created"),
),
params(
CreateDb,
)
)]
pub(crate) async fn create_db(
State(_state): State<DbPool>,
Query(request): Query<CreateDb>,
) -> StatusCode {
println!("Creating db '{}' ({:?})", request.name, request.db_type);
StatusCode::CREATED
}
Expand Down Expand Up @@ -95,6 +113,8 @@ mod tests {
use axum::http::Request;
use axum::http::StatusCode;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::sync::Arc;
use std::sync::RwLock;
use tower::ServiceExt;
Expand Down Expand Up @@ -143,4 +163,11 @@ mod tests {
assert_eq!(response.status(), StatusCode::CREATED);
Ok(())
}

#[test]
fn generate_openapi_schema() {
let schema = Api::openapi().to_pretty_json().unwrap();
let mut file = File::create("openapi/schema.json").unwrap();
file.write_all(schema.as_bytes()).unwrap();
}
}
2 changes: 1 addition & 1 deletion agdb_server/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;

const SERVER_DB_NAME: &str = "agdb_studio.agdb";
const SERVER_DB_NAME: &str = "agdb_server.agdb";

#[allow(dead_code)]
pub(crate) enum DbType {
Expand Down
42 changes: 26 additions & 16 deletions agdb_server/src/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ pub(crate) async fn logger(
next: Next<BoxBody>,
) -> Result<impl IntoResponse, Response> {
let mut log_record = LogRecord::default();
let request = request_log(request, &mut log_record).await?;
let skip_body = request.uri().path().starts_with("/openapi");
let request = request_log(request, &mut log_record, skip_body).await?;
let now = Instant::now();
let response = next.run(request).await;
log_record.time = now.elapsed().as_micros();
let response = response_log(response, &mut log_record).await;
let response = response_log(response, &mut log_record, skip_body).await;
let message = serde_json::to_string(&log_record).unwrap_or_default();

match log_record.status {
Expand All @@ -47,6 +48,7 @@ pub(crate) async fn logger(
async fn request_log(
request: Request<BoxBody>,
log_record: &mut LogRecord,
skip_body: bool,
) -> Result<Request<BoxBody>, Response> {
log_record.method = request.method().to_string();
log_record.uri = request.uri().to_string();
Expand All @@ -57,20 +59,24 @@ async fn request_log(
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect();

let (parts, body) = request.into_parts();
let response = hyper::body::to_bytes(body).await.map_err(map_error)?;
if !skip_body {
let (parts, body) = request.into_parts();
let bytes = hyper::body::to_bytes(body).await.map_err(map_error)?;
log_record.request_body = String::from_utf8_lossy(&bytes).to_string();

log_record.request_body = String::from_utf8_lossy(&response).to_string();
return Ok(Request::from_parts(
parts,
axum::body::boxed(Full::from(bytes)),
));
}

Ok(Request::from_parts(
parts,
axum::body::boxed(Full::from(response)),
))
Ok(request)
}

async fn response_log(
response: Response<BoxBody>,
log_record: &mut LogRecord,
skip_body: bool,
) -> Result<impl IntoResponse, Response> {
log_record.status = response.status().as_u16();
log_record.response_headers = response
Expand All @@ -79,14 +85,18 @@ async fn response_log(
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect();

let (parts, body) = response.into_parts();
let resposne = hyper::body::to_bytes(body).await.map_err(map_error)?;
log_record.response = String::from_utf8_lossy(&resposne).to_string();
if !skip_body {
let (parts, body) = response.into_parts();
let resposne = hyper::body::to_bytes(body).await.map_err(map_error)?;
log_record.response = String::from_utf8_lossy(&resposne).to_string();

return Ok(Response::from_parts(
parts,
axum::body::boxed(Full::from(resposne)),
));
}

Ok(Response::from_parts(
parts,
axum::body::boxed(Full::from(resposne)),
))
Ok(response)
}

fn map_error(error: AxumError) -> Response {
Expand Down
1 change: 1 addition & 0 deletions agdb_server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod api;
mod app;
mod config;
mod db;
Expand Down
18 changes: 18 additions & 0 deletions agdb_server/tests/server_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,21 @@ async fn config() -> anyhow::Result<()> {
assert!(server.wait()?.success());
Ok(())
}

#[tokio::test]
async fn openapi() -> anyhow::Result<()> {
let test_config = TestConfig::new_content("port: 5000");
let mut server = Command::cargo_bin("agdb_server")?
.current_dir(&test_config.dir)
.spawn()?;
assert!(reqwest::get("http://127.0.0.1:5000/openapi")
.await?
.status()
.is_success());
assert!(reqwest::get("http://127.0.0.1:5000/shutdown")
.await?
.status()
.is_success());
assert!(server.wait()?.success());
Ok(())
}
2 changes: 1 addition & 1 deletion agdb_server/tests/test_config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ pub struct TestConfig {
const CONFIG_FILE: &str = "agdb_server.yaml";

impl TestConfig {
#[allow(dead_code)]
#[track_caller]
pub fn new() -> Self {
let caller = Location::caller();
Expand All @@ -28,6 +27,7 @@ impl TestConfig {
Self { dir }
}

#[track_caller]
pub fn new_content(content: &str) -> Self {
let test_config = Self::new();

Expand Down

0 comments on commit add9133

Please sign in to comment.