1use std::convert::TryFrom;
18use std::env::current_exe;
19use std::fs::File;
20use std::io::{BufRead, BufReader, Cursor};
21use std::path::{Path, PathBuf};
22
23use crate::dic::subset::InfoSubset;
24use crate::error::SudachiError;
25use lazy_static::lazy_static;
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28use thiserror::Error;
29
30const DEFAULT_SETTING_FILE: &str = "sudachi.json";
31const DEFAULT_SETTING_BYTES: &[u8] = include_bytes!("../../resources/sudachi.json");
32pub(crate) const DEFAULT_CHAR_DEF_FILE: &str = "char.def";
33pub(crate) const DEFAULT_CHAR_DEF_BYTES: &[u8] = include_bytes!("../../resources/char.def");
34pub(crate) const DEFAULT_UNK_DEF_FILE: &str = "unk.def";
35const DEFAULT_UNK_DEF_BYTES: &[u8] = include_bytes!("../../resources/unk.def");
36pub(crate) const DEFAULT_REWRITE_DEF_FILE: &str = "rewrite.def";
37const DEFAULT_REWRITE_DEF_BYTES: &[u8] = include_bytes!("../../resources/rewrite.def");
38
39#[derive(Error, Debug)]
41pub enum ConfigError {
42 #[error("IO Error: {0}")]
43 Io(#[from] std::io::Error),
44
45 #[error("Serde error: {0}")]
46 SerdeError(#[from] serde_json::Error),
47
48 #[error("Config file not found")]
49 FileNotFound(String),
50
51 #[error("Invalid format: {0}")]
52 InvalidFormat(String),
53
54 #[error("Argument {0} is missing")]
55 MissingArgument(String),
56
57 #[error("{0} is only available as an embedded resource")]
58 EmbeddedResourcePath(String),
59
60 #[error("Failed to resolve relative path {0}, tried: {1:?}")]
61 PathResolution(String, Vec<String>),
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub(crate) enum EmbeddedResource {
66 Config,
67 CharDef,
68 UnkDef,
69 RewriteDef,
70}
71
72impl EmbeddedResource {
73 fn name(self) -> &'static str {
74 match self {
75 EmbeddedResource::Config => DEFAULT_SETTING_FILE,
76 EmbeddedResource::CharDef => DEFAULT_CHAR_DEF_FILE,
77 EmbeddedResource::UnkDef => DEFAULT_UNK_DEF_FILE,
78 EmbeddedResource::RewriteDef => DEFAULT_REWRITE_DEF_FILE,
79 }
80 }
81
82 fn bytes(self) -> &'static [u8] {
83 match self {
84 EmbeddedResource::Config => DEFAULT_SETTING_BYTES,
85 EmbeddedResource::CharDef => DEFAULT_CHAR_DEF_BYTES,
86 EmbeddedResource::UnkDef => DEFAULT_UNK_DEF_BYTES,
87 EmbeddedResource::RewriteDef => DEFAULT_REWRITE_DEF_BYTES,
88 }
89 }
90
91 fn from_path<P: AsRef<Path>>(path: P) -> Option<Self> {
92 let path = path.as_ref();
93 if path.is_absolute() || path.components().count() != 1 {
94 return None;
95 }
96 match path.to_str()? {
97 DEFAULT_SETTING_FILE => Some(EmbeddedResource::Config),
98 DEFAULT_CHAR_DEF_FILE => Some(EmbeddedResource::CharDef),
99 DEFAULT_UNK_DEF_FILE => Some(EmbeddedResource::UnkDef),
100 DEFAULT_REWRITE_DEF_FILE => Some(EmbeddedResource::RewriteDef),
101 _ => None,
102 }
103 }
104}
105
106#[derive(Clone, Debug, Eq, PartialEq)]
107pub(crate) enum ResolvedResource {
108 Path(PathBuf),
109 Embedded(EmbeddedResource),
110}
111
112impl ResolvedResource {
113 pub fn read_bytes(self) -> Result<Vec<u8>, ConfigError> {
114 match self {
115 ResolvedResource::Path(path) => std::fs::read(path).map_err(ConfigError::from),
116 ResolvedResource::Embedded(resource) => Ok(resource.bytes().to_vec()),
117 }
118 }
119
120 pub fn reader(self) -> Result<Box<dyn BufRead>, ConfigError> {
121 match self {
122 ResolvedResource::Path(path) => {
123 let file = File::open(path)?;
124 Ok(Box::new(BufReader::new(file)))
125 }
126 ResolvedResource::Embedded(resource) => {
127 Ok(Box::new(BufReader::new(Cursor::new(resource.bytes()))))
128 }
129 }
130 }
131
132 pub fn into_path(self) -> Result<PathBuf, ConfigError> {
133 match self {
134 ResolvedResource::Path(path) => Ok(path),
135 ResolvedResource::Embedded(resource) => Err(ConfigError::EmbeddedResourcePath(
136 resource.name().to_owned(),
137 )),
138 }
139 }
140}
141
142#[derive(Clone, Debug, Eq, PartialEq)]
143enum ResolverRoot {
144 Filesystem(PathBuf),
145 Embedded,
146}
147
148#[derive(Default, Debug, Clone)]
149pub struct PathResolver {
150 roots: Vec<ResolverRoot>,
151}
152
153impl PathResolver {
154 pub fn new() -> PathResolver {
155 Self::default()
156 }
157
158 pub fn from_path<P: Into<PathBuf>>(path: P) -> Self {
159 let mut resolver = Self::new();
160 resolver.push_root(path);
161 resolver
162 }
163
164 pub fn from_embedded() -> Self {
165 let mut resolver = Self::new();
166 resolver.push_embedded();
167 resolver
168 }
169
170 pub fn push_root<P: Into<PathBuf>>(&mut self, path: P) {
171 let path = path.into();
172 if !self.contains(&path) {
173 self.roots.push(ResolverRoot::Filesystem(path))
174 }
175 }
176
177 pub fn prepend_root<P: Into<PathBuf>>(&mut self, path: P) {
178 let path = path.into();
179 self.roots.retain(|root| match root {
180 ResolverRoot::Filesystem(existing) => existing != &path,
181 ResolverRoot::Embedded => true,
182 });
183 self.roots.insert(0, ResolverRoot::Filesystem(path));
184 }
185
186 pub fn push_embedded(&mut self) {
187 if !self.contains_embedded() {
188 self.roots.push(ResolverRoot::Embedded)
189 }
190 }
191
192 pub fn prepend_embedded(&mut self) {
193 self.roots.retain(|root| root != &ResolverRoot::Embedded);
194 self.roots.insert(0, ResolverRoot::Embedded);
195 }
196
197 pub fn append(&mut self, other: PathResolver) {
198 for root in other.roots {
199 match root {
200 ResolverRoot::Filesystem(path) => self.push_root(path),
201 ResolverRoot::Embedded => self.push_embedded(),
202 }
203 }
204 }
205
206 pub fn prepend(&mut self, other: PathResolver) {
207 for root in other.roots.into_iter().rev() {
208 match root {
209 ResolverRoot::Filesystem(path) => self.prepend_root(path),
210 ResolverRoot::Embedded => self.prepend_embedded(),
211 }
212 }
213 }
214
215 fn contains<P: AsRef<Path>>(&self, path: P) -> bool {
216 let query = path.as_ref();
217 self.roots.iter().any(|p| match p {
218 ResolverRoot::Filesystem(root) => root.as_path() == query,
219 ResolverRoot::Embedded => false,
220 })
221 }
222
223 fn contains_embedded(&self) -> bool {
224 self.roots.contains(&ResolverRoot::Embedded)
225 }
226
227 fn first_existing<P: AsRef<Path> + Clone>(&self, path: P) -> Option<ResolvedResource> {
228 self.roots.iter().find_map(|root| match root {
229 ResolverRoot::Filesystem(base) => {
230 let candidate = base.join(path.clone());
231 candidate
232 .exists()
233 .then_some(ResolvedResource::Path(candidate))
234 }
235 ResolverRoot::Embedded => {
236 EmbeddedResource::from_path(path.clone()).map(ResolvedResource::Embedded)
237 }
238 })
239 }
240
241 fn resolution_failure<P: AsRef<Path> + Clone>(&self, path: P) -> ConfigError {
242 let candidates = self.all_candidates(path.clone()).collect();
243
244 ConfigError::PathResolution(path.as_ref().to_string_lossy().into_owned(), candidates)
245 }
246
247 fn all_candidates<'a, P: AsRef<Path> + Clone + 'a>(
248 &'a self,
249 path: P,
250 ) -> impl Iterator<Item = String> + 'a {
251 self.roots.iter().map(move |root| match root {
252 ResolverRoot::Filesystem(base) => {
253 base.join(path.clone()).to_string_lossy().into_owned()
254 }
255 ResolverRoot::Embedded => format!("<embedded>/{}", path.as_ref().display()),
256 })
257 }
258
259 fn filesystem_roots(&self) -> impl Iterator<Item = &PathBuf> {
260 self.roots.iter().filter_map(|root| match root {
261 ResolverRoot::Filesystem(path) => Some(path),
262 ResolverRoot::Embedded => None,
263 })
264 }
265}
266
267#[derive(Deserialize, Serialize, Clone, Copy, Debug, Eq, PartialEq, Default)]
268#[serde(rename_all = "snake_case")]
269pub enum SurfaceProjection {
270 #[default]
271 Surface,
272 Normalized,
273 Reading,
274 Dictionary,
275 DictionaryAndSurface,
276 NormalizedAndSurface,
277 NormalizedNouns,
278}
279
280impl SurfaceProjection {
281 pub fn required_subset(&self) -> InfoSubset {
283 match *self {
284 SurfaceProjection::Surface => InfoSubset::empty(),
285 SurfaceProjection::Normalized => InfoSubset::NORMALIZED_FORM,
286 SurfaceProjection::Reading => InfoSubset::READING_FORM,
287 SurfaceProjection::Dictionary => InfoSubset::DICTIONARY_FORM,
288 SurfaceProjection::DictionaryAndSurface => InfoSubset::DICTIONARY_FORM,
289 SurfaceProjection::NormalizedAndSurface => InfoSubset::NORMALIZED_FORM,
290 SurfaceProjection::NormalizedNouns => InfoSubset::NORMALIZED_FORM,
291 }
292 }
293}
294
295impl TryFrom<&str> for SurfaceProjection {
296 type Error = SudachiError;
297
298 fn try_from(value: &str) -> Result<Self, Self::Error> {
299 match value {
300 "surface" => Ok(SurfaceProjection::Surface),
301 "normalized" => Ok(SurfaceProjection::Normalized),
302 "reading" => Ok(SurfaceProjection::Reading),
303 "dictionary" => Ok(SurfaceProjection::Dictionary),
304 "dictionary_and_surface" => Ok(SurfaceProjection::DictionaryAndSurface),
305 "normalized_and_surface" => Ok(SurfaceProjection::NormalizedAndSurface),
306 "normalized_nouns" => Ok(SurfaceProjection::NormalizedNouns),
307 _ => Err(ConfigError::InvalidFormat(format!("unknown projection: {value}")).into()),
308 }
309 }
310}
311
312#[allow(non_snake_case)]
316#[derive(Deserialize, Serialize, Debug, Clone, Default)]
317pub struct RawConfig {
318 path: Option<PathBuf>,
320 #[serde(alias = "system")]
321 systemDict: Option<PathBuf>,
322 #[serde(alias = "user")]
323 userDict: Option<Vec<PathBuf>>,
324 characterDefinitionFile: Option<PathBuf>,
325 connectionCostPlugin: Option<Vec<Value>>,
326 inputTextPlugin: Option<Vec<Value>>,
327 oovProviderPlugin: Option<Vec<Value>>,
328 pathRewritePlugin: Option<Vec<Value>>,
329 projection: Option<SurfaceProjection>,
330}
331
332#[derive(Debug, Clone, Default)]
333pub struct ConfigBuilder {
336 data: RawConfig,
337 resolver: PathResolver,
338}
339
340macro_rules! merge_cfg_value {
341 ($base: expr, $o: expr, $name: tt) => {
342 $base.$name = $base.$name.or_else(|| $o.$name.clone())
343 };
344}
345
346impl ConfigBuilder {
347 fn from_data(data: RawConfig) -> Self {
352 let mut resolver = PathResolver::new();
353 if let Some(path) = data.path.clone() {
354 resolver.push_root(path);
355 }
356 Self { data, resolver }
357 }
358
359 pub fn from_opt_file(config_file: Option<&Path>) -> Result<Self, ConfigError> {
360 match config_file {
361 None => Self::from_embedded(),
362 Some(cfg) => Self::from_file(cfg),
363 }
364 }
365
366 pub fn from_file(config_file: &Path) -> Result<Self, ConfigError> {
372 let file = File::open(config_file)?;
373 let reader = BufReader::new(file);
374 let data: RawConfig = serde_json::from_reader(reader).map_err(ConfigError::from)?;
375 let mut cfg = Self::from_data(data);
376 if let Some(parent) = config_file.parent() {
377 cfg.resolver.push_root(parent);
378 }
379 Ok(cfg)
380 }
381
382 pub fn from_bytes(data: &[u8]) -> Result<Self, ConfigError> {
387 let data = serde_json::from_slice(data).map_err(ConfigError::from)?;
388 Ok(Self::from_data(data))
389 }
390
391 pub fn from_embedded() -> Result<Self, ConfigError> {
396 Self::from_bytes(DEFAULT_SETTING_BYTES)
397 }
398
399 pub fn empty() -> Self {
400 Self::default()
401 }
402
403 pub fn system_dict(mut self, dict: impl Into<PathBuf>) -> Self {
404 self.data.systemDict = Some(dict.into());
405 self
406 }
407
408 pub fn user_dict(mut self, dict: impl Into<PathBuf>) -> Self {
409 let dicts = match self.data.userDict.as_mut() {
410 None => {
411 self.data.userDict = Some(Default::default());
412 self.data.userDict.as_mut().unwrap()
413 }
414 Some(dicts) => dicts,
415 };
416 dicts.push(dict.into());
417 self
418 }
419
420 pub fn character_definition_file(mut self, path: impl Into<PathBuf>) -> Self {
421 self.data.characterDefinitionFile = Some(path.into());
422 self
423 }
424
425 pub fn projection(mut self, projection: SurfaceProjection) -> Self {
426 self.data.projection = Some(projection);
427 self
428 }
429
430 pub fn with_resolver(mut self, resolver: PathResolver) -> Self {
431 self.resolver = resolver;
432 self
433 }
434
435 pub fn append_resolver(mut self, other: PathResolver) -> Self {
436 self.resolver.append(other);
437 self
438 }
439
440 pub fn prepend_resolver(mut self, other: PathResolver) -> Self {
441 self.resolver.prepend(other);
442 self
443 }
444
445 pub fn push_resolver_root(mut self, path: impl Into<PathBuf>) -> Self {
446 self.resolver.push_root(path);
447 self
448 }
449
450 pub fn prepend_resolver_root(mut self, path: impl Into<PathBuf>) -> Self {
451 self.resolver.prepend_root(path);
452 self
453 }
454
455 pub fn push_embedded(mut self) -> Self {
456 self.resolver.push_embedded();
457 self
458 }
459
460 pub fn fallback_data(mut self, other: &ConfigBuilder) -> ConfigBuilder {
461 merge_cfg_value!(self.data, other.data, path);
462 merge_cfg_value!(self.data, other.data, systemDict);
463 merge_cfg_value!(self.data, other.data, userDict);
464 merge_cfg_value!(self.data, other.data, characterDefinitionFile);
465 merge_cfg_value!(self.data, other.data, connectionCostPlugin);
466 merge_cfg_value!(self.data, other.data, inputTextPlugin);
467 merge_cfg_value!(self.data, other.data, oovProviderPlugin);
468 merge_cfg_value!(self.data, other.data, pathRewritePlugin);
469 merge_cfg_value!(self.data, other.data, projection);
470 self
471 }
472
473 pub fn raw_config(&self) -> &RawConfig {
474 &self.data
475 }
476
477 pub fn into_raw_config(self) -> RawConfig {
478 self.data
479 }
480
481 pub fn build(self) -> Config {
482 let character_definition_file = self
483 .data
484 .characterDefinitionFile
485 .unwrap_or(PathBuf::from(DEFAULT_CHAR_DEF_FILE));
486
487 Config {
488 resolver: self.resolver,
489 system_dict: self.data.systemDict,
490 user_dicts: self.data.userDict.unwrap_or_default(),
491 character_definition_file,
492
493 connection_cost_plugins: self.data.connectionCostPlugin.unwrap_or_default(),
494 input_text_plugins: self.data.inputTextPlugin.unwrap_or_default(),
495 oov_provider_plugins: self.data.oovProviderPlugin.unwrap_or_default(),
496 path_rewrite_plugins: self.data.pathRewritePlugin.unwrap_or_default(),
497 projection: self.data.projection.unwrap_or(SurfaceProjection::Surface),
498 }
499 }
500}
501
502#[derive(Debug, Default, Clone)]
504pub struct Config {
505 pub resolver: PathResolver,
507 pub system_dict: Option<PathBuf>,
508 pub user_dicts: Vec<PathBuf>,
509 pub character_definition_file: PathBuf,
510
511 pub connection_cost_plugins: Vec<Value>,
512 pub input_text_plugins: Vec<Value>,
513 pub oov_provider_plugins: Vec<Value>,
514 pub path_rewrite_plugins: Vec<Value>,
515 pub projection: SurfaceProjection,
517}
518
519impl Config {
520 pub fn new(
531 config_file: Option<PathBuf>,
532 resource_dir: Option<PathBuf>,
533 dictionary_path: Option<PathBuf>,
534 ) -> Result<Self, ConfigError> {
535 let raw_config = ConfigBuilder::from_opt_file(config_file.as_deref())?;
537
538 let raw_config = match resource_dir {
540 Some(p) => raw_config.prepend_resolver_root(p),
541 None => raw_config,
542 }
543 .push_embedded();
544
545 let raw_config = match dictionary_path {
547 Some(p) => raw_config.system_dict(p),
548 None => raw_config,
549 };
550
551 Ok(raw_config.build())
552 }
553
554 pub fn empty() -> Self {
555 Self::default()
556 }
557
558 pub fn new_embedded() -> Result<Self, ConfigError> {
559 let raw_config = ConfigBuilder::from_embedded()?.push_embedded();
560 Ok(raw_config.build())
561 }
562
563 pub fn minimal_at(resolver: PathResolver) -> Config {
565 let mut cfg = ConfigBuilder::empty().append_resolver(resolver).build();
566 cfg.oov_provider_plugins = vec![serde_json::json!(
567 { "class" : "com.worksap.nlp.sudachi.SimpleOovPlugin",
568 "oovPOS" : [ "名詞", "普通名詞", "一般", "*", "*", "*" ],
569 "leftId" : 0,
570 "rightId" : 0,
571 "cost" : 30000 }
572 )];
573 cfg
574 }
575
576 pub fn with_system_dic(mut self, system: impl Into<PathBuf>) -> Config {
578 self.system_dict = Some(system.into());
579 self
580 }
581
582 pub fn resolve_paths(&self, mut path: String) -> Vec<String> {
583 if path.starts_with("$exe") {
584 path.replace_range(0..4, &CURRENT_EXE_DIR);
585
586 let mut path2 = path.clone();
587 path2.insert_str(CURRENT_EXE_DIR.len(), "/deps");
588 return vec![path2, path];
589 }
590
591 if path.starts_with("$cfg/") || path.starts_with("$cfg\\") {
592 let mut result = Vec::new();
593 path.replace_range(0..5, "");
594 for root in self.resolver.filesystem_roots() {
595 let subpath = root.join(&path);
596 result.push(subpath.to_string_lossy().into_owned());
597 }
598 return result;
599 }
600
601 vec![path]
602 }
603
604 pub fn complete_path<P: AsRef<Path> + Into<PathBuf>>(
610 &self,
611 file_path: P,
612 ) -> Result<PathBuf, ConfigError> {
613 self.resolve_resource(file_path)?.into_path()
614 }
615
616 pub(crate) fn resolve_resource<P: AsRef<Path> + Into<PathBuf>>(
617 &self,
618 file_path: P,
619 ) -> Result<ResolvedResource, ConfigError> {
620 let pref = file_path.as_ref();
621 if pref.is_absolute() {
623 return Ok(ResolvedResource::Path(file_path.into()));
624 }
625
626 if let Some(p) = self.resolver.first_existing(pref) {
628 return Ok(p);
629 }
630
631 if pref.exists() {
633 return Ok(ResolvedResource::Path(file_path.into()));
634 }
635
636 Err(self.resolver.resolution_failure(&file_path))
638 }
639
640 pub fn resolved_system_dict(&self) -> Result<PathBuf, ConfigError> {
641 match self.system_dict.as_ref() {
642 Some(p) => self.complete_path(p),
643 None => Err(ConfigError::MissingArgument("systemDict".to_owned())),
644 }
645 }
646
647 pub fn resolved_user_dicts(&self) -> Result<Vec<PathBuf>, ConfigError> {
648 self.user_dicts
649 .iter()
650 .map(|p| self.complete_path(p))
651 .collect()
652 }
653}
654
655fn current_exe_dir() -> String {
656 let exe = current_exe().unwrap_or_else(|e| panic!("Current exe is not available {:?}", e));
657
658 let parent = exe
659 .parent()
660 .unwrap_or_else(|| panic!("Path to executable must have a parent"));
661
662 parent.to_str().map(|s| s.to_owned()).unwrap_or_else(|| {
663 panic!("placing Sudachi in directories with non-utf paths is not supported")
664 })
665}
666
667lazy_static! {
668 static ref CURRENT_EXE_DIR: String = current_exe_dir();
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674 use crate::prelude::SudachiResult;
675
676 use super::CURRENT_EXE_DIR;
677
678 #[test]
679 fn resolve_exe() -> SudachiResult<()> {
680 let cfg = Config::new(None, None, None)?;
681 let npath = cfg.resolve_paths("$exe/data".to_owned());
682 let exe_dir: &str = &CURRENT_EXE_DIR;
683 assert_eq!(npath.len(), 2);
684 assert!(npath[0].starts_with(exe_dir));
685 Ok(())
686 }
687
688 #[test]
689 fn resolve_cfg() -> SudachiResult<()> {
690 let cfg = Config::new(None, None, None)?;
691 let npath = cfg.resolve_paths("$cfg/data".to_owned());
692 assert!(npath.is_empty());
693 Ok(())
694 }
695
696 #[test]
697 fn config_builder_fallback() {
698 let cfg = ConfigBuilder::from_bytes(br#"{ "path": "test" }"#).unwrap();
699 let cfg2 = ConfigBuilder::empty();
700 let cfg2 = cfg2.fallback_data(&cfg);
701 assert_eq!(cfg2.raw_config().path, Some("test".into()));
702 }
703
704 #[test]
705 fn embedded_resources_can_be_enabled_explicitly() -> SudachiResult<()> {
706 let cfg = ConfigBuilder::empty().push_embedded().build();
707 let res = cfg.resolve_resource(DEFAULT_CHAR_DEF_FILE)?;
708 assert!(matches!(
709 res,
710 ResolvedResource::Embedded(EmbeddedResource::CharDef)
711 ));
712 Ok(())
713 }
714
715 #[test]
716 fn embedded_resources_are_disabled_by_default() {
717 let cfg = ConfigBuilder::empty().build();
718 let err = cfg.resolve_resource(DEFAULT_CHAR_DEF_FILE).unwrap_err();
719 assert!(matches!(err, ConfigError::PathResolution(_, _)));
720 }
721
722 #[test]
723 fn embedded_resource_can_not_be_forced_into_path() {
724 let cfg = ConfigBuilder::empty().push_embedded().build();
725 let err = cfg.complete_path(DEFAULT_CHAR_DEF_FILE).unwrap_err();
726 assert!(matches!(
727 err,
728 ConfigError::EmbeddedResourcePath(name) if name == DEFAULT_CHAR_DEF_FILE
729 ));
730 }
731
732 #[test]
733 fn from_file_sets_path_before_parent() -> SudachiResult<()> {
734 let cfg_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/resources/sudachi.json");
735 let cfg = ConfigBuilder::from_file(&cfg_path)?;
736 let roots: Vec<_> = cfg.raw_config().path.iter().cloned().collect();
737 assert_eq!(roots, vec![PathBuf::from("tests/resources/")]);
738 let npath = cfg.build().resolve_paths("$cfg/data".to_owned());
739 assert_eq!(
740 PathBuf::from(&npath[0]),
741 Path::new("tests/resources").join("data")
742 );
743 assert_eq!(
744 PathBuf::from(&npath[1]),
745 cfg_path.parent().unwrap().join("data")
746 );
747 Ok(())
748 }
749
750 #[test]
751 fn prepend_resolver_root_takes_priority() {
752 let cfg = ConfigBuilder::from_bytes(br#"{ "path": "config-root" }"#)
753 .unwrap()
754 .prepend_resolver_root("resource-root");
755 let roots: Vec<_> = cfg.resolver.filesystem_roots().cloned().collect();
756 assert_eq!(
757 roots,
758 vec![PathBuf::from("resource-root"), PathBuf::from("config-root")]
759 );
760 }
761
762 #[test]
763 fn surface_projection_tryfrom() {
764 assert_eq!(
765 SurfaceProjection::Surface,
766 SurfaceProjection::try_from("surface").unwrap()
767 );
768 }
769
770 #[test]
771 fn config_builder_sets_character_definition_file() {
772 let cfg = ConfigBuilder::empty()
773 .character_definition_file("custom.def")
774 .build();
775 assert_eq!(cfg.character_definition_file, PathBuf::from("custom.def"));
776 }
777
778 #[test]
779 fn config_builder_sets_projection() {
780 let cfg = ConfigBuilder::empty()
781 .projection(SurfaceProjection::Reading)
782 .build();
783 assert_eq!(cfg.projection, SurfaceProjection::Reading);
784 }
785}