#![forbid(unsafe_code)]
#![deny(missing_docs)]
pub use selene_core::Value;
use std::time::Duration;
use selene_core::DbString;
use crate::{GqlStatus, GqlType, runtime::ProcedureContext};
pub trait ProcedureRegistry: Send + Sync {
fn lookup(&self, name: &[DbString]) -> Option<ProcedureMetadata>;
fn registry_version(&self) -> u64 {
0
}
fn iter_handles(&self) -> Box<dyn Iterator<Item = (Vec<DbString>, ProcedureMetadata)> + '_> {
Box::new(std::iter::empty())
}
fn execute(
&self,
handle: ProcedureHandle,
args: &[Value],
ctx: &mut ProcedureContext<'_, '_>,
) -> Result<ProcedureResult, ProcedureError>;
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ProcedureMetadata {
pub handle: ProcedureHandle,
pub description: &'static str,
pub signature: ProcedureSignature,
pub output_schema: ProcedureOutputSchema,
pub tier: ProcedureTier,
pub mutability: ProcedureMutability,
}
impl ProcedureMetadata {
#[must_use]
pub const fn new(
handle: ProcedureHandle,
signature: ProcedureSignature,
output_schema: ProcedureOutputSchema,
tier: ProcedureTier,
mutability: ProcedureMutability,
) -> Self {
Self {
handle,
description: "",
signature,
output_schema,
tier,
mutability,
}
}
#[must_use]
pub const fn with_description(mut self, description: &'static str) -> Self {
self.description = description;
self
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ProcedureHandle(u64);
impl ProcedureHandle {
#[must_use]
pub const fn new(raw: u64) -> Self {
Self(raw)
}
#[must_use]
pub const fn raw(self) -> u64 {
self.0
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ProcedureSignature {
pub parameters: Vec<ProcedureParameter>,
pub since_version: &'static str,
}
impl ProcedureSignature {
#[must_use]
pub const fn new(parameters: Vec<ProcedureParameter>) -> Self {
Self {
parameters,
since_version: "1.0.0",
}
}
#[must_use]
pub const fn with_since_version(mut self, since_version: &'static str) -> Self {
self.since_version = since_version;
self
}
#[must_use]
pub fn arity(&self) -> ProcedureArity {
let maximum = self.parameters.len();
let minimum = self
.parameters
.iter()
.position(|parameter| parameter.default.is_some())
.unwrap_or(maximum);
debug_assert!(
self.parameters[minimum..]
.iter()
.all(|parameter| parameter.default.is_some()),
"procedure defaults must be a trailing suffix"
);
ProcedureArity { minimum, maximum }
}
}
impl Default for ProcedureSignature {
fn default() -> Self {
Self::new(Vec::new())
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ProcedureParameter {
pub name: DbString,
pub ty: GqlType,
pub nullable: bool,
pub description: &'static str,
pub default_doc: Option<&'static str>,
pub default: Option<ProcedureDefaultValue>,
}
impl ProcedureParameter {
#[must_use]
pub const fn new(name: DbString, ty: GqlType, nullable: bool) -> Self {
Self {
name,
ty,
nullable,
description: "",
default_doc: None,
default: None,
}
}
#[must_use]
pub const fn with_description(mut self, description: &'static str) -> Self {
self.description = description;
self
}
#[must_use]
pub const fn with_default_doc(mut self, default_doc: &'static str) -> Self {
self.default_doc = Some(default_doc);
self
}
#[must_use]
pub const fn with_default(mut self, default: ProcedureDefaultValue) -> Self {
self.default = Some(default);
self
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ProcedureDefaultValue {
Boolean(bool),
Null,
Integer(i64),
String(&'static str),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ProcedureArity {
pub minimum: usize,
pub maximum: usize,
}
impl ProcedureArity {
#[must_use]
pub const fn accepts(self, actual: usize) -> bool {
self.minimum <= actual && actual <= self.maximum
}
#[must_use]
pub const fn is_exact(self) -> bool {
self.minimum == self.maximum
}
}
#[derive(Clone, Debug, Default)]
pub struct ProcedureOutputSchema {
pub columns: Vec<ProcedureOutputColumn>,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ProcedureOutputColumn {
pub name: DbString,
pub ty: GqlType,
pub nullable: bool,
pub description: &'static str,
}
impl ProcedureOutputColumn {
#[must_use]
pub const fn new(name: DbString, ty: GqlType) -> Self {
Self {
name,
ty,
nullable: false,
description: "",
}
}
#[must_use]
pub const fn with_nullable(mut self, nullable: bool) -> Self {
self.nullable = nullable;
self
}
#[must_use]
pub const fn with_description(mut self, description: &'static str) -> Self {
self.description = description;
self
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProcedureTier {
Graph,
Mutation,
Maintenance,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProcedureMutability {
Read,
SchemaWrite,
MaintenanceWrite,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ProcedureResult {
pub rows: Vec<Vec<Value>>,
}
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ProcedureError {
#[error("unknown procedure")]
UnknownProcedure {
name: Box<[DbString]>,
},
#[error("invalid procedure argument: {detail}")]
InvalidArgument {
detail: String,
},
#[error("procedure tier mismatch: expected {expected:?}, actual {actual:?}")]
TierMismatch {
expected: ProcedureTier,
actual: ProcedureTier,
},
#[error("procedure internal error: {detail}")]
Internal {
detail: String,
},
#[error("procedure cancelled")]
Cancelled,
#[error("procedure deadline exceeded")]
Timeout {
elapsed: Duration,
},
#[error("procedure node scan budget exceeded ({scanned} > {limit})")]
NodeScanBudgetExceeded {
limit: usize,
scanned: usize,
},
}
impl ProcedureError {
#[must_use]
pub const fn gqlstatus(&self) -> GqlStatus {
match self {
Self::UnknownProcedure { .. } => GqlStatus::UNKNOWN_PROCEDURE,
Self::InvalidArgument { .. } => GqlStatus::INVALID_PROCEDURE_ARGUMENT,
Self::TierMismatch { .. } | Self::Internal { .. } => {
GqlStatus::IMPLEMENTATION_DEFINED_ERROR
}
Self::Cancelled => GqlStatus::OPERATION_CANCELLED,
Self::Timeout { .. } => GqlStatus::DEADLINE_EXCEEDED,
Self::NodeScanBudgetExceeded { .. } => GqlStatus::PROGRAM_LIMIT_EXCEEDED,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct EmptyProcedureRegistry;
impl ProcedureRegistry for EmptyProcedureRegistry {
fn lookup(&self, _name: &[DbString]) -> Option<ProcedureMetadata> {
None
}
fn execute(
&self,
_handle: ProcedureHandle,
_args: &[Value],
_ctx: &mut ProcedureContext<'_, '_>,
) -> Result<ProcedureResult, ProcedureError> {
Err(ProcedureError::UnknownProcedure { name: Box::new([]) })
}
}