shelly-shell 0.5.4

A Rust based Unix style shell with a typed and structured language syntax.

use std::{
    collections::HashSet,
    time::{Duration, Instant},
};

use crate::language::{
    data::{
        formats::json::{document::Codec, parser::JsonError},
        native_value::NativeValue,
        types::{TypeId, TypeKind},
        value::Value,
    },
    interpreter::{Interpreter, exceptions::CleanupGuard},
    text::location::Location,
};

use super::{Operation, Result, enum_name, field, text};

impl Interpreter
{
    pub(super) fn json_make_codec(
        &self,
        command: &str,
        args: &[Value],
        operation: &Operation,
    ) -> Result<Value>
    {
        let Some(Value::Type(target)) = args.first()
        else
        {
            return Err(JsonError::new("ValidationError", "Codec requires a target Type").into());
        };
        let mut codec = Codec {
            target: target.id,
            enum_names: Vec::new(),
            field_names: Vec::new(),
            omit_null: Vec::new(),
            ignore_unknown: None,
        };
        let empty = Value::None;
        let options = args.get(1).unwrap_or(&empty);
        if command == "enum_codec"
        {
            let TypeKind::Enum(variants) = &target.kind
            else
            {
                return Err(
                    JsonError::new("ValidationError", "enum_codec requires unit enum").into(),
                );
            };
            let Value::HashMap(names) = options.underlying()
            else
            {
                return Err(JsonError::new("ValidationError", "enum_codec requires a hash").into());
            };
            for (key, value) in names.iter()
            {
                let Value::Enum(item) = key.to_value()
                else
                {
                    return Err(JsonError::new(
                        "ValidationError",
                        "Codec keys must be enum variants",
                    )
                    .into());
                };
                if item.definition.id != target.id
                {
                    return Err(JsonError::new(
                        "ValidationError",
                        "Codec variant belongs to another type",
                    )
                    .into());
                }
                codec.enum_names.push((item.variant, text(value)?.into()));
            }
            let mut names = HashSet::new();
            for (index, variant) in variants.iter().enumerate()
            {
                let name = codec
                    .enum_names
                    .iter()
                    .find(|(id, _)| *id == index)
                    .map(|(_, name)| name)
                    .unwrap_or(&variant.name);
                if !names.insert(name)
                {
                    return Err(
                        JsonError::new("ValidationError", "Duplicate enum wire name").into(),
                    );
                }
            }
        }
        else
        {
            let TypeKind::Struct(fields) = &target.kind
            else
            {
                return Err(
                    JsonError::new("ValidationError", "struct_codec requires a struct").into(),
                );
            };
            if let Some(rename) = field(options, "rename_fields")
            {
                let Value::HashMap(rename) = rename.underlying()
                else
                {
                    return Err(
                        JsonError::new("ValidationError", "rename_fields must be a hash").into(),
                    );
                };
                for (key, value) in rename.iter()
                {
                    let key = key.to_value();
                    let key = text(&key)?;
                    if !fields.iter().any(|field| field.name == key && field.public)
                    {
                        return Err(JsonError::new(
                            "ValidationError",
                            "Codec references inaccessible field",
                        )
                        .into());
                    }
                    codec.field_names.push((key.into(), text(value)?.into()));
                }
            }
            if let Some(omit) = field(options, "omit_null_fields")
            {
                let Value::Array(omit) = omit.underlying()
                else
                {
                    return Err(JsonError::new(
                        "ValidationError",
                        "omit_null_fields must be an array",
                    )
                    .into());
                };
                for value in omit.iter()
                {
                    let name = text(value)?;
                    let field = fields
                        .iter()
                        .find(|field| field.name == name && field.public)
                        .ok_or_else(|| {
                            JsonError::new("ValidationError", "Codec references inaccessible field")
                        })?;
                    if operation
                        .types
                        .validate(field.type_id, &Value::None)
                        .is_err()
                    {
                        return Err(JsonError::new(
                            "ValidationError",
                            "Omitted-null field must allow null",
                        )
                        .into());
                    }
                    codec.omit_null.push(name.into());
                }
            }
            let mut names = HashSet::new();
            for field in fields.iter().filter(|field| field.public)
            {
                let name = codec
                    .field_names
                    .iter()
                    .find(|(original, _)| *original == field.name)
                    .map(|(_, wire)| wire)
                    .unwrap_or(&field.name);
                if !names.insert(name)
                {
                    return Err(
                        JsonError::new("ValidationError", "Duplicate struct wire name").into(),
                    );
                }
            }
            codec.ignore_unknown = field(options, "unknown_fields")
                .and_then(enum_name)
                .map(|name| name == "Ignore");
        }
        Ok(Value::Native(NativeValue::new(codec)))
    }

    pub(super) fn json_codec(
        &mut self,
        location: &Location,
        target: TypeId,
        operation: &mut Operation,
    ) -> Result<Option<Codec>>
    {
        if let Some(codec) = operation.codecs.get(&target)
        {
            return Ok(codec.clone());
        }
        let Some(provider) = self.json_method(operation, target, "json_codec")
        else
        {
            operation.codecs.insert(target, None);
            return Ok(None);
        };
        if !self.exceptions.json_providers.insert(target)
        {
            return Err(JsonError::new("ValidationError", "Recursive json_codec provider").into());
        }
        let parent = self.exceptions.cleanup.take();
        let budget = parent
            .as_ref()
            .map_or(100_000, |guard| guard.instructions.min(100_000));
        let deadline = Instant::now() + Duration::from_millis(100);
        self.exceptions.cleanup = Some(CleanupGuard {
            hook: true,
            deadline: parent
                .as_ref()
                .map_or(deadline, |guard| guard.deadline.min(deadline)),
            instructions: budget,
            interrupted: parent.as_ref().is_some_and(|guard| guard.interrupted),
            function_depth: self.exceptions.frames.len(),
        });
        let reporting = self.exceptions.reporting;
        let floor = self.exceptions.report_floor;
        self.exceptions.reporting = true;
        self.exceptions.report_floor = self.exceptions.regions.len();
        let outcome = self.json_invoke(location, &provider, &[], operation);
        let completed = self.exceptions.cleanup.take();
        let interrupted = completed.as_ref().is_some_and(|guard| guard.interrupted);
        let consumed = budget.saturating_sub(completed.map_or(0, |guard| guard.instructions));
        self.exceptions.cleanup = parent.map(|mut guard| {
            guard.instructions = guard.instructions.saturating_sub(consumed);
            guard.interrupted |= interrupted;
            guard
        });
        self.exceptions.reporting = reporting;
        self.exceptions.report_floor = floor;
        self.exceptions.json_providers.remove(&target);
        let result = outcome?;
        self.consume_success(location, &result)?;
        let codec = if let Value::Native(native) = result.underlying()
        {
            native.downcast::<Codec>().cloned()
        }
        else
        {
            None
        };
        let codec = codec
            .ok_or_else(|| JsonError::new("ValidationError", "Provider must return json::Codec"))?;
        if codec.target != target
        {
            return Err(JsonError::new(
                "ValidationError",
                "Codec target differs from its provider type",
            )
            .into());
        }
        operation.codecs.insert(target, Some(codec.clone()));
        Ok(Some(codec))
    }
}