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 let inner = idl_type_snapshot_to_rust_string(&vec.vec);
55 match vec.length_prefix {
56 Some(arete_idl::types::IdlLengthPrefix::U64) => format!("VecU64Len<{}>", inner),
57 _ => format!("Vec<{}>", inner),
58 }
59 }
60 IdlTypeSnapshot::HashMap(map) => {
61 let key_type = idl_type_snapshot_to_rust_string(&map.hash_map.0);
62 let val_type = idl_type_snapshot_to_rust_string(&map.hash_map.1);
63 format!("std::collections::HashMap<{}, {}>", key_type, val_type)
64 }
65 IdlTypeSnapshot::Tuple(tuple) => format!(
66 "({})",
67 tuple
68 .tuple
69 .iter()
70 .map(idl_type_snapshot_to_rust_string)
71 .collect::<Vec<_>>()
72 .join(", ")
73 ),
74 IdlTypeSnapshot::Defined(def) => match &def.defined {
75 IdlDefinedInnerSnapshot::Named { name } => name.clone(),
76 IdlDefinedInnerSnapshot::Simple(s) => s.clone(),
77 },
78 }
79}
80
81fn map_simple_idl_type(idl_type: &str) -> String {
82 match idl_type {
83 "u8" => "u8".to_string(),
84 "u16" => "u16".to_string(),
85 "u32" => "u32".to_string(),
86 "u64" => "u64".to_string(),
87 "u128" => "u128".to_string(),
88 "i8" => "i8".to_string(),
89 "i16" => "i16".to_string(),
90 "i32" => "i32".to_string(),
91 "i64" => "i64".to_string(),
92 "i128" => "i128".to_string(),
93 "bool" => "bool".to_string(),
94 "string" => "String".to_string(),
95 "publicKey" | "pubkey" => "solana_pubkey::Pubkey".to_string(),
96 "bytes" => "Vec<u8>".to_string(),
97 _ => idl_type.to_string(),
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102pub struct FieldPath {
103 pub segments: Vec<String>,
104 pub offsets: Option<Vec<usize>>,
105}
106
107impl FieldPath {
108 pub fn new(segments: &[&str]) -> Self {
109 FieldPath {
110 segments: segments.iter().map(|s| s.to_string()).collect(),
111 offsets: None,
112 }
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
117pub enum Transformation {
118 HexEncode,
119 HexDecode,
120 Base58Encode,
121 Base58Decode,
122 ToString,
123 ToNumber,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub enum PopulationStrategy {
128 SetOnce,
129 LastWrite,
130 Append,
131 Merge,
132 Max,
133 Sum,
135 Count,
137 Min,
139 UniqueCount,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ComputedFieldSpec {
151 pub target_path: String,
153 pub expression: ComputedExpr,
155 pub result_type: String,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
164#[serde(rename_all = "lowercase")]
165pub enum ResolverType {
166 Token,
167 Url(UrlResolverConfig),
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
171#[serde(rename_all = "lowercase")]
172pub enum HttpMethod {
173 #[default]
174 Get,
175 Post,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
179pub enum UrlTemplatePart {
180 Literal(String),
181 FieldRef(String),
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
185pub enum UrlSource {
186 FieldPath(String),
187 Template(Vec<UrlTemplatePart>),
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
191pub struct UrlResolverConfig {
192 pub url_source: UrlSource,
193 #[serde(default)]
194 pub method: HttpMethod,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub extract_path: Option<String>,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
200pub struct ResolverExtractSpec {
201 pub target_path: String,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub source_path: Option<String>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub transform: Option<Transformation>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
209pub enum ResolveStrategy {
210 #[default]
211 SetOnce,
212 LastWrite,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
216pub struct ResolverCondition {
217 pub field_path: String,
218 pub op: ComparisonOp,
219 pub value: Value,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct ResolverSpec {
224 pub resolver: ResolverType,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub input_path: Option<String>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub input_value: Option<Value>,
229 #[serde(default)]
230 pub strategy: ResolveStrategy,
231 pub extracts: Vec<ResolverExtractSpec>,
232 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub condition: Option<ResolverCondition>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub schedule_at: Option<String>,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
248pub enum ComputedExpr {
249 FieldRef {
252 path: String,
253 },
254
255 UnwrapOr {
257 expr: Box<ComputedExpr>,
258 default: serde_json::Value,
259 },
260
261 Binary {
263 op: BinaryOp,
264 left: Box<ComputedExpr>,
265 right: Box<ComputedExpr>,
266 },
267
268 Cast {
270 expr: Box<ComputedExpr>,
271 to_type: String,
272 },
273
274 MethodCall {
276 expr: Box<ComputedExpr>,
277 method: String,
278 args: Vec<ComputedExpr>,
279 },
280
281 ResolverComputed {
283 resolver: String,
284 method: String,
285 args: Vec<ComputedExpr>,
286 },
287
288 Literal {
290 value: serde_json::Value,
291 },
292
293 Paren {
295 expr: Box<ComputedExpr>,
296 },
297
298 Var {
300 name: String,
301 },
302
303 Let {
305 name: String,
306 value: Box<ComputedExpr>,
307 body: Box<ComputedExpr>,
308 },
309
310 If {
312 condition: Box<ComputedExpr>,
313 then_branch: Box<ComputedExpr>,
314 else_branch: Box<ComputedExpr>,
315 },
316
317 None,
319 Some {
320 value: Box<ComputedExpr>,
321 },
322
323 Slice {
325 expr: Box<ComputedExpr>,
326 start: usize,
327 end: usize,
328 },
329 Index {
330 expr: Box<ComputedExpr>,
331 index: usize,
332 },
333
334 U64FromLeBytes {
336 bytes: Box<ComputedExpr>,
337 },
338 U64FromBeBytes {
339 bytes: Box<ComputedExpr>,
340 },
341
342 ByteArray {
344 bytes: Vec<u8>,
345 },
346
347 Closure {
349 param: String,
350 body: Box<ComputedExpr>,
351 },
352
353 Unary {
355 op: UnaryOp,
356 expr: Box<ComputedExpr>,
357 },
358
359 JsonToBytes {
361 expr: Box<ComputedExpr>,
362 },
363
364 ContextSlot,
367 ContextTimestamp,
369
370 Keccak256 {
373 expr: Box<ComputedExpr>,
374 },
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub enum BinaryOp {
380 Add,
382 Sub,
383 Mul,
384 Div,
385 Mod,
386 Gt,
388 Lt,
389 Gte,
390 Lte,
391 Eq,
392 Ne,
393 And,
395 Or,
396 Xor,
398 BitAnd,
399 BitOr,
400 Shl,
401 Shr,
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
406pub enum UnaryOp {
407 Not,
408 ReverseBits,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct SerializableStreamSpec {
414 #[serde(default = "default_ast_version")]
417 pub ast_version: String,
418 pub state_name: String,
419 #[serde(default)]
421 pub program_id: Option<String>,
422 #[serde(default)]
424 pub idl: Option<IdlSnapshot>,
425 pub identity: IdentitySpec,
426 pub handlers: Vec<SerializableHandlerSpec>,
427 pub sections: Vec<EntitySection>,
428 pub field_mappings: BTreeMap<String, FieldTypeInfo>,
429 pub resolver_hooks: Vec<ResolverHook>,
430 pub instruction_hooks: Vec<InstructionHook>,
431 #[serde(default)]
432 pub resolver_specs: Vec<ResolverSpec>,
433 #[serde(default)]
435 pub computed_fields: Vec<String>,
436 #[serde(default)]
438 pub computed_field_specs: Vec<ComputedFieldSpec>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
442 pub content_hash: Option<String>,
443 #[serde(default)]
445 pub views: Vec<ViewDef>,
446}
447
448impl SerializableStreamSpec {
449 pub fn normalize_event_names(&mut self) {
451 for handler in &mut self.handlers {
452 handler.normalize_event_names();
453 }
454
455 for hook in &mut self.instruction_hooks {
456 hook.instruction_type =
457 crate::event_type_helpers::canonicalize_event_type_name(&hook.instruction_type);
458 }
459 }
460}
461
462#[derive(Debug, Clone)]
463pub struct TypedStreamSpec<S> {
464 pub state_name: String,
465 pub identity: IdentitySpec,
466 pub handlers: Vec<TypedHandlerSpec<S>>,
467 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>,
472 pub computed_fields: Vec<String>, _phantom: PhantomData<S>,
474}
475
476impl<S> TypedStreamSpec<S> {
477 pub fn new(
478 state_name: String,
479 identity: IdentitySpec,
480 handlers: Vec<TypedHandlerSpec<S>>,
481 ) -> Self {
482 TypedStreamSpec {
483 state_name,
484 identity,
485 handlers,
486 sections: Vec::new(),
487 field_mappings: BTreeMap::new(),
488 resolver_hooks: Vec::new(),
489 instruction_hooks: Vec::new(),
490 resolver_specs: Vec::new(),
491 computed_fields: Vec::new(),
492 _phantom: PhantomData,
493 }
494 }
495
496 pub fn with_type_info(
498 state_name: String,
499 identity: IdentitySpec,
500 handlers: Vec<TypedHandlerSpec<S>>,
501 sections: Vec<EntitySection>,
502 field_mappings: BTreeMap<String, FieldTypeInfo>,
503 ) -> Self {
504 TypedStreamSpec {
505 state_name,
506 identity,
507 handlers,
508 sections,
509 field_mappings,
510 resolver_hooks: Vec::new(),
511 instruction_hooks: Vec::new(),
512 resolver_specs: Vec::new(),
513 computed_fields: Vec::new(),
514 _phantom: PhantomData,
515 }
516 }
517
518 pub fn with_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
519 self.resolver_specs = resolver_specs;
520 self
521 }
522
523 pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
525 self.field_mappings.get(path)
526 }
527
528 pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
530 self.sections
531 .iter()
532 .find(|s| s.name == section_name)
533 .map(|s| &s.fields)
534 }
535
536 pub fn get_section_names(&self) -> Vec<&String> {
538 self.sections.iter().map(|s| &s.name).collect()
539 }
540
541 pub fn to_serializable(&self) -> SerializableStreamSpec {
543 let mut spec = SerializableStreamSpec {
544 ast_version: CURRENT_AST_VERSION.to_string(),
545 state_name: self.state_name.clone(),
546 program_id: None,
547 idl: None,
548 identity: self.identity.clone(),
549 handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
550 sections: self.sections.clone(),
551 field_mappings: self.field_mappings.clone(),
552 resolver_hooks: self.resolver_hooks.clone(),
553 instruction_hooks: self.instruction_hooks.clone(),
554 resolver_specs: self.resolver_specs.clone(),
555 computed_fields: self.computed_fields.clone(),
556 computed_field_specs: Vec::new(),
557 content_hash: None,
558 views: Vec::new(),
559 };
560 spec.content_hash = Some(spec.compute_content_hash());
561 spec
562 }
563
564 pub fn from_serializable(mut spec: SerializableStreamSpec) -> Self {
566 spec.normalize_event_names();
567 TypedStreamSpec {
568 state_name: spec.state_name,
569 identity: spec.identity,
570 handlers: spec
571 .handlers
572 .into_iter()
573 .map(|h| TypedHandlerSpec::from_serializable(h))
574 .collect(),
575 sections: spec.sections,
576 field_mappings: spec.field_mappings,
577 resolver_hooks: spec.resolver_hooks,
578 instruction_hooks: spec.instruction_hooks,
579 resolver_specs: spec.resolver_specs,
580 computed_fields: spec.computed_fields,
581 _phantom: PhantomData,
582 }
583 }
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
587pub struct IdentitySpec {
588 pub primary_keys: Vec<String>,
589 pub lookup_indexes: Vec<LookupIndexSpec>,
590}
591
592#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct LookupIndexSpec {
594 pub field_name: String,
595 pub temporal_field: Option<String>,
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct ResolverHook {
605 pub account_type: String,
607
608 pub strategy: ResolverStrategy,
610}
611
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub enum ResolverStrategy {
614 PdaReverseLookup {
616 lookup_name: String,
617 queue_discriminators: Vec<Vec<u8>>,
619 },
620
621 DirectField { field_path: FieldPath },
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize)]
627pub struct InstructionHook {
628 pub instruction_type: String,
630
631 pub actions: Vec<HookAction>,
633
634 pub lookup_by: Option<FieldPath>,
636}
637
638#[derive(Debug, Clone, Serialize, Deserialize)]
639pub enum HookAction {
640 RegisterPdaMapping {
642 pda_field: FieldPath,
643 seed_field: FieldPath,
644 lookup_name: String,
645 },
646
647 SetField {
649 target_field: String,
650 source: MappingSource,
651 condition: Option<ConditionExpr>,
652 },
653
654 IncrementField {
656 target_field: String,
657 increment_by: i64,
658 condition: Option<ConditionExpr>,
659 },
660}
661
662#[derive(Debug, Clone, Serialize, Deserialize)]
664pub struct ConditionExpr {
665 pub expression: String,
667
668 pub parsed: Option<ParsedCondition>,
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize)]
673pub enum ParsedCondition {
674 Comparison {
676 field: FieldPath,
677 op: ComparisonOp,
678 value: serde_json::Value,
679 },
680
681 Logical {
683 op: LogicalOp,
684 conditions: Vec<ParsedCondition>,
685 },
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
689pub enum ComparisonOp {
690 Equal,
691 NotEqual,
692 GreaterThan,
693 GreaterThanOrEqual,
694 LessThan,
695 LessThanOrEqual,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize)]
699pub enum LogicalOp {
700 And,
701 Or,
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize)]
706pub struct SerializableHandlerSpec {
707 pub source: SourceSpec,
708 pub key_resolution: KeyResolutionStrategy,
709 pub mappings: Vec<SerializableFieldMapping>,
710 pub conditions: Vec<Condition>,
711 pub emit: bool,
712}
713
714impl SerializableHandlerSpec {
715 pub fn normalize_event_names(&mut self) {
716 let SourceSpec::Source { type_name, .. } = &mut self.source;
717 *type_name = crate::event_type_helpers::canonicalize_event_type_name(type_name);
718
719 for mapping in &mut self.mappings {
720 if let Some(when) = &mut mapping.when {
721 *when = crate::event_type_helpers::canonicalize_event_type_name(when);
722 }
723 if let Some(stop) = &mut mapping.stop {
724 *stop = crate::event_type_helpers::canonicalize_event_type_name(stop);
725 }
726 }
727 }
728}
729
730#[derive(Debug, Clone)]
731pub struct TypedHandlerSpec<S> {
732 pub source: SourceSpec,
733 pub key_resolution: KeyResolutionStrategy,
734 pub mappings: Vec<TypedFieldMapping<S>>,
735 pub conditions: Vec<Condition>,
736 pub emit: bool,
737 _phantom: PhantomData<S>,
738}
739
740impl<S> TypedHandlerSpec<S> {
741 pub fn new(
742 source: SourceSpec,
743 key_resolution: KeyResolutionStrategy,
744 mappings: Vec<TypedFieldMapping<S>>,
745 emit: bool,
746 ) -> Self {
747 TypedHandlerSpec {
748 source,
749 key_resolution,
750 mappings,
751 conditions: vec![],
752 emit,
753 _phantom: PhantomData,
754 }
755 }
756
757 pub fn to_serializable(&self) -> SerializableHandlerSpec {
759 SerializableHandlerSpec {
760 source: self.source.clone(),
761 key_resolution: self.key_resolution.clone(),
762 mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
763 conditions: self.conditions.clone(),
764 emit: self.emit,
765 }
766 }
767
768 pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
770 TypedHandlerSpec {
771 source: spec.source,
772 key_resolution: spec.key_resolution,
773 mappings: spec
774 .mappings
775 .into_iter()
776 .map(|m| TypedFieldMapping::from_serializable(m))
777 .collect(),
778 conditions: spec.conditions,
779 emit: spec.emit,
780 _phantom: PhantomData,
781 }
782 }
783}
784
785#[derive(Debug, Clone, Serialize, Deserialize)]
786pub enum KeyResolutionStrategy {
787 Embedded {
788 primary_field: FieldPath,
789 },
790 Lookup {
791 primary_field: FieldPath,
792 },
793 Computed {
794 primary_field: FieldPath,
795 compute_partition: ComputeFunction,
796 },
797 TemporalLookup {
798 lookup_field: FieldPath,
799 timestamp_field: FieldPath,
800 index_name: String,
801 },
802}
803
804#[derive(Debug, Clone, Serialize, Deserialize)]
805pub enum SourceSpec {
806 Source {
807 program_id: Option<String>,
808 discriminator: Option<Vec<u8>>,
809 type_name: String,
810 #[serde(default, skip_serializing_if = "Option::is_none")]
811 serialization: Option<IdlSerializationSnapshot>,
812 #[serde(default)]
817 is_account: bool,
818 },
819}
820
821#[derive(Debug, Clone, Serialize, Deserialize)]
823pub struct SerializableFieldMapping {
824 pub target_path: String,
825 pub source: MappingSource,
826 pub transform: Option<Transformation>,
827 pub population: PopulationStrategy,
828 #[serde(default, skip_serializing_if = "Option::is_none")]
829 pub condition: Option<ConditionExpr>,
830 #[serde(default, skip_serializing_if = "Option::is_none")]
831 pub when: Option<String>,
832 #[serde(default, skip_serializing_if = "Option::is_none")]
833 pub stop: Option<String>,
834 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
835 pub emit: bool,
836}
837
838fn default_emit() -> bool {
839 true
840}
841
842fn default_instruction_discriminant_size() -> usize {
843 8
844}
845
846fn is_true(value: &bool) -> bool {
847 *value
848}
849
850#[derive(Debug, Clone)]
851pub struct TypedFieldMapping<S> {
852 pub target_path: String,
853 pub source: MappingSource,
854 pub transform: Option<Transformation>,
855 pub population: PopulationStrategy,
856 pub condition: Option<ConditionExpr>,
857 pub when: Option<String>,
858 pub stop: Option<String>,
859 pub emit: bool,
860 _phantom: PhantomData<S>,
861}
862
863impl<S> TypedFieldMapping<S> {
864 pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
865 TypedFieldMapping {
866 target_path,
867 source,
868 transform: None,
869 population,
870 condition: None,
871 when: None,
872 stop: None,
873 emit: true,
874 _phantom: PhantomData,
875 }
876 }
877
878 pub fn with_transform(mut self, transform: Transformation) -> Self {
879 self.transform = Some(transform);
880 self
881 }
882
883 pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
884 self.condition = Some(condition);
885 self
886 }
887
888 pub fn with_when(mut self, when: String) -> Self {
889 self.when = Some(when);
890 self
891 }
892
893 pub fn with_stop(mut self, stop: String) -> Self {
894 self.stop = Some(stop);
895 self
896 }
897
898 pub fn with_emit(mut self, emit: bool) -> Self {
899 self.emit = emit;
900 self
901 }
902
903 pub fn to_serializable(&self) -> SerializableFieldMapping {
905 SerializableFieldMapping {
906 target_path: self.target_path.clone(),
907 source: self.source.clone(),
908 transform: self.transform.clone(),
909 population: self.population.clone(),
910 condition: self.condition.clone(),
911 when: self.when.clone(),
912 stop: self.stop.clone(),
913 emit: self.emit,
914 }
915 }
916
917 pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
919 TypedFieldMapping {
920 target_path: mapping.target_path,
921 source: mapping.source,
922 transform: mapping.transform,
923 population: mapping.population,
924 condition: mapping.condition,
925 when: mapping.when,
926 stop: mapping.stop,
927 emit: mapping.emit,
928 _phantom: PhantomData,
929 }
930 }
931}
932
933#[derive(Debug, Clone, Serialize, Deserialize)]
934pub enum MappingSource {
935 FromSource {
936 path: FieldPath,
937 default: Option<Value>,
938 transform: Option<Transformation>,
939 },
940 Constant(Value),
941 Computed {
942 inputs: Vec<FieldPath>,
943 function: ComputeFunction,
944 },
945 FromState {
946 path: String,
947 },
948 AsEvent {
949 fields: Vec<Box<MappingSource>>,
950 },
951 WholeSource,
952 AsCapture {
955 field_transforms: BTreeMap<String, Transformation>,
956 },
957 FromContext {
960 field: String,
961 },
962 FromExpr {
967 expr: ComputedExpr,
968 },
969}
970
971impl MappingSource {
972 pub fn with_transform(self, transform: Transformation) -> Self {
973 match self {
974 MappingSource::FromSource {
975 path,
976 default,
977 transform: _,
978 } => MappingSource::FromSource {
979 path,
980 default,
981 transform: Some(transform),
982 },
983 other => other,
984 }
985 }
986}
987
988#[derive(Debug, Clone, Serialize, Deserialize)]
989pub enum ComputeFunction {
990 Sum,
991 Concat,
992 Format(String),
993 Custom(String),
994}
995
996#[derive(Debug, Clone, Serialize, Deserialize)]
997pub struct Condition {
998 pub field: FieldPath,
999 pub operator: ConditionOp,
1000 pub value: Value,
1001}
1002
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004pub enum ConditionOp {
1005 Equals,
1006 NotEquals,
1007 GreaterThan,
1008 LessThan,
1009 Contains,
1010 Exists,
1011}
1012
1013#[derive(Debug, Clone, Serialize, Deserialize)]
1015pub struct FieldTypeInfo {
1016 pub field_name: String,
1017 #[serde(default, skip_serializing_if = "Option::is_none")]
1018 pub raw_name: Option<String>,
1019 #[serde(default, skip_serializing_if = "Option::is_none")]
1020 pub canonical_name: Option<String>,
1021 pub rust_type_name: String, pub base_type: BaseType, #[serde(default, skip_serializing_if = "Option::is_none")]
1024 pub integer_kind: Option<IntegerKind>,
1025 pub is_optional: bool, pub is_array: bool, pub inner_type: Option<String>, pub source_path: Option<String>, #[serde(default)]
1031 pub resolved_type: Option<ResolvedStructType>,
1032 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
1033 pub emit: bool,
1034}
1035
1036#[derive(Debug, Clone, Serialize, Deserialize)]
1038pub struct ResolvedStructType {
1039 pub type_name: String,
1040 pub fields: Vec<ResolvedField>,
1041 pub is_instruction: bool,
1042 pub is_account: bool,
1043 pub is_event: bool,
1044 #[serde(default)]
1046 pub is_enum: bool,
1047 #[serde(default)]
1049 pub enum_variants: Vec<String>,
1050}
1051
1052#[derive(Debug, Clone, Serialize, Deserialize)]
1054pub struct ResolvedField {
1055 pub field_name: String,
1056 #[serde(default, skip_serializing_if = "Option::is_none")]
1057 pub raw_name: Option<String>,
1058 #[serde(default, skip_serializing_if = "Option::is_none")]
1059 pub canonical_name: Option<String>,
1060 pub field_type: String,
1061 pub base_type: BaseType,
1062 #[serde(default, skip_serializing_if = "Option::is_none")]
1063 pub integer_kind: Option<IntegerKind>,
1064 pub is_optional: bool,
1065 pub is_array: bool,
1066}
1067
1068#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1069pub enum IntegerKind {
1070 U8,
1071 U16,
1072 U32,
1073 U64,
1074 U128,
1075 Usize,
1076 I8,
1077 I16,
1078 I32,
1079 I64,
1080 I128,
1081 Isize,
1082}
1083
1084impl IntegerKind {
1085 pub fn from_rust_type(type_str: &str) -> Option<Self> {
1086 match type_str.trim() {
1087 "u8" => Some(Self::U8),
1088 "u16" => Some(Self::U16),
1089 "u32" => Some(Self::U32),
1090 "u64" => Some(Self::U64),
1091 "u128" => Some(Self::U128),
1092 "usize" => Some(Self::Usize),
1093 "i8" => Some(Self::I8),
1094 "i16" => Some(Self::I16),
1095 "i32" => Some(Self::I32),
1096 "i64" => Some(Self::I64),
1097 "i128" => Some(Self::I128),
1098 "isize" => Some(Self::Isize),
1099 _ => None,
1100 }
1101 }
1102
1103 pub fn is_bigint(self) -> bool {
1104 matches!(self, Self::U64 | Self::U128 | Self::I64 | Self::I128)
1105 }
1106}
1107
1108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1110pub enum BaseType {
1111 Integer, Float, String, Boolean, Object, Array, Binary, Timestamp, Pubkey, Any, }
1127
1128#[derive(Debug, Clone, Serialize, Deserialize)]
1130pub struct EntitySection {
1131 pub name: String,
1132 pub fields: Vec<FieldTypeInfo>,
1133 pub is_nested_struct: bool,
1134 pub parent_field: Option<String>, }
1136
1137impl FieldTypeInfo {
1138 pub fn new(field_name: String, rust_type_name: String) -> Self {
1139 let (base_type, integer_kind, is_optional, is_array, inner_type) =
1140 Self::analyze_rust_type(&rust_type_name);
1141 let canonical_name = to_camel_case_owned(&field_name);
1142
1143 FieldTypeInfo {
1144 field_name: field_name.clone(),
1145 raw_name: Some(field_name.clone()),
1146 canonical_name: Some(canonical_name),
1147 rust_type_name,
1148 base_type: Self::infer_semantic_type(&field_name, base_type),
1149 integer_kind,
1150 is_optional,
1151 is_array,
1152 inner_type,
1153 source_path: None,
1154 resolved_type: None,
1155 emit: true,
1156 }
1157 }
1158
1159 pub fn with_source_path(mut self, source_path: String) -> Self {
1160 self.source_path = Some(source_path);
1161 self
1162 }
1163
1164 pub fn raw_field_name(&self) -> &str {
1165 self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1166 }
1167
1168 pub fn canonical_field_name(&self) -> String {
1169 self.canonical_name
1170 .clone()
1171 .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1172 }
1173
1174 pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1175 self.integer_kind.or_else(|| {
1176 IntegerKind::from_rust_type(
1177 self.inner_type
1178 .as_deref()
1179 .unwrap_or(self.rust_type_name.as_str()),
1180 )
1181 })
1182 }
1183
1184 fn analyze_rust_type(
1186 rust_type: &str,
1187 ) -> (BaseType, Option<IntegerKind>, bool, bool, Option<String>) {
1188 let type_str = rust_type.trim();
1189
1190 if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1192 let (inner_base_type, inner_integer_kind, _, inner_is_array, inner_inner_type) =
1193 Self::analyze_rust_type(&inner);
1194 return (
1195 inner_base_type,
1196 inner_integer_kind,
1197 true,
1198 inner_is_array,
1199 inner_inner_type.or(Some(inner)),
1200 );
1201 }
1202
1203 if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1205 let (_inner_base_type, inner_integer_kind, inner_is_optional, _, inner_inner_type) =
1206 Self::analyze_rust_type(&inner);
1207 return (
1208 BaseType::Array,
1209 inner_integer_kind,
1210 inner_is_optional,
1211 true,
1212 inner_inner_type.or(Some(inner)),
1213 );
1214 }
1215
1216 let integer_kind = IntegerKind::from_rust_type(type_str);
1218 let base_type = match integer_kind {
1219 Some(_) => BaseType::Integer,
1220 None => match type_str {
1221 "f32" | "f64" => BaseType::Float,
1222 "bool" => BaseType::Boolean,
1223 "String" | "&str" | "str" => BaseType::String,
1224 "Value" | "serde_json::Value" => BaseType::Any,
1225 "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1226 _ => {
1227 if type_str.contains("Bytes") || type_str.contains("bytes") {
1229 BaseType::Binary
1230 } else if type_str.contains("Pubkey") {
1231 BaseType::Pubkey
1232 } else {
1233 BaseType::Object
1234 }
1235 }
1236 },
1237 };
1238
1239 (base_type, integer_kind, false, false, None)
1240 }
1241
1242 fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1244 let pattern = format!("{}<", generic_name);
1245 if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1246 let start = pattern.len();
1247 let end = type_str.len() - 1;
1248 if end > start {
1249 return Some(type_str[start..end].trim().to_string());
1250 }
1251 }
1252 None
1253 }
1254
1255 fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1257 let lower_name = field_name.to_lowercase();
1258
1259 if base_type == BaseType::Integer
1261 && (lower_name.ends_with("_at")
1262 || lower_name.ends_with("_time")
1263 || lower_name.contains("timestamp")
1264 || lower_name.contains("created")
1265 || lower_name.contains("settled")
1266 || lower_name.contains("activated"))
1267 {
1268 return BaseType::Timestamp;
1269 }
1270
1271 base_type
1272 }
1273}
1274
1275impl ResolvedField {
1276 pub fn raw_field_name(&self) -> &str {
1277 self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1278 }
1279
1280 pub fn canonical_field_name(&self) -> String {
1281 self.canonical_name
1282 .clone()
1283 .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1284 }
1285
1286 pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1287 self.integer_kind
1288 .or_else(|| IntegerKind::from_rust_type(self.field_type.as_str()))
1289 }
1290}
1291
1292pub(crate) fn to_camel_case_owned(s: &str) -> String {
1293 let mut result = String::new();
1294 let mut uppercase_next = false;
1295
1296 for ch in s.chars() {
1297 if matches!(ch, '_' | '-' | '.') {
1298 uppercase_next = true;
1299 continue;
1300 }
1301
1302 if result.is_empty() {
1303 result.extend(ch.to_lowercase());
1304 continue;
1305 }
1306
1307 if uppercase_next {
1308 result.extend(ch.to_uppercase());
1309 uppercase_next = false;
1310 } else {
1311 result.push(ch);
1312 }
1313 }
1314
1315 result
1316}
1317
1318pub trait FieldAccessor<S> {
1319 fn path(&self) -> String;
1320}
1321
1322impl SerializableStreamSpec {
1327 pub fn compute_content_hash(&self) -> String {
1333 use sha2::{Digest, Sha256};
1334
1335 let mut spec_for_hash = self.clone();
1337 spec_for_hash.content_hash = None;
1338
1339 let json =
1341 serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1342
1343 let mut hasher = Sha256::new();
1345 hasher.update(json.as_bytes());
1346 let result = hasher.finalize();
1347
1348 hex::encode(result)
1350 }
1351
1352 pub fn verify_content_hash(&self) -> bool {
1355 match &self.content_hash {
1356 Some(hash) => {
1357 let computed = self.compute_content_hash();
1358 hash == &computed
1359 }
1360 None => true, }
1362 }
1363
1364 pub fn with_content_hash(mut self) -> Self {
1366 self.content_hash = Some(self.compute_content_hash());
1367 self
1368 }
1369}
1370
1371#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1378pub struct PdaDefinition {
1379 pub name: String,
1381
1382 pub seeds: Vec<PdaSeedDef>,
1384
1385 #[serde(default, skip_serializing_if = "Option::is_none")]
1388 pub program_id: Option<String>,
1389
1390 #[serde(default, skip_serializing_if = "Option::is_none")]
1393 pub program: Option<PdaProgramDef>,
1394}
1395
1396#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1397#[serde(tag = "type", rename_all = "camelCase")]
1398pub enum PdaProgramDef {
1399 AccountRef { account_name: String },
1400 ArgRef { arg_name: String },
1401}
1402
1403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1405#[serde(tag = "type", rename_all = "camelCase")]
1406pub enum PdaSeedDef {
1407 Literal { value: String },
1409
1410 Bytes { value: Vec<u8> },
1412
1413 ArgRef {
1415 arg_name: String,
1416 #[serde(default, skip_serializing_if = "Option::is_none")]
1418 arg_type: Option<String>,
1419 },
1420
1421 AccountRef { account_name: String },
1423}
1424
1425#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1427#[serde(tag = "category", rename_all = "camelCase")]
1428pub enum AccountResolution {
1429 Signer,
1433
1434 Known { address: String },
1436
1437 PdaRef { pda_name: String },
1439
1440 PdaInline {
1442 seeds: Vec<PdaSeedDef>,
1443 #[serde(default, skip_serializing_if = "Option::is_none")]
1444 program_id: Option<String>,
1445 #[serde(default, skip_serializing_if = "Option::is_none")]
1446 program: Option<PdaProgramDef>,
1447 },
1448
1449 UserProvided,
1451}
1452
1453#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1455pub struct InstructionAccountDef {
1456 pub name: String,
1458
1459 #[serde(default)]
1461 pub is_signer: bool,
1462
1463 #[serde(default)]
1465 pub is_writable: bool,
1466
1467 pub resolution: AccountResolution,
1469
1470 #[serde(default)]
1472 pub is_optional: bool,
1473
1474 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1476 pub docs: Vec<String>,
1477}
1478
1479#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1481#[serde(rename_all = "camelCase")]
1482pub struct InstructionAmountHint {
1483 pub decimals_source: AmountDecimalsSource,
1484}
1485
1486#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1487#[serde(
1488 tag = "kind",
1489 rename_all = "camelCase",
1490 rename_all_fields = "camelCase"
1491)]
1492pub enum AmountDecimalsSource {
1493 ArgMint { arg_name: String },
1494 ArgDecimals { arg_name: String },
1495 KnownAccount { account_name: String },
1496 Constant { decimals: u8 },
1497}
1498
1499#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1501pub struct InstructionArgDef {
1502 pub name: String,
1504
1505 #[serde(rename = "type")]
1507 pub arg_type: String,
1508
1509 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1511 pub docs: Vec<String>,
1512
1513 #[serde(default, skip_serializing_if = "Option::is_none")]
1515 pub amount_hint: Option<InstructionAmountHint>,
1516}
1517
1518#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1520pub struct InstructionDef {
1521 pub name: String,
1523
1524 pub discriminator: Vec<u8>,
1526
1527 #[serde(default = "default_instruction_discriminant_size")]
1529 pub discriminator_size: usize,
1530
1531 pub accounts: Vec<InstructionAccountDef>,
1533
1534 pub args: Vec<InstructionArgDef>,
1536
1537 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1539 pub errors: Vec<IdlErrorSnapshot>,
1540
1541 #[serde(default, skip_serializing_if = "Option::is_none")]
1543 pub program_id: Option<String>,
1544
1545 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1547 pub docs: Vec<String>,
1548}
1549
1550#[derive(Debug, Clone, Serialize, Deserialize)]
1557pub struct SerializableStackSpec {
1558 #[serde(default = "default_ast_version")]
1561 pub ast_version: String,
1562 pub stack_name: String,
1564 #[serde(default)]
1566 pub program_ids: Vec<String>,
1567 #[serde(default)]
1569 pub idls: Vec<IdlSnapshot>,
1570 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1572 pub program_specs: Vec<arete_hash::ProgramSpecV1>,
1573 pub entities: Vec<SerializableStreamSpec>,
1575 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1578 pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1579 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1581 pub instructions: Vec<InstructionDef>,
1582 #[serde(default, skip_serializing_if = "Option::is_none")]
1584 pub content_hash: Option<String>,
1585}
1586
1587impl SerializableStackSpec {
1588 pub fn normalize_event_names(&mut self) {
1590 for entity in &mut self.entities {
1591 entity.normalize_event_names();
1592 }
1593 }
1594
1595 pub fn compute_content_hash(&self) -> String {
1597 use sha2::{Digest, Sha256};
1598 let mut spec_for_hash = self.clone();
1599 spec_for_hash.content_hash = None;
1600 let json = serde_json::to_string(&spec_for_hash)
1601 .expect("Failed to serialize stack spec for hashing");
1602 let mut hasher = Sha256::new();
1603 hasher.update(json.as_bytes());
1604 hex::encode(hasher.finalize())
1605 }
1606
1607 pub fn with_content_hash(mut self) -> Self {
1608 self.content_hash = Some(self.compute_content_hash());
1609 self
1610 }
1611}
1612
1613#[cfg(test)]
1614mod tests {
1615 use super::{
1616 HookAction, IdentitySpec, InstructionHook, MappingSource, PopulationStrategy,
1617 SerializableFieldMapping, SerializableHandlerSpec, SerializableStackSpec,
1618 SerializableStreamSpec, SourceSpec, TypedStreamSpec, CURRENT_AST_VERSION,
1619 };
1620 use serde_json::Value;
1621
1622 fn legacy_stream_spec() -> SerializableStreamSpec {
1623 SerializableStreamSpec {
1624 ast_version: CURRENT_AST_VERSION.to_string(),
1625 state_name: "PumpfunToken".to_string(),
1626 program_id: None,
1627 idl: None,
1628 identity: IdentitySpec {
1629 primary_keys: vec!["id.mint".to_string()],
1630 lookup_indexes: vec![],
1631 },
1632 handlers: vec![SerializableHandlerSpec {
1633 source: SourceSpec::Source {
1634 program_id: None,
1635 discriminator: None,
1636 type_name: "pump::buyIxState".to_string(),
1637 serialization: None,
1638 is_account: false,
1639 },
1640 key_resolution: super::KeyResolutionStrategy::Embedded {
1641 primary_field: super::FieldPath::new(&["accounts", "mint"]),
1642 },
1643 mappings: vec![SerializableFieldMapping {
1644 target_path: "info.last_buy_at".to_string(),
1645 source: MappingSource::Constant(Value::Null),
1646 transform: None,
1647 population: PopulationStrategy::SetOnce,
1648 condition: None,
1649 when: Some("pump::sellIxState".to_string()),
1650 stop: Some("pump::buy_exact_sol_inIxState".to_string()),
1651 emit: true,
1652 }],
1653 conditions: vec![],
1654 emit: true,
1655 }],
1656 sections: vec![],
1657 field_mappings: Default::default(),
1658 resolver_hooks: vec![],
1659 instruction_hooks: vec![InstructionHook {
1660 instruction_type: "pump::buyIxState".to_string(),
1661 actions: vec![HookAction::SetField {
1662 target_field: "info.last_buy_at".to_string(),
1663 source: MappingSource::Constant(Value::Null),
1664 condition: None,
1665 }],
1666 lookup_by: None,
1667 }],
1668 resolver_specs: vec![],
1669 computed_fields: vec![],
1670 computed_field_specs: vec![],
1671 content_hash: None,
1672 views: vec![],
1673 }
1674 }
1675
1676 #[test]
1677 fn typed_stream_spec_from_serializable_normalizes_legacy_instruction_event_names() {
1678 let typed = TypedStreamSpec::<Value>::from_serializable(legacy_stream_spec());
1679
1680 let handler = &typed.handlers[0];
1681 let SourceSpec::Source { type_name, .. } = &handler.source;
1682 assert_eq!(type_name, "pump::BuyIxState");
1683 assert_eq!(
1684 handler.mappings[0].when.as_deref(),
1685 Some("pump::SellIxState")
1686 );
1687 assert_eq!(
1688 handler.mappings[0].stop.as_deref(),
1689 Some("pump::BuyExactSolInIxState")
1690 );
1691 assert_eq!(
1692 typed.instruction_hooks[0].instruction_type,
1693 "pump::BuyIxState"
1694 );
1695 }
1696
1697 #[test]
1698 fn stack_spec_normalize_event_names_updates_all_entities() {
1699 let mut stack = SerializableStackSpec {
1700 ast_version: CURRENT_AST_VERSION.to_string(),
1701 stack_name: "PumpStack".to_string(),
1702 program_ids: vec![],
1703 idls: vec![],
1704 program_specs: vec![],
1705 entities: vec![legacy_stream_spec()],
1706 pdas: Default::default(),
1707 instructions: vec![],
1708 content_hash: None,
1709 };
1710
1711 stack.normalize_event_names();
1712
1713 let SourceSpec::Source { type_name, .. } = &stack.entities[0].handlers[0].source;
1714 assert_eq!(type_name, "pump::BuyIxState");
1715 assert_eq!(
1716 stack.entities[0].instruction_hooks[0].instruction_type,
1717 "pump::BuyIxState"
1718 );
1719 }
1720
1721 #[test]
1722 fn field_type_info_recognizes_128_bit_integers() {
1723 let unsigned = super::FieldTypeInfo::new("amount".to_string(), "u128".to_string());
1724 assert_eq!(unsigned.base_type, super::BaseType::Integer);
1725 assert!(!unsigned.is_optional);
1726 assert_eq!(unsigned.integer_kind, Some(super::IntegerKind::U128));
1727 assert_eq!(unsigned.raw_field_name(), "amount");
1728 assert_eq!(unsigned.canonical_field_name(), "amount");
1729
1730 let signed = super::FieldTypeInfo::new("delta".to_string(), "Option<i128>".to_string());
1731 assert_eq!(signed.base_type, super::BaseType::Integer);
1732 assert!(signed.is_optional);
1733 assert_eq!(signed.integer_kind, Some(super::IntegerKind::I128));
1734 assert_eq!(signed.inner_type.as_deref(), Some("i128"));
1735 }
1736
1737 #[test]
1738 fn field_type_info_preserves_timestamp_integer_kind_and_names() {
1739 let timestamp =
1740 super::FieldTypeInfo::new("last_updated_at".to_string(), "Option<i64>".to_string());
1741
1742 assert_eq!(timestamp.base_type, super::BaseType::Timestamp);
1743 assert_eq!(timestamp.integer_kind, Some(super::IntegerKind::I64));
1744 assert!(timestamp.is_optional);
1745 assert_eq!(timestamp.raw_field_name(), "last_updated_at");
1746 assert_eq!(timestamp.canonical_field_name(), "lastUpdatedAt");
1747 }
1748}
1749
1750#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1756#[serde(rename_all = "lowercase")]
1757pub enum SortOrder {
1758 #[default]
1759 Asc,
1760 Desc,
1761}
1762
1763#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1765pub enum CompareOp {
1766 Eq,
1767 Ne,
1768 Gt,
1769 Gte,
1770 Lt,
1771 Lte,
1772}
1773
1774#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1776pub enum PredicateValue {
1777 Literal(serde_json::Value),
1779 Dynamic(String),
1781 Field(FieldPath),
1783}
1784
1785#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1787pub enum Predicate {
1788 Compare {
1790 field: FieldPath,
1791 op: CompareOp,
1792 value: PredicateValue,
1793 },
1794 And(Vec<Predicate>),
1796 Or(Vec<Predicate>),
1798 Not(Box<Predicate>),
1800 Exists { field: FieldPath },
1802}
1803
1804#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1806pub enum ViewTransform {
1807 Filter { predicate: Predicate },
1809
1810 Sort {
1812 key: FieldPath,
1813 #[serde(default)]
1814 order: SortOrder,
1815 },
1816
1817 Take { count: usize },
1819
1820 Skip { count: usize },
1822
1823 First,
1825
1826 Last,
1828
1829 MaxBy { key: FieldPath },
1831
1832 MinBy { key: FieldPath },
1834}
1835
1836#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1838pub enum ViewSource {
1839 Entity { name: String },
1841 View { id: String },
1843}
1844
1845#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1847pub enum ViewOutput {
1848 #[default]
1850 Collection,
1851 Single,
1853 Keyed { key_field: FieldPath },
1855}
1856
1857#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1859pub struct ViewDef {
1860 pub id: String,
1862
1863 pub source: ViewSource,
1865
1866 #[serde(default)]
1868 pub pipeline: Vec<ViewTransform>,
1869
1870 #[serde(default)]
1872 pub output: ViewOutput,
1873}
1874
1875impl ViewDef {
1876 pub fn list(entity_name: &str) -> Self {
1878 ViewDef {
1879 id: format!("{}/list", entity_name),
1880 source: ViewSource::Entity {
1881 name: entity_name.to_string(),
1882 },
1883 pipeline: vec![],
1884 output: ViewOutput::Collection,
1885 }
1886 }
1887
1888 pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1890 ViewDef {
1891 id: format!("{}/state", entity_name),
1892 source: ViewSource::Entity {
1893 name: entity_name.to_string(),
1894 },
1895 pipeline: vec![],
1896 output: ViewOutput::Keyed {
1897 key_field: FieldPath::new(key_field),
1898 },
1899 }
1900 }
1901
1902 pub fn is_single(&self) -> bool {
1904 matches!(self.output, ViewOutput::Single)
1905 }
1906
1907 pub fn has_single_transform(&self) -> bool {
1909 self.pipeline.iter().any(|t| {
1910 matches!(
1911 t,
1912 ViewTransform::First
1913 | ViewTransform::Last
1914 | ViewTransform::MaxBy { .. }
1915 | ViewTransform::MinBy { .. }
1916 )
1917 })
1918 }
1919}
1920
1921#[macro_export]
1922macro_rules! define_accessor {
1923 ($name:ident, $state:ty, $path:expr) => {
1924 pub struct $name;
1925
1926 impl $crate::ast::FieldAccessor<$state> for $name {
1927 fn path(&self) -> String {
1928 $path.to_string()
1929 }
1930 }
1931 };
1932}