Echo Writes Code

lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
pub mod configuration;
pub mod errors;
pub mod math;

pub use configuration::{ * };
pub use errors::{ * };
pub use math::{ * };

use kdl::{ KdlDocument };
use zip::{ ZipArchive };

use std::fs::{ self, File };
use std::io::{ Read };
use std::path::{ Path, PathBuf };

/// The top-level object in any Crucible application.
#[derive(Debug)]
pub struct Application {
  configuration: ApplicationConfiguration,
  source: Option<PathBuf>,
}

impl Application {
  /// Construct an `Application` instance from a package on disk.
  ///
  /// The package can be either a directory or a zip file.
  pub fn from_package<P: AsRef<Path>>(path: P) -> Result<Application> {
    let path = path.as_ref();

    if path.is_dir() {
      Application::from_directory(path)
    } else {
      Application::from_zip_archive(path)
    }
  }

  /// Construct an `Application` instance from a directory.
  pub fn from_directory<P: AsRef<Path>>(path: P) -> Result<Application> {
    let Some(kdl) = Application::load_crucible_kdl_from_directory(&path)? else {
      return Ok(Application::default());
    };

    let configuration = kdl
      .get("application")
      .map(|node| ApplicationConfiguration::from_kdl_node(node))
      .unwrap_or(Ok(ApplicationConfiguration::default()))?;

    let source = Some(path.as_ref().to_path_buf());

    Ok(Application {
      configuration,
      source,
    })
  }

  /// Construct an `Application` instance from a zip file.
  pub fn from_zip_archive<P: AsRef<Path>>(path: P) -> Result<Application> {
    let Some(kdl) = Application::load_crucible_kdl_from_zip_archive(&path)? else {
      return Ok(Application::default());
    };

    let configuration = kdl
      .get("application")
      .map(|node| ApplicationConfiguration::from_kdl_node(node))
      .unwrap_or(Ok(ApplicationConfiguration::default()))?;

    let source = Some(path.as_ref().to_path_buf());

    Ok(Application {
      configuration,
      source,
    })
  }

  /// Get a reference to the application's configuration as loaded from the package.
  pub fn configuration(&self) -> &ApplicationConfiguration {
    &self.configuration
  }

  /// Get a reference to the source path of the application's package.
  pub fn source(&self) -> Option<&Path> {
    self.source.as_deref()
  }

  fn load_crucible_kdl_from_directory<P: AsRef<Path>>(path: P) -> Result<Option<KdlDocument>> {
    let path = path.as_ref();

    for entry in path.read_dir()? {
      let entry = entry?;
      let entry_path = entry.path();

      if !entry_path.is_file() {
        continue;
      }

      if !ApplicationConfiguration::is_configuration_file(&entry_path) {
        continue;
      }

      let content = fs::read_to_string(entry_path)?;
      let kdl = content.parse::<KdlDocument>()?;
      return Ok(Some(kdl))
    }

    Ok(None)
  }

  fn load_crucible_kdl_from_zip_archive<P: AsRef<Path>>(path: P) -> Result<Option<KdlDocument>> {
    let path = path.as_ref();
    let file = File::open(path)?;
    let mut archive = ZipArchive::new(file)?;

    for i in 0..archive.len() {
      let mut entry = archive.by_index(i)?;

      if !entry.is_file() {
        continue;
      }

      let entry_path = entry
        .enclosed_name()
        .ok_or(Error::BadZipFileEntry(i))?;

      if !ApplicationConfiguration::is_configuration_file(entry_path) {
        continue;
      }

      let content = {
        let mut buffer = String::new();
        entry.read_to_string(&mut buffer)?;
        buffer
      };

      let kdl = content.parse::<KdlDocument>()?;
      return Ok(Some(kdl))
    }

    Ok(None)
  }
}

impl Default for Application {
  fn default() -> Application {
    Application {
      configuration: ApplicationConfiguration::default(),
      source: None,
    }
  }
}