fig 1.0.0

Parse, edit, and convert config files while preserving comments. Supports JSON, YAML, TOML, and more.
//! Comment-preserving editing of a config embedded in a host file.
//!
//! [`Embed`] opens the config region selected by an [`EmbedType`] — markdown
//! YAML frontmatter, JSON frontmatter, or YAML endmatter — and edits only that
//! block in its inner format. The fences and surrounding host text are left
//! byte-identical, and within the embed only the changed node's bytes move
//! (comments, key order, and formatting are preserved). This generalizes the
//! former YAML-frontmatter-only `Frontmatter`.
//!
//! Value-taking methods mirror [`crate::Editor`]: `*_value` take a [`Value`] and
//! are always available; the `serde`-gated forms accept any `Serialize`.

use std::ptr::NonNull;

use crate::editor::{Segment, borrow_str, to_ffi_keys, to_ffi_path};
use crate::error::Error;
use crate::value::{Value, value_text};
use crate::{Format, ffi};

/// Which embedded config to open. Each fixes both the host delimiters and the
/// inner format, mirroring fig's `Embed.Type`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EmbedType {
    /// `---` … `---` YAML frontmatter at the top of a markdown file.
    FrontmatterYaml,
    /// `;;;` … `;;;` JSON frontmatter at the top of a markdown file.
    FrontmatterJson,
    /// YAML in a trailing ```` ```endmatter ```` code block.
    EndmatterYaml,
}

impl EmbedType {
    fn ffi(self) -> ffi::FigEmbedType {
        match self {
            EmbedType::FrontmatterYaml => ffi::FigEmbedType::FrontmatterYaml,
            EmbedType::FrontmatterJson => ffi::FigEmbedType::FrontmatterJson,
            EmbedType::EndmatterYaml => ffi::FigEmbedType::EndmatterYaml,
        }
    }

    /// The inner format values are serialized to when spliced in.
    fn inner_format(self) -> Format {
        match self {
            EmbedType::FrontmatterYaml | EmbedType::EndmatterYaml => Format::Yaml,
            EmbedType::FrontmatterJson => Format::Json,
        }
    }
}

/// An editor over an embedded config region of a host file.
#[derive(Debug)]
pub struct Embed {
    raw: NonNull<ffi::FigEmbed>,
    inner: Format,
}

impl Embed {
    /// Open the embed of `kind` in `host`. Returns [`Error::NotFound`] if no such
    /// region exists.
    pub fn open(host: &[u8], kind: EmbedType) -> Result<Self, Error> {
        let mut raw = std::ptr::null_mut();
        let status =
            unsafe { ffi::fig_embed_open(host.as_ptr(), host.len(), kind.ffi() as i32, &mut raw) };
        Error::from_status(status)?;
        let raw = NonNull::new(raw).ok_or(Error::Internal)?;
        Ok(Self {
            raw,
            inner: kind.inner_format(),
        })
    }

    /// Open the markdown YAML frontmatter of `markdown` — the common case.
    pub fn frontmatter(markdown: &[u8]) -> Result<Self, Error> {
        Self::open(markdown, EmbedType::FrontmatterYaml)
    }

    fn ptr(&self) -> *mut ffi::FigEmbed {
        self.raw.as_ptr()
    }

    // ── value edits (over `Value`) ──────────────────────────────────────────

    /// Replace the value at `path` with `value`.
    pub fn replace_value(&mut self, path: &[Segment], value: &Value) -> Result<(), Error> {
        let repl = value_text(value, self.inner)?;
        let p = to_ffi_path(path);
        let status = unsafe {
            ffi::fig_embed_replace_val(self.ptr(), p.as_ptr(), p.len(), repl.as_ptr(), repl.len())
        };
        Error::from_status(status)
    }

    /// Replace the key at `path` with `key`.
    pub fn replace_key(&mut self, path: &[Segment], key: &str) -> Result<(), Error> {
        let repl = value_text(&Value::Str(key.to_string()), self.inner)?;
        let p = to_ffi_path(path);
        let status = unsafe {
            ffi::fig_embed_replace_key(self.ptr(), p.as_ptr(), p.len(), repl.as_ptr(), repl.len())
        };
        Error::from_status(status)
    }

