1extern crate self as fig;
4
5mod diagnostics;
6mod editor;
7mod embed;
8mod error;
9mod value;
10
11pub(crate) use fig_sys as ffi;
15
16#[cfg(feature = "derive")]
17mod convert;
18#[cfg(feature = "serde")]
19mod de;
20#[cfg(feature = "serde")]
21mod ser;
22
23use std::os::raw::c_int;
24use std::ptr::NonNull;
25
26pub use diagnostics::{Warning, WarningCause, WarningCode};
27pub use editor::{Editor, Segment};
28pub use embed::{Embed, EmbedType, Extracted, Region, Span, detect, split};
29pub use error::{Error, ParseError};
30pub use value::{ExtKind, Value};
31
32#[cfg(feature = "derive")]
33pub use convert::{FromValue, ToValue};
34#[cfg(feature = "derive")]
37#[doc(hidden)]
38pub use convert::{field, field_or_default, map_get};
39#[cfg(feature = "derive")]
42pub use fig_macros::{FromValue, ToValue};
43
44#[cfg(feature = "serde")]
45pub use de::{from_slice, from_str};
46#[cfg(feature = "serde")]
47pub use ser::{to_string, to_value};
48
49use ffi::{FIG_NODE_NONE, FigNodeId, FigNodeKind};
50
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65#[non_exhaustive]
66pub enum Format {
67 Json,
68 Jsonc,
69 Json5,
70 Yaml,
71 Toml,
72 Zon,
73 Fig,
76}
77
78impl From<Format> for ffi::FigFormat {
79 fn from(format: Format) -> Self {
80 match format {
81 Format::Json => ffi::FigFormat::Json,
82 Format::Jsonc => ffi::FigFormat::Jsonc,
83 Format::Json5 => ffi::FigFormat::Json5,
84 Format::Yaml => ffi::FigFormat::Yaml,
85 Format::Toml => ffi::FigFormat::Toml,
86 Format::Zon => ffi::FigFormat::Zon,
87 Format::Fig => ffi::FigFormat::Fig,
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103#[non_exhaustive]
104pub struct SerializeOptions {
105 pub pretty: bool,
109 pub indent: u8,
112 pub strip_comments: bool,
115 pub lossless: bool,
121 pub width: u16,
138}
139
140impl Default for SerializeOptions {
141 fn default() -> Self {
142 Self { pretty: true, indent: 2, strip_comments: false, lossless: false, width: 80 }
143 }
144}
145
146impl SerializeOptions {
152 pub fn compact() -> Self {
154 Self { pretty: false, ..Self::default() }
155 }
156
157 pub fn pretty(indent: u8) -> Self {
159 Self { pretty: true, indent, ..Self::default() }
160 }
161
162 pub fn indent(self, indent: u8) -> Self {
166 Self { indent, ..self }
167 }
168
169 pub fn lossless(self) -> Self {
172 Self { lossless: true, ..self }
173 }
174
175 pub fn strip_comments(self) -> Self {
177 Self { strip_comments: true, ..self }
178 }
179
180 pub fn width(self, width: u16) -> Self {
185 Self { width, ..self }
186 }
187}
188
189impl From<SerializeOptions> for ffi::FigSerializeOptions {
190 fn from(o: SerializeOptions) -> Self {
191 ffi::FigSerializeOptions {
192 size: std::mem::size_of::<ffi::FigSerializeOptions>() as u32,
193 pretty: u8::from(o.pretty),
194 indent: o.indent,
195 strip_comments: u8::from(o.strip_comments),
196 lossless: u8::from(o.lossless),
197 width: o.width,
198 flow: 0,
201 }
202 }
203}
204
205#[derive(Clone, Copy, Debug, Eq, PartialEq)]
207#[non_exhaustive]
208pub struct Version {
209 pub major: u8,
210 pub minor: u8,
211 pub patch: u8,
212}
213
214pub fn version() -> Version {
217 let packed = unsafe { ffi::fig_version() };
218 Version {
219 major: (packed >> 16) as u8,
220 minor: (packed >> 8) as u8,
221 patch: packed as u8,
222 }
223}
224
225pub fn version_string() -> &'static str {
227 let ptr = unsafe { ffi::fig_version_string() };
230 unsafe { std::ffi::CStr::from_ptr(ptr) }
231 .to_str()
232 .unwrap_or("")
233}
234
235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
240#[non_exhaustive]
241pub struct Capabilities {
242 pub read: bool,
244 pub edit: bool,
246 pub serialize: bool,
248}
249
250pub fn capabilities(format: Format) -> Capabilities {
253 let ffi_format: ffi::FigFormat = format.into();
254 let bits = unsafe { ffi::fig_format_capabilities(ffi_format as c_int) };
255 Capabilities {
256 read: bits & (1 << 0) != 0,
257 edit: bits & (1 << 1) != 0,
258 serialize: bits & (1 << 2) != 0,
259 }
260}
261
262#[derive(Debug)]
263pub struct Document {
264 raw: NonNull<ffi::FigDocument>,
265}
266
267impl Document {
268 pub fn parse(input: &[u8], format: Format) -> Result<Self, Error> {
269 let mut raw = std::ptr::null_mut();
270 let ffi_format: ffi::FigFormat = format.into();
271
272 let mut err = ffi::FigError::new();
276 let status = unsafe {
277 ffi::fig_parse_ex(input.as_ptr(), input.len(), ffi_format as i32, &mut raw, &mut err)
278 };
279 if status != ffi::FigStatus(ffi::FigStatus::OK) {
280 if status == ffi::FigStatus(ffi::FigStatus::PARSE_ERROR) {
281 return Err(Error::Parse(crate::error::ParseError::from_ffi(&err)));
282 }
283 Error::from_status(status)?;
284 }
285
286 let raw = NonNull::new(raw).ok_or(Error::Internal)?;
287 Ok(Self { raw })
288 }
289}
290
291impl Document {
294 pub fn to_value(&self) -> Result<Value, Error> {
298 match self.root() {
299 None => Ok(Value::Null),
300 Some(id) => self.node_to_value(id),
301 }
302 }
303
304 fn node_to_value(&self, id: FigNodeId) -> Result<Value, Error> {
305 if let Some((kind, text)) = self.extended(id) {
308 return Ok(Value::Extended { kind, text });
309 }
310 let kind = self.kind(id);
311 match kind {
312 FigNodeKind::Null => Ok(Value::Null),
313 FigNodeKind::Bool => Ok(Value::Bool(self.get_bool(id).ok_or(Error::Internal)?)),
314 FigNodeKind::Int | FigNodeKind::Float => {
315 let raw = self.number_raw(id).ok_or(Error::Internal)??;
316 value::number_from_raw(raw, kind == FigNodeKind::Float)
317 }
318 FigNodeKind::String => Ok(Value::Str(
319 self.get_str(id).ok_or(Error::Internal)??.to_owned(),
320 )),
321 FigNodeKind::Sequence => {
322 let mut items = Vec::with_capacity(self.child_count(id));
323 let mut next = self.first_child(id);
324 while let Some(child) = next {
325 items.push(self.node_to_value(child)?);
326 next = self.next_sibling(child);
327 }
328 Ok(Value::Seq(items))
329 }
330 FigNodeKind::Mapping => {
331 let mut entries = Vec::with_capacity(self.child_count(id));
332 let mut next = self.first_child(id);
333 while let Some(kv) = next {
334 let key = self.kv_key(kv).ok_or(Error::Internal)?;
335 let value = match self.kv_value(kv) {
336 Some(vid) => self.node_to_value(vid)?,
337 None => Value::Null,
338 };
339 entries.push((self.node_to_value(key)?, value));
340 next = self.next_sibling(kv);
341 }
342 Ok(Value::Map(entries))
343 }
344 FigNodeKind::Keyvalue | FigNodeKind::Invalid | FigNodeKind::Alias => {
347 Err(Error::Internal)
348 }
349 }
350 }
351
352 fn ptr(&self) -> *const ffi::FigDocument {
353 self.raw.as_ptr()
354 }
355
356 pub(crate) fn root(&self) -> Option<FigNodeId> {
358 normalize(unsafe { ffi::fig_document_root(self.ptr()) })
359 }
360
361 pub(crate) fn kind(&self, node: FigNodeId) -> FigNodeKind {
362 FigNodeKind::from_c(unsafe { ffi::fig_node_kind(self.ptr(), node) })
365 }
366
367 pub(crate) fn first_child(&self, node: FigNodeId) -> Option<FigNodeId> {
368 normalize(unsafe { ffi::fig_node_first_child(self.ptr(), node) })
369 }
370
371 pub(crate) fn next_sibling(&self, node: FigNodeId) -> Option<FigNodeId> {
372 normalize(unsafe { ffi::fig_node_next_sibling(self.ptr(), node) })
373 }
374
375 pub(crate) fn child_count(&self, node: FigNodeId) -> usize {
376 unsafe { ffi::fig_node_child_count(self.ptr(), node) }
377 }
378
379 pub(crate) fn kv_key(&self, node: FigNodeId) -> Option<FigNodeId> {
380 normalize(unsafe { ffi::fig_keyvalue_key(self.ptr(), node) })
381 }
382
383 pub(crate) fn kv_value(&self, node: FigNodeId) -> Option<FigNodeId> {
384 normalize(unsafe { ffi::fig_keyvalue_value(self.ptr(), node) })
385 }
386
387 pub(crate) fn get_bool(&self, node: FigNodeId) -> Option<bool> {
388 let mut out = false;
389 unsafe { ffi::fig_node_bool(self.ptr(), node, &mut out) }.then_some(out)
390 }
391
392 pub(crate) fn number_raw(&self, node: FigNodeId) -> Option<Result<&str, Error>> {
394 self.scalar_bytes(node, ffi::fig_node_number)
395 .map(|bytes| std::str::from_utf8(bytes).map_err(|_| Error::Utf8))
396 }
397
398 pub(crate) fn get_str(&self, node: FigNodeId) -> Option<Result<&str, Error>> {
400 self.scalar_bytes(node, ffi::fig_node_string)
401 .map(|bytes| std::str::from_utf8(bytes).map_err(|_| Error::Utf8))
402 }
403
404 pub(crate) fn extended(&self, node: FigNodeId) -> Option<(ExtKind, String)> {
409 let mut kind: c_int = 0;
410 let mut ptr: *const u8 = std::ptr::null();
411 let mut len: usize = 0;
412 let ok = unsafe { ffi::fig_node_extended(self.ptr(), node, &mut kind, &mut ptr, &mut len) };
413 if !ok {
414 return None;
415 }
416 let ext = ExtKind::from_c(kind)?;
417 let text = if len == 0 {
418 String::new()
419 } else {
420 let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
423 std::str::from_utf8(bytes).ok()?.to_owned()
424 };
425 Some((ext, text))
426 }
427
428 fn scalar_bytes(
432 &self,
433 node: FigNodeId,
434 accessor: unsafe extern "C" fn(
435 *const ffi::FigDocument,
436 FigNodeId,
437 *mut *const u8,
438 *mut usize,
439 ) -> bool,
440 ) -> Option<&[u8]> {
441 let mut ptr: *const u8 = std::ptr::null();
442 let mut len: usize = 0;
443 let ok = unsafe { accessor(self.ptr(), node, &mut ptr, &mut len) };
444 if !ok {
445 return None;
446 }
447 if len == 0 {
448 return Some(&[]);
449 }
450 Some(unsafe { std::slice::from_raw_parts(ptr, len) })
454 }
455}
456
457impl Document {
459 pub fn serialize(&self, format: Format) -> Result<String, Error> {
466 self.serialize_with(format, SerializeOptions::default())
467 }
468
469 pub fn serialize_with(&self, format: Format, options: SerializeOptions) -> Result<String, Error> {
472 let ffi_format: ffi::FigFormat = format.into();
473 let ffi_options: ffi::FigSerializeOptions = options.into();
474 let mut ptr_out: *const u8 = std::ptr::null();
475 let mut len: usize = 0;
476 Error::from_status(unsafe {
477 ffi::fig_document_serialize(
478 self.raw.as_ptr(),
479 ffi_format as c_int,
480 &ffi_options,
481 &mut ptr_out,
482 &mut len,
483 )
484 })?;
485 let bytes = if len == 0 {
488 &[][..]
489 } else {
490 unsafe { std::slice::from_raw_parts(ptr_out, len) }
491 };
492 Ok(std::str::from_utf8(bytes).map_err(|_| Error::Utf8)?.to_owned())
493 }
494
495 pub fn diagnose(&self, format: Format, options: SerializeOptions) -> Result<Vec<Warning>, Error> {
500 let ffi_format: ffi::FigFormat = format.into();
501 let ffi_options: ffi::FigSerializeOptions = options.into();
502 let mut count: usize = 0;
503 Error::from_status(unsafe {
504 ffi::fig_document_diagnose(self.raw.as_ptr(), ffi_format as c_int, &ffi_options, &mut count)
505 })?;
506 let mut out = Vec::with_capacity(count);
507 for i in 0..count {
508 let mut w = ffi::FigWarning::new();
509 Error::from_status(unsafe { ffi::fig_document_warning(self.raw.as_ptr(), i, &mut w) })?;
510 out.push(unsafe { Warning::from_ffi(&w) });
513 }
514 Ok(out)
515 }
516}
517
518impl Drop for Document {
519 fn drop(&mut self) {
520 unsafe {
521 ffi::fig_document_destroy(self.raw.as_ptr());
522 }
523 }
524}
525
526fn normalize(id: FigNodeId) -> Option<FigNodeId> {
527 if id == FIG_NODE_NONE { None } else { Some(id) }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::{Document, Embed, EmbedType, Error, Format, Segment};
533
534 #[test]
535 fn parses_json_document() {
536 let doc = Document::parse(br#"{"name":"fig","ok":true}"#, Format::Json);
537 assert!(doc.is_ok());
538 }
539
540 #[test]
541 fn parse_error_is_reported() {
542 let err = Document::parse(br#"{"name":"fig""#, Format::Json).unwrap_err();
543 let Error::Parse(detail) = &err else {
544 panic!("expected Error::Parse, got {err:?}");
545 };
546 assert!(!detail.message.is_empty());
549 assert_eq!(detail.byte_offset, None);
550 }
551
552 #[test]
553 fn version_and_capabilities() {
554 use super::{capabilities, version, version_string, Format};
555 let v = version();
556 assert_eq!(version_string(), format!("{}.{}.{}", v.major, v.minor, v.patch));
558 let json = capabilities(Format::Json);
560 assert!(json.read && json.edit && json.serialize);
561 }
562
563 #[test]
564 fn document_serialize_converts_cross_format() {
565 let doc = Document::parse(b"name: fig\nnums:\n- 1\n- 2\n", Format::Yaml).unwrap();
567 assert_eq!(
568 doc.serialize(Format::Json).unwrap(),
569 "{\n \"name\": \"fig\",\n \"nums\": [\n 1,\n 2\n ]\n}\n",
570 );
571 }
572
573 #[test]
574 #[cfg(feature = "toml")]
575 fn document_diagnose_reports_dropped_null() {
576 use super::{SerializeOptions, WarningCause, WarningCode};
577 let doc = Document::parse(b"a: null\nb: 1\n", Format::Yaml).unwrap();
578 let warns = doc.diagnose(Format::Toml, SerializeOptions::default()).unwrap();
580 assert_eq!(warns.len(), 1);
581 assert_eq!(warns[0].code, WarningCode::ValueDropped);
582 assert_eq!(warns[0].cause, WarningCause::FormatLimitation);
583 assert_eq!(warns[0].path, "a");
584 let none = doc
586 .diagnose(Format::Toml, SerializeOptions::default().lossless())
587 .unwrap();
588 assert!(none.is_empty());
589 }
590
591 #[test]
592 fn parse_error_message_is_surfaced_in_display() {
593 let err = Document::parse(br#"{"name":"fig""#, Format::Json).unwrap_err();
594 assert!(err.to_string().starts_with("failed to parse input: "));
596 }
597
598 #[test]
599 fn editor_comment_ops_add_set_and_delete() {
600 use super::{Editor, Segment};
601 let mut ed = Editor::open(b"a: 1\nb: 2\n", Format::Yaml).unwrap();
602 ed.add_leading_comment(&[Segment::Key("b")], "why").unwrap();
603 ed.set_trailing_comment(&[Segment::Key("b")], "two").unwrap();
604 assert_eq!(ed.source().unwrap(), "a: 1\n# why\nb: 2 # two\n");
605 ed.delete_trailing_comment(&[Segment::Key("b")]).unwrap();
606 ed.delete_leading_comments(&[Segment::Key("b")]).unwrap();
607 assert_eq!(ed.source().unwrap(), "a: 1\nb: 2\n");
608 }
609
610 #[test]
611 fn editor_comments_unsupported_in_strict_json() {
612 use super::{Editor, Error, Segment};
613 let mut ed = Editor::open(br#"{"a":1}"#, Format::Json).unwrap();
614 assert!(matches!(
615 ed.add_leading_comment(&[Segment::Key("a")], "x"),
616 Err(Error::UnsupportedFormat)
617 ));
618 }
619
620 #[test]
621 fn frontmatter_reorder_keys_preserves_comments_and_body() {
622 let md = "---\ntitle: Hi\n# a comment\ntags:\n- x\nauthor: me\n---\n# Body\n";
623 let mut fm = Embed::open(md.as_bytes(), EmbedType::FrontmatterYaml).unwrap();
624 let order = vec![String::from("author"), String::from("title")];
626 fm.reorder_keys(&[], &order).unwrap();
627 assert_eq!(
628 fm.render().unwrap(),
629 "---\nauthor: me\ntitle: Hi\n# a comment\ntags:\n- x\n---\n# Body\n",
630 );
631 }
632
633 #[test]
634 fn frontmatter_move_key_preserves_comments_and_body() {
635 let md = "---\na: 1\n# note for c\nc: 3\nb: 2\n---\nbody\n";
636 let mut fm = Embed::open(md.as_bytes(), EmbedType::FrontmatterYaml).unwrap();
637 fm.move_key(&[Segment::Key("c")], &[Segment::Key("a")])
638 .unwrap();
639 assert_eq!(
640 fm.render().unwrap(),
641 "---\n# note for c\nc: 3\na: 1\nb: 2\n---\nbody\n",
642 );
643 }
644
645 #[test]
646 fn frontmatter_reorder_items_in_block_sequence() {
647 let md = "---\ntags:\n- x\n- y\n- z\n---\nbody\n";
648 let mut fm = Embed::open(md.as_bytes(), EmbedType::FrontmatterYaml).unwrap();
649 fm.reorder_items(&[Segment::Key("tags")], &[2, 0]).unwrap();
650 assert_eq!(
651 fm.render().unwrap(),
652 "---\ntags:\n- z\n- x\n- y\n---\nbody\n",
653 );
654 }
655
656 #[test]
657 fn frontmatter_move_item_in_flow_sequence_keeps_separators() {
658 let md = "---\ntags: [x, y, z]\n---\nbody\n";
659 let mut fm = Embed::open(md.as_bytes(), EmbedType::FrontmatterYaml).unwrap();
660 fm.move_item(&[Segment::Key("tags")], 2, 0).unwrap();
661 assert_eq!(fm.render().unwrap(), "---\ntags: [z, x, y]\n---\nbody\n");
662 }
663
664 #[test]
665 fn html_code_edit_is_span_aware_over_the_entity_codec() {
666 let html =
670 "<pre><code class=\"language-figl\">\nexpr = \"a < b\"\nnote = \"x < y\"\n</code></pre>\n";
671 let mut ec = Embed::open(html.as_bytes(), EmbedType::HtmlCodeFig).unwrap();
672 ec.replace_value(&[Segment::Key("expr")], "p > q").unwrap();
673 assert_eq!(
676 ec.render().unwrap(),
677 "<pre><code class=\"language-figl\">\nexpr = p > q\nnote = \"x < y\"\n</code></pre>\n",
678 );
679 }
680}