laburnum 1.17.0

An LSP framework for building language servers and compilers, powered by an incremental query tree with content-addressed storage, task-based dataflow, and parallel queries.
Documentation
// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

/// Macro for defining compile-time watcher handlers using PK-based dispatch.
///
/// Implements the KeyWatcher trait for the specified struct, generating a
/// struct that holds handler lists per PK, with each handler optionally having
/// glob filtering.
///
/// # Syntax
///
/// ```ignore
/// watchers! {
///   (
///     Server: MyServer,
///     Storage: MyStorage,
///   ),
///   pk => handler_function,
///   pk => another_handler("*.rs", "*.toml"),
/// }
/// ```
///
/// # Example
///
/// ```ignore
/// watchers! {
///   (
///     Server: MyLanguageServer,
///     Storage: MyStorage,
///   ),
///   MyPartition::KEY => on_symbol_change,
///   ConfigPartition::KEY => on_config_change("*.rs"),
/// }
///
/// async fn on_symbol_change<P: Partitions, T: LanguageServer<P>>(
///   ctx: &mut TaskContext<P, T>,
///   writer: &mut PartitionWriteContextRef<'_, P>,
/// ) {
///   // Each key is a RecordKey with opaque partition_key + sort_key
///   for key in ctx.matched_keys_updated() {
///     let results = key.get_record(ctx.query_client()).await;
///     // process results...
///   }
/// }
/// ```
#[macro_export]
macro_rules! watchers {
  // Entry point - with Server and Storage (with globs)
  (
    (
      Server: $server:ident,
      Storage: $storage:ty $(,)?
    ),
    $(
      $pk:path => $handler_fn:ident ($($glob:literal),+ $(,)?)
    ),* $(,)?
  ) => {
    $crate::watchers!(@expand_entries $server, [$storage] [] $($pk => $handler_fn ($($glob),+)),*);
  };

  // Entry point - with Server and Storage (no globs)
  (
    (
      Server: $server:ident,
      Storage: $storage:ty $(,)?
    ),
    $(
      $pk:path => $handler_fn:ident
    ),* $(,)?
  ) => {
    $crate::watchers!(@expand_entries $server, [$storage] [] $($pk => $handler_fn),*);
  };



  // Expand entries - base case: all entries expanded, now continue to impl (with storage bounds)
  (@expand_entries $struct_name:ident, [$storage_ty:ty] [$($expanded:tt)*]) => {
    $crate::watchers!(@impl $struct_name, [$storage_ty]
      // User-defined watchers
      $($expanded)*
    );
  };

  // Expand entries - with globs: duplicate handler as path (with storage bounds)
  (@expand_entries $struct_name:ident, [$storage_ty:ty] [$($expanded:tt)*] $pk:path => $handler:ident ($($glob:literal),+) $(, $($rest:tt)*)?) => {
    $crate::watchers!(@expand_entries $struct_name, [$storage_ty] [
      $($expanded)*
      [$pk] => $handler [$handler] [$($glob),+],
    ] $($($rest)*)?);
  };

  // Expand entries - without globs: duplicate handler as path (with storage bounds)
  (@expand_entries $struct_name:ident, [$storage_ty:ty] [$($expanded:tt)*] $pk:path => $handler:ident $(, $($rest:tt)*)?) => {
    $crate::watchers!(@expand_entries $struct_name, [$storage_ty] [
      $($expanded)*
      [$pk] => $handler [$handler],
    ] $($($rest)*)?);
  };

  // Implementation - collect all handlers and generate structs (with storage bounds)
  (@impl $struct_name:ident, [$storage_ty:ty] $($handlers:tt)*) => {
    $crate::watchers!(@collect_handlers $struct_name, [$storage_ty] [] $($handlers)*);
  };

  // Collect handlers - base case, forward to generate (with storage bounds)
  (@collect_handlers $struct_name:ident, [$storage_ty:ty] [$($grouped:tt)*]) => {
    $crate::watchers!(@generate_all $struct_name, [$storage_ty] $($grouped)*);
  };

  // Collect handlers - with globs (with storage bounds)
  (@collect_handlers $struct_name:ident, [$storage_ty:ty] [$($grouped:tt)*] [$pk:path] => $handler:ident [$handler_path:path] [$($glob:literal),+] $(, $($rest:tt)*)?) => {
    $crate::watchers!(@collect_handlers $struct_name, [$storage_ty] [
      $($grouped)*
      ([$pk], $handler, $handler_path, [$($glob),+])
    ] $($($rest)*)?);
  };

  // Collect handlers - without globs (with storage bounds)
  (@collect_handlers $struct_name:ident, [$storage_ty:ty] [$($grouped:tt)*] [$pk:path] => $handler:ident [$handler_path:path] $(, $($rest:tt)*)?) => {
    $crate::watchers!(@collect_handlers $struct_name, [$storage_ty] [
      $($grouped)*
      ([$pk], $handler, $handler_path)
    ] $($($rest)*)?);
  };

  // Generate all structures and impl - takes tuples: ([pk], handler_name, handler_path, optional_glob_array) (with storage bounds)
  (@generate_all $struct_name:ident, [$storage_ty:ty] $(([$pk:path], $handler:ident, $handler_path:path $(, [$($glob:literal),+])?)) *) => {
    paste::paste! {
      struct [<$struct_name HandlerMeta>] {
        task_id: $crate::Ident,
        pk: $crate::Ident,
        sk_globset: Option<globset::GlobSet>,
      }

      struct [<$struct_name Meta>] {
        $(
          [<$handler:snake>]: [<$struct_name HandlerMeta>],
        )*
      }

      static [<$struct_name:upper _META>]: std::sync::LazyLock<[<$struct_name Meta>]> = std::sync::LazyLock::new(|| {
        [<$struct_name Meta>] {
          $(
            [<$handler:snake>]: [<$struct_name HandlerMeta>] {
              task_id: $crate::Ident::new(concat!("watcher:", stringify!($pk), ":", stringify!($handler))),
              pk: $pk,
              sk_globset: $crate::watchers!(@build_globset $handler, $([$($glob),+])?),
            },
          )*
        }
      });

      impl $crate::scheduler::key_watcher::KeyWatcher<$storage_ty, $struct_name> for $struct_name
      {
        fn dispatch_watcher<F>(
          pk: $crate::Ident,
          updated_sks: Vec<String>,
          deleted_sks: Vec<String>,
          spawn: F,
        )
        where
          F: Fn(
            $crate::Ident,
            Vec<String>,
            Vec<String>,
            for<'a> fn(
              &'a mut $crate::scheduler::task::TaskContext<$storage_ty, $struct_name>,
              &'a mut $crate::database::PartitionWriteContextRef<'a, $storage_ty>)
                -> std::pin::Pin<std::boxed::Box<dyn std::future::Future<Output = $crate::scheduler::key_watcher::WatcherResult<$storage_ty, $struct_name>> + std::marker::Send + 'a>>),
        {
          $(
            if pk == [<$struct_name:upper _META>].[<$handler:snake>].pk {
              $crate::watchers!(@spawn_filtered $struct_name, pk, updated_sks, deleted_sks, spawn, $handler, $handler_path $(, [$($glob),+])?);
            }
          )*
        }
      }
    }
  };

  // Spawn with filtering - no globs (spawn all keys)
  (@spawn_filtered $struct_name:ty, $pk:ident, $updated_sks:ident, $deleted_sks:ident, $spawn:ident, $handler:ident, $handler_path:path) => {
    paste::paste! {
      if !$updated_sks.is_empty() || !$deleted_sks.is_empty() {
        let meta = &[<$struct_name:upper _META>].[<$handler:snake>];
        $spawn(meta.pk.clone(), $updated_sks.clone(), $deleted_sks.clone(), $handler_path);
      }
    }
  };

  // Spawn with filtering - with globs
  (@spawn_filtered $struct_name:ty, $pk:ident, $updated_sks:ident, $deleted_sks:ident, $spawn:ident, $handler:ident, $handler_path:path, [$($glob:literal),+]) => {
    paste::paste! {
      {
        let meta = &[<$struct_name:upper _META>].[<$handler:snake>];
        let Some(glob_set) = meta.sk_globset.as_ref() else {
          // No glob set configured - skip filtering
          return;
        };
        let mut filtered_updated = $updated_sks.clone();
        let mut filtered_deleted = $deleted_sks.clone();
        filtered_updated.retain(|sk| glob_set.is_match(sk));
        filtered_deleted.retain(|sk| glob_set.is_match(sk));
        if !filtered_updated.is_empty() || !filtered_deleted.is_empty() {
          $spawn(meta.pk.clone(), filtered_updated, filtered_deleted, $handler_path);
        }
      }
    }
  };

  // Helper: create optional GlobSet for LazyLock initialization
  (@build_globset $handler:ident,) => { None };
  (@build_globset $handler:ident, [$($glob:literal),+]) => {
    {
      let mut builder = globset::GlobSetBuilder::new();
      $(
        match globset::Glob::new($glob) {
          | Ok(glob) => { builder.add(glob); },
          | Err(e) => {
            eprintln!("Warning: Invalid glob pattern `{}` in handler `{}`: {}", $glob, stringify!($handler), e);
          }
        }
      )+
      match builder.build() {
        | Ok(globset) => Some(globset),
        | Err(e) => {
          eprintln!("Warning: Failed to build GlobSet for handler `{}`: {}", stringify!($handler), e);
          None
        }
      }
    }
  };

  // Helper: create optional GlobSet (legacy)
  (@make_globset) => { None };
  (@make_globset $($glob:literal),+) => {
    {
      let mut builder = globset::GlobSetBuilder::new();
      $(
        match globset::Glob::new($glob) {
          | Ok(glob) => { builder.add(glob); },
          | Err(e) => {
            eprintln!("Warning: Invalid glob pattern `{}`: {}", $glob, e);
          }
        }
      )+
      match builder.build() {
        | Ok(globset) => Some(globset),
        | Err(e) => {
          eprintln!("Warning: Failed to build GlobSet: {}", e);
          None
        }
      }
    }
  };
}