use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::marker::PhantomData;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlSnapshot {
pub name: String,
pub version: String,
pub accounts: Vec<IdlAccountSnapshot>,
pub instructions: Vec<IdlInstructionSnapshot>,
#[serde(default)]
pub types: Vec<IdlTypeDefSnapshot>,
#[serde(default)]
pub events: Vec<IdlEventSnapshot>,
#[serde(default)]
pub errors: Vec<IdlErrorSnapshot>,
#[serde(default = "default_discriminant_size")]
pub discriminant_size: usize,
}
fn default_discriminant_size() -> usize {
8
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlAccountSnapshot {
pub name: String,
pub discriminator: Vec<u8>,
#[serde(default)]
pub docs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlInstructionSnapshot {
pub name: String,
pub discriminator: Vec<u8>,
#[serde(default)]
pub docs: Vec<String>,
pub accounts: Vec<IdlInstructionAccountSnapshot>,
pub args: Vec<IdlFieldSnapshot>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlInstructionAccountSnapshot {
pub name: String,
#[serde(default)]
pub writable: bool,
#[serde(default)]
pub signer: bool,
#[serde(default)]
pub optional: bool,
#[serde(default)]
pub address: Option<String>,
#[serde(default)]
pub docs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlFieldSnapshot {
pub name: String,
#[serde(rename = "type")]
pub type_: IdlTypeSnapshot,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum IdlTypeSnapshot {
Simple(String),
Array(IdlArrayTypeSnapshot),
Option(IdlOptionTypeSnapshot),
Vec(IdlVecTypeSnapshot),
Defined(IdlDefinedTypeSnapshot),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlArrayTypeSnapshot {
pub array: Vec<IdlArrayElementSnapshot>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum IdlArrayElementSnapshot {
Type(IdlTypeSnapshot),
TypeName(String),
Size(u32),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlOptionTypeSnapshot {
pub option: Box<IdlTypeSnapshot>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlVecTypeSnapshot {
pub vec: Box<IdlTypeSnapshot>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlDefinedTypeSnapshot {
pub defined: IdlDefinedInnerSnapshot,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum IdlDefinedInnerSnapshot {
Named { name: String },
Simple(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlTypeDefSnapshot {
pub name: String,
#[serde(default)]
pub docs: Vec<String>,
#[serde(rename = "type")]
pub type_def: IdlTypeDefKindSnapshot,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum IdlTypeDefKindSnapshot {
Struct {
kind: String,
fields: Vec<IdlFieldSnapshot>,
},
Enum {
kind: String,
variants: Vec<IdlEnumVariantSnapshot>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlEnumVariantSnapshot {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlEventSnapshot {
pub name: String,
pub discriminator: Vec<u8>,
#[serde(default)]
pub docs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlErrorSnapshot {
pub code: u32,
pub name: String,
pub msg: String,
}
impl IdlTypeSnapshot {
pub fn to_rust_type_string(&self) -> String {
match self {
IdlTypeSnapshot::Simple(s) => Self::map_simple_type(s),
IdlTypeSnapshot::Array(arr) => {
if arr.array.len() == 2 {
match (&arr.array[0], &arr.array[1]) {
(
IdlArrayElementSnapshot::TypeName(t),
IdlArrayElementSnapshot::Size(size),
) => {
format!("[{}; {}]", Self::map_simple_type(t), size)
}
(
IdlArrayElementSnapshot::Type(nested),
IdlArrayElementSnapshot::Size(size),
) => {
format!("[{}; {}]", nested.to_rust_type_string(), size)
}
_ => "Vec<u8>".to_string(),
}
} else {
"Vec<u8>".to_string()
}
}
IdlTypeSnapshot::Option(opt) => {
format!("Option<{}>", opt.option.to_rust_type_string())
}
IdlTypeSnapshot::Vec(vec) => {
format!("Vec<{}>", vec.vec.to_rust_type_string())
}
IdlTypeSnapshot::Defined(def) => match &def.defined {
IdlDefinedInnerSnapshot::Named { name } => name.clone(),
IdlDefinedInnerSnapshot::Simple(s) => s.clone(),
},
}
}
fn map_simple_type(idl_type: &str) -> String {
match idl_type {
"u8" => "u8".to_string(),
"u16" => "u16".to_string(),
"u32" => "u32".to_string(),
"u64" => "u64".to_string(),
"u128" => "u128".to_string(),
"i8" => "i8".to_string(),
"i16" => "i16".to_string(),
"i32" => "i32".to_string(),
"i64" => "i64".to_string(),
"i128" => "i128".to_string(),
"bool" => "bool".to_string(),
"string" => "String".to_string(),
"publicKey" | "pubkey" => "solana_pubkey::Pubkey".to_string(),
"bytes" => "Vec<u8>".to_string(),
_ => idl_type.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FieldPath {
pub segments: Vec<String>,
pub offsets: Option<Vec<usize>>,
}
impl FieldPath {
pub fn new(segments: &[&str]) -> Self {
FieldPath {
segments: segments.iter().map(|s| s.to_string()).collect(),
offsets: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Transformation {
HexEncode,
HexDecode,
Base58Encode,
Base58Decode,
ToString,
ToNumber,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PopulationStrategy {
SetOnce,
LastWrite,
Append,
Merge,
Max,
Sum,
Count,
Min,
UniqueCount,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComputedFieldSpec {
pub target_path: String,
pub expression: ComputedExpr,
pub result_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComputedExpr {
FieldRef {
path: String,
},
UnwrapOr {
expr: Box<ComputedExpr>,
default: serde_json::Value,
},
Binary {
op: BinaryOp,
left: Box<ComputedExpr>,
right: Box<ComputedExpr>,
},
Cast {
expr: Box<ComputedExpr>,
to_type: String,
},
MethodCall {
expr: Box<ComputedExpr>,
method: String,
args: Vec<ComputedExpr>,
},
Literal {
value: serde_json::Value,
},
Paren {
expr: Box<ComputedExpr>,
},
Var {
name: String,
},
Let {
name: String,
value: Box<ComputedExpr>,
body: Box<ComputedExpr>,
},
If {
condition: Box<ComputedExpr>,
then_branch: Box<ComputedExpr>,
else_branch: Box<ComputedExpr>,
},
None,
Some {
value: Box<ComputedExpr>,
},
Slice {
expr: Box<ComputedExpr>,
start: usize,
end: usize,
},
Index {
expr: Box<ComputedExpr>,
index: usize,
},
U64FromLeBytes {
bytes: Box<ComputedExpr>,
},
U64FromBeBytes {
bytes: Box<ComputedExpr>,
},
ByteArray {
bytes: Vec<u8>,
},
Closure {
param: String,
body: Box<ComputedExpr>,
},
Unary {
op: UnaryOp,
expr: Box<ComputedExpr>,
},
JsonToBytes {
expr: Box<ComputedExpr>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BinaryOp {
Add,
Sub,
Mul,
Div,
Mod,
Gt,
Lt,
Gte,
Lte,
Eq,
Ne,
And,
Or,
Xor,
BitAnd,
BitOr,
Shl,
Shr,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UnaryOp {
Not,
ReverseBits,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableStreamSpec {
pub state_name: String,
#[serde(default)]
pub program_id: Option<String>,
#[serde(default)]
pub idl: Option<IdlSnapshot>,
pub identity: IdentitySpec,
pub handlers: Vec<SerializableHandlerSpec>,
pub sections: Vec<EntitySection>,
pub field_mappings: BTreeMap<String, FieldTypeInfo>,
pub resolver_hooks: Vec<ResolverHook>,
pub instruction_hooks: Vec<InstructionHook>,
#[serde(default)]
pub computed_fields: Vec<String>,
#[serde(default)]
pub computed_field_specs: Vec<ComputedFieldSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TypedStreamSpec<S> {
pub state_name: String,
pub identity: IdentitySpec,
pub handlers: Vec<TypedHandlerSpec<S>>,
pub sections: Vec<EntitySection>, pub field_mappings: BTreeMap<String, FieldTypeInfo>, pub resolver_hooks: Vec<ResolverHook>, pub instruction_hooks: Vec<InstructionHook>, pub computed_fields: Vec<String>, _phantom: PhantomData<S>,
}
impl<S> TypedStreamSpec<S> {
pub fn new(
state_name: String,
identity: IdentitySpec,
handlers: Vec<TypedHandlerSpec<S>>,
) -> Self {
TypedStreamSpec {
state_name,
identity,
handlers,
sections: Vec::new(),
field_mappings: BTreeMap::new(),
resolver_hooks: Vec::new(),
instruction_hooks: Vec::new(),
computed_fields: Vec::new(),
_phantom: PhantomData,
}
}
pub fn with_type_info(
state_name: String,
identity: IdentitySpec,
handlers: Vec<TypedHandlerSpec<S>>,
sections: Vec<EntitySection>,
field_mappings: BTreeMap<String, FieldTypeInfo>,
) -> Self {
TypedStreamSpec {
state_name,
identity,
handlers,
sections,
field_mappings,
resolver_hooks: Vec::new(),
instruction_hooks: Vec::new(),
computed_fields: Vec::new(),
_phantom: PhantomData,
}
}
pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
self.field_mappings.get(path)
}
pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
self.sections
.iter()
.find(|s| s.name == section_name)
.map(|s| &s.fields)
}
pub fn get_section_names(&self) -> Vec<&String> {
self.sections.iter().map(|s| &s.name).collect()
}
pub fn to_serializable(&self) -> SerializableStreamSpec {
let mut spec = SerializableStreamSpec {
state_name: self.state_name.clone(),
program_id: None, idl: None, identity: self.identity.clone(),
handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
sections: self.sections.clone(),
field_mappings: self.field_mappings.clone(),
resolver_hooks: self.resolver_hooks.clone(),
instruction_hooks: self.instruction_hooks.clone(),
computed_fields: self.computed_fields.clone(),
computed_field_specs: Vec::new(), content_hash: None,
};
spec.content_hash = Some(spec.compute_content_hash());
spec
}
pub fn from_serializable(spec: SerializableStreamSpec) -> Self {
TypedStreamSpec {
state_name: spec.state_name,
identity: spec.identity,
handlers: spec
.handlers
.into_iter()
.map(|h| TypedHandlerSpec::from_serializable(h))
.collect(),
sections: spec.sections,
field_mappings: spec.field_mappings,
resolver_hooks: spec.resolver_hooks,
instruction_hooks: spec.instruction_hooks,
computed_fields: spec.computed_fields,
_phantom: PhantomData,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentitySpec {
pub primary_keys: Vec<String>,
pub lookup_indexes: Vec<LookupIndexSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LookupIndexSpec {
pub field_name: String,
pub temporal_field: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolverHook {
pub account_type: String,
pub strategy: ResolverStrategy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ResolverStrategy {
PdaReverseLookup {
lookup_name: String,
queue_discriminators: Vec<Vec<u8>>,
},
DirectField { field_path: FieldPath },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstructionHook {
pub instruction_type: String,
pub actions: Vec<HookAction>,
pub lookup_by: Option<FieldPath>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HookAction {
RegisterPdaMapping {
pda_field: FieldPath,
seed_field: FieldPath,
lookup_name: String,
},
SetField {
target_field: String,
source: MappingSource,
condition: Option<ConditionExpr>,
},
IncrementField {
target_field: String,
increment_by: i64,
condition: Option<ConditionExpr>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConditionExpr {
pub expression: String,
pub parsed: Option<ParsedCondition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ParsedCondition {
Comparison {
field: FieldPath,
op: ComparisonOp,
value: serde_json::Value,
},
Logical {
op: LogicalOp,
conditions: Vec<ParsedCondition>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComparisonOp {
Equal,
NotEqual,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LogicalOp {
And,
Or,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableHandlerSpec {
pub source: SourceSpec,
pub key_resolution: KeyResolutionStrategy,
pub mappings: Vec<SerializableFieldMapping>,
pub conditions: Vec<Condition>,
pub emit: bool,
}
#[derive(Debug, Clone)]
pub struct TypedHandlerSpec<S> {
pub source: SourceSpec,
pub key_resolution: KeyResolutionStrategy,
pub mappings: Vec<TypedFieldMapping<S>>,
pub conditions: Vec<Condition>,
pub emit: bool,
_phantom: PhantomData<S>,
}
impl<S> TypedHandlerSpec<S> {
pub fn new(
source: SourceSpec,
key_resolution: KeyResolutionStrategy,
mappings: Vec<TypedFieldMapping<S>>,
emit: bool,
) -> Self {
TypedHandlerSpec {
source,
key_resolution,
mappings,
conditions: vec![],
emit,
_phantom: PhantomData,
}
}
pub fn to_serializable(&self) -> SerializableHandlerSpec {
SerializableHandlerSpec {
source: self.source.clone(),
key_resolution: self.key_resolution.clone(),
mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
conditions: self.conditions.clone(),
emit: self.emit,
}
}
pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
TypedHandlerSpec {
source: spec.source,
key_resolution: spec.key_resolution,
mappings: spec
.mappings
.into_iter()
.map(|m| TypedFieldMapping::from_serializable(m))
.collect(),
conditions: spec.conditions,
emit: spec.emit,
_phantom: PhantomData,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KeyResolutionStrategy {
Embedded {
primary_field: FieldPath,
},
Lookup {
primary_field: FieldPath,
},
Computed {
primary_field: FieldPath,
compute_partition: ComputeFunction,
},
TemporalLookup {
lookup_field: FieldPath,
timestamp_field: FieldPath,
index_name: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SourceSpec {
Source {
program_id: Option<String>,
discriminator: Option<Vec<u8>>,
type_name: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableFieldMapping {
pub target_path: String,
pub source: MappingSource,
pub transform: Option<Transformation>,
pub population: PopulationStrategy,
}
#[derive(Debug, Clone)]
pub struct TypedFieldMapping<S> {
pub target_path: String,
pub source: MappingSource,
pub transform: Option<Transformation>,
pub population: PopulationStrategy,
_phantom: PhantomData<S>,
}
impl<S> TypedFieldMapping<S> {
pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
TypedFieldMapping {
target_path,
source,
transform: None,
population,
_phantom: PhantomData,
}
}
pub fn with_transform(mut self, transform: Transformation) -> Self {
self.transform = Some(transform);
self
}
pub fn to_serializable(&self) -> SerializableFieldMapping {
SerializableFieldMapping {
target_path: self.target_path.clone(),
source: self.source.clone(),
transform: self.transform.clone(),
population: self.population.clone(),
}
}
pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
TypedFieldMapping {
target_path: mapping.target_path,
source: mapping.source,
transform: mapping.transform,
population: mapping.population,
_phantom: PhantomData,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MappingSource {
FromSource {
path: FieldPath,
default: Option<Value>,
transform: Option<Transformation>,
},
Constant(Value),
Computed {
inputs: Vec<FieldPath>,
function: ComputeFunction,
},
FromState {
path: String,
},
AsEvent {
fields: Vec<Box<MappingSource>>,
},
WholeSource,
AsCapture {
field_transforms: BTreeMap<String, Transformation>,
},
FromContext {
field: String,
},
}
impl MappingSource {
pub fn with_transform(self, transform: Transformation) -> Self {
match self {
MappingSource::FromSource {
path,
default,
transform: _,
} => MappingSource::FromSource {
path,
default,
transform: Some(transform),
},
other => other,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComputeFunction {
Sum,
Concat,
Format(String),
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Condition {
pub field: FieldPath,
pub operator: ConditionOp,
pub value: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConditionOp {
Equals,
NotEquals,
GreaterThan,
LessThan,
Contains,
Exists,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldTypeInfo {
pub field_name: String,
pub rust_type_name: String, pub base_type: BaseType, pub is_optional: bool, pub is_array: bool, pub inner_type: Option<String>, pub source_path: Option<String>, #[serde(default)]
pub resolved_type: Option<ResolvedStructType>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedStructType {
pub type_name: String,
pub fields: Vec<ResolvedField>,
pub is_instruction: bool,
pub is_account: bool,
pub is_event: bool,
#[serde(default)]
pub is_enum: bool,
#[serde(default)]
pub enum_variants: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedField {
pub field_name: String,
pub field_type: String,
pub base_type: BaseType,
pub is_optional: bool,
pub is_array: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BaseType {
Integer, Float, String, Boolean, Object, Array, Binary, Timestamp, Pubkey, Any, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntitySection {
pub name: String,
pub fields: Vec<FieldTypeInfo>,
pub is_nested_struct: bool,
pub parent_field: Option<String>, }
impl FieldTypeInfo {
pub fn new(field_name: String, rust_type_name: String) -> Self {
let (base_type, is_optional, is_array, inner_type) =
Self::analyze_rust_type(&rust_type_name);
FieldTypeInfo {
field_name: field_name.clone(),
rust_type_name,
base_type: Self::infer_semantic_type(&field_name, base_type),
is_optional,
is_array,
inner_type,
source_path: None,
resolved_type: None,
}
}
pub fn with_source_path(mut self, source_path: String) -> Self {
self.source_path = Some(source_path);
self
}
fn analyze_rust_type(rust_type: &str) -> (BaseType, bool, bool, Option<String>) {
let type_str = rust_type.trim();
if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
let (inner_base_type, _, inner_is_array, inner_inner_type) =
Self::analyze_rust_type(&inner);
return (
inner_base_type,
true,
inner_is_array,
inner_inner_type.or(Some(inner)),
);
}
if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
let (_inner_base_type, inner_is_optional, _, inner_inner_type) =
Self::analyze_rust_type(&inner);
return (
BaseType::Array,
inner_is_optional,
true,
inner_inner_type.or(Some(inner)),
);
}
let base_type = match type_str {
"i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64" | "usize" => {
BaseType::Integer
}
"f32" | "f64" => BaseType::Float,
"bool" => BaseType::Boolean,
"String" | "&str" | "str" => BaseType::String,
"Value" | "serde_json::Value" => BaseType::Any,
"Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
_ => {
if type_str.contains("Bytes") || type_str.contains("bytes") {
BaseType::Binary
} else if type_str.contains("Pubkey") {
BaseType::Pubkey
} else {
BaseType::Object
}
}
};
(base_type, false, false, None)
}
fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
let pattern = format!("{}<", generic_name);
if type_str.starts_with(&pattern) && type_str.ends_with('>') {
let start = pattern.len();
let end = type_str.len() - 1;
if end > start {
return Some(type_str[start..end].trim().to_string());
}
}
None
}
fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
let lower_name = field_name.to_lowercase();
if base_type == BaseType::Integer
&& (lower_name.ends_with("_at")
|| lower_name.ends_with("_time")
|| lower_name.contains("timestamp")
|| lower_name.contains("created")
|| lower_name.contains("settled")
|| lower_name.contains("activated"))
{
return BaseType::Timestamp;
}
base_type
}
}
pub trait FieldAccessor<S> {
fn path(&self) -> String;
}
impl SerializableStreamSpec {
pub fn compute_content_hash(&self) -> String {
use sha2::{Digest, Sha256};
let mut spec_for_hash = self.clone();
spec_for_hash.content_hash = None;
let json =
serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
let mut hasher = Sha256::new();
hasher.update(json.as_bytes());
let result = hasher.finalize();
hex::encode(result)
}
pub fn verify_content_hash(&self) -> bool {
match &self.content_hash {
Some(hash) => {
let computed = self.compute_content_hash();
hash == &computed
}
None => true, }
}
pub fn with_content_hash(mut self) -> Self {
self.content_hash = Some(self.compute_content_hash());
self
}
}
#[macro_export]
macro_rules! define_accessor {
($name:ident, $state:ty, $path:expr) => {
pub struct $name;
impl $crate::ast::FieldAccessor<$state> for $name {
fn path(&self) -> String {
$path.to_string()
}
}
};
}