fraiseql-core 2.2.0

Core execution engine for FraiseQL v2 - Compiled GraphQL over SQL
Documentation
//! Field introspection helpers.
//!
//! Converts `FieldDefinition`, `FieldType`, and validation rules from the compiled
//! schema into their `__Field`, `__InputValue`, and related introspection types.

use super::{
    super::{FieldDefinition, FieldType},
    types::{
        IntrospectionField, IntrospectionInputValue, IntrospectionType,
        IntrospectionValidationRule, TypeKind,
    },
};

// =============================================================================
// Field and type-reference helpers (used by IntrospectionBuilder)
// =============================================================================

/// Build `__Field` introspection from `FieldDefinition`.
pub(super) fn build_field(field: &FieldDefinition) -> IntrospectionField {
    IntrospectionField {
        name:               field.output_name().to_string(),
        description:        field.description.clone(),
        args:               vec![], // Regular fields don't have args
        field_type:         field_type_to_introspection(&field.field_type, field.nullable),
        is_deprecated:      field.is_deprecated(),
        deprecation_reason: field.deprecation_reason().map(ToString::to_string),
    }
}

/// Convert `FieldType` to introspection type, wrapping in `NON_NULL` when not nullable.
pub(super) fn field_type_to_introspection(
    field_type: &FieldType,
    nullable: bool,
) -> IntrospectionType {
    let inner = match field_type {
        FieldType::Int => type_ref("Int"),
        FieldType::Float => type_ref("Float"),
        FieldType::String => type_ref("String"),
        FieldType::Boolean => type_ref("Boolean"),
        FieldType::Id => type_ref("ID"),
        FieldType::DateTime => type_ref("DateTime"),
        FieldType::Date => type_ref("Date"),
        FieldType::Time => type_ref("Time"),
        FieldType::Uuid => type_ref("UUID"),
        // JSON and Vector are both exposed as JSON in introspection
        FieldType::Json | FieldType::Vector => type_ref("JSON"),
        FieldType::Decimal => type_ref("Decimal"),
        FieldType::Scalar(name) => type_ref(name), // Rich/custom scalars
        FieldType::Object(name) => type_ref_with_kind(name, TypeKind::Object),
        FieldType::Enum(name) => type_ref_with_kind(name, TypeKind::Enum),
        FieldType::Input(name) => type_ref_with_kind(name, TypeKind::InputObject),
        FieldType::Interface(name) => type_ref_with_kind(name, TypeKind::Interface),
        FieldType::Union(name) => type_ref_with_kind(name, TypeKind::Union),
        FieldType::List(inner) => IntrospectionType {
            kind:               TypeKind::List,
            name:               None,
            description:        None,
            fields:             None,
            interfaces:         None,
            possible_types:     None,
            enum_values:        None,
            input_fields:       None,
            of_type:            Some(Box::new(field_type_to_introspection(inner, true))),
            specified_by_u_r_l: None,
        },
    };

    if nullable {
        inner
    } else {
        // Wrap in NON_NULL
        IntrospectionType {
            kind:               TypeKind::NonNull,
            name:               None,
            description:        None,
            fields:             None,
            interfaces:         None,
            possible_types:     None,
            enum_values:        None,
            input_fields:       None,
            of_type:            Some(Box::new(inner)),
            specified_by_u_r_l: None,
        }
    }
}

/// Create a named type reference node with `TypeKind::Scalar`.
///
/// Used for built-in and custom scalar types. For composite types (Object,
/// Enum, `InputObject`, Interface, Union) use [`type_ref_with_kind`] instead.
pub fn type_ref(name: &str) -> IntrospectionType {
    type_ref_with_kind(name, TypeKind::Scalar)
}

/// Create a named type reference node with the given `TypeKind`.
///
/// Introspection clients (`GraphiQL`, code-generators) rely on the `kind`
/// field to distinguish Object from Enum from `InputObject` etc. Hardcoding
/// `Scalar` for every named type breaks tooling that reads `__schema`.
pub fn type_ref_with_kind(name: &str, kind: TypeKind) -> IntrospectionType {
    IntrospectionType {
        kind,
        name: Some(name.to_string()),
        description: None,
        fields: None,
        interfaces: None,
        possible_types: None,
        enum_values: None,
        input_fields: None,
        of_type: None,
        specified_by_u_r_l: None,
    }
}

/// Convert a `ValidationRule` to its introspection representation.
pub(super) fn build_validation_rule(
    rule: &crate::validation::rules::ValidationRule,
) -> IntrospectionValidationRule {
    use crate::validation::rules::ValidationRule;

    match rule {
        ValidationRule::Required => IntrospectionValidationRule {
            rule_type:       "required".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     Some("Field is required".to_string()),
        },
        ValidationRule::Pattern { pattern, message } => IntrospectionValidationRule {
            rule_type:       "pattern".to_string(),
            pattern:         Some(pattern.clone()),
            pattern_message: message.clone(),
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     message.clone(),
        },
        ValidationRule::Length { min, max } => IntrospectionValidationRule {
            rule_type:       "length".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             min.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
            max:             max.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     None,
        },
        ValidationRule::Range { min, max } => IntrospectionValidationRule {
            rule_type:       "range".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             *min,
            max:             *max,
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     None,
        },
        ValidationRule::Enum { values } => IntrospectionValidationRule {
            rule_type:       "enum".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  Some(values.clone()),
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     None,
        },
        ValidationRule::Checksum { algorithm } => IntrospectionValidationRule {
            rule_type:       "checksum".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       Some(algorithm.clone()),
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     None,
        },
        ValidationRule::CrossField { field, operator } => IntrospectionValidationRule {
            rule_type:       "cross_field".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       None,
            field_reference: Some(field.clone()),
            operator:        Some(operator.clone()),
            field_list:      None,
            description:     None,
        },
        ValidationRule::Conditional { .. } => IntrospectionValidationRule {
            rule_type:       "conditional".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     Some("Conditional validation".to_string()),
        },
        ValidationRule::All(_) => IntrospectionValidationRule {
            rule_type:       "all".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     Some("All rules must pass".to_string()),
        },
        ValidationRule::Any(_) => IntrospectionValidationRule {
            rule_type:       "any".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     Some("At least one rule must pass".to_string()),
        },
        ValidationRule::OneOf { fields } => IntrospectionValidationRule {
            rule_type:       "one_of".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      Some(fields.clone()),
            description:     None,
        },
        ValidationRule::AnyOf { fields } => IntrospectionValidationRule {
            rule_type:       "any_of".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      Some(fields.clone()),
            description:     None,
        },
        ValidationRule::ConditionalRequired { .. } => IntrospectionValidationRule {
            rule_type:       "conditional_required".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     None,
        },
        ValidationRule::RequiredIfAbsent { .. } => IntrospectionValidationRule {
            rule_type:       "required_if_absent".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     None,
        },
        ValidationRule::Email => IntrospectionValidationRule {
            rule_type:       "email".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     Some("Must be a valid email address".to_string()),
        },
        ValidationRule::Phone => IntrospectionValidationRule {
            rule_type:       "phone".to_string(),
            pattern:         None,
            pattern_message: None,
            min:             None,
            max:             None,
            allowed_values:  None,
            algorithm:       None,
            field_reference: None,
            operator:        None,
            field_list:      None,
            description:     Some("Must be a valid E.164 phone number".to_string()),
        },
    }
}

/// Build `__InputValue` for a query/mutation/subscription argument.
pub(super) fn build_arg_input_value(
    arg: &super::super::ArgumentDefinition,
) -> IntrospectionInputValue {
    IntrospectionInputValue {
        name:               arg.name.clone(),
        description:        arg.description.clone(),
        input_type:         field_type_to_introspection(&arg.arg_type, arg.nullable),
        default_value:      arg.default_value.as_ref().map(|v| v.to_string()),
        is_deprecated:      arg.is_deprecated(),
        deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
        validation_rules:   vec![],
    }
}