Skip to content

Commit 380792f

Browse files
author
developerworks
committed
Add factory registry and config-driven task binding
- Introduce TaskFactoryRegistry with typed descriptors for declarative factory_key resolution - Add factory_binding module to bind config-derived child specs to runtime factories - Add factory_schema module for schema generation from registry metadata - Wire factory lookup into child spec building and validation - Update runtime supervisor and control loop to use resolved factories - Add integration test for factory-bound add_child transaction
1 parent c22f67f commit 380792f

18 files changed

Lines changed: 957 additions & 3 deletions

examples/split_config_supervisor.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,33 @@
33
44
use rust_supervisor::config::loader::load_config_from_yaml_file;
55

6+
// Define the example result type used by the split configuration demo.
67
type ExampleResult = Result<(), rust_supervisor::error::types::SupervisorError>;
78

89
/// Loads the split example config and prints derived child and group counts.
910
fn main() -> ExampleResult {
11+
// Load the split configuration through the public YAML loader.
1012
let state = load_config_from_yaml_file("examples/config/split/supervisor.yaml")?;
1113

14+
// Print the selected root supervision strategy.
1215
println!("strategy: {:?}", state.supervisor.strategy);
16+
// Print the number of configured groups.
1317
println!("groups: {}", state.groups.len());
18+
// Print the number of configured children.
1419
println!("children: {}", state.children.len());
1520

21+
// Print every configured group name.
1622
for group in &state.groups {
23+
// Print the current group name.
1724
println!(" group: {}", group.name);
1825
}
1926

27+
// Print every configured child name.
2028
for child in &state.children {
29+
// Print the current child name.
2130
println!(" child: {}", child.name);
2231
}
2332

33+
// Return success after the split configuration summary is printed.
2434
Ok(())
2535
}

