use std::collections::HashMap;
use std::pin::Pin;
use std::sync::{Arc, RwLock};
use futures_util::Stream;
use reqwest::Method;
use serde::Deserialize;
use serde_json::Value;
use crate::conclusion::ConclusionScope;
use crate::dialectic_stream::DialecticStream;
use crate::error::{HonchoError, Result};
use crate::http::client::HttpClient;
use crate::http::routes;
use crate::http::sse::parse_sse_stream;
use crate::types::dialectic::RepresentationResponse;
use crate::types::dialectic::{DialecticOptions, ReasoningLevel};
use crate::types::message::{MessageCreate, MessageResponse, MessageSearchOptions};
use crate::types::pagination::{self, Page};
use crate::types::peer::Peer as PeerResponse;
use crate::types::peer::{PeerCardResponse, PeerCardSet, PeerConfig, PeerContext};
use crate::types::session::{Session, SessionListOptions};
pub(crate) struct PeerInner {
http: HttpClient,
workspace_id: String,
id: String,
metadata: RwLock<Option<HashMap<String, Value>>>,
configuration: RwLock<Option<PeerConfig>>,
}
#[derive(Clone)]
pub struct Peer {
inner: Arc<PeerInner>,
}
#[derive(Deserialize)]
struct ChatResponse {
#[serde(default)]
content: Option<String>,
}
impl std::fmt::Debug for Peer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Peer")
.field("id", &self.inner.id)
.field("workspace_id", &self.inner.workspace_id)
.finish()
}
}
impl Peer {
pub(crate) fn from_parts(
http: HttpClient,
workspace_id: String,
resp: PeerResponse,
) -> Result<Self> {
let config = map_to_peer_config(&resp.configuration)?;
Ok(Self {
inner: Arc::new(PeerInner {
http,
workspace_id,
id: resp.id,
metadata: RwLock::new(Some(resp.metadata)),
configuration: RwLock::new(config),
}),
})
}
pub(crate) fn from_response(honcho: &crate::Honcho, resp: PeerResponse) -> Result<Self> {
Self::from_parts(
honcho.http().clone(),
honcho.workspace_id().to_owned(),
resp,
)
}
#[must_use]
pub fn id(&self) -> &str {
&self.inner.id
}
#[must_use]
pub fn metadata(&self) -> Option<HashMap<String, Value>> {
self.inner
.metadata
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
#[must_use]
pub fn configuration(&self) -> Option<PeerConfig> {
self.inner
.configuration
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub async fn refresh(&self) -> Result<()> {
let mut body_map = serde_json::Map::new();
body_map.insert("id".into(), Value::String(self.inner.id.clone()));
let body = Value::Object(body_map);
let resp: PeerResponse = self
.inner
.http
.post(&routes::peers(&self.inner.workspace_id)?, Some(&body), &[])
.await?;
*self
.inner
.metadata
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(resp.metadata);
*self
.inner
.configuration
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
map_to_peer_config(&resp.configuration)?;
Ok(())
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(peer_id = self.inner.id.as_str())))]
pub async fn get_metadata(&self) -> Result<HashMap<String, Value>> {
self.refresh().await?;
Ok(self
.inner
.metadata
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.unwrap_or_default())
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, metadata), fields(peer_id = self.inner.id.as_str())))]
pub async fn set_metadata(&self, metadata: HashMap<String, Value>) -> Result<()> {
let body = crate::types::peer::PeerMetadataSet { metadata };
let resp: PeerResponse = self
.inner
.http
.put(
&routes::peer(&self.inner.workspace_id, &self.inner.id)?,
Some(&body),
&[],
)
.await?;
*self
.inner
.metadata
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(resp.metadata);
Ok(())
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(peer_id = self.inner.id.as_str())))]
pub async fn get_configuration(&self) -> Result<PeerConfig> {
self.refresh().await?;
Ok(self
.inner
.configuration
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.unwrap_or_default())
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, config), fields(peer_id = self.inner.id.as_str())))]
pub async fn set_configuration(&self, config: &PeerConfig) -> Result<()> {
let body = crate::types::peer::PeerConfigurationSet {
configuration: config.clone(),
};
let resp: PeerResponse = self
.inner
.http
.put(
&routes::peer(&self.inner.workspace_id, &self.inner.id)?,
Some(&body),
&[],
)
.await?;
*self
.inner
.configuration
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
map_to_peer_config(&resp.configuration)?;
Ok(())
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(peer_id = self.inner.id.as_str())))]
pub async fn get_configuration_raw(&self) -> Result<HashMap<String, Value>> {
let mut body_map = serde_json::Map::new();
body_map.insert("id".into(), Value::String(self.inner.id.clone()));
let body = Value::Object(body_map);
let resp: PeerResponse = self
.inner
.http
.post(&routes::peers(&self.inner.workspace_id)?, Some(&body), &[])
.await?;
*self
.inner
.metadata
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(resp.metadata.clone());
*self
.inner
.configuration
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
map_to_peer_config(&resp.configuration)?;
Ok(resp.configuration)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, config), fields(peer_id = self.inner.id.as_str())))]
pub async fn set_configuration_raw(&self, config: HashMap<String, Value>) -> Result<()> {
let body = serde_json::json!({"configuration": config});
let resp: PeerResponse = self
.inner
.http
.put(
&routes::peer(&self.inner.workspace_id, &self.inner.id)?,
Some(&body),
&[],
)
.await?;
*self
.inner
.configuration
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
map_to_peer_config(&resp.configuration)?;
Ok(())
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, metadata), fields(peer_id = self.inner.id.as_str())))]
pub async fn update(&self, metadata: HashMap<String, Value>) -> Result<()> {
let body = crate::types::peer::PeerMetadataSet { metadata };
let resp: PeerResponse = self
.inner
.http
.put(
&routes::peer(&self.inner.workspace_id, &self.inner.id)?,
Some(&body),
&[],
)
.await?;
*self
.inner
.metadata
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(resp.metadata);
Ok(())
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(peer_id = self.inner.id.as_str())))]
pub async fn chat(&self, query: &str) -> Result<Option<String>> {
if query.is_empty() {
return Err(HonchoError::Validation(
"query must not be empty".to_owned(),
));
}
let body = crate::types::dialectic::DialecticOptions {
query: query.to_owned(),
session_id: None,
target: None,
stream: false,
reasoning_level: crate::types::dialectic::ReasoningLevel::default(),
};
let resp: ChatResponse = self
.inner
.http
.post(
&routes::peer_chat(&self.inner.workspace_id, &self.inner.id)?,
Some(&body),
&[],
)
.await?;
match resp.content {
Some(c) if !c.is_empty() => Ok(Some(c)),
_ => Ok(None),
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, options), fields(peer_id = self.inner.id.as_str())))]
pub async fn chat_with_options(&self, options: &DialecticOptions) -> Result<Option<String>> {
if options.query.is_empty() {
return Err(HonchoError::Validation(
"query must not be empty".to_owned(),
));
}
let resp: ChatResponse = self
.inner
.http
.post(
&routes::peer_chat(&self.inner.workspace_id, &self.inner.id)?,
Some(options),
&[],
)
.await?;
match resp.content {
Some(c) if !c.is_empty() => Ok(Some(c)),
_ => Ok(None),
}
}
pub fn chat_stream(&self, query: impl Into<String>) -> ChatStreamBuilder {
ChatStreamBuilder {
http: self.inner.http.clone(),
workspace_id: self.inner.workspace_id.clone(),
peer_id: self.inner.id.clone(),
query: query.into(),
target: None,
session_id: None,
reasoning_level: None,
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(peer_id = self.inner.id.as_str())))]
pub async fn representation(&self) -> Result<String> {
let route = routes::peer_representation(&self.inner.workspace_id, &self.inner.id)?;
let body = crate::types::peer::PeerRepresentationGet {
session_id: None,
target: None,
search_query: None,
search_top_k: None,
search_max_distance: None,
include_most_frequent: None,
max_conclusions: None,
};
let resp: RepresentationResponse = self.inner.http.post(&route, Some(&body), &[]).await?;
Ok(resp.representation)
}
#[must_use]
pub fn representation_builder(&self) -> RepresentationBuilder {
RepresentationBuilder {
http: self.inner.http.clone(),
workspace_id: self.inner.workspace_id.clone(),
peer_id: self.inner.id.clone(),
session_id: None,
target: None,
search_query: None,
search_top_k: None,
search_max_distance: None,
include_most_frequent: None,
max_conclusions: None,
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(peer_id = self.inner.id.as_str())))]
pub async fn context(&self) -> Result<PeerContext> {
self.context_builder().send().await
}
#[must_use]
pub fn context_builder(&self) -> ContextBuilder {
ContextBuilder {
http: self.inner.http.clone(),
workspace_id: self.inner.workspace_id.clone(),
peer_id: self.inner.id.clone(),
target: None,
summary: None,
limit_to_session: None,
max_conclusions: None,
search_query: None,
search_top_k: None,
search_max_distance: None,
include_most_frequent: None,
}
}
#[deprecated(since = "0.1.1", note = "use `Peer::context_builder()` instead")]
#[allow(deprecated)]
pub async fn context_with_target(&self, target: &str) -> Result<PeerContext> {
let opts = crate::types::peer::PeerContextOptions::builder()
.target(target)
.build();
self.context_with_options(&opts).await
}
#[deprecated(since = "0.1.1", note = "use `Peer::context_builder()` instead")]
#[allow(deprecated)]
pub async fn context_with_options(
&self,
options: &crate::types::peer::PeerContextOptions,
) -> Result<PeerContext> {
let route = routes::peer_context(&self.inner.workspace_id, &self.inner.id)?;
let mut params: Vec<(&str, String)> = Vec::new();
if let Some(ref v) = options.target {
params.push(("target", v.clone()));
}
if let Some(ref v) = options.search_query {
params.push(("search_query", v.clone()));
}
if let Some(ref v) = options.search_top_k {
params.push(("search_top_k", v.to_string()));
}
if let Some(ref v) = options.search_max_distance {
params.push(("search_max_distance", v.to_string()));
}
if let Some(ref v) = options.include_most_frequent {
params.push((
"include_most_frequent",
if *v { "true" } else { "false" }.to_string(),
));
}
if let Some(ref v) = options.max_conclusions {
params.push(("max_conclusions", v.to_string()));
}
let refs: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
self.inner.http.get(&route, &refs).await
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(peer_id = self.inner.id.as_str())))]
pub async fn sessions(&self) -> Result<Page<Session>> {
let route = routes::peer_sessions_list(&self.inner.workspace_id, &self.inner.id)?;
pagination::paginate_post(&self.inner.http, &route, None, 1, 50, false).await
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, options), fields(peer_id = self.inner.id.as_str())))]
pub async fn sessions_with_options(
&self,
options: &SessionListOptions,
) -> Result<Page<Session>> {
let route = routes::peer_sessions_list(&self.inner.workspace_id, &self.inner.id)?;
let body = options
.filters
.as_ref()
.map(|f| crate::types::session::SessionGet {
filters: Some(f.clone()),
});
let body_val = body
.as_ref()
.map(|b| serde_json::to_value(b).map_err(|e| HonchoError::Configuration(e.to_string())))
.transpose()?;
pagination::paginate_post(
&self.inner.http,
&route,
body_val.as_ref(),
options.page,
options.size,
options.reverse,
)
.await
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(peer_id = self.inner.id.as_str())))]
pub async fn search(&self, query: &str) -> Result<Vec<crate::Message>> {
self.search_with_options(&MessageSearchOptions {
query: query.to_string(),
filters: None,
limit: 10,
})
.await
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, options), fields(peer_id = self.inner.id.as_str())))]
pub async fn search_with_options(
&self,
options: &MessageSearchOptions,
) -> Result<Vec<crate::Message>> {
if options.query.is_empty() {
return Err(HonchoError::Validation(
"query must not be empty".to_string(),
));
}
let responses: Vec<MessageResponse> = self
.inner
.http
.post(
&routes::peer_search(&self.inner.workspace_id, &self.inner.id)?,
Some(&options),
&[],
)
.await?;
Ok(responses
.into_iter()
.map(|r| crate::Message::from_raw(self.inner.workspace_id.clone(), r))
.collect())
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(peer_id = self.inner.id.as_str())))]
pub async fn get_card(&self) -> Result<Option<Vec<String>>> {
let resp: PeerCardResponse = self
.inner
.http
.get(
&routes::peer_card(&self.inner.workspace_id, &self.inner.id)?,
&[],
)
.await?;
Ok(resp.peer_card)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(peer_id = self.inner.id.as_str())))]
pub async fn get_card_with_target(&self, target: &str) -> Result<Option<Vec<String>>> {
let resp: PeerCardResponse = self
.inner
.http
.get(
&routes::peer_card(&self.inner.workspace_id, &self.inner.id)?,
&[("target", target)],
)
.await?;
Ok(resp.peer_card)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, card), fields(peer_id = self.inner.id.as_str())))]
pub async fn set_card(&self, card: Vec<String>) -> Result<Option<Vec<String>>> {
let body = PeerCardSet::builder().peer_card(card).build();
let resp: PeerCardResponse = self
.inner
.http
.put(
&routes::peer_card(&self.inner.workspace_id, &self.inner.id)?,
Some(&body),
&[],
)
.await?;
Ok(resp.peer_card)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, card), fields(peer_id = self.inner.id.as_str())))]
pub async fn set_card_with_target(
&self,
card: Vec<String>,
target: &str,
) -> Result<Option<Vec<String>>> {
let body = PeerCardSet::builder().peer_card(card).build();
let resp: PeerCardResponse = self
.inner
.http
.put(
&routes::peer_card(&self.inner.workspace_id, &self.inner.id)?,
Some(&body),
&[("target", target)],
)
.await?;
Ok(resp.peer_card)
}
#[must_use]
pub fn conclusions(&self) -> ConclusionScope {
ConclusionScope::new(
self.inner.http.clone(),
self.inner.workspace_id.clone(),
self.id().to_owned(),
self.id().to_owned(),
)
}
#[must_use]
pub fn conclusions_of(&self, target: impl Into<String>) -> ConclusionScope {
ConclusionScope::new(
self.inner.http.clone(),
self.inner.workspace_id.clone(),
self.id().to_owned(),
target.into(),
)
}
#[must_use]
pub fn message(&self, content: impl Into<String>) -> MessageBuilder {
MessageBuilder {
peer_id: self.inner.id.clone(),
content: content.into(),
metadata: None,
configuration: None,
created_at: None,
}
}
}
pub struct ChatStreamBuilder {
http: HttpClient,
workspace_id: String,
peer_id: String,
query: String,
target: Option<String>,
session_id: Option<String>,
reasoning_level: Option<ReasoningLevel>,
}
impl ChatStreamBuilder {
#[must_use]
pub fn target(mut self, target: impl Into<String>) -> Self {
self.target = Some(target.into());
self
}
#[must_use]
pub fn session(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
#[must_use]
pub fn reasoning_level(mut self, level: ReasoningLevel) -> Self {
self.reasoning_level = Some(level);
self
}
#[allow(clippy::type_complexity)]
pub async fn send(
self,
) -> Result<DialecticStream<Pin<Box<dyn Stream<Item = Result<String>> + Send>>>> {
if self.query.is_empty() {
return Err(HonchoError::Validation(
"query must not be empty".to_owned(),
));
}
let opts = DialecticOptions::builder()
.query(self.query)
.stream(true)
.maybe_target(self.target)
.maybe_session_id(self.session_id)
.reasoning_level(self.reasoning_level.unwrap_or_default())
.build();
let route = routes::peer_chat(&self.workspace_id, &self.peer_id)?;
let response = self
.http
.request_streaming(
Method::POST,
&route,
Some(
&serde_json::to_value(&opts)
.map_err(|e| HonchoError::Configuration(e.to_string()))?,
),
&[],
)
.await?;
Ok(DialecticStream::new(Box::pin(parse_sse_stream(
response.bytes_stream(),
))))
}
}
pub struct RepresentationBuilder {
http: HttpClient,
workspace_id: String,
peer_id: String,
session_id: Option<String>,
target: Option<String>,
search_query: Option<String>,
search_top_k: Option<u32>,
search_max_distance: Option<f64>,
include_most_frequent: Option<bool>,
max_conclusions: Option<u32>,
}
impl RepresentationBuilder {
#[must_use]
pub fn session_id(mut self, val: impl Into<String>) -> Self {
self.session_id = Some(val.into());
self
}
#[must_use]
pub fn target(mut self, val: impl Into<String>) -> Self {
self.target = Some(val.into());
self
}
#[must_use]
pub fn search_query(mut self, val: impl Into<String>) -> Self {
self.search_query = Some(val.into());
self
}
#[must_use]
pub fn search_top_k(mut self, val: u32) -> Self {
self.search_top_k = Some(val);
self
}
#[must_use]
pub fn search_max_distance(mut self, val: f64) -> Self {
self.search_max_distance = Some(val);
self
}
#[must_use]
pub fn include_most_frequent(mut self, val: bool) -> Self {
self.include_most_frequent = Some(val);
self
}
#[must_use]
pub fn max_conclusions(mut self, val: u32) -> Self {
self.max_conclusions = Some(val);
self
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(peer_id = self.peer_id.as_str())))]
pub async fn send(self) -> Result<String> {
if let Some(k) = self.search_top_k
&& !(1..=100).contains(&k)
{
return Err(HonchoError::Validation(format!(
"search_top_k must be between 1 and 100, got {k}"
)));
}
if let Some(d) = self.search_max_distance
&& !(0.0..=1.0).contains(&d)
{
return Err(HonchoError::Validation(format!(
"search_max_distance must be between 0.0 and 1.0, got {d}"
)));
}
if let Some(c) = self.max_conclusions
&& !(1..=100).contains(&c)
{
return Err(HonchoError::Validation(format!(
"max_conclusions must be between 1 and 100, got {c}"
)));
}
let params = crate::types::peer::PeerRepresentationGet {
session_id: self.session_id,
target: self.target,
search_query: self.search_query,
search_top_k: self.search_top_k,
search_max_distance: self.search_max_distance,
include_most_frequent: self.include_most_frequent,
max_conclusions: self.max_conclusions,
};
let route = routes::peer_representation(&self.workspace_id, &self.peer_id)?;
let resp: RepresentationResponse = self.http.post(&route, Some(¶ms), &[]).await?;
Ok(resp.representation)
}
}
pub struct ContextBuilder {
http: HttpClient,
workspace_id: String,
peer_id: String,
target: Option<String>,
summary: Option<bool>,
limit_to_session: Option<bool>,
max_conclusions: Option<u32>,
search_query: Option<String>,
search_top_k: Option<u32>,
search_max_distance: Option<f64>,
include_most_frequent: Option<bool>,
}
impl ContextBuilder {
#[must_use]
pub fn target(mut self, val: impl Into<String>) -> Self {
self.target = Some(val.into());
self
}
#[must_use]
pub fn summary(mut self, val: bool) -> Self {
self.summary = Some(val);
self
}
#[must_use]
pub fn limit_to_session(mut self, val: bool) -> Self {
self.limit_to_session = Some(val);
self
}
#[must_use]
pub fn max_conclusions(mut self, val: u32) -> Self {
self.max_conclusions = Some(val);
self
}
#[must_use]
pub fn search_query(mut self, val: impl Into<String>) -> Self {
self.search_query = Some(val.into());
self
}
#[must_use]
pub fn search_top_k(mut self, val: u32) -> Self {
self.search_top_k = Some(val);
self
}
#[must_use]
pub fn search_max_distance(mut self, val: f64) -> Self {
self.search_max_distance = Some(val);
self
}
#[must_use]
pub fn include_most_frequent(mut self, val: bool) -> Self {
self.include_most_frequent = Some(val);
self
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(peer_id = self.peer_id.as_str())))]
pub async fn send(self) -> Result<PeerContext> {
if let Some(k) = self.search_top_k
&& !(1..=100).contains(&k)
{
return Err(HonchoError::Validation(format!(
"search_top_k must be between 1 and 100, got {k}"
)));
}
if let Some(d) = self.search_max_distance
&& !(0.0..=1.0).contains(&d)
{
return Err(HonchoError::Validation(format!(
"search_max_distance must be between 0.0 and 1.0, got {d}"
)));
}
if let Some(c) = self.max_conclusions
&& !(1..=100).contains(&c)
{
return Err(HonchoError::Validation(format!(
"max_conclusions must be between 1 and 100, got {c}"
)));
}
let route = routes::peer_context(&self.workspace_id, &self.peer_id)?;
let mut params: Vec<(&str, String)> = Vec::new();
if let Some(ref v) = self.target {
params.push(("target", v.clone()));
}
if let Some(v) = self.summary {
params.push(("summary", if v { "true" } else { "false" }.to_string()));
}
if let Some(v) = self.limit_to_session {
params.push((
"limit_to_session",
if v { "true" } else { "false" }.to_string(),
));
}
if let Some(ref v) = self.search_query {
params.push(("search_query", v.clone()));
}
if let Some(v) = self.search_top_k {
params.push(("search_top_k", v.to_string()));
}
if let Some(v) = self.search_max_distance {
params.push(("search_max_distance", v.to_string()));
}
if let Some(v) = self.include_most_frequent {
params.push((
"include_most_frequent",
if v { "true" } else { "false" }.to_string(),
));
}
if let Some(v) = self.max_conclusions {
params.push(("max_conclusions", v.to_string()));
}
let refs: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
self.http.get(&route, &refs).await
}
}
const MAX_MESSAGE_CONTENT_LENGTH: usize = 25_000;
pub struct MessageBuilder {
peer_id: String,
content: String,
metadata: Option<HashMap<String, Value>>,
configuration: Option<crate::types::message::MessageConfiguration>,
created_at: Option<chrono::DateTime<chrono::Utc>>,
}
impl MessageBuilder {
#[must_use]
pub fn metadata(mut self, val: HashMap<String, Value>) -> Self {
self.metadata = Some(val);
self
}
#[must_use]
pub fn configuration(mut self, val: crate::types::message::MessageConfiguration) -> Self {
self.configuration = Some(val);
self
}
#[must_use]
pub fn created_at(mut self, val: chrono::DateTime<chrono::Utc>) -> Self {
self.created_at = Some(val);
self
}
pub fn build(self) -> Result<MessageCreate> {
if self.content.trim().is_empty() {
return Err(HonchoError::Validation("content must not be empty".into()));
}
if self.content.len() > MAX_MESSAGE_CONTENT_LENGTH {
return Err(HonchoError::Validation(format!(
"content must be at most {} characters, got {}",
MAX_MESSAGE_CONTENT_LENGTH,
self.content.len(),
)));
}
Ok(MessageCreate {
peer_id: self.peer_id,
content: self.content,
metadata: self.metadata,
configuration: self.configuration,
created_at: self.created_at,
})
}
}
fn map_to_peer_config(map: &HashMap<String, Value>) -> Result<Option<PeerConfig>> {
let val = serde_json::to_value(map).map_err(|e| HonchoError::Configuration(e.to_string()))?;
serde_json::from_value(val)
.map(Some)
.map_err(|e| HonchoError::Configuration(e.to_string()))
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unnecessary_wraps,
clippy::needless_pass_by_value,
clippy::unused_async
)]
mod tests {
use super::*;
use std::pin::Pin;
#[allow(dead_code)]
fn assert_send_static<T: Send + 'static>(_: &T) {}
#[test]
fn chat_stream_return_type_is_send_static() {
fn _assertion(
stream: DialecticStream<
Pin<Box<dyn futures_util::Stream<Item = Result<String>> + Send>>,
>,
) {
assert_send_static(&stream);
}
}
}