Add carrier name lookup from bundled prefix data#101
Conversation
Expose carrier_mapper module with longest-prefix matching against the existing assets/carrier/ data compiled at build time via postcard. Public API: - name_for_number: carrier name with number-type filtering - name_for_valid_number: carrier name without type check - safe_display_name: carrier name only for non-portable countries Supports 10 languages with locale fallback (zh_TW → zh_Hant, CJK does not fall back to English). Includes 38 unit tests covering French/international carriers, overlapping prefixes, multi-language, locale normalization, type filtering, and database sanity checks.
rubdos
left a comment
There was a problem hiding this comment.
I only looked at the build script for now, it needs some work on the failure modes. I'll try to have a pass over your actual library code during the week. Thanks for filing your PR!
| if !carrier_dir.is_dir() { | ||
| // Write empty data if carrier directory is missing. | ||
| let out_path = Path::new(&env::var("OUT_DIR").unwrap()).join("carrier_data.bin"); | ||
| type CarrierEntries = Vec<(String, Vec<(String, String)>)>; | ||
| let empty: (CarrierEntries, usize) = (Vec::new(), 0); | ||
| let mut out = | ||
| BufWriter::new(File::create(&out_path).expect("could not create carrier data file")); | ||
| postcard::to_io(&empty, &mut out).expect("failed to serialize carrier data"); | ||
| return; | ||
| } |
There was a problem hiding this comment.
To deduplicate this: the !carrier_dir case is the case where you do not populate the entries map, i.e., the case where lang_dirs is just a Vec::default(). In practice, I would even try to complete avoid this case, and just assert that the directory exists. This is completely in control of our repo, and the disappearance of the directory is a bug.
| lang_dirs.sort_by_key(|e| e.file_name()); | ||
|
|
||
| for lang_entry in lang_dirs { | ||
| let lang = lang_entry.file_name().to_string_lossy().to_string(); |
There was a problem hiding this comment.
| let lang = lang_entry.file_name().to_string_lossy().to_string(); | |
| let lang = lang_entry.file_name().to_str().expect("unicode carrier directory"); |
| // Walk each language directory: assets/carrier/en/, assets/carrier/zh/, etc. | ||
| let mut lang_dirs: Vec<_> = fs::read_dir(carrier_dir) | ||
| .expect("could not read carrier directory") | ||
| .filter_map(|e| e.ok()) |
There was a problem hiding this comment.
Again, I'd handle this error instead of ignoring it. Just .expect() it, and make any cases that you actually want to ignore explicit.
|
|
||
| let mut txt_files: Vec<_> = fs::read_dir(&lang_path) | ||
| .unwrap_or_else(|e| panic!("could not read {}: {e}", lang_path.display())) | ||
| .filter_map(|e| e.ok()) |
There was a problem hiding this comment.
Same here: handle this error instead of ignoring it. Just .expect() it, and make any cases that you actually want to ignore explicit.
| e.path() | ||
| .extension() | ||
| .map(|ext| ext == "txt") | ||
| .unwrap_or(false) |
There was a problem hiding this comment.
This basically means: if upstream changes the extension, we have an empty database. So we need to somehow guard against that case. Then again: you have test cases that assert some entries, so this is probably fine.
| if let Some((prefix, name)) = line.split_once('|') { | ||
| let prefix = prefix.trim(); | ||
| let name = name.trim(); | ||
| if !prefix.is_empty() && prefix.bytes().all(|b| b.is_ascii_digit()) { | ||
| max_prefix_len = max_prefix_len.max(prefix.len()); | ||
| entries | ||
| .entry(prefix.to_string()) | ||
| .or_default() | ||
| .insert(lang.clone(), name.to_string()); | ||
| } | ||
| } |
There was a problem hiding this comment.
| if let Some((prefix, name)) = line.split_once('|') { | |
| let prefix = prefix.trim(); | |
| let name = name.trim(); | |
| if !prefix.is_empty() && prefix.bytes().all(|b| b.is_ascii_digit()) { | |
| max_prefix_len = max_prefix_len.max(prefix.len()); | |
| entries | |
| .entry(prefix.to_string()) | |
| .or_default() | |
| .insert(lang.clone(), name.to_string()); | |
| } | |
| } | |
| let (prefix, name) = line.split_once('|').expect("line format"); | |
| let prefix = prefix.trim(); | |
| let name = name.trim(); | |
| if !prefix.is_empty() && prefix.bytes().all(|b| b.is_ascii_digit()) { | |
| max_prefix_len = max_prefix_len.max(prefix.len()); | |
| entries | |
| .entry(prefix.to_string()) | |
| .or_default() | |
| .insert(lang.clone(), name.to_string()); | |
| } |
Replace silent error swallowing with explicit panics per reviewer request: assert carrier directory exists, use .expect() over filter_map(|e| e.ok()), use .to_str().expect() over to_string_lossy(), unwrap split_once result, simplify extension check with is_some_and. Also align both OUT_DIR lookups to .expect() for consistency with the rest of the file.
|
Thanks for the review! All changes applied:
I also noticed that |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #101 +/- ##
==========================================
+ Coverage 67.30% 70.02% +2.71%
==========================================
Files 18 19 +1
Lines 2138 2372 +234
==========================================
+ Hits 1439 1661 +222
- Misses 699 711 +12 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Summary
carrier_mappermodule that resolves carrier names from the existingassets/carrier/prefix data compiled at build timezh_TW→zh_Hant, CJK does not fall back to English)Public API
carrier_mapper::name_for_number(number, lang)— carrier name with number-type filtering (mobile, fixed-line-or-mobile, pager)carrier_mapper::name_for_valid_number(number, lang)— carrier name without type checkcarrier_mapper::safe_display_name(number, lang)— carrier name only for countries without mobile number portabilityChanges
src/carrier_mapper.rs— new module (3 public functions, locale resolution, 40 unit tests)build.rs— addbuild_carrier_data()to compileassets/carrier/txt files into a binary blob via postcardsrc/lib.rs— exposecarrier_mappermoduleREADME.md— add carrier lookup exampleNo data files were modified. This only adds code to use the carrier data that is already shipped with the crate.
Test plan
cargo test— 134 existing tests pass, 40 new carrier tests pass, 2 doctests passcargo clippy— no warnings