Skip to main content

ordinary_modify/
content.rs

1// Copyright (C) 2026 Ordinary Labs, LLC.
2//
3// SPDX-License-Identifier: AGPL-3.0-only
4
5use ordinary_config::{Content, ContentDefinition, OrdinaryConfig};
6use ordinary_types::{ContentObject, Field};
7use std::io::Write;
8use std::path::Path;
9use tracing::instrument;
10
11#[instrument(err)]
12pub fn add_def(path: &str, name: &String, json_fields: &str) -> anyhow::Result<()> {
13    let proj_path = Path::new(path);
14    let mut app_config = OrdinaryConfig::get(path, false)?;
15
16    let mut file_path = "./content.json".to_string();
17
18    let mut content_defs = if let Some(d) = app_config.content {
19        file_path = d.file_path;
20        d.definitions
21    } else {
22        let mut file = fs_err::File::create(proj_path.join("content.json"))?;
23        file.write_all(b"[]")?;
24
25        vec![]
26    };
27
28    if content_defs.len() < 255
29        && let Ok(fields) = serde_json::from_str::<Vec<Field>>(json_fields)
30    {
31        let next_idx = u8::try_from(content_defs.len())?;
32
33        content_defs.push(ContentDefinition {
34            idx: next_idx,
35            name: name.clone(),
36            fields,
37            lifecycle: None,
38        });
39
40        app_config.content = Some(Content {
41            definitions: content_defs,
42            file_path,
43            update: None,
44        });
45
46        let ordinary_json = serde_json::to_string_pretty(&app_config)?;
47
48        let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
49        file.write_all(ordinary_json.as_bytes())?;
50    } else {
51        tracing::error!("cannot support more than 255 content definitions for a single project.");
52    }
53
54    Ok(())
55}
56
57#[instrument(err)]
58pub fn edit_def(
59    path: &str,
60    idx: u8,
61    name: Option<String>,
62    json_fields: &str,
63) -> anyhow::Result<()> {
64    let proj_path = Path::new(path);
65    let mut app_config = OrdinaryConfig::get(path, false)?;
66
67    if let Some(content) = app_config.content.as_mut()
68        && let Some(def) = content.definitions.iter_mut().find(|v| v.idx == idx)
69        && let Ok(fields) = serde_json::from_str::<Vec<Field>>(json_fields)
70    {
71        def.fields = fields;
72
73        if let Some(name) = name {
74            def.name = name;
75        }
76    }
77
78    let ordinary_json = serde_json::to_string_pretty(&app_config)?;
79
80    let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
81    file.write_all(ordinary_json.as_bytes())?;
82
83    Ok(())
84}
85
86#[instrument(skip_all, err)]
87fn before_all(
88    proj_path: &Path,
89    content_def: &ContentDefinition,
90    config: &OrdinaryConfig,
91) -> anyhow::Result<()> {
92    if let Some(lifecycle_config) = &config.lifecycle
93        && let Some(before_all) = &lifecycle_config.before_all
94    {
95        OrdinaryConfig::exec_script(proj_path, &None, "all", "before", before_all)?;
96    }
97
98    if let Some(lifecycle_config) = &content_def.lifecycle
99        && let Some(before_all) = &lifecycle_config.before_all
100    {
101        OrdinaryConfig::exec_script(
102            proj_path,
103            &None,
104            &content_def.name,
105            "before_all",
106            before_all,
107        )?;
108    }
109
110    Ok(())
111}
112
113#[instrument(skip_all, err)]
114pub fn add_obj(path: &str, object_json: &str) -> anyhow::Result<()> {
115    let proj_path = Path::new(path);
116    let app_config = OrdinaryConfig::get(path, false)?;
117
118    let new_obj: ContentObject = serde_json::from_str(object_json)?;
119
120    if let Some(content) = &app_config.content
121        && let Some(content_def) = content
122            .definitions
123            .iter()
124            .find(|d| d.name == new_obj.instance_of)
125    {
126        let content_json = fs_err::read_to_string(proj_path.join(&content.file_path))?;
127        let mut content_objects: Vec<ContentObject> = serde_json::from_str(&content_json)?;
128
129        before_all(proj_path, content_def, &app_config)?;
130
131        if let Some(lifecycle_config) = &content_def.lifecycle
132            && let Some(on_add) = &lifecycle_config.on_add
133            && let Some(before) = &on_add.before
134        {
135            OrdinaryConfig::exec_script(proj_path, &None, &content_def.name, "before", before)?;
136        }
137
138        // todo: do more to verify the structure of the object is compatible with its instance_of
139
140        content_objects.push(new_obj.clone());
141
142        let new_objects = serde_json::to_string_pretty(&content_objects)?;
143
144        let mut file = fs_err::File::create(proj_path.join(&content.file_path))?;
145        file.write_all(new_objects.as_bytes())?;
146
147        if let Some(lifecycle_config) = &content_def.lifecycle
148            && let Some(on_add) = &lifecycle_config.on_add
149            && let Some(after) = &on_add.after
150        {
151            OrdinaryConfig::exec_script(
152                proj_path,
153                &Some(serde_json::to_string(&new_obj)?),
154                &content_def.name,
155                "after",
156                after,
157            )?;
158        }
159    }
160
161    Ok(())
162}
163
164#[instrument(skip_all, err)]
165pub fn edit_obj(path: &str, object_json: &str) -> anyhow::Result<()> {
166    let proj_path = Path::new(path);
167    let app_config = OrdinaryConfig::get(path, false)?;
168    let new_obj: ContentObject = serde_json::from_str(object_json)?;
169
170    if let Some(content) = &app_config.content
171        && let Some(content_def) = content
172            .definitions
173            .iter()
174            .find(|d| d.name == new_obj.instance_of)
175    {
176        let content_json = fs_err::read_to_string(proj_path.join(&content.file_path))?;
177        let mut content_objects: Vec<ContentObject> = serde_json::from_str(&content_json)?;
178
179        before_all(proj_path, content_def, &app_config)?;
180
181        if let Some(obj) = content_objects
182            .iter_mut()
183            .find(|obj| obj.uuid == new_obj.uuid && obj.instance_of == new_obj.instance_of)
184        {
185            if let Some(lifecycle_config) = &content_def.lifecycle
186                && let Some(on_edit) = &lifecycle_config.on_edit
187                && let Some(before) = &on_edit.before
188            {
189                OrdinaryConfig::exec_script(proj_path, &None, &content_def.name, "before", before)?;
190            }
191
192            obj.fields.clone_from(&new_obj.fields);
193        }
194
195        let new_objects = serde_json::to_string_pretty(&content_objects)?;
196
197        let mut file = fs_err::File::create(proj_path.join(&content.file_path))?;
198        file.write_all(new_objects.as_bytes())?;
199
200        if let Some(lifecycle_config) = &content_def.lifecycle
201            && let Some(on_edit) = &lifecycle_config.on_edit
202            && let Some(after) = &on_edit.after
203        {
204            OrdinaryConfig::exec_script(
205                proj_path,
206                &Some(serde_json::to_string(&new_obj)?),
207                &content_def.name,
208                "after",
209                after,
210            )?;
211        }
212    }
213    Ok(())
214}
215
216#[instrument(skip_all, err)]
217pub fn delete_obj(path: &str, instance_of: &str, uuid: &str) -> anyhow::Result<()> {
218    let proj_path = Path::new(path);
219    let app_config = OrdinaryConfig::get(path, false)?;
220
221    if let Some(content) = &app_config.content
222        && let Some(content_def) = content.definitions.iter().find(|d| d.name == instance_of)
223    {
224        let content_json = fs_err::read_to_string(proj_path.join(&content.file_path))?;
225        let mut content_objects: Vec<ContentObject> = serde_json::from_str(&content_json)?;
226
227        before_all(proj_path, content_def, &app_config)?;
228
229        let mut removed = None;
230
231        if let Some(pos) = content_objects
232            .iter()
233            .position(|obj| obj.uuid == uuid && obj.instance_of == instance_of)
234        {
235            if let Some(lifecycle_config) = &content_def.lifecycle
236                && let Some(on_delete) = &lifecycle_config.on_delete
237                && let Some(before) = &on_delete.before
238            {
239                OrdinaryConfig::exec_script(proj_path, &None, &content_def.name, "before", before)?;
240            }
241
242            removed = Some(content_objects.remove(pos));
243        }
244
245        let new_objects = serde_json::to_string_pretty(&content_objects)?;
246
247        let mut file = fs_err::File::create(proj_path.join(&content.file_path))?;
248        file.write_all(new_objects.as_bytes())?;
249
250        if let Some(removed) = removed
251            && let Some(lifecycle_config) = &content_def.lifecycle
252            && let Some(on_delete) = &lifecycle_config.on_delete
253            && let Some(after) = &on_delete.after
254        {
255            OrdinaryConfig::exec_script(
256                proj_path,
257                &Some(serde_json::to_string(&removed)?),
258                &content_def.name,
259                "after",
260                after,
261            )?;
262        }
263    }
264
265    Ok(())
266}