From 7c2577d196c059ab6e2d5b0efe5e036bdad75be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cel=20=F0=9F=8C=B8?= Date: Tue, 3 Dec 2024 03:51:26 +0000 Subject: [PATCH] implement remaining rfc6120 xml schemas --- src/jabber.rs | 13 +- src/lib.rs | 10 +- src/stanza/client/error.rs | 4 +- src/stanza/client/message.rs | 155 +++++++++++++++++- src/stanza/client/mod.rs | 42 +++++ src/stanza/client/presence.rs | 194 ++++++++++++++++++++++- src/stanza/mod.rs | 5 +- src/stanza/{error.rs => stanza_error.rs} | 0 src/stanza/stream.rs | 30 +++- src/stanza/stream_error.rs | 137 ++++++++++++++++ 10 files changed, 562 insertions(+), 28 deletions(-) rename src/stanza/{error.rs => stanza_error.rs} (100%) create mode 100644 src/stanza/stream_error.rs diff --git a/src/jabber.rs b/src/jabber.rs index 96cd73a..d5cfe13 100644 --- a/src/jabber.rs +++ b/src/jabber.rs @@ -2,19 +2,16 @@ use std::str; use std::sync::Arc; use async_recursion::async_recursion; -use peanuts::element::{FromElement, IntoElement}; +use peanuts::element::IntoElement; use peanuts::{Reader, Writer}; use rsasl::prelude::{Mechname, SASLClient, SASLConfig}; -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter, ReadHalf, WriteHalf}; -use tokio::time::timeout; +use tokio::io::{AsyncRead, AsyncWrite, ReadHalf, WriteHalf}; use tokio_native_tls::native_tls::TlsConnector; -use tracing::{debug, info, instrument, trace}; -use trust_dns_resolver::proto::rr::domain::IntoLabel; +use tracing::{debug, instrument}; use crate::connection::{Tls, Unencrypted}; use crate::error::Error; use crate::stanza::bind::{Bind, BindType, FullJidType, ResourceType}; -use crate::stanza::client::error::Error as ClientError; use crate::stanza::client::iq::{Iq, IqType, Query}; use crate::stanza::sasl::{Auth, Challenge, Mechanisms, Response, ServerResponse}; use crate::stanza::starttls::{Proceed, StartTls}; @@ -257,7 +254,7 @@ where // server to client // may or may not send a declaration - let decl = self.reader.read_prolog().await?; + let _decl = self.reader.read_prolog().await?; // receive stream element and validate let text = str::from_utf8(self.reader.buffer.data()).unwrap(); @@ -471,7 +468,7 @@ mod tests { #[tokio::test] async fn negotiate() { - let jabber = Connection::connect_user("test@blos.sm", "slayed".to_string()) + let _jabber = Connection::connect_user("test@blos.sm", "slayed".to_string()) .await .unwrap() .ensure_tls() diff --git a/src/lib.rs b/src/lib.rs index 88b91a6..681d1d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,14 +9,20 @@ pub mod jid; pub mod stanza; pub use connection::Connection; +use connection::Tls; pub use error::Error; pub use jabber::Jabber; pub use jid::JID; pub type Result = std::result::Result; -pub async fn login, P: AsRef>(jid: J, password: P) -> Result { - todo!() +pub async fn login, P: AsRef>(jid: J, password: P) -> Result> { + Ok(Connection::connect_user(jid, password.as_ref().to_string()) + .await? + .ensure_tls() + .await? + .negotiate() + .await?) } #[cfg(test)] diff --git a/src/stanza/client/error.rs b/src/stanza/client/error.rs index fc5ed21..545b9a7 100644 --- a/src/stanza/client/error.rs +++ b/src/stanza/client/error.rs @@ -3,8 +3,8 @@ use std::str::FromStr; use peanuts::element::{FromElement, IntoElement}; use peanuts::{DeserializeError, Element}; -use crate::stanza::error::Text; -use crate::stanza::Error as StanzaError; +use crate::stanza::stanza_error::Error as StanzaError; +use crate::stanza::stanza_error::Text; use super::XMLNS; diff --git a/src/stanza/client/message.rs b/src/stanza/client/message.rs index cdfda5d..626d781 100644 --- a/src/stanza/client/message.rs +++ b/src/stanza/client/message.rs @@ -1,37 +1,186 @@ +use std::str::FromStr; + +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, XML_NS, +}; + use crate::JID; +use super::XMLNS; + pub struct Message { from: Option, id: Option, to: Option, - r#type: Option, + // can be omitted, if so default to normal + r#type: MessageType, + lang: Option, // children subject: Option, body: Option, thread: Option, - lang: Option, } +impl FromElement for Message { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult { + element.check_name("message")?; + element.check_namespace(XMLNS)?; + + let from = element.attribute_opt("from")?; + let id = element.attribute_opt("id")?; + let to = element.attribute_opt("to")?; + let r#type = element.attribute_opt("type")?.unwrap_or_default(); + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + + let subject = element.child_opt()?; + let body = element.child_opt()?; + let thread = element.child_opt()?; + + Ok(Message { + from, + id, + to, + r#type, + lang, + subject, + body, + thread, + }) + } +} + +impl IntoElement for Message { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("message", Some(XMLNS)) + .push_attribute_opt("from", self.from.clone()) + .push_attribute_opt("id", self.id.clone()) + .push_attribute_opt("to", self.to.clone()) + .push_attribute_opt("type", { + if self.r#type == MessageType::Normal { + None + } else { + Some(self.r#type) + } + }) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_child_opt(self.subject.clone()) + .push_child_opt(self.body.clone()) + .push_child_opt(self.thread.clone()) + } +} + +#[derive(Default, PartialEq, Eq, Copy, Clone)] pub enum MessageType { Chat, Error, Groupchat, Headline, + #[default] Normal, } +impl FromStr for MessageType { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result { + match s { + "chat" => Ok(MessageType::Chat), + "error" => Ok(MessageType::Error), + "groupchat" => Ok(MessageType::Groupchat), + "headline" => Ok(MessageType::Headline), + "normal" => Ok(MessageType::Normal), + _ => Err(DeserializeError::FromStr(s.to_string())), + } + } +} + +impl ToString for MessageType { + fn to_string(&self) -> String { + match self { + MessageType::Chat => "chat".to_string(), + MessageType::Error => "error".to_string(), + MessageType::Groupchat => "groupchat".to_string(), + MessageType::Headline => "headline".to_string(), + MessageType::Normal => "normal".to_string(), + } + } +} + +#[derive(Clone)] pub struct Body { lang: Option, body: Option, } +impl FromElement for Body { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + element.check_name("body")?; + element.check_namespace(XMLNS)?; + + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + let body = element.pop_value_opt()?; + + Ok(Body { lang, body }) + } +} + +impl IntoElement for Body { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("body", Some(XMLNS)) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_text_opt(self.body.clone()) + } +} + +#[derive(Clone)] pub struct Subject { lang: Option, subject: Option, } +impl FromElement for Subject { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + element.check_name("subject")?; + element.check_namespace(XMLNS)?; + + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + let subject = element.pop_value_opt()?; + + Ok(Subject { lang, subject }) + } +} + +impl IntoElement for Subject { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("subject", Some(XMLNS)) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_text_opt(self.subject.clone()) + } +} + +#[derive(Clone)] pub struct Thread { - // TODO: NOT DONE parent: Option, thread: Option, } + +impl FromElement for Thread { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + element.check_name("thread")?; + element.check_namespace(XMLNS)?; + + let parent = element.attribute_opt("parent")?; + let thread = element.pop_value_opt()?; + + Ok(Thread { parent, thread }) + } +} + +impl IntoElement for Thread { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("thread", Some(XMLNS)) + .push_attribute_opt("parent", self.parent.clone()) + .push_text_opt(self.thread.clone()) + } +} diff --git a/src/stanza/client/mod.rs b/src/stanza/client/mod.rs index 7b25b15..25d7b56 100644 --- a/src/stanza/client/mod.rs +++ b/src/stanza/client/mod.rs @@ -1,6 +1,48 @@ +use iq::Iq; +use message::Message; +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, +}; +use presence::Presence; + +use super::stream::{self, Error as StreamError}; + pub mod error; pub mod iq; pub mod message; pub mod presence; pub const XMLNS: &str = "jabber:client"; + +pub enum Stanza { + Message(Message), + Presence(Presence), + Iq(Iq), + Error(StreamError), +} + +impl FromElement for Stanza { + fn from_element(element: peanuts::Element) -> peanuts::element::DeserializeResult { + match element.identify() { + (Some(XMLNS), "message") => Ok(Stanza::Message(Message::from_element(element)?)), + (Some(XMLNS), "presence") => Ok(Stanza::Presence(Presence::from_element(element)?)), + (Some(XMLNS), "iq") => Ok(Stanza::Iq(Iq::from_element(element)?)), + (Some(stream::XMLNS), "error") => { + Ok(Stanza::Error(StreamError::from_element(element)?)) + } + _ => Err(DeserializeError::UnexpectedElement(element)), + } + } +} + +impl IntoElement for Stanza { + fn builder(&self) -> peanuts::element::ElementBuilder { + match self { + Stanza::Message(message) => message.builder(), + Stanza::Presence(presence) => presence.builder(), + Stanza::Iq(iq) => iq.builder(), + Stanza::Error(error) => error.builder(), + } + } +} diff --git a/src/stanza/client/presence.rs b/src/stanza/client/presence.rs index 46194f3..bcb04d4 100644 --- a/src/stanza/client/presence.rs +++ b/src/stanza/client/presence.rs @@ -1,24 +1,77 @@ -use peanuts::element::{FromElement, IntoElement}; +use std::str::FromStr; + +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, XML_NS, +}; use crate::JID; -use super::error::Error; +use super::{error::Error, XMLNS}; pub struct Presence { from: Option, id: Option, to: Option, - r#type: PresenceType, + r#type: Option, lang: Option, // children show: Option, status: Option, priority: Option, + // TODO: ##other + // other: Vec, errors: Vec, - // ##other - // content: Vec>, } +impl FromElement for Presence { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult { + element.check_name("presence")?; + element.check_namespace(XMLNS)?; + + let from = element.attribute_opt("from")?; + let id = element.attribute_opt("id")?; + let to = element.attribute_opt("to")?; + let r#type = element.attribute_opt("type")?; + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + + let show = element.child_opt()?; + let status = element.child_opt()?; + let priority = element.child_opt()?; + let errors = element.children()?; + + Ok(Presence { + from, + id, + to, + r#type, + lang, + show, + status, + priority, + errors, + }) + } +} + +impl IntoElement for Presence { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("presence", Some(XMLNS)) + .push_attribute_opt("from", self.from.clone()) + .push_attribute_opt("id", self.id.clone()) + .push_attribute_opt("to", self.to.clone()) + .push_attribute_opt("type", self.r#type) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_child_opt(self.show) + .push_child_opt(self.status.clone()) + .push_child_opt(self.priority) + .push_children(self.errors.clone()) + } +} + +pub enum Other {} + +#[derive(Copy, Clone)] pub enum PresenceType { Error, Probe, @@ -29,6 +82,38 @@ pub enum PresenceType { Unsubscribed, } +impl FromStr for PresenceType { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result { + match s { + "error" => Ok(PresenceType::Error), + "probe" => Ok(PresenceType::Probe), + "subscribe" => Ok(PresenceType::Subscribe), + "subscribed" => Ok(PresenceType::Subscribed), + "unavailable" => Ok(PresenceType::Unavailable), + "unsubscribe" => Ok(PresenceType::Unsubscribe), + "unsubscribed" => Ok(PresenceType::Unsubscribed), + s => Err(DeserializeError::FromStr(s.to_string())), + } + } +} + +impl ToString for PresenceType { + fn to_string(&self) -> String { + match self { + PresenceType::Error => "error".to_string(), + PresenceType::Probe => "probe".to_string(), + PresenceType::Subscribe => "subscribe".to_string(), + PresenceType::Subscribed => "subscribed".to_string(), + PresenceType::Unavailable => "unavailable".to_string(), + PresenceType::Unsubscribe => "unsubscribe".to_string(), + PresenceType::Unsubscribed => "unsubscribed".to_string(), + } + } +} + +#[derive(Copy, Clone)] pub enum Show { Away, Chat, @@ -36,13 +121,106 @@ pub enum Show { Xa, } +impl FromElement for Show { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult { + element.check_name("show")?; + element.check_namespace(XMLNS)?; + + Ok(element.pop_value()?) + } +} + +impl FromStr for Show { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result { + match s { + "away" => Ok(Show::Away), + "chat" => Ok(Show::Chat), + "dnd" => Ok(Show::Dnd), + "xa" => Ok(Show::Xa), + s => Err(DeserializeError::FromStr(s.to_string())), + } + } +} + +impl IntoElement for Show { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("show", Some(XMLNS)).push_text(*self) + } +} + +impl ToString for Show { + fn to_string(&self) -> String { + match self { + Show::Away => "away".to_string(), + Show::Chat => "chat".to_string(), + Show::Dnd => "dnd".to_string(), + Show::Xa => "xa".to_string(), + } + } +} + +#[derive(Clone)] pub struct Status { lang: Option, status: String1024, } -// minLength 1 maxLength 1024 -pub struct String1024(String); +impl FromElement for Status { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult { + element.check_name("status")?; + element.check_namespace(XMLNS)?; + + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + let status = element.pop_value()?; + + Ok(Status { lang, status }) + } +} + +impl IntoElement for Status { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("status", Some(XMLNS)) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_text(self.status.clone()) + } +} + +// TODO: enforce? +/// minLength 1 maxLength 1024 +#[derive(Clone)] +pub struct String1024(pub String); + +impl FromStr for String1024 { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result { + Ok(String1024(s.to_string())) + } +} + +impl ToString for String1024 { + fn to_string(&self) -> String { + self.0.clone() + } +} // xs:byte -pub struct Priority(u8); +#[derive(Clone, Copy)] +pub struct Priority(pub i8); + +impl FromElement for Priority { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + element.check_name("priority")?; + element.check_namespace(XMLNS)?; + + Ok(Priority(element.pop_value()?)) + } +} + +impl IntoElement for Priority { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("priority", Some(XMLNS)).push_text(self.0) + } +} diff --git a/src/stanza/mod.rs b/src/stanza/mod.rs index 84e80ab..32716d3 100644 --- a/src/stanza/mod.rs +++ b/src/stanza/mod.rs @@ -2,11 +2,10 @@ use peanuts::declaration::VersionInfo; pub mod bind; pub mod client; -pub mod error; pub mod sasl; +pub mod stanza_error; pub mod starttls; pub mod stream; +pub mod stream_error; pub static XML_VERSION: VersionInfo = VersionInfo::One; - -pub use error::Error; diff --git a/src/stanza/error.rs b/src/stanza/stanza_error.rs similarity index 100% rename from src/stanza/error.rs rename to src/stanza/stanza_error.rs diff --git a/src/stanza/stream.rs b/src/stanza/stream.rs index c49a2bc..4f3c435 100644 --- a/src/stanza/stream.rs +++ b/src/stanza/stream.rs @@ -6,11 +6,12 @@ use peanuts::{element::Name, Element}; use tracing::debug; use crate::stanza::bind; -use crate::{Error, JID}; +use crate::JID; -use super::client; use super::sasl::{self, Mechanisms}; use super::starttls::{self, StartTls}; +use super::stream_error::{Error as StreamError, Text}; +use super::{client, stream_error}; pub const XMLNS: &str = "http://etherx.jabber.org/streams"; @@ -162,3 +163,28 @@ impl FromElement for Feature { } } } + +pub struct Error { + error: StreamError, + text: Option, +} + +impl FromElement for Error { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult { + element.check_name("error")?; + element.check_namespace(XMLNS)?; + + let error = element.pop_child_one()?; + let text = element.pop_child_opt()?; + + Ok(Error { error, text }) + } +} + +impl IntoElement for Error { + fn builder(&self) -> ElementBuilder { + Element::builder("error", Some(XMLNS)) + .push_child(self.error.clone()) + .push_child_opt(self.text.clone()) + } +} diff --git a/src/stanza/stream_error.rs b/src/stanza/stream_error.rs new file mode 100644 index 0000000..37db8a1 --- /dev/null +++ b/src/stanza/stream_error.rs @@ -0,0 +1,137 @@ +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, XML_NS, +}; + +pub const XMLNS: &str = "urn:ietf:params:xml:ns:xmpp-streams"; + +#[derive(Clone)] +pub enum Error { + BadFormat, + BadNamespacePrefix, + Conflict, + ConnectionTimeout, + HostGone, + HostUnknown, + ImproperAddressing, + InternalServerError, + InvalidFrom, + InvalidId, + InvalidNamespace, + InvalidXml, + NotAuthorized, + NotWellFormed, + PolicyViolation, + RemoteConnectionFailed, + Reset, + ResourceConstraint, + RestrictedXml, + SeeOtherHost(Option), + SystemShutdown, + UndefinedCondition, + UnsupportedEncoding, + UnsupportedStanzaType, + UnsupportedVersion, +} + +impl FromElement for Error { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + let error; + match element.identify() { + (Some(XMLNS), "bad-format") => error = Error::BadFormat, + (Some(XMLNS), "bad-namespace-prefix") => error = Error::BadNamespacePrefix, + (Some(XMLNS), "conflict") => error = Error::Conflict, + (Some(XMLNS), "connection-timeout") => error = Error::ConnectionTimeout, + (Some(XMLNS), "host-gone") => error = Error::HostGone, + (Some(XMLNS), "host-unknown") => error = Error::HostUnknown, + (Some(XMLNS), "improper-addressing") => error = Error::ImproperAddressing, + (Some(XMLNS), "internal-server-error") => error = Error::InternalServerError, + (Some(XMLNS), "invalid-from") => error = Error::InvalidFrom, + (Some(XMLNS), "invalid-id") => error = Error::InvalidId, + (Some(XMLNS), "invalid-namespace") => error = Error::InvalidNamespace, + (Some(XMLNS), "invalid-xml") => error = Error::InvalidXml, + (Some(XMLNS), "not-authorized") => error = Error::NotAuthorized, + (Some(XMLNS), "not-well-formed") => error = Error::NotWellFormed, + (Some(XMLNS), "policy-violation") => error = Error::PolicyViolation, + (Some(XMLNS), "remote-connection-failed") => error = Error::RemoteConnectionFailed, + (Some(XMLNS), "reset") => error = Error::Reset, + (Some(XMLNS), "resource-constraint") => error = Error::ResourceConstraint, + (Some(XMLNS), "restricted-xml") => error = Error::RestrictedXml, + (Some(XMLNS), "see-other-host") => { + return Ok(Error::SeeOtherHost(element.pop_value_opt()?)) + } + (Some(XMLNS), "system-shutdown") => error = Error::SystemShutdown, + (Some(XMLNS), "undefined-condition") => error = Error::UndefinedCondition, + (Some(XMLNS), "unsupported-encoding") => error = Error::UnsupportedEncoding, + (Some(XMLNS), "unsupported-stanza-type") => error = Error::UnsupportedStanzaType, + (Some(XMLNS), "unsupported-version") => error = Error::UnsupportedVersion, + _ => return Err(DeserializeError::UnexpectedElement(element)), + } + element.no_more_content()?; + return Ok(error); + } +} + +impl IntoElement for Error { + fn builder(&self) -> peanuts::element::ElementBuilder { + match self { + Error::BadFormat => Element::builder("bad-format", Some(XMLNS)), + Error::BadNamespacePrefix => Element::builder("bad-namespace-prefix", Some(XMLNS)), + Error::Conflict => Element::builder("conflict", Some(XMLNS)), + Error::ConnectionTimeout => Element::builder("connection-timeout", Some(XMLNS)), + Error::HostGone => Element::builder("host-gone", Some(XMLNS)), + Error::HostUnknown => Element::builder("host-unknown", Some(XMLNS)), + Error::ImproperAddressing => Element::builder("improper-addressing", Some(XMLNS)), + Error::InternalServerError => Element::builder("internal-server-error", Some(XMLNS)), + Error::InvalidFrom => Element::builder("invalid-from", Some(XMLNS)), + Error::InvalidId => Element::builder("invalid-id", Some(XMLNS)), + Error::InvalidNamespace => Element::builder("invalid-namespace", Some(XMLNS)), + Error::InvalidXml => Element::builder("invalid-xml", Some(XMLNS)), + Error::NotAuthorized => Element::builder("not-authorized", Some(XMLNS)), + Error::NotWellFormed => Element::builder("not-well-formed", Some(XMLNS)), + Error::PolicyViolation => Element::builder("policy-violation", Some(XMLNS)), + Error::RemoteConnectionFailed => { + Element::builder("remote-connection-failed", Some(XMLNS)) + } + Error::Reset => Element::builder("reset", Some(XMLNS)), + Error::ResourceConstraint => Element::builder("resource-constraint", Some(XMLNS)), + Error::RestrictedXml => Element::builder("restricted-xml", Some(XMLNS)), + Error::SeeOtherHost(h) => { + Element::builder("see-other-host", Some(XMLNS)).push_text_opt(h.clone()) + } + Error::SystemShutdown => Element::builder("system-shutdown", Some(XMLNS)), + Error::UndefinedCondition => Element::builder("undefined-condition", Some(XMLNS)), + Error::UnsupportedEncoding => Element::builder("unsupported-encoding", Some(XMLNS)), + Error::UnsupportedStanzaType => { + Element::builder("unsupported-stanza-type", Some(XMLNS)) + } + Error::UnsupportedVersion => Element::builder("unsupported-version", Some(XMLNS)), + } + } +} + +#[derive(Clone)] +pub struct Text { + text: Option, + lang: Option, +} + +impl FromElement for Text { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + element.check_name("text")?; + element.check_name(XMLNS)?; + + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + let text = element.pop_value_opt()?; + + Ok(Text { lang, text }) + } +} + +impl IntoElement for Text { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("text", Some(XMLNS)) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_text_opt(self.text.clone()) + } +}