feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,641 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::hir::Program;
|
||||
use crate::types::{Definition, Shape};
|
||||
use crate::wire::{Policy, Schema};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ModelSchema {
|
||||
pub record: usize,
|
||||
pub page: usize,
|
||||
pub cursor: usize,
|
||||
pub package: String,
|
||||
pub name: String,
|
||||
pub scope: ModelScope,
|
||||
pub table: String,
|
||||
pub connection: ConnectionSelector,
|
||||
pub fields: Vec<ModelField>,
|
||||
pub choices: Vec<ModelChoice>,
|
||||
pub indexes: Vec<ModelIndex>,
|
||||
pub relations: Vec<ModelRelation>,
|
||||
pub sql: Vec<ModelSql>,
|
||||
pub seed: Vec<SeedRow>,
|
||||
pub migrations: Vec<ModelMigration>,
|
||||
pub revision: String,
|
||||
pub seed_revision: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum ModelScope {
|
||||
Global,
|
||||
Tenant,
|
||||
}
|
||||
|
||||
impl ModelScope {
|
||||
pub(crate) fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Global => "global",
|
||||
Self::Tenant => "tenant",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct RestContract {
|
||||
pub read: Vec<usize>,
|
||||
pub create: Vec<RestWriteField>,
|
||||
pub replace: Vec<RestWriteField>,
|
||||
pub owner: Option<(usize, RestOwnerSource)>,
|
||||
pub search: Option<RestSearch>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct RestWriteField {
|
||||
pub field: usize,
|
||||
pub source: RestValueSource,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum RestValueSource {
|
||||
Input,
|
||||
Binding {
|
||||
expression: crate::hir::Expression,
|
||||
uses_input: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct RestSearch {
|
||||
pub field: usize,
|
||||
pub expression: crate::hir::Expression,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum RestOwnerSource {
|
||||
User,
|
||||
Tenant,
|
||||
}
|
||||
|
||||
impl ModelSchema {
|
||||
pub(crate) fn rest_contract(&self, types: &[Definition]) -> Result<RestContract, String> {
|
||||
let Shape::Record(fields) = &types[self.record].shape else {
|
||||
unreachable!("checked Model record")
|
||||
};
|
||||
let mut contract = RestContract {
|
||||
read: Vec::new(),
|
||||
create: Vec::new(),
|
||||
replace: Vec::new(),
|
||||
owner: None,
|
||||
search: None,
|
||||
};
|
||||
for (index, (storage, field)) in self.fields.iter().zip(fields).enumerate() {
|
||||
if let Some(owner) = storage.owner {
|
||||
contract.owner = Some((index, owner));
|
||||
}
|
||||
if field.private {
|
||||
continue;
|
||||
}
|
||||
Schema::build(&field.ty, types, Policy::Output).map_err(|error| {
|
||||
format!(
|
||||
"REST field '{}.{}' cannot be serialized: {error}",
|
||||
self.package, storage.name
|
||||
)
|
||||
})?;
|
||||
contract.read.push(index);
|
||||
if storage.owner.is_some() {
|
||||
continue;
|
||||
}
|
||||
if storage.generated {
|
||||
continue;
|
||||
}
|
||||
Schema::build(&field.ty, types, Policy::ApiInput).map_err(|error| {
|
||||
format!(
|
||||
"REST field '{}.{}' cannot be read from a request: {error}",
|
||||
self.package, storage.name
|
||||
)
|
||||
})?;
|
||||
contract.replace.push(RestWriteField {
|
||||
field: index,
|
||||
source: RestValueSource::Input,
|
||||
});
|
||||
if storage.default.is_none() {
|
||||
contract.create.push(RestWriteField {
|
||||
field: index,
|
||||
source: RestValueSource::Input,
|
||||
});
|
||||
}
|
||||
}
|
||||
if contract.replace.is_empty() {
|
||||
return Err(format!(
|
||||
"REST Model '{}' has no writable public fields",
|
||||
self.package
|
||||
));
|
||||
}
|
||||
Ok(contract)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum ConnectionSelector {
|
||||
Explicit(String),
|
||||
PackageRoot(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub(crate) enum DatabaseOwner {
|
||||
Model(usize),
|
||||
Job(usize),
|
||||
}
|
||||
|
||||
impl Program {
|
||||
pub(crate) fn rest_binding_roots(&self) -> std::collections::BTreeSet<usize> {
|
||||
let mut roots = std::collections::BTreeSet::new();
|
||||
for route in &self.api_rest {
|
||||
for expression in route
|
||||
.contract
|
||||
.create
|
||||
.iter()
|
||||
.chain(&route.contract.replace)
|
||||
.filter_map(|write| match &write.source {
|
||||
RestValueSource::Binding { expression, .. } => Some(expression),
|
||||
RestValueSource::Input => None,
|
||||
})
|
||||
.chain(
|
||||
route
|
||||
.contract
|
||||
.search
|
||||
.iter()
|
||||
.map(|search| &search.expression),
|
||||
)
|
||||
{
|
||||
crate::check::visit_expression(expression, |value| {
|
||||
if let Some((target, arguments)) = value.kind.static_call() {
|
||||
if let crate::hir::CallTarget::Function(target) = target {
|
||||
roots.insert(target);
|
||||
}
|
||||
for argument in arguments {
|
||||
if let crate::hir::CallArgument::Handler(
|
||||
crate::hir::HandlerTarget::Function(target),
|
||||
) = argument
|
||||
{
|
||||
roots.insert(*target);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let crate::hir::ExpressionKind::Collection {
|
||||
handler: Some(crate::hir::HandlerTarget::Function(target)),
|
||||
..
|
||||
} = &value.kind
|
||||
{
|
||||
roots.insert(*target);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
pub(crate) fn database_binding(&self, owner: DatabaseOwner) -> (&ConnectionSelector, &str) {
|
||||
match owner {
|
||||
DatabaseOwner::Model(id) => (&self.models[id].connection, &self.models[id].package),
|
||||
DatabaseOwner::Job(function) => {
|
||||
let job = self
|
||||
.jobs
|
||||
.iter()
|
||||
.find(|job| job.function == function)
|
||||
.expect("checked Job target");
|
||||
(&job.connection, &self.functions[function].name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn database_selector(&self, owner: DatabaseOwner) -> (Option<&str>, &str) {
|
||||
let (connection, identity) = self.database_binding(owner);
|
||||
match connection {
|
||||
ConnectionSelector::Explicit(name) => (Some(name), identity.split('.').next().unwrap()),
|
||||
ConnectionSelector::PackageRoot(root) => (None, root),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ModelField {
|
||||
pub name: String,
|
||||
pub ty: ModelFieldType,
|
||||
pub nullable: bool,
|
||||
pub generated: bool,
|
||||
pub default: Option<ModelValue>,
|
||||
pub rename_from: Option<String>,
|
||||
pub logical_reference: bool,
|
||||
pub owner: Option<RestOwnerSource>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum ModelFieldType {
|
||||
Bool,
|
||||
Int,
|
||||
Float,
|
||||
Decimal { precision: u8, scale: u8 },
|
||||
Text { minimum: u32, maximum: Option<u32> },
|
||||
Bytes { minimum: u32, maximum: Option<u32> },
|
||||
Uuid,
|
||||
DateTime,
|
||||
Date,
|
||||
Time,
|
||||
Duration,
|
||||
Json,
|
||||
Choice(String),
|
||||
ModelId(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum ModelValue {
|
||||
Null,
|
||||
Bool(bool),
|
||||
Number(String),
|
||||
Text(String),
|
||||
Choice(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ModelChoice {
|
||||
pub name: String,
|
||||
pub options: Vec<(String, Option<String>)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ModelIndex {
|
||||
pub fields: Vec<String>,
|
||||
pub unique: bool,
|
||||
pub implicit: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ModelRelation {
|
||||
pub name: String,
|
||||
pub model: String,
|
||||
pub field: String,
|
||||
pub kind: ModelRelationKind,
|
||||
pub logical: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum ModelRelationKind {
|
||||
ToOne,
|
||||
ToMany,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ModelSql {
|
||||
pub name: String,
|
||||
pub parameters: Vec<ModelSqlParameter>,
|
||||
pub result: ModelSqlResult,
|
||||
pub sqlite: String,
|
||||
pub postgres: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ModelSqlParameter {
|
||||
pub name: String,
|
||||
pub ty: ModelFieldType,
|
||||
pub nullable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ModelSqlResult {
|
||||
pub record: usize,
|
||||
pub cardinality: ModelSqlCardinality,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum ModelSqlCardinality {
|
||||
One,
|
||||
Optional,
|
||||
Many,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct SeedRow {
|
||||
pub fields: Vec<(String, ModelValue)>,
|
||||
pub identity: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ModelMigration {
|
||||
pub name: String,
|
||||
pub drops: Vec<String>,
|
||||
pub statements: Vec<ModelMigrationSql>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ModelMigrationSql {
|
||||
pub phase: crate::syntax::MigrationPhase,
|
||||
pub sqlite: String,
|
||||
pub postgres: String,
|
||||
pub parameters: Vec<(ModelFieldType, ModelValue)>,
|
||||
}
|
||||
|
||||
impl ModelMigration {
|
||||
pub fn revision(&self) -> String {
|
||||
let identity = if self.statements.is_empty() {
|
||||
format!("{}:{}", self.name, self.drops.join(","))
|
||||
} else {
|
||||
format!("{:?}:{:?}:{:?}", self.name, self.drops, self.statements)
|
||||
};
|
||||
checksum(&identity)
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
/// Stable logical schema output. SQL dialect details and source locations are excluded.
|
||||
pub fn model_snapshot(&self) -> String {
|
||||
let mut models = self.models.iter().collect::<Vec<_>>();
|
||||
models.sort_by(|left, right| left.package.cmp(&right.package));
|
||||
let mut output = String::from("dever-model 1\n");
|
||||
for model in models {
|
||||
output.push_str(&format!(
|
||||
"model {} name={} table={} connection={} scope={} revision={} seed={}\n",
|
||||
model.package,
|
||||
model.name,
|
||||
model.table,
|
||||
connection(&model.connection),
|
||||
model.scope.name(),
|
||||
model.revision,
|
||||
model.seed_revision,
|
||||
));
|
||||
for choice in &model.choices {
|
||||
let options = choice
|
||||
.options
|
||||
.iter()
|
||||
.map(|(name, label)| match label {
|
||||
Some(label) => format!("{name}={label:?}"),
|
||||
None => name.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
output.push_str(&format!("choice {} [{options}]\n", choice.name));
|
||||
}
|
||||
for field in &model.fields {
|
||||
let reference = if field.logical_reference {
|
||||
" reference=logical"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let owner = match field.owner {
|
||||
Some(RestOwnerSource::User) => " owner=auth.user_id",
|
||||
Some(RestOwnerSource::Tenant) => " owner=auth.tenant_id",
|
||||
None => "",
|
||||
};
|
||||
output.push_str(&format!(
|
||||
"field {} {} nullable={} generated={} default={} from={}{reference}{owner}\n",
|
||||
field.name,
|
||||
field_type(&field.ty),
|
||||
field.nullable,
|
||||
field.generated,
|
||||
field.default.as_ref().map(value).unwrap_or("-".into()),
|
||||
field.rename_from.as_deref().unwrap_or("-"),
|
||||
));
|
||||
}
|
||||
for index in &model.indexes {
|
||||
output.push_str(&format!(
|
||||
"index {} ({}) implicit={}\n",
|
||||
if index.unique { "unique" } else { "plain" },
|
||||
index.fields.join(","),
|
||||
index.implicit,
|
||||
));
|
||||
}
|
||||
for relation in &model.relations {
|
||||
let kind = match relation.kind {
|
||||
ModelRelationKind::ToOne => "to-one",
|
||||
ModelRelationKind::ToMany => "to-many",
|
||||
};
|
||||
output.push_str(&format!(
|
||||
"relation {}{} {} = {}.{}\n",
|
||||
if relation.logical { "logical " } else { "" },
|
||||
kind,
|
||||
relation.name,
|
||||
relation.model,
|
||||
relation.field
|
||||
));
|
||||
}
|
||||
for sql in &model.sql {
|
||||
output.push_str(&format!(
|
||||
"sql {} ({}) -> {:?}:{}\n",
|
||||
sql.name,
|
||||
sql.parameters
|
||||
.iter()
|
||||
.map(|parameter| format!(
|
||||
"{}:{}{}",
|
||||
parameter.name,
|
||||
field_type(¶meter.ty),
|
||||
if parameter.nullable { "?" } else { "" }
|
||||
))
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
sql.result.cardinality,
|
||||
self.types[sql.result.record].name,
|
||||
));
|
||||
}
|
||||
for row in &model.seed {
|
||||
output.push_str(&format!(
|
||||
"seed key({}) {{{}}}\n",
|
||||
row.identity.join(","),
|
||||
row.fields
|
||||
.iter()
|
||||
.map(|(name, item)| format!("{name}={}", value(item)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
));
|
||||
}
|
||||
for migration in &model.migrations {
|
||||
output.push_str(&format!(
|
||||
"migrate {} drop({})\n",
|
||||
migration.name,
|
||||
migration.drops.join(",")
|
||||
));
|
||||
if !migration.statements.is_empty() {
|
||||
output.push_str(&format!(
|
||||
"migration-data {} revision={}\n",
|
||||
migration.name,
|
||||
migration.revision()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
pub fn validate_database_settings(
|
||||
&self,
|
||||
settings: &dever_runtime::config::Settings,
|
||||
) -> Result<dever_runtime::config::RuntimeProfile, String> {
|
||||
self.validate_database_bindings(&settings.compilation_bindings())
|
||||
}
|
||||
|
||||
pub fn validate_database_bindings(
|
||||
&self,
|
||||
settings: &dever_runtime::config::CompilationBindings,
|
||||
) -> Result<dever_runtime::config::RuntimeProfile, String> {
|
||||
for job in &self.jobs {
|
||||
let (explicit, root) = self.database_selector(DatabaseOwner::Job(job.function));
|
||||
settings.resolve_database(explicit, root)?;
|
||||
}
|
||||
let connections = self
|
||||
.models
|
||||
.iter()
|
||||
.map(|model| resolve_connection(settings, model))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let model_ids = self
|
||||
.models
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, model)| (model.package.as_str(), index))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let mut tables = BTreeMap::new();
|
||||
for (index, model) in self.models.iter().enumerate() {
|
||||
if matches!(
|
||||
connections[index].1,
|
||||
dever_runtime::config::DatabaseDriver::Postgres
|
||||
) {
|
||||
validate_postgres_identifiers(model)?;
|
||||
}
|
||||
if let Some(existing) = tables.insert(
|
||||
(connections[index].0, model.table.as_str()),
|
||||
model.package.as_str(),
|
||||
) {
|
||||
return Err(format!(
|
||||
"Models '{}' and '{}' resolve to database '{}' and share table '{}'",
|
||||
existing, model.package, connections[index].0, model.table
|
||||
));
|
||||
}
|
||||
for field in &model.fields {
|
||||
let ModelFieldType::ModelId(target) = &field.ty else {
|
||||
continue;
|
||||
};
|
||||
let target = *model_ids
|
||||
.get(target.as_str())
|
||||
.expect("validated Model foreign key");
|
||||
if connections[index].0 != connections[target].0 {
|
||||
return Err(format!(
|
||||
"foreign key '{}.{}' crosses database connections '{}' and '{}'",
|
||||
model.package, field.name, connections[index].0, connections[target].0
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let instances = crate::specialize::concrete(self)
|
||||
.map_err(|_| "recursive specialized handler calls are not allowed".to_owned())?;
|
||||
for instance in instances {
|
||||
let function = &self.functions[instance.function];
|
||||
if function.kind != crate::syntax::FunctionKind::Transaction {
|
||||
continue;
|
||||
}
|
||||
let mut connection = None;
|
||||
for owner in crate::specialize::database_effects(self, &instance) {
|
||||
let (explicit, root) = self.database_selector(owner);
|
||||
let name = settings.resolve_database(explicit, root)?.0;
|
||||
if connection
|
||||
.replace(name)
|
||||
.is_some_and(|current| current != name)
|
||||
{
|
||||
return Err(format!(
|
||||
"transaction '{}' spans multiple database connections",
|
||||
function.name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(settings.profile())
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_connection<'a>(
|
||||
settings: &'a dever_runtime::config::CompilationBindings,
|
||||
model: &ModelSchema,
|
||||
) -> Result<(&'a str, &'a dever_runtime::config::DatabaseDriver), String> {
|
||||
match &model.connection {
|
||||
ConnectionSelector::Explicit(name) => settings.resolve_database(Some(name), &model.package),
|
||||
ConnectionSelector::PackageRoot(root) => settings.resolve_database(None, root),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_postgres_identifiers(model: &ModelSchema) -> Result<(), String> {
|
||||
let identifiers = std::iter::once(("table", model.table.as_str()))
|
||||
.chain(model.fields.iter().flat_map(|field| {
|
||||
std::iter::once(("field", field.name.as_str())).chain(
|
||||
field
|
||||
.rename_from
|
||||
.iter()
|
||||
.map(|name| ("renamed field", name.as_str())),
|
||||
)
|
||||
}))
|
||||
.chain(model.migrations.iter().flat_map(|migration| {
|
||||
migration
|
||||
.drops
|
||||
.iter()
|
||||
.map(|name| ("dropped field", name.as_str()))
|
||||
}));
|
||||
for (kind, identifier) in identifiers {
|
||||
if identifier.len() > 63 {
|
||||
return Err(format!(
|
||||
"PostgreSQL {kind} identifier '{}.{}' exceeds 63 bytes",
|
||||
model.package, identifier
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn connection(selector: &ConnectionSelector) -> String {
|
||||
match selector {
|
||||
ConnectionSelector::Explicit(name) => format!("explicit:{name}"),
|
||||
ConnectionSelector::PackageRoot(name) => format!("package:{name}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn field_type(ty: &ModelFieldType) -> String {
|
||||
match ty {
|
||||
ModelFieldType::Bool => "Bool".into(),
|
||||
ModelFieldType::Int => "Int".into(),
|
||||
ModelFieldType::Float => "Float".into(),
|
||||
ModelFieldType::Decimal { precision, scale } => format!("Decimal({precision},{scale})"),
|
||||
ModelFieldType::Text { minimum, maximum } => bounded("Text", *minimum, *maximum),
|
||||
ModelFieldType::Bytes { minimum, maximum } => bounded("Bytes", *minimum, *maximum),
|
||||
ModelFieldType::Uuid => "Uuid".into(),
|
||||
ModelFieldType::DateTime => "DateTime".into(),
|
||||
ModelFieldType::Date => "Date".into(),
|
||||
ModelFieldType::Time => "Time".into(),
|
||||
ModelFieldType::Duration => "Duration".into(),
|
||||
ModelFieldType::Json => "Json".into(),
|
||||
ModelFieldType::Choice(name) => format!("choice:{name}"),
|
||||
ModelFieldType::ModelId(model) => format!("id:{model}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn bounded(name: &str, minimum: u32, maximum: Option<u32>) -> String {
|
||||
match (minimum, maximum) {
|
||||
(0, None) => name.into(),
|
||||
(0, Some(maximum)) => format!("{name}({maximum})"),
|
||||
(minimum, Some(maximum)) => format!("{name}({minimum},{maximum})"),
|
||||
(_, None) => unreachable!("minimum requires a maximum"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn value(value: &ModelValue) -> String {
|
||||
match value {
|
||||
ModelValue::Null => "null".into(),
|
||||
ModelValue::Bool(value) => value.to_string(),
|
||||
ModelValue::Number(value) => value.clone(),
|
||||
ModelValue::Text(value) => format!("{value:?}"),
|
||||
ModelValue::Choice(value) => value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn checksum(text: &str) -> String {
|
||||
let mut hash = 0xcbf29ce484222325u64;
|
||||
for byte in text.bytes() {
|
||||
hash ^= u64::from(byte);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
format!("{hash:016x}")
|
||||
}
|
||||
Reference in new issue
Block a user