use std::io::Read;
use std::path::Path;
use crate::file::FieldKind;
use crate::id::{ConceptId, MemberId, ModuleId, RefsetId, Sctid};
use crate::reader::{FieldError, Record, Rf2Error, Rf2Reader};
use crate::time::EffectiveTime;
pub const MEMBER_COLUMNS: [&str; 6] = [
"id",
"effectiveTime",
"active",
"moduleId",
"refsetId",
"referencedComponentId",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldValue {
Component(Sctid),
Integer(i64),
String(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Member {
pub id: MemberId,
pub effective_time: EffectiveTime,
pub active: bool,
pub module_id: ModuleId,
pub refset_id: RefsetId,
pub referenced_component_id: Sctid,
pub fields: Vec<(String, FieldValue)>,
}
impl Member {
#[must_use]
pub fn field(&self, column: &str) -> Option<&FieldValue> {
self.fields
.iter()
.find(|(name, _)| name == column)
.map(|(_, value)| value)
}
fn component(&self, column: &str) -> Result<Sctid, ViewError> {
match self.field(column) {
Some(FieldValue::Component(id)) => Ok(*id),
other => Err(ViewError::field(column, other)),
}
}
fn concept(&self, column: &str) -> Result<ConceptId, ViewError> {
let id = self.component(column)?;
let Ok(id) = ConceptId::try_from(id) else {
return Err(ViewError::field(column, self.field(column)));
};
Ok(id)
}
fn string(&self, column: &str) -> Result<&str, ViewError> {
match self.field(column) {
Some(FieldValue::String(text)) => Ok(text),
other => Err(ViewError::field(column, other)),
}
}
fn integer(&self, column: &str) -> Result<i64, ViewError> {
match self.field(column) {
Some(FieldValue::Integer(value)) => Ok(*value),
other => Err(ViewError::field(column, other)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ViewError {
#[error("column {column} is {found}")]
Field {
column: String,
found: String,
},
#[error("column {column}: {source}")]
Value {
column: String,
#[source]
source: FieldError,
},
}
impl ViewError {
fn field(column: &str, found: Option<&FieldValue>) -> Self {
Self::Field {
column: column.to_owned(),
found: match found {
None => String::from("absent"),
Some(FieldValue::Component(_)) => String::from("a component reference"),
Some(FieldValue::Integer(_)) => String::from("an integer"),
Some(FieldValue::String(_)) => String::from("a string"),
},
}
}
}
#[derive(Debug)]
pub struct Members<R: Read> {
reader: Rf2Reader<R>,
kinds: Vec<FieldKind>,
}
impl Members<std::io::BufReader<std::fs::File>> {
pub fn open(path: &Path, kinds: &[FieldKind]) -> Result<Self, Rf2Error> {
let file = std::fs::File::open(path).map_err(|source| Rf2Error::Io {
path: path.to_path_buf(),
source,
})?;
Self::new(path, std::io::BufReader::new(file), kinds)
}
}
impl<R: Read> Members<R> {
pub fn new(path: &Path, read: R, kinds: &[FieldKind]) -> Result<Self, Rf2Error> {
let reader = Rf2Reader::new_with(path, read, |actual| {
let fixed_ok = actual.len() == MEMBER_COLUMNS.len() + kinds.len()
&& actual.iter().zip(MEMBER_COLUMNS).all(|(a, e)| a == e);
if fixed_ok {
Ok(())
} else {
let mut expected: Vec<String> =
MEMBER_COLUMNS.iter().map(|c| (*c).to_owned()).collect();
expected.extend(kinds.iter().map(|k| format!("<{k:?}>")));
Err(Rf2Error::Header {
path: path.to_path_buf(),
expected,
actual: actual.to_vec(),
})
}
})?;
Ok(Self {
reader,
kinds: kinds.to_vec(),
})
}
fn member(kinds: &[FieldKind], record: &Record<'_>) -> Result<Member, Rf2Error> {
let mut fields = Vec::with_capacity(kinds.len());
for (offset, kind) in kinds.iter().enumerate() {
let index = MEMBER_COLUMNS.len() + offset;
let name = record.column_name(index).unwrap_or_default().to_owned();
let value = match kind {
FieldKind::Component => FieldValue::Component(record.parse(index, Sctid::parse)?),
FieldKind::Integer => FieldValue::Integer(record.integer(index)?),
FieldKind::String => FieldValue::String(record.text(index)?.to_owned()),
};
fields.push((name, value));
}
Ok(Member {
id: record.parse(0, MemberId::parse)?,
effective_time: record.parse(1, EffectiveTime::parse)?,
active: record.boolean(2)?,
module_id: record.parse(3, ModuleId::parse)?,
refset_id: record.parse(4, RefsetId::parse)?,
referenced_component_id: record.parse(5, Sctid::parse)?,
fields,
})
}
}
impl<R: Read> Iterator for Members<R> {
type Item = Result<Member, Rf2Error>;
fn next(&mut self) -> Option<Self::Item> {
match self.reader.next_record() {
Ok(Some(record)) => Some(Self::member(&self.kinds, &record)),
Ok(None) => None,
Err(e) => Some(Err(e)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LanguageMember {
pub member: Member,
pub acceptability_id: ConceptId,
}
impl TryFrom<Member> for LanguageMember {
type Error = ViewError;
fn try_from(member: Member) -> Result<Self, ViewError> {
let acceptability_id = member.concept("acceptabilityId")?;
Ok(Self {
member,
acceptability_id,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssociationMember {
pub member: Member,
pub target_component_id: Sctid,
}
impl TryFrom<Member> for AssociationMember {
type Error = ViewError;
fn try_from(member: Member) -> Result<Self, ViewError> {
let target_component_id = member.component("targetComponentId")?;
Ok(Self {
member,
target_component_id,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttributeValueMember {
pub member: Member,
pub value_id: ConceptId,
}
impl TryFrom<Member> for AttributeValueMember {
type Error = ViewError;
fn try_from(member: Member) -> Result<Self, ViewError> {
let value_id = member.concept("valueId")?;
Ok(Self { member, value_id })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SimpleMapMember {
pub member: Member,
pub map_target: String,
}
impl TryFrom<Member> for SimpleMapMember {
type Error = ViewError;
fn try_from(member: Member) -> Result<Self, ViewError> {
let map_target = member.string("mapTarget")?.to_owned();
Ok(Self { member, map_target })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtendedMapMember {
pub member: Member,
pub map_group: i64,
pub map_priority: i64,
pub map_rule: String,
pub map_advice: String,
pub map_target: String,
pub correlation_id: ConceptId,
pub map_category_id: ConceptId,
}
impl TryFrom<Member> for ExtendedMapMember {
type Error = ViewError;
fn try_from(member: Member) -> Result<Self, ViewError> {
Ok(Self {
map_group: member.integer("mapGroup")?,
map_priority: member.integer("mapPriority")?,
map_rule: member.string("mapRule")?.to_owned(),
map_advice: member.string("mapAdvice")?.to_owned(),
map_target: member.string("mapTarget")?.to_owned(),
correlation_id: member.concept("correlationId")?,
map_category_id: member.concept("mapCategoryId")?,
member,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModuleDependencyMember {
pub member: Member,
pub source_effective_time: EffectiveTime,
pub target_effective_time: EffectiveTime,
}
impl TryFrom<Member> for ModuleDependencyMember {
type Error = ViewError;
fn try_from(member: Member) -> Result<Self, ViewError> {
let time = |column: &str| -> Result<EffectiveTime, ViewError> {
EffectiveTime::parse(member.string(column)?).map_err(|source| ViewError::Value {
column: column.to_owned(),
source: source.into(),
})
};
Ok(Self {
source_effective_time: time("sourceEffectiveTime")?,
target_effective_time: time("targetEffectiveTime")?,
member,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OwlExpressionMember {
pub member: Member,
pub owl_expression: String,
}
impl TryFrom<Member> for OwlExpressionMember {
type Error = ViewError;
fn try_from(member: Member) -> Result<Self, ViewError> {
let owl_expression = member.string("owlExpression")?.to_owned();
Ok(Self {
member,
owl_expression,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DescriptorMember {
pub member: Member,
pub attribute_description: ConceptId,
pub attribute_type: ConceptId,
pub attribute_order: i64,
}
impl TryFrom<Member> for DescriptorMember {
type Error = ViewError;
fn try_from(member: Member) -> Result<Self, ViewError> {
Ok(Self {
attribute_description: member.concept("attributeDescription")?,
attribute_type: member.concept("attributeType")?,
attribute_order: member.integer("attributeOrder")?,
member,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DescriptionTypeMember {
pub member: Member,
pub description_format: ConceptId,
pub description_length: i64,
}
impl TryFrom<Member> for DescriptionTypeMember {
type Error = ViewError;
fn try_from(member: Member) -> Result<Self, ViewError> {
Ok(Self {
description_format: member.concept("descriptionFormat")?,
description_length: member.integer("descriptionLength")?,
member,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefsetKind {
Language,
OwlExpression,
ModuleDependency,
Content,
}
impl RefsetKind {
#[must_use]
pub fn of_columns(columns: &[String]) -> Self {
let has = |name: &str| columns.iter().any(|column| column == name);
if has("acceptabilityId") {
Self::Language
} else if has("owlExpression") {
Self::OwlExpression
} else if has("sourceEffectiveTime") && has("targetEffectiveTime") {
Self::ModuleDependency
} else {
Self::Content
}
}
}
pub fn kind(path: &Path) -> Result<RefsetKind, Rf2Error> {
let file = std::fs::File::open(path).map_err(|source| Rf2Error::Io {
path: path.to_path_buf(),
source,
})?;
let reader = Rf2Reader::new_with(path, std::io::BufReader::new(file), |actual| {
if actual.len() >= MEMBER_COLUMNS.len()
&& actual.iter().zip(MEMBER_COLUMNS).all(|(a, e)| a == e)
{
Ok(())
} else {
Err(Rf2Error::Header {
path: path.to_path_buf(),
expected: MEMBER_COLUMNS.iter().map(|c| (*c).to_owned()).collect(),
actual: actual.to_vec(),
})
}
})?;
let additional: Vec<String> = reader
.columns()
.iter()
.skip(MEMBER_COLUMNS.len())
.cloned()
.collect();
Ok(RefsetKind::of_columns(&additional))
}
#[cfg(test)]
mod kind_tests {
use super::RefsetKind;
fn columns(names: &[&str]) -> Vec<String> {
names.iter().map(|n| (*n).to_owned()).collect()
}
#[test]
fn the_additional_columns_name_the_kind() {
assert_eq!(
RefsetKind::of_columns(&columns(&["acceptabilityId"])),
RefsetKind::Language
);
assert_eq!(
RefsetKind::of_columns(&columns(&["owlExpression"])),
RefsetKind::OwlExpression
);
assert_eq!(
RefsetKind::of_columns(&columns(&["sourceEffectiveTime", "targetEffectiveTime"])),
RefsetKind::ModuleDependency
);
assert_eq!(
RefsetKind::of_columns(&columns(&["targetComponentId"])),
RefsetKind::Content
);
assert_eq!(RefsetKind::of_columns(&[]), RefsetKind::Content);
}
}