Skip to content

Commit

Permalink
Query result validator (#304)
Browse files Browse the repository at this point in the history
* Refactoring validator

* Move code_generator out in a dedicated file

* Draft result validator

* Fix typecheck test

* Remove unwanted fields

* Fix unions

* Update deno.lock

* Field inclusion, fix unions

* Rename interface

* Fix test

* Integer validation

* Fix tests

* Move the computation into into its own module

* Apply suggestions

* Add some documentation comments

* Test nested unions

* Test selection sets

* Test boolean validator

* Test multilevel unions

* Fix merge confusions

* Fix tests

* Fix tests

* Fix tests
  • Loading branch information
Natoandro authored May 15, 2023
1 parent d7b9489 commit 0f05845
Show file tree
Hide file tree
Showing 45 changed files with 2,839 additions and 1,554 deletions.
25 changes: 25 additions & 0 deletions libs/common/src/typegraph/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,29 @@ impl TypeNode {
| Any { base, .. } => base,
}
}

pub fn type_name(&self) -> &'static str {
use TypeNode::*;
match self {
Optional { .. } => "optional",
Boolean { .. } => "booleal",
Number { .. } => "number",
Integer { .. } => "integer",
String { .. } => "string",
Object { .. } => "object",
Array { .. } => "array",
Function { .. } => "function",
Union { .. } => "union",
Either { .. } => "either",
Any { .. } => "any",
}
}

pub fn is_scalar(&self) -> bool {
use TypeNode::*;
matches!(
self,
Boolean { .. } | Number { .. } | Integer { .. } | String { .. }
)
}
}
87 changes: 85 additions & 2 deletions libs/common/src/typegraph/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ struct Validator {
}

