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