|
3 | 3 | use pyo3::prelude::*; |
4 | 4 | use pyo3::types::{PyDict, PyList}; |
5 | 5 |
|
| 6 | +use crate::analyze::{ |
| 7 | + self, AnnotationKind, DiagnosticCode, Severity, ValueAccessor, |
| 8 | +}; |
6 | 9 | use crate::lexer::Token; |
7 | 10 | use crate::AstNode; |
8 | 11 |
|
@@ -77,10 +80,166 @@ fn compute_text(node: &AstNode, tokens: &[Token]) -> String { |
77 | 80 | s |
78 | 81 | } |
79 | 82 |
|
| 83 | +// ── QuestionnaireIndex wrapper ────────────────────────────────────────── |
| 84 | + |
| 85 | +#[pyclass(name = "QuestionnaireIndex")] |
| 86 | +struct PyQuestionnaireIndex { |
| 87 | + inner: analyze::QuestionnaireIndex, |
| 88 | +} |
| 89 | + |
| 90 | +#[pymethods] |
| 91 | +impl PyQuestionnaireIndex { |
| 92 | + #[new] |
| 93 | + fn new(questionnaire_json: &str) -> PyResult<Self> { |
| 94 | + let value: serde_json::Value = serde_json::from_str(questionnaire_json) |
| 95 | + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; |
| 96 | + Ok(Self { |
| 97 | + inner: analyze::QuestionnaireIndex::build(&value), |
| 98 | + }) |
| 99 | + } |
| 100 | + |
| 101 | + fn resolve_item_text(&self, link_id: &str) -> Option<String> { |
| 102 | + self.inner.resolve_item_text(link_id).map(|s| s.to_string()) |
| 103 | + } |
| 104 | + |
| 105 | + fn resolve_code_display(&self, link_id: &str, system: &str, code: &str) -> Option<String> { |
| 106 | + self.inner |
| 107 | + .resolve_code_display(link_id, system, code) |
| 108 | + .map(|s| s.to_string()) |
| 109 | + } |
| 110 | + |
| 111 | + fn contains(&self, link_id: &str) -> bool { |
| 112 | + self.inner.contains(link_id) |
| 113 | + } |
| 114 | +} |
| 115 | + |
| 116 | +// ── Annotation / analysis helpers ────────────────────────────────────── |
| 117 | + |
| 118 | +fn accessor_to_str(accessor: &ValueAccessor) -> &'static str { |
| 119 | + match accessor { |
| 120 | + ValueAccessor::Value => "value", |
| 121 | + ValueAccessor::Code => "code", |
| 122 | + ValueAccessor::Display => "display", |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +fn severity_to_str(severity: &Severity) -> &'static str { |
| 127 | + match severity { |
| 128 | + Severity::Error => "error", |
| 129 | + Severity::Warning => "warning", |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +fn diagnostic_code_to_str(code: &DiagnosticCode) -> &'static str { |
| 134 | + match code { |
| 135 | + DiagnosticCode::UnknownLinkId => "unknown_link_id", |
| 136 | + DiagnosticCode::UnreachableLinkId => "unreachable_link_id", |
| 137 | + DiagnosticCode::InvalidAccessorForType => "invalid_accessor_for_type", |
| 138 | + DiagnosticCode::MissingAccessorForCoding => "missing_accessor_for_coding", |
| 139 | + DiagnosticCode::ItemReferenceTargetsLeaf => "item_reference_targets_leaf", |
| 140 | + DiagnosticCode::ContextUnreachableFromParent => "context_unreachable_from_parent", |
| 141 | + } |
| 142 | +} |
| 143 | + |
| 144 | +fn annotation_to_pydict(py: Python<'_>, ann: &analyze::Annotation) -> PyResult<PyObject> { |
| 145 | + let dict = PyDict::new(py); |
| 146 | + dict.set_item("start", ann.span.start)?; |
| 147 | + dict.set_item("end", ann.span.end)?; |
| 148 | + match &ann.kind { |
| 149 | + AnnotationKind::AnswerReference { link_ids, accessor } => { |
| 150 | + dict.set_item("kind", "answer_reference")?; |
| 151 | + let ids = PyList::new(py, link_ids.iter().map(|s| s.as_str()).collect::<Vec<_>>())?; |
| 152 | + dict.set_item("link_ids", ids)?; |
| 153 | + dict.set_item("accessor", accessor_to_str(accessor))?; |
| 154 | + } |
| 155 | + AnnotationKind::ItemReference { link_ids } => { |
| 156 | + dict.set_item("kind", "item_reference")?; |
| 157 | + let ids = PyList::new(py, link_ids.iter().map(|s| s.as_str()).collect::<Vec<_>>())?; |
| 158 | + dict.set_item("link_ids", ids)?; |
| 159 | + } |
| 160 | + AnnotationKind::CodedValue { |
| 161 | + code, |
| 162 | + system, |
| 163 | + context_link_id, |
| 164 | + } => { |
| 165 | + dict.set_item("kind", "coded_value")?; |
| 166 | + dict.set_item("code", code.as_str())?; |
| 167 | + match system { |
| 168 | + Some(s) => dict.set_item("system", s.as_str())?, |
| 169 | + None => dict.set_item("system", py.None())?, |
| 170 | + } |
| 171 | + dict.set_item("context_link_id", context_link_id.as_str())?; |
| 172 | + } |
| 173 | + } |
| 174 | + Ok(dict.into()) |
| 175 | +} |
| 176 | + |
| 177 | +fn diagnostic_to_pydict(py: Python<'_>, diag: &analyze::Diagnostic) -> PyResult<PyObject> { |
| 178 | + let dict = PyDict::new(py); |
| 179 | + dict.set_item("severity", severity_to_str(&diag.severity))?; |
| 180 | + dict.set_item("code", diagnostic_code_to_str(&diag.code))?; |
| 181 | + dict.set_item("message", diag.message.as_str())?; |
| 182 | + dict.set_item("start", diag.span.start)?; |
| 183 | + dict.set_item("end", diag.span.end)?; |
| 184 | + Ok(dict.into()) |
| 185 | +} |
| 186 | + |
| 187 | +// ── Python-facing functions ──────────────────────────────────────────── |
| 188 | + |
| 189 | +/// Annotate a FHIRPath expression, returning a list of annotation dicts. |
| 190 | +#[pyfunction] |
| 191 | +fn annotate_expression(py: Python<'_>, expr: &str) -> PyResult<PyObject> { |
| 192 | + let annotations = analyze::annotate_expression(expr) |
| 193 | + .map_err(|e| pyo3::exceptions::PySyntaxError::new_err(e.0))?; |
| 194 | + let result: Vec<PyObject> = annotations |
| 195 | + .iter() |
| 196 | + .map(|a| annotation_to_pydict(py, a)) |
| 197 | + .collect::<PyResult<Vec<_>>>()?; |
| 198 | + Ok(PyList::new(py, result)?.into()) |
| 199 | +} |
| 200 | + |
| 201 | +/// Analyze a FHIRPath expression against a QuestionnaireIndex, returning |
| 202 | +/// annotations and diagnostics. |
| 203 | +#[pyfunction] |
| 204 | +#[pyo3(signature = (expr, index, context_link_id=None, parent_context_expr=None))] |
| 205 | +fn analyze_expression( |
| 206 | + py: Python<'_>, |
| 207 | + expr: &str, |
| 208 | + index: &PyQuestionnaireIndex, |
| 209 | + context_link_id: Option<&str>, |
| 210 | + parent_context_expr: Option<&str>, |
| 211 | +) -> PyResult<PyObject> { |
| 212 | + let context = analyze::AnalysisContext { |
| 213 | + scope_link_id: context_link_id.map(|s| s.to_string()), |
| 214 | + parent_context_expr: parent_context_expr.map(|s| s.to_string()), |
| 215 | + }; |
| 216 | + let result = analyze::analyze_expression(expr, &index.inner, &context) |
| 217 | + .map_err(|e| pyo3::exceptions::PySyntaxError::new_err(e.0))?; |
| 218 | + |
| 219 | + let annotations: Vec<PyObject> = result |
| 220 | + .annotations |
| 221 | + .iter() |
| 222 | + .map(|a| annotation_to_pydict(py, a)) |
| 223 | + .collect::<PyResult<Vec<_>>>()?; |
| 224 | + let diagnostics: Vec<PyObject> = result |
| 225 | + .diagnostics |
| 226 | + .iter() |
| 227 | + .map(|d| diagnostic_to_pydict(py, d)) |
| 228 | + .collect::<PyResult<Vec<_>>>()?; |
| 229 | + |
| 230 | + let dict = PyDict::new(py); |
| 231 | + dict.set_item("annotations", PyList::new(py, annotations)?)?; |
| 232 | + dict.set_item("diagnostics", PyList::new(py, diagnostics)?)?; |
| 233 | + Ok(dict.into()) |
| 234 | +} |
| 235 | + |
80 | 236 | #[pymodule] |
81 | 237 | pub fn _rust(m: &Bound<'_, PyModule>) -> PyResult<()> { |
82 | 238 | m.add("IMPLEMENTED", true)?; |
83 | 239 | m.add_function(wrap_pyfunction!(parse, m)?)?; |
| 240 | + m.add_class::<PyQuestionnaireIndex>()?; |
| 241 | + m.add_function(wrap_pyfunction!(annotate_expression, m)?)?; |
| 242 | + m.add_function(wrap_pyfunction!(analyze_expression, m)?)?; |
84 | 243 | Ok(()) |
85 | 244 | } |
86 | 245 |
|
|
0 commit comments