Skip to main content

sudachi/
config.rs

1/*
2 * Copyright (c) 2021-2026 Works Applications Co., Ltd.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use 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/// Sudachi Error
40#[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    /// Return required InfoSubset for the current projection type
282    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/// Struct corresponds with raw config json file.
313/// You must use filed names defined here as json object key.
314/// For plugins, refer to each plugin.
315#[allow(non_snake_case)]
316#[derive(Deserialize, Serialize, Debug, Clone, Default)]
317pub struct RawConfig {
318    /// Analogue to Java Implementation path Override.
319    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)]
333/// Editable configuration source consisting of normalized config data and
334/// explicit resource-resolution state.
335pub 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    /// Creates a builder from already-deserialized config data.
348    ///
349    /// If the raw config contains `path`, it is immediately appended to the
350    /// resolver so runtime resolution state is explicit from construction time.
351    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    /// Loads config JSON from a file.
367    ///
368    /// The resulting builder contains the deserialized config data, appends the
369    /// config's `path` field to the resolver if present, and then appends the
370    /// parent directory of `config_file` as an additional filesystem root.
371    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    /// Loads config JSON from raw bytes.
383    ///
384    /// The resulting builder contains the deserialized config data and appends
385    /// the config's `path` field to the resolver if present.
386    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    /// Loads the bundled default config JSON.
392    ///
393    /// This only loads the embedded JSON contents; embedded resources
394    /// themselves are not enabled unless `push_embedded()` is called.
395    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/// Setting data loaded from config file
503#[derive(Debug, Default, Clone)]
504pub struct Config {
505    /// Paths will be resolved against these roots, until a file will be found
506    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    // this option is Python-only and is ignored in Rust APIs
516    pub projection: SurfaceProjection,
517}
518
519impl Config {
520    /// Creates a config from optional config, resource, and dictionary paths.
521    ///
522    /// Resolution precedence is:
523    /// 1. `resource_dir` (if given).
524    /// 2. The `path` field in the `config_file` (if set).
525    /// 3. The parent directory of the `config_file` (if given).
526    /// 4. The default (embedded) resources.
527    ///
528    /// When `config_file` is `None`, the embedded default `sudachi.json` is used as config
529    /// data.
530    pub fn new(
531        config_file: Option<PathBuf>,
532        resource_dir: Option<PathBuf>,
533        dictionary_path: Option<PathBuf>,
534    ) -> Result<Self, ConfigError> {
535        // prioritize arg (cli option) > default
536        let raw_config = ConfigBuilder::from_opt_file(config_file.as_deref())?;
537
538        // prioritize arg (cli option) > config file
539        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        // prioritize arg (cli option) > config file
546        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    /// Creates a minimal config with the provided path resolver
564    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    /// Sets the system dictionary to the provided path
577    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    /// Resolves a possibly relative path with regards to all possible anchors:
605    /// 1. Absolute paths stay as they are
606    /// 2. Paths are resolved wrt to anchors, returning the first existing one
607    /// 3. Path are checked wrt to CWD
608    /// 4. If all fail, return an error with all candidate paths listed
609    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        // 1. absolute paths are not normalized
622        if pref.is_absolute() {
623            return Ok(ResolvedResource::Path(file_path.into()));
624        }
625
626        // 2. try to resolve paths wrt anchors
627        if let Some(p) = self.resolver.first_existing(pref) {
628            return Ok(p);
629        }
630
631        // 3. try to resolve path wrt CWD
632        if pref.exists() {
633            return Ok(ResolvedResource::Path(file_path.into()));
634        }
635
636        // Report an error
637        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}