use styx_parse::{ScalarKind, Span};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "facet", derive(facet::Facet))]
#[cfg_attr(feature = "facet", facet(skip_all_unless_truthy))]
pub struct Value {
pub tag: Option<Tag>,
pub payload: Option<Payload>,
pub span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "facet", derive(facet::Facet))]
#[cfg_attr(feature = "facet", facet(skip_all_unless_truthy))]
pub struct Tag {
pub name: String,
pub span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "facet", derive(facet::Facet))]
#[repr(u8)]
pub enum Payload {
Scalar(Scalar),
Sequence(Sequence),
Object(Object),
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "facet", derive(facet::Facet))]
#[cfg_attr(feature = "facet", facet(skip_all_unless_truthy))]
pub struct Scalar {
pub text: String,
pub kind: ScalarKind,
pub span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "facet", derive(facet::Facet))]
#[cfg_attr(feature = "facet", facet(skip_all_unless_truthy))]
pub struct Sequence {
pub items: Vec<Value>,
pub span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "facet", derive(facet::Facet))]
#[cfg_attr(feature = "facet", facet(skip_all_unless_truthy))]
pub struct Object {
pub entries: Vec<Entry>,
pub span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "facet", derive(facet::Facet))]
#[cfg_attr(feature = "facet", facet(skip_all_unless_truthy))]
pub struct Entry {
pub key: Value,
pub value: Value,
pub doc_comment: Option<String>,
}
impl Value {
pub fn unit() -> Self {
Value {
tag: None,
payload: None,
span: None,
}
}
pub fn scalar(text: impl Into<String>) -> Self {
Value {
tag: None,
payload: Some(Payload::Scalar(Scalar {
text: text.into(),
kind: ScalarKind::Bare,
span: None,
})),
span: None,
}
}
pub fn tag(name: impl Into<String>) -> Self {
Value {
tag: Some(Tag {
name: name.into(),
span: None,
}),
payload: None,
span: None,
}
}
pub fn tagged(name: impl Into<String>, payload: Value) -> Self {
Value {
tag: Some(Tag {
name: name.into(),
span: None,
}),
payload: payload.payload,
span: None,
}
}
pub fn sequence() -> Self {
Value {
tag: None,
payload: Some(Payload::Sequence(Sequence {
items: Vec::new(),
span: None,
})),
span: None,
}
}
pub fn seq(items: Vec<Value>) -> Self {
Value {
tag: None,
payload: Some(Payload::Sequence(Sequence { items, span: None })),
span: None,
}
}
pub fn object() -> Self {
Value {
tag: None,
payload: Some(Payload::Object(Object {
entries: Vec::new(),
span: None,
})),
span: None,
}
}
pub fn is_unit(&self) -> bool {
self.tag.is_none() && self.payload.is_none()
}
pub fn is_schema_tag(&self) -> bool {
self.tag_name() == Some("schema")
}
pub fn tag_name(&self) -> Option<&str> {
self.tag.as_ref().map(|t| t.name.as_str())
}
pub fn as_str(&self) -> Option<&str> {
if self.tag.is_some() {
return None;
}
match &self.payload {
Some(Payload::Scalar(s)) => Some(&s.text),
_ => None,
}
}
pub fn scalar_text(&self) -> Option<&str> {
match &self.payload {
Some(Payload::Scalar(s)) => Some(&s.text),
_ => None,
}
}
pub fn as_object(&self) -> Option<&Object> {
match &self.payload {
Some(Payload::Object(o)) => Some(o),
_ => None,
}
}
pub fn as_object_mut(&mut self) -> Option<&mut Object> {
match &mut self.payload {
Some(Payload::Object(o)) => Some(o),
_ => None,
}
}
pub fn as_sequence(&self) -> Option<&Sequence> {
match &self.payload {
Some(Payload::Sequence(s)) => Some(s),
_ => None,
}
}
pub fn as_sequence_mut(&mut self) -> Option<&mut Sequence> {
match &mut self.payload {
Some(Payload::Sequence(s)) => Some(s),
_ => None,
}
}
pub fn with_tag(mut self, name: impl Into<String>) -> Self {
self.tag = Some(Tag {
name: name.into(),
span: None,
});
self
}
pub fn get(&self, path: &str) -> Option<&Value> {
if path.is_empty() {
return Some(self);
}
let (segment, rest) = split_path(path);
match &self.payload {
Some(Payload::Object(obj)) => {
let value = obj.get(segment)?;
if rest.is_empty() {
Some(value)
} else {
value.get(rest)
}
}
Some(Payload::Sequence(seq)) => {
if segment.starts_with('[') && segment.ends_with(']') {
let idx: usize = segment[1..segment.len() - 1].parse().ok()?;
let value = seq.get(idx)?;
if rest.is_empty() {
Some(value)
} else {
value.get(rest)
}
} else {
None
}
}
_ => None,
}
}
pub fn get_mut(&mut self, path: &str) -> Option<&mut Value> {
if path.is_empty() {
return Some(self);
}
let (segment, rest) = split_path(path);
match &mut self.payload {
Some(Payload::Object(obj)) => {
let value = obj.get_mut(segment)?;
if rest.is_empty() {
Some(value)
} else {
value.get_mut(rest)
}
}
Some(Payload::Sequence(seq)) => {
if segment.starts_with('[') && segment.ends_with(']') {
let idx: usize = segment[1..segment.len() - 1].parse().ok()?;
let value = seq.get_mut(idx)?;
if rest.is_empty() {
Some(value)
} else {
value.get_mut(rest)
}
} else {
None
}
}
_ => None,
}
}
}
impl Object {
pub fn get(&self, key: &str) -> Option<&Value> {
self.entries
.iter()
.find(|e| e.key.as_str() == Some(key))
.map(|e| &e.value)
}
pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
self.entries
.iter_mut()
.find(|e| e.key.as_str() == Some(key))
.map(|e| &mut e.value)
}
pub fn get_unit(&self) -> Option<&Value> {
self.entries
.iter()
.find(|e| e.key.is_unit())
.map(|e| &e.value)
}
pub fn get_unit_mut(&mut self) -> Option<&mut Value> {
self.entries
.iter_mut()
.find(|e| e.key.is_unit())
.map(|e| &mut e.value)
}
pub fn iter(&self) -> impl Iterator<Item = (&Value, &Value)> {
self.entries.iter().map(|e| (&e.key, &e.value))
}
pub fn contains_key(&self, key: &str) -> bool {
self.entries.iter().any(|e| e.key.as_str() == Some(key))
}
pub fn contains_unit_key(&self) -> bool {
self.entries.iter().any(|e| e.key.is_unit())
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn insert(&mut self, key: impl Into<String>, value: Value) {
let key_str = key.into();
if let Some(entry) = self
.entries
.iter_mut()
.find(|e| e.key.as_str() == Some(&key_str))
{
entry.value = value;
} else {
self.entries.push(Entry {
key: Value::scalar(key_str),
value,
doc_comment: None,
});
}
}
pub fn insert_unit(&mut self, value: Value) {
if let Some(entry) = self.entries.iter_mut().find(|e| e.key.is_unit()) {
entry.value = value;
} else {
self.entries.push(Entry {
key: Value::unit(),
value,
doc_comment: None,
});
}
}
}
impl Sequence {
pub fn get(&self, index: usize) -> Option<&Value> {
self.items.get(index)
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut Value> {
self.items.get_mut(index)
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Value> {
self.items.iter()
}
pub fn push(&mut self, value: Value) {
self.items.push(value);
}
}
fn split_path(path: &str) -> (&str, &str) {
if path.starts_with('[')
&& let Some(end) = path.find(']')
{
let segment = &path[..=end];
let rest = &path[end + 1..];
let rest = rest.strip_prefix('.').unwrap_or(rest);
return (segment, rest);
}
let dot_pos = path.find('.');
let bracket_pos = path.find('[');
match (dot_pos, bracket_pos) {
(Some(d), Some(b)) if b < d => (&path[..b], &path[b..]),
(Some(d), _) => (&path[..d], &path[d + 1..]),
(None, Some(b)) => (&path[..b], &path[b..]),
(None, None) => (path, ""),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_path() {
assert_eq!(split_path("foo"), ("foo", ""));
assert_eq!(split_path("foo.bar"), ("foo", "bar"));
assert_eq!(split_path("foo.bar.baz"), ("foo", "bar.baz"));
assert_eq!(split_path("[0]"), ("[0]", ""));
assert_eq!(split_path("[0].foo"), ("[0]", "foo"));
assert_eq!(split_path("foo[0]"), ("foo", "[0]"));
assert_eq!(split_path("foo[0].bar"), ("foo", "[0].bar"));
}
#[test]
fn test_unit_value() {
let v = Value::unit();
assert!(v.is_unit());
assert!(v.tag.is_none());
assert!(v.payload.is_none());
}
#[test]
fn test_scalar_value() {
let v = Value::scalar("hello");
assert!(!v.is_unit());
assert!(v.tag.is_none());
assert_eq!(v.as_str(), Some("hello"));
}
#[test]
fn test_tagged_value() {
let v = Value::tag("string");
assert!(!v.is_unit());
assert_eq!(v.tag_name(), Some("string"));
assert!(v.payload.is_none());
}
#[test]
fn test_object_get() {
let mut obj = Object {
entries: vec![Entry {
key: Value::scalar("name"),
value: Value::scalar("Alice"),
doc_comment: None,
}],
span: None,
};
assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("Alice"));
assert_eq!(obj.get("missing"), None);
obj.insert("age", Value::scalar("30"));
assert_eq!(obj.get("age").and_then(|v| v.as_str()), Some("30"));
}
#[test]
fn test_object_unit_key() {
let mut obj = Object {
entries: vec![],
span: None,
};
obj.insert_unit(Value::scalar("root"));
assert!(obj.contains_unit_key());
assert_eq!(obj.get_unit().and_then(|v| v.as_str()), Some("root"));
}
#[test]
fn test_value_path_access() {
let value = Value {
tag: None,
payload: Some(Payload::Object(Object {
entries: vec![
Entry {
key: Value::scalar("user"),
value: Value {
tag: None,
payload: Some(Payload::Object(Object {
entries: vec![Entry {
key: Value::scalar("name"),
value: Value::scalar("Alice"),
doc_comment: None,
}],
span: None,
})),
span: None,
},
doc_comment: None,
},
Entry {
key: Value::scalar("items"),
value: Value {
tag: None,
payload: Some(Payload::Sequence(Sequence {
items: vec![
Value::scalar("a"),
Value::scalar("b"),
Value::scalar("c"),
],
span: None,
})),
span: None,
},
doc_comment: None,
},
],
span: None,
})),
span: None,
};
assert_eq!(
value.get("user.name").and_then(|v| v.as_str()),
Some("Alice")
);
assert_eq!(value.get("items[0]").and_then(|v| v.as_str()), Some("a"));
assert_eq!(value.get("items[2]").and_then(|v| v.as_str()), Some("c"));
assert_eq!(value.get("missing"), None);
}
#[test]
#[cfg(feature = "facet")]
fn test_value_json_roundtrip() {
let value = Value {
tag: None,
payload: Some(Payload::Object(Object {
entries: vec![
Entry {
key: Value::tag("schema"),
value: Value::scalar("my-schema.styx"),
doc_comment: Some("Schema for this config".to_string()),
},
Entry {
key: Value::scalar("name"),
value: Value::scalar("my-app"),
doc_comment: None,
},
Entry {
key: Value::scalar("port"),
value: Value::tagged("int", Value::scalar("8080")),
doc_comment: None,
},
Entry {
key: Value::scalar("server"),
value: Value {
tag: None,
payload: Some(Payload::Object(Object {
entries: vec![
Entry {
key: Value::scalar("host"),
value: Value::scalar("localhost"),
doc_comment: None,
},
Entry {
key: Value::scalar("tls"),
value: Value {
tag: Some(Tag {
name: "object".to_string(),
span: None,
}),
payload: Some(Payload::Object(Object {
entries: vec![
Entry {
key: Value::scalar("cert"),
value: Value::scalar("/path/to/cert.pem"),
doc_comment: None,
},
Entry {
key: Value::scalar("key"),
value: Value::scalar("/path/to/key.pem"),
doc_comment: None,
},
],
span: None,
})),
span: None,
},
doc_comment: Some("TLS configuration".to_string()),
},
],
span: None,
})),
span: None,
},
doc_comment: Some("Server settings".to_string()),
},
Entry {
key: Value::scalar("tags"),
value: Value {
tag: None,
payload: Some(Payload::Sequence(Sequence {
items: vec![
Value::scalar("production"),
Value::scalar("web"),
Value::tagged("important", Value::unit()),
],
span: None,
})),
span: None,
},
doc_comment: None,
},
Entry {
key: Value::scalar("debug"),
value: Value::unit(),
doc_comment: None,
},
],
span: Some(Span::new(0, 100)),
})),
span: Some(Span::new(0, 100)),
};
let json = facet_json::to_string(&value).expect("should serialize");
eprintln!("JSON representation:\n{json}");
let roundtripped: Value = facet_json::from_str(&json).expect("should deserialize");
assert_eq!(value, roundtripped, "Value should survive JSON roundtrip");
}
#[test]
#[cfg(feature = "facet")]
fn test_value_postcard_roundtrip() {
let v = Value::scalar("hello");
let bytes = facet_postcard::to_vec(&v).expect("serialize scalar");
let v2: Value = facet_postcard::from_slice(&bytes).expect("deserialize scalar");
assert_eq!(v, v2);
let v = Value::tag("string");
let bytes = facet_postcard::to_vec(&v).expect("serialize tagged");
let v2: Value = facet_postcard::from_slice(&bytes).expect("deserialize tagged");
assert_eq!(v, v2);
let v = Value {
tag: None,
payload: Some(Payload::Object(Object {
entries: vec![
Entry {
key: Value::scalar("name"),
value: Value::scalar("Alice"),
doc_comment: None,
},
Entry {
key: Value::scalar("nested"),
value: Value {
tag: None,
payload: Some(Payload::Object(Object {
entries: vec![Entry {
key: Value::scalar("inner"),
value: Value::scalar("value"),
doc_comment: None,
}],
span: None,
})),
span: None,
},
doc_comment: Some("A nested object".to_string()),
},
],
span: None,
})),
span: None,
};
let bytes = facet_postcard::to_vec(&v).expect("serialize nested");
let v2: Value = facet_postcard::from_slice(&bytes).expect("deserialize nested");
assert_eq!(v, v2);
let v = Value::seq(vec![
Value::scalar("a"),
Value::scalar("b"),
Value::tagged("important", Value::unit()),
]);
let bytes = facet_postcard::to_vec(&v).expect("serialize sequence");
let v2: Value = facet_postcard::from_slice(&bytes).expect("deserialize sequence");
assert_eq!(v, v2);
}
}