chisel/
session.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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
//! ChiselSession
//!
//! This module contains the `ChiselSession` struct, which is the top-level
//! wrapper for a serializable REPL session.

use crate::prelude::{SessionSource, SessionSourceConfig};
use eyre::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
use time::{format_description, OffsetDateTime};

/// A Chisel REPL Session
#[derive(Debug, Serialize, Deserialize)]
pub struct ChiselSession {
    /// The `SessionSource` object that houses the REPL session.
    pub session_source: SessionSource,
    /// The current session's identifier
    pub id: Option<String>,
}

// ChiselSession Common Associated Functions
impl ChiselSession {
    /// Create a new `ChiselSession` with a specified `solc` version and configuration.
    ///
    /// ### Takes
    ///
    /// An instance of [SessionSourceConfig]
    ///
    /// ### Returns
    ///
    /// A new instance of [ChiselSession]
    pub fn new(config: SessionSourceConfig) -> Result<Self> {
        let solc = config.solc()?;
        // Return initialized ChiselSession with set solc version
        Ok(Self { session_source: SessionSource::new(solc, config), id: None })
    }

    /// Render the full source code for the current session.
    ///
    /// ### Returns
    ///
    /// Returns the full, flattened source code for the current session.
    ///
    /// ### Notes
    ///
    /// This function will not panic, but will return a blank string if the
    /// session's [SessionSource] is None.
    pub fn contract_source(&self) -> String {
        self.session_source.to_repl_source()
    }

