use std::collections::{BTreeMap, BTreeSet};
use heck::{ToPascalCase, ToSnakeCase};
use ordinary_config::{
AuthConfig, ContentDefinition, ErrorConfig, Global, ModelConfig, TemplateConfig, TemplateRef,
TemplateRefField, TemplateRefFieldBind,
};
use ordinary_types::{Field, Kind};
const BASE_SERVER: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/static/server_template.rs"
));
#[allow(clippy::too_many_lines)]
fn build_ref_fields(
base_name: &str,
ref_fields: &Vec<TemplateRefField>,
fields: &Vec<Field>,
idx_chain: &[u8],
base_access: &str,
model_map: &BTreeMap<String, ModelConfig>,
) -> (String, String, Vec<String>) {
let mut field_map = BTreeMap::new();
for field in fields {
field_map.insert(field.name.clone(), field.clone());
}
let mut def_fields = String::new();
let mut instance_fields = String::new();
let mut additional_structs = vec![];
for ref_field in ref_fields {
let mut idx_chain = idx_chain.to_vec();
idx_chain.push(ref_field.idx);
if let Some(field) = field_map.get(&ref_field.name) {
if let Some(sub_fields) = &ref_field.fields {
match &field.kind {
Kind::List { kind } => {
if let Kind::Object { name, fields } = &**kind {
let struct_name = format!("{base_name}{name}").to_pascal_case();
let (sub_def_fields, sub_instance_fields, sub_additional_structs) =
build_ref_fields(
&struct_name,
sub_fields,
fields,
&[],
&format!("{}.as_vector()", &name),
model_map,
);
for additional_struct in sub_additional_structs {
additional_structs.push(additional_struct);
}
let struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {struct_name}<'a> {{{sub_def_fields}
}}"
);
additional_structs.push(struct_def);
def_fields = format!(
"{}\n {}: Vec<{}<'a>>,",
def_fields, field.name, struct_name
);
instance_fields = format!(
r"{}
{}: {}.idx({}).as_vector().iter().map(|{}| {} {{{}
}}).collect(),",
instance_fields,
field.name,
base_access,
field.idx,
name,
struct_name,
sub_instance_fields,
);
} else {
}
}
Kind::Object { name, fields } => {
let struct_name = format!("{base_name}{name}").to_pascal_case();
let (sub_def_fields, sub_instance_fields, sub_additional_structs) =
build_ref_fields(
&struct_name,
sub_fields,
fields,
&idx_chain,
base_access,
model_map,
);
for additional_struct in sub_additional_structs {
additional_structs.push(additional_struct);
}
let struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {struct_name}<'a> {{{sub_def_fields}
}}"
);
additional_structs.push(struct_def);
def_fields =
format!("{}\n {}: {}<'a>,", def_fields, field.name, struct_name);
instance_fields = format!(
r"{}
{}: {} {{{}
}},",
instance_fields, field.name, struct_name, sub_instance_fields,
);
}
Kind::Ref {
model,
field: _,
many,
} => {
let struct_name = format!("{base_name}_{model}").to_pascal_case();
if let Some(ref_model) = model_map.get(model) {
let (sub_def_fields, sub_instance_fields, sub_additional_structs) =
build_ref_fields(
&struct_name,
sub_fields,
&ref_model.fields,
&idx_chain,
base_access,
model_map,
);
for additional_struct in sub_additional_structs {
additional_structs.push(additional_struct);
}
let struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {struct_name}<'a> {{{sub_def_fields}
}}"
);
additional_structs.push(struct_def);
if many == &Some(true) {
def_fields = format!(
"{}\n {}: Vec<{}<'a>>,",
def_fields, field.name, struct_name
);
} else {
def_fields = format!(
"{}\n {}: {}<'a>,",
def_fields, field.name, struct_name
);
}
instance_fields = format!(
r"{}
{}: {} {{{}
}},",
instance_fields, field.name, struct_name, sub_instance_fields,
);
}
}
_ => {
}
}
} else if let Kind::Object { name, fields: _ } = &field.kind {
tracing::error!("template must list fields for object '{name}'.");
} else {
let (t, _) = field
.kind
.to_stringified_type_w_lifetime(base_name, "a", false);
def_fields = format!("{}\n {}: {},", def_fields, field.name, t);
let mut access = base_access.to_string();
for (i, idx) in idx_chain.iter().enumerate() {
if i == idx_chain.len() - 1 {
if let Some(accessor) = field.kind.to_flexbuffer_accessor() {
access = format!("{access}.idx({idx}){accessor}");
} else if let Kind::List { kind } = &field.kind {
if let Some(accessor) = kind.to_flexbuffer_accessor() {
access = format!(
"{access}.idx({idx}).as_vector().iter().map(|v| v{accessor}).collect()"
);
} else {
}
} else {
}
} else {
access = format!("{access}.idx({idx}).as_vector()");
}
}
instance_fields = format!("{}\n {}: {},", instance_fields, field.name, access);
}
} else {
}
}
(def_fields, instance_fields, additional_structs)
}
fn build_content_struct_for_ref(
template_name: &str,
template_ref: &TemplateRef,
content_def: &ContentDefinition,
idx_chain: &[u8],
base_access: &str,
) -> (String, String, String) {
let mut str_struct_defs = String::new();
let struct_name = format!(
"{}TemplateContent{}",
template_name.to_pascal_case(),
content_def.name.to_pascal_case()
);
let (struct_def_fields, struct_instance_fields, additional_struct_defs) =
match &template_ref.all {
Some(_) => build_ref_fields(
&struct_name,
&template_ref.fields,
&content_def.fields,
idx_chain,
&format!("{}.as_vector()", &template_ref.name),
&BTreeMap::new(),
),
None => build_ref_fields(
&struct_name,
&template_ref.fields,
&content_def.fields,
idx_chain,
base_access,
&BTreeMap::new(),
),
};
for struct_def in additional_struct_defs {
str_struct_defs = format!("{str_struct_defs}\n{struct_def}");
}
let struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {struct_name}<'a> {{{struct_def_fields}
}}",
);
let struct_instantiation = match &template_ref.all {
Some(name) => format!(
r" {}: {}.iter().map(|{}| {} {{{}
}}).collect(),",
name,
base_access,
&template_ref.name,
struct_name.to_pascal_case(),
struct_instance_fields
),
None => format!(
r" {}: {} {{{}
}},",
template_ref.name,
struct_name.to_pascal_case(),
struct_instance_fields
),
};
str_struct_defs = format!("{str_struct_defs}\n{struct_def}");
(struct_name, str_struct_defs, struct_instantiation)
}
fn build_model_struct_for_ref(
template_name: &str,
template_ref: &TemplateRef,
model_config: &ModelConfig,
model_config_map: &BTreeMap<String, ModelConfig>,
idx_chain: &[u8],
base_access: &str,
is_multi: bool,
) -> (String, String, String) {
let mut str_struct_defs = String::new();
let struct_name = format!(
"{}TemplateModels{}",
template_name.to_pascal_case(),
model_config.name.to_pascal_case()
);
let mut model_fields = model_config.fields.clone();
model_fields.push(Field {
idx: 0,
name: "uuid".into(),
kind: Kind::Uuid,
indexed: Some(true),
queryable: None,
searchable: None,
mapping: None,
doc: None,
encrypted: None,
compressed: None,
});
let (struct_def_fields, struct_instance_fields, additional_struct_defs) = if is_multi {
build_ref_fields(
&struct_name,
&template_ref.fields,
&model_fields,
idx_chain,
&format!("{}.as_vector()", &template_ref.name),
model_config_map,
)
} else {
build_ref_fields(
&struct_name,
&template_ref.fields,
&model_fields,
idx_chain,
base_access,
model_config_map,
)
};
for struct_def in additional_struct_defs {
str_struct_defs = format!("{str_struct_defs}\n{struct_def}");
}
let struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {struct_name}<'a> {{{struct_def_fields}
}}"
);
let struct_instantiation = if is_multi {
format!(
r" {}s: {}.iter().map(|{}| {} {{{}
}}).collect(),",
&template_ref.name,
base_access,
&template_ref.name,
struct_name.to_pascal_case(),
struct_instance_fields,
)
} else {
format!(
r" {}: {} {{{}
}},",
template_ref.name,
struct_name.to_pascal_case(),
struct_instance_fields,
)
};
str_struct_defs = format!("{str_struct_defs}\n{struct_def}");
(struct_name, str_struct_defs, struct_instantiation)
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines, clippy::ref_option)]
pub fn generate_template_renderers(
domain: &str,
version: &str,
canonical: &str,
generator: &str,
file_name: &str,
template_config: TemplateConfig,
content_def_map: &BTreeMap<String, ContentDefinition>,
model_config_map: &BTreeMap<String, ModelConfig>,
global_vars: &BTreeMap<String, Global>,
error_config: &Option<ErrorConfig>,
auth_config: &Option<AuthConfig>,
) -> (String, String) {
let mut structs = vec![];
let mut top_level_fields = vec![];
let mut filled_structs = vec![];
let mut idx_set = BTreeSet::new();
let mut special_param_idx = 0;
if let Some(params) = &template_config.params {
let struct_name = format!("{}TemplateParams", template_config.name.to_pascal_case());
let mut base_struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {struct_name}<'a> {{"
);
let mut ref_instantiations = String::new();
for param_ref in params {
special_param_idx += 1;
if idx_set.contains(¶m_ref.idx) {
tracing::warn!(
"duplicate index '{}'. will cause errors at runtime",
param_ref.idx
);
}
idx_set.insert(param_ref.idx);
let base_access = format!("root.as_vector().idx({}).as_str()", param_ref.idx);
base_struct_def = format!("{}\n{}: &'a str,", base_struct_def, param_ref.name);
ref_instantiations = format!(
"{}\n{}: {},",
ref_instantiations, param_ref.name, base_access
);
}
base_struct_def = format!("{base_struct_def}\n}}");
structs.push(base_struct_def);
filled_structs.push(format!(
r"let params = {struct_name} {{
{ref_instantiations}
}};"
));
top_level_fields.push(("params".into(), format!("{struct_name}<'a>")));
}
if let Some(flags) = &template_config.flags {
let struct_name = format!("{}TemplateFlags", template_config.name.to_pascal_case());
let mut base_struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {struct_name}<'a> {{"
);
let mut ref_instantiations = String::new();
for flag_ref in flags {
special_param_idx += 1;
if idx_set.contains(&flag_ref.idx) {
tracing::warn!(
"duplicate index '{}'. will cause errors at runtime",
flag_ref.idx
);
}
idx_set.insert(flag_ref.idx);
let base_access = format!("root.as_vector().idx({}).as_str()", flag_ref.idx);
base_struct_def = format!("{}\n{}: &'a str,", base_struct_def, flag_ref.name);
ref_instantiations = format!(
"{}\n{}: {},",
ref_instantiations, flag_ref.name, base_access
);
}
base_struct_def = format!("{base_struct_def}\n}}");
structs.push(base_struct_def);
filled_structs.push(format!(
r"let flags = {struct_name} {{
{ref_instantiations}
}};"
));
top_level_fields.push(("flags".into(), format!("{struct_name}<'a>")));
}
if let Some(content_refs) = &template_config.content {
let struct_name = format!("{}TemplateContent", template_config.name.to_pascal_case());
let mut base_struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {struct_name}<'a> {{"
);
let mut content_ref_instantiations = String::new();
for content_ref in content_refs {
special_param_idx += 1;
if idx_set.contains(&content_ref.idx) {
tracing::warn!(
"duplicate index '{}'. will cause errors at runtime",
content_ref.idx
);
}
idx_set.insert(content_ref.idx);
let idx_chain = vec![];
let base_access = format!("root.as_vector().idx({}).as_vector()", content_ref.idx);
if let Some(content_def) = content_def_map.get(&content_ref.name) {
let (struct_name, str_struct_defs, struct_instantiation) =
build_content_struct_for_ref(
&template_config.name,
content_ref,
content_def,
&idx_chain,
&base_access,
);
content_ref_instantiations =
format!("{content_ref_instantiations}\n{struct_instantiation}");
if let Some(name) = &content_ref.all {
base_struct_def =
format!("{base_struct_def}\n {name}: Vec<{struct_name}<'a>>,");
} else {
base_struct_def = format!(
"{}\n {}: {}<'a>,",
base_struct_def, content_ref.name, struct_name
);
}
structs.push(str_struct_defs);
}
}
base_struct_def = format!("{base_struct_def}\n}}");
structs.push(base_struct_def);
filled_structs.push(format!(
r"let content = {struct_name} {{
{content_ref_instantiations}
}};"
));
top_level_fields.push(("content".into(), format!("{struct_name}<'a>")));
}
if let Some(model_refs) = &template_config.models {
let struct_name = format!("{}TemplateModels", template_config.name.to_pascal_case());
let mut base_struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {struct_name}<'a> {{"
);
let mut model_ref_instantiations = String::new();
for model_ref in model_refs {
special_param_idx += 1;
if idx_set.contains(&model_ref.idx) {
tracing::warn!(
"duplicate index '{}'. will cause errors at runtime",
model_ref.idx
);
}
idx_set.insert(model_ref.idx);
let mut is_vec_of = false;
for field in &model_ref.fields {
if let Some(bind) = &field.bind {
let exp = match bind {
TemplateRefFieldBind::Token {
field: _,
expression,
}
| TemplateRefFieldBind::Segment {
name: _,
expression,
} => expression,
};
if exp.is_some() {
is_vec_of = true;
}
}
}
let idx_chain = vec![];
let base_access = format!("root.as_vector().idx({}).as_vector()", model_ref.idx);
if let Some(model_def) = model_config_map.get(&model_ref.name) {
let (struct_name, str_struct_defs, struct_instantiation) =
build_model_struct_for_ref(
&template_config.name,
model_ref,
model_def,
model_config_map,
&idx_chain,
&base_access,
is_vec_of,
);
model_ref_instantiations =
format!("{model_ref_instantiations}\n{struct_instantiation}");
if is_vec_of {
base_struct_def = format!(
"{}\n {}s: Vec<{}<'a>>,",
base_struct_def, model_ref.name, struct_name
);
} else {
base_struct_def = format!(
"{}\n {}: {}<'a>,",
base_struct_def, model_ref.name, struct_name
);
}
structs.push(str_struct_defs);
}
}
base_struct_def = format!("{base_struct_def}\n}}");
structs.push(base_struct_def);
filled_structs.push(format!(
r"let models = {struct_name} {{
{model_ref_instantiations}
}};"
));
top_level_fields.push(("models".into(), format!("{struct_name}<'a>")));
}
if let Some(error_config) = error_config
&& let Some(template_name) = &error_config.template
&& &template_config.name == template_name
{
let struct_name = format!("{}TemplateError", template_name.to_pascal_case());
let mut base_struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {struct_name}<'a> {{"
);
if idx_set.contains(&special_param_idx) {
tracing::warn!(
"duplicate index '{}'. will cause errors at runtime",
special_param_idx
);
}
idx_set.insert(special_param_idx);
let mut ref_instantiations = String::new();
let base_access = format!("root.as_vector().idx({special_param_idx}).as_vector()");
base_struct_def = format!("{base_struct_def}\nmessage: &'a str,");
base_struct_def = format!("{base_struct_def}\ncode: u16,");
ref_instantiations =
format!("{ref_instantiations}\nmessage: {base_access}.idx(0).as_str(),");
ref_instantiations = format!("{ref_instantiations}\ncode: {base_access}.idx(1).as_u16(),");
base_struct_def = format!("{base_struct_def}\n}}");
structs.push(base_struct_def);
filled_structs.push(format!(
r"let error = {struct_name} {{
{ref_instantiations}
}};"
));
top_level_fields.push(("error".into(), format!("{struct_name}<'a>")));
special_param_idx += 1;
}
if let Some(auth_config) = auth_config
&& let Some(totp_template) = &auth_config.mfa.totp.template
&& totp_template == &template_config.name
{
let struct_name = format!("{}TemplateTotp", template_config.name.to_pascal_case());
let mut base_struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {struct_name}<'a> {{"
);
if idx_set.contains(&special_param_idx) {
tracing::warn!(
"duplicate index '{}'. will cause errors at runtime",
special_param_idx
);
}
idx_set.insert(special_param_idx);
let mut ref_instantiations = String::new();
let base_access = format!("root.as_vector().idx({special_param_idx}).as_vector()");
base_struct_def = format!("{base_struct_def}\nqr_code: &'a str,");
base_struct_def = format!("{base_struct_def}\naccount: &'a str,");
base_struct_def = format!("{base_struct_def}\nrecovery_codes: Vec<&'a str>,");
ref_instantiations =
format!("{ref_instantiations}\nqr_code: {base_access}.idx(0).as_str(),");
ref_instantiations =
format!("{ref_instantiations}\naccount: {base_access}.idx(1).as_str(),");
ref_instantiations = format!(
"{ref_instantiations}\nrecovery_codes: {base_access}.idx(2).as_vector().iter().map(|v| v.as_str()).collect(),"
);
base_struct_def = format!("{base_struct_def}\n}}");
structs.push(base_struct_def);
filled_structs.push(format!(
r"let totp = {struct_name} {{
{ref_instantiations}
}};"
));
top_level_fields.push(("totp".into(), format!("{struct_name}<'a>")));
special_param_idx += 1;
}
if idx_set.contains(&special_param_idx) {
tracing::warn!(
"duplicate index '{}'. will cause errors at runtime",
special_param_idx
);
}
idx_set.insert(special_param_idx);
let host_access = format!("root.as_vector().idx({special_param_idx}).as_str()");
special_param_idx += 1;
let mut top_level_interior =
"\n domain: &'a str,\n version: &'a str,\n canonical: &'a str,\n generator: &'a str,\n host: &'a str,"
.to_string();
let mut guts = format!(
" domain: \"{domain}\",\n version: \"{version}\",\n canonical: \"{canonical}\",\n generator: \"{generator}\",\n host: {host_access},"
);
if let Some(globals) = template_config.globals {
let prefix = format!("{}TemplateGlobals", template_config.name.to_pascal_case());
top_level_fields.push(("globals".to_string(), format!("{prefix}<'a>")));
let mut str_struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {}<'a> {{",
&prefix
);
let mut str_struct_fill = format!(
r"
let globals_bytes = root.as_vector().idx({special_param_idx}).as_blob().0;
let globals_vec = flexbuffers::Reader::get_root(globals_bytes).unwrap().as_vector();
let globals = {} {{",
&prefix
);
special_param_idx += 1;
for (i, global_ref) in globals.iter().enumerate() {
if let Some(global) = global_vars.get(global_ref)
&& let Some(accessor) = global.kind.to_flexbuffer_accessor()
{
let (str_type, _) = global.kind.to_stringified_type_w_lifetime("", "a", false);
str_struct_def = format!("{str_struct_def}\n {}: {},", global.name, str_type);
str_struct_fill = format!(
"{str_struct_fill}\n {}: globals_vec.idx({}){},",
global.name, i, accessor
);
}
}
str_struct_def = format!("{str_struct_def}\n}}");
str_struct_fill = format!("{str_struct_fill}\n }};");
structs.push(str_struct_def);
filled_structs.push(str_struct_fill);
}
if let Some(fields) = template_config.fields {
let prefix = format!("{}TemplateFields", template_config.name.to_pascal_case());
top_level_fields.push(("template".to_string(), format!("{prefix}<'a>")));
let mut str_struct_def = format!(
r"#[derive(Serialize, Clone)]
struct {}<'a> {{",
&prefix
);
let mut str_struct_fill = format!(
r"
let template_bytes = root.as_vector().idx({special_param_idx}).as_blob().0;
let template_vec = flexbuffers::Reader::get_root(template_bytes).unwrap().as_vector();
let template = {} {{",
&prefix
);
for (i, field) in fields.iter().enumerate() {
if let Some(accessor) = field.kind.to_flexbuffer_accessor() {
let (str_type, _) = field.kind.to_stringified_type_w_lifetime("", "a", false);
str_struct_def = format!("{str_struct_def}\n {}: {},", field.name, str_type);
str_struct_fill = format!(
"{str_struct_fill}\n {}: template_vec.idx({}){},",
field.name, i, accessor
);
}
}
str_struct_def = format!("{str_struct_def}\n}}");
str_struct_fill = format!("{str_struct_fill}\n }};");
structs.push(str_struct_def);
filled_structs.push(str_struct_fill);
}
let mut str_structs = String::new();
let mut str_filled_structs = String::new();
for str_struct in structs {
str_structs = format!("{str_structs}{str_struct}\n");
}
for str_filled_struct in filled_structs {
str_filled_structs = format!(" {str_filled_structs}{str_filled_struct}\n");
}
for (field_name, struct_name) in top_level_fields {
top_level_interior = format!("{top_level_interior} {field_name}: {struct_name},\n");
guts = format!("{guts} {field_name},\n");
}
let server = format!(
r#"{}
{}
#[derive(Template)]
#[template(path = "{}")]
struct {}Template<'a> {{{}}}
fn main() -> Result<(), Box<dyn Error>> {{
let args = recv_in()?;
let root = flexbuffers::Reader::get_root(args.unwrap_or(EMPTY))?;
{}
let mut rendered = Vec::new();
{}Template {{
{}
}}.write_into(&mut rendered)?;
send_out(Some(rendered))
}}
"#,
BASE_SERVER,
str_structs,
file_name,
&template_config.name.to_pascal_case(),
&top_level_interior,
&str_filled_structs,
&template_config.name.to_pascal_case(),
&guts,
);
let client = format!(
r#"{}
#[derive(Template)]
#[template(path = "{}")]
struct {}Template<'a> {{{}}}
#[wasm_bindgen]
pub fn render_{}(args: Option<Vec<u8>>) -> Result<String, String> {{
let root = match args {{
Some(args) => args,
None => {{
let root = flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
root.view().to_vec()
}}
}};
match flexbuffers::Reader::get_root(&root[..]) {{
Ok(root) => {{
{}
match ({}Template {{
{}
}}).render() {{
Ok(res) => Ok(res),
Err(err) => Err(err.to_string())
}}
}}
Err(err) => Err(err.to_string())
}}
}}
"#,
str_structs,
file_name,
&template_config.name.to_pascal_case(),
&top_level_interior,
&template_config.name.to_snake_case(),
&str_filled_structs,
&template_config.name.to_pascal_case(),
&guts,
);
(server, client)
}