use serde_json_bytes::Value as JSON;
use shape::Shape;
use crate::connectors::json_selection::ApplyToError;
use crate::connectors::json_selection::ApplyToInternal;
use crate::connectors::json_selection::MethodArgs;
use crate::connectors::json_selection::PathList;
use crate::connectors::json_selection::ShapeContext;
use crate::connectors::json_selection::VarsWithPathsMap;
use crate::connectors::json_selection::immutable::InputPath;
use crate::connectors::json_selection::known_var::KnownVariable;
use crate::connectors::json_selection::lit_expr::LitExpr;
use crate::connectors::json_selection::location::Ranged;
use crate::connectors::json_selection::location::WithRange;
use crate::connectors::spec::ConnectSpec;
use crate::impl_arrow_method;
impl_arrow_method!(AsMethod, as_method, as_shape);
fn as_method(
method_name: &WithRange<String>,
method_args: Option<&MethodArgs>,
data: &JSON,
vars: &VarsWithPathsMap,
input_path: &InputPath<JSON>,
spec: ConnectSpec,
) -> (Option<JSON>, Vec<ApplyToError>) {
let (var_name_opt, mut errors) = check_method_args(method_name, method_args, input_path, spec);
let result_opt =
if let Some(expr_arg) = method_args.and_then(|MethodArgs { args, .. }| args.get(1)) {
let (result_opt, mut expr_errors) =
expr_arg.apply_to_path(data, vars, input_path, spec);
errors.append(&mut expr_errors);
result_opt
} else {
Some(data.clone())
};
let mut map = serde_json_bytes::Map::new();
if let (Some(var_name), Some(result)) = (var_name_opt, result_opt) {
map.insert(var_name, result);
}
(Some(JSON::Object(map)), errors)
}
fn check_method_args(
method_name: &WithRange<String>,
method_args: Option<&MethodArgs>,
input_path: &InputPath<JSON>,
spec: ConnectSpec,
) -> (Option<String>, Vec<ApplyToError>) {
let mut var_name_opt = None;
let mut errors = vec![];
if let Some(MethodArgs { args, .. }) = method_args {
if args.is_empty() || args.len() > 2 {
errors.push(ApplyToError::new(
format!(
"Method ->{} requires one or two arguments (got {})",
method_name.as_ref(),
args.len()
),
input_path.to_vec(),
method_name.range(),
spec,
));
}
if let Some(var_arg) = args.first() {
match var_arg.as_ref() {
LitExpr::Path(path) => match path.path.as_ref() {
PathList::Var(known_var, tail) => {
if !matches!(tail.as_ref(), PathList::Empty) {
errors.push(ApplyToError::new(
format!(
"First argument to ->{} must be a single $variable name with no path suffix",
method_name.as_ref()
),
input_path.to_vec(),
method_name.range(),
spec,
));
}
match known_var.as_ref() {
KnownVariable::Local(var_name) => {
var_name_opt = Some(var_name.clone());
}
KnownVariable::Dollar | KnownVariable::AtSign => {
errors.push(ApplyToError::new(
format!(
"First argument to ->{} must be a named $variable, not {}",
method_name.as_ref(),
known_var.as_str()
),
input_path.to_vec(),
method_name.range(),
spec,
));
}
KnownVariable::External(var_name) => {
errors.push(ApplyToError::new(
format!(
"Argument {} to ->{} must not be an external variable",
var_name, method_name.as_ref()
),
input_path.to_vec(),
method_name.range(),
spec,
));
}
}
}
_ => {
errors.push(ApplyToError::new(
format!(
"First argument to ->{} must be a single $variable name",
method_name.as_ref()
),
input_path.to_vec(),
method_name.range(),
spec,
));
}
},
_ => {
errors.push(ApplyToError::new(
format!(
"First argument to ->{} must be a single $variable name",
method_name.as_ref()
),
input_path.to_vec(),
method_name.range(),
spec,
));
}
}
}
} else {
errors.push(ApplyToError::new(
format!(
"Method ->{} requires one or two arguments (got 0)",
method_name.as_ref()
),
input_path.to_vec(),
method_name.range(),
spec,
));
}
(var_name_opt, errors)
}
fn as_shape(
context: &ShapeContext,
method_name: &WithRange<String>,
method_args: Option<&MethodArgs>,
input_shape: Shape,
dollar_shape: Shape,
) -> Shape {
let (var_name_opt, method_args_errors) = check_method_args(
method_name,
method_args,
&InputPath::empty(),
context.spec(),
);
let result_shape = if let Some(expr_arg) = method_args.and_then(|args| args.args.get(1)) {
expr_arg.compute_output_shape(context, input_shape, dollar_shape)
} else {
input_shape
};
let bound_vars_shape = if let Some(var_name) = var_name_opt.as_ref() {
Shape::record(
[(var_name.clone(), result_shape)].into(),
[],
)
} else {
Shape::dict(result_shape, [])
};
if method_args_errors.is_empty() {
bound_vars_shape
} else {
Shape::error_with_partial(
method_args_errors
.iter()
.map(|e| e.message().to_string())
.collect::<Vec<_>>()
.join("\n"),
bound_vars_shape,
method_name.shape_location(context.source_id()),
)
}
}
#[cfg(test)]
mod tests {
use apollo_compiler::collections::IndexMap;
use serde_json_bytes::json;
use shape::Shape;
use shape::location::SourceId;
use crate::connectors::ApplyToError;
use crate::connectors::ConnectSpec;
use crate::connectors::json_selection::ShapeContext;
use crate::selection;
#[test]
fn test_too_few_as_args() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!("person->as", spec)
.apply_to(&json!({ "person": {"id": 1, "name": "Alice" }})),
(
Some(json!({ "id": 1, "name": "Alice" })),
vec![ApplyToError::new(
"Method ->as requires one or two arguments (got 0)".to_string(),
vec![json!("person"), json!("->as")],
Some(8..10),
spec,
)],
),
);
assert_eq!(
selection!("person->as()", spec)
.apply_to(&json!({ "person": {"id": 1, "name": "Alice" }})),
(
Some(json!({ "id": 1, "name": "Alice" })),
vec![ApplyToError::new(
"Method ->as requires one or two arguments (got 0)".to_string(),
vec![json!("person"), json!("->as")],
Some(8..10),
spec,
)],
),
);
}
#[test]
fn test_too_few_as_args_shape() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!("person->as", spec).shape().pretty_print(),
"Error<\n \"Method ->as requires one or two arguments (got 0)\",\n $root.person,\n>",
);
assert_eq!(
selection!("person->as()", spec).shape().pretty_print(),
"Error<\n \"Method ->as requires one or two arguments (got 0)\",\n $root.person,\n>",
);
}
#[test]
fn test_too_many_as_args() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!("person->as($x, @.id, @.name)", spec)
.apply_to(&json!({ "person": {"id": 1, "name": "Alice" }})),
(
Some(json!({ "id": 1, "name": "Alice" })),
vec![ApplyToError::new(
"Method ->as requires one or two arguments (got 3)".to_string(),
vec![json!("person"), json!("->as")],
Some(8..10),
spec,
)],
),
);
}
#[test]
fn test_too_many_as_args_shape() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!("person->as($x, @.id, @.name)", spec)
.shape()
.pretty_print(),
"Error<\n \"Method ->as requires one or two arguments (got 3)\",\n $root.person,\n>",
);
}
#[test]
fn test_invalid_as_args() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!("person->as(123)", spec)
.apply_to(&json!({ "person": {"id": 1, "name": "Alice" }})),
(
Some(json!({ "id": 1, "name": "Alice" })),
vec![ApplyToError::new(
"First argument to ->as must be a single $variable name".to_string(),
vec![json!("person"), json!("->as")],
Some(8..10),
spec,
)],
),
);
assert_eq!(
selection!("person->as($x.id)", spec)
.apply_to(&json!({ "person": {"id": 1, "name": "Alice" }})),
(
Some(json!({ "id": 1, "name": "Alice" })),
vec![ApplyToError::new(
"First argument to ->as must be a single $variable name with no path suffix"
.to_string(),
vec![json!("person"), json!("->as")],
Some(8..10),
spec,
)],
),
);
assert_eq!(
selection!("person->as(p)", spec)
.apply_to(&json!({ "person": {"id": 1, "name": "Alice" }})),
(
Some(json!({ "id": 1, "name": "Alice" })),
vec![ApplyToError::new(
"First argument to ->as must be a single $variable name".to_string(),
vec![json!("person"), json!("->as")],
Some(8..10),
spec,
)],
)
);
}
#[test]
fn test_invalid_as_args_shape() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!("person->as(123)", spec).shape().pretty_print(),
"Error<\n \"First argument to ->as must be a single $variable name\",\n $root.person,\n>",
);
assert_eq!(
selection!("person->as($x.id)", spec).shape().pretty_print(),
"Error<\n \"First argument to ->as must be a single $variable name with no path suffix\",\n $root.person,\n>",
);
assert_eq!(
selection!("person->as(p)", spec).shape().pretty_print(),
"Error<\n \"First argument to ->as must be a single $variable name\",\n $root.person,\n>",
);
}
#[test]
fn test_basic_as_with_echo() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!("person->as($p)->echo($p)", spec)
.apply_to(&json!({ "person": {"id": 1, "name": "Alice" }})),
(Some(json!({ "id": 1, "name": "Alice" })), vec![]),
);
assert_eq!(
selection!("person->as($name, @.name)->echo([@, $name])", spec)
.apply_to(&json!({ "person": { "id": 1, "name": "Alice" }})),
(Some(json!([{ "id": 1, "name": "Alice" }, "Alice"])), vec![]),
);
}
#[test]
fn test_basic_as_with_echo_shape() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!("person->as($p)->echo($p)", spec)
.shape()
.pretty_print(),
"$root.person",
);
assert_eq!(
selection!("person->as($name, @.name)->echo([@, $name])", spec)
.shape()
.pretty_print(),
"[$root.person, $root.person.name]",
);
}
#[test]
fn test_invalid_external_var_shadowing() {
let spec = ConnectSpec::V0_3;
let mut vars = IndexMap::default();
vars.insert("$yikes".to_string(), json!("external"));
assert_eq!(
selection!("person->as($yikes).name", spec).apply_with_vars(
&json!({ "person": {"id": 1, "name": "Alice" }, "ext": "external" }),
&vars,
),
(Some(json!("Alice")), vec![],),
);
assert_eq!(
selection!(
"unbound: $yikes bound: person.name->as($yikes)->echo($yikes)",
spec
)
.apply_with_vars(
&json!({ "person": {"id": 1, "name": "Alice" }, "ext": "external" }),
&vars,
),
(
Some(json!({ "unbound": "external", "bound": "Alice" })),
vec![],
),
);
}
#[test]
fn test_invalid_external_var_shadowing_shape() {
let spec = ConnectSpec::V0_3;
let mut vars = IndexMap::default();
vars.insert("$yikes".to_string(), Shape::dict(Shape::int([]), []));
assert_eq!(
selection!("person->as($yikes) { id name }", spec)
.compute_output_shape(
&ShapeContext::new(SourceId::Other("JSONSelection".into()))
.with_spec(spec)
.with_named_shapes(vars),
Shape::record(
{
let mut map = Shape::empty_map();
map.insert(
"person".to_string(),
Shape::record(
{
let mut map = Shape::empty_map();
map.insert("id".to_string(), Shape::int([]));
map.insert("name".to_string(), Shape::string([]));
map
},
[],
),
);
map
},
[]
),
)
.pretty_print(),
"{ id: Int, name: String }",
);
}
#[test]
fn test_nested_loops() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!(
r#"
listsOfPairs: xs->map(@->as($x)->echo(ys->map([$x, @])))
"#,
spec
)
.apply_to(&json!({
"xs": [1, 2],
"ys": ["a", "b"]
})),
(
Some(json!({
"listsOfPairs": [
[[1, "a"], [1, "b"]],
[[2, "a"], [2, "b"]],
]
})),
vec![],
),
);
assert_eq!(
selection!(
r#"
xs->map(@->as($x)->echo(ys->map(@->as($y)->echo([$x, $y]))))
"#,
spec
)
.apply_to(&json!({
"xs": [1, 2],
"ys": ["a", "b"]
})),
(
Some(json!([[[1, "a"], [1, "b"]], [[2, "a"], [2, "b"]]])),
vec![],
),
);
}
#[test]
fn test_nested_loop_shapes() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!(
r#"
listsOfPairs: xs->map(@->as($x)->echo(ys->map([$x, @])))
"#,
spec
)
.shape()
.pretty_print(),
"{ listsOfPairs: List<List<[$root.*.xs.*, $root.*.ys.*]>> }",
);
assert_eq!(
selection!(
r#"
xs->map(@->as($x)->echo(ys->map(@->as($y)->echo([$x, $y]))))
"#,
spec
)
.shape()
.pretty_print(),
"List<List<[$root.xs.*, $root.ys.*]>>",
);
}
#[test]
fn test_invalid_sibling_path_variable_access() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!(
r#"
good: person->as($x) {
id: $x.id
}
bad: {
id: $x.id
}
"#,
spec
)
.apply_to(&json!({
"person": { "id": 2, "name": "Ben" },
})),
(
Some(json!({
"good": { "id": 2 },
"bad": {},
})),
vec![ApplyToError::new(
"Variable $x not found".to_string(),
vec![],
Some(135..137),
spec,
)],
),
);
}
#[test]
fn test_invalid_sibling_path_variable_access_shape() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!(
r#"
good: person->as($x) {
id: $x.id
}
bad: {
id: $x.id
}
"#,
spec
)
.shape()
.pretty_print(),
"{ bad: { id: $x.id }, good: { id: $root.*.person.id } }"
);
}
#[test]
fn test_optional_as_expr_arg() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!("person->as($name, @.name)->echo([@.id, $name])", spec)
.apply_to(&json!({ "person": {"id": 1, "name": "Alice" }})),
(Some(json!([1, "Alice"])), vec![]),
);
assert_eq!(
selection!("person->as($oyez, 'oyez') { id name oyez: $oyez }", spec)
.apply_to(&json!({ "person": {"id": 1, "name": "Alice" }})),
(
Some(json!({ "id": 1, "name": "Alice", "oyez": "oyez" })),
vec![]
),
);
}
#[test]
fn test_optional_as_expr_arg_shape() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!("person->as($name, @.name)->echo([@.id, $name])", spec)
.shape()
.pretty_print(),
"[$root.person.id, $root.person.name]",
);
assert_eq!(
selection!("person->as($oyez, 'oyez') { id name oyez: $oyez }", spec)
.shape()
.pretty_print(),
"{\n id: $root.person.*.id,\n name: $root.person.*.name,\n oyez: \"oyez\",\n}",
);
}
#[test]
fn test_as_inside_expr_parens() {
let spec = ConnectSpec::V0_3;
assert_eq!(
selection!("$([1, 2, 3])->as($arr)->first->add($arr->last)", spec).apply_to(&json!({})),
(Some(json!(4)), vec![]),
);
assert_eq!(
selection!("$([1, 2, 3]->as($arr))->first->add($arr->last)", spec).apply_to(&json!({})),
(
None,
vec![
ApplyToError::new(
"Variable $arr not found".to_string(),
vec![json!("->first"), json!("->add")],
Some(35..39),
spec,
),
ApplyToError::new(
"Method ->add requires numeric arguments".to_string(),
vec![json!("->first"), json!("->add")],
Some(35..45),
spec,
),
]
),
);
}
}