    /// Clears the cache directory
    ///
    /// ### WARNING
    ///
    /// This will delete all sessions from the cache.
    /// There is no method of recovering these deleted sessions.
    pub fn clear_cache() -> Result<()> {
        let cache_dir = Self::cache_dir()?;
        for entry in std::fs::read_dir(cache_dir)? {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                std::fs::remove_dir_all(path)?;
            } else {
                std::fs::remove_file(path)?;
            }
        }
        Ok(())
    }

    /// Writes the ChiselSession to a file by serializing it to a JSON string
    ///
    /// ### Returns
    ///
    /// Returns the path of the new cache file
    pub fn write(&mut self) -> Result<String> {
        // Try to create the cache directory
        let cache_dir = Self::cache_dir()?;
        std::fs::create_dir_all(&cache_dir)?;

        let cache_file_name = match self.id.as_ref() {
            Some(id) => {
                // ID is already set- use the existing cache file.
                format!("{cache_dir}chisel-{id}.json")
            }
            None => {
                // Get the next session cache ID / file
                let (id, file_name) = Self::next_cached_session()?;
                // Set the session's ID
                self.id = Some(id);
                // Return the new session's cache file name
                file_name
            }
        };

        // Write the current ChiselSession to that file
        let serialized_contents = serde_json::to_string_pretty(self)?;
        std::fs::write(&cache_file_name, serialized_contents)?;

        // Return the full cache file path
        // Ex: /home/user/.foundry/cache/chisel/chisel-0.json
        Ok(cache_file_name)
    }

    /// Get the next default session cache file name
    ///
    /// ### Returns
    ///
    /// Optionally, returns a tuple containing the next cached session's id and file name.
    pub fn next_cached_session() -> Result<(String, String)> {
        let cache_dir = Self::cache_dir()?;
        let mut entries = std::fs::read_dir(&cache_dir)?;

        // If there are no existing cached sessions, just create the first one: "chisel-0.json"
        let mut latest = if let Some(e) = entries.next() {
            e?
        } else {
            return Ok((String::from("0"), format!("{cache_dir}chisel-0.json")))
        };

        let mut session_num = 1;
        // Get the latest cached session
        for entry in entries {
            let entry = entry?;
            if entry.metadata()?.modified()? >= latest.metadata()?.modified()? {
                latest = entry;
            }

            // Increase session_num counter rather than cloning the iterator and using `.count`
            session_num += 1;
        }

        Ok((format!("{session_num}"), format!("{cache_dir}chisel-{session_num}.json")))
    }

    /// The Chisel Cache Directory
    ///
    /// ### Returns
    ///
    /// Optionally, the directory of the chisel cache.
    pub fn cache_dir() -> Result<String> {
        let home_dir =
            dirs::home_dir().ok_or_else(|| eyre::eyre!("Failed to grab home directory"))?;
        let home_dir_str = home_dir
            .to_str()
            .ok_or_else(|| eyre::eyre!("Failed to convert home directory to string"))?;
        Ok(format!("{home_dir_str}/.foundry/cache/chisel/"))
    }

    /// Create the cache directory if it does not exist
    ///
    /// ### Returns
    ///
    /// The unit type if the operation was successful.
    pub fn create_cache_dir() -> Result<()> {
        let cache_dir = Self::cache_dir()?;
        if !Path::new(&cache_dir).exists() {
            std::fs::create_dir_all(&cache_dir)?;
        }
        Ok(())
    }

    /// Lists all available cached sessions
    ///
    /// ### Returns
    ///
    /// Optionally, a vector containing tuples of session IDs and cache-file names.
    pub fn list_sessions() -> Result<Vec<(String, String)>> {
        // Read the cache directory entries
        let cache_dir = Self::cache_dir()?;
        let entries = std::fs::read_dir(cache_dir)?;

        // For each entry, get the file name and modified time
        let mut sessions = Vec::new();
        for entry in entries {
            let entry = entry?;
            let modified_time = entry.metadata()?.modified()?;
            let file_name = entry.file_name();
            let file_name = file_name
                .into_string()
                .map_err(|e| eyre::eyre!(format!("{}", e.to_string_lossy())))?;
            sessions.push((
                systemtime_strftime(modified_time, "[year]-[month]-[day] [hour]:[minute]:[second]")
                    .unwrap(),
                file_name,
            ));
        }

        if sessions.is_empty() {
            eyre::bail!("No sessions found!")
        } else {
            // Return the list of sessions and their modified times
            Ok(sessions)
        }
    }

    /// Loads a specific ChiselSession from the specified cache file
    ///
    /// ### Takes
    ///
    /// The ID of the chisel session that you wish to load.
    ///
    /// ### Returns
    ///
    /// Optionally, an owned instance of the loaded chisel session.
    pub fn load(id: &str) -> Result<Self> {
        let cache_dir = Self::cache_dir()?;
        let contents = std::fs::read_to_string(Path::new(&format!("{cache_dir}chisel-{id}.json")))?;
        let chisel_env: Self = serde_json::from_str(&contents)?;
        Ok(chisel_env)
    }

    /// Gets the most recent chisel session from the cache dir
    ///
    /// ### Returns
    ///
    /// Optionally, the file name of the most recently modified cached session.
    pub fn latest_cached_session() -> Result<String> {
        let cache_dir = Self::cache_dir()?;
        let mut entries = std::fs::read_dir(cache_dir)?;
        let mut latest = entries.next().ok_or_else(|| eyre::eyre!("No entries found!"))??;
        for entry in entries {
            let entry = entry?;
            if entry.metadata()?.modified()? > latest.metadata()?.modified()? {
                latest = entry;
            }
        }
        Ok(latest
            .path()
            .to_str()
            .ok_or_else(|| eyre::eyre!("Failed to get session path!"))?
            .to_string())
    }

    /// Loads the latest ChiselSession from the cache file
    ///
    /// ### Returns
    ///
    /// Optionally, an owned instance of the most recently modified cached session.
    pub fn latest() -> Result<Self> {
        let last_session = Self::latest_cached_session()?;
        let last_session_contents = std::fs::read_to_string(Path::new(&last_session))?;
        let chisel_env: Self = serde_json::from_str(&last_session_contents)?;
        Ok(chisel_env)
    }
}

/// Generic helper function that attempts to convert a type that has
/// an [`Into<OffsetDateTime>`] implementation into a formatted date string.
fn systemtime_strftime<T>(dt: T, format: &str) -> Result<String>
where
    T: Into<OffsetDateTime>,
{
    Ok(dt.into().format(&format_description::parse(format)?)?)
}