impl Validator {
fn push_error(&mut self, path: &[PathSegment], message: String) {
fn push_error(&mut self, path: &[PathSegment], message: impl Into<String>) {
self.errors.push(ValidatorError {
path: Path(path).to_string(),
message,
message: message.into(),
});
}
}
Expand All @@ -40,6 +40,25 @@ fn to_string(value: &Value) -> String {
serde_json::to_string(value).unwrap()
}

impl Typegraph {
fn collect_nested_variants_into(&self, out: &mut Vec<u32>, variants: &[u32]) {
for idx in variants {
let node = self.types.get(*idx as usize).unwrap();
match node {
TypeNode::Union {
data: UnionTypeData { any_of: variants },
..
}
| TypeNode::Either {
data: EitherTypeData { one_of: variants },
..
} => self.collect_nested_variants_into(out, variants),
_ => out.push(*idx),
}
}
}
}

impl TypeVisitor for Validator {
type Return = Vec<ValidatorError>;

Expand All @@ -48,9 +67,73 @@ impl TypeVisitor for Validator {
type_idx: u32,
path: &[PathSegment],
tg: &Typegraph,
as_input: bool,
) -> VisitResult<Self::Return> {
let node = &tg.types[type_idx as usize];

if as_input {
// do not allow t.func in input types
if let TypeNode::Function { .. } = node {
self.push_error(path, "function is not allowed in input types");
return VisitResult::Continue(false);
}
} else {
match node {
TypeNode::Union { .. } | TypeNode::Either { .. } => {
let mut variants = vec![];
tg.collect_nested_variants_into(&mut variants, &[type_idx]);
let mut object_count = 0;

for variant_type in variants
.iter()
.map(|idx| tg.types.get(*idx as usize).unwrap())
{
match variant_type {
TypeNode::Object { .. } => object_count += 1,
TypeNode::Boolean { .. }
| TypeNode::Number { .. }
| TypeNode::Integer { .. }
| TypeNode::String { .. } => {
// scalar
}
TypeNode::Array { data, .. } => {
let item_type = tg.types.get(data.items as usize).unwrap();
if !item_type.is_scalar() {
self.push_error(
path,
format!(
"array of '{}' not allowed as union/either variant",
item_type.type_name()
),
);
return VisitResult::Continue(false);
}
}
_ => {
self.push_error(
path,
format!(
"type '{}' not allowed as union/either variant",
variant_type.type_name()
),
);
return VisitResult::Continue(false);
}
}
}

if object_count != 0 && object_count != variants.len() {
self.push_error(
path,
"union variants must either be all scalars or all objects",
);
return VisitResult::Continue(false);
}
}
_ => {}
}
}

if let Some(enumeration) = &node.base().enumeration {
if matches!(node, TypeNode::Optional { .. }) {
self.push_error(
Expand Down
75 changes: 58 additions & 17 deletions libs/common/src/typegraph/visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ impl Typegraph {
let mut traversal = TypegraphTraversal {
tg: self,
path: vec![],
as_input: false,
visited_types: HashSet::new(),
visited_input_types: HashSet::new(),
visitor,
};
traversal
Expand All @@ -28,18 +30,31 @@ impl Typegraph {
struct TypegraphTraversal<'a, V: TypeVisitor + Sized> {
tg: &'a Typegraph,
path: Vec<PathSegment<'a>>,
visited_types: HashSet<u32>,
as_input: bool,
visited_types: HashSet<u32>, // non input types
visited_input_types: HashSet<u32>,
visitor: V,
}

impl<'a, V: TypeVisitor + Sized> TypegraphTraversal<'a, V> {
fn visit_type(&mut self, type_idx: u32) -> Option<V::Return> {
if self.visited_types.contains(&type_idx) {
return None;
if self.as_input {
if self.visited_input_types.contains(&type_idx) {
return None;
}
self.visited_input_types.insert(type_idx);
} else {
if self.visited_types.contains(&type_idx) {
return None;
}
self.visited_types.insert(type_idx);
}
self.visited_types.insert(type_idx);
let res = self.visitor.visit(type_idx, &self.path, self.tg);

let res = self
.visitor
.visit(type_idx, &self.path, self.tg, self.as_input);
let type_node = &self.tg.types[type_idx as usize];

match res {
VisitResult::Continue(deeper) if deeper => match type_node {
TypeNode::Optional { data, .. } => self.visit_optional(type_idx, data.item),
Expand Down Expand Up @@ -71,6 +86,7 @@ impl<'a, V: TypeVisitor + Sized> TypegraphTraversal<'a, V> {
edge: Edge::OptionalItem,
},
item_type_idx,
false,
)
}

Expand All @@ -81,6 +97,7 @@ impl<'a, V: TypeVisitor + Sized> TypegraphTraversal<'a, V> {
edge: Edge::ArrayItem,
},
item_type_idx,
false,
)
}

Expand All @@ -96,6 +113,7 @@ impl<'a, V: TypeVisitor + Sized> TypegraphTraversal<'a, V> {
edge: Edge::ObjectProp(prop_name),
},
*prop_type,
false,
);
if let Some(res) = res {
return Some(res);
Expand All @@ -112,6 +130,7 @@ impl<'a, V: TypeVisitor + Sized> TypegraphTraversal<'a, V> {
edge: Edge::UnionVariant(i),
},
*variant_type,
false,
);
if let Some(ret) = res {
return Some(ret);
Expand All @@ -128,28 +147,47 @@ impl<'a, V: TypeVisitor + Sized> TypegraphTraversal<'a, V> {
edge: Edge::EitherVariant(i),
},
*t,
false,
)
})
}

fn visit_function(&mut self, type_idx: u32, input: u32, output: u32) -> Option<V::Return> {
[(Edge::FunctionInput, input), (Edge::FunctionOutput, output)]
.into_iter()
.find_map(|(edge, t)| {
self.visit_child(
PathSegment {
from: type_idx,
edge,
},
t,
)
})
[
(Edge::FunctionInput, input, true),
(Edge::FunctionOutput, output, false),
]
.into_iter()
.find_map(|(edge, t, as_input)| {
self.visit_child(
PathSegment {
from: type_idx,
edge,
},
t,
as_input,
)
})
}

fn visit_child(&mut self, segment: PathSegment<'a>, type_idx: u32) -> Option<V::Return> {
fn visit_child(
&mut self,
segment: PathSegment<'a>,
type_idx: u32,
as_input: bool,
) -> Option<V::Return> {
let root_input = as_input && !self.as_input;
if root_input {
self.as_input = as_input;
}

self.path.push(segment);
let res = self.visit_type(type_idx);
self.path.pop().unwrap();

if root_input {
self.as_input = false;
}
res
}
}
Expand Down Expand Up @@ -204,13 +242,16 @@ pub enum VisitResult<T> {

pub trait TypeVisitor {
type Return: Sized;

/// return true to continue the traversal on the subgraph
fn visit(
&mut self,
type_idx: u32,
path: &[PathSegment],
tg: &Typegraph,
as_input: bool,
) -> VisitResult<Self::Return>;

fn get_result(self) -> Option<Self::Return>
where
Self: Sized,
Expand Down
2 changes: 1 addition & 1 deletion meta-cli/src/codegen/deno.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ mod tests {
ensure_venv()?;
let test_folder = Path::new("./src/tests/typegraphs").normalize()?;
std::env::set_current_dir(&test_folder)?;
trace!("Test folder: {test_folder:?}");
trace!("Test folder: {:?}", test_folder.as_path());
let tests = fs::read_dir(&test_folder).unwrap();
let config = Config::default_in(".");
let config = Arc::new(config);
Expand Down
6 changes: 3 additions & 3 deletions meta-cli/src/tests/typegraphs/union_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from typegraph.runtimes.deno import ModuleMat

with TypeGraph("union") as g:
rgb = t.array(t.integer().min(0).max(255)).min(3).max(3).named("RGB")
hex = t.string().pattern("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$").named("HEX")
rgb = t.array(t.integer().min(0).max(255)).min(3).max(3).named("RGBArray")
hex = t.string().pattern("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$").named("HexColor")
colorName = (
t.string()
.enum(
Expand All @@ -18,7 +18,7 @@
"yellow",
]
)
.named("ColorName")
.named("NamedColor")
)

color = t.union((rgb, hex, colorName)).named("Color")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,20 @@ expression: "String::from_utf8(output.stderr).expect(\"failed to decode output\"
[ERROR] at validator:/test/[in]/e: Required field "a" not found in object '{}'
[ERROR] at validator:/test/[in]/f: Required field "a" not found in object '{"b":1}'
[ERROR] at validator:/test/[in]/g: Unexpected fields "b" in object "{\"a\":2,\"b\":1}"
[ERROR] at validator:/test/[out]/a: Expected number got '"1"'
[ERROR] at validator:/test/[out]/b: Expected a string, got '["h","e","l","l","o"]'
[ERROR] at validator:/test/[out]/c: Expected a minimum value of 2, got 0
[ERROR] at validator:/test/[out]/d: Expected a maximun length of 4, got "hello" (len=5)
[ERROR] at validator:/test/[out]/e: Required field "a" not found in object '{}'
[ERROR] at validator:/test/[out]/f: Required field "a" not found in object '{"b":1}'
[ERROR] at validator:/test/[out]/g: Unexpected fields "b" in object "{\"a\":2,\"b\":1}"
[ERROR] at validator:/testEnums/[in]/a: Expected a minimum length of 4, got "hi" (len=2)
[ERROR] at validator:/testEnums/[in]/a: Expected a string, got '12'
[ERROR] at validator:/testEnums/[in]/b: Expected number got '"13"'
[ERROR] at validator:/testEnums/[in]/c: optional not cannot have enumerated values
[ERROR] at validator:/testEnums/[out]/a: Expected a minimum length of 4, got "hi" (len=2)
[ERROR] at validator:/testEnums/[out]/a: Expected a string, got '12'
[ERROR] at validator:/testEnums/[out]/b: Expected number got '"13"'
[ERROR] at validator:/testEnums/[out]/c: optional not cannot have enumerated values
Error: Error while post processing typegraph validator from "tests/e2e/typegraphs/validator.py": Typegraph validator failed validation

4 changes: 2 additions & 2 deletions meta-cli/tests/e2e/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ use std::process::Command;
#[test]
fn test_invalid_injections() {
let status = Command::new("cargo")
.args(&["build", "-p", "meta-cli", "--quiet"])
.args(["build", "-p", "meta-cli", "--quiet"])
.status()
.expect("failed to execute build for meta-cli");
assert!(status.success(), "`cargo build -p meta-cli` failed");

let root = project_root::get_project_root().unwrap();
let bin = root.join("target/debug/meta");
let output = Command::new(bin)
.args(&["serialize", "-f", "tests/e2e/typegraphs/validator.py"])
.args(["serialize", "-f", "tests/e2e/typegraphs/validator.py"])
.env("RUST_LOG", "error")
.output()
.expect("failed to execute process");
Expand Down
Loading

0 comments on commit 0f05845

Please sign in to comment.