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}