Repository: mirumee/ariadne
Pull request #1158 (2024-02-14 10:44:08)

Add support for federation versions up to 2.6
Added support so we can parse fed 2.4, 2.5 and 2.6. schemas... 

Though I haven't added any extra code to mange those new directives or anything like that .

Also, refactored a bit the logic to fallback to the latest avaiable version when using `links` directive, which I think it's better comparing with defaulting to Federation v1

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/mirumee/ariadne/pull/1158?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`f8b3560`)](https://app.codecov.io/gh/mirumee/ariadne/commit/f8b3560a588d3ec2a0e9dfd5126769c7100e0725?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee) 97.88% compared to head [(`935fe27`)](https://app.codecov.io/gh/mirumee/ariadne/pull/1158?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee) 97.88%.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #1158   +/-   ##
=======================================
  Coverage   97.88%   97.88%           
=======================================
  Files         109      109           
  Lines        7879     7905   +26     
=======================================
+ Hits         7712     7738   +26     
  Misses        167      167           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/mirumee/ariadne/pull/1158?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee).

nogates: @rafalp Could you have a look when you got a sec? 🙏 
nogates: ready for another review @rafalp. Thanks!

Patch:
diff --git a/ariadne/contrib/federation/definitions/fed1_0.graphql b/ariadne/contrib/federation/definitions/fed_v1.0.graphql
similarity index 100%
rename from ariadne/contrib/federation/definitions/fed1_0.graphql
rename to ariadne/contrib/federation/definitions/fed_v1.0.graphql
diff --git a/ariadne/contrib/federation/definitions/fed2_0.graphql b/ariadne/contrib/federation/definitions/fed_v2.0.graphql
similarity index 100%
rename from ariadne/contrib/federation/definitions/fed2_0.graphql
rename to ariadne/contrib/federation/definitions/fed_v2.0.graphql
diff --git a/ariadne/contrib/federation/definitions/fed2_1.graphql b/ariadne/contrib/federation/definitions/fed_v2.1.graphql
similarity index 100%
rename from ariadne/contrib/federation/definitions/fed2_1.graphql
rename to ariadne/contrib/federation/definitions/fed_v2.1.graphql
diff --git a/ariadne/contrib/federation/definitions/fed2_2.graphql b/ariadne/contrib/federation/definitions/fed_v2.2.graphql
similarity index 100%
rename from ariadne/contrib/federation/definitions/fed2_2.graphql
rename to ariadne/contrib/federation/definitions/fed_v2.2.graphql
diff --git a/ariadne/contrib/federation/definitions/fed2_3.graphql b/ariadne/contrib/federation/definitions/fed_v2.3.graphql
similarity index 100%
rename from ariadne/contrib/federation/definitions/fed2_3.graphql
rename to ariadne/contrib/federation/definitions/fed_v2.3.graphql
diff --git a/ariadne/contrib/federation/definitions/fed_v2.4.graphql b/ariadne/contrib/federation/definitions/fed_v2.4.graphql
new file mode 100644
index 0000000..4466f96
--- /dev/null
+++ b/ariadne/contrib/federation/definitions/fed_v2.4.graphql
@@ -0,0 +1,63 @@
+#
+# https://specs.apollo.dev/federation/v2.0/federation-v2.0.graphql
+#
+
+directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
+directive @requires(fields: FieldSet!) on FIELD_DEFINITION
+directive @provides(fields: FieldSet!) on FIELD_DEFINITION
+directive @external on OBJECT | FIELD_DEFINITION
+directive @extends on OBJECT | INTERFACE
+directive @override(from: String!) on FIELD_DEFINITION
+directive @inaccessible on
+    | FIELD_DEFINITION
+    | OBJECT
+    | INTERFACE
+    | UNION
+    | ENUM
+    | ENUM_VALUE
+    | SCALAR
+    | INPUT_OBJECT
+    | INPUT_FIELD_DEFINITION
+    | ARGUMENT_DEFINITION
+directive @tag(name: String!) repeatable on
+    | FIELD_DEFINITION
+    | INTERFACE
+    | OBJECT
+    | UNION
+    | ARGUMENT_DEFINITION
+    | SCALAR
+    | ENUM
+    | ENUM_VALUE
+    | INPUT_OBJECT
+    | INPUT_FIELD_DEFINITION
+scalar FieldSet
+
+#
+# federation-v2.1
+#
+
+directive @composeDirective(name: String!) repeatable on SCHEMA
+
+#
+# https://specs.apollo.dev/link/v1.0/link-v1.0.graphql
+#
+
+directive @link(
+    url: String!,
+    as: String,
+    import: [Import])
+repeatable on SCHEMA
+
+scalar Import
+
+#
+# federation-v2.2
+#
+
+directive @shareable repeatable on FIELD_DEFINITION | OBJECT
+
+#
+# federation-v2.3
+#
+
+directive @interfaceObject on OBJECT
diff --git a/ariadne/contrib/federation/definitions/fed_v2.5.graphql b/ariadne/contrib/federation/definitions/fed_v2.5.graphql
new file mode 100644
index 0000000..7e9ea9b
--- /dev/null
+++ b/ariadne/contrib/federation/definitions/fed_v2.5.graphql
@@ -0,0 +1,84 @@
+#
+# https://specs.apollo.dev/federation/v2.0/federation-v2.0.graphql
+#
+
+directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
+directive @requires(fields: FieldSet!) on FIELD_DEFINITION
+directive @provides(fields: FieldSet!) on FIELD_DEFINITION
+directive @external on OBJECT | FIELD_DEFINITION
+directive @extends on OBJECT | INTERFACE
+directive @override(from: String!) on FIELD_DEFINITION
+directive @inaccessible on
+    | FIELD_DEFINITION
+    | OBJECT
+    | INTERFACE
+    | UNION
+    | ENUM
+    | ENUM_VALUE
+    | SCALAR
+    | INPUT_OBJECT
+    | INPUT_FIELD_DEFINITION
+    | ARGUMENT_DEFINITION
+directive @tag(name: String!) repeatable on
+    | FIELD_DEFINITION
+    | INTERFACE
+    | OBJECT
+    | UNION
+    | ARGUMENT_DEFINITION
+    | SCALAR
+    | ENUM
+    | ENUM_VALUE
+    | INPUT_OBJECT
+    | INPUT_FIELD_DEFINITION
+scalar FieldSet
+
+#
+# federation-v2.1
+#
+
+directive @composeDirective(name: String!) repeatable on SCHEMA
+
+#
+# https://specs.apollo.dev/link/v1.0/link-v1.0.graphql
+#
+
+directive @link(
+    url: String!,
+    as: String,
+    import: [Import])
+repeatable on SCHEMA
+
+scalar Import
+
+#
+# federation-v2.2
+#
+
+directive @shareable repeatable on FIELD_DEFINITION | OBJECT
+
+#
+# federation-v2.3
+#
+
+directive @interfaceObject on OBJECT
+
+#
+# federation-v2.5
+#
+
+directive @authenticated on
+    FIELD_DEFINITION
+  | OBJECT
+  | INTERFACE
+  | SCALAR
+  | ENUM
+
+scalar federation__Scope
+
+directive @requiresScopes(scopes: [[federation__Scope!]!]!) on
+    FIELD_DEFINITION
+  | OBJECT
+  | INTERFACE
+  | SCALAR
+  | ENUM
+
diff --git a/ariadne/contrib/federation/definitions/fed_v2.6.graphql b/ariadne/contrib/federation/definitions/fed_v2.6.graphql
new file mode 100644
index 0000000..dd89b84
--- /dev/null
+++ b/ariadne/contrib/federation/definitions/fed_v2.6.graphql
@@ -0,0 +1,95 @@
+#
+# https://specs.apollo.dev/federation/v2.0/federation-v2.0.graphql
+#
+
+directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
+directive @requires(fields: FieldSet!) on FIELD_DEFINITION
+directive @provides(fields: FieldSet!) on FIELD_DEFINITION
+directive @external on OBJECT | FIELD_DEFINITION
+directive @extends on OBJECT | INTERFACE
+directive @override(from: String!) on FIELD_DEFINITION
+directive @inaccessible on
+    | FIELD_DEFINITION
+    | OBJECT
+    | INTERFACE
+    | UNION
+    | ENUM
+    | ENUM_VALUE
+    | SCALAR
+    | INPUT_OBJECT
+    | INPUT_FIELD_DEFINITION
+    | ARGUMENT_DEFINITION
+directive @tag(name: String!) repeatable on
+    | FIELD_DEFINITION
+    | INTERFACE
+    | OBJECT
+    | UNION
+    | ARGUMENT_DEFINITION
+    | SCALAR
+    | ENUM
+    | ENUM_VALUE
+    | INPUT_OBJECT
+    | INPUT_FIELD_DEFINITION
+scalar FieldSet
+
+#
+# federation-v2.1
+#
+
+directive @composeDirective(name: String!) repeatable on SCHEMA
+
+#
+# https://specs.apollo.dev/link/v1.0/link-v1.0.graphql
+#
+
+directive @link(
+    url: String!,
+    as: String,
+    import: [Import])
+repeatable on SCHEMA
+
+scalar Import
+
+#
+# federation-v2.2
+#
+
+directive @shareable repeatable on FIELD_DEFINITION | OBJECT
+
+#
+# federation-v2.3
+#
+
+directive @interfaceObject on OBJECT
+
+#
+# federation-v2.5
+#
+
+directive @authenticated on
+    FIELD_DEFINITION
+  | OBJECT
+  | INTERFACE
+  | SCALAR
+  | ENUM
+
+scalar federation__Scope
+
+directive @requiresScopes(scopes: [[federation__Scope!]!]!) on
+    FIELD_DEFINITION
+  | OBJECT
+  | INTERFACE
+  | SCALAR
+  | ENUM
+
+#
+# federation-v2.5
+#
+
+scalar federation__Policy
+directive @policy(policies: [[federation__Policy!]!]!) on
+  | FIELD_DEFINITION
+  | OBJECT
+  | INTERFACE
+  | SCALAR
+  | ENUM
\ No newline at end of file
diff --git a/ariadne/contrib/federation/schema.py b/ariadne/contrib/federation/schema.py
index e9b9753..20d724b 100644
--- a/ariadne/contrib/federation/schema.py
+++ b/ariadne/contrib/federation/schema.py
@@ -76,25 +76,26 @@ def make_federated_schema(
         r"(?<=@link).*?url:.*?\"(.*?)\".?[^)]+?", sdl, re.MULTILINE | re.DOTALL
     )  # use regex to parse if it's fed 1 or fed 2; adds dedicated typedefs per spec
     dirname = os.path.dirname(__file__)
-    if link and link.group(1) == "https://specs.apollo.dev/federation/v2.0":
-        definitions = load_schema_from_path(
-            os.path.join(dirname, "./definitions/fed2_0.graphql")
-        )
-    elif link and link.group(1) == "https://specs.apollo.dev/federation/v2.1":
-        definitions = load_schema_from_path(
-            os.path.join(dirname, "./definitions/fed2_1.graphql")
-        )
-    elif link and link.group(1) == "https://specs.apollo.dev/federation/v2.2":
-        definitions = load_schema_from_path(
-            os.path.join(dirname, "./definitions/fed2_2.graphql")
-        )
-    elif link and link.group(1) == "https://specs.apollo.dev/federation/v2.3":
-        definitions = load_schema_from_path(
-            os.path.join(dirname, "./definitions/fed2_3.graphql")
+    definitions_folder = os.path.join(dirname, "definitions")
+    definition_files = os.listdir(definitions_folder)
+    definition_files.sort()
+    last_version_file = definition_files[-1]
+
+    if link and link.group(1).startswith("https://specs.apollo.dev/federation/"):
+        fed_version = link.group(1).split("/")[-1]
+        versioned_schema_file = os.path.join(
+            definitions_folder, f"fed_{fed_version}.graphql"
         )
+
+        if os.path.isfile(versioned_schema_file):
+            definitions = load_schema_from_path(versioned_schema_file)
+        else:
+            definitions = load_schema_from_path(
+                os.path.join(definitions_folder, last_version_file)
+            )
     else:
         definitions = load_schema_from_path(
-            os.path.join(dirname, "./definitions/fed1_0.graphql")
+            os.path.join(dirname, "./definitions/fed_v1.0.graphql")
         )
 
     tdl.append(definitions)
diff --git a/ariadne/contrib/federation/utils.py b/ariadne/contrib/federation/utils.py
index 6a0f483..bb6bc1d 100644
--- a/ariadne/contrib/federation/utils.py
+++ b/ariadne/contrib/federation/utils.py
@@ -60,6 +60,9 @@ _allowed_directives = [
     "inaccessible",  # Federation 2 directive.
     "composeDirective",  # Federation 2.1 directive.
     "interfaceObject",  # Federation 2.3 directive.
+    "authenticated",  # Federation 2.5 directive.
+    "requiresScopes",  # Federation 2.5 directive.
+    "policy",  # Federation 2.6 directive.
 ]
 
 


Test patch:
diff --git a/tests/federation/test_schema_v2.py b/tests/federation/test_schema_v2.py
index 979b5bd..8f8921b 100644
--- a/tests/federation/test_schema_v2.py
+++ b/tests/federation/test_schema_v2.py
@@ -162,3 +162,321 @@ def test_federated_schema_query_service_interface_object_federation_directive():
             }
         """
     )
+
+
+def test_federation_2_4_version_is_detected_in_schema():
+    type_defs = """
+        extend schema
+            @link(
+                url: "https://specs.apollo.dev/federation/v2.4",
+                import: [
+                    "@key",
+                    "@shareable",
+                    "@provides",
+                    "@external",
+                    "@tag",
+                    "@extends",
+                    "@override",
+                    "@interfaceObject"
+                ]
+            )
+
+        type Query {
+            rootField: Review
+        }
+
+        type Review @interfaceObject @key(fields: "id") {
+            id: ID!
+        }
+    """
+
+    schema = make_federated_schema(type_defs)
+
+    result = graphql_sync(
+        schema,
+        """
+            query GetServiceDetails {
+                _service {
+                    sdl
+                }
+            }
+        """,
+    )
+
+    assert result.errors is None
+    assert sic(result.data["_service"]["sdl"]) == sic(
+        """
+            extend schema
+                @link(
+                    url: "https://specs.apollo.dev/federation/v2.4",
+                    import: [
+                        "@key",
+                        "@shareable",
+                        "@provides",
+                        "@external",
+                        "@tag",
+                        "@extends",
+                        "@override",
+                        "@interfaceObject"
+                    ]
+                )
+
+            type Query {
+                rootField: Review
+            }
+
+            type Review @interfaceObject @key(fields: "id") {
+                id: ID!
+            }
+        """
+    )
+
+
+def test_federation_2_5_version_is_detected_in_schema():
+    type_defs = """
+        extend schema
+            @link(
+                url: "https://specs.apollo.dev/federation/v2.5",
+                import: [
+                    "@key",
+                    "@shareable",
+                    "@provides",
+                    "@external",
+                    "@tag",
+                    "@extends",
+                    "@override",
+                    "@interfaceObject",
+                    "@authenticated",
+                    "@requiresScopes",
+
+                ]
+            )
+
+        type Query {
+            rootField: Review
+        }
+
+        type Review
+            @interfaceObject
+            @key(fields: "id")
+            @authenticated
+            @requiresScopes(scopes: [["read:review"]]) {
+            id: ID!
+        }
+    """
+
+    schema = make_federated_schema(type_defs)
+
+    result = graphql_sync(
+        schema,
+        """
+            query GetServiceDetails {
+                _service {
+                    sdl
+                }
+            }
+        """,
+    )
+
+    assert result.errors is None
+    assert sic(result.data["_service"]["sdl"]) == sic(
+        """
+            extend schema
+                @link(
+                    url: "https://specs.apollo.dev/federation/v2.5",
+                    import: [
+                        "@key",
+                        "@shareable",
+                        "@provides",
+                        "@external",
+                        "@tag",
+                        "@extends",
+                        "@override",
+                        "@interfaceObject"
+                        "@authenticated",
+                        "@requiresScopes",
+                    ]
+                )
+
+            type Query {
+                rootField: Review
+            }
+
+            type Review
+                @interfaceObject @key(fields: "id")
+                @authenticated
+                @requiresScopes(scopes: [["read:review"]]) {
+                id: ID!
+            }
+        """
+    )
+
+
+def test_federation_2_6_version_is_detected_in_schema():
+    type_defs = """
+        extend schema
+            @link(
+                url: "https://specs.apollo.dev/federation/v2.6",
+                import: [
+                    "@key",
+                    "@shareable",
+                    "@provides",
+                    "@external",
+                    "@tag",
+                    "@extends",
+                    "@override",
+                    "@interfaceObject",
+                    "@authenticated",
+                    "@requiresScopes",
+
+                ]
+            )
+
+        type Query {
+            rootField: Review
+        }
+
+        type Review
+            @interfaceObject
+            @key(fields: "id")
+            @authenticated
+            @requiresScopes(scopes: [["read:review"]])
+            @policy(policies: [["role:admin"]])
+             {
+            id: ID!
+        }
+    """
+
+    schema = make_federated_schema(type_defs)
+
+    result = graphql_sync(
+        schema,
+        """
+            query GetServiceDetails {
+                _service {
+                    sdl
+                }
+            }
+        """,
+    )
+
+    assert result.errors is None
+    assert sic(result.data["_service"]["sdl"]) == sic(
+        """
+            extend schema
+                @link(
+                    url: "https://specs.apollo.dev/federation/v2.6",
+                    import: [
+                        "@key",
+                        "@shareable",
+                        "@provides",
+                        "@external",
+                        "@tag",
+                        "@extends",
+                        "@override",
+                        "@interfaceObject"
+                        "@authenticated",
+                        "@requiresScopes",
+                    ]
+                )
+
+            type Query {
+                rootField: Review
+            }
+
+            type Review
+                @interfaceObject
+                @key(fields: "id")
+                @authenticated
+                @requiresScopes(scopes: [["read:review"]])
+                @policy(policies: [["role:admin"]])
+                {
+                id: ID!
+            }
+        """
+    )
+
+
+def test_federation_version_not_supported_is_detected_in_schema():
+    type_defs = """
+        extend schema
+            @link(
+                url: "https://specs.apollo.dev/federation/v2.25",
+                import: [
+                    "@key",
+                    "@shareable",
+                    "@provides",
+                    "@external",
+                    "@tag",
+                    "@extends",
+                    "@override",
+                    "@interfaceObject",
+                    "@authenticated",
+                    "@requiresScopes",
+
+                ]
+            )
+
+        type Query {
+            rootField: Review
+        }
+
+        type Review
+            @interfaceObject
+            @key(fields: "id")
+            @authenticated
+            @requiresScopes(scopes: [["read:review"]])
+            @policy(policies: [["role:admin"]])
+             {
+            id: ID!
+        }
+    """
+
+    schema = make_federated_schema(type_defs)
+
+    result = graphql_sync(
+        schema,
+        """
+            query GetServiceDetails {
+                _service {
+                    sdl
+                }
+            }
+        """,
+    )
+
+    assert result.errors is None
+    assert sic(result.data["_service"]["sdl"]) == sic(
+        """
+            extend schema
+                @link(
+                    url: "https://specs.apollo.dev/federation/v2.25",
+                    import: [
+                        "@key",
+                        "@shareable",
+                        "@provides",
+                        "@external",
+                        "@tag",
+                        "@extends",
+                        "@override",
+                        "@interfaceObject"
+                        "@authenticated",
+                        "@requiresScopes",
+                    ]
+                )
+
+            type Query {
+                rootField: Review
+            }
+
+            type Review
+                @interfaceObject
+                @key(fields: "id")
+                @authenticated
+                @requiresScopes(scopes: [["read:review"]])
+                @policy(policies: [["role:admin"]])
+                {
+                id: ID!
+            }
+        """
+    )