systemprompt_content/models/
content.rs1use 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}