src/config/factory_binding.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//! Factory binding from configuration keys to runtime child specs.
2+
//!
3+
//! The functions in this module keep executable task factories out of raw YAML
4+
//! loading while still providing a single startup-time binding step.
5+
6+
use crate::error::types::SupervisorError;
7+
use crate::spec::child::{ChildSpec, TaskKind};
8+
use crate::task::factory_registry::TaskFactoryRegistry;
9+
10+
/// Binds all worker child factory keys to executable task factories.
11+
///
12+
/// # Arguments
13+
///
14+
/// - `children`: Child specifications converted from configuration.
15+
/// - `registry`: Registry that owns the executable factories.
16+
///
17+
/// # Returns
18+
///
19+
/// Returns `Ok(())` when every worker child has a usable factory.
20+
///
21+
/// # Errors
22+
///
23+
/// Returns [`SupervisorError`] when a worker is missing `factory_key`, a key is
24+
/// unknown, a key does not support the declared task kind, or a supervisor child
25+
/// declares a factory key.
26+
///
27+
/// # Examples
28+
///
29+
/// ```
30+
/// use rust_supervisor::config::factory_binding::bind_task_factories;
31+
/// use rust_supervisor::id::types::ChildId;
32+
/// use rust_supervisor::spec::child_builder::ChildSpecBuilder;
33+
/// use rust_supervisor::task::factory::{TaskResult, service_fn};
34+
/// use rust_supervisor::task::factory_registry::{
35+
/// TaskFactoryDescriptor, TaskFactoryRegistry,
36+
/// };
37+
/// use rust_supervisor::spec::child::TaskKind;
38+
/// use std::sync::Arc;
39+
///
40+
/// # fn example() -> Result<(), rust_supervisor::error::types::SupervisorError> {
41+
/// let mut registry = TaskFactoryRegistry::new();
42+
/// registry.register(TaskFactoryDescriptor::new(
43+
/// "worker",
44+
/// "Worker",
45+
/// "Runs one worker.",
46+
/// [TaskKind::AsyncWorker],
47+
/// Arc::new(service_fn(|_ctx| async { TaskResult::Succeeded })),
48+
/// ))?;
49+
/// let child = ChildSpecBuilder::new(ChildId::new("worker"), "worker")
50+
/// .kind(TaskKind::AsyncWorker)
51+
/// .factory_key("worker")
52+
/// .factory(Arc::new(service_fn(|_ctx| async { TaskResult::Succeeded })))
53+
/// .build()?;
54+
/// let mut children = vec![child];
55+
/// bind_task_factories(&mut children, &registry)?;
56+
/// assert!(children[0].factory.is_some());
57+
/// # Ok(())
58+
/// # }
59+
/// ```
60+
pub fn bind_task_factories(
61+
children: &mut [ChildSpec],
62+
registry: &TaskFactoryRegistry,
63+
) -> Result<(), SupervisorError> {
64+
for child in children {
65+
bind_child_factory(child, registry)?;
66+
}
67+
Ok(())
68+
}
69+
70+
/// Binds one child factory key to an executable task factory.
71+
///
72+
/// # Arguments
73+
///
74+
/// - `child`: Child specification converted from configuration.
75+
/// - `registry`: Registry that owns executable task factories.
76+
///
77+
/// # Returns
78+
///
79+
/// Returns `Ok(())` after the child has a valid factory assignment or does not
80+
/// require one.
81+
///
82+
/// # Errors
83+
///
84+
/// Returns [`SupervisorError`] when the child factory declaration is invalid.
85+
pub fn bind_child_factory(
86+
child: &mut ChildSpec,
87+
registry: &TaskFactoryRegistry,
88+
) -> Result<(), SupervisorError> {
89+
match child.kind {
90+
TaskKind::AsyncWorker | TaskKind::BlockingWorker => {
91+
let key = child
92+
.factory_key
93+
.as_deref()
94+
.filter(|key| !key.trim().is_empty())
95+
.ok_or_else(|| {
96+
SupervisorError::fatal_config(format!(
97+
"worker child '{}' requires factory_key",
98+
child.id
99+
))
100+
})?;
101+
child.factory = Some(registry.resolve(key, child.kind)?);
102+
Ok(())
103+
}
104+
TaskKind::Supervisor => {
105+
if child
106+
.factory_key
107+
.as_deref()
108+
.is_some_and(|key| !key.trim().is_empty())
109+
{
110+
return Err(SupervisorError::fatal_config(format!(
111+
"supervisor child '{}' must not declare factory_key",
112+
child.id
113+
)));
114+
}
115+
child.factory = None;
116+
Ok(())
117+
}
118+
}
119+
}

