grafbase-local-common 0.15.0

Common code used in multiple crates in the CLI workspace
Documentation
#![allow(dead_code)]

use crate::{
    consts::{DOT_GRAFBASE_DIRECTORY, GRAFBASE_DIRECTORY, GRAFBASE_SCHEMA, REGISTRY_FILE},
    errors::CommonError,
};
use once_cell::sync::OnceCell;
use std::{
    env,
    path::{Path, PathBuf},
};

/// a static representation of the current environment
///
/// must be initialized before use
#[derive(Debug)]
pub struct Environment {
    /// the path of the (assumed) user project root (`$PROJECT`), the nearest ancestor directory
    /// with a `grafbase/schema.graphql` file
    pub project_path: PathBuf,
    /// the path of `$PROJECT/.grafbase/`, the Grafbase local developer tool cache and database directory,
    /// in the nearest ancestor directory with `grafbase/schema.graphql`
    pub project_dot_grafbase_path: PathBuf,
    /// the path of `$PROJECT/grafbase/`, the Grafbase schema directory in the nearest ancestor directory
    /// with `grafbase/schema.graphql`
    pub project_grafbase_path: PathBuf,
    /// the path of `$PROJECT/grafbase/schema.graphql`, the Grafbase schema,
    /// in the nearest ancestor directory with said directory and file
    pub project_grafbase_schema_path: PathBuf,
    /// the path of `$HOME/.grafbase`, the user level local developer tool cache directory
    pub user_dot_grafbase_path: PathBuf,
    /// the path of `$PROJECT/.grafbase/registry.json`, the registry derived from `schema.graphql`,
    /// in the nearest ancestor directory with a `grabase/schema.graphql` file
    pub project_grafbase_registry_path: PathBuf,
}

/// static singleton for the environment struct
static ENVIRONMENT: OnceCell<Environment> = OnceCell::new();

impl Environment {
    /// initializes the static Environment instance
    ///
    /// # Errors
    ///
    /// returns [`CommonError::ReadCurrentDirectory`] if the current directory path cannot be read
    ///
    /// returns [`CommonError::FindGrafbaseDirectory`] if the grafbase directory is not found
    pub fn try_init() -> Result<(), CommonError> {
        let project_grafbase_schema_path =
            Self::get_project_grafbase_path()?.ok_or(CommonError::FindGrafbaseDirectory)?;
        let project_grafbase_path = project_grafbase_schema_path
            .parent()
            .expect("the schema directory must have a parent by definiton")
            .to_path_buf();
        let project_path = project_grafbase_path
            .parent()
            .expect("the grafbase directory must have a parent directory by definition")
            .to_path_buf();
        let project_dot_grafbase_path = project_path.join(DOT_GRAFBASE_DIRECTORY);
        let user_dot_grafbase_path = {
            let home = dirs::home_dir().unwrap_or_else(|| project_grafbase_path.clone());
            home.join(DOT_GRAFBASE_DIRECTORY)
        };
        let project_grafbase_registry_path = project_dot_grafbase_path.join(REGISTRY_FILE);
        ENVIRONMENT
            .set(Self {
                project_path,
                project_dot_grafbase_path,
                project_grafbase_path,
                project_grafbase_schema_path,
                user_dot_grafbase_path,
                project_grafbase_registry_path,
            })
            .expect("cannot set environment twice");

        Ok(())
    }

    /// returns a reference to the static Environment instance
    ///
    /// # Panics
    ///
    /// panics if the Environment object was not previously initialized using `Environment::try_init()`
    #[must_use]
    pub fn get() -> &'static Self {
        match ENVIRONMENT.get() {
            Some(environment) => environment,
            // must be initialized in `main`
            #[allow(clippy::panic)]
            None => panic!("the environment object is uninitialized"),
        }
    }

    /// searches for the closest ancestor directory
    /// named "grafbase" which contains a "schema.graphql" file.
    /// if already inside a `grafbase` directory, looks for `schema.graphql` inside the current ancestor as well
    ///
    /// # Errors
    ///
    /// returns [`CommonError::ReadCurrentDirectory`] if the current directory path cannot be read
    fn get_project_grafbase_path() -> Result<Option<PathBuf>, CommonError> {
        let project_grafbase_path = env::current_dir()
            .map_err(|_| CommonError::ReadCurrentDirectory)?
            .ancestors()
            .find_map(|ancestor| {
                let mut path = PathBuf::from(ancestor);

                // if we're looking at a directory called `grafbase`, also check for the schema in the current directory
                if let Some(first) = path.components().next() {
                    if Path::new(&first) == PathBuf::from(GRAFBASE_DIRECTORY) {
                        path.push(GRAFBASE_SCHEMA);
                        if path.is_file() {
                            return Some(path);
                        }
                        path.pop();
                    }
                }

                path.push([GRAFBASE_DIRECTORY, GRAFBASE_SCHEMA].iter().collect::<PathBuf>());

                if path.is_file() {
                    Some(path)
                } else {
                    None
                }
            });

        Ok(project_grafbase_path)
    }
}