1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use std::marker::PhantomData;
5
6pub use hyperstack_idl::snapshot::*;
7
8pub fn idl_type_snapshot_to_rust_string(ty: &IdlTypeSnapshot) -> String {
9 match ty {
10 IdlTypeSnapshot::Simple(s) => map_simple_idl_type(s),
11 IdlTypeSnapshot::Array(arr) => {
12 if arr.array.len() == 2 {
13 match (&arr.array[0], &arr.array[1]) {
14 (IdlArrayElementSnapshot::TypeName(t), IdlArrayElementSnapshot::Size(size)) => {
15 format!("[{}; {}]", map_simple_idl_type(t), size)
16 }
17 (
18 IdlArrayElementSnapshot::Type(nested),
19 IdlArrayElementSnapshot::Size(size),
20 ) => {
21 format!("[{}; {}]", idl_type_snapshot_to_rust_string(nested), size)
22 }
23 _ => "Vec<u8>".to_string(),
24 }
25 } else {
26 "Vec<u8>".to_string()
27 }
28 }
29 IdlTypeSnapshot::Option(opt) => {
30 format!("Option<{}>", idl_type_snapshot_to_rust_string(&opt.option))
31 }
32 IdlTypeSnapshot::Vec(vec) => {
33 format!("Vec<{}>", idl_type_snapshot_to_rust_string(&vec.vec))
34 }
35 IdlTypeSnapshot::HashMap(map) => {
36 let key_type = idl_type_snapshot_to_rust_string(&map.hash_map.0);
37 let val_type = idl_type_snapshot_to_rust_string(&map.hash_map.1);
38 format!("std::collections::HashMap<{}, {}>", key_type, val_type)
39 }
40 IdlTypeSnapshot::Defined(def) => match &def.defined {
41 IdlDefinedInnerSnapshot::Named { name } => name.clone(),
42 IdlDefinedInnerSnapshot::Simple(s) => s.clone(),
43 },
44 }
45}
46
47fn map_simple_idl_type(idl_type: &str) -> String {
48 match idl_type {
49 "u8" => "u8".to_string(),
50 "u16" => "u16".to_string(),
51 "u32" => "u32".to_string(),
52 "u64" => "u64".to_string(),
53 "u128" => "u128".to_string(),
54 "i8" => "i8".to_string(),
55 "i16" => "i16".to_string(),
56 "i32" => "i32".to_string(),
57 "i64" => "i64".to_string(),
58 "i128" => "i128".to_string(),
59 "bool" => "bool".to_string(),
60 "string" => "String".to_string(),
61 "publicKey" | "pubkey" => "solana_pubkey::Pubkey".to_string(),
62 "bytes" => "Vec<u8>".to_string(),
63 _ => idl_type.to_string(),
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
68pub struct FieldPath {
69 pub segments: Vec<String>,
70 pub offsets: Option<Vec<usize>>,
71}
72
73impl FieldPath {
74 pub fn new(segments: &[&str]) -> Self {
75 FieldPath {
76 segments: segments.iter().map(|s| s.to_string()).collect(),
77 offsets: None,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83pub enum Transformation {
84 HexEncode,
85 HexDecode,
86 Base58Encode,
87 Base58Decode,
88 ToString,
89 ToNumber,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub enum PopulationStrategy {
94 SetOnce,
95 LastWrite,
96 Append,
97 Merge,
98 Max,
99 Sum,
101 Count,
103 Min,
105 UniqueCount,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ComputedFieldSpec {
117 pub target_path: String,
119 pub expression: ComputedExpr,
121 pub result_type: String,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
130#[serde(rename_all = "lowercase")]
131pub enum ResolverType {
132 Token,
133 Url(UrlResolverConfig),
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
137#[serde(rename_all = "lowercase")]
138pub enum HttpMethod {
139 #[default]
140 Get,
141 Post,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
145pub enum UrlTemplatePart {
146 Literal(String),
147 FieldRef(String),
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
151pub enum UrlSource {
152 FieldPath(String),
153 Template(Vec<UrlTemplatePart>),
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
157pub struct UrlResolverConfig {
158 pub url_source: UrlSource,
159 #[serde(default)]
160 pub method: HttpMethod,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub extract_path: Option<String>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
166pub struct ResolverExtractSpec {
167 pub target_path: String,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub source_path: Option<String>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub transform: Option<Transformation>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
175pub enum ResolveStrategy {
176 #[default]
177 SetOnce,
178 LastWrite,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
182pub struct ResolverCondition {
183 pub field_path: String,
184 pub op: ComparisonOp,
185 pub value: Value,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct ResolverSpec {
190 pub resolver: ResolverType,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub input_path: Option<String>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub input_value: Option<Value>,
195 #[serde(default)]
196 pub strategy: ResolveStrategy,
197 pub extracts: Vec<ResolverExtractSpec>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub condition: Option<ResolverCondition>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub schedule_at: Option<String>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
214pub enum ComputedExpr {
215 FieldRef {
218 path: String,
219 },
220
221 UnwrapOr {
223 expr: Box<ComputedExpr>,
224 default: serde_json::Value,
225 },
226
227 Binary {
229 op: BinaryOp,
230 left: Box<ComputedExpr>,
231 right: Box<ComputedExpr>,
232 },
233
234 Cast {
236 expr: Box<ComputedExpr>,
237 to_type: String,
238 },
239
240 MethodCall {
242 expr: Box<ComputedExpr>,
243 method: String,
244 args: Vec<ComputedExpr>,
245 },
246
247 ResolverComputed {
249 resolver: String,
250 method: String,
251 args: Vec<ComputedExpr>,
252 },
253
254 Literal {
256 value: serde_json::Value,
257 },
258
259 Paren {
261 expr: Box<ComputedExpr>,
262 },
263
264 Var {
266 name: String,
267 },
268
269 Let {
271 name: String,
272 value: Box<ComputedExpr>,
273 body: Box<ComputedExpr>,
274 },
275
276 If {
278 condition: Box<ComputedExpr>,
279 then_branch: Box<ComputedExpr>,
280 else_branch: Box<ComputedExpr>,
281 },
282
283 None,
285 Some {
286 value: Box<ComputedExpr>,
287 },
288
289 Slice {
291 expr: Box<ComputedExpr>,
292 start: usize,
293 end: usize,
294 },
295 Index {
296 expr: Box<ComputedExpr>,
297 index: usize,
298 },
299
300 U64FromLeBytes {
302 bytes: Box<ComputedExpr>,
303 },
304 U64FromBeBytes {
305 bytes: Box<ComputedExpr>,
306 },
307
308 ByteArray {
310 bytes: Vec<u8>,
311 },
312
313 Closure {
315 param: String,
316 body: Box<ComputedExpr>,
317 },
318
319 Unary {
321 op: UnaryOp,
322 expr: Box<ComputedExpr>,
323 },
324
325 JsonToBytes {
327 expr: Box<ComputedExpr>,
328 },
329
330 ContextSlot,
333 ContextTimestamp,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339pub enum BinaryOp {
340 Add,
342 Sub,
343 Mul,
344 Div,
345 Mod,
346 Gt,
348 Lt,
349 Gte,
350 Lte,
351 Eq,
352 Ne,
353 And,
355 Or,
356 Xor,
358 BitAnd,
359 BitOr,
360 Shl,
361 Shr,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
366pub enum UnaryOp {
367 Not,
368 ReverseBits,
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct SerializableStreamSpec {
374 pub state_name: String,
375 #[serde(default)]
377 pub program_id: Option<String>,
378 #[serde(default)]
380 pub idl: Option<IdlSnapshot>,
381 pub identity: IdentitySpec,
382 pub handlers: Vec<SerializableHandlerSpec>,
383 pub sections: Vec<EntitySection>,
384 pub field_mappings: BTreeMap<String, FieldTypeInfo>,
385 pub resolver_hooks: Vec<ResolverHook>,
386 pub instruction_hooks: Vec<InstructionHook>,
387 #[serde(default)]
388 pub resolver_specs: Vec<ResolverSpec>,
389 #[serde(default)]
391 pub computed_fields: Vec<String>,
392 #[serde(default)]
394 pub computed_field_specs: Vec<ComputedFieldSpec>,
395 #[serde(default, skip_serializing_if = "Option::is_none")]
398 pub content_hash: Option<String>,
399 #[serde(default)]
401 pub views: Vec<ViewDef>,
402}
403
404#[derive(Debug, Clone)]
405pub struct TypedStreamSpec<S> {
406 pub state_name: String,
407 pub identity: IdentitySpec,
408 pub handlers: Vec<TypedHandlerSpec<S>>,
409 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>,
414 pub computed_fields: Vec<String>, _phantom: PhantomData<S>,
416}
417
418impl<S> TypedStreamSpec<S> {
419 pub fn new(
420 state_name: String,
421 identity: IdentitySpec,
422 handlers: Vec<TypedHandlerSpec<S>>,
423 ) -> Self {
424 TypedStreamSpec {
425 state_name,
426 identity,
427 handlers,
428 sections: Vec::new(),
429 field_mappings: BTreeMap::new(),
430 resolver_hooks: Vec::new(),
431 instruction_hooks: Vec::new(),
432 resolver_specs: Vec::new(),
433 computed_fields: Vec::new(),
434 _phantom: PhantomData,
435 }
436 }
437
438 pub fn with_type_info(
440 state_name: String,
441 identity: IdentitySpec,
442 handlers: Vec<TypedHandlerSpec<S>>,
443 sections: Vec<EntitySection>,
444 field_mappings: BTreeMap<String, FieldTypeInfo>,
445 ) -> Self {
446 TypedStreamSpec {
447 state_name,
448 identity,
449 handlers,
450 sections,
451 field_mappings,
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_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
461 self.resolver_specs = resolver_specs;
462 self
463 }
464
465 pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
467 self.field_mappings.get(path)
468 }
469
470 pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
472 self.sections
473 .iter()
474 .find(|s| s.name == section_name)
475 .map(|s| &s.fields)
476 }
477
478 pub fn get_section_names(&self) -> Vec<&String> {
480 self.sections.iter().map(|s| &s.name).collect()
481 }
482
483 pub fn to_serializable(&self) -> SerializableStreamSpec {
485 let mut spec = SerializableStreamSpec {
486 state_name: self.state_name.clone(),
487 program_id: None,
488 idl: None,
489 identity: self.identity.clone(),
490 handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
491 sections: self.sections.clone(),
492 field_mappings: self.field_mappings.clone(),
493 resolver_hooks: self.resolver_hooks.clone(),
494 instruction_hooks: self.instruction_hooks.clone(),
495 resolver_specs: self.resolver_specs.clone(),
496 computed_fields: self.computed_fields.clone(),
497 computed_field_specs: Vec::new(),
498 content_hash: None,
499 views: Vec::new(),
500 };
501 spec.content_hash = Some(spec.compute_content_hash());
502 spec
503 }
504
505 pub fn from_serializable(spec: SerializableStreamSpec) -> Self {
507 TypedStreamSpec {
508 state_name: spec.state_name,
509 identity: spec.identity,
510 handlers: spec
511 .handlers
512 .into_iter()
513 .map(|h| TypedHandlerSpec::from_serializable(h))
514 .collect(),
515 sections: spec.sections,
516 field_mappings: spec.field_mappings,
517 resolver_hooks: spec.resolver_hooks,
518 instruction_hooks: spec.instruction_hooks,
519 resolver_specs: spec.resolver_specs,
520 computed_fields: spec.computed_fields,
521 _phantom: PhantomData,
522 }
523 }
524}
525
526#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct IdentitySpec {
528 pub primary_keys: Vec<String>,
529 pub lookup_indexes: Vec<LookupIndexSpec>,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize)]
533pub struct LookupIndexSpec {
534 pub field_name: String,
535 pub temporal_field: Option<String>,
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct ResolverHook {
545 pub account_type: String,
547
548 pub strategy: ResolverStrategy,
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize)]
553pub enum ResolverStrategy {
554 PdaReverseLookup {
556 lookup_name: String,
557 queue_discriminators: Vec<Vec<u8>>,
559 },
560
561 DirectField { field_path: FieldPath },
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct InstructionHook {
568 pub instruction_type: String,
570
571 pub actions: Vec<HookAction>,
573
574 pub lookup_by: Option<FieldPath>,
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize)]
579pub enum HookAction {
580 RegisterPdaMapping {
582 pda_field: FieldPath,
583 seed_field: FieldPath,
584 lookup_name: String,
585 },
586
587 SetField {
589 target_field: String,
590 source: MappingSource,
591 condition: Option<ConditionExpr>,
592 },
593
594 IncrementField {
596 target_field: String,
597 increment_by: i64,
598 condition: Option<ConditionExpr>,
599 },
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct ConditionExpr {
605 pub expression: String,
607
608 pub parsed: Option<ParsedCondition>,
610}
611
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub enum ParsedCondition {
614 Comparison {
616 field: FieldPath,
617 op: ComparisonOp,
618 value: serde_json::Value,
619 },
620
621 Logical {
623 op: LogicalOp,
624 conditions: Vec<ParsedCondition>,
625 },
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
629pub enum ComparisonOp {
630 Equal,
631 NotEqual,
632 GreaterThan,
633 GreaterThanOrEqual,
634 LessThan,
635 LessThanOrEqual,
636}
637
638#[derive(Debug, Clone, Serialize, Deserialize)]
639pub enum LogicalOp {
640 And,
641 Or,
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize)]
646pub struct SerializableHandlerSpec {
647 pub source: SourceSpec,
648 pub key_resolution: KeyResolutionStrategy,
649 pub mappings: Vec<SerializableFieldMapping>,
650 pub conditions: Vec<Condition>,
651 pub emit: bool,
652}
653
654#[derive(Debug, Clone)]
655pub struct TypedHandlerSpec<S> {
656 pub source: SourceSpec,
657 pub key_resolution: KeyResolutionStrategy,
658 pub mappings: Vec<TypedFieldMapping<S>>,
659 pub conditions: Vec<Condition>,
660 pub emit: bool,
661 _phantom: PhantomData<S>,
662}
663
664impl<S> TypedHandlerSpec<S> {
665 pub fn new(
666 source: SourceSpec,
667 key_resolution: KeyResolutionStrategy,
668 mappings: Vec<TypedFieldMapping<S>>,
669 emit: bool,
670 ) -> Self {
671 TypedHandlerSpec {
672 source,
673 key_resolution,
674 mappings,
675 conditions: vec![],
676 emit,
677 _phantom: PhantomData,
678 }
679 }
680
681 pub fn to_serializable(&self) -> SerializableHandlerSpec {
683 SerializableHandlerSpec {
684 source: self.source.clone(),
685 key_resolution: self.key_resolution.clone(),
686 mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
687 conditions: self.conditions.clone(),
688 emit: self.emit,
689 }
690 }
691
692 pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
694 TypedHandlerSpec {
695 source: spec.source,
696 key_resolution: spec.key_resolution,
697 mappings: spec
698 .mappings
699 .into_iter()
700 .map(|m| TypedFieldMapping::from_serializable(m))
701 .collect(),
702 conditions: spec.conditions,
703 emit: spec.emit,
704 _phantom: PhantomData,
705 }
706 }
707}
708
709#[derive(Debug, Clone, Serialize, Deserialize)]
710pub enum KeyResolutionStrategy {
711 Embedded {
712 primary_field: FieldPath,
713 },
714 Lookup {
715 primary_field: FieldPath,
716 },
717 Computed {
718 primary_field: FieldPath,
719 compute_partition: ComputeFunction,
720 },
721 TemporalLookup {
722 lookup_field: FieldPath,
723 timestamp_field: FieldPath,
724 index_name: String,
725 },
726}
727
728#[derive(Debug, Clone, Serialize, Deserialize)]
729pub enum SourceSpec {
730 Source {
731 program_id: Option<String>,
732 discriminator: Option<Vec<u8>>,
733 type_name: String,
734 #[serde(default, skip_serializing_if = "Option::is_none")]
735 serialization: Option<IdlSerializationSnapshot>,
736 },
737}
738
739#[derive(Debug, Clone, Serialize, Deserialize)]
741pub struct SerializableFieldMapping {
742 pub target_path: String,
743 pub source: MappingSource,
744 pub transform: Option<Transformation>,
745 pub population: PopulationStrategy,
746 #[serde(default, skip_serializing_if = "Option::is_none")]
747 pub condition: Option<ConditionExpr>,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
749 pub when: Option<String>,
750 #[serde(default, skip_serializing_if = "Option::is_none")]
751 pub stop: Option<String>,
752 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
753 pub emit: bool,
754}
755
756fn default_emit() -> bool {
757 true
758}
759
760fn default_instruction_discriminant_size() -> usize {
761 8
762}
763
764fn is_true(value: &bool) -> bool {
765 *value
766}
767
768#[derive(Debug, Clone)]
769pub struct TypedFieldMapping<S> {
770 pub target_path: String,
771 pub source: MappingSource,
772 pub transform: Option<Transformation>,
773 pub population: PopulationStrategy,
774 pub condition: Option<ConditionExpr>,
775 pub when: Option<String>,
776 pub stop: Option<String>,
777 pub emit: bool,
778 _phantom: PhantomData<S>,
779}
780
781impl<S> TypedFieldMapping<S> {
782 pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
783 TypedFieldMapping {
784 target_path,
785 source,
786 transform: None,
787 population,
788 condition: None,
789 when: None,
790 stop: None,
791 emit: true,
792 _phantom: PhantomData,
793 }
794 }
795
796 pub fn with_transform(mut self, transform: Transformation) -> Self {
797 self.transform = Some(transform);
798 self
799 }
800
801 pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
802 self.condition = Some(condition);
803 self
804 }
805
806 pub fn with_when(mut self, when: String) -> Self {
807 self.when = Some(when);
808 self
809 }
810
811 pub fn with_stop(mut self, stop: String) -> Self {
812 self.stop = Some(stop);
813 self
814 }
815
816 pub fn with_emit(mut self, emit: bool) -> Self {
817 self.emit = emit;
818 self
819 }
820
821 pub fn to_serializable(&self) -> SerializableFieldMapping {
823 SerializableFieldMapping {
824 target_path: self.target_path.clone(),
825 source: self.source.clone(),
826 transform: self.transform.clone(),
827 population: self.population.clone(),
828 condition: self.condition.clone(),
829 when: self.when.clone(),
830 stop: self.stop.clone(),
831 emit: self.emit,
832 }
833 }
834
835 pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
837 TypedFieldMapping {
838 target_path: mapping.target_path,
839 source: mapping.source,
840 transform: mapping.transform,
841 population: mapping.population,
842 condition: mapping.condition,
843 when: mapping.when,
844 stop: mapping.stop,
845 emit: mapping.emit,
846 _phantom: PhantomData,
847 }
848 }
849}
850
851#[derive(Debug, Clone, Serialize, Deserialize)]
852pub enum MappingSource {
853 FromSource {
854 path: FieldPath,
855 default: Option<Value>,
856 transform: Option<Transformation>,
857 },
858 Constant(Value),
859 Computed {
860 inputs: Vec<FieldPath>,
861 function: ComputeFunction,
862 },
863 FromState {
864 path: String,
865 },
866 AsEvent {
867 fields: Vec<Box<MappingSource>>,
868 },
869 WholeSource,
870 AsCapture {
873 field_transforms: BTreeMap<String, Transformation>,
874 },
875 FromContext {
878 field: String,
879 },
880}
881
882impl MappingSource {
883 pub fn with_transform(self, transform: Transformation) -> Self {
884 match self {
885 MappingSource::FromSource {
886 path,
887 default,
888 transform: _,
889 } => MappingSource::FromSource {
890 path,
891 default,
892 transform: Some(transform),
893 },
894 other => other,
895 }
896 }
897}
898
899#[derive(Debug, Clone, Serialize, Deserialize)]
900pub enum ComputeFunction {
901 Sum,
902 Concat,
903 Format(String),
904 Custom(String),
905}
906
907#[derive(Debug, Clone, Serialize, Deserialize)]
908pub struct Condition {
909 pub field: FieldPath,
910 pub operator: ConditionOp,
911 pub value: Value,
912}
913
914#[derive(Debug, Clone, Serialize, Deserialize)]
915pub enum ConditionOp {
916 Equals,
917 NotEquals,
918 GreaterThan,
919 LessThan,
920 Contains,
921 Exists,
922}
923
924#[derive(Debug, Clone, Serialize, Deserialize)]
926pub struct FieldTypeInfo {
927 pub field_name: String,
928 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)]
936 pub resolved_type: Option<ResolvedStructType>,
937 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
938 pub emit: bool,
939}
940
941#[derive(Debug, Clone, Serialize, Deserialize)]
943pub struct ResolvedStructType {
944 pub type_name: String,
945 pub fields: Vec<ResolvedField>,
946 pub is_instruction: bool,
947 pub is_account: bool,
948 pub is_event: bool,
949 #[serde(default)]
951 pub is_enum: bool,
952 #[serde(default)]
954 pub enum_variants: Vec<String>,
955}
956
957#[derive(Debug, Clone, Serialize, Deserialize)]
959pub struct ResolvedField {
960 pub field_name: String,
961 pub field_type: String,
962 pub base_type: BaseType,
963 pub is_optional: bool,
964 pub is_array: bool,
965}
966
967#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
969pub enum BaseType {
970 Integer, Float, String, Boolean, Object, Array, Binary, Timestamp, Pubkey, Any, }
986
987#[derive(Debug, Clone, Serialize, Deserialize)]
989pub struct EntitySection {
990 pub name: String,
991 pub fields: Vec<FieldTypeInfo>,
992 pub is_nested_struct: bool,
993 pub parent_field: Option<String>, }
995
996impl FieldTypeInfo {
997 pub fn new(field_name: String, rust_type_name: String) -> Self {
998 let (base_type, is_optional, is_array, inner_type) =
999 Self::analyze_rust_type(&rust_type_name);
1000
1001 FieldTypeInfo {
1002 field_name: field_name.clone(),
1003 rust_type_name,
1004 base_type: Self::infer_semantic_type(&field_name, base_type),
1005 is_optional,
1006 is_array,
1007 inner_type,
1008 source_path: None,
1009 resolved_type: None,
1010 emit: true,
1011 }
1012 }
1013
1014 pub fn with_source_path(mut self, source_path: String) -> Self {
1015 self.source_path = Some(source_path);
1016 self
1017 }
1018
1019 fn analyze_rust_type(rust_type: &str) -> (BaseType, bool, bool, Option<String>) {
1021 let type_str = rust_type.trim();
1022
1023 if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1025 let (inner_base_type, _, inner_is_array, inner_inner_type) =
1026 Self::analyze_rust_type(&inner);
1027 return (
1028 inner_base_type,
1029 true,
1030 inner_is_array,
1031 inner_inner_type.or(Some(inner)),
1032 );
1033 }
1034
1035 if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1037 let (_inner_base_type, inner_is_optional, _, inner_inner_type) =
1038 Self::analyze_rust_type(&inner);
1039 return (
1040 BaseType::Array,
1041 inner_is_optional,
1042 true,
1043 inner_inner_type.or(Some(inner)),
1044 );
1045 }
1046
1047 let base_type = match type_str {
1049 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64" | "usize" => {
1050 BaseType::Integer
1051 }
1052 "f32" | "f64" => BaseType::Float,
1053 "bool" => BaseType::Boolean,
1054 "String" | "&str" | "str" => BaseType::String,
1055 "Value" | "serde_json::Value" => BaseType::Any,
1056 "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1057 _ => {
1058 if type_str.contains("Bytes") || type_str.contains("bytes") {
1060 BaseType::Binary
1061 } else if type_str.contains("Pubkey") {
1062 BaseType::Pubkey
1063 } else {
1064 BaseType::Object
1065 }
1066 }
1067 };
1068
1069 (base_type, false, false, None)
1070 }
1071
1072 fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1074 let pattern = format!("{}<", generic_name);
1075 if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1076 let start = pattern.len();
1077 let end = type_str.len() - 1;
1078 if end > start {
1079 return Some(type_str[start..end].trim().to_string());
1080 }
1081 }
1082 None
1083 }
1084
1085 fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1087 let lower_name = field_name.to_lowercase();
1088
1089 if base_type == BaseType::Integer
1091 && (lower_name.ends_with("_at")
1092 || lower_name.ends_with("_time")
1093 || lower_name.contains("timestamp")
1094 || lower_name.contains("created")
1095 || lower_name.contains("settled")
1096 || lower_name.contains("activated"))
1097 {
1098 return BaseType::Timestamp;
1099 }
1100
1101 base_type
1102 }
1103}
1104
1105pub trait FieldAccessor<S> {
1106 fn path(&self) -> String;
1107}
1108
1109impl SerializableStreamSpec {
1114 pub fn compute_content_hash(&self) -> String {
1120 use sha2::{Digest, Sha256};
1121
1122 let mut spec_for_hash = self.clone();
1124 spec_for_hash.content_hash = None;
1125
1126 let json =
1128 serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1129
1130 let mut hasher = Sha256::new();
1132 hasher.update(json.as_bytes());
1133 let result = hasher.finalize();
1134
1135 hex::encode(result)
1137 }
1138
1139 pub fn verify_content_hash(&self) -> bool {
1142 match &self.content_hash {
1143 Some(hash) => {
1144 let computed = self.compute_content_hash();
1145 hash == &computed
1146 }
1147 None => true, }
1149 }
1150
1151 pub fn with_content_hash(mut self) -> Self {
1153 self.content_hash = Some(self.compute_content_hash());
1154 self
1155 }
1156}
1157
1158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1165pub struct PdaDefinition {
1166 pub name: String,
1168
1169 pub seeds: Vec<PdaSeedDef>,
1171
1172 #[serde(default, skip_serializing_if = "Option::is_none")]
1175 pub program_id: Option<String>,
1176}
1177
1178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1180#[serde(tag = "type", rename_all = "camelCase")]
1181pub enum PdaSeedDef {
1182 Literal { value: String },
1184
1185 Bytes { value: Vec<u8> },
1187
1188 ArgRef {
1190 arg_name: String,
1191 #[serde(default, skip_serializing_if = "Option::is_none")]
1193 arg_type: Option<String>,
1194 },
1195
1196 AccountRef { account_name: String },
1198}
1199
1200#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1202#[serde(tag = "category", rename_all = "camelCase")]
1203pub enum AccountResolution {
1204 Signer,
1206
1207 Known { address: String },
1209
1210 PdaRef { pda_name: String },
1212
1213 PdaInline {
1215 seeds: Vec<PdaSeedDef>,
1216 #[serde(default, skip_serializing_if = "Option::is_none")]
1217 program_id: Option<String>,
1218 },
1219
1220 UserProvided,
1222}
1223
1224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1226pub struct InstructionAccountDef {
1227 pub name: String,
1229
1230 #[serde(default)]
1232 pub is_signer: bool,
1233
1234 #[serde(default)]
1236 pub is_writable: bool,
1237
1238 pub resolution: AccountResolution,
1240
1241 #[serde(default)]
1243 pub is_optional: bool,
1244
1245 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1247 pub docs: Vec<String>,
1248}
1249
1250#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1252pub struct InstructionArgDef {
1253 pub name: String,
1255
1256 #[serde(rename = "type")]
1258 pub arg_type: String,
1259
1260 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1262 pub docs: Vec<String>,
1263}
1264
1265#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1267pub struct InstructionDef {
1268 pub name: String,
1270
1271 pub discriminator: Vec<u8>,
1273
1274 #[serde(default = "default_instruction_discriminant_size")]
1276 pub discriminator_size: usize,
1277
1278 pub accounts: Vec<InstructionAccountDef>,
1280
1281 pub args: Vec<InstructionArgDef>,
1283
1284 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1286 pub errors: Vec<IdlErrorSnapshot>,
1287
1288 #[serde(default, skip_serializing_if = "Option::is_none")]
1290 pub program_id: Option<String>,
1291
1292 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1294 pub docs: Vec<String>,
1295}
1296
1297#[derive(Debug, Clone, Serialize, Deserialize)]
1304pub struct SerializableStackSpec {
1305 pub stack_name: String,
1307 #[serde(default)]
1309 pub program_ids: Vec<String>,
1310 #[serde(default)]
1312 pub idls: Vec<IdlSnapshot>,
1313 pub entities: Vec<SerializableStreamSpec>,
1315 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1318 pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1319 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1321 pub instructions: Vec<InstructionDef>,
1322 #[serde(default, skip_serializing_if = "Option::is_none")]
1324 pub content_hash: Option<String>,
1325}
1326
1327impl SerializableStackSpec {
1328 pub fn compute_content_hash(&self) -> String {
1330 use sha2::{Digest, Sha256};
1331 let mut spec_for_hash = self.clone();
1332 spec_for_hash.content_hash = None;
1333 let json = serde_json::to_string(&spec_for_hash)
1334 .expect("Failed to serialize stack spec for hashing");
1335 let mut hasher = Sha256::new();
1336 hasher.update(json.as_bytes());
1337 hex::encode(hasher.finalize())
1338 }
1339
1340 pub fn with_content_hash(mut self) -> Self {
1341 self.content_hash = Some(self.compute_content_hash());
1342 self
1343 }
1344}
1345
1346#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1352#[serde(rename_all = "lowercase")]
1353pub enum SortOrder {
1354 #[default]
1355 Asc,
1356 Desc,
1357}
1358
1359#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1361pub enum CompareOp {
1362 Eq,
1363 Ne,
1364 Gt,
1365 Gte,
1366 Lt,
1367 Lte,
1368}
1369
1370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1372pub enum PredicateValue {
1373 Literal(serde_json::Value),
1375 Dynamic(String),
1377 Field(FieldPath),
1379}
1380
1381#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1383pub enum Predicate {
1384 Compare {
1386 field: FieldPath,
1387 op: CompareOp,
1388 value: PredicateValue,
1389 },
1390 And(Vec<Predicate>),
1392 Or(Vec<Predicate>),
1394 Not(Box<Predicate>),
1396 Exists { field: FieldPath },
1398}
1399
1400#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1402pub enum ViewTransform {
1403 Filter { predicate: Predicate },
1405
1406 Sort {
1408 key: FieldPath,
1409 #[serde(default)]
1410 order: SortOrder,
1411 },
1412
1413 Take { count: usize },
1415
1416 Skip { count: usize },
1418
1419 First,
1421
1422 Last,
1424
1425 MaxBy { key: FieldPath },
1427
1428 MinBy { key: FieldPath },
1430}
1431
1432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1434pub enum ViewSource {
1435 Entity { name: String },
1437 View { id: String },
1439}
1440
1441#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1443pub enum ViewOutput {
1444 #[default]
1446 Collection,
1447 Single,
1449 Keyed { key_field: FieldPath },
1451}
1452
1453#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1455pub struct ViewDef {
1456 pub id: String,
1458
1459 pub source: ViewSource,
1461
1462 #[serde(default)]
1464 pub pipeline: Vec<ViewTransform>,
1465
1466 #[serde(default)]
1468 pub output: ViewOutput,
1469}
1470
1471impl ViewDef {
1472 pub fn list(entity_name: &str) -> Self {
1474 ViewDef {
1475 id: format!("{}/list", entity_name),
1476 source: ViewSource::Entity {
1477 name: entity_name.to_string(),
1478 },
1479 pipeline: vec![],
1480 output: ViewOutput::Collection,
1481 }
1482 }
1483
1484 pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1486 ViewDef {
1487 id: format!("{}/state", entity_name),
1488 source: ViewSource::Entity {
1489 name: entity_name.to_string(),
1490 },
1491 pipeline: vec![],
1492 output: ViewOutput::Keyed {
1493 key_field: FieldPath::new(key_field),
1494 },
1495 }
1496 }
1497
1498 pub fn is_single(&self) -> bool {
1500 matches!(self.output, ViewOutput::Single)
1501 }
1502
1503 pub fn has_single_transform(&self) -> bool {
1505 self.pipeline.iter().any(|t| {
1506 matches!(
1507 t,
1508 ViewTransform::First
1509 | ViewTransform::Last
1510 | ViewTransform::MaxBy { .. }
1511 | ViewTransform::MinBy { .. }
1512 )
1513 })
1514 }
1515}
1516
1517#[macro_export]
1518macro_rules! define_accessor {
1519 ($name:ident, $state:ty, $path:expr) => {
1520 pub struct $name;
1521
1522 impl $crate::ast::FieldAccessor<$state> for $name {
1523 fn path(&self) -> String {
1524 $path.to_string()
1525 }
1526 }
1527 };
1528}