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