tartarus-api 0.1.1

Structured API for sandboxing system (currently utilizing `bubblewrap`)
Documentation
use crate::{
    config::FileContents,
    error::{Error, ErrorKind, with_io_context},
};
use jsonc_parser::{
    ParseOptions,
    cst::{CstInputValue, CstObject, CstRootNode},
    errors::ParseError,
};
use std::io::{Read, Write};

pub(super) struct Root {
    config_name: &'static str,
    value: CstRootNode,
    contents: FileContents,
    schema_error_fn: fn(String) -> ErrorKind,
}

impl Root {
    pub(super) fn read_from(
        config_name: &'static str,
        mut contents: FileContents,
        syntax_error_fn: fn(ParseError) -> ErrorKind,
        schema_error_fn: fn(String) -> ErrorKind,
    ) -> Result<Self, Error> {
        let mut config_contents = String::new();
        contents
            .original
            .read_to_string(&mut config_contents)
            .map_err(|e| with_io_context(config_name, "reading config", e))?;

        let root =
            CstRootNode::parse(&config_contents, &ParseOptions::default()).map_err(|e| Error {
                context: format!("parsing {config_name} config"),
                kind: syntax_error_fn(e),
            })?;

        Ok(Self {
            config_name,
            value: root,
            contents,
            schema_error_fn,
        })
    }

    fn schema_error(&self, reason: String) -> Error {
        Error {
            kind: (self.schema_error_fn)(reason),
            context: format!("parsing {} config", self.config_name),
        }
    }

    pub(super) fn get_object(&self) -> Result<Object, Error> {
        let object = self.value.object_value().ok_or_else(|| {
            self.schema_error(format!("{} config must be an object", self.config_name))
        })?;

        Ok(Object {
            config_name: self.config_name,
            value: object,
            schema_error_fn: self.schema_error_fn,
        })
    }

    pub(super) fn write(&mut self) -> Result<(), Error> {
        self.contents
            .output
            .write_all(self.value.to_string().as_bytes())
            .map_err(|e| with_io_context(self.config_name, "writing config", e))
    }
}

#[derive(Debug)]
pub struct Object {
    config_name: &'static str,
    value: CstObject,
    schema_error_fn: fn(String) -> ErrorKind,
}

impl Object {
    fn schema_error(&self, reason: String) -> Error {
        Error {
            kind: (self.schema_error_fn)(reason),
            context: format!("parsing {} config", self.config_name),
        }
    }

    pub(super) fn get_object_value(&self, name: &str) -> Result<Self, Error> {
        let object = self
            .value
            .object_value_or_create(name)
            .ok_or_else(|| self.schema_error(format!("{name} field must be an object")))?;

        Ok(Self {
            config_name: self.config_name,
            value: object,
            schema_error_fn: self.schema_error_fn,
        })
    }

    pub(super) fn insert_value(&self, name: &str, value: impl Into<CstInputValue>) {
        if let Some(existing) = self.value.get(name) {
            existing.set_value(value.into());
        } else {
            self.value.append(name, value.into());
        }
    }
}