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