Skip to main content

rxnorm_rrf/
row.rs

1// SPDX-FileCopyrightText: Cadasto B.V.
2// SPDX-License-Identifier: BUSL-1.1
3
4//! The typed rows, by the documented column positions.
5
6use std::path::Path;
7
8use crate::RrfError;
9
10/// A row type read from a byte record.
11pub trait FromRecord: Sized {
12    /// Reads the row at `line` of `path`.
13    ///
14    /// # Errors
15    ///
16    /// Returns [`RrfError`] when the record is short, not UTF-8, or carries
17    /// a malformed identifier.
18    fn from_record(record: &csv::ByteRecord, path: &Path, line: u64) -> Result<Self, RrfError>;
19}
20
21/// The fields of a record as strings, checked for count and UTF-8.
22struct Fields<'a> {
23    record: &'a csv::ByteRecord,
24    path: &'a Path,
25    line: u64,
26}
27
28impl Fields<'_> {
29    fn new<'a>(
30        record: &'a csv::ByteRecord,
31        path: &'a Path,
32        line: u64,
33        expected: usize,
34    ) -> Result<Fields<'a>, RrfError> {
35        if record.len() < expected {
36            return Err(RrfError::Columns {
37                path: path.to_path_buf(),
38                line,
39                columns: record.len(),
40                expected,
41            });
42        }
43        Ok(Fields { record, path, line })
44    }
45
46    fn text(&self, index: usize) -> Result<String, RrfError> {
47        let bytes = self.record.get(index).unwrap_or_default();
48        std::str::from_utf8(bytes)
49            .map(str::to_owned)
50            .map_err(|source| RrfError::Utf8 {
51                path: self.path.to_path_buf(),
52                line: self.line,
53                source,
54            })
55    }
56
57    fn optional(&self, index: usize) -> Result<Option<String>, RrfError> {
58        let text = self.text(index)?;
59        Ok((!text.is_empty()).then_some(text))
60    }
61
62    fn id(&self, index: usize) -> Result<u64, RrfError> {
63        let text = self.text(index)?;
64        text.parse().map_err(|source| RrfError::Identifier {
65            path: self.path.to_path_buf(),
66            line: self.line,
67            value: text,
68            source,
69        })
70    }
71
72    fn optional_id(&self, index: usize) -> Result<Option<u64>, RrfError> {
73        match self.optional(index)? {
74            None => Ok(None),
75            Some(text) => text
76                .parse()
77                .map(Some)
78                .map_err(|source| RrfError::Identifier {
79                    path: self.path.to_path_buf(),
80                    line: self.line,
81                    value: text,
82                    source,
83                }),
84        }
85    }
86}
87
88/// One `RXNCONSO` row: an atom (a name from one source) of a concept.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct Atom {
91    /// `RXCUI`: the concept.
92    pub rxcui: u64,
93    /// `LAT`: the language (`ENG`).
94    pub language: String,
95    /// `RXAUI`: the atom.
96    pub rxaui: u64,
97    /// `SAB`: the source vocabulary (`RXNORM`, `MTHSPL`, ...).
98    pub sab: String,
99    /// `TTY`: the term type (`SCD`, `SBD`, `IN`, ...).
100    pub tty: String,
101    /// `CODE`: the source's own code for the atom.
102    pub code: String,
103    /// `STR`: the name.
104    pub name: String,
105    /// `SUPPRESS`: `N`, `O`, `Y`, or `E`.
106    pub suppress: String,
107}
108
109impl FromRecord for Atom {
110    fn from_record(record: &csv::ByteRecord, path: &Path, line: u64) -> Result<Self, RrfError> {
111        let f = Fields::new(record, path, line, 18)?;
112        Ok(Self {
113            rxcui: f.id(0)?,
114            language: f.text(1)?,
115            rxaui: f.id(7)?,
116            sab: f.text(11)?,
117            tty: f.text(12)?,
118            code: f.text(13)?,
119            name: f.text(14)?,
120            suppress: f.text(16)?,
121        })
122    }
123}
124
125/// One `RXNREL` row: the second concept or atom has `rel` (and `rela`) to the first.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct Relationship {
128    /// `RXCUI1`, absent for an atom-level row.
129    pub rxcui1: Option<u64>,
130    /// `RXAUI1`, absent for a concept-level row.
131    pub rxaui1: Option<u64>,
132    /// `REL`: `RO`, `RN`, `RB`, `SY`, `PAR`, `CHD`, `SIB`.
133    pub rel: String,
134    /// `RXCUI2`, absent for an atom-level row.
135    pub rxcui2: Option<u64>,
136    /// `RXAUI2`, absent for a concept-level row.
137    pub rxaui2: Option<u64>,
138    /// `RELA`: the relationship label (`has_ingredient`, `isa`, ...).
139    pub rela: Option<String>,
140    /// `SAB`: the source asserting the relationship.
141    pub sab: String,
142}
143
144impl FromRecord for Relationship {
145    fn from_record(record: &csv::ByteRecord, path: &Path, line: u64) -> Result<Self, RrfError> {
146        let f = Fields::new(record, path, line, 16)?;
147        Ok(Self {
148            rxcui1: f.optional_id(0)?,
149            rxaui1: f.optional_id(1)?,
150            rel: f.text(3)?,
151            rxcui2: f.optional_id(4)?,
152            rxaui2: f.optional_id(5)?,
153            rela: f.optional(7)?,
154            sab: f.text(10)?,
155        })
156    }
157}
158
159/// One `RXNSAT` row: an attribute of a concept or atom.
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct Attribute {
162    /// `RXCUI`.
163    pub rxcui: u64,
164    /// `RXAUI`, absent for a concept-level attribute.
165    pub rxaui: Option<u64>,
166    /// `ATN`: the attribute name (`NDC`, `RXN_AVAILABLE_STRENGTH`, ...).
167    pub name: String,
168    /// `SAB`: the source asserting the attribute.
169    pub sab: String,
170    /// `ATV`: the value.
171    pub value: String,
172    /// `SUPPRESS`.
173    pub suppress: String,
174}
175
176impl FromRecord for Attribute {
177    fn from_record(record: &csv::ByteRecord, path: &Path, line: u64) -> Result<Self, RrfError> {
178        let f = Fields::new(record, path, line, 13)?;
179        Ok(Self {
180            rxcui: f.id(0)?,
181            rxaui: f.optional_id(3)?,
182            name: f.text(8)?,
183            sab: f.text(9)?,
184            value: f.text(10)?,
185            suppress: f.text(11)?,
186        })
187    }
188}
189
190/// One `RXNSTY` row: a semantic type of a concept.
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct SemanticType {
193    /// `RXCUI`.
194    pub rxcui: u64,
195    /// `TUI`: the semantic type identifier (`T121`).
196    pub tui: String,
197    /// `STY`: the semantic type name (`Pharmacologic Substance`).
198    pub name: String,
199}
200
201impl FromRecord for SemanticType {
202    fn from_record(record: &csv::ByteRecord, path: &Path, line: u64) -> Result<Self, RrfError> {
203        let f = Fields::new(record, path, line, 6)?;
204        Ok(Self {
205            rxcui: f.id(0)?,
206            tui: f.text(1)?,
207            name: f.text(3)?,
208        })
209    }
210}