use std::collections::HashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use oso::{Class, FromPolar, Oso, OsoError, PolarClass, PolarValue};
use polar_core::error as polar_error;
use maplit::hashmap;
mod common;
use common::OsoTest;
fn test_file_path() -> PathBuf {
let path = Path::new(env!("CARGO_MANIFEST_DIR"));
path.join(Path::new("tests/test_file.polar"))
}
fn test_file_other_path() -> PathBuf {
let path = Path::new(env!("CARGO_MANIFEST_DIR"));
path.join(Path::new("tests/other/test_file.polar"))
}
fn test_file_gx_path() -> PathBuf {
let path = Path::new(env!("CARGO_MANIFEST_DIR"));
path.join(Path::new("tests/test_file_gx.polar"))
}
#[derive(PolarClass, Debug, Clone, PartialEq)]
struct Widget {
#[polar(attribute)]
id: i64,
}
impl Widget {
pub fn new(id: i64) -> Self {
Self { id }
}
fn polar_class() -> Class {
Widget::get_polar_class_builder()
.name("Widget")
.set_constructor(Self::new)
.build()
}
}
#[derive(PolarClass, Debug, Clone, PartialEq)]
struct User {
#[polar(attribute)]
name: String,
}
impl User {
pub fn new(name: String) -> Self {
Self { name }
}
pub fn widget(&self) -> Widget {
Widget::new(1)
}
#[allow(dead_code)]
pub fn widgets() {
todo!("Iterator returning multiple choices not yet implemented.");
}
fn polar_class() -> Class {
User::get_polar_class_builder()
.name("User")
.add_method("widget", User::widget)
.build()
}
}
fn test_oso() -> OsoTest {
let mut test = OsoTest::new();
test.oso.register_class(Widget::polar_class()).unwrap();
test.oso.register_class(User::polar_class()).unwrap();
test
}
#[test]
fn test_anything_works() -> oso::Result<()> {
common::setup();
let mut oso = Oso::new();
oso.load_str("f(1);")?;
let mut query = oso.query("f(x)")?;
let next = query.next().unwrap()?;
let x: i64 = next.get_typed("x")?;
assert_eq!(x, 1);
assert_eq!(
next.keys().map(&str::to_owned).collect::<Vec<_>>(),
vec!["x"]
);
Ok(())
}
#[test]
fn test_data_conversions_polar_values() -> oso::Result<()> {
common::setup();
let mut test_oso = OsoTest::new();
test_oso.load_str(r#"f({x: [1, "two", true], y: {z: false}});"#);
let mut query = test_oso.oso.query("f(x)")?;
let x: HashMap<String, PolarValue> = query.next().unwrap()?.get_typed("x")?;
let v_x = x.get("x").unwrap();
if let PolarValue::List(x_vec) = v_x {
assert_eq!(i64::from_polar(x_vec.get(0).unwrap().to_owned())?, 1);
assert_eq!(
String::from_polar(x_vec.get(1).unwrap().to_owned())?,
String::from("two")
);
assert!(bool::from_polar(x_vec.get(2).unwrap().to_owned())?);
} else {
panic!("x not list.");
}
let v_y = x.get("y").unwrap();
let y: HashMap<String, bool> = HashMap::<String, bool>::from_polar(v_y.to_owned())?;
assert_eq!(y, hashmap! {String::from("z") => false});
Ok(())
}
#[ignore]
#[test]
fn test_data_conversions_predicates() -> oso::Result<()> {
common::setup();
let mut test_oso = OsoTest::new();
test_oso.load_str("f(x) if pred(1, 2);");
todo!("No predicate in API");
}
#[test]
fn test_data_conversions_instances() {
}
#[test]
fn test_data_conversions_externals() -> oso::Result<()> {
common::setup();
let mut oso = test_oso();
let actor = User::new(String::from("sam"));
let widget = Widget::new(1);
oso.load_str("allow(actor, _action, resource) if actor.widget().id = resource.id;");
let query_results = oso
.oso
.query_rule("allow", (actor, "read", widget))?
.count();
assert_eq!(query_results, 1);
Ok(())
}
#[ignore]
#[test]
fn test_data_conversion_iterator_external_calls() {
todo!("Unimplemented");
}
#[ignore]
#[test]
fn test_data_conversions_no_leak() {
todo!("Unimplemented.");
}
#[test]
fn test_load_file_error_contains_filename() {
common::setup();
let mut oso = test_oso();
let mut tempfile = tempfile::Builder::new()
.suffix(".polar")
.tempfile()
.unwrap();
let file = tempfile.as_file_mut();
writeln!(file, ";").unwrap();
file.sync_all().unwrap();
let err = oso.oso.load_files(vec![tempfile.path()]).unwrap_err();
if let OsoError::Polar(err) = err {
assert_eq!(
err.to_string(),
format!(
"did not expect to find the token ';' at line 1, column 1 of file {}:\n\t001: ;\n\t ^\n",
tempfile.path().to_string_lossy().into_owned()
)
);
} else {
panic!("Unexpected error type {:?}", err);
}
}
#[test]
fn test_load_file_extension_check() {
common::setup();
let mut oso = test_oso();
let err = oso.oso.load_files(vec!["not_polar_file.txt"]).unwrap_err();
assert!(
matches!(err, OsoError::IncorrectFileType { filename } if filename == "not_polar_file.txt")
);
}
#[test]
fn test_load_file_nonexistent_file() {
common::setup();
let mut oso = test_oso();
let err = oso.oso.load_files(vec!["not_a_file.polar"]).unwrap_err();
assert!(matches!(err, OsoError::Io(_)));
}
#[test]
fn test_already_loaded_file_error() -> oso::Result<()> {
common::setup();
let mut oso = test_oso();
let path = test_file_path();
let err = oso.oso.load_files(vec![&path, &path]).unwrap_err();
assert!(
matches!(&err, OsoError::Polar(polar_error::PolarError(
polar_error::ErrorKind::Validation(polar_error::ValidationError::FileLoading { .. })
)) if err.to_string().starts_with(&format!("Problem loading file: File {} has already been loaded.", path.to_string_lossy()))),
"Error was {}",
err
);
Ok(())
}
#[test]
fn test_load_multiple_files() -> oso::Result<()> {
common::setup();
let mut oso = test_oso();
let path = test_file_path();
let path_gx = test_file_gx_path();
oso.oso.load_files(vec![path, path_gx])?;
assert_eq!(oso.qvar::<i64>("f(x)", "x"), vec![1, 2, 3]);
assert_eq!(oso.qvar::<i64>("g(x)", "x"), vec![1, 2, 3]);
Ok(())
}
#[test]
fn test_load_multiple_files_same_name_different_path() -> oso::Result<()> {
common::setup();
let mut oso = test_oso();
let path = test_file_path();
let other_path = test_file_other_path();
oso.oso.load_files(vec![path, other_path])?;
assert_eq!(oso.qvar::<i64>("f(x)", "x"), vec![1, 2, 3]);
assert_eq!(oso.qvar::<i64>("g(x)", "x"), vec![1, 2, 3]);
Ok(())
}
#[test]
fn test_clear_rules() -> oso::Result<()> {
common::setup();
let mut oso = test_oso();
oso.oso.load_files(vec![test_file_path()])?;
assert_eq!(oso.qvar::<i64>("f(x)", "x"), vec![1, 2, 3]);
#[derive(PolarClass, Default, Debug, Clone)]
struct Foo;
impl Foo {
pub fn new() -> Self {
Self {}
}
}
let foo_class = Foo::get_polar_class_builder()
.name("Foo")
.set_constructor(Foo::new)
.build();
oso.oso.register_class(foo_class)?;
assert!(matches!(oso.oso.clear_rules(), Ok(())));
let err = oso.query_err("f(x)");
assert!(err.contains("Query for undefined rule `f`"));
assert_eq!(oso.query("x = new Foo()").len(), 1);
Ok(())
}
#[test]
fn test_basic_queries() {
common::setup();
let mut oso = test_oso();
oso.load_str("f(1);");
let results = oso.query("f(1)");
assert_eq!(results.len(), 1);
assert!(results
.get(0)
.map(|r| r.keys().next().is_none())
.unwrap_or_default());
}
#[test]
fn test_register_constant() -> oso::Result<()> {
common::setup();
let mut oso = test_oso();
let d = hashmap! {String::from("a") => 1};
oso.oso.register_constant(d, "d")?;
assert_eq!(oso.qvar::<i64>("d.a = x", "x"), vec![1]);
Ok(())
}
#[ignore]
#[test]
fn test_host_method_string() {
todo!();
}
#[ignore]
#[test]
fn test_host_method_integer() {
todo!();
}
#[ignore]
#[test]
fn test_host_method_float() {
todo!();
}
#[ignore]
#[test]
fn test_host_method_list() {
todo!();
}
#[ignore]
#[test]
fn test_host_method_dict() {
todo!();
}
#[test]
fn test_duplicate_register_class() -> oso::Result<()> {
common::setup();
let mut oso = test_oso();
#[derive(PolarClass, Default, Debug, Clone)]
struct Foo {}
let foo_class = Foo::get_polar_class_builder().name("Foo").build();
oso.oso.register_class(foo_class.clone())?;
let err = oso.oso.register_class(foo_class).unwrap_err();
assert!(matches!(err, OsoError::DuplicateClassError { name } if &name == "Foo"));
Ok(())
}
#[test]
fn test_register_class() -> oso::Result<()> {
common::setup();
let mut oso = test_oso();
#[derive(PolarClass, Default, Debug, Clone)]
struct Bar;
impl Bar {
pub fn y(&self) -> &'static str {
"y"
}
}
let bar_class = Bar::get_polar_class_builder()
.name("Bar")
.add_method("y", Bar::y)
.build();
#[derive(PolarClass, Debug, Clone)]
struct Foo {
#[polar(attribute)]
a: String,
}
impl Foo {
pub fn new(a: String) -> Self {
Self { a }
}
pub fn b(&self) -> Vec<&'static str> {
vec!["b"]
}
pub fn c(&self) -> &'static str {
"c"
}
pub fn d(&self, x: String) -> String {
x
}
pub fn bar(&self) -> Bar {
Bar::default()
}
pub fn e(&self) -> Vec<i64> {
vec![1, 2, 3]
}
pub fn f(&self) -> Vec<Vec<i64>> {
vec![vec![1, 2, 3], vec![4, 5, 6]]
}
pub fn g(&self) -> HashMap<&'static str, &'static str> {
hashmap! {"hello" => "world"}
}
pub fn h(&self) -> bool {
true
}
}
let foo_class = Foo::get_polar_class_builder()
.name("Foo")
.set_constructor(|| Foo::new("A".to_owned()))
.add_method("b", Foo::b)
.add_method("c", Foo::c)
.add_method("d", Foo::d)
.add_method("bar", Foo::bar)
.add_method("e", Foo::e)
.add_method("f", Foo::f)
.add_method("g", Foo::g)
.add_method("h", Foo::h)
.build();
oso.oso.register_class(bar_class)?;
oso.oso.register_class(foo_class)?;
oso.qvar_one("new Foo().a = x", "x", String::from("A"));
oso.query_err("new Foo().b = x");
oso.qvar_one("new Foo().b() = x", "x", vec!["b".to_owned()]);
oso.qvar_one("new Foo().c() = x", "x", "c".to_owned());
oso.qvar_one("new Foo() = f and f.a = x", "x", "A".to_owned());
oso.qvar_one("new Foo().bar().y() = x", "x", "y".to_owned());
oso.qvar_one("new Foo().e() = x", "x", vec![1, 2, 3]);
oso.qvar_one("new Foo().g().hello = x", "x", "world".to_owned());
oso.qvar_one("new Foo().h() = x", "x", true);
Ok(())
}
#[test]
fn test_animals() -> oso::Result<()> {
common::setup();
let mut oso = test_oso();
#[derive(PolarClass, Debug, Clone, PartialEq)]
struct Animal {
#[polar(attribute)]
family: String,
#[polar(attribute)]
genus: String,
#[polar(attribute)]
species: String,
}
impl Animal {
pub fn new(family: String, genus: String, species: String) -> Self {
Self {
family,
genus,
species,
}
}
}
let animal_class = Animal::get_polar_class_builder()
.name("Animal")
.set_constructor(Animal::new)
.with_equality_check()
.build();
oso.oso.register_class(animal_class)?;
let wolf = r#"new Animal("canidae", "canis", "canis lupus")"#;
let dog = r#"new Animal("canidae", "canis", "canis familiaris")"#;
let canine = r#"new Animal("canidae", "canis", "")"#;
let canid = r#"new Animal("canidae", "", "")"#;
let animal = r#"new Animal("", "", "")"#;
oso.load_str(
r#"
yup() if new Animal("steve", "", "") = new Animal("steve", "", "");
nope() if new Animal("steve", "", "") = new Animal("gabe", "", "");
"#,
);
oso.qeval("yup()");
oso.qnull("nope()");
oso.clear_rules();
oso.load_str(
r#"
what_is(_: {genus: "canis"}, r) if r = "canine";
what_is(_: {species: "canis lupus", genus: "canis"}, r) if r = "wolf";
what_is(_: {species: "canis familiaris", genus: "canis"}, r) if r = "dog";
"#,
);
assert_eq!(
oso.qvar::<String>(&format!("what_is({}, r)", wolf), "r"),
vec!["wolf".to_owned(), "canine".to_owned()]
);
assert_eq!(
oso.qvar::<String>(&format!("what_is({}, r)", dog), "r"),
vec!["dog".to_owned(), "canine".to_owned()]
);
assert_eq!(
oso.qvar::<String>(&format!("what_is({}, r)", canine), "r"),
vec!["canine".to_owned()]
);
oso.clear_rules();
oso.load_str(
r#"
what_is_class(_: Animal{}, r) if r = "animal";
what_is_class(_: Animal{genus: "canis"}, r) if r = "canine";
what_is_class(_: Animal{family: "canidae"}, r) if r = "canid";
what_is_class(_: Animal{species: "canis lupus", genus: "canis"}, r) if r = "wolf";
what_is_class(_: Animal{species: "canis familiaris", genus: "canis"}, r) if r = "dog";
what_is_class(_: Animal{species: s, genus: "canis"}, r) if r = s;
"#,
);
assert_eq!(
oso.qvar::<String>(&format!("what_is_class({}, r)", wolf), "r"),
vec![
"wolf".to_owned(),
"canis lupus".to_owned(),
"canine".to_owned(),
"canid".to_owned(),
"animal".to_owned()
]
);
assert_eq!(
oso.qvar::<String>(&format!("what_is_class({}, r)", dog), "r"),
vec![
"dog".to_owned(),
"canis familiaris".to_owned(),
"canine".to_owned(),
"canid".to_owned(),
"animal".to_owned()
]
);
assert_eq!(
oso.qvar::<String>(&format!("what_is_class({}, r)", canine), "r"),
vec![
"".to_owned(),
"canine".to_owned(),
"canid".to_owned(),
"animal".to_owned()
]
);
assert_eq!(
oso.qvar::<String>(&format!("what_is_class({}, r)", canid), "r"),
vec!["canid".to_owned(), "animal".to_owned()]
);
assert_eq!(
oso.qvar::<String>(&format!("what_is_class({}, r)", animal), "r"),
vec!["animal".to_owned()]
);
oso.clear_rules();
oso.load_str(
r#"
what_is_mix(_: Animal{}, r) if r = "animal_class";
what_is_mix(_: Animal{genus: "canis"}, r) if r = "canine_class";
what_is_mix(_: {genus: "canis"}, r) if r = "canine_dict";
what_is_mix(_: Animal{family: "canidae"}, r) if r = "canid_class";
what_is_mix(_: {species: "canis lupus", genus: "canis"}, r) if r = "wolf_dict";
what_is_mix(_: {species: "canis familiaris", genus: "canis"}, r) if r = "dog_dict";
what_is_mix(_: Animal{species: "canis lupus", genus: "canis"}, r) if r = "wolf_class";
what_is_mix(_: Animal{species: "canis familiaris", genus: "canis"}, r) if r = "dog_class";
"#,
);
let wolf_dict = r#"{species: "canis lupus", genus: "canis", family: "canidae"}"#;
let dog_dict = r#"{species: "canis familiaris", genus: "canis", family: "canidae"}"#;
let canine_dict = r#"{genus: "canis", family: "canidae"}"#;
assert_eq!(
oso.qvar::<String>(&format!("what_is_mix({}, r)", wolf), "r"),
vec![
"wolf_class".to_owned(),
"canine_class".to_owned(),
"canid_class".to_owned(),
"animal_class".to_owned(),
"wolf_dict".to_owned(),
"canine_dict".to_owned()
]
);
assert_eq!(
oso.qvar::<String>(&format!("what_is_mix({}, r)", dog), "r"),
vec![
"dog_class".to_owned(),
"canine_class".to_owned(),
"canid_class".to_owned(),
"animal_class".to_owned(),
"dog_dict".to_owned(),
"canine_dict".to_owned()
]
);
assert_eq!(
oso.qvar::<String>(&format!("what_is_mix({}, r)", canine), "r"),
vec![
"canine_class".to_owned(),
"canid_class".to_owned(),
"animal_class".to_owned(),
"canine_dict".to_owned()
]
);
assert_eq!(
oso.qvar::<String>(&format!("what_is_mix({}, r)", wolf_dict), "r"),
vec!["wolf_dict".to_owned(), "canine_dict".to_owned()]
);
assert_eq!(
oso.qvar::<String>(&format!("what_is_mix({}, r)", dog_dict), "r"),
vec!["dog_dict".to_owned(), "canine_dict".to_owned()]
);
assert_eq!(
oso.qvar::<String>(&format!("what_is_mix({}, r)", canine_dict), "r"),
vec!["canine_dict".to_owned()]
);
Ok(())
}
#[test]
fn test_inline_queries() {
common::setup();
let mut oso = test_oso();
oso.load_str("f(1); f(2); ?= f(1); ?= not f(3);");
oso.oso.load_str("g(1); ?= g(2);").unwrap_err();
}
#[test]
fn test_predicate_return_list() {
common::setup();
#[derive(PolarClass, Debug, Clone)]
struct User;
impl User {
pub fn new() -> Self {
Self
}
pub fn groups(&self) -> Vec<String> {
vec![
"engineering".to_owned(),
"social".to_owned(),
"admin".to_owned(),
]
}
}
let actor_class = User::get_polar_class_builder()
.name("UserTwo")
.add_method("groups", User::groups)
.build();
let mut oso = test_oso();
oso.load_str(r#"allow(actor: UserTwo, "join", "party") if "social" in actor.groups();"#);
oso.oso.register_class(actor_class).unwrap();
let mut query = oso
.oso
.query_rule("allow", (User::new(), "join", "party"))
.unwrap();
let result = query.next().unwrap().unwrap();
assert_eq!(result.keys().count(), 0);
}
#[test]
fn test_variables_as_arguments() -> oso::Result<()> {
common::setup();
let mut oso = test_oso();
oso.oso.load_files(vec![test_file_path()])?;
let query = oso
.oso
.query_rule("f", (PolarValue::Variable("a".to_owned()),))?;
let a_var = query
.map(|r| r.unwrap().get_typed::<i64>("a").unwrap())
.collect::<Vec<_>>();
assert_eq!(a_var, vec![1, 2, 3]);
Ok(())
}
#[test]
fn test_lookup_runtime_error() {
common::setup();
let mut oso = test_oso();
oso.query(r#"new Widget(1) = {bar: "bar"}"#);
oso.query_err(r#"new Widget(1).bar = "bar""#);
}
#[test]
fn test_returns_unbound_variable() -> oso::Result<()> {
common::setup();
let mut oso = test_oso();
oso.load_str("rule(_, y) if y = 1;");
let first = oso.query("rule(x, y)").pop().unwrap();
assert_eq!(first.get_typed::<i64>("y")?, 1);
assert!(matches!(first.get_typed("x")?, PolarValue::Variable(_)));
Ok(())
}
#[test]
fn test_nan_inf() -> oso::Result<()> {
common::setup();
let mut oso = test_oso();
oso.oso.register_constant(std::f64::INFINITY, "inf")?;
oso.oso
.register_constant(std::f64::NEG_INFINITY, "neg_inf")?;
oso.oso.register_constant(std::f64::NAN, "nan")?;
let x = oso.qvar::<f64>("x = nan", "x").pop().unwrap();
assert!(x.is_nan());
oso.qnull("nan = nan");
assert!(oso.qvar::<f64>("x = inf", "x").pop().unwrap().is_infinite());
assert!(oso.query("inf = inf").pop().is_some());
oso.qvar_one("x = neg_inf", "x", std::f64::NEG_INFINITY);
assert!(oso.query("neg_inf = neg_inf").pop().is_some());
Ok(())
}
#[test]
fn test_iterators() -> oso::Result<()> {
common::setup();
#[derive(Default, PolarClass)]
struct Foo {}
#[derive(Clone, PolarClass)]
struct Bar(Vec<u32>);
impl IntoIterator for Bar {
type Item = u32;
type IntoIter = std::vec::IntoIter<u32>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl Bar {
fn new(list: Vec<u32>) -> Self {
Self(list)
}
fn sum(&self) -> u32 {
self.0.iter().sum()
}
}
let mut oso = test_oso();
oso.oso.register_class(
Foo::get_polar_class_builder()
.set_constructor(Foo::default)
.build(),
)?;
oso.oso.register_class(
Bar::get_polar_class_builder()
.set_constructor(Bar::new)
.add_method("sum", Bar::sum)
.with_iter()
.build(),
)?;
assert_eq!(
oso.query_err("x in new Foo()"),
"Unsupported operation in for type Foo."
);
assert_eq!(
oso.qvar::<u32>("x in new Bar([1, 2, 3])", "x"),
vec![1, 2, 3]
);
oso.qvar_one("x = new Bar([1, 2, 3]).sum()", "x", 6u32);
Ok(())
}
#[test]
fn test_nil() {
common::setup();
let mut oso = test_oso();
oso.load_str("null(nil);");
oso.qvar_one("null(x)", "x", Option::<PolarValue>::None);
assert_eq!(
oso.oso
.query_rule("null", (Option::<PolarValue>::None,))
.unwrap()
.count(),
1
);
assert_eq!(
oso.oso
.query_rule("null", (Vec::<PolarValue>::new(),))
.unwrap()
.count(),
0
);
oso.qeval("nil.is_none()");
oso.qnull("x in nil");
}
#[test]
fn test_expression_error() {
common::setup();
let mut oso = test_oso();
oso.load_str("f(x) if x > 2;");
let err = oso.query_err("f(x)");
assert!(err.contains("unbound"));
}
#[test]
fn test_rule_types() {
common::setup();
let mut oso = test_oso();
let mut policy = r#"type is_actor(_actor: User);
is_actor(_actor: User);"#
.to_owned();
oso.load_str(&policy);
oso.clear_rules();
policy += "is_actor(_actor: Widget);";
let err = oso
.oso
.load_str(&policy)
.expect_err("Expected validation error");
assert!(matches!(
&err,
OsoError::Polar(polar_error::PolarError(polar_error::ErrorKind::Validation(
polar_error::ValidationError::InvalidRule { .. }
)))
));
}
#[test]
fn test_exclusive_isa() {
common::setup();
let mut oso = test_oso();
oso.qnull("x matches Integer and x matches String");
oso.qnull("x.a matches Integer and x.a matches String");
let policy = r#"
foo(x: Integer) if x matches String;
boo(x) if x.a matches String and x.a matches Integer;
moo(x: Integer) if y matches String and y = x;
goo(x) if x.a matches String and y.a matches Integer and x = y;
zoo(x) if x.a.b matches String and y.a.b matches Integer and x = y;
roo(x) if x.a.b matches String and x.a.b matches Integer;
"#;
oso.load_str(policy);
oso.qnull("foo(x)");
oso.qnull("boo(x)");
oso.qnull("goo(x)");
oso.qnull("moo(x)");
oso.qnull("roo(x)");
oso.qnull("zoo(x)");
}