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}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ContentLinkMetadata {
92    pub title: String,
93    pub url: String,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
97pub struct Tag {
98    pub id: TagId,
99    pub name: String,
100    pub slug: String,
101    pub created_at: Option<DateTime<Utc>>,
102    pub updated_at: Option<DateTime<Utc>>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct IngestionReport {
107    pub files_found: usize,
108    pub files_processed: usize,
109    pub errors: Vec<String>,
110    #[serde(default)]
111    pub warnings: Vec<String>,
112    #[serde(default, skip_serializing_if = "Vec::is_empty")]
113    pub would_create: Vec<String>,
114    #[serde(default, skip_serializing_if = "Vec::is_empty")]
115    pub would_update: Vec<String>,
116    #[serde(default)]
117    pub unchanged_count: usize,
118    #[serde(default)]
119    pub skipped_count: usize,
120}
121
122impl IngestionReport {
123    pub const fn new() -> Self {
124        Self {
125            files_found: 0,
126            files_processed: 0,
127            errors: Vec::new(),
128            warnings: Vec::new(),
129            would_create: Vec::new(),
130            would_update: Vec::new(),
131            unchanged_count: 0,
132            skipped_count: 0,
133        }
134    }
135
136    pub fn is_success(&self) -> bool {
137        self.errors.is_empty()
138    }
139}
140
141impl Default for IngestionReport {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147#[derive(Debug, Clone, Copy, Default)]
148pub struct IngestionOptions {
149    pub override_existing: bool,
150    pub recursive: bool,
151    pub dry_run: bool,
152}
153
154impl IngestionOptions {
155    pub const fn with_override(mut self, override_existing: bool) -> Self {
156        self.override_existing = override_existing;
157        self
158    }
159
160    pub const fn with_recursive(mut self, recursive: bool) -> Self {
161        self.recursive = recursive;
162        self
163    }
164
165    pub const fn with_dry_run(mut self, dry_run: bool) -> Self {
166        self.dry_run = dry_run;
167        self
168    }
169}
170
171#[derive(Debug, Clone)]
172pub struct IngestionSource<'a> {
173    pub source_id: &'a SourceId,
174    pub source_name: &'a str,
175    pub category_id: &'a CategoryId,
176}
177
178impl<'a> IngestionSource<'a> {
179    pub const fn new(
180        source_id: &'a SourceId,
181        source_name: &'a str,
182        category_id: &'a CategoryId,
183    ) -> Self {
184        Self {
185            source_id,
186            source_name,
187            category_id,
188        }
189    }
190}