Skip to main content

systemprompt_content/models/
content.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value as JsonValue;
4use sqlx::FromRow;
5use systemprompt_identifiers::{CategoryId, ContentId, SourceId, TagId};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
8#[serde(rename_all = "lowercase")]
9pub enum ContentKind {
10    #[default]
11    Article,
12    Guide,
13    Tutorial,
14}
15
16impl ContentKind {
17    pub const fn as_str(&self) -> &'static str {
18        match self {
19            Self::Article => "article",
20            Self::Guide => "guide",
21            Self::Tutorial => "tutorial",
22        }
23    }
24}
25
26impl std::fmt::Display for ContentKind {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(f, "{}", self.as_str())
29    }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
33pub struct Content {
34    pub id: ContentId,
35    pub slug: String,
36    pub title: String,
37    pub description: String,
38    pub body: String,
39    pub author: String,
40    pub published_at: DateTime<Utc>,
41    pub keywords: String,
42    pub kind: String,
43    pub image: Option<String>,
44    pub category_id: Option<CategoryId>,
45    pub source_id: SourceId,
46    pub version_hash: String,
47    pub public: bool,
48    #[serde(default)]
49    pub links: JsonValue,
50    pub updated_at: DateTime<Utc>,
51}
52
53impl Content {
54    pub fn links_metadata(&self) -> Result<Vec<ContentLinkMetadata>, serde_json::Error> {
55        serde_json::from_value(self.links.clone())
56    }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ContentSummary {
61    pub id: ContentId,
62    pub slug: String,
63    pub title: String,
64    pub description: String,
65    pub published_at: DateTime<Utc>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ContentMetadata {
70    pub title: String,
71    #[serde(default)]
72    pub description: String,
73    #[serde(default)]
74    pub author: String,
75    pub published_at: String,
76    pub slug: String,
77    #[serde(default)]
78    pub keywords: String,
79    pub kind: String,
80    #[serde(default)]
81    pub image: Option<String>,
82    #[serde(default)]
83    pub category: Option<String>,
84    #[serde(default)]
85    pub tags: Vec<String>,
86    #[serde(default)]
87    pub links: Vec<ContentLinkMetadata>,
88    #[serde(default)]
89    pub public: Option<bool>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ContentLinkMetadata {
94    pub title: String,
95    pub url: String,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
99pub struct Tag {
100    pub id: TagId,
101    pub name: String,
102    pub slug: String,
103    pub created_at: Option<DateTime<Utc>>,
104    pub updated_at: Option<DateTime<Utc>>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct IngestionReport {
109    pub files_found: usize,
110    pub files_processed: usize,
111    pub errors: Vec<String>,
112    #[serde(default)]
113    pub warnings: Vec<String>,
114    #[serde(default, skip_serializing_if = "Vec::is_empty")]
115    pub would_create: Vec<String>,
116    #[serde(default, skip_serializing_if = "Vec::is_empty")]
117    pub would_update: Vec<String>,
118    #[serde(default)]
119    pub unchanged_count: usize,
120    #[serde(default)]
121    pub skipped_count: usize,
122}
123
124impl IngestionReport {
125    pub const fn new() -> Self {
126        Self {
127            files_found: 0,
128            files_processed: 0,
129            errors: Vec::new(),
130            warnings: Vec::new(),
131            would_create: Vec::new(),
132            would_update: Vec::new(),
133            unchanged_count: 0,
134            skipped_count: 0,
135        }
136    }
137
138    pub fn is_success(&self) -> bool {
139        self.errors.is_empty()
140    }
141}
142
143impl Default for IngestionReport {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149#[derive(Debug, Clone, Copy, Default)]
150pub struct IngestionOptions {
151    pub override_existing: bool,
152    pub recursive: bool,
153    pub dry_run: bool,
154}
155
156impl IngestionOptions {
157    pub const fn with_override(mut self, override_existing: bool) -> Self {
158        self.override_existing = override_existing;
159        self
160    }
161
162    pub const fn with_recursive(mut self, recursive: bool) -> Self {
163        self.recursive = recursive;
164        self
165    }
166
167    pub const fn with_dry_run(mut self, dry_run: bool) -> Self {
168        self.dry_run = dry_run;
169        self
170    }
171}
172
173#[derive(Debug, Clone)]
174pub struct IngestionSource<'a> {
175    pub source_id: &'a SourceId,
176    pub source_name: &'a str,
177    pub category_id: &'a CategoryId,
178}
179
180impl<'a> IngestionSource<'a> {
181    pub const fn new(
182        source_id: &'a SourceId,
183        source_name: &'a str,
184        category_id: &'a CategoryId,
185    ) -> Self {
186        Self {
187            source_id,
188            source_name,
189            category_id,
190        }
191    }
192}