ordinary-modify 0.6.0-pre.14

Project manipulation tool for Ordinary
Documentation
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use ordinary_config::{Content, ContentDefinition, OrdinaryConfig};
use ordinary_types::{ContentObject, Field};
use std::io::Write;
use std::path::Path;
use tracing::instrument;

#[instrument(err)]
pub fn add_def(path: &str, name: &String, json_fields: &str) -> anyhow::Result<()> {
    let proj_path = Path::new(path);
    let mut app_config = OrdinaryConfig::get(path)?;

    let mut file_path = "./content.json".to_string();

    let mut content_defs = if let Some(d) = app_config.content {
        file_path = d.file_path;
        d.definitions
    } else {
        let mut file = fs_err::File::create(proj_path.join("content.json"))?;
        file.write_all(b"[]")?;

        vec![]
    };

    if content_defs.len() < 255
        && let Ok(fields) = serde_json::from_str::<Vec<Field>>(json_fields)
    {
        let next_idx = u8::try_from(content_defs.len())?;

        content_defs.push(ContentDefinition {
            idx: next_idx,
            name: name.clone(),
            fields,
            lifecycle: None,
        });

        app_config.content = Some(Content {
            definitions: content_defs,
            file_path,
            update: None,
        });

        let ordinary_json = serde_json::to_string_pretty(&app_config)?;

        let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
        file.write_all(ordinary_json.as_bytes())?;
    } else {
        tracing::error!("cannot support more than 255 content definitions for a single project.");
    }

    Ok(())
}

#[instrument(err)]
pub fn edit_def(
    path: &str,
    idx: u8,
    name: Option<String>,
    json_fields: &str,
) -> anyhow::Result<()> {
    let proj_path = Path::new(path);
    let mut app_config = OrdinaryConfig::get(path)?;

    if let Some(content) = app_config.content.as_mut()
        && let Some(def) = content.definitions.iter_mut().find(|v| v.idx == idx)
        && let Ok(fields) = serde_json::from_str::<Vec<Field>>(json_fields)
    {
        def.fields = fields;

        if let Some(name) = name {
            def.name = name;
        }
    }

    let ordinary_json = serde_json::to_string_pretty(&app_config)?;

    let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
    file.write_all(ordinary_json.as_bytes())?;

    Ok(())
}

#[instrument(skip_all, err)]
fn before_all(
    proj_path: &Path,
    content_def: &ContentDefinition,
    config: &OrdinaryConfig,
) -> anyhow::Result<()> {
    if let Some(lifecycle_config) = &config.lifecycle
        && let Some(before_all) = &lifecycle_config.before_all
    {
        OrdinaryConfig::exec_lifecycle_script(proj_path, &None, "all", "before", before_all)?;
    }

    if let Some(lifecycle_config) = &content_def.lifecycle
        && let Some(before_all) = &lifecycle_config.before_all
    {
        OrdinaryConfig::exec_lifecycle_script(
            proj_path,
            &None,
            &content_def.name,
            "before_all",
            before_all,
        )?;
    }

    Ok(())
}

#[instrument(skip_all, err)]
pub fn add_obj(path: &str, object_json: &str) -> anyhow::Result<()> {
    let proj_path = Path::new(path);
    let app_config = OrdinaryConfig::get(path)?;

    let new_obj: ContentObject = serde_json::from_str(object_json)?;

    if let Some(content) = &app_config.content
        && let Some(content_def) = content
            .definitions
            .iter()
            .find(|d| d.name == new_obj.instance_of)
    {
        let content_json = fs_err::read_to_string(proj_path.join(&content.file_path))?;
        let mut content_objects: Vec<ContentObject> = serde_json::from_str(&content_json)?;

        before_all(proj_path, content_def, &app_config)?;

        if let Some(lifecycle_config) = &content_def.lifecycle
            && let Some(on_add) = &lifecycle_config.on_add
            && let Some(before) = &on_add.before
        {
            OrdinaryConfig::exec_lifecycle_script(
                proj_path,
                &None,
                &content_def.name,
                "before",
                before,
            )?;
        }

        // todo: do more to verify the structure of the object is compatible with its instance_of

        content_objects.push(new_obj.clone());

        let new_objects = serde_json::to_string_pretty(&content_objects)?;

        let mut file = fs_err::File::create(proj_path.join(&content.file_path))?;
        file.write_all(new_objects.as_bytes())?;

        if let Some(lifecycle_config) = &content_def.lifecycle
            && let Some(on_add) = &lifecycle_config.on_add
            && let Some(after) = &on_add.after
        {
            OrdinaryConfig::exec_lifecycle_script(
                proj_path,
                &Some(serde_json::to_string(&new_obj)?),
                &content_def.name,
                "after",
                after,
            )?;
        }
    }

    Ok(())
}

