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