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 fs_err::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,
52 secrets: None,
53
54 assets: None,
56 content: None,
57 error: None,
58
59 flags: None,
60
61 templates: None,
62 auth: None,
63 actions: None,
64 models: None,
65 integrations: None,
66 };
67
68 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
69
70 let mut file = fs_err::File::create(path.join("ordinary.json"))?;
71 file.write_all(ordinary_json.as_bytes())?;
72
73 if let Some(path) = path.to_str() {
74 add_template(path, "index", "/", "text/html")?;
75 }
76
77 Ok(())
78}
79
80#[instrument(err)]
81#[allow(
82 clippy::too_many_arguments,
83 clippy::used_underscore_binding,
84 clippy::too_many_lines
85)]
86pub fn add_action(
87 path: &str,
88 name: &str,
89 lang: &str,
90 _access: (),
91 _accepts: (),
92 _returns: (),
93 transactional: &Option<bool>,
94 _protected: (),
95) -> anyhow::Result<()> {
96 let proj_path = Path::new(path);
97 let ordinary_json = fs_err::read_to_string(proj_path.join("ordinary.json"))?;
98
99 let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
100
101 let mut actions = app_config.actions.unwrap_or_default();
102
103 if actions.len() <= 225 {
104 let next_idx = actions.len();
105
106 let language = match lang {
107 "Rust" | "rust" | "rs" => ordinary_config::ActionLang::Rust,
108 "JavaScript" | "javascript" | "JS" | "js" => ordinary_config::ActionLang::JavaScript,
109 _ => bail!("language {lang} not yet supported"),
110 };
111
112 let action_dir_path = proj_path.join("actions").join(name);
113 fs_err::create_dir_all(&action_dir_path)?;
114
115 match language {
116 ordinary_config::ActionLang::Rust => {
117 let gitignore = "/target";
118
119 let cargo_toml = format!(
120 r#"[workspace]
121
122[package]
123name = "action"
124version = "0.1.0"
125edition = "2024"
126
127[dependencies]
128ordinary = {{ path = "../../.ordinary/gen/actions/{name}" }}
129
130[profile.release]
131strip = "symbols"
132lto = "fat"
133opt-level = "z"
134codegen-units = 1
135panic = "abort"
136"#
137 );
138
139 let main_rs = r#"use std::error::Error;
140use ordinary::{recv_in, send_out};
141
142fn main() -> Result<(), Box<dyn Error>> {
143 let _input = recv_in()?;
144
145 ordinary::trace(2, "Hello, Ordinary!")?;
146
147 send_out(())
148}
149"#;
150
151 let mut gitignore_file = fs_err::File::create(action_dir_path.join(".gitignore"))?;
152 gitignore_file.write_all(gitignore.as_bytes())?;
153
154 let mut cargo_toml_file = fs_err::File::create(action_dir_path.join("Cargo.toml"))?;
155 cargo_toml_file.write_all(cargo_toml.as_bytes())?;
156
157 fs_err::create_dir_all(action_dir_path.join("src"))?;
158 let mut main_rs_file =
159 fs_err::File::create(action_dir_path.join("src").join("main.rs"))?;
160 main_rs_file.write_all(main_rs.as_bytes())?;
161 }
162 ordinary_config::ActionLang::JavaScript => {
163 let gitignore = r"/target
164node_modules";
165
166 let package_json = r#"{
167 "name": "action",
168 "version": "0.1.0",
169 "main": "index.js",
170 "files": [
171 "index.js",
172 "index.d.ts"
173 ],
174 "types": "client.d.ts",
175 "scripts": {
176 "build": "esbuild main.js --bundle --minify --outfile=index.js --target=es2020 --format=iife --global-name=action"
177 },
178 "devDependencies": {
179 "esbuild": "0.25.10"
180 }
181}
182 "#;
183
184 let main_js = r#"export function main(input) {
185 console.log("Hello, Ordinary!");
186}"#;
187
188 let mut gitignore_file = fs_err::File::create(action_dir_path.join(".gitignore"))?;
189 gitignore_file.write_all(gitignore.as_bytes())?;
190
191 let mut package_json_file =
192 fs_err::File::create(action_dir_path.join("package.json"))?;
193 package_json_file.write_all(package_json.as_bytes())?;
194
195 let mut main_js_file = fs_err::File::create(action_dir_path.join("main.js"))?;
196 main_js_file.write_all(main_js.as_bytes())?;
197 }
198 }
199
200 actions.push(ActionConfig {
201 ffi: ActionFfi {
202 version: ActionFfiVersion::V1,
203 serialization: ActionFfiSerialization::FlexBufferVector,
204 },
205 idx: u8::try_from(next_idx)?,
206 dir_path: format!("./actions/{name}"),
207 name: name.to_string(),
208 lang: language,
209 access: vec![],
210 accepts: ordinary_types::Kind::Void,
211 returns: ordinary_types::Kind::Void,
212 triggered_by: vec![],
213 transactional: *transactional,
214 protected: None,
215 wasm_opt: None,
216 timeout: None,
217 cors: None,
218 privileged: None,
219 });
220
221 app_config.actions = Some(actions);
222
223 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
224
225 let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
226 file.write_all(ordinary_json.as_bytes())?;
227 } else {
228 tracing::error!("cannot support more than 255 actions for a single project.");
229 }
230
231 Ok(())
232}
233
234#[instrument(err)]
235pub fn add_template(path: &str, name: &str, route: &str, mime: &str) -> anyhow::Result<()> {
236 let proj_path = Path::new(path);
237 let ordinary_json = fs_err::read_to_string(proj_path.join("ordinary.json"))?;
238
239 let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
240
241 let mut templates = app_config.templates.unwrap_or_default();
242
243 if templates.len() <= 255 {
244 let next_idx = templates.len();
245
246 let cache = Some(TemplateCache {
247 stored: None,
248 http: None,
249 });
250
251 let file_ext = match mime {
252 "text/html" => "html",
253 "text/xml" => "xml",
254 "text/plain" => "txt",
255 _ => "",
256 };
257
258 fs_err::create_dir_all(proj_path.join("templates"))?;
259
260 let file_name = format!("{name}.{file_ext}");
261
262 let mut file = fs_err::File::create(proj_path.join("templates").join(&file_name))?;
263 match mime {
266 "text/html" => {
267 let default_html = format!(
268 r#"<!DOCTYPE html>
269<html lang="en">
270
271<head>
272 <meta charset="UTF-8">
273 <meta name="viewport" content="width=device-width, initial-scale=1.0">
274</head>
275
276<body>
277 <header><a href="/">{{{{ domain }}}}</a></header>
278 <h1>{}</h1>
279</body>
280"#,
281 name.to_title_case()
282 );
283 file.write_all(default_html.as_bytes())?;
284 }
285 _ => {
286 file.write_all(&[][..])?;
287 }
288 }
289
290 templates.push(TemplateConfig {
291 ffi: TemplateFfi {
292 version: TemplateFfiVersion::V1,
293 serialization: TemplateFfiSerialization::FlexBufferVector,
294 },
295 idx: u8::try_from(next_idx)?,
296 name: name.to_string(),
297 route: route.to_string(),
299 mime: mime.to_string(),
301 cache,
302 csp: None,
303 cors: None,
304 path: Some(format!("./templates/{file_name}")),
305 protected: None,
306 globals: None,
307 flags: None,
308 fields: None,
309 params: None,
310 content: None,
311 models: None,
312 actions: None,
313 minify: None,
314 wasm_opt: None,
315 timeout: None,
316 variables: 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 = fs_err::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 = fs_err::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 = fs_err::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 = fs_err::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 = fs_err::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 = fs_err::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 = fs_err::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 = fs_err::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 = fs_err::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 = fs_err::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 = fs_err::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}