implement remaining rfc6120 xml schemas

This commit is contained in:
cel 🌸 2024-12-03 03:51:26 +00:00
parent be198ca15b
commit 7c2577d196
10 changed files with 562 additions and 28 deletions

View File

@ -2,19 +2,16 @@ use std::str;
use std::sync::Arc; use std::sync::Arc;
use async_recursion::async_recursion; use async_recursion::async_recursion;
use peanuts::element::{FromElement, IntoElement}; use peanuts::element::IntoElement;
use peanuts::{Reader, Writer}; use peanuts::{Reader, Writer};
use rsasl::prelude::{Mechname, SASLClient, SASLConfig}; use rsasl::prelude::{Mechname, SASLClient, SASLConfig};
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter, ReadHalf, WriteHalf}; use tokio::io::{AsyncRead, AsyncWrite, ReadHalf, WriteHalf};
use tokio::time::timeout;
use tokio_native_tls::native_tls::TlsConnector; use tokio_native_tls::native_tls::TlsConnector;
use tracing::{debug, info, instrument, trace}; use tracing::{debug, instrument};
use trust_dns_resolver::proto::rr::domain::IntoLabel;
use crate::connection::{Tls, Unencrypted}; use crate::connection::{Tls, Unencrypted};
use crate::error::Error; use crate::error::Error;
use crate::stanza::bind::{Bind, BindType, FullJidType, ResourceType}; 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::client::iq::{Iq, IqType, Query};
use crate::stanza::sasl::{Auth, Challenge, Mechanisms, Response, ServerResponse}; use crate::stanza::sasl::{Auth, Challenge, Mechanisms, Response, ServerResponse};
use crate::stanza::starttls::{Proceed, StartTls}; use crate::stanza::starttls::{Proceed, StartTls};
@ -257,7 +254,7 @@ where
// server to client // server to client
// may or may not send a declaration // 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 // receive stream element and validate
let text = str::from_utf8(self.reader.buffer.data()).unwrap(); let text = str::from_utf8(self.reader.buffer.data()).unwrap();
@ -471,7 +468,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn negotiate() { 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 .await
.unwrap() .unwrap()
.ensure_tls() .ensure_tls()

View File

@ -9,14 +9,20 @@ pub mod jid;
pub mod stanza; pub mod stanza;
pub use connection::Connection; pub use connection::Connection;
use connection::Tls;
pub use error::Error; pub use error::Error;
pub use jabber::Jabber; pub use jabber::Jabber;
pub use jid::JID; pub use jid::JID;
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
pub async fn login<J: TryInto<JID>, P: AsRef<str>>(jid: J, password: P) -> Result<Connection> { pub async fn login<J: AsRef<str>, P: AsRef<str>>(jid: J, password: P) -> Result<Jabber<Tls>> {
todo!() Ok(Connection::connect_user(jid, password.as_ref().to_string())
.await?
.ensure_tls()
.await?
.negotiate()
.await?)
} }
#[cfg(test)] #[cfg(test)]

View File

@ -3,8 +3,8 @@ use std::str::FromStr;
use peanuts::element::{FromElement, IntoElement}; use peanuts::element::{FromElement, IntoElement};
use peanuts::{DeserializeError, Element}; use peanuts::{DeserializeError, Element};
use crate::stanza::error::Text; use crate::stanza::stanza_error::Error as StanzaError;
use crate::stanza::Error as StanzaError; use crate::stanza::stanza_error::Text;
use super::XMLNS; use super::XMLNS;

View File

@ -1,37 +1,186 @@
use std::str::FromStr;
use peanuts::{
element::{FromElement, IntoElement},
DeserializeError, Element, XML_NS,
};
use crate::JID; use crate::JID;
use super::XMLNS;
pub struct Message { pub struct Message {
from: Option<JID>, from: Option<JID>,
id: Option<String>, id: Option<String>,
to: Option<JID>, to: Option<JID>,
r#type: Option<MessageType>, // can be omitted, if so default to normal
r#type: MessageType,
lang: Option<String>,
// children // children
subject: Option<Subject>, subject: Option<Subject>,
body: Option<Body>, body: Option<Body>,
thread: Option<Thread>, thread: Option<Thread>,
lang: Option<String>,
} }
impl FromElement for Message {
fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> {
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 { pub enum MessageType {
Chat, Chat,
Error, Error,
Groupchat, Groupchat,
Headline, Headline,
#[default]
Normal, Normal,
} }
impl FromStr for MessageType {
type Err = DeserializeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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 { pub struct Body {
lang: Option<String>, lang: Option<String>,
body: Option<String>, body: Option<String>,
} }
impl FromElement for Body {
fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
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 { pub struct Subject {
lang: Option<String>, lang: Option<String>,
subject: Option<String>, subject: Option<String>,
} }
impl FromElement for Subject {
fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
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 { pub struct Thread {
// TODO: NOT DONE
parent: Option<String>, parent: Option<String>,
thread: Option<String>, thread: Option<String>,
} }
impl FromElement for Thread {
fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
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())
}
}

View File

@ -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 error;
pub mod iq; pub mod iq;
pub mod message; pub mod message;
pub mod presence; pub mod presence;
pub const XMLNS: &str = "jabber:client"; 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<Self> {
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(),
}
}
}

View File

@ -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 crate::JID;
use super::error::Error; use super::{error::Error, XMLNS};
pub struct Presence { pub struct Presence {
from: Option<JID>, from: Option<JID>,
id: Option<String>, id: Option<String>,
to: Option<JID>, to: Option<JID>,
r#type: PresenceType, r#type: Option<PresenceType>,
lang: Option<String>, lang: Option<String>,
// children // children
show: Option<Show>, show: Option<Show>,
status: Option<Status>, status: Option<Status>,
priority: Option<Priority>, priority: Option<Priority>,
// TODO: ##other
// other: Vec<Other>,
errors: Vec<Error>, errors: Vec<Error>,
// ##other
// content: Vec<Box<dyn AsElement>>,
} }
impl FromElement for Presence {
fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> {
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 { pub enum PresenceType {
Error, Error,
Probe, Probe,
@ -29,6 +82,38 @@ pub enum PresenceType {
Unsubscribed, Unsubscribed,
} }
impl FromStr for PresenceType {
type Err = DeserializeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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 { pub enum Show {
Away, Away,
Chat, Chat,
@ -36,13 +121,106 @@ pub enum Show {
Xa, Xa,
} }
impl FromElement for Show {
fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> {
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<Self, Self::Err> {
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 { pub struct Status {
lang: Option<String>, lang: Option<String>,
status: String1024, status: String1024,
} }
// minLength 1 maxLength 1024 impl FromElement for Status {
pub struct String1024(String); fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> {
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<Self, Self::Err> {
Ok(String1024(s.to_string()))
}
}
impl ToString for String1024 {
fn to_string(&self) -> String {
self.0.clone()
}
}
// xs:byte // 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<Self> {
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)
}
}

View File

@ -2,11 +2,10 @@ use peanuts::declaration::VersionInfo;
pub mod bind; pub mod bind;
pub mod client; pub mod client;
pub mod error;
pub mod sasl; pub mod sasl;
pub mod stanza_error;
pub mod starttls; pub mod starttls;
pub mod stream; pub mod stream;
pub mod stream_error;
pub static XML_VERSION: VersionInfo = VersionInfo::One; pub static XML_VERSION: VersionInfo = VersionInfo::One;
pub use error::Error;

View File

@ -6,11 +6,12 @@ use peanuts::{element::Name, Element};
use tracing::debug; use tracing::debug;
use crate::stanza::bind; use crate::stanza::bind;
use crate::{Error, JID}; use crate::JID;
use super::client;
use super::sasl::{self, Mechanisms}; use super::sasl::{self, Mechanisms};
use super::starttls::{self, StartTls}; 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"; pub const XMLNS: &str = "http://etherx.jabber.org/streams";
@ -162,3 +163,28 @@ impl FromElement for Feature {
} }
} }
} }
pub struct Error {
error: StreamError,
text: Option<Text>,
}
impl FromElement for Error {
fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> {
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())
}
}

137
src/stanza/stream_error.rs Normal file
View File

@ -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<String>),
SystemShutdown,
UndefinedCondition,
UnsupportedEncoding,
UnsupportedStanzaType,
UnsupportedVersion,
}
impl FromElement for Error {
fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
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<String>,
lang: Option<String>,
}
impl FromElement for Text {
fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
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())
}
}