use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "output_type", rename_all = "snake_case")]
pub enum Output {
ExecuteResult {
execution_count: Option<u32>,
data: MimeBundle,
#[serde(default)]
metadata: OutputMetadata,
},
DisplayData {
data: MimeBundle,
#[serde(default)]
metadata: OutputMetadata,
},
Stream {
name: StreamName,
text: MultilineString,
},
Error {
ename: String,
evalue: String,
traceback: Vec<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum StreamName {
Stdout,
Stderr,
}
pub type MimeBundle = IndexMap<String, MimeData>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MimeData {
String(String),
Lines(Vec<String>),
Json(serde_json::Value),
}
impl MimeData {
pub fn as_string(&self) -> String {
match self {
MimeData::String(s) => s.clone(),
MimeData::Lines(lines) => lines.join(""),
MimeData::Json(value) => serde_json::to_string(value).unwrap_or_default(),
}
}
pub fn from_string(s: String) -> Self {
MimeData::String(s)
}
pub fn from_lines(lines: Vec<String>) -> Self {
MimeData::Lines(lines)
}
pub fn from_json(value: serde_json::Value) -> Self {
MimeData::Json(value)
}
pub fn is_json(&self) -> bool {
matches!(self, MimeData::Json(_))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MultilineString {
String(String),
Lines(Vec<String>),
}
impl MultilineString {
pub fn as_string(&self) -> String {
match self {
MultilineString::String(s) => s.clone(),
MultilineString::Lines(lines) => lines.join(""),
}
}
pub fn from_string(s: impl Into<String>) -> Self {
MultilineString::String(s.into())
}
pub fn from_lines(lines: Vec<String>) -> Self {
MultilineString::Lines(lines)
}
pub fn to_lines(&self) -> Vec<String> {
match self {
MultilineString::String(s) => split_into_lines(s),
MultilineString::Lines(lines) => lines.clone(),
}
}
}
impl Default for MultilineString {
fn default() -> Self {
MultilineString::String(String::new())
}
}
impl From<String> for MultilineString {
fn from(s: String) -> Self {
MultilineString::String(s)
}
}
impl From<&str> for MultilineString {
fn from(s: &str) -> Self {
MultilineString::String(s.to_string())
}
}
pub type OutputMetadata = IndexMap<String, serde_json::Value>;
pub fn split_into_lines(s: &str) -> Vec<String> {
if s.is_empty() {
return vec![];
}
let mut lines = Vec::new();
let mut start = 0;
for (i, c) in s.char_indices() {
if c == '\n' {
lines.push(s[start..=i].to_string());
start = i + 1;
}
}
if start < s.len() {
lines.push(s[start..].to_string());
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_into_lines_empty() {
assert_eq!(split_into_lines(""), Vec::<String>::new());
}
#[test]
fn test_split_into_lines_no_newline() {
assert_eq!(split_into_lines("hello"), vec!["hello"]);
}
#[test]
fn test_split_into_lines_single_newline() {
assert_eq!(split_into_lines("hello\n"), vec!["hello\n"]);
}
#[test]
fn test_split_into_lines_multiple() {
assert_eq!(
split_into_lines("line1\nline2\nline3"),
vec!["line1\n", "line2\n", "line3"]
);
}
#[test]
fn test_split_into_lines_trailing_newline() {
assert_eq!(
split_into_lines("line1\nline2\n"),
vec!["line1\n", "line2\n"]
);
}
#[test]
fn test_multiline_string_round_trip() {
let original = "line1\nline2\nline3";
let ms = MultilineString::from_string(original);
assert_eq!(ms.as_string(), original);
let lines = ms.to_lines();
let rejoined: String = lines.join("");
assert_eq!(rejoined, original);
}
}