From 30799ddb0bb39e7d13448330be01f8add38d34e1 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 18 May 2026 16:16:08 +0200 Subject: [PATCH] feat: core domain types and Filter predicate --- src/model.rs | 144 ++++++++++++++++++++++++++++++++++++++++++- tests/model_tests.rs | 20 ++++++ 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 tests/model_tests.rs diff --git a/src/model.rs b/src/model.rs index 4b0fd9d..24e1855 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1 +1,143 @@ -// implemented in a later task +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Debug, Clone)] +pub struct AlphaIdEntry { + pub alpha_id: String, + pub valid: bool, + pub icd_primary: String, // Field 3, normalized below + pub icd_star: String, // Field 4 + pub icd_addon: String, // Field 5 ("!") — NOT ICD exclusion + pub icd_primary2: String, // Field 6 + pub orpha: String, // Field 7 + pub text: String, // Field 8 +} + +#[derive(Debug, Clone, Default)] +pub struct IcdMeta { + pub code: String, // normalized, no + * ! + pub description: String, + pub chapter: String, + pub group: String, + pub para295: String, // P/O/Z/V + pub para301: String, + pub exotic: bool, + pub ifsg: bool, + pub content: bool, +} + +#[derive(Debug, Clone, Serialize)] +pub struct Tags { + pub billable: bool, // para295 == "P" + pub valid: bool, // Alpha-ID field 1 + pub chapter: String, + pub exotic: bool, +} + +#[derive(Debug, Clone)] +pub struct Filter { + pub billable_only: bool, + pub valid_only: bool, + pub chapters: Option>, + pub exclude_exotic: bool, +} + +impl Default for Filter { + fn default() -> Self { + Filter { billable_only: false, valid_only: false, chapters: None, exclude_exotic: false } + } +} + +impl Filter { + pub fn accepts(&self, t: &Tags) -> bool { + if self.billable_only && !t.billable { return false; } + if self.valid_only && !t.valid { return false; } + if self.exclude_exotic && t.exotic { return false; } + if let Some(ch) = &self.chapters { + if !ch.iter().any(|c| c == &t.chapter) { return false; } + } + true + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Mode { C, A } + +impl FromStr for Mode { + type Err = String; + fn from_str(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "c" => Ok(Mode::C), + "a" => Ok(Mode::A), + other => Err(format!("unknown mode: {other}")), + } + } +} + +#[derive(Debug, Clone)] +pub struct Candidate { + pub icd_code: String, + pub alpha_text: String, + pub segment_idx: usize, + pub lexical: Option, + pub semantic: Option, + pub rerank: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct Suggestion { + pub icd_code: String, + pub description: String, + pub score: f32, + pub matched_phrase: String, + pub source_segments: Vec, + pub tags: Tags, +} + +#[derive(Debug, Clone, Serialize, Default)] +pub struct Diagnostics { + pub mode: String, + pub degraded: bool, + pub segment_count: usize, + pub ionos_calls: usize, + pub millis: u128, +} + +#[derive(Debug, Serialize)] +pub struct SuggestResult { + pub suggestions: Vec, + pub diagnostics: Diagnostics, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + pub ionos_base_url: String, + pub token_path: String, + pub embed_model: String, + pub rerank_model: String, + pub alpha_id_path: String, + pub claml_path: String, + pub index_dir: String, + pub pool_size: usize, + pub top_k: usize, +} + +impl Config { + pub fn load(path: &str) -> Result { + let s = std::fs::read_to_string(path) + .map_err(|e| AppError::Io(format!("config {path}: {e}")))?; + toml::from_str(&s).map_err(|e| AppError::Parse(format!("config: {e}"))) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum AppError { + #[error("io error: {0}")] + Io(String), + #[error("parse error: {0}")] + Parse(String), + #[error("ionos error: {0}")] + Ionos(String), + #[error("config error: {0}")] + Config(String), +} diff --git a/tests/model_tests.rs b/tests/model_tests.rs new file mode 100644 index 0000000..0c89587 --- /dev/null +++ b/tests/model_tests.rs @@ -0,0 +1,20 @@ +use alpha_id::model::{Filter, Tags, Mode}; + +#[test] +fn filter_billable_only_rejects_non_primary() { + let tags = Tags { billable: false, valid: true, chapter: "I".into(), exotic: false }; + let f = Filter { billable_only: true, valid_only: false, chapters: None, exclude_exotic: false }; + assert!(!f.accepts(&tags)); +} + +#[test] +fn filter_default_accepts_everything() { + let tags = Tags { billable: false, valid: false, chapter: "II".into(), exotic: true }; + assert!(Filter::default().accepts(&tags)); +} + +#[test] +fn mode_parses_from_str() { + assert_eq!("a".parse::().unwrap(), Mode::A); + assert_eq!("c".parse::().unwrap(), Mode::C); +}