edi/
transaction.rs

1use crate::edi_parse_error::EdiParseError;
2use crate::generic_segment::GenericSegment;
3use crate::tokenizer::SegmentTokens;
4use csv::ReaderBuilder;
5use lazy_static::lazy_static;
6use rust_embed::RustEmbed;
7use serde::{Deserialize, Serialize};
8use std::borrow::Cow;
9use std::collections::{HashMap, VecDeque};
10
11/// Represents a transaction in an EDI document. A transaction is initialized with an ST segment
12/// and ended with an SE segment.
13#[derive(PartialEq, Debug, Serialize, Deserialize)]
14pub struct Transaction<'a> {
15    /// The numeric code which represents the type of transaction.
16    #[serde(borrow)]
17    pub transaction_code: Cow<'a, str>,
18    /// The name of the transaction type in human-readable form.
19    #[serde(borrow)]
20    pub transaction_name: Cow<'a, str>,
21    /// Each transaction within a functional group also has a control number.
22    /// Typically, trading partners use a number relative to the functional group in which they are contained.
23    #[serde(borrow)]
24    pub transaction_set_control_number: Cow<'a, str>,
25    /// Identifier of the implementation convention reference. Valid value is up to 35 standard characters. Optional.
26    #[serde(borrow)]
27    pub implementation_convention_reference: Option<Cow<'a, str>>,
28    /// The [GenericSegment]s contained within this transaction.
29    #[serde(borrow)]
30    pub segments: VecDeque<GenericSegment<'a>>,
31}
32
33#[derive(RustEmbed)]
34#[folder = "$CARGO_MANIFEST_DIR/resources"]
35#[prefix = "resources/"]
36struct Resources;
37
38// Load the potential transaction schema names from a csv
39// source: scraped from https://www.arcesb.com/edi/standards/x12/
40lazy_static! {
41    static ref SCHEMAS: HashMap<String, String> = {
42        let mut map = HashMap::new();
43        let file_content = Resources::get("resources/schemas.csv").unwrap();
44        let file_content_str = String::from_utf8_lossy(file_content.data.as_ref());
45        let mut schemas_csv = ReaderBuilder::new()
46            .has_headers(false)
47            .from_reader(file_content_str.as_bytes());
48        for record in schemas_csv.records() {
49            let record = record.unwrap();
50            map.insert(record[0].to_string(), record[1].to_string());
51        }
52        map
53    };
54}
55
56impl<'a> Transaction<'a> {
57    /// Given [SegmentTokens] (where the first token is "ST"), construct a [Transaction].
58    pub(crate) fn parse_from_tokens(
59        input: SegmentTokens<'a>,
60    ) -> Result<Transaction<'a>, EdiParseError> {
61        let elements: Vec<&str> = input.iter().map(|x| x.trim()).collect();
62        // I always inject invariants wherever I can to ensure debugging is quick and painless,
63        // and to check my assumptions.
64        edi_assert!(
65            elements[0] == "ST",
66            "attempted to parse ST from non-ST segment",
67            input
68        );
69        edi_assert!(
70            elements.len() >= 3,
71            "ST segment does not contain enough elements. At least 3 required",
72            input
73        );
74
75        let (transaction_code, transaction_set_control_number) =
76            (Cow::from(elements[1]), Cow::from(elements[2]));
77        let implementation_convention_reference = if elements.len() >= 4 {
78            Some(Cow::from(elements[3]))
79        } else {
80            None
81        };
82        let transaction_name = if let Some(name) = SCHEMAS.get(&transaction_code.to_string()) {
83            name
84        } else {
85            "unidentified"
86        };
87
88        Ok(Transaction {
89            transaction_code,
90            transaction_name: Cow::from(transaction_name),
91            transaction_set_control_number,
92            implementation_convention_reference,
93            segments: VecDeque::new(),
94        })
95    }
96
97    /// Enqueue a [GenericSegment](struct.GenericSegment.html) into the transaction.
98    pub(crate) fn add_generic_segment(
99        &mut self,
100        tokens: SegmentTokens<'a>,
101    ) -> Result<(), EdiParseError> {
102        self.segments
103            .push_back(GenericSegment::parse_from_tokens(tokens)?);
104        Ok(())
105    }
106
107    /// Validate this transaction with an SE segment.
108    pub(crate) fn validate_transaction(
109        &self,
110        tokens: SegmentTokens<'a>,
111    ) -> Result<(), EdiParseError> {
112        edi_assert!(
113            tokens[0] == "SE",
114            "attempted to validate transaction with non-SE segment",
115            tokens
116        );
117        // we have to add two here because transaction counts include ST and SE
118        edi_assert!(
119            str::parse::<usize>(tokens[1]).unwrap() == self.segments.len() + 2,
120            "transaction validation failed: incorrect number of segments",
121            tokens[1],
122            self.segments.len() + 2,
123            tokens
124        );
125        edi_assert!(
126            tokens[2] == self.transaction_set_control_number,
127            "transaction validation failed: incorrect transaction ID",
128            tokens[2],
129            self.transaction_set_control_number,
130            tokens
131        );
132        Ok(())
133    }
134
135    /// Converts this [Transaction] into an ANSI x12 string to be used in an EDI document.
136    pub fn to_x12_string(&self, segment_delimiter: char, element_delimiter: char) -> String {
137        let mut header = "ST".to_string();
138        header.push(element_delimiter);
139        header.push_str(&self.transaction_code);
140        header.push(element_delimiter);
141        header.push_str(&self.transaction_set_control_number);
142        header.push(element_delimiter);
143        header.push_str(
144            &self
145                .implementation_convention_reference
146                .clone()
147                .unwrap_or(Cow::Borrowed("")),
148        );
149
150        let mut final_string = self.segments.iter().fold(header, |mut acc, segment| {
151            acc.push(segment_delimiter);
152            acc.push_str(&segment.to_x12_string(element_delimiter));
153            acc
154        });
155
156        let mut closer = "SE".to_string();
157        closer.push(element_delimiter);
158        closer.push_str(&(self.segments.len() + 2).to_string()); // +2 because the count includes the ST and SE segments
159        closer.push(element_delimiter);
160        closer.push_str(&self.transaction_set_control_number.clone());
161
162        final_string.push(segment_delimiter);
163        final_string.push_str(&closer);
164
165        final_string
166    }
167}
168
169#[test]
170fn transaction_to_string() {
171    use std::iter::FromIterator;
172    let segments = VecDeque::from_iter(vec![
173        GenericSegment {
174            segment_abbreviation: Cow::from("BGN"),
175            elements: ["20", "TEST_ID", "200615", "0000"]
176                .iter()
177                .map(|x| Cow::from(*x))
178                .collect::<VecDeque<Cow<str>>>(),
179        },
180        GenericSegment {
181            segment_abbreviation: Cow::from("BGN"),
182            elements: ["15", "OTHER_TEST_ID", "", "", "END"]
183                .iter()
184                .map(|x| Cow::from(*x))
185                .collect::<VecDeque<Cow<str>>>(),
186        },
187    ]);
188    let transaction = Transaction {
189        transaction_code: Cow::from("140"),
190        transaction_name: Cow::from(""),
191        transaction_set_control_number: Cow::from("100000001"),
192        implementation_convention_reference: None,
193        segments,
194    };
195
196    assert_eq!(
197        transaction.to_x12_string('~', '*'),
198        "ST*140*100000001*~BGN*20*TEST_ID*200615*0000~BGN*15*OTHER_TEST_ID***END~SE*4*100000001"
199    );
200}
201
202#[test]
203fn construct_transaction() {
204    let expected_result = Transaction {
205        transaction_code: Cow::from("850"),
206        transaction_name: Cow::from(SCHEMAS.get(&"850".to_string()).unwrap()), // should be "Purchase Order"
207        transaction_set_control_number: Cow::from("000000001"),
208        implementation_convention_reference: None,
209        segments: VecDeque::new(),
210    };
211    let test_input = vec!["ST", "850", "000000001"];
212
213    assert_eq!(
214        Transaction::parse_from_tokens(test_input).unwrap(),
215        expected_result
216    );
217}
218
219#[test]
220fn spot_check_schemas() {
221    assert_eq!(SCHEMAS.get(&"850".to_string()).unwrap(), "Purchase Order");
222    assert_eq!(
223        SCHEMAS.get(&"100".to_string()).unwrap(),
224        "Insurance Plan Description"
225    );
226    assert_eq!(
227        SCHEMAS.get(&"999".to_string()).unwrap(),
228        "Implementation Acknowledgment"
229    );
230}