nautilus-orm-schema 1.0.1

Schema parsing and validation for Nautilus ORM
Documentation
use super::*;

impl SchemaValidator {
    pub(super) fn validate_datasources(&mut self) {
        let datasources: Vec<_> = self
            .schema
            .declarations
            .iter()
            .filter_map(|decl| match decl {
                Declaration::Datasource(datasource) => Some(datasource.clone()),
                _ => None,
            })
            .collect();

        for datasource in &datasources {
            self.validate_datasource(datasource);
        }
    }

    pub(super) fn validate_datasource(&mut self, datasource: &DatasourceDecl) {
        for field in &datasource.fields {
            if !KNOWN_DATASOURCE_FIELDS.contains(&field.name.value.as_str()) {
                self.errors.push_back(SchemaError::Validation(
                    format!(
                        "Unknown field '{}' in datasource block. Valid fields: {}",
                        field.name.value,
                        KNOWN_DATASOURCE_FIELDS.join(", ")
                    ),
                    field.span,
                ));
            }
        }

        if let Err(err) = Self::datasource_provider_value(datasource) {
            self.errors.push_back(err);
        }

        if let Err(err) = Self::datasource_url_value(datasource) {
            self.errors.push_back(err);
        }

        if let Err(err) = Self::datasource_direct_url_value(datasource) {
            self.errors.push_back(err);
        }
    }

    pub(super) fn validate_generators(&mut self) {
        let generators: Vec<_> = self
            .schema
            .declarations
            .iter()
            .filter_map(|decl| match decl {
                Declaration::Generator(generator) => Some(generator.clone()),
                _ => None,
            })
            .collect();

        for generator in &generators {
            self.validate_generator(generator);
        }
    }

    pub(super) fn validate_generator(&mut self, generator: &GeneratorDecl) {
        let provider_info = match Self::generator_provider_info(generator) {
            Ok(info) => Some(info),
            Err(err) => {
                self.errors.push_back(err);
                None
            }
        };

        if let Err(err) = Self::generator_output_value(generator) {
            self.errors.push_back(err);
        }

        if let Err(err) = Self::generator_interface_kind(generator) {
            self.errors.push_back(err);
        }

        if let Some((provider, client_provider)) = provider_info.as_ref() {
            if let Err(err) =
                Self::generator_recursive_type_depth(generator, *client_provider, provider)
            {
                self.errors.push_back(err);
            }
        }

        let valid_fields = Self::valid_generator_fields(provider_info.as_ref().map(|(_, p)| *p));
        let valid_fields_text = valid_fields.join(", ");

        for field in &generator.fields {
            if valid_fields.contains(&field.name.value.as_str()) {
                continue;
            }

            let message = if let Some((provider, _)) = provider_info.as_ref() {
                format!(
                    "Unknown field '{}' in generator block. Valid fields for '{}': {}",
                    field.name.value, provider, valid_fields_text
                )
            } else {
                format!(
                    "Unknown field '{}' in generator block. Valid fields: {}",
                    field.name.value, valid_fields_text
                )
            };

            self.errors
                .push_back(SchemaError::Validation(message, field.span));
        }
    }

    pub(super) fn datasource_provider_value(datasource: &DatasourceDecl) -> Result<String> {
        let provider_field = datasource.find_field("provider").ok_or_else(|| {
            SchemaError::Validation(
                "Datasource missing required 'provider' field".to_string(),
                datasource.span,
            )
        })?;

        let provider = if let Expr::Literal(Literal::String(s, _)) = &provider_field.value {
            s.clone()
        } else {
            return Err(SchemaError::Validation(
                "Datasource 'provider' must be a string literal".to_string(),
                provider_field.span,
            ));
        };

        if provider.parse::<DatabaseProvider>().is_err() {
            return Err(SchemaError::Validation(
                format!(
                    "Unknown datasource provider '{}'. Valid providers: {}",
                    provider,
                    DatabaseProvider::ALL.join(", ")
                ),
                provider_field.span,
            ));
        }

        Ok(provider)
    }

    pub(super) fn datasource_url_value(datasource: &DatasourceDecl) -> Result<String> {
        Self::datasource_optional_url_value(datasource, "url")?.ok_or_else(|| {
            SchemaError::Validation(
                "Datasource missing required 'url' field".to_string(),
                datasource.span,
            )
        })
    }

    pub(super) fn datasource_direct_url_value(
        datasource: &DatasourceDecl,
    ) -> Result<Option<String>> {
        Self::datasource_optional_url_value(datasource, "direct_url")
    }

    fn datasource_optional_url_value(
        datasource: &DatasourceDecl,
        field_name: &str,
    ) -> Result<Option<String>> {
        let Some(url_field) = datasource.find_field(field_name) else {
            return Ok(None);
        };

        match &url_field.value {
            Expr::Literal(Literal::String(s, _)) => Ok(Some(s.clone())),
            Expr::FunctionCall { name, args, .. } if name.value == "env" => match args.as_slice() {
                [Expr::Literal(Literal::String(var_name, _))] => {
                    Ok(Some(format!("env({})", var_name)))
                }
                _ => Err(SchemaError::Validation(
                    format!(
                        "Datasource '{}' env() call requires a single string argument",
                        field_name
                    ),
                    url_field.span,
                )),
            },
            _ => Err(SchemaError::Validation(
                format!(
                    "Datasource '{}' must be a string literal or env() call",
                    field_name
                ),
                url_field.span,
            )),
        }
    }

    pub(super) fn generator_provider_info(
        generator: &GeneratorDecl,
    ) -> Result<(String, ClientProvider)> {
        let provider_field = generator.find_field("provider").ok_or_else(|| {
            SchemaError::Validation(
                "Generator missing required 'provider' field".to_string(),
                generator.span,
            )
        })?;

        let provider = if let Expr::Literal(Literal::String(s, _)) = &provider_field.value {
            s.clone()
        } else {
            return Err(SchemaError::Validation(
                "Generator 'provider' must be a string literal".to_string(),
                provider_field.span,
            ));
        };

        let client_provider = if let Ok(provider_kind) = provider.parse::<ClientProvider>() {
            provider_kind
        } else {
            return Err(SchemaError::Validation(
                format!(
                    "Unknown generator provider '{}'. Valid providers: {}",
                    provider,
                    ClientProvider::ALL.join(", ")
                ),
                provider_field.span,
            ));
        };

        Ok((provider, client_provider))
    }

    pub(super) fn generator_output_value(generator: &GeneratorDecl) -> Result<Option<String>> {
        let Some(output_field) = generator.find_field("output") else {
            return Ok(None);
        };

        match &output_field.value {
            Expr::Literal(Literal::String(s, _)) => Ok(Some(s.clone())),
            _ => Err(SchemaError::Validation(
                "Generator 'output' must be a string literal".to_string(),
                output_field.span,
            )),
        }
    }

    pub(super) fn generator_interface_kind(generator: &GeneratorDecl) -> Result<InterfaceKind> {
        let Some(iface_field) = generator.find_field("interface") else {
            return Ok(InterfaceKind::Sync);
        };

        let iface = if let Expr::Literal(Literal::String(s, _)) = &iface_field.value {
            s.as_str()
        } else {
            return Err(SchemaError::Validation(
                "Generator 'interface' must be a string literal".to_string(),
                iface_field.span,
            ));
        };

        match iface {
            "sync" => Ok(InterfaceKind::Sync),
            "async" | "asyncio" => Ok(InterfaceKind::Async),
            other => Err(SchemaError::Validation(
                format!(
                    "Invalid value '{}' for generator field 'interface'. \
                     Allowed values: \"sync\", \"async\"",
                    other
                ),
                iface_field.span,
            )),
        }
    }

    pub(super) fn generator_recursive_type_depth(
        generator: &GeneratorDecl,
        client_provider: ClientProvider,
        provider: &str,
    ) -> Result<usize> {
        let Some(depth_field) = generator.find_field("recursive_type_depth") else {
            return Ok(5);
        };

        if client_provider != ClientProvider::Python {
            return Err(SchemaError::Validation(
                format!(
                    "Field 'recursive_type_depth' is only supported for \
                     'nautilus-client-py', not for '{}'",
                    provider
                ),
                depth_field.span,
            ));
        }

        if let Expr::Literal(Literal::Number(s, _)) = &depth_field.value {
            Ok(s.parse::<usize>().unwrap_or(5).max(1))
        } else {
            Err(SchemaError::Validation(
                "Invalid value for generator field 'recursive_type_depth'. Expected a positive integer.".to_string(),
                depth_field.span,
            ))
        }
    }

    pub(super) fn valid_generator_fields(
        client_provider: Option<ClientProvider>,
    ) -> Vec<&'static str> {
        let mut fields = KNOWN_GENERATOR_FIELDS.to_vec();
        if client_provider.is_none() || client_provider == Some(ClientProvider::Python) {
            fields.extend_from_slice(PYTHON_ONLY_GENERATOR_FIELDS);
        }
        fields
    }
}