Skip to main content

live_stream/
catalog.rs

1//! Browseable view over a parsed manifest.
2
3use live_data::{FeedDescriptor, FeedManifest, FormatPreference, TransportTag};
4
5/// Wraps a parsed [`FeedManifest`] and exposes filter, search, and
6/// find-by-name helpers for consumer code that wants to browse what a
7/// publisher offers.
8#[derive(Debug, Clone)]
9pub struct FeedCatalog {
10    manifest: FeedManifest,
11}
12
13impl FeedCatalog {
14    /// Take ownership of a manifest.
15    pub fn new(manifest: FeedManifest) -> Self {
16        Self { manifest }
17    }
18
19    /// Parse a manifest from JSON bytes.
20    pub fn from_json(bytes: &[u8]) -> serde_json::Result<Self> {
21        let manifest = serde_json::from_slice(bytes)?;
22        Ok(Self::new(manifest))
23    }
24
25    /// Parse a manifest from a JSON string.
26    pub fn from_json_str(s: &str) -> serde_json::Result<Self> {
27        Self::from_json(s.as_bytes())
28    }
29
30    pub fn manifest(&self) -> &FeedManifest {
31        &self.manifest
32    }
33
34    pub fn into_manifest(self) -> FeedManifest {
35        self.manifest
36    }
37
38    pub fn protocol_version(&self) -> u32 {
39        self.manifest.protocol_version
40    }
41
42    pub fn server_version(&self) -> &str {
43        &self.manifest.server_version
44    }
45
46    pub fn feeds(&self) -> &[FeedDescriptor] {
47        &self.manifest.feeds
48    }
49
50    pub fn len(&self) -> usize {
51        self.manifest.feeds.len()
52    }
53
54    pub fn is_empty(&self) -> bool {
55        self.manifest.feeds.is_empty()
56    }
57
58    /// Locate a feed by exact name.
59    pub fn find(&self, name: &str) -> Option<&FeedDescriptor> {
60        self.manifest.feeds.iter().find(|f| f.name == name)
61    }
62
63    /// All feeds carrying the given tag.
64    pub fn filter_by_tag<'a>(
65        &'a self,
66        tag: &'a str,
67    ) -> impl Iterator<Item = &'a FeedDescriptor> + 'a {
68        self.manifest
69            .feeds
70            .iter()
71            .filter(move |f| f.tags.iter().any(|t| t == tag))
72    }
73
74    /// All feeds advertising the given transport.
75    pub fn filter_by_transport(
76        &self,
77        t: TransportTag,
78    ) -> impl Iterator<Item = &FeedDescriptor> {
79        self.manifest.feeds.iter().filter(move |f| f.transports.contains(&t))
80    }
81
82    /// All feeds advertising the given format.
83    pub fn filter_by_format(
84        &self,
85        f: FormatPreference,
86    ) -> impl Iterator<Item = &FeedDescriptor> {
87        self.manifest.feeds.iter().filter(move |feed| feed.formats.contains(&f))
88    }
89
90    /// Case-insensitive substring match on feed name or description.
91    pub fn search<'a>(
92        &'a self,
93        query: &'a str,
94    ) -> impl Iterator<Item = &'a FeedDescriptor> + 'a {
95        let q = query.to_ascii_lowercase();
96        self.manifest.feeds.iter().filter(move |f| {
97            f.name.to_ascii_lowercase().contains(&q)
98                || f.description
99                    .as_ref()
100                    .map(|d| d.to_ascii_lowercase().contains(&q))
101                    .unwrap_or(false)
102        })
103    }
104}
105
106impl From<FeedManifest> for FeedCatalog {
107    fn from(m: FeedManifest) -> Self {
108        Self::new(m)
109    }
110}