#![allow(dead_code)]
use std::sync::Arc;
use shape::Shape;
use shape::location::SourceId;
use super::JSONSelection;
use super::SelectionTrie;
use super::apply_to::ShapeContext;
#[derive(Debug, Clone)]
pub(crate) struct SelectionAnalysis {
selection: Arc<JSONSelection>,
output_shape: Shape,
consumption: SelectionTrie,
}
impl SelectionAnalysis {
pub(crate) fn new(selection: impl Into<Arc<JSONSelection>>) -> Self {
let selection: Arc<JSONSelection> = selection.into();
let context =
ShapeContext::new(SourceId::Other("JSONSelection".into())).with_spec(selection.spec());
let output_shape =
selection.compute_output_shape(&context, Shape::name("$root", Vec::new()));
let consumption = context.consumption().borrow().clone();
Self {
selection,
output_shape,
consumption,
}
}
pub(crate) fn selection(&self) -> Arc<JSONSelection> {
Arc::clone(&self.selection)
}
pub(crate) fn output_shape(&self) -> Shape {
self.output_shape.clone()
}
pub(crate) fn consumption(&self) -> &SelectionTrie {
&self.consumption
}
pub(crate) fn with_input_shape(&self, input: Shape) -> Self {
let context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
.with_spec(self.selection.spec());
let output_shape = self.selection.compute_output_shape(&context, input);
let consumption = context.consumption().borrow().clone();
Self {
selection: Arc::clone(&self.selection),
output_shape,
consumption,
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::SelectionAnalysis;
use crate::connectors::ConnectSpec;
use crate::connectors::json_selection::JSONSelection;
fn analyze(input: &str) -> SelectionAnalysis {
analyze_with_spec(input, ConnectSpec::V0_4)
}
fn analyze_with_spec(input: &str, spec: ConnectSpec) -> SelectionAnalysis {
let selection = JSONSelection::parse_with_spec(input, spec).expect("valid selection");
SelectionAnalysis::new(selection)
}
#[test]
fn consumption_for_simple_field_selections() {
let cases = [
("a { b { c } d { e } }", "$root { a { b { c } d { e } } }"),
("id name email", "$root { email id name }"),
];
for (input, expected) in cases {
let analysis = analyze(input);
assert_eq!(
analysis.consumption().to_string(),
expected,
"consumption trie mismatch for {input:?}",
);
}
}
#[test]
fn aliased_subselection_records_structural_root_navigation() {
let analysis = analyze("a { b { c: $args.c } d { e: $this.e } }");
assert_eq!(
analysis.consumption().to_string(),
"$args { c } $root { a { b d } } $this { e }",
);
}
#[test]
fn root_consumption_for_bare_subselection() {
let analysis = analyze("a { b { c } d { e } }");
let root = analysis.consumption().get("$root").expect("$root entry");
assert_eq!(root.to_string(), "a { b { c } d { e } }");
}
#[test]
fn args_and_this_consumption() {
let analysis = analyze("id: $args.id name: $this.name email: $args.contact.email");
let args = analysis.consumption().get("$args").expect("$args entry");
assert_eq!(args.to_string(), "contact { email } id");
let this = analysis.consumption().get("$this").expect("$this entry");
assert_eq!(this.to_string(), "name");
}
#[test]
fn pure_literal_selection_has_no_root_consumption() {
let analysis = analyze("answer: $(42) greeting: $(\"hello\")");
match analysis.consumption().get("$root") {
None => {}
Some(root) => assert!(root.is_empty(), "expected no $root consumption, got {root}",),
}
}
#[test]
fn rh_1345_filter_with_subselection_records_full_consumption() {
let analysis = analyze("$this.items->filter(@.product) { id name }");
let this = analysis.consumption().get("$this").expect("$this entry");
assert_eq!(this.to_string(), "items { id name product }");
}
#[test]
fn filter_predicate_consumption_without_subselection() {
let analysis = analyze("$this.items->filter(@.product)");
let this = analysis.consumption().get("$this").expect("$this entry");
assert_eq!(this.to_string(), "items { product }");
}
#[test]
fn slice_with_subselection_recurses_into_element_shape() {
let analysis = analyze("$this.items->slice(0, 5) { id name }");
let this = analysis.consumption().get("$this").expect("$this entry");
assert_eq!(this.to_string(), "items { id name }");
}
#[test]
fn size_terminates_consumption_at_method_boundary() {
let analysis = analyze("count: $this.items->size");
let this = analysis.consumption().get("$this").expect("$this entry");
assert_eq!(this.to_string(), "items");
}
#[test]
fn chained_filter_then_slice_with_subselection() {
let analysis = analyze("$this.items->filter(@.active)->slice(0, 3) { id }");
let this = analysis.consumption().get("$this").expect("$this entry");
assert_eq!(this.to_string(), "items { active id }");
}
#[test]
fn output_shape_is_cached_and_stable() {
let analysis = analyze("id name");
let first = analysis.output_shape().pretty_print();
let second = analysis.output_shape().pretty_print();
assert_eq!(first, second);
}
#[test]
fn selection_is_arc_shared() {
let selection =
Arc::new(JSONSelection::parse_with_spec("id", ConnectSpec::V0_4).expect("valid"));
let before = Arc::strong_count(&selection);
let analysis = SelectionAnalysis::new(Arc::clone(&selection));
assert_eq!(Arc::strong_count(&selection), before + 1);
#[allow(clippy::redundant_clone)] let cloned = analysis.clone();
assert_eq!(Arc::strong_count(&selection), before + 2);
drop(cloned);
assert_eq!(Arc::strong_count(&selection), before + 1);
}
}