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