Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ Optional settings:
- `plugins` (defaults to `[]`) - list of plugins to use during generation
- `enable_custom_operations` (defaults to `false`) - enables building custom operations. Generates additional files that contains all the classes and methods for generation.

These options control which fields are included in the GraphQL introspection query when using `remote_schema_url`.

- `introspection_descriptions` (defaults to `false`) – include descriptions in the introspection result
- `introspection_input_value_deprecation` (defaults to `false`) – include deprecation information for input values
- `introspection_specified_by_url` (defaults to `false`) – include `specifiedByUrl` for custom scalars
- `introspection_schema_description` (defaults to `false`) – include schema description
- `introspection_directive_is_repeatable` (defaults to `false`) – include `isRepeatable` information for directives
- `introspection_input_object_one_of` (defaults to `false`) – include `oneOf` information for input objects

## Custom operation builder

The custom operation builder allows you to create complex GraphQL queries in a structured and intuitive way.
Expand Down Expand Up @@ -178,7 +187,7 @@ Ariadne Codegen ships with optional plugins importable from the `ariadne_codegen
[tool.ariadne-codegen]
...
plugins = ["ariadne_codegen.contrib.extract_operations.ExtractOperationsPlugin"]

[tool.ariadne-codegen.extract_operations]
operations_module_name = "custom_operations_module_name"
```
Expand Down
2 changes: 2 additions & 0 deletions ariadne_codegen/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def client(config_dict):
headers=settings.remote_schema_headers,
verify_ssl=settings.remote_schema_verify_ssl,
timeout=settings.remote_schema_timeout,
introspection_settings=settings.introspection_settings,
)

plugin_manager = PluginManager(
Expand Down Expand Up @@ -95,6 +96,7 @@ def graphql_schema(config_dict):
headers=settings.remote_schema_headers,
verify_ssl=settings.remote_schema_verify_ssl,
timeout=settings.remote_schema_timeout,
introspection_settings=settings.introspection_settings,
)
)
plugin_manager = PluginManager(
Expand Down
19 changes: 15 additions & 4 deletions ariadne_codegen/schema.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections.abc import Generator
from dataclasses import asdict
from pathlib import Path
from typing import Optional, cast

Expand Down Expand Up @@ -29,6 +30,7 @@
InvalidGraphqlSyntax,
InvalidOperationForSchema,
)
from .settings import IntrospectionSettings