#[instrument(skip_all, err)]
pub fn edit_obj(path: &str, object_json: &str) -> anyhow::Result<()> {
    let proj_path = Path::new(path);
    let app_config = OrdinaryConfig::get(path)?;
    let new_obj: ContentObject = serde_json::from_str(object_json)?;

    if let Some(content) = &app_config.content
        && let Some(content_def) = content
            .definitions
            .iter()
            .find(|d| d.name == new_obj.instance_of)
    {
        let content_json = fs_err::read_to_string(proj_path.join(&content.file_path))?;
        let mut content_objects: Vec<ContentObject> = serde_json::from_str(&content_json)?;

        before_all(proj_path, content_def, &app_config)?;

        if let Some(obj) = content_objects
            .iter_mut()
            .find(|obj| obj.uuid == new_obj.uuid && obj.instance_of == new_obj.instance_of)
        {
            if let Some(lifecycle_config) = &content_def.lifecycle
                && let Some(on_edit) = &lifecycle_config.on_edit
                && let Some(before) = &on_edit.before
            {
                OrdinaryConfig::exec_lifecycle_script(
                    proj_path,
                    &None,
                    &content_def.name,
                    "before",
                    before,
                )?;
            }

            obj.fields.clone_from(&new_obj.fields);
        }

        let new_objects = serde_json::to_string_pretty(&content_objects)?;

        let mut file = fs_err::File::create(proj_path.join(&content.file_path))?;
        file.write_all(new_objects.as_bytes())?;

        if let Some(lifecycle_config) = &content_def.lifecycle
            && let Some(on_edit) = &lifecycle_config.on_edit
            && let Some(after) = &on_edit.after
        {
            OrdinaryConfig::exec_lifecycle_script(
                proj_path,
                &Some(serde_json::to_string(&new_obj)?),
                &content_def.name,
                "after",
                after,
            )?;
        }
    }
    Ok(())
}

#[instrument(skip_all, err)]
pub fn delete_obj(path: &str, instance_of: &str, uuid: &str) -> anyhow::Result<()> {
    let proj_path = Path::new(path);
    let app_config = OrdinaryConfig::get(path)?;

    if let Some(content) = &app_config.content
        && let Some(content_def) = content.definitions.iter().find(|d| d.name == instance_of)
    {
        let content_json = fs_err::read_to_string(proj_path.join(&content.file_path))?;
        let mut content_objects: Vec<ContentObject> = serde_json::from_str(&content_json)?;

        before_all(proj_path, content_def, &app_config)?;

        let mut removed = None;

        if let Some(pos) = content_objects
            .iter()
            .position(|obj| obj.uuid == uuid && obj.instance_of == instance_of)
        {
            if let Some(lifecycle_config) = &content_def.lifecycle
                && let Some(on_delete) = &lifecycle_config.on_delete
                && let Some(before) = &on_delete.before
            {
                OrdinaryConfig::exec_lifecycle_script(
                    proj_path,
                    &None,
                    &content_def.name,
                    "before",
                    before,
                )?;
            }

            removed = Some(content_objects.remove(pos));
        }

        let new_objects = serde_json::to_string_pretty(&content_objects)?;

        let mut file = fs_err::File::create(proj_path.join(&content.file_path))?;
        file.write_all(new_objects.as_bytes())?;

        if let Some(removed) = removed
            && let Some(lifecycle_config) = &content_def.lifecycle
            && let Some(on_delete) = &lifecycle_config.on_delete
            && let Some(after) = &on_delete.after
        {
            OrdinaryConfig::exec_lifecycle_script(
                proj_path,
                &Some(serde_json::to_string(&removed)?),
                &content_def.name,
                "after",
                after,
            )?;
        }
    }

    Ok(())
}