    /// Insert `key: value` into the mapping at `path` (empty path = root).
    pub fn insert_value(
        &mut self,
        path: &[Segment],
        key: &str,
        value: &Value,
    ) -> Result<(), Error> {
        let key_text = value_text(&Value::Str(key.to_string()), self.inner)?;
        let val = value_text(value, self.inner)?;
        let p = to_ffi_path(path);
        let status = unsafe {
            ffi::fig_embed_insert_key(
                self.ptr(),
                p.as_ptr(),
                p.len(),
                key_text.as_ptr(),
                key_text.len(),
                val.as_ptr(),
                val.len(),
            )
        };
        Error::from_status(status)
    }

    /// Append `value` to the sequence at `path`.
    pub fn append_value(&mut self, path: &[Segment], value: &Value) -> Result<(), Error> {
        let val = value_text(value, self.inner)?;
        let p = to_ffi_path(path);
        let status = unsafe {
            ffi::fig_embed_append_seq(self.ptr(), p.as_ptr(), p.len(), val.as_ptr(), val.len())
        };
        Error::from_status(status)
    }

    /// Prepend `value` to the sequence at `path`.
    pub fn prepend_value(&mut self, path: &[Segment], value: &Value) -> Result<(), Error> {
        let val = value_text(value, self.inner)?;
        let p = to_ffi_path(path);
        let status = unsafe {
            ffi::fig_embed_prepend_seq(self.ptr(), p.as_ptr(), p.len(), val.as_ptr(), val.len())
        };
        Error::from_status(status)
    }

    // ── value edits (serde convenience) ─────────────────────────────────────

    /// Replace the value at `path` with the serialized form of `value`.
    #[cfg(feature = "serde")]
    pub fn replace<T: serde::Serialize + ?Sized>(
        &mut self,
        path: &[Segment],
        value: &T,
    ) -> Result<(), Error> {
        self.replace_value(path, &crate::ser::to_value(value)?)
    }

    /// Insert `key: value` into the mapping at `path` (empty path = root).
    #[cfg(feature = "serde")]
    pub fn insert<T: serde::Serialize + ?Sized>(
        &mut self,
        path: &[Segment],
        key: &str,
        value: &T,
    ) -> Result<(), Error> {
        self.insert_value(path, key, &crate::ser::to_value(value)?)
    }

    /// Append the serialized form of `value` to the sequence at `path`.
    #[cfg(feature = "serde")]
    pub fn append<T: serde::Serialize + ?Sized>(
        &mut self,
        path: &[Segment],
        value: &T,
    ) -> Result<(), Error> {
        self.append_value(path, &crate::ser::to_value(value)?)
    }

    /// Prepend the serialized form of `value` to the sequence at `path`.
    #[cfg(feature = "serde")]
    pub fn prepend<T: serde::Serialize + ?Sized>(
        &mut self,
        path: &[Segment],
        value: &T,
    ) -> Result<(), Error> {
        self.prepend_value(path, &crate::ser::to_value(value)?)
    }

    // ── comment editing ─────────────────────────────────────────────────────

    /// Add an own-line comment ABOVE the node at `path`. Mirrors
    /// [`crate::Editor::add_leading_comment`] (YAML frontmatter uses `#`; JSON
    /// frontmatter is strict JSON and returns [`Error::UnsupportedFormat`]).
    pub fn add_leading_comment(&mut self, path: &[Segment], text: &str) -> Result<(), Error> {
        let p = to_ffi_path(path);
        let status = unsafe {
            ffi::fig_embed_add_leading_comment(self.ptr(), p.as_ptr(), p.len(), text.as_ptr(), text.len())
        };
        Error::from_status(status)
    }

    /// Set the same-line trailing comment on the value at `path`. Mirrors
    /// [`crate::Editor::set_trailing_comment`].
    pub fn set_trailing_comment(&mut self, path: &[Segment], text: &str) -> Result<(), Error> {
        let p = to_ffi_path(path);
        let status = unsafe {
            ffi::fig_embed_set_trailing_comment(self.ptr(), p.as_ptr(), p.len(), text.as_ptr(), text.len())
        };
        Error::from_status(status)
    }

    /// Remove the own-line comment block above the node at `path` (no-op if none).
    pub fn delete_leading_comments(&mut self, path: &[Segment]) -> Result<(), Error> {
        let p = to_ffi_path(path);
        let status = unsafe { ffi::fig_embed_delete_leading_comments(self.ptr(), p.as_ptr(), p.len()) };
        Error::from_status(status)
    }

    /// Remove the same-line trailing comment on the value at `path` (no-op if none).
    pub fn delete_trailing_comment(&mut self, path: &[Segment]) -> Result<(), Error> {
        let p = to_ffi_path(path);
        let status = unsafe { ffi::fig_embed_delete_trailing_comment(self.ptr(), p.as_ptr(), p.len()) };
        Error::from_status(status)
    }

    // ── structural edits (no value) ─────────────────────────────────────────

    /// Delete the mapping entry named by `path`.
    pub fn delete(&mut self, path: &[Segment]) -> Result<(), Error> {
        let p = to_ffi_path(path);
        let status = unsafe { ffi::fig_embed_delete_key(self.ptr(), p.as_ptr(), p.len()) };
        Error::from_status(status)
    }

    /// Remove the item at `index` from the sequence at `path`.
    pub fn remove_item(&mut self, path: &[Segment], index: usize) -> Result<(), Error> {
        let p = to_ffi_path(path);
        let status =
            unsafe { ffi::fig_embed_remove_seq_item(self.ptr(), p.as_ptr(), p.len(), index) };
        Error::from_status(status)
    }

    /// Move the mapping entry at `src_path` to immediately before the entry at
    /// `dest_path`. Both must name keys in the same mapping. The moved entry
    /// keeps its owned comments; bytes between the two entries are preserved.
    pub fn move_key(&mut self, src_path: &[Segment], dest_path: &[Segment]) -> Result<(), Error> {
        let s = to_ffi_path(src_path);
        let d = to_ffi_path(dest_path);
        let status = unsafe {
            ffi::fig_embed_move_key(self.ptr(), s.as_ptr(), s.len(), d.as_ptr(), d.len())
        };
        Error::from_status(status)
    }

    /// Reorder the entries of the mapping at `path` (empty path = root) so
    /// `keys` come first in that order; entries whose key is not listed keep
    /// their original relative order and follow. Unknown keys are ignored. Each
    /// entry's comments and interleaved trivia are preserved.
    pub fn reorder_keys<S: AsRef<str>>(
        &mut self,
        path: &[Segment],
        keys: &[S],
    ) -> Result<(), Error> {
        let p = to_ffi_path(path);
        let k = to_ffi_keys(keys);
        let status = unsafe {
            ffi::fig_embed_reorder_keys(self.ptr(), p.as_ptr(), p.len(), k.as_ptr(), k.len())
        };
        Error::from_status(status)
    }

    /// Move the sequence item at index `from` to index `to` (array-move
    /// semantics). A block item keeps its owned comments; a flow sequence keeps
    /// its separators. No-op when `from == to`.
    pub fn move_item(&mut self, path: &[Segment], from: usize, to: usize) -> Result<(), Error> {
        let p = to_ffi_path(path);
        let status = unsafe { ffi::fig_embed_move_item(self.ptr(), p.as_ptr(), p.len(), from, to) };
        Error::from_status(status)
    }

    /// Reorder the items of the sequence at `path` so the items at `indices`
    /// (positions in the current order) come first, in that order; items not
    /// listed keep their original relative order and follow. Out-of-range
    /// indices are ignored.
    pub fn reorder_items(&mut self, path: &[Segment], indices: &[usize]) -> Result<(), Error> {
        let p = to_ffi_path(path);
        let status = unsafe {
            ffi::fig_embed_reorder_items(
                self.ptr(),
                p.as_ptr(),
                p.len(),
                indices.as_ptr(),
                indices.len(),
            )
        };
        Error::from_status(status)
    }

    /// Render the full host file with the edited embed spliced back between the
    /// (untouched) fences. Borrows handle memory; invalidated by the next call
    /// or edit. Takes `&mut self` because the render buffer is rebuilt in place.
    pub fn render(&mut self) -> Result<&str, Error> {
        let mut ptr: *const u8 = std::ptr::null();
        let mut len: usize = 0;
        let status = unsafe { ffi::fig_embed_render(self.raw.as_ptr(), &mut ptr, &mut len) };
        Error::from_status(status)?;
        borrow_str(ptr, len)
    }
}

impl Drop for Embed {
    fn drop(&mut self) {
        unsafe { ffi::fig_embed_destroy(self.raw.as_ptr()) };
    }
}