Skip to content

Commit

Permalink
refactor, add pretty printing, remove anyhow
Browse files Browse the repository at this point in the history
  • Loading branch information
decahedron1 committed Nov 6, 2023
1 parent d916d5f commit 9129622
Show file tree
Hide file tree
Showing 18 changed files with 665 additions and 324 deletions.
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ edition = "2021"
repository = "https://github.com/pykeio/ssml"

[dependencies]
anyhow = "1.0"
dyn-clone = "1.0"
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Currently, `ssml-rs` focuses on supporting the subsets of SSML supported by majo
let doc = ssml::speak(Some("en-US"), ["Hello, world!"]);

use ssml::Serialize;
let str = doc.serialize_to_string(ssml::Flavor::AmazonPolly)?;
assert_eq!(str, r#"<speak xml:lang="en-US">Hello, world!</speak>"#);
let str = doc.serialize_to_string(&ssml::SerializeOptions::default().flavor(Flavor::AmazonPolly))?;
assert_eq!(
str,
r#"<speak xml:lang="en-US">Hello, world!</speak>"#
);
```
84 changes: 41 additions & 43 deletions src/audio.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
use std::io::Write;

use crate::{
speak::Element,
unit::{Decibels, TimeDesignation},
util, Flavor, Serialize
util, Element, Flavor, Serialize, SerializeOptions, XmlWriter
};

/// Specify repeating an [`Audio`] element's playback for a certain number of times, or for a determined duration.
Expand Down Expand Up @@ -148,52 +145,48 @@ impl Audio {
}

impl Serialize for Audio {
fn serialize<W: Write>(&self, writer: &mut W, flavor: Flavor) -> anyhow::Result<()> {
writer.write_all(b"<audio")?;
if !self.src.is_empty() {
util::write_attr(writer, "src", &self.src)?;
} else if flavor == Flavor::GoogleCloudTextToSpeech {
// https://cloud.google.com/text-to-speech/docs/ssml#attributes_1
return Err(crate::error!("GCTTS requires <audio> elements to have a valid `src`."))?;
fn serialize_xml(&self, writer: &mut XmlWriter<'_>, options: &SerializeOptions) -> crate::Result<()> {
if options.perform_checks {
if options.flavor == Flavor::GoogleCloudTextToSpeech && self.src.is_empty() {
// https://cloud.google.com/text-to-speech/docs/ssml#attributes_1
return Err(crate::error!("GCTTS requires <audio> elements to have a valid `src`."))?;
}
if let Some(AudioRepeat::Times(times)) = &self.repeat {
if times.is_sign_negative() {
return Err(crate::error!("`times` cannot be negative"))?;
}
}
if let Some(speed) = &self.speed {
if speed.is_sign_negative() {
return Err(crate::error!("`speed` cannot be negative"))?;
}
}
}

if let Some(clip_begin) = &self.clip.0 {
util::write_attr(writer, "clipBegin", clip_begin.to_string())?;
}
if let Some(clip_end) = &self.clip.1 {
util::write_attr(writer, "clipEnd", clip_end.to_string())?;
}
writer.element("audio", |writer| {
writer.attr("src", &self.src)?;

writer.attr_opt("clipBegin", self.clip.0.as_ref().map(|t| t.to_string()))?;
writer.attr_opt("clipEnd", self.clip.1.as_ref().map(|t| t.to_string()))?;

if let Some(repeat) = &self.repeat {
match repeat {
AudioRepeat::Duration(dur) => util::write_attr(writer, "repeatDur", dur.to_string())?,
AudioRepeat::Times(times) => {
if times.is_sign_negative() {
return Err(crate::error!("`times` cannot be negative"))?;
}
util::write_attr(writer, "times", times.to_string())?;
if let Some(repeat) = &self.repeat {
match repeat {
AudioRepeat::Duration(dur) => writer.attr("repeatDur", dur.to_string())?,
AudioRepeat::Times(times) => writer.attr("times", times.to_string())?
}
}
}

if let Some(sound_level) = &self.sound_level {
util::write_attr(writer, "soundLevel", sound_level.to_string())?;
}
writer.attr_opt("soundLevel", self.sound_level.as_ref().map(|t| t.to_string()))?;
writer.attr_opt("speed", self.speed.map(|s| format!("{}%", s * 100.)))?;

if let Some(speed) = &self.speed {
if speed.is_sign_negative() {
return Err(crate::error!("`speed` cannot be negative"))?;
if let Some(desc) = &self.desc {
writer.element("desc", |writer| writer.text(desc))?;
}
util::write_attr(writer, "speed", format!("{}%", speed * 100.))?;
}

writer.write_all(b">")?;
if let Some(desc) = &self.desc {
writer.write_fmt(format_args!("<desc>{}</desc>", util::escape(desc)))?;
}
util::serialize_elements(writer, &self.alternate, flavor)?;
writer.write_all(b"</audio>")?;
util::serialize_elements(writer, &self.alternate, options)?;

Ok(())
})?;
Ok(())
}
}
Expand All @@ -210,19 +203,24 @@ pub fn audio(src: impl ToString) -> Audio {
#[cfg(test)]
mod tests {
use super::{Audio, AudioRepeat};
use crate::{Flavor, Serialize};
use crate::{Serialize, SerializeOptions};

#[test]
fn non_negative_speed() {
assert!(Audio::default().with_speed(-1.0).serialize_to_string(Flavor::Generic).is_err());
assert!(
Audio::default()
.with_speed(-1.0)
.serialize_to_string(&SerializeOptions::default())
.is_err()
);
}

#[test]
fn non_negative_repeat_times() {
assert!(
Audio::default()
.with_repeat(AudioRepeat::Times(-1.0))
.serialize_to_string(Flavor::Generic)
.serialize_to_string(&SerializeOptions::default())
.is_err()
);
}
Expand Down
33 changes: 0 additions & 33 deletions src/custom.rs

This file was deleted.

156 changes: 156 additions & 0 deletions src/element.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use std::fmt::Debug;

use dyn_clone::DynClone;

use crate::{Audio, Meta, Serialize, SerializeOptions, Text, Voice, XmlWriter};

macro_rules! el {
(
$(#[$outer:meta])*
pub enum $name:ident {
$(
$(#[$innermeta:meta])*
$variant:ident($inner:ty)
),*
}
) => {
$(#[$outer])*
pub enum $name {
$(
$(#[$innermeta])*
$variant($inner)
),*
}

$(impl From<$inner> for $name {
fn from(val: $inner) -> $name {
$name::$variant(val)
}
})*

impl $crate::Serialize for $name {
fn serialize_xml(&self, writer: &mut $crate::XmlWriter<'_>, options: &$crate::SerializeOptions) -> crate::Result<()> {
match self {
$($name::$variant(inner) => inner.serialize_xml(writer, options),)*
}
}
}
};
}

el! {
/// Represents all SSML elements.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Element {
Text(Text),
Audio(Audio),
Voice(Voice),
Meta(Meta),
/// A dyn element can be used to implement your own custom elements outside of the `ssml` crate. See
/// [`DynElement`] for more information and examples.
Dyn(Box<dyn DynElement>)
// Break(BreakElement),
// Emphasis(EmphasisElement),
// Lang(LangElement),
// Mark(MarkElement),
// Paragraph(ParagraphElement),
// Phoneme(PhonemeElement),
// Prosody(ProsodyElement),
// SayAs(SayAsElement),
// Sub(SubElement),
// Sentence(SentenceElement),
// Voice(VoiceElement),
// Word(WordElement)
}
}

impl<T: ToString> From<T> for Element {
fn from(value: T) -> Self {
Element::Text(Text(value.to_string()))
}
}

/// A dynamic element which can be used to implement non-standard SSML elements outside of the `ssml` crate.
///
/// ```
/// use ssml::{DynElement, Element, Serialize, SerializeOptions, XmlWriter};
///
/// #[derive(Debug, Clone)]
/// pub struct TomfooleryElement {
/// value: f32,
/// children: Vec<Element>
/// }
///
/// impl TomfooleryElement {
/// // Increase the tomfoolery level of a section of elements.
/// // ...
/// pub fn new<S: Into<Element>, I: IntoIterator<Item = S>>(value: f32, elements: I) -> Self {
/// Self {
/// value,
/// children: elements.into_iter().map(|f| f.into()).collect()
/// }
/// }
///
/// // not required, but makes your code much cleaner!
/// pub fn into_dyn(self) -> Element {
/// Element::Dyn(Box::new(self))
/// }
/// }
///
/// impl DynElement for TomfooleryElement {
/// fn serialize_xml(&self, writer: &mut XmlWriter<'_>, options: &SerializeOptions) -> ssml::Result<()> {
/// writer.element("tomfoolery", |writer| {
/// writer.attr("influence", self.value.to_string())?;
/// ssml::util::serialize_elements(writer, &self.children, options)
/// })
/// }
/// }
///
/// # fn main() -> ssml::Result<()> {
/// let doc = ssml::speak(
/// Some("en-US"),
/// [TomfooleryElement::new(2.0, ["Approaching dangerous levels of tomfoolery!"]).into_dyn()]
/// );
/// let str = doc.serialize_to_string(&ssml::SerializeOptions::default().pretty())?;
/// assert_eq!(
/// str,
/// r#"<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
/// <tomfoolery influence="2">
/// Approaching dangerous levels of tomfoolery!
/// </tomfoolery>
/// </speak>"#
/// );
/// # Ok(())
/// # }
/// ```
pub trait DynElement: Debug + DynClone + Send {
/// Serialize this dynamic element into an [`XmlWriter`].
///
/// See [`Serialize::serialize_xml`] for more information.
fn serialize_xml(&self, writer: &mut XmlWriter<'_>, options: &SerializeOptions) -> crate::Result<()>;

/// An optional tag representing this dynamic element.
fn tag_name(&self) -> Option<&str> {
None
}

/// If this element has children, returns a reference to the vector containing the element's children.
fn children(&self) -> Option<&Vec<Element>> {
None
}

/// If this element has children, returns a mutable reference to the vector containing the element's children.
fn children_mut(&mut self) -> Option<&mut Vec<Element>> {
None
}
}

dyn_clone::clone_trait_object!(DynElement);

impl Serialize for Box<dyn DynElement> {
fn serialize_xml(&self, writer: &mut XmlWriter<'_>, options: &SerializeOptions) -> crate::Result<()> {
DynElement::serialize_xml(self.as_ref(), writer, options)?;
Ok(())
}
}
49 changes: 42 additions & 7 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,57 @@
use std::{error::Error, fmt::Display};
use std::{error::Error as StdError, fmt::Display, io, str::Utf8Error};

use crate::{DecibelsError, TimeDesignationError};

#[derive(Debug)]
pub(crate) struct GenericError(pub String);
#[non_exhaustive]
pub enum Error {
IoError(io::Error),
TimeDesignationError(TimeDesignationError),
DecibelsError(DecibelsError),
AttributesInChildContext,
Generic(String),
Utf8Error(Utf8Error)
}

unsafe impl Send for Error {}

impl Display for GenericError {
macro_rules! impl_from {
($($variant:ident => $t:ty),*) => {
$(impl From<$t> for Error {
fn from(e: $t) -> Self {
Error::$variant(e)
}
})*
};
}

impl_from! {
IoError => io::Error, Utf8Error => Utf8Error, TimeDesignationError => TimeDesignationError, DecibelsError => DecibelsError
}

impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
match self {
Error::IoError(e) => e.fmt(f),
Error::Utf8Error(e) => e.fmt(f),
Error::TimeDesignationError(e) => e.fmt(f),
Error::DecibelsError(e) => e.fmt(f),
Error::AttributesInChildContext => f.write_str("invalid ordering: attempted to write attributes after writing children"),
Error::Generic(s) => f.write_str(s)
}
}
}

impl Error for GenericError {}
impl StdError for Error {}

pub type Result<T, E = Error> = std::result::Result<T, E>;

macro_rules! error {
($m:literal) => {
$crate::GenericError(format!($m))
$crate::Error::Generic(format!($m))
};
($fmt:expr, $($arg:tt)*) => {
$crate::GenericError(format!($fmt, $($arg)*))
$crate::Error::Generic(format!($fmt, $($arg)*))
};
}
pub(crate) use error;
Loading

0 comments on commit 9129622

Please sign in to comment.