panini_lang_core/
component.rs1use std::fmt::Debug;
2
3use serde::de::DeserializeOwned;
4
5use crate::aggregable::digest::AggregationSink;
6use crate::traits::LinguisticDefinition;
7
8#[derive(Debug, Clone)]
10pub struct LanguageLevel {
11 pub iso_639_3: String,
12 pub level: String,
13}
14
15pub struct ComponentContext<'a> {
17 pub targets: &'a [String],
18 pub learner_ui_language: &'a str,
19 pub pedagogical_context: Option<&'a str>,
20 pub skill_path: Option<&'a str>,
21 pub linguistic_background: &'a [LanguageLevel],
22}
23
24pub trait AnalysisComponent<L: LinguisticDefinition>: Send + Sync + Debug {
31 fn name(&self) -> &'static str;
33
34 fn schema_key(&self) -> &'static str;
36
37 fn schema_fragment(&self, lang: &L) -> serde_json::Value;
40
41 fn prompt_fragment(&self, lang: &L, ctx: &ComponentContext) -> String;
43
44 fn output_instruction(&self) -> Option<&str> {
46 None
47 }
48
49 fn pre_process(&self, raw: &str) -> String {
52 raw.to_string()
53 }
54
55 fn validate(&self, _lang: &L, _section: &serde_json::Value) -> Result<(), String> {
60 Ok(())
61 }
62
63 fn post_process(&self, _lang: &L, _section: &mut serde_json::Value) -> Result<(), String> {
68 Ok(())
69 }
70
71 fn is_compatible(&self, _lang: &L) -> bool {
74 true
75 }
76
77 fn as_aggregating(&self) -> Option<&dyn Aggregating<L>> {
82 None
83 }
84}
85
86#[derive(Debug, thiserror::Error)]
90pub enum ExtractionResultError {
91 #[error("key not found: {key}")]
92 KeyNotFound { key: String },
93 #[error("deserialization error for key '{key}': {source}")]
94 DeserializeError {
95 key: String,
96 source: serde_json::Error,
97 },
98}
99
100#[derive(Debug, Clone)]
105pub struct ExtractionResult {
106 raw: serde_json::Value,
107 requested_keys: Vec<&'static str>,
108}
109
110impl ExtractionResult {
111 #[must_use]
114 pub const fn new(raw: serde_json::Value, requested_keys: Vec<&'static str>) -> Self {
115 Self {
116 raw,
117 requested_keys,
118 }
119 }
120
121 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, ExtractionResultError> {
127 let section = self
128 .raw
129 .get(key)
130 .ok_or_else(|| ExtractionResultError::KeyNotFound {
131 key: key.to_string(),
132 })?;
133 serde_json::from_value(section.clone()).map_err(|e| {
134 ExtractionResultError::DeserializeError {
135 key: key.to_string(),
136 source: e,
137 }
138 })
139 }
140
141 #[must_use]
143 pub fn get_raw(&self, key: &str) -> Option<&serde_json::Value> {
144 self.raw.get(key)
145 }
146
147 pub fn iter_raw(&self) -> impl Iterator<Item = (&str, &serde_json::Value)> {
149 self.raw
150 .as_object()
151 .into_iter()
152 .flat_map(|obj| obj.iter().map(|(k, v)| (k.as_str(), v)))
153 }
154
155 #[must_use]
157 pub fn requested_keys(&self) -> &[&'static str] {
158 &self.requested_keys
159 }
160
161 #[must_use]
163 pub fn into_raw(self) -> serde_json::Value {
164 self.raw
165 }
166}
167
168#[derive(Debug, thiserror::Error)]
172pub enum AggregationError {
173 #[error("failed to deserialize section '{key}': {source}")]
174 Deserialize {
175 key: &'static str,
176 #[source]
177 source: serde_json::Error,
178 },
179 #[error("aggregation hook for '{key}' failed: {message}")]
180 Hook { key: &'static str, message: String },
181}
182
183pub trait Aggregating<L: LinguisticDefinition>: AnalysisComponent<L> {
191 fn aggregate_section(
198 &self,
199 lang: &L,
200 section: &serde_json::Value,
201 sink: &mut dyn AggregationSink,
202 ) -> Result<(), AggregationError>;
203}
204
205pub trait ComponentRequires<L> {}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn get_typed_value() {
221 let raw = serde_json::json!({
222 "pedagogical_explanation": "This is a test.",
223 "morphology": { "target_features": [], "context_features": [] }
224 });
225 let result = ExtractionResult::new(raw, vec!["pedagogical_explanation", "morphology"]);
226
227 let explanation: String = result.get("pedagogical_explanation").unwrap();
228 assert_eq!(explanation, "This is a test.");
229 }
230
231 #[test]
232 fn get_missing_key_returns_key_not_found() {
233 let raw = serde_json::json!({ "morphology": {} });
234 let result = ExtractionResult::new(raw, vec!["morphology"]);
235
236 let err = result.get::<String>("nonexistent").unwrap_err();
237 assert!(matches!(err, ExtractionResultError::KeyNotFound { .. }));
238 }
239
240 #[test]
241 fn get_raw_returns_section() {
242 let raw = serde_json::json!({ "morphology": { "target_features": [] } });
243 let result = ExtractionResult::new(raw, vec!["morphology"]);
244
245 assert!(result.get_raw("morphology").is_some());
246 assert!(result.get_raw("nonexistent").is_none());
247 }
248
249 #[test]
250 fn iter_raw_returns_all_entries() {
251 let raw = serde_json::json!({
252 "a": 1,
253 "b": 2,
254 "c": 3
255 });
256 let result = ExtractionResult::new(raw, vec![]);
257
258 let keys: Vec<&str> = result.iter_raw().map(|(k, _)| k).collect();
259 assert_eq!(keys.len(), 3);
260 assert!(keys.contains(&"a"));
261 assert!(keys.contains(&"b"));
262 assert!(keys.contains(&"c"));
263 }
264
265 #[test]
266 fn into_raw_consumes() {
267 let raw = serde_json::json!({ "key": "value" });
268 let result = ExtractionResult::new(raw.clone(), vec!["key"]);
269 assert_eq!(result.into_raw(), raw);
270 }
271}