chisel/
session.rs

1//! ChiselSession
2//!
3//! This module contains the `ChiselSession` struct, which is the top-level
4//! wrapper for a serializable REPL session.
5
6use crate::prelude::{SessionSource, SessionSourceConfig};
7use eyre::Result;
8use serde::{Deserialize, Serialize};
9use std::path::Path;
10use time::{format_description, OffsetDateTime};
11
12/// A Chisel REPL Session
13#[derive(Debug, Serialize, Deserialize)]
14pub struct ChiselSession {
15    /// The `SessionSource` object that houses the REPL session.
16    pub session_source: SessionSource,
17    /// The current session's identifier
18    pub id: Option<String>,
19}
20
21// ChiselSession Common Associated Functions
22impl ChiselSession {
23    /// Create a new `ChiselSession` with a specified `solc` version and configuration.
24    ///
25    /// ### Takes
26    ///
27    /// An instance of [SessionSourceConfig]
28    ///
29    /// ### Returns
30    ///
31    /// A new instance of [ChiselSession]
32    pub fn new(config: SessionSourceConfig) -> Result<Self> {
33        let solc = config.solc()?;
34        // Return initialized ChiselSession with set solc version
35        Ok(Self { session_source: SessionSource::new(solc, config), id: None })
36    }
37
38    /// Render the full source code for the current session.
39    ///
40    /// ### Returns
41    ///
42    /// Returns the full, flattened source code for the current session.
43    ///
44    /// ### Notes
45    ///
46    /// This function will not panic, but will return a blank string if the
47    /// session's [SessionSource] is None.
48    pub fn contract_source(&self) -> String {
49        self.session_source.to_repl_source()
50    }
51
52    /// Clears the cache directory
53    ///
54    /// ### WARNING
55    ///
56    /// This will delete all sessions from the cache.
57    /// There is no method of recovering these deleted sessions.
58    pub fn clear_cache() -> Result<()> {
59        let cache_dir = Self::cache_dir()?;
60        for entry in std::fs::read_dir(cache_dir)? {
61            let entry = entry?;
62            let path = entry.path();
63            if path.is_dir() {
64                std::fs::remove_dir_all(path)?;
65            } else {
66                std::fs::remove_file(path)?;
67            }
68        }
69        Ok(())
70    }
71
72    /// Writes the ChiselSession to a file by serializing it to a JSON string
73    ///
74    /// ### Returns
75    ///
76    /// Returns the path of the new cache file
77    pub fn write(&mut self) -> Result<String> {
78        // Try to create the cache directory
79        let cache_dir = Self::cache_dir()?;
80        std::fs::create_dir_all(&cache_dir)?;
81
82        let cache_file_name = match self.id.as_ref() {
83            Some(id) => {
84                // ID is already set- use the existing cache file.
85                format!("{cache_dir}chisel-{id}.json")
86            }
87            None => {
88                // Get the next session cache ID / file
89                let (id, file_name) = Self::next_cached_session()?;
90                // Set the session's ID
91                self.id = Some(id);
92                // Return the new session's cache file name
93                file_name
94            }
95        };
96
97        // Write the current ChiselSession to that file
98        let serialized_contents = serde_json::to_string_pretty(self)?;
99        std::fs::write(&cache_file_name, serialized_contents)?;
100
101        // Return the full cache file path
102        // Ex: /home/user/.foundry/cache/chisel/chisel-0.json
103        Ok(cache_file_name)
104    }
105
106    /// Get the next default session cache file name
107    ///
108    /// ### Returns
109    ///
110    /// Optionally, returns a tuple containing the next cached session's id and file name.
111    pub fn next_cached_session() -> Result<(String, String)> {
112        let cache_dir = Self::cache_dir()?;
113        let mut entries = std::fs::read_dir(&cache_dir)?;
114
115        // If there are no existing cached sessions, just create the first one: "chisel-0.json"
116        let mut latest = if let Some(e) = entries.next() {
117            e?
118        } else {
119            return Ok((String::from("0"), format!("{cache_dir}chisel-0.json")))
120        };
121
122        let mut session_num = 1;
123        // Get the latest cached session
124        for entry in entries {
125            let entry = entry?;
126            if entry.metadata()?.modified()? >= latest.metadata()?.modified()? {
127                latest = entry;
128            }
129
130            // Increase session_num counter rather than cloning the iterator and using `.count`
131            session_num += 1;
132        }
133
134        Ok((format!("{session_num}"), format!("{cache_dir}chisel-{session_num}.json")))
135    }
136
137    /// The Chisel Cache Directory
138    ///
139    /// ### Returns
140    ///
141    /// Optionally, the directory of the chisel cache.
142    pub fn cache_dir() -> Result<String> {
143        let home_dir =
144            dirs::home_dir().ok_or_else(|| eyre::eyre!("Failed to grab home directory"))?;
145        let home_dir_str = home_dir
146            .to_str()
147            .ok_or_else(|| eyre::eyre!("Failed to convert home directory to string"))?;
148        Ok(format!("{home_dir_str}/.foundry/cache/chisel/"))
149    }
150
151    /// Create the cache directory if it does not exist
152    ///
153    /// ### Returns
154    ///
155    /// The unit type if the operation was successful.
156    pub fn create_cache_dir() -> Result<()> {
157        let cache_dir = Self::cache_dir()?;
158        if !Path::new(&cache_dir).exists() {
159            std::fs::create_dir_all(&cache_dir)?;
160        }
161        Ok(())
162    }
163
164    /// Lists all available cached sessions
165    ///
166    /// ### Returns
167    ///
168    /// Optionally, a vector containing tuples of session IDs and cache-file names.
169    pub fn list_sessions() -> Result<Vec<(String, String)>> {
170        // Read the cache directory entries
171        let cache_dir = Self::cache_dir()?;
172        let entries = std::fs::read_dir(cache_dir)?;
173
174        // For each entry, get the file name and modified time
175        let mut sessions = Vec::new();
176        for entry in entries {
177            let entry = entry?;
178            let modified_time = entry.metadata()?.modified()?;
179            let file_name = entry.file_name();
180            let file_name = file_name
181                .into_string()
182                .map_err(|e| eyre::eyre!(format!("{}", e.to_string_lossy())))?;
183            sessions.push((
184                systemtime_strftime(modified_time, "[year]-[month]-[day] [hour]:[minute]:[second]")
185                    .unwrap(),
186                file_name,
187            ));
188        }
189
190        if sessions.is_empty() {
191            eyre::bail!("No sessions found!")
192        } else {
193            // Return the list of sessions and their modified times
194            Ok(sessions)
195        }
196    }
197
198    /// Loads a specific ChiselSession from the specified cache file
199    ///
200    /// ### Takes
201    ///
202    /// The ID of the chisel session that you wish to load.
203    ///
204    /// ### Returns
205    ///
206    /// Optionally, an owned instance of the loaded chisel session.
207    pub fn load(id: &str) -> Result<Self> {
208        let cache_dir = Self::cache_dir()?;
209        let contents = std::fs::read_to_string(Path::new(&format!("{cache_dir}chisel-{id}.json")))?;
210        let chisel_env: Self = serde_json::from_str(&contents)?;
211        Ok(chisel_env)
212    }
213
214    /// Gets the most recent chisel session from the cache dir
215    ///
216    /// ### Returns
217    ///
218    /// Optionally, the file name of the most recently modified cached session.
219    pub fn latest_cached_session() -> Result<String> {
220        let cache_dir = Self::cache_dir()?;
221        let mut entries = std::fs::read_dir(cache_dir)?;
222        let mut latest = entries.next().ok_or_else(|| eyre::eyre!("No entries found!"))??;
223        for entry in entries {
224            let entry = entry?;
225            if entry.metadata()?.modified()? > latest.metadata()?.modified()? {
226                latest = entry;
227            }
228        }
229        Ok(latest
230            .path()
231            .to_str()
232            .ok_or_else(|| eyre::eyre!("Failed to get session path!"))?
233            .to_string())
234    }
235
236    /// Loads the latest ChiselSession from the cache file
237    ///
238    /// ### Returns
239    ///
240    /// Optionally, an owned instance of the most recently modified cached session.
241    pub fn latest() -> Result<Self> {
242        let last_session = Self::latest_cached_session()?;
243        let last_session_contents = std::fs::read_to_string(Path::new(&last_session))?;
244        let chisel_env: Self = serde_json::from_str(&last_session_contents)?;
245        Ok(chisel_env)
246    }
247}
248
249/// Generic helper function that attempts to convert a type that has
250/// an [`Into<OffsetDateTime>`] implementation into a formatted date string.
251fn systemtime_strftime<T>(dt: T, format: &str) -> Result<String>
252where
253    T: Into<OffsetDateTime>,
254{
255    Ok(dt.into().format(&format_description::parse(format)?)?)
256}