def filter_operations_definitions(
Expand Down Expand Up @@ -68,10 +70,15 @@ def get_graphql_schema_from_url(
headers: Optional[dict[str, str]] = None,
verify_ssl: bool = True,
timeout: float = 5,
introspection_settings: Optional[IntrospectionSettings] = None,
) -> GraphQLSchema:
return build_client_schema(
introspect_remote_schema(
url=url, headers=headers, verify_ssl=verify_ssl, timeout=timeout
url=url,
headers=headers,
verify_ssl=verify_ssl,
timeout=timeout,
introspection_settings=introspection_settings,
),
assume_valid=True,
)
Expand All @@ -82,11 +89,15 @@ def introspect_remote_schema(
headers: Optional[dict[str, str]] = None,
verify_ssl: bool = True,
timeout: float = 5,
introspection_settings: Optional[IntrospectionSettings] = None,
) -> IntrospectionQuery:
# If introspection settings are not provided, use default values.
settings = introspection_settings or IntrospectionSettings()
query = get_introspection_query(**asdict(settings))
try:
response = httpx.post(
url,
json={"query": get_introspection_query(descriptions=False)},
json={"query": query},
headers=headers,
verify=verify_ssl,
timeout=timeout,
Expand All @@ -105,14 +116,14 @@ def introspect_remote_schema(
except ValueError as exc:
raise IntrospectionError("Introspection result is not a valid json.") from exc

if (not isinstance(response_json, dict)) or ("data" not in response_json):
if not isinstance(response_json, dict):
raise IntrospectionError("Invalid introspection result format.")

errors = response_json.get("errors")
if errors:
raise IntrospectionError(f"Introspection errors: {errors}")

data = response_json["data"]
data = response_json.get("data")
if not isinstance(data, dict):
raise IntrospectionError("Invalid data key in introspection result.")

Expand Down
63 changes: 62 additions & 1 deletion ariadne_codegen/settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import enum
import os
import re
from dataclasses import dataclass, field
from dataclasses import asdict, dataclass, field
from keyword import iskeyword
from pathlib import Path
from textwrap import dedent
Expand Down Expand Up @@ -31,6 +31,21 @@ class Strategy(str, enum.Enum):
GRAPHQL_SCHEMA = "graphqlschema"


@dataclass
class IntrospectionSettings:
"""
Introspection settings for schema generation.
"""

descriptions: bool = False
input_value_deprecation: bool = False
specified_by_url: bool = False
schema_description: bool = False
directive_is_repeatable: bool = False
# graphql-core will rename this to one_of in a future version (update when bumping)
input_object_one_of: bool = False


@dataclass
class BaseSettings:
schema_path: str = ""
Expand All @@ -40,6 +55,12 @@ class BaseSettings:
remote_schema_timeout: float = 5
enable_custom_operations: bool = False
plugins: list[str] = field(default_factory=list)
introspection_descriptions: bool = False
introspection_input_value_deprecation: bool = False
introspection_specified_by_url: bool = False
introspection_schema_description: bool = False
introspection_directive_is_repeatable: bool = False
introspection_input_object_one_of: bool = False

def __post_init__(self):
if not self.schema_path and not self.remote_schema_url:
Expand All @@ -54,6 +75,37 @@ def __post_init__(self):
if self.remote_schema_url:
self.remote_schema_url = resolve_env_vars_in_string(self.remote_schema_url)

@property
def using_remote_schema(self) -> bool:
"""
Return true if remote schema is used as source, false otherwise.
"""
return bool(self.remote_schema_url) and not bool(self.schema_path)

@property
def introspection_settings(self) -> IntrospectionSettings:
"""
Return ``IntrospectionSettings`` instance build from provided configuration.
"""
return IntrospectionSettings(
descriptions=self.introspection_descriptions,
input_value_deprecation=self.introspection_input_value_deprecation,
specified_by_url=self.introspection_specified_by_url,
schema_description=self.introspection_schema_description,
directive_is_repeatable=self.introspection_directive_is_repeatable,
input_object_one_of=self.introspection_input_object_one_of,
)

def _introspection_settings_message(self) -> str:
"""
Return human readable message with introspection settings values.
"""
formatted = ", ".join(
f"{key}={str(value).lower()}"
for key, value in asdict(self.introspection_settings).items()
)
return f"Introspection settings: {formatted}"


@dataclass
class ClientSettings(BaseSettings):
Expand Down Expand Up @@ -177,10 +229,14 @@ def used_settings_message(self) -> str:
if self.include_typename
else "Not including __typename fields in generated queries."
)
introspection_msg = (
self._introspection_settings_message() if self.using_remote_schema else ""
)
return dedent(
f"""\
Selected strategy: {Strategy.CLIENT}
Using schema from '{self.schema_path or self.remote_schema_url}'.
{introspection_msg}
Reading queries from '{self.queries_path}'.
Using '{self.target_package_name}' as package name.
Generating package into '{self.target_package_path}'.
Expand Down Expand Up @@ -221,12 +277,16 @@ def used_settings_message(self):
if self.plugins
else "No plugin is being used."
)
introspection_msg = (
self._introspection_settings_message() if self.using_remote_schema else ""
)

if self.target_file_format == "py":
return dedent(
f"""\
Selected strategy: {Strategy.GRAPHQL_SCHEMA}
Using schema from {self.schema_path or self.remote_schema_url}
{introspection_msg}
Saving graphql schema to: {self.target_file_path}
Using {self.schema_variable_name} as variable name for schema.
Using {self.type_map_variable_name} as variable name for type map.
Expand All @@ -238,6 +298,7 @@ def used_settings_message(self):
f"""\
Selected strategy: {Strategy.GRAPHQL_SCHEMA}
Using schema from {self.schema_path or self.remote_schema_url}
{introspection_msg}
Saving graphql schema to: {self.target_file_path}
{plugins_msg}
"""
Expand Down
84 changes: 81 additions & 3 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
read_graphql_file,
walk_graphql_files,
)
from ariadne_codegen.settings import IntrospectionSettings


@pytest.fixture
Expand Down Expand Up @@ -298,9 +299,11 @@ def test_introspect_remote_schema_raises_introspection_error_for_not_dict_respon
return_value=httpx.Response(status_code=200, content="[]"),
)

with pytest.raises(IntrospectionError):
with pytest.raises(IntrospectionError) as exc:
introspect_remote_schema("http://testserver/graphql/")

assert "Invalid introspection result format." in str(exc.value)


def test_introspect_remote_schema_raises_introspection_error_for_json_without_data_key(
mocker,
Expand All @@ -310,9 +313,11 @@ def test_introspect_remote_schema_raises_introspection_error_for_json_without_da
return_value=httpx.Response(status_code=200, content='{"not_data": null}'),
)

with pytest.raises(IntrospectionError):
with pytest.raises(IntrospectionError) as exc:
introspect_remote_schema("http://testserver/graphql/")

assert "Invalid data key in introspection result." in str(exc.value)


def test_introspect_remote_schema_raises_introspection_error_for_graphql_errors(mocker):
mocker.patch(
Expand Down Expand Up @@ -349,9 +354,11 @@ def test_introspect_remote_schema_raises_introspection_error_for_invalid_data_va
),
)

