use std::{fmt, ops::Deref};
use crate::{
args::{ToArgs, ToMontyObject},
exceptions::{ExcType, MontyException},
file_mode::FileMode,
format::StringRepr,
object::{MontyObject, MontyTimeZone},
};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, strum::IntoStaticStr)]
pub enum OsFunctionCall {
#[strum(serialize = "Path.exists")]
Exists(MontyPath),
#[strum(serialize = "Path.is_file")]
IsFile(MontyPath),
#[strum(serialize = "Path.is_dir")]
IsDir(MontyPath),
#[strum(serialize = "Path.is_symlink")]
IsSymlink(MontyPath),
#[strum(serialize = "Path.read_text")]
ReadText(MontyPath),
#[strum(serialize = "Path.read_bytes")]
ReadBytes(MontyPath),
#[strum(serialize = "Path.stat")]
Stat(MontyPath),
#[strum(serialize = "Path.iterdir")]
Iterdir(MontyPath),
#[strum(serialize = "Path.resolve")]
Resolve(MontyPath),
#[strum(serialize = "Path.absolute")]
Absolute(MontyPath),
#[strum(serialize = "Path.write_text")]
WriteText(PathStringDataArgs),
#[strum(serialize = "Path.append_text")]
AppendText(PathStringDataArgs),
#[strum(serialize = "Path.write_bytes")]
WriteBytes(PathBytesDataArgs),
#[strum(serialize = "Path.append_bytes")]
AppendBytes(PathBytesDataArgs),
#[strum(serialize = "open")]
Open(OpenCallArgs),
#[strum(serialize = "Path.mkdir")]
Mkdir(MkdirCallArgs),
#[strum(serialize = "Path.unlink")]
Unlink(MontyPath),
#[strum(serialize = "Path.rmdir")]
Rmdir(MontyPath),
#[strum(serialize = "Path.rename")]
Rename(RenameCallArgs),
#[strum(serialize = "os.getenv")]
Getenv(GetenvArgs),
#[strum(serialize = "os.environ")]
GetEnviron,
#[strum(serialize = "date.today")]
DateToday,
#[strum(serialize = "datetime.now")]
DateTimeNow(Option<MontyTimeZone>),
}
impl OsFunctionCall {
#[must_use]
pub fn name(&self) -> &'static str {
self.into()
}
#[must_use]
pub fn to_args(self) -> (Vec<MontyObject>, Vec<(MontyObject, MontyObject)>) {
match self {
Self::Exists(p)
| Self::IsFile(p)
| Self::IsDir(p)
| Self::IsSymlink(p)
| Self::ReadText(p)
| Self::ReadBytes(p)
| Self::Stat(p)
| Self::Iterdir(p)
| Self::Resolve(p)
| Self::Absolute(p)
| Self::Unlink(p)
| Self::Rmdir(p) => (vec![p.into_monty_object()], vec![]),
Self::WriteText(a) | Self::AppendText(a) => a.to_args(),
Self::WriteBytes(a) | Self::AppendBytes(a) => a.to_args(),
Self::Open(a) => a.to_args(),
Self::Mkdir(a) => a.to_args(),
Self::Rename(a) => a.to_args(),
Self::Getenv(a) => a.to_args(),
Self::GetEnviron | Self::DateToday => (vec![], vec![]),
Self::DateTimeNow(tz) => (vec![tz.map_or(MontyObject::None, MontyObject::TimeZone)], vec![]),
}
}
#[must_use]
pub fn is_filesystem(&self) -> bool {
matches!(
self,
Self::Exists(_)
| Self::IsFile(_)
| Self::IsDir(_)
| Self::IsSymlink(_)
| Self::ReadText(_)
| Self::ReadBytes(_)
| Self::WriteText(_)
| Self::WriteBytes(_)
| Self::AppendText(_)
| Self::AppendBytes(_)
| Self::Stat(_)
| Self::Iterdir(_)
| Self::Resolve(_)
| Self::Absolute(_)
| Self::Open(_)
| Self::Mkdir(_)
| Self::Unlink(_)
| Self::Rmdir(_)
| Self::Rename(_)
)
}
#[must_use]
pub fn is_write(&self) -> bool {
match self {
Self::WriteText(_)
| Self::WriteBytes(_)
| Self::AppendText(_)
| Self::AppendBytes(_)
| Self::Mkdir(_)
| Self::Unlink(_)
| Self::Rmdir(_)
| Self::Rename(_) => true,
Self::Open(args) => args.mode.create(),
_ => false,
}
}
#[must_use]
pub fn is_existence_check(&self) -> bool {
matches!(
self,
Self::Exists(_) | Self::IsFile(_) | Self::IsDir(_) | Self::IsSymlink(_)
)
}
#[must_use]
pub fn primary_path(&self) -> Option<&str> {
match self {
Self::Exists(p)
| Self::IsFile(p)
| Self::IsDir(p)
| Self::IsSymlink(p)
| Self::ReadText(p)
| Self::ReadBytes(p)
| Self::Stat(p)
| Self::Iterdir(p)
| Self::Resolve(p)
| Self::Absolute(p)
| Self::Unlink(p)
| Self::Rmdir(p) => Some(p.as_str()),
Self::WriteText(a) | Self::AppendText(a) => Some(a.path.as_str()),
Self::WriteBytes(a) | Self::AppendBytes(a) => Some(a.path.as_str()),
Self::Open(a) => Some(a.path.as_str()),
Self::Mkdir(a) => Some(a.path.as_str()),
Self::Rename(a) => Some(a.src.as_str()),
Self::Getenv(_) | Self::GetEnviron | Self::DateToday | Self::DateTimeNow(_) => None,
}
}
#[must_use]
pub fn rename_destination(&self) -> Option<&str> {
match self {
Self::Rename(a) => Some(a.dst.as_str()),
_ => None,
}
}
#[must_use]
pub fn on_no_handler(&self) -> MontyException {
if self.is_filesystem() {
let path = self.primary_path().unwrap_or("<unknown>");
MontyException::new(
ExcType::PermissionError,
Some(format!("Permission denied: {}", StringRepr(path))),
)
} else {
MontyException::new(
ExcType::RuntimeError,
Some(format!("'{}' is not supported in this environment", self.name())),
)
}
}
}
impl fmt::Display for OsFunctionCall {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct PathStringDataArgs {
pub path: MontyPath,
pub data: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct PathBytesDataArgs {
pub path: MontyPath,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct OpenCallArgs {
pub path: MontyPath,
pub mode: FileMode,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct MkdirCallArgs {
pub path: MontyPath,
#[from_args(kw_only)]
pub parents: bool,
#[from_args(kw_only)]
pub exist_ok: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct RenameCallArgs {
pub src: MontyPath,
pub dst: MontyPath,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct GetenvArgs {
pub key: String,
pub default: MontyObject,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MontyPath(String);
impl MontyPath {
#[must_use]
pub fn new(path: String) -> Self {
Self(path)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl Deref for MontyPath {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl From<String> for MontyPath {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for MontyPath {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl ToMontyObject for MontyPath {
fn into_monty_object(self) -> MontyObject {
MontyObject::Path(self.0)
}
}
#[must_use]
pub fn file_stat(mode: i64, size: i64, mtime: f64) -> MontyObject {
let mode = if mode < 0o1000 { mode | 0o100_000 } else { mode };
stat_result(mode, 0, 0, 1, 0, 0, size, mtime, mtime, mtime)
}
#[must_use]
pub fn dir_stat(mode: i64, mtime: f64) -> MontyObject {
let mode = if mode < 0o1000 { mode | 0o040_000 } else { mode };
stat_result(mode, 0, 0, 2, 0, 0, 4096, mtime, mtime, mtime)
}
#[must_use]
pub fn symlink_stat(mode: i64, mtime: f64) -> MontyObject {
let mode = if mode < 0o1000 { mode | 0o120_000 } else { mode };
stat_result(mode, 0, 0, 1, 0, 0, 0, mtime, mtime, mtime)
}
#[must_use]
#[expect(clippy::too_many_arguments)]
pub fn stat_result(
st_mode: i64,
st_ino: i64,
st_dev: i64,
st_nlink: i64,
st_uid: i64,
st_gid: i64,
st_size: i64,
st_atime: f64,
st_mtime: f64,
st_ctime: f64,
) -> MontyObject {
MontyObject::NamedTuple {
type_name: STAT_RESULT_TYPE_NAME.to_owned(),
field_names: STAT_RESULT_FIELDS.iter().map(|s| (*s).to_owned()).collect(),
values: vec![
MontyObject::Int(st_mode),
MontyObject::Int(st_ino),
MontyObject::Int(st_dev),
MontyObject::Int(st_nlink),
MontyObject::Int(st_uid),
MontyObject::Int(st_gid),
MontyObject::Int(st_size),
MontyObject::Float(st_atime),
MontyObject::Float(st_mtime),
MontyObject::Float(st_ctime),
],
}
}
const STAT_RESULT_TYPE_NAME: &str = "StatResult";
const STAT_RESULT_FIELDS: &[&str] = &[
"st_mode", "st_ino", "st_dev", "st_nlink", "st_uid", "st_gid", "st_size", "st_atime", "st_mtime", "st_ctime",
];