use indexmap::IndexMap;
use serde_json::{Map as JsonMap, Value as JsonValue};
use super::prescan::{CommentPathSegment, NestedComment};
use crate::value::QuillValue;
use crate::version::QuillReference;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetaKey {
Ext,
Seed,
}
impl MetaKey {
pub fn as_str(self) -> &'static str {
match self {
MetaKey::Ext => "$ext",
MetaKey::Seed => "$seed",
}
}
pub fn from_key_str(key: &str) -> Option<Self> {
match key {
"$ext" => Some(MetaKey::Ext),
"$seed" => Some(MetaKey::Seed),
_ => None,
}
}
fn rank(self) -> u8 {
match self {
MetaKey::Ext => 3,
MetaKey::Seed => 4,
}
}
pub fn is_root_only(self) -> bool {
matches!(self, MetaKey::Seed)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PayloadItem {
Quill { reference: QuillReference },
Kind { value: String },
Id { value: String },
Meta {
key: MetaKey,
value: JsonMap<String, JsonValue>,
nested_comments: Vec<NestedComment>,
},
Field {
key: String,
value: QuillValue,
fill: bool,
nested_comments: Vec<NestedComment>,
},
Comment { text: String, inline: bool },
}
impl PayloadItem {
pub fn field(key: impl Into<String>, value: QuillValue) -> Self {
PayloadItem::Field {
key: key.into(),
value,
fill: false,
nested_comments: Vec::new(),
}
}
pub fn nested_comments(&self) -> &[NestedComment] {
match self {
PayloadItem::Field {
nested_comments, ..
}
| PayloadItem::Meta {
nested_comments, ..
} => nested_comments,
_ => &[],
}
}
pub fn comment(text: impl Into<String>) -> Self {
PayloadItem::Comment {
text: text.into(),
inline: false,
}
}
pub fn comment_inline(text: impl Into<String>) -> Self {
PayloadItem::Comment {
text: text.into(),
inline: true,
}
}
fn meta_rank(&self) -> Option<u8> {
match self {
PayloadItem::Quill { .. } => Some(0),
PayloadItem::Kind { .. } => Some(1),
PayloadItem::Id { .. } => Some(2),
PayloadItem::Meta { key, .. } => Some(key.rank()),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Payload {
items: Vec<PayloadItem>,
}
impl Payload {
pub fn new() -> Self {
Self::default()
}
pub fn from_index_map(map: IndexMap<String, QuillValue>) -> Self {
let items = map
.into_iter()
.map(|(key, value)| PayloadItem::Field {
key,
value,
fill: false,
nested_comments: Vec::new(),
})
.collect();
Self { items }
}
pub fn from_items(items: Vec<PayloadItem>) -> Self {
Self { items }
}
pub(crate) fn from_items_with_flat_nested(
mut items: Vec<PayloadItem>,
nested_comments: Vec<NestedComment>,
) -> Self {
for nc in nested_comments {
let Some((first, rest)) = nc.container_path.split_first() else {
continue;
};
let target_key = match first {
CommentPathSegment::Key(k) => k.clone(),
CommentPathSegment::Index(_) => continue,
};
let relative = NestedComment {
container_path: rest.to_vec(),
position: nc.position,
text: nc.text,
inline: nc.inline,
};
let slot = if let Some(meta_key) = MetaKey::from_key_str(&target_key) {
items.iter_mut().find_map(|i| match i {
PayloadItem::Meta {
key,
nested_comments,
..
} if *key == meta_key => Some(nested_comments),
_ => None,
})
} else {
items.iter_mut().find_map(|i| match i {
PayloadItem::Field {
key,
nested_comments,
..
} if key == &target_key => Some(nested_comments),
_ => None,
})
};
if let Some(slot) = slot {
slot.push(relative);
}
}
Self { items }
}
pub(crate) fn flat_nested_comments(&self) -> Vec<NestedComment> {
let mut out = Vec::new();
for item in &self.items {
let (prefix, comments) = match item {
PayloadItem::Field {
key,
nested_comments,
..
} => (key.clone(), nested_comments),
PayloadItem::Meta {
key,
nested_comments,
..
} => (key.as_str().to_string(), nested_comments),
_ => continue,
};
for nc in comments {
let mut path = Vec::with_capacity(nc.container_path.len() + 1);
path.push(CommentPathSegment::Key(prefix.clone()));
path.extend(nc.container_path.iter().cloned());
out.push(NestedComment {
container_path: path,
position: nc.position,
text: nc.text.clone(),
inline: nc.inline,
});
}
}
out
}
pub fn items(&self) -> &[PayloadItem] {
&self.items
}
pub fn items_mut(&mut self) -> &mut [PayloadItem] {
&mut self.items
}
pub fn quill(&self) -> Option<&QuillReference> {
self.items.iter().find_map(|i| match i {
PayloadItem::Quill { reference } => Some(reference),
_ => None,
})
}
pub fn kind(&self) -> Option<&str> {
self.items.iter().find_map(|i| match i {
PayloadItem::Kind { value } => Some(value.as_str()),
_ => None,
})
}
pub fn id(&self) -> Option<&str> {
self.items.iter().find_map(|i| match i {
PayloadItem::Id { value } => Some(value.as_str()),
_ => None,
})
}
fn meta(&self, want: MetaKey) -> Option<&JsonMap<String, JsonValue>> {
self.items.iter().find_map(|i| match i {
PayloadItem::Meta { key, value, .. } if *key == want => Some(value),
_ => None,
})
}
pub fn ext(&self) -> Option<&JsonMap<String, JsonValue>> {
self.meta(MetaKey::Ext)
}
pub fn seed(&self) -> Option<&JsonMap<String, JsonValue>> {
self.meta(MetaKey::Seed)
}
pub fn set_quill(&mut self, reference: QuillReference) {
self.upsert_meta(PayloadItem::Quill { reference });
}
pub fn set_kind(&mut self, kind: impl Into<String>) {
self.upsert_meta(PayloadItem::Kind { value: kind.into() });
}
pub fn set_id(&mut self, id: impl Into<String>) {
self.upsert_meta(PayloadItem::Id { value: id.into() });
}
fn set_meta(&mut self, key: MetaKey, value: JsonMap<String, JsonValue>) {
self.upsert_meta(PayloadItem::Meta {
key,
value,
nested_comments: Vec::new(),
});
}
pub fn set_ext(&mut self, value: JsonMap<String, JsonValue>) {
self.set_meta(MetaKey::Ext, value);
}
pub fn set_seed(&mut self, value: JsonMap<String, JsonValue>) {
self.set_meta(MetaKey::Seed, value);
}
fn take_meta(&mut self, want: MetaKey) -> Option<JsonMap<String, JsonValue>> {
let pos = self
.items
.iter()
.position(|i| matches!(i, PayloadItem::Meta { key, .. } if *key == want))?;
match self.items.remove(pos) {
PayloadItem::Meta { value, .. } => Some(value),
_ => unreachable!(),
}
}
pub fn take_ext(&mut self) -> Option<JsonMap<String, JsonValue>> {
self.take_meta(MetaKey::Ext)
}
pub fn take_seed(&mut self) -> Option<JsonMap<String, JsonValue>> {
self.take_meta(MetaKey::Seed)
}
fn upsert_meta(&mut self, new: PayloadItem) {
let new_rank = new
.meta_rank()
.expect("upsert_meta only accepts $-typed items");
for slot in self.items.iter_mut() {
if slot.meta_rank() == Some(new_rank) {
*slot = new;
return;
}
}
let insert_at = self
.items
.iter()
.position(|i| matches!(i.meta_rank(), Some(r) if r > new_rank))
.unwrap_or_else(|| {
self.items
.iter()
.rposition(|i| matches!(i.meta_rank(), Some(r) if r < new_rank))
.map(|p| p + 1)
.unwrap_or(0)
});
self.items.insert(insert_at, new);
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &QuillValue)> + '_ {
self.items.iter().filter_map(|item| match item {
PayloadItem::Field { key, value, .. } => Some((key, value)),
_ => None,
})
}
pub fn keys(&self) -> impl Iterator<Item = &String> + '_ {
self.items.iter().filter_map(|item| match item {
PayloadItem::Field { key, .. } => Some(key),
_ => None,
})
}
pub fn len(&self) -> usize {
self.items
.iter()
.filter(|item| matches!(item, PayloadItem::Field { .. }))
.count()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn get(&self, key: &str) -> Option<&QuillValue> {
self.items.iter().find_map(|item| match item {
PayloadItem::Field { key: k, value, .. } if k == key => Some(value),
_ => None,
})
}
pub fn contains_key(&self, key: &str) -> bool {
self.get(key).is_some()
}
pub fn is_fill(&self, key: &str) -> bool {
self.items.iter().any(|item| match item {
PayloadItem::Field { key: k, fill, .. } => k == key && *fill,
_ => false,
})
}
pub fn insert(&mut self, key: impl Into<String>, value: QuillValue) -> Option<QuillValue> {
let key = key.into();
for item in self.items.iter_mut() {
if let PayloadItem::Field {
key: k,
value: v,
fill,
nested_comments,
} = item
{
if k == &key {
let old = std::mem::replace(v, value);
*fill = false;
nested_comments.clear();
return Some(old);
}
}
}
self.items.push(PayloadItem::Field {
key,
value,
fill: false,
nested_comments: Vec::new(),
});
None
}
pub fn insert_fill(&mut self, key: impl Into<String>, value: QuillValue) -> Option<QuillValue> {
let key = key.into();
for item in self.items.iter_mut() {
if let PayloadItem::Field {
key: k,
value: v,
fill,
nested_comments,
} = item
{
if k == &key {
let old = std::mem::replace(v, value);
*fill = true;
nested_comments.clear();
return Some(old);
}
}
}
self.items.push(PayloadItem::Field {
key,
value,
fill: true,
nested_comments: Vec::new(),
});
None
}
pub fn remove(&mut self, key: &str) -> Option<QuillValue> {
let pos = self
.items
.iter()
.position(|item| matches!(item, PayloadItem::Field { key: k, .. } if k == key))?;
match self.items.remove(pos) {
PayloadItem::Field { value, .. } => Some(value),
_ => unreachable!(),
}
}
pub fn to_index_map(&self) -> IndexMap<String, QuillValue> {
let mut map = IndexMap::new();
for item in &self.items {
if let PayloadItem::Field { key, value, .. } = item {
map.insert(key.clone(), value.clone());
}
}
map
}
}
impl<'a> IntoIterator for &'a Payload {
type Item = (&'a String, &'a QuillValue);
type IntoIter = std::iter::FilterMap<
std::slice::Iter<'a, PayloadItem>,
fn(&'a PayloadItem) -> Option<(&'a String, &'a QuillValue)>,
>;
fn into_iter(self) -> Self::IntoIter {
fn filter(item: &PayloadItem) -> Option<(&String, &QuillValue)> {
match item {
PayloadItem::Field { key, value, .. } => Some((key, value)),
_ => None,
}
}
self.items.iter().filter_map(filter)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn qv(s: &str) -> QuillValue {
QuillValue::from_json(serde_json::json!(s))
}
#[test]
fn insert_new_appends_after_meta() {
let mut fm = Payload::new();
fm.set_quill("foo@0.1".parse().unwrap());
fm.set_kind("main");
fm.insert("title", qv("Hello"));
let last = fm.items().last().unwrap();
assert!(matches!(last, PayloadItem::Field { key, .. } if key == "title"));
}
#[test]
fn insert_existing_preserves_position() {
let mut fm = Payload::new();
fm.insert("a", qv("1"));
fm.insert("b", qv("2"));
fm.insert("a", qv("updated"));
let keys: Vec<&String> = fm.keys().collect();
assert_eq!(keys, vec!["a", "b"]);
assert_eq!(fm.get("a").unwrap().as_str(), Some("updated"));
}
#[test]
fn insert_clears_fill() {
let mut fm = Payload::new();
fm.insert_fill("k", qv("placeholder"));
assert!(fm.is_fill("k"));
fm.insert("k", qv("user value"));
assert!(!fm.is_fill("k"));
}
#[test]
fn map_style_iter_skips_meta_and_comments() {
let mut fm = Payload::new();
fm.set_quill("foo@0.1".parse().unwrap());
fm.set_kind("main");
fm.insert("title", qv("Hello"));
let items = std::mem::take(&mut fm).items().to_vec();
let mut items_with_comment = items;
items_with_comment.insert(2, PayloadItem::comment("c"));
let fm = Payload::from_items(items_with_comment);
let pairs: Vec<(String, String)> = fm
.iter()
.map(|(k, v)| (k.clone(), v.as_str().unwrap_or_default().to_string()))
.collect();
assert_eq!(pairs, vec![("title".to_string(), "Hello".to_string())]);
assert_eq!(fm.kind(), Some("main"));
}
#[test]
fn set_quill_inserts_at_position_zero() {
let mut fm = Payload::new();
fm.set_kind("main");
fm.set_quill("foo@0.1".parse().unwrap());
assert!(matches!(fm.items()[0], PayloadItem::Quill { .. }));
assert!(matches!(fm.items()[1], PayloadItem::Kind { .. }));
}
#[test]
fn set_id_inserts_after_quill_and_kind() {
let mut fm = Payload::new();
fm.set_quill("foo@0.1".parse().unwrap());
fm.set_kind("main");
fm.set_id("rev-1");
assert_eq!(fm.items().len(), 3);
assert!(matches!(fm.items()[2], PayloadItem::Id { .. }));
}
#[test]
fn set_replaces_in_place_preserving_comments() {
let mut fm = Payload::from_items(vec![
PayloadItem::Quill {
reference: "foo@0.1".parse().unwrap(),
},
PayloadItem::comment_inline("trailing"),
PayloadItem::Kind {
value: "main".into(),
},
]);
fm.set_quill("bar@0.2".parse().unwrap());
assert_eq!(fm.quill().unwrap().to_string(), "bar@0.2");
assert_eq!(fm.items().len(), 3);
assert!(matches!(fm.items()[1], PayloadItem::Comment { .. }));
}
#[test]
fn remove_leaves_comments_and_meta_alone() {
let mut fm = Payload::from_items(vec![
PayloadItem::Quill {
reference: "q".parse().unwrap(),
},
PayloadItem::Kind {
value: "main".into(),
},
PayloadItem::comment("header"),
PayloadItem::field("a", qv("1")),
PayloadItem::comment("mid"),
PayloadItem::field("b", qv("2")),
]);
let removed = fm.remove("a").unwrap();
assert_eq!(removed.as_str(), Some("1"));
assert!(matches!(fm.items()[0], PayloadItem::Quill { .. }));
assert!(matches!(fm.items()[1], PayloadItem::Kind { .. }));
let comments: Vec<&str> = fm
.items()
.iter()
.filter_map(|item| match item {
PayloadItem::Comment { text, .. } => Some(text.as_str()),
_ => None,
})
.collect();
assert_eq!(comments, vec!["header", "mid"]);
}
}