cedar_policy_formatter/pprint/
fmt.rsuse std::collections::BTreeMap;
use miette::{miette, Result, WrapErr};
use cedar_policy_core::ast::PolicySet;
use cedar_policy_core::parser::parse_policyset;
use cedar_policy_core::parser::text_to_cst::parse_policies;
use smol_str::ToSmolStr;
use super::lexer::get_token_stream;
use super::utils::remove_empty_lines;
use super::config::{self, Config};
use super::doc::*;
fn tree_to_pretty<T: Doc>(t: &T, context: &mut config::Context<'_, '_>) -> Result<String> {
let mut w = Vec::new();
let config = context.config;
let doc = t.to_doc(context);
doc.ok_or(miette!("failed to produce doc"))?
.render(config.line_width, &mut w)
.map_err(|err| miette!(format!("failed to render doc: {err}")))?;
String::from_utf8(w)
.map_err(|err| miette!(format!("failed to convert rendered doc to string: {err}")))
}
fn soundness_check(ps: &str, ast: &PolicySet) -> Result<()> {
let formatted_ast =
parse_policyset(ps).wrap_err(format!("formatter produced an invalid policy set:\n{ps}"))?;
let (formatted_policies, policies) = (
formatted_ast
.policies()
.map(|p| (p.id().to_smolstr(), p))
.collect::<BTreeMap<_, _>>(),
ast.policies()
.map(|p| (p.id().to_smolstr(), p))
.collect::<BTreeMap<_, _>>(),
);
if formatted_policies.len() != policies.len() {
return Err(miette!(
"formatter changed the number of policies from {} to {}",
policies.len(),
formatted_policies.len()
));
}
for ((f_p_id, f_p), (p_id, p)) in formatted_policies.into_iter().zip(policies.into_iter()) {
if f_p_id != p_id {
return Err(miette!(
"formatter changed the policy id from {p_id} to {f_p_id}"
));
}
let (f_anno, anno) = (
f_p.annotations()
.map(|(k, v)| (k, &v.val))
.collect::<std::collections::BTreeMap<_, _>>(),
p.annotations()
.map(|(k, v)| (k, &v.val))
.collect::<std::collections::BTreeMap<_, _>>(),
);
if f_anno != anno {
return Err(miette!(
"formatter changed the annotations from {anno:?} to {f_anno:?}"
));
}
if !(f_p.effect() == p.effect()
&& f_p.principal_constraint() == p.principal_constraint()
&& f_p.action_constraint() == p.action_constraint()
&& f_p.resource_constraint() == p.resource_constraint()
&& f_p
.non_scope_constraints()
.eq_shape(p.non_scope_constraints()))
{
return Err(miette!(
"formatter changed the policy structure:\noriginal:\n{p}\nformatted:\n{f_p}"
));
}
}
Ok(())
}
pub fn policies_str_to_pretty(ps: &str, config: &Config) -> Result<String> {
let cst = parse_policies(ps).wrap_err("cannot parse input policies")?;
let ast = cst.to_policyset().wrap_err("cannot parse input policies")?;
let (tokens, end_of_file_comment) =
get_token_stream(ps).ok_or(miette!("cannot get token stream"))?;
let mut context = config::Context { config, tokens };
let mut formatted_policies = cst
.as_inner()
.ok_or(miette!("fail to get input policy CST"))?
.0
.iter()
.map(|p| Ok(remove_empty_lines(&tree_to_pretty(p, &mut context)?)))
.collect::<Result<Vec<String>>>()?
.join("\n\n");
formatted_policies.push('\n');
for comment_line in end_of_file_comment {
formatted_policies.push_str(comment_line);
formatted_policies.push('\n');
}
soundness_check(&formatted_policies, &ast).wrap_err(
"internal error: please file an issue at <https://github.com/cedar-policy/cedar/issues>",
)?;
Ok(formatted_policies)
}
#[cfg(test)]
mod tests {
use insta::{assert_snapshot, glob, with_settings};
use std::fs;
use super::*;
#[test]
fn test_soundness_check() {
let p1 = r#"permit (principal, action, resource)
when { "
a
" };"#;
let p2 = r#"permit (principal, action, resource)
when { "
a
" };"#;
assert!(soundness_check(p2, &parse_policyset(p1).unwrap()).is_err());
let p1 = r#"
permit (principal, action, resource)
when { "a"};
permit (principal, action, resource)
when { "
a
" };"#;
let p2 = r#"
permit (principal, action, resource)
when { "
a
" };
permit (principal, action, resource)
when { "a"};"#;
assert!(soundness_check(p2, &parse_policyset(p1).unwrap()).is_err());
let p1 = r#"
permit (principal, action, resource)
when { "a" };
permit (principal, action, resource)
when { "b" };"#;
let p2 = r#"
permit (principal, action, resource)
when { "a" };
permit (principal, action, resource)
when { "b"};"#;
assert!(soundness_check(p2, &parse_policyset(p1).unwrap()).is_ok());
}
#[test]
fn test_add_trailing_newline() {
let config = Config {
line_width: 80,
indent_width: 2,
};
let formatted_p = "permit (principal, action, resource);\n";
let p1 = "permit (principal, action, resource);";
let p2 = "permit (principal, action, resource);\r\n";
let p3 = "permit (principal, action, resource);\n\r\n\n";
assert_eq!(
policies_str_to_pretty(formatted_p, &config).unwrap(),
formatted_p
);
assert_eq!(policies_str_to_pretty(p1, &config).unwrap(), formatted_p);
assert_eq!(policies_str_to_pretty(p2, &config).unwrap(), formatted_p);
assert_eq!(policies_str_to_pretty(p3, &config).unwrap(), formatted_p);
let formatted_p = "permit (principal, action, resource);\n//foo\n";
let p1 = "permit (principal, action, resource);\n//foo";
let p2 = "permit (principal, action, resource);\n//foo\n\n\n";
assert_eq!(
policies_str_to_pretty(formatted_p, &config).unwrap(),
formatted_p
);
assert_eq!(policies_str_to_pretty(p1, &config).unwrap(), formatted_p);
assert_eq!(policies_str_to_pretty(p2, &config).unwrap(), formatted_p);
let formatted_p = "permit (principal, action, resource);\n//foo\n//bar\n";
let p1 = "permit (principal, action, resource);\n//foo\n//bar";
let p2 = "permit (principal, action, resource);\n//foo\n//bar ";
let p3 = "permit (principal, action, resource);\n//foo\n//bar\n\n\n";
let p4 = "permit (principal, action, resource);\n//foo\n//bar \n\n\n";
let p5 = "permit (principal, action, resource);\n//foo\n//bar \n \n \n";
assert_eq!(
policies_str_to_pretty(formatted_p, &config).unwrap(),
formatted_p
);
assert_eq!(policies_str_to_pretty(p1, &config).unwrap(), formatted_p);
assert_eq!(policies_str_to_pretty(p2, &config).unwrap(), formatted_p);
assert_eq!(policies_str_to_pretty(p3, &config).unwrap(), formatted_p);
assert_eq!(policies_str_to_pretty(p4, &config).unwrap(), formatted_p);
assert_eq!(policies_str_to_pretty(p5, &config).unwrap(), formatted_p);
}
#[test]
fn test_format_files() {
let config = Config {
line_width: 80,
indent_width: 2,
};
with_settings!(
{ snapshot_path => "../../tests/snapshots/" },
{
glob!("../../tests", "*.cedar", |path| {
let cedar_source = fs::read_to_string(path).unwrap();
let formatted = policies_str_to_pretty(&cedar_source, &config).unwrap();
assert_snapshot!(formatted);
});
}
);
with_settings!(
{ snapshot_path => "../../tests/cli-snapshots/" },
{
glob!("../../../cedar-policy-cli/sample-data", "**/*.cedar", |path| {
let cedar_source = fs::read_to_string(path).unwrap();
let formatted = policies_str_to_pretty(&cedar_source, &config).unwrap();
assert_snapshot!(formatted);
});
}
)
}
}