dotfiles_actions/link/
directive.rs

1// Copyright (c) 2021-2022 Miguel Barreto and others
2//
3// Permission is hereby granted, free of charge, to any person obtaining
4// a copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to
8// permit persons to whom the Software is furnished to do so, subject to
9// the following conditions:
10//
11// The above copyright notice and this permission notice shall be
12// included in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22//! This module defines [LinkDirective].
23
24extern crate strict_yaml_rust;
25
26use crate::filesystem::FileSystemDirective;
27use crate::link::action::LinkAction;
28use dotfiles_core::action::ActionParser;
29use dotfiles_core::action::SKIP_IN_CI_SETTING;
30use dotfiles_core::directive::Directive;
31use dotfiles_core::directive::DirectiveData;
32use dotfiles_core::directive::HasDirectiveData;
33use dotfiles_core::error::DotfilesError;
34use dotfiles_core::error::ErrorType;
35use dotfiles_core::settings::initialize_settings_object;
36use dotfiles_core::settings::Setting;
37use dotfiles_core::settings::Settings;
38use dotfiles_core::yaml_util::*;
39use dotfiles_core_macros::Directive;
40use filesystem::FakeFileSystem;
41use filesystem::FileSystem;
42use filesystem::OsFileSystem;
43use filesystem::UnixFileSystem;
44use std::marker::PhantomData;
45use std::path::Path;
46use strict_yaml_rust::StrictYaml;
47
48/// Name of the link directive
49pub const DIRECTIVE_NAME: &str = "link";
50/// Path setting (path of the symlink)
51pub const PATH_SETTING: &str = "path";
52/// Target setting (path to the file the symlink points to)
53pub const TARGET_SETTING: &str = "target";
54/// Force setting, replaces any other file or directory
55pub const FORCE_SETTING: &str = "force";
56/// Relink setting, if true the action relinks an existing symlink
57/// (applies if force is false)
58pub const RELINK_SETTING: &str = "relink";
59/// Create parent dirs if they don't exist
60pub const CREATE_PARENT_DIRS_SETTING: &str = "create_parent_dirs";
61/// Create the symlink even if the target file does not exist
62pub const IGNORE_MISSING_TARGET_SETTING: &str = "ignore_missing_target";
63/// Resolves the target if it is a symlink and uses the final target file as the target.
64pub const RESOLVE_SYMLINK_TARGET_SETTING: &str = "resolve_symlink_target";
65
66/// Initialize the defaults for the LinkDirective.
67pub fn init_directive_data() -> DirectiveData {
68  DirectiveData::from(
69    DIRECTIVE_NAME.into(),
70    initialize_settings_object(&[
71      (FORCE_SETTING.to_owned(), Setting::Boolean(false)),
72      (RELINK_SETTING.to_owned(), Setting::Boolean(false)),
73      (
74        CREATE_PARENT_DIRS_SETTING.to_owned(),
75        Setting::Boolean(false),
76      ),
77      (
78        IGNORE_MISSING_TARGET_SETTING.to_owned(),
79        Setting::Boolean(false),
80      ),
81      (
82        RESOLVE_SYMLINK_TARGET_SETTING.to_owned(),
83        Setting::Boolean(false),
84      ),
85      (SKIP_IN_CI_SETTING.to_owned(), Setting::Boolean(false)),
86    ]),
87  )
88}
89
90/// A directive that can build [LinkAction]s to create directories
91/// in the filesystem.
92#[derive(Directive, Clone)]
93pub struct LinkDirective<'a, F: FileSystem + UnixFileSystem + Default> {
94  fs: F,
95  data: DirectiveData,
96  phantom: PhantomData<&'a F>,
97}
98
99/// [LinkDirective] that uses the native [OsFileSystem].
100pub type NativeLinkDirective<'a> = LinkDirective<'a, OsFileSystem>;
101/// [LinkDirective] that uses the native [FakeFileSystem] for testing.
102pub type FakeLinkDirective<'a> = LinkDirective<'a, FakeFileSystem>;
103
104impl<'a, F: FileSystem + UnixFileSystem + Default> Default for LinkDirective<'a, F> {
105  fn default() -> Self {
106    Self {
107      fs: Default::default(),
108      data: init_directive_data(),
109      phantom: Default::default(),
110    }
111  }
112}
113
114impl<'a, F: FileSystem + UnixFileSystem + Default> FileSystemDirective<'a, F>
115  for LinkDirective<'a, F>
116{
117  fn fs(&self) -> &F {
118    &self.fs
119  }
120
121  fn mut_fs(&mut self) -> &mut F {
122    &mut self.fs
123  }
124}
125
126impl<'a, F: FileSystem + UnixFileSystem + Default> LinkDirective<'a, F>
127where
128  LinkDirective<'a, F>: HasDirectiveData<'a> + Directive<'a>,
129{
130  /// Returns the [FileSystem] instance being used.
131  pub fn fs(&self) -> &F {
132    &self.fs
133  }
134
135  fn parse_full_action(
136    &'a self,
137    context_settings: &Settings,
138    yaml: &StrictYaml,
139    current_dir: &Path,
140  ) -> Result<LinkAction<'a, F>, DotfilesError> {
141    let path = get_string_setting_from_yaml_or_context(
142      PATH_SETTING,
143      yaml,
144      context_settings,
145      self.data.defaults(),
146    )?;
147    let target = get_string_setting_from_yaml_or_context(
148      TARGET_SETTING,
149      yaml,
150      context_settings,
151      self.data.defaults(),
152    )?;
153    let action_settings: Result<Settings, DotfilesError> = self
154      .directive_data()
155      .defaults()
156      .iter()
157      .map(|(name, _)| {
158        self
159          .get_setting_from_yaml_hash_or_from_context(name, yaml, context_settings)
160          .map(|setting| (name.to_owned(), setting))
161      })
162      .collect();
163
164    LinkAction::<'a, F>::new(
165      &self.fs,
166      path,
167      target,
168      &action_settings?,
169      self.data.defaults(),
170      current_dir.to_owned(),
171    )
172  }
173
174  /// Parse a shortened action with only link name to target name
175  pub fn parse_shortened_action(
176    &'a self,
177    context_settings: &Settings,
178    yaml: &StrictYaml,
179    current_dir: &Path,
180  ) -> Result<LinkAction<'a, F>, DotfilesError> {
181    if let StrictYaml::Hash(hash) = yaml {
182      match hash.len() {
183        1 => {
184          if let (StrictYaml::String(path), StrictYaml::String(target)) = hash.front().unwrap() {
185            LinkAction::<'a, F>::new(
186              &self.fs,
187              path.clone(),
188              target.clone(),
189              context_settings,
190              self.data.defaults(),
191              current_dir.to_owned()
192            )
193          } else {
194            Err(DotfilesError::from_wrong_yaml(
195                        "StrictYaml passed to configure a short Link action is not a hash of string to string, cant parse".into(),
196                        yaml.to_owned(), StrictYaml::Hash(Default::default())))
197          }
198        }
199
200        x => Err(DotfilesError::from(
201          format!(
202            "StrictYaml passed to configure a short Link action is a hash with {x} values, must be just 1",),
203          ErrorType::InconsistentConfigurationError,
204        )),
205      }
206    } else {
207      Err(DotfilesError::from_wrong_yaml(
208        "StrictYaml passed to configure a Link action is not a Hash".into(),
209        yaml.to_owned(),
210        StrictYaml::Hash(Default::default()),
211      ))
212    }
213  }
214}
215
216impl<'a, F: FileSystem + UnixFileSystem + Default> ActionParser<'a> for LinkDirective<'a, F> {
217  type ActionType = LinkAction<'a, F>;
218
219  fn parse_action(
220    &'a self,
221    settings: &Settings,
222    yaml: &StrictYaml,
223    current_directory: &Path,
224  ) -> Result<LinkAction<'a, F>, DotfilesError> {
225    self
226      .parse_shortened_action(settings, yaml, current_directory)
227      .or_else(|_| self.parse_full_action(settings, yaml, current_directory))
228  }
229}