use std::{collections::HashMap, path::Path};
use fraiseql_error::{FraiseQLError, Result};
use fraiseql_functions::{
FunctionModule, FunctionObserver, RuntimeType, triggers::TriggerRegistry,
types::FunctionDefinition,
};
use super::FunctionsSubsystem;
use crate::schema::loader::FunctionsConfig;
#[derive(Debug, Clone, Copy)]
pub struct SourceConnector<'a> {
pub source: &'a str,
pub function: &'a str,
}
pub fn build_functions_subsystem(
config: FunctionsConfig,
connectors: &[SourceConnector<'_>],
) -> Result<FunctionsSubsystem> {
let module_registry = load_modules(&config, connectors)?;
let trigger_registry =
TriggerRegistry::load_from_definitions(&config.definitions).map_err(|error| {
FraiseQLError::Configuration {
message: format!("invalid function triggers: {error}"),
}
})?;
let mut observer = FunctionObserver::new();
observer.register_runtime(
RuntimeType::Wasm,
fraiseql_functions::runtime::wasm::WasmRuntime::new(
&fraiseql_functions::runtime::wasm::WasmConfig::default(),
)
.map_err(|error| FraiseQLError::Configuration {
message: format!("failed to initialize the WASM function runtime: {error}"),
})?,
);
#[cfg(feature = "functions-runtime-deno")]
observer.register_runtime(
RuntimeType::Deno,
fraiseql_functions::runtime::deno::DenoRuntime::new(
&fraiseql_functions::runtime::deno::DenoConfig::default(),
)
.map_err(|error| FraiseQLError::Configuration {
message: format!("failed to initialize the Deno function runtime: {error}"),
})?,
);
Ok(FunctionsSubsystem {
observer: std::sync::Arc::new(observer),
trigger_registry,
module_registry,
config,
})
}
fn load_modules(
config: &FunctionsConfig,
connectors: &[SourceConnector<'_>],
) -> Result<HashMap<String, FunctionModule>> {
let mut registry = HashMap::with_capacity(config.definitions.len() + connectors.len());
for definition in &config.definitions {
let module = load_one_module(&config.module_dir, definition)?;
registry.insert(definition.name.clone(), module);
}
for connector in connectors {
if !registry.contains_key(connector.function) {
let module = load_connector(&config.module_dir, *connector)?;
registry.insert(connector.function.to_string(), module);
}
}
Ok(registry)
}
fn load_connector(module_dir: &Path, connector: SourceConnector<'_>) -> Result<FunctionModule> {
let runtime = RuntimeType::Deno;
if !runtime_compiled_in(runtime) {
return Err(FraiseQLError::Configuration {
message: format!(
"source {:?} runs the Deno connector {:?}, but the Deno runtime is not compiled \
into this build (enable `functions-runtime-deno`)",
connector.source, connector.function
),
});
}
let Some(path) = runtime.resolve_module_path(module_dir, connector.function) else {
return Err(FraiseQLError::Configuration {
message: format!(
"source {:?} runs the connector {:?}, but no module was found at {} — a \
connector is loaded from `<module_dir>/<function>.<ext>`, so the file name \
must match the source's `function`",
connector.source,
connector.function,
runtime.module_path_pattern(module_dir, connector.function),
),
});
};
build_module(connector.function, runtime, &path)
}
fn load_one_module(module_dir: &Path, definition: &FunctionDefinition) -> Result<FunctionModule> {
if !runtime_compiled_in(definition.runtime) {
return Err(FraiseQLError::Configuration {
message: format!(
"function {:?} targets the {:?} runtime, which is not compiled into this build \
(enable the corresponding `functions-runtime*` feature)",
definition.name, definition.runtime
),
});
}
if let Some(path) = definition.resolve_module_path(module_dir) {
return build_module(&definition.name, definition.runtime, &path);
}
Err(FraiseQLError::Configuration {
message: format!(
"function {:?} declares the {:?} runtime but no module file was found at {}",
definition.name,
definition.runtime,
definition.module_path_pattern(module_dir),
),
})
}
fn build_module(name: &str, runtime: RuntimeType, path: &Path) -> Result<FunctionModule> {
match runtime {
RuntimeType::Wasm => {
let bytecode = std::fs::read(path).map_err(|error| FraiseQLError::Configuration {
message: format!(
"failed to read WASM module for function {name:?} at {}: {error}",
path.display()
),
})?;
Ok(FunctionModule::from_bytecode(name.to_string(), bytecode.into()))
},
RuntimeType::Deno => {
let source =
std::fs::read_to_string(path).map_err(|error| FraiseQLError::Configuration {
message: format!(
"failed to read Deno module for function {name:?} at {}: {error}",
path.display()
),
})?;
Ok(FunctionModule::from_source(name.to_string(), source, RuntimeType::Deno))
},
other => Err(FraiseQLError::Configuration {
message: format!("function {name:?} declares an unsupported runtime {other:?}"),
}),
}
}
const fn runtime_compiled_in(runtime: RuntimeType) -> bool {
match runtime {
RuntimeType::Wasm => true,
RuntimeType::Deno => cfg!(feature = "functions-runtime-deno"),
_ => false,
}
}
#[cfg(test)]
mod tests;