Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6c559ad
Port test for directiveArgumentMergeStrategies
conwuegb Sep 19, 2025
d85bd32
Omit json to simplify test case struct
conwuegb Sep 19, 2025
230b350
Make assert_composition_success() more in line with TS version
conwuegb Sep 19, 2025
98eb302
Run formatter
conwuegb Sep 19, 2025
60ec5b3
Minor comment updates
conwuegb Sep 19, 2025
ab98e85
Merge branch 'dev' into conwuegb/fed-686
conwuegb Oct 23, 2025
05d5a99
Move assert_hints_equal() up to mod level
conwuegb Oct 23, 2025
a1dc015
Merge branch 'dev' into conwuegb/fed-686
conwuegb Oct 24, 2025
3f6bd3b
Merge branch 'dev' of https://github.com/apollographql/router into co…
conwuegb Oct 27, 2025
e4fd7ce
Reset visibility of argument_composition_strategies mod
conwuegb Oct 27, 2025
da5c360
fix(composition): Give correct argument name to merger when merging d…
tninesling Oct 27, 2025
21b883f
Update tests to use public-facing directives.
conwuegb Oct 28, 2025
d5da1fd
Remove test for mismatched composition strategy and argument type
conwuegb Oct 29, 2025
7c414fa
Add test for nullable_max strategy
conwuegb Oct 30, 2025
36961f6
Add test for nullable_and strategy
conwuegb Oct 30, 2025
cf8522d
Add test for nullable_union strategy
conwuegb Oct 30, 2025
5ec1423
Merge branch 'dev' of https://github.com/apollographql/router into co…
conwuegb Oct 30, 2025
25a5ec0
Fix incorrect error code in compose_validation test
conwuegb Oct 30, 2025
5c168b5
Fix incorrect error code in compose_inacessible test
conwuegb Oct 30, 2025
754cb69
Revert change in error code for compose_inaccessible test
conwuegb Oct 31, 2025
df90fe2
Merge branch 'dev' of https://github.com/apollographql/router into co…
conwuegb Oct 31, 2025
85da655
Merge branch 'dev' of https://github.com/apollographql/router into co…
conwuegb Oct 31, 2025
e7b355e
Check error code before checking error msg
conwuegb Oct 31, 2025
db90254
Add useful context to composition errors
conwuegb Oct 31, 2025
081019d
Fix lint errors
conwuegb Oct 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::schema::FederationSchema;

#[allow(dead_code)]
#[derive(Clone, Copy)]
pub(crate) enum ArgumentCompositionStrategy {
pub enum ArgumentCompositionStrategy {
Max,
Min,
// Sum,
Expand Down Expand Up @@ -52,7 +52,7 @@ impl ArgumentCompositionStrategy {
}
}

pub(crate) fn name(&self) -> &str {
pub fn name(&self) -> &str {
self.get_impl().name()
}

Expand Down
2 changes: 1 addition & 1 deletion apollo-federation/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ use crate::schema::position::TypeDefinitionPosition;
use crate::schema::position::UnionTypeDefinitionPosition;
use crate::schema::subgraph_metadata::SubgraphMetadata;

pub(crate) mod argument_composition_strategies;
pub mod argument_composition_strategies;
pub(crate) mod blueprint;
pub(crate) mod definitions;
pub(crate) mod directive_location;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
use std::iter::zip;

use apollo_compiler::ast;
use apollo_compiler::schema;
use apollo_federation::schema::argument_composition_strategies::ArgumentCompositionStrategy;
use apollo_federation::supergraph::CompositionHint;

use super::ServiceDefinition;
use super::assert_composition_success;
use super::compose_as_fed2_subgraphs;
use super::errors;

// Helper function to create directive strings from applied directives using schema::DirectiveList
fn directive_strings_schema(directives: &schema::DirectiveList, target: &str) -> Vec<String> {
directives
.iter()
.map(|dir| dir.to_string())
.filter(|s| s.contains(target))
.collect()
}

// Helper function to create directive strings from applied directives using ast::DirectiveList
fn directive_strings_ast(directives: &ast::DirectiveList, target: &str) -> Vec<String> {
directives
.iter()
.map(|dir| dir.to_string())
.filter(|s| s.contains(target))
.collect()
}

fn assert_hints_equal(actual_hints: &Vec<CompositionHint>, expected_hints: &Vec<CompositionHint>) {
if actual_hints.len() != expected_hints.len() {
panic!("Mismatched number of hints")
}
let zipped = zip(actual_hints, expected_hints);
zipped
.for_each(|(ch1, ch2)| assert!(ch1.code() == ch2.code() && ch1.message() == ch2.message()));
}

#[cfg(test)]
mod tests {
use core::str;
use std::collections::HashMap;
use std::sync::LazyLock;

use super::*;

// Test cases for different argument composition strategies
struct CompositionStrategyTestCase<'a> {
name: &'a str,
composition_strategy: ArgumentCompositionStrategy,
arg_values_s1: HashMap<&'a str, &'a str>,
arg_values_s2: HashMap<&'a str, &'a str>,
result_values: HashMap<&'a str, &'a str>,
}

static TEST_CASES: LazyLock<HashMap<&str, CompositionStrategyTestCase>> = LazyLock::new(|| {
HashMap::from([
(
"max",
CompositionStrategyTestCase {
name: "max",
composition_strategy: ArgumentCompositionStrategy::Max,
arg_values_s1: HashMap::from([("t", "3"), ("k", "1")]),
arg_values_s2: HashMap::from([("t", "2"), ("k", "5"), ("b", "4")]),
result_values: HashMap::from([("t", "3"), ("k", "5"), ("b", "4")]),
},
),
(
"min",
CompositionStrategyTestCase {
name: "min",
composition_strategy: ArgumentCompositionStrategy::Min,
arg_values_s1: HashMap::from([("t", "3"), ("k", "1")]),
arg_values_s2: HashMap::from([("t", "2"), ("k", "5"), ("b", "4")]),
result_values: HashMap::from([("t", "2"), ("k", "1"), ("b", "4")]),
},
),
(
"intersection",
CompositionStrategyTestCase {
name: "intersection",
composition_strategy: ArgumentCompositionStrategy::Intersection,
arg_values_s1: HashMap::from([("t", r#"["foo", "bar"]"#), ("k", r#"[]"#)]),
arg_values_s2: HashMap::from([
("t", r#"["foo"]"#),
("k", r#"["v1", "v2"]"#),
("b", r#"["x"]"#),
]),
result_values: HashMap::from([
("t", r#"["foo"]"#),
("k", r#"[]"#),
("b", r#"["x"]"#),
]),
},
),
(
"union",
CompositionStrategyTestCase {
name: "union",
composition_strategy: ArgumentCompositionStrategy::Union,
arg_values_s1: HashMap::from([("t", r#"["foo", "bar"]"#), ("k", r#"[]"#)]),
arg_values_s2: HashMap::from([
("t", r#"["foo"]"#),
("k", r#"["v1", "v2"]"#),
("b", r#"["x"]"#),
]),
result_values: HashMap::from([
("t", r#"["foo", "bar"]"#),
("k", r#"["v1", "v2"]"#),
("b", r#"["x"]"#),
]),
},
),
(
"nullable_and",
CompositionStrategyTestCase {
name: "nullable_and",
composition_strategy: ArgumentCompositionStrategy::NullableAnd,
arg_values_s1: HashMap::from([("t", "true"), ("k", "true")]),
arg_values_s2: HashMap::from([("t", "null"), ("k", "false"), ("b", "false")]),
result_values: HashMap::from([("t", "true"), ("k", "false"), ("b", "false")]),
},
),
(
"nullable_max",
CompositionStrategyTestCase {
name: "nullable_max",
composition_strategy: ArgumentCompositionStrategy::NullableMax,
arg_values_s1: HashMap::from([("t", "3"), ("k", "1")]),
arg_values_s2: HashMap::from([("t", "2"), ("k", "null"), ("b", "null")]),
result_values: HashMap::from([("t", "3"), ("k", "1"), ("b", "null")]),
},
),
(
"nullable_union",
CompositionStrategyTestCase {
name: "nullable_union",
composition_strategy: ArgumentCompositionStrategy::NullableUnion,
arg_values_s1: HashMap::from([("t", r#"["foo", "bar"]"#), ("k", r#"[]"#)]),
arg_values_s2: HashMap::from([
("t", r#"["foo"]"#),
("k", r#"["v1", "v2"]"#),
("b", r#"["x"]"#),
]),
result_values: HashMap::from([
("t", r#"["foo", "bar"]"#),
("k", r#"["v1", "v2"]"#),
("b", r#"["x"]"#),
]),
},
),
])
});

fn test_composition_of_directive_with_non_trivial_argument_strategies(
test_case: &CompositionStrategyTestCase,
) {
let subgraph1 = ServiceDefinition {
name: "Subgraph1",
type_defs: &format!(
r#"
extend schema @link(url: "https://specs.apollo.dev/{}/v0.1")
type Query {{
t: T
}}
type T
@key(fields: "k")
@{}(value: {})
{{
k: ID @{}(value: {})
}}
"#,
test_case.name,
test_case.name,
test_case.arg_values_s1["t"],
test_case.name,
test_case.arg_values_s1["k"]
),
};

let subgraph2 = ServiceDefinition {
name: "Subgraph2",
type_defs: &format!(
r#"
extend schema @link(url: "https://specs.apollo.dev/{}/v0.1")
type T
@key(fields: "k")
@{}(value: {})
{{
k: ID @{}(value: {})
a: Int
b: String @{}(value: {})
}}
"#,
test_case.name,
test_case.name,
test_case.arg_values_s2["t"],
test_case.name,
test_case.arg_values_s2["k"],
test_case.name,
test_case.arg_values_s2["b"]
),
};

let result = compose_as_fed2_subgraphs(&[subgraph1, subgraph2]);
let result_sg = assert_composition_success(result);

// Check expected hints
let expected_hints = vec![
CompositionHint {
code: String::from("MERGED_NON_REPEATABLE_DIRECTIVE_ARGUMENTS"),
message: format!(
"Directive @{} is applied to \"T\" in multiple subgraphs with different arguments. Merging strategies used by arguments: {{ \"value\": {} }}",
test_case.name,
test_case.composition_strategy.name()
),
locations: Vec::new(),
},
CompositionHint {
code: String::from("MERGED_NON_REPEATABLE_DIRECTIVE_ARGUMENTS"),
message: format!(
"Directive @{} is applied to \"T.k\" in multiple subgraphs with different arguments. Merging strategies used by arguments: {{ \"value\": {} }}",
test_case.name,
test_case.composition_strategy.name()
),
locations: Vec::new(),
},
];
assert_hints_equal(result_sg.hints(), &expected_hints);

// Check expected directive strings
let schema = result_sg.schema().schema();
assert_eq!(
directive_strings_schema(&schema.schema_definition.directives, test_case.name),
vec![format!(
r#"@link(url: "https://specs.apollo.dev/{}/v0.1")"#,
test_case.name
)]
);

let t = schema.get_object("T").unwrap();
assert_eq!(
directive_strings_schema(&t.directives, test_case.name),
[format!(
r#"@{}(value: {})"#,
test_case.name, test_case.result_values["t"]
)]
);
assert_eq!(
directive_strings_ast(&t.fields.get("k").unwrap().directives, test_case.name),
[format!(
r#"@{}(value: {})"#,
test_case.name, test_case.result_values["k"]
)]
);
assert_eq!(
directive_strings_ast(&t.fields.get("b").unwrap().directives, test_case.name),
[format!(
r#"@{}(value: {})"#,
test_case.name, test_case.result_values["b"]
)]
);
}

#[test]
#[ignore = "Directive argument merge strategies not yet implemented"]
fn works_for_max() {
let test_case = TEST_CASES.get("max").expect("Test case not found");
test_composition_of_directive_with_non_trivial_argument_strategies(test_case);
}

#[test]
#[ignore = "Directive argument merge strategies not yet implemented"]
fn works_for_min() {
let test_case = TEST_CASES.get("min").expect("Test case not found");
test_composition_of_directive_with_non_trivial_argument_strategies(test_case);
}

#[test]
#[ignore = "Directive argument merge strategies not yet implemented"]
fn works_for_intersection() {
let test_case = TEST_CASES.get("intersection").expect("Test case not found");
test_composition_of_directive_with_non_trivial_argument_strategies(test_case);
}

#[test]
#[ignore = "Directive argument merge strategies not yet implemented"]
fn works_for_union() {
let test_case = TEST_CASES.get("union").expect("Test case not found");
test_composition_of_directive_with_non_trivial_argument_strategies(test_case);
}

#[test]
#[ignore = "Directive argument merge strategies not yet implemented"]
fn works_for_nullable_and() {
let test_case = TEST_CASES.get("nullable_and").expect("Test case not found");
test_composition_of_directive_with_non_trivial_argument_strategies(test_case);
}

#[test]
#[ignore = "Directive argument merge strategies not yet implemented"]
fn works_for_nullable_max() {
let test_case = TEST_CASES.get("nullable_max").expect("Test case not found");
test_composition_of_directive_with_non_trivial_argument_strategies(test_case);
}

#[test]
#[ignore = "Directive argument merge strategies not yet implemented"]
fn works_for_nullable_union() {
let test_case = TEST_CASES
.get("nullable_union")
.expect("Test case not found");
test_composition_of_directive_with_non_trivial_argument_strategies(test_case);
}

#[test]
#[ignore = "Directive argument merge strategies not yet implemented"]
fn errors_when_declaring_strategy_that_does_not_match_the_argument_type() {
let subgraph1 = ServiceDefinition {
name: "Subgraph1",
type_defs: r#"
extend schema @link(url: "https://specs.apollo.dev/foo/v0.1")
type Query {
t: T
}
type T {
v: String @foo(value: "bar")
}
"#,
};

let subgraph2 = ServiceDefinition {
name: "Subgraph2",
type_defs: r#"
extend schema @link(url: "https://specs.apollo.dev/foo/v0.1")
type T {
v: String @foo(value: "bar")
}
"#,
};

let result = compose_as_fed2_subgraphs(&[subgraph1, subgraph2]);
let errs = errors(&result);
assert_eq!(
errs.iter().map(|(_, message)| message).collect::<Vec<_>>(),
[
r#"Invalid composition strategy MAX for argument @foo(value:) of type String; MAX only supports type(s) Int!"#
]
);
}
}
Loading