1#![doc = include_str!("../README.md")]
2#![warn(clippy::all, clippy::pedantic)]
3#![allow(clippy::missing_errors_doc)]
4
5use anyhow::bail;
10use heck::ToTitleCase;
11use ordinary_config::{
12 ActionConfig, ActionFfi, ActionFfiSerialization, ActionFfiVersion, Content, ContentDefinition,
13 IntegrationConfig, IntegrationProtocol, ModelConfig, OrdinaryConfig, TemplateCache,
14 TemplateConfig, TemplateFfi, TemplateFfiSerialization, TemplateFfiVersion, UuidVersion,
15};
16use ordinary_types::ContentObject;
17use std::{io::Write, path::Path};
18use tracing::instrument;
19
20#[instrument(err)]
21pub fn create_project(path: &String, domain: &String) -> anyhow::Result<()> {
22 let path = Path::new(path).join(domain);
23
24 let gitignore = r".ordinary
25.env
26/target
27";
28
29 tracing::info!("creating project at {:?}", path);
30 fs_err::create_dir_all(&path)?;
31
32 let app_config = OrdinaryConfig {
33 domain: domain.clone(),
34 cnames: None,
35
36 version: "0.1.0".into(),
37 contacts: vec![],
38 hide_contacts: None,
39
40 storage_size: 10_000_000,
41
42 default_timeout: None,
43 csp: None,
44 cors: None,
45
46 runtime: None,
47
48 hide_schema: None,
49
50 obfuscation: None,
51 client_rendering: None,
52 client_events: None,
53
54 port: None,
55 redirect_port: None,
56 logging: None,
57 globals: None,
58 secrets: None,
59
60 assets: None,
62 content: None,
63 error: None,
64
65 flags: None,
66
67 templates: None,
68 auth: None,
69 actions: None,
70 models: None,
71 integrations: None,
72 };
73
74 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
75
76 let mut file = fs_err::File::create(path.join("ordinary.json"))?;
77 file.write_all(ordinary_json.as_bytes())?;
78
79 let mut file = fs_err::File::create(path.join(".gitignore"))?;
80 file.write_all(gitignore.as_bytes())?;
81
82 if let Some(path) = path.to_str() {
83 add_template(path, "index", "/", "text/html")?;
84 }
85
86 Ok(())
87}
88
89#[instrument(err)]
90#[allow(
91 clippy::too_many_arguments,
92 clippy::used_underscore_binding,
93 clippy::too_many_lines
94)]
95pub fn add_action(
96 path: &str,
97 name: &str,
98 lang: &str,
99 _access: (),
100 _accepts: (),
101 _returns: (),
102 transactional: &Option<bool>,
103 _protected: (),
104) -> anyhow::Result<()> {
105 let proj_path = Path::new(path);
106 let ordinary_json = fs_err::read_to_string(proj_path.join("ordinary.json"))?;
107
108 let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
109
110 let mut actions = app_config.actions.unwrap_or_default();
111
112 if actions.len() <= 225 {
113 let next_idx = actions.len();
114
115 let language = match lang {
116 "Rust" | "rust" | "rs" => ordinary_config::ActionLang::Rust,
117 "JavaScript" | "javascript" | "JS" | "js" => ordinary_config::ActionLang::JavaScript,
118 _ => bail!("language {lang} not yet supported"),
119 };
120
121 let action_dir_path = proj_path.join("actions").join(name);
122 fs_err::create_dir_all(&action_dir_path)?;
123
124 match language {
125 ordinary_config::ActionLang::Rust => {
126 let gitignore = "/target";
127
128 let cargo_toml = format!(
129 r#"[workspace]
130
131[package]
132name = "action"
133version = "0.1.0"
134edition = "2024"
135
136[dependencies]
137ordinary = {{ path = "../../.ordinary/gen/actions/{name}" }}
138
139[profile.release]
140strip = "symbols"
141lto = "fat"
142opt-level = "z"
143codegen-units = 1
144panic = "abort"
145"#
146 );
147
148 let main_rs = r#"use std::error::Error;
149use ordinary::{recv_in, send_out};
150
151fn main() -> Result<(), Box<dyn Error>> {
152 let _input = recv_in()?;
153
154 ordinary::trace(2, "Hello, Ordinary!")?;
155
156 send_out(())
157}
158"#;
159
160 let mut gitignore_file = fs_err::File::create(action_dir_path.join(".gitignore"))?;
161 gitignore_file.write_all(gitignore.as_bytes())?;
162
163 let mut cargo_toml_file = fs_err::File::create(action_dir_path.join("Cargo.toml"))?;
164 cargo_toml_file.write_all(cargo_toml.as_bytes())?;
165
166 fs_err::create_dir_all(action_dir_path.join("src"))?;
167 let mut main_rs_file =
168 fs_err::File::create(action_dir_path.join("src").join("main.rs"))?;
169 main_rs_file.write_all(main_rs.as_bytes())?;
170 }
171 ordinary_config::ActionLang::JavaScript => {
172 let gitignore = r"/target
173node_modules";
174
175 let package_json = r#"{
176 "name": "action",
177 "version": "0.1.0",
178 "main": "index.js",
179 "files": [
180 "index.js",
181 "index.d.ts"
182 ],
183 "types": "client.d.ts",
184 "scripts": {
185 "build": "esbuild main.js --bundle --minify --outfile=index.js --target=es2020 --format=iife --global-name=action"
186 },
187 "devDependencies": {
188 "esbuild": "0.25.10"
189 }
190}
191 "#;
192
193 let main_js = r#"export function main(input) {
194 console.log("Hello, Ordinary!");
195}"#;
196
197 let mut gitignore_file = fs_err::File::create(action_dir_path.join(".gitignore"))?;
198 gitignore_file.write_all(gitignore.as_bytes())?;
199
200 let mut package_json_file =
201 fs_err::File::create(action_dir_path.join("package.json"))?;
202 package_json_file.write_all(package_json.as_bytes())?;
203
204 let mut main_js_file = fs_err::File::create(action_dir_path.join("main.js"))?;
205 main_js_file.write_all(main_js.as_bytes())?;
206 }
207 }
208
209 actions.push(ActionConfig {
210 ffi: ActionFfi {
211 version: ActionFfiVersion::V1,
212 serialization: ActionFfiSerialization::FlexBufferVector,
213 },
214 idx: u8::try_from(next_idx)?,
215 dir_path: format!("./actions/{name}"),
216 name: name.to_string(),
217 lang: language,
218 access: vec![],
219 accepts: ordinary_types::Kind::Void,
220 returns: ordinary_types::Kind::Void,
221 triggered_by: vec![],
222 transactional: *transactional,
223 protected: None,
224 wasm_opt: None,
225 timeout: None,
226 cors: None,
227 privileged: None,
228 variables: None,
229 });
230
231 app_config.actions = Some(actions);
232
233 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
234
235 let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
236 file.write_all(ordinary_json.as_bytes())?;
237 } else {
238 tracing::error!("cannot support more than 255 actions for a single project.");
239 }
240
241 Ok(())
242}
243
244#[instrument(err)]
245pub fn add_template(path: &str, name: &str, route: &str, mime: &str) -> anyhow::Result<()> {
246 let proj_path = Path::new(path);
247 let ordinary_json = fs_err::read_to_string(proj_path.join("ordinary.json"))?;
248
249 let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
250
251 let mut templates = app_config.templates.unwrap_or_default();
252
253 if templates.len() <= 255 {
254 let next_idx = templates.len();
255
256 let cache = Some(TemplateCache {
257 stored: None,
258 http: None,
259 });
260
261 let file_ext = match mime {
262 "text/html" => "html",
263 "text/xml" => "xml",
264 "text/plain" => "txt",
265 _ => "",
266 };
267
268 fs_err::create_dir_all(proj_path.join("templates"))?;
269
270 let file_name = format!("{name}.{file_ext}");
271
272 let mut file = fs_err::File::create(proj_path.join("templates").join(&file_name))?;
273 match mime {
276 "text/html" => {
277 let default_html = format!(
278 r#"<!DOCTYPE html>
279<html lang="en">
280
281<head>
282 <meta charset="UTF-8">
283 <meta name="viewport" content="width=device-width, initial-scale=1.0">
284
285 <title>{} | {{{{ domain }}}}</title>
286</head>
287
288<body>
289 <header>
290 <a href="/">{{{{ domain }}}}</a>
291 </header>
292 <main>
293 <h1>{}</h1>
294 </main>
295 <hr>
296 <footer>
297 © 2026 Your Name | powered by <a href="https://codeberg.org/ordinarylabs/Ordinary">Ordinary</a>
298 </footer>
299</body>
300"#,
301 name,
302 name.to_title_case()
303 );
304 file.write_all(default_html.as_bytes())?;
305 }
306 _ => {
307 file.write_all(&[][..])?;
308 }
309 }
310
311 templates.push(TemplateConfig {
312 ffi: TemplateFfi {
313 version: TemplateFfiVersion::V1,
314 serialization: TemplateFfiSerialization::FlexBufferVector,
315 },
316 idx: u8::try_from(next_idx)?,
317 name: name.to_string(),
318 route: route.to_string(),
320 mime: mime.to_string(),
322 cache,
323 csp: None,
324 cors: None,
325 path: Some(format!("./templates/{file_name}")),
326 protected: None,
327 globals: None,
328 flags: None,
329 fields: None,
330 params: None,
331 content: None,
332 models: None,
333 actions: None,
334 minify: None,
335 wasm_opt: None,
336 timeout: None,
337 variables: None,
338 });
339
340 app_config.templates = Some(templates);
341
342 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
343
344 let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
345 file.write_all(ordinary_json.as_bytes())?;
346 } else {
347 tracing::error!("cannot support more than 255 templates for a single project.");
348 }
349
350 Ok(())
351}
352
353#[instrument(err)]
354pub fn add_integration(
355 path: &str,
356 name: &str,
357 endpoint: &str,
358 protocol: &str,
359) -> anyhow::Result<()> {
360 let proj_path = Path::new(path);
361 let ordinary_json = fs_err::read_to_string(proj_path.join("ordinary.json"))?;
362
363 let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
364
365 let mut integrations = app_config.integrations.unwrap_or_default();
366
367 if integrations.len() <= 255 {
368 let next_idx = integrations.len();
369
370 integrations.push(IntegrationConfig {
371 idx: u8::try_from(next_idx)?,
372 name: name.to_string(),
373 protocol: match protocol {
374 "JSON" => IntegrationProtocol::Http {
375 method: "GET".to_string(),
376 headers: vec![],
377 send_encoding: ordinary_config::IntegrationProtocolHttpEncoding::Text,
378 recv_encoding: ordinary_config::IntegrationProtocolHttpEncoding::Text,
379 },
380 _ => bail!("invalid protocol"),
381 },
382 endpoint: endpoint.to_string(),
383 send: ordinary_types::Kind::Json,
384 recv: ordinary_types::Kind::Json,
385 secrets: None,
386 timeout: None,
387 });
388
389 app_config.integrations = Some(integrations);
390
391 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
392
393 let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
394 file.write_all(ordinary_json.as_bytes())?;
395 } else {
396 tracing::error!("cannot support more than 255 integrations for a single project.");
397 }
398
399 Ok(())
400}
401
402#[instrument(err)]
403pub fn add_content_def(path: &str, name: &String) -> anyhow::Result<()> {
404 let proj_path = Path::new(path);
405 let ordinary_json = fs_err::read_to_string(proj_path.join("ordinary.json"))?;
406
407 let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
408
409 let mut file_path = "./content.json".to_string();
410
411 let mut content_defs = if let Some(d) = app_config.content {
412 file_path = d.file_path;
413 d.definitions
414 } else {
415 let mut file = fs_err::File::create(proj_path.join("content.json"))?;
416 file.write_all(b"[]")?;
417
418 vec![]
419 };
420
421 if content_defs.len() < 255 {
422 let next_idx = content_defs.len();
423
424 content_defs.push(ContentDefinition {
425 idx: u8::try_from(next_idx)?,
426 name: name.clone(),
427 fields: vec![],
428 });
429
430 app_config.content = Some(Content {
431 definitions: content_defs,
432 file_path,
433 });
434
435 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
436
437 let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
438 file.write_all(ordinary_json.as_bytes())?;
439 } else {
440 tracing::error!("cannot support more than 255 content definitions for a single project.");
441 }
442
443 Ok(())
444}
445
446#[instrument(err)]
447pub fn add_content_obj(path: &str, object_json: &String) -> anyhow::Result<()> {
448 let proj_path = Path::new(path);
449 let ordinary_json = fs_err::read_to_string(proj_path.join("ordinary.json"))?;
450
451 let app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
452
453 if let Some(content) = app_config.content {
454 let content_json = fs_err::read_to_string(proj_path.join(&content.file_path))?;
455
456 let mut content_objects: Vec<ContentObject> = serde_json::from_str(&content_json)?;
457
458 let new_obj: ContentObject = serde_json::from_str(object_json)?;
459
460 content_objects.push(new_obj);
463
464 let new_objects = serde_json::to_string_pretty(&content_objects)?;
465
466 let mut file = fs_err::File::create(proj_path.join(&content.file_path))?;
467 file.write_all(new_objects.as_bytes())?;
468 }
469
470 Ok(())
471}
472
473#[instrument(err)]
474pub fn add_model(path: &str, name: &str, uuid: Option<&str>) -> anyhow::Result<()> {
475 let proj_path = Path::new(path);
476 let ordinary_json = fs_err::read_to_string(proj_path.join("ordinary.json"))?;
477
478 let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
479
480 let mut models = app_config.models.unwrap_or_default();
481
482 if models.len() <= 255 {
483 let next_idx = models.len();
484
485 models.push(ModelConfig {
486 idx: u8::try_from(next_idx)?,
487 name: name.to_string(),
488 fields: vec![],
489 uuid: match uuid {
490 Some(uuid) => match uuid {
491 "v4" => Some(UuidVersion::V4),
492 "v7" => Some(UuidVersion::V7),
493 _ => bail!("invalid UUID version"),
494 },
495 None => None,
496 },
497 });
498
499 app_config.models = Some(models);
500
501 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
502
503 let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
504 file.write_all(ordinary_json.as_bytes())?;
505 } else {
506 tracing::error!("cannot support more than 255 models for a single project.");
507 }
508
509 Ok(())
510}