#![doc = include_str!("../README.md")]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
use std::error::Error;
use std::{io::Write, path::Path};
use heck::ToTitleCase;
use ordinary_config::{
ActionConfig, ActionFfi, ActionFfiSerialization, ActionFfiVersion, Content, ContentDefinition,
IntegrationConfig, IntegrationProtocol, ModelConfig, OrdinaryConfig, TemplateCache,
TemplateConfig, TemplateFfi, TemplateFfiSerialization, TemplateFfiVersion, UuidVersion,
};
use ordinary_types::ContentObject;
use tracing::instrument;
#[instrument(err)]
pub fn create_project(path: &String, domain: &String) -> Result<(), Box<dyn Error>> {
let path = Path::new(path).join(domain);
tracing::info!("creating project at {:?}", path);
std::fs::create_dir_all(&path)?;
let app_config = OrdinaryConfig {
domain: domain.clone(),
cnames: None,
version: "0.1.0".into(),
contacts: vec![],
storage_size: 10_000_000,
default_timeout: None,
csp: None,
cors: None,
runtime: None,
hide_schema: None,
obfuscation: None,
client_rendering: None,
client_events: None,
port: None,
redirect_port: None,
logging: None,
globals: None,
secrets: None,
assets: None,
content: None,
error: None,
flags: None,
templates: None,
auth: None,
actions: None,
models: None,
integrations: None,
};
let ordinary_json = serde_json::to_string_pretty(&app_config)?;
let mut file = std::fs::File::create(path.join("ordinary.json"))?;
file.write_all(ordinary_json.as_bytes())?;
if let Some(path) = path.to_str() {
add_template(path, "index", "/", "text/html")?;
}
Ok(())
}
#[instrument(err)]
#[allow(
clippy::too_many_arguments,
clippy::used_underscore_binding,
clippy::too_many_lines
)]
pub fn add_action(
path: &str,
name: &str,
lang: &str,
_access: (),
_accepts: (),
_returns: (),
transactional: &Option<bool>,
_protected: (),
) -> Result<(), Box<dyn Error>> {
let proj_path = Path::new(path);
let ordinary_json = std::fs::read_to_string(proj_path.join("ordinary.json"))?;
let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
let mut actions = app_config.actions.unwrap_or_default();
if actions.len() <= 225 {
let next_idx = actions.len();
let language = match lang {
"Rust" | "rs" => ordinary_config::ActionLang::Rust,
"JavaScript" | "JS" | "js" => ordinary_config::ActionLang::JavaScript,
_ => return Err(format!("language {lang} not yet supported").into()),
};
let action_dir_path = proj_path.join("actions").join(name);
std::fs::create_dir_all(&action_dir_path)?;
match language {
ordinary_config::ActionLang::Rust => {
let gitignore = "/target";
let cargo_toml = format!(
r#"[workspace]
[package]
name = "action"
version = "0.1.0"
edition = "2024"
[dependencies]
ordinary = {{ path = "../../.ordinary/gen/actions/{name}" }}
[profile.release]
strip = "symbols"
lto = "fat"
opt-level = "z"
codegen-units = 1
panic = "abort"
"#
);
let main_rs = r#"use std::error::Error;
use ordinary::{recv_in, send_out};
fn main() -> Result<(), Box<dyn Error>> {
let _input = recv_in()?;
ordinary::trace(2, "Hello, Ordinary!")?;
send_out(())
}
"#;
let mut gitignore_file = std::fs::File::create(action_dir_path.join(".gitignore"))?;
gitignore_file.write_all(gitignore.as_bytes())?;
let mut cargo_toml_file =
std::fs::File::create(action_dir_path.join("Cargo.toml"))?;
cargo_toml_file.write_all(cargo_toml.as_bytes())?;
std::fs::create_dir_all(action_dir_path.join("src"))?;
let mut main_rs_file =
std::fs::File::create(action_dir_path.join("src").join("main.rs"))?;
main_rs_file.write_all(main_rs.as_bytes())?;
}
ordinary_config::ActionLang::JavaScript => {
let gitignore = r"/target
node_modules";
let package_json = r#"{
"name": "action",
"version": "0.1.0",
"main": "index.js",
"files": [
"index.js",
"index.d.ts"
],
"types": "client.d.ts",
"scripts": {
"build": "esbuild main.js --bundle --minify --outfile=index.js --target=es2020 --format=iife --global-name=action"
},
"devDependencies": {
"esbuild": "0.25.10"
}
}
"#;
let main_js = r#"export function main(input) {
console.log("Hello, Ordinary!");
}"#;
let mut gitignore_file = std::fs::File::create(action_dir_path.join(".gitignore"))?;
gitignore_file.write_all(gitignore.as_bytes())?;
let mut package_json_file =
std::fs::File::create(action_dir_path.join("package.json"))?;
package_json_file.write_all(package_json.as_bytes())?;
let mut main_js_file = std::fs::File::create(action_dir_path.join("main.js"))?;
main_js_file.write_all(main_js.as_bytes())?;
}
}
actions.push(ActionConfig {
ffi: ActionFfi {
version: ActionFfiVersion::V1,
serialization: ActionFfiSerialization::FlexBufferVector,
},
idx: u8::try_from(next_idx)?,
dir_path: format!("./actions/{name}"),
name: name.to_string(),
lang: language,
access: vec![],
accepts: ordinary_types::Kind::Void,
returns: ordinary_types::Kind::Void,
triggered_by: vec![],
transactional: *transactional,
protected: None,
wasm_opt: None,
timeout: None,
cors: None,
});
app_config.actions = Some(actions);
let ordinary_json = serde_json::to_string_pretty(&app_config)?;
let mut file = std::fs::File::create(proj_path.join("ordinary.json"))?;
file.write_all(ordinary_json.as_bytes())?;
} else {
tracing::error!("cannot support more than 255 actions for a single project.");
}
Ok(())
}
#[instrument(err)]
pub fn add_template(path: &str, name: &str, route: &str, mime: &str) -> Result<(), Box<dyn Error>> {
let proj_path = Path::new(path);
let ordinary_json = std::fs::read_to_string(proj_path.join("ordinary.json"))?;
let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
let mut templates = app_config.templates.unwrap_or_default();
if templates.len() <= 255 {
let next_idx = templates.len();
let cache = Some(TemplateCache {
stored: None,
http: None,
});
let file_ext = match mime {
"text/html" => "html",
"text/xml" => "xml",
"text/plain" => "txt",
_ => "",
};
std::fs::create_dir_all(proj_path.join("templates"))?;
let file_name = format!("{name}.{file_ext}");
let mut file = std::fs::File::create(proj_path.join("templates").join(&file_name))?;
match mime {
"text/html" => {
let default_html = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<header><a href="/">{{{{ domain }}}}</a></header>
<h1>{}</h1>
</body>
"#,
name.to_title_case()
);
file.write_all(default_html.as_bytes())?;
}
_ => {
file.write_all(&[][..])?;
}
}
templates.push(TemplateConfig {
ffi: TemplateFfi {
version: TemplateFfiVersion::V1,
serialization: TemplateFfiSerialization::FlexBufferVector,
},
idx: u8::try_from(next_idx)?,
name: name.to_string(),
route: route.to_string(),
mime: mime.to_string(),
cache,
csp: None,
cors: None,
path: Some(format!("./templates/{file_name}")),
protected: None,
globals: None,
flags: None,
fields: None,
params: None,
content: None,
models: None,
actions: None,
minify: None,
wasm_opt: None,
timeout: None,
});
app_config.templates = Some(templates);
let ordinary_json = serde_json::to_string_pretty(&app_config)?;
let mut file = std::fs::File::create(proj_path.join("ordinary.json"))?;
file.write_all(ordinary_json.as_bytes())?;
} else {
tracing::error!("cannot support more than 255 templates for a single project.");
}
Ok(())
}
#[instrument(err)]
pub fn add_integration(
path: &str,
name: &str,
endpoint: &str,
protocol: &str,
) -> Result<(), Box<dyn Error>> {
let proj_path = Path::new(path);
let ordinary_json = std::fs::read_to_string(proj_path.join("ordinary.json"))?;
let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
let mut integrations = app_config.integrations.unwrap_or_default();
if integrations.len() <= 255 {
let next_idx = integrations.len();
integrations.push(IntegrationConfig {
idx: u8::try_from(next_idx)?,
name: name.to_string(),
protocol: match protocol {
"JSON" => IntegrationProtocol::Http {
method: "GET".to_string(),
headers: vec![],
send_encoding: ordinary_config::IntegrationProtocolHttpEncoding::Text,
recv_encoding: ordinary_config::IntegrationProtocolHttpEncoding::Text,
},
_ => return Err("invalid protocol".into()),
},
endpoint: endpoint.to_string(),
send: ordinary_types::Kind::Json,
recv: ordinary_types::Kind::Json,
secrets: None,
timeout: None,
});
app_config.integrations = Some(integrations);
let ordinary_json = serde_json::to_string_pretty(&app_config)?;
let mut file = std::fs::File::create(proj_path.join("ordinary.json"))?;
file.write_all(ordinary_json.as_bytes())?;
} else {
tracing::error!("cannot support more than 255 integrations for a single project.");
}
Ok(())
}
#[instrument(err)]
pub fn add_content_def(path: &str, name: &String) -> Result<(), Box<dyn Error>> {
let proj_path = Path::new(path);
let ordinary_json = std::fs::read_to_string(proj_path.join("ordinary.json"))?;
let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
let mut file_path = "./content.json".to_string();
let mut content_defs = if let Some(d) = app_config.content {
file_path = d.file_path;
d.definitions
} else {
let mut file = std::fs::File::create(proj_path.join("content.json"))?;
file.write_all(b"[]")?;
vec![]
};
if content_defs.len() < 255 {
let next_idx = content_defs.len();
content_defs.push(ContentDefinition {
idx: u8::try_from(next_idx)?,
name: name.clone(),
fields: vec![],
});
app_config.content = Some(Content {
definitions: content_defs,
file_path,
});
let ordinary_json = serde_json::to_string_pretty(&app_config)?;
let mut file = std::fs::File::create(proj_path.join("ordinary.json"))?;
file.write_all(ordinary_json.as_bytes())?;
} else {
tracing::error!("cannot support more than 255 content definitions for a single project.");
}
Ok(())
}
#[instrument(err)]
pub fn add_content_obj(path: &str, object_json: &String) -> Result<(), Box<dyn Error>> {
let proj_path = Path::new(path);
let ordinary_json = std::fs::read_to_string(proj_path.join("ordinary.json"))?;
let app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
if let Some(content) = app_config.content {
let content_json = std::fs::read_to_string(proj_path.join(&content.file_path))?;
let mut content_objects: Vec<ContentObject> = serde_json::from_str(&content_json)?;
let new_obj: ContentObject = serde_json::from_str(object_json)?;
content_objects.push(new_obj);
let new_objects = serde_json::to_string_pretty(&content_objects)?;
let mut file = std::fs::File::create(proj_path.join(&content.file_path))?;
file.write_all(new_objects.as_bytes())?;
}
Ok(())
}
#[instrument(err)]
pub fn add_model(path: &str, name: &str, uuid: Option<&str>) -> Result<(), Box<dyn Error>> {
let proj_path = Path::new(path);
let ordinary_json = std::fs::read_to_string(proj_path.join("ordinary.json"))?;
let mut app_config: OrdinaryConfig = serde_json::from_str(&ordinary_json)?;
let mut models = app_config.models.unwrap_or_default();
if models.len() <= 255 {
let next_idx = models.len();
models.push(ModelConfig {
idx: u8::try_from(next_idx)?,
name: name.to_string(),
fields: vec![],
uuid: match uuid {
Some(uuid) => match uuid {
"v4" => Some(UuidVersion::V4),
"v7" => Some(UuidVersion::V7),
_ => return Err("invalid UUID version".into()),
},
None => None,
},
});
app_config.models = Some(models);
let ordinary_json = serde_json::to_string_pretty(&app_config)?;
let mut file = std::fs::File::create(proj_path.join("ordinary.json"))?;
file.write_all(ordinary_json.as_bytes())?;
} else {
tracing::error!("cannot support more than 255 models for a single project.");
}
Ok(())
}