use crate::{OrdinaryConfig, TemplateConfig, TemplateRef, TemplateRefField};
use anyhow::bail;
use hashbrown::{HashMap, HashSet};
use ordinary_types::{Field, Kind, json_to_flexbuffer_vec};
use regex::Regex;
use tracing::Span;
use url::Url;
struct NestedFields(HashMap<String, Option<NestedFields>>);
pub static DOMAIN_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$")
.expect("failed to create regex")
});
#[allow(clippy::too_many_lines)]
pub fn validate(config: &OrdinaryConfig) -> anyhow::Result<()> {
let port = config.port.unwrap_or(433);
let redirect_port = config.redirect_port.unwrap_or(80);
if port == redirect_port {
bail!("port and redirect_port cannot be same value {port}");
}
if !DOMAIN_REGEX.is_match(&config.domain) {
bail!("'domain' field is not a valid domain name");
}
if let Some(cnames) = &config.cnames {
if cnames.contains(&config.domain) {
bail!("CNAMEs cannot include the primary domain");
}
for cname in cnames {
if !DOMAIN_REGEX.is_match(cname) {
bail!("cname '{cname}' is not valid domain name");
}
}
}
if let Some(redirects) = &config.redirects {
if let Some(host_redirects) = &redirects.host {
let mut from_set = HashSet::new();
let mut to_set = HashSet::new();
let mut host_set = HashSet::new();
host_set.insert(config.domain.clone());
if let Some(cnames) = &config.cnames {
for cname in cnames {
host_set.insert(cname.clone());
}
}
for host_redirect in host_redirects {
if !DOMAIN_REGEX.is_match(&host_redirect.from) {
bail!(
"host redirect from '{}' is not valid domain name",
host_redirect.from
);
}
if !DOMAIN_REGEX.is_match(&host_redirect.to) {
bail!(
"host redirect to '{}' is not valid domain name",
host_redirect.to
);
}
if !host_set.contains(&host_redirect.from) {
bail!(
"'{}' for from is not in 'cnames' or the 'domain' field",
host_redirect.from
);
}
if !host_set.contains(&host_redirect.to) {
bail!(
"'{}' for to is not in 'cnames' or the 'domain' field",
host_redirect.from
);
}
if from_set.contains(&host_redirect.from) {
bail!(
"cannot have multiple 'from' host redirect rules for a single host {}",
host_redirect.from
);
}
from_set.insert(host_redirect.from.clone());
if to_set.contains(&host_redirect.from) {
bail!(
"cannot have chained to/from redirects '{}'",
host_redirect.from
);
}
if from_set.contains(&host_redirect.to) {
bail!(
"cannot have chained to/from redirects '{}'",
host_redirect.to
);
}
to_set.insert(host_redirect.to.clone());
}
}
if let Some(route_redirects) = &redirects.route {
for route_redirect in route_redirects {
if !route_redirect.condition.starts_with('/') {
bail!(
"route redirect condition {} must start with a leading forward slash",
route_redirect.condition
);
}
}
}
}
if let Some(proxies) = &config.proxies {
for proxy in proxies {
if proxy.path.is_none() && proxy.domain.is_none() && proxy.port.is_none() {
bail!(
"neither the proxy `path` nor the proxy `domain` nor the proxy `port` is set for target '{}'",
proxy.target
);
}
if let Some(proxy_port) = &proxy.port {
if proxy_port == &port {
bail!(
"proxy port {proxy_port} for target {} cannot be the same as primary port",
proxy.target
)
}
if proxy_port == &redirect_port {
bail!(
"proxy port {proxy_port} for target {} cannot be the same as redirect_port",
proxy.target
)
}
}
if proxy.path.is_some() && proxy.domain.is_some() {
tracing::warn!(
"`path` {} and `domain` {} should not be set at the same time, only one or the other should be picked; if both are set, only the `path` will be evaluated",
proxy.path.as_ref().unwrap_or(&String::new()),
proxy.domain.as_ref().unwrap_or(&String::new()),
);
}
if let Some(custom_domain) = &proxy.domain {
if !DOMAIN_REGEX.is_match(custom_domain) {
bail!("proxy domain '{custom_domain}' is not valid domain name");
}
if custom_domain == &config.domain {
bail!(
"proxy domain {custom_domain} for target {} is the same as the primary app domain.",
proxy.target
);
}
if let Some(cnames) = &config.cnames
&& cnames.contains(custom_domain)
{
bail!(
"proxy domain {custom_domain} for target {} contained in CNAMEs {cnames:?}",
proxy.target
);
}
let port_str = format!(":{port}");
let redirect_port_str = format!(":{redirect_port}");
if (custom_domain.starts_with("localhost")
|| custom_domain.starts_with("127.0.0.1"))
&& (custom_domain.contains(&port_str)
|| custom_domain.contains(&redirect_port_str))
{
bail!(
"loopback domain {custom_domain} port for target {} collide with primary {port_str} or redirect {redirect_port_str} ports",
proxy.target
);
}
}
if let Some(path) = &proxy.path
&& !path.ends_with("/{*path}")
{
bail!(
"path for target '{}' must end in `/{{*path}}`",
proxy.target
);
}
Url::parse(&proxy.target)?;
}
}
if let Some(assets) = &config.assets {
if !assets.base_route.starts_with('/') {
bail!("assets base route must start with leading forward slash");
}
if assets.append_index_html == Some(true) && assets.append_html_ext == Some(true) {
tracing::warn!(
"'assets.append_index_html' and 'assets.append_index_ext' will conflict at runtime with 'assets.append_index_html' taking precedence. only one or the other should be selected."
);
}
}
if let Some(globals) = &config.globals {
if globals.is_empty() {
tracing::error!("empty 'globals' serves no purpose");
bail!("empty 'globals' serves no purpose");
}
let mut set = HashSet::new();
let mut builder = flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
let mut builder_vec = builder.start_vector();
for global in globals {
match global.kind {
Kind::List { .. } | Kind::Object { .. } => {
tracing::error!(name = %global.name, "cannot include nested structures in global");
bail!(
"cannot include nested structures in global '{}'",
global.name
);
}
_ => (),
}
json_to_flexbuffer_vec(&global.kind, &global.value, &mut builder_vec)?;
if set.contains(&global.name) {
tracing::error!(name = %global.name, "globals cannot have duplicated names");
bail!("globals cannot have duplicated names '{}'", global.name);
}
set.insert(global.name.clone());
}
}
if let Some(content) = &config.content {
if content.definitions.is_empty() {
tracing::error!("empty 'content.definitions' serves no purpose");
bail!("empty 'content.definitions' serves no purpose");
}
let span = tracing::debug_span!(
"content_def",
name = tracing::field::Empty,
idx = tracing::field::Empty
);
let name_idx_fields_pairs = content
.definitions
.iter()
.map(|cd| (cd.name.clone(), cd.idx, Some(cd.fields.clone())))
.collect::<Vec<(String, u8, Option<Vec<Field>>)>>();
check_defs("content definition", &span, &name_idx_fields_pairs)?;
}
if let Some(templates) = &config.templates {
if templates.is_empty() {
tracing::error!("empty 'templates' serves no purpose");
bail!("empty 'templates' serves no purpose");
}
let span = tracing::debug_span!(
"template",
name = tracing::field::Empty,
idx = tracing::field::Empty
);
let name_idx_fields_pairs = templates
.iter()
.map(|cd| (cd.name.clone(), cd.idx, None))
.collect::<Vec<(String, u8, Option<Vec<Field>>)>>();
check_defs("template", &span, &name_idx_fields_pairs)?;
for template in templates {
let span = tracing::debug_span!("template", name = %template.name, idx = template.idx);
span.in_scope(|| {
if !template.route.starts_with('/') {
tracing::error!(route = %template.route, "route must start with a leading forward slash");
bail!("template: {} ({}) -- route '{}' must start with a leading forward slash", template.name, template.idx, template.route)
}
if let Some(template_globals) = &template.globals {
if template_globals.is_empty() {
tracing::error!("empty 'globals' serves no purpose");
bail!("template: {} ({}) -- empty 'globals' serves no purpose", template.name, template.idx);
}
if let Some(top_globals) = &config.globals {
let top_globals_set = top_globals
.iter()
.map(|g| g.name.clone())
.collect::<HashSet<String>>();
for template_global in template_globals {
if !top_globals_set.contains(template_global) {
tracing::error!(global = template_global, options = ?top_globals_set, "does not exist");
bail!("template: {} ({}) -- global '{template_global}' is not defined on the ordinary.json. valid options are: {top_globals_set:?}", template.name, template.idx);
}
}
} else {
tracing::error!("no globals defined in the ordinary.json");
bail!("template: {} ({}) -- no globals defined in the ordinary.json", template.name, template.idx);
}
}
if let Some(template_fields) = &template.fields {
if template_fields.is_empty() {
tracing::error!("empty 'fields' serves no purpose");
bail!("template: {} ({}) -- empty 'fields' serves no purpose", template.name, template.idx);
}
let mut set = HashSet::new();
let mut builder = flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
let mut builder_vec = builder.start_vector();
for field in template_fields {
match field.kind {
Kind::List { .. } | Kind::Object { .. } => {
tracing::error!(name = %field.name, "cannot include nested structures in template field");
bail!("template: {} ({}) -- cannot include nested structures in template field '{}'", template.name, template.idx, field.name);
},
_ => ()
}
json_to_flexbuffer_vec(&field.kind, &field.value, &mut builder_vec)?;
if set.contains(&field.name) {
tracing::error!(name = %field.name, "fields cannot have duplicated names");
bail!("template: {} ({}) -- fields cannot have duplicated names '{}'", template.name, template.idx, field.name);
}
set.insert(field.name.clone());
}
}
if let Some(template_content) = &template.content {
if template_content.is_empty() {
tracing::error!("empty 'content' serves no purpose");
bail!("template: {} ({}) -- empty 'content' serves no purpose", template.name, template.idx);
}
let name_idx_fields_pairs = template_content
.iter()
.map(|cd| (cd.name.clone(), cd.idx, cd.fields.clone()))
.collect::<Vec<(String, u8, Vec<TemplateRefField>)>>();
check_refs(&format!("'{}' template content", template.name), &span, &name_idx_fields_pairs)?;
if let Some(content) = &config.content {
let mut content_map: HashMap<String, NestedFields> = HashMap::new();
for content_def in &content.definitions {
let fields = content_def_field_builder(&content_def.fields);
content_map.insert(content_def.name.clone(), fields);
}
for template_ref in template_content {
if let Some(content_fields) = content_map.get(&template_ref.name) {
template_ref_field_checker(content_fields, &template_ref.fields, template_ref, template, &[])?;
} else {
tracing::error!(content_def = %template_ref.name, options = ?content_map.keys(), "does not exist");
bail!("template: {} ({}) -- content_def '{}' is not defined on the ordinary.json. valid options are: {:?}", template.name, template.idx, template_ref.name, content_map.keys());
}
}
} else {
tracing::error!("no content defined in the ordinary.json");
bail!("template: {} ({}) -- no content defined in the ordinary.json", template.name, template.idx);
}
}
Ok(())
})?;
}
}
Ok(())
}
fn check_defs(
top_name: &str,
span: &Span,
name_idx_fields_pairs: &Vec<(String, u8, Option<Vec<Field>>)>,
) -> anyhow::Result<()> {
let mut def_idx = vec![];
let mut def_names = HashMap::new();
for (name, idx, fields) in name_idx_fields_pairs {
span.record("name", tracing::field::display(name));
span.record("idx", idx);
span.in_scope(|| {
if let Some(collision_idx) = def_names.get(name) {
tracing::error!(
collision = collision_idx,
"cannot duplicate names"
);
bail!(
"cannot duplicate {top_name} names '{name}' for idx {collision_idx} and {idx}",
);
}
def_idx.push(idx);
def_names.insert(name.clone(), idx);
if let Some(fields) = fields {
if fields.is_empty() {
tracing::error!("empty 'fields' serves no purpose");
bail!("empty 'fields' serves no purpose on {top_name}");
}
let mut field_idx = vec![];
let mut field_names = HashMap::new();
for field in fields {
let span = tracing::debug_span!("field", name = %field.name, idx = field.idx);
span.in_scope(|| {
if let Some(collision_idx) = field_names.get(&field.name) {
tracing::error!(
collision = collision_idx,
name = field.name,
"cannot duplicate field names"
);
bail!(
"cannot duplicate '{name}' field names '{}' for idx {collision_idx} and {} on {top_name}",
field.name,
field.idx,
);
}
field_idx.push(field.idx);
field_names.insert(field.name.clone(), field.idx);
Ok(())
})?;
}
field_idx.sort_unstable();
for (i, idx) in field_idx.iter().enumerate() {
if i != *idx as usize {
tracing::error!("field idx must start at 0 and cannot have gaps");
bail!("'{name}' field idx must start at 0 and cannot have gaps on {top_name}")
}
}
}
Ok(())
})?;
def_idx.sort_unstable();
for (i, idx) in def_idx.iter().enumerate() {
if i != **idx as usize {
tracing::error!("{top_name} idx must start at 0 and cannot have gaps");
bail!("{top_name} idx must start at 0 and cannot have gaps");
}
}
}
Ok(())
}
fn check_refs(
top_name: &str,
span: &Span,
name_idx_fields_pairs: &Vec<(String, u8, Vec<TemplateRefField>)>,
) -> anyhow::Result<()> {
let mut def_idx = vec![];
let mut def_names = HashMap::new();
for (name, idx, fields) in name_idx_fields_pairs {
span.record("name", tracing::field::display(name));
span.record("idx", idx);
span.in_scope(|| {
if fields.is_empty() {
tracing::error!(content_def = %name, "empty 'fields' serves no purpose");
bail!("empty content '{name}' 'fields' serves no purpose on {top_name}");
}
if let Some(collision_idx) = def_names.get(name) {
tracing::error!(
collision = collision_idx,
"cannot duplicate names"
);
bail!(
"cannot duplicate {top_name} names '{name}' for idx {collision_idx} and {idx}",
);
}
def_idx.push(idx);
def_names.insert(name.clone(), idx);
let mut field_idx = vec![];
let mut field_names = HashMap::new();
for field in fields {
let span = tracing::debug_span!("field", name = %field.name, idx = field.idx);
span.in_scope(|| {
if let Some(collision_idx) = field_names.get(&field.name) {
tracing::error!(
collision = collision_idx,
name = field.name,
"cannot duplicate field names"
);
bail!(
"cannot duplicate '{name}' field names '{}' for idx {collision_idx} and {} on {top_name}",
field.name,
field.idx,
);
}
field_idx.push(field.idx);
field_names.insert(field.name.clone(), field.idx);
Ok(())
})?;
}
field_idx.sort_unstable();
for (i, idx) in field_idx.iter().enumerate() {
if i != *idx as usize {
tracing::error!("field idx must start at 0 and cannot have gaps");
bail!("'{name}' field idx must start at 0 and cannot have gaps on {top_name}");
}
}
Ok(())
})?;
def_idx.sort_unstable();
for (i, idx) in def_idx.iter().enumerate() {
if i != **idx as usize {
tracing::error!("{top_name} idx must start at 0 and cannot have gaps");
bail!("{top_name} idx must start at 0 and cannot have gaps");
}
}
}
Ok(())
}
fn content_def_field_builder(fields: &Vec<Field>) -> NestedFields {
let mut map = NestedFields(HashMap::new());
for field in fields {
match &field.kind {
Kind::Object { fields, .. } => map
.0
.insert(field.name.clone(), Some(content_def_field_builder(fields))),
Kind::List { kind } => match &**kind {
Kind::Object { fields, .. } => map
.0
.insert(field.name.clone(), Some(content_def_field_builder(fields))),
_ => map.0.insert(field.name.clone(), None),
},
_ => map.0.insert(field.name.clone(), None),
};
}
map
}
fn template_ref_field_checker(
content_fields: &NestedFields,
template_fields: &Vec<TemplateRefField>,
template_ref: &TemplateRef,
template: &TemplateConfig,
field_chain: &[String],
) -> anyhow::Result<()> {
if template_fields.is_empty() {
tracing::error!(field = %field_chain.join("."), content_def = %template_ref.name, "empty 'fields' serves no purpose");
bail!(
"template: {} ({}) -- empty 'fields' serves no purpose on {}",
template.name,
template.idx,
template_ref.name
);
}
for template_field in template_fields {
let mut field_chain = field_chain.to_vec();
field_chain.push(template_field.name.clone());
if let Some(nested_fields) = content_fields.0.get(&template_field.name) {
if let Some(nested_fields) = nested_fields {
if let Some(template_fields) = &template_field.fields {
template_ref_field_checker(
nested_fields,
template_fields,
template_ref,
template,
&field_chain,
)?;
} else {
tracing::error!(field = %field_chain.join("."), content_def = %template_ref.name, options = ?nested_fields.0.keys(), "no nested fields provided");
bail!(
"template: {} ({}) -- field '{}' does not list any nested fields '{}'. include some of the following: {:?}",
template.name,
template.idx,
field_chain.join("."),
template_ref.name,
nested_fields.0.keys()
);
}
} else if !content_fields.0.contains_key(&template_field.name) {
tracing::error!(field = %field_chain.join("."), content_def = %template_ref.name, options = ?content_fields.0.keys(), "does not exist");
bail!(
"template: {} ({}) -- field '{}' is not defined on the content_def '{}'. valid options are: {:?}",
template.name,
template.idx,
field_chain.join("."),
template_ref.name,
content_fields.0.keys()
);
}
} else {
tracing::error!(field = %field_chain.join("."), content_def = %template_ref.name, options = ?content_fields.0.keys(), "does not exist");
bail!(
"template: {} ({}) -- field '{}' is not defined on the content_def '{}'. valid options are: {:?}",
template.name,
template.idx,
field_chain.join("."),
template_ref.name,
content_fields.0.keys()
);
}
}
Ok(())
}