1use lenso_bootstrap::CompositionProfile;
10use platform_core::AppContext;
11use platform_http::{ApiOpenApiRouter, OpenApiRouter, base_router};
12use utoipa::OpenApi;
13use utoipa::openapi::RefOr;
14use utoipa::openapi::content::Content;
15use utoipa::openapi::path::Operation;
16use utoipa::openapi::response::Response;
17
18#[derive(OpenApi)]
24#[openapi(
25 info(
26 title = "Lenso API",
27 version = "1.0.0",
28 description = "Rust-first modular monolith API contract"
29 ),
30 tags((name = "auth", description = "Auth module development session APIs"))
31)]
32struct ApiDoc;
33
34pub(crate) fn api_router() -> ApiOpenApiRouter {
41 api_router_for_profile(CompositionProfile::default())
42}
43
44pub(crate) fn api_router_for_profile(profile: CompositionProfile) -> ApiOpenApiRouter {
45 let base = OpenApiRouter::with_openapi(openapi_document_for_profile_with_composition(
46 profile,
47 &lenso_bootstrap::HostComposition::default(),
48 ))
49 .merge(base_router());
50 lenso_bootstrap::merge_linked_http_for_profile(base, profile)
51 .merge(crate::console_bridge::router())
52 .merge(platform_provider::router())
53}
54
55pub(crate) fn api_router_for_context_with_composition(
56 ctx: &AppContext,
57 composition: &lenso_bootstrap::HostComposition,
58) -> platform_core::AppResult<ApiOpenApiRouter> {
59 let profile = CompositionProfile::from_config(&ctx.config)?;
60 let base = OpenApiRouter::with_openapi(openapi_document_for_profile_with_composition(
61 profile,
62 composition,
63 ))
64 .merge(base_router());
65 let router =
66 lenso_bootstrap::merge_linked_http_for_context_with_composition(base, ctx, composition)?;
67 let router = if composition.console_bridge_authority().is_some() {
68 router.merge(crate::console_bridge::router())
69 } else {
70 router
71 };
72 Ok(router.merge(platform_provider::router()))
73}
74
75fn openapi_document_for_profile_with_composition(
76 profile: CompositionProfile,
77 composition: &lenso_bootstrap::HostComposition,
78) -> utoipa::openapi::OpenApi {
79 let mut document = ApiDoc::openapi();
80 if let Some(tags) = &mut document.tags {
81 let has_auth = profile == CompositionProfile::Demo
82 || composition
83 .linked_modules()
84 .iter()
85 .any(|module| module.module_name == "auth");
86 match profile {
87 CompositionProfile::Core => tags.retain(|tag| has_auth || tag.name != "auth"),
88 CompositionProfile::Demo => {}
89 }
90 }
91 document
92}
93
94#[must_use]
96pub fn openapi_document() -> utoipa::openapi::OpenApi {
97 let mut document = api_router().to_openapi();
98 normalize_error_response_content_types(&mut document);
99 document
100}
101
102pub(crate) fn normalize_error_response_content_types(document: &mut utoipa::openapi::OpenApi) {
103 for path_item in document.paths.paths.values_mut() {
104 normalize_operation_error_responses(path_item.get.as_mut());
105 normalize_operation_error_responses(path_item.put.as_mut());
106 normalize_operation_error_responses(path_item.post.as_mut());
107 normalize_operation_error_responses(path_item.delete.as_mut());
108 normalize_operation_error_responses(path_item.options.as_mut());
109 normalize_operation_error_responses(path_item.head.as_mut());
110 normalize_operation_error_responses(path_item.patch.as_mut());
111 normalize_operation_error_responses(path_item.trace.as_mut());
112 }
113}
114
115fn normalize_operation_error_responses(operation: Option<&mut Operation>) {
116 let Some(operation) = operation else {
117 return;
118 };
119 for response in operation.responses.responses.values_mut() {
120 if let RefOr::T(response) = response {
121 normalize_response_error_content_type(response);
122 }
123 }
124}
125
126fn normalize_response_error_content_type(response: &mut Response) {
127 let Some(content) = response.content.get("application/json") else {
128 return;
129 };
130 if !is_error_response_content(content) {
131 return;
132 }
133
134 let content = response
135 .content
136 .shift_remove("application/json")
137 .expect("application/json content should exist");
138 response
139 .content
140 .insert("application/problem+json".to_owned(), content);
141}
142
143fn is_error_response_content(content: &Content) -> bool {
144 matches!(
145 &content.schema,
146 Some(RefOr::Ref(reference))
147 if reference.ref_location == "#/components/schemas/ErrorResponse"
148 )
149}