Skip to main content

systemprompt_content/services/
content_provider.rs

1//! Default `ContentProvider` backed by the content repository.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use async_trait::async_trait;
7use systemprompt_identifiers::LocaleCode;
8use systemprompt_traits::content::{ContentFilter, ContentItem, ContentProvider, ContentSummary};
9
10use crate::error::ContentError;
11use crate::repository::{ContentRepository, SearchRepository};
12
13#[derive(Debug)]
14pub struct DefaultContentProvider {
15    repo: ContentRepository,
16    search_repo: SearchRepository,
17}
18
19impl DefaultContentProvider {
20    pub const fn new(repo: ContentRepository, search_repo: SearchRepository) -> Self {
21        Self { repo, search_repo }
22    }
23}
24
25#[async_trait]
26impl ContentProvider for DefaultContentProvider {
27    type Error = ContentError;
28
29    async fn find_content(
30        &self,
31        id: &systemprompt_identifiers::ContentId,
32    ) -> Result<Option<ContentItem>, Self::Error> {
33        let content = self.repo.get_by_id(id).await?;
34
35        Ok(content.map(|c| ContentItem {
36            id: c.id,
37            slug: c.slug,
38            title: c.title,
39            description: c.description,
40            body: c.body,
41            author: c.author,
42            published_at: c.published_at,
43            keywords: c.keywords,
44            kind: c.kind,
45            image: c.image,
46            source_id: c.source_id,
47            category_id: c.category_id.map(|id| id.to_string()),
48        }))
49    }
50
51    async fn find_content_by_slug(&self, slug: &str) -> Result<Option<ContentItem>, Self::Error> {
52        let content = self.repo.get_by_slug(slug, &LocaleCode::new("en")).await?;
53
54        Ok(content.map(|c| ContentItem {
55            id: c.id,
56            slug: c.slug,
57            title: c.title,
58            description: c.description,
59            body: c.body,
60            author: c.author,
61            published_at: c.published_at,
62            keywords: c.keywords,
63            kind: c.kind,
64            image: c.image,
65            source_id: c.source_id,
66            category_id: c.category_id.map(|id| id.to_string()),
67        }))
68    }
69
70    async fn find_content_by_source_and_slug(
71        &self,
72        source_id: &systemprompt_identifiers::SourceId,
73        slug: &str,
74    ) -> Result<Option<ContentItem>, Self::Error> {
75        let content = self
76            .repo
77            .get_by_source_and_slug(source_id, slug, &LocaleCode::new("en"))
78            .await?;
79
80        Ok(content.map(|c| ContentItem {
81            id: c.id,
82            slug: c.slug,
83            title: c.title,
84            description: c.description,
85            body: c.body,
86            author: c.author,
87            published_at: c.published_at,
88            keywords: c.keywords,
89            kind: c.kind,
90            image: c.image,
91            source_id: c.source_id,
92            category_id: c.category_id.map(|id| id.to_string()),
93        }))
94    }
95
96    async fn list_content(
97        &self,
98        filter: ContentFilter,
99    ) -> Result<Vec<ContentSummary>, Self::Error> {
100        let limit = filter.limit.unwrap_or(100);
101        let offset = filter.offset.unwrap_or(0);
102
103        let contents = if let Some(source_id) = &filter.source_id {
104            self.repo
105                .list_by_source(source_id, &LocaleCode::new("en"))
106                .await?
107        } else {
108            self.repo.list(limit, offset).await?
109        };
110
111        Ok(contents
112            .into_iter()
113            .map(|c| ContentSummary {
114                id: c.id,
115                slug: c.slug,
116                title: c.title,
117                description: c.description,
118                published_at: c.published_at,
119                kind: c.kind,
120                source_id: c.source_id,
121            })
122            .collect())
123    }
124
125    async fn search(
126        &self,
127        query: &str,
128        limit: Option<i64>,
129    ) -> Result<Vec<ContentSummary>, Self::Error> {
130        let limit = limit.unwrap_or(50);
131        let results = self.search_repo.search_by_keyword(query, limit).await?;
132
133        Ok(results
134            .into_iter()
135            .map(|r| ContentSummary {
136                id: r.id,
137                slug: r.slug,
138                title: r.title,
139                description: r.description,
140                published_at: chrono::Utc::now(),
141                kind: String::new(),
142                source_id: r.source_id,
143            })
144            .collect())
145    }
146}