use std::fmt::Debug;
use serde::de::DeserializeOwned;
use crate::aggregable::digest::AggregationSink;
use crate::traits::LinguisticDefinition;
#[derive(Debug, Clone)]
pub struct LanguageLevel {
pub iso_639_3: String,
pub level: String,
}
pub struct ComponentContext<'a> {
pub targets: &'a [String],
pub learner_ui_language: &'a str,
pub pedagogical_context: Option<&'a str>,
pub skill_path: Option<&'a str>,
pub linguistic_background: &'a [LanguageLevel],
}
pub trait AnalysisComponent<L: LinguisticDefinition>: Send + Sync + Debug {
fn name(&self) -> &'static str;
fn schema_key(&self) -> &'static str;
fn schema_fragment(&self, lang: &L) -> serde_json::Value;
fn prompt_fragment(&self, lang: &L, ctx: &ComponentContext) -> String;
fn output_instruction(&self) -> Option<&str> {
None
}
fn pre_process(&self, raw: &str) -> String {
raw.to_string()
}
fn validate(&self, _lang: &L, _section: &serde_json::Value) -> Result<(), String> {
Ok(())
}
fn post_process(&self, _lang: &L, _section: &mut serde_json::Value) -> Result<(), String> {
Ok(())
}
fn is_compatible(&self, _lang: &L) -> bool {
true
}
fn as_aggregating(&self) -> Option<&dyn Aggregating<L>> {
None
}
}
#[derive(Debug, thiserror::Error)]
pub enum ExtractionResultError {
#[error("key not found: {key}")]
KeyNotFound { key: String },
#[error("deserialization error for key '{key}': {source}")]
DeserializeError {
key: String,
source: serde_json::Error,
},
}
#[derive(Debug, Clone)]
pub struct ExtractionResult {
raw: serde_json::Value,
requested_keys: Vec<&'static str>,
}
impl ExtractionResult {
#[must_use]
pub const fn new(raw: serde_json::Value, requested_keys: Vec<&'static str>) -> Self {
Self {
raw,
requested_keys,
}
}
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, ExtractionResultError> {
let section = self
.raw
.get(key)
.ok_or_else(|| ExtractionResultError::KeyNotFound {
key: key.to_string(),
})?;
serde_json::from_value(section.clone()).map_err(|e| {
ExtractionResultError::DeserializeError {
key: key.to_string(),
source: e,
}
})
}
#[must_use]
pub fn get_raw(&self, key: &str) -> Option<&serde_json::Value> {
self.raw.get(key)
}
pub fn iter_raw(&self) -> impl Iterator<Item = (&str, &serde_json::Value)> {
self.raw
.as_object()
.into_iter()
.flat_map(|obj| obj.iter().map(|(k, v)| (k.as_str(), v)))
}
#[must_use]
pub fn requested_keys(&self) -> &[&'static str] {
&self.requested_keys
}
#[must_use]
pub fn into_raw(self) -> serde_json::Value {
self.raw
}
}
#[derive(Debug, thiserror::Error)]
pub enum AggregationError {
#[error("failed to deserialize section '{key}': {source}")]
Deserialize {
key: &'static str,
#[source]
source: serde_json::Error,
},
#[error("aggregation hook for '{key}' failed: {message}")]
Hook { key: &'static str, message: String },
}
pub trait Aggregating<L: LinguisticDefinition>: AnalysisComponent<L> {
fn aggregate_section(
&self,
lang: &L,
section: &serde_json::Value,
sink: &mut dyn AggregationSink,
) -> Result<(), AggregationError>;
}
pub trait ComponentRequires<L> {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_typed_value() {
let raw = serde_json::json!({
"pedagogical_explanation": "This is a test.",
"morphology": { "target_features": [], "context_features": [] }
});
let result = ExtractionResult::new(raw, vec!["pedagogical_explanation", "morphology"]);
let explanation: String = result.get("pedagogical_explanation").unwrap();
assert_eq!(explanation, "This is a test.");
}
#[test]
fn get_missing_key_returns_key_not_found() {
let raw = serde_json::json!({ "morphology": {} });
let result = ExtractionResult::new(raw, vec!["morphology"]);
let err = result.get::<String>("nonexistent").unwrap_err();
assert!(matches!(err, ExtractionResultError::KeyNotFound { .. }));
}
#[test]
fn get_raw_returns_section() {
let raw = serde_json::json!({ "morphology": { "target_features": [] } });
let result = ExtractionResult::new(raw, vec!["morphology"]);
assert!(result.get_raw("morphology").is_some());
assert!(result.get_raw("nonexistent").is_none());
}
#[test]
fn iter_raw_returns_all_entries() {
let raw = serde_json::json!({
"a": 1,
"b": 2,
"c": 3
});
let result = ExtractionResult::new(raw, vec![]);
let keys: Vec<&str> = result.iter_raw().map(|(k, _)| k).collect();
assert_eq!(keys.len(), 3);
assert!(keys.contains(&"a"));
assert!(keys.contains(&"b"));
assert!(keys.contains(&"c"));
}
#[test]
fn into_raw_consumes() {
let raw = serde_json::json!({ "key": "value" });
let result = ExtractionResult::new(raw.clone(), vec!["key"]);
assert_eq!(result.into_raw(), raw);
}
}