1use std::sync::Arc;
27
28use jmdict_fast as core;
29
30pub use core::{
31 DataVersion, DeinflectionInfo, Entry, GlossEntry, KanaEntry, KanjiEntry, LanguageSource,
32 MatchMode, MatchType, SenseEntry, Xref,
33};
34
35#[derive(Debug, Clone)]
44pub enum Error {
45 DataNotFound,
47 DataVersionMismatch { expected: u32, found: u32 },
49 DataCorrupted,
51 InvalidQuery,
53 Io { message: String },
55 Deserialization,
57 #[cfg(feature = "install")]
65 CacheDirRequired { platform: String },
66 #[cfg(feature = "install")]
70 CacheDirAlreadySet,
71 #[cfg(feature = "install")]
74 Network { message: String },
75}
76
77impl Error {
78 pub fn code(&self) -> u32 {
80 match self {
81 Error::DataNotFound => 1,
82 Error::DataVersionMismatch { .. } => 2,
83 Error::DataCorrupted => 3,
84 Error::InvalidQuery => 4,
85 Error::Io { .. } => 5,
86 Error::Deserialization => 6,
87 #[cfg(feature = "install")]
88 Error::CacheDirRequired { .. } => 7,
89 #[cfg(feature = "install")]
90 Error::Network { .. } => 8,
91 #[cfg(feature = "install")]
92 Error::CacheDirAlreadySet => 9,
93 }
94 }
95}
96
97impl std::fmt::Display for Error {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 match self {
100 Error::DataNotFound => write!(
101 f,
102 "Dictionary data files not found. Provide a path to load(), set JMDICT_DATA, or place files under dist/."
103 ),
104 Error::DataVersionMismatch { expected, found } => write!(
105 f,
106 "Data format version {found}, library expects {expected}. Regenerate with `cargo xtask generate`."
107 ),
108 Error::DataCorrupted => write!(f, "Dictionary data is corrupted or has an invalid format."),
109 Error::InvalidQuery => write!(f, "The search query is invalid."),
110 Error::Io { message } => write!(f, "I/O error: {message}"),
111 Error::Deserialization => write!(f, "Failed to deserialize dictionary entry data."),
112 #[cfg(feature = "install")]
113 Error::CacheDirRequired { platform } => write!(
114 f,
115 "Cache directory required on {platform}: call init_sdk_cache_dir(path) from the host \
116 (e.g. path_provider on Flutter, FileManager on iOS, Context.getCacheDir on Android), \
117 or pass InstallOptions::cache_dir(path) per call."
118 ),
119 #[cfg(feature = "install")]
120 Error::CacheDirAlreadySet => write!(
121 f,
122 "init_sdk_cache_dir was already called for this process; the cache root is one-shot."
123 ),
124 #[cfg(feature = "install")]
125 Error::Network { message } => write!(f, "Network error during install: {message}"),
126 }
127 }
128}
129
130impl std::error::Error for Error {}
131
132impl From<core::JmdictError> for Error {
133 fn from(err: core::JmdictError) -> Self {
134 match err {
135 core::JmdictError::DataNotFound => Error::DataNotFound,
136 core::JmdictError::DataVersionMismatch { expected, found } => {
137 Error::DataVersionMismatch { expected, found }
138 }
139 core::JmdictError::DataCorrupted => Error::DataCorrupted,
140 core::JmdictError::InvalidQuery => Error::InvalidQuery,
141 core::JmdictError::IoError(e) => Error::Io { message: e.to_string() },
142 core::JmdictError::DeserializationError => Error::Deserialization,
143 #[cfg(feature = "install")]
144 core::JmdictError::CacheDirRequired { platform } => Error::CacheDirRequired {
145 platform: platform.to_string(),
146 },
147 #[cfg(feature = "install")]
148 core::JmdictError::CacheDirAlreadySet => Error::CacheDirAlreadySet,
149 #[cfg(feature = "install")]
150 core::JmdictError::NetworkError(message) => Error::Network { message },
151 }
152 }
153}
154
155#[derive(Debug, Clone)]
164pub struct LookupResult {
165 pub entry: Entry,
166 pub match_type: MatchType,
167 pub match_key: String,
168 pub score: f64,
169 pub deinflection: Option<DeinflectionInfo>,
170}
171
172impl From<core::LookupResult> for LookupResult {
173 fn from(r: core::LookupResult) -> Self {
174 Self {
175 entry: r.entry,
176 match_type: r.match_type,
177 match_key: r.match_key,
178 score: r.score,
179 deinflection: r.deinflection,
180 }
181 }
182}
183
184#[derive(Debug, Clone)]
190pub struct QueryOptions {
191 pub mode: MatchMode,
192 pub common_only: bool,
193 pub pos: Vec<String>,
194 pub misc: Vec<String>,
195 pub field: Vec<String>,
196 pub dialect: Vec<String>,
197 pub limit: Option<u32>,
200 pub max_distance: u32,
203}
204
205impl Default for QueryOptions {
206 fn default() -> Self {
207 Self {
208 mode: MatchMode::Exact,
209 common_only: false,
210 pos: Vec::new(),
211 misc: Vec::new(),
212 field: Vec::new(),
213 dialect: Vec::new(),
214 limit: None,
215 max_distance: 2,
216 }
217 }
218}
219
220#[derive(Debug, Clone)]
223pub struct BatchResult {
224 pub term: String,
225 pub results: Vec<LookupResult>,
226}
227
228pub struct Dict {
236 inner: core::Dict,
237}
238
239impl Dict {
240 pub fn load(path: String) -> Result<Arc<Self>, Error> {
242 let inner = core::Dict::load(&path)?;
243 Ok(Arc::new(Self { inner }))
244 }
245
246 pub fn load_default() -> Result<Arc<Self>, Error> {
249 let inner = core::Dict::load_default()?;
250 Ok(Arc::new(Self { inner }))
251 }
252
253 pub fn entry_count(&self) -> u64 {
254 self.inner.entry_count() as u64
255 }
256
257 pub fn version(&self) -> DataVersion {
258 self.inner.version()
259 }
260
261 pub fn lookup_exact(&self, term: String) -> Vec<LookupResult> {
264 self.inner
265 .lookup_exact(&term)
266 .into_iter()
267 .map(Into::into)
268 .collect()
269 }
270
271 pub fn lookup_partial(&self, prefix: String) -> Vec<LookupResult> {
272 self.inner
273 .lookup_partial(&prefix)
274 .into_iter()
275 .map(Into::into)
276 .collect()
277 }
278
279 pub fn lookup_exact_with_deinflection(&self, term: String) -> Vec<LookupResult> {
280 self.inner
281 .lookup_exact_with_deinflection(&term)
282 .into_iter()
283 .map(Into::into)
284 .collect()
285 }
286
287 pub fn lookup_gloss(&self, query: String) -> Vec<LookupResult> {
288 self.inner
289 .lookup_gloss(&query)
290 .into_iter()
291 .map(Into::into)
292 .collect()
293 }
294
295 pub fn lookup_by_id(&self, jmdict_id: String) -> Option<LookupResult> {
296 self.inner.lookup_by_id(&jmdict_id).map(Into::into)
297 }
298
299 pub fn resolve_xref(&self, xref: Xref) -> Vec<LookupResult> {
300 self.inner
301 .resolve_xref(&xref)
302 .into_iter()
303 .map(Into::into)
304 .collect()
305 }
306
307 pub fn lookup_with_options(
311 &self,
312 term: String,
313 options: QueryOptions,
314 ) -> Result<Vec<LookupResult>, Error> {
315 let pos: Vec<&str> = options.pos.iter().map(String::as_str).collect();
316 let misc: Vec<&str> = options.misc.iter().map(String::as_str).collect();
317 let field: Vec<&str> = options.field.iter().map(String::as_str).collect();
318 let dialect: Vec<&str> = options.dialect.iter().map(String::as_str).collect();
319
320 let mut builder = self
321 .inner
322 .lookup(&term)
323 .mode(options.mode)
324 .common_only(options.common_only)
325 .pos(&pos)
326 .misc(&misc)
327 .field(&field)
328 .dialect(&dialect)
329 .max_distance(options.max_distance);
330 if let Some(limit) = options.limit {
331 builder = builder.limit(limit as usize);
332 }
333
334 Ok(builder.execute()?.into_iter().map(Into::into).collect())
335 }
336
337 pub fn lookup_batch(
343 &self,
344 terms: Vec<String>,
345 options: QueryOptions,
346 ) -> Result<Vec<BatchResult>, Error> {
347 let term_refs: Vec<&str> = terms.iter().map(String::as_str).collect();
348 let pos: Vec<&str> = options.pos.iter().map(String::as_str).collect();
349 let misc: Vec<&str> = options.misc.iter().map(String::as_str).collect();
350 let field: Vec<&str> = options.field.iter().map(String::as_str).collect();
351 let dialect: Vec<&str> = options.dialect.iter().map(String::as_str).collect();
352
353 let mut builder = self
354 .inner
355 .lookup_batch(&term_refs)
356 .mode(options.mode)
357 .common_only(options.common_only)
358 .pos(&pos)
359 .misc(&misc)
360 .field(&field)
361 .dialect(&dialect)
362 .max_distance(options.max_distance);
363 if let Some(limit) = options.limit {
364 builder = builder.limit(limit as usize);
365 }
366
367 Ok(builder
368 .execute()?
369 .into_iter()
370 .map(|(term, results)| BatchResult {
371 term,
372 results: results.into_iter().map(Into::into).collect(),
373 })
374 .collect())
375 }
376
377 pub fn get(&self, seq_id: u64) -> Option<Entry> {
381 self.inner.get(seq_id)
382 }
383
384 pub fn iter_entries(&self, start: u64, count: u64) -> Vec<Entry> {
389 let total = self.inner.entry_count() as u64;
390 let end = start.saturating_add(count).min(total);
391 (start..end).filter_map(|i| self.inner.get(i)).collect()
392 }
393}
394
395#[cfg(feature = "install")]
403#[derive(Debug, Clone)]
404pub enum InstallSource {
405 OfficialRelease,
408 Url { url: String },
410 Tarball { path: String },
412}
413
414#[cfg(feature = "install")]
415impl Default for InstallSource {
416 fn default() -> Self {
417 InstallSource::OfficialRelease
418 }
419}
420
421#[cfg(feature = "install")]
424#[derive(Debug, Clone, Default)]
425pub struct InstallOptions {
426 pub cache_dir: Option<String>,
429 pub source: InstallSource,
430 pub force: bool,
433}
434
435#[cfg(feature = "install")]
436impl Dict {
437 pub fn install() -> Result<Arc<Self>, Error> {
440 Self::install_with(InstallOptions::default())
441 }
442
443 pub fn install_from_url(url: String) -> Result<Arc<Self>, Error> {
445 Self::install_with(InstallOptions {
446 source: InstallSource::Url { url },
447 ..Default::default()
448 })
449 }
450
451 pub fn install_from_tarball(path: String) -> Result<Arc<Self>, Error> {
453 Self::install_with(InstallOptions {
454 source: InstallSource::Tarball { path },
455 ..Default::default()
456 })
457 }
458
459 pub fn install_with(options: InstallOptions) -> Result<Arc<Self>, Error> {
461 let mut core_opts = core::install::InstallOptions::default()
462 .source(match options.source {
463 InstallSource::OfficialRelease => core::install::InstallSource::OfficialRelease,
464 InstallSource::Url { url } => core::install::InstallSource::Url(url),
465 InstallSource::Tarball { path } => {
466 core::install::InstallSource::Tarball(std::path::PathBuf::from(path))
467 }
468 })
469 .force(options.force);
470 if let Some(p) = options.cache_dir {
471 core_opts = core_opts.cache_dir(std::path::PathBuf::from(p));
472 }
473 let inner = core::Dict::install_with(core_opts)?;
474 Ok(Arc::new(Self { inner }))
475 }
476}
477
478#[cfg(feature = "install")]
485pub fn init_sdk_cache_dir(path: String) -> Result<(), Error> {
486 core::install::init_sdk_cache_dir(std::path::PathBuf::from(path)).map_err(Into::into)
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 #[test]
494 fn error_codes_match_core() {
495 assert_eq!(Error::DataNotFound.code(), 1);
498 assert_eq!(
499 Error::DataVersionMismatch {
500 expected: 4,
501 found: 3
502 }
503 .code(),
504 2
505 );
506 assert_eq!(Error::DataCorrupted.code(), 3);
507 assert_eq!(Error::InvalidQuery.code(), 4);
508 assert_eq!(
509 Error::Io {
510 message: "boom".into()
511 }
512 .code(),
513 5
514 );
515 assert_eq!(Error::Deserialization.code(), 6);
516 }
517
518 #[test]
519 fn error_from_jmdict_io_collapses_to_string() {
520 let io = std::io::Error::new(std::io::ErrorKind::Other, "disk on fire");
521 let core_err = core::JmdictError::IoError(io);
522 match Error::from(core_err) {
523 Error::Io { message } => assert!(message.contains("disk on fire")),
524 other => panic!("expected Error::Io, got {other:?}"),
525 }
526 }
527
528 #[test]
529 fn query_options_defaults() {
530 let q = QueryOptions::default();
531 assert_eq!(q.mode, MatchMode::Exact);
532 assert!(!q.common_only);
533 assert!(q.pos.is_empty());
534 assert!(q.misc.is_empty());
535 assert!(q.field.is_empty());
536 assert!(q.dialect.is_empty());
537 assert_eq!(q.limit, None);
538 assert_eq!(q.max_distance, 2);
539 }
540}