1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use std::marker::PhantomData;
5
6pub use hyperstack_idl::snapshot::*;
7
8pub const CURRENT_AST_VERSION: &str = "0.0.1";
15
16fn default_ast_version() -> String {
17 CURRENT_AST_VERSION.to_string()
18}
19
20pub fn idl_type_snapshot_to_rust_string(ty: &IdlTypeSnapshot) -> String {
21 match ty {
22 IdlTypeSnapshot::Simple(s) => map_simple_idl_type(s),
23 IdlTypeSnapshot::Array(arr) => {
24 if arr.array.len() == 2 {
25 match (&arr.array[0], &arr.array[1]) {
26 (IdlArrayElementSnapshot::TypeName(t), IdlArrayElementSnapshot::Size(size)) => {
27 format!("[{}; {}]", map_simple_idl_type(t), size)
28 }
29 (
30 IdlArrayElementSnapshot::Type(nested),
31 IdlArrayElementSnapshot::Size(size),
32 ) => {
33 format!("[{}; {}]", idl_type_snapshot_to_rust_string(nested), size)
34 }
35 _ => "Vec<u8>".to_string(),
36 }
37 } else {
38 "Vec<u8>".to_string()
39 }
40 }
41 IdlTypeSnapshot::Option(opt) => {
42 format!("Option<{}>", idl_type_snapshot_to_rust_string(&opt.option))
43 }
44 IdlTypeSnapshot::Vec(vec) => {
45 format!("Vec<{}>", idl_type_snapshot_to_rust_string(&vec.vec))
46 }
47 IdlTypeSnapshot::HashMap(map) => {
48 let key_type = idl_type_snapshot_to_rust_string(&map.hash_map.0);
49 let val_type = idl_type_snapshot_to_rust_string(&map.hash_map.1);
50 format!("std::collections::HashMap<{}, {}>", key_type, val_type)
51 }
52 IdlTypeSnapshot::Defined(def) => match &def.defined {
53 IdlDefinedInnerSnapshot::Named { name } => name.clone(),
54 IdlDefinedInnerSnapshot::Simple(s) => s.clone(),
55 },
56 }
57}
58
59fn map_simple_idl_type(idl_type: &str) -> String {
60 match idl_type {
61 "u8" => "u8".to_string(),
62 "u16" => "u16".to_string(),
63 "u32" => "u32".to_string(),
64 "u64" => "u64".to_string(),
65 "u128" => "u128".to_string(),
66 "i8" => "i8".to_string(),
67 "i16" => "i16".to_string(),
68 "i32" => "i32".to_string(),
69 "i64" => "i64".to_string(),
70 "i128" => "i128".to_string(),
71 "bool" => "bool".to_string(),
72 "string" => "String".to_string(),
73 "publicKey" | "pubkey" => "solana_pubkey::Pubkey".to_string(),
74 "bytes" => "Vec<u8>".to_string(),
75 _ => idl_type.to_string(),
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
80pub struct FieldPath {
81 pub segments: Vec<String>,
82 pub offsets: Option<Vec<usize>>,
83}
84
85impl FieldPath {
86 pub fn new(segments: &[&str]) -> Self {
87 FieldPath {
88 segments: segments.iter().map(|s| s.to_string()).collect(),
89 offsets: None,
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95pub enum Transformation {
96 HexEncode,
97 HexDecode,
98 Base58Encode,
99 Base58Decode,
100 ToString,
101 ToNumber,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub enum PopulationStrategy {
106 SetOnce,
107 LastWrite,
108 Append,
109 Merge,
110 Max,
111 Sum,
113 Count,
115 Min,
117 UniqueCount,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ComputedFieldSpec {
129 pub target_path: String,
131 pub expression: ComputedExpr,
133 pub result_type: String,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
142#[serde(rename_all = "lowercase")]
143pub enum ResolverType {
144 Token,
145 Url(UrlResolverConfig),
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
149#[serde(rename_all = "lowercase")]
150pub enum HttpMethod {
151 #[default]
152 Get,
153 Post,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
157pub enum UrlTemplatePart {
158 Literal(String),
159 FieldRef(String),
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
163pub enum UrlSource {
164 FieldPath(String),
165 Template(Vec<UrlTemplatePart>),
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
169pub struct UrlResolverConfig {
170 pub url_source: UrlSource,
171 #[serde(default)]
172 pub method: HttpMethod,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub extract_path: Option<String>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
178pub struct ResolverExtractSpec {
179 pub target_path: String,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub source_path: Option<String>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub transform: Option<Transformation>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
187pub enum ResolveStrategy {
188 #[default]
189 SetOnce,
190 LastWrite,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
194pub struct ResolverCondition {
195 pub field_path: String,
196 pub op: ComparisonOp,
197 pub value: Value,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct ResolverSpec {
202 pub resolver: ResolverType,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub input_path: Option<String>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub input_value: Option<Value>,
207 #[serde(default)]
208 pub strategy: ResolveStrategy,
209 pub extracts: Vec<ResolverExtractSpec>,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub condition: Option<ResolverCondition>,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub schedule_at: Option<String>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
226pub enum ComputedExpr {
227 FieldRef {
230 path: String,
231 },
232
233 UnwrapOr {
235 expr: Box<ComputedExpr>,
236 default: serde_json::Value,
237 },
238
239 Binary {
241 op: BinaryOp,
242 left: Box<ComputedExpr>,
243 right: Box<ComputedExpr>,
244 },
245
246 Cast {
248 expr: Box<ComputedExpr>,
249 to_type: String,
250 },
251
252 MethodCall {
254 expr: Box<ComputedExpr>,
255 method: String,
256 args: Vec<ComputedExpr>,
257 },
258
259 ResolverComputed {
261 resolver: String,
262 method: String,
263 args: Vec<ComputedExpr>,
264 },
265
266 Literal {
268 value: serde_json::Value,
269 },
270
271 Paren {
273 expr: Box<ComputedExpr>,
274 },
275
276 Var {
278 name: String,
279 },
280
281 Let {
283 name: String,
284 value: Box<ComputedExpr>,
285 body: Box<ComputedExpr>,
286 },
287
288 If {
290 condition: Box<ComputedExpr>,
291 then_branch: Box<ComputedExpr>,
292 else_branch: Box<ComputedExpr>,
293 },
294
295 None,
297 Some {
298 value: Box<ComputedExpr>,
299 },
300
301 Slice {
303 expr: Box<ComputedExpr>,
304 start: usize,
305 end: usize,
306 },
307 Index {
308 expr: Box<ComputedExpr>,
309 index: usize,
310 },
311
312 U64FromLeBytes {
314 bytes: Box<ComputedExpr>,
315 },
316 U64FromBeBytes {
317 bytes: Box<ComputedExpr>,
318 },
319
320 ByteArray {
322 bytes: Vec<u8>,
323 },
324
325 Closure {
327 param: String,
328 body: Box<ComputedExpr>,
329 },
330
331 Unary {
333 op: UnaryOp,
334 expr: Box<ComputedExpr>,
335 },
336
337 JsonToBytes {
339 expr: Box<ComputedExpr>,
340 },
341
342 ContextSlot,
345 ContextTimestamp,
347
348 Keccak256 {
351 expr: Box<ComputedExpr>,
352 },
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357pub enum BinaryOp {
358 Add,
360 Sub,
361 Mul,
362 Div,
363 Mod,
364 Gt,
366 Lt,
367 Gte,
368 Lte,
369 Eq,
370 Ne,
371 And,
373 Or,
374 Xor,
376 BitAnd,
377 BitOr,
378 Shl,
379 Shr,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
384pub enum UnaryOp {
385 Not,
386 ReverseBits,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct SerializableStreamSpec {
392 #[serde(default = "default_ast_version")]
395 pub ast_version: String,
396 pub state_name: String,
397 #[serde(default)]
399 pub program_id: Option<String>,
400 #[serde(default)]
402 pub idl: Option<IdlSnapshot>,
403 pub identity: IdentitySpec,
404 pub handlers: Vec<SerializableHandlerSpec>,
405 pub sections: Vec<EntitySection>,
406 pub field_mappings: BTreeMap<String, FieldTypeInfo>,
407 pub resolver_hooks: Vec<ResolverHook>,
408 pub instruction_hooks: Vec<InstructionHook>,
409 #[serde(default)]
410 pub resolver_specs: Vec<ResolverSpec>,
411 #[serde(default)]
413 pub computed_fields: Vec<String>,
414 #[serde(default)]
416 pub computed_field_specs: Vec<ComputedFieldSpec>,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub content_hash: Option<String>,
421 #[serde(default)]
423 pub views: Vec<ViewDef>,
424}
425
426#[derive(Debug, Clone)]
427pub struct TypedStreamSpec<S> {
428 pub state_name: String,
429 pub identity: IdentitySpec,
430 pub handlers: Vec<TypedHandlerSpec<S>>,
431 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>,
436 pub computed_fields: Vec<String>, _phantom: PhantomData<S>,
438}
439
440impl<S> TypedStreamSpec<S> {
441 pub fn new(
442 state_name: String,
443 identity: IdentitySpec,
444 handlers: Vec<TypedHandlerSpec<S>>,
445 ) -> Self {
446 TypedStreamSpec {
447 state_name,
448 identity,
449 handlers,
450 sections: Vec::new(),
451 field_mappings: BTreeMap::new(),
452 resolver_hooks: Vec::new(),
453 instruction_hooks: Vec::new(),
454 resolver_specs: Vec::new(),
455 computed_fields: Vec::new(),
456 _phantom: PhantomData,
457 }
458 }
459
460 pub fn with_type_info(
462 state_name: String,
463 identity: IdentitySpec,
464 handlers: Vec<TypedHandlerSpec<S>>,
465 sections: Vec<EntitySection>,
466 field_mappings: BTreeMap<String, FieldTypeInfo>,
467 ) -> Self {
468 TypedStreamSpec {
469 state_name,
470 identity,
471 handlers,
472 sections,
473 field_mappings,
474 resolver_hooks: Vec::new(),
475 instruction_hooks: Vec::new(),
476 resolver_specs: Vec::new(),
477 computed_fields: Vec::new(),
478 _phantom: PhantomData,
479 }
480 }
481
482 pub fn with_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
483 self.resolver_specs = resolver_specs;
484 self
485 }
486
487 pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
489 self.field_mappings.get(path)
490 }
491
492 pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
494 self.sections
495 .iter()
496 .find(|s| s.name == section_name)
497 .map(|s| &s.fields)
498 }
499
500 pub fn get_section_names(&self) -> Vec<&String> {
502 self.sections.iter().map(|s| &s.name).collect()
503 }
504
505 pub fn to_serializable(&self) -> SerializableStreamSpec {
507 let mut spec = SerializableStreamSpec {
508 ast_version: CURRENT_AST_VERSION.to_string(),
509 state_name: self.state_name.clone(),
510 program_id: None,
511 idl: None,
512 identity: self.identity.clone(),
513 handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
514 sections: self.sections.clone(),
515 field_mappings: self.field_mappings.clone(),
516 resolver_hooks: self.resolver_hooks.clone(),
517 instruction_hooks: self.instruction_hooks.clone(),
518 resolver_specs: self.resolver_specs.clone(),
519 computed_fields: self.computed_fields.clone(),
520 computed_field_specs: Vec::new(),
521 content_hash: None,
522 views: Vec::new(),
523 };
524 spec.content_hash = Some(spec.compute_content_hash());
525 spec
526 }
527
528 pub fn from_serializable(spec: SerializableStreamSpec) -> Self {
530 TypedStreamSpec {
531 state_name: spec.state_name,
532 identity: spec.identity,
533 handlers: spec
534 .handlers
535 .into_iter()
536 .map(|h| TypedHandlerSpec::from_serializable(h))
537 .collect(),
538 sections: spec.sections,
539 field_mappings: spec.field_mappings,
540 resolver_hooks: spec.resolver_hooks,
541 instruction_hooks: spec.instruction_hooks,
542 resolver_specs: spec.resolver_specs,
543 computed_fields: spec.computed_fields,
544 _phantom: PhantomData,
545 }
546 }
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct IdentitySpec {
551 pub primary_keys: Vec<String>,
552 pub lookup_indexes: Vec<LookupIndexSpec>,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct LookupIndexSpec {
557 pub field_name: String,
558 pub temporal_field: Option<String>,
559}
560
561#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct ResolverHook {
568 pub account_type: String,
570
571 pub strategy: ResolverStrategy,
573}
574
575#[derive(Debug, Clone, Serialize, Deserialize)]
576pub enum ResolverStrategy {
577 PdaReverseLookup {
579 lookup_name: String,
580 queue_discriminators: Vec<Vec<u8>>,
582 },
583
584 DirectField { field_path: FieldPath },
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct InstructionHook {
591 pub instruction_type: String,
593
594 pub actions: Vec<HookAction>,
596
597 pub lookup_by: Option<FieldPath>,
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
602pub enum HookAction {
603 RegisterPdaMapping {
605 pda_field: FieldPath,
606 seed_field: FieldPath,
607 lookup_name: String,
608 },
609
610 SetField {
612 target_field: String,
613 source: MappingSource,
614 condition: Option<ConditionExpr>,
615 },
616
617 IncrementField {
619 target_field: String,
620 increment_by: i64,
621 condition: Option<ConditionExpr>,
622 },
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize)]
627pub struct ConditionExpr {
628 pub expression: String,
630
631 pub parsed: Option<ParsedCondition>,
633}
634
635#[derive(Debug, Clone, Serialize, Deserialize)]
636pub enum ParsedCondition {
637 Comparison {
639 field: FieldPath,
640 op: ComparisonOp,
641 value: serde_json::Value,
642 },
643
644 Logical {
646 op: LogicalOp,
647 conditions: Vec<ParsedCondition>,
648 },
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
652pub enum ComparisonOp {
653 Equal,
654 NotEqual,
655 GreaterThan,
656 GreaterThanOrEqual,
657 LessThan,
658 LessThanOrEqual,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize)]
662pub enum LogicalOp {
663 And,
664 Or,
665}
666
667#[derive(Debug, Clone, Serialize, Deserialize)]
669pub struct SerializableHandlerSpec {
670 pub source: SourceSpec,
671 pub key_resolution: KeyResolutionStrategy,
672 pub mappings: Vec<SerializableFieldMapping>,
673 pub conditions: Vec<Condition>,
674 pub emit: bool,
675}
676
677#[derive(Debug, Clone)]
678pub struct TypedHandlerSpec<S> {
679 pub source: SourceSpec,
680 pub key_resolution: KeyResolutionStrategy,
681 pub mappings: Vec<TypedFieldMapping<S>>,
682 pub conditions: Vec<Condition>,
683 pub emit: bool,
684 _phantom: PhantomData<S>,
685}
686
687impl<S> TypedHandlerSpec<S> {
688 pub fn new(
689 source: SourceSpec,
690 key_resolution: KeyResolutionStrategy,
691 mappings: Vec<TypedFieldMapping<S>>,
692 emit: bool,
693 ) -> Self {
694 TypedHandlerSpec {
695 source,
696 key_resolution,
697 mappings,
698 conditions: vec![],
699 emit,
700 _phantom: PhantomData,
701 }
702 }
703
704 pub fn to_serializable(&self) -> SerializableHandlerSpec {
706 SerializableHandlerSpec {
707 source: self.source.clone(),
708 key_resolution: self.key_resolution.clone(),
709 mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
710 conditions: self.conditions.clone(),
711 emit: self.emit,
712 }
713 }
714
715 pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
717 TypedHandlerSpec {
718 source: spec.source,
719 key_resolution: spec.key_resolution,
720 mappings: spec
721 .mappings
722 .into_iter()
723 .map(|m| TypedFieldMapping::from_serializable(m))
724 .collect(),
725 conditions: spec.conditions,
726 emit: spec.emit,
727 _phantom: PhantomData,
728 }
729 }
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize)]
733pub enum KeyResolutionStrategy {
734 Embedded {
735 primary_field: FieldPath,
736 },
737 Lookup {
738 primary_field: FieldPath,
739 },
740 Computed {
741 primary_field: FieldPath,
742 compute_partition: ComputeFunction,
743 },
744 TemporalLookup {
745 lookup_field: FieldPath,
746 timestamp_field: FieldPath,
747 index_name: String,
748 },
749}
750
751#[derive(Debug, Clone, Serialize, Deserialize)]
752pub enum SourceSpec {
753 Source {
754 program_id: Option<String>,
755 discriminator: Option<Vec<u8>>,
756 type_name: String,
757 #[serde(default, skip_serializing_if = "Option::is_none")]
758 serialization: Option<IdlSerializationSnapshot>,
759 #[serde(default)]
764 is_account: bool,
765 },
766}
767
768#[derive(Debug, Clone, Serialize, Deserialize)]
770pub struct SerializableFieldMapping {
771 pub target_path: String,
772 pub source: MappingSource,
773 pub transform: Option<Transformation>,
774 pub population: PopulationStrategy,
775 #[serde(default, skip_serializing_if = "Option::is_none")]
776 pub condition: Option<ConditionExpr>,
777 #[serde(default, skip_serializing_if = "Option::is_none")]
778 pub when: Option<String>,
779 #[serde(default, skip_serializing_if = "Option::is_none")]
780 pub stop: Option<String>,
781 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
782 pub emit: bool,
783}
784
785fn default_emit() -> bool {
786 true
787}
788
789fn default_instruction_discriminant_size() -> usize {
790 8
791}
792
793fn is_true(value: &bool) -> bool {
794 *value
795}
796
797#[derive(Debug, Clone)]
798pub struct TypedFieldMapping<S> {
799 pub target_path: String,
800 pub source: MappingSource,
801 pub transform: Option<Transformation>,
802 pub population: PopulationStrategy,
803 pub condition: Option<ConditionExpr>,
804 pub when: Option<String>,
805 pub stop: Option<String>,
806 pub emit: bool,
807 _phantom: PhantomData<S>,
808}
809
810impl<S> TypedFieldMapping<S> {
811 pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
812 TypedFieldMapping {
813 target_path,
814 source,
815 transform: None,
816 population,
817 condition: None,
818 when: None,
819 stop: None,
820 emit: true,
821 _phantom: PhantomData,
822 }
823 }
824
825 pub fn with_transform(mut self, transform: Transformation) -> Self {
826 self.transform = Some(transform);
827 self
828 }
829
830 pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
831 self.condition = Some(condition);
832 self
833 }
834
835 pub fn with_when(mut self, when: String) -> Self {
836 self.when = Some(when);
837 self
838 }
839
840 pub fn with_stop(mut self, stop: String) -> Self {
841 self.stop = Some(stop);
842 self
843 }
844
845 pub fn with_emit(mut self, emit: bool) -> Self {
846 self.emit = emit;
847 self
848 }
849
850 pub fn to_serializable(&self) -> SerializableFieldMapping {
852 SerializableFieldMapping {
853 target_path: self.target_path.clone(),
854 source: self.source.clone(),
855 transform: self.transform.clone(),
856 population: self.population.clone(),
857 condition: self.condition.clone(),
858 when: self.when.clone(),
859 stop: self.stop.clone(),
860 emit: self.emit,
861 }
862 }
863
864 pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
866 TypedFieldMapping {
867 target_path: mapping.target_path,
868 source: mapping.source,
869 transform: mapping.transform,
870 population: mapping.population,
871 condition: mapping.condition,
872 when: mapping.when,
873 stop: mapping.stop,
874 emit: mapping.emit,
875 _phantom: PhantomData,
876 }
877 }
878}
879
880#[derive(Debug, Clone, Serialize, Deserialize)]
881pub enum MappingSource {
882 FromSource {
883 path: FieldPath,
884 default: Option<Value>,
885 transform: Option<Transformation>,
886 },
887 Constant(Value),
888 Computed {
889 inputs: Vec<FieldPath>,
890 function: ComputeFunction,
891 },
892 FromState {
893 path: String,
894 },
895 AsEvent {
896 fields: Vec<Box<MappingSource>>,
897 },
898 WholeSource,
899 AsCapture {
902 field_transforms: BTreeMap<String, Transformation>,
903 },
904 FromContext {
907 field: String,
908 },
909}
910
911impl MappingSource {
912 pub fn with_transform(self, transform: Transformation) -> Self {
913 match self {
914 MappingSource::FromSource {
915 path,
916 default,
917 transform: _,
918 } => MappingSource::FromSource {
919 path,
920 default,
921 transform: Some(transform),
922 },
923 other => other,
924 }
925 }
926}
927
928#[derive(Debug, Clone, Serialize, Deserialize)]
929pub enum ComputeFunction {
930 Sum,
931 Concat,
932 Format(String),
933 Custom(String),
934}
935
936#[derive(Debug, Clone, Serialize, Deserialize)]
937pub struct Condition {
938 pub field: FieldPath,
939 pub operator: ConditionOp,
940 pub value: Value,
941}
942
943#[derive(Debug, Clone, Serialize, Deserialize)]
944pub enum ConditionOp {
945 Equals,
946 NotEquals,
947 GreaterThan,
948 LessThan,
949 Contains,
950 Exists,
951}
952
953#[derive(Debug, Clone, Serialize, Deserialize)]
955pub struct FieldTypeInfo {
956 pub field_name: String,
957 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)]
965 pub resolved_type: Option<ResolvedStructType>,
966 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
967 pub emit: bool,
968}
969
970#[derive(Debug, Clone, Serialize, Deserialize)]
972pub struct ResolvedStructType {
973 pub type_name: String,
974 pub fields: Vec<ResolvedField>,
975 pub is_instruction: bool,
976 pub is_account: bool,
977 pub is_event: bool,
978 #[serde(default)]
980 pub is_enum: bool,
981 #[serde(default)]
983 pub enum_variants: Vec<String>,
984}
985
986#[derive(Debug, Clone, Serialize, Deserialize)]
988pub struct ResolvedField {
989 pub field_name: String,
990 pub field_type: String,
991 pub base_type: BaseType,
992 pub is_optional: bool,
993 pub is_array: bool,
994}
995
996#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
998pub enum BaseType {
999 Integer, Float, String, Boolean, Object, Array, Binary, Timestamp, Pubkey, Any, }
1015
1016#[derive(Debug, Clone, Serialize, Deserialize)]
1018pub struct EntitySection {
1019 pub name: String,
1020 pub fields: Vec<FieldTypeInfo>,
1021 pub is_nested_struct: bool,
1022 pub parent_field: Option<String>, }
1024
1025impl FieldTypeInfo {
1026 pub fn new(field_name: String, rust_type_name: String) -> Self {
1027 let (base_type, is_optional, is_array, inner_type) =
1028 Self::analyze_rust_type(&rust_type_name);
1029
1030 FieldTypeInfo {
1031 field_name: field_name.clone(),
1032 rust_type_name,
1033 base_type: Self::infer_semantic_type(&field_name, base_type),
1034 is_optional,
1035 is_array,
1036 inner_type,
1037 source_path: None,
1038 resolved_type: None,
1039 emit: true,
1040 }
1041 }
1042
1043 pub fn with_source_path(mut self, source_path: String) -> Self {
1044 self.source_path = Some(source_path);
1045 self
1046 }
1047
1048 fn analyze_rust_type(rust_type: &str) -> (BaseType, bool, bool, Option<String>) {
1050 let type_str = rust_type.trim();
1051
1052 if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1054 let (inner_base_type, _, inner_is_array, inner_inner_type) =
1055 Self::analyze_rust_type(&inner);
1056 return (
1057 inner_base_type,
1058 true,
1059 inner_is_array,
1060 inner_inner_type.or(Some(inner)),
1061 );
1062 }
1063
1064 if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1066 let (_inner_base_type, inner_is_optional, _, inner_inner_type) =
1067 Self::analyze_rust_type(&inner);
1068 return (
1069 BaseType::Array,
1070 inner_is_optional,
1071 true,
1072 inner_inner_type.or(Some(inner)),
1073 );
1074 }
1075
1076 let base_type = match type_str {
1078 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64" | "usize" => {
1079 BaseType::Integer
1080 }
1081 "f32" | "f64" => BaseType::Float,
1082 "bool" => BaseType::Boolean,
1083 "String" | "&str" | "str" => BaseType::String,
1084 "Value" | "serde_json::Value" => BaseType::Any,
1085 "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1086 _ => {
1087 if type_str.contains("Bytes") || type_str.contains("bytes") {
1089 BaseType::Binary
1090 } else if type_str.contains("Pubkey") {
1091 BaseType::Pubkey
1092 } else {
1093 BaseType::Object
1094 }
1095 }
1096 };
1097
1098 (base_type, false, false, None)
1099 }
1100
1101 fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1103 let pattern = format!("{}<", generic_name);
1104 if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1105 let start = pattern.len();
1106 let end = type_str.len() - 1;
1107 if end > start {
1108 return Some(type_str[start..end].trim().to_string());
1109 }
1110 }
1111 None
1112 }
1113
1114 fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1116 let lower_name = field_name.to_lowercase();
1117
1118 if base_type == BaseType::Integer
1120 && (lower_name.ends_with("_at")
1121 || lower_name.ends_with("_time")
1122 || lower_name.contains("timestamp")
1123 || lower_name.contains("created")
1124 || lower_name.contains("settled")
1125 || lower_name.contains("activated"))
1126 {
1127 return BaseType::Timestamp;
1128 }
1129
1130 base_type
1131 }
1132}
1133
1134pub trait FieldAccessor<S> {
1135 fn path(&self) -> String;
1136}
1137
1138impl SerializableStreamSpec {
1143 pub fn compute_content_hash(&self) -> String {
1149 use sha2::{Digest, Sha256};
1150
1151 let mut spec_for_hash = self.clone();
1153 spec_for_hash.content_hash = None;
1154
1155 let json =
1157 serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1158
1159 let mut hasher = Sha256::new();
1161 hasher.update(json.as_bytes());
1162 let result = hasher.finalize();
1163
1164 hex::encode(result)
1166 }
1167
1168 pub fn verify_content_hash(&self) -> bool {
1171 match &self.content_hash {
1172 Some(hash) => {
1173 let computed = self.compute_content_hash();
1174 hash == &computed
1175 }
1176 None => true, }
1178 }
1179
1180 pub fn with_content_hash(mut self) -> Self {
1182 self.content_hash = Some(self.compute_content_hash());
1183 self
1184 }
1185}
1186
1187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1194pub struct PdaDefinition {
1195 pub name: String,
1197
1198 pub seeds: Vec<PdaSeedDef>,
1200
1201 #[serde(default, skip_serializing_if = "Option::is_none")]
1204 pub program_id: Option<String>,
1205}
1206
1207#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1209#[serde(tag = "type", rename_all = "camelCase")]
1210pub enum PdaSeedDef {
1211 Literal { value: String },
1213
1214 Bytes { value: Vec<u8> },
1216
1217 ArgRef {
1219 arg_name: String,
1220 #[serde(default, skip_serializing_if = "Option::is_none")]
1222 arg_type: Option<String>,
1223 },
1224
1225 AccountRef { account_name: String },
1227}
1228
1229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1231#[serde(tag = "category", rename_all = "camelCase")]
1232pub enum AccountResolution {
1233 Signer,
1235
1236 Known { address: String },
1238
1239 PdaRef { pda_name: String },
1241
1242 PdaInline {
1244 seeds: Vec<PdaSeedDef>,
1245 #[serde(default, skip_serializing_if = "Option::is_none")]
1246 program_id: Option<String>,
1247 },
1248
1249 UserProvided,
1251}
1252
1253#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1255pub struct InstructionAccountDef {
1256 pub name: String,
1258
1259 #[serde(default)]
1261 pub is_signer: bool,
1262
1263 #[serde(default)]
1265 pub is_writable: bool,
1266
1267 pub resolution: AccountResolution,
1269
1270 #[serde(default)]
1272 pub is_optional: bool,
1273
1274 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1276 pub docs: Vec<String>,
1277}
1278
1279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1281pub struct InstructionArgDef {
1282 pub name: String,
1284
1285 #[serde(rename = "type")]
1287 pub arg_type: String,
1288
1289 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1291 pub docs: Vec<String>,
1292}
1293
1294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1296pub struct InstructionDef {
1297 pub name: String,
1299
1300 pub discriminator: Vec<u8>,
1302
1303 #[serde(default = "default_instruction_discriminant_size")]
1305 pub discriminator_size: usize,
1306
1307 pub accounts: Vec<InstructionAccountDef>,
1309
1310 pub args: Vec<InstructionArgDef>,
1312
1313 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1315 pub errors: Vec<IdlErrorSnapshot>,
1316
1317 #[serde(default, skip_serializing_if = "Option::is_none")]
1319 pub program_id: Option<String>,
1320
1321 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1323 pub docs: Vec<String>,
1324}
1325
1326#[derive(Debug, Clone, Serialize, Deserialize)]
1333pub struct SerializableStackSpec {
1334 #[serde(default = "default_ast_version")]
1337 pub ast_version: String,
1338 pub stack_name: String,
1340 #[serde(default)]
1342 pub program_ids: Vec<String>,
1343 #[serde(default)]
1345 pub idls: Vec<IdlSnapshot>,
1346 pub entities: Vec<SerializableStreamSpec>,
1348 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1351 pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1352 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1354 pub instructions: Vec<InstructionDef>,
1355 #[serde(default, skip_serializing_if = "Option::is_none")]
1357 pub content_hash: Option<String>,
1358}
1359
1360impl SerializableStackSpec {
1361 pub fn compute_content_hash(&self) -> String {
1363 use sha2::{Digest, Sha256};
1364 let mut spec_for_hash = self.clone();
1365 spec_for_hash.content_hash = None;
1366 let json = serde_json::to_string(&spec_for_hash)
1367 .expect("Failed to serialize stack spec for hashing");
1368 let mut hasher = Sha256::new();
1369 hasher.update(json.as_bytes());
1370 hex::encode(hasher.finalize())
1371 }
1372
1373 pub fn with_content_hash(mut self) -> Self {
1374 self.content_hash = Some(self.compute_content_hash());
1375 self
1376 }
1377}
1378
1379#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1385#[serde(rename_all = "lowercase")]
1386pub enum SortOrder {
1387 #[default]
1388 Asc,
1389 Desc,
1390}
1391
1392#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1394pub enum CompareOp {
1395 Eq,
1396 Ne,
1397 Gt,
1398 Gte,
1399 Lt,
1400 Lte,
1401}
1402
1403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1405pub enum PredicateValue {
1406 Literal(serde_json::Value),
1408 Dynamic(String),
1410 Field(FieldPath),
1412}
1413
1414#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1416pub enum Predicate {
1417 Compare {
1419 field: FieldPath,
1420 op: CompareOp,
1421 value: PredicateValue,
1422 },
1423 And(Vec<Predicate>),
1425 Or(Vec<Predicate>),
1427 Not(Box<Predicate>),
1429 Exists { field: FieldPath },
1431}
1432
1433#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1435pub enum ViewTransform {
1436 Filter { predicate: Predicate },
1438
1439 Sort {
1441 key: FieldPath,
1442 #[serde(default)]
1443 order: SortOrder,
1444 },
1445
1446 Take { count: usize },
1448
1449 Skip { count: usize },
1451
1452 First,
1454
1455 Last,
1457
1458 MaxBy { key: FieldPath },
1460
1461 MinBy { key: FieldPath },
1463}
1464
1465#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1467pub enum ViewSource {
1468 Entity { name: String },
1470 View { id: String },
1472}
1473
1474#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1476pub enum ViewOutput {
1477 #[default]
1479 Collection,
1480 Single,
1482 Keyed { key_field: FieldPath },
1484}
1485
1486#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1488pub struct ViewDef {
1489 pub id: String,
1491
1492 pub source: ViewSource,
1494
1495 #[serde(default)]
1497 pub pipeline: Vec<ViewTransform>,
1498
1499 #[serde(default)]
1501 pub output: ViewOutput,
1502}
1503
1504impl ViewDef {
1505 pub fn list(entity_name: &str) -> Self {
1507 ViewDef {
1508 id: format!("{}/list", entity_name),
1509 source: ViewSource::Entity {
1510 name: entity_name.to_string(),
1511 },
1512 pipeline: vec![],
1513 output: ViewOutput::Collection,
1514 }
1515 }
1516
1517 pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1519 ViewDef {
1520 id: format!("{}/state", entity_name),
1521 source: ViewSource::Entity {
1522 name: entity_name.to_string(),
1523 },
1524 pipeline: vec![],
1525 output: ViewOutput::Keyed {
1526 key_field: FieldPath::new(key_field),
1527 },
1528 }
1529 }
1530
1531 pub fn is_single(&self) -> bool {
1533 matches!(self.output, ViewOutput::Single)
1534 }
1535
1536 pub fn has_single_transform(&self) -> bool {
1538 self.pipeline.iter().any(|t| {
1539 matches!(
1540 t,
1541 ViewTransform::First
1542 | ViewTransform::Last
1543 | ViewTransform::MaxBy { .. }
1544 | ViewTransform::MinBy { .. }
1545 )
1546 })
1547 }
1548}
1549
1550#[macro_export]
1551macro_rules! define_accessor {
1552 ($name:ident, $state:ty, $path:expr) => {
1553 pub struct $name;
1554
1555 impl $crate::ast::FieldAccessor<$state> for $name {
1556 fn path(&self) -> String {
1557 $path.to_string()
1558 }
1559 }
1560 };
1561}