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}
963
964impl MappingSource {
965 pub fn with_transform(self, transform: Transformation) -> Self {
966 match self {
967 MappingSource::FromSource {
968 path,
969 default,
970 transform: _,
971 } => MappingSource::FromSource {
972 path,
973 default,
974 transform: Some(transform),
975 },
976 other => other,
977 }
978 }
979}
980
981#[derive(Debug, Clone, Serialize, Deserialize)]
982pub enum ComputeFunction {
983 Sum,
984 Concat,
985 Format(String),
986 Custom(String),
987}
988
989#[derive(Debug, Clone, Serialize, Deserialize)]
990pub struct Condition {
991 pub field: FieldPath,
992 pub operator: ConditionOp,
993 pub value: Value,
994}
995
996#[derive(Debug, Clone, Serialize, Deserialize)]
997pub enum ConditionOp {
998 Equals,
999 NotEquals,
1000 GreaterThan,
1001 LessThan,
1002 Contains,
1003 Exists,
1004}
1005
1006#[derive(Debug, Clone, Serialize, Deserialize)]
1008pub struct FieldTypeInfo {
1009 pub field_name: String,
1010 #[serde(default, skip_serializing_if = "Option::is_none")]
1011 pub raw_name: Option<String>,
1012 #[serde(default, skip_serializing_if = "Option::is_none")]
1013 pub canonical_name: Option<String>,
1014 pub rust_type_name: String, pub base_type: BaseType, #[serde(default, skip_serializing_if = "Option::is_none")]
1017 pub integer_kind: Option<IntegerKind>,
1018 pub is_optional: bool, pub is_array: bool, pub inner_type: Option<String>, pub source_path: Option<String>, #[serde(default)]
1024 pub resolved_type: Option<ResolvedStructType>,
1025 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
1026 pub emit: bool,
1027}
1028
1029#[derive(Debug, Clone, Serialize, Deserialize)]
1031pub struct ResolvedStructType {
1032 pub type_name: String,
1033 pub fields: Vec<ResolvedField>,
1034 pub is_instruction: bool,
1035 pub is_account: bool,
1036 pub is_event: bool,
1037 #[serde(default)]
1039 pub is_enum: bool,
1040 #[serde(default)]
1042 pub enum_variants: Vec<String>,
1043}
1044
1045#[derive(Debug, Clone, Serialize, Deserialize)]
1047pub struct ResolvedField {
1048 pub field_name: String,
1049 #[serde(default, skip_serializing_if = "Option::is_none")]
1050 pub raw_name: Option<String>,
1051 #[serde(default, skip_serializing_if = "Option::is_none")]
1052 pub canonical_name: Option<String>,
1053 pub field_type: String,
1054 pub base_type: BaseType,
1055 #[serde(default, skip_serializing_if = "Option::is_none")]
1056 pub integer_kind: Option<IntegerKind>,
1057 pub is_optional: bool,
1058 pub is_array: bool,
1059}
1060
1061#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1062pub enum IntegerKind {
1063 U8,
1064 U16,
1065 U32,
1066 U64,
1067 U128,
1068 Usize,
1069 I8,
1070 I16,
1071 I32,
1072 I64,
1073 I128,
1074 Isize,
1075}
1076
1077impl IntegerKind {
1078 pub fn from_rust_type(type_str: &str) -> Option<Self> {
1079 match type_str.trim() {
1080 "u8" => Some(Self::U8),
1081 "u16" => Some(Self::U16),
1082 "u32" => Some(Self::U32),
1083 "u64" => Some(Self::U64),
1084 "u128" => Some(Self::U128),
1085 "usize" => Some(Self::Usize),
1086 "i8" => Some(Self::I8),
1087 "i16" => Some(Self::I16),
1088 "i32" => Some(Self::I32),
1089 "i64" => Some(Self::I64),
1090 "i128" => Some(Self::I128),
1091 "isize" => Some(Self::Isize),
1092 _ => None,
1093 }
1094 }
1095
1096 pub fn is_bigint(self) -> bool {
1097 matches!(self, Self::U64 | Self::U128 | Self::I64 | Self::I128)
1098 }
1099}
1100
1101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1103pub enum BaseType {
1104 Integer, Float, String, Boolean, Object, Array, Binary, Timestamp, Pubkey, Any, }
1120
1121#[derive(Debug, Clone, Serialize, Deserialize)]
1123pub struct EntitySection {
1124 pub name: String,
1125 pub fields: Vec<FieldTypeInfo>,
1126 pub is_nested_struct: bool,
1127 pub parent_field: Option<String>, }
1129
1130impl FieldTypeInfo {
1131 pub fn new(field_name: String, rust_type_name: String) -> Self {
1132 let (base_type, integer_kind, is_optional, is_array, inner_type) =
1133 Self::analyze_rust_type(&rust_type_name);
1134 let canonical_name = to_camel_case_owned(&field_name);
1135
1136 FieldTypeInfo {
1137 field_name: field_name.clone(),
1138 raw_name: Some(field_name.clone()),
1139 canonical_name: Some(canonical_name),
1140 rust_type_name,
1141 base_type: Self::infer_semantic_type(&field_name, base_type),
1142 integer_kind,
1143 is_optional,
1144 is_array,
1145 inner_type,
1146 source_path: None,
1147 resolved_type: None,
1148 emit: true,
1149 }
1150 }
1151
1152 pub fn with_source_path(mut self, source_path: String) -> Self {
1153 self.source_path = Some(source_path);
1154 self
1155 }
1156
1157 pub fn raw_field_name(&self) -> &str {
1158 self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1159 }
1160
1161 pub fn canonical_field_name(&self) -> String {
1162 self.canonical_name
1163 .clone()
1164 .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1165 }
1166
1167 pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1168 self.integer_kind.or_else(|| {
1169 IntegerKind::from_rust_type(
1170 self.inner_type
1171 .as_deref()
1172 .unwrap_or(self.rust_type_name.as_str()),
1173 )
1174 })
1175 }
1176
1177 fn analyze_rust_type(
1179 rust_type: &str,
1180 ) -> (BaseType, Option<IntegerKind>, bool, bool, Option<String>) {
1181 let type_str = rust_type.trim();
1182
1183 if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1185 let (inner_base_type, inner_integer_kind, _, inner_is_array, inner_inner_type) =
1186 Self::analyze_rust_type(&inner);
1187 return (
1188 inner_base_type,
1189 inner_integer_kind,
1190 true,
1191 inner_is_array,
1192 inner_inner_type.or(Some(inner)),
1193 );
1194 }
1195
1196 if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1198 let (_inner_base_type, inner_integer_kind, inner_is_optional, _, inner_inner_type) =
1199 Self::analyze_rust_type(&inner);
1200 return (
1201 BaseType::Array,
1202 inner_integer_kind,
1203 inner_is_optional,
1204 true,
1205 inner_inner_type.or(Some(inner)),
1206 );
1207 }
1208
1209 let integer_kind = IntegerKind::from_rust_type(type_str);
1211 let base_type = match integer_kind {
1212 Some(_) => BaseType::Integer,
1213 None => match type_str {
1214 "f32" | "f64" => BaseType::Float,
1215 "bool" => BaseType::Boolean,
1216 "String" | "&str" | "str" => BaseType::String,
1217 "Value" | "serde_json::Value" => BaseType::Any,
1218 "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1219 _ => {
1220 if type_str.contains("Bytes") || type_str.contains("bytes") {
1222 BaseType::Binary
1223 } else if type_str.contains("Pubkey") {
1224 BaseType::Pubkey
1225 } else {
1226 BaseType::Object
1227 }
1228 }
1229 },
1230 };
1231
1232 (base_type, integer_kind, false, false, None)
1233 }
1234
1235 fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1237 let pattern = format!("{}<", generic_name);
1238 if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1239 let start = pattern.len();
1240 let end = type_str.len() - 1;
1241 if end > start {
1242 return Some(type_str[start..end].trim().to_string());
1243 }
1244 }
1245 None
1246 }
1247
1248 fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1250 let lower_name = field_name.to_lowercase();
1251
1252 if base_type == BaseType::Integer
1254 && (lower_name.ends_with("_at")
1255 || lower_name.ends_with("_time")
1256 || lower_name.contains("timestamp")
1257 || lower_name.contains("created")
1258 || lower_name.contains("settled")
1259 || lower_name.contains("activated"))
1260 {
1261 return BaseType::Timestamp;
1262 }
1263
1264 base_type
1265 }
1266}
1267
1268impl ResolvedField {
1269 pub fn raw_field_name(&self) -> &str {
1270 self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1271 }
1272
1273 pub fn canonical_field_name(&self) -> String {
1274 self.canonical_name
1275 .clone()
1276 .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1277 }
1278
1279 pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1280 self.integer_kind
1281 .or_else(|| IntegerKind::from_rust_type(self.field_type.as_str()))
1282 }
1283}
1284
1285fn to_camel_case_owned(s: &str) -> String {
1286 let mut result = String::new();
1287 let mut uppercase_next = false;
1288
1289 for ch in s.chars() {
1290 if matches!(ch, '_' | '-' | '.') {
1291 uppercase_next = true;
1292 continue;
1293 }
1294
1295 if result.is_empty() {
1296 result.extend(ch.to_lowercase());
1297 continue;
1298 }
1299
1300 if uppercase_next {
1301 result.extend(ch.to_uppercase());
1302 uppercase_next = false;
1303 } else {
1304 result.push(ch);
1305 }
1306 }
1307
1308 result
1309}
1310
1311pub trait FieldAccessor<S> {
1312 fn path(&self) -> String;
1313}
1314
1315impl SerializableStreamSpec {
1320 pub fn compute_content_hash(&self) -> String {
1326 use sha2::{Digest, Sha256};
1327
1328 let mut spec_for_hash = self.clone();
1330 spec_for_hash.content_hash = None;
1331
1332 let json =
1334 serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1335
1336 let mut hasher = Sha256::new();
1338 hasher.update(json.as_bytes());
1339 let result = hasher.finalize();
1340
1341 hex::encode(result)
1343 }
1344
1345 pub fn verify_content_hash(&self) -> bool {
1348 match &self.content_hash {
1349 Some(hash) => {
1350 let computed = self.compute_content_hash();
1351 hash == &computed
1352 }
1353 None => true, }
1355 }
1356
1357 pub fn with_content_hash(mut self) -> Self {
1359 self.content_hash = Some(self.compute_content_hash());
1360 self
1361 }
1362}
1363
1364#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1371pub struct PdaDefinition {
1372 pub name: String,
1374
1375 pub seeds: Vec<PdaSeedDef>,
1377
1378 #[serde(default, skip_serializing_if = "Option::is_none")]
1381 pub program_id: Option<String>,
1382
1383 #[serde(default, skip_serializing_if = "Option::is_none")]
1386 pub program: Option<PdaProgramDef>,
1387}
1388
1389#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1390#[serde(tag = "type", rename_all = "camelCase")]
1391pub enum PdaProgramDef {
1392 AccountRef { account_name: String },
1393 ArgRef { arg_name: String },
1394}
1395
1396#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1398#[serde(tag = "type", rename_all = "camelCase")]
1399pub enum PdaSeedDef {
1400 Literal { value: String },
1402
1403 Bytes { value: Vec<u8> },
1405
1406 ArgRef {
1408 arg_name: String,
1409 #[serde(default, skip_serializing_if = "Option::is_none")]
1411 arg_type: Option<String>,
1412 },
1413
1414 AccountRef { account_name: String },
1416}
1417
1418#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1420#[serde(tag = "category", rename_all = "camelCase")]
1421pub enum AccountResolution {
1422 Signer,
1426
1427 Known { address: String },
1429
1430 PdaRef { pda_name: String },
1432
1433 PdaInline {
1435 seeds: Vec<PdaSeedDef>,
1436 #[serde(default, skip_serializing_if = "Option::is_none")]
1437 program_id: Option<String>,
1438 #[serde(default, skip_serializing_if = "Option::is_none")]
1439 program: Option<PdaProgramDef>,
1440 },
1441
1442 UserProvided,
1444}
1445
1446#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1448pub struct InstructionAccountDef {
1449 pub name: String,
1451
1452 #[serde(default)]
1454 pub is_signer: bool,
1455
1456 #[serde(default)]
1458 pub is_writable: bool,
1459
1460 pub resolution: AccountResolution,
1462
1463 #[serde(default)]
1465 pub is_optional: bool,
1466
1467 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1469 pub docs: Vec<String>,
1470}
1471
1472#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1474#[serde(rename_all = "camelCase")]
1475pub struct InstructionAmountHint {
1476 pub decimals_source: AmountDecimalsSource,
1477}
1478
1479#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1480#[serde(
1481 tag = "kind",
1482 rename_all = "camelCase",
1483 rename_all_fields = "camelCase"
1484)]
1485pub enum AmountDecimalsSource {
1486 ArgMint { arg_name: String },
1487 ArgDecimals { arg_name: String },
1488 KnownAccount { account_name: String },
1489 Constant { decimals: u8 },
1490}
1491
1492#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1494pub struct InstructionArgDef {
1495 pub name: String,
1497
1498 #[serde(rename = "type")]
1500 pub arg_type: String,
1501
1502 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1504 pub docs: Vec<String>,
1505
1506 #[serde(default, skip_serializing_if = "Option::is_none")]
1508 pub amount_hint: Option<InstructionAmountHint>,
1509}
1510
1511#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1513pub struct InstructionDef {
1514 pub name: String,
1516
1517 pub discriminator: Vec<u8>,
1519
1520 #[serde(default = "default_instruction_discriminant_size")]
1522 pub discriminator_size: usize,
1523
1524 pub accounts: Vec<InstructionAccountDef>,
1526
1527 pub args: Vec<InstructionArgDef>,
1529
1530 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1532 pub errors: Vec<IdlErrorSnapshot>,
1533
1534 #[serde(default, skip_serializing_if = "Option::is_none")]
1536 pub program_id: Option<String>,
1537
1538 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1540 pub docs: Vec<String>,
1541}
1542
1543#[derive(Debug, Clone, Serialize, Deserialize)]
1550pub struct SerializableStackSpec {
1551 #[serde(default = "default_ast_version")]
1554 pub ast_version: String,
1555 pub stack_name: String,
1557 #[serde(default)]
1559 pub program_ids: Vec<String>,
1560 #[serde(default)]
1562 pub idls: Vec<IdlSnapshot>,
1563 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1565 pub program_specs: Vec<arete_hash::ProgramSpecV1>,
1566 pub entities: Vec<SerializableStreamSpec>,
1568 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1571 pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1572 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1574 pub instructions: Vec<InstructionDef>,
1575 #[serde(default, skip_serializing_if = "Option::is_none")]
1577 pub content_hash: Option<String>,
1578}
1579
1580impl SerializableStackSpec {
1581 pub fn normalize_event_names(&mut self) {
1583 for entity in &mut self.entities {
1584 entity.normalize_event_names();
1585 }
1586 }
1587
1588 pub fn compute_content_hash(&self) -> String {
1590 use sha2::{Digest, Sha256};
1591 let mut spec_for_hash = self.clone();
1592 spec_for_hash.content_hash = None;
1593 let json = serde_json::to_string(&spec_for_hash)
1594 .expect("Failed to serialize stack spec for hashing");
1595 let mut hasher = Sha256::new();
1596 hasher.update(json.as_bytes());
1597 hex::encode(hasher.finalize())
1598 }
1599
1600 pub fn with_content_hash(mut self) -> Self {
1601 self.content_hash = Some(self.compute_content_hash());
1602 self
1603 }
1604}
1605
1606#[cfg(test)]
1607mod tests {
1608 use super::{
1609 HookAction, IdentitySpec, InstructionHook, MappingSource, PopulationStrategy,
1610 SerializableFieldMapping, SerializableHandlerSpec, SerializableStackSpec,
1611 SerializableStreamSpec, SourceSpec, TypedStreamSpec, CURRENT_AST_VERSION,
1612 };
1613 use serde_json::Value;
1614
1615 fn legacy_stream_spec() -> SerializableStreamSpec {
1616 SerializableStreamSpec {
1617 ast_version: CURRENT_AST_VERSION.to_string(),
1618 state_name: "PumpfunToken".to_string(),
1619 program_id: None,
1620 idl: None,
1621 identity: IdentitySpec {
1622 primary_keys: vec!["id.mint".to_string()],
1623 lookup_indexes: vec![],
1624 },
1625 handlers: vec![SerializableHandlerSpec {
1626 source: SourceSpec::Source {
1627 program_id: None,
1628 discriminator: None,
1629 type_name: "pump::buyIxState".to_string(),
1630 serialization: None,
1631 is_account: false,
1632 },
1633 key_resolution: super::KeyResolutionStrategy::Embedded {
1634 primary_field: super::FieldPath::new(&["accounts", "mint"]),
1635 },
1636 mappings: vec![SerializableFieldMapping {
1637 target_path: "info.last_buy_at".to_string(),
1638 source: MappingSource::Constant(Value::Null),
1639 transform: None,
1640 population: PopulationStrategy::SetOnce,
1641 condition: None,
1642 when: Some("pump::sellIxState".to_string()),
1643 stop: Some("pump::buy_exact_sol_inIxState".to_string()),
1644 emit: true,
1645 }],
1646 conditions: vec![],
1647 emit: true,
1648 }],
1649 sections: vec![],
1650 field_mappings: Default::default(),
1651 resolver_hooks: vec![],
1652 instruction_hooks: vec![InstructionHook {
1653 instruction_type: "pump::buyIxState".to_string(),
1654 actions: vec![HookAction::SetField {
1655 target_field: "info.last_buy_at".to_string(),
1656 source: MappingSource::Constant(Value::Null),
1657 condition: None,
1658 }],
1659 lookup_by: None,
1660 }],
1661 resolver_specs: vec![],
1662 computed_fields: vec![],
1663 computed_field_specs: vec![],
1664 content_hash: None,
1665 views: vec![],
1666 }
1667 }
1668
1669 #[test]
1670 fn typed_stream_spec_from_serializable_normalizes_legacy_instruction_event_names() {
1671 let typed = TypedStreamSpec::<Value>::from_serializable(legacy_stream_spec());
1672
1673 let handler = &typed.handlers[0];
1674 let SourceSpec::Source { type_name, .. } = &handler.source;
1675 assert_eq!(type_name, "pump::BuyIxState");
1676 assert_eq!(
1677 handler.mappings[0].when.as_deref(),
1678 Some("pump::SellIxState")
1679 );
1680 assert_eq!(
1681 handler.mappings[0].stop.as_deref(),
1682 Some("pump::BuyExactSolInIxState")
1683 );
1684 assert_eq!(
1685 typed.instruction_hooks[0].instruction_type,
1686 "pump::BuyIxState"
1687 );
1688 }
1689
1690 #[test]
1691 fn stack_spec_normalize_event_names_updates_all_entities() {
1692 let mut stack = SerializableStackSpec {
1693 ast_version: CURRENT_AST_VERSION.to_string(),
1694 stack_name: "PumpStack".to_string(),
1695 program_ids: vec![],
1696 idls: vec![],
1697 program_specs: vec![],
1698 entities: vec![legacy_stream_spec()],
1699 pdas: Default::default(),
1700 instructions: vec![],
1701 content_hash: None,
1702 };
1703
1704 stack.normalize_event_names();
1705
1706 let SourceSpec::Source { type_name, .. } = &stack.entities[0].handlers[0].source;
1707 assert_eq!(type_name, "pump::BuyIxState");
1708 assert_eq!(
1709 stack.entities[0].instruction_hooks[0].instruction_type,
1710 "pump::BuyIxState"
1711 );
1712 }
1713
1714 #[test]
1715 fn field_type_info_recognizes_128_bit_integers() {
1716 let unsigned = super::FieldTypeInfo::new("amount".to_string(), "u128".to_string());
1717 assert_eq!(unsigned.base_type, super::BaseType::Integer);
1718 assert!(!unsigned.is_optional);
1719 assert_eq!(unsigned.integer_kind, Some(super::IntegerKind::U128));
1720 assert_eq!(unsigned.raw_field_name(), "amount");
1721 assert_eq!(unsigned.canonical_field_name(), "amount");
1722
1723 let signed = super::FieldTypeInfo::new("delta".to_string(), "Option<i128>".to_string());
1724 assert_eq!(signed.base_type, super::BaseType::Integer);
1725 assert!(signed.is_optional);
1726 assert_eq!(signed.integer_kind, Some(super::IntegerKind::I128));
1727 assert_eq!(signed.inner_type.as_deref(), Some("i128"));
1728 }
1729
1730 #[test]
1731 fn field_type_info_preserves_timestamp_integer_kind_and_names() {
1732 let timestamp =
1733 super::FieldTypeInfo::new("last_updated_at".to_string(), "Option<i64>".to_string());
1734
1735 assert_eq!(timestamp.base_type, super::BaseType::Timestamp);
1736 assert_eq!(timestamp.integer_kind, Some(super::IntegerKind::I64));
1737 assert!(timestamp.is_optional);
1738 assert_eq!(timestamp.raw_field_name(), "last_updated_at");
1739 assert_eq!(timestamp.canonical_field_name(), "lastUpdatedAt");
1740 }
1741}
1742
1743#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1749#[serde(rename_all = "lowercase")]
1750pub enum SortOrder {
1751 #[default]
1752 Asc,
1753 Desc,
1754}
1755
1756#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1758pub enum CompareOp {
1759 Eq,
1760 Ne,
1761 Gt,
1762 Gte,
1763 Lt,
1764 Lte,
1765}
1766
1767#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1769pub enum PredicateValue {
1770 Literal(serde_json::Value),
1772 Dynamic(String),
1774 Field(FieldPath),
1776}
1777
1778#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1780pub enum Predicate {
1781 Compare {
1783 field: FieldPath,
1784 op: CompareOp,
1785 value: PredicateValue,
1786 },
1787 And(Vec<Predicate>),
1789 Or(Vec<Predicate>),
1791 Not(Box<Predicate>),
1793 Exists { field: FieldPath },
1795}
1796
1797#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1799pub enum ViewTransform {
1800 Filter { predicate: Predicate },
1802
1803 Sort {
1805 key: FieldPath,
1806 #[serde(default)]
1807 order: SortOrder,
1808 },
1809
1810 Take { count: usize },
1812
1813 Skip { count: usize },
1815
1816 First,
1818
1819 Last,
1821
1822 MaxBy { key: FieldPath },
1824
1825 MinBy { key: FieldPath },
1827}
1828
1829#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1831pub enum ViewSource {
1832 Entity { name: String },
1834 View { id: String },
1836}
1837
1838#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1840pub enum ViewOutput {
1841 #[default]
1843 Collection,
1844 Single,
1846 Keyed { key_field: FieldPath },
1848}
1849
1850#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1852pub struct ViewDef {
1853 pub id: String,
1855
1856 pub source: ViewSource,
1858
1859 #[serde(default)]
1861 pub pipeline: Vec<ViewTransform>,
1862
1863 #[serde(default)]
1865 pub output: ViewOutput,
1866}
1867
1868impl ViewDef {
1869 pub fn list(entity_name: &str) -> Self {
1871 ViewDef {
1872 id: format!("{}/list", entity_name),
1873 source: ViewSource::Entity {
1874 name: entity_name.to_string(),
1875 },
1876 pipeline: vec![],
1877 output: ViewOutput::Collection,
1878 }
1879 }
1880
1881 pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1883 ViewDef {
1884 id: format!("{}/state", entity_name),
1885 source: ViewSource::Entity {
1886 name: entity_name.to_string(),
1887 },
1888 pipeline: vec![],
1889 output: ViewOutput::Keyed {
1890 key_field: FieldPath::new(key_field),
1891 },
1892 }
1893 }
1894
1895 pub fn is_single(&self) -> bool {
1897 matches!(self.output, ViewOutput::Single)
1898 }
1899
1900 pub fn has_single_transform(&self) -> bool {
1902 self.pipeline.iter().any(|t| {
1903 matches!(
1904 t,
1905 ViewTransform::First
1906 | ViewTransform::Last
1907 | ViewTransform::MaxBy { .. }
1908 | ViewTransform::MinBy { .. }
1909 )
1910 })
1911 }
1912}
1913
1914#[macro_export]
1915macro_rules! define_accessor {
1916 ($name:ident, $state:ty, $path:expr) => {
1917 pub struct $name;
1918
1919 impl $crate::ast::FieldAccessor<$state> for $name {
1920 fn path(&self) -> String {
1921 $path.to_string()
1922 }
1923 }
1924 };
1925}