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 variables: 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 = fs_err::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 = fs_err::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 fs_err::create_dir_all(proj_path.join("templates"))?;
260
261 let file_name = format!("{name}.{file_ext}");
262
263 let mut file = fs_err::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 variables: None,
318 });
319
320 app_config.templates = Some(templates);
321
322 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
323
324 let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
325 file.write_all(ordinary_json.as_bytes())?;
326 } else {
327 tracing::error!("cannot support more than 255 templates for a single project.");
328 }
329
330 Ok(())
331}
332
333#[instrument(err)]
334pub fn add_integration(
335 path: &str,
336 name: &str,
337 endpoint: &str,
338 protocol: &str,
339) -> anyhow::Result<()> {
340 let proj_path = Path::new(path);
341 let ordinary_json = fs_err::read_to_string(proj_path.join("ordinary.json"))?;
342
343 let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
344
345 let mut integrations = app_config.integrations.unwrap_or_default();
346
347 if integrations.len() <= 255 {
348 let next_idx = integrations.len();
349
350 integrations.push(IntegrationConfig {
351 idx: u8::try_from(next_idx)?,
352 name: name.to_string(),
353 protocol: match protocol {
354 "JSON" => IntegrationProtocol::Http {
355 method: "GET".to_string(),
356 headers: vec![],
357 send_encoding: ordinary_config::IntegrationProtocolHttpEncoding::Text,
358 recv_encoding: ordinary_config::IntegrationProtocolHttpEncoding::Text,
359 },
360 _ => bail!("invalid protocol"),
361 },
362 endpoint: endpoint.to_string(),
363 send: ordinary_types::Kind::Json,
364 recv: ordinary_types::Kind::Json,
365 secrets: None,
366 timeout: None,
367 });
368
369 app_config.integrations = Some(integrations);
370
371 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
372
373 let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
374 file.write_all(ordinary_json.as_bytes())?;
375 } else {
376 tracing::error!("cannot support more than 255 integrations for a single project.");
377 }
378
379 Ok(())
380}
381
382#[instrument(err)]
383pub fn add_content_def(path: &str, name: &String) -> anyhow::Result<()> {
384 let proj_path = Path::new(path);
385 let ordinary_json = fs_err::read_to_string(proj_path.join("ordinary.json"))?;
386
387 let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
388
389 let mut file_path = "./content.json".to_string();
390
391 let mut content_defs = if let Some(d) = app_config.content {
392 file_path = d.file_path;
393 d.definitions
394 } else {
395 let mut file = fs_err::File::create(proj_path.join("content.json"))?;
396 file.write_all(b"[]")?;
397
398 vec![]
399 };
400
401 if content_defs.len() < 255 {
402 let next_idx = content_defs.len();
403
404 content_defs.push(ContentDefinition {
405 idx: u8::try_from(next_idx)?,
406 name: name.clone(),
407 fields: vec![],
408 });
409
410 app_config.content = Some(Content {
411 definitions: content_defs,
412 file_path,
413 });
414
415 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
416
417 let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
418 file.write_all(ordinary_json.as_bytes())?;
419 } else {
420 tracing::error!("cannot support more than 255 content definitions for a single project.");
421 }
422
423 Ok(())
424}
425
426#[instrument(err)]
427pub fn add_content_obj(path: &str, object_json: &String) -> anyhow::Result<()> {
428 let proj_path = Path::new(path);
429 let ordinary_json = fs_err::read_to_string(proj_path.join("ordinary.json"))?;
430
431 let app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
432
433 if let Some(content) = app_config.content {
434 let content_json = fs_err::read_to_string(proj_path.join(&content.file_path))?;
435
436 let mut content_objects: Vec<ContentObject> = serde_json::from_str(&content_json)?;
437
438 let new_obj: ContentObject = serde_json::from_str(object_json)?;
439
440 content_objects.push(new_obj);
443
444 let new_objects = serde_json::to_string_pretty(&content_objects)?;
445
446 let mut file = fs_err::File::create(proj_path.join(&content.file_path))?;
447 file.write_all(new_objects.as_bytes())?;
448 }
449
450 Ok(())
451}
452
453#[instrument(err)]
454pub fn add_model(path: &str, name: &str, uuid: Option<&str>) -> anyhow::Result<()> {
455 let proj_path = Path::new(path);
456 let ordinary_json = fs_err::read_to_string(proj_path.join("ordinary.json"))?;
457
458 let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
459
460 let mut models = app_config.models.unwrap_or_default();
461
462 if models.len() <= 255 {
463 let next_idx = models.len();
464
465 models.push(ModelConfig {
466 idx: u8::try_from(next_idx)?,
467 name: name.to_string(),
468 fields: vec![],
469 uuid: match uuid {
470 Some(uuid) => match uuid {
471 "v4" => Some(UuidVersion::V4),
472 "v7" => Some(UuidVersion::V7),
473 _ => bail!("invalid UUID version"),
474 },
475 None => None,
476 },
477 });
478
479 app_config.models = Some(models);
480
481 let ordinary_json = serde_json::to_string_pretty(&app_config)?;
482
483 let mut file = fs_err::File::create(proj_path.join("ordinary.json"))?;
484 file.write_all(ordinary_json.as_bytes())?;
485 } else {
486 tracing::error!("cannot support more than 255 models for a single project.");
487 }
488
489 Ok(())
490}