limithub_code_block_sdk/
lib.rs1pub mod error;
2pub mod port;
3pub mod value;
4
5use std::{
6 collections::{BTreeSet, HashMap},
7 future::Future,
8 io,
9 net::SocketAddr,
10 sync::Arc,
11};
12
13use axum::{
14 http::StatusCode,
15 routing::{get, post},
16 Extension, Json, Router,
17};
18
19pub use error::{Error, Result};
20pub use port::Port;
21use serde::Serialize;
22use tokio::net::TcpListener;
23pub use value::{PortValue, PortValueType};
24
25#[derive(Debug, Clone, Serialize)]
26pub struct CodeBlockAppBuilder {
27 inputs: Vec<Port>,
28 outputs: Vec<Port>,
29}
30
31impl CodeBlockAppBuilder {
32 pub fn new() -> Self {
33 CodeBlockAppBuilder {
34 inputs: Vec::new(),
35 outputs: Vec::new(),
36 }
37 }
38
39 pub fn inputs(mut self, inputs: impl IntoIterator<Item = Port>) -> Self {
41 self.inputs.extend(inputs);
42 self
43 }
44
45 pub fn outputs(mut self, outputs: impl IntoIterator<Item = Port>) -> Self {
47 self.outputs.extend(outputs);
48 self
49 }
50
51 pub fn build(self) -> Result<CodeBlockApp> {
52 if self.inputs.is_empty() {
53 return Err(Error::EmptyPorts("inputs"));
54 }
55
56 let mut inputs = BTreeSet::new();
57 let mut outputs = BTreeSet::new();
58
59 for input in self.inputs {
60 let name = input.name.clone();
61 inputs
62 .insert(input)
63 .then_some(())
64 .ok_or(Error::NameConflict(name))?;
65 }
66
67 for output in self.outputs {
68 let name = output.name.clone();
69 outputs
70 .insert(output)
71 .then_some(())
72 .ok_or(Error::NameConflict(name))?;
73 }
74
75 Ok(CodeBlockApp { inputs, outputs })
76 }
77}
78
79#[derive(Debug, Clone, Serialize)]
80pub struct CodeBlockApp {
81 inputs: BTreeSet<Port>,
82 outputs: BTreeSet<Port>,
83}
84
85impl CodeBlockApp {
86 pub fn serve<Fut>(
88 self,
89 handler: fn(HashMap<String, PortValue>) -> Fut,
90 ) -> impl Future<Output = io::Result<()>>
91 where
92 Fut: Future<Output = Result<HashMap<String, PortValue>, String>> + Send + 'static,
93 {
94 let app = Router::new()
95 .route("/ports", get(get_ports))
96 .route("/run", post(move |app, req| run(app, req, handler)))
97 .layer(Extension(Arc::new(self)));
98
99 let port = std::env::var("PORT")
100 .ok()
101 .and_then(|p| p.parse().ok())
102 .unwrap_or(3000);
103
104 async move {
105 let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], port))).await?;
106 axum::serve(listener, app).await
107 }
108 }
109
110 fn validate_inputs(&self, inputs: &HashMap<String, PortValue>) -> Result<()> {
111 self.inputs
112 .iter()
113 .map(|i| {
114 inputs
115 .contains_key(&i.name)
116 .then_some(())
117 .ok_or(Error::MissingPort(i.name.clone()))
118 })
119 .collect::<Result<()>>()
120 }
121
122 fn validate_outputs(&self, outputs: &HashMap<String, PortValue>) -> Result<()> {
123 self.outputs
124 .iter()
125 .map(|i| {
126 outputs
127 .contains_key(&i.name)
128 .then_some(())
129 .ok_or(Error::MissingPort(i.name.clone()))
130 })
131 .collect::<Result<()>>()
132 }
133}
134
135async fn get_ports(Extension(app): Extension<Arc<CodeBlockApp>>) -> Json<CodeBlockApp> {
136 Json(app.as_ref().clone())
137}
138
139async fn run<Fut>(
140 Extension(app): Extension<Arc<CodeBlockApp>>,
141 Json(req): Json<HashMap<String, PortValue>>,
142 handler: fn(HashMap<String, PortValue>) -> Fut,
143) -> Result<Json<HashMap<String, PortValue>>, (StatusCode, String)>
144where
145 Fut: Future<Output = Result<HashMap<String, PortValue>, String>> + 'static + Send,
146{
147 app.validate_inputs(&req)
148 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
149
150 let outputs = handler(req)
151 .await
152 .map(|res| Json(res))
153 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
154
155 app.validate_outputs(&outputs)
156 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
157
158 Ok(outputs)
159}
160
161#[cfg(test)]
162mod tests {
163 use std::collections::HashMap;
164
165 use crate::{CodeBlockAppBuilder, Port, PortValue, PortValueType};
166
167 #[test]
168 fn test() {
169 _ = CodeBlockAppBuilder::new()
170 .inputs([Port::new(String::from("port1"), PortValueType::Bool)])
171 .build()
172 .unwrap()
173 .serve(handler);
174 }
175
176 async fn handler(
177 _req: HashMap<String, PortValue>,
178 ) -> Result<HashMap<String, PortValue>, String> {
179 Ok(HashMap::new())
180 }
181}