itsalive/
lib.rs

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        // todo: set this on last arm of path
30        globals: None,
31        secrets: None,
32
33        // todo: all of these should be set with defaults
34        assets: None,
35        content: None,
36        error: None,
37        // todo /
38        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        // todo: have a default file for each extension/mime type
249
250        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            // todo: verify that route begins with "/" and is valid HTTP route
279            route: route.clone(),
280            // todo: check against valid mime types
281            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        // todo: do more to verify the structure of the object is compatible with its instance_of
418
419        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}