Skip to main content

systemprompt_cli/commands/core/content/
edit_apply.rs

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