Skip to main content

ordinary_modify/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(clippy::all, clippy::pedantic)]
3#![allow(clippy::missing_errors_doc)]
4
5// Copyright (C) 2026 Ordinary Labs, LLC.
6//
7// SPDX-License-Identifier: AGPL-3.0-only
8
9pub mod content;
10pub mod project;
11
12use anyhow::bail;
13use ordinary_config::{
14    ActionConfig, ActionFfi, ActionFfiSerialization, ActionFfiVersion, IntegrationConfig,
15    IntegrationProtocol, ModelConfig, OrdinaryConfig, UuidVersion,
16};
17use std::{io::Write, path::Path};
18use tracing::instrument;
19
20#[instrument(err)]
21#[allow(
22    clippy::too_many_arguments,
23    clippy::used_underscore_binding,
24    clippy::too_many_lines
25)]
26pub fn add_action(
27    path: &str,
28    name: &str,
29    lang: &str,
30    _access: (),
31    _accepts: (),
32    _returns: (),
33    transactional: &Option<bool>,
34    _protected: (),
35) -> anyhow::Result<()> {
36    let proj_path = Path::new(path);
37    let mut app_config = OrdinaryConfig::get(path, false)?;
38
39    let mut actions = app_config.actions.unwrap_or_default();
40
41    if actions.len() <= 225 {
42        let next_idx = actions.len();
43
44        let language = match lang {
45            "Rust" | "rust" | "rs" => ordinary_config::ActionLang::Rust,
46            "JavaScript" | "javascript" | "JS" | "js" => ordinary_config::ActionLang::JavaScript,
47            _ => bail!("language {lang} not yet supported"),
48        };
49
50        let action_dir_path = proj_path.join("actions").join(name);
51        fs_err::create_dir_all(&action_dir_path)?;
52
53        match language {
54            ordinary_config::ActionLang::Rust => {
55                let gitignore = "/target";
56
57                let cargo_toml = format!(
58                    r#"[workspace]
59
60[package]
61name = "action"
62version = "0.1.0"
63edition = "2024"
64
65[dependencies]
66ordinary = {{ path = "../../.ordinary/gen/actions/{name}" }}
67
68[profile.release]
69strip = "symbols"
70lto = "fat"
71opt-level = "z"
72codegen-units = 1
73panic = "abort"
74"#
75                );
76
77                let main_rs = r#"use std::error::Error;
78use ordinary::{recv_in, send_out};
79
80fn main() -> Result<(), Box<dyn Error>> {
81    let _input = recv_in()?;
82
83    ordinary::trace(2, "Hello, Ordinary!")?;
84
85    send_out(())
86}
87"#;
88
89                let mut gitignore_file = fs_err::File::create(action_dir_path.join(".gitignore"))?;
90                gitignore_file.write_all(gitignore.as_bytes())?;
91
92                let mut cargo_toml_file = fs_err::File::create(action_dir_path.join("Cargo.toml"))?;
93                cargo_toml_file.write_all(cargo_toml.as_bytes())?;
94
95                fs_err::create_dir_all(action_dir_path.join("src"))?;
96                let mut main_rs_file =
97                    fs_err::File::create(action_dir_path.join("src").join("main.rs"))?;
98                main_rs_file.write_all(main_rs.as_bytes())?;
99            }
100            ordinary_config::ActionLang::JavaScript => {
101                let gitignore = r"/target
102node_modules";
103
104                let package_json = r#"{
105  "name": "action",
106  "version": "0.1.0",
107  "main": "index.js",
108  "files": [
109    "index.js",
110    "index.d.ts"
111  ],
112  "types": "client.d.ts",
113  "scripts": {
114    "build": "esbuild main.js --bundle --minify --outfile=index.js --target=es2020 --format=iife --global-name=action"
115  },
116  "devDependencies": {
117    "esbuild": "0.25.10"
118  }
119}
120                "#;
121
122                let main_js = r#"export function main(input) {
123    console.log("Hello, Ordinary!");
124}"#;
125
126                let mut gitignore_file = fs_err::File::create(action_dir_path.join(".gitignore"))?;
127                gitignore_file.write_all(gitignore.as_bytes())?;
128
129                let mut package_json_file =
130                    fs_err::File::create(action_dir_path.join("package.json"))?;
131                package_json_file.write_all(package_json.as_bytes())?;
132
133                let mut main_js_file = fs_err::File::create(action_dir_path.join("main.js"))?;
134                main_js_file.write_all(main_js.as_bytes())?;
135            }
136        }
137
138        actions.push(ActionConfig {
139            ffi: ActionFfi {
140                version: ActionFfiVersion::V1,
141                serialization: ActionFfiSerialization::FlexBufferVector,
142            },
143            idx: u8::try_from(next_idx)?,
144            dir_path: Some(format!("./actions/{name}")),
145            name: name.to_string(),
146            lang: language,
147            access: vec![],
148            accepts: ordinary_types::Kind::Void,
149            returns: ordinary_types::Kind::Void,
150            triggered_by: vec![],
151            transactional: *transactional,
152            protected: None,
153            timeout: None,
154            cors: None,
155            privileged: None,
156            variables: None,
157            middlewares: None,
158        });
159
160        app_config.actions = Some(actions);
161
162        let ordinary_json = serde_json::to_string_pretty(&app_config)?;
163
164        let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
165        file.write_all(ordinary_json.as_bytes())?;
166    } else {
167        tracing::error!("cannot support more than 255 actions for a single project.");
168    }
169
170    Ok(())
171}
172
173#[instrument(err)]
174pub fn add_integration(
175    path: &str,
176    name: &str,
177    endpoint: &str,
178    protocol: &str,
179) -> anyhow::Result<()> {
180    let proj_path = Path::new(path);
181    let mut app_config = OrdinaryConfig::get(path, false)?;
182
183    let mut integrations = app_config.integrations.unwrap_or_default();
184
185    if integrations.len() <= 255 {
186        let next_idx = integrations.len();
187
188        integrations.push(IntegrationConfig {
189            idx: u8::try_from(next_idx)?,
190            name: name.to_string(),
191            protocol: match protocol {
192                "JSON" => IntegrationProtocol::Http {
193                    method: "GET".to_string(),
194                    headers: vec![],
195                    send_encoding: ordinary_config::IntegrationProtocolHttpEncoding::Text,
196                    recv_encoding: ordinary_config::IntegrationProtocolHttpEncoding::Text,
197                },
198                _ => bail!("invalid protocol"),
199            },
200            endpoint: endpoint.to_string(),
201            send: ordinary_types::Kind::Json,
202            recv: ordinary_types::Kind::Json,
203            secrets: None,
204            timeout: None,
205        });
206
207        app_config.integrations = Some(integrations);
208
209        let ordinary_json = serde_json::to_string_pretty(&app_config)?;
210
211        let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
212        file.write_all(ordinary_json.as_bytes())?;
213    } else {
214        tracing::error!("cannot support more than 255 integrations for a single project.");
215    }
216
217    Ok(())
218}
219
220#[instrument(err)]
221pub fn add_model(path: &str, name: &str, uuid: Option<&str>) -> anyhow::Result<()> {
222    let proj_path = Path::new(path);
223    let mut app_config = OrdinaryConfig::get(path, false)?;
224
225    let mut models = app_config.models.unwrap_or_default();
226
227    if models.len() <= 255 {
228        let next_idx = models.len();
229
230        models.push(ModelConfig {
231            idx: u8::try_from(next_idx)?,
232            name: name.to_string(),
233            fields: vec![],
234            uuid: match uuid {
235                Some(uuid) => match uuid {
236                    "v4" => Some(UuidVersion::V4),
237                    "v7" => Some(UuidVersion::V7),
238                    _ => bail!("invalid UUID version"),
239                },
240                None => None,
241            },
242        });
243
244        app_config.models = Some(models);
245
246        let ordinary_json = serde_json::to_string_pretty(&app_config)?;
247
248        let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
249        file.write_all(ordinary_json.as_bytes())?;
250    } else {
251        tracing::error!("cannot support more than 255 models for a single project.");
252    }
253
254    Ok(())
255}