Source code for ace.core.graphql.selection

"""
GraphQL Selection Set Module

This module provides classes for building GraphQL selection sets,
including support for nested fields, predefined selections, and directives.

Directives:
    Supports @include, @skip, and custom directives on nested fields.

    # Using shortcuts
    builder.nested("addMetadata", args={"metadata": "$metadata"})
        .include_if("$includeMetadata")
        .select("entityType")
    .end()

    # Using generic directive
    builder.nested("someField")
        .directive("cacheControl", {"maxAge": 30})
        .select("id")
    .end()
"""

from typing import List, Dict, Any, Optional, Union, TYPE_CHECKING

if TYPE_CHECKING:
    from ace.core.graphql.builder import OperationBuilder


# ==============================================================================================================
# Directive Classes
# ==============================================================================================================

[docs] class Directive: """ Represents a GraphQL directive (e.g., @include, @skip, @deprecated). Args: name: Directive name (without @) args: Directive arguments Example: Directive("include", {"if": "$includeMetadata"}) -> @include(if: $includeMetadata) Directive("skip", {"if": "$hideField"}) -> @skip(if: $hideField) Directive("deprecated", {"reason": "Use newField"}) -> @deprecated(reason: "Use newField") """
[docs] def __init__(self, name: str, args: Optional[Dict[str, Any]] = None): self.name = name self.args = args or {}
[docs] def build(self) -> str: """Build the directive string.""" if not self.args: return f"@{self.name}" args_parts = [] for key, value in self.args.items(): if isinstance(value, str) and value.startswith("$"): args_parts.append(f"{key}: {value}") elif isinstance(value, str): args_parts.append(f'{key}: "{value}"') elif isinstance(value, bool): args_parts.append(f"{key}: {str(value).lower()}") elif value is None: args_parts.append(f"{key}: null") else: args_parts.append(f"{key}: {value}") return f"@{self.name}({', '.join(args_parts)})"
def __repr__(self) -> str: return f"Directive(@{self.name}, args={self.args})" def __eq__(self, other) -> bool: if not isinstance(other, Directive): return False return self.name == other.name and self.args == other.args
[docs] class Include(Directive): """Shorthand for @include(if: $variable) directive."""
[docs] def __init__(self, variable: str): if not variable.startswith("$"): variable = f"${variable}" super().__init__("include", {"if": variable})
# ============================================================================================================== # SelectionSet — reusable, declarative selections # ==============================================================================================================
[docs] class SelectionSet: """ Represents a reusable GraphQL selection set. Can be used standalone or merged into builders using .use() Example: # Define a reusable selection POLICY_INFO = SelectionSet( "policyId", "policyNumber", "effectiveDate", nested={"coverages": SelectionSet("id", "coverageNumber")} ) # Use in builder builder.nested("info").use(POLICY_INFO).end() """
[docs] def __init__( self, *fields: str, nested: Optional[Dict[str, "SelectionSet"]] = None ): """ Initialize a SelectionSet. Args: *fields: Scalar field names to select nested: Dictionary of nested field name -> SelectionSet """ self.fields: List[str] = list(fields) self.nested: Dict[str, SelectionSet] = nested or {}
[docs] def add_field(self, field: str) -> "SelectionSet": """Add a scalar field to the selection.""" if field not in self.fields: self.fields.append(field) return self
[docs] def add_nested(self, name: str, selection: "SelectionSet") -> "SelectionSet": """Add a nested selection.""" self.nested[name] = selection return self
[docs] def merge(self, other: "SelectionSet") -> "SelectionSet": """ Merge another SelectionSet into this one. Args: other: SelectionSet to merge Returns: Self for chaining """ for f in other.fields: if f not in self.fields: self.fields.append(f) for name, selection in other.nested.items(): if name in self.nested: self.nested[name].merge(selection) else: self.nested[name] = selection return self
[docs] def build(self, indent: int = 0) -> str: """ Build the selection set string. Args: indent: Current indentation level (for formatting) Returns: GraphQL selection set string """ if not self.fields and not self.nested: return "" parts = [] # Add scalar fields parts.extend(self.fields) # Add nested selections for name, selection in self.nested.items(): nested_str = selection.build(indent + 1) if nested_str: parts.append(f"{name} {{ {nested_str} }}") else: parts.append(name) return " ".join(parts)
def __repr__(self) -> str: return f"SelectionSet(fields={self.fields}, nested={list(self.nested.keys())})"
# ============================================================================================================== # SelectionBuilder — fluent API for building selections # ==============================================================================================================
[docs] class SelectionBuilder: """ Builder for constructing selection sets with a fluent API. This is used internally by OperationBuilder but can also be used standalone. Supports directives on nested fields: builder.nested("addMetadata", args={"metadata": "$metadata"}) .include_if("$includeMetadata") .select("entityType") .end() """
[docs] def __init__(self, parent: Optional["OperationBuilder"] = None, field_name: Optional[str] = None, args: Optional[Dict[str, Any]] = None): """ Initialize a SelectionBuilder. Args: parent: Parent builder (for .end() navigation) field_name: Name of the field this selection is for args: Arguments for this field (if it's a nested operation) """ self._parent = parent self._field_name = field_name self._args = args or {} self._directives: List[Directive] = [] self._fields: List[str] = [] self._nested: Dict[str, "SelectionBuilder"] = {} self._nested_order: List[str] = [] # Preserve insertion order
[docs] def select(self, *fields: str) -> "SelectionBuilder": """ Add scalar fields to the selection. Args: *fields: Field names to select Returns: Self for chaining """ for f in fields: if f not in self._fields: self._fields.append(f) return self
[docs] def nested(self, field_name: str, args: Optional[Dict[str, Any]] = None) -> "SelectionBuilder": """ Add a nested field selection. Args: field_name: Name of the nested field args: Arguments for the nested field (for operations like linkParticipants) Returns: New SelectionBuilder for the nested field """ nested_builder = SelectionBuilder(parent=self, field_name=field_name, args=args) self._nested[field_name] = nested_builder if field_name not in self._nested_order: self._nested_order.append(field_name) return nested_builder
[docs] def use(self, selection: SelectionSet) -> "SelectionBuilder": """ Apply a predefined SelectionSet to this builder. Args: selection: SelectionSet to apply Returns: Self for chaining """ # Add scalar fields for f in selection.fields: if f not in self._fields: self._fields.append(f) # Add nested selections for name, nested_selection in selection.nested.items(): if name not in self._nested: nested_builder = SelectionBuilder(parent=self, field_name=name) self._nested[name] = nested_builder self._nested_order.append(name) self._nested[name].use(nested_selection) return self
# ================================== # Directive Support # ==================================
[docs] def directive(self, name: str, args: Optional[Dict[str, Any]] = None) -> "SelectionBuilder": """ Add a directive to this field. This is the generic method for adding any GraphQL directive. Args: name: Directive name (without @) args: Directive arguments Returns: Self for chaining Example: .nested("addMetadata", args={"metadata": "$metadata"}) .directive("include", {"if": "$includeMetadata"}) .select("entityType") .end() .nested("someField") .directive("cacheControl", {"maxAge": 30}) .select("id") .end() """ self._directives.append(Directive(name, args)) return self
[docs] def include_if(self, variable: str) -> "SelectionBuilder": """ Add @include(if: $variable) directive to this field. The field and its selections will only be included in the response when the variable evaluates to true. Args: variable: Boolean variable name (with or without $ prefix) Returns: Self for chaining Example: .nested("addMetadata", args={"metadata": "$metadata"}) .include_if("$includeMetadata") .select("entityType") .nested("data").select("key", "value").end() .end() """ self._directives.append(Include(variable)) return self
[docs] def skip_if(self, variable: str) -> "SelectionBuilder": """ Add @skip(if: $variable) directive to this field. The field and its selections will be skipped (excluded from response) when the variable evaluates to true. Args: variable: Boolean variable name (with or without $ prefix) Returns: Self for chaining Example: .nested("sensitiveData") .skip_if("$redacted") .select("ssn", "taxId") .end() """ self._directives.append(Skip(variable)) return self
# ================================== # Navigation and Building # ==================================
[docs] def end(self) -> Union["SelectionBuilder", "OperationBuilder"]: """ Return to the parent builder. Returns: Parent builder for continued chaining """ if self._parent is None: raise ValueError("Cannot call end() on root builder") return self._parent
def _format_args(self) -> str: """Format arguments for this field.""" if not self._args: return "" args_parts = [] for key, value in self._args.items(): if isinstance(value, str) and value.startswith("$"): args_parts.append(f"{key}: {value}") elif isinstance(value, str): args_parts.append(f'{key}: "{value}"') elif isinstance(value, bool): args_parts.append(f"{key}: {str(value).lower()}") elif value is None: args_parts.append(f"{key}: null") else: args_parts.append(f"{key}: {value}") return f"({', '.join(args_parts)})" def _format_directives(self) -> str: """Format all directives for this field.""" if not self._directives: return "" return " " + " ".join(d.build() for d in self._directives)
[docs] def build(self) -> str: """ Build the selection set string. Returns: GraphQL selection set string (without outer braces) """ parts = [] # Add scalar fields parts.extend(self._fields) # Add nested selections in order for name in self._nested_order: nested_builder = self._nested[name] nested_content = nested_builder.build() args_str = nested_builder._format_args() directives_str = nested_builder._format_directives() if nested_content: parts.append(f"{name}{args_str}{directives_str} {{ {nested_content} }}") else: parts.append(f"{name}{args_str}{directives_str}") return " ".join(parts)
[docs] def to_selection_set(self) -> SelectionSet: """ Convert this builder to a reusable SelectionSet. Note: Directives are not preserved in SelectionSet (they are execution-time concerns). Returns: SelectionSet representation """ nested = {} for name in self._nested_order: nested[name] = self._nested[name].to_selection_set() return SelectionSet(*self._fields, nested=nested)