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