src/config/factory_schema.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//! JSON Schema completion support for declarative task factories.
2+
//!
3+
//! This module enriches the generated supervisor configuration schema with
4+
//! `factory_key` values from the caller-provided task factory registry.
5+
6+
use crate::config::configurable::SupervisorConfig;
7+
use crate::error::types::SupervisorError;
8+
use crate::task::factory_registry::TaskFactoryRegistry;
9+
use serde_json::{Value, json};
10+
11+
/// Builds a supervisor configuration schema with `factory_key` completion values.
12+
///
13+
/// # Arguments
14+
///
15+
/// - `registry`: Task factory registry that supplies valid completion keys.
16+
///
17+
/// # Returns
18+
///
19+
/// Returns a JSON Schema value whose `ChildDeclaration.factory_key` field
20+
/// contains registry-backed completion candidates.
21+
///
22+
/// # Errors
23+
///
24+
/// Returns [`SupervisorError`] when the generated schema does not expose the
25+
/// expected `ChildDeclaration.factory_key` property.
26+
///
27+
/// # Examples
28+
///
29+
/// ```
30+
/// use rust_supervisor::config::factory_schema::supervisor_schema_with_factory_registry;
31+
/// use rust_supervisor::spec::child::TaskKind;
32+
/// use rust_supervisor::task::factory::{TaskResult, service_fn};
33+
/// use rust_supervisor::task::factory_registry::{
34+
/// TaskFactoryDescriptor, TaskFactoryRegistry,
35+
/// };
36+
/// use std::sync::Arc;
37+
///
38+
/// # fn example() -> Result<(), rust_supervisor::error::types::SupervisorError> {
39+
/// let mut registry = TaskFactoryRegistry::new();
40+
/// registry.register(TaskFactoryDescriptor::new(
41+
/// "worker",
42+
/// "Worker",
43+
/// "Runs one worker.",
44+
/// [TaskKind::AsyncWorker],
45+
/// Arc::new(service_fn(|_ctx| async { TaskResult::Succeeded })),
46+
/// ))?;
47+
/// let schema = supervisor_schema_with_factory_registry(&registry)?;
48+
/// let schema_text = serde_json::to_string(&schema).unwrap();
49+
/// assert!(schema_text.contains("worker"));
50+
/// # Ok(())
51+
/// # }
52+
/// ```
53+
pub fn supervisor_schema_with_factory_registry(
54+
registry: &TaskFactoryRegistry,
55+
) -> Result<Value, SupervisorError> {
56+
let schema = schemars::schema_for!(SupervisorConfig);
57+
let mut value = serde_json::to_value(&schema).map_err(|error| {
58+
SupervisorError::fatal_config(format!("failed to serialize supervisor schema: {error}"))
59+
})?;
60+
inject_factory_key_completions(&mut value, registry)?;
61+
Ok(value)
62+
}
63+
64+
/// Injects registry-backed completion values into an existing schema.
65+
///
66+
/// # Arguments
67+
///
68+
/// - `schema`: Schema value generated from [`SupervisorConfig`].
69+
/// - `registry`: Task factory registry that supplies valid completion keys.
70+
///
71+
/// # Returns
72+
///
73+
/// Returns `Ok(())` after completion candidates have been injected.
74+
///
75+
/// # Errors
76+
///
77+
/// Returns [`SupervisorError`] when the schema does not expose the expected
78+
/// `ChildDeclaration.factory_key` property.
79+
pub fn inject_factory_key_completions(
80+
schema: &mut Value,
81+
registry: &TaskFactoryRegistry,
82+
) -> Result<(), SupervisorError> {
83+
let pointer = if schema
84+
.pointer("/definitions/ChildDeclaration/properties/factory_key")
85+
.is_some()
86+
{
87+
"/definitions/ChildDeclaration/properties/factory_key"
88+
} else if schema
89+
.pointer("/$defs/ChildDeclaration/properties/factory_key")
90+
.is_some()
91+
{
92+
"/$defs/ChildDeclaration/properties/factory_key"
93+
} else {
94+
return Err(SupervisorError::fatal_config(
95+
"supervisor schema is missing ChildDeclaration.factory_key",
96+
));
97+
};
98+
let factory_key_schema = schema.pointer_mut(pointer).ok_or_else(|| {
99+
SupervisorError::fatal_config("supervisor schema is missing ChildDeclaration.factory_key")
100+
})?;
101+
102+
let choices = registry
103+
.descriptors()
104+
.into_iter()
105+
.map(|descriptor| {
106+
json!({
107+
"const": descriptor.key,
108+
"title": descriptor.title,
109+
"description": descriptor.description,
110+
})
111+
})
112+
.collect::<Vec<_>>();
113+
114+
factory_key_schema["oneOf"] = Value::Array(choices);
115+
factory_key_schema["description"] = Value::String(
116+
"TaskFactory registry key used to bind worker children before startup.".to_owned(),
117+
);
118+
Ok(())
119+
}

src/config/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
pub mod audit;
22
pub mod configurable;
3+
pub mod factory_binding;
4+
pub mod factory_schema;
35
pub mod ipc_security;
46
pub mod loader;
57
pub mod policy;

