use crate::{
ecs::components::buffer::{BufferEntity, ViewEntity},
plugin::PluginIdentity,
text_stream::TextRevision,
};
use std::{
fmt::{Debug, Display, Formatter},
num::{NonZeroU64, NonZeroUsize},
};
pub const DEFAULT_PLUGIN_HANDLE_LIMIT: usize = 4096;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PluginHandleKind {
Buffer,
View,
}
impl PluginHandleKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Buffer => "buffer",
Self::View => "view",
}
}
}
impl Display for PluginHandleKind {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct PluginHandleGeneration(u64);
impl PluginHandleGeneration {
#[must_use]
pub const fn as_u64(self) -> u64 {
self.0
}
const fn advance(&mut self) {
self.0 = self.0.saturating_add(1);
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PluginHandleLimit {
max_handles: NonZeroUsize,
}
impl PluginHandleLimit {
pub const fn try_new(max_handles: usize) -> Result<Self, PluginHandleError> {
match NonZeroUsize::new(max_handles) {
Some(max_handles) => Ok(Self { max_handles }),
None => Err(PluginHandleError::ZeroLimit),
}
}
#[must_use]
pub const fn max_handles(self) -> usize {
self.max_handles.get()
}
}
impl Default for PluginHandleLimit {
fn default() -> Self {
Self::try_new(DEFAULT_PLUGIN_HANDLE_LIMIT).expect("default handle limit should be non-zero")
}
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct PluginBufferHandle {
raw: NonZeroU64,
generation: PluginHandleGeneration,
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct PluginViewHandle {
raw: NonZeroU64,
generation: PluginHandleGeneration,
}
macro_rules! impl_plugin_handle {
($handle:ident, $kind:expr) => {
impl $handle {
#[must_use]
pub const fn shape(self) -> PluginHandleShape {
PluginHandleShape {
kind: $kind,
raw: self.raw,
generation: self.generation,
}
}
}
impl Debug for $handle {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_tuple(stringify!($handle))
.field(&self.shape())
.finish()
}
}
};
}
impl_plugin_handle!(PluginBufferHandle, PluginHandleKind::Buffer);
impl_plugin_handle!(PluginViewHandle, PluginHandleKind::View);
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct PluginHandleShape {
kind: PluginHandleKind,
raw: NonZeroU64,
generation: PluginHandleGeneration,
}
impl PluginHandleShape {
#[must_use]
pub const fn kind(self) -> PluginHandleKind {
self.kind
}
#[must_use]
pub const fn generation(self) -> PluginHandleGeneration {
self.generation
}
#[must_use]
pub const fn raw(self) -> NonZeroU64 {
self.raw
}
}
impl Debug for PluginHandleShape {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PluginHandleShape")
.field("kind", &self.kind)
.field("raw", &self.raw)
.field("generation", &self.generation)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum PluginHandleError {
ZeroLimit,
TooManyHandles {
identity: PluginIdentity,
limit: usize,
},
ExhaustedIdentifiers {
identity: PluginIdentity,
},
StaleGeneration {
handle: PluginHandleShape,
current: PluginHandleGeneration,
},
Missing {
handle: PluginHandleShape,
},
}
impl std::fmt::Display for PluginHandleError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::ZeroLimit => formatter.write_str("plugin handle limit must be non-zero"),
Self::TooManyHandles { identity, limit } => {
let identity = identity.as_str();
write!(
formatter,
"plugin {identity:?} exceeded live handle limit of {limit}"
)
}
Self::ExhaustedIdentifiers { identity } => {
let identity = identity.as_str();
write!(
formatter,
"plugin {identity:?} exhausted handle identifiers"
)
}
Self::StaleGeneration { handle, current } => write!(
formatter,
"plugin handle generation {} is stale; current generation is {}",
handle.generation().as_u64(),
current.as_u64()
),
Self::Missing { handle } => write!(
formatter,
"plugin handle {} is not live in the current store",
handle.kind()
),
}
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct PluginBufferSnapshot {
revision: TextRevision,
text: String,
}
impl PluginBufferSnapshot {
#[must_use]
pub fn new(revision: TextRevision, text: impl Into<String>) -> Self {
Self {
revision,
text: text.into(),
}
}
#[must_use]
pub const fn revision(&self) -> TextRevision {
self.revision
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub const fn byte_len(&self) -> usize {
self.text.len()
}
}
impl Debug for PluginBufferSnapshot {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PluginBufferSnapshot")
.field("revision", &self.revision)
.field("byte_len", &self.text.len())
.finish()
}
}
#[derive(Eq, PartialEq)]
pub struct ResolvedPluginBuffer {
target: BufferEntity,
observed_revision: Option<TextRevision>,
observed_snapshot: Option<PluginBufferSnapshot>,
source_view: Option<ViewEntity>,
}
impl ResolvedPluginBuffer {
#[must_use]
pub const fn target(&self) -> BufferEntity {
self.target
}
#[must_use]
pub const fn observed_revision(&self) -> Option<TextRevision> {
self.observed_revision
}
#[must_use]
pub const fn observed_snapshot(&self) -> Option<&PluginBufferSnapshot> {
self.observed_snapshot.as_ref()
}
#[must_use]
pub const fn source_view(&self) -> Option<ViewEntity> {
self.source_view
}
const fn shape(&self) -> ResolvedPluginBufferShape {
ResolvedPluginBufferShape {
target: PluginHandleKind::Buffer,
observed_revision: self.observed_revision,
has_observed_snapshot: self.observed_snapshot.is_some(),
has_source_view: self.source_view.is_some(),
}
}
}
impl Debug for ResolvedPluginBuffer {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ResolvedPluginBuffer")
.field("shape", &self.shape())
.finish()
}
}
#[derive(Eq, PartialEq)]
pub struct ResolvedPluginView {
target: ViewEntity,
}
impl ResolvedPluginView {
#[must_use]
pub const fn target(&self) -> ViewEntity {
self.target
}
}
impl Debug for ResolvedPluginView {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ResolvedPluginView")
.field("target", &PluginHandleKind::View)
.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ResolvedPluginBufferShape {
target: PluginHandleKind,
observed_revision: Option<TextRevision>,
has_observed_snapshot: bool,
has_source_view: bool,
}
#[derive(Clone, Eq, PartialEq)]
struct BufferHandleRecord {
handle: PluginBufferHandle,
target: BufferEntity,
observed_revision: Option<TextRevision>,
observed_snapshot: Option<PluginBufferSnapshot>,
source_view: Option<ViewEntity>,
}
impl BufferHandleRecord {
const fn shape(&self) -> BufferHandleRecordShape {
BufferHandleRecordShape {
handle: self.handle.shape(),
target: PluginHandleKind::Buffer,
observed_revision: self.observed_revision,
has_observed_snapshot: self.observed_snapshot.is_some(),
has_source_view: self.source_view.is_some(),
}
}
}
impl Debug for BufferHandleRecord {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("BufferHandleRecord")
.field("shape", &self.shape())
.finish()
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
struct ViewHandleRecord {
handle: PluginViewHandle,
target: ViewEntity,
}
impl ViewHandleRecord {
const fn shape(self) -> ViewHandleRecordShape {
ViewHandleRecordShape {
handle: self.handle.shape(),
target: PluginHandleKind::View,
}
}
}
impl Debug for ViewHandleRecord {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ViewHandleRecord")
.field("shape", &self.shape())
.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct BufferHandleRecordShape {
handle: PluginHandleShape,
target: PluginHandleKind,
observed_revision: Option<TextRevision>,
has_observed_snapshot: bool,
has_source_view: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ViewHandleRecordShape {
handle: PluginHandleShape,
target: PluginHandleKind,
}
#[derive(Eq, PartialEq)]
pub struct PluginHandleStore {
identity: PluginIdentity,
generation: PluginHandleGeneration,
limit: PluginHandleLimit,
next_raw: NonZeroU64,
buffers: Vec<BufferHandleRecord>,
views: Vec<ViewHandleRecord>,
}
impl PluginHandleStore {
#[must_use]
pub fn new(identity: PluginIdentity) -> Self {
Self::from_validated_limit(identity, PluginHandleLimit::default())
}
#[must_use]
pub fn with_limit(identity: PluginIdentity, limit: PluginHandleLimit) -> Self {
Self::from_validated_limit(identity, limit)
}
#[must_use]
pub(in crate::plugin) fn from_validated_limit(
identity: PluginIdentity,
limit: PluginHandleLimit,
) -> Self {
Self {
identity,
generation: PluginHandleGeneration::default(),
limit,
next_raw: NonZeroU64::MIN,
buffers: Vec::new(),
views: Vec::new(),
}
}
#[must_use]
pub const fn identity(&self) -> &PluginIdentity {
&self.identity
}
#[must_use]
pub const fn generation(&self) -> PluginHandleGeneration {
self.generation
}
#[must_use]
pub const fn limit(&self) -> PluginHandleLimit {
self.limit
}
#[must_use]
pub const fn live_handle_count(&self) -> usize {
self.buffers.len() + self.views.len()
}
pub fn revoke_all(&mut self) {
self.generation.advance();
self.buffers.clear();
self.views.clear();
}
pub fn issue_buffer(
&mut self,
target: BufferEntity,
observed_revision: Option<TextRevision>,
source_view: Option<ViewEntity>,
) -> Result<PluginBufferHandle, PluginHandleError> {
let raw = self.prepare_raw()?;
self.commit_raw(raw);
let handle = PluginBufferHandle {
raw,
generation: self.generation,
};
self.buffers.push(BufferHandleRecord {
handle,
target,
observed_revision,
observed_snapshot: None,
source_view,
});
Ok(handle)
}
pub fn issue_observed_buffer(
&mut self,
target: BufferEntity,
snapshot: PluginBufferSnapshot,
source_view: Option<ViewEntity>,
) -> Result<PluginBufferHandle, PluginHandleError> {
let observed_revision = Some(snapshot.revision());
let raw = self.prepare_raw()?;
self.commit_raw(raw);
let handle = PluginBufferHandle {
raw,
generation: self.generation,
};
self.buffers.push(BufferHandleRecord {
handle,
target,
observed_revision,
observed_snapshot: Some(snapshot),
source_view,
});
Ok(handle)
}
pub fn issue_view(
&mut self,
target: ViewEntity,
) -> Result<PluginViewHandle, PluginHandleError> {
let raw = self.prepare_raw()?;
self.commit_raw(raw);
let handle = PluginViewHandle {
raw,
generation: self.generation,
};
self.views.push(ViewHandleRecord { handle, target });
Ok(handle)
}
pub fn resolve_buffer(
&self,
handle: PluginBufferHandle,
) -> Result<ResolvedPluginBuffer, PluginHandleError> {
self.ensure_generation(handle.shape())?;
self.buffers
.iter()
.find(|record| record.handle == handle)
.map(|record| ResolvedPluginBuffer {
target: record.target,
observed_revision: record.observed_revision,
observed_snapshot: record.observed_snapshot.clone(),
source_view: record.source_view,
})
.ok_or_else(|| PluginHandleError::Missing {
handle: handle.shape(),
})
}
pub fn resolve_view(
&self,
handle: PluginViewHandle,
) -> Result<ResolvedPluginView, PluginHandleError> {
self.ensure_generation(handle.shape())?;
self.views
.iter()
.find(|record| record.handle == handle)
.map(|record| ResolvedPluginView {
target: record.target,
})
.ok_or_else(|| PluginHandleError::Missing {
handle: handle.shape(),
})
}
#[cfg(feature = "plugin-runtime")]
#[must_use]
pub(in crate::plugin) fn first_buffer_handle(&self) -> Option<PluginBufferHandle> {
self.buffers.first().map(|record| record.handle)
}
#[cfg(feature = "plugin-runtime")]
#[must_use]
pub(in crate::plugin) fn first_view_handle(&self) -> Option<PluginViewHandle> {
self.views.first().map(|record| record.handle)
}
#[must_use]
pub(in crate::plugin) fn snapshot(&self) -> PluginHandleStoreSnapshot {
PluginHandleStoreSnapshot {
identity: self.identity.clone(),
generation: self.generation,
buffers: self.buffers.clone(),
views: self.views.clone(),
}
}
fn prepare_raw(&self) -> Result<NonZeroU64, PluginHandleError> {
if self.live_handle_count() >= self.limit.max_handles() {
return Err(PluginHandleError::TooManyHandles {
identity: self.identity.clone(),
limit: self.limit.max_handles(),
});
}
let _next = self.next_raw.get().checked_add(1).ok_or_else(|| {
PluginHandleError::ExhaustedIdentifiers {
identity: self.identity.clone(),
}
})?;
Ok(self.next_raw)
}
const fn commit_raw(&mut self, raw: NonZeroU64) {
self.next_raw = NonZeroU64::new(raw.get() + 1)
.expect("prepare_raw rejects the only overflowing identifier");
}
fn ensure_generation(&self, handle: PluginHandleShape) -> Result<(), PluginHandleError> {
if handle.generation != self.generation {
return Err(PluginHandleError::StaleGeneration {
handle,
current: self.generation,
});
}
Ok(())
}
}
impl Debug for PluginHandleStore {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PluginHandleStore")
.field("identity", &self.identity)
.field("generation", &self.generation)
.field("limit", &self.limit)
.field("next_raw", &self.next_raw)
.field("buffer_handles", &self.buffers.len())
.field("view_handles", &self.views.len())
.finish()
}
}
#[derive(Clone, Eq, PartialEq)]
pub(in crate::plugin) struct PluginHandleStoreSnapshot {
identity: PluginIdentity,
generation: PluginHandleGeneration,
buffers: Vec<BufferHandleRecord>,
views: Vec<ViewHandleRecord>,
}
impl PluginHandleStoreSnapshot {
pub(in crate::plugin) fn resolve_buffer(
&self,
handle: PluginBufferHandle,
) -> Result<ResolvedPluginBuffer, PluginHandleError> {
self.ensure_generation(handle.shape())?;
self.buffers
.iter()
.find(|record| record.handle == handle)
.map(|record| ResolvedPluginBuffer {
target: record.target,
observed_revision: record.observed_revision,
observed_snapshot: record.observed_snapshot.clone(),
source_view: record.source_view,
})
.ok_or_else(|| PluginHandleError::Missing {
handle: handle.shape(),
})
}
pub(in crate::plugin) fn resolve_view(
&self,
handle: PluginViewHandle,
) -> Result<ResolvedPluginView, PluginHandleError> {
self.ensure_generation(handle.shape())?;
self.views
.iter()
.find(|record| record.handle == handle)
.map(|record| ResolvedPluginView {
target: record.target,
})
.ok_or_else(|| PluginHandleError::Missing {
handle: handle.shape(),
})
}
fn ensure_generation(&self, handle: PluginHandleShape) -> Result<(), PluginHandleError> {
if handle.generation != self.generation {
return Err(PluginHandleError::StaleGeneration {
handle,
current: self.generation,
});
}
Ok(())
}
}
impl Debug for PluginHandleStoreSnapshot {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PluginHandleStoreSnapshot")
.field("identity", &self.identity)
.field("generation", &self.generation)
.field("buffer_handles", &self.buffers.len())
.field("view_handles", &self.views.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::{PluginHandleError, PluginHandleKind, PluginHandleLimit, PluginHandleStore};
use crate::{
ecs::components::buffer::{BufferEntity, ViewEntity},
plugin::PluginIdentity,
text_stream::TextRevision,
};
use bevy::prelude::Entity;
use proptest::prelude::*;
#[test]
fn buffer_handles_resolve_to_host_targets_with_revision_provenance() {
let buffer = BufferEntity(test_entity(1));
let view = ViewEntity(test_entity(2));
let mut store = handle_store();
let handle = store
.issue_buffer(buffer, Some(TextRevision::from(7)), Some(view))
.expect("handle issuance should fit");
let resolved = store
.resolve_buffer(handle)
.expect("issued handle should resolve");
assert_eq!(resolved.target(), buffer);
assert_eq!(resolved.observed_revision(), Some(TextRevision::from(7)));
assert_eq!(resolved.source_view(), Some(view));
}
#[test]
fn resolved_handle_debug_redacts_host_targets_and_snapshots() {
let buffer = BufferEntity(test_entity(1));
let view = ViewEntity(test_entity(2));
let mut store = handle_store();
let buffer_handle = store
.issue_observed_buffer(
buffer,
super::PluginBufferSnapshot::new(TextRevision::from(7), "secret buffer text"),
Some(view),
)
.expect("buffer handle");
let view_handle = store.issue_view(view).expect("view handle");
let buffer_debug = format!(
"{:?}",
store
.resolve_buffer(buffer_handle)
.expect("buffer should resolve")
);
let view_debug = format!(
"{:?}",
store
.resolve_view(view_handle)
.expect("view should resolve")
);
assert!(buffer_debug.contains("ResolvedPluginBuffer"));
assert!(buffer_debug.contains("observed_revision"));
assert!(buffer_debug.contains("has_observed_snapshot"));
assert!(buffer_debug.contains("has_source_view"));
assert!(view_debug.contains("ResolvedPluginView"));
for debug in [buffer_debug.as_str(), view_debug.as_str()] {
assert!(!debug.contains("Entity"));
assert!(!debug.contains("BufferEntity"));
assert!(!debug.contains("ViewEntity"));
assert!(!debug.contains("secret"));
assert!(!debug.contains("buffer text"));
}
}
#[test]
fn revocation_invalidates_previously_issued_handles() {
let buffer = BufferEntity(test_entity(1));
let mut store = handle_store();
let handle = store
.issue_buffer(buffer, None, None)
.expect("handle issuance should fit");
let old_generation = store.generation();
store.revoke_all();
assert_ne!(store.generation(), old_generation);
assert_eq!(
store.resolve_buffer(handle),
Err(PluginHandleError::StaleGeneration {
handle: handle.shape(),
current: store.generation(),
})
);
}
#[test]
fn snapshots_capture_the_generation_visible_to_one_update() {
let buffer = BufferEntity(test_entity(1));
let mut store = handle_store();
let buffer_handle = store
.issue_buffer(buffer, Some(TextRevision::from(1)), None)
.expect("buffer handle");
let snapshot = store.snapshot();
store.revoke_all();
assert_eq!(
snapshot
.resolve_buffer(buffer_handle)
.expect("captured buffer should resolve")
.target(),
buffer
);
assert_eq!(
store.resolve_buffer(buffer_handle),
Err(PluginHandleError::StaleGeneration {
handle: buffer_handle.shape(),
current: store.generation(),
})
);
}
#[test]
fn handle_store_rejects_zero_limit() {
assert_eq!(
PluginHandleLimit::try_new(0),
Err(PluginHandleError::ZeroLimit)
);
}
#[test]
fn handle_kinds_have_stable_redacted_text() {
for (kind, expected) in [
(PluginHandleKind::Buffer, "buffer"),
(PluginHandleKind::View, "view"),
] {
assert_eq!(kind.as_str(), expected);
assert_eq!(kind.to_string(), expected);
}
}
#[test]
fn handle_store_bounds_live_handles_without_partial_issuance() {
let mut store = PluginHandleStore::from_validated_limit(
PluginIdentity::try_new("formatter").expect("identity is valid"),
PluginHandleLimit::try_new(1).expect("limit should be valid"),
);
let _handle = store
.issue_view(ViewEntity(test_entity(1)))
.expect("first handle should fit");
assert_eq!(store.live_handle_count(), 1);
let error = store
.issue_view(ViewEntity(test_entity(2)))
.expect_err("second live handle should exceed limit");
assert_eq!(
error,
PluginHandleError::TooManyHandles {
identity: PluginIdentity::try_new("formatter").expect("identity is valid"),
limit: 1,
}
);
assert_eq!(store.live_handle_count(), 1);
}
#[test]
fn revocation_clears_live_handle_count_and_preserves_limit() {
let mut store = PluginHandleStore::from_validated_limit(
PluginIdentity::try_new("formatter").expect("identity is valid"),
PluginHandleLimit::try_new(1).expect("limit should be valid"),
);
let _handle = store
.issue_view(ViewEntity(test_entity(1)))
.expect("first handle should fit");
store.revoke_all();
assert_eq!(store.live_handle_count(), 0);
assert_eq!(store.limit().max_handles(), 1);
let _handle = store
.issue_view(ViewEntity(test_entity(2)))
.expect("new generation can issue up to the same limit");
assert_eq!(store.live_handle_count(), 1);
}
#[test]
fn handle_store_debug_redacts_host_targets() {
let mut store = handle_store();
let _handle = store
.issue_buffer(BufferEntity(test_entity(1)), None, None)
.expect("handle issuance should fit");
let debug = format!("{store:?}");
assert!(debug.contains("PluginHandleStore"));
assert!(debug.contains("buffer_handles"));
assert!(!debug.contains("Entity"));
}
proptest! {
#[test]
fn handle_limit_and_revocation_are_generation_scoped(limit in 1usize..32) {
let mut store = PluginHandleStore::from_validated_limit(
PluginIdentity::try_new("formatter").expect("identity is valid"),
PluginHandleLimit::try_new(limit).expect("limit should be valid"),
);
let mut handles = Vec::with_capacity(limit);
for index in 0..limit {
let target = ViewEntity(test_entity(u32::try_from(index + 1).expect("index fits")));
let handle = store.issue_view(target).expect("handle issuance should fit");
prop_assert_eq!(
store
.resolve_view(handle)
.expect("issued handle should resolve")
.target(),
target
);
handles.push(handle);
}
prop_assert_eq!(store.live_handle_count(), limit);
let error = store
.issue_view(ViewEntity(test_entity(100)))
.expect_err("over-limit handle should reject");
prop_assert_eq!(
error,
PluginHandleError::TooManyHandles {
identity: PluginIdentity::try_new("formatter").expect("identity is valid"),
limit,
}
);
prop_assert_eq!(store.live_handle_count(), limit);
let old_generation = store.generation();
store.revoke_all();
prop_assert_ne!(store.generation(), old_generation);
prop_assert_eq!(store.live_handle_count(), 0);
for handle in handles {
prop_assert_eq!(
store.resolve_view(handle),
Err(PluginHandleError::StaleGeneration {
handle: handle.shape(),
current: store.generation(),
})
);
}
let fresh = store
.issue_view(ViewEntity(test_entity(200)))
.expect("new generation should issue up to the same limit");
prop_assert_eq!(fresh.shape().generation(), store.generation());
}
}
fn handle_store() -> PluginHandleStore {
PluginHandleStore::new(PluginIdentity::try_new("formatter").expect("identity is valid"))
}
fn test_entity(index: u32) -> Entity {
Entity::from_raw_u32(index).expect("test entity index should be valid")
}
}