use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::marker::PhantomData;
pub use hyperstack_idl::snapshot::*;
pub const CURRENT_AST_VERSION: &str = "0.0.1";
fn default_ast_version() -> String {
CURRENT_AST_VERSION.to_string()
}
pub fn idl_type_snapshot_to_rust_string(ty: &IdlTypeSnapshot) -> String {
match ty {
IdlTypeSnapshot::Simple(s) => map_simple_idl_type(s),
IdlTypeSnapshot::Array(arr) => {
if arr.array.len() == 2 {
match (&arr.array[0], &arr.array[1]) {
(IdlArrayElementSnapshot::TypeName(t), IdlArrayElementSnapshot::Size(size)) => {
format!("[{}; {}]", map_simple_idl_type(t), size)
}
(
IdlArrayElementSnapshot::Type(nested),
IdlArrayElementSnapshot::Size(size),
) => {
format!("[{}; {}]", idl_type_snapshot_to_rust_string(nested), size)
}
_ => "Vec<u8>".to_string(),
}
} else {
"Vec<u8>".to_string()
}
}
IdlTypeSnapshot::Option(opt) => {
format!("Option<{}>", idl_type_snapshot_to_rust_string(&opt.option))
}
IdlTypeSnapshot::Vec(vec) => {
format!("Vec<{}>", idl_type_snapshot_to_rust_string(&vec.vec))
}
IdlTypeSnapshot::HashMap(map) => {
let key_type = idl_type_snapshot_to_rust_string(&map.hash_map.0);
let val_type = idl_type_snapshot_to_rust_string(&map.hash_map.1);
format!("std::collections::HashMap<{}, {}>", key_type, val_type)
}
IdlTypeSnapshot::Defined(def) => match &def.defined {
IdlDefinedInnerSnapshot::Named { name } => name.clone(),
IdlDefinedInnerSnapshot::Simple(s) => s.clone(),
},
}
}
fn map_simple_idl_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, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum ResolverType {
Token,
Url(UrlResolverConfig),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
#[serde(rename_all = "lowercase")]
pub enum HttpMethod {
#[default]
Get,
Post,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum UrlTemplatePart {
Literal(String),
FieldRef(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum UrlSource {
FieldPath(String),
Template(Vec<UrlTemplatePart>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct UrlResolverConfig {
pub url_source: UrlSource,
#[serde(default)]
pub method: HttpMethod,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extract_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ResolverExtractSpec {
pub target_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transform: Option<Transformation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum ResolveStrategy {
#[default]
SetOnce,
LastWrite,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ResolverCondition {
pub field_path: String,
pub op: ComparisonOp,
pub value: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolverSpec {
pub resolver: ResolverType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_value: Option<Value>,
#[serde(default)]
pub strategy: ResolveStrategy,
pub extracts: Vec<ResolverExtractSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub condition: Option<ResolverCondition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schedule_at: Option<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>,
},
ResolverComputed {
resolver: String,
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>,
},
ContextSlot,
ContextTimestamp,
Keccak256 {
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 {
#[serde(default = "default_ast_version")]
pub ast_version: String,
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 resolver_specs: Vec<ResolverSpec>,
#[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>,
#[serde(default)]
pub views: Vec<ViewDef>,
}
#[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 resolver_specs: Vec<ResolverSpec>,
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(),
resolver_specs: 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(),
resolver_specs: Vec::new(),
computed_fields: Vec::new(),
_phantom: PhantomData,
}
}
pub fn with_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
self.resolver_specs = resolver_specs;
self
}
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 {
ast_version: CURRENT_AST_VERSION.to_string(),
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(),
resolver_specs: self.resolver_specs.clone(),
computed_fields: self.computed_fields.clone(),
computed_field_specs: Vec::new(),
content_hash: None,
views: Vec::new(),
};
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,
resolver_specs: spec.resolver_specs,
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, PartialEq, Eq)]
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,
#[serde(default, skip_serializing_if = "Option::is_none")]
serialization: Option<IdlSerializationSnapshot>,
#[serde(default)]
is_account: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableFieldMapping {
pub target_path: String,
pub source: MappingSource,
pub transform: Option<Transformation>,
pub population: PopulationStrategy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub condition: Option<ConditionExpr>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub when: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop: Option<String>,
#[serde(default = "default_emit", skip_serializing_if = "is_true")]
pub emit: bool,
}
fn default_emit() -> bool {
true
}
fn default_instruction_discriminant_size() -> usize {
8
}
fn is_true(value: &bool) -> bool {
*value
}
#[derive(Debug, Clone)]
pub struct TypedFieldMapping<S> {
pub target_path: String,
pub source: MappingSource,
pub transform: Option<Transformation>,
pub population: PopulationStrategy,
pub condition: Option<ConditionExpr>,
pub when: Option<String>,
pub stop: Option<String>,
pub emit: bool,
_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,
condition: None,
when: None,
stop: None,
emit: true,
_phantom: PhantomData,
}
}
pub fn with_transform(mut self, transform: Transformation) -> Self {
self.transform = Some(transform);
self
}
pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
self.condition = Some(condition);
self
}
pub fn with_when(mut self, when: String) -> Self {
self.when = Some(when);
self
}
pub fn with_stop(mut self, stop: String) -> Self {
self.stop = Some(stop);
self
}
pub fn with_emit(mut self, emit: bool) -> Self {
self.emit = emit;
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(),
condition: self.condition.clone(),
when: self.when.clone(),
stop: self.stop.clone(),
emit: self.emit,
}
}
pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
TypedFieldMapping {
target_path: mapping.target_path,
source: mapping.source,
transform: mapping.transform,
population: mapping.population,
condition: mapping.condition,
when: mapping.when,
stop: mapping.stop,
emit: mapping.emit,
_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>,
#[serde(default = "default_emit", skip_serializing_if = "is_true")]
pub emit: bool,
}
#[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,
emit: true,
}
}
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
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PdaDefinition {
pub name: String,
pub seeds: Vec<PdaSeedDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub program_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PdaSeedDef {
Literal { value: String },
Bytes { value: Vec<u8> },
ArgRef {
arg_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
arg_type: Option<String>,
},
AccountRef { account_name: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "category", rename_all = "camelCase")]
pub enum AccountResolution {
Signer,
Known { address: String },
PdaRef { pda_name: String },
PdaInline {
seeds: Vec<PdaSeedDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
program_id: Option<String>,
},
UserProvided,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InstructionAccountDef {
pub name: String,
#[serde(default)]
pub is_signer: bool,
#[serde(default)]
pub is_writable: bool,
pub resolution: AccountResolution,
#[serde(default)]
pub is_optional: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub docs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InstructionArgDef {
pub name: String,
#[serde(rename = "type")]
pub arg_type: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub docs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InstructionDef {
pub name: String,
pub discriminator: Vec<u8>,
#[serde(default = "default_instruction_discriminant_size")]
pub discriminator_size: usize,
pub accounts: Vec<InstructionAccountDef>,
pub args: Vec<InstructionArgDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<IdlErrorSnapshot>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub program_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub docs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableStackSpec {
#[serde(default = "default_ast_version")]
pub ast_version: String,
pub stack_name: String,
#[serde(default)]
pub program_ids: Vec<String>,
#[serde(default)]
pub idls: Vec<IdlSnapshot>,
pub entities: Vec<SerializableStreamSpec>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub instructions: Vec<InstructionDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
}
impl SerializableStackSpec {
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 stack spec for hashing");
let mut hasher = Sha256::new();
hasher.update(json.as_bytes());
hex::encode(hasher.finalize())
}
pub fn with_content_hash(mut self) -> Self {
self.content_hash = Some(self.compute_content_hash());
self
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SortOrder {
#[default]
Asc,
Desc,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum CompareOp {
Eq,
Ne,
Gt,
Gte,
Lt,
Lte,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum PredicateValue {
Literal(serde_json::Value),
Dynamic(String),
Field(FieldPath),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Predicate {
Compare {
field: FieldPath,
op: CompareOp,
value: PredicateValue,
},
And(Vec<Predicate>),
Or(Vec<Predicate>),
Not(Box<Predicate>),
Exists { field: FieldPath },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ViewTransform {
Filter { predicate: Predicate },
Sort {
key: FieldPath,
#[serde(default)]
order: SortOrder,
},
Take { count: usize },
Skip { count: usize },
First,
Last,
MaxBy { key: FieldPath },
MinBy { key: FieldPath },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ViewSource {
Entity { name: String },
View { id: String },
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub enum ViewOutput {
#[default]
Collection,
Single,
Keyed { key_field: FieldPath },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ViewDef {
pub id: String,
pub source: ViewSource,
#[serde(default)]
pub pipeline: Vec<ViewTransform>,
#[serde(default)]
pub output: ViewOutput,
}
impl ViewDef {
pub fn list(entity_name: &str) -> Self {
ViewDef {
id: format!("{}/list", entity_name),
source: ViewSource::Entity {
name: entity_name.to_string(),
},
pipeline: vec![],
output: ViewOutput::Collection,
}
}
pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
ViewDef {
id: format!("{}/state", entity_name),
source: ViewSource::Entity {
name: entity_name.to_string(),
},
pipeline: vec![],
output: ViewOutput::Keyed {
key_field: FieldPath::new(key_field),
},
}
}
pub fn is_single(&self) -> bool {
matches!(self.output, ViewOutput::Single)
}
pub fn has_single_transform(&self) -> bool {
self.pipeline.iter().any(|t| {
matches!(
t,
ViewTransform::First
| ViewTransform::Last
| ViewTransform::MaxBy { .. }
| ViewTransform::MinBy { .. }
)
})
}
}
#[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()
}
}
};
}