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