sysfuss 0.3.0

sysfs wrapper for convenience
Documentation
use std::path::{Path, PathBuf};
use std::convert::AsRef;
use std::io::Result as IoResult;

const SYS_CLASS_PATH: &str = "sys/class";

/// Generic entity path with basic functionality
#[cfg_attr(feature = "derive", derive(Debug, Clone))]
pub struct BasicEntityPath {
    path: PathBuf
}

impl BasicEntityPath {
    /// Create a new BasicEntityPath for manipulating path
    pub fn new(path: impl AsRef<Path>) -> Self {
        Self {
            path: path.as_ref().to_path_buf(),
        }
    }
    
    pub(crate) fn all(root: impl AsRef<Path>, class: impl AsRef<Path>) -> IoResult<impl Iterator<Item=IoResult<Self>>> {
        let dir_path = root.as_ref().join(SYS_CLASS_PATH).join(class);
        Ok(dir_path.read_dir()?
                .filter_map(
                    |entry| entry.map(
                        |entry| if entry.path().is_file() { None } else { Some(Self::new(entry.path())) }
                    ).transpose()))
    }
    
    /// Filter out attributes that do not exist on this entity
    pub fn filter_capabilities<A: crate::SysAttribute>(&self, attributes: impl Iterator<Item=A>) -> impl Iterator<Item=A> {
        let found_attrs: Vec<PathBuf> = if let Ok(dir_iter) = self.path.read_dir() {
            dir_iter.filter_map(
                |entry| entry.ok().filter(|entry| entry.path().is_file()).map(|entry| PathBuf::from(entry.file_name()))
            ).collect()
        } else {
            Vec::with_capacity(0)
        };
        
        attributes.filter(move |attr| found_attrs.contains(&attr.filename()))
    }
}

impl AsRef<Path> for BasicEntityPath {
    fn as_ref(&self) -> &Path {
        self.path.as_path()
    }
}

impl crate::SysEntity for BasicEntityPath {
    fn to_entity_path(self) -> crate::EntityPath {
        crate::EntityPath::Generic(self)
    }
    
    fn name(&self) -> IoResult<String> {
        if let Some(s) = self.path.file_name().map(|name| name.to_str().map(|s| s.to_owned())).flatten() {
            Ok(s)
        } else {
            Err(std::io::Error::from_raw_os_error(std::io::ErrorKind::InvalidInput as _))
        }
    }
}

impl crate::SysEntityAttributes<String> for BasicEntityPath {
    fn capabilities(&self) -> Vec<String> {
        if let Ok(dir_iter) = self.path.read_dir() {
            dir_iter.filter_map(
                |entry| entry.ok().filter(|entry| entry.path().is_file()).and_then(|entry| entry.file_name().to_str().map(|s| s.to_owned()))
            ).collect()
        } else {
            Vec::with_capacity(0)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::SysEntity;
    use crate::SysEntityRawExt;

    #[test]
    fn basic_entity_all() -> std::io::Result<()> {
        let sys = crate::SysPath::default();
        let all_basic: Vec<_> = BasicEntityPath::all(sys, "drm")?.collect();
        assert!(!all_basic.is_empty());
        for ent in all_basic.into_iter() {
            assert!(ent?.attribute_str("uevent")? != "");
        }
        Ok(())
    }

    #[test]
    fn basic_capabilities() -> std::io::Result<()> {
        let sys = crate::SysPath::default();
        if std::fs::read_to_string("/sys/class/hwmon/hwmon0/name")?.trim() != "nvme" {
            // skip if system has different hwmons
            // TODO account for different test systems
            eprintln!("basic entity test skipped since hwmon0 is not nvme (maybe running on a different PC?)");
            return Ok(())
        }
        let basic = sys.class("hwmon", crate::capability::attributes([
            "name".to_string(),
            "temp1_alarm".to_string(),
            "temp1_crit".to_string(),
        ].into_iter()))?.next().expect("Missing any hwmon");
        let attr_value = crate::SysEntityAttributesExt::attribute::<String, _>(&basic, "name".to_owned()).expect("name capable but also incapable");
        println!("Attribute ./name = '{}'", attr_value);
        assert!(attr_value == "nvme");
        Ok(())
    }

    #[test]
    fn basic_drm_capabilities() -> std::io::Result<()> {
        let sys = crate::SysPath::default();
        let expected_dev = if let Ok(dev) = std::fs::read_to_string("/sys/class/drm/card1/dev") {
            dev.trim().to_owned()
        } else {
            // skip if system has different hwmons
            // TODO account for different test systems
            eprintln!("basic entity test skipped since drm card0 does not exist (maybe running on a different PC?)");
            return Ok(())
        };
        let basic = sys.class("drm", crate::capability::attributes([
            "dev".to_string(),
            "uevent".to_string(),
        ].into_iter()))?.filter(|basic| basic.name().expect("no name").starts_with("card")).next().expect("Missing drm");
        let attr_value = crate::SysEntityAttributesExt::attribute::<String, _>(&basic, "dev".to_owned()).expect("dev capable but also incapable");
        println!("Attribute ./dev = '{}'", attr_value);
        assert!(attr_value == expected_dev);
        Ok(())
    }
}