1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use std::marker::PhantomData;
5
6pub use arete_idl::snapshot::*;
7
8pub const CURRENT_AST_VERSION: &str = "0.0.4";
19
20pub const COMPATIBLE_AST_VERSIONS: &[&str] = &["0.0.1", "0.0.2", "0.0.3"];
23
24fn default_ast_version() -> String {
25 CURRENT_AST_VERSION.to_string()
26}
27
28pub fn idl_type_snapshot_to_rust_string(ty: &IdlTypeSnapshot) -> String {
29 match ty {
30 IdlTypeSnapshot::Simple(s) => map_simple_idl_type(s),
31 IdlTypeSnapshot::Array(arr) => {
32 if arr.array.len() == 2 {
33 match (&arr.array[0], &arr.array[1]) {
34 (IdlArrayElementSnapshot::TypeName(t), IdlArrayElementSnapshot::Size(size)) => {
35 format!("[{}; {}]", map_simple_idl_type(t), size)
36 }
37 (
38 IdlArrayElementSnapshot::Type(nested),
39 IdlArrayElementSnapshot::Size(size),
40 ) => {
41 format!("[{}; {}]", idl_type_snapshot_to_rust_string(nested), size)
42 }
43 _ => "Vec<u8>".to_string(),
44 }
45 } else {
46 "Vec<u8>".to_string()
47 }
48 }
49 IdlTypeSnapshot::Option(opt) => {
50 format!("Option<{}>", idl_type_snapshot_to_rust_string(&opt.option))
51 }
52 IdlTypeSnapshot::Vec(vec) => {
53 format!("Vec<{}>", idl_type_snapshot_to_rust_string(&vec.vec))
54 }
55 IdlTypeSnapshot::HashMap(map) => {
56 let key_type = idl_type_snapshot_to_rust_string(&map.hash_map.0);
57 let val_type = idl_type_snapshot_to_rust_string(&map.hash_map.1);
58 format!("std::collections::HashMap<{}, {}>", key_type, val_type)
59 }
60 IdlTypeSnapshot::Defined(def) => match &def.defined {
61 IdlDefinedInnerSnapshot::Named { name } => name.clone(),
62 IdlDefinedInnerSnapshot::Simple(s) => s.clone(),
63 },
64 }
65}
66
67fn map_simple_idl_type(idl_type: &str) -> String {
68 match idl_type {
69 "u8" => "u8".to_string(),
70 "u16" => "u16".to_string(),
71 "u32" => "u32".to_string(),
72 "u64" => "u64".to_string(),
73 "u128" => "u128".to_string(),
74 "i8" => "i8".to_string(),
75 "i16" => "i16".to_string(),
76 "i32" => "i32".to_string(),
77 "i64" => "i64".to_string(),
78 "i128" => "i128".to_string(),
79 "bool" => "bool".to_string(),
80 "string" => "String".to_string(),
81 "publicKey" | "pubkey" => "solana_pubkey::Pubkey".to_string(),
82 "bytes" => "Vec<u8>".to_string(),
83 _ => idl_type.to_string(),
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
88pub struct FieldPath {
89 pub segments: Vec<String>,
90 pub offsets: Option<Vec<usize>>,
91}
92
93impl FieldPath {
94 pub fn new(segments: &[&str]) -> Self {
95 FieldPath {
96 segments: segments.iter().map(|s| s.to_string()).collect(),
97 offsets: None,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103pub enum Transformation {
104 HexEncode,
105 HexDecode,
106 Base58Encode,
107 Base58Decode,
108 ToString,
109 ToNumber,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub enum PopulationStrategy {
114 SetOnce,
115 LastWrite,
116 Append,
117 Merge,
118 Max,
119 Sum,
121 Count,
123 Min,
125 UniqueCount,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ComputedFieldSpec {
137 pub target_path: String,
139 pub expression: ComputedExpr,
141 pub result_type: String,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
150#[serde(rename_all = "lowercase")]
151pub enum ResolverType {
152 Token,
153 Url(UrlResolverConfig),
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
157#[serde(rename_all = "lowercase")]
158pub enum HttpMethod {
159 #[default]
160 Get,
161 Post,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
165pub enum UrlTemplatePart {
166 Literal(String),
167 FieldRef(String),
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
171pub enum UrlSource {
172 FieldPath(String),
173 Template(Vec<UrlTemplatePart>),
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
177pub struct UrlResolverConfig {
178 pub url_source: UrlSource,
179 #[serde(default)]
180 pub method: HttpMethod,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub extract_path: Option<String>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
186pub struct ResolverExtractSpec {
187 pub target_path: String,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub source_path: Option<String>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub transform: Option<Transformation>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
195pub enum ResolveStrategy {
196 #[default]
197 SetOnce,
198 LastWrite,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
202pub struct ResolverCondition {
203 pub field_path: String,
204 pub op: ComparisonOp,
205 pub value: Value,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct ResolverSpec {
210 pub resolver: ResolverType,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub input_path: Option<String>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub input_value: Option<Value>,
215 #[serde(default)]
216 pub strategy: ResolveStrategy,
217 pub extracts: Vec<ResolverExtractSpec>,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub condition: Option<ResolverCondition>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub schedule_at: Option<String>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
234pub enum ComputedExpr {
235 FieldRef {
238 path: String,
239 },
240
241 UnwrapOr {
243 expr: Box<ComputedExpr>,
244 default: serde_json::Value,
245 },
246
247 Binary {
249 op: BinaryOp,
250 left: Box<ComputedExpr>,
251 right: Box<ComputedExpr>,
252 },
253
254 Cast {
256 expr: Box<ComputedExpr>,
257 to_type: String,
258 },
259
260 MethodCall {
262 expr: Box<ComputedExpr>,
263 method: String,
264 args: Vec<ComputedExpr>,
265 },
266
267 ResolverComputed {
269 resolver: String,
270 method: String,
271 args: Vec<ComputedExpr>,
272 },
273
274 Literal {
276 value: serde_json::Value,
277 },
278
279 Paren {
281 expr: Box<ComputedExpr>,
282 },
283
284 Var {
286 name: String,
287 },
288
289 Let {
291 name: String,
292 value: Box<ComputedExpr>,
293 body: Box<ComputedExpr>,
294 },
295
296 If {
298 condition: Box<ComputedExpr>,
299 then_branch: Box<ComputedExpr>,
300 else_branch: Box<ComputedExpr>,
301 },
302
303 None,
305 Some {
306 value: Box<ComputedExpr>,
307 },
308
309 Slice {
311 expr: Box<ComputedExpr>,
312 start: usize,
313 end: usize,
314 },
315 Index {
316 expr: Box<ComputedExpr>,
317 index: usize,
318 },
319
320 U64FromLeBytes {
322 bytes: Box<ComputedExpr>,
323 },
324 U64FromBeBytes {
325 bytes: Box<ComputedExpr>,
326 },
327
328 ByteArray {
330 bytes: Vec<u8>,
331 },
332
333 Closure {
335 param: String,
336 body: Box<ComputedExpr>,
337 },
338
339 Unary {
341 op: UnaryOp,
342 expr: Box<ComputedExpr>,
343 },
344
345 JsonToBytes {
347 expr: Box<ComputedExpr>,
348 },
349
350 ContextSlot,
353 ContextTimestamp,
355
356 Keccak256 {
359 expr: Box<ComputedExpr>,
360 },
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
365pub enum BinaryOp {
366 Add,
368 Sub,
369 Mul,
370 Div,
371 Mod,
372 Gt,
374 Lt,
375 Gte,
376 Lte,
377 Eq,
378 Ne,
379 And,
381 Or,
382 Xor,
384 BitAnd,
385 BitOr,
386 Shl,
387 Shr,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
392pub enum UnaryOp {
393 Not,
394 ReverseBits,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct SerializableStreamSpec {
400 #[serde(default = "default_ast_version")]
403 pub ast_version: String,
404 pub state_name: String,
405 #[serde(default)]
407 pub program_id: Option<String>,
408 #[serde(default)]
410 pub idl: Option<IdlSnapshot>,
411 pub identity: IdentitySpec,
412 pub handlers: Vec<SerializableHandlerSpec>,
413 pub sections: Vec<EntitySection>,
414 pub field_mappings: BTreeMap<String, FieldTypeInfo>,
415 pub resolver_hooks: Vec<ResolverHook>,
416 pub instruction_hooks: Vec<InstructionHook>,
417 #[serde(default)]
418 pub resolver_specs: Vec<ResolverSpec>,
419 #[serde(default)]
421 pub computed_fields: Vec<String>,
422 #[serde(default)]
424 pub computed_field_specs: Vec<ComputedFieldSpec>,
425 #[serde(default, skip_serializing_if = "Option::is_none")]
428 pub content_hash: Option<String>,
429 #[serde(default)]
431 pub views: Vec<ViewDef>,
432}
433
434impl SerializableStreamSpec {
435 pub fn normalize_event_names(&mut self) {
437 for handler in &mut self.handlers {
438 handler.normalize_event_names();
439 }
440
441 for hook in &mut self.instruction_hooks {
442 hook.instruction_type =
443 crate::event_type_helpers::canonicalize_event_type_name(&hook.instruction_type);
444 }
445 }
446}
447
448#[derive(Debug, Clone)]
449pub struct TypedStreamSpec<S> {
450 pub state_name: String,
451 pub identity: IdentitySpec,
452 pub handlers: Vec<TypedHandlerSpec<S>>,
453 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>,
458 pub computed_fields: Vec<String>, _phantom: PhantomData<S>,
460}
461
462impl<S> TypedStreamSpec<S> {
463 pub fn new(
464 state_name: String,
465 identity: IdentitySpec,
466 handlers: Vec<TypedHandlerSpec<S>>,
467 ) -> Self {
468 TypedStreamSpec {
469 state_name,
470 identity,
471 handlers,
472 sections: Vec::new(),
473 field_mappings: BTreeMap::new(),
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_type_info(
484 state_name: String,
485 identity: IdentitySpec,
486 handlers: Vec<TypedHandlerSpec<S>>,
487 sections: Vec<EntitySection>,
488 field_mappings: BTreeMap<String, FieldTypeInfo>,
489 ) -> Self {
490 TypedStreamSpec {
491 state_name,
492 identity,
493 handlers,
494 sections,
495 field_mappings,
496 resolver_hooks: Vec::new(),
497 instruction_hooks: Vec::new(),
498 resolver_specs: Vec::new(),
499 computed_fields: Vec::new(),
500 _phantom: PhantomData,
501 }
502 }
503
504 pub fn with_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
505 self.resolver_specs = resolver_specs;
506 self
507 }
508
509 pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
511 self.field_mappings.get(path)
512 }
513
514 pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
516 self.sections
517 .iter()
518 .find(|s| s.name == section_name)
519 .map(|s| &s.fields)
520 }
521
522 pub fn get_section_names(&self) -> Vec<&String> {
524 self.sections.iter().map(|s| &s.name).collect()
525 }
526
527 pub fn to_serializable(&self) -> SerializableStreamSpec {
529 let mut spec = SerializableStreamSpec {
530 ast_version: CURRENT_AST_VERSION.to_string(),
531 state_name: self.state_name.clone(),
532 program_id: None,
533 idl: None,
534 identity: self.identity.clone(),
535 handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
536 sections: self.sections.clone(),
537 field_mappings: self.field_mappings.clone(),
538 resolver_hooks: self.resolver_hooks.clone(),
539 instruction_hooks: self.instruction_hooks.clone(),
540 resolver_specs: self.resolver_specs.clone(),
541 computed_fields: self.computed_fields.clone(),
542 computed_field_specs: Vec::new(),
543 content_hash: None,
544 views: Vec::new(),
545 };
546 spec.content_hash = Some(spec.compute_content_hash());
547 spec
548 }
549
550 pub fn from_serializable(mut spec: SerializableStreamSpec) -> Self {
552 spec.normalize_event_names();
553 TypedStreamSpec {
554 state_name: spec.state_name,
555 identity: spec.identity,
556 handlers: spec
557 .handlers
558 .into_iter()
559 .map(|h| TypedHandlerSpec::from_serializable(h))
560 .collect(),
561 sections: spec.sections,
562 field_mappings: spec.field_mappings,
563 resolver_hooks: spec.resolver_hooks,
564 instruction_hooks: spec.instruction_hooks,
565 resolver_specs: spec.resolver_specs,
566 computed_fields: spec.computed_fields,
567 _phantom: PhantomData,
568 }
569 }
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
573pub struct IdentitySpec {
574 pub primary_keys: Vec<String>,
575 pub lookup_indexes: Vec<LookupIndexSpec>,
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct LookupIndexSpec {
580 pub field_name: String,
581 pub temporal_field: Option<String>,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct ResolverHook {
591 pub account_type: String,
593
594 pub strategy: ResolverStrategy,
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize)]
599pub enum ResolverStrategy {
600 PdaReverseLookup {
602 lookup_name: String,
603 queue_discriminators: Vec<Vec<u8>>,
605 },
606
607 DirectField { field_path: FieldPath },
609}
610
611#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct InstructionHook {
614 pub instruction_type: String,
616
617 pub actions: Vec<HookAction>,
619
620 pub lookup_by: Option<FieldPath>,
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize)]
625pub enum HookAction {
626 RegisterPdaMapping {
628 pda_field: FieldPath,
629 seed_field: FieldPath,
630 lookup_name: String,
631 },
632
633 SetField {
635 target_field: String,
636 source: MappingSource,
637 condition: Option<ConditionExpr>,
638 },
639
640 IncrementField {
642 target_field: String,
643 increment_by: i64,
644 condition: Option<ConditionExpr>,
645 },
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct ConditionExpr {
651 pub expression: String,
653
654 pub parsed: Option<ParsedCondition>,
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize)]
659pub enum ParsedCondition {
660 Comparison {
662 field: FieldPath,
663 op: ComparisonOp,
664 value: serde_json::Value,
665 },
666
667 Logical {
669 op: LogicalOp,
670 conditions: Vec<ParsedCondition>,
671 },
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
675pub enum ComparisonOp {
676 Equal,
677 NotEqual,
678 GreaterThan,
679 GreaterThanOrEqual,
680 LessThan,
681 LessThanOrEqual,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize)]
685pub enum LogicalOp {
686 And,
687 Or,
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct SerializableHandlerSpec {
693 pub source: SourceSpec,
694 pub key_resolution: KeyResolutionStrategy,
695 pub mappings: Vec<SerializableFieldMapping>,
696 pub conditions: Vec<Condition>,
697 pub emit: bool,
698}
699
700impl SerializableHandlerSpec {
701 pub fn normalize_event_names(&mut self) {
702 let SourceSpec::Source { type_name, .. } = &mut self.source;
703 *type_name = crate::event_type_helpers::canonicalize_event_type_name(type_name);
704
705 for mapping in &mut self.mappings {
706 if let Some(when) = &mut mapping.when {
707 *when = crate::event_type_helpers::canonicalize_event_type_name(when);
708 }
709 if let Some(stop) = &mut mapping.stop {
710 *stop = crate::event_type_helpers::canonicalize_event_type_name(stop);
711 }
712 }
713 }
714}
715
716#[derive(Debug, Clone)]
717pub struct TypedHandlerSpec<S> {
718 pub source: SourceSpec,
719 pub key_resolution: KeyResolutionStrategy,
720 pub mappings: Vec<TypedFieldMapping<S>>,
721 pub conditions: Vec<Condition>,
722 pub emit: bool,
723 _phantom: PhantomData<S>,
724}
725
726impl<S> TypedHandlerSpec<S> {
727 pub fn new(
728 source: SourceSpec,
729 key_resolution: KeyResolutionStrategy,
730 mappings: Vec<TypedFieldMapping<S>>,
731 emit: bool,
732 ) -> Self {
733 TypedHandlerSpec {
734 source,
735 key_resolution,
736 mappings,
737 conditions: vec![],
738 emit,
739 _phantom: PhantomData,
740 }
741 }
742
743 pub fn to_serializable(&self) -> SerializableHandlerSpec {
745 SerializableHandlerSpec {
746 source: self.source.clone(),
747 key_resolution: self.key_resolution.clone(),
748 mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
749 conditions: self.conditions.clone(),
750 emit: self.emit,
751 }
752 }
753
754 pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
756 TypedHandlerSpec {
757 source: spec.source,
758 key_resolution: spec.key_resolution,
759 mappings: spec
760 .mappings
761 .into_iter()
762 .map(|m| TypedFieldMapping::from_serializable(m))
763 .collect(),
764 conditions: spec.conditions,
765 emit: spec.emit,
766 _phantom: PhantomData,
767 }
768 }
769}
770
771#[derive(Debug, Clone, Serialize, Deserialize)]
772pub enum KeyResolutionStrategy {
773 Embedded {
774 primary_field: FieldPath,
775 },
776 Lookup {
777 primary_field: FieldPath,
778 },
779 Computed {
780 primary_field: FieldPath,
781 compute_partition: ComputeFunction,
782 },
783 TemporalLookup {
784 lookup_field: FieldPath,
785 timestamp_field: FieldPath,
786 index_name: String,
787 },
788}
789
790#[derive(Debug, Clone, Serialize, Deserialize)]
791pub enum SourceSpec {
792 Source {
793 program_id: Option<String>,
794 discriminator: Option<Vec<u8>>,
795 type_name: String,
796 #[serde(default, skip_serializing_if = "Option::is_none")]
797 serialization: Option<IdlSerializationSnapshot>,
798 #[serde(default)]
803 is_account: bool,
804 },
805}
806
807#[derive(Debug, Clone, Serialize, Deserialize)]
809pub struct SerializableFieldMapping {
810 pub target_path: String,
811 pub source: MappingSource,
812 pub transform: Option<Transformation>,
813 pub population: PopulationStrategy,
814 #[serde(default, skip_serializing_if = "Option::is_none")]
815 pub condition: Option<ConditionExpr>,
816 #[serde(default, skip_serializing_if = "Option::is_none")]
817 pub when: Option<String>,
818 #[serde(default, skip_serializing_if = "Option::is_none")]
819 pub stop: Option<String>,
820 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
821 pub emit: bool,
822}
823
824fn default_emit() -> bool {
825 true
826}
827
828fn default_instruction_discriminant_size() -> usize {
829 8
830}
831
832fn is_true(value: &bool) -> bool {
833 *value
834}
835
836#[derive(Debug, Clone)]
837pub struct TypedFieldMapping<S> {
838 pub target_path: String,
839 pub source: MappingSource,
840 pub transform: Option<Transformation>,
841 pub population: PopulationStrategy,
842 pub condition: Option<ConditionExpr>,
843 pub when: Option<String>,
844 pub stop: Option<String>,
845 pub emit: bool,
846 _phantom: PhantomData<S>,
847}
848
849impl<S> TypedFieldMapping<S> {
850 pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
851 TypedFieldMapping {
852 target_path,
853 source,
854 transform: None,
855 population,
856 condition: None,
857 when: None,
858 stop: None,
859 emit: true,
860 _phantom: PhantomData,
861 }
862 }
863
864 pub fn with_transform(mut self, transform: Transformation) -> Self {
865 self.transform = Some(transform);
866 self
867 }
868
869 pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
870 self.condition = Some(condition);
871 self
872 }
873
874 pub fn with_when(mut self, when: String) -> Self {
875 self.when = Some(when);
876 self
877 }
878
879 pub fn with_stop(mut self, stop: String) -> Self {
880 self.stop = Some(stop);
881 self
882 }
883
884 pub fn with_emit(mut self, emit: bool) -> Self {
885 self.emit = emit;
886 self
887 }
888
889 pub fn to_serializable(&self) -> SerializableFieldMapping {
891 SerializableFieldMapping {
892 target_path: self.target_path.clone(),
893 source: self.source.clone(),
894 transform: self.transform.clone(),
895 population: self.population.clone(),
896 condition: self.condition.clone(),
897 when: self.when.clone(),
898 stop: self.stop.clone(),
899 emit: self.emit,
900 }
901 }
902
903 pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
905 TypedFieldMapping {
906 target_path: mapping.target_path,
907 source: mapping.source,
908 transform: mapping.transform,
909 population: mapping.population,
910 condition: mapping.condition,
911 when: mapping.when,
912 stop: mapping.stop,
913 emit: mapping.emit,
914 _phantom: PhantomData,
915 }
916 }
917}
918
919#[derive(Debug, Clone, Serialize, Deserialize)]
920pub enum MappingSource {
921 FromSource {
922 path: FieldPath,
923 default: Option<Value>,
924 transform: Option<Transformation>,
925 },
926 Constant(Value),
927 Computed {
928 inputs: Vec<FieldPath>,
929 function: ComputeFunction,
930 },
931 FromState {
932 path: String,
933 },
934 AsEvent {
935 fields: Vec<Box<MappingSource>>,
936 },
937 WholeSource,
938 AsCapture {
941 field_transforms: BTreeMap<String, Transformation>,
942 },
943 FromContext {
946 field: String,
947 },
948}
949
950impl MappingSource {
951 pub fn with_transform(self, transform: Transformation) -> Self {
952 match self {
953 MappingSource::FromSource {
954 path,
955 default,
956 transform: _,
957 } => MappingSource::FromSource {
958 path,
959 default,
960 transform: Some(transform),
961 },
962 other => other,
963 }
964 }
965}
966
967#[derive(Debug, Clone, Serialize, Deserialize)]
968pub enum ComputeFunction {
969 Sum,
970 Concat,
971 Format(String),
972 Custom(String),
973}
974
975#[derive(Debug, Clone, Serialize, Deserialize)]
976pub struct Condition {
977 pub field: FieldPath,
978 pub operator: ConditionOp,
979 pub value: Value,
980}
981
982#[derive(Debug, Clone, Serialize, Deserialize)]
983pub enum ConditionOp {
984 Equals,
985 NotEquals,
986 GreaterThan,
987 LessThan,
988 Contains,
989 Exists,
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize)]
994pub struct FieldTypeInfo {
995 pub field_name: String,
996 #[serde(default, skip_serializing_if = "Option::is_none")]
997 pub raw_name: Option<String>,
998 #[serde(default, skip_serializing_if = "Option::is_none")]
999 pub canonical_name: Option<String>,
1000 pub rust_type_name: String, pub base_type: BaseType, #[serde(default, skip_serializing_if = "Option::is_none")]
1003 pub integer_kind: Option<IntegerKind>,
1004 pub is_optional: bool, pub is_array: bool, pub inner_type: Option<String>, pub source_path: Option<String>, #[serde(default)]
1010 pub resolved_type: Option<ResolvedStructType>,
1011 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
1012 pub emit: bool,
1013}
1014
1015#[derive(Debug, Clone, Serialize, Deserialize)]
1017pub struct ResolvedStructType {
1018 pub type_name: String,
1019 pub fields: Vec<ResolvedField>,
1020 pub is_instruction: bool,
1021 pub is_account: bool,
1022 pub is_event: bool,
1023 #[serde(default)]
1025 pub is_enum: bool,
1026 #[serde(default)]
1028 pub enum_variants: Vec<String>,
1029}
1030
1031#[derive(Debug, Clone, Serialize, Deserialize)]
1033pub struct ResolvedField {
1034 pub field_name: String,
1035 #[serde(default, skip_serializing_if = "Option::is_none")]
1036 pub raw_name: Option<String>,
1037 #[serde(default, skip_serializing_if = "Option::is_none")]
1038 pub canonical_name: Option<String>,
1039 pub field_type: String,
1040 pub base_type: BaseType,
1041 #[serde(default, skip_serializing_if = "Option::is_none")]
1042 pub integer_kind: Option<IntegerKind>,
1043 pub is_optional: bool,
1044 pub is_array: bool,
1045}
1046
1047#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1048pub enum IntegerKind {
1049 U8,
1050 U16,
1051 U32,
1052 U64,
1053 U128,
1054 Usize,
1055 I8,
1056 I16,
1057 I32,
1058 I64,
1059 I128,
1060 Isize,
1061}
1062
1063impl IntegerKind {
1064 pub fn from_rust_type(type_str: &str) -> Option<Self> {
1065 match type_str.trim() {
1066 "u8" => Some(Self::U8),
1067 "u16" => Some(Self::U16),
1068 "u32" => Some(Self::U32),
1069 "u64" => Some(Self::U64),
1070 "u128" => Some(Self::U128),
1071 "usize" => Some(Self::Usize),
1072 "i8" => Some(Self::I8),
1073 "i16" => Some(Self::I16),
1074 "i32" => Some(Self::I32),
1075 "i64" => Some(Self::I64),
1076 "i128" => Some(Self::I128),
1077 "isize" => Some(Self::Isize),
1078 _ => None,
1079 }
1080 }
1081
1082 pub fn is_bigint(self) -> bool {
1083 matches!(self, Self::U64 | Self::U128 | Self::I64 | Self::I128)
1084 }
1085}
1086
1087#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1089pub enum BaseType {
1090 Integer, Float, String, Boolean, Object, Array, Binary, Timestamp, Pubkey, Any, }
1106
1107#[derive(Debug, Clone, Serialize, Deserialize)]
1109pub struct EntitySection {
1110 pub name: String,
1111 pub fields: Vec<FieldTypeInfo>,
1112 pub is_nested_struct: bool,
1113 pub parent_field: Option<String>, }
1115
1116impl FieldTypeInfo {
1117 pub fn new(field_name: String, rust_type_name: String) -> Self {
1118 let (base_type, integer_kind, is_optional, is_array, inner_type) =
1119 Self::analyze_rust_type(&rust_type_name);
1120 let canonical_name = to_camel_case_owned(&field_name);
1121
1122 FieldTypeInfo {
1123 field_name: field_name.clone(),
1124 raw_name: Some(field_name.clone()),
1125 canonical_name: Some(canonical_name),
1126 rust_type_name,
1127 base_type: Self::infer_semantic_type(&field_name, base_type),
1128 integer_kind,
1129 is_optional,
1130 is_array,
1131 inner_type,
1132 source_path: None,
1133 resolved_type: None,
1134 emit: true,
1135 }
1136 }
1137
1138 pub fn with_source_path(mut self, source_path: String) -> Self {
1139 self.source_path = Some(source_path);
1140 self
1141 }
1142
1143 pub fn raw_field_name(&self) -> &str {
1144 self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1145 }
1146
1147 pub fn canonical_field_name(&self) -> String {
1148 self.canonical_name
1149 .clone()
1150 .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1151 }
1152
1153 pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1154 self.integer_kind.or_else(|| {
1155 IntegerKind::from_rust_type(
1156 self.inner_type
1157 .as_deref()
1158 .unwrap_or(self.rust_type_name.as_str()),
1159 )
1160 })
1161 }
1162
1163 fn analyze_rust_type(
1165 rust_type: &str,
1166 ) -> (BaseType, Option<IntegerKind>, bool, bool, Option<String>) {
1167 let type_str = rust_type.trim();
1168
1169 if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1171 let (inner_base_type, inner_integer_kind, _, inner_is_array, inner_inner_type) =
1172 Self::analyze_rust_type(&inner);
1173 return (
1174 inner_base_type,
1175 inner_integer_kind,
1176 true,
1177 inner_is_array,
1178 inner_inner_type.or(Some(inner)),
1179 );
1180 }
1181
1182 if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1184 let (_inner_base_type, inner_integer_kind, inner_is_optional, _, inner_inner_type) =
1185 Self::analyze_rust_type(&inner);
1186 return (
1187 BaseType::Array,
1188 inner_integer_kind,
1189 inner_is_optional,
1190 true,
1191 inner_inner_type.or(Some(inner)),
1192 );
1193 }
1194
1195 let integer_kind = IntegerKind::from_rust_type(type_str);
1197 let base_type = match integer_kind {
1198 Some(_) => BaseType::Integer,
1199 None => match type_str {
1200 "f32" | "f64" => BaseType::Float,
1201 "bool" => BaseType::Boolean,
1202 "String" | "&str" | "str" => BaseType::String,
1203 "Value" | "serde_json::Value" => BaseType::Any,
1204 "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1205 _ => {
1206 if type_str.contains("Bytes") || type_str.contains("bytes") {
1208 BaseType::Binary
1209 } else if type_str.contains("Pubkey") {
1210 BaseType::Pubkey
1211 } else {
1212 BaseType::Object
1213 }
1214 }
1215 },
1216 };
1217
1218 (base_type, integer_kind, false, false, None)
1219 }
1220
1221 fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1223 let pattern = format!("{}<", generic_name);
1224 if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1225 let start = pattern.len();
1226 let end = type_str.len() - 1;
1227 if end > start {
1228 return Some(type_str[start..end].trim().to_string());
1229 }
1230 }
1231 None
1232 }
1233
1234 fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1236 let lower_name = field_name.to_lowercase();
1237
1238 if base_type == BaseType::Integer
1240 && (lower_name.ends_with("_at")
1241 || lower_name.ends_with("_time")
1242 || lower_name.contains("timestamp")
1243 || lower_name.contains("created")
1244 || lower_name.contains("settled")
1245 || lower_name.contains("activated"))
1246 {
1247 return BaseType::Timestamp;
1248 }
1249
1250 base_type
1251 }
1252}
1253
1254impl ResolvedField {
1255 pub fn raw_field_name(&self) -> &str {
1256 self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1257 }
1258
1259 pub fn canonical_field_name(&self) -> String {
1260 self.canonical_name
1261 .clone()
1262 .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1263 }
1264
1265 pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1266 self.integer_kind
1267 .or_else(|| IntegerKind::from_rust_type(self.field_type.as_str()))
1268 }
1269}
1270
1271fn to_camel_case_owned(s: &str) -> String {
1272 let mut result = String::new();
1273 let mut uppercase_next = false;
1274
1275 for ch in s.chars() {
1276 if matches!(ch, '_' | '-' | '.') {
1277 uppercase_next = true;
1278 continue;
1279 }
1280
1281 if result.is_empty() {
1282 result.extend(ch.to_lowercase());
1283 continue;
1284 }
1285
1286 if uppercase_next {
1287 result.extend(ch.to_uppercase());
1288 uppercase_next = false;
1289 } else {
1290 result.push(ch);
1291 }
1292 }
1293
1294 result
1295}
1296
1297pub trait FieldAccessor<S> {
1298 fn path(&self) -> String;
1299}
1300
1301impl SerializableStreamSpec {
1306 pub fn compute_content_hash(&self) -> String {
1312 use sha2::{Digest, Sha256};
1313
1314 let mut spec_for_hash = self.clone();
1316 spec_for_hash.content_hash = None;
1317
1318 let json =
1320 serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1321
1322 let mut hasher = Sha256::new();
1324 hasher.update(json.as_bytes());
1325 let result = hasher.finalize();
1326
1327 hex::encode(result)
1329 }
1330
1331 pub fn verify_content_hash(&self) -> bool {
1334 match &self.content_hash {
1335 Some(hash) => {
1336 let computed = self.compute_content_hash();
1337 hash == &computed
1338 }
1339 None => true, }
1341 }
1342
1343 pub fn with_content_hash(mut self) -> Self {
1345 self.content_hash = Some(self.compute_content_hash());
1346 self
1347 }
1348}
1349
1350#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1357pub struct PdaDefinition {
1358 pub name: String,
1360
1361 pub seeds: Vec<PdaSeedDef>,
1363
1364 #[serde(default, skip_serializing_if = "Option::is_none")]
1367 pub program_id: Option<String>,
1368}
1369
1370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1372#[serde(tag = "type", rename_all = "camelCase")]
1373pub enum PdaSeedDef {
1374 Literal { value: String },
1376
1377 Bytes { value: Vec<u8> },
1379
1380 ArgRef {
1382 arg_name: String,
1383 #[serde(default, skip_serializing_if = "Option::is_none")]
1385 arg_type: Option<String>,
1386 },
1387
1388 AccountRef { account_name: String },
1390}
1391
1392#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1394#[serde(tag = "category", rename_all = "camelCase")]
1395pub enum AccountResolution {
1396 Signer,
1398
1399 Known { address: String },
1401
1402 PdaRef { pda_name: String },
1404
1405 PdaInline {
1407 seeds: Vec<PdaSeedDef>,
1408 #[serde(default, skip_serializing_if = "Option::is_none")]
1409 program_id: Option<String>,
1410 },
1411
1412 UserProvided,
1414}
1415
1416#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1418pub struct InstructionAccountDef {
1419 pub name: String,
1421
1422 #[serde(default)]
1424 pub is_signer: bool,
1425
1426 #[serde(default)]
1428 pub is_writable: bool,
1429
1430 pub resolution: AccountResolution,
1432
1433 #[serde(default)]
1435 pub is_optional: bool,
1436
1437 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1439 pub docs: Vec<String>,
1440}
1441
1442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1444#[serde(rename_all = "camelCase")]
1445pub struct InstructionAmountHint {
1446 pub decimals_source: AmountDecimalsSource,
1447}
1448
1449#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1450#[serde(
1451 tag = "kind",
1452 rename_all = "camelCase",
1453 rename_all_fields = "camelCase"
1454)]
1455pub enum AmountDecimalsSource {
1456 ArgMint { arg_name: String },
1457 ArgDecimals { arg_name: String },
1458 KnownAccount { account_name: String },
1459 Constant { decimals: u8 },
1460}
1461
1462#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1464pub struct InstructionArgDef {
1465 pub name: String,
1467
1468 #[serde(rename = "type")]
1470 pub arg_type: String,
1471
1472 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1474 pub docs: Vec<String>,
1475
1476 #[serde(default, skip_serializing_if = "Option::is_none")]
1478 pub amount_hint: Option<InstructionAmountHint>,
1479}
1480
1481#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1483pub struct InstructionDef {
1484 pub name: String,
1486
1487 pub discriminator: Vec<u8>,
1489
1490 #[serde(default = "default_instruction_discriminant_size")]
1492 pub discriminator_size: usize,
1493
1494 pub accounts: Vec<InstructionAccountDef>,
1496
1497 pub args: Vec<InstructionArgDef>,
1499
1500 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1502 pub errors: Vec<IdlErrorSnapshot>,
1503
1504 #[serde(default, skip_serializing_if = "Option::is_none")]
1506 pub program_id: Option<String>,
1507
1508 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1510 pub docs: Vec<String>,
1511}
1512
1513#[derive(Debug, Clone, Serialize, Deserialize)]
1520pub struct SerializableStackSpec {
1521 #[serde(default = "default_ast_version")]
1524 pub ast_version: String,
1525 pub stack_name: String,
1527 #[serde(default)]
1529 pub program_ids: Vec<String>,
1530 #[serde(default)]
1532 pub idls: Vec<IdlSnapshot>,
1533 pub entities: Vec<SerializableStreamSpec>,
1535 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1538 pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1539 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1541 pub instructions: Vec<InstructionDef>,
1542 #[serde(default, skip_serializing_if = "Option::is_none")]
1544 pub content_hash: Option<String>,
1545}
1546
1547impl SerializableStackSpec {
1548 pub fn normalize_event_names(&mut self) {
1550 for entity in &mut self.entities {
1551 entity.normalize_event_names();
1552 }
1553 }
1554
1555 pub fn compute_content_hash(&self) -> String {
1557 use sha2::{Digest, Sha256};
1558 let mut spec_for_hash = self.clone();
1559 spec_for_hash.content_hash = None;
1560 let json = serde_json::to_string(&spec_for_hash)
1561 .expect("Failed to serialize stack spec for hashing");
1562 let mut hasher = Sha256::new();
1563 hasher.update(json.as_bytes());
1564 hex::encode(hasher.finalize())
1565 }
1566
1567 pub fn with_content_hash(mut self) -> Self {
1568 self.content_hash = Some(self.compute_content_hash());
1569 self
1570 }
1571}
1572
1573#[cfg(test)]
1574mod tests {
1575 use super::{
1576 HookAction, IdentitySpec, InstructionHook, MappingSource, PopulationStrategy,
1577 SerializableFieldMapping, SerializableHandlerSpec, SerializableStackSpec,
1578 SerializableStreamSpec, SourceSpec, TypedStreamSpec, CURRENT_AST_VERSION,
1579 };
1580 use serde_json::Value;
1581
1582 fn legacy_stream_spec() -> SerializableStreamSpec {
1583 SerializableStreamSpec {
1584 ast_version: CURRENT_AST_VERSION.to_string(),
1585 state_name: "PumpfunToken".to_string(),
1586 program_id: None,
1587 idl: None,
1588 identity: IdentitySpec {
1589 primary_keys: vec!["id.mint".to_string()],
1590 lookup_indexes: vec![],
1591 },
1592 handlers: vec![SerializableHandlerSpec {
1593 source: SourceSpec::Source {
1594 program_id: None,
1595 discriminator: None,
1596 type_name: "pump::buyIxState".to_string(),
1597 serialization: None,
1598 is_account: false,
1599 },
1600 key_resolution: super::KeyResolutionStrategy::Embedded {
1601 primary_field: super::FieldPath::new(&["accounts", "mint"]),
1602 },
1603 mappings: vec![SerializableFieldMapping {
1604 target_path: "info.last_buy_at".to_string(),
1605 source: MappingSource::Constant(Value::Null),
1606 transform: None,
1607 population: PopulationStrategy::SetOnce,
1608 condition: None,
1609 when: Some("pump::sellIxState".to_string()),
1610 stop: Some("pump::buy_exact_sol_inIxState".to_string()),
1611 emit: true,
1612 }],
1613 conditions: vec![],
1614 emit: true,
1615 }],
1616 sections: vec![],
1617 field_mappings: Default::default(),
1618 resolver_hooks: vec![],
1619 instruction_hooks: vec![InstructionHook {
1620 instruction_type: "pump::buyIxState".to_string(),
1621 actions: vec![HookAction::SetField {
1622 target_field: "info.last_buy_at".to_string(),
1623 source: MappingSource::Constant(Value::Null),
1624 condition: None,
1625 }],
1626 lookup_by: None,
1627 }],
1628 resolver_specs: vec![],
1629 computed_fields: vec![],
1630 computed_field_specs: vec![],
1631 content_hash: None,
1632 views: vec![],
1633 }
1634 }
1635
1636 #[test]
1637 fn typed_stream_spec_from_serializable_normalizes_legacy_instruction_event_names() {
1638 let typed = TypedStreamSpec::<Value>::from_serializable(legacy_stream_spec());
1639
1640 let handler = &typed.handlers[0];
1641 let SourceSpec::Source { type_name, .. } = &handler.source;
1642 assert_eq!(type_name, "pump::BuyIxState");
1643 assert_eq!(
1644 handler.mappings[0].when.as_deref(),
1645 Some("pump::SellIxState")
1646 );
1647 assert_eq!(
1648 handler.mappings[0].stop.as_deref(),
1649 Some("pump::BuyExactSolInIxState")
1650 );
1651 assert_eq!(
1652 typed.instruction_hooks[0].instruction_type,
1653 "pump::BuyIxState"
1654 );
1655 }
1656
1657 #[test]
1658 fn stack_spec_normalize_event_names_updates_all_entities() {
1659 let mut stack = SerializableStackSpec {
1660 ast_version: CURRENT_AST_VERSION.to_string(),
1661 stack_name: "PumpStack".to_string(),
1662 program_ids: vec![],
1663 idls: vec![],
1664 entities: vec![legacy_stream_spec()],
1665 pdas: Default::default(),
1666 instructions: vec![],
1667 content_hash: None,
1668 };
1669
1670 stack.normalize_event_names();
1671
1672 let SourceSpec::Source { type_name, .. } = &stack.entities[0].handlers[0].source;
1673 assert_eq!(type_name, "pump::BuyIxState");
1674 assert_eq!(
1675 stack.entities[0].instruction_hooks[0].instruction_type,
1676 "pump::BuyIxState"
1677 );
1678 }
1679
1680 #[test]
1681 fn field_type_info_recognizes_128_bit_integers() {
1682 let unsigned = super::FieldTypeInfo::new("amount".to_string(), "u128".to_string());
1683 assert_eq!(unsigned.base_type, super::BaseType::Integer);
1684 assert!(!unsigned.is_optional);
1685 assert_eq!(unsigned.integer_kind, Some(super::IntegerKind::U128));
1686 assert_eq!(unsigned.raw_field_name(), "amount");
1687 assert_eq!(unsigned.canonical_field_name(), "amount");
1688
1689 let signed = super::FieldTypeInfo::new("delta".to_string(), "Option<i128>".to_string());
1690 assert_eq!(signed.base_type, super::BaseType::Integer);
1691 assert!(signed.is_optional);
1692 assert_eq!(signed.integer_kind, Some(super::IntegerKind::I128));
1693 assert_eq!(signed.inner_type.as_deref(), Some("i128"));
1694 }
1695
1696 #[test]
1697 fn field_type_info_preserves_timestamp_integer_kind_and_names() {
1698 let timestamp =
1699 super::FieldTypeInfo::new("last_updated_at".to_string(), "Option<i64>".to_string());
1700
1701 assert_eq!(timestamp.base_type, super::BaseType::Timestamp);
1702 assert_eq!(timestamp.integer_kind, Some(super::IntegerKind::I64));
1703 assert!(timestamp.is_optional);
1704 assert_eq!(timestamp.raw_field_name(), "last_updated_at");
1705 assert_eq!(timestamp.canonical_field_name(), "lastUpdatedAt");
1706 }
1707}
1708
1709#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1715#[serde(rename_all = "lowercase")]
1716pub enum SortOrder {
1717 #[default]
1718 Asc,
1719 Desc,
1720}
1721
1722#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1724pub enum CompareOp {
1725 Eq,
1726 Ne,
1727 Gt,
1728 Gte,
1729 Lt,
1730 Lte,
1731}
1732
1733#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1735pub enum PredicateValue {
1736 Literal(serde_json::Value),
1738 Dynamic(String),
1740 Field(FieldPath),
1742}
1743
1744#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1746pub enum Predicate {
1747 Compare {
1749 field: FieldPath,
1750 op: CompareOp,
1751 value: PredicateValue,
1752 },
1753 And(Vec<Predicate>),
1755 Or(Vec<Predicate>),
1757 Not(Box<Predicate>),
1759 Exists { field: FieldPath },
1761}
1762
1763#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1765pub enum ViewTransform {
1766 Filter { predicate: Predicate },
1768
1769 Sort {
1771 key: FieldPath,
1772 #[serde(default)]
1773 order: SortOrder,
1774 },
1775
1776 Take { count: usize },
1778
1779 Skip { count: usize },
1781
1782 First,
1784
1785 Last,
1787
1788 MaxBy { key: FieldPath },
1790
1791 MinBy { key: FieldPath },
1793}
1794
1795#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1797pub enum ViewSource {
1798 Entity { name: String },
1800 View { id: String },
1802}
1803
1804#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1806pub enum ViewOutput {
1807 #[default]
1809 Collection,
1810 Single,
1812 Keyed { key_field: FieldPath },
1814}
1815
1816#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1818pub struct ViewDef {
1819 pub id: String,
1821
1822 pub source: ViewSource,
1824
1825 #[serde(default)]
1827 pub pipeline: Vec<ViewTransform>,
1828
1829 #[serde(default)]
1831 pub output: ViewOutput,
1832}
1833
1834impl ViewDef {
1835 pub fn list(entity_name: &str) -> Self {
1837 ViewDef {
1838 id: format!("{}/list", entity_name),
1839 source: ViewSource::Entity {
1840 name: entity_name.to_string(),
1841 },
1842 pipeline: vec![],
1843 output: ViewOutput::Collection,
1844 }
1845 }
1846
1847 pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1849 ViewDef {
1850 id: format!("{}/state", entity_name),
1851 source: ViewSource::Entity {
1852 name: entity_name.to_string(),
1853 },
1854 pipeline: vec![],
1855 output: ViewOutput::Keyed {
1856 key_field: FieldPath::new(key_field),
1857 },
1858 }
1859 }
1860
1861 pub fn is_single(&self) -> bool {
1863 matches!(self.output, ViewOutput::Single)
1864 }
1865
1866 pub fn has_single_transform(&self) -> bool {
1868 self.pipeline.iter().any(|t| {
1869 matches!(
1870 t,
1871 ViewTransform::First
1872 | ViewTransform::Last
1873 | ViewTransform::MaxBy { .. }
1874 | ViewTransform::MinBy { .. }
1875 )
1876 })
1877 }
1878}
1879
1880#[macro_export]
1881macro_rules! define_accessor {
1882 ($name:ident, $state:ty, $path:expr) => {
1883 pub struct $name;
1884
1885 impl $crate::ast::FieldAccessor<$state> for $name {
1886 fn path(&self) -> String {
1887 $path.to_string()
1888 }
1889 }
1890 };
1891}