ocre-cli 0.2.0

Command-line tool for Ocre: create, generate, migrate, run and deploy apps.
//! `ocre generate data <name>`: read-only data shipped with the Worker (Loco's
//! data loaders): `data/<name>.json`, compiled into the build with
//! `include_str!` and parsed once per Worker instance by `src/data/<name>.rs`.
//! A Worker has no disk, so the file cannot change without a deploy.

use super::{Edits, MODULES_MARKER, insert_after_marker};
use crate::{CliResult, names::is_identifier, output::CliError, project::Project};

const DATA_MARKER: &str = "// ocre:data";

pub fn data(project: &Project, name: &str) -> CliResult {
    if !is_identifier(name) {
        return Err(CliError::new(format!("invalid data name `{name}`"))
            .hint("use snake_case starting with a letter, e.g. `ocre g data countries`"));
    }
    let mut edits = Edits::new(project);
    let registry = match edits.read("src/data/mod.rs")? {
        Some(source) => source,
        None => {
            let lib = edits.read("src/lib.rs")?.unwrap_or_default();
            let lib = insert_after_marker(&lib, MODULES_MARKER, "mod data;").ok_or_else(|| {
                CliError::new(format!("src/lib.rs is missing the `{MODULES_MARKER}` marker"))
                    .hint(format!("put `{MODULES_MARKER}` on its own line where `mod` declarations go"))
            })?;
            edits.update("src/lib.rs", lib);
            format!(
                "//! Read-only data compiled into the Worker. `ocre g data` adds modules below.\n\n// An app rarely reads every loader it has.\n#![allow(dead_code)]\n\n{DATA_MARKER}\n"
            )
        }
    };
    let registry = insert_after_marker(&registry, DATA_MARKER, &format!("pub mod {name};"))
        .ok_or_else(|| CliError::new(format!("src/data/mod.rs is missing the `{DATA_MARKER}` marker")))?;
    edits.update("src/data/mod.rs", registry);
    edits.create(&format!("data/{name}.json"), "[\n  { \"name\": \"Example\" }\n]\n".to_owned())?;
    edits.create(&format!("src/data/{name}.rs"), loader_rs(name))?;
    let mut report = edits.apply("generate data")?;
    report.next = vec![
        format!("put the entries in data/{name}.json and their fields in `Entry` (src/data/{name}.rs)"),
        format!("read them with `crate::data::{name}::all()`"),
    ];
    Ok(report)
}

fn loader_rs(name: &str) -> String {
    format!(
        r#"//! `data/{name}.json`, compiled into the Worker and parsed on first use (once
//! per Worker instance). Generated by `ocre g data {name}`.

use std::sync::LazyLock;

use serde::Deserialize;

/// One entry of data/{name}.json: declare the fields it holds.
#[derive(Debug, Clone, Deserialize)]
pub struct Entry {{
    pub name: String,
}}

static ENTRIES: LazyLock<Vec<Entry>> = LazyLock::new(|| {{
    ocre::serde_json::from_str(include_str!("../../data/{name}.json"))
        .unwrap_or_else(|err| panic!("data/{name}.json does not match `Entry`: {{err}}"))
}});

/// Every entry of data/{name}.json, in file order.
pub fn all() -> &'static [Entry] {{
    &ENTRIES
}}

#[cfg(test)]
mod tests {{
    #[test]
    fn the_file_matches_entry() {{
        assert!(!super::all().is_empty());
    }}
}}
"#
    )
}