Skip to main content

src/molt/internal/document/values.gleam

import gleam/bool
import gleam/list
import gleam/option.{Some}
import gleam/result
import greenwood.{type Element, Node, NodeElement as N}
import greenwood/zipper
import molt/cst
import molt/error.{type MoltError}
import molt/internal/cst/builder
import molt/internal/cst/elements
import molt/internal/cst/query
import molt/internal/document/arrays
import molt/internal/document/index.{Fresh, Hit, Miss, RootDottedKey}
import molt/internal/document/primitives
import molt/internal/document/remove
import molt/internal/document/structure
import molt/internal/path
import molt/internal/utils
import molt/ops
import molt/types.{type Document, type Path, type TomlKind, KeySegment}
import molt/value.{type Value}

pub fn set(
  doc doc: Document,
  path p: String,
  value val: Value,
) -> Result(Document, MoltError) {
  let type_of = value.type_of(val)

  use <- bool.guard(
    type_of == "invalid",
    return: Error(error.InvalidTomlValue(path: p, text: value.invalid_text(val))),
  )

  // `Set` writes value nodes only. A section-table or array of tables `Value`
  // encodes header intent, not a value: reject it rather than silently
  // creating a header (Place's job) or coercing it inline (the `as_*` coercions).
  use <- bool.guard(
    type_of == "table" || type_of == "array_of_tables",
    return: Error(error.TypeMismatch(
      path: Some(p),
      expected: "scalar, array, or inline-table value",
      got: type_of,
    )),
  )

  use segments <- result.try(path.parse(p))
  use idx <- index.with_index(doc)
  case index.resolve(idx, segments) {
    Hit(_, types.IndexScalarValue(..))
    | Hit(_, types.IndexArrayValue(..))
    | Hit(_, types.IndexInlineTableValue(..)) ->
      primitives.cursor_replace_value(
        doc:,
        idx:,
        path: segments,
        new_value: value.to_cst(val),
      )

    Hit(_, types.IndexImplicitTable(..)) ->
      Error(error.TypeMismatch(
        path: Some(p),
        expected: "scalar or array value",
        got: "implicit table",
      ))

    Hit(_, types.IndexTable(..)) ->
      Error(error.TypeMismatch(
        path: Some(p),
        expected: "scalar or array value",
        got: "table",
      ))

    Hit(_, types.IndexArrayOfTables(..)) ->
      Error(error.TypeMismatch(
        path: Some(p),
        expected: "scalar or array value (use an index to select an entry)",
        got: "array of tables",
      ))

    Hit(_, types.IndexArrayOfTablesEntry(..)) ->
      Error(error.TypeMismatch(
        path: Some(p),
        expected: "a key within the array entry",
        got: "array of tables entry",
      ))

    Miss(ancestor_path:, ancestor:, ..) -> {
      use site <- result.try(index.insertion_site(
        idx:,
        ancestor_path:,
        ancestor:,
        full_path: segments,
      ))
      primitives.write_at_site(doc:, idx:, site:, full_path: segments, val:)
    }

    Fresh(_) ->
      primitives.write_at_site(
        doc:,
        idx:,
        site: RootDottedKey(segments),
        full_path: segments,
        val:,
      )
  }
}

pub fn replace(
  doc doc: Document,
  path p: String,
  value val: Value,
) -> Result(Document, MoltError) {
  use <- bool.guard(
    value.type_of(val) == "invalid",
    return: Error(error.InvalidTomlValue(path: p, text: value.invalid_text(val))),
  )
  use segments <- result.try(path.parse(p))
  use idx <- index.with_index(doc)
  use doc2 <- result.try(case index.resolve(idx, segments) {
    Hit(..) -> remove.prune(doc:, idx:, path: segments)
    Miss(..) | Fresh(_) -> Ok(doc)
  })
  case value.type_of(val) {
    "table" -> {
      // `doc2` has been pruned, so `path` is absent: ensure the table, then
      // populate it.
      use entries <- result.try(value.table_to_list(val))
      use doc3 <- result.try(structure.ensure_exists(
        doc: doc2,
        path: p,
        kind: types.Table,
      ))
      structure.merge_values(
        doc: doc3,
        path: p,
        entries:,
        on_conflict: ops.OnConflictError,
      )
    }
    "array_of_tables" -> {
      use items <- result.try(value.array_to_list(val))
      replace_array_of_tables(doc: doc2, path: p, items:)
    }
    _ -> set(doc: doc2, path: p, value: val)
  }
}

