use color_eyre::eyre;
use color_eyre::eyre::{bail, eyre, Error, OptionExt};
use color_eyre::owo_colors::OwoColorize;
use futures::TryStreamExt;
use itertools::Itertools;
use chris::types::{FeedId, PluginInstanceId};
use chris::{
Access, BaseChrisClient, ChrisClient, EitherClient, Feed, FeedRo, PluginInstance,
PluginInstanceRo, PluginInstanceRw, RoAccess,
};
use crate::arg::GivenPluginInstanceOrPath;
use crate::error_messages::CANNOT_ANONYMOUSLY_SEARCH;
#[derive(Debug, Clone)]
pub enum GivenDataNode {
FeedId { id: FeedId, original: String },
FeedName(String),
PluginInstanceOrPath(GivenPluginInstanceOrPath),
Ambiguous(String),
}
pub enum FeedOrPluginInstance<A: Access> {
Feed(Feed<A>),
PluginInstance(PluginInstance<A>),
}
impl From<String> for GivenDataNode {
fn from(value: String) -> Self {
if let Some(id) = parse_feed_id_from_url(&value) {
GivenDataNode::FeedId {
id,
original: value,
}
} else {
value
.split_once('/')
.and_then(parse_split_qualified_feed)
.unwrap_or_else(|| differentiate_plinst_or_ambiguous(value))
}
}
}
impl From<PluginInstanceId> for GivenDataNode {
fn from(value: PluginInstanceId) -> Self {
let orig = format!("plugininstance/{}", value.0);
GivenDataNode::PluginInstanceOrPath(GivenPluginInstanceOrPath::Id(value, orig))
}
}
fn parse_split_qualified_feed((left, right): (&str, &str)) -> Option<GivenDataNode> {
if left == "f" || left == "feed" {
Some(
right
.parse::<u32>()
.map(FeedId)
.map(|id| GivenDataNode::FeedId {
id,
original: right.to_string(),
})
.unwrap_or(GivenDataNode::FeedName(right.to_string())),
)
} else {
None
}
}
fn differentiate_plinst_or_ambiguous(value: String) -> GivenDataNode {
let plinst = GivenPluginInstanceOrPath::from(value);
if let GivenPluginInstanceOrPath::Title(ambiguous) = plinst {
GivenDataNode::Ambiguous(ambiguous)
} else {
GivenDataNode::PluginInstanceOrPath(plinst)
}
}
fn parse_feed_id_from_url(url: &str) -> Option<FeedId> {
if !url.starts_with("http://") && !url.starts_with("https://") {
return None;
}
url.split_once("/api/v1/")
.map(|(_, right)| right)
.and_then(|s| s.strip_suffix('/'))
.and_then(|s| s.parse().ok())
.map(FeedId)
}
impl GivenDataNode {
pub fn as_arg_str(&self) -> &str {
match self {
GivenDataNode::FeedId { original, .. } => original.as_str(),
GivenDataNode::FeedName(name) => name.as_str(),
GivenDataNode::PluginInstanceOrPath(p) => p.as_arg_str(),
GivenDataNode::Ambiguous(s) => s.as_str(),
}
}
pub fn is_path(&self) -> bool {
if let GivenDataNode::PluginInstanceOrPath(p) = &self {
matches!(
p,
GivenPluginInstanceOrPath::AbsolutePath(_)
| GivenPluginInstanceOrPath::RelativePath(_)
)
} else {
false
}
}
pub async fn into_or(
self,
client: &EitherClient,
old: Option<PluginInstanceId>,
) -> eyre::Result<FeedOrPluginInstance<RoAccess>> {
match self {
GivenDataNode::FeedId { id, .. } => client
.get_feed(id)
.await
.map(FeedOrPluginInstance::Feed)
.map_err(Error::new),
GivenDataNode::FeedName(name) => get_feedro_by_name(client, &name)
.await
.map(FeedOrPluginInstance::Feed),
GivenDataNode::PluginInstanceOrPath(p) => p.get_using_either(client, old).await
.map(FeedOrPluginInstance::PluginInstance),
GivenDataNode::Ambiguous(_) => Err(Error::msg(
"Operand is ambiguous, cannot differentiate between feed name or plugin instance title",
)),
}
}
pub async fn into_plinst_rw(
self,
client: &ChrisClient,
old: Option<PluginInstanceId>,
) -> eyre::Result<PluginInstanceRw> {
match self {
GivenDataNode::FeedId { id, .. } => get_plinst_of_feed(client, id).await,
GivenDataNode::FeedName(name) => {
let feed_id = get_feedid_by_name(client, name).await?;
get_plinst_of_feed(client, feed_id).await
}
GivenDataNode::PluginInstanceOrPath(given) => given.get_using_rw(client, old).await,
GivenDataNode::Ambiguous(value) => {
GivenPluginInstanceOrPath::from(value)
.get_using_rw(client, old)
.await
}
}
}
pub async fn into_plinst_either(
self,
client: &EitherClient,
old: Option<PluginInstanceId>,
) -> eyre::Result<PluginInstanceRo> {
if let Some(logged_in) = client.logged_in_ref() {
match self {
GivenDataNode::FeedId { id, .. } => {
return get_plinst_of_feed(logged_in, id).await.map(|p| p.into());
}
GivenDataNode::FeedName(name) => {
let feed_id = get_feedid_by_name(logged_in, name).await?;
return get_plinst_of_feed(logged_in, feed_id)
.await
.map(|p| p.into());
}
_ => (),
}
};
match self {
GivenDataNode::FeedId { .. } => Err(eyre!(CANNOT_ANONYMOUSLY_SEARCH)),
GivenDataNode::FeedName(_) => Err(eyre!(CANNOT_ANONYMOUSLY_SEARCH)),
GivenDataNode::PluginInstanceOrPath(given) => given.get_using_either(client, old).await,
GivenDataNode::Ambiguous(given) => {
GivenPluginInstanceOrPath::from(given)
.get_using_either(client, old)
.await
}
}
}
pub async fn into_path(
self,
client: &EitherClient,
old: Option<PluginInstanceId>,
) -> eyre::Result<String> {
if let Some(logged_in) = client.logged_in_ref() {
match self {
GivenDataNode::FeedId { id, .. } => {
return get_plinst_of_feed(logged_in, id).await.map(plinst_path);
}
GivenDataNode::FeedName(name) => {
let feed_id = get_feedid_by_name(logged_in, name).await?;
return get_plinst_of_feed(logged_in, feed_id)
.await
.map(plinst_path);
}
_ => (),
}
}
match self {
GivenDataNode::FeedId { .. } => Err(eyre!(CANNOT_ANONYMOUSLY_SEARCH)),
GivenDataNode::FeedName(_) => Err(eyre!(CANNOT_ANONYMOUSLY_SEARCH)),
GivenDataNode::PluginInstanceOrPath(given) => given.into_path(client, old).await,
GivenDataNode::Ambiguous(given) => {
GivenPluginInstanceOrPath::from(given)
.into_path(client, old)
.await
}
}
}
}
fn plinst_path<A: Access>(p: PluginInstance<A>) -> String {
p.object
.output_path
.strip_suffix("/data")
.map(|p| p.to_string())
.unwrap_or(p.object.output_path)
}
async fn get_plinst_of_feed(
client: &ChrisClient,
feed_id: FeedId,
) -> eyre::Result<PluginInstanceRw> {
client
.plugin_instances()
.feed_id(feed_id)
.search()
.page_limit(1)
.max_items(1)
.get_first()
.await?
.ok_or_else(|| {
eyre!(
"feed/{} does not contain plugin instances. This is a CUBE bug.",
feed_id.0
)
})
}
async fn get_feedid_by_name(client: &ChrisClient, name: String) -> eyre::Result<FeedId> {
let items: Vec<_> = client
.feeds()
.name_exact(name)
.search()
.page_limit(2)
.max_items(2)
.stream()
.map_ok(|f| f.id)
.try_collect()
.await?;
if items.len() > 1 {
bail!("Multiple feeds found, please be more specific.\nHint: run `{}` and specify feed by feed/{}", "chrs list".bold(), "ID".bold().green())
}
items.into_iter().next().ok_or_eyre("Feed not found")
}
async fn get_feedro_by_name(client: &EitherClient, name: &str) -> color_eyre::Result<FeedRo> {
let feeds: Vec<_> = match client {
EitherClient::Anon(c) => {
c.public_feeds()
.name(name)
.search()
.page_limit(10)
.max_items(10)
.stream_connected()
.try_collect()
.await
}
EitherClient::LoggedIn(c) => {
let private_feeds: Vec<_> = c
.feeds()
.name(name)
.search()
.page_limit(10)
.max_items(10)
.stream_connected()
.map_ok(|f| f.into())
.try_collect()
.await?;
if private_feeds.is_empty() {
c.public_feeds()
.name(name)
.search()
.page_limit(10)
.max_items(10)
.stream_connected()
.try_collect()
.await
} else {
Ok(private_feeds)
}
}
}?;
if feeds.len() > 1 {
bail!(
"More than one feed found: {}",
feeds
.iter()
.map(|f| format!("feed/{}", f.object.id.0))
.join(" ")
)
}
feeds.into_iter().next().ok_or_eyre("Feed not found")
}