//! C ABI for `fig`
//!
//! This file is the entry point for programs accessing `fig` outside of the Zig language.
const std = @import("std");
const builtin = @import("builtin");
const Document = @import("document.zig");
const AST = @import("ast/ast.zig");
const Span = @import("util/span.zig");
const Embed = @import("embed.zig");
const Editor = @import("editor.zig").Editor;
const build_options = @import("build_options");
/// The language registry (`languages/language.zig`'s `dialects`): the table
/// `FigFormat` is reified from, and the one every dispatch in this file reads.
/// A gated-out format is `void` in it — that is the build gate, and it is why
/// this file no longer names a single `languages/<lang>/…` module directly.
/// There is nothing left here to keep behind a `build_options.lang_*` test by
/// hand: each derived arm's first statement is the `d.Lang == void` guard, and
/// a `void` language has no parser, printer, dialect or `caps` to reach past
/// it.
const Languages = @import("languages/language.zig");
// Cross-format conversion helpers used by `fig_document_serialize`. Both are
// format-agnostic AST passes, always compiled in: `Lossless` for the `$fig`
// envelope, `Materialize` for collapsing a reference layer on the way out of
// a language that declares one (`Caps.references` — see `prepareDocumentAst`).
const Lossless = @import("lossless.zig");
const Materialize = @import("materialize.zig");
const Diagnostics = @import("diagnostics.zig");
/// A format registered at runtime (`fig_language_register`): the vtable and
/// node-table shapes this file re-exports under their `Fig*` names, the
/// registry an integer at or above `FIG_FORMAT_RUNTIME_BASE` resolves in,
/// and the one `Language` every such format is read, edited and printed
/// through. Every entry point that takes a format integer asks
/// `runtimeOf` first; see `docs/proposals/runtime-languages.md` §8.2.
const Runtime = @import("languages/runtime.zig");
// The wasm module's half of the runtime-language carrier: a language
// whose functions are JavaScript, reached through a wasm import. Exported
// by the wasm build alone — it is the TypeScript binding's bridge, not a
// C ABI entry point, and `fig.h` does not declare it. `abi-check` reads
// this file's `export fn` markers, so the export lives in its own file.
comptime {
if (builtin.cpu.arch.isWasm()) _ = @import("wasm_host.zig");
}
/// Logging for the C ABI build (this file is the static-lib root, so its
/// `std_options` wins). The default `std.log` handler writes to stderr via
/// `std.Io.Threaded`, which does not exist on `wasm32-freestanding` (no posix
/// I/O) — referencing it fails to compile. A library has no business writing to
/// stderr regardless, so drop logs on wasm and defer to the default elsewhere.
pub const std_options: std.Options = .{ .logFn = figLogFn };
fn figLogFn(
comptime level: std.log.Level,
comptime scope: @EnumLiteral(),
comptime format: []const u8,
args: anytype,
) void {
if (builtin.cpu.arch.isWasm()) return;
std.log.defaultLog(level, scope, format, args);
}
/// Translation of `fig` errors to C ABI.
pub const FigStatus = enum(c_int) {
ok = 0,
invalid_argument = 1,
parse_error = 2,
out_of_memory = 3,
unsupported_format = 4,
not_found = 5,
/// The operation is not defined for these arguments, though every argument
/// is individually valid — as distinct from `invalid_argument` (a malformed
/// call) and `unsupported_format` (a format this build cannot handle).
/// Returned by `fig_embed_retype` when asked to move a mid-document block to
/// an archetype that sits at an edge of the file.
unsupported_operation = 6,
internal_error = 255,
};
/// Translation of `fig.Language.Type` to the C ABI, REIFIED from the format
/// registry: one member per `Languages.dialects` entry, named for the entry and
/// valued at its `abi_value`. Two facts about the result are worth stating,
/// since neither is visible in the two lines that build it:
///
/// * The VALUES are the ABI, and a released one is permanent — which is
/// exactly why they are registry DATA rather than member positions. They
/// run 1,2,7 down the JSON family because JSON5 was appended after
/// generic XML took 6 — a value retired with the format in core 3.0 and
/// never reused — and 8..13 for the members that arrived later still. `zig build
/// abi-check` diffs them against fig.h's `FIG_FORMAT_*` enumerators in
/// both directions, and the literal pin below restates them a third time.
/// * The member ORDER is now the registry's (json, jsonc, json5, yaml, …)
/// rather than the ABI-value order this enum used to be written in.
/// Nothing depends on it — what a compiled caller holds is the value, and
/// every switch over this enum is either named-arm or `inline else`.
///
/// Every function accepts every member of a full build: `fig_parse`, the
/// serializers (`fig_value_serialize`, `fig_document_serialize`) and the
/// editor (`fig_editor_*`) alike. Whether a format is editable is not spelled
/// here: it is the language's own `caps.edit`, which `fig_editor_create`
/// reads, and a format compiled out of this build is rejected with
/// `unsupported_format` the same way. `fig_format_capabilities` reports the
/// exact support in a given build; the per-format prose (what each dialect is,
/// and what it can and cannot represent) lives on the registry entries and in
/// fig.h. JSONC = plain-JSON syntax with comments.
pub const FigFormat = @Enum(c_int, .exhaustive, format_names, &format_abi_values);
const format_names = Languages.namesOf(.all);
/// `FigFormat`'s tag values: each registry entry's frozen `abi_value`, in
/// registry order (so index-for-index with `format_names`).
const format_abi_values = blk: {
var values: [format_names.len]c_int = undefined;
var i: usize = 0;
for (Languages.dialects) |d| {
values[i] = d.abi_value;
i += 1;
}
break :blk values;
};
// The ABI pin, restated as LITERALS. `FigFormat` is now built FROM the registry,
// so asserting that the two agree would be circular — checking a derivation
// against what it derives from proves nothing. What is left to state is the
// contract itself: these thirteen names carry these thirteen integers,
// permanently, because that pairing is what every compiled caller holds. With
// `zig build abi-check` (registry vs fig.h's `FIG_FORMAT_*`, both directions)
// the numbering stays pinned from three independent directions: this literal
// table, the header, and the registry that now feeds the enum.
comptime {
const pinned = .{
.{ "json", 1 }, .{ "jsonc", 2 }, .{ "yaml", 3 },
.{ "toml", 4 }, .{ "zon", 5 }, .{ "json5", 7 },
.{ "fig", 8 }, .{ "ini", 9 }, .{ "dotenv", 10 },
.{ "properties", 11 }, .{ "plist", 12 }, .{ "nestedtext", 13 },
};
// Values a released ABI once carried and no longer does. Retired, not
// free: a caller compiled against the old header still holds the integer,
// and a new dialect taking it would be silently misread by that caller.
const retired = .{
.{ "xml", 6 }, // generic XML, core 2.x; removed in core 3.0
};
for (retired) |r| {
if (@hasField(FigFormat, r[0]))
@compileError("`FigFormat." ++ r[0] ++ "` was removed from the C ABI and cannot come back under that name");
for (pinned) |p| if (p[1] == r[1])
@compileError("ABI value " ++ std.fmt.comptimePrint("{d}", .{r[1]}) ++
" is retired (it was '" ++ r[0] ++ "') and cannot be given to '" ++ p[0] ++ "'");
}
if (@typeInfo(FigFormat).@"enum".fields.len != pinned.len)
@compileError("`FigFormat` no longer has exactly " ++
std.fmt.comptimePrint("{d}", .{pinned.len}) ++ " members — a format added to the" ++
" registry is a new C ABI value, so it must be added to this pin (and to fig.h) too");
// The pin itself stays below the runtime range. The registry check in
// `language.zig` proves the same of the rows the enum is built from; this
// one proves it of the literals, so that a pin written for a future format
// cannot promise a caller an integer the runtime registry may already have
// handed out.
for (pinned) |p| {
if (p[1] >= Languages.runtime_abi_base)
@compileError("the ABI pin gives '" ++ p[0] ++ "' the value " ++
std.fmt.comptimePrint("{d}", .{p[1]}) ++ ", which is inside the range reserved" ++
" for runtime-registered languages (FIG_FORMAT_RUNTIME_BASE = " ++
std.fmt.comptimePrint("{d}", .{Languages.runtime_abi_base}) ++ ")");
}
for (pinned) |p| {
if (!@hasField(FigFormat, p[0]))
@compileError("`FigFormat` has no member '" ++ p[0] ++
"' — a released ABI member cannot be renamed or dropped");
const got = @intFromEnum(@field(FigFormat, p[0]));
if (got != p[1])
@compileError("`FigFormat." ++ p[0] ++ "` is " ++
std.fmt.comptimePrint("{d}", .{got}) ++ " but the released C ABI says " ++
std.fmt.comptimePrint("{d}", .{p[1]}) ++
" — a released ABI value is permanent, so the registry entry is the mistake");
}
}
// ==================
// VERSION + CAPABILITIES
// ==================
//
// The stable query surface a host uses to interrogate the linked library before
// trusting it: which version it is, and what it can actually do in THIS build
// (formats can be compiled out — see `build_options.lang_*`). Both are pure
// functions: no handle, no allocation, safe to call from any thread at any time.
/// Packed library version `(major << 16) | (minor << 8) | patch`. A host can
/// compare this against the `FIG_VERSION_*` macros it compiled with to detect a
/// header/library skew. Sourced from `build.zig` (kept in sync with build.zig.zon).
pub export fn fig_version() u32 {
return (@as(u32, build_options.version_major) << 16) |
(@as(u32, build_options.version_minor) << 8) |
@as(u32, build_options.version_patch);
}
/// Null-terminated semantic version string of the linked library (e.g. "0.0.0").
/// Static storage — the caller must NOT free the returned pointer.
pub export fn fig_version_string() [*:0]const u8 {
const s = std.fmt.comptimePrint("{d}.{d}.{d}", .{
build_options.version_major,
build_options.version_minor,
build_options.version_patch,
});
return s;
}
/// Binary C ABI contract version (see `FIG_ABI_VERSION` in fig.h) — a monotonic
/// counter that moves only on a breaking ABI change, distinct from the marketing
/// version. Sourced from `build.zig`; `zig build abi-check` asserts the fig.h
/// macro matches this, and `semver-check` requires it to increment on any
/// breaking ABI diff.
pub export fn fig_abi_version() u32 {
return build_options.abi_version;
}
/// Capability bits returned (OR-combined) by `fig_format_capabilities`.
pub const FigCapability = enum(u32) {
/// `fig_parse` accepts this format.
read = 1 << 0,
/// `fig_editor_*` / `fig_embed_*` accept this format.
edit = 1 << 1,
/// `fig_*_serialize` can write this format.
serialize = 1 << 2,
/// The format has a reference layer — anchors, aliases, merges, tags —
/// which `fig_document_serialize` collapses when a document leaves it for
/// a format without one, and keeps when the target has one too.
references = 1 << 3,
_,
};
/// Report what `fig` can do with `format` in THIS build as a bitmask of
/// `FigCapability` (read | edit | serialize). Reflects both the format's inherent
/// support (its `Language.caps` declaration) and
/// build-time gating: a format compiled out reports 0, as does an unknown
/// `format` value. JSON/JSONC/JSON5 are always fully supported. Lets a host pick
/// a working format up front instead of probing via `unsupported_format` returns.
pub export fn fig_format_capabilities(format: c_int) u32 {
@setEvalBranchQuota(30_000);
// An unknown integer reports nothing, which is also what a `void` (compiled
// out) language reports — `capsOf` returns 0 for it, so the build gate needs
// no test of its own here.
if (runtimeOf(format)) |e| return capsBits(e.language.caps);
const f = std.enums.fromInt(FigFormat, format) orelse return 0;
return switch (f) {
inline else => |tag| comptime capsOf(Languages.entryFor(@tagName(tag)).Lang),
};
}
/// The registry entry a format integer at or above `FIG_FORMAT_RUNTIME_BASE`
/// names, or null — for an integer below it (a compiled format, resolved by
/// the caller's own switch) and for one never handed out. The checked
/// lookup every entry point taking `int format` opens with.
fn runtimeOf(format: c_int) ?*const Runtime.Entry {
return Runtime.entryByAbi(format);
}
// ==================
// RUNTIME LANGUAGES
// ==================
//
// A format fig did not compile in, registered by a host as a vtable. The
// shapes are `languages/runtime.zig`'s, stated in fig.h under these names
// and held to it by `zig build abi-check`; the registry, the validation and
// the load-time harness are that file's too. This section is the two entry
// points and the re-exports.
pub const FigLanguageVTable = Runtime.VTable;
pub const FigNodeTable = Runtime.NodeTable;
pub const FigNodeRow = Runtime.NodeRow;
pub const FigRegionRow = Runtime.RegionRow;
pub const FigMentionRow = Runtime.MentionRow;
pub const FigCommentRow = Runtime.CommentRow;
pub const FigSyntax = Runtime.SyntaxDesc;
pub const FigComments = Runtime.CommentsDesc;
pub const FigCommentDelimiter = Runtime.CommentDelimiterDesc;
pub const FigSectionHeader = Runtime.SectionHeaderDesc;
pub const FigClosedContainers = Runtime.ClosedContainersDesc;
pub const FigDialectDesc = Runtime.DialectDesc;
pub const FigPrintOptions = Runtime.PrintOptions;
/// The `version` a `FigLanguageVTable` must carry; fig.h's
/// `FIG_LANGUAGE_VTABLE_VERSION`, held equal by abi-check.
pub export fn fig_language_vtable_version() u32 {
return Runtime.vtable_version;
}
/// Register a language. On `.ok`, `*out_format` is the format integer of
/// its first dialect row. A row takes an integer at or above
/// `FIG_FORMAT_RUNTIME_BASE`, assigned per process — except a row named
/// after a format compiled out of this build, which takes that format's own
/// integer and stands in for it (`FIG_FORMAT_YAML` for a row named "yaml").
/// So the rows' integers are not consecutive in general; `fig_format_by_name`
/// finds each. Every entry point taking a format accepts them from then on,
/// at the tier the record's `caps` declare.
///
/// The record is validated by the rules a compiled format is held to, and
/// its `samples` are parsed, printed, reparsed and edited before anything is
/// registered; a record that fails either is refused with the reason in
/// `out_err` and registers nothing. `fig_language_register` copies what
/// `vt` points to; `vt->ctx` and the function pointers must stay valid for
/// the life of the process, since a format cannot be unregistered.
///
/// A name already registered — or the name of a format compiled into this
/// build — is refused as `invalid_argument`. Registration takes a lock; every other call on a
/// registered format is lock-free and reads only settled values, so the
/// threading note at the top of fig.h holds.
pub export fn fig_language_register(vt: ?*const FigLanguageVTable, out_format: ?*c_int, out_err: ?*FigError) FigStatus {
const out = out_format orelse return fillError(out_err, .invalid_argument, "out_format is null");
out.* = -1;
const record = vt orelse return fillError(out_err, .invalid_argument, "vtable is null");
const abi = Runtime.register(activeAllocator(), record) catch |err| return switch (err) {
error.OutOfMemory => fillError(out_err, .out_of_memory, "out of memory"),
error.InvalidLanguage, error.HarnessFailed, error.NameTaken => fillError(out_err, .invalid_argument, Runtime.lastRefusal()),
error.AllocatorMismatch, error.RegistryFull => fillError(out_err, .unsupported_operation, "the language registry cannot take another registration"),
};
out.* = abi;
return .ok;
}
/// The format integer of the dialect named `name` — a compiled format's
/// registry name (`json5`, `yaml`, …) or a registered language's — or -1.
/// The only stable way to persist a runtime format: its integer is assigned
/// per process, its name is not.
pub export fn fig_format_by_name(name: ?[*:0]const u8) c_int {
const n = std.mem.span(name orelse return -1);
if (Runtime.entryByName(n)) |e| return e.abi;
inline for (Languages.dialects) |d| {
if (std.mem.eql(u8, d.name, n)) return d.abi_value;
}
return -1;
}
fn capsBits(caps: Languages.Caps) u32 {
var bits: u32 = 0;
if (caps.read) bits |= @intFromEnum(FigCapability.read);
if (caps.edit) bits |= @intFromEnum(FigCapability.edit);
if (caps.serialize) bits |= @intFromEnum(FigCapability.serialize);
if (caps.references) bits |= @intFromEnum(FigCapability.references);
return bits;
}
/// The `FigCapability` bits `Lang` declares, or 0 when it is compiled out.
///
/// The values come from `Lang.caps` — the format's own declaration in
/// `<lang>/<lang>.zig`, required and checked by `language.validate` — rather
/// than being spelled a second time here. This switch WAS that second spelling,
/// and the drift it invited was silent both ways: a format gaining an editor
/// without its bit being set here reads as uneditable to every C host, and a
/// bit set here for support that does not exist sends hosts down a path that
/// fails with `unsupported_format`.
///
/// No `build_options.lang_*` test either: a compiled-out format is already
/// `void` in `languages/language.zig`, which is the same fact the gate stated.
///
/// The mapping above is no longer hand-written either: `FigFormat` is
/// per-DIALECT (json/jsonc/json5 are three ABI values over one `Language`)
/// while `caps` is per-LANGUAGE, and the bridge between the two tables is the
/// format registry's `Lang` field — so the caller looks the entry up by member
/// name and hands its language straight to this function. See the proposal's
/// §8.5.
fn capsOf(comptime Lang: type) u32 {
if (Lang == void) return 0;
var bits: u32 = 0;
if (Lang.caps.read) bits |= @intFromEnum(FigCapability.read);
if (Lang.caps.edit) bits |= @intFromEnum(FigCapability.edit);
if (Lang.caps.serialize) bits |= @intFromEnum(FigCapability.serialize);
if (Lang.caps.references) bits |= @intFromEnum(FigCapability.references);
return bits;
}
/// A handle to a `fig` document. (See `DocumentHandle` and `handle.*` declaration in `fig_parse`)
pub const FigDocument = opaque {};
const DocumentHandle = struct {
allocator: std.mem.Allocator,
source: []u8,
document: Document,
/// The format `source` was parsed as. `fig_document_serialize` consults it to
/// decide whether to collapse a reference layer before printing.
/// Meaningless when `runtime` is set.
format: FigFormat,
/// The runtime language `source` was parsed by, when it was one; its
/// entry's `caps.references` then says whether there is a layer.
runtime: ?*const Runtime.Entry = null,
/// Reused across `fig_document_serialize` calls; holds the bytes the most
/// recent call returned (cleared and refilled each time). Mirrors
/// `ValueHandle.rendered`.
rendered: std.Io.Writer.Allocating,
/// Backs the warnings (and their path strings) the most recent
/// `fig_document_diagnose` produced; reset (not freed) each call, so the
/// `path`/`note` bytes a `FigWarning` borrows are valid only until the next
/// diagnose on this handle or `fig_document_destroy`.
diag_arena: std.heap.ArenaAllocator,
/// The warning set the most recent `fig_document_diagnose` computed (stored
/// in `diag_arena`); `fig_document_warning` indexes into it. Empty until the
/// first diagnose. Replaced (not appended) each diagnose call.
diag_warnings: []const Diagnostics.Warning = &.{},
};
fn activeAllocator() std.mem.Allocator {
// wasm: no libc, `wasm_allocator` is the only option.
if (builtin.cpu.arch.isWasm()) return std.heap.wasm_allocator;
// Android: the shared library is built WITHOUT libc, because Zig ships
// glibc/musl but not Bionic, so a self-contained (NDK-free) `.so` cannot
// link one (see `addCApiLibrary` in build.zig). That rules out
// `c_allocator` (it wraps libc `malloc`), so use the libc-free
// `smp_allocator`. This stays internally consistent — every fig allocation
// and its matching `fig_free`/`fig_document_free` route back through here —
// so the swapped allocator never crosses the ABI boundary.
if (builtin.abi.isAndroid()) return std.heap.smp_allocator;
// Everywhere else: the process libc allocator.
return std.heap.c_allocator;
}
// ==================
// RAW MEMORY (for hosts without a shared allocator)
// ==================
//
// A caller that does not share this library's address space — chiefly the
// WebAssembly bindings — cannot otherwise place input bytes where the API can
// read them, nor read borrowed output without first copying it into a buffer it
// owns. These two entry points expose `activeAllocator()` for exactly that: in
// the wasm build they let JavaScript allocate inside linear memory, write the
// input, hand the pointer to `fig_parse`/`fig_editor_*`/…, then release it.
// Buffers obtained here MUST be released with `fig_free`, passing the same
// length that was requested.
/// Allocate `len` bytes and return a pointer to them, or null on failure / a
/// zero-length request. Bytes are uninitialized. Release with `fig_free`.
pub export fn fig_alloc(len: usize) ?[*]u8 {
if (len == 0) return null;
const mem = activeAllocator().alloc(u8, len) catch return null;
return mem.ptr;
}
/// Release a buffer obtained from `fig_alloc`. `len` must equal the length that
/// was requested. A null pointer or zero length is a no-op.
pub export fn fig_free(ptr: ?[*]u8, len: usize) void {
const p = ptr orelse return;
if (len == 0) return;
activeAllocator().free(p[0..len]);
}
/// Caller-allocated parse diagnostic. Mirrors `FigError` in `include/fig.h`.
/// Caller-allocated + size-versioned for the same reason `FigSerializeOptions`
/// is: the library writes only the fields the caller's `size` covers, so fields
/// may be appended later without breaking an older layout. Caller-owned (no
/// allocation, no handle lifetime) is what lets it carry a message for a failure
/// that happens *before* any document handle exists.
///
/// One definition: `Runtime.ErrorInfo` is this struct, since a runtime
/// language's `parse` fills the same record a C caller reads.
pub const FigError = Runtime.ErrorInfo;
/// Whether the caller-reported `FigError.size` covers `field` (same rule as
/// `optionCovers`). A field past `size` is absent in the caller's layout and
/// must not be written.
fn errCovers(size: u32, comptime field: []const u8) bool {
const end = @offsetOf(FigError, field) + @sizeOf(@FieldType(FigError, field));
return size >= end;
}
/// Fill `out_err` (if non-null) with `status` + `message`, then return `status`
/// so a failure path can `return fillError(...)`. Every field is gated on the
/// caller's `size`, so an older/smaller struct receives only the fields it
/// declared. `byte_offset`/`line`/`column` are 0 ("unknown") in this release —
/// surfacing the failing span from each parser is a planned follow-up.
/// Public for `wasm_host.zig`, whose one export reports the same way.
pub fn fillError(out_err: ?*FigError, status: FigStatus, message: []const u8) FigStatus {
const e = out_err orelse return status;
const size = e.size;
if (errCovers(size, "code")) e.code = @intFromEnum(status);
if (errCovers(size, "byte_offset")) e.byte_offset = 0;
if (errCovers(size, "line")) e.line = 0;
if (errCovers(size, "column")) e.column = 0;
// `message_len` and `message` are written together, and only when `size`
// covers the whole inline array — a partially-covered buffer gets nothing
// rather than a string truncated without its NUL terminator.
if (errCovers(size, "message")) {
const n = @min(message.len, e.message.len - 1);
@memcpy(e.message[0..n], message[0..n]);
e.message[n] = 0;
e.message_len = n;
}
return status;
}
pub export fn fig_parse(
input_ptr: ?[*]const u8,
input_len: usize,
format: c_int,
out_doc: ?*?*FigDocument,
) FigStatus {
return fig_parse_ex(input_ptr, input_len, format, out_doc, null);
}
/// As `fig_parse`, but on a nonzero return also fills `out_err` (caller-allocated;
/// nullable — NULL makes this identical to `fig_parse`) with a diagnostic. On
/// `.ok` the contents of `out_err` are left unspecified.
pub export fn fig_parse_ex(
input_ptr: ?[*]const u8,
input_len: usize,
format: c_int,
out_doc: ?*?*FigDocument,
out_err: ?*FigError,
) FigStatus {
@setEvalBranchQuota(30_000);
const out = out_doc orelse return fillError(out_err, .invalid_argument, "out_doc is null");
out.* = null;
// Empty input (len 0, null pointer or not) is a valid slice handed to the
// parser, which judges it per format (YAML → null document, TOML → empty
// table, JSON/JSON5/ZON → parse_error). Only a null pointer paired with a
// nonzero length is a malformed argument. This mirrors `fig_editor_create`.
const input = sliceOf(input_ptr, input_len) orelse
return fillError(out_err, .invalid_argument, "null input with nonzero length");
const runtime_entry = runtimeOf(format);
const fig_format = if (runtime_entry != null)
// Any member: `handle.format` is not read for a runtime document.
@as(FigFormat, @enumFromInt(format_abi_values[0]))
else
std.enums.fromInt(FigFormat, format) orelse
return fillError(out_err, .unsupported_format, "unsupported or unknown format");
const allocator = activeAllocator();
const source = allocator.dupe(u8, input) catch
return fillError(out_err, .out_of_memory, "out of memory");
const handle = allocator.create(DocumentHandle) catch {
allocator.free(source);
return fillError(out_err, .out_of_memory, "out of memory");
};
// On any parser error: free the not-yet-installed source/handle and report
// the error name as the message. The parser error set is payload-free, so the
// name is the best diagnostic available until per-parser span plumbing lands;
// `byte_offset`/`line`/`column` stay 0 for now. A runtime language's
// parser fills a `FigError` itself, and that is what is reported.
const doc = if (runtime_entry) |e| blk: {
var parser: Runtime.Language.Parser = .{ .allocator = allocator };
break :blk Runtime.Language.parse(&parser, source, e.typeOf()) catch |err| {
allocator.free(source);
allocator.destroy(handle);
if (err == error.OutOfMemory) return fillError(out_err, .out_of_memory, "out of memory");
const status = fillError(out_err, .parse_error, parser.lastMessage());
if (out_err) |oe| if (errCovers(oe.size, "byte_offset")) {
oe.byte_offset = parser.last_error.byte_offset;
};
return status;
};
} else switch (fig_format) {
inline else => |f| blk: {
const d = comptime Languages.entryFor(@tagName(f));
// The void guard first: a format compiled out of this build has no
// parser to call, and reporting that is `formatDisabled`.
if (comptime d.Lang == void) return formatDisabled(out_err, source, handle);
// `d.dialect` is the language default for every entry but the JSON
// trio, which is the whole reason the registry carries it — three
// ABI values (json/jsonc/json5) over one parser.
break :blk d.Lang.Parser.parse(allocator, source, d.dialect) catch |err|
return parseFailed(out_err, err, source, handle);
},
};
handle.* = .{
.allocator = allocator,
.source = source,
.document = doc,
.format = fig_format,
.runtime = runtime_entry,
.rendered = std.Io.Writer.Allocating.init(allocator),
.diag_arena = std.heap.ArenaAllocator.init(allocator),
};
out.* = @ptrCast(handle);
return .ok;
}
fn parseFailureStatus(err: anyerror) FigStatus {
return switch (err) {
error.OutOfMemory => .out_of_memory,
else => .parse_error,
};
}
/// Shared cleanup + diagnostic for a failed parse: release the not-yet-installed
/// `source`/`handle`, then report the error (its name as the message).
fn parseFailed(out_err: ?*FigError, err: anyerror, source: []u8, handle: *DocumentHandle) FigStatus {
const allocator = activeAllocator();
allocator.free(source);
allocator.destroy(handle);
return fillError(out_err, parseFailureStatus(err), @errorName(err));
}
/// Cleanup + diagnostic for a format compiled out of this build.
fn formatDisabled(out_err: ?*FigError, source: []u8, handle: *DocumentHandle) FigStatus {
const allocator = activeAllocator();
allocator.free(source);
allocator.destroy(handle);
return fillError(out_err, .unsupported_format, "format not compiled into this build");
}
/// Memory allocated by this API should be freed by this API.
pub export fn fig_document_destroy(doc: ?*FigDocument) void {
const public_doc = doc orelse return;
const handle: *DocumentHandle = @ptrCast(@alignCast(public_doc));
handle.rendered.deinit();
handle.diag_arena.deinit();
handle.document.deinit(handle.allocator);
handle.allocator.free(handle.source);
handle.allocator.destroy(handle);
}
// ==================
// DOCUMENT TRAVERSAL
// ==================
pub const FigNodeId = u32;
const fig_node_none: FigNodeId = 0xFFFFFFFF;
/// Translation of an AST node's kind to the C ABI. Mirrors `FigNodeKind` in
/// `include/fig.h`.
pub const FigNodeKind = enum(c_int) {
invalid = -1,
null_ = 0,
bool_ = 1,
int = 2,
float = 3,
string = 4,
sequence = 5,
mapping = 6,
keyvalue = 7,
alias = 8,
/// A format-specific scalar with no C type of its own (a TOML datetime,
/// a ZON enum or char literal, a plist date): `fig_node_extended` says
/// which, and gives its text.
extended = 9,
};
fn handleFrom(doc: ?*const FigDocument) ?*const DocumentHandle {
const public_doc = doc orelse return null;
return @ptrCast(@alignCast(public_doc));
}
/// Returns the node at `id`, or null if `doc` is null or `id` is out of range.
fn nodeAt(doc: ?*const FigDocument, id: FigNodeId) ?AST.Node {
const handle = handleFrom(doc) orelse return null;
const nodes = handle.document.ast.nodes;
if (id >= nodes.len) return null;
return nodes[id];
}
pub export fn fig_document_root(doc: ?*const FigDocument) FigNodeId {
const handle = handleFrom(doc) orelse return fig_node_none;
const ast = handle.document.ast;
if (ast.root >= ast.nodes.len) return fig_node_none;
return ast.root;
}
/// A `FigNodeKind` value, as the `int` fig.h returns so that a binding
/// never decodes a kind a later fig adds into a closed enum.
pub export fn fig_node_kind(doc: ?*const FigDocument, node: FigNodeId) c_int {
return @intFromEnum(nodeKind(doc, node));
}
fn nodeKind(doc: ?*const FigDocument, node: FigNodeId) FigNodeKind {
const n = nodeAt(doc, node) orelse return .invalid;
return switch (n.kind) {
.null_ => .null_,
.boolean => .bool_,
.string => .string,
.number => |num| switch (num.kind) {
.integer => .int,
.float => .float,
},
.extended => .extended,
.sequence => .sequence,
.mapping => .mapping,
.keyvalue => .keyvalue,
.alias => .alias,
};
}
pub export fn fig_node_first_child(doc: ?*const FigDocument, node: FigNodeId) FigNodeId {
const n = nodeAt(doc, node) orelse return fig_node_none;
const child_id = switch (n.kind) {
.sequence, .mapping => |first| first,
else => return fig_node_none,
};
return child_id orelse fig_node_none;
}
pub export fn fig_node_next_sibling(doc: ?*const FigDocument, node: FigNodeId) FigNodeId {
const n = nodeAt(doc, node) orelse return fig_node_none;
return n.next_sibling orelse fig_node_none;
}
pub export fn fig_node_child_count(doc: ?*const FigDocument, node: FigNodeId) usize {
const handle = handleFrom(doc) orelse return 0;
const nodes = handle.document.ast.nodes;
if (node >= nodes.len) return 0;
var current: ?FigNodeId = switch (nodes[node].kind) {
.sequence, .mapping => |first| first,
else => return 0,
};
var count: usize = 0;
while (current) |id| {
if (id >= nodes.len) break;
count += 1;
current = nodes[id].next_sibling;
}
return count;
}
pub export fn fig_keyvalue_key(doc: ?*const FigDocument, node: FigNodeId) FigNodeId {
const n = nodeAt(doc, node) orelse return fig_node_none;
return switch (n.kind) {
.keyvalue => |kv| kv.key,
else => fig_node_none,
};
}
pub export fn fig_keyvalue_value(doc: ?*const FigDocument, node: FigNodeId) FigNodeId {
const n = nodeAt(doc, node) orelse return fig_node_none;
return switch (n.kind) {
.keyvalue => |kv| kv.value,
else => fig_node_none,
};
}
pub export fn fig_node_bool(doc: ?*const FigDocument, node: FigNodeId, out: ?*bool) bool {
const out_ptr = out orelse return false;
const n = nodeAt(doc, node) orelse return false;
switch (n.kind) {
.boolean => |b| {
out_ptr.* = b;
return true;
},
else => return false,
}
}
pub export fn fig_node_number(
doc: ?*const FigDocument,
node: FigNodeId,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) bool {
const p = out_ptr orelse return false;
const l = out_len orelse return false;
const n = nodeAt(doc, node) orelse return false;
switch (n.kind) {
.number => |num| {
p.* = num.raw.ptr;
l.* = num.raw.len;
return true;
},
// A char literal reads out as its decimal codepoint (see fig_node_kind).
.extended => |ext| switch (ext.kind) {
.char_literal => {
p.* = ext.text.ptr;
l.* = ext.text.len;
return true;
},
else => return false,
},
else => return false,
}
}
pub export fn fig_node_string(
doc: ?*const FigDocument,
node: FigNodeId,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) bool {
const p = out_ptr orelse return false;
const l = out_len orelse return false;
const n = nodeAt(doc, node) orelse return false;
switch (n.kind) {
.string => |s| {
p.* = s.ptr;
l.* = s.len;
return true;
},
// Datetimes and enum literals read out as their text (see fig_node_kind);
// a char literal is a number, handled by fig_node_number instead.
.extended => |ext| switch (ext.kind) {
.char_literal => return false,
else => {
p.* = ext.text.ptr;
l.* = ext.text.len;
return true;
},
},
else => return false,
}
}
/// Recover the precise kind and text of a format-specific extended scalar (TOML
/// datetime, ZON enum/char literal). Returns true and writes its `FigExtKind` to
/// `out_kind` and source text to `out_ptr`/`out_len` when `node` is extended;
/// otherwise returns false, leaving the out-params untouched.
///
/// `fig_node_kind` reports these nodes as `FIG_NODE_EXTENDED`; this accessor
/// says which kind and gives the text. `fig_node_string` (and, for a char
/// literal, `fig_node_number`) still yield it too.
pub export fn fig_node_extended(
doc: ?*const FigDocument,
node: FigNodeId,
out_kind: ?*c_int,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) bool {
const k = out_kind orelse return false;
const p = out_ptr orelse return false;
const l = out_len orelse return false;
const n = nodeAt(doc, node) orelse return false;
switch (n.kind) {
.extended => |ext| {
k.* = @intFromEnum(figExtKindOf(ext.kind));
p.* = ext.text.ptr;
l.* = ext.text.len;
return true;
},
else => return false,
}
}
/// Map an AST extended kind to its C ABI enumerator. The two enums carry the
/// same cases in the same order; the explicit switch keeps them pinned together.
fn figExtKindOf(kind: AST.Node.Kind.Extended.ExtKind) FigExtKind {
return switch (kind) {
.offset_datetime => .offset_datetime,
.local_datetime => .local_datetime,
.local_date => .local_date,
.local_time => .local_time,
.enum_literal => .enum_literal,
.char_literal => .char_literal,
.number_special => .number_special,
.plist_date => .plist_date,
.plist_data => .plist_data,
};
}
// ======
// EDITING
// ======
//
// The write path mirrors the read path: an opaque handle owns the source +
// parse, and edits splice bytes in place (preserving comments/formatting) then
// reparse. `fig_editor_*` works on a whole document; `fig_embed_*` is a thin
// embed-aware layer that edits the config inside a host file (e.g. markdown
// frontmatter) and re-assembles it. Path segments cross the boundary as an array
// of `FigPathSegment` (key string | sequence index), mirroring `AST.PathSegment`.
/// One step of a path: `kind == 0` selects mapping key `key_ptr[0..key_len]`;
/// `kind == 1` selects sequence element `index`. `kind` is a C `int` (not a
/// fixed-width `int32_t`), matching the other small discriminants crossing this
/// ABI — `format`, `container`, and the `kind` of `fig_value_extended` /
/// `fig_node_extended` — so every enum-like field is the one integer type.
pub const FigPathSegment = extern struct {
kind: c_int,
key_ptr: ?[*]const u8,
key_len: usize,
index: usize,
};
const max_path_len = 128;
const max_keys_len = 512;
/// A borrowed UTF-8 string slice passed across the C ABI: `ptr[0..len]`. Used
/// for the key list of `fig_*_reorder_keys`.
pub const FigStr = extern struct {
ptr: ?[*]const u8,
len: usize,
};
/// Decode a C array of `FigStr` into `buf`. Returns the populated slice, or
/// null on a malformed entry / over-long list. A zero-length entry decodes to
/// an empty key regardless of its (possibly null) pointer.
fn decodeKeys(
keys_ptr: ?[*]const FigStr,
keys_len: usize,
buf: [][]const u8,
) ?[][]const u8 {
if (keys_len == 0) return buf[0..0];
if (keys_len > buf.len) return null;
const ks = keys_ptr orelse return null;
for (0..keys_len) |i| {
buf[i] = if (ks[i].len == 0) &.{} else (ks[i].ptr orelse return null)[0..ks[i].len];
}
return buf[0..keys_len];
}
/// View a C `usize` array as a Zig slice. Returns null only when the pointer is
/// null for a non-empty length (a zero length is a valid empty list).
fn decodeIndices(ptr: ?[*]const usize, len: usize) ?[]const usize {
if (len == 0) return &.{};
const p = ptr orelse return null;
return p[0..len];
}
/// Decode a C path array into `buf`. Returns the populated slice, or null on a
/// malformed segment / over-long path.
fn decodePath(
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
buf: []AST.PathSegment,
) ?[]AST.PathSegment {
if (path_len == 0) return buf[0..0];
if (path_len > buf.len) return null;
const segs = path_ptr orelse return null;
for (0..path_len) |i| {
buf[i] = switch (segs[i].kind) {
0 => .{ .key = (segs[i].key_ptr orelse return null)[0..segs[i].key_len] },
1 => .{ .index = segs[i].index },
else => return null,
};
}
return buf[0..path_len];
}
/// Translate the editor/AST error set onto `FigStatus`.
fn editStatus(err: anyerror) FigStatus {
return switch (err) {
error.OutOfMemory => .out_of_memory,
error.NotFound => .not_found,
error.NotAMapping, error.NotASequence, error.NotAContainer, error.InvalidDocument => .invalid_argument,
// A key rename at a path that names no key: a sequence item, or the root.
error.NotAKey => .invalid_argument,
// `setSequence` declines a shape it can't safely diff (empty target,
// empty/non-scalar list, a format whose scalars can't stand alone).
error.UnsupportedShape => .invalid_argument,
// TOML structural edits reject a request that doesn't match the document
// shape (e.g. appending to a non-array-of-tables, deleting a table by the
// scalar ops, inserting a key/table that already exists). These are caller
// errors, not malformed-source reparse failures.
error.NotATable, error.NotAnInlineArray, error.NotAnArrayOfTables, error.TableExists, error.SectionExists, error.ContainerExists, error.DuplicateKey, error.MergeOnlyKey => .invalid_argument,
// The pre-op guards: a scalar op addressed a whole scattered container,
// whose node span covers only the header line (delete) or just the name
// inside it (replace), so the generic splice would orphan or rename what
// it can't see. One error per format vocabulary — TOML tables, INI
// sections, fig block containers — and all of them are caller errors
// about the request, not malformed source: the whole-container ops
// (`deleteContainer`, `renameContainer`, …) are what handle these shapes.
error.CannotDeleteTable, error.CannotDeleteSection, error.CannotDeleteContainer => .invalid_argument,
error.CannotReplaceTable, error.CannotReplaceSection, error.CannotReplaceContainer => .invalid_argument,
// The same family for the two ops that RELOCATE an entry's block: the
// block of a `[header]`/`[section]` entry is its header line, so a move
// strands the body and a reorder hands it to the entry that lands
// before it. `fig_editor_move_container`/`fig_editor_reorder_containers`
// are the ops that relocate a scattered container whole.
error.CannotMoveTable, error.CannotMoveSection, error.CannotMoveContainer => .invalid_argument,
error.CannotReorderTables, error.CannotReorderSections, error.CannotReorderContainers => .invalid_argument,
// NestedText declines two shapes outright: inserting into an inline
// `{}`/`[]` (expanding one into block form is out of scope) and renaming
// a key to text that needs the multiline `: key` form.
error.EmptyInlineContainer, error.KeyRequiresMultilineForm => .invalid_argument,
// A runtime language's renderer declined the text (its reason is in
// `Runtime.lastRefusal()`): the request, not the source.
error.RendererRefused => .invalid_argument,
// A block-spelled value (`- a`, `k: v`, `|`) was handed to a splice into
// a flow `{…}`/`[…]` container, which has no way to hold it.
error.BlockValueIntoFlow => .invalid_argument,
// A container that closes on its last entry's line has no line to
// append a new entry after (`<key>o</key><dict>…</dict>`).
error.ContainerClosesOnItsLine => .invalid_argument,
// An insert into a section with no header line of its own — an
// implicit TOML table — where every line it is named on opens a child.
error.ImplicitSection => .invalid_argument,
// The target dialect has no comment syntax (strict JSON).
error.CommentsUnsupported => .unsupported_format,
// The addressed node shares its parent's line inside a flow collection
// (`members = ["a", "b"]`), so it owns neither the line above nor that
// line's end — a fact about the request's PATH, not about the format,
// which is why this is `invalid_argument` and `CommentsUnsupported`
// above is not.
error.CommentsUnanchored => .invalid_argument,
// `fig_editor_uncomment_*` stripped the markers and the result parsed,
// but to a document whose other nodes had moved — so those lines were
// not the entry the caller took them for, and the splice was rolled
// back. Every argument was individually valid, which is what
// `unsupported_operation` says and `invalid_argument` would not.
error.CommentNotAnEntry => .unsupported_operation,
// A value the format cannot represent at all — plist has no null. Same
// answer `serializeStatus` gives it, since it is the same fact about the
// format either way.
error.NullUnsupported => .unsupported_format,
// A trailing comment was given multi-line text, or (plist) comment text
// carrying `--`, which cannot go inside ``.
error.MultilineComment, error.InvalidComment => .invalid_argument,
error.NotInitialized, error.MultipleInit, error.InvalidSpan => .internal_error,
// A reparse after a malformed edit lands here.
else => .parse_error,
};
}
pub const FigEditor = opaque {};
// The editor backends, shared by the document editor and the embed editor: a
// tagged union with one variant per editable language COMPILED INTO THIS BUILD
// (json/yaml/toml/zon/fig/…, every one whose `caps.edit` is true). The type is assembled from
// only the enabled languages — rather than carrying `void` placeholder fields —
// so the `inline else` switches over `handle.inner` stay valid: every variant
// has a real `Editor` payload to act on.
//
// Both halves of "editable and enabled" are read rather than restated:
// `Languages.compiled` is already the enabled list (a gated-out language is
// absent from it), and `caps.edit` is the format's own declaration of whether
// an `Editor` can be instantiated for it — the same bit `fig_editor_create`
// tests and `fig_format_capabilities` reports. This is per-LANGUAGE, not
// per-dialect: the three JSON dialects share one variant, which is what makes
// `@unionInit(EditorUnion, d.Lang.name, …)` in `fig_editor_create` land jsonc
// and json5 on the `json` backend with their own `format` payload.
//
// The variant ORDER is `compiled`'s (it was hand-written as json, yaml, toml,
// fig, zon, … before). Nothing observes it: the tag is internal, never
// serialized, never crosses the ABI, and every switch over the union is
// `inline else`.
//
// Plus one arm that is not a compiled language: `runtime`, the `Editor` over
// `Runtime.Language`, which every runtime format is edited through — the
// `format` payload names the registry entry. It is the `.runtime` member the
// proposal's §10 asks for, and the whole of what the C API's editor gains.
const editor_variants = blk: {
const Variant = struct { name: [:0]const u8, Lang: type };
var variants: []const Variant = &.{};
for (Languages.compiled) |Lang| {
if (!Lang.caps.edit) continue;
variants = variants ++ &[_]Variant{.{ .name = Lang.name, .Lang = Lang }};
}
variants = variants ++ &[_]Variant{.{ .name = "runtime", .Lang = Runtime.Language }};
break :blk variants;
};
const EditorUnion = blk: {
if (editor_variants.len == 0)
@compileError("fig C ABI: no editable language enabled; build with at least one of -Djson/-Dyaml/-Dtoml");
const n = editor_variants.len;
const IntTag = std.math.IntFittingRange(0, n - 1);
var names: [n][:0]const u8 = undefined;
var types: [n]type = undefined;
var values: [n]IntTag = undefined;
var attrs: [n]std.builtin.Type.UnionField.Attributes = undefined;
for (editor_variants, 0..) |v, i| {
names[i] = v.name;
types[i] = Editor(v.Lang);
values[i] = @intCast(i);
attrs[i] = .{};
}
// This Zig spells type reification as granular builtins (`@Enum`/`@Union`)
// rather than `@Type(.{...})`.
const Tag = @Enum(IntTag, .exhaustive, &names, &values);
break :blk @Union(.auto, Tag, &names, &types, &attrs);
};
const EditorHandle = struct {
allocator: std.mem.Allocator,
inner: EditorUnion,
/// Reused buffer backing the borrowed bytes returned by the comment-read
/// exports (`fig_editor_get_*_comment`). Refilled per call; valid until the
/// next read call on this handle or `fig_editor_destroy`.
scratch: std.ArrayList(u8) = .empty,
fn deinit(self: *EditorHandle) void {
switch (self.inner) {
inline else => |*e| e.deinit(),
}
self.scratch.deinit(self.allocator);
}
};
fn editorFrom(ed: ?*FigEditor) ?*EditorHandle {
const p = ed orelse return null;
return @ptrCast(@alignCast(p));
}
pub export fn fig_editor_create(
input_ptr: ?[*]const u8,
input_len: usize,
format: c_int,
out_editor: ?*?*FigEditor,
) FigStatus {
@setEvalBranchQuota(30_000);
const out = out_editor orelse return .invalid_argument;
out.* = null;
// Empty input (len 0, with or without a null pointer) is a valid empty
// document; a non-null pointer with a length is read as-is.
const slice = sliceOf(input_ptr, input_len) orelse return .invalid_argument;
const allocator = activeAllocator();
// The backend is chosen BEFORE the handle is allocated, so the three ways a
// format can be refused — unknown value, compiled out, no in-place editor —
// all return without anything to free. `activeAllocator` itself allocates
// nothing.
const inner: EditorUnion = if (runtimeOf(format)) |e| blk: {
if (!e.language.caps.edit) return .unsupported_format;
break :blk @unionInit(EditorUnion, "runtime", .{ .allocator = allocator, .format = e.typeOf() });
} else switch (std.enums.fromInt(FigFormat, format) orelse return .unsupported_format) {
inline else => |f| blk: {
const d = comptime Languages.entryFor(@tagName(f));
// Compiled out of this build, and a format with a reader and a
// writer but no in-place editor (`caps.edit = false`, which no
// format in tree declares any more). Both are the same
// answer to a caller, and both are read rather than listed: a
// `void` language has no `Editor` instantiation, and `caps.edit` is
// the format's own declaration (`EditorUnion` is built from exactly
// this pair of tests, so a variant is guaranteed to exist below).
if (comptime d.Lang == void) return .unsupported_format;
if (comptime !d.Lang.caps.edit) return .unsupported_format;
// Keyed by the LANGUAGE name, so all three JSON dialects land on the
// one `json` variant and separate by payload: jsonc and json5 carry
// their own `format`, which is what makes the JSON5 editor accept
// unquoted keys, trailing commas and `//` comments (it splices
// source in place, so all of that survives untouched outside the
// edited span). For every other entry `d.dialect` IS
// `Lang.default_type`, which is the field's default.
break :blk @unionInit(EditorUnion, d.Lang.name, .{ .allocator = allocator, .format = d.dialect });
},
};
const handle = allocator.create(EditorHandle) catch return .out_of_memory;
handle.allocator = allocator;
// Field-by-field init (not a struct literal), so set the read scratch buffer's
// default explicitly — otherwise destroy frees uninitialized memory.
handle.scratch = .empty;
handle.inner = inner;
switch (handle.inner) {
inline else => |*e| e.init(slice) catch |err| {
e.deinit();
allocator.destroy(handle);
return editStatus(err);
},
}
out.* = @ptrCast(handle);
return .ok;
}
pub export fn fig_editor_destroy(ed: ?*FigEditor) void {
const handle = editorFrom(ed) orelse return;
const allocator = handle.allocator;
handle.deinit();
allocator.destroy(handle);
}
pub export fn fig_editor_replace_val(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
repl_ptr: ?[*]const u8,
repl_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const repl = sliceOf(repl_ptr, repl_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.replaceValAtPath(path, repl)) .ok else |err| editStatus(err),
};
}
pub export fn fig_editor_replace_key(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
repl_ptr: ?[*]const u8,
repl_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const repl = sliceOf(repl_ptr, repl_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.replaceKeyAtPath(path, repl)) .ok else |err| editStatus(err),
};
}
/// `fig_editor_replace_key` with the new key's NAME rather than its syntax,
/// spelled as the document's format spells a key — as
/// `fig_editor_insert_named_key` is to `fig_editor_insert_key`.
pub export fn fig_editor_replace_named_key(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
name_ptr: ?[*]const u8,
name_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const name = sliceOf(name_ptr, name_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.replaceNamedKey(path, name)) .ok else |err| editStatus(err),
};
}
/// Upsert: replace the value at `path`, or insert it when only the trailing key
/// is absent (the `path` must end in a key). Folds replace-or-insert into one
/// op; see `Editor.set`.
pub export fn fig_editor_set(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
val_ptr: ?[*]const u8,
val_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const val = sliceOf(val_ptr, val_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.set(path, val)) .ok else |err| editStatus(err),
};
}
// ── Comment editing ─────────────────────────────────────────────────────────
// Splice comment trivia around the node at `path`, preserving the rest of the
// document byte-for-byte. The marker (`#`, `//`) is added by the editor; a
// dialect without comment syntax (strict JSON) returns `unsupported_format`.
//
// An element or entry of a one-line flow collection owns no comment at all —
// the line is its parent's — so the two writes return `invalid_argument`, the
// deletes are a no-op, and the reads answer `not_found`. See
// `Editor.commentsUnanchored`.
/// Add an own-line comment ABOVE the node at `path`. `text` may be multi-line
/// (one comment line per row), at the node's indentation, nearest the node.
pub export fn fig_editor_add_leading_comment(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
text_ptr: ?[*]const u8,
text_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const text = sliceOf(text_ptr, text_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.addLeadingComment(path, text)) .ok else |err| editStatus(err),
};
}
/// Set the same-line trailing comment on the value at `path` (replace existing
/// or append). `text` must be single-line (else `invalid_argument`).
pub export fn fig_editor_set_trailing_comment(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
text_ptr: ?[*]const u8,
text_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const text = sliceOf(text_ptr, text_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.setTrailingComment(path, text)) .ok else |err| editStatus(err),
};
}
/// Remove the own-line comment block immediately above the node at `path` (no-op
/// when there is none).
pub export fn fig_editor_delete_leading_comments(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.deleteLeadingComments(path)) .ok else |err| editStatus(err),
};
}
/// Remove the same-line trailing comment on the value at `path` (no-op when
/// there is none).
pub export fn fig_editor_delete_trailing_comment(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.deleteTrailingComment(path)) .ok else |err| editStatus(err),
};
}
// ── Comment reading ─────────────────────────────────────────────────────────
// Read back a comment without mutating the document. The returned bytes (marker
// stripped) are BORROWED from the editor handle's scratch buffer: valid until the
// next read call on this handle or `fig_editor_destroy`. The distinction between
// an ABSENT comment and a PRESENT-BUT-EMPTY one (a bare `#`/`//`) is carried by
// the status: `not_found` means absent; `ok` with `out_len == 0` means present and
// empty. Strict JSON (no comment syntax) returns `unsupported_format`.
/// Hand a comment read's bytes back across the ABI: copy them into `scratch`
/// (borrowed until the next read on this handle), free the original, and set
/// the out-parameters. A null `maybe` — no such comment — is `not_found`, which
/// is what distinguishes an ABSENT comment from a present-but-empty one.
/// Shared by both handles' reads, and by the dangling read's own entry point.
fn fillCommentOut(
allocator: std.mem.Allocator,
scratch: *std.ArrayList(u8),
maybe: ?[]u8,
p: *[*c]const u8,
l: *usize,
) FigStatus {
const bytes = maybe orelse return .not_found;
defer allocator.free(bytes);
scratch.clearRetainingCapacity();
// Keep a valid (non-dangling) pointer even for a zero-length present comment.
scratch.ensureTotalCapacity(allocator, bytes.len + 1) catch return .out_of_memory;
scratch.appendSliceAssumeCapacity(bytes);
p.* = scratch.items.ptr;
l.* = scratch.items.len;
return .ok;
}
fn editorGetComment(
handle: *EditorHandle,
path: []const AST.PathSegment,
trailing: bool,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
const p = out_ptr orelse return .invalid_argument;
const l = out_len orelse return .invalid_argument;
const maybe = (switch (handle.inner) {
inline else => |*e| if (trailing) e.getTrailingComment(path) else e.getLeadingComment(path),
}) catch |err| return editStatus(err);
return fillCommentOut(handle.allocator, &handle.scratch, maybe, p, l);
}
/// Read the own-line comment block immediately ABOVE the node at `path`, joined by
/// '\n' with markers and indentation stripped. `not_found` when there is no block.
pub export fn fig_editor_get_leading_comment(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return editorGetComment(handle, path, false, out_ptr, out_len);
}
/// Read the same-line trailing comment on the value at `path`, marker stripped.
/// `not_found` when there is none.
pub export fn fig_editor_get_trailing_comment(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return editorGetComment(handle, path, true, out_ptr, out_len);
}
// ── The dangling anchor, and comment-out ────────────────────────────────────
// A container's third comment anchor — the run at the end of its body, after
// its last entry — plus the pair that turns an entry into a comment run and
// back. See `Editor.getDanglingComment` and `Editor.commentOut`.
/// Add own-line comment line(s) at the END of the container at `path`'s body
/// (empty path = the root), at the body's child depth.
pub export fn fig_editor_add_dangling_comment(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
text_ptr: ?[*]const u8,
text_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const text = sliceOf(text_ptr, text_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.addDanglingComment(path, text)) .ok else |err| editStatus(err),
};
}
/// Remove the whole dangling run at the end of the container at `path`'s body
/// (no-op when there is none).
pub export fn fig_editor_delete_dangling_comments(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.deleteDanglingComments(path)) .ok else |err| editStatus(err),
};
}
/// Read the dangling run at the end of the container at `path`'s body, joined
/// by '\n' with markers and indentation stripped. Same borrowed-bytes and
/// `not_found`-means-absent contract as the other comment reads.
pub export fn fig_editor_get_dangling_comment(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const p = out_ptr orelse return .invalid_argument;
const l = out_len orelse return .invalid_argument;
const maybe = (switch (handle.inner) {
inline else => |*e| e.getDanglingComment(path),
}) catch |err| return editStatus(err);
return fillCommentOut(handle.allocator, &handle.scratch, maybe, p, l);
}
/// Turn the node at `path` into a comment run: every line of its span gains the
/// line marker at that line's own indentation. Its own leading comment block
/// stays above it; afterwards the tree no longer has the node.
pub export fn fig_editor_comment_out(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.commentOut(path)) .ok else |err| editStatus(err),
};
}
/// Bring `line_count` lines of the LEADING comment block above the node at
/// `path`, starting at `first_line`, back as entries. `unsupported_operation`
/// when the result parses but is not the entry it was taken for; the document
/// is unchanged on every failure.
pub export fn fig_editor_uncomment_leading(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
first_line: usize,
line_count: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.uncommentLeading(path, first_line, line_count)) .ok else |err| editStatus(err),
};
}
/// The dangling twin of `fig_editor_uncomment_leading`: the run at the end of
/// the container at `path`'s body — where a commented-out LAST entry lands.
pub export fn fig_editor_uncomment_dangling(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
first_line: usize,
line_count: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.uncommentDangling(path, first_line, line_count)) .ok else |err| editStatus(err),
};
}
pub export fn fig_editor_insert_key(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
key_ptr: ?[*]const u8,
key_len: usize,
val_ptr: ?[*]const u8,
val_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const key = sliceOf(key_ptr, key_len) orelse return .invalid_argument;
const val = sliceOf(val_ptr, val_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.insertKey(path, key, val)) .ok else |err| editStatus(err),
};
}
/// `fig_editor_insert_key` with the key's NAME rather than its syntax: the
/// name is spelled as the document's format spells a key — `.name` in ZON,
/// quoted in strict JSON, quoted when it must be in TOML — which is what a
/// binding that took the name from its caller wants. `fig_editor_insert_key`
/// stays for a caller that has the syntax already.
pub export fn fig_editor_insert_named_key(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
name_ptr: ?[*]const u8,
name_len: usize,
val_ptr: ?[*]const u8,
val_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const name = sliceOf(name_ptr, name_len) orelse return .invalid_argument;
const val = sliceOf(val_ptr, val_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.insertNamedKey(path, name, val)) .ok else |err| editStatus(err),
};
}
pub export fn fig_editor_delete_key(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.deleteKey(path)) .ok else |err| editStatus(err),
};
}
pub export fn fig_editor_append_seq(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
val_ptr: ?[*]const u8,
val_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const val = sliceOf(val_ptr, val_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.appendToSeq(path, val)) .ok else |err| editStatus(err),
};
}
pub export fn fig_editor_prepend_seq(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
val_ptr: ?[*]const u8,
val_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const val = sliceOf(val_ptr, val_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.prependToSeq(path, val)) .ok else |err| editStatus(err),
};
}
pub export fn fig_editor_remove_seq_item(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
index: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.removeSeqItem(path, index)) .ok else |err| editStatus(err),
};
}
pub export fn fig_editor_move_key(
ed: ?*FigEditor,
src_ptr: ?[*]const FigPathSegment,
src_len: usize,
dest_ptr: ?[*]const FigPathSegment,
dest_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var src_buf: [max_path_len]AST.PathSegment = undefined;
var dest_buf: [max_path_len]AST.PathSegment = undefined;
const src = decodePath(src_ptr, src_len, &src_buf) orelse return .invalid_argument;
const dest = decodePath(dest_ptr, dest_len, &dest_buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.moveKey(src, dest)) .ok else |err| editStatus(err),
};
}
pub export fn fig_editor_reorder_keys(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
keys_ptr: ?[*]const FigStr,
keys_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var path_buf: [max_path_len]AST.PathSegment = undefined;
var keys_buf: [max_keys_len][]const u8 = undefined;
const path = decodePath(path_ptr, path_len, &path_buf) orelse return .invalid_argument;
const keys = decodeKeys(keys_ptr, keys_len, &keys_buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.reorderKeys(path, keys)) .ok else |err| editStatus(err),
};
}
pub export fn fig_editor_move_item(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
from: usize,
to: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.moveItem(path, from, to)) .ok else |err| editStatus(err),
};
}
pub export fn fig_editor_reorder_items(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
indices_ptr: ?[*]const usize,
indices_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const indices = decodeIndices(indices_ptr, indices_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.reorderItems(path, indices)) .ok else |err| editStatus(err),
};
}
pub export fn fig_editor_set_sequence(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
items_ptr: ?[*]const FigStr,
items_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var path_buf: [max_path_len]AST.PathSegment = undefined;
var items_buf: [max_keys_len][]const u8 = undefined;
const path = decodePath(path_ptr, path_len, &path_buf) orelse return .invalid_argument;
const items = decodeKeys(items_ptr, items_len, &items_buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (e.setSequence(path, items)) .ok else |err| editStatus(err),
};
}
// ============
// EDITOR — WHOLE-CONTAINER OPS
// ============
//
// The six ops for containers that are SCATTERED through the source: a TOML
// `[header]` table (whose body is the lines after it, extended by every
// `[a.b]` header elsewhere in the file), an INI `[section]`, a fig block
// container. Such a container owns no single range to splice, so the key ops
// above cannot address it — `fig_editor_delete_key` and
// `fig_editor_replace_val` at a table path return `invalid_argument` (the
// editor's pre-op guards), and these are where that request goes instead.
//
// Only the section formats have them (`Editor.hasContainerOp`): TOML all six,
// INI and fig the four that need no header syntax (delete, move, reorder and
// rename, over the header lines and name mentions their parsers record; the
// two that write a `[header]` line need a `section_header`), and everything else none
// — YAML, JSON and the rest nest their containers in one contiguous region, so
// the key ops already handle them and there is nothing to derive. A format
// that lacks the op answers `unsupported_format`, the same answer
// `fig_editor_create` gives for a format with no editor at all.
//
// Paths are the usual `FigPathSegment` array; `body_text` is verbatim entry
// lines for the target format (`ip = "10.0.0.1"\n`), spliced and reparsed like
// every other edit, so a body that doesn't parse rolls back.
/// Delete the whole container at `path` — every scattered region of its
/// subtree, leaving interleaved foreign content in place.
pub export fn fig_editor_delete_container(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (comptime @TypeOf(e.*).hasContainerOp("deleteContainer"))
if (!e.dialectHasContainerOp("deleteContainer")) .unsupported_format else (if (e.deleteContainer(path)) .ok else |err| editStatus(err))
else
.unsupported_format,
};
}
/// Create a new container at `path` with `body_text` as its entries, spliced
/// past the parent's whole subtree so no existing key is reparented.
pub export fn fig_editor_insert_container(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
body_ptr: ?[*]const u8,
body_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const body = sliceOf(body_ptr, body_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (comptime @TypeOf(e.*).hasContainerOp("insertContainer"))
if (!e.dialectHasContainerOp("insertContainer")) .unsupported_format else (if (e.insertContainer(path, body)) .ok else |err| editStatus(err))
else
.unsupported_format,
};
}
/// Rename the container at `path` to `new_leaf`, rewriting every line that
/// names it — `[a]`, `[a.b]`, `[[a.c]]` all follow an `a` → `q` rename.
///
/// This is what `fig_editor_replace_key` at a block-table path routes to
/// internally; called directly it also reaches a container the key op cannot
/// address.
pub export fn fig_editor_rename_container(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
leaf_ptr: ?[*]const u8,
leaf_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const leaf = sliceOf(leaf_ptr, leaf_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (comptime @TypeOf(e.*).hasContainerOp("renameContainer"))
if (!e.dialectHasContainerOp("renameContainer")) .unsupported_format else (if (e.renameContainer(path, leaf)) .ok else |err| editStatus(err))
else
.unsupported_format,
};
}
/// Move the container at `src_path` to sit before the one at `dest_path`,
/// re-emitting its scattered fragments contiguously; interleaved foreign
/// containers stay put.
///
/// A NULL `dest_ptr` means "to the end of the document" — distinct from a
/// zero-length destination path, which every other export reads as the root.
pub export fn fig_editor_move_container(
ed: ?*FigEditor,
src_ptr: ?[*]const FigPathSegment,
src_len: usize,
dest_ptr: ?[*]const FigPathSegment,
dest_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var src_buf: [max_path_len]AST.PathSegment = undefined;
var dest_buf: [max_path_len]AST.PathSegment = undefined;
const src = decodePath(src_ptr, src_len, &src_buf) orelse return .invalid_argument;
const dest: ?[]AST.PathSegment = if (dest_ptr == null)
null
else
(decodePath(dest_ptr, dest_len, &dest_buf) orelse return .invalid_argument);
return switch (handle.inner) {
inline else => |*e| if (comptime @TypeOf(e.*).hasContainerOp("moveContainer"))
if (!e.dialectHasContainerOp("moveContainer")) .unsupported_format else (if (e.moveContainer(src, dest)) .ok else |err| editStatus(err))
else
.unsupported_format,
};
}
/// Reorder top-level containers among themselves so the ones named in `order`
/// come first, in that order; each is re-emitted contiguously at the position
/// the earliest of them currently occupies. Containers not named keep their
/// places.
pub export fn fig_editor_reorder_containers(
ed: ?*FigEditor,
order_ptr: ?[*]const FigStr,
order_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_keys_len][]const u8 = undefined;
const order = decodeKeys(order_ptr, order_len, &buf) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (comptime @TypeOf(e.*).hasContainerOp("reorderContainers"))
if (!e.dialectHasContainerOp("reorderContainers")) .unsupported_format else (if (e.reorderContainers(order)) .ok else |err| editStatus(err))
else
.unsupported_format,
};
}
/// Append a new element with body `body_text` to the container sequence at
/// `path` — TOML's `[[header]]` array-of-tables append — past every line of the
/// current last element's subtree.
pub export fn fig_editor_append_container_to_seq(
ed: ?*FigEditor,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
body_ptr: ?[*]const u8,
body_len: usize,
) FigStatus {
const handle = editorFrom(ed) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const body = sliceOf(body_ptr, body_len) orelse return .invalid_argument;
return switch (handle.inner) {
inline else => |*e| if (comptime @TypeOf(e.*).hasContainerOp("appendContainerToSeq"))
if (!e.dialectHasContainerOp("appendContainerToSeq")) .unsupported_format else (if (e.appendContainerToSeq(path, body)) .ok else |err| editStatus(err))
else
.unsupported_format,
};
}
/// Borrow the editor's current source bytes. Valid until the next mutation or
/// `fig_editor_destroy`.
pub export fn fig_editor_source(
ed: ?*const FigEditor,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
const p = out_ptr orelse return .invalid_argument;
const l = out_len orelse return .invalid_argument;
const handle: *const EditorHandle = @ptrCast(@alignCast(ed orelse return .invalid_argument));
switch (handle.inner) {
inline else => |*e| {
p.* = e.source.items.ptr;
l.* = e.source.items.len;
},
}
return .ok;
}
fn sliceOf(ptr: ?[*]const u8, len: usize) ?[]const u8 {
if (len == 0) return &.{};
const p = ptr orelse return null;
return p[0..len];
}
// ============
// EMBED (LOW-LEVEL)
// ============
pub const FigSpan = extern struct { start: usize, end: usize };
/// Caller-allocated, size-versioned like `FigError`: the caller sets `size` to
/// `@sizeOf(FigRegion)` and the library writes only the fields it covers, so
/// trailing fields may be appended later without breaking an older layout.
pub const FigRegion = extern struct {
size: u32,
open_fence: FigSpan,
content: FigSpan,
close_fence: FigSpan,
/// The host body outside the fences (suffix for frontmatter, prefix for
/// endmatter) — the read-side twin of `content`. One-sided: for a
/// mid-document block (an HTML `<script>` island) it names only the text
/// AFTER the block. Use `body_before`/`body_after` to see all of the host.
body: FigSpan,
/// `[0, open_fence.start)` — the host text before the block, BOM included.
/// Added in core 2.7.0: a caller whose `size` predates it is unwritten.
body_before: FigSpan,
/// `[close_fence.end, input_len)` — the host text after the block. With
/// `body_before` and the three region spans this tiles the input exactly,
/// so a caller can rebuild the file without losing a byte.
/// Added in core 2.7.0: a caller whose `size` predates it is unwritten.
body_after: FigSpan,
};
/// Whether the caller-reported `FigRegion.size` covers `field` (same rule as
/// `errCovers`). A field past `size` is absent in the caller's layout and must
/// not be written.
fn regionCovers(size: u32, comptime field: []const u8) bool {
const end = @offsetOf(FigRegion, field) + @sizeOf(@FieldType(FigRegion, field));
return size >= end;
}
/// The container half of an embed selector — the C mirror of `Embed.Type`'s
/// tags. The four parametric families carry their content's format in a
/// second `FigFormat` argument beside this one; the three presets pin their
/// own format (`;;;` is JSON, `+++` is TOML, ```` ```endmatter ```` is YAML)
pub const FigEmbedContainer = enum(c_int) {
md_frontmatter = 0,
fenced = 1,
html_script = 2,
html_code = 3,
semicolons_json = 4,
plus_toml = 5,
endmatter_yaml = 6,
};
fn embedInnerOf(format: c_int) ?Embed.InnerFormat {
const f = std.enums.fromInt(FigFormat, format) orelse return null;
return switch (f) {
inline else => |tag| if (@hasField(Embed.InnerFormat, @tagName(tag)))
@field(Embed.InnerFormat, @tagName(tag))
else
null,
};
}
fn embedTypeOf(container: c_int, format: c_int) ?Embed.Type {
const c = std.enums.fromInt(FigEmbedContainer, container) orelse return null;
return switch (c) {
.semicolons_json => .semicolons_json,
.plus_toml => .plus_toml,
.endmatter_yaml => .endmatter_yaml,
.md_frontmatter => .{ .frontmatter = embedInnerOf(format) orelse return null },
.fenced => .{ .fenced = embedInnerOf(format) orelse return null },
.html_script => .{ .html_script = embedInnerOf(format) orelse return null },
.html_code => .{ .html_code = embedInnerOf(format) orelse return null },
};
}
const FigEmbedPair = struct { container: FigEmbedContainer, format: FigFormat };
fn figEmbedPairOf(t: Embed.Type) FigEmbedPair {
const container: FigEmbedContainer = switch (t) {
.frontmatter => .md_frontmatter,
.fenced => .fenced,
.html_script => .html_script,
.html_code => .html_code,
.semicolons_json => .semicolons_json,
.plus_toml => .plus_toml,
.endmatter_yaml => .endmatter_yaml,
};
const format: FigFormat = switch (Embed.innerFormat(t)) {
inline else => |f| @field(FigFormat, @tagName(f)),
};
return .{ .container = container, .format = format };
}
fn toFigSpan(s: Span) FigSpan {
return .{ .start = s.start, .end = s.end };
}
pub export fn fig_embed_extract(
input_ptr: ?[*]const u8,
input_len: usize,
container: c_int,
format: c_int,
out_region: ?*FigRegion,
) FigStatus {
const out = out_region orelse return .invalid_argument;
const input = sliceOf(input_ptr, input_len) orelse return .invalid_argument;
const t = embedTypeOf(container, format) orelse return .invalid_argument;
const region = Embed.locateRegion(input, t) catch |err| return switch (err) {
error.NotFound => .not_found,
else => .parse_error,
};
const size = out.size;
if (regionCovers(size, "open_fence")) out.open_fence = toFigSpan(region.open_fence);
if (regionCovers(size, "content")) out.content = toFigSpan(region.content);
if (regionCovers(size, "close_fence")) out.close_fence = toFigSpan(region.close_fence);
if (regionCovers(size, "body")) out.body = toFigSpan(region.body);
if (regionCovers(size, "body_before")) out.body_before = toFigSpan(region.body_before);
if (regionCovers(size, "body_after")) out.body_after = toFigSpan(region.body_after);
return .ok;
}
pub export fn fig_embed_detect(
input_ptr: ?[*]const u8,
input_len: usize,
out_container: ?*c_int,
out_format: ?*c_int,
) FigStatus {
const out_c = out_container orelse return .invalid_argument;
const out_f = out_format orelse return .invalid_argument;
const input = sliceOf(input_ptr, input_len) orelse return .invalid_argument;
const t = Embed.detect(input) orelse return .not_found;
const pair = figEmbedPairOf(t);
out_c.* = @intFromEnum(pair.container);
out_f.* = @intFromEnum(pair.format);
return .ok;
}
pub export fn fig_embed_retype(
input_ptr: ?[*]const u8,
input_len: usize,
from_container: c_int,
from_format: c_int,
to_container: c_int,
to_format: c_int,
content_ptr: ?[*]const u8,
content_len: usize,
out_ptr: ?*[*]u8,
out_len: ?*usize,
) FigStatus {
const op = out_ptr orelse return .invalid_argument;
const ol = out_len orelse return .invalid_argument;
const input = sliceOf(input_ptr, input_len) orelse return .invalid_argument;
const content = sliceOf(content_ptr, content_len) orelse return .invalid_argument;
const from = embedTypeOf(from_container, from_format) orelse return .invalid_argument;
const to = embedTypeOf(to_container, to_format) orelse return .invalid_argument;
const region = Embed.locateRegion(input, from) catch |err| return switch (err) {
error.NotFound => .not_found,
error.Unterminated => .parse_error,
};
const out = Embed.retype(activeAllocator(), input, region, from, to, content) catch |err| return switch (err) {
error.MidDocumentRegionCannotMove => .unsupported_operation,
error.OutOfMemory => .out_of_memory,
};
op.* = out.ptr;
ol.* = out.len;
return .ok;
}
pub const FigEmbed = opaque {};
const EmbedHandle = struct {
allocator: std.mem.Allocator,
host: []u8,
region: Embed.Region,
body_before: bool,
one_body: bool,
body_override: ?[]u8 = null,
editor: EditorUnion,
codec: Embed.Codec = .identity,
decoded: Embed.Decoded = .{ .text = "" },
rendered: std.ArrayList(u8) = .empty,
scratch: std.ArrayList(u8) = .empty,
fn deinit(self: *EmbedHandle) void {
switch (self.editor) {
inline else => |*e| e.deinit(),
}
self.decoded.deinit(self.allocator);
if (self.body_override) |b| self.allocator.free(b);
self.rendered.deinit(self.allocator);
self.scratch.deinit(self.allocator);
self.allocator.free(self.host);
}
};
fn embedFrom(em: ?*FigEmbed) ?*EmbedHandle {
const p = em orelse return null;
return @ptrCast(@alignCast(p));
}
fn embedInnerSupported(t: Embed.Type) bool {
return switch (Embed.innerFormat(t)) {
inline else => |f| blk: {
const d = comptime Languages.entryFor(@tagName(f));
if (comptime d.Lang != void) break :blk true;
const e = runtimeOf(d.abi_value) orelse break :blk false;
break :blk e.language.caps.edit;
},
};
}
fn embedHandleFromHost(
allocator: std.mem.Allocator,
host: []u8,
region: Embed.Region,
t: Embed.Type,
out: *?*FigEmbed,
) FigStatus {
const handle = allocator.create(EmbedHandle) catch {
allocator.free(host);
return .out_of_memory;
};
handle.* = .{
.allocator = allocator,
.host = host,
.region = region,
.body_before = Embed.bodyIsBefore(t),
.one_body = Embed.hasOneBody(t),
.editor = switch (Embed.innerFormat(t)) {
inline else => |f| blk: {
const d = comptime Languages.entryFor(@tagName(f));
if (comptime d.Lang == void) {
const e = runtimeOf(d.abi_value).?;
break :blk @unionInit(EditorUnion, "runtime", .{ .allocator = allocator, .format = e.typeOf() });
}
break :blk @unionInit(EditorUnion, d.Lang.name, .{ .allocator = allocator, .format = d.dialect });
},
},
};
const content = host[region.content.start..region.content.end];
handle.codec = Embed.codecOf(t);
handle.decoded = Embed.decodeForParse(allocator, content, handle.codec) catch {
switch (handle.editor) {
inline else => |*e| e.deinit(),
}
allocator.free(host);
allocator.destroy(handle);
return .out_of_memory;
};
switch (handle.editor) {
inline else => |*e| e.init(handle.decoded.text) catch |err| {
e.deinit();
handle.decoded.deinit(allocator);
allocator.free(host);
allocator.destroy(handle);
return editStatus(err);
},
}
out.* = @ptrCast(handle);
return .ok;
}
pub export fn fig_embed_open(
input_ptr: ?[*]const u8,
input_len: usize,
container: c_int,
format: c_int,
out_embed: ?*?*FigEmbed,
) FigStatus {
const out = out_embed orelse return .invalid_argument;
out.* = null;
const input = sliceOf(input_ptr, input_len) orelse return .invalid_argument;
const t = embedTypeOf(container, format) orelse return .invalid_argument;
if (!embedInnerSupported(t)) return .unsupported_format;
const region = Embed.locateRegion(input, t) catch |err| return switch (err) {
error.NotFound => .not_found,
else => .parse_error,
};
const allocator = activeAllocator();
const host = allocator.dupe(u8, input) catch return .out_of_memory;
return embedHandleFromHost(allocator, host, region, t, out);
}
pub export fn fig_embed_open_or_init(
input_ptr: ?[*]const u8,
input_len: usize,
container: c_int,
format: c_int,
out_embed: ?*?*FigEmbed,
) FigStatus {
const out = out_embed orelse return .invalid_argument;
out.* = null;
const input = sliceOf(input_ptr, input_len) orelse return .invalid_argument;
const t = embedTypeOf(container, format) orelse return .invalid_argument;
if (!embedInnerSupported(t)) return .unsupported_format;
const allocator = activeAllocator();
if (Embed.locateRegion(input, t)) |region| {
const host = allocator.dupe(u8, input) catch return .out_of_memory;
return embedHandleFromHost(allocator, host, region, t, out);
} else |err| switch (err) {
error.NotFound => {
const created = Embed.initRegion(allocator, input, t) catch |init_err| return switch (init_err) {
error.FrontmatterExists => .unsupported_operation,
error.OutOfMemory => .out_of_memory,
};
return embedHandleFromHost(allocator, created.host, created.region, t, out);
},
else => return .parse_error,
}
}
pub export fn fig_embed_destroy(em: ?*FigEmbed) void {
const handle = embedFrom(em) orelse return;
const allocator = handle.allocator;
handle.deinit();
allocator.destroy(handle);
}
pub export fn fig_embed_replace_val(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
repl_ptr: ?[*]const u8,
repl_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const repl = sliceOf(repl_ptr, repl_len) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.replaceValAtPath(path, repl)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_replace_key(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
repl_ptr: ?[*]const u8,
repl_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const repl = sliceOf(repl_ptr, repl_len) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.replaceKeyAtPath(path, repl)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_replace_named_key(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
name_ptr: ?[*]const u8,
name_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const name = sliceOf(name_ptr, name_len) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.replaceNamedKey(path, name)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_set(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
val_ptr: ?[*]const u8,
val_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const val = sliceOf(val_ptr, val_len) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.set(path, val)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_add_leading_comment(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
text_ptr: ?[*]const u8,
text_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const text = sliceOf(text_ptr, text_len) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.addLeadingComment(path, text)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_set_trailing_comment(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
text_ptr: ?[*]const u8,
text_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const text = sliceOf(text_ptr, text_len) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.setTrailingComment(path, text)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_delete_leading_comments(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.deleteLeadingComments(path)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_delete_trailing_comment(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.deleteTrailingComment(path)) .ok else |err| editStatus(err),
};
}
fn embedGetComment(
handle: *EmbedHandle,
path: []const AST.PathSegment,
trailing: bool,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
const p = out_ptr orelse return .invalid_argument;
const l = out_len orelse return .invalid_argument;
const maybe = (switch (handle.editor) {
inline else => |*e| if (trailing) e.getTrailingComment(path) else e.getLeadingComment(path),
}) catch |err| return editStatus(err);
return fillCommentOut(handle.allocator, &handle.scratch, maybe, p, l);
}
pub export fn fig_embed_get_leading_comment(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return embedGetComment(handle, path, false, out_ptr, out_len);
}
pub export fn fig_embed_get_trailing_comment(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return embedGetComment(handle, path, true, out_ptr, out_len);
}
pub export fn fig_embed_add_dangling_comment(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
text_ptr: ?[*]const u8,
text_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const text = sliceOf(text_ptr, text_len) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.addDanglingComment(path, text)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_delete_dangling_comments(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.deleteDanglingComments(path)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_get_dangling_comment(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const p = out_ptr orelse return .invalid_argument;
const l = out_len orelse return .invalid_argument;
const maybe = (switch (handle.editor) {
inline else => |*e| e.getDanglingComment(path),
}) catch |err| return editStatus(err);
return fillCommentOut(handle.allocator, &handle.scratch, maybe, p, l);
}
pub export fn fig_embed_comment_out(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.commentOut(path)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_uncomment_leading(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
first_line: usize,
line_count: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.uncommentLeading(path, first_line, line_count)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_uncomment_dangling(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
first_line: usize,
line_count: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.uncommentDangling(path, first_line, line_count)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_insert_named_key(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
name_ptr: ?[*]const u8,
name_len: usize,
val_ptr: ?[*]const u8,
val_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const name = sliceOf(name_ptr, name_len) orelse return .invalid_argument;
const val = sliceOf(val_ptr, val_len) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.insertNamedKey(path, name, val)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_insert_key(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
key_ptr: ?[*]const u8,
key_len: usize,
val_ptr: ?[*]const u8,
val_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const key = sliceOf(key_ptr, key_len) orelse return .invalid_argument;
const val = sliceOf(val_ptr, val_len) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.insertKey(path, key, val)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_delete_key(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.deleteKey(path)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_append_seq(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
val_ptr: ?[*]const u8,
val_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const val = sliceOf(val_ptr, val_len) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.appendToSeq(path, val)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_prepend_seq(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
val_ptr: ?[*]const u8,
val_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const val = sliceOf(val_ptr, val_len) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.prependToSeq(path, val)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_remove_seq_item(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
index: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.removeSeqItem(path, index)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_move_key(
em: ?*FigEmbed,
src_ptr: ?[*]const FigPathSegment,
src_len: usize,
dest_ptr: ?[*]const FigPathSegment,
dest_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var src_buf: [max_path_len]AST.PathSegment = undefined;
var dest_buf: [max_path_len]AST.PathSegment = undefined;
const src = decodePath(src_ptr, src_len, &src_buf) orelse return .invalid_argument;
const dest = decodePath(dest_ptr, dest_len, &dest_buf) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.moveKey(src, dest)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_reorder_keys(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
keys_ptr: ?[*]const FigStr,
keys_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var path_buf: [max_path_len]AST.PathSegment = undefined;
var keys_buf: [max_keys_len][]const u8 = undefined;
const path = decodePath(path_ptr, path_len, &path_buf) orelse return .invalid_argument;
const keys = decodeKeys(keys_ptr, keys_len, &keys_buf) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.reorderKeys(path, keys)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_move_item(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
from: usize,
to: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.moveItem(path, from, to)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_reorder_items(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
indices_ptr: ?[*]const usize,
indices_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var buf: [max_path_len]AST.PathSegment = undefined;
const path = decodePath(path_ptr, path_len, &buf) orelse return .invalid_argument;
const indices = decodeIndices(indices_ptr, indices_len) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.reorderItems(path, indices)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_set_sequence(
em: ?*FigEmbed,
path_ptr: ?[*]const FigPathSegment,
path_len: usize,
items_ptr: ?[*]const FigStr,
items_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
var path_buf: [max_path_len]AST.PathSegment = undefined;
var items_buf: [max_keys_len][]const u8 = undefined;
const path = decodePath(path_ptr, path_len, &path_buf) orelse return .invalid_argument;
const items = decodeKeys(items_ptr, items_len, &items_buf) orelse return .invalid_argument;
return switch (handle.editor) {
inline else => |*e| if (e.setSequence(path, items)) .ok else |err| editStatus(err),
};
}
pub export fn fig_embed_render(
em: ?*FigEmbed,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
const p = out_ptr orelse return .invalid_argument;
const l = out_len orelse return .invalid_argument;
const handle = embedFrom(em) orelse return .invalid_argument;
const edited = switch (handle.editor) {
inline else => |*e| e.source.items,
};
handle.rendered.clearRetainingCapacity();
const host = handle.host;
const region = handle.region;
var content_owned: ?[]u8 = null;
defer if (content_owned) |c| handle.allocator.free(c);
const src: []const u8 = if (handle.codec == .identity) edited else blk: {
const orig = host[region.content.start..region.content.end];
const enc = Embed.reencodeEdited(handle.allocator, handle.codec, orig, handle.decoded, edited) catch return .out_of_memory;
content_owned = enc;
break :blk enc;
};
const append = struct {
fn f(h: *EmbedHandle, bytes: []const u8) FigStatus {
h.rendered.appendSlice(h.allocator, bytes) catch return .out_of_memory;
return .ok;
}
}.f;
if (handle.body_before) {
if (handle.body_override) |b| {
if (append(handle, b) != .ok) return .out_of_memory;
if (append(handle, host[region.open_fence.start..region.content.start]) != .ok) return .out_of_memory;
} else {
if (append(handle, host[0..region.content.start]) != .ok) return .out_of_memory;
}
if (append(handle, src) != .ok) return .out_of_memory;
if (append(handle, host[region.content.end..]) != .ok) return .out_of_memory;
} else {
if (append(handle, host[0..region.content.start]) != .ok) return .out_of_memory;
if (append(handle, src) != .ok) return .out_of_memory;
if (handle.body_override) |b| {
if (append(handle, host[region.content.end..region.close_fence.end]) != .ok) return .out_of_memory;
if (append(handle, b) != .ok) return .out_of_memory;
} else {
if (append(handle, host[region.content.end..]) != .ok) return .out_of_memory;
}
}
p.* = handle.rendered.items.ptr;
l.* = handle.rendered.items.len;
return .ok;
}
pub export fn fig_embed_replace_body(
em: ?*FigEmbed,
body_ptr: ?[*]const u8,
body_len: usize,
) FigStatus {
const handle = embedFrom(em) orelse return .invalid_argument;
if (!handle.one_body) return .unsupported_operation;
const body = if (body_len == 0) "" else (body_ptr orelse return .invalid_argument)[0..body_len];
const owned = handle.allocator.dupe(u8, body) catch return .out_of_memory;
if (handle.body_override) |old| handle.allocator.free(old);
handle.body_override = owned;
return .ok;
}
pub const FigValue = opaque {};
pub const FigKeyValue = extern struct {
key: FigNodeId,
value: FigNodeId,
};
pub const FigExtKind = enum(c_int) {
offset_datetime = 0,
local_datetime = 1,
local_date = 2,
local_time = 3,
enum_literal = 4,
char_literal = 5,
number_special = 6,
plist_date = 7,
plist_data = 8,
};
const ValueHandle = struct {
allocator: std.mem.Allocator,
builder: AST.Builder,
rendered: std.Io.Writer.Allocating,
diag_arena: std.heap.ArenaAllocator,
diag_warnings: []const Diagnostics.Warning = &.{},
};
fn valueFrom(value: ?*FigValue) ?*ValueHandle {
const p = value orelse return null;
return @ptrCast(@alignCast(p));
}
fn extKindOf(kind: c_int) ?AST.Node.Kind.Extended.ExtKind {
return switch (kind) {
@intFromEnum(FigExtKind.offset_datetime) => .offset_datetime,
@intFromEnum(FigExtKind.local_datetime) => .local_datetime,
@intFromEnum(FigExtKind.local_date) => .local_date,
@intFromEnum(FigExtKind.local_time) => .local_time,
@intFromEnum(FigExtKind.enum_literal) => .enum_literal,
@intFromEnum(FigExtKind.char_literal) => .char_literal,
@intFromEnum(FigExtKind.number_special) => .number_special,
@intFromEnum(FigExtKind.plist_date) => .plist_date,
@intFromEnum(FigExtKind.plist_data) => .plist_data,
else => null,
};
}
const Target = Runtime.Target;
fn targetOf(format: c_int) ?Target {
if (runtimeOf(format)) |e| {
if (!e.language.caps.serialize) return null;
return .{ .runtime = e };
}
return .{ .compiled = serializeFormatOf(format) orelse return null };
}
fn serializeFormatOf(format: c_int) ?AST.SerializeFormat {
@setEvalBranchQuota(30_000);
const f = std.enums.fromInt(FigFormat, format) orelse return null;
return switch (f) {
inline else => |tag| {
const d = comptime Languages.entryFor(@tagName(tag));
if (comptime d.Lang == void) return null;
return @field(AST.SerializeFormat, d.name);
},
};
}
fn serializeStatus(err: AST.SerializeError) FigStatus {
return switch (err) {
error.UnresolvedAlias,
error.NullUnsupported,
error.NonStringKey,
error.FormatDisabled,
error.NestingTooDeep,
error.UnexpectedNodeKind,
error.FigUnrepresentableRoot,
error.UnsupportedValue,
error.InvalidKey,
=> .unsupported_format,
error.WriteFailed => .out_of_memory,
};
}
fn runtimePrintStatus(err: anyerror) FigStatus {
return switch (err) {
error.OutOfMemory, error.WriteFailed => .out_of_memory,
else => .unsupported_format,
};
}
fn emitNode(out_id: ?*FigNodeId, result: std.mem.Allocator.Error!AST.Node.Id) FigStatus {
const out = out_id orelse return .invalid_argument;
const id = result catch return .out_of_memory;
out.* = id;
return .ok;
}
pub export fn fig_value_create(out_value: ?*?*FigValue) FigStatus {
const out = out_value orelse return .invalid_argument;
out.* = null;
const allocator = activeAllocator();
const handle = allocator.create(ValueHandle) catch return .out_of_memory;
handle.* = .{
.allocator = allocator,
.builder = AST.Builder.init(allocator),
.rendered = std.Io.Writer.Allocating.init(allocator),
.diag_arena = std.heap.ArenaAllocator.init(allocator),
};
out.* = @ptrCast(handle);
return .ok;
}
pub export fn fig_value_destroy(value: ?*FigValue) void {
const handle = valueFrom(value) orelse return;
const allocator = handle.allocator;
handle.builder.deinit();
handle.rendered.deinit();
handle.diag_arena.deinit();
allocator.destroy(handle);
}
pub export fn fig_value_null(value: ?*FigValue, out_id: ?*FigNodeId) FigStatus {
const handle = valueFrom(value) orelse return .invalid_argument;
return emitNode(out_id, handle.builder.addNull());
}
pub export fn fig_value_bool(value: ?*FigValue, b: bool, out_id: ?*FigNodeId) FigStatus {
const handle = valueFrom(value) orelse return .invalid_argument;
return emitNode(out_id, handle.builder.addBool(b));
}
pub export fn fig_value_int(value: ?*FigValue, n: i64, out_id: ?*FigNodeId) FigStatus {
const handle = valueFrom(value) orelse return .invalid_argument;
return emitNode(out_id, handle.builder.addInt(n));
}
pub export fn fig_value_uint(value: ?*FigValue, n: u64, out_id: ?*FigNodeId) FigStatus {
const handle = valueFrom(value) orelse return .invalid_argument;
return emitNode(out_id, handle.builder.addUint(n));
}
pub export fn fig_value_number(
value: ?*FigValue,
raw_ptr: ?[*]const u8,
raw_len: usize,
is_float: bool,
out_id: ?*FigNodeId,
) FigStatus {
const handle = valueFrom(value) orelse return .invalid_argument;
const raw = sliceOf(raw_ptr, raw_len) orelse return .invalid_argument;
return emitNode(out_id, handle.builder.addNumberRaw(raw, is_float));
}
pub export fn fig_value_string(
value: ?*FigValue,
ptr: ?[*]const u8,
len: usize,
out_id: ?*FigNodeId,
) FigStatus {
const handle = valueFrom(value) orelse return .invalid_argument;
const s = sliceOf(ptr, len) orelse return .invalid_argument;
return emitNode(out_id, handle.builder.addString(s));
}
pub export fn fig_value_extended(
value: ?*FigValue,
kind: c_int,
text_ptr: ?[*]const u8,
text_len: usize,
out_id: ?*FigNodeId,
) FigStatus {
const handle = valueFrom(value) orelse return .invalid_argument;
const ext_kind = extKindOf(kind) orelse return .invalid_argument;
const text = sliceOf(text_ptr, text_len) orelse return .invalid_argument;
return emitNode(out_id, handle.builder.addExtended(ext_kind, text));
}
pub export fn fig_value_seq(
value: ?*FigValue,
items_ptr: ?[*]const FigNodeId,
items_len: usize,
out_id: ?*FigNodeId,
) FigStatus {
const handle = valueFrom(value) orelse return .invalid_argument;
const items: []const FigNodeId = if (items_len == 0) &.{} else (items_ptr orelse return .invalid_argument)[0..items_len];
const count = handle.builder.nodes.items.len;
for (items) |id| if (id >= count) return .invalid_argument;
return emitNode(out_id, handle.builder.addSequence(items));
}
pub export fn fig_value_map(
value: ?*FigValue,
entries_ptr: ?[*]const FigKeyValue,
entries_len: usize,
out_id: ?*FigNodeId,
) FigStatus {
const handle = valueFrom(value) orelse return .invalid_argument;
if (entries_len == 0) return emitNode(out_id, handle.builder.addMapping(&.{}));
const c_entries = (entries_ptr orelse return .invalid_argument)[0..entries_len];
const count = handle.builder.nodes.items.len;
const entries = handle.allocator.alloc(AST.Builder.Entry, entries_len) catch return .out_of_memory;
defer handle.allocator.free(entries);
for (c_entries, entries) |c, *e| {
if (c.key >= count or c.value >= count) return .invalid_argument;
e.* = .{ .key = c.key, .value = c.value };
}
return emitNode(out_id, handle.builder.addMapping(entries));
}
pub const FigSerializeOptions = extern struct {
size: u32 = @sizeOf(FigSerializeOptions),
pretty: u8 = 1,
indent: u8 = 2,
strip_comments: u8 = 0,
lossless: u8 = 0,
width: u16 = 80,
flow: u8 = 0,
splice: u8 = 0,
};
fn optionCovers(size: u32, comptime field: []const u8) bool {
const end = @offsetOf(FigSerializeOptions, field) + @sizeOf(@FieldType(FigSerializeOptions, field));
return size >= end;
}
fn serializeOptionsOf(options: ?*const FigSerializeOptions) AST.SerializeOptions {
const o = options orelse return .{};
var out: AST.SerializeOptions = .{};
if (optionCovers(o.size, "pretty")) out.pretty = o.pretty != 0;
if (optionCovers(o.size, "indent")) out.indent = if (o.indent == 0) 2 else o.indent;
if (optionCovers(o.size, "strip_comments")) out.strip_comments = o.strip_comments != 0;
if (optionCovers(o.size, "width")) out.width = if (o.width == 0) 80 else o.width;
if (optionCovers(o.size, "flow")) out.flow = o.flow != 0;
if (optionCovers(o.size, "splice")) out.splice = o.splice != 0;
return out;
}
fn losslessRequested(options: ?*const FigSerializeOptions) bool {
const o = options orelse return false;
return optionCovers(o.size, "lossless") and o.lossless != 0;
}
pub export fn fig_value_serialize(
value: ?*FigValue,
root: FigNodeId,
format: c_int,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
return fig_value_serialize_opts(value, root, format, null, out_ptr, out_len);
}
pub export fn fig_value_serialize_opts(
value: ?*FigValue,
root: FigNodeId,
format: c_int,
options: ?*const FigSerializeOptions,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
const p = out_ptr orelse return .invalid_argument;
const l = out_len orelse return .invalid_argument;
const handle = valueFrom(value) orelse return .invalid_argument;
const target = targetOf(format) orelse return .unsupported_format;
if (root >= handle.builder.nodes.items.len) return .invalid_argument;
handle.rendered.clearRetainingCapacity();
const ast = handle.builder.view(root) catch return .out_of_memory; switch (target) {
.compiled => |fmt| ast.serializeFragmentWith(&handle.rendered.writer, fmt, serializeOptionsOf(options)) catch |err| return serializeStatus(err),
.runtime => |e| Runtime.printNodeWith(e, &handle.rendered.writer, &ast, root, serializeOptionsOf(options)) catch |err| return runtimePrintStatus(err),
}
const bytes = handle.rendered.written();
p.* = bytes.ptr;
l.* = bytes.len;
return .ok;
}
fn convertStatus(err: anyerror) FigStatus {
return switch (err) {
error.OutOfMemory => .out_of_memory,
else => .unsupported_format,
};
}
pub export fn fig_document_serialize(
doc: ?*FigDocument,
format: c_int,
options: ?*const FigSerializeOptions,
out_ptr: ?*[*c]const u8,
out_len: ?*usize,
) FigStatus {
const p = out_ptr orelse return .invalid_argument;
const l = out_len orelse return .invalid_argument;
const public_doc = doc orelse return .invalid_argument;
const handle: *DocumentHandle = @ptrCast(@alignCast(public_doc));
const target = targetOf(format) orelse return .unsupported_format;
const opts = serializeOptionsOf(options);
var arena_state = std.heap.ArenaAllocator.init(handle.allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const ast = prepareDocumentAst(handle, target, options, arena) catch |err| return convertStatus(err);
handle.rendered.clearRetainingCapacity();
switch (target) {
.compiled => |fmt| ast.serializeWith(&handle.rendered.writer, fmt, opts) catch |err| return serializeStatus(err),
.runtime => |e| Runtime.printWith(e, &handle.rendered.writer, ast, opts) catch |err| return runtimePrintStatus(err),
}
const bytes = handle.rendered.written();
p.* = bytes.ptr;
l.* = bytes.len;
return .ok;
}
fn sourceCarriesReferences(handle: *const DocumentHandle) bool {
if (handle.runtime) |e| return e.language.caps.references;
return switch (handle.format) {
inline else => |f| comptime blk: {
const d = Languages.entryFor(@tagName(f));
break :blk d.Lang != void and d.Lang.caps.references;
},
};
}
fn targetCarriesReferences(target: Target) bool {
return switch (target) {
.runtime => |e| e.language.caps.references,
.compiled => |fmt| switch (fmt) {
.canonical => false,
inline else => |f| comptime blk: {
const d = Languages.entryFor(@tagName(f));
break :blk d.Lang != void and d.Lang.caps.references;
},
},
};
}
fn prepareDocumentAst(handle: *DocumentHandle, target: Target, options: ?*const FigSerializeOptions, arena: std.mem.Allocator) !*const AST {
@setEvalBranchQuota(30_000);
const source_refs = sourceCarriesReferences(handle);
const ref_layer_round_trip = source_refs and targetCarriesReferences(target);
const base_ast: *const AST = if (source_refs and !ref_layer_round_trip) blk: {
const mat = try arena.create(AST);
mat.* = try Materialize.materialize(arena, &handle.document.ast, .strict);
break :blk mat;
} else &handle.document.ast;
if (losslessRequested(options) and !ref_layer_round_trip) {
const native: ?Lossless.NativeKinds = switch (target) {
.compiled => |fmt| Lossless.nativeFor(fmt),
.runtime => |e| e.language.caps.lossless,
};
const decoded = try arena.create(AST);
decoded.* = try Lossless.decode(arena, base_ast);
const n = native orelse return decoded;
const encoded = try arena.create(AST);
encoded.* = try Lossless.encode(arena, decoded, n);
return encoded;
}
return base_ast;
}
comptime {
for (Languages.dialects) |d| {
if (d.Lang == void) continue;
const declares = d.Lang.caps.references;
const expected = std.mem.eql(u8, d.name, "yaml");
if (declares and !expected)
@compileError("'" ++ d.name ++ "' now declares `caps.references`, so `prepareDocumentAst`" ++
" collapses its reference layer on the way out — correct, but new: add it to this" ++
" pin once that is what you meant");
if (!declares and expected)
@compileError("`yaml` no longer declares `caps.references`, so `fig_document_serialize` has" ++
" stopped collapsing its reference layer when leaving YAML");
}
}
pub const FigWarningCode = enum(c_int) {
comment_dropped = 0,
comment_style_degraded = 1,
value_dropped = 2,
type_degraded = 3,
};
pub const FigWarningCause = enum(c_int) {
format_limitation = 0,
explicit_option = 1,
};
pub const FigWarning = extern struct {
size: u32,
code: c_int,
cause: c_int,
path: [*c]const u8,
path_len: usize,
note: [*c]const u8,
note_len: usize,
};
fn warnCovers(size: u32, comptime field: []const u8) bool {
const end = @offsetOf(FigWarning, field) + @sizeOf(@FieldType(FigWarning, field));
return size >= end;
}
fn writeWarning(out: *FigWarning, w: Diagnostics.Warning) void {
const size = out.size;
if (warnCovers(size, "code")) out.code = warningCodeInt(w.code);
if (warnCovers(size, "cause")) out.cause = warningCauseInt(w.cause);
if (warnCovers(size, "path")) out.path = w.path.ptr;
if (warnCovers(size, "path_len")) out.path_len = w.path.len;
if (warnCovers(size, "note")) out.note = w.note.ptr;
if (warnCovers(size, "note_len")) out.note_len = w.note.len;
}
fn warningCodeInt(code: Diagnostics.Warning.Code) c_int {
return @intFromEnum(@as(FigWarningCode, switch (code) {
.comment_dropped => .comment_dropped,
.comment_style_degraded => .comment_style_degraded,
.value_dropped => .value_dropped,
.type_degraded => .type_degraded,
}));
}
fn warningCauseInt(cause: Diagnostics.Warning.Cause) c_int {
return @intFromEnum(@as(FigWarningCause, switch (cause) {
.format_limitation => .format_limitation,
.explicit_option => .explicit_option,
}));
}
fn diagnoseOptionsOf(options: ?*const FigSerializeOptions) Diagnostics.Options {
const so = serializeOptionsOf(options);
return .{
.pretty = so.pretty,
.strip_comments = so.strip_comments,
.lossless = losslessRequested(options),
};
}
pub export fn fig_document_diagnose(
doc: ?*FigDocument,
format: c_int,
options: ?*const FigSerializeOptions,
out_count: ?*usize,
) FigStatus {
const oc = out_count orelse return .invalid_argument;
oc.* = 0;
const public_doc = doc orelse return .invalid_argument;
const handle: *DocumentHandle = @ptrCast(@alignCast(public_doc));
const target = targetOf(format) orelse return .unsupported_format;
_ = handle.diag_arena.reset(.retain_capacity);
handle.diag_warnings = &.{}; const arena = handle.diag_arena.allocator();
const ast = prepareDocumentAst(handle, target, options, arena) catch |err| return convertStatus(err);
const warnings = Diagnostics.analyzeFor(arena, ast, ast.root, target, diagnoseOptionsOf(options)) catch return .out_of_memory;
handle.diag_warnings = warnings;
oc.* = warnings.len;
return .ok;
}
pub export fn fig_document_warning(doc: ?*FigDocument, index: usize, out: ?*FigWarning) FigStatus {
const o = out orelse return .invalid_argument;
const public_doc = doc orelse return .invalid_argument;
const handle: *DocumentHandle = @ptrCast(@alignCast(public_doc));
if (index >= handle.diag_warnings.len) return .invalid_argument;
writeWarning(o, handle.diag_warnings[index]);
return .ok;
}
pub export fn fig_value_diagnose(
value: ?*FigValue,
root: FigNodeId,
format: c_int,
options: ?*const FigSerializeOptions,
out_count: ?*usize,
) FigStatus {
const oc = out_count orelse return .invalid_argument;
oc.* = 0;
const handle = valueFrom(value) orelse return .invalid_argument;
const target = targetOf(format) orelse return .unsupported_format; if (root >= handle.builder.nodes.items.len) return .invalid_argument;
_ = handle.diag_arena.reset(.retain_capacity);
handle.diag_warnings = &.{};
const arena = handle.diag_arena.allocator();
const ast = handle.builder.view(root) catch return .out_of_memory; const warnings = Diagnostics.analyzeFor(arena, &ast, root, target, diagnoseOptionsOf(options)) catch return .out_of_memory;
handle.diag_warnings = warnings;
oc.* = warnings.len;
return .ok;
}
pub export fn fig_value_warning(value: ?*FigValue, index: usize, out: ?*FigWarning) FigStatus {
const o = out orelse return .invalid_argument;
const handle = valueFrom(value) orelse return .invalid_argument;
if (index >= handle.diag_warnings.len) return .invalid_argument;
writeWarning(o, handle.diag_warnings[index]);
return .ok;
}
test "traversal over a parsed mapping" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const src = "title: Hello\ncount: 42\ntags:\n- a\n- b\n";
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.yaml), &out_doc));
defer fig_document_destroy(out_doc);
const doc: ?*const FigDocument = out_doc;
const root = fig_document_root(doc);
try std.testing.expect(root != fig_node_none);
try std.testing.expectEqual(@as(c_int, @intFromEnum(FigNodeKind.mapping)), fig_node_kind(doc, root));
try std.testing.expectEqual(@as(usize, 3), fig_node_child_count(doc, root));
const first = fig_node_first_child(doc, root);
try std.testing.expectEqual(@as(c_int, @intFromEnum(FigNodeKind.keyvalue)), fig_node_kind(doc, first));
const key = fig_keyvalue_key(doc, first);
const val = fig_keyvalue_value(doc, first);
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expect(fig_node_string(doc, key, &ptr, &len));
try std.testing.expectEqualStrings("title", ptr[0..len]);
try std.testing.expect(fig_node_string(doc, val, &ptr, &len));
try std.testing.expectEqualStrings("Hello", ptr[0..len]);
const second = fig_node_next_sibling(doc, first);
const count_val = fig_keyvalue_value(doc, second);
try std.testing.expectEqual(@as(c_int, @intFromEnum(FigNodeKind.int)), fig_node_kind(doc, count_val));
try std.testing.expect(fig_node_number(doc, count_val, &ptr, &len));
try std.testing.expectEqualStrings("42", ptr[0..len]);
const third = fig_node_next_sibling(doc, second);
const tags_val = fig_keyvalue_value(doc, third);
try std.testing.expectEqual(@as(c_int, @intFromEnum(FigNodeKind.sequence)), fig_node_kind(doc, tags_val));
try std.testing.expectEqual(@as(usize, 2), fig_node_child_count(doc, tags_val));
}
test "fig_document_diagnose reports a dropped null for TOML" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
if (comptime !(build_options.lang_yaml and build_options.lang_toml)) return error.SkipZigTest;
const src = "a: null\nb: 1\n";
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.yaml), &out_doc));
defer fig_document_destroy(out_doc);
var count: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_document_diagnose(out_doc, @intFromEnum(FigFormat.toml), null, &count));
try std.testing.expectEqual(@as(usize, 1), count);
var w: FigWarning = undefined;
w.size = @sizeOf(FigWarning);
try std.testing.expectEqual(FigStatus.ok, fig_document_warning(out_doc, 0, &w));
try std.testing.expectEqual(@intFromEnum(FigWarningCode.value_dropped), w.code);
try std.testing.expectEqual(@intFromEnum(FigWarningCause.format_limitation), w.cause);
try std.testing.expectEqualStrings("a", w.path[0..w.path_len]);
try std.testing.expectEqual(FigStatus.invalid_argument, fig_document_warning(out_doc, 1, &w));
var opts: FigSerializeOptions = .{ .lossless = 1 };
try std.testing.expectEqual(FigStatus.ok, fig_document_diagnose(out_doc, @intFromEnum(FigFormat.toml), &opts, &count));
try std.testing.expectEqual(@as(usize, 0), count);
try std.testing.expectEqual(FigStatus.invalid_argument, fig_document_warning(out_doc, 0, &w));
try std.testing.expectEqual(FigStatus.ok, fig_document_diagnose(out_doc, @intFromEnum(FigFormat.json), null, &count));
try std.testing.expectEqual(@as(usize, 0), count);
}
test "fig_value_diagnose reports a degraded datetime" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
var v: ?*FigValue = null;
try std.testing.expectEqual(FigStatus.ok, fig_value_create(&v));
defer fig_value_destroy(v);
const ts = "1979-05-27T07:32:00Z";
var dt: FigNodeId = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_extended(v, @intFromEnum(FigExtKind.offset_datetime), ts.ptr, ts.len, &dt));
var count: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_diagnose(v, dt, @intFromEnum(FigFormat.json), null, &count));
try std.testing.expectEqual(@as(usize, 1), count);
var w: FigWarning = undefined;
w.size = @sizeOf(FigWarning);
try std.testing.expectEqual(FigStatus.ok, fig_value_warning(v, 0, &w));
try std.testing.expectEqual(@intFromEnum(FigWarningCode.type_degraded), w.code);
try std.testing.expectEqualStrings("string", w.note[0..w.note_len]);
if (comptime build_options.lang_toml) {
try std.testing.expectEqual(FigStatus.ok, fig_value_diagnose(v, dt, @intFromEnum(FigFormat.toml), null, &count));
try std.testing.expectEqual(@as(usize, 0), count);
}
}
test "fig_value_warning honors a truncated (size-gated) FigWarning" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
var v: ?*FigValue = null;
try std.testing.expectEqual(FigStatus.ok, fig_value_create(&v));
defer fig_value_destroy(v);
const ts = "1979-05-27T07:32:00Z";
var dt: FigNodeId = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_extended(v, @intFromEnum(FigExtKind.offset_datetime), ts.ptr, ts.len, &dt));
var count: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_diagnose(v, dt, @intFromEnum(FigFormat.json), null, &count));
try std.testing.expectEqual(@as(usize, 1), count);
var w: FigWarning = undefined;
w.size = @offsetOf(FigWarning, "cause") + @sizeOf(c_int);
w.path = null;
w.path_len = 12345;
try std.testing.expectEqual(FigStatus.ok, fig_value_warning(v, 0, &w));
try std.testing.expectEqual(@intFromEnum(FigWarningCode.type_degraded), w.code);
try std.testing.expectEqual(@as(usize, 12345), w.path_len); }
test "fig_parse_ex fills FigError on a parse failure" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
const bad = "{ \"a\":";
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(
FigStatus.parse_error,
fig_parse_ex(bad.ptr, bad.len, @intFromEnum(FigFormat.json), &out_doc, null),
);
try std.testing.expectEqual(@as(?*FigDocument, null), out_doc);
var err: FigError = undefined;
err.size = @sizeOf(FigError);
try std.testing.expectEqual(
FigStatus.parse_error,
fig_parse_ex(bad.ptr, bad.len, @intFromEnum(FigFormat.json), &out_doc, &err),
);
try std.testing.expectEqual(@intFromEnum(FigStatus.parse_error), err.code);
try std.testing.expect(err.message_len > 0);
try std.testing.expectEqual(@as(u8, 0), err.message[err.message_len]); try std.testing.expectEqual(@as(usize, 0), err.byte_offset);
try std.testing.expectEqual(@as(u32, 0), err.line);
err.size = @sizeOf(FigError);
try std.testing.expectEqual(
FigStatus.unsupported_format,
fig_parse_ex(bad.ptr, bad.len, 0xBEEF, &out_doc, &err),
);
try std.testing.expectEqual(@intFromEnum(FigStatus.unsupported_format), err.code);
}
test "fig_parse_ex honors a truncated (size-gated) FigError" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
const bad = "[1,";
var err: FigError = undefined;
err.size = @offsetOf(FigError, "byte_offset"); err.message_len = 999;
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(
FigStatus.parse_error,
fig_parse_ex(bad.ptr, bad.len, @intFromEnum(FigFormat.json), &out_doc, &err),
);
try std.testing.expectEqual(@intFromEnum(FigStatus.parse_error), err.code);
try std.testing.expectEqual(@as(usize, 999), err.message_len); }
test "fig_parse_ex leaves out_doc null and succeeds on a valid parse" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
const src = "{\"a\":1}";
var out_doc: ?*FigDocument = null;
var err: FigError = undefined;
err.size = @sizeOf(FigError);
try std.testing.expectEqual(
FigStatus.ok,
fig_parse_ex(src.ptr, src.len, @intFromEnum(FigFormat.json), &out_doc, &err),
);
defer fig_document_destroy(out_doc);
try std.testing.expect(out_doc != null);
}
test "a compiled-out format is unsupported in serialize and diagnose alike" {
if (comptime build_options.lang_toml) return error.SkipZigTest;
const src = "{\"a\": 1}"; var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.json), &out_doc));
defer fig_document_destroy(out_doc);
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(
FigStatus.unsupported_format,
fig_document_serialize(out_doc, @intFromEnum(FigFormat.toml), null, &ptr, &len),
);
var count: usize = 999;
try std.testing.expectEqual(
FigStatus.unsupported_format,
fig_document_diagnose(out_doc, @intFromEnum(FigFormat.toml), null, &count),
);
try std.testing.expectEqual(@as(usize, 0), count);
try std.testing.expectEqual(@as(u32, 0), fig_format_capabilities(@intFromEnum(FigFormat.toml)));
}
test "parse c abi reads toml and zon" {
if (comptime !(build_options.lang_toml and build_options.lang_zon)) return error.SkipZigTest;
{
var out_doc: ?*FigDocument = null;
const src = "name = \"fig\"\ncount = 42\n";
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.toml), &out_doc));
defer fig_document_destroy(out_doc);
const root = fig_document_root(out_doc);
try std.testing.expectEqual(@as(c_int, @intFromEnum(FigNodeKind.mapping)), fig_node_kind(out_doc, root));
try std.testing.expectEqual(@as(usize, 2), fig_node_child_count(out_doc, root));
}
{
var out_doc: ?*FigDocument = null;
const src = ".{ .name = \"fig\", .count = 42 }";
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.zon), &out_doc));
defer fig_document_destroy(out_doc);
const root = fig_document_root(out_doc);
try std.testing.expectEqual(@as(c_int, @intFromEnum(FigNodeKind.mapping)), fig_node_kind(out_doc, root));
}
}
test "parse c abi reads json5 and rejects it under strict json" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
const src = "{\n // c\n host: 'localhost',\n port: 8080,\n}\n";
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.json5), &out_doc));
defer fig_document_destroy(out_doc);
const root = fig_document_root(out_doc);
try std.testing.expectEqual(@as(c_int, @intFromEnum(FigNodeKind.mapping)), fig_node_kind(out_doc, root));
try std.testing.expectEqual(@as(usize, 2), fig_node_child_count(out_doc, root));
var strict_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.parse_error, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.json), &strict_doc));
}
test "fig_node_extended recovers datetime and char-literal scalars" {
if (comptime !(build_options.lang_toml and build_options.lang_zon)) return error.SkipZigTest;
var kind: c_int = undefined;
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
{
var out_doc: ?*FigDocument = null;
const src = "d = 2026-06-18\n";
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.toml), &out_doc));
defer fig_document_destroy(out_doc);
const val = fig_keyvalue_value(out_doc, fig_node_first_child(out_doc, fig_document_root(out_doc)));
try std.testing.expectEqual(@as(c_int, @intFromEnum(FigNodeKind.extended)), fig_node_kind(out_doc, val));
try std.testing.expect(fig_node_extended(out_doc, val, &kind, &ptr, &len));
try std.testing.expectEqual(@intFromEnum(FigExtKind.local_date), kind);
try std.testing.expectEqualStrings("2026-06-18", ptr[0..len]);
}
{
var out_doc: ?*FigDocument = null;
const src = ".{ .c = 'a' }";
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.zon), &out_doc));
defer fig_document_destroy(out_doc);
const val = fig_keyvalue_value(out_doc, fig_node_first_child(out_doc, fig_document_root(out_doc)));
try std.testing.expectEqual(@as(c_int, @intFromEnum(FigNodeKind.extended)), fig_node_kind(out_doc, val));
try std.testing.expect(fig_node_extended(out_doc, val, &kind, &ptr, &len));
try std.testing.expectEqual(@intFromEnum(FigExtKind.char_literal), kind);
try std.testing.expectEqualStrings("97", ptr[0..len]);
}
{
var out_doc: ?*FigDocument = null;
const src = "s = \"hi\"\n";
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.toml), &out_doc));
defer fig_document_destroy(out_doc);
const val = fig_keyvalue_value(out_doc, fig_node_first_child(out_doc, fig_document_root(out_doc)));
try std.testing.expect(!fig_node_extended(out_doc, val, &kind, &ptr, &len));
}
}
fn keySeg(s: []const u8) FigPathSegment {
return .{ .kind = 0, .key_ptr = s.ptr, .key_len = s.len, .index = 0 };
}
test "editor c abi insert + source round-trip" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const src = "a: 1\nb: 2\n";
var out_ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.yaml), &out_ed));
defer fig_editor_destroy(out_ed);
const c = "c";
const three = "3";
try std.testing.expectEqual(FigStatus.ok, fig_editor_insert_key(out_ed, null, 0, c.ptr, c.len, three.ptr, three.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(out_ed, &ptr, &len));
try std.testing.expectEqualStrings("a: 1\nb: 2\nc: 3\n", ptr[0..len]);
}
test "editor c abi accepts empty input as an empty document" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
var out_ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(null, 0, @intFromEnum(FigFormat.yaml), &out_ed));
defer fig_editor_destroy(out_ed);
const k = "k";
const v = "v";
try std.testing.expectEqual(FigStatus.ok, fig_editor_insert_key(out_ed, null, 0, k.ptr, k.len, v.ptr, v.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(out_ed, &ptr, &len));
try std.testing.expectEqualStrings("k: v\n", ptr[0..len]);
}
test "toml editor c abi insert + replace + delete round-trip" {
if (comptime !build_options.lang_toml) return error.SkipZigTest;
const src = "[server]\nhost = \"a\"\nport = 1\n";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.toml), &ed));
defer fig_editor_destroy(ed);
const server = [_]FigPathSegment{keySeg("server")};
const tls = "tls";
const tval = "true";
try std.testing.expectEqual(FigStatus.ok, fig_editor_insert_key(ed, &server, 1, tls.ptr, tls.len, tval.ptr, tval.len));
const port = [_]FigPathSegment{ keySeg("server"), keySeg("port") };
const nine = "9090";
try std.testing.expectEqual(FigStatus.ok, fig_editor_replace_val(ed, &port, 2, nine.ptr, nine.len));
const host = [_]FigPathSegment{ keySeg("server"), keySeg("host") };
try std.testing.expectEqual(FigStatus.ok, fig_editor_delete_key(ed, &host, 2));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings("[server]\nport = 9090\ntls = true\n", ptr[0..len]);
}
test "toml editor c abi add leading comment uses the # marker" {
if (comptime !build_options.lang_toml) return error.SkipZigTest;
const src = "a = 1\nb = 2\n";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.toml), &ed));
defer fig_editor_destroy(ed);
const b = [_]FigPathSegment{keySeg("b")};
const note = "note";
try std.testing.expectEqual(FigStatus.ok, fig_editor_add_leading_comment(ed, &b, 1, note.ptr, note.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings("a = 1\n# note\nb = 2\n", ptr[0..len]);
}
test "toml editor c abi maps a shape-mismatch edit to invalid_argument" {
if (comptime !build_options.lang_toml) return error.SkipZigTest;
const src = "a = 1\n";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.toml), &ed));
defer fig_editor_destroy(ed);
const a = "a";
const two = "2";
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_insert_key(ed, null, 0, a.ptr, a.len, two.ptr, two.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings("a = 1\n", ptr[0..len]);
}
test "yaml editor c abi refuses inserting a key that already exists" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const src = "a: 1\n";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.yaml), &ed));
defer fig_editor_destroy(ed);
const a = "a";
const two = "2";
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_insert_named_key(ed, null, 0, a.ptr, a.len, two.ptr, two.len));
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_insert_key(ed, null, 0, a.ptr, a.len, two.ptr, two.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings("a: 1\n", ptr[0..len]);
}
test "frontmatter c abi preserves fences and body" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const md = "---\ntitle: Hi\n# keep\ntags:\n- x\n---\n# Body\ntext\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open(md.ptr, md.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.yaml), &out_fm));
defer fig_embed_destroy(out_fm);
const author = "author";
const me = "me";
try std.testing.expectEqual(FigStatus.ok, fig_embed_insert_key(out_fm, null, 0, author.ptr, author.len, me.ptr, me.len));
const tags = [_]FigPathSegment{keySeg("tags")};
const y = "y";
try std.testing.expectEqual(FigStatus.ok, fig_embed_append_seq(out_fm, &tags, 1, y.ptr, y.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings(
"---\ntitle: Hi\n# keep\ntags:\n- x\n- y\nauthor: me\n---\n# Body\ntext\n",
ptr[0..len],
);
}
fn figStr(s: []const u8) FigStr {
return .{ .ptr = s.ptr, .len = s.len };
}
test "frontmatter c abi reorder keys preserves comments, fences, body" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const md = "---\ntitle: Hi\n# keep\ntags:\n- x\nauthor: me\n---\n# Body\ntext\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open(md.ptr, md.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.yaml), &out_fm));
defer fig_embed_destroy(out_fm);
const keys = [_]FigStr{ figStr("author"), figStr("title") };
try std.testing.expectEqual(FigStatus.ok, fig_embed_reorder_keys(out_fm, null, 0, &keys, keys.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings(
"---\nauthor: me\ntitle: Hi\n# keep\ntags:\n- x\n---\n# Body\ntext\n",
ptr[0..len],
);
}
test "frontmatter c abi move key preserves fences and body" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const md = "---\na: 1\nb: 2\nc: 3\n---\nbody\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open(md.ptr, md.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.yaml), &out_fm));
defer fig_embed_destroy(out_fm);
const src = [_]FigPathSegment{keySeg("c")};
const dest = [_]FigPathSegment{keySeg("a")};
try std.testing.expectEqual(FigStatus.ok, fig_embed_move_key(out_fm, &src, 1, &dest, 1));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings("---\nc: 3\na: 1\nb: 2\n---\nbody\n", ptr[0..len]);
}
test "frontmatter c abi reorder items in a block sequence value" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const md = "---\ntags:\n- x\n- y\n- z\n---\nbody\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open(md.ptr, md.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.yaml), &out_fm));
defer fig_embed_destroy(out_fm);
const path = [_]FigPathSegment{keySeg("tags")};
const indices = [_]usize{ 2, 0 };
try std.testing.expectEqual(FigStatus.ok, fig_embed_reorder_items(out_fm, &path, 1, &indices, indices.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings("---\ntags:\n- z\n- x\n- y\n---\nbody\n", ptr[0..len]);
}
test "frontmatter c abi move item in a flow sequence value" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const md = "---\ntags: [x, y, z]\n---\nbody\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open(md.ptr, md.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.yaml), &out_fm));
defer fig_embed_destroy(out_fm);
const path = [_]FigPathSegment{keySeg("tags")};
try std.testing.expectEqual(FigStatus.ok, fig_embed_move_item(out_fm, &path, 1, 2, 0));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings("---\ntags: [z, x, y]\n---\nbody\n", ptr[0..len]);
}
test "fig_embed_retype re-houses a block, keeping every host byte" {
const md = "---\ntitle: hi\n---\n# body\n";
const content = "{\"title\":\"hi\"}\n";
var ptr: [*]u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_retype(
md.ptr,
md.len,
@intFromEnum(FigEmbedContainer.md_frontmatter),
@intFromEnum(FigFormat.yaml),
@intFromEnum(FigEmbedContainer.semicolons_json),
@intFromEnum(FigFormat.json),
content.ptr,
content.len,
&ptr,
&len,
));
defer fig_free(ptr, len);
try std.testing.expectEqualStrings(";;;\n{\"title\":\"hi\"}\n;;;\n# body\n", ptr[0..len]);
}
test "fig_embed_retype refuses to move a mid-document block to an edge" {
const html = "<head>\n<script type=\"application/yaml\">\nk: v\n</script>\n</head>\n";
const content = "k: v\n";
var ptr: [*]u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.unsupported_operation, fig_embed_retype(
html.ptr,
html.len,
@intFromEnum(FigEmbedContainer.html_script),
@intFromEnum(FigFormat.yaml),
@intFromEnum(FigEmbedContainer.md_frontmatter),
@intFromEnum(FigFormat.yaml),
content.ptr,
content.len,
&ptr,
&len,
));
// Mid-document to mid-document is fine, and splices in place.
try std.testing.expectEqual(FigStatus.ok, fig_embed_retype(
html.ptr,
html.len,
@intFromEnum(FigEmbedContainer.html_script),
@intFromEnum(FigFormat.yaml),
@intFromEnum(FigEmbedContainer.html_code),
@intFromEnum(FigFormat.yaml),
content.ptr,
content.len,
&ptr,
&len,
));
defer fig_free(ptr, len);
try std.testing.expectEqualStrings(
"<head>\n<pre><code class=\"language-yaml\">\nk: v\n</code></pre>\n</head>\n",
ptr[0..len],
);
}
test "fig_embed_retype reports a missing or unterminated region" {
var ptr: [*]u8 = undefined;
var len: usize = undefined;
const plain = "# just markdown\n";
try std.testing.expectEqual(FigStatus.not_found, fig_embed_retype(plain.ptr, plain.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.yaml), @intFromEnum(FigEmbedContainer.plus_toml), @intFromEnum(FigFormat.toml), "", 0, &ptr, &len));
const unterminated = "---\nk: v\nno close\n";
try std.testing.expectEqual(FigStatus.parse_error, fig_embed_retype(unterminated.ptr, unterminated.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.yaml), @intFromEnum(FigEmbedContainer.plus_toml), @intFromEnum(FigFormat.toml), "", 0, &ptr, &len));
// Null out params are rejected, not crashed on.
try std.testing.expectEqual(FigStatus.invalid_argument, fig_embed_retype(plain.ptr, plain.len, 0, 0, 0, 0, "", 0, null, &len));
}
test "embed c abi locates region with content and body spans" {
const md = "---\nk: v\n---\nbody\n";
var region: FigRegion = .{ .size = @sizeOf(FigRegion), .open_fence = undefined, .content = undefined, .close_fence = undefined, .body = undefined, .body_before = undefined, .body_after = undefined };
try std.testing.expectEqual(FigStatus.ok, fig_embed_extract(md.ptr, md.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.yaml), ®ion));
try std.testing.expectEqualStrings("k: v\n", md[region.content.start..region.content.end]);
// The body is the suffix after the close fence.
try std.testing.expectEqualStrings("body\n", md[region.body.start..region.body.end]);
}
test "embed c abi locates a ```fig fenced frontmatter block (extract-only)" {
if (comptime !build_options.lang_fig) return error.SkipZigTest;
const md = "```fig\nk = v\n```\nbody\n";
var region: FigRegion = .{ .size = @sizeOf(FigRegion), .open_fence = undefined, .content = undefined, .close_fence = undefined, .body = undefined, .body_before = undefined, .body_after = undefined };
try std.testing.expectEqual(FigStatus.ok, fig_embed_extract(md.ptr, md.len, @intFromEnum(FigEmbedContainer.fenced), @intFromEnum(FigFormat.fig), ®ion));
try std.testing.expectEqualStrings("k = v\n", md[region.content.start..region.content.end]);
try std.testing.expectEqualStrings("body\n", md[region.body.start..region.body.end]);
}
test "embed c abi detects each archetype by its open delimiter" {
// A preset container reports the format it pins, so every pair read back
// names a real inner format.
const cases = [_]struct { src: []const u8, container: FigEmbedContainer, format: FigFormat }{
.{ .src = "---\nk: v\n---\nbody\n", .container = .md_frontmatter, .format = .yaml },
.{ .src = ";;;\n{\"k\": 1}\n;;;\nbody\n", .container = .semicolons_json, .format = .json },
.{ .src = "```fig\nk = v\n```\nbody\n", .container = .fenced, .format = .fig },
.{ .src = "body\n```endmatter\nk: v\n```\n", .container = .endmatter_yaml, .format = .yaml },
.{ .src = "+++\nk = \"v\"\n+++\nbody\n", .container = .plus_toml, .format = .toml },
.{ .src = "```toml\nk = \"v\"\n```\nbody\n", .container = .fenced, .format = .toml },
.{ .src = "```yaml\nk: v\n```\nbody\n", .container = .fenced, .format = .yaml },
.{ .src = "```json\n{\"k\": 1}\n```\nbody\n", .container = .fenced, .format = .json },
.{ .src = "---toml\nk = \"v\"\n---\nbody\n", .container = .md_frontmatter, .format = .toml },
.{ .src = "<html><head>\n<script type=\"application/figl\">\nk = \"v\"\n</script>\n</head></html>\n", .container = .html_script, .format = .fig },
.{ .src = "<pre><code class=\"language-yaml\">\nk: v\n</code></pre>\n", .container = .html_code, .format = .yaml },
};
for (cases) |case| {
var out_c: c_int = -1;
var out_f: c_int = -1;
try std.testing.expectEqual(FigStatus.ok, fig_embed_detect(case.src.ptr, case.src.len, &out_c, &out_f));
try std.testing.expectEqual(@intFromEnum(case.container), out_c);
try std.testing.expectEqual(@intFromEnum(case.format), out_f);
}
}
test "embed c abi refuses a pair the model cannot spell" {
const md = "---\nk: v\n---\nbody\n";
var region: FigRegion = .{ .size = @sizeOf(FigRegion), .open_fence = undefined, .content = undefined, .close_fence = undefined, .body = undefined, .body_before = undefined, .body_after = undefined };
// A format with no embedded spelling, an unknown format, an unknown
// container: each is a malformed selector, not a missing region.
try std.testing.expectEqual(FigStatus.invalid_argument, fig_embed_extract(md.ptr, md.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.ini), ®ion));
try std.testing.expectEqual(FigStatus.invalid_argument, fig_embed_extract(md.ptr, md.len, @intFromEnum(FigEmbedContainer.md_frontmatter), 6, ®ion));
try std.testing.expectEqual(FigStatus.invalid_argument, fig_embed_extract(md.ptr, md.len, 99, @intFromEnum(FigFormat.yaml), ®ion));
// A preset ignores the format argument — even a nonsense one.
const jf = ";;;\n{\"k\": 1}\n;;;\nbody\n";
try std.testing.expectEqual(FigStatus.ok, fig_embed_extract(jf.ptr, jf.len, @intFromEnum(FigEmbedContainer.semicolons_json), -1, ®ion));
try std.testing.expectEqualStrings("{\"k\": 1}\n", jf[region.content.start..region.content.end]);
}
test "embed c abi detect: not_found leaves out untouched; unterminated still detects" {
// Plain markdown — no archetype opens it.
const plain = "# just a note\n";
var out_c: c_int = -7;
var out_f: c_int = -7;
try std.testing.expectEqual(FigStatus.not_found, fig_embed_detect(plain.ptr, plain.len, &out_c, &out_f));
try std.testing.expectEqual(@as(c_int, -7), out_c);
try std.testing.expectEqual(@as(c_int, -7), out_f);
// An unterminated fence is still recognized (open-delimiter-only sniff), so
// the follow-up extract reports the real error rather than not_found.
const unterminated = "---\nk: v\nno close\n";
try std.testing.expectEqual(FigStatus.ok, fig_embed_detect(unterminated.ptr, unterminated.len, &out_c, &out_f));
try std.testing.expectEqual(@intFromEnum(FigEmbedContainer.md_frontmatter), out_c);
try std.testing.expectEqual(@intFromEnum(FigFormat.yaml), out_f);
var region: FigRegion = .{ .size = @sizeOf(FigRegion), .open_fence = undefined, .content = undefined, .close_fence = undefined, .body = undefined, .body_before = undefined, .body_after = undefined };
try std.testing.expectEqual(FigStatus.parse_error, fig_embed_extract(unterminated.ptr, unterminated.len, out_c, out_f, ®ion));
// A null out param is invalid, not a crash — either of them.
try std.testing.expectEqual(FigStatus.invalid_argument, fig_embed_detect(plain.ptr, plain.len, null, &out_f));
try std.testing.expectEqual(FigStatus.invalid_argument, fig_embed_detect(plain.ptr, plain.len, &out_c, null));
}
test "embed c abi fig_embed_open edits a ```fig fenced frontmatter block" {
if (comptime !build_options.lang_fig) return error.SkipZigTest;
const md = "```fig\nk = v\n```\nbody\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open(md.ptr, md.len, @intFromEnum(FigEmbedContainer.fenced), @intFromEnum(FigFormat.fig), &out_fm));
defer fig_embed_destroy(out_fm);
const path = [_]FigPathSegment{keySeg("k")};
try std.testing.expectEqual(FigStatus.ok, fig_embed_replace_val(out_fm, &path, 1, "w", 1));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings("```fig\nk = w\n```\nbody\n", ptr[0..len]);
}
test "embed c abi edits a <code> block span-aware (untouched entity encoding preserved)" {
if (comptime !build_options.lang_fig) return error.SkipZigTest;
// Mixed original encodings: `expr` uses numeric <, `note` uses named <.
const html = "<pre><code class=\"language-figl\">\nexpr = \"a < b\"\nnote = \"x < y\"\n</code></pre>\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open(html.ptr, html.len, @intFromEnum(FigEmbedContainer.html_code), @intFromEnum(FigFormat.fig), &out_fm));
defer fig_embed_destroy(out_fm);
// Edit `expr` to a quoted value containing `>` — it must canonically re-encode.
const path = [_]FigPathSegment{keySeg("expr")};
try std.testing.expectEqual(FigStatus.ok, fig_embed_replace_val(out_fm, &path, 1, "\"p > q\"", 7));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
// The edited value is canonically encoded; the untouched `note` keeps its
// ORIGINAL `<` byte-for-byte, and the fences stay identical.
try std.testing.expectEqualStrings(
"<pre><code class=\"language-figl\">\nexpr = \"p > q\"\nnote = \"x < y\"\n</code></pre>\n",
ptr[0..len],
);
}
test "embed c abi fig_embed_set splices a block map into a ```fig fence" {
if (comptime !build_options.lang_fig) return error.SkipZigTest;
// Regression for the prov follow-up: a block-map value CAN now be
// spliced into a fenced embed — it re-frames as a nested section under the
// key rather than failing (previously the caller had to fall back to
// per-key flow inserts).
const md = "```fig\ntitle = hi\n```\nbody\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open(md.ptr, md.len, @intFromEnum(FigEmbedContainer.fenced), @intFromEnum(FigFormat.fig), &out_fm));
defer fig_embed_destroy(out_fm);
const path = [_]FigPathSegment{keySeg("registry")};
try std.testing.expectEqual(FigStatus.ok, fig_embed_set(out_fm, &path, 1, "a = 1\nb = 2\n", 12));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings("```fig\ntitle = hi\nregistry\n> a = 1\n> b = 2\n```\nbody\n", ptr[0..len]);
}
test "embed c abi region size-gate leaves uncovered fields untouched" {
const md = "---\nk: v\n---\nbody\n";
// A caller whose `size` reaches only through `content` must not have its
// `close_fence`/`body` (past `size`) overwritten — they keep their sentinels.
const partial_size: u32 = @offsetOf(FigRegion, "content") + @sizeOf(FigSpan);
var region: FigRegion = .{
.size = partial_size,
.open_fence = undefined,
.content = undefined,
.close_fence = .{ .start = 111, .end = 222 },
.body = .{ .start = 333, .end = 444 },
.body_before = .{ .start = 555, .end = 666 },
.body_after = .{ .start = 777, .end = 888 },
};
try std.testing.expectEqual(FigStatus.ok, fig_embed_extract(md.ptr, md.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.yaml), ®ion));
try std.testing.expectEqualStrings("k: v\n", md[region.content.start..region.content.end]);
try std.testing.expectEqual(@as(usize, 111), region.close_fence.start);
try std.testing.expectEqual(@as(usize, 333), region.body.start);
// The two sides were appended after `body`, so they are past `size` too.
try std.testing.expectEqual(@as(usize, 555), region.body_before.start);
try std.testing.expectEqual(@as(usize, 777), region.body_after.start);
}
test "fig_embed_replace_body swaps the body, keeps fences + edited content" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const md = "---\ntitle: Hi\n---\nold body\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open(md.ptr, md.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.yaml), &out_fm));
defer fig_embed_destroy(out_fm);
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
// Body-only swap leaves the frontmatter byte-identical.
const body = "new body\n";
try std.testing.expectEqual(FigStatus.ok, fig_embed_replace_body(out_fm, body.ptr, body.len));
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings("---\ntitle: Hi\n---\nnew body\n", ptr[0..len]);
// Composes with a frontmatter edit, in one render.
const title = [_]FigPathSegment{keySeg("title")};
const hello = "Hello";
try std.testing.expectEqual(FigStatus.ok, fig_embed_replace_val(out_fm, &title, 1, hello.ptr, hello.len));
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings("---\ntitle: Hello\n---\nnew body\n", ptr[0..len]);
}
test "fig_embed_replace_body refuses a mid-document block, which has no one body" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
// Host text on both sides: swapping the side after the block lost `</head>`.
const html = "<head>\n<script type=\"application/yaml\">\nk: v\n</script>\n</head>\n";
var em: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open(html.ptr, html.len, @intFromEnum(FigEmbedContainer.html_script), @intFromEnum(FigFormat.yaml), &em));
defer fig_embed_destroy(em);
const body = "NEW\n";
try std.testing.expectEqual(FigStatus.unsupported_operation, fig_embed_replace_body(em, body.ptr, body.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(em, &ptr, &len));
try std.testing.expectEqualStrings(html, ptr[0..len]);
}
test "fig_embed_open_or_init creates a frontmatter block where none exists" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const md = "# Just a body\n\nprose\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open_or_init(md.ptr, md.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.yaml), &out_fm));
defer fig_embed_destroy(out_fm);
// The synthesized block is empty; the first set lands the opening key.
const title = [_]FigPathSegment{keySeg("title")};
const hi = "Hi";
try std.testing.expectEqual(FigStatus.ok, fig_embed_set(out_fm, &title, 1, hi.ptr, hi.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings("---\ntitle: Hi\n---\n# Just a body\n\nprose\n", ptr[0..len]);
}
test "fig_embed_open_or_init opens an existing region unchanged" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const md = "---\ntitle: Old # c\n---\nbody\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open_or_init(md.ptr, md.len, @intFromEnum(FigEmbedContainer.md_frontmatter), @intFromEnum(FigFormat.yaml), &out_fm));
defer fig_embed_destroy(out_fm);
// Behaves like open: edits the existing region, comment + body preserved.
const title = [_]FigPathSegment{keySeg("title")};
const new = "New";
try std.testing.expectEqual(FigStatus.ok, fig_embed_set(out_fm, &title, 1, new.ptr, new.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings("---\ntitle: New # c\n---\nbody\n", ptr[0..len]);
}
test "fig_embed_open_or_init creates a JSON (;;;) frontmatter block" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
const md = "# Doc\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open_or_init(md.ptr, md.len, @intFromEnum(FigEmbedContainer.semicolons_json), @intFromEnum(FigFormat.json), &out_fm));
defer fig_embed_destroy(out_fm);
const title = [_]FigPathSegment{keySeg("title")};
const hi = "\"Hi\""; // strict JSON value: a quoted string
try std.testing.expectEqual(FigStatus.ok, fig_embed_set(out_fm, &title, 1, hi.ptr, hi.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
// The key is quoted for JSON, and the close fence stays on its own line.
try std.testing.expectEqualStrings(";;;\n{\"title\": \"Hi\"}\n;;;\n# Doc\n", ptr[0..len]);
}
test "fig_embed_open_or_init appends an endmatter block at the bottom" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const md = "# Title\n\nbody text\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open_or_init(md.ptr, md.len, @intFromEnum(FigEmbedContainer.endmatter_yaml), @intFromEnum(FigFormat.yaml), &out_fm));
defer fig_embed_destroy(out_fm);
const k = [_]FigPathSegment{keySeg("k")};
const v = "v";
try std.testing.expectEqual(FigStatus.ok, fig_embed_set(out_fm, &k, 1, v.ptr, v.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings("# Title\n\nbody text\n```endmatter\nk: v\n```\n", ptr[0..len]);
}
test "fig_embed_open_or_init refuses a leading block in front of frontmatter of another archetype" {
if (comptime !build_options.lang_json or !build_options.lang_yaml) return error.SkipZigTest;
// Prepending `;;;` used to leave the `---` block under it, no longer on
// the first line and so no longer frontmatter.
const md = "---\ntitle: x\n---\nbody\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.unsupported_operation, fig_embed_open_or_init(md.ptr, md.len, @intFromEnum(FigEmbedContainer.semicolons_json), @intFromEnum(FigFormat.json), &out_fm));
try std.testing.expect(out_fm == null);
// Endmatter goes after everything and displaces nothing.
try std.testing.expectEqual(FigStatus.ok, fig_embed_open_or_init(md.ptr, md.len, @intFromEnum(FigEmbedContainer.endmatter_yaml), @intFromEnum(FigFormat.yaml), &out_fm));
fig_embed_destroy(out_fm);
}
test "embed c abi edits json frontmatter (`;;;` fences, JSON inner editor)" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
const md = ";;;\n{\"title\": \"Hi\", \"draft\": true}\n;;;\n# Body\n";
var out_fm: ?*FigEmbed = null;
try std.testing.expectEqual(FigStatus.ok, fig_embed_open(md.ptr, md.len, @intFromEnum(FigEmbedContainer.semicolons_json), @intFromEnum(FigFormat.json), &out_fm));
defer fig_embed_destroy(out_fm);
// The replacement crosses the ABI already serialized — JSON value text here.
const title = [_]FigPathSegment{keySeg("title")};
const hello = "\"Hello\"";
try std.testing.expectEqual(FigStatus.ok, fig_embed_replace_val(out_fm, &title, 1, hello.ptr, hello.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_embed_render(out_fm, &ptr, &len));
try std.testing.expectEqualStrings(";;;\n{\"title\": \"Hello\", \"draft\": true}\n;;;\n# Body\n", ptr[0..len]);
}
test "value c abi builds and serializes to multiple formats" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
var out_value: ?*FigValue = null;
try std.testing.expectEqual(FigStatus.ok, fig_value_create(&out_value));
defer fig_value_destroy(out_value);
// Build { "name": "fig", "nums": [1, 2] } bottom-up.
var id: FigNodeId = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_string(out_value, "fig", 3, &id));
const v_name = id;
try std.testing.expectEqual(FigStatus.ok, fig_value_int(out_value, 1, &id));
const n1 = id;
try std.testing.expectEqual(FigStatus.ok, fig_value_int(out_value, 2, &id));
const n2 = id;
const items = [_]FigNodeId{ n1, n2 };
try std.testing.expectEqual(FigStatus.ok, fig_value_seq(out_value, &items, items.len, &id));
const v_nums = id;
try std.testing.expectEqual(FigStatus.ok, fig_value_string(out_value, "name", 4, &id));
const k_name = id;
try std.testing.expectEqual(FigStatus.ok, fig_value_string(out_value, "nums", 4, &id));
const k_nums = id;
const entries = [_]FigKeyValue{ .{ .key = k_name, .value = v_name }, .{ .key = k_nums, .value = v_nums } };
try std.testing.expectEqual(FigStatus.ok, fig_value_map(out_value, &entries, entries.len, &id));
const root = id;
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_serialize(out_value, root, @intFromEnum(FigFormat.json), &ptr, &len));
try std.testing.expectEqualStrings("{\n \"name\": \"fig\",\n \"nums\": [\n 1,\n 2\n ]\n}\n", ptr[0..len]);
// Same value, different format — the borrowed bytes are refreshed in place.
if (comptime build_options.lang_yaml) {
try std.testing.expectEqual(FigStatus.ok, fig_value_serialize(out_value, root, @intFromEnum(FigFormat.yaml), &ptr, &len));
try std.testing.expectEqualStrings("name: fig\nnums: [1, 2]\n", ptr[0..len]);
}
}
test "value c abi maps an unrepresentable value to unsupported_format" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
var out_value: ?*FigValue = null;
try std.testing.expectEqual(FigStatus.ok, fig_value_create(&out_value));
defer fig_value_destroy(out_value);
// { "k": null } — TOML has no null, so serializing to TOML must fail cleanly.
var id: FigNodeId = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_null(out_value, &id));
const v_null = id;
try std.testing.expectEqual(FigStatus.ok, fig_value_string(out_value, "k", 1, &id));
const k = id;
const entries = [_]FigKeyValue{.{ .key = k, .value = v_null }};
try std.testing.expectEqual(FigStatus.ok, fig_value_map(out_value, &entries, entries.len, &id));
const root = id;
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.unsupported_format, fig_value_serialize(out_value, root, @intFromEnum(FigFormat.toml), &ptr, &len));
// The same value serializes fine to a format that has null.
try std.testing.expectEqual(FigStatus.ok, fig_value_serialize(out_value, root, @intFromEnum(FigFormat.json), &ptr, &len));
try std.testing.expectEqualStrings("{\n \"k\": null\n}\n", ptr[0..len]);
}
test "value c abi serialize options honor the size/version field" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
var out_value: ?*FigValue = null;
try std.testing.expectEqual(FigStatus.ok, fig_value_create(&out_value));
defer fig_value_destroy(out_value);
// [ 1, 2 ] — exercise pretty on/off via the options struct.
var id: FigNodeId = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_int(out_value, 1, &id));
const a = id;
try std.testing.expectEqual(FigStatus.ok, fig_value_int(out_value, 2, &id));
const b = id;
const items = [_]FigNodeId{ a, b };
try std.testing.expectEqual(FigStatus.ok, fig_value_seq(out_value, &items, items.len, &id));
const root = id;
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
// A fully-populated options struct: compact output.
var opts: FigSerializeOptions = .{ .pretty = 0 };
try std.testing.expectEqual(FigStatus.ok, fig_value_serialize_opts(out_value, root, @intFromEnum(FigFormat.json), &opts, &ptr, &len));
try std.testing.expectEqualStrings("[1,2]\n", ptr[0..len]);
// A `size` that does not reach `pretty` must leave it (and `indent`) at the
// default — i.e. behave as if those fields were absent, not read as garbage.
// This is the forward-compat contract: an older/under-sized layout defaults.
opts.size = @offsetOf(FigSerializeOptions, "pretty"); // covers only `size`
try std.testing.expectEqual(FigStatus.ok, fig_value_serialize_opts(out_value, root, @intFromEnum(FigFormat.json), &opts, &ptr, &len));
try std.testing.expectEqualStrings("[\n 1,\n 2\n]\n", ptr[0..len]);
}
test "value c abi serialize options carry TOML width through to the inline/section choice" {
if (comptime !build_options.lang_toml) return error.SkipZigTest;
var out_value: ?*FigValue = null;
try std.testing.expectEqual(FigStatus.ok, fig_value_create(&out_value));
defer fig_value_destroy(out_value);
// { point: { x = 1, y = 2 } } — a small mapping value whose layout flips on
// the width budget.
var id: FigNodeId = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_int(out_value, 1, &id));
const x_val = id;
try std.testing.expectEqual(FigStatus.ok, fig_value_int(out_value, 2, &id));
const y_val = id;
try std.testing.expectEqual(FigStatus.ok, fig_value_string(out_value, "x", 1, &id));
const x_key = id;
try std.testing.expectEqual(FigStatus.ok, fig_value_string(out_value, "y", 1, &id));
const y_key = id;
const inner = [_]FigKeyValue{ .{ .key = x_key, .value = x_val }, .{ .key = y_key, .value = y_val } };
try std.testing.expectEqual(FigStatus.ok, fig_value_map(out_value, &inner, inner.len, &id));
const point_val = id;
try std.testing.expectEqual(FigStatus.ok, fig_value_string(out_value, "point", 5, &id));
const point_key = id;
const outer = [_]FigKeyValue{.{ .key = point_key, .value = point_val }};
try std.testing.expectEqual(FigStatus.ok, fig_value_map(out_value, &outer, outer.len, &id));
const root = id;
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
// Default width (80): the mapping fits, so it stays an inline table.
try std.testing.expectEqual(FigStatus.ok, fig_value_serialize(out_value, root, @intFromEnum(FigFormat.toml), &ptr, &len));
try std.testing.expectEqualStrings("point = { x = 1, y = 2 }\n", ptr[0..len]);
// A tight width budget forces it to expand to a [section].
const opts: FigSerializeOptions = .{ .width = 8 };
try std.testing.expectEqual(FigStatus.ok, fig_value_serialize_opts(out_value, root, @intFromEnum(FigFormat.toml), &opts, &ptr, &len));
try std.testing.expectEqualStrings("[point]\nx = 1\ny = 2\n", ptr[0..len]);
}
test "value c abi flow option renders fig-dialect container fragments inline" {
if (comptime !build_options.lang_fig) return error.SkipZigTest;
var out_value: ?*FigValue = null;
try std.testing.expectEqual(FigStatus.ok, fig_value_create(&out_value));
defer fig_value_destroy(out_value);
// ["a.md", "b.md"] — the shape the editors splice after `key = `.
var id: FigNodeId = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_string(out_value, "a.md", 4, &id));
const a = id;
try std.testing.expectEqual(FigStatus.ok, fig_value_string(out_value, "b.md", 4, &id));
const b = id;
const items = [_]FigNodeId{ a, b };
try std.testing.expectEqual(FigStatus.ok, fig_value_seq(out_value, &items, items.len, &id));
const root = id;
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
// flow set: the inline spelling — the only one that survives a splice.
const flow_opts: FigSerializeOptions = .{ .flow = 1 };
try std.testing.expectEqual(FigStatus.ok, fig_value_serialize_opts(out_value, root, @intFromEnum(FigFormat.fig), &flow_opts, &ptr, &len));
try std.testing.expectEqualStrings("[a.md, b.md]\n", ptr[0..len]);
// flow unset (default): unchanged block rendering.
try std.testing.expectEqual(FigStatus.ok, fig_value_serialize(out_value, root, @intFromEnum(FigFormat.fig), &ptr, &len));
try std.testing.expectEqualStrings("* a.md\n* b.md\n", ptr[0..len]);
}
test "value c abi rejects an out-of-range child id" {
var out_value: ?*FigValue = null;
try std.testing.expectEqual(FigStatus.ok, fig_value_create(&out_value));
defer fig_value_destroy(out_value);
var id: FigNodeId = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_int(out_value, 1, &id));
// id 99 was never created.
const items = [_]FigNodeId{ id, 99 };
try std.testing.expectEqual(FigStatus.invalid_argument, fig_value_seq(out_value, &items, items.len, &id));
}
test "fig_parse empty input is judged per format" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
// (null ptr, len 0) reaches the parser — same as (ptr, len 0). YAML treats
// an empty stream as a null document and TOML as an empty table (both ok);
// JSON requires a value (parse_error). A null ptr with a nonzero len is a
// malformed argument regardless of format.
{
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.parse_error, fig_parse(null, 0, @intFromEnum(FigFormat.json), &out_doc));
try std.testing.expect(out_doc == null);
}
{
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.invalid_argument, fig_parse(null, 5, @intFromEnum(FigFormat.json), &out_doc));
try std.testing.expect(out_doc == null);
}
if (comptime build_options.lang_yaml) {
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(null, 0, @intFromEnum(FigFormat.yaml), &out_doc));
defer fig_document_destroy(out_doc);
try std.testing.expectEqual(@as(c_int, @intFromEnum(FigNodeKind.null_)), fig_node_kind(out_doc, fig_document_root(out_doc)));
}
if (comptime build_options.lang_toml) {
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(null, 0, @intFromEnum(FigFormat.toml), &out_doc));
defer fig_document_destroy(out_doc);
try std.testing.expectEqual(@as(c_int, @intFromEnum(FigNodeKind.mapping)), fig_node_kind(out_doc, fig_document_root(out_doc)));
try std.testing.expectEqual(@as(usize, 0), fig_node_child_count(out_doc, fig_document_root(out_doc)));
}
}
test "fig_document_serialize converts JSON to YAML" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const src = "{\"name\":\"fig\",\"nums\":[1,2]}";
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.json), &out_doc));
defer fig_document_destroy(out_doc);
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_document_serialize(out_doc, @intFromEnum(FigFormat.yaml), null, &ptr, &len));
try std.testing.expectEqualStrings("name: fig\nnums: [1, 2]\n", ptr[0..len]);
// Same handle, re-serialize to TOML — the borrowed bytes refresh in place.
if (comptime build_options.lang_toml) {
try std.testing.expectEqual(FigStatus.ok, fig_document_serialize(out_doc, @intFromEnum(FigFormat.toml), null, &ptr, &len));
try std.testing.expectEqualStrings("name = \"fig\"\nnums = [1, 2]\n", ptr[0..len]);
}
}
test "fig_parse parses the fig authoring dialect" {
if (comptime !build_options.lang_fig) return error.SkipZigTest;
const src = "title = Hello\ncount = 42\n";
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.fig), &out_doc));
defer fig_document_destroy(out_doc);
if (comptime build_options.lang_json) {
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_document_serialize(out_doc, @intFromEnum(FigFormat.json), null, &ptr, &len));
try std.testing.expectEqualStrings("{\n \"title\": \"Hello\",\n \"count\": 42\n}\n", ptr[0..len]);
}
}
test "fig_document_serialize converts JSON to the fig authoring dialect" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
if (comptime !build_options.lang_fig) return error.SkipZigTest;
const src = "{\"name\":\"fig\",\"nums\":[1,2]}";
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.json), &out_doc));
defer fig_document_destroy(out_doc);
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_document_serialize(out_doc, @intFromEnum(FigFormat.fig), null, &ptr, &len));
try std.testing.expectEqualStrings("name = fig\nnums = [1, 2]\n", ptr[0..len]);
}
test "fig_editor_create edits the fig authoring dialect" {
if (comptime !build_options.lang_fig) return error.SkipZigTest;
const src = "title = old\nport = 8080\n";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.fig), &ed));
defer fig_editor_destroy(ed);
const path = [_]FigPathSegment{keySeg("port")};
const repl = "9090";
try std.testing.expectEqual(FigStatus.ok, fig_editor_replace_val(ed, &path, 1, repl.ptr, repl.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings("title = old\nport = 9090\n", ptr[0..len]);
}
test "fig_editor_create edits ZON through the C ABI" {
if (comptime !build_options.lang_zon) return error.SkipZigTest;
const src = ".{ .title = \"old\", .port = 8080 }";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.zon), &ed));
defer fig_editor_destroy(ed);
const path = [_]FigPathSegment{keySeg("port")};
const repl = "9090";
try std.testing.expectEqual(FigStatus.ok, fig_editor_replace_val(ed, &path, 1, repl.ptr, repl.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings(".{ .title = \"old\", .port = 9090 }", ptr[0..len]);
}
test "fig_editor_insert_named_key spells the name as the format does" {
// `insert_key` takes key SYNTAX; a binding that has a name from its
// caller used to spell it as a string value, which is `"new"` in ZON —
// a string where a `.new` field must be. The named entry spells it.
if (comptime build_options.lang_zon) {
const src = ".{ .a = 1 }";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.zon), &ed));
defer fig_editor_destroy(ed);
const name = "new";
const val = "true";
try std.testing.expectEqual(FigStatus.ok, fig_editor_insert_named_key(ed, null, 0, name.ptr, name.len, val.ptr, val.len));
const spaced = "has space";
try std.testing.expectEqual(FigStatus.ok, fig_editor_insert_named_key(ed, null, 0, spaced.ptr, spaced.len, val.ptr, val.len));
try expectEditorSource(ed, ".{ .a = 1, .new = true, .@\"has space\" = true }");
}
if (comptime build_options.lang_json) {
const src = "{\"a\": 1}";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.json), &ed));
defer fig_editor_destroy(ed);
const name = "k\"q";
const val = "2";
try std.testing.expectEqual(FigStatus.ok, fig_editor_insert_named_key(ed, null, 0, name.ptr, name.len, val.ptr, val.len));
try expectEditorSource(ed, "{\"a\": 1, \"k\\\"q\": 2}");
}
}
test "fig_editor_replace_named_key spells the new name as the format does" {
// ZON's key span is the field name after its `.`, so a rename keeps
// the dot where it stands and spells only the name.
if (comptime build_options.lang_zon) {
const src = ".{ .a = 1, .b = 2 }";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.zon), &ed));
defer fig_editor_destroy(ed);
const a = [_]FigPathSegment{keySeg("a")};
const b = [_]FigPathSegment{keySeg("b")};
const k = "k";
const spaced = "has space";
try std.testing.expectEqual(FigStatus.ok, fig_editor_replace_named_key(ed, &a, 1, k.ptr, k.len));
try std.testing.expectEqual(FigStatus.ok, fig_editor_replace_named_key(ed, &b, 1, spaced.ptr, spaced.len));
try expectEditorSource(ed, ".{ .k = 1, .@\"has space\" = 2 }");
}
if (comptime build_options.lang_plist) {
const src = "<dict><key>a</key><string>x</string></dict>";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.plist), &ed));
defer fig_editor_destroy(ed);
const a = [_]FigPathSegment{keySeg("a")};
const name = "b&c";
try std.testing.expectEqual(FigStatus.ok, fig_editor_replace_named_key(ed, &a, 1, name.ptr, name.len));
try expectEditorSource(ed, "<dict><key>b&c</key><string>x</string></dict>");
}
}
test "fig_editor_replace_key refuses a path with no key and a name already held" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
const src = "{\"l\": [1, 2], \"a\": 1, \"b\": 2}";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.json), &ed));
defer fig_editor_destroy(ed);
// A sequence item has no key: the old splice overwrote the item's value.
const item = [_]FigPathSegment{ keySeg("l"), .{ .kind = 1, .key_ptr = null, .key_len = 0, .index = 0 } };
const z = "\"z\"";
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_replace_key(ed, &item, item.len, z.ptr, z.len));
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_replace_key(ed, null, 0, z.ptr, z.len));
// JSON's parser accepts a repeated key, so the engine refuses it.
const a = [_]FigPathSegment{keySeg("a")};
const b = "b";
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_replace_named_key(ed, &a, 1, b.ptr, b.len));
const b_syntax = "\"b\"";
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_replace_key(ed, &a, 1, b_syntax.ptr, b_syntax.len));
try expectEditorSource(ed, src);
}
/// Read back an editor's source, for the whole-container tests below.
fn expectEditorSource(ed: ?*FigEditor, expected: []const u8) !void {
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings(expected, ptr[0..len]);
}
test "fig_editor whole-container ops reach a TOML table the key ops cannot" {
if (comptime !build_options.lang_toml) return error.SkipZigTest;
// The pairing these exports exist for: at a `[header]` path the key ops
// refuse (the editor's pre-op guards), and the container op does the job.
const src = "[a]\nx = 1\n[b]\ny = 2\n";
const path_a = [_]FigPathSegment{keySeg("a")};
const path_b = [_]FigPathSegment{keySeg("b")};
{
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.toml), &ed));
defer fig_editor_destroy(ed);
// Refused by the key op…
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_delete_key(ed, &path_a, 1));
// …done by the container op, which takes the body with the header.
try std.testing.expectEqual(FigStatus.ok, fig_editor_delete_container(ed, &path_a, 1));
try expectEditorSource(ed, "[b]\ny = 2\n");
}
{
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.toml), &ed));
defer fig_editor_destroy(ed);
const leaf = "q";
try std.testing.expectEqual(FigStatus.ok, fig_editor_rename_container(ed, &path_a, 1, leaf.ptr, leaf.len));
try expectEditorSource(ed, "[q]\nx = 1\n[b]\ny = 2\n");
}
{
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.toml), &ed));
defer fig_editor_destroy(ed);
const body = "z = 3\n";
const path_c = [_]FigPathSegment{keySeg("c")};
try std.testing.expectEqual(FigStatus.ok, fig_editor_insert_container(ed, &path_c, 1, body.ptr, body.len));
try expectEditorSource(ed, "[a]\nx = 1\n[b]\ny = 2\n\n[c]\nz = 3\n");
}
{
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.toml), &ed));
defer fig_editor_destroy(ed);
// The generic move is refused; the container move relocates the whole
// table. A NULL destination is "to EOF", which is why it cannot be
// spelled as a zero-length path (that is the root).
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_move_key(ed, &path_a, 1, &path_b, 1));
try std.testing.expectEqual(FigStatus.ok, fig_editor_move_container(ed, &path_a, 1, null, 0));
try expectEditorSource(ed, "[b]\ny = 2\n\n[a]\nx = 1\n");
}
{
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.toml), &ed));
defer fig_editor_destroy(ed);
const order = [_]FigStr{ figStr("b"), figStr("a") };
// `fig_editor_reorder_keys` at the root would have moved the header
// lines alone; this moves the tables.
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_reorder_keys(ed, &.{}, 0, &order, order.len));
try std.testing.expectEqual(FigStatus.ok, fig_editor_reorder_containers(ed, &order, order.len));
try expectEditorSource(ed, "[b]\ny = 2\n[a]\nx = 1\n");
}
}
test "fig_editor_append_container_to_seq appends a TOML array-of-tables element" {
if (comptime !build_options.lang_toml) return error.SkipZigTest;
const src = "[[bin]]\nname = \"a\"\n";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.toml), &ed));
defer fig_editor_destroy(ed);
const path = [_]FigPathSegment{keySeg("bin")};
const body = "name = \"b\"\n";
try std.testing.expectEqual(FigStatus.ok, fig_editor_append_container_to_seq(ed, &path, 1, body.ptr, body.len));
try expectEditorSource(ed, "[[bin]]\nname = \"a\"\n\n[[bin]]\nname = \"b\"\n");
}
test "fig_editor whole-container ops answer unsupported_format where the format has none" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
if (comptime !build_options.lang_ini) return error.SkipZigTest;
const path = [_]FigPathSegment{keySeg("a")};
// YAML nests its containers in one contiguous region, so it declares none of
// the six — the key ops already handle a YAML block mapping (which is why
// `fig_editor_delete_key` below succeeds where TOML's refuses).
{
const src = "a:\n x: 1\nb:\n y: 2\n";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.yaml), &ed));
defer fig_editor_destroy(ed);
try std.testing.expectEqual(FigStatus.unsupported_format, fig_editor_delete_container(ed, &path, 1));
try std.testing.expectEqual(FigStatus.ok, fig_editor_delete_key(ed, &path, 1));
try expectEditorSource(ed, "b:\n y: 2\n");
}
// INI has the delete/move/reorder three and rename — its parser records
// every `[section]` header's name, reopenings included, so a rename
// reaches each — but not the two that write a header line: it declares
// no `section_header`, and a section has no `[[array]]` form.
{
const src = "[a]\nx = 1\n[b]\ny = 2\n[a]\nz = 3\n";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.ini), &ed));
defer fig_editor_destroy(ed);
const leaf = "q";
const body = "";
try std.testing.expectEqual(FigStatus.unsupported_format, fig_editor_insert_container(ed, &path, 1, body.ptr, body.len));
try std.testing.expectEqual(FigStatus.ok, fig_editor_rename_container(ed, &path, 1, leaf.ptr, leaf.len));
try expectEditorSource(ed, "[q]\nx = 1\n[b]\ny = 2\n[q]\nz = 3\n");
const q = [_]FigPathSegment{keySeg("q")};
try std.testing.expectEqual(FigStatus.ok, fig_editor_delete_container(ed, &q, 1));
try expectEditorSource(ed, "[b]\ny = 2\n");
}
}
test "fig_document_serialize materializes the YAML reference layer when leaving YAML" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
// `b` aliases the anchor defined on `a`; converting to JSON must expand it to
// a copied value, not leak `*x` or fail with unsupported_format.
const src = "a: &x 1\nb: *x\n";
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.yaml), &out_doc));
defer fig_document_destroy(out_doc);
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_document_serialize(out_doc, @intFromEnum(FigFormat.json), null, &ptr, &len));
try std.testing.expectEqualStrings("{\n \"a\": 1,\n \"b\": 1\n}\n", ptr[0..len]);
// YAML→YAML keeps the reference layer intact (no materialize).
try std.testing.expectEqual(FigStatus.ok, fig_document_serialize(out_doc, @intFromEnum(FigFormat.yaml), null, &ptr, &len));
try std.testing.expect(std.mem.indexOf(u8, ptr[0..len], "*x") != null);
}
test "fig_document_serialize honors the lossless option for TOML null" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
if (comptime !build_options.lang_toml) return error.SkipZigTest;
// A JSON null has no TOML representation. Lossy (default) reports it; lossless
// wraps it in a `$fig` envelope so the document still serializes.
const src = "{\"k\":null}";
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.json), &out_doc));
defer fig_document_destroy(out_doc);
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.unsupported_format, fig_document_serialize(out_doc, @intFromEnum(FigFormat.toml), null, &ptr, &len));
var opts: FigSerializeOptions = .{ .lossless = 1 };
try std.testing.expectEqual(FigStatus.ok, fig_document_serialize(out_doc, @intFromEnum(FigFormat.toml), &opts, &ptr, &len));
try std.testing.expect(std.mem.indexOf(u8, ptr[0..len], "$fig") != null);
}
test "fig_document_serialize preserves comments across formats" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
// The motivating case the parse→rebuild→fig_value_serialize detour could not
// serve: a comment captured from JSON5 re-emitted into YAML.
const src = "{\n // hello\n a: 1,\n}\n";
var out_doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, @intFromEnum(FigFormat.json5), &out_doc));
defer fig_document_destroy(out_doc);
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_document_serialize(out_doc, @intFromEnum(FigFormat.yaml), null, &ptr, &len));
try std.testing.expect(std.mem.indexOf(u8, ptr[0..len], "# hello") != null);
try std.testing.expect(std.mem.indexOf(u8, ptr[0..len], "a: 1") != null);
// strip_comments drops it.
var opts: FigSerializeOptions = .{ .strip_comments = 1 };
try std.testing.expectEqual(FigStatus.ok, fig_document_serialize(out_doc, @intFromEnum(FigFormat.yaml), &opts, &ptr, &len));
try std.testing.expect(std.mem.indexOf(u8, ptr[0..len], "hello") == null);
}
test "fig_version matches build options and string form" {
const v = fig_version();
try std.testing.expectEqual(@as(u32, build_options.version_major), v >> 16);
try std.testing.expectEqual(@as(u32, build_options.version_minor), (v >> 8) & 0xFF);
try std.testing.expectEqual(@as(u32, build_options.version_patch), v & 0xFF);
const s = fig_version_string();
const expected = std.fmt.comptimePrint("{d}.{d}.{d}", .{
build_options.version_major,
build_options.version_minor,
build_options.version_patch,
});
try std.testing.expectEqualStrings(expected, std.mem.span(s));
}
test "a runtime language registers through the C ABI and is a peer at every entry point" {
defer Runtime.deinitAll();
var alloc: Runtime.test_language.Alloc = .{ .allocator = std.testing.allocator };
const vt = Runtime.test_language.vtable(&alloc);
// Register; the integer is in the runtime range, resolves by name, and
// reports the record's capabilities.
var format: c_int = 0;
var err: FigError = Runtime.ErrorInfo.empty;
try std.testing.expectEqual(FigStatus.ok, fig_language_register(&vt, &format, &err));
try std.testing.expect(format >= Languages.runtime_abi_base);
try std.testing.expectEqual(format, fig_format_by_name("tinykv"));
try std.testing.expectEqual(@as(c_int, -1), fig_format_by_name("nosuch"));
try std.testing.expectEqual(@as(c_int, 1), fig_format_by_name("json"));
const all = @intFromEnum(FigCapability.read) | @intFromEnum(FigCapability.edit) | @intFromEnum(FigCapability.serialize);
try std.testing.expectEqual(all, fig_format_capabilities(format));
try std.testing.expectEqual(@as(u32, 0), fig_format_capabilities(format + 7));
// A second registration of the same name is refused with the reason.
var again: c_int = 0;
try std.testing.expectEqual(FigStatus.invalid_argument, fig_language_register(&vt, &again, &err));
try std.testing.expect(std.mem.indexOf(u8, err.text(), "already registered") != null);
try std.testing.expectEqual(@as(c_int, -1), again);
// Parse, walk, convert out — and convert a JSON document in.
const src = "# note\nx=1\ny=two\n";
var doc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(src.ptr, src.len, format, &doc));
defer fig_document_destroy(doc.?);
const root = fig_document_root(doc);
try std.testing.expectEqual(@as(c_int, @intFromEnum(FigNodeKind.mapping)), fig_node_kind(doc, root));
try std.testing.expectEqual(@as(usize, 2), fig_node_child_count(doc, root));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
if (comptime build_options.lang_json) {
try std.testing.expectEqual(FigStatus.ok, fig_document_serialize(doc, @intFromEnum(FigFormat.json), null, &ptr, &len));
try std.testing.expectEqualStrings("{\n \"x\": \"1\",\n \"y\": \"two\"\n}\n", ptr[0..len]);
const json = "{\"k\": \"v\"}";
var jdoc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(json.ptr, json.len, @intFromEnum(FigFormat.json), &jdoc));
defer fig_document_destroy(jdoc.?);
try std.testing.expectEqual(FigStatus.ok, fig_document_serialize(jdoc, format, null, &ptr, &len));
try std.testing.expectEqualStrings("k=v\n", ptr[0..len]);
// Diagnose against a runtime target answers from its record:
// nothing lost from a flat string map, and a table nested past its
// `max_mapping_depth` of 0 dropped, as the print drops it.
var n: usize = 9;
try std.testing.expectEqual(FigStatus.ok, fig_document_diagnose(jdoc, format, null, &n));
try std.testing.expectEqual(@as(usize, 0), n);
const nested = "{\"a\": {\"b\": \"1\"}, \"c\": \"2\"}";
var ndoc: ?*FigDocument = null;
try std.testing.expectEqual(FigStatus.ok, fig_parse(nested.ptr, nested.len, @intFromEnum(FigFormat.json), &ndoc));
defer fig_document_destroy(ndoc.?);
try std.testing.expectEqual(FigStatus.ok, fig_document_diagnose(ndoc, format, null, &n));
try std.testing.expectEqual(@as(usize, 1), n);
var w: FigWarning = .{ .size = @sizeOf(FigWarning), .code = 0, .cause = 0, .path = null, .path_len = 0, .note = null, .note_len = 0 };
try std.testing.expectEqual(FigStatus.ok, fig_document_warning(ndoc, 0, &w));
try std.testing.expectEqual(warningCodeInt(.value_dropped), w.code);
try std.testing.expectEqualStrings("a", w.path[0..w.path_len]);
}
// Print it as itself, comment and all.
try std.testing.expectEqual(FigStatus.ok, fig_document_serialize(doc, format, null, &ptr, &len));
try std.testing.expectEqualStrings(src, ptr[0..len]);
// A parse failure carries the helper's message and offset.
const bad = "x=1\nnope\n";
var bad_doc: ?*FigDocument = null;
var perr: FigError = Runtime.ErrorInfo.empty;
try std.testing.expectEqual(FigStatus.parse_error, fig_parse_ex(bad.ptr, bad.len, format, &bad_doc, &perr));
try std.testing.expectEqualStrings("expected key=value", perr.text());
try std.testing.expectEqual(@as(usize, 4), perr.byte_offset);
// Edit through the one runtime editor arm.
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, format, &ed));
defer fig_editor_destroy(ed);
const seg = [_]FigPathSegment{.{ .kind = 0, .key_ptr = "x", .key_len = 1, .index = 0 }};
const ten = "10";
try std.testing.expectEqual(FigStatus.ok, fig_editor_replace_val(ed, &seg, 1, ten.ptr, ten.len));
const z = "z";
const three = "3";
try std.testing.expectEqual(FigStatus.ok, fig_editor_insert_key(ed, null, 0, z.ptr, z.len, three.ptr, three.len));
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings("# note\nx=10\ny=two\nz=3\n", ptr[0..len]);
// tinykv declares no sections, so the whole-container ops are not its,
// as they are not YAML's: `unsupported_format`, and nothing changed.
try std.testing.expectEqual(FigStatus.unsupported_format, fig_editor_delete_container(ed, &seg, 1));
try std.testing.expectEqual(FigStatus.unsupported_format, fig_editor_insert_container(ed, &seg, 1, three.ptr, three.len));
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings("# note\nx=10\ny=two\nz=3\n", ptr[0..len]);
// A value built by hand prints through the vtable too.
var value: ?*FigValue = null;
try std.testing.expectEqual(FigStatus.ok, fig_value_create(&value));
defer fig_value_destroy(value);
var k: FigNodeId = undefined;
var v: FigNodeId = undefined;
var m: FigNodeId = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_string(value, "a", 1, &k));
try std.testing.expectEqual(FigStatus.ok, fig_value_string(value, "b", 1, &v));
const pair = [_]FigKeyValue{.{ .key = k, .value = v }};
try std.testing.expectEqual(FigStatus.ok, fig_value_map(value, &pair, 1, &m));
try std.testing.expectEqual(FigStatus.ok, fig_value_serialize(value, m, format, &ptr, &len));
try std.testing.expectEqualStrings("a=b\n", ptr[0..len]);
}
test "fig_format_capabilities reports the per-format matrix" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
const read = @intFromEnum(FigCapability.read);
const edit = @intFromEnum(FigCapability.edit);
const serialize = @intFromEnum(FigCapability.serialize);
const references = @intFromEnum(FigCapability.references);
// JSON family: always fully supported, regardless of build options.
for ([_]FigFormat{ .json, .jsonc, .json5 }) |f| {
try std.testing.expectEqual(read | edit | serialize, fig_format_capabilities(@intFromEnum(f)));
}
// Gated formats: capabilities track both inherent support and the build gate.
// YAML alone has a reference layer.
try std.testing.expectEqual(
if (build_options.lang_yaml) read | edit | serialize | references else 0,
fig_format_capabilities(@intFromEnum(FigFormat.yaml)),
);
try std.testing.expectEqual(
if (build_options.lang_toml) read | edit | serialize else 0,
fig_format_capabilities(@intFromEnum(FigFormat.toml)),
);
try std.testing.expectEqual(
if (build_options.lang_zon) read | edit | serialize else 0,
fig_format_capabilities(@intFromEnum(FigFormat.zon)),
);
try std.testing.expectEqual(
if (build_options.lang_fig) read | edit | serialize else 0,
fig_format_capabilities(@intFromEnum(FigFormat.fig)),
);
// Unknown / out-of-range format values report no capabilities.
try std.testing.expectEqual(@as(u32, 0), fig_format_capabilities(0));
try std.testing.expectEqual(@as(u32, 0), fig_format_capabilities(9999));
try std.testing.expectEqual(@as(u32, 0), fig_format_capabilities(-1));
}
test "fig_format_capabilities agrees with actual READ/EDIT/SERIALIZE behavior" {
// The capability matrix is only useful if it matches reality. Rather than
// re-encode the matrix as a hand-maintained constant (which drifts silently
// when core gains a capability the ABI hasn't surfaced — exactly how TOML
// editing stayed hidden), assert each advertised bit against what the
// corresponding entry point actually does on a valid input. This fails the
// build the moment a format's real capability and its bit diverge — in either
// direction, and under any build-flag combination.
const read = @intFromEnum(FigCapability.read);
const edit = @intFromEnum(FigCapability.edit);
const serialize = @intFromEnum(FigCapability.serialize);
const Case = struct { fmt: FigFormat, sample: []const u8 };
const cases = [_]Case{
.{ .fmt = .json, .sample = "{\"a\":1}" },
.{ .fmt = .jsonc, .sample = "{\"a\":1}" },
.{ .fmt = .json5, .sample = "{a:1}" },
.{ .fmt = .yaml, .sample = "a: 1\n" },
.{ .fmt = .toml, .sample = "a = 1\n" },
.{ .fmt = .zon, .sample = ".{ .a = 1 }" },
.{ .fmt = .fig, .sample = "a = 1\n" },
};
// A value every writable format can represent ({"a": 1}); used for the
// SERIALIZE probe so a failure can only mean "format not writable", never
// "value unrepresentable in this format".
var value: ?*FigValue = null;
try std.testing.expectEqual(FigStatus.ok, fig_value_create(&value));
defer fig_value_destroy(value);
var id: FigNodeId = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_value_int(value, 1, &id));
const v_one = id;
try std.testing.expectEqual(FigStatus.ok, fig_value_string(value, "a", 1, &id));
const k_a = id;
const entries = [_]FigKeyValue{.{ .key = k_a, .value = v_one }};
try std.testing.expectEqual(FigStatus.ok, fig_value_map(value, &entries, entries.len, &id));
const root = id;
for (cases) |c| {
const fmt = @intFromEnum(c.fmt);
const caps = fig_format_capabilities(fmt);
// READ: a valid sample parses iff the format is compiled in (which is
// exactly when the read bit is set).
var doc: ?*FigDocument = null;
const parse_status = fig_parse(c.sample.ptr, c.sample.len, fmt, &doc);
defer if (doc != null) fig_document_destroy(doc);
try std.testing.expectEqual((caps & read) != 0, parse_status == .ok);
// EDIT: create rejects a non-editable or gated format with
// unsupported_format before it ever parses, so a valid sample never
// yields parse_error here — the only non-ok outcome is unsupported_format.
var ed: ?*FigEditor = null;
const edit_status = fig_editor_create(c.sample.ptr, c.sample.len, fmt, &ed);
defer if (ed != null) fig_editor_destroy(ed);
try std.testing.expectEqual((caps & edit) != 0, edit_status != .unsupported_format);
// SERIALIZE: rendering a representable value succeeds iff the format is
// writable in this build.
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
const ser_status = fig_value_serialize(value, root, fmt, &ptr, &len);
try std.testing.expectEqual((caps & serialize) != 0, ser_status != .unsupported_format);
}
}
test "fig_editor comment ops add, set, and delete through the C ABI" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const src = "a: 1\nb: 2\n";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.yaml), &ed));
defer fig_editor_destroy(ed);
// path = ["b"]
var key = [_]u8{'b'};
const path = [_]FigPathSegment{.{ .kind = 0, .key_ptr = &key, .key_len = 1, .index = 0 }};
const leading = "why";
try std.testing.expectEqual(FigStatus.ok, fig_editor_add_leading_comment(ed, &path, 1, leading.ptr, leading.len));
const trailing = "two";
try std.testing.expectEqual(FigStatus.ok, fig_editor_set_trailing_comment(ed, &path, 1, trailing.ptr, trailing.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings("a: 1\n# why\nb: 2 # two\n", ptr[0..len]);
// Delete both back out.
try std.testing.expectEqual(FigStatus.ok, fig_editor_delete_trailing_comment(ed, &path, 1));
try std.testing.expectEqual(FigStatus.ok, fig_editor_delete_leading_comments(ed, &path, 1));
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings("a: 1\nb: 2\n", ptr[0..len]);
}
test "fig_editor_set_sequence reconciles a list, preserving survivors' comments" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const src = "tags:\n- a # first\n- b # second\n- c # third\n";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.yaml), &ed));
defer fig_editor_destroy(ed);
var key = [_]u8{ 't', 'a', 'g', 's' };
const path = [_]FigPathSegment{.{ .kind = 0, .key_ptr = &key, .key_len = key.len, .index = 0 }};
// -> [c, a, d]: drop b, add d, reorder. a and c keep their comments.
const items = [_]FigStr{
.{ .ptr = "c", .len = 1 },
.{ .ptr = "a", .len = 1 },
.{ .ptr = "d", .len = 1 },
};
try std.testing.expectEqual(FigStatus.ok, fig_editor_set_sequence(ed, &path, path.len, &items, items.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings("tags:\n- c # third\n- a # first\n- d\n", ptr[0..len]);
// An empty target is declined with invalid_argument (caller falls back).
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_set_sequence(ed, &path, path.len, &items, 0));
}
test "fig_editor comment ops reject strict JSON with unsupported_format" {
if (comptime !build_options.lang_json) return error.SkipZigTest;
const src = "{\"a\":1}";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.json), &ed));
defer fig_editor_destroy(ed);
var key = [_]u8{'a'};
const path = [_]FigPathSegment{.{ .kind = 0, .key_ptr = &key, .key_len = 1, .index = 0 }};
const text = "x";
try std.testing.expectEqual(FigStatus.unsupported_format, fig_editor_add_leading_comment(ed, &path, 1, text.ptr, text.len));
try std.testing.expectEqual(FigStatus.unsupported_format, fig_editor_delete_trailing_comment(ed, &path, 1));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.unsupported_format, fig_editor_get_leading_comment(ed, &path, 1, &ptr, &len));
try std.testing.expectEqual(FigStatus.unsupported_format, fig_editor_get_trailing_comment(ed, &path, 1, &ptr, &len));
}
test "fig_editor comment ops on a one-line flow item answer as if it had none" {
if (comptime !build_options.lang_toml) return error.SkipZigTest;
// The item sits on `members`' line, so the block above and the comment at
// the line's end are `members`'. Every op used to reach them through the
// item; across the ABI that is `invalid_argument` for the writes and
// `not_found` (= absent) for the reads.
const src = "# above members\nmembers = [\"a\", \"b\"] # note\n";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.toml), &ed));
defer fig_editor_destroy(ed);
var key = [_]u8{ 'm', 'e', 'm', 'b', 'e', 'r', 's' };
const item0 = [_]FigPathSegment{
.{ .kind = 0, .key_ptr = &key, .key_len = key.len, .index = 0 },
.{ .kind = 1, .key_ptr = null, .key_len = 0, .index = 0 },
};
const text = "mine";
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_add_leading_comment(ed, &item0, item0.len, text.ptr, text.len));
try std.testing.expectEqual(FigStatus.invalid_argument, fig_editor_set_trailing_comment(ed, &item0, item0.len, text.ptr, text.len));
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
try std.testing.expectEqual(FigStatus.not_found, fig_editor_get_leading_comment(ed, &item0, item0.len, &ptr, &len));
try std.testing.expectEqual(FigStatus.not_found, fig_editor_get_trailing_comment(ed, &item0, item0.len, &ptr, &len));
// The deletes report success and change nothing.
try std.testing.expectEqual(FigStatus.ok, fig_editor_delete_leading_comments(ed, &item0, item0.len));
try std.testing.expectEqual(FigStatus.ok, fig_editor_delete_trailing_comment(ed, &item0, item0.len));
try std.testing.expectEqual(FigStatus.ok, fig_editor_source(ed, &ptr, &len));
try std.testing.expectEqualStrings(src, ptr[0..len]);
// `members` itself still owns both.
const parent = [_]FigPathSegment{item0[0]};
try std.testing.expectEqual(FigStatus.ok, fig_editor_get_leading_comment(ed, &parent, 1, &ptr, &len));
try std.testing.expectEqualStrings("above members", ptr[0..len]);
try std.testing.expectEqual(FigStatus.ok, fig_editor_get_trailing_comment(ed, &parent, 1, &ptr, &len));
try std.testing.expectEqualStrings("note", ptr[0..len]);
}
test "fig_editor comment reads return bytes, distinguishing absent from empty" {
if (comptime !build_options.lang_yaml) return error.SkipZigTest;
const src = "# why\na: 1 # two\nb: 2 #\nc: 3\n";
var ed: ?*FigEditor = null;
try std.testing.expectEqual(FigStatus.ok, fig_editor_create(src.ptr, src.len, @intFromEnum(FigFormat.yaml), &ed));
defer fig_editor_destroy(ed);
var ptr: [*c]const u8 = undefined;
var len: usize = undefined;
const at = struct {
fn key(k: *[1]u8) [1]FigPathSegment {
return .{.{ .kind = 0, .key_ptr = k, .key_len = 1, .index = 0 }};
}
};
// a: present leading ("why") and present trailing ("two").
var ka = [_]u8{'a'};
const pa = at.key(&ka);
try std.testing.expectEqual(FigStatus.ok, fig_editor_get_leading_comment(ed, &pa, 1, &ptr, &len));
try std.testing.expectEqualStrings("why", ptr[0..len]);
try std.testing.expectEqual(FigStatus.ok, fig_editor_get_trailing_comment(ed, &pa, 1, &ptr, &len));
try std.testing.expectEqualStrings("two", ptr[0..len]);
// b: a bare `#` trailing → present but EMPTY (ok, len 0), not absent.
var kb = [_]u8{'b'};
const pb = at.key(&kb);
try std.testing.expectEqual(FigStatus.ok, fig_editor_get_trailing_comment(ed, &pb, 1, &ptr, &len));
try std.testing.expectEqual(@as(usize, 0), len);
// c: no comment either way → not_found (absent).
var kc = [_]u8{'c'};
const pc = at.key(&kc);
try std.testing.expectEqual(FigStatus.not_found, fig_editor_get_leading_comment(ed, &pc, 1, &ptr, &len));
try std.testing.expectEqual(FigStatus.not_found, fig_editor_get_trailing_comment(ed, &pc, 1, &ptr, &len));
}
test "editStatus: every editor refusal is a caller error, not parse_error" {
// `editStatus`'s fall-through is `.parse_error` — the right answer for a
// reparse failure after a splice, and the wrong one for every REFUSAL, which
// is about the request rather than the source. A new refusal error that
// nobody adds here inherits that fall-through silently, so this pins the set:
// each of these is raised by an editor op (not by a parser) and must map to a
// caller-facing status. INI's and fig's delete guards had drifted this way.
const refusals = [_]anyerror{
error.NotFound, error.NotAMapping,
error.NotASequence, error.NotAContainer,
error.UnsupportedShape, error.NotATable,
error.NotAnInlineArray, error.NotAnArrayOfTables,
error.TableExists, error.SectionExists,
error.ContainerExists, error.DuplicateKey,
error.MergeOnlyKey, error.CannotDeleteTable,
error.CannotDeleteSection, error.CannotDeleteContainer,
error.CannotReplaceTable, error.CannotReplaceSection,
error.CannotReplaceContainer, error.CannotMoveContainer,
error.CannotReorderContainers, error.CannotMoveTable,
error.CannotMoveSection, error.CannotReorderTables,
error.CannotReorderSections, error.EmptyInlineContainer,
error.KeyRequiresMultilineForm, error.BlockValueIntoFlow,
error.CommentsUnsupported, error.NullUnsupported,
error.MultilineComment, error.InvalidComment,
error.CommentsUnanchored, error.RendererRefused,
error.ImplicitSection, error.ContainerClosesOnItsLine,
error.NotAKey,
};
for (refusals) |err| {
const status = editStatus(err);
if (status == .parse_error) {
std.log.err("editStatus({s}) is parse_error; add it to the mapping", .{@errorName(err)});
return error.TestUnexpectedResult;
}
}
// The two fig PARSER errors an edit's reparse surfaces stay parse errors:
// they describe source that no longer parses, which is what the caller needs
// to hear.
try std.testing.expectEqual(FigStatus.parse_error, editStatus(error.FigEmptyContainer));
try std.testing.expectEqual(FigStatus.parse_error, editStatus(error.FigDuplicateKey));
}