src/config/policy.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,22 +322,26 @@ pub struct GroupsConfigSection {
322322
}
323323

324324
impl Default for GroupsConfigSection {
325+
/// Returns an empty group configuration section.
325326
fn default() -> Self {
326327
Self { items: Vec::new() }
327328
}
328329
}
329330

330331
impl JsonSchema for GroupsConfigSection {
332+
/// Returns the schema name used for split group sections.
331333
fn schema_name() -> Cow<'static, str> {
332334
Cow::Borrowed("GroupsConfigSection")
333335
}
334336

337+
/// Returns the transparent array schema for group declarations.
335338
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
336339
Vec::<GroupConfig>::json_schema(generator)
337340
}
338341
}
339342

340343
impl Serialize for GroupsConfigSection {
344+
/// Serializes group section entries as a transparent array.
341345
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
342346
where
343347
S: confique::serde::Serializer,
@@ -347,6 +351,7 @@ impl Serialize for GroupsConfigSection {
347351
}
348352

349353
impl<'de> Deserialize<'de> for GroupsConfigSection {
354+
/// Deserializes group section entries from a transparent array.
350355
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
351356
where
352357
D: confique::serde::Deserializer<'de>,
@@ -375,6 +380,7 @@ impl GroupsConfigSection {
375380
}
376381

377382
impl From<GroupsConfigSection> for Vec<GroupConfig> {
383+
/// Converts a group section into its transparent entry vector.
378384
fn from(section: GroupsConfigSection) -> Self {
379385
section.items
380386
}

src/config/state.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,46 @@ impl ConfigState {
388388
pub fn to_supervisor_spec(
389389
&self,
390390
) -> Result<crate::spec::supervisor::SupervisorSpec, crate::error::types::SupervisorError> {
391+
let spec = self.build_supervisor_spec();
392+
spec.validate()?;
393+
Ok(spec)
394+
}
395+
396+
/// Converts validated configuration into a supervisor declaration and binds factories.
397+
///
398+
/// # Arguments
399+
///
400+
/// - `registry`: Task factory registry used to resolve worker `factory_key` values.
401+
///
402+
/// # Returns
403+
///
404+
/// Returns a [`crate::spec::supervisor::SupervisorSpec`] with executable
405+
/// task factories assigned to worker children.
406+
///
407+
/// # Errors
408+
///
409+
/// Returns [`crate::error::types::SupervisorError`] when factory binding or
410+
/// supervisor validation fails.
411+
pub fn to_supervisor_spec_with_factories(
412+
&self,
413+
registry: &crate::task::factory_registry::TaskFactoryRegistry,
414+
) -> Result<crate::spec::supervisor::SupervisorSpec, crate::error::types::SupervisorError> {
415+
let mut spec = self.build_supervisor_spec();
416+
crate::config::factory_binding::bind_task_factories(&mut spec.children, registry)?;
417+
spec.validate()?;
418+
Ok(spec)
419+
}
420+
421+
/// Builds a supervisor specification before final validation.
422+
///
423+
/// # Arguments
424+
///
425+
/// This function has no arguments.
426+
///
427+
/// # Returns
428+
///
429+
/// Returns a supervisor specification assembled from validated config state.
430+
fn build_supervisor_spec(&self) -> crate::spec::supervisor::SupervisorSpec {
391431
let mut spec = crate::spec::supervisor::SupervisorSpec::root(self.children.clone());
392432
spec.strategy = self.supervisor.strategy;
393433
spec.config_version = self.config_version();
@@ -454,8 +494,7 @@ impl ConfigState {
454494
));
455495
spec.metrics_enabled = self.observability.metrics_enabled;
456496
spec.audit_enabled = self.observability.audit_enabled;
457-
spec.validate()?;
458-
Ok(spec)
497+
spec
459498
}
460499

461500
/// Builds a stable configuration version string from configured values.

0 commit comments

Comments
 (0)