/// Build `[[path]]` with one entry per table-like item. The first item fills the
/// entry created by `ensure_exists`; the rest are appended. An empty value adds
/// nothing (matching the old per-item fold over `[]`). `path` is assumed absent
/// (the `Place` caller prunes it first).
fn replace_array_of_tables(
  doc doc: Document,
  path p: String,
  items items: List(Value),
) -> Result(Document, MoltError) {
  case items {
    [] -> Ok(doc)
    [first, ..rest] -> {
      use first_entries <- result.try(value.table_to_list(first))
      use d0 <- result.try(structure.ensure_exists(
        doc:,
        path: p,
        kind: types.ArrayOfTables,
      ))
      use d1 <- result.try(structure.merge_values(
        doc: d0,
        path: p <> "[0]",
        entries: first_entries,
        on_conflict: ops.OnConflictError,
      ))
      list.try_fold(rest, d1, fn(d, item) {
        arrays.append(doc: d, path: p, value: item)
      })
    }
  }
}

pub fn insert_key(
  doc doc: Document,
  path p: String,
  before before: String,
  key key: String,
  value val: Value,
) -> Result(Document, MoltError) {
  use segments <- result.try(path.parse(p))
  use idx <- index.with_index(doc)
  case index.resolve(idx, segments) {
    Hit(_, types.IndexTable(..)) | Hit(_, types.IndexArrayOfTablesEntry(..)) -> {
      let kv =
        builder.build_kv_node(
          key: utils.quote_key(key),
          value: value.to_cst(val),
        )
      // The op layer is intentionally forgiving: a missing `before` anchor
      // falls back to appending rather than failing: a benign positioning
      // miss must not halt a `run` batch. The `cst.insert_kv` primitive stays
      // strict; we recover only the not-found-anchor case, so a genuine key
      // collision still surfaces as an error.
      use new_tree <- result.try(
        case
          cst.insert_kv(
            node: doc.tree,
            into: segments,
            kv:,
            at: cst.BeforeKey(before),
          )
        {
          Error(error.NotFound(..)) ->
            cst.insert_kv(node: doc.tree, into: segments, kv:, at: cst.KvAtEnd)
          other -> other
        },
      )
      let full_path = list.append(segments, [KeySegment(key)])
      let idx_key = index.path_to_index_key(full_path)
      let entry = index.entry_for_value(val, container: segments)
      let idx = index.insert_entry(idx:, key: idx_key, entry:)
      Ok(primitives.patch(doc:, tree: new_tree, idx:))
    }

    Hit(_, types.IndexImplicitTable(..)) ->
      insert_key_in_implicit(
        doc:,
        idx:,
        container: segments,
        before:,
        key:,
        val:,
      )

    Hit(_, entry) ->
      Error(error.TypeMismatch(
        path: Some(p),
        expected: "table or array of tables entry",
        got: utils.index_entry_to_string(entry),
      ))

    Miss(..) | Fresh(_) -> Error(error.not_found(p))
  }
}

pub fn update(
  doc doc: Document,
  path p: String,
  with transform: fn(Value) -> Result(Value, MoltError),
) -> Result(Document, MoltError) {
  use segments <- result.try(path.parse(p))
  use idx <- index.with_index(doc)
  case index.resolve(idx, segments) {
    Hit(_, types.IndexScalarValue(..))
    | Hit(_, types.IndexArrayValue(..))
    | Hit(_, types.IndexInlineTableValue(..)) ->
      cursor_transform_value(doc:, idx:, path: segments, with: transform)

    Hit(_, entry) ->
      Error(error.TypeMismatch(
        path: Some(p),
        expected: "scalar, array, or inline table",
        got: utils.index_entry_to_string(entry),
      ))

    Miss(ancestor_path: _, ancestor:, ..) ->
      case ancestor {
        types.IndexScalarValue(..)
        | types.IndexArrayValue(..)
        | types.IndexInlineTableValue(..) ->
          cursor_transform_value(doc:, idx:, path: segments, with: transform)
        _ -> Error(error.not_found(p))
      }

    Fresh(_) -> Error(error.not_found(p))
  }
}

// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------

fn cursor_transform_value(
  doc doc: Document,
  idx idx: types.DocumentIndex,
  path segments: Path,
  with transform: fn(Value) -> Result(Value, MoltError),
) -> Result(Document, MoltError) {
  use cursor <- result.try(
    query.get_cursor(node: doc.tree, path: segments)
    |> result.replace_error(error.not_found_path(segments)),
  )
  let current = value.from_cst(cursor.focus)
  use new_val <- result.try(transform(current))
  let path_str = path.to_string(segments)
  let new_val_type = value.type_of(new_val)
  use <- bool.guard(
    new_val_type == "table" || new_val_type == "array_of_tables",
    return: Error(error.TypeMismatch(
      path: Some(path_str),
      expected: "scalar or inline value",
      got: new_val_type,
    )),
  )
  use <- bool.guard(
    new_val_type == "invalid",
    return: Error(error.InvalidTomlValue(
      path: path_str,
      text: value.invalid_text(new_val),
    )),
  )
  let new_node = case cursor.focus.kind {
    types.ArrayElement ->
      primitives.rebuild_array_element(cursor.focus, value.to_cst(new_val))
    _ -> primitives.rebuild_kv_value(cursor.focus, value.to_cst(new_val))
  }
  zipper.set_focus(zipper: cursor, node: new_node)
  |> zipper.unzip
  |> primitives.patch(doc:, idx:)
  |> Ok
}

fn insert_key_in_implicit(
  doc doc: Document,
  idx idx: types.DocumentIndex,
  container container: Path,
  before before: String,
  key key: String,
  val val: Value,
) -> Result(Document, MoltError) {
  case path.split_at_last_index(container) {
    // No index: insert among the document's root key-values (existing path).
    Error(Nil) -> {
      let before_keys =
        path.path_to_table_header(list.append(container, [KeySegment(before)]))
      let new_key_path = list.append(container, [KeySegment(key)])
      let kv =
        builder.build_kv_from_path(
          key: key_segment_names(new_key_path),
          value: value.to_cst(val),
        )
      let new_children =
        insert_before_root_kv_with_path(
          children: doc.tree.children,
          target_key_path: before_keys,
          new_el: N(kv),
        )
      let new_tree = Node(..doc.tree, children: new_children)
      let idx_key = index.path_to_index_key(new_key_path)
      let entry = index.entry_for_value(val, container:)
      let idx = index.insert_entry(idx:, key: idx_key, entry:)
      Ok(primitives.patch(doc:, tree: new_tree, idx:))
    }

    // Indexed: insert into the implicit table within the array of tables entry,
    // relative to that entry's subtree, then rebuild.
    Ok(#(entry_path, rel_container)) -> {
      let before_keys =
        path.path_to_table_header(
          list.append(rel_container, [KeySegment(before)]),
        )
      let rel_key_path = list.append(rel_container, [KeySegment(key)])
      let kv =
        builder.build_kv_from_path(
          key: key_segment_names(rel_key_path),
          value: value.to_cst(val),
        )
      use cursor <- result.try(
        query.get_cursor(node: doc.tree, path: entry_path)
        |> result.replace_error(error.not_found_path(container)),
      )
      let entry = cursor.focus
      let new_entry =
        Node(
          ..entry,
          children: insert_before_root_kv_with_path(
            children: entry.children,
            target_key_path: before_keys,
            new_el: N(kv),
          ),
        )
      let new_tree =
        zipper.set_focus(zipper: cursor, node: new_entry) |> zipper.unzip
      Ok(primitives.rebuild(doc:, tree: new_tree))
    }
  }
}

fn key_segment_names(path: Path) -> List(String) {
  list.filter_map(path, fn(seg) {
    case seg {
      KeySegment(name) -> Ok(utils.quote_key(name))
      _ -> Error(Nil)
    }
  })
}

fn insert_before_root_kv_with_path(
  children children: List(Element(TomlKind)),
  target_key_path target_key_path: List(String),
  new_el new_el: Element(TomlKind),
) -> List(Element(TomlKind)) {
  case children {
    [] -> [new_el]
    [N(n), ..rest] if n.kind == types.KeyValue ->
      case elements.key_path(n.children) {
        Some(kp) if kp == target_key_path -> [new_el, N(n), ..rest]
        _ -> [
          N(n),
          ..insert_before_root_kv_with_path(
            children: rest,
            target_key_path:,
            new_el:,
          )
        ]
      }
    [el, ..rest] -> [
      el,
      ..insert_before_root_kv_with_path(
        children: rest,
        target_key_path:,
        new_el:,
      )
    ]
  }
}