hen 0.22.0

Run protocol-aware API request collections from the command line or through MCP.
Documentation
use std::path::{Path, PathBuf};

use super::Rule;

pub(in crate::parser) fn reject_misplaced_dotenv_directive(
    raw: &str,
    span: pest::Span<'_>,
) -> Result<(), pest::error::Error<Rule>> {
    if looks_like_dotenv_directive(raw) {
        return Err(custom_error(
            "dotenv directives must appear before the first ---".to_string(),
            span,
        ));
    }

    Ok(())
}

pub(in crate::parser) fn resolve_dotenv_path(working_dir: &Path, raw_path: &str) -> PathBuf {
    let path = PathBuf::from(raw_path);
    let resolved = if path.is_absolute() {
        path
    } else {
        working_dir.join(path)
    };

    normalize_path_lexically(resolved)
}

fn looks_like_dotenv_directive(raw: &str) -> bool {
    let trimmed = strip_guard_prefix(raw.trim_start());
    let Some(path) = trimmed.strip_prefix("dotenv") else {
        return false;
    };

    let path = path.trim_start();
    !path.is_empty() && !path.contains("{{") && !path.contains("[[")
}

fn strip_guard_prefix(raw: &str) -> &str {
    if !raw.starts_with('[') {
        return raw;
    }

    let mut in_single = false;
    let mut in_double = false;

    for (index, ch) in raw.char_indices().skip(1) {
        match ch {
            '\'' if !in_double => in_single = !in_single,
            '"' if !in_single => in_double = !in_double,
            ']' if !in_single && !in_double => return raw[index + 1..].trim_start(),
            _ => {}
        }
    }

    raw
}

fn custom_error(message: String, span: pest::Span<'_>) -> pest::error::Error<Rule> {
    pest::error::Error::new_from_span(
        pest::error::ErrorVariant::CustomError { message },
        span,
    )
}

fn normalize_path_lexically(path: PathBuf) -> PathBuf {
    let mut normalized = PathBuf::new();
    let is_absolute = path.is_absolute();

    for component in path.components() {
        match component {
            std::path::Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
            std::path::Component::RootDir => normalized.push(component.as_os_str()),
            std::path::Component::CurDir => {}
            std::path::Component::ParentDir => {
                if !normalized.pop() && !is_absolute {
                    normalized.push(component.as_os_str());
                }
            }
            std::path::Component::Normal(part) => normalized.push(part),
        }
    }

    if normalized.as_os_str().is_empty() {
        PathBuf::from(".")
    } else {
        normalized
    }
}