pub mod combinators;
pub mod error;
use std::collections::HashMap;
use rand::Rng;
use crate::corpus::{Corpus, ValueProvenance};
pub use error::AethelError;
#[derive(Debug, Clone)]
pub struct ComposedValue {
pub value: String,
pub provenance: Vec<ValueProvenance>,
}
impl ComposedValue {
pub fn merge(mut self, other: ComposedValue) -> Self {
self.value.push_str(&other.value);
self.provenance.extend(other.provenance);
self
}
}
pub struct GenerationContext<'a> {
pub corpus: &'a Corpus,
pub history: HashMap<String, ComposedValue>,
}
impl<'a> GenerationContext<'a> {
pub fn new(corpus: &'a Corpus) -> Self {
Self {
corpus,
history: HashMap::new(),
}
}
pub fn get_previous(&self, key: &str) -> Option<&ComposedValue> {
self.history.get(key)
}
}
pub trait Rule {
fn name(&self) -> &str;
fn execute<'a>(
&self,
ctx: &GenerationContext<'a>,
rng: &mut dyn Rng,
) -> Result<ComposedValue, AethelError>;
}
pub struct InlineRule<F> {
name: String,
logic: F,
}
impl<F> InlineRule<F> {
pub fn new(name: impl Into<String>, logic: F) -> Self {
Self {
name: name.into(),
logic,
}
}
}
impl<F> Rule for InlineRule<F>
where
F: for<'a> Fn(&GenerationContext<'a>, &mut dyn Rng) -> Result<ComposedValue, AethelError>,
{
fn name(&self) -> &str {
&self.name
}
fn execute<'a>(
&self,
ctx: &GenerationContext<'a>,
rng: &mut dyn Rng,
) -> Result<ComposedValue, AethelError> {
(self.logic)(ctx, rng)
}
}
pub struct Engine<'a, R: Rng> {
corpus: &'a Corpus,
rng: R,
rules: Vec<Box<dyn Rule>>,
}
impl<'a, R: Rng> Engine<'a, R> {
pub fn new(corpus: &'a Corpus, rng: R) -> Self {
Self {
corpus,
rng,
rules: Vec::new(),
}
}
pub fn with_rule(mut self, rule: impl Rule + 'static) -> Self {
self.rules.push(Box::new(rule));
self
}
pub fn generate(mut self) -> Result<GenerationContext<'a>, AethelError> {
let mut ctx = GenerationContext::new(self.corpus);
for rule in self.rules {
let name = rule.name().to_string();
let result = rule.execute(&ctx, &mut self.rng)?;
ctx.history.insert(name, result);
}
Ok(ctx)
}
}
#[cfg(test)]
mod tests {
use rand::{Rng, SeedableRng, rngs::StdRng};
use crate::corpus::{Corpus, ValueProvenance};
use super::{AethelError, ComposedValue, Engine, GenerationContext, InlineRule, Rule};
fn test_corpus() -> Corpus {
let raw = r#"
[header]
title = "engine test"
target = "weapon"
[name]
first = ["ash", "birch"]
"#;
Corpus::builder("weapon")
.add_str("engine-test", raw)
.build()
.expect("corpus should build")
}
fn provenance(source_id: &str) -> ValueProvenance {
ValueProvenance {
source_id: source_id.to_string(),
document_title: "doc".to_string(),
section: "name".to_string(),
field: "first".to_string(),
}
}
#[test]
fn composed_value_merge_appends_string_and_provenance() {
let left = ComposedValue {
value: "A".to_string(),
provenance: vec![provenance("left")],
};
let right = ComposedValue {
value: "B".to_string(),
provenance: vec![provenance("right")],
};
let merged = left.merge(right);
assert_eq!(merged.value, "AB");
assert_eq!(merged.provenance.len(), 2);
assert_eq!(merged.provenance[0].source_id, "left");
assert_eq!(merged.provenance[1].source_id, "right");
}
#[test]
fn generation_context_get_previous_returns_inserted_value() {
let corpus = test_corpus();
let mut ctx = GenerationContext::new(&corpus);
ctx.history.insert(
"greeting".to_string(),
ComposedValue {
value: "hello".to_string(),
provenance: vec![],
},
);
let found = ctx.get_previous("greeting").expect("key should exist");
assert_eq!(found.value, "hello");
assert!(ctx.get_previous("missing").is_none());
}
#[test]
fn inline_rule_exposes_name_and_executes_logic() {
let corpus = test_corpus();
let ctx = GenerationContext::new(&corpus);
let mut rng = StdRng::seed_from_u64(7);
let rule = InlineRule::new(
"fixed",
|_ctx: &GenerationContext<'_>,
_rng: &mut dyn Rng|
-> Result<ComposedValue, AethelError> {
Ok(ComposedValue {
value: "ok".to_string(),
provenance: vec![],
})
},
);
assert_eq!(rule.name(), "fixed");
let output = rule.execute(&ctx, &mut rng).expect("rule should execute");
assert_eq!(output.value, "ok");
}
#[test]
fn engine_generate_runs_rules_and_tracks_history() {
let corpus = test_corpus();
let rng = StdRng::seed_from_u64(42);
let first = InlineRule::new(
"first",
|_ctx: &GenerationContext<'_>,
_rng: &mut dyn Rng|
-> Result<ComposedValue, AethelError> {
Ok(ComposedValue {
value: "A".to_string(),
provenance: vec![],
})
},
);
let second = InlineRule::new(
"second",
|ctx: &GenerationContext<'_>,
_rng: &mut dyn Rng|
-> Result<ComposedValue, AethelError> {
let base = ctx
.get_previous("first")
.ok_or_else(|| AethelError::MissingDependency("first".to_string()))?;
Ok(ComposedValue {
value: format!("{}B", base.value),
provenance: vec![],
})
},
);
let ctx = Engine::new(&corpus, rng)
.with_rule(first)
.with_rule(second)
.generate()
.expect("engine should generate");
assert_eq!(ctx.history.len(), 2);
assert_eq!(ctx.get_previous("first").expect("first exists").value, "A");
assert_eq!(
ctx.get_previous("second").expect("second exists").value,
"AB"
);
}
}