with pytest.raises(IntrospectionError):
with pytest.raises(IntrospectionError) as exc:
introspect_remote_schema("http://testserver/graphql/")

assert "Invalid data key in introspection result." in str(exc.value)


def test_introspect_remote_schema_returns_introspection_result(mocker):
mocker.patch(
Expand Down Expand Up @@ -440,3 +447,74 @@ def test_get_graphql_queries_with_invalid_query_for_schema_raises_invalid_operat
get_graphql_queries(
invalid_query_for_schema_file.as_posix(), build_schema(schema_str)
)


def test_introspect_remote_schema_passes_introspection_settings_to_introspection_query(
mocker,
):
"""
Test that the introspection settings are passed to the get_introspection_query
function when introspecting the remote schema.
"""
mocked_get_query = mocker.patch(
"ariadne_codegen.schema.get_introspection_query",
return_value="query { __schema { queryType { name } } }",
)
mocker.patch(
"ariadne_codegen.schema.httpx.post",
return_value=httpx.Response(
status_code=200, content='{"data": {"__schema": {}}}'
),
)

settings = IntrospectionSettings(
descriptions=True,
input_value_deprecation=True,
specified_by_url=True,
schema_description=True,
directive_is_repeatable=True,
input_object_one_of=True,
)

introspect_remote_schema(
"http://testserver/graphql/", introspection_settings=settings
)

mocked_get_query.assert_called_once_with(
descriptions=True,
specified_by_url=True,
directive_is_repeatable=True,
schema_description=True,
input_value_deprecation=True,
input_object_one_of=True,
)


def test_introspect_remote_schema_uses_default_introspection_settings_when_not_provided(
mocker,
):
"""
Test that when introspection settings are not provided, the default values are used
in the get_introspection_query call.
"""
mocked_get_query = mocker.patch(
"ariadne_codegen.schema.get_introspection_query",
return_value="query { __schema { queryType { name } } }",
)
mocker.patch(
"ariadne_codegen.schema.httpx.post",
return_value=httpx.Response(
status_code=200, content='{"data": {"__schema": {}}}'
),
)

introspect_remote_schema("http://testserver/graphql/")

mocked_get_query.assert_called_once_with(
descriptions=False,
specified_by_url=False,
directive_is_repeatable=False,
schema_description=False,
input_value_deprecation=False,
input_object_one_of=False,
)
Loading
Loading