haste_fhir_search/pg_search/
keys.rs1use std::collections::HashMap;
14
15use sha2::{Digest, Sha256};
16
17#[must_use]
20pub fn scope_key(tenant: &str, project: &str) -> i64 {
21 hash_parts(&[b"scope", tenant.as_bytes(), project.as_bytes()])
22}
23
24#[must_use]
26pub fn param_identity(tenant: &str, project: &str, param_url: &str) -> i64 {
27 hash_parts(&[
28 b"param",
29 tenant.as_bytes(),
30 project.as_bytes(),
31 param_url.as_bytes(),
32 ])
33}
34
35pub fn check_identities<'a>(
41 tenant: &str,
42 project: &str,
43 param_urls: impl IntoIterator<Item = &'a str>,
44) -> Result<(), (String, String)> {
45 param_urls
46 .into_iter()
47 .try_fold(HashMap::<i64, &str>::new(), |mut seen, url| {
48 match seen.insert(param_identity(tenant, project, url), url) {
49 Some(existing) if existing != url => Err((existing.to_string(), url.to_string())),
50 _ => Ok(seen),
51 }
52 })
53 .map(|_| ())
54}
55
56fn hash_parts(parts: &[&[u8]]) -> i64 {
58 let digest = parts
59 .iter()
60 .fold(Sha256::new(), |hasher, part| {
61 hasher
62 .chain_update((part.len() as u64).to_be_bytes())
63 .chain_update(part)
64 })
65 .finalize();
66
67 let mut first = [0u8; 8];
68 first.copy_from_slice(&digest[..8]);
69 i64::from_be_bytes(first)
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
78 fn hashes_are_stable() {
79 assert_eq!(scope_key("t", "p"), scope_key("t", "p"));
80 assert_eq!(
81 param_identity("t", "p", "http://hl7.org/fhir/SearchParameter/Patient-name"),
82 param_identity("t", "p", "http://hl7.org/fhir/SearchParameter/Patient-name"),
83 );
84 }
85
86 #[test]
87 fn part_boundaries_are_part_of_the_input() {
88 assert_ne!(scope_key("ab", "c"), scope_key("a", "bc"));
89 assert_ne!(
90 param_identity("t", "pa", "b"),
91 param_identity("t", "p", "ab")
92 );
93 }
94
95 #[test]
96 fn identities_are_scoped_to_the_project() {
97 let url = "http://hl7.org/fhir/SearchParameter/Patient-name";
98 assert_ne!(
99 param_identity("t", "p1", url),
100 param_identity("t", "p2", url)
101 );
102 assert_ne!(
103 param_identity("t1", "p", url),
104 param_identity("t2", "p", url)
105 );
106 }
107
108 #[test]
109 fn scopes_and_identities_do_not_share_a_space() {
110 assert_ne!(scope_key("t", "p"), param_identity("t", "p", ""));
111 }
112
113 #[test]
114 fn repeated_urls_are_not_collisions() {
115 assert!(check_identities("t", "p", ["a", "b", "a"]).is_ok());
116 }
117}