use std::hash::{Hash, Hasher};
use std::sync::Arc;
use crate::handler::HandlerFn;
use crate::operation_name::{OperationName, OperationNameError};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum EffectClass {
Inspect,
Compute,
Persist,
Emit,
Control,
}
impl EffectClass {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Inspect => "inspect",
Self::Compute => "compute",
Self::Persist => "persist",
Self::Emit => "emit",
Self::Control => "control",
}
}
#[must_use]
pub fn from_catalog_str(value: &str) -> Option<Self> {
match value {
"inspect" => Some(Self::Inspect),
"compute" => Some(Self::Compute),
"persist" => Some(Self::Persist),
"emit" => Some(Self::Emit),
"control" => Some(Self::Control),
_ => None,
}
}
}
pub type OperationInput = Vec<u8>;
pub type OperationOutput = Vec<u8>;
pub const MAX_OPERATION_NAME_BYTES: usize = 128;
pub const MAX_DESCRIPTOR_REF_BYTES: usize = 256;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct OperationDescriptor {
name: DescriptorText,
title: Option<DescriptorText>,
pub effect: EffectClass,
input_schema_ref: DescriptorText,
output_schema_ref: DescriptorText,
receipt_kind: DescriptorText,
}
#[derive(Clone, Debug, Eq)]
enum DescriptorText {
Static(&'static str),
Owned(Arc<str>),
}
impl DescriptorText {
const fn static_str(value: &'static str) -> Self {
Self::Static(value)
}
fn owned(value: impl Into<String>) -> Self {
Self::Owned(Arc::from(value.into()))
}
fn as_str(&self) -> &str {
match self {
Self::Static(value) => value,
Self::Owned(value) => value.as_ref(),
}
}
}
impl PartialEq for DescriptorText {
fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str()
}
}
impl Hash for DescriptorText {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}
impl OperationDescriptor {
#[must_use]
pub const fn new(
name: &'static str,
effect: EffectClass,
input_schema_ref: &'static str,
output_schema_ref: &'static str,
receipt_kind: &'static str,
) -> Self {
Self {
name: DescriptorText::static_str(name),
title: None,
effect,
input_schema_ref: DescriptorText::static_str(input_schema_ref),
output_schema_ref: DescriptorText::static_str(output_schema_ref),
receipt_kind: DescriptorText::static_str(receipt_kind),
}
}
#[must_use]
pub const fn new_with_title(
name: &'static str,
effect: EffectClass,
input_schema_ref: &'static str,
output_schema_ref: &'static str,
receipt_kind: &'static str,
title: &'static str,
) -> Self {
Self {
name: DescriptorText::static_str(name),
title: Some(DescriptorText::static_str(title)),
effect,
input_schema_ref: DescriptorText::static_str(input_schema_ref),
output_schema_ref: DescriptorText::static_str(output_schema_ref),
receipt_kind: DescriptorText::static_str(receipt_kind),
}
}
#[must_use]
pub fn owned(
name: impl Into<String>,
effect: EffectClass,
input_schema_ref: impl Into<String>,
output_schema_ref: impl Into<String>,
receipt_kind: impl Into<String>,
) -> Self {
Self {
name: DescriptorText::owned(name),
title: None,
effect,
input_schema_ref: DescriptorText::owned(input_schema_ref),
output_schema_ref: DescriptorText::owned(output_schema_ref),
receipt_kind: DescriptorText::owned(receipt_kind),
}
}
#[must_use]
pub fn with_title(mut self, title: &'static str) -> Self {
self.title = Some(DescriptorText::static_str(title));
self
}
#[must_use]
pub fn with_owned_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(DescriptorText::owned(title));
self
}
#[must_use]
pub fn name(&self) -> &str {
self.name.as_str()
}
#[must_use]
pub fn title(&self) -> Option<&str> {
self.title.as_ref().map(DescriptorText::as_str)
}
#[must_use]
pub fn input_schema_ref(&self) -> &str {
self.input_schema_ref.as_str()
}
#[must_use]
pub fn output_schema_ref(&self) -> &str {
self.output_schema_ref.as_str()
}
#[must_use]
pub fn receipt_kind(&self) -> &str {
self.receipt_kind.as_str()
}
pub fn validate(&self) -> Result<(), DescriptorValidationError> {
OperationName::new(self.name()).map_err(|error| {
DescriptorValidationError::from_operation_name_error("name", self.name(), &error)
})?;
validate_stable_ref(
self.name(),
"input_schema_ref",
self.input_schema_ref(),
MAX_DESCRIPTOR_REF_BYTES,
)?;
validate_stable_ref(
self.name(),
"output_schema_ref",
self.output_schema_ref(),
MAX_DESCRIPTOR_REF_BYTES,
)?;
validate_stable_ref(
self.name(),
"receipt_kind",
self.receipt_kind(),
MAX_DESCRIPTOR_REF_BYTES,
)
}
}
#[derive(Clone)]
pub struct OperationRegisterItem {
descriptor: OperationDescriptor,
handler: HandlerFn,
}
impl OperationRegisterItem {
#[must_use]
pub fn new(descriptor: OperationDescriptor, handler: HandlerFn) -> Self {
Self {
descriptor,
handler,
}
}
#[must_use]
pub fn descriptor(&self) -> &OperationDescriptor {
&self.descriptor
}
#[must_use]
pub fn handler(&self) -> HandlerFn {
self.handler
}
#[must_use]
pub fn into_parts(self) -> (OperationDescriptor, HandlerFn) {
(self.descriptor, self.handler)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DescriptorValidationError {
pub field: &'static str,
pub value: String,
pub message: &'static str,
}
impl DescriptorValidationError {
fn new(field: &'static str, value: impl Into<String>, message: &'static str) -> Self {
Self {
field,
value: value.into(),
message,
}
}
fn from_operation_name_error(
field: &'static str,
value: &str,
error: &OperationNameError,
) -> Self {
let message = match error {
OperationNameError::Empty => "empty",
OperationNameError::TooLong { .. } => "too long",
OperationNameError::LeadingOrTrailingDot | OperationNameError::ConsecutiveDots => {
"dot-separated tokens must be non-empty"
}
OperationNameError::IllegalCharacter { .. } => {
"expected ASCII letters, digits, '.', '_' or '-'"
}
};
Self::new(field, value, message)
}
}
impl std::fmt::Display for DescriptorValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} `{}` is invalid: {}",
self.field, self.value, self.message
)
}
}
impl std::error::Error for DescriptorValidationError {}
fn validate_stable_ref(
operation_name: &str,
field: &'static str,
value: &str,
max: usize,
) -> Result<(), DescriptorValidationError> {
validate_stable_ref_token(field, value, max).map_err(|error| DescriptorValidationError {
field: error.field,
value: format!("{operation_name}:{}", error.value),
message: error.message,
})
}
fn validate_stable_ref_token(
field: &'static str,
value: &str,
max: usize,
) -> Result<(), DescriptorValidationError> {
if value.is_empty() {
return Err(DescriptorValidationError::new(field, value, "empty"));
}
if value.len() > max {
return Err(DescriptorValidationError::new(field, value, "too long"));
}
if value
.bytes()
.any(|byte| !matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'_' | b'-'))
{
return Err(DescriptorValidationError::new(
field,
value,
"expected ASCII letters, digits, '.', '_' or '-'",
));
}
if value.starts_with('.') || value.ends_with('.') || value.contains("..") {
return Err(DescriptorValidationError::new(
field,
value,
"dot-separated tokens must be non-empty",
));
}
Ok(())
}