linkrs_server/http/handlers/
function.rs1use axum::{
4 extract::{Extension, Json, Path, State},
5 response::Json as JsonResponse,
6};
7use parking_lot::RwLock;
8use serde::Deserialize;
9use serde_json;
10
11use crate::http::{error::HttpError, state::AppState};
12use crate::query::executor::expression::functions::{FunctionRegistry, UdfError};
13use crate::storage::{
14 StorageClient, StorageOperationContextOps, StorageSchemaContextOps, StorageSyncContextOps,
15};
16
17#[derive(Debug)]
19pub(crate) enum FunctionOpError {
20 NotFound(String),
21 Conflict(String),
22 Invalid(String),
23 Failed(String),
24}
25
26pub(crate) fn udf_error_to_op(error: UdfError) -> FunctionOpError {
27 let message = error.to_string();
28 match error {
29 UdfError::InvalidPath(_, _) | UdfError::InvalidName(_) => FunctionOpError::Invalid(message),
30 UdfError::AlreadyLoaded(_, _) | UdfError::BuiltinConflict(_) => {
31 FunctionOpError::Conflict(message)
32 }
33 UdfError::NotLoaded(_) => FunctionOpError::NotFound(message),
34 _ => FunctionOpError::Failed(message),
35 }
36}
37
38pub(crate) fn register_udf_from_source(
45 registry: &RwLock<FunctionRegistry>,
46 name: &str,
47 implementation: &str,
48) -> Result<String, FunctionOpError> {
49 if name.trim().is_empty() {
50 return Err(FunctionOpError::Invalid(
51 "function name must not be empty".to_string(),
52 ));
53 }
54 if registry.read().contains(name) {
55 return Err(FunctionOpError::Conflict(format!(
56 "function '{name}' already exists"
57 )));
58 }
59 if implementation.trim().is_empty() {
60 return Err(FunctionOpError::Invalid(
61 "implementation must be a local UDF library path; remote sources and embedded code are not supported".to_string(),
62 ));
63 }
64 let mut guard = registry.write();
65 match guard.install_dynamic_udf(implementation) {
66 Ok(loaded_name) => {
67 if loaded_name.to_uppercase() != name.to_uppercase() {
68 let _ = guard.unload_dynamic_udf(&loaded_name);
69 Err(FunctionOpError::Invalid(format!(
70 "library exports '{loaded_name}', which does not match requested '{name}'"
71 )))
72 } else {
73 Ok(loaded_name)
74 }
75 }
76 Err(e) => Err(udf_error_to_op(e)),
77 }
78}
79
80pub(crate) fn unregister_udf_by_name(
82 registry: &RwLock<FunctionRegistry>,
83 name: &str,
84) -> Result<(), FunctionOpError> {
85 if !registry.read().contains(name) {
86 return Err(FunctionOpError::NotFound(format!(
87 "function '{name}' does not exist"
88 )));
89 }
90 if registry.read().get_builtin(name).is_some() {
91 return Err(FunctionOpError::Invalid(format!(
92 "built-in function '{name}' cannot be unregistered"
93 )));
94 }
95 let mut guard = registry.write();
96 if guard.is_dynamic(name) {
97 guard.unload_dynamic_udf(name).map_err(udf_error_to_op)
98 } else if guard.unregister_custom(name) {
99 Ok(())
100 } else {
101 Err(FunctionOpError::NotFound(format!(
102 "function '{name}' does not exist"
103 )))
104 }
105}
106
107pub(crate) fn op_error_to_http(error: FunctionOpError) -> HttpError {
108 match error {
109 FunctionOpError::NotFound(message) => HttpError::not_found(message),
110 FunctionOpError::Conflict(message) | FunctionOpError::Invalid(message) => {
111 HttpError::bad_request(message)
112 }
113 FunctionOpError::Failed(message) => HttpError::internal(message),
114 }
115}
116
117#[utoipa::path(
118 post,
119 path = "/v1/functions",
120 tag = "Function",
121 request_body = RegisterFunctionRequest,
122 responses(
123 (status = 200, body = serde_json::Value, description = "Function registered"),
124 (status = 500, description = "Internal error")
125 )
126)]
127pub async fn register<
129 S: StorageClient
130 + StorageSchemaContextOps
131 + StorageSyncContextOps
132 + StorageOperationContextOps
133 + Clone
134 + Send
135 + Sync
136 + 'static,
137>(
138 State(state): State<AppState<S>>,
139 Extension(session_id): Extension<i64>,
140 Json(request): Json<RegisterFunctionRequest>,
141) -> Result<JsonResponse<serde_json::Value>, HttpError> {
142 super::authz::require_admin_session(&state, session_id)?;
143 let implementation = match &request.implementation {
144 Some(serde_json::Value::String(path)) => path.clone(),
145 Some(other) => {
146 return Err(HttpError::bad_request(format!(
147 "implementation must be a local UDF library path string, got {other}"
148 )));
149 }
150 None => String::new(),
151 };
152 let registry = state.server.get_function_registry();
153 let registered_name = register_udf_from_source(®istry, &request.name, &implementation)
154 .map_err(op_error_to_http)?;
155 let registry_guard = registry.read();
156 let return_type = registry_guard
157 .get_return_type(®istered_name)
158 .map(|t| format!("{t:?}"))
159 .unwrap_or_else(|| "unknown".to_string());
160
161 Ok(JsonResponse(serde_json::json!({
162 "function_id": registered_name,
163 "name": registered_name,
164 "function_type": "custom",
165 "return_type": return_type,
166 "status": "registered",
167 "message": "Function registered successfully",
168 })))
169}
170
171#[utoipa::path(
172 get,
173 path = "/v1/functions",
174 tag = "Function",
175 responses(
176 (status = 200, body = serde_json::Value, description = "Function list"),
177 (status = 500, description = "Internal error")
178 )
179)]
180pub async fn list<
182 S: StorageClient
183 + StorageSchemaContextOps
184 + StorageSyncContextOps
185 + StorageOperationContextOps
186 + Clone
187 + Send
188 + Sync
189 + 'static,
190>(
191 State(state): State<AppState<S>>,
192) -> Result<JsonResponse<serde_json::Value>, HttpError> {
193 let registry = state.server.get_function_registry();
194 let registry_guard = registry.read();
195
196 let function_names = registry_guard.function_names();
197 let functions: Vec<serde_json::Value> = function_names
198 .iter()
199 .map(|name| {
200 serde_json::json!({
201 "name": name,
202 })
203 })
204 .collect();
205
206 Ok(JsonResponse(serde_json::json!({
207 "functions": functions,
208 "total": functions.len(),
209 })))
210}
211
212#[utoipa::path(
213 get,
214 path = "/v1/functions/{name}",
215 tag = "Function",
216 params(("name" = String, Path, description = "Function name")),
217 responses(
218 (status = 200, body = serde_json::Value, description = "Function details"),
219 (status = 404, description = "Not found"),
220 (status = 500, description = "Internal error")
221 )
222)]
223pub async fn info<
225 S: StorageClient
226 + StorageSchemaContextOps
227 + StorageSyncContextOps
228 + StorageOperationContextOps
229 + Clone
230 + Send
231 + Sync
232 + 'static,
233>(
234 State(state): State<AppState<S>>,
235 Path(name): Path<String>,
236) -> Result<JsonResponse<serde_json::Value>, HttpError> {
237 let registry = state.server.get_function_registry();
238 let registry_guard = registry.read();
239
240 if !registry_guard.contains(&name) {
241 return Err(HttpError::not_found(format!(
242 "Function '{}' does not exist",
243 name
244 )));
245 }
246
247 let is_builtin = registry_guard.get_builtin(&name).is_some();
248 let is_custom = registry_guard.get_custom(&name).is_some();
249
250 let function_type = if is_builtin {
251 "builtin"
252 } else if is_custom {
253 "custom"
254 } else {
255 "unknown"
256 };
257 let return_type = registry_guard
258 .get_return_type(&name)
259 .map(|t| format!("{t:?}"))
260 .unwrap_or_else(|| "unknown".to_string());
261 let registered_at = registry_guard
264 .dynamic_udf_mtime(&name)
265 .map(|mtime| chrono::DateTime::<chrono::Utc>::from(mtime).to_rfc3339());
266
267 Ok(JsonResponse(serde_json::json!({
268 "name": name,
269 "type": function_type,
270 "is_builtin": is_builtin,
271 "is_custom": is_custom,
272 "parameters": [],
273 "return_type": return_type,
274 "registered_at": registered_at,
275 })))
276}
277
278#[utoipa::path(
279 delete,
280 path = "/v1/functions/{name}",
281 tag = "Function",
282 params(("name" = String, Path, description = "Function name")),
283 responses(
284 (status = 200, body = serde_json::Value, description = "Function unregistered"),
285 (status = 404, description = "Not found"),
286 (status = 500, description = "Internal error")
287 )
288)]
289pub async fn unregister<
291 S: StorageClient
292 + StorageSchemaContextOps
293 + StorageSyncContextOps
294 + StorageOperationContextOps
295 + Clone
296 + Send
297 + Sync
298 + 'static,
299>(
300 State(state): State<AppState<S>>,
301 Extension(session_id): Extension<i64>,
302 Path(name): Path<String>,
303) -> Result<JsonResponse<serde_json::Value>, HttpError> {
304 super::authz::require_admin_session(&state, session_id)?;
305 let registry = state.server.get_function_registry();
306 unregister_udf_by_name(®istry, &name).map_err(op_error_to_http)?;
307
308 Ok(JsonResponse(serde_json::json!({
309 "message": "Function unregistered",
310 "name": name,
311 })))
312}
313
314#[derive(Debug, Deserialize, utoipa::ToSchema)]
320pub struct RegisterFunctionRequest {
321 pub name: String,
322 #[serde(rename = "type", default)]
323 pub function_type: String,
324 #[serde(default)]
325 pub parameters: Vec<String>,
326 #[serde(rename = "return_type", default)]
327 pub return_type: String,
328 pub description: Option<String>,
329 pub implementation: Option<serde_json::Value>,
330}