lenso_openapi_plugin/
lib.rs1mod assemble;
8mod config;
9
10use std::{cell::RefCell, rc::Rc};
11
12use lenso::prelude::ManyPort;
13use lenso::{ActivateContext, DeactivateContext, Lifecycle, provides};
14use lenso_capability_http_endpoint as http_endpoint;
15use lenso_capability_http_endpoint::{
16 DescribeError, DescribeRequest, DescribeResponse, DescribeResponseRoutesItem, EndpointDescribe,
17 EndpointHandle, EndpointProvider, HandleError, HandleRequest, HandleResponse,
18 HandleResponseHeadersItem,
19};
20use lenso_kernel::{InvocationContext, NativeRequestFuture, RuntimeFailure};
21use serde_json::json;
22
23pub use config::OpenApiConfig;
24
25pub const DOCUMENT_ROUTE_ID: &str = "lenso.openapi.document";
26
27fn validate_config(config: &OpenApiConfig) -> Result<(), RuntimeFailure> {
28 config
29 .validate()
30 .map_err(|detail| RuntimeFailure::InvalidResolvedPlan {
31 detail: format!("OpenAPI configuration is invalid: {detail}"),
32 })
33}
34
35#[lenso::plugin(
36 lifecycle,
37 validate = validate_config,
38 configuration_schema = "config.schema.json"
39)]
40#[derive(Clone, Debug)]
41struct OpenApiPlugin {
42 #[config]
43 config: OpenApiConfig,
44 endpoints: ManyPort<http_endpoint::EndpointClient>,
45 document: Rc<RefCell<Option<Vec<u8>>>>,
46}
47
48#[provides(http_endpoint::Endpoint)]
49impl EndpointProvider for OpenApiPlugin {
50 fn describe(
51 &self,
52 _context: InvocationContext,
53 _request: DescribeRequest,
54 ) -> NativeRequestFuture<EndpointDescribe> {
55 let route = DescribeResponseRoutesItem {
56 method: "GET".to_owned(),
57 openapi: Some(
58 json!({
59 "summary": "Get the OpenAPI document",
60 "responses": {
61 "200": {
62 "description": "OpenAPI 3.1 document",
63 "content": {
64 "application/json": {
65 "schema": {"type": "object"}
66 }
67 }
68 }
69 }
70 })
71 .as_object()
72 .expect("the OpenAPI operation is an object")
73 .clone()
74 .into_iter()
75 .collect(),
76 ),
77 path: self.config.document_path().to_owned(),
78 route_id: DOCUMENT_ROUTE_ID.to_owned(),
79 };
80 Box::pin(futures::future::ready(Ok(Ok(DescribeResponse {
81 routes: vec![route],
82 }))))
83 }
84
85 fn handle(
86 &self,
87 _context: InvocationContext,
88 request: HandleRequest,
89 ) -> NativeRequestFuture<EndpointHandle> {
90 let result = if request.route_id == DOCUMENT_ROUTE_ID {
91 self.document.borrow().clone().map_or_else(
92 || {
93 Err(RuntimeFailure::Internal {
94 detail: "OpenAPI document is unavailable before activation".to_owned(),
95 })
96 },
97 |document| {
98 Ok(Ok(HandleResponse {
99 body: document.into(),
100 headers: vec![HandleResponseHeadersItem {
101 name: "content-type".to_owned(),
102 value: "application/json; charset=utf-8".to_owned(),
103 }],
104 status: 200,
105 }))
106 },
107 )
108 } else {
109 Ok(Err(HandleError::Rejected))
110 };
111 Box::pin(futures::future::ready(result))
112 }
113}
114
115#[allow(unknown_lints, clippy::unused_async_trait_impl)]
116impl Lifecycle for OpenApiPlugin {
117 async fn activate(&self, _context: ActivateContext) -> Result<(), RuntimeFailure> {
118 let mut descriptions = Vec::with_capacity(self.endpoints.len());
119 for (provider_index, endpoint) in self.endpoints.iter().enumerate() {
120 let description = endpoint
121 .describe(DescribeRequest {})
122 .await
123 .map_err(|error| match error {
124 http_endpoint::EndpointDescribeInvocationError::Domain(error) => {
125 describe_failure(provider_index, &error)
126 }
127 http_endpoint::EndpointDescribeInvocationError::Runtime(error) => error,
128 })?;
129 descriptions.push(description);
130 }
131 let assembled = assemble::assemble(&self.config, descriptions).map_err(plugin_failure)?;
132 self.document.borrow_mut().replace(assembled);
133 Ok(())
134 }
135
136 async fn deactivate(&self, _context: DeactivateContext) -> Result<(), RuntimeFailure> {
137 self.document.borrow_mut().take();
138 Ok(())
139 }
140}
141
142fn describe_failure(provider_index: usize, error: &DescribeError) -> RuntimeFailure {
143 plugin_failure(format!(
144 "OpenAPI Endpoint provider {provider_index} rejected its description: {error:?}"
145 ))
146}
147
148fn plugin_failure(detail: impl Into<String>) -> RuntimeFailure {
149 RuntimeFailure::PluginFailure {
150 detail: detail.into(),
151 }
152}