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}
119
120impl IngestionReport {
121    pub const fn new() -> Self {
122        Self {
123            files_found: 0,
124            files_processed: 0,
125            errors: Vec::new(),
126            warnings: Vec::new(),
127            would_create: Vec::new(),
128            would_update: Vec::new(),
129            unchanged_count: 0,
130        }
131    }
132
133    pub fn is_success(&self) -> bool {
134        self.errors.is_empty()
135    }
136}
137
138impl Default for IngestionReport {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144#[derive(Debug, Clone, Copy, Default)]
145pub struct IngestionOptions {
146    pub override_existing: bool,
147    pub recursive: bool,
148    pub dry_run: bool,
149}
150
151impl IngestionOptions {
152    pub const fn with_override(mut self, override_existing: bool) -> Self {
153        self.override_existing = override_existing;
154        self
155    }
156
157    pub const fn with_recursive(mut self, recursive: bool) -> Self {
158        self.recursive = recursive;
159        self
160    }
161
162    pub const fn with_dry_run(mut self, dry_run: bool) -> Self {
163        self.dry_run = dry_run;
164        self
165    }
166}
167
168#[derive(Debug, Clone)]
169pub struct IngestionSource<'a> {
170    pub source_id: &'a SourceId,
171    pub category_id: &'a CategoryId,
172}
173
174impl<'a> IngestionSource<'a> {
175    pub const fn new(source_id: &'a SourceId, category_id: &'a CategoryId) -> Self {
176        Self {
177            source_id,
178            category_id,
179        }
180    }
181}