Skip to content
This repository was archived by the owner on Feb 20, 2024. It is now read-only.

Commit 70fc9cf

Browse files
authored
Merge pull request #26 from Virtual-Finland-Development/draft/openapi-router
feat/openapi router
2 parents 554c5dc + 1016bb0 commit 70fc9cf

38 files changed

Lines changed: 563 additions & 376 deletions

File tree

Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,29 @@ name = "testbed_api"
33
version = "1.0.0"
44
edition = "2021"
55

6+
[workspace]
7+
8+
[workspace.dependencies]
9+
serde_json = "^1.0.87"
10+
serde = { version = "^1.0.147", features = ["derive"] }
11+
log = "^0.4.17"
12+
http = "^0.2.8"
13+
reqwest = { version = "^0.11.12", features = ["json"] }
14+
simple_logger = "^4.0.0"
15+
lambda_http = "^0.7.1"
16+
futures = "^0.3.18"
17+
itertools = "^0.10.5"
18+
libmath = "^0.2.1"
19+
stopwatch = "^0.0.7"
20+
utoipa = "3"
21+
622
[dependencies]
723
hot-lib-reloader = { version = "^0.6", optional = true }
824
tokio = { version = "^1.21.2", features = ["full"] }
925
dotenv = "^0.15.0"
26+
log = { workspace = true }
27+
simple_logger = { workspace = true }
28+
lambda_http = { workspace = true }
1029
api_app = { path = "./src/lib/api_app" }
1130
http_server = { path = "./src/lib/http_server" }
1231
lambda_service = { path = "./src/lib/lambda_service" }

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,7 @@ make run-sam
8383
### Docker
8484

8585
- https://hub.docker.com/_/rust/
86+
87+
### OpenAPI
88+
89+
- Automatic generation of OpenAPI spec: https://github.com/juhaku/utoipa

openapi/index.html

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1" />
77
<meta name="description" content="SwaggerUI" />
88
<title>SwaggerUI</title>
9-
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui.css" />
9+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
1010
</head>
1111

1212
<body>
1313
<div id="swagger-ui"></div>
14-
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js" crossorigin></script>
14+
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-bundle.js" crossorigin></script>
1515
<script>
1616
// @see: https://stackoverflow.com/a/68871496
1717
// Examples map
@@ -46,7 +46,7 @@
4646
}
4747

4848
// Custom plugin for the logic that happens before the response element is created
49-
const CustomPlugin = () => {
49+
const ExtrenalSchemaPopulatePlugin = () => {
5050
return {
5151
wrapComponents: {
5252
RequestBody: (Original, { React, oas3Actions, oas3Selectors }) => (props) => {
@@ -155,7 +155,13 @@
155155
plugins: [
156156
SwaggerUIBundle.plugins.DownloadUrl,
157157
// Add custom plugin
158-
CustomPlugin
158+
ExtrenalSchemaPopulatePlugin
159+
],
160+
layout: "BaseLayout",
161+
deepLinking: true,
162+
presets: [
163+
SwaggerUIBundle.presets.apis,
164+
SwaggerUIBundle.SwaggerUIStandalonePreset
159165
],
160166
});
161167
};

src/lib/api_app/Cargo.toml

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@ edition = "2021"
77
crate-type = ["rlib", "dylib"]
88

99
[dependencies]
10-
serde_json = "^1.0.87"
11-
serde = { version = "^1.0.147", features = ["derive"] }
12-
log = "^0.4.17"
13-
http = "^0.2.8"
14-
reqwest = { version = "^0.11.12", features = ["json"] }
15-
simple_logger = "^4.0.0"
16-
lambda_http = "^0.7.1"
17-
futures = "^0.3.18"
18-
itertools = "^0.10.5"
19-
libmath = "^0.2.1"
20-
stopwatch = "^0.0.7"
21-
utoipa = "3"
10+
serde_json = { workspace = true }
11+
serde = { workspace = true }
12+
log = { workspace = true }
13+
http = { workspace = true }
14+
reqwest = { workspace = true }
15+
simple_logger = { workspace = true }
16+
lambda_http = { workspace = true }
17+
futures = { workspace = true }
18+
itertools = { workspace = true }
19+
libmath = { workspace = true }
20+
stopwatch = { workspace = true }
21+
utoipa = { workspace = true }
22+
23+
app = { path = "./src/lib/app" }
24+
utils = { path = "./src/lib/utils" }

src/lib/api_app/src/api/mod.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,29 @@ use http::Response;
22
use lambda_http::Request;
33
use log;
44

5-
use self::{responses::APIRoutingResponse, routes::get_router_response, utils::ParsedRequest};
5+
use self::routes::get_router_response;
6+
use app::{
7+
responses::APIRoutingResponse,
8+
router::{parse_router_request, ParsedRequest},
9+
};
10+
use utils::strings;
611

7-
mod requests;
8-
mod responses;
912
pub mod routes;
1013

11-
pub mod utils;
12-
1314
/**
1415
* The handler function for the lambda.
1516
*/
1617
pub async fn handler(
1718
request: Request,
1819
) -> Result<lambda_http::Response<String>, std::convert::Infallible> {
19-
let parsed_request = utils::parse_router_request(request);
20+
let parsed_request = parse_router_request(request);
2021

2122
log::info!("{} {}", parsed_request.method, parsed_request.path);
2223
let router_response = exec_router_request(parsed_request).await;
2324
log::debug!(
2425
"Response: {:#?},\nBody: {:#?},\nHeaders: {:#?}",
2526
router_response.status_code,
26-
utils::strings::truncate_too_long_string(router_response.body.to_string(), 5000, "..."),
27+
strings::truncate_too_long_string(router_response.body.to_string(), 5000, "..."),
2728
router_response.headers
2829
);
2930

src/lib/api_app/src/api/routes/application.rs

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ use http::{HeaderMap, HeaderValue, Method, StatusCode};
22
use reqwest::Response;
33
use serde_json::{json, Value as JsonValue};
44
use std::{env, fs};
5+
use utoipa::openapi::OpenApi;
56

6-
use crate::api::{
7+
use app::{
78
requests::engage_many_plain_requests,
8-
responses::{resolve_external_service_bad_response, APIRoutingError, APIRoutingResponse},
9-
utils::{get_cors_response_headers, get_default_headers, get_plain_headers, ParsedRequest},
9+
responses::{resolve_external_service_bad_response, APIResponse, APIRoutingResponse},
10+
router::ParsedRequest,
1011
};
12+
use utils::api::{get_cors_response_headers, get_default_headers, get_plain_headers};
1113

12-
pub async fn cors_preflight_response(
13-
_request: ParsedRequest,
14-
) -> Result<APIRoutingResponse, APIRoutingError> {
14+
pub async fn cors_preflight_response(_request: ParsedRequest) -> APIResponse {
1515
Ok(APIRoutingResponse::new(
1616
StatusCode::OK,
1717
"",
@@ -20,7 +20,7 @@ pub async fn cors_preflight_response(
2020
}
2121

2222
#[utoipa::path(get, path = "/", responses((status = 303, description = "Redirect to /docs")))]
23-
pub async fn index(_request: ParsedRequest) -> Result<APIRoutingResponse, APIRoutingError> {
23+
pub async fn index(_request: ParsedRequest) -> APIResponse {
2424
Ok(APIRoutingResponse::new(
2525
StatusCode::TEMPORARY_REDIRECT,
2626
"Redirecting to /docs",
@@ -37,7 +37,7 @@ pub async fn index(_request: ParsedRequest) -> Result<APIRoutingResponse, APIRou
3737
path = "/docs",
3838
responses((status = 200, description = "API documentation", content_type = "text/html"))
3939
)]
40-
pub async fn docs(_request: ParsedRequest) -> Result<APIRoutingResponse, APIRoutingError> {
40+
pub async fn docs(_request: ParsedRequest) -> APIResponse {
4141
let body =
4242
fs::read_to_string("./openapi/index.html").expect("Unable to read index.html file");
4343
Ok(APIRoutingResponse::new(StatusCode::OK, body.as_ref(), {
@@ -47,10 +47,20 @@ pub async fn docs(_request: ParsedRequest) -> Result<APIRoutingResponse, APIRout
4747
}))
4848
}
4949

50-
pub async fn openapi_spec(json_spec: String) -> Result<APIRoutingResponse, APIRoutingError> {
50+
pub async fn openapi_spec(openapi: OpenApi) -> APIResponse {
51+
let json_spec_orig_serialized = openapi.to_json().expect("Failed to parse openapi spec");
52+
53+
// Inject the app specific securitySchemas to the spec as a workaround for utoipa::OpenApi zeroing the parse_meta output of derived OpenApi structs when using the security attribute
54+
let mut json_spec: JsonValue =
55+
serde_json::from_str(&json_spec_orig_serialized).expect("Failed to parse openapi spec");
56+
json_spec["components"]["securitySchemes"] = json!({
57+
"BearerAuth": { "scheme": "bearer", "type": "http" }
58+
});
59+
let json_spec_serialized = json_spec.to_string();
60+
5161
Ok(APIRoutingResponse::new(
5262
StatusCode::OK,
53-
json_spec.as_ref(),
63+
json_spec_serialized.as_ref(),
5464
{
5565
let mut headers = HeaderMap::new();
5666
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
@@ -70,17 +80,15 @@ pub async fn openapi_spec(json_spec: String) -> Result<APIRoutingResponse, APIRo
7080
example = json!("OK"),
7181
))
7282
)]
73-
pub async fn health_check(
74-
_request: ParsedRequest,
75-
) -> Result<APIRoutingResponse, APIRoutingError> {
83+
pub async fn health_check(_request: ParsedRequest) -> APIResponse {
7684
Ok(APIRoutingResponse::new(
7785
StatusCode::OK,
7886
"OK",
7987
get_plain_headers(),
8088
))
8189
}
8290

83-
pub async fn not_found(_request: ParsedRequest) -> Result<APIRoutingResponse, APIRoutingError> {
91+
pub async fn not_found(_request: ParsedRequest) -> APIResponse {
8492
Ok(APIRoutingResponse {
8593
status_code: StatusCode::NOT_FOUND,
8694
body: json!({
@@ -91,9 +99,7 @@ pub async fn not_found(_request: ParsedRequest) -> Result<APIRoutingResponse, AP
9199
})
92100
}
93101

94-
pub async fn get_external_service_bad_response(
95-
response: Response,
96-
) -> Result<APIRoutingResponse, APIRoutingError> {
102+
pub async fn get_external_service_bad_response(response: Response) -> APIResponse {
97103
let status_code = response.status();
98104
let response_body = response.text().await?;
99105
resolve_external_service_bad_response(status_code, response_body)
@@ -115,9 +121,7 @@ pub async fn get_external_service_bad_response(
115121
}),
116122
))
117123
)]
118-
pub async fn wake_up_external_services(
119-
_request: ParsedRequest,
120-
) -> Result<APIRoutingResponse, APIRoutingError> {
124+
pub async fn wake_up_external_services(_request: ParsedRequest) -> APIResponse {
121125
let endpoints = vec![
122126
format!(
123127
"{}/health",

src/lib/api_app/src/api/routes/jmf/mod.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
use std::env;
22

3-
use crate::api::{
4-
requests::post_json_request,
5-
responses::{APIRoutingError, APIRoutingResponse},
6-
utils::{get_default_headers, ParsedRequest},
7-
};
3+
use app::{requests::post_json_request, responses::APIResponse, router::ParsedRequest};
4+
use utils::api::get_default_headers;
85

96
pub mod models;
107
use models::{RecommendationsRequest, RecommendationsResponse};
118

129
#[utoipa::path(
1310
post,
14-
path = "/testbed/productizers/user-profile",
11+
path = "/jmf/recommendations",
1512
request_body(
1613
content = RecommendationsRequest,
1714
description = "Job Market Finland recommended skills and occupations"
@@ -22,9 +19,7 @@ use models::{RecommendationsRequest, RecommendationsResponse};
2219
description = "The recommendations response",
2320
))
2421
)]
25-
pub async fn fetch_jmf_recommendations(
26-
request: ParsedRequest,
27-
) -> Result<APIRoutingResponse, APIRoutingError> {
22+
pub async fn fetch_jmf_recommendations(request: ParsedRequest) -> APIResponse {
2823
let endpoint_url = env::var("JMF_SKILL_RECOMMENDATIONS_ENDPOINT")
2924
.expect("JMF_SKILL_RECOMMENDATIONS_ENDPOINT must be set");
3025
let request_input: RecommendationsRequest = serde_json::from_str(request.body.as_str())?;
Lines changed: 12 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
use super::{
2-
responses::{APIRoutingError, APIRoutingResponse},
3-
utils::ParsedRequest,
1+
use app::{
2+
responses::APIResponse,
3+
router::{OpenApiRouter, ParsedRequest},
44
};
5-
mod openapi_helpers;
6-
use openapi_helpers::get_openapi_operation_id;
5+
use futures::{future::BoxFuture, FutureExt};
76
use utoipa::OpenApi;
87

98
pub mod application;
109
pub mod jmf;
1110
pub mod testbed;
1211

13-
#[derive(OpenApi)]
12+
#[derive(OpenApi, OpenApiRouter)]
1413
#[openapi(
1514
info(
1615
title = "Testbed API",
@@ -53,52 +52,20 @@ pub mod testbed;
5352
jmf::models::Skill,
5453
))
5554
)]
56-
struct ApiDoc;
55+
struct Api;
5756

5857
/**
59-
* API router - // @TODO: would be nice to auto-generate routes from openapi spec
58+
* API router
6059
*/
61-
pub async fn get_router_response(
62-
parsed_request: ParsedRequest,
63-
) -> Result<APIRoutingResponse, APIRoutingError> {
64-
let openapi = ApiDoc::openapi(); // @TODO: ensure as singelton
60+
pub async fn get_router_response(parsed_request: ParsedRequest) -> APIResponse {
61+
let openapi = Api::openapi(); // @TODO: ensure as singelton
62+
let router = Api;
6563

6664
match (parsed_request.method.as_str(), parsed_request.path.as_str()) {
6765
// System routes
6866
("OPTIONS", _) => application::cors_preflight_response(parsed_request).await,
69-
("GET", "/openapi.json") => {
70-
application::openapi_spec(
71-
ApiDoc::openapi()
72-
.to_json()
73-
.expect("Failed to parse openapi spec"),
74-
)
75-
.await
76-
}
67+
("GET", "/openapi.json") => application::openapi_spec(openapi).await,
7768
// OpenAPI specified routes
78-
_ => {
79-
let operation_id = get_openapi_operation_id(
80-
openapi,
81-
parsed_request.method.as_str(),
82-
parsed_request.path.as_str(),
83-
);
84-
match operation_id.as_str() { // @TODO: would be nice to auto-generate this match
85-
"index" => application::index(parsed_request).await,
86-
"docs" => application::docs(parsed_request).await,
87-
"health_check" => application::health_check(parsed_request).await,
88-
"wake_up_external_services" => application::wake_up_external_services(parsed_request).await,
89-
"engage_reverse_proxy_request" => testbed::engage_reverse_proxy_request(parsed_request).await,
90-
"get_population" => testbed::productizers::figure::get_population(parsed_request).await,
91-
"find_job_postings" => testbed::productizers::job::find_job_postings(parsed_request).await,
92-
"fetch_user_profile" => testbed::productizers::user::fetch_user_profile(parsed_request).await,
93-
"fetch_user_status_info" => testbed::productizers::user::fetch_user_status_info(parsed_request).await,
94-
"update_user_status_info" => testbed::productizers::user::update_user_status_info(parsed_request).await,
95-
"fetch_jmf_recommendations" => jmf::fetch_jmf_recommendations(parsed_request).await,
96-
"get_basic_information" => testbed::productizers::person::basic_information::get_basic_information(parsed_request).await,
97-
"write_basic_information" => testbed::productizers::person::basic_information::write_basic_information(parsed_request).await,
98-
"get_job_applicant_profile" => testbed::productizers::person::job_applicant_profile::get_job_applicant_profile(parsed_request).await,
99-
"write_job_applicant_profile" => testbed::productizers::person::job_applicant_profile::write_job_applicant_profile(parsed_request).await,
100-
_ => application::not_found(parsed_request).await, // Catch all 404
101-
}
102-
}
69+
_ => router.handle(openapi, parsed_request).await,
10370
}
10471
}

0 commit comments

Comments
 (0)