@@ -24,6 +24,8 @@ use std::{
2424 collections:: { BTreeMap , HashMap } ,
2525} ;
2626
27+ use indexmap:: IndexMap ;
28+
2729use serde:: { Deserialize , Serialize } ;
2830use thiserror:: Error ;
2931
@@ -40,9 +42,9 @@ use crate::{
4042/// format.
4143#[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize , Default ) ]
4244pub struct PantryConf {
43- /// Map of sections to their items (BTreeMap for consistent ordering )
45+ /// Map of sections to their items (IndexMap to preserve file order )
4446 #[ serde( flatten) ]
45- pub sections : BTreeMap < String , Vec < PantryItem > > ,
47+ pub sections : IndexMap < String , Vec < PantryItem > > ,
4648
4749 /// Index for fast ingredient lookups (lowercase name -> (section, index))
4850 /// Using BTreeMap for better cache locality and predictable iteration
@@ -327,7 +329,7 @@ fn parse_core(
327329 message : "Expected TOML table at root" . to_string ( ) ,
328330 } ) ?;
329331
330- let mut sections = BTreeMap :: new ( ) ;
332+ let mut sections = IndexMap :: new ( ) ;
331333 let mut general_items = Vec :: new ( ) ; // For top-level items
332334
333335 for ( section_name, section_value) in toml_table {
@@ -656,11 +658,74 @@ pub fn parse_lenient(input: &str) -> PassResult<PantryConf> {
656658 }
657659}
658660
661+ /// Serialize a [`PantryConf`] to a TOML string.
662+ ///
663+ /// Uses `toml_edit` so that keys are properly escaped (including non-ASCII
664+ /// characters) and item attributes are written as inline tables.
665+ pub fn to_toml_string ( conf : & PantryConf ) -> String {
666+ let mut doc = toml_edit:: DocumentMut :: new ( ) ;
667+
668+ // General items go at the top level as plain strings (the parser treats
669+ // top-level inline tables as section headers, not items).
670+ if let Some ( general_items) = conf. sections . get ( "general" ) {
671+ for item in general_items {
672+ doc[ item. name ( ) ] = general_item_to_toml_item ( item) ;
673+ }
674+ }
675+
676+ for ( section_name, items) in & conf. sections {
677+ if section_name == "general" {
678+ continue ;
679+ }
680+ let mut table = toml_edit:: Table :: new ( ) ;
681+ for item in items {
682+ table[ item. name ( ) ] = item_to_toml_item ( item) ;
683+ }
684+ doc[ section_name] = toml_edit:: Item :: Table ( table) ;
685+ }
686+
687+ doc. to_string ( )
688+ }
689+
659690/// Write a [`PantryConf`] in TOML format
660691pub fn write ( conf : & PantryConf , mut write : impl std:: io:: Write ) -> std:: io:: Result < ( ) > {
661- let toml_string = toml:: to_string_pretty ( conf)
662- . map_err ( |e| std:: io:: Error :: new ( std:: io:: ErrorKind :: Other , e) ) ?;
663- write. write_all ( toml_string. as_bytes ( ) )
692+ write. write_all ( to_toml_string ( conf) . as_bytes ( ) )
693+ }
694+
695+ /// Convert a pantry item to an inline-table `toml_edit::Item` for use
696+ /// inside a `[section]`.
697+ fn item_to_toml_item ( item : & PantryItem ) -> toml_edit:: Item {
698+ match item {
699+ PantryItem :: Simple ( _) => toml_edit:: value ( toml_edit:: InlineTable :: new ( ) ) ,
700+ PantryItem :: WithAttributes ( attrs) => {
701+ let mut table = toml_edit:: InlineTable :: new ( ) ;
702+ if let Some ( qty) = & attrs. quantity {
703+ table. insert ( "quantity" , qty. as_str ( ) . into ( ) ) ;
704+ }
705+ if let Some ( bought) = & attrs. bought {
706+ table. insert ( "bought" , bought. as_str ( ) . into ( ) ) ;
707+ }
708+ if let Some ( expire) = & attrs. expire {
709+ table. insert ( "expire" , expire. as_str ( ) . into ( ) ) ;
710+ }
711+ if let Some ( low) = & attrs. low {
712+ table. insert ( "low" , low. as_str ( ) . into ( ) ) ;
713+ }
714+ toml_edit:: value ( table)
715+ }
716+ }
717+ }
718+
719+ /// Convert a pantry item to a plain-string `toml_edit::Item` for use at the
720+ /// top level ("general" section). The parser only supports `key = "string"`
721+ /// at the top level.
722+ fn general_item_to_toml_item ( item : & PantryItem ) -> toml_edit:: Item {
723+ match item {
724+ PantryItem :: Simple ( _) => toml_edit:: value ( "" ) ,
725+ PantryItem :: WithAttributes ( attrs) => {
726+ toml_edit:: value ( attrs. quantity . as_deref ( ) . unwrap_or ( "" ) )
727+ }
728+ }
664729}
665730
666731/// Error generated by [`parse`].
@@ -987,7 +1052,7 @@ rice = "5%kg"
9871052 #[ test]
9881053 fn test_index_performance ( ) {
9891054 // Create a large pantry to test performance
990- let mut sections = BTreeMap :: new ( ) ;
1055+ let mut sections = IndexMap :: new ( ) ;
9911056
9921057 // Add 100 sections with 100 items each = 10,000 items
9931058 for section_num in 0 ..100 {
0 commit comments