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)?;
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)?;
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_lifecycle_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_lifecycle_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)?;
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_lifecycle_script(
136                proj_path,
137                &None,
138                &content_def.name,
139                "before",
140                before,
141            )?;
142        }
143
144        // todo: do more to verify the structure of the object is compatible with its instance_of
145
146        content_objects.push(new_obj.clone());
147
148        let new_objects = serde_json::to_string_pretty(&content_objects)?;
149
150        let mut file = fs_err::File::create(proj_path.join(&content.file_path))?;
151        file.write_all(new_objects.as_bytes())?;
152
153        if let Some(lifecycle_config) = &content_def.lifecycle
154            && let Some(on_add) = &lifecycle_config.on_add
155            && let Some(after) = &on_add.after
156        {
157            OrdinaryConfig::exec_lifecycle_script(
158                proj_path,
159                &Some(serde_json::to_string(&new_obj)?),
160                &content_def.name,
161                "after",
162                after,
163            )?;
164        }
165    }
166
167    Ok(())
168}
169
170#[instrument(skip_all, err)]
171pub fn edit_obj(path: &str, object_json: &str) -> anyhow::Result<()> {
172    let proj_path = Path::new(path);
173    let app_config = OrdinaryConfig::get(path)?;
174    let new_obj: ContentObject = serde_json::from_str(object_json)?;
175
176    if let Some(content) = &app_config.content
177        && let Some(content_def) = content
178            .definitions
179            .iter()
180            .find(|d| d.name == new_obj.instance_of)
181    {
182        let content_json = fs_err::read_to_string(proj_path.join(&content.file_path))?;
183        let mut content_objects: Vec<ContentObject> = serde_json::from_str(&content_json)?;
184
185        before_all(proj_path, content_def, &app_config)?;
186
187        if let Some(obj) = content_objects
188            .iter_mut()
189            .find(|obj| obj.uuid == new_obj.uuid && obj.instance_of == new_obj.instance_of)
190        {
191            if let Some(lifecycle_config) = &content_def.lifecycle
192                && let Some(on_edit) = &lifecycle_config.on_edit
193                && let Some(before) = &on_edit.before
194            {
195                OrdinaryConfig::exec_lifecycle_script(
196                    proj_path,
197                    &None,
198                    &content_def.name,
199                    "before",
200                    before,
201                )?;
202            }
203
204            obj.fields.clone_from(&new_obj.fields);
205        }
206
207        let new_objects = serde_json::to_string_pretty(&content_objects)?;
208
209        let mut file = fs_err::File::create(proj_path.join(&content.file_path))?;
210        file.write_all(new_objects.as_bytes())?;
211
212        if let Some(lifecycle_config) = &content_def.lifecycle
213            && let Some(on_edit) = &lifecycle_config.on_edit
214            && let Some(after) = &on_edit.after
215        {
216            OrdinaryConfig::exec_lifecycle_script(
217                proj_path,
218                &Some(serde_json::to_string(&new_obj)?),
219                &content_def.name,
220                "after",
221                after,
222            )?;
223        }
224    }
225    Ok(())
226}
227
228#[instrument(skip_all, err)]
229pub fn delete_obj(path: &str, instance_of: &str, uuid: &str) -> anyhow::Result<()> {
230    let proj_path = Path::new(path);
231    let app_config = OrdinaryConfig::get(path)?;
232
233    if let Some(content) = &app_config.content
234        && let Some(content_def) = content.definitions.iter().find(|d| d.name == instance_of)
235    {
236        let content_json = fs_err::read_to_string(proj_path.join(&content.file_path))?;
237        let mut content_objects: Vec<ContentObject> = serde_json::from_str(&content_json)?;
238
239        before_all(proj_path, content_def, &app_config)?;
240
241        let mut removed = None;
242
243        if let Some(pos) = content_objects
244            .iter()
245            .position(|obj| obj.uuid == uuid && obj.instance_of == instance_of)
246        {
247            if let Some(lifecycle_config) = &content_def.lifecycle
248                && let Some(on_delete) = &lifecycle_config.on_delete
249                && let Some(before) = &on_delete.before
250            {
251                OrdinaryConfig::exec_lifecycle_script(
252                    proj_path,
253                    &None,
254                    &content_def.name,
255                    "before",
256                    before,
257                )?;
258            }
259
260            removed = Some(content_objects.remove(pos));
261        }
262
263        let new_objects = serde_json::to_string_pretty(&content_objects)?;
264
265        let mut file = fs_err::File::create(proj_path.join(&content.file_path))?;
266        file.write_all(new_objects.as_bytes())?;
267
268        if let Some(removed) = removed
269            && let Some(lifecycle_config) = &content_def.lifecycle
270            && let Some(on_delete) = &lifecycle_config.on_delete
271            && let Some(after) = &on_delete.after
272        {
273            OrdinaryConfig::exec_lifecycle_script(
274                proj_path,
275                &Some(serde_json::to_string(&removed)?),
276                &content_def.name,
277                "after",
278                after,
279            )?;
280        }
281    }
282
283    Ok(())
284}