use std::io::Read;
use crate::constants;
use crate::id::{ConceptId, DescriptionId, ModuleId, RelationshipId, Sctid};
use crate::reader::{FieldError, Record, Rf2Error, Rf2Reader};
use crate::time::EffectiveTime;
pub trait Component: Sized {
const COLUMNS: &[&str];
fn from_record(record: &Record<'_>) -> Result<Self, Rf2Error>;
}
#[derive(Debug)]
pub struct Rows<R: Read, T: Component> {
reader: Rf2Reader<R>,
marker: std::marker::PhantomData<T>,
}
impl<R: Read, T: Component> Rows<R, T> {
pub fn new(path: &std::path::Path, read: R) -> Result<Self, Rf2Error> {
Ok(Self {
reader: Rf2Reader::new(path, read, T::COLUMNS)?,
marker: std::marker::PhantomData,
})
}
}
impl<T: Component> Rows<std::io::BufReader<std::fs::File>, T> {
pub fn open(path: &std::path::Path) -> Result<Self, Rf2Error> {
Ok(Self {
reader: Rf2Reader::open(path, T::COLUMNS)?,
marker: std::marker::PhantomData,
})
}
}
impl<R: Read, T: Component> Iterator for Rows<R, T> {
type Item = Result<T, Rf2Error>;
fn next(&mut self) -> Option<Self::Item> {
match self.reader.next_record() {
Ok(Some(record)) => Some(T::from_record(&record)),
Ok(None) => None,
Err(e) => Some(Err(e)),
}
}
}
pub trait CharacteristicTyped {
fn characteristic_type_id(&self) -> ConceptId;
}
impl CharacteristicTyped for Relationship {
fn characteristic_type_id(&self) -> ConceptId {
self.characteristic_type_id
}
}
impl CharacteristicTyped for ConcreteRelationship {
fn characteristic_type_id(&self) -> ConceptId {
self.characteristic_type_id
}
}
impl<R: Read, T: Component + CharacteristicTyped> Rows<R, T> {
#[must_use]
pub fn inferred(self) -> InferredRows<R, T> {
InferredRows {
rows: self,
skipped: 0,
}
}
}
#[derive(Debug)]
pub struct InferredRows<R: Read, T: Component + CharacteristicTyped> {
rows: Rows<R, T>,
skipped: u64,
}
impl<R: Read, T: Component + CharacteristicTyped> InferredRows<R, T> {
#[must_use]
pub fn skipped(&self) -> u64 {
self.skipped
}
}
impl<R: Read, T: Component + CharacteristicTyped> Iterator for InferredRows<R, T> {
type Item = Result<T, Rf2Error>;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.rows.next()? {
Ok(row) if row.characteristic_type_id() != constants::INFERRED => {
self.skipped = self.skipped.saturating_add(1);
}
row => return Some(row),
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComponentBase {
pub effective_time: EffectiveTime,
pub active: bool,
pub module_id: ModuleId,
}
fn base(record: &Record<'_>) -> Result<ComponentBase, Rf2Error> {
Ok(ComponentBase {
effective_time: record.parse(1, EffectiveTime::parse)?,
active: record.boolean(2)?,
module_id: record.parse(3, ModuleId::parse)?,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Concept {
pub id: ConceptId,
pub base: ComponentBase,
pub definition_status_id: ConceptId,
}
impl Component for Concept {
const COLUMNS: &[&str] = &[
"id",
"effectiveTime",
"active",
"moduleId",
"definitionStatusId",
];
fn from_record(record: &Record<'_>) -> Result<Self, Rf2Error> {
Ok(Self {
id: record.parse(0, ConceptId::parse)?,
base: base(record)?,
definition_status_id: record.parse(4, ConceptId::parse)?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Description {
pub id: DescriptionId,
pub base: ComponentBase,
pub concept_id: ConceptId,
pub language_code: String,
pub type_id: ConceptId,
pub term: String,
pub case_significance_id: ConceptId,
}
impl Component for Description {
const COLUMNS: &[&str] = &[
"id",
"effectiveTime",
"active",
"moduleId",
"conceptId",
"languageCode",
"typeId",
"term",
"caseSignificanceId",
];
fn from_record(record: &Record<'_>) -> Result<Self, Rf2Error> {
Ok(Self {
id: record.parse(0, DescriptionId::parse)?,
base: base(record)?,
concept_id: record.parse(4, ConceptId::parse)?,
language_code: record.text(5)?.to_owned(),
type_id: record.parse(6, ConceptId::parse)?,
term: record.text(7)?.to_owned(),
case_significance_id: record.parse(8, ConceptId::parse)?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[expect(
clippy::struct_field_names,
reason = "the fields are the RF2 columns, named as the release file specification names them"
)]
pub struct Relationship {
pub id: RelationshipId,
pub base: ComponentBase,
pub source_id: ConceptId,
pub destination_id: ConceptId,
pub relationship_group: u32,
pub type_id: ConceptId,
pub characteristic_type_id: ConceptId,
pub modifier_id: ConceptId,
}
impl Component for Relationship {
const COLUMNS: &[&str] = &[
"id",
"effectiveTime",
"active",
"moduleId",
"sourceId",
"destinationId",
"relationshipGroup",
"typeId",
"characteristicTypeId",
"modifierId",
];
fn from_record(record: &Record<'_>) -> Result<Self, Rf2Error> {
Ok(Self {
id: record.parse(0, RelationshipId::parse)?,
base: base(record)?,
source_id: record.parse(4, ConceptId::parse)?,
destination_id: record.parse(5, ConceptId::parse)?,
relationship_group: group(record, 6)?,
type_id: record.parse(7, ConceptId::parse)?,
characteristic_type_id: record.parse(8, ConceptId::parse)?,
modifier_id: record.parse(9, ConceptId::parse)?,
})
}
}
fn group(record: &Record<'_>, index: usize) -> Result<u32, Rf2Error> {
let value = record.integer(index)?;
let Ok(group) = u32::try_from(value) else {
return Err(record.field_error(
index,
FieldError::Integer {
text: value.to_string(),
},
));
};
Ok(group)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConcreteValue {
Number(String),
String(String),
}
impl ConcreteValue {
fn parse(text: &str) -> Result<Self, FieldError> {
if let Some(number) = text.strip_prefix('#') {
let valid = !number.is_empty()
&& number
.bytes()
.all(|b| b.is_ascii_digit() || b == b'.' || b == b'-')
&& number.bytes().filter(|b| *b == b'.').count() <= 1;
return if valid {
Ok(Self::Number(number.to_owned()))
} else {
Err(FieldError::Invalid {
what: "concrete number",
text: text.to_owned(),
})
};
}
if let Some(inner) = text.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
return Ok(Self::String(inner.to_owned()));
}
Err(FieldError::Invalid {
what: "concrete value",
text: text.to_owned(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConcreteRelationship {
pub id: RelationshipId,
pub base: ComponentBase,
pub source_id: ConceptId,
pub value: ConcreteValue,
pub relationship_group: u32,
pub type_id: ConceptId,
pub characteristic_type_id: ConceptId,
pub modifier_id: ConceptId,
}
impl Component for ConcreteRelationship {
const COLUMNS: &[&str] = &[
"id",
"effectiveTime",
"active",
"moduleId",
"sourceId",
"value",
"relationshipGroup",
"typeId",
"characteristicTypeId",
"modifierId",
];
fn from_record(record: &Record<'_>) -> Result<Self, Rf2Error> {
Ok(Self {
id: record.parse(0, RelationshipId::parse)?,
base: base(record)?,
source_id: record.parse(4, ConceptId::parse)?,
value: record.parse(5, ConcreteValue::parse)?,
relationship_group: group(record, 6)?,
type_id: record.parse(7, ConceptId::parse)?,
characteristic_type_id: record.parse(8, ConceptId::parse)?,
modifier_id: record.parse(9, ConceptId::parse)?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[expect(
clippy::struct_field_names,
reason = "the fields are the RF2 columns, named as the release file specification names them"
)]
pub struct AlternateIdentifier {
pub alternate_identifier: String,
pub base: ComponentBase,
pub identifier_scheme_id: ConceptId,
pub referenced_component_id: Sctid,
}
impl Component for AlternateIdentifier {
const COLUMNS: &[&str] = &[
"alternateIdentifier",
"effectiveTime",
"active",
"moduleId",
"identifierSchemeId",
"referencedComponentId",
];
fn from_record(record: &Record<'_>) -> Result<Self, Rf2Error> {
Ok(Self {
alternate_identifier: record.text(0)?.to_owned(),
base: base(record)?,
identifier_scheme_id: record.parse(4, ConceptId::parse)?,
referenced_component_id: record.parse(5, Sctid::parse)?,
})
}
}
#[cfg(test)]
mod tests {
use super::ConcreteValue;
#[test]
fn concrete_values_keep_their_marker_semantics() {
assert_eq!(
ConcreteValue::parse("#3"),
Ok(ConcreteValue::Number("3".to_owned()))
);
assert_eq!(
ConcreteValue::parse("#2.5"),
Ok(ConcreteValue::Number("2.5".to_owned()))
);
assert_eq!(
ConcreteValue::parse("\"mg\""),
Ok(ConcreteValue::String("mg".to_owned()))
);
assert!(ConcreteValue::parse("3").is_err());
assert!(ConcreteValue::parse("#").is_err());
assert!(ConcreteValue::parse("\"open").is_err());
}
}