covid_cert_uvci/
lib.rs

1use itertools::Itertools;
2use luhn::Luhn;
3use std::fmt;
4
5/// EU Digital COVID Certificate UVCI (Unique Vaccination Certificate/Assertion Identifier) data.
6#[derive(Clone)]
7pub struct Uvci {
8    /// Version of the UVCI schema, the version is composed of two digits, 0 for unknown
9    pub version: u8,
10    /// Country code is specified by ISO 3166-1
11    pub country: String,
12    /// EU member states can deploy different option in different version of the UVCI schema
13    pub schema_option_number: u8,
14    /// EU member states can deploy different option in different version of the UVCI schema, 0 for unknown
15    pub schema_option_desc: String,
16    /// The authority issuing the COVID certificate
17    pub issuing_entity: String,
18    /// Vaccine product identifier, vaccine/lot identifier(s) etc
19    pub vaccine_id: String,
20    /// The unique identifier of the vaccination in the national vaccination registry of the corresponding country
21    pub opaque_unique_string: String,
22    /// The unique opaque identifier of the vaccination in the national vaccination registry of the corresponding country
23    pub opaque_id: String,
24    /// The unique opaque issuance of the vaccination in the national vaccination registry of the corresponding country
25    pub opaque_issuance: String,
26    /// The opaque vaccination month of the vaccination in the national vaccination registry of the corresponding country
27    pub opaque_vaccination_month: u8,
28    /// The opaque vaccination year of the vaccination in the national vaccination registry of the corresponding country
29    pub opaque_vaccination_year: u16,
30    /// The ISO-7812-1 (LUHN-10) checksum used to verify the integrity of the UVCI
31    pub checksum: String,
32    /// Checksum verification. For successful verification the value is 'true', else 'false'
33    pub checksum_verification: bool,
34}
35
36/// Display the parsed EU Digital COVID Certificate UVCI (Unique Vaccination Certificate/Assertion Identifier) data
37impl fmt::Display for Uvci {
38    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
39        write!(
40            f,
41            "version                  : {}\n\
42            country                  : {}\n\
43            schema_option_number     : {}\n\
44            schema_option_desc       : {}\n\
45            issuing_entity           : {}\n\
46            vaccine_id               : {}\n\
47            opaque_unique_string     : {}\n\
48            opaque_id                : {}\n\
49            opaque_issuance          : {}\n\
50            opaque_vaccination_month : {}\n\
51            opaque_vaccination_year  : {}\n\
52            checksum                 : {}\n\
53            checksum_verification    : {}\n",
54            &self.version.to_string(),
55            &self.country,
56            &self.schema_option_number.to_string(),
57            &self.schema_option_desc,
58            &self.issuing_entity,
59            &self.vaccine_id,
60            &self.opaque_unique_string,
61            &self.opaque_id,
62            &self.opaque_issuance,
63            &self.opaque_vaccination_month,
64            &self.opaque_vaccination_year,
65            &self.checksum,
66            &self.checksum_verification
67        )
68    }
69}
70
71/// Export a EU Digital COVID Certificate UVCI to CSV
72/// # Arguments
73///
74/// * `cert_id` - the UVCI (Unique Vaccination Certificate/Assertion Identifier), e.g. "URN:UVCI:01:SE:EHM/V12907267LAJW#E"
75pub fn uvci_to_csv(cert_id: &str) -> String {
76    return to_csv(parse(cert_id));
77}
78
79/// Export the parsed EU Digital COVID Certificate UVCI data to CSV
80fn to_csv(uvci: Uvci) -> String {
81    let mut output = "".to_string();
82    output.push_str(&uvci.version.to_string());
83    output.push_str(",");
84    output.push_str(&uvci.country);
85    output.push_str(",");
86    output.push_str(&uvci.schema_option_number.to_string());
87    output.push_str(",");
88    output.push_str(&uvci.schema_option_desc);
89    output.push_str(",");
90    output.push_str(&uvci.issuing_entity);
91    output.push_str(",");
92    output.push_str(&uvci.vaccine_id);
93    output.push_str(",");
94    output.push_str(&uvci.opaque_unique_string);
95    output.push_str(",");
96    output.push_str(&uvci.opaque_id);
97    output.push_str(",");
98    output.push_str(&uvci.opaque_issuance);
99    output.push_str(",");
100    output.push_str(&uvci.opaque_vaccination_month.to_string());
101    output.push_str(",");
102    output.push_str(&uvci.opaque_vaccination_year.to_string());
103    output.push_str(",");
104    output.push_str(&uvci.checksum);
105    output.push_str(",");
106    output.push_str(&uvci.checksum_verification.to_string());
107    return output.to_string();
108}
109
110/// Export a vector of EU Digital COVID Certificate UVCI to Neo4j Cypher Graph
111///
112/// Only for Sweden EHM-issued COVID certificates
113/// # Arguments
114///
115/// * `cert_ids` - String vector of UVCI (Unique Vaccination Certificate/Assertion Identifier)
116pub fn uvcis_to_graph(cert_ids: &Vec<String>) -> String {
117    let mut cypher_cmd = "".to_string();
118    for cert_id in cert_ids {
119        cypher_cmd.push_str(&uvci_to_graph(cert_id));
120    }
121    // Remove duplicates
122    let values: Vec<_> = cypher_cmd.split('\n').collect();
123    let values: Vec<_> = values.into_iter().unique().collect();
124    let cypher_output: String = values.into_iter().collect();
125    let cypher_output = cypher_output.replace("CREATE", "\nCREATE");
126    return cypher_output;
127}
128
129/// Export a EU Digital COVID Certificate UVCI to Neo4j Cypher Graph
130///
131/// Only for Sweden EHM-issued COVID certificates
132/// # Arguments
133///
134/// * `cert_id` - the UVCI (Unique Vaccination Certificate/Assertion Identifier), e.g. "URN:UVCI:01:SE:EHM/V12907267LAJW#E"
135pub fn uvci_to_graph(cert_id: &str) -> String {
136    return to_graph(parse(cert_id));
137}
138
139/// Export the parsed EU Digital COVID Certificate UVCI data to Neo4j Cypher Graph
140/// Only for Sweden EHM-issued COVID certificates
141/// # Arguments
142///
143/// * `cert_id` - the UVCI (Unique Vaccination Certificate/Assertion Identifier), e.g. "URN:UVCI:01:SE:EHM/V12907267LAJW#E"
144fn to_graph(uvci_data: Uvci) -> String {
145    // Only for Sweden EHM-issued COVID certificates
146    if !((uvci_data.version == 1)
147        && (uvci_data.country == "SE")
148        && (uvci_data.issuing_entity == "EHM")
149        && (uvci_data.schema_option_number == 3))
150    {
151        return "".to_string();
152    }
153
154    // Init
155    let mut cypher_cmd = "".to_string();
156    let var_country = "Sweden";
157    let var_issuer = "E-Hälso Myndigheten";
158
159    // CREATE (SE:country {name:'Sweden'})-[:COUNTRY_OF {}]->(EHM:issuing_entity {name:'E-Hälso Myndigheten'})
160    cypher_cmd.push_str("CREATE (");
161    cypher_cmd.push_str(&uvci_data.country);
162    cypher_cmd.push_str(":country {name:'");
163    cypher_cmd.push_str(var_country);
164    cypher_cmd.push_str("'})-[:COUNTRY_OF {}]->(");
165    cypher_cmd.push_str(&uvci_data.issuing_entity);
166    cypher_cmd.push_str(":issuing_entity {name:'");
167    cypher_cmd.push_str(var_issuer);
168    cypher_cmd.push_str("'})\n");
169
170    // CREATE (EHM)-[:ISSUER_OF {}]->(V11916227:opaque_id {name:'V11916227'})
171    cypher_cmd.push_str("CREATE (");
172    cypher_cmd.push_str(&uvci_data.issuing_entity);
173    cypher_cmd.push_str(")-[:ISSUER_OF {}]->(");
174    cypher_cmd.push_str(&uvci_data.opaque_id);
175    cypher_cmd.push_str(":opaque_id {name:'");
176    cypher_cmd.push_str(&uvci_data.opaque_id);
177    cypher_cmd.push_str("'})\n");
178
179    // CREATE (d20218:vac_date {name:'Aug 2021'})
180    let mut var_date_name = "d".to_string();
181    var_date_name.push_str(&uvci_data.opaque_vaccination_year.to_string());
182    var_date_name.push_str(&uvci_data.opaque_vaccination_month.to_string());
183
184    let var_month_name;
185    match uvci_data.opaque_vaccination_month {
186        1 => var_month_name = "Jan".to_string(),
187        2 => var_month_name = "Feb".to_string(),
188        3 => var_month_name = "Mar".to_string(),
189        4 => var_month_name = "Apr".to_string(),
190        5 => var_month_name = "May".to_string(),
191        6 => var_month_name = "Jun".to_string(),
192        7 => var_month_name = "Jul".to_string(),
193        8 => var_month_name = "Aug".to_string(),
194        9 => var_month_name = "Sep".to_string(),
195        10 => var_month_name = "Oct".to_string(),
196        11 => var_month_name = "Nov".to_string(),
197        12 => var_month_name = "Dec".to_string(),
198        _ => var_month_name = "Unknown".to_string(),
199    }
200    let mut var_date_data = "".to_string();
201    var_date_data.push_str(&var_month_name);
202    var_date_data.push_str(" ");
203    var_date_data.push_str(&uvci_data.opaque_vaccination_year.to_string());
204
205    // CREATE (d20218:vac_date {name:'Aug 2021'})
206    cypher_cmd.push_str("CREATE (");
207    cypher_cmd.push_str(&var_date_name);
208    cypher_cmd.push_str(":vac_date {name:'");
209    cypher_cmd.push_str(&var_date_data);
210    cypher_cmd.push_str("'})\n");
211
212    // CREATE (d20218)-[:VAC_DATE_OF {}]->(V12916227)
213    cypher_cmd.push_str("CREATE (");
214    cypher_cmd.push_str(&var_date_name);
215    cypher_cmd.push_str(")-[:VAC_DATE_OF {}]->(");
216    cypher_cmd.push_str(&uvci_data.opaque_id);
217    cypher_cmd.push_str(")\n");
218
219    // CREATE (V11916227TFJJ:reissue_id {name:'TFJJ'})-[:REISSUE_OF {}]->(V11916227)
220    cypher_cmd.push_str("CREATE (");
221    cypher_cmd.push_str(&uvci_data.opaque_unique_string);
222    cypher_cmd.push_str(":reissue_id {name:'");
223    cypher_cmd.push_str(&uvci_data.opaque_issuance);
224    cypher_cmd.push_str("'})-[:REISSUE_OF {}]->(");
225    cypher_cmd.push_str(&uvci_data.opaque_id);
226    cypher_cmd.push_str(")\n");
227
228    // cypher_cmd.push_str("return *");
229    return cypher_cmd;
230}
231
232/// ## EU Digital COVID Certificate UVCI (Unique Vaccination Certificate/Assertion Identifier) Parser
233/// Tool to parse and verify the EU Digital COVID Certificate UVCI (Unique Vaccination Certificate/Assertion Identifier).
234/// Following the conclusions of the European Council of 10-11 December 2020 and of 21 January 2021 that called for
235/// “a coordinated approach to vaccination certificates”, these guidelines establish a unique identifier for vaccination certificates.
236/// This software library parses the EU Digital COVID Certificate UVCI according to eHealth Network Guidelines on
237/// 'verifiable vaccination certificates - basic interoperability elements' - Release 2.
238/// The inclusion of the checksum is optional. The prefix "URN:UVCI:" may be added.
239/// Verification is performed by this crate.
240///
241/// Parsing of Swedish UVCI 'Opaque Unique String' is experimental.
242/// The Swedish vaccination dates are derived from the UVCI aganist national statistics for vaccination against COVID-19.
243/// The statistics is from the Public Health Agency of Sweden (Folkhalsomyndigheten) based on cumulatively number of vaccinations per week.
244/// The Swedish vaccination dates are predicted with an accuracy of approximately +/- 1 month.
245/// Test UVCI is generated using software from Sweden's Agency for Digital Government (Myndigheten för digital förvaltning).
246///
247///
248/// ```no_run
249/// // URN:UVCI:01:SE:EHM/V12916227TFJJ#Q
250/// // version                  : 1
251/// // country                  : SE
252/// // schema_option_number     : 3
253/// // schema_option_desc       : some semantics
254/// // issuing_entity           : EHM
255/// // vaccine_id               :
256/// // opaque_unique_string     : V12916227TFJJ
257/// // opaque_id                : V12916227
258/// // opaque_issuance          : TFJJ
259/// // opaque_vaccination_month : 8
260/// // opaque_vaccination_year  : 2021
261/// // checksum                 : Q
262/// // checksum_verification    : true
263/// //
264/// // URN:UVCI:01:SE:EHM/C878/123456789ABC#B
265/// // version                  : 1
266/// // country                  : SE
267/// // schema_option_number     : 1
268/// // schema_option_desc       : identifier with semantics
269/// // issuing_entity           : EHM
270/// // vaccine_id               : C878
271/// // opaque_unique_string     : 123456789ABC
272/// // opaque_id                :
273/// // opaque_issuance          :
274/// // opaque_vaccination_month : 0
275/// // opaque_vaccination_year  : 0
276/// // checksum                 : B
277/// // checksum_verification    : true
278/// ```
279///
280/// # Arguments
281///
282/// * `cert_id` - the UVCI (Unique Vaccination Certificate/Assertion Identifier), e.g. "URN:UVCI:01:SE:EHM/V12907267LAJW#E"
283pub fn parse(cert_id: &str) -> Uvci {
284    let mut uvci_data = Uvci {
285        version: 0,
286        country: "".to_string(),
287        schema_option_number: 0,
288        schema_option_desc: "".to_string(),
289        issuing_entity: "".to_string(),
290        vaccine_id: "".to_string(),
291        opaque_unique_string: "".to_string(),
292        opaque_id: "".to_string(),
293        opaque_issuance: "".to_string(),
294        opaque_vaccination_month: 0,
295        opaque_vaccination_year: 0,
296        checksum: "".to_string(),
297        checksum_verification: false,
298    };
299
300    // Reject if empty
301    if cert_id.is_empty() {
302        return uvci_data;
303    }
304
305    // Up to a total length of 72 characters
306    if cert_id.len() > 72 {
307        return uvci_data;
308    }
309
310    // Only uppercase characters are allowed
311    let cert_id = cert_id.to_uppercase();
312
313    // Headers
314    let mut cert_id2 = cert_id.clone();
315    if !cert_id.starts_with("URN:UVCI:") {
316        cert_id2 = "URN:UVCI:".to_owned() + &cert_id2;
317    }
318    let cert_id = cert_id2;
319
320    // Verify integrity of the UVCI
321    let l = Luhn::new("/0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ").expect("invalid alphabet given");
322    uvci_data.checksum_verification = l.validate(rearrange(cert_id.to_string())).unwrap();
323
324    // Start parsing
325    let split_checksum = cert_id.split("#");
326    let vec: Vec<&str> = split_checksum.collect();
327    if vec.len() > 1 {
328        uvci_data.checksum = vec[1].to_string();
329    }
330
331    // Verify that the prefix "URN:UVCI:" is added
332    let split_blocks = vec[0].split(":");
333    let vec: Vec<&str> = split_blocks.collect();
334    if vec[0] != "URN" && vec[1] != "UVCI" {
335        return uvci_data;
336    }
337
338    // Detect schema
339    if vec.len() < 4 {
340        return uvci_data;
341    }
342
343    // UVCI schema version
344    let temp = vec[2].to_string();
345    if temp.parse::<u8>().is_ok() {
346        uvci_data.version = temp.parse::<u8>().unwrap();
347    }
348
349    // ISO 3166-1 country code
350    uvci_data.country = vec[3].to_string();
351
352    // Detect schema
353    if vec.len() < 5 {
354        return uvci_data;
355    }
356    let split_options = vec[4].split("/");
357    let vec: Vec<&str> = split_options.collect();
358    match vec.len() {
359        3 => {
360            uvci_data.schema_option_number = 1;
361            uvci_data.schema_option_desc = "identifier with semantics".to_string();
362            uvci_data.issuing_entity = vec[0].to_string();
363            uvci_data.vaccine_id = vec[1].to_string();
364            uvci_data.opaque_unique_string = vec[2].to_string();
365        }
366        1 => {
367            uvci_data.schema_option_number = 2;
368            uvci_data.schema_option_desc = "opaque identifier - no structure".to_string();
369            uvci_data.opaque_unique_string = vec[0].to_string();
370        }
371        2 => {
372            uvci_data.schema_option_number = 3;
373            uvci_data.schema_option_desc = "some semantics".to_string();
374            uvci_data.issuing_entity = vec[0].to_string();
375            uvci_data.opaque_unique_string = vec[1].to_string();
376        }
377        _ => (),
378    }
379
380    // Only for Sweden EHM-issued COVID certificates
381    if (uvci_data.version == 1)
382        && (uvci_data.country == "SE")
383        && (uvci_data.issuing_entity == "EHM")
384        && (uvci_data.schema_option_number == 3)
385    {
386        if uvci_data.opaque_unique_string.len() == 13 {
387            uvci_data.opaque_id = (&uvci_data.opaque_unique_string[0..9]).to_string();
388            uvci_data.opaque_issuance = (&uvci_data.opaque_unique_string[9..13]).to_string();
389
390            let vaccination_date = get_vaccination_date_tan(uvci_data.opaque_id.clone());
391            uvci_data.opaque_vaccination_month = vaccination_date.0;
392            uvci_data.opaque_vaccination_year = vaccination_date.1;
393        }
394    }
395
396    return uvci_data;
397}
398
399/// Rearrange the UVCI characters to enable validation of the checksum
400///
401/// EU Digital COVID Certificate UVCI uses "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/:",
402/// whereas 'luhn-rs' crate uses "/0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
403/// # Arguments
404///
405/// * `cert_id` - the UVCI (Unique Vaccination Certificate/Assertion Identifier), e.g. "URN:UVCI:01:SE:EHM/V12907267LAJW#E"
406fn rearrange(cert_id: String) -> String {
407    let cert_id = cert_id.to_uppercase();
408    let cert_id = cert_id.replace("#", "");
409    let cert_id = cert_id.replace("M", "a");
410    let cert_id = cert_id.replace("N", "b");
411    let cert_id = cert_id.replace("O", "c");
412    let cert_id = cert_id.replace("P", "d");
413    let cert_id = cert_id.replace("Q", "e");
414    let cert_id = cert_id.replace("R", "f");
415    let cert_id = cert_id.replace("S", "g");
416    let cert_id = cert_id.replace("T", "h");
417    let cert_id = cert_id.replace("U", "i");
418    let cert_id = cert_id.replace("V", "j");
419    let cert_id = cert_id.replace("W", "k");
420    let cert_id = cert_id.replace("X", "l");
421    let cert_id = cert_id.replace("Y", "m");
422    let cert_id = cert_id.replace("Z", "m");
423    let cert_id = cert_id.replace("0", "o");
424    let cert_id = cert_id.replace("1", "p");
425    let cert_id = cert_id.replace("2", "q");
426    let cert_id = cert_id.replace("3", "r");
427    let cert_id = cert_id.replace("4", "s");
428    let cert_id = cert_id.replace("5", "t");
429    let cert_id = cert_id.replace("6", "u");
430    let cert_id = cert_id.replace("7", "v");
431    let cert_id = cert_id.replace("8", "w");
432    let cert_id = cert_id.replace("9", "x");
433    let cert_id = cert_id.replace("/", "y");
434    let cert_id = cert_id.replace(":", "z");
435    let cert_id = cert_id.replace("A", "/");
436    let cert_id = cert_id.replace("B", "0");
437    let cert_id = cert_id.replace("C", "1");
438    let cert_id = cert_id.replace("D", "2");
439    let cert_id = cert_id.replace("E", "3");
440    let cert_id = cert_id.replace("F", "4");
441    let cert_id = cert_id.replace("G", "5");
442    let cert_id = cert_id.replace("H", "6");
443    let cert_id = cert_id.replace("I", "7");
444    let cert_id = cert_id.replace("J", "8");
445    let cert_id = cert_id.replace("K", "9");
446    let cert_id = cert_id.replace("L", ":");
447    return cert_id.to_uppercase();
448}
449
450/// Estimate vaccination month & year from opaque_issuance_id in UVCI opaque_unique_string
451///
452/// # Arguments
453///
454/// * `opaque_id` - e.g. "V12907267"
455fn get_vaccination_date_tan(opaque_id: String) -> (u8, u16) {
456    // vaccination_month from 0-xxxx
457    let opaque_id = opaque_id.replace("V", "");
458    if !opaque_id.parse::<f32>().is_ok() {
459        return (0, 0);
460    }
461    let mut vaccination_doses = opaque_id.parse::<f32>().unwrap();
462
463    // Reject negative numbers
464    if vaccination_doses < 0.0 {
465        return (0, 0);
466    }
467
468    let mut vaccination_month;
469    if vaccination_doses <= 13983264.0 {
470        // Use tangent cruve
471        vaccination_doses = (6991632.0 - vaccination_doses) / 5536858.0;
472        let mth_f = 5.03 + ((-vaccination_doses.tan()) * 1.6);
473        let mth_u8 = mth_f.round() as u16;
474        vaccination_month = mth_u8;
475    } else {
476        // Assuming 1552008 doses a month
477        vaccination_month = (vaccination_doses / 1552008.0) as u16;
478    }
479
480    // vaccination_year from 2020-xxxx
481    let vaccination_year;
482    if vaccination_month == 0 {
483        vaccination_year = 2020;
484    } else {
485        vaccination_year = ((vaccination_month - 1) / 12) + 2021;
486    }
487
488    // Reformat vaccination_month from 0-11 to 1-12
489    if vaccination_month == 0 {
490        vaccination_month = 12;
491    }
492    while vaccination_month > 12 {
493        vaccination_month = vaccination_month - 12;
494    }
495
496    // Return data
497    return (vaccination_month as u8, vaccination_year as u16);
498}
499
500#[cfg(test)]
501mod tests {
502    use super::get_vaccination_date_tan;
503    use super::parse;
504    use super::uvci_to_csv;
505
506    #[test]
507    fn uvci_csv() {
508        assert!(
509            uvci_to_csv("URN:UVCI:01:SE:EHM/V00016227TFJJ#Q")
510                == "1,SE,3,some semantics,EHM,,V00016227TFJJ,V00016227,TFJJ,12,2020,Q,false"
511        );
512    }
513
514    #[test]
515    fn swedish_uvci_opaque_date() {
516        assert!(
517            get_vaccination_date_tan("0".to_string()) == (12, 2020),
518            "Dec, Wrong date"
519        );
520        assert!(
521            get_vaccination_date_tan("2014920".to_string()) == (3, 2021),
522            "March, Wrong date"
523        );
524        assert!(
525            get_vaccination_date_tan("6991632".to_string()) == (5, 2021),
526            "May, Wrong date"
527        );
528        assert!(
529            get_vaccination_date_tan("12916227".to_string()) == (8, 2021),
530            "Aug, Wrong date"
531        );
532        assert!(
533            get_vaccination_date_tan("13592955".to_string()) == (9, 2021),
534            "Sep, Wrong date"
535        );
536        assert!(
537            get_vaccination_date_tan("13983264".to_string()) == (10, 2021),
538            "Oct, Wrong date"
539        );
540        assert!(
541            get_vaccination_date_tan("99999999".to_string()) == (4, 2026),
542            "Max, wrong date"
543        );
544        // Sweden Population = 10427296, Reference period: August 2021
545        assert!(
546            get_vaccination_date_tan("10427296".to_string()) == (6, 2021),
547            "Single dose, wrong date"
548        );
549        assert!(
550            get_vaccination_date_tan("20854592".to_string()) == (1, 2022),
551            "Double dose, wrong date"
552        );
553        assert!(
554            get_vaccination_date_tan("31281888".to_string()) == (8, 2022),
555            "Double dose + booster, wrong date"
556        );
557    }
558
559    #[test]
560    fn swedish_uvci_opaque_data() {
561        assert!(
562            parse("URN:UVCI:01:SE:EHM/V12907267LAJW#E").opaque_unique_string == "V12907267LAJW",
563            "wrong opaque_unique_string"
564        );
565        assert!(
566            parse("URN:UVCI:01:SE:EHM/V12907267LAJW#E").opaque_id == "V12907267",
567            "wrong opaque_id"
568        );
569        assert!(
570            parse("URN:UVCI:01:SE:EHM/V12907267LAJW#E").opaque_issuance == "LAJW",
571            "wrong opaque_issuance"
572        );
573        assert!(
574            parse("URN:UVCI:01:SE:EHM/V12907267LAJW#E").opaque_vaccination_month == 8,
575            "wrong opaque_vaccination_month"
576        );
577        assert!(
578            parse("URN:UVCI:01:SE:EHM/V12907267LAJW#E").opaque_vaccination_year == 2021,
579            "wrong opaque_vaccination_month"
580        );
581    }
582
583    #[test]
584    fn swedish_uvci_with_checksum_valid() {
585        let cert_ids_sweden: [&str; 15] = [
586            "URN:UVCI:01:SE:EHM/V12907267LAJW#E",
587            "URN:UVCI:01:SE:EHM/V12916227TFJJ#Q",
588            "URN:UVCI:01:SE:EHM/V12920064NYOH#4",
589            "URN:UVCI:01:SE:EHM/V12923931NNBY#T",
590            "URN:UVCI:01:SE:EHM/V12939008LSVR#F",
591            "URN:UVCI:01:SE:EHM/V12939037PXFJ#V",
592            "URN:UVCI:01:SE:EHM/V12940126MRXQ#N",
593            "URN:UVCI:01:SE:EHM/V12956472WRGE#7",
594            "URN:UVCI:01:SE:EHM/V12965046ALNM#I",
595            "URN:UVCI:01:SE:EHM/V12982924YQMV#T",
596            "URN:UVCI:01:SE:EHM/V12991074UCIC#4",
597            "URN:UVCI:01:SE:EHM/V12993686OVCX#R",
598            "URN:UVCI:01:SE:EHM/V12996544DVKM#M",
599            "URN:UVCI:01:SE:EHM/V12997980ASMG#1",
600            "URN:UVCI:01:SE:EHM/V12998404MNQF#6",
601        ];
602        for cert_id in &cert_ids_sweden {
603            println!("{}\n{}\n", cert_id, parse(cert_id));
604            assert!(
605                parse(cert_id).checksum_verification,
606                "checksum verification failed"
607            );
608        }
609    }
610
611    #[test]
612    fn swedish_uvci_with_checksum_invalid() {
613        let cert_ids_sweden: [&str; 15] = [
614            "URN:UVCI:01:SE:EHM/V12907267LAJW#A",
615            "URN:UVCI:01:SE:EHM/V12916227TFJJ#B",
616            "URN:UVCI:01:SE:EHM/V12920064NYOH#C",
617            "URN:UVCI:01:SE:EHM/V12923931NNBY#D",
618            "URN:UVCI:01:SE:EHM/V12939008LSVR#E",
619            "URN:UVCI:01:SE:EHM/V12939037PXFJ#F",
620            "URN:UVCI:01:SE:EHM/V12940126MRXQ#G",
621            "URN:UVCI:01:SE:EHM/V12956472WRGE#H",
622            "URN:UVCI:01:SE:EHM/V12965046ALNM#0",
623            "URN:UVCI:01:SE:EHM/V12982924YQMV#1",
624            "URN:UVCI:01:SE:EHM/V12991074UCIC#2",
625            "URN:UVCI:01:SE:EHM/V12993686OVCX#3",
626            "URN:UVCI:01:SE:EHM/V12996544DVKM#4",
627            "URN:UVCI:01:SE:EHM/V12997980ASMG#5",
628            "URN:UVCI:01:SE:EHM/V12998404MNQF#9",
629        ];
630        for cert_id in &cert_ids_sweden {
631            println!("{}\n{}\n", cert_id, parse(cert_id));
632            assert!(
633                !parse(cert_id).checksum_verification,
634                "checksum verification failed"
635            );
636        }
637    }
638
639    #[test]
640    fn assorted_uvci() {
641        let cert_ids_assorted: [&str; 18] = [
642            "",
643            "a",
644            "::::::::::",
645            "//////////",
646            "a:a:a:a:a:a:a:a:a:a:a",
647            "URN:UVCI:01:SE://////////",
648            "URN:UVCI:01:AT:10807843F94AEE0EE5093FBC254BD8131080784F94AEE0E43C25D813#B",
649            "URN:UVCI:01:SE:EHM/C878/123456789ABC",
650            "URN:UVCI:01:SE:EHM/C878/123456789ABC#B",
651            "01:SE:EHM/C878/123456789ABC#B",
652            "URN:UVCI:01:SE:123456789ABC",
653            "URN:UVCI:01:AT:10807843F94AEE0EE5093FBC254BD813#B",
654            "URN:UVCI:01:SE:EHM/V12916227TFJJ#Q",
655            "URN:UVCI:01:NL:187/37512422923",
656            "urn:uvci:01:se:ehm/v12982924yqmv#t",
657            "urn:uvci:98:se:ehm/v12982924yqmv#t",
658            "URN:UVCI:01:IT:84A0F1A35F1D454C96939812CA55D571#F",
659            "01:IT:84A0F1A35F1D454C96939812CA55D571#F",
660        ];
661
662        for cert_id in &cert_ids_assorted {
663            println!("{}\n{}\n", cert_id, parse(cert_id));
664            assert!(
665                parse(cert_id).schema_option_number <= 3,
666                "schema_option_number larger than 3"
667            );
668        }
669    }
670}