Skip to main content

systemprompt_cli/commands/core/content/
edit_apply.rs

1use anyhow::{Context, Result, anyhow};
2use std::fs;
3use std::path::Path;
4use systemprompt_content::{CategoryIdUpdate, ContentRepository};
5use systemprompt_identifiers::CategoryId;
6
7use super::edit::EditArgs;
8
9const VALID_KINDS: &[&str] = &["article", "paper", "guide", "tutorial"];
10
11#[derive(Debug)]
12pub struct ContentEditState {
13    pub title: String,
14    pub description: String,
15    pub body: String,
16    pub keywords: String,
17    pub image: Option<String>,
18    pub category_id: CategoryIdUpdate,
19    pub public_value: Option<bool>,
20    pub kind_value: Option<String>,
21}
22
23pub fn apply_visibility_flags(
24    args: &EditArgs,
25    state: &mut ContentEditState,
26    changes: &mut Vec<String>,
27) {
28    if args.public {
29        state.public_value = Some(true);
30        changes.push("public: true".to_owned());
31    }
32    if args.private {
33        state.public_value = Some(false);
34        changes.push("public: false".to_owned());
35    }
36}
37
38pub fn apply_body_flags(
39    args: &EditArgs,
40    state: &mut ContentEditState,
41    changes: &mut Vec<String>,
42) -> Result<()> {
43    if let Some(b) = &args.body {
44        state.body.clone_from(b);
45        changes.push("body: updated".to_owned());
46    }
47    if let Some(file) = &args.body_file {
48        let path = Path::new(file);
49        state.body = fs::read_to_string(path)
50            .with_context(|| format!("Failed to read body file: {}", path.display()))?;
51        changes.push("body: updated from file".to_owned());
52    }
53    Ok(())
54}
55
56pub async fn apply_set_value_flags(
57    args: &EditArgs,
58    state: &mut ContentEditState,
59    changes: &mut Vec<String>,
60    repo: &ContentRepository,
61) -> Result<()> {
62    for set_value in &args.set_values {
63        let parts: Vec<&str> = set_value.splitn(2, '=').collect();
64        if parts.len() != 2 {
65            return Err(anyhow!(
66                "Invalid --set format: '{}'. Expected key=value",
67                set_value
68            ));
69        }
70        let key = parts[0].trim();
71        let value = parts[1].trim();
72        apply_set_field(key, value, state, changes, repo).await?;
73    }
74    Ok(())
75}
76
77async fn apply_set_field(
78    key: &str,
79    value: &str,
80    state: &mut ContentEditState,
81    changes: &mut Vec<String>,
82    repo: &ContentRepository,
83) -> Result<()> {
84    match key {
85        "title" => {
86            state.title = value.to_owned();
87            changes.push(format!("title: {}", value));
88        },
89        "description" => {
90            state.description = value.to_owned();
91            changes.push(format!("description: {}", value));
92        },
93        "keywords" => {
94            state.keywords = value.to_owned();
95            changes.push(format!("keywords: {}", value));
96        },
97        "image" => apply_image_field(value, state, changes),
98        "category_id" | "category" => apply_category_field(value, state, changes, repo).await?,
99        "kind" => apply_kind_field(value, state, changes)?,
100        "public" => apply_public_field(value, state, changes)?,
101        _ => {
102            return Err(anyhow!(
103                "Unknown field: '{}'. Supported fields: title, description, keywords, image, \
104                 category_id, kind, public",
105                key
106            ));
107        },
108    }
109    Ok(())
110}
111
112fn apply_image_field(value: &str, state: &mut ContentEditState, changes: &mut Vec<String>) {
113    if value.eq_ignore_ascii_case("none") || value.is_empty() {
114        state.image = None;
115        changes.push("image: cleared".to_owned());
116    } else {
117        state.image = Some(value.to_owned());
118        changes.push(format!("image: {}", value));
119    }
120}
121
122async fn apply_category_field(
123    value: &str,
124    state: &mut ContentEditState,
125    changes: &mut Vec<String>,
126    repo: &ContentRepository,
127) -> Result<()> {
128    if value.eq_ignore_ascii_case("none") || value.is_empty() {
129        state.category_id = CategoryIdUpdate::Clear;
130        changes.push("category_id: cleared".to_owned());
131        return Ok(());
132    }
133    let cat_id = CategoryId::new(value.to_owned());
134    if !repo.category_exists(&cat_id).await? {
135        return Err(anyhow!(
136            "Category '{}' not found. Please use an existing category ID.",
137            value
138        ));
139    }
140    state.category_id = CategoryIdUpdate::Set(cat_id);
141    changes.push(format!("category_id: {}", value));
142    Ok(())
143}
144
145fn apply_kind_field(
146    value: &str,
147    state: &mut ContentEditState,
148    changes: &mut Vec<String>,
149) -> Result<()> {
150    if !VALID_KINDS.contains(&value) {
151        return Err(anyhow!(
152            "Invalid kind '{}'. Must be one of: {}",
153            value,
154            VALID_KINDS.join(", ")
155        ));
156    }
157    state.kind_value = Some(value.to_owned());
158    changes.push(format!("kind: {}", value));
159    Ok(())
160}
161
162fn apply_public_field(
163    value: &str,
164    state: &mut ContentEditState,
165    changes: &mut Vec<String>,
166) -> Result<()> {
167    let p = value.parse::<bool>().map_err(|_e| {
168        anyhow!(
169            "Invalid boolean value for public: '{}'. Use true or false",
170            value
171        )
172    })?;
173    state.public_value = Some(p);
174    changes.push(format!("public: {}", p));
175    Ok(())
176}