blob: 97e43574e93f811d082bff953a0ff753442844cb [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::Error;
use maka_plugin_core::scope::Scope;
use serde::{Deserialize, Serialize};
use serde_json::Value;
mod directory;
pub use directory::{Directories, Directory};
pub const MAX_PAGE_BYTES: usize = 2 * 1024 * 1024;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Namespace {
package: String,
scope: Scope,
}
impl Namespace {
pub fn new(package: impl Into<String>, scope: Scope) -> Result<Self, Error> {
let package = package.into();
maka_plugin_core::identifier(&package).map_err(|e| Error::Invalid(e.to_string()))?;
if let Scope::Session(id) = &scope {
crate::name(id)?;
}
Ok(Self { package, scope })
}
pub fn package(&self) -> &str {
&self.package
}
pub fn scope(&self) -> &Scope {
&self.scope
}
}
/// Deletions retain a revision, preventing stale writers from recreating a key
/// as if it had never existed. Revision is not a business schema version.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Record {
pub revision: u64,
pub data: Data,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Scan {
#[serde(default)]
pub prefix: String,
pub after: Option<String>,
}
impl Scan {
pub fn validate(&self) -> Result<(), Error> {
if !self.prefix.is_empty() {
validate_key(&self.prefix)?;
}
if let Some(after) = &self.after {
validate_key(after)?;
if !after.starts_with(&self.prefix) {
return Err(Error::Invalid(
"storage cursor is outside its prefix".into(),
));
}
}
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Entry {
pub key: String,
pub record: Record,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Page {
pub entries: Vec<Entry>,
pub next_after: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(
tag = "kind",
content = "value",
rename_all = "snake_case",
deny_unknown_fields
)]
pub enum Data {
Present(Value),
Deleted,
}
impl Data {
pub fn value(&self) -> Option<&Value> {
match self {
Self::Present(value) => Some(value),
Self::Deleted => None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Mutation {
pub key: String,
pub expected_revision: Option<u64>,
pub data: Data,
}
impl Mutation {
pub fn validate(&self) -> Result<(), Error> {
validate_key(&self.key)?;
if self
.expected_revision
.is_some_and(|revision| revision == 0 || revision >= (1 << 53) - 1)
{
return Err(Error::Invalid("invalid plugin data revision".into()));
}
Ok(())
}
}
pub fn validate_key(key: &str) -> Result<(), Error> {
if key.is_empty() || key.chars().any(char::is_control) {
return Err(Error::Invalid("invalid plugin storage key".into()));
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("plugin storage capability is retired")]
Retired,
#[error("plugin data revision conflict: expected {expected}, found {actual}")]
Conflict { expected: String, actual: String },
#[error("plugin data commit outcome is unknown: {0}")]
OutcomeUnknown(String),
#[error("plugin storage is unavailable: {0}")]
Unavailable(String),
}
/// Namespace is bound by Host, not supplied by each plugin operation.
pub trait Store: Send + Sync {
/// Ordered, bounded enumeration including tombstones. Each page is current,
/// not a cross-page snapshot; domain consistency uses revisions and batches.
fn scan(&self, query: Scan) -> futures_util::future::BoxFuture<'_, Result<Page, StoreError>>;
fn read(
&self,
key: String,
) -> futures_util::future::BoxFuture<'_, Result<Option<Record>, StoreError>>;
/// Atomic CAS of the complete selected mutation set.
fn batch(
&self,
mutations: Vec<Mutation>,
) -> futures_util::future::BoxFuture<'_, Result<Vec<Record>, StoreError>>;
}