use std::borrow::Cow;
use std::collections::BTreeMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use super::{Result, SourceError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawContent {
data: Vec<u8>,
source_path: Option<PathBuf>,
encoding: Option<String>,
}
impl RawContent {
#[must_use]
pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
Self {
data: bytes.into(),
source_path: None,
encoding: None,
}
}
#[must_use]
pub fn from_string(s: impl Into<String>) -> Self {
Self {
data: s.into().into_bytes(),
source_path: None,
encoding: Some("utf-8".to_string()),
}
}
#[must_use]
pub fn with_source_path(mut self, path: impl Into<PathBuf>) -> Self {
self.source_path = Some(path.into());
self
}
#[must_use]
pub fn with_encoding(mut self, encoding: impl Into<String>) -> Self {
self.encoding = Some(encoding.into());
self
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
pub fn as_str(&self) -> Result<Cow<'_, str>> {
String::from_utf8(self.data.clone())
.map(Cow::Owned)
.map_err(|_| SourceError::serialization("content is not valid UTF-8"))
}
#[must_use]
pub fn as_str_lossy(&self) -> Cow<'_, str> {
String::from_utf8_lossy(&self.data)
}
#[must_use]
pub fn source_path(&self) -> Option<&std::path::Path> {
self.source_path.as_deref()
}
#[must_use]
pub fn encoding(&self) -> Option<&str> {
self.encoding.as_deref()
}
#[must_use]
pub const fn len(&self) -> usize {
self.data.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.data.is_empty()
}
#[must_use]
pub fn is_text(&self) -> bool {
std::str::from_utf8(&self.data).is_ok()
}
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
self.data
}
pub fn into_string(self) -> Result<String> {
String::from_utf8(self.data)
.map_err(|_| SourceError::serialization("content is not valid UTF-8"))
}
}
impl From<Vec<u8>> for RawContent {
fn from(bytes: Vec<u8>) -> Self {
Self::from_bytes(bytes)
}
}
impl From<String> for RawContent {
fn from(s: String) -> Self {
Self::from_string(s)
}
}
impl From<&str> for RawContent {
fn from(s: &str) -> Self {
Self::from_string(s)
}
}
impl From<&[u8]> for RawContent {
fn from(bytes: &[u8]) -> Self {
Self::from_bytes(bytes.to_vec())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(untagged)]
pub enum ParsedContent {
#[default]
Null,
Bool(bool),
Integer(i64),
Float(f64),
String(String),
Array(Vec<Self>),
Object(BTreeMap<String, Self>),
}
impl ParsedContent {
#[cfg(feature = "toml")]
#[must_use]
pub fn from_toml(value: toml::Value) -> Self {
match value {
toml::Value::String(s) => Self::String(s),
toml::Value::Integer(i) => Self::Integer(i),
toml::Value::Float(f) => Self::Float(f),
toml::Value::Boolean(b) => Self::Bool(b),
toml::Value::Datetime(d) => Self::String(d.to_string()),
toml::Value::Array(arr) => Self::Array(arr.into_iter().map(Self::from_toml).collect()),
toml::Value::Table(table) => {
let map = table
.iter()
.map(|(k, v)| (k.clone(), Self::from_toml(v.clone())))
.collect();
Self::Object(map)
}
}
}
#[cfg(feature = "json")]
#[must_use]
pub fn from_json(value: serde_json::Value) -> Self {
match value {
serde_json::Value::Null => Self::Null,
serde_json::Value::Bool(b) => Self::Bool(b),
serde_json::Value::Number(n) => n.as_i64().map_or_else(
|| n.as_f64().map_or(Self::Float(0.0), Self::Float),
Self::Integer,
),
serde_json::Value::String(s) => Self::String(s),
serde_json::Value::Array(arr) => {
Self::Array(arr.into_iter().map(Self::from_json).collect())
}
serde_json::Value::Object(obj) => {
let map = obj
.into_iter()
.map(|(k, v)| (k, Self::from_json(v)))
.collect();
Self::Object(map)
}
}
}
#[cfg(feature = "yaml")]
#[must_use]
pub fn from_yaml(value: serde_yaml::Value) -> Self {
match value {
serde_yaml::Value::Null => Self::Null,
serde_yaml::Value::Bool(b) => Self::Bool(b),
serde_yaml::Value::Number(n) => n.as_i64().map_or_else(
|| n.as_f64().map_or(Self::Float(0.0), Self::Float),
Self::Integer,
),
serde_yaml::Value::String(s) => Self::String(s),
serde_yaml::Value::Sequence(seq) => {
Self::Array(seq.into_iter().map(Self::from_yaml).collect())
}
serde_yaml::Value::Mapping(map) => {
let obj = map
.iter()
.filter_map(|(k, v)| {
k.as_str()
.map(|key| (key.to_string(), Self::from_yaml(v.clone())))
})
.collect();
Self::Object(obj)
}
serde_yaml::Value::Tagged(tagged) => Self::from_yaml(tagged.value),
}
}
#[must_use]
pub const fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
#[must_use]
pub const fn is_bool(&self) -> bool {
matches!(self, Self::Bool(_))
}
#[must_use]
pub const fn is_integer(&self) -> bool {
matches!(self, Self::Integer(_))
}
#[must_use]
pub const fn is_float(&self) -> bool {
matches!(self, Self::Float(_))
}
#[must_use]
pub const fn is_string(&self) -> bool {
matches!(self, Self::String(_))
}
#[must_use]
pub const fn is_array(&self) -> bool {
matches!(self, Self::Array(_))
}
#[must_use]
pub const fn is_object(&self) -> bool {
matches!(self, Self::Object(_))
}
#[must_use]
pub const fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(b) => Some(*b),
_ => None,
}
}
#[must_use]
pub const fn as_integer(&self) -> Option<i64> {
match self {
Self::Integer(i) => Some(*i),
_ => None,
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub const fn as_float(&self) -> Option<f64> {
match self {
Self::Float(f) => Some(*f),
Self::Integer(i) => Some(*i as f64),
_ => None,
}
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s) => Some(s),
_ => None,
}
}
#[must_use]
pub fn as_array(&self) -> Option<&[Self]> {
match self {
Self::Array(arr) => Some(arr),
_ => None,
}
}
#[must_use]
pub const fn as_object(&self) -> Option<&BTreeMap<String, Self>> {
match self {
Self::Object(obj) => Some(obj),
_ => None,
}
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&Self> {
self.as_object().and_then(|obj| obj.get(key))
}
#[must_use]
pub fn get_index(&self, index: usize) -> Option<&Self> {
self.as_array().and_then(|arr| arr.get(index))
}
pub fn into_type<T: serde::de::DeserializeOwned>(self) -> Result<T> {
serde_json::from_value(serde_json::to_value(&self).unwrap_or(serde_json::Value::Null))
.map_err(|e| SourceError::serialization(&e.to_string()))
}
pub fn to_type<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
serde_json::from_value(serde_json::to_value(self).unwrap_or(serde_json::Value::Null))
.map_err(|e| SourceError::serialization(&e.to_string()))
}
#[must_use]
pub fn merge(mut self, other: &Self) -> Self {
self.merge_in_place(other);
self
}
pub fn merge_in_place(&mut self, other: &Self) {
match (self, other) {
(Self::Object(self_obj), Self::Object(other_obj)) => {
for (key, value) in other_obj {
match self_obj.get_mut(key) {
Some(existing) => existing.merge_in_place(value),
None => {
self_obj.insert(key.clone(), value.clone());
}
}
}
}
(self_value, other_value) => {
*self_value = other_value.clone();
}
}
}
}
impl std::ops::Index<&str> for ParsedContent {
type Output = Self;
fn index(&self, key: &str) -> &Self::Output {
self.get(key).unwrap_or(&Self::Null)
}
}
impl std::ops::Index<usize> for ParsedContent {
type Output = Self;
fn index(&self, index: usize) -> &Self::Output {
self.get_index(index).unwrap_or(&Self::Null)
}
}
#[cfg(test)]
#[allow(clippy::approx_constant)]
mod tests {
use super::*;
#[test]
fn test_raw_content_from_bytes() {
let content = RawContent::from_bytes(b"hello".to_vec());
assert_eq!(content.as_bytes(), b"hello");
assert_eq!(content.len(), 5);
}
#[test]
fn test_raw_content_from_string() {
let content = RawContent::from_string("hello");
assert_eq!(content.as_bytes(), b"hello");
assert!(content.is_text());
}
#[test]
fn test_raw_content_as_str() {
let content = RawContent::from_string("hello");
assert_eq!(content.as_str().unwrap().as_ref(), "hello");
}
#[test]
fn test_raw_content_with_source_path() {
let content = RawContent::from_string("data").with_source_path("/path/to/config.toml");
assert_eq!(
content.source_path().unwrap().to_str(),
Some("/path/to/config.toml")
);
}
#[test]
fn test_raw_content_from_impls() {
let from_str: RawContent = "hello".into();
assert_eq!(from_str.as_bytes(), b"hello");
let from_string: RawContent = String::from("world").into();
assert_eq!(from_string.as_bytes(), b"world");
let from_vec: RawContent = vec![1, 2, 3].into();
assert_eq!(from_vec.as_bytes(), &[1, 2, 3]);
}
#[test]
fn test_parsed_content_null() {
let content = ParsedContent::Null;
assert!(content.is_null());
assert!(!content.is_bool());
}
#[test]
fn test_parsed_content_bool() {
let content = ParsedContent::Bool(true);
assert!(content.is_bool());
assert_eq!(content.as_bool(), Some(true));
}
#[test]
fn test_parsed_content_integer() {
let content = ParsedContent::Integer(42);
assert!(content.is_integer());
assert_eq!(content.as_integer(), Some(42));
assert_eq!(content.as_float(), Some(42.0));
}
#[test]
fn test_parsed_content_float() {
let content = ParsedContent::Float(3.14);
assert!(content.is_float());
assert_eq!(content.as_float(), Some(3.14));
}
#[test]
fn test_parsed_content_string() {
let content = ParsedContent::String("hello".to_string());
assert!(content.is_string());
assert_eq!(content.as_str(), Some("hello"));
}
#[test]
fn test_parsed_content_array() {
let content =
ParsedContent::Array(vec![ParsedContent::Integer(1), ParsedContent::Integer(2)]);
assert!(content.is_array());
assert_eq!(content.as_array().unwrap().len(), 2);
assert_eq!(content.get_index(0), Some(&ParsedContent::Integer(1)));
}
#[test]
fn test_parsed_content_object() {
let mut obj = BTreeMap::new();
obj.insert(
"key".to_string(),
ParsedContent::String("value".to_string()),
);
let content = ParsedContent::Object(obj);
assert!(content.is_object());
assert_eq!(content.get("key").unwrap().as_str(), Some("value"));
assert_eq!(content["key"].as_str(), Some("value"));
}
#[test]
fn test_parsed_content_index() {
let arr = ParsedContent::Array(vec![ParsedContent::Integer(1), ParsedContent::Integer(2)]);
assert_eq!(arr[0], ParsedContent::Integer(1));
let mut obj = BTreeMap::new();
obj.insert(
"name".to_string(),
ParsedContent::String("test".to_string()),
);
let content = ParsedContent::Object(obj);
assert_eq!(content["name"].as_str(), Some("test"));
}
#[test]
#[cfg(feature = "json")]
fn test_parsed_content_from_json() {
let json = serde_json::json!({
"server": {
"host": "localhost",
"port": 8080
}
});
let content = ParsedContent::from_json(json);
assert!(content.is_object());
let server = content.get("server").unwrap();
assert_eq!(server.get("host").unwrap().as_str(), Some("localhost"));
assert_eq!(server.get("port").unwrap().as_integer(), Some(8080));
}
#[test]
#[cfg(feature = "toml")]
fn test_parsed_content_from_toml() {
let toml: toml::Value = toml::from_str(
r#"
[server]
host = "localhost"
port = 8080
"#,
)
.unwrap();
let content = ParsedContent::from_toml(toml);
assert!(content.is_object());
let server = content.get("server").unwrap();
assert_eq!(server.get("host").unwrap().as_str(), Some("localhost"));
}
#[test]
fn test_parsed_content_merge() {
let mut obj1 = BTreeMap::new();
obj1.insert("a".to_string(), ParsedContent::Integer(1));
obj1.insert(
"b".to_string(),
ParsedContent::String("original".to_string()),
);
let mut obj2 = BTreeMap::new();
obj2.insert(
"b".to_string(),
ParsedContent::String("updated".to_string()),
);
obj2.insert("c".to_string(), ParsedContent::Integer(3));
let content1 = ParsedContent::Object(obj1);
let content2 = ParsedContent::Object(obj2);
let merged = content1.merge(&content2);
assert_eq!(merged.get("a").unwrap().as_integer(), Some(1));
assert_eq!(merged.get("b").unwrap().as_str(), Some("updated"));
assert_eq!(merged.get("c").unwrap().as_integer(), Some(3));
}
#[test]
fn test_parsed_content_to_type() {
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq)]
struct Config {
name: String,
value: i64,
}
let mut obj = BTreeMap::new();
obj.insert(
"name".to_string(),
ParsedContent::String("test".to_string()),
);
obj.insert("value".to_string(), ParsedContent::Integer(42));
let content = ParsedContent::Object(obj);
let config: Config = content.to_type().unwrap();
assert_eq!(config.name, "test");
assert_eq!(config.value, 42);
}
#[test]
fn test_parsed_content_serialization() {
let content = ParsedContent::String("hello".to_string());
let json = serde_json::to_string(&content).unwrap();
assert_eq!(json, "\"hello\"");
let decoded: ParsedContent = serde_json::from_str(&json).unwrap();
assert_eq!(content, decoded);
}
}