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