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 Keccak256 {
339 expr: Box<ComputedExpr>,
340 },
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345pub enum BinaryOp {
346 Add,
348 Sub,
349 Mul,
350 Div,
351 Mod,
352 Gt,
354 Lt,
355 Gte,
356 Lte,
357 Eq,
358 Ne,
359 And,
361 Or,
362 Xor,
364 BitAnd,
365 BitOr,
366 Shl,
367 Shr,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372pub enum UnaryOp {
373 Not,
374 ReverseBits,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct SerializableStreamSpec {
380 pub state_name: String,
381 #[serde(default)]
383 pub program_id: Option<String>,
384 #[serde(default)]
386 pub idl: Option<IdlSnapshot>,
387 pub identity: IdentitySpec,
388 pub handlers: Vec<SerializableHandlerSpec>,
389 pub sections: Vec<EntitySection>,
390 pub field_mappings: BTreeMap<String, FieldTypeInfo>,
391 pub resolver_hooks: Vec<ResolverHook>,
392 pub instruction_hooks: Vec<InstructionHook>,
393 #[serde(default)]
394 pub resolver_specs: Vec<ResolverSpec>,
395 #[serde(default)]
397 pub computed_fields: Vec<String>,
398 #[serde(default)]
400 pub computed_field_specs: Vec<ComputedFieldSpec>,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub content_hash: Option<String>,
405 #[serde(default)]
407 pub views: Vec<ViewDef>,
408}
409
410#[derive(Debug, Clone)]
411pub struct TypedStreamSpec<S> {
412 pub state_name: String,
413 pub identity: IdentitySpec,
414 pub handlers: Vec<TypedHandlerSpec<S>>,
415 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>,
420 pub computed_fields: Vec<String>, _phantom: PhantomData<S>,
422}
423
424impl<S> TypedStreamSpec<S> {
425 pub fn new(
426 state_name: String,
427 identity: IdentitySpec,
428 handlers: Vec<TypedHandlerSpec<S>>,
429 ) -> Self {
430 TypedStreamSpec {
431 state_name,
432 identity,
433 handlers,
434 sections: Vec::new(),
435 field_mappings: BTreeMap::new(),
436 resolver_hooks: Vec::new(),
437 instruction_hooks: Vec::new(),
438 resolver_specs: Vec::new(),
439 computed_fields: Vec::new(),
440 _phantom: PhantomData,
441 }
442 }
443
444 pub fn with_type_info(
446 state_name: String,
447 identity: IdentitySpec,
448 handlers: Vec<TypedHandlerSpec<S>>,
449 sections: Vec<EntitySection>,
450 field_mappings: BTreeMap<String, FieldTypeInfo>,
451 ) -> Self {
452 TypedStreamSpec {
453 state_name,
454 identity,
455 handlers,
456 sections,
457 field_mappings,
458 resolver_hooks: Vec::new(),
459 instruction_hooks: Vec::new(),
460 resolver_specs: Vec::new(),
461 computed_fields: Vec::new(),
462 _phantom: PhantomData,
463 }
464 }
465
466 pub fn with_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
467 self.resolver_specs = resolver_specs;
468 self
469 }
470
471 pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
473 self.field_mappings.get(path)
474 }
475
476 pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
478 self.sections
479 .iter()
480 .find(|s| s.name == section_name)
481 .map(|s| &s.fields)
482 }
483
484 pub fn get_section_names(&self) -> Vec<&String> {
486 self.sections.iter().map(|s| &s.name).collect()
487 }
488
489 pub fn to_serializable(&self) -> SerializableStreamSpec {
491 let mut spec = SerializableStreamSpec {
492 state_name: self.state_name.clone(),
493 program_id: None,
494 idl: None,
495 identity: self.identity.clone(),
496 handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
497 sections: self.sections.clone(),
498 field_mappings: self.field_mappings.clone(),
499 resolver_hooks: self.resolver_hooks.clone(),
500 instruction_hooks: self.instruction_hooks.clone(),
501 resolver_specs: self.resolver_specs.clone(),
502 computed_fields: self.computed_fields.clone(),
503 computed_field_specs: Vec::new(),
504 content_hash: None,
505 views: Vec::new(),
506 };
507 spec.content_hash = Some(spec.compute_content_hash());
508 spec
509 }
510
511 pub fn from_serializable(spec: SerializableStreamSpec) -> Self {
513 TypedStreamSpec {
514 state_name: spec.state_name,
515 identity: spec.identity,
516 handlers: spec
517 .handlers
518 .into_iter()
519 .map(|h| TypedHandlerSpec::from_serializable(h))
520 .collect(),
521 sections: spec.sections,
522 field_mappings: spec.field_mappings,
523 resolver_hooks: spec.resolver_hooks,
524 instruction_hooks: spec.instruction_hooks,
525 resolver_specs: spec.resolver_specs,
526 computed_fields: spec.computed_fields,
527 _phantom: PhantomData,
528 }
529 }
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize)]
533pub struct IdentitySpec {
534 pub primary_keys: Vec<String>,
535 pub lookup_indexes: Vec<LookupIndexSpec>,
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
539pub struct LookupIndexSpec {
540 pub field_name: String,
541 pub temporal_field: Option<String>,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct ResolverHook {
551 pub account_type: String,
553
554 pub strategy: ResolverStrategy,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub enum ResolverStrategy {
560 PdaReverseLookup {
562 lookup_name: String,
563 queue_discriminators: Vec<Vec<u8>>,
565 },
566
567 DirectField { field_path: FieldPath },
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize)]
573pub struct InstructionHook {
574 pub instruction_type: String,
576
577 pub actions: Vec<HookAction>,
579
580 pub lookup_by: Option<FieldPath>,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize)]
585pub enum HookAction {
586 RegisterPdaMapping {
588 pda_field: FieldPath,
589 seed_field: FieldPath,
590 lookup_name: String,
591 },
592
593 SetField {
595 target_field: String,
596 source: MappingSource,
597 condition: Option<ConditionExpr>,
598 },
599
600 IncrementField {
602 target_field: String,
603 increment_by: i64,
604 condition: Option<ConditionExpr>,
605 },
606}
607
608#[derive(Debug, Clone, Serialize, Deserialize)]
610pub struct ConditionExpr {
611 pub expression: String,
613
614 pub parsed: Option<ParsedCondition>,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize)]
619pub enum ParsedCondition {
620 Comparison {
622 field: FieldPath,
623 op: ComparisonOp,
624 value: serde_json::Value,
625 },
626
627 Logical {
629 op: LogicalOp,
630 conditions: Vec<ParsedCondition>,
631 },
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
635pub enum ComparisonOp {
636 Equal,
637 NotEqual,
638 GreaterThan,
639 GreaterThanOrEqual,
640 LessThan,
641 LessThanOrEqual,
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize)]
645pub enum LogicalOp {
646 And,
647 Or,
648}
649
650#[derive(Debug, Clone, Serialize, Deserialize)]
652pub struct SerializableHandlerSpec {
653 pub source: SourceSpec,
654 pub key_resolution: KeyResolutionStrategy,
655 pub mappings: Vec<SerializableFieldMapping>,
656 pub conditions: Vec<Condition>,
657 pub emit: bool,
658}
659
660#[derive(Debug, Clone)]
661pub struct TypedHandlerSpec<S> {
662 pub source: SourceSpec,
663 pub key_resolution: KeyResolutionStrategy,
664 pub mappings: Vec<TypedFieldMapping<S>>,
665 pub conditions: Vec<Condition>,
666 pub emit: bool,
667 _phantom: PhantomData<S>,
668}
669
670impl<S> TypedHandlerSpec<S> {
671 pub fn new(
672 source: SourceSpec,
673 key_resolution: KeyResolutionStrategy,
674 mappings: Vec<TypedFieldMapping<S>>,
675 emit: bool,
676 ) -> Self {
677 TypedHandlerSpec {
678 source,
679 key_resolution,
680 mappings,
681 conditions: vec![],
682 emit,
683 _phantom: PhantomData,
684 }
685 }
686
687 pub fn to_serializable(&self) -> SerializableHandlerSpec {
689 SerializableHandlerSpec {
690 source: self.source.clone(),
691 key_resolution: self.key_resolution.clone(),
692 mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
693 conditions: self.conditions.clone(),
694 emit: self.emit,
695 }
696 }
697
698 pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
700 TypedHandlerSpec {
701 source: spec.source,
702 key_resolution: spec.key_resolution,
703 mappings: spec
704 .mappings
705 .into_iter()
706 .map(|m| TypedFieldMapping::from_serializable(m))
707 .collect(),
708 conditions: spec.conditions,
709 emit: spec.emit,
710 _phantom: PhantomData,
711 }
712 }
713}
714
715#[derive(Debug, Clone, Serialize, Deserialize)]
716pub enum KeyResolutionStrategy {
717 Embedded {
718 primary_field: FieldPath,
719 },
720 Lookup {
721 primary_field: FieldPath,
722 },
723 Computed {
724 primary_field: FieldPath,
725 compute_partition: ComputeFunction,
726 },
727 TemporalLookup {
728 lookup_field: FieldPath,
729 timestamp_field: FieldPath,
730 index_name: String,
731 },
732}
733
734#[derive(Debug, Clone, Serialize, Deserialize)]
735pub enum SourceSpec {
736 Source {
737 program_id: Option<String>,
738 discriminator: Option<Vec<u8>>,
739 type_name: String,
740 #[serde(default, skip_serializing_if = "Option::is_none")]
741 serialization: Option<IdlSerializationSnapshot>,
742 #[serde(default)]
747 is_account: bool,
748 },
749}
750
751#[derive(Debug, Clone, Serialize, Deserialize)]
753pub struct SerializableFieldMapping {
754 pub target_path: String,
755 pub source: MappingSource,
756 pub transform: Option<Transformation>,
757 pub population: PopulationStrategy,
758 #[serde(default, skip_serializing_if = "Option::is_none")]
759 pub condition: Option<ConditionExpr>,
760 #[serde(default, skip_serializing_if = "Option::is_none")]
761 pub when: Option<String>,
762 #[serde(default, skip_serializing_if = "Option::is_none")]
763 pub stop: Option<String>,
764 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
765 pub emit: bool,
766}
767
768fn default_emit() -> bool {
769 true
770}
771
772fn default_instruction_discriminant_size() -> usize {
773 8
774}
775
776fn is_true(value: &bool) -> bool {
777 *value
778}
779
780#[derive(Debug, Clone)]
781pub struct TypedFieldMapping<S> {
782 pub target_path: String,
783 pub source: MappingSource,
784 pub transform: Option<Transformation>,
785 pub population: PopulationStrategy,
786 pub condition: Option<ConditionExpr>,
787 pub when: Option<String>,
788 pub stop: Option<String>,
789 pub emit: bool,
790 _phantom: PhantomData<S>,
791}
792
793impl<S> TypedFieldMapping<S> {
794 pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
795 TypedFieldMapping {
796 target_path,
797 source,
798 transform: None,
799 population,
800 condition: None,
801 when: None,
802 stop: None,
803 emit: true,
804 _phantom: PhantomData,
805 }
806 }
807
808 pub fn with_transform(mut self, transform: Transformation) -> Self {
809 self.transform = Some(transform);
810 self
811 }
812
813 pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
814 self.condition = Some(condition);
815 self
816 }
817
818 pub fn with_when(mut self, when: String) -> Self {
819 self.when = Some(when);
820 self
821 }
822
823 pub fn with_stop(mut self, stop: String) -> Self {
824 self.stop = Some(stop);
825 self
826 }
827
828 pub fn with_emit(mut self, emit: bool) -> Self {
829 self.emit = emit;
830 self
831 }
832
833 pub fn to_serializable(&self) -> SerializableFieldMapping {
835 SerializableFieldMapping {
836 target_path: self.target_path.clone(),
837 source: self.source.clone(),
838 transform: self.transform.clone(),
839 population: self.population.clone(),
840 condition: self.condition.clone(),
841 when: self.when.clone(),
842 stop: self.stop.clone(),
843 emit: self.emit,
844 }
845 }
846
847 pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
849 TypedFieldMapping {
850 target_path: mapping.target_path,
851 source: mapping.source,
852 transform: mapping.transform,
853 population: mapping.population,
854 condition: mapping.condition,
855 when: mapping.when,
856 stop: mapping.stop,
857 emit: mapping.emit,
858 _phantom: PhantomData,
859 }
860 }
861}
862
863#[derive(Debug, Clone, Serialize, Deserialize)]
864pub enum MappingSource {
865 FromSource {
866 path: FieldPath,
867 default: Option<Value>,
868 transform: Option<Transformation>,
869 },
870 Constant(Value),
871 Computed {
872 inputs: Vec<FieldPath>,
873 function: ComputeFunction,
874 },
875 FromState {
876 path: String,
877 },
878 AsEvent {
879 fields: Vec<Box<MappingSource>>,
880 },
881 WholeSource,
882 AsCapture {
885 field_transforms: BTreeMap<String, Transformation>,
886 },
887 FromContext {
890 field: String,
891 },
892}
893
894impl MappingSource {
895 pub fn with_transform(self, transform: Transformation) -> Self {
896 match self {
897 MappingSource::FromSource {
898 path,
899 default,
900 transform: _,
901 } => MappingSource::FromSource {
902 path,
903 default,
904 transform: Some(transform),
905 },
906 other => other,
907 }
908 }
909}
910
911#[derive(Debug, Clone, Serialize, Deserialize)]
912pub enum ComputeFunction {
913 Sum,
914 Concat,
915 Format(String),
916 Custom(String),
917}
918
919#[derive(Debug, Clone, Serialize, Deserialize)]
920pub struct Condition {
921 pub field: FieldPath,
922 pub operator: ConditionOp,
923 pub value: Value,
924}
925
926#[derive(Debug, Clone, Serialize, Deserialize)]
927pub enum ConditionOp {
928 Equals,
929 NotEquals,
930 GreaterThan,
931 LessThan,
932 Contains,
933 Exists,
934}
935
936#[derive(Debug, Clone, Serialize, Deserialize)]
938pub struct FieldTypeInfo {
939 pub field_name: String,
940 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)]
948 pub resolved_type: Option<ResolvedStructType>,
949 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
950 pub emit: bool,
951}
952
953#[derive(Debug, Clone, Serialize, Deserialize)]
955pub struct ResolvedStructType {
956 pub type_name: String,
957 pub fields: Vec<ResolvedField>,
958 pub is_instruction: bool,
959 pub is_account: bool,
960 pub is_event: bool,
961 #[serde(default)]
963 pub is_enum: bool,
964 #[serde(default)]
966 pub enum_variants: Vec<String>,
967}
968
969#[derive(Debug, Clone, Serialize, Deserialize)]
971pub struct ResolvedField {
972 pub field_name: String,
973 pub field_type: String,
974 pub base_type: BaseType,
975 pub is_optional: bool,
976 pub is_array: bool,
977}
978
979#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
981pub enum BaseType {
982 Integer, Float, String, Boolean, Object, Array, Binary, Timestamp, Pubkey, Any, }
998
999#[derive(Debug, Clone, Serialize, Deserialize)]
1001pub struct EntitySection {
1002 pub name: String,
1003 pub fields: Vec<FieldTypeInfo>,
1004 pub is_nested_struct: bool,
1005 pub parent_field: Option<String>, }
1007
1008impl FieldTypeInfo {
1009 pub fn new(field_name: String, rust_type_name: String) -> Self {
1010 let (base_type, is_optional, is_array, inner_type) =
1011 Self::analyze_rust_type(&rust_type_name);
1012
1013 FieldTypeInfo {
1014 field_name: field_name.clone(),
1015 rust_type_name,
1016 base_type: Self::infer_semantic_type(&field_name, base_type),
1017 is_optional,
1018 is_array,
1019 inner_type,
1020 source_path: None,
1021 resolved_type: None,
1022 emit: true,
1023 }
1024 }
1025
1026 pub fn with_source_path(mut self, source_path: String) -> Self {
1027 self.source_path = Some(source_path);
1028 self
1029 }
1030
1031 fn analyze_rust_type(rust_type: &str) -> (BaseType, bool, bool, Option<String>) {
1033 let type_str = rust_type.trim();
1034
1035 if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1037 let (inner_base_type, _, inner_is_array, inner_inner_type) =
1038 Self::analyze_rust_type(&inner);
1039 return (
1040 inner_base_type,
1041 true,
1042 inner_is_array,
1043 inner_inner_type.or(Some(inner)),
1044 );
1045 }
1046
1047 if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1049 let (_inner_base_type, inner_is_optional, _, inner_inner_type) =
1050 Self::analyze_rust_type(&inner);
1051 return (
1052 BaseType::Array,
1053 inner_is_optional,
1054 true,
1055 inner_inner_type.or(Some(inner)),
1056 );
1057 }
1058
1059 let base_type = match type_str {
1061 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64" | "usize" => {
1062 BaseType::Integer
1063 }
1064 "f32" | "f64" => BaseType::Float,
1065 "bool" => BaseType::Boolean,
1066 "String" | "&str" | "str" => BaseType::String,
1067 "Value" | "serde_json::Value" => BaseType::Any,
1068 "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1069 _ => {
1070 if type_str.contains("Bytes") || type_str.contains("bytes") {
1072 BaseType::Binary
1073 } else if type_str.contains("Pubkey") {
1074 BaseType::Pubkey
1075 } else {
1076 BaseType::Object
1077 }
1078 }
1079 };
1080
1081 (base_type, false, false, None)
1082 }
1083
1084 fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1086 let pattern = format!("{}<", generic_name);
1087 if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1088 let start = pattern.len();
1089 let end = type_str.len() - 1;
1090 if end > start {
1091 return Some(type_str[start..end].trim().to_string());
1092 }
1093 }
1094 None
1095 }
1096
1097 fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1099 let lower_name = field_name.to_lowercase();
1100
1101 if base_type == BaseType::Integer
1103 && (lower_name.ends_with("_at")
1104 || lower_name.ends_with("_time")
1105 || lower_name.contains("timestamp")
1106 || lower_name.contains("created")
1107 || lower_name.contains("settled")
1108 || lower_name.contains("activated"))
1109 {
1110 return BaseType::Timestamp;
1111 }
1112
1113 base_type
1114 }
1115}
1116
1117pub trait FieldAccessor<S> {
1118 fn path(&self) -> String;
1119}
1120
1121impl SerializableStreamSpec {
1126 pub fn compute_content_hash(&self) -> String {
1132 use sha2::{Digest, Sha256};
1133
1134 let mut spec_for_hash = self.clone();
1136 spec_for_hash.content_hash = None;
1137
1138 let json =
1140 serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1141
1142 let mut hasher = Sha256::new();
1144 hasher.update(json.as_bytes());
1145 let result = hasher.finalize();
1146
1147 hex::encode(result)
1149 }
1150
1151 pub fn verify_content_hash(&self) -> bool {
1154 match &self.content_hash {
1155 Some(hash) => {
1156 let computed = self.compute_content_hash();
1157 hash == &computed
1158 }
1159 None => true, }
1161 }
1162
1163 pub fn with_content_hash(mut self) -> Self {
1165 self.content_hash = Some(self.compute_content_hash());
1166 self
1167 }
1168}
1169
1170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1177pub struct PdaDefinition {
1178 pub name: String,
1180
1181 pub seeds: Vec<PdaSeedDef>,
1183
1184 #[serde(default, skip_serializing_if = "Option::is_none")]
1187 pub program_id: Option<String>,
1188}
1189
1190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1192#[serde(tag = "type", rename_all = "camelCase")]
1193pub enum PdaSeedDef {
1194 Literal { value: String },
1196
1197 Bytes { value: Vec<u8> },
1199
1200 ArgRef {
1202 arg_name: String,
1203 #[serde(default, skip_serializing_if = "Option::is_none")]
1205 arg_type: Option<String>,
1206 },
1207
1208 AccountRef { account_name: String },
1210}
1211
1212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1214#[serde(tag = "category", rename_all = "camelCase")]
1215pub enum AccountResolution {
1216 Signer,
1218
1219 Known { address: String },
1221
1222 PdaRef { pda_name: String },
1224
1225 PdaInline {
1227 seeds: Vec<PdaSeedDef>,
1228 #[serde(default, skip_serializing_if = "Option::is_none")]
1229 program_id: Option<String>,
1230 },
1231
1232 UserProvided,
1234}
1235
1236#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1238pub struct InstructionAccountDef {
1239 pub name: String,
1241
1242 #[serde(default)]
1244 pub is_signer: bool,
1245
1246 #[serde(default)]
1248 pub is_writable: bool,
1249
1250 pub resolution: AccountResolution,
1252
1253 #[serde(default)]
1255 pub is_optional: bool,
1256
1257 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1259 pub docs: Vec<String>,
1260}
1261
1262#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1264pub struct InstructionArgDef {
1265 pub name: String,
1267
1268 #[serde(rename = "type")]
1270 pub arg_type: String,
1271
1272 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1274 pub docs: Vec<String>,
1275}
1276
1277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1279pub struct InstructionDef {
1280 pub name: String,
1282
1283 pub discriminator: Vec<u8>,
1285
1286 #[serde(default = "default_instruction_discriminant_size")]
1288 pub discriminator_size: usize,
1289
1290 pub accounts: Vec<InstructionAccountDef>,
1292
1293 pub args: Vec<InstructionArgDef>,
1295
1296 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1298 pub errors: Vec<IdlErrorSnapshot>,
1299
1300 #[serde(default, skip_serializing_if = "Option::is_none")]
1302 pub program_id: Option<String>,
1303
1304 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1306 pub docs: Vec<String>,
1307}
1308
1309#[derive(Debug, Clone, Serialize, Deserialize)]
1316pub struct SerializableStackSpec {
1317 pub stack_name: String,
1319 #[serde(default)]
1321 pub program_ids: Vec<String>,
1322 #[serde(default)]
1324 pub idls: Vec<IdlSnapshot>,
1325 pub entities: Vec<SerializableStreamSpec>,
1327 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1330 pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1331 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1333 pub instructions: Vec<InstructionDef>,
1334 #[serde(default, skip_serializing_if = "Option::is_none")]
1336 pub content_hash: Option<String>,
1337}
1338
1339impl SerializableStackSpec {
1340 pub fn compute_content_hash(&self) -> String {
1342 use sha2::{Digest, Sha256};
1343 let mut spec_for_hash = self.clone();
1344 spec_for_hash.content_hash = None;
1345 let json = serde_json::to_string(&spec_for_hash)
1346 .expect("Failed to serialize stack spec for hashing");
1347 let mut hasher = Sha256::new();
1348 hasher.update(json.as_bytes());
1349 hex::encode(hasher.finalize())
1350 }
1351
1352 pub fn with_content_hash(mut self) -> Self {
1353 self.content_hash = Some(self.compute_content_hash());
1354 self
1355 }
1356}
1357
1358#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1364#[serde(rename_all = "lowercase")]
1365pub enum SortOrder {
1366 #[default]
1367 Asc,
1368 Desc,
1369}
1370
1371#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1373pub enum CompareOp {
1374 Eq,
1375 Ne,
1376 Gt,
1377 Gte,
1378 Lt,
1379 Lte,
1380}
1381
1382#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1384pub enum PredicateValue {
1385 Literal(serde_json::Value),
1387 Dynamic(String),
1389 Field(FieldPath),
1391}
1392
1393#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1395pub enum Predicate {
1396 Compare {
1398 field: FieldPath,
1399 op: CompareOp,
1400 value: PredicateValue,
1401 },
1402 And(Vec<Predicate>),
1404 Or(Vec<Predicate>),
1406 Not(Box<Predicate>),
1408 Exists { field: FieldPath },
1410}
1411
1412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1414pub enum ViewTransform {
1415 Filter { predicate: Predicate },
1417
1418 Sort {
1420 key: FieldPath,
1421 #[serde(default)]
1422 order: SortOrder,
1423 },
1424
1425 Take { count: usize },
1427
1428 Skip { count: usize },
1430
1431 First,
1433
1434 Last,
1436
1437 MaxBy { key: FieldPath },
1439
1440 MinBy { key: FieldPath },
1442}
1443
1444#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1446pub enum ViewSource {
1447 Entity { name: String },
1449 View { id: String },
1451}
1452
1453#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1455pub enum ViewOutput {
1456 #[default]
1458 Collection,
1459 Single,
1461 Keyed { key_field: FieldPath },
1463}
1464
1465#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1467pub struct ViewDef {
1468 pub id: String,
1470
1471 pub source: ViewSource,
1473
1474 #[serde(default)]
1476 pub pipeline: Vec<ViewTransform>,
1477
1478 #[serde(default)]
1480 pub output: ViewOutput,
1481}
1482
1483impl ViewDef {
1484 pub fn list(entity_name: &str) -> Self {
1486 ViewDef {
1487 id: format!("{}/list", entity_name),
1488 source: ViewSource::Entity {
1489 name: entity_name.to_string(),
1490 },
1491 pipeline: vec![],
1492 output: ViewOutput::Collection,
1493 }
1494 }
1495
1496 pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1498 ViewDef {
1499 id: format!("{}/state", entity_name),
1500 source: ViewSource::Entity {
1501 name: entity_name.to_string(),
1502 },
1503 pipeline: vec![],
1504 output: ViewOutput::Keyed {
1505 key_field: FieldPath::new(key_field),
1506 },
1507 }
1508 }
1509
1510 pub fn is_single(&self) -> bool {
1512 matches!(self.output, ViewOutput::Single)
1513 }
1514
1515 pub fn has_single_transform(&self) -> bool {
1517 self.pipeline.iter().any(|t| {
1518 matches!(
1519 t,
1520 ViewTransform::First
1521 | ViewTransform::Last
1522 | ViewTransform::MaxBy { .. }
1523 | ViewTransform::MinBy { .. }
1524 )
1525 })
1526 }
1527}
1528
1529#[macro_export]
1530macro_rules! define_accessor {
1531 ($name:ident, $state:ty, $path:expr) => {
1532 pub struct $name;
1533
1534 impl $crate::ast::FieldAccessor<$state> for $name {
1535 fn path(&self) -> String {
1536 $path.to_string()
1537 }
1538 }
1539 };
1540}