#![allow(clippy::missing_safety_doc)]
use std::fs::File;
use std::path::Path;
use std::slice;
use crate::autocorrect::{
key_slip_repaired_outputs, roman_repaired_outputs, FstLexicon, FstLoanwordMatch,
FstRepairedBaseline, FstSuggestOptions, FstSuggestResult, LoanwordLexicon,
LoanwordSearchOptions, RomanRepairOptions, FST_MAX_LEVENSHTEIN_DISTANCE,
};
use crate::autosuggest::{
AutosuggestLm, AutosuggestOptions, AutosuggestSession, CommitStrength,
PersonalAutosuggestConfig, PersonalAutosuggestTextSuggestion,
};
use crate::ObadhEngine;
pub const OBADH_ABI_VERSION: u32 = 1;
const AUTOCORRECT_POOL_LIMIT: usize = 24;
const AUTOCORRECT_RESPONSE_LIMIT: usize = 8;
unsafe fn input_str<'a>(ptr: *const u8, len: usize) -> Option<&'a str> {
if len == 0 {
return Some("");
}
if ptr.is_null() {
return None;
}
std::str::from_utf8(slice::from_raw_parts(ptr, len)).ok()
}
unsafe fn write_bytes(bytes: &[u8], out: *mut u8, cap: usize) -> usize {
if !out.is_null() && cap >= bytes.len() {
slice::from_raw_parts_mut(out, cap)[..bytes.len()].copy_from_slice(bytes);
}
bytes.len()
}
unsafe fn write_str_list(items: &[String], out: *mut u8, cap: usize) -> usize {
let mut packed = Vec::with_capacity(4 + items.iter().map(|s| 4 + s.len()).sum::<usize>());
packed.extend_from_slice(&(items.len() as u32).to_le_bytes());
for item in items {
packed.extend_from_slice(&(item.len() as u32).to_le_bytes());
packed.extend_from_slice(item.as_bytes());
}
write_bytes(&packed, out, cap)
}
#[no_mangle]
pub extern "C" fn obadh_abi_version() -> u32 {
OBADH_ABI_VERSION
}
#[no_mangle]
pub unsafe extern "C" fn obadh_engine_version(out: *mut u8, cap: usize) -> usize {
write_bytes(env!("CARGO_PKG_VERSION").as_bytes(), out, cap)
}
#[no_mangle]
pub extern "C" fn obadh_engine_new() -> *mut ObadhEngine {
Box::into_raw(Box::new(ObadhEngine::new()))
}
#[no_mangle]
pub unsafe extern "C" fn obadh_engine_free(engine: *mut ObadhEngine) {
if !engine.is_null() {
drop(Box::from_raw(engine));
}
}
#[no_mangle]
pub unsafe extern "C" fn obadh_transliterate(
engine: *const ObadhEngine,
input: *const u8,
input_len: usize,
out: *mut u8,
cap: usize,
) -> usize {
let (Some(engine), Some(input)) = (engine.as_ref(), input_str(input, input_len)) else {
return 0;
};
write_bytes(engine.transliterate(input).as_bytes(), out, cap)
}
#[no_mangle]
pub unsafe extern "C" fn obadh_transliterate_lenient(
engine: *const ObadhEngine,
input: *const u8,
input_len: usize,
out: *mut u8,
cap: usize,
) -> usize {
let (Some(engine), Some(input)) = (engine.as_ref(), input_str(input, input_len)) else {
return 0;
};
write_bytes(engine.transliterate_lenient(input).as_bytes(), out, cap)
}
pub struct ObadhAutocorrect {
engine: ObadhEngine,
lexicon: FstLexicon<memmap2::Mmap>,
loanwords: Option<LoanwordLexicon<Vec<u8>>>,
}
unsafe fn mmap_fst_lexicon(path: &str) -> Option<FstLexicon<memmap2::Mmap>> {
let file = File::open(Path::new(path)).ok()?;
let mmap = memmap2::MmapOptions::new().map(&file).ok()?;
Some(FstLexicon::from_map(fst::Map::new(mmap).ok()?))
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autocorrect_open(
fst_path: *const u8,
fst_path_len: usize,
loanword_path: *const u8,
loanword_path_len: usize,
) -> *mut ObadhAutocorrect {
let Some(fst_path) = input_str(fst_path, fst_path_len) else {
return std::ptr::null_mut();
};
let Some(lexicon) = mmap_fst_lexicon(fst_path) else {
return std::ptr::null_mut();
};
let loanwords = match input_str(loanword_path, loanword_path_len) {
Some(path) if !path.is_empty() => match std::fs::read(path) {
Ok(bytes) => match LoanwordLexicon::from_bytes(bytes) {
Ok(loanwords) => Some(loanwords),
Err(_) => return std::ptr::null_mut(),
},
Err(_) => return std::ptr::null_mut(),
},
Some(_) => None,
None => return std::ptr::null_mut(),
};
Box::into_raw(Box::new(ObadhAutocorrect {
engine: ObadhEngine::new(),
lexicon,
loanwords,
}))
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autocorrect_free(autocorrect: *mut ObadhAutocorrect) {
if !autocorrect.is_null() {
drop(Box::from_raw(autocorrect));
}
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autocorrect_fingerprint(
autocorrect: *const ObadhAutocorrect,
) -> u64 {
match autocorrect.as_ref() {
Some(autocorrect) => autocorrect.lexicon.artifact_fingerprint(),
None => 0,
}
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autocorrect_is_lexicon_word(
autocorrect: *const ObadhAutocorrect,
word: *const u8,
word_len: usize,
) -> i32 {
let (Some(autocorrect), Some(word)) = (autocorrect.as_ref(), input_str(word, word_len)) else {
return 0;
};
i32::from(autocorrect.lexicon.exact_frequency(word).is_some())
}
impl ObadhAutocorrect {
fn suggest_result(&self, roman: &str) -> Option<FstSuggestResult> {
if roman.trim().is_empty() {
return None;
}
let baseline = self.engine.transliterate(roman);
let mut repairs =
roman_repaired_outputs(roman, &baseline, RomanRepairOptions::default(), |text| {
self.engine.transliterate(text)
});
repairs.extend(key_slip_repaired_outputs(
roman,
&baseline,
self.lexicon.exact_frequency(&baseline),
|text| self.engine.transliterate(text),
|word| self.lexicon.exact_frequency(word).is_some(),
));
let repaired_baselines = repairs
.iter()
.map(|repair| FstRepairedBaseline {
roman_input: repair.roman_input.as_str(),
bangla_output: repair.bangla_output.as_str(),
repair_kind: repair.repair_kind,
repair_cost: repair.repair_cost,
})
.collect::<Vec<_>>();
let loanword_suggestions = match &self.loanwords {
Some(loanwords) => loanwords
.suggestions(roman, LoanwordSearchOptions::for_input(roman))
.unwrap_or_default(),
None => Vec::new(),
};
let loanword_matches = loanword_suggestions
.iter()
.map(|entry| FstLoanwordMatch {
roman_input: roman,
roman_repair: entry.english.as_str(),
bangla_output: entry.bangla.as_str(),
frequency: entry.frequency,
repair_kind: entry.kind.as_str(),
repair_cost: entry.edit_cost,
})
.collect::<Vec<_>>();
let options = FstSuggestOptions {
max_distance: FST_MAX_LEVENSHTEIN_DISTANCE,
max_candidates: AUTOCORRECT_POOL_LIMIT,
response_candidates: AUTOCORRECT_RESPONSE_LIMIT,
max_prefix_candidates: AUTOCORRECT_RESPONSE_LIMIT,
..FstSuggestOptions::default()
};
self.lexicon
.suggest_with_repaired_baselines_and_loanwords(
&baseline,
&repaired_baselines,
&loanword_matches,
options,
)
.ok()
}
fn suggest_texts(&self, roman: &str, limit: usize) -> Vec<String> {
let limit = limit.clamp(1, AUTOCORRECT_RESPONSE_LIMIT);
match self.suggest_result(roman) {
Some(result) => result
.candidates
.into_iter()
.map(|candidate| candidate.text)
.take(limit)
.collect(),
None => Vec::new(),
}
}
fn compose_texts(&self, roman: &str, limit: usize) -> Vec<String> {
if roman.trim().is_empty() {
return Vec::new();
}
let limit = limit.clamp(1, AUTOCORRECT_RESPONSE_LIMIT);
let mut candidates = Vec::with_capacity(limit);
candidates.push(self.engine.transliterate(roman));
for candidate in self.suggest_texts(roman, limit.saturating_sub(1)) {
if candidates.len() >= limit {
break;
}
if !candidates.contains(&candidate) {
candidates.push(candidate);
}
}
candidates
}
fn word_alternatives_texts(&self, word: &str, limit: usize) -> Vec<String> {
if word.trim().is_empty() {
return Vec::new();
}
let limit = limit.clamp(1, AUTOCORRECT_RESPONSE_LIMIT);
let options = FstSuggestOptions {
max_distance: FST_MAX_LEVENSHTEIN_DISTANCE,
max_candidates: AUTOCORRECT_POOL_LIMIT,
response_candidates: limit,
max_prefix_candidates: limit,
..FstSuggestOptions::default()
};
match self
.lexicon
.suggest_with_repaired_baselines_and_loanwords(word, &[], &[], options)
{
Ok(result) => result
.candidates
.into_iter()
.map(|candidate| candidate.text)
.take(limit)
.collect(),
Err(_) => Vec::new(),
}
}
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autocorrect_suggest(
autocorrect: *const ObadhAutocorrect,
roman: *const u8,
roman_len: usize,
limit: usize,
out: *mut u8,
cap: usize,
) -> usize {
let (Some(autocorrect), Some(roman)) = (autocorrect.as_ref(), input_str(roman, roman_len))
else {
return 0;
};
write_str_list(&autocorrect.suggest_texts(roman, limit), out, cap)
}
#[no_mangle]
pub unsafe extern "C" fn obadh_compose_suggestions(
autocorrect: *const ObadhAutocorrect,
roman: *const u8,
roman_len: usize,
limit: usize,
out: *mut u8,
cap: usize,
) -> usize {
let (Some(autocorrect), Some(roman)) = (autocorrect.as_ref(), input_str(roman, roman_len))
else {
return 0;
};
write_str_list(&autocorrect.compose_texts(roman, limit), out, cap)
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autocorrect_word_alternatives(
autocorrect: *const ObadhAutocorrect,
word: *const u8,
word_len: usize,
limit: usize,
out: *mut u8,
cap: usize,
) -> usize {
let (Some(autocorrect), Some(word)) = (autocorrect.as_ref(), input_str(word, word_len)) else {
return 0;
};
write_str_list(&autocorrect.word_alternatives_texts(word, limit), out, cap)
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autocorrect_should_replace(
autocorrect: *const ObadhAutocorrect,
roman: *const u8,
roman_len: usize,
out: *mut u8,
cap: usize,
needed_len: *mut usize,
) -> i32 {
if let Some(needed_len) = needed_len.as_mut() {
*needed_len = 0;
}
let (Some(autocorrect), Some(roman)) = (autocorrect.as_ref(), input_str(roman, roman_len))
else {
return 0;
};
let Some(result) = autocorrect.suggest_result(roman) else {
return 0;
};
let Some(replacement) = result.auto_replacement() else {
return 0;
};
let written = write_bytes(replacement.text.as_bytes(), out, cap);
if let Some(needed_len) = needed_len.as_mut() {
*needed_len = written;
}
1
}
pub struct ObadhAutosuggest {
session: AutosuggestSession<'static, memmap2::Mmap>,
_lm: Box<AutosuggestLm<memmap2::Mmap>>,
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autosuggest_open(
path: *const u8,
path_len: usize,
) -> *mut ObadhAutosuggest {
let Some(path) = input_str(path, path_len) else {
return std::ptr::null_mut();
};
let Ok(lm) = AutosuggestLm::from_path(path) else {
return std::ptr::null_mut();
};
let lm = Box::new(lm);
let lm_ref: &'static AutosuggestLm<memmap2::Mmap> =
&*(lm.as_ref() as *const AutosuggestLm<memmap2::Mmap>);
let session = AutosuggestSession::with_personal_config(
lm_ref,
PersonalAutosuggestConfig::default(),
AutosuggestOptions { max_candidates: 8 },
);
Box::into_raw(Box::new(ObadhAutosuggest { session, _lm: lm }))
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autosuggest_free(autosuggest: *mut ObadhAutosuggest) {
if !autosuggest.is_null() {
drop(Box::from_raw(autosuggest));
}
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autosuggest_fingerprint(
autosuggest: *const ObadhAutosuggest,
) -> u64 {
match autosuggest.as_ref() {
Some(autosuggest) => autosuggest._lm.artifact_fingerprint(),
None => 0,
}
}
fn commit_strength_from_code(code: u32) -> CommitStrength {
match code {
1 => CommitStrength::CorrectionRejected,
2 => CommitStrength::ManuallyAdded,
_ => CommitStrength::Committed,
}
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autosuggest_commit(
autosuggest: *mut ObadhAutosuggest,
token: *const u8,
token_len: usize,
strength: u32,
) -> i32 {
let (Some(autosuggest), Some(token)) = (autosuggest.as_mut(), input_str(token, token_len))
else {
return 0;
};
let learned = autosuggest
.session
.commit_token_with_strength(token, commit_strength_from_code(strength))
.unwrap_or(false);
i32::from(learned)
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autosuggest_established_weight(
autosuggest: *const ObadhAutosuggest,
word: *const u8,
word_len: usize,
) -> u32 {
let (Some(autosuggest), Some(word)) = (autosuggest.as_ref(), input_str(word, word_len)) else {
return 0;
};
u32::from(autosuggest.session.established_weight(word))
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autosuggest_is_word_established(
autosuggest: *const ObadhAutosuggest,
word: *const u8,
word_len: usize,
min_weight: u32,
) -> i32 {
let (Some(autosuggest), Some(word)) = (autosuggest.as_ref(), input_str(word, word_len)) else {
return 0;
};
let min_weight = min_weight.min(u32::from(u16::MAX)) as u16;
i32::from(autosuggest.session.is_word_established(word, min_weight))
}
impl ObadhAutosuggest {
fn session_suggestions(&mut self, limit: usize) -> Vec<String> {
self.session.set_options(AutosuggestOptions {
max_candidates: limit,
});
if self.session.suggest().is_err() {
return Vec::new();
}
self.session.suggest_personal_text();
let personal = self.session.personal_text_suggestions().to_vec();
let model: Vec<String> = self
.session
.candidates()
.iter()
.map(|candidate| candidate.text.to_string())
.collect();
let mut values = Vec::with_capacity(limit);
self.push_personal(&personal, true, limit, &mut values);
for candidate in model {
if values.len() >= limit {
break;
}
if !values.contains(&candidate) {
values.push(candidate);
}
}
self.push_personal(&personal, false, limit, &mut values);
values
}
fn push_personal(
&self,
suggestions: &[PersonalAutosuggestTextSuggestion],
contextual: bool,
limit: usize,
values: &mut Vec<String>,
) {
for suggestion in suggestions {
if values.len() >= limit {
break;
}
if (suggestion.context_len > 0) != contextual {
continue;
}
if let Some(text) = self.session.personal_text_suggestion_text(*suggestion) {
let text = text.to_string();
if !values.contains(&text) {
values.push(text);
}
}
}
}
fn context_suggestions(&self, context: &str, limit: usize) -> Vec<String> {
match self._lm.suggest_for_text(
context,
AutosuggestOptions {
max_candidates: limit,
},
) {
Ok(result) => result
.candidates
.into_iter()
.map(|candidate| candidate.text.to_string())
.take(limit)
.collect(),
Err(_) => Vec::new(),
}
}
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autosuggest_suggest(
autosuggest: *mut ObadhAutosuggest,
limit: usize,
out: *mut u8,
cap: usize,
) -> usize {
let Some(autosuggest) = autosuggest.as_mut() else {
return 0;
};
write_str_list(
&autosuggest.session_suggestions(limit.clamp(1, 16)),
out,
cap,
)
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autosuggest_suggest_for_context(
autosuggest: *const ObadhAutosuggest,
context: *const u8,
context_len: usize,
limit: usize,
out: *mut u8,
cap: usize,
) -> usize {
let (Some(autosuggest), Some(context)) =
(autosuggest.as_ref(), input_str(context, context_len))
else {
return 0;
};
write_str_list(
&autosuggest.context_suggestions(context, limit.clamp(1, 16)),
out,
cap,
)
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autosuggest_clear_session(autosuggest: *mut ObadhAutosuggest) {
if let Some(autosuggest) = autosuggest.as_mut() {
autosuggest.session.clear_context();
}
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autosuggest_clear_personal(autosuggest: *mut ObadhAutosuggest) {
if let Some(autosuggest) = autosuggest.as_mut() {
autosuggest.session.personal_mut().clear();
}
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autosuggest_export_personal(
autosuggest: *const ObadhAutosuggest,
out: *mut u8,
cap: usize,
) -> usize {
let Some(autosuggest) = autosuggest.as_ref() else {
return 0;
};
let mut bytes = Vec::with_capacity(autosuggest.session.personal_snapshot_len());
autosuggest.session.write_personal_snapshot_into(&mut bytes);
write_bytes(&bytes, out, cap)
}
#[no_mangle]
pub unsafe extern "C" fn obadh_autosuggest_import_personal(
autosuggest: *mut ObadhAutosuggest,
input: *const u8,
input_len: usize,
) -> i32 {
if input.is_null() || input_len == 0 {
return 0;
}
let Some(autosuggest) = autosuggest.as_mut() else {
return 0;
};
let bytes = slice::from_raw_parts(input, input_len);
i32::from(autosuggest.session.import_personal_snapshot(bytes).is_ok())
}
#[cfg(test)]
mod tests {
use super::*;
use std::ptr;
unsafe fn read_sized(mut writer: impl FnMut(*mut u8, usize) -> usize) -> Vec<u8> {
let needed = writer(ptr::null_mut(), 0);
let mut buffer = vec![0_u8; needed];
let written = writer(buffer.as_mut_ptr(), buffer.len());
assert_eq!(written, needed);
buffer
}
fn parse_str_list(bytes: &[u8]) -> Vec<String> {
let count = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
let mut offset = 4;
let mut items = Vec::with_capacity(count);
for _ in 0..count {
let len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
items.push(String::from_utf8(bytes[offset..offset + len].to_vec()).unwrap());
offset += len;
}
items
}
#[test]
fn abi_version_is_pinned() {
assert_eq!(obadh_abi_version(), OBADH_ABI_VERSION);
}
#[test]
fn engine_transliterates_through_the_snprintf_contract() {
let engine = obadh_engine_new();
let input = b"ami";
let output = unsafe {
read_sized(|out, cap| {
obadh_transliterate(engine, input.as_ptr(), input.len(), out, cap)
})
};
assert_eq!(String::from_utf8(output).unwrap(), "আমি");
unsafe { obadh_engine_free(engine) };
}
#[test]
fn engine_version_matches_the_crate() {
let bytes = unsafe { read_sized(|out, cap| obadh_engine_version(out, cap)) };
assert_eq!(String::from_utf8(bytes).unwrap(), env!("CARGO_PKG_VERSION"));
}
#[test]
fn str_list_round_trips_including_empty_and_newline() {
let items = vec!["আমি".to_string(), String::new(), "ভাত\nখাই".to_string()];
let bytes = unsafe { read_sized(|out, cap| write_str_list(&items, out, cap)) };
assert_eq!(parse_str_list(&bytes), items);
}
#[test]
fn input_str_handles_empty_null_and_invalid() {
unsafe {
assert_eq!(input_str(ptr::null(), 0), Some(""));
assert_eq!(input_str(ptr::null(), 4), None);
let valid = b"hi";
assert_eq!(input_str(valid.as_ptr(), valid.len()), Some("hi"));
let invalid = [0xff_u8, 0xfe];
assert_eq!(input_str(invalid.as_ptr(), invalid.len()), None);
}
}
#[test]
fn opening_a_missing_artifact_returns_null_not_a_crash() {
let path = b"/nonexistent/obadh/bn.fst";
let autocorrect =
unsafe { obadh_autocorrect_open(path.as_ptr(), path.len(), ptr::null(), 0) };
assert!(autocorrect.is_null());
let autosuggest = unsafe { obadh_autosuggest_open(path.as_ptr(), path.len()) };
assert!(autosuggest.is_null());
unsafe {
obadh_autocorrect_free(ptr::null_mut());
obadh_autosuggest_free(ptr::null_mut());
obadh_engine_free(ptr::null_mut());
}
}
#[test]
fn null_handles_are_safe_no_ops() {
let word = b"word";
unsafe {
assert_eq!(
obadh_transliterate(ptr::null(), word.as_ptr(), word.len(), ptr::null_mut(), 0),
0
);
assert_eq!(
obadh_autocorrect_is_lexicon_word(ptr::null(), word.as_ptr(), word.len()),
0
);
assert_eq!(obadh_autocorrect_fingerprint(ptr::null()), 0);
assert_eq!(
obadh_autosuggest_established_weight(ptr::null(), word.as_ptr(), word.len()),
0
);
}
}
fn write_temp(name: &str, bytes: &[u8]) -> std::path::PathBuf {
let path = std::env::temp_dir().join(format!("obadh_cabi_{name}"));
std::fs::write(&path, bytes).expect("temp write");
path
}
fn temp_fst(name: &str, entries: &[(&str, u64)]) -> std::path::PathBuf {
let mut sorted = entries.to_vec();
sorted.sort_by(|left, right| left.0.as_bytes().cmp(right.0.as_bytes()));
let mut builder = fst::MapBuilder::memory();
for (word, frequency) in sorted {
builder.insert(word.as_bytes(), frequency).expect("insert");
}
write_temp(name, builder.into_map().as_fst().as_bytes())
}
#[test]
fn autocorrect_compose_puts_the_baseline_first_and_word_alternatives_work() {
let path = temp_fst("ac.fst", &[("বাংলা", 10_000), ("বাংলাদেশ", 8_000)]);
let path_bytes = path.to_str().unwrap().as_bytes();
let autocorrect = unsafe {
obadh_autocorrect_open(path_bytes.as_ptr(), path_bytes.len(), ptr::null(), 0)
};
assert!(!autocorrect.is_null());
unsafe {
let word = "বাংলা".as_bytes();
assert_eq!(
obadh_autocorrect_is_lexicon_word(autocorrect, word.as_ptr(), word.len()),
1
);
assert_ne!(obadh_autocorrect_fingerprint(autocorrect), 0);
let roman = b"bangla";
let packed = read_sized(|out, cap| {
obadh_compose_suggestions(autocorrect, roman.as_ptr(), roman.len(), 5, out, cap)
});
let composed = parse_str_list(&packed);
let baseline = ObadhEngine::new().transliterate("bangla");
assert_eq!(composed.first(), Some(&baseline));
let alternatives = read_sized(|out, cap| {
obadh_autocorrect_word_alternatives(
autocorrect,
word.as_ptr(),
word.len(),
5,
out,
cap,
)
});
assert!(!parse_str_list(&alternatives).is_empty());
obadh_autocorrect_free(autocorrect);
}
let _ = std::fs::remove_file(path);
}
#[test]
fn autosuggest_learns_a_word_and_surfaces_it_through_the_abi() {
use crate::autosuggest::artifact::test_support::{build_fixture, Row};
let tokens = ["<pad>", "<bos>", "<unk>", "আমি", "আজ", "ভাত", "খাই"];
let fixture = build_fixture(
&tokens,
&[(5, 100, 100), (6, 90, 90)],
&[Row {
context: vec![3],
candidates: vec![(6, 20, 20), (5, 10, 10)],
}],
);
let path = write_temp("as.bin", &fixture);
let path_bytes = path.to_str().unwrap().as_bytes();
let autosuggest = unsafe { obadh_autosuggest_open(path_bytes.as_ptr(), path_bytes.len()) };
assert!(!autosuggest.is_null());
unsafe {
let name = "নাসির".as_bytes();
assert_eq!(
obadh_autosuggest_established_weight(autosuggest, name.as_ptr(), name.len()),
0
);
assert_eq!(
obadh_autosuggest_commit(
autosuggest,
name.as_ptr(),
name.len(),
OBADH_COMMIT_MANUALLY_ADDED_CODE
),
1
);
assert_eq!(
obadh_autosuggest_established_weight(autosuggest, name.as_ptr(), name.len()),
u32::from(CommitStrength::ManuallyAdded.weight())
);
assert_eq!(
obadh_autosuggest_is_word_established(autosuggest, name.as_ptr(), name.len(), 2),
1
);
let _ = read_sized(|out, cap| obadh_autosuggest_suggest(autosuggest, 5, out, cap));
let context = "আমি".as_bytes();
let _ = read_sized(|out, cap| {
obadh_autosuggest_suggest_for_context(
autosuggest,
context.as_ptr(),
context.len(),
5,
out,
cap,
)
});
obadh_autosuggest_free(autosuggest);
}
let _ = std::fs::remove_file(path);
}
const OBADH_COMMIT_MANUALLY_ADDED_CODE: u32 = 2;
#[test]
fn c_header_matches_the_exported_surface() {
let source = include_str!("cabi.rs");
let header = include_str!("../include/obadh.h");
let marker = "extern \"C\" fn ";
let mut missing = Vec::new();
for line in source.lines() {
let Some(index) = line.find(marker) else {
continue;
};
let name: String = line[index + marker.len()..]
.chars()
.take_while(|character| character.is_alphanumeric() || *character == '_')
.collect();
if name.starts_with("obadh_") && !header.contains(&name) {
missing.push(name);
}
}
assert!(
missing.is_empty(),
"C header is missing declarations for: {missing:?}"
);
assert!(
header.contains(&format!("#define OBADH_ABI_VERSION {OBADH_ABI_VERSION}")),
"C header ABI version does not match OBADH_ABI_VERSION"
);
}
}