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::String(string) if string.is_u64_prefixed() => "StringU64Len".to_string(),
61 IdlTypeSnapshot::String(_) => "String".to_string(),
62 IdlTypeSnapshot::HashMap(map) => {
63 let key_type = idl_type_snapshot_to_rust_string(&map.hash_map.0);
64 let val_type = idl_type_snapshot_to_rust_string(&map.hash_map.1);
65 format!("std::collections::HashMap<{}, {}>", key_type, val_type)
66 }
67 IdlTypeSnapshot::Tuple(tuple) => format!(
68 "({})",
69 tuple
70 .tuple
71 .iter()
72 .map(idl_type_snapshot_to_rust_string)
73 .collect::<Vec<_>>()
74 .join(", ")
75 ),
76 IdlTypeSnapshot::Defined(def) => match &def.defined {
77 IdlDefinedInnerSnapshot::Named { name } => name.clone(),
78 IdlDefinedInnerSnapshot::Simple(s) => s.clone(),
79 },
80 }
81}
82
83fn map_simple_idl_type(idl_type: &str) -> String {
84 match idl_type {
85 "u8" => "u8".to_string(),
86 "u16" => "u16".to_string(),
87 "u32" => "u32".to_string(),
88 "u64" => "u64".to_string(),
89 "u128" => "u128".to_string(),
90 "i8" => "i8".to_string(),
91 "i16" => "i16".to_string(),
92 "i32" => "i32".to_string(),
93 "i64" => "i64".to_string(),
94 "i128" => "i128".to_string(),
95 "bool" => "bool".to_string(),
96 "string" => "String".to_string(),
97 "publicKey" | "pubkey" => "solana_pubkey::Pubkey".to_string(),
98 "bytes" => "Vec<u8>".to_string(),
99 _ => idl_type.to_string(),
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104pub struct FieldPath {
105 pub segments: Vec<String>,
106 pub offsets: Option<Vec<usize>>,
107}
108
109impl FieldPath {
110 pub fn new(segments: &[&str]) -> Self {
111 FieldPath {
112 segments: segments.iter().map(|s| s.to_string()).collect(),
113 offsets: None,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
119pub enum Transformation {
120 HexEncode,
121 HexDecode,
122 Base58Encode,
123 Base58Decode,
124 ToString,
125 ToNumber,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub enum PopulationStrategy {
130 SetOnce,
131 LastWrite,
132 Append,
133 Merge,
134 Max,
135 Sum,
137 Count,
139 Min,
141 UniqueCount,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct ComputedFieldSpec {
153 pub target_path: String,
155 pub expression: ComputedExpr,
157 pub result_type: String,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
166#[serde(rename_all = "lowercase")]
167pub enum ResolverType {
168 Token,
169 Url(UrlResolverConfig),
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
173#[serde(rename_all = "lowercase")]
174pub enum HttpMethod {
175 #[default]
176 Get,
177 Post,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
181pub enum UrlTemplatePart {
182 Literal(String),
183 FieldRef(String),
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
187pub enum UrlSource {
188 FieldPath(String),
189 Template(Vec<UrlTemplatePart>),
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
193pub struct UrlResolverConfig {
194 pub url_source: UrlSource,
195 #[serde(default)]
196 pub method: HttpMethod,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub extract_path: Option<String>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
202pub struct ResolverExtractSpec {
203 pub target_path: String,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub source_path: Option<String>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub transform: Option<Transformation>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
211pub enum ResolveStrategy {
212 #[default]
213 SetOnce,
214 LastWrite,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
218pub struct ResolverCondition {
219 pub field_path: String,
220 pub op: ComparisonOp,
221 pub value: Value,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct ResolverSpec {
226 pub resolver: ResolverType,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub input_path: Option<String>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub input_value: Option<Value>,
231 #[serde(default)]
232 pub strategy: ResolveStrategy,
233 pub extracts: Vec<ResolverExtractSpec>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub condition: Option<ResolverCondition>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub schedule_at: Option<String>,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
250pub enum ComputedExpr {
251 FieldRef {
254 path: String,
255 },
256
257 UnwrapOr {
259 expr: Box<ComputedExpr>,
260 default: serde_json::Value,
261 },
262
263 Binary {
265 op: BinaryOp,
266 left: Box<ComputedExpr>,
267 right: Box<ComputedExpr>,
268 },
269
270 Cast {
272 expr: Box<ComputedExpr>,
273 to_type: String,
274 },
275
276 MethodCall {
278 expr: Box<ComputedExpr>,
279 method: String,
280 args: Vec<ComputedExpr>,
281 },
282
283 ResolverComputed {
285 resolver: String,
286 method: String,
287 args: Vec<ComputedExpr>,
288 },
289
290 Literal {
292 value: serde_json::Value,
293 },
294
295 Paren {
297 expr: Box<ComputedExpr>,
298 },
299
300 Var {
302 name: String,
303 },
304
305 Let {
307 name: String,
308 value: Box<ComputedExpr>,
309 body: Box<ComputedExpr>,
310 },
311
312 If {
314 condition: Box<ComputedExpr>,
315 then_branch: Box<ComputedExpr>,
316 else_branch: Box<ComputedExpr>,
317 },
318
319 None,
321 Some {
322 value: Box<ComputedExpr>,
323 },
324
325 Slice {
327 expr: Box<ComputedExpr>,
328 start: usize,
329 end: usize,
330 },
331 Index {
332 expr: Box<ComputedExpr>,
333 index: usize,
334 },
335
336 U64FromLeBytes {
338 bytes: Box<ComputedExpr>,
339 },
340 U64FromBeBytes {
341 bytes: Box<ComputedExpr>,
342 },
343
344 ByteArray {
346 bytes: Vec<u8>,
347 },
348
349 Closure {
351 param: String,
352 body: Box<ComputedExpr>,
353 },
354
355 Unary {
357 op: UnaryOp,
358 expr: Box<ComputedExpr>,
359 },
360
361 JsonToBytes {
363 expr: Box<ComputedExpr>,
364 },
365
366 ContextSlot,
369 ContextTimestamp,
371
372 Keccak256 {
375 expr: Box<ComputedExpr>,
376 },
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381pub enum BinaryOp {
382 Add,
384 Sub,
385 Mul,
386 Div,
387 Mod,
388 Gt,
390 Lt,
391 Gte,
392 Lte,
393 Eq,
394 Ne,
395 And,
397 Or,
398 Xor,
400 BitAnd,
401 BitOr,
402 Shl,
403 Shr,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
408pub enum UnaryOp {
409 Not,
410 ReverseBits,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct SerializableStreamSpec {
416 #[serde(default = "default_ast_version")]
419 pub ast_version: String,
420 pub state_name: String,
421 #[serde(default)]
423 pub program_id: Option<String>,
424 #[serde(default)]
426 pub idl: Option<IdlSnapshot>,
427 pub identity: IdentitySpec,
428 pub handlers: Vec<SerializableHandlerSpec>,
429 pub sections: Vec<EntitySection>,
430 pub field_mappings: BTreeMap<String, FieldTypeInfo>,
431 pub resolver_hooks: Vec<ResolverHook>,
432 pub instruction_hooks: Vec<InstructionHook>,
433 #[serde(default)]
434 pub resolver_specs: Vec<ResolverSpec>,
435 #[serde(default)]
437 pub computed_fields: Vec<String>,
438 #[serde(default)]
440 pub computed_field_specs: Vec<ComputedFieldSpec>,
441 #[serde(default, skip_serializing_if = "Option::is_none")]
444 pub content_hash: Option<String>,
445 #[serde(default)]
447 pub views: Vec<ViewDef>,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub scope_field: Option<String>,
454}
455
456impl SerializableStreamSpec {
457 pub fn normalize_event_names(&mut self) {
459 for handler in &mut self.handlers {
460 handler.normalize_event_names();
461 }
462
463 for hook in &mut self.instruction_hooks {
464 hook.instruction_type =
465 crate::event_type_helpers::canonicalize_event_type_name(&hook.instruction_type);
466 }
467 }
468}
469
470#[derive(Debug, Clone)]
471pub struct TypedStreamSpec<S> {
472 pub state_name: String,
473 pub identity: IdentitySpec,
474 pub handlers: Vec<TypedHandlerSpec<S>>,
475 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>,
480 pub computed_fields: Vec<String>, _phantom: PhantomData<S>,
482}
483
484impl<S> TypedStreamSpec<S> {
485 pub fn new(
486 state_name: String,
487 identity: IdentitySpec,
488 handlers: Vec<TypedHandlerSpec<S>>,
489 ) -> Self {
490 TypedStreamSpec {
491 state_name,
492 identity,
493 handlers,
494 sections: Vec::new(),
495 field_mappings: BTreeMap::new(),
496 resolver_hooks: Vec::new(),
497 instruction_hooks: Vec::new(),
498 resolver_specs: Vec::new(),
499 computed_fields: Vec::new(),
500 _phantom: PhantomData,
501 }
502 }
503
504 pub fn with_type_info(
506 state_name: String,
507 identity: IdentitySpec,
508 handlers: Vec<TypedHandlerSpec<S>>,
509 sections: Vec<EntitySection>,
510 field_mappings: BTreeMap<String, FieldTypeInfo>,
511 ) -> Self {
512 TypedStreamSpec {
513 state_name,
514 identity,
515 handlers,
516 sections,
517 field_mappings,
518 resolver_hooks: Vec::new(),
519 instruction_hooks: Vec::new(),
520 resolver_specs: Vec::new(),
521 computed_fields: Vec::new(),
522 _phantom: PhantomData,
523 }
524 }
525
526 pub fn with_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
527 self.resolver_specs = resolver_specs;
528 self
529 }
530
531 pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
533 self.field_mappings.get(path)
534 }
535
536 pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
538 self.sections
539 .iter()
540 .find(|s| s.name == section_name)
541 .map(|s| &s.fields)
542 }
543
544 pub fn get_section_names(&self) -> Vec<&String> {
546 self.sections.iter().map(|s| &s.name).collect()
547 }
548
549 pub fn to_serializable(&self) -> SerializableStreamSpec {
551 let mut spec = SerializableStreamSpec {
552 ast_version: CURRENT_AST_VERSION.to_string(),
553 state_name: self.state_name.clone(),
554 program_id: None,
555 idl: None,
556 identity: self.identity.clone(),
557 handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
558 sections: self.sections.clone(),
559 field_mappings: self.field_mappings.clone(),
560 resolver_hooks: self.resolver_hooks.clone(),
561 instruction_hooks: self.instruction_hooks.clone(),
562 resolver_specs: self.resolver_specs.clone(),
563 computed_fields: self.computed_fields.clone(),
564 computed_field_specs: Vec::new(),
565 content_hash: None,
566 views: Vec::new(),
567 scope_field: None,
568 };
569 spec.content_hash = Some(spec.compute_content_hash());
570 spec
571 }
572
573 pub fn from_serializable(mut spec: SerializableStreamSpec) -> Self {
575 spec.normalize_event_names();
576 TypedStreamSpec {
577 state_name: spec.state_name,
578 identity: spec.identity,
579 handlers: spec
580 .handlers
581 .into_iter()
582 .map(|h| TypedHandlerSpec::from_serializable(h))
583 .collect(),
584 sections: spec.sections,
585 field_mappings: spec.field_mappings,
586 resolver_hooks: spec.resolver_hooks,
587 instruction_hooks: spec.instruction_hooks,
588 resolver_specs: spec.resolver_specs,
589 computed_fields: spec.computed_fields,
590 _phantom: PhantomData,
591 }
592 }
593}
594
595#[derive(Debug, Clone, Serialize, Deserialize)]
596pub struct IdentitySpec {
597 pub primary_keys: Vec<String>,
598 pub lookup_indexes: Vec<LookupIndexSpec>,
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
602pub struct LookupIndexSpec {
603 pub field_name: String,
604 pub temporal_field: Option<String>,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct ResolverHook {
614 pub account_type: String,
616
617 pub strategy: ResolverStrategy,
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize)]
622pub enum ResolverStrategy {
623 PdaReverseLookup {
625 lookup_name: String,
626 queue_discriminators: Vec<Vec<u8>>,
628 },
629
630 DirectField { field_path: FieldPath },
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize)]
636pub struct InstructionHook {
637 pub instruction_type: String,
639
640 pub actions: Vec<HookAction>,
642
643 pub lookup_by: Option<FieldPath>,
645}
646
647#[derive(Debug, Clone, Serialize, Deserialize)]
648pub enum HookAction {
649 RegisterPdaMapping {
651 pda_field: FieldPath,
652 seed_field: FieldPath,
653 lookup_name: String,
654 #[serde(default, skip_serializing_if = "Option::is_none")]
658 sibling: Option<SiblingSelector>,
659 #[serde(default, skip_serializing_if = "Option::is_none")]
663 seed_expr: Option<ComputedExpr>,
664 #[serde(default, skip_serializing_if = "Option::is_none")]
667 guard: Option<ComputedExpr>,
668 },
669
670 SetField {
672 target_field: String,
673 source: MappingSource,
674 condition: Option<ConditionExpr>,
675 },
676
677 IncrementField {
679 target_field: String,
680 increment_by: i64,
681 condition: Option<ConditionExpr>,
682 },
683}
684
685#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
689pub struct SiblingSelector {
690 pub instruction_type: String,
692 #[serde(default, skip_serializing_if = "is_false")]
695 pub pda_from_sibling: bool,
696 #[serde(default, skip_serializing_if = "is_false")]
699 pub seed_from_sibling: bool,
700 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
702 pub required_fields: std::collections::BTreeMap<String, serde_json::Value>,
703}
704
705fn is_false(value: &bool) -> bool {
706 !*value
707}
708
709#[derive(Debug, Clone, Serialize, Deserialize)]
711pub struct ConditionExpr {
712 pub expression: String,
714
715 pub parsed: Option<ParsedCondition>,
717}
718
719#[derive(Debug, Clone, Serialize, Deserialize)]
720pub enum ParsedCondition {
721 Comparison {
723 field: FieldPath,
724 op: ComparisonOp,
725 value: serde_json::Value,
726 },
727
728 Logical {
730 op: LogicalOp,
731 conditions: Vec<ParsedCondition>,
732 },
733
734 Guard { expr: ComputedExpr },
741}
742
743#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
744pub enum ComparisonOp {
745 Equal,
746 NotEqual,
747 GreaterThan,
748 GreaterThanOrEqual,
749 LessThan,
750 LessThanOrEqual,
751}
752
753#[derive(Debug, Clone, Serialize, Deserialize)]
754pub enum LogicalOp {
755 And,
756 Or,
757}
758
759#[derive(Debug, Clone, Serialize, Deserialize)]
761pub struct SerializableHandlerSpec {
762 pub source: SourceSpec,
763 pub key_resolution: KeyResolutionStrategy,
764 pub mappings: Vec<SerializableFieldMapping>,
765 pub conditions: Vec<Condition>,
766 pub emit: bool,
767}
768
769impl SerializableHandlerSpec {
770 pub fn normalize_event_names(&mut self) {
771 let SourceSpec::Source { type_name, .. } = &mut self.source;
772 *type_name = crate::event_type_helpers::canonicalize_event_type_name(type_name);
773
774 for mapping in &mut self.mappings {
775 if let Some(when) = &mut mapping.when {
776 *when = crate::event_type_helpers::canonicalize_event_type_name(when);
777 }
778 if let Some(stop) = &mut mapping.stop {
779 *stop = crate::event_type_helpers::canonicalize_event_type_name(stop);
780 }
781 }
782 }
783}
784
785#[derive(Debug, Clone)]
786pub struct TypedHandlerSpec<S> {
787 pub source: SourceSpec,
788 pub key_resolution: KeyResolutionStrategy,
789 pub mappings: Vec<TypedFieldMapping<S>>,
790 pub conditions: Vec<Condition>,
791 pub emit: bool,
792 _phantom: PhantomData<S>,
793}
794
795impl<S> TypedHandlerSpec<S> {
796 pub fn new(
797 source: SourceSpec,
798 key_resolution: KeyResolutionStrategy,
799 mappings: Vec<TypedFieldMapping<S>>,
800 emit: bool,
801 ) -> Self {
802 TypedHandlerSpec {
803 source,
804 key_resolution,
805 mappings,
806 conditions: vec![],
807 emit,
808 _phantom: PhantomData,
809 }
810 }
811
812 pub fn to_serializable(&self) -> SerializableHandlerSpec {
814 SerializableHandlerSpec {
815 source: self.source.clone(),
816 key_resolution: self.key_resolution.clone(),
817 mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
818 conditions: self.conditions.clone(),
819 emit: self.emit,
820 }
821 }
822
823 pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
825 TypedHandlerSpec {
826 source: spec.source,
827 key_resolution: spec.key_resolution,
828 mappings: spec
829 .mappings
830 .into_iter()
831 .map(|m| TypedFieldMapping::from_serializable(m))
832 .collect(),
833 conditions: spec.conditions,
834 emit: spec.emit,
835 _phantom: PhantomData,
836 }
837 }
838}
839
840#[derive(Debug, Clone, Serialize, Deserialize)]
841pub enum KeyResolutionStrategy {
842 Embedded {
843 primary_field: FieldPath,
844 },
845 Lookup {
847 primary_field: FieldPath,
848 #[serde(default, skip_serializing_if = "Option::is_none")]
862 index_name: Option<String>,
863 },
864 Computed {
865 primary_field: FieldPath,
866 compute_partition: ComputeFunction,
867 },
868 TemporalLookup {
869 lookup_field: FieldPath,
870 timestamp_field: FieldPath,
871 index_name: String,
872 },
873}
874
875#[derive(Debug, Clone, Serialize, Deserialize)]
876pub enum SourceSpec {
877 Source {
878 program_id: Option<String>,
879 discriminator: Option<Vec<u8>>,
880 type_name: String,
881 #[serde(default, skip_serializing_if = "Option::is_none")]
882 serialization: Option<IdlSerializationSnapshot>,
883 #[serde(default)]
888 is_account: bool,
889 },
890}
891
892#[derive(Debug, Clone, Serialize, Deserialize)]
894pub struct SerializableFieldMapping {
895 pub target_path: String,
896 pub source: MappingSource,
897 pub transform: Option<Transformation>,
898 pub population: PopulationStrategy,
899 #[serde(default, skip_serializing_if = "Option::is_none")]
900 pub condition: Option<ConditionExpr>,
901 #[serde(default, skip_serializing_if = "Option::is_none")]
902 pub when: Option<String>,
903 #[serde(default, skip_serializing_if = "Option::is_none")]
904 pub stop: Option<String>,
905 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
906 pub emit: bool,
907}
908
909fn default_emit() -> bool {
910 true
911}
912
913fn default_instruction_discriminant_size() -> usize {
914 8
915}
916
917fn is_true(value: &bool) -> bool {
918 *value
919}
920
921#[derive(Debug, Clone)]
922pub struct TypedFieldMapping<S> {
923 pub target_path: String,
924 pub source: MappingSource,
925 pub transform: Option<Transformation>,
926 pub population: PopulationStrategy,
927 pub condition: Option<ConditionExpr>,
928 pub when: Option<String>,
929 pub stop: Option<String>,
930 pub emit: bool,
931 _phantom: PhantomData<S>,
932}
933
934impl<S> TypedFieldMapping<S> {
935 pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
936 TypedFieldMapping {
937 target_path,
938 source,
939 transform: None,
940 population,
941 condition: None,
942 when: None,
943 stop: None,
944 emit: true,
945 _phantom: PhantomData,
946 }
947 }
948
949 pub fn with_transform(mut self, transform: Transformation) -> Self {
950 self.transform = Some(transform);
951 self
952 }
953
954 pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
955 self.condition = Some(condition);
956 self
957 }
958
959 pub fn with_when(mut self, when: String) -> Self {
960 self.when = Some(when);
961 self
962 }
963
964 pub fn with_stop(mut self, stop: String) -> Self {
965 self.stop = Some(stop);
966 self
967 }
968
969 pub fn with_emit(mut self, emit: bool) -> Self {
970 self.emit = emit;
971 self
972 }
973
974 pub fn to_serializable(&self) -> SerializableFieldMapping {
976 SerializableFieldMapping {
977 target_path: self.target_path.clone(),
978 source: self.source.clone(),
979 transform: self.transform.clone(),
980 population: self.population.clone(),
981 condition: self.condition.clone(),
982 when: self.when.clone(),
983 stop: self.stop.clone(),
984 emit: self.emit,
985 }
986 }
987
988 pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
990 TypedFieldMapping {
991 target_path: mapping.target_path,
992 source: mapping.source,
993 transform: mapping.transform,
994 population: mapping.population,
995 condition: mapping.condition,
996 when: mapping.when,
997 stop: mapping.stop,
998 emit: mapping.emit,
999 _phantom: PhantomData,
1000 }
1001 }
1002}
1003
1004#[derive(Debug, Clone, Serialize, Deserialize)]
1005pub enum MappingSource {
1006 FromSource {
1007 path: FieldPath,
1008 default: Option<Value>,
1009 transform: Option<Transformation>,
1010 },
1011 Constant(Value),
1012 Computed {
1013 inputs: Vec<FieldPath>,
1014 function: ComputeFunction,
1015 },
1016 FromState {
1017 path: String,
1018 },
1019 AsEvent {
1020 fields: Vec<Box<MappingSource>>,
1021 },
1022 WholeSource,
1023 AsCapture {
1026 field_transforms: BTreeMap<String, Transformation>,
1027 },
1028 FromContext {
1031 field: String,
1032 },
1033 FromExpr {
1038 expr: ComputedExpr,
1039 },
1040}
1041
1042impl MappingSource {
1043 pub fn with_transform(self, transform: Transformation) -> Self {
1044 match self {
1045 MappingSource::FromSource {
1046 path,
1047 default,
1048 transform: _,
1049 } => MappingSource::FromSource {
1050 path,
1051 default,
1052 transform: Some(transform),
1053 },
1054 other => other,
1055 }
1056 }
1057}
1058
1059#[derive(Debug, Clone, Serialize, Deserialize)]
1060pub enum ComputeFunction {
1061 Sum,
1062 Concat,
1063 Format(String),
1064 Custom(String),
1065}
1066
1067#[derive(Debug, Clone, Serialize, Deserialize)]
1068pub struct Condition {
1069 pub field: FieldPath,
1070 pub operator: ConditionOp,
1071 pub value: Value,
1072}
1073
1074#[derive(Debug, Clone, Serialize, Deserialize)]
1075pub enum ConditionOp {
1076 Equals,
1077 NotEquals,
1078 GreaterThan,
1079 LessThan,
1080 Contains,
1081 Exists,
1082}
1083
1084#[derive(Debug, Clone, Serialize, Deserialize)]
1086pub struct FieldTypeInfo {
1087 pub field_name: String,
1088 #[serde(default, skip_serializing_if = "Option::is_none")]
1089 pub raw_name: Option<String>,
1090 #[serde(default, skip_serializing_if = "Option::is_none")]
1091 pub canonical_name: Option<String>,
1092 pub rust_type_name: String, pub base_type: BaseType, #[serde(default, skip_serializing_if = "Option::is_none")]
1095 pub integer_kind: Option<IntegerKind>,
1096 pub is_optional: bool, pub is_array: bool, pub inner_type: Option<String>, pub source_path: Option<String>, #[serde(default)]
1102 pub resolved_type: Option<ResolvedStructType>,
1103 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
1104 pub emit: bool,
1105}
1106
1107#[derive(Debug, Clone, Serialize, Deserialize)]
1109pub struct ResolvedStructType {
1110 pub type_name: String,
1111 pub fields: Vec<ResolvedField>,
1112 pub is_instruction: bool,
1113 pub is_account: bool,
1114 pub is_event: bool,
1115 #[serde(default)]
1117 pub is_enum: bool,
1118 #[serde(default)]
1120 pub enum_variants: Vec<String>,
1121}
1122
1123#[derive(Debug, Clone, Serialize, Deserialize)]
1125pub struct ResolvedField {
1126 pub field_name: String,
1127 #[serde(default, skip_serializing_if = "Option::is_none")]
1128 pub raw_name: Option<String>,
1129 #[serde(default, skip_serializing_if = "Option::is_none")]
1130 pub canonical_name: Option<String>,
1131 pub field_type: String,
1132 pub base_type: BaseType,
1133 #[serde(default, skip_serializing_if = "Option::is_none")]
1134 pub integer_kind: Option<IntegerKind>,
1135 pub is_optional: bool,
1136 pub is_array: bool,
1137}
1138
1139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1140pub enum IntegerKind {
1141 U8,
1142 U16,
1143 U32,
1144 U64,
1145 U128,
1146 Usize,
1147 I8,
1148 I16,
1149 I32,
1150 I64,
1151 I128,
1152 Isize,
1153}
1154
1155impl IntegerKind {
1156 pub fn from_rust_type(type_str: &str) -> Option<Self> {
1157 match type_str.trim() {
1158 "u8" => Some(Self::U8),
1159 "u16" => Some(Self::U16),
1160 "u32" => Some(Self::U32),
1161 "u64" => Some(Self::U64),
1162 "u128" => Some(Self::U128),
1163 "usize" => Some(Self::Usize),
1164 "i8" => Some(Self::I8),
1165 "i16" => Some(Self::I16),
1166 "i32" => Some(Self::I32),
1167 "i64" => Some(Self::I64),
1168 "i128" => Some(Self::I128),
1169 "isize" => Some(Self::Isize),
1170 _ => None,
1171 }
1172 }
1173
1174 pub fn is_bigint(self) -> bool {
1175 matches!(self, Self::U64 | Self::U128 | Self::I64 | Self::I128)
1176 }
1177}
1178
1179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1181pub enum BaseType {
1182 Integer, Float, String, Boolean, Object, Array, Binary, Timestamp, Pubkey, Any, }
1198
1199#[derive(Debug, Clone, Serialize, Deserialize)]
1201pub struct EntitySection {
1202 pub name: String,
1203 pub fields: Vec<FieldTypeInfo>,
1204 pub is_nested_struct: bool,
1205 pub parent_field: Option<String>, }
1207
1208impl FieldTypeInfo {
1209 pub fn new(field_name: String, rust_type_name: String) -> Self {
1210 let (base_type, integer_kind, is_optional, is_array, inner_type) =
1211 Self::analyze_rust_type(&rust_type_name);
1212 let canonical_name = to_camel_case_owned(&field_name);
1213
1214 FieldTypeInfo {
1215 field_name: field_name.clone(),
1216 raw_name: Some(field_name.clone()),
1217 canonical_name: Some(canonical_name),
1218 rust_type_name,
1219 base_type: Self::infer_semantic_type(&field_name, base_type),
1220 integer_kind,
1221 is_optional,
1222 is_array,
1223 inner_type,
1224 source_path: None,
1225 resolved_type: None,
1226 emit: true,
1227 }
1228 }
1229
1230 pub fn with_source_path(mut self, source_path: String) -> Self {
1231 self.source_path = Some(source_path);
1232 self
1233 }
1234
1235 pub fn raw_field_name(&self) -> &str {
1236 self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1237 }
1238
1239 pub fn canonical_field_name(&self) -> String {
1240 self.canonical_name
1241 .clone()
1242 .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1243 }
1244
1245 pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1246 self.integer_kind.or_else(|| {
1247 IntegerKind::from_rust_type(
1248 self.inner_type
1249 .as_deref()
1250 .unwrap_or(self.rust_type_name.as_str()),
1251 )
1252 })
1253 }
1254
1255 fn analyze_rust_type(
1257 rust_type: &str,
1258 ) -> (BaseType, Option<IntegerKind>, bool, bool, Option<String>) {
1259 let type_str = rust_type.trim();
1260
1261 if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1263 let (inner_base_type, inner_integer_kind, _, inner_is_array, inner_inner_type) =
1264 Self::analyze_rust_type(&inner);
1265 return (
1266 inner_base_type,
1267 inner_integer_kind,
1268 true,
1269 inner_is_array,
1270 inner_inner_type.or(Some(inner)),
1271 );
1272 }
1273
1274 if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1276 let (_inner_base_type, inner_integer_kind, inner_is_optional, _, inner_inner_type) =
1277 Self::analyze_rust_type(&inner);
1278 return (
1279 BaseType::Array,
1280 inner_integer_kind,
1281 inner_is_optional,
1282 true,
1283 inner_inner_type.or(Some(inner)),
1284 );
1285 }
1286
1287 let integer_kind = IntegerKind::from_rust_type(type_str);
1289 let base_type = match integer_kind {
1290 Some(_) => BaseType::Integer,
1291 None => match type_str {
1292 "f32" | "f64" => BaseType::Float,
1293 "bool" => BaseType::Boolean,
1294 "String" | "&str" | "str" => BaseType::String,
1295 "Value" | "serde_json::Value" => BaseType::Any,
1296 "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1297 _ => {
1298 if type_str.contains("Bytes") || type_str.contains("bytes") {
1300 BaseType::Binary
1301 } else if type_str.contains("Pubkey") {
1302 BaseType::Pubkey
1303 } else {
1304 BaseType::Object
1305 }
1306 }
1307 },
1308 };
1309
1310 (base_type, integer_kind, false, false, None)
1311 }
1312
1313 fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1315 let pattern = format!("{}<", generic_name);
1316 if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1317 let start = pattern.len();
1318 let end = type_str.len() - 1;
1319 if end > start {
1320 return Some(type_str[start..end].trim().to_string());
1321 }
1322 }
1323 None
1324 }
1325
1326 fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1328 let lower_name = field_name.to_lowercase();
1329
1330 if base_type == BaseType::Integer
1332 && (lower_name.ends_with("_at")
1333 || lower_name.ends_with("_time")
1334 || lower_name.contains("timestamp")
1335 || lower_name.contains("created")
1336 || lower_name.contains("settled")
1337 || lower_name.contains("activated"))
1338 {
1339 return BaseType::Timestamp;
1340 }
1341
1342 base_type
1343 }
1344}
1345
1346impl ResolvedField {
1347 pub fn raw_field_name(&self) -> &str {
1348 self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1349 }
1350
1351 pub fn canonical_field_name(&self) -> String {
1352 self.canonical_name
1353 .clone()
1354 .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1355 }
1356
1357 pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1358 self.integer_kind
1359 .or_else(|| IntegerKind::from_rust_type(self.field_type.as_str()))
1360 }
1361}
1362
1363pub(crate) fn to_camel_case_owned(s: &str) -> String {
1364 let mut result = String::new();
1365 let mut uppercase_next = false;
1366
1367 for ch in s.chars() {
1368 if matches!(ch, '_' | '-' | '.') {
1369 uppercase_next = true;
1370 continue;
1371 }
1372
1373 if result.is_empty() {
1374 result.extend(ch.to_lowercase());
1375 continue;
1376 }
1377
1378 if uppercase_next {
1379 result.extend(ch.to_uppercase());
1380 uppercase_next = false;
1381 } else {
1382 result.push(ch);
1383 }
1384 }
1385
1386 result
1387}
1388
1389pub trait FieldAccessor<S> {
1390 fn path(&self) -> String;
1391}
1392
1393impl SerializableStreamSpec {
1398 pub fn compute_content_hash(&self) -> String {
1404 use sha2::{Digest, Sha256};
1405
1406 let mut spec_for_hash = self.clone();
1408 spec_for_hash.content_hash = None;
1409
1410 let json =
1412 serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1413
1414 let mut hasher = Sha256::new();
1416 hasher.update(json.as_bytes());
1417 let result = hasher.finalize();
1418
1419 hex::encode(result)
1421 }
1422
1423 pub fn verify_content_hash(&self) -> bool {
1426 match &self.content_hash {
1427 Some(hash) => {
1428 let computed = self.compute_content_hash();
1429 hash == &computed
1430 }
1431 None => true, }
1433 }
1434
1435 pub fn with_content_hash(mut self) -> Self {
1437 self.content_hash = Some(self.compute_content_hash());
1438 self
1439 }
1440}
1441
1442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1449pub struct PdaDefinition {
1450 pub name: String,
1452
1453 pub seeds: Vec<PdaSeedDef>,
1455
1456 #[serde(default, skip_serializing_if = "Option::is_none")]
1459 pub program_id: Option<String>,
1460
1461 #[serde(default, skip_serializing_if = "Option::is_none")]
1464 pub program: Option<PdaProgramDef>,
1465}
1466
1467#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1468#[serde(tag = "type", rename_all = "camelCase")]
1469pub enum PdaProgramDef {
1470 AccountRef { account_name: String },
1471 ArgRef { arg_name: String },
1472}
1473
1474#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1476#[serde(tag = "type", rename_all = "camelCase")]
1477pub enum PdaSeedDef {
1478 Literal { value: String },
1480
1481 Bytes { value: Vec<u8> },
1483
1484 ArgRef {
1486 arg_name: String,
1487 #[serde(default, skip_serializing_if = "Option::is_none")]
1489 arg_type: Option<String>,
1490 },
1491
1492 AccountRef { account_name: String },
1494}
1495
1496#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1498#[serde(tag = "category", rename_all = "camelCase")]
1499pub enum AccountResolution {
1500 Signer,
1504
1505 Known { address: String },
1507
1508 PdaRef { pda_name: String },
1510
1511 PdaInline {
1513 seeds: Vec<PdaSeedDef>,
1514 #[serde(default, skip_serializing_if = "Option::is_none")]
1515 program_id: Option<String>,
1516 #[serde(default, skip_serializing_if = "Option::is_none")]
1517 program: Option<PdaProgramDef>,
1518 },
1519
1520 UserProvided,
1522}
1523
1524#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1526pub struct InstructionAccountDef {
1527 pub name: String,
1529
1530 #[serde(default)]
1532 pub is_signer: bool,
1533
1534 #[serde(default)]
1536 pub is_writable: bool,
1537
1538 pub resolution: AccountResolution,
1540
1541 #[serde(default)]
1543 pub is_optional: bool,
1544
1545 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1547 pub docs: Vec<String>,
1548}
1549
1550#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1552#[serde(rename_all = "camelCase")]
1553pub struct InstructionAmountHint {
1554 pub decimals_source: AmountDecimalsSource,
1555}
1556
1557#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1558#[serde(
1559 tag = "kind",
1560 rename_all = "camelCase",
1561 rename_all_fields = "camelCase"
1562)]
1563pub enum AmountDecimalsSource {
1564 ArgMint { arg_name: String },
1565 ArgDecimals { arg_name: String },
1566 KnownAccount { account_name: String },
1567 Constant { decimals: u8 },
1568}
1569
1570#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1572pub struct InstructionArgDef {
1573 pub name: String,
1575
1576 #[serde(rename = "type")]
1578 pub arg_type: String,
1579
1580 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1582 pub docs: Vec<String>,
1583
1584 #[serde(default, skip_serializing_if = "Option::is_none")]
1586 pub amount_hint: Option<InstructionAmountHint>,
1587}
1588
1589#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1591pub struct InstructionDef {
1592 pub name: String,
1594
1595 pub discriminator: Vec<u8>,
1597
1598 #[serde(default = "default_instruction_discriminant_size")]
1600 pub discriminator_size: usize,
1601
1602 pub accounts: Vec<InstructionAccountDef>,
1604
1605 pub args: Vec<InstructionArgDef>,
1607
1608 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1610 pub errors: Vec<IdlErrorSnapshot>,
1611
1612 #[serde(default, skip_serializing_if = "Option::is_none")]
1614 pub program_id: Option<String>,
1615
1616 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1618 pub docs: Vec<String>,
1619}
1620
1621#[derive(Debug, Clone, Serialize, Deserialize)]
1628pub struct SerializableStackSpec {
1629 #[serde(default = "default_ast_version")]
1632 pub ast_version: String,
1633 pub stack_name: String,
1635 #[serde(default)]
1637 pub program_ids: Vec<String>,
1638 #[serde(default)]
1640 pub idls: Vec<IdlSnapshot>,
1641 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1643 pub program_specs: Vec<arete_hash::ProgramSpecV1>,
1644 pub entities: Vec<SerializableStreamSpec>,
1646 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1649 pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1650 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1652 pub instructions: Vec<InstructionDef>,
1653 #[serde(default, skip_serializing_if = "Option::is_none")]
1655 pub content_hash: Option<String>,
1656}
1657
1658impl SerializableStackSpec {
1659 pub fn normalize_event_names(&mut self) {
1661 for entity in &mut self.entities {
1662 entity.normalize_event_names();
1663 }
1664 }
1665
1666 pub fn compute_content_hash(&self) -> String {
1668 use sha2::{Digest, Sha256};
1669 let mut spec_for_hash = self.clone();
1670 spec_for_hash.content_hash = None;
1671 let json = serde_json::to_string(&spec_for_hash)
1672 .expect("Failed to serialize stack spec for hashing");
1673 let mut hasher = Sha256::new();
1674 hasher.update(json.as_bytes());
1675 hex::encode(hasher.finalize())
1676 }
1677
1678 pub fn with_content_hash(mut self) -> Self {
1679 self.content_hash = Some(self.compute_content_hash());
1680 self
1681 }
1682}
1683
1684#[cfg(test)]
1685mod tests {
1686 use super::{
1687 HookAction, IdentitySpec, InstructionHook, MappingSource, PopulationStrategy,
1688 SerializableFieldMapping, SerializableHandlerSpec, SerializableStackSpec,
1689 SerializableStreamSpec, SourceSpec, TypedStreamSpec, CURRENT_AST_VERSION,
1690 };
1691 use serde_json::Value;
1692
1693 fn legacy_stream_spec() -> SerializableStreamSpec {
1694 SerializableStreamSpec {
1695 ast_version: CURRENT_AST_VERSION.to_string(),
1696 state_name: "PumpfunToken".to_string(),
1697 program_id: None,
1698 idl: None,
1699 identity: IdentitySpec {
1700 primary_keys: vec!["id.mint".to_string()],
1701 lookup_indexes: vec![],
1702 },
1703 handlers: vec![SerializableHandlerSpec {
1704 source: SourceSpec::Source {
1705 program_id: None,
1706 discriminator: None,
1707 type_name: "pump::buyIxState".to_string(),
1708 serialization: None,
1709 is_account: false,
1710 },
1711 key_resolution: super::KeyResolutionStrategy::Embedded {
1712 primary_field: super::FieldPath::new(&["accounts", "mint"]),
1713 },
1714 mappings: vec![SerializableFieldMapping {
1715 target_path: "info.last_buy_at".to_string(),
1716 source: MappingSource::Constant(Value::Null),
1717 transform: None,
1718 population: PopulationStrategy::SetOnce,
1719 condition: None,
1720 when: Some("pump::sellIxState".to_string()),
1721 stop: Some("pump::buy_exact_sol_inIxState".to_string()),
1722 emit: true,
1723 }],
1724 conditions: vec![],
1725 emit: true,
1726 }],
1727 sections: vec![],
1728 field_mappings: Default::default(),
1729 resolver_hooks: vec![],
1730 instruction_hooks: vec![InstructionHook {
1731 instruction_type: "pump::buyIxState".to_string(),
1732 actions: vec![HookAction::SetField {
1733 target_field: "info.last_buy_at".to_string(),
1734 source: MappingSource::Constant(Value::Null),
1735 condition: None,
1736 }],
1737 lookup_by: None,
1738 }],
1739 resolver_specs: vec![],
1740 computed_fields: vec![],
1741 computed_field_specs: vec![],
1742 content_hash: None,
1743 views: vec![],
1744 scope_field: None,
1745 }
1746 }
1747
1748 #[test]
1749 fn typed_stream_spec_from_serializable_normalizes_legacy_instruction_event_names() {
1750 let typed = TypedStreamSpec::<Value>::from_serializable(legacy_stream_spec());
1751
1752 let handler = &typed.handlers[0];
1753 let SourceSpec::Source { type_name, .. } = &handler.source;
1754 assert_eq!(type_name, "pump::BuyIxState");
1755 assert_eq!(
1756 handler.mappings[0].when.as_deref(),
1757 Some("pump::SellIxState")
1758 );
1759 assert_eq!(
1760 handler.mappings[0].stop.as_deref(),
1761 Some("pump::BuyExactSolInIxState")
1762 );
1763 assert_eq!(
1764 typed.instruction_hooks[0].instruction_type,
1765 "pump::BuyIxState"
1766 );
1767 }
1768
1769 #[test]
1770 fn stack_spec_normalize_event_names_updates_all_entities() {
1771 let mut stack = SerializableStackSpec {
1772 ast_version: CURRENT_AST_VERSION.to_string(),
1773 stack_name: "PumpStack".to_string(),
1774 program_ids: vec![],
1775 idls: vec![],
1776 program_specs: vec![],
1777 entities: vec![legacy_stream_spec()],
1778 pdas: Default::default(),
1779 instructions: vec![],
1780 content_hash: None,
1781 };
1782
1783 stack.normalize_event_names();
1784
1785 let SourceSpec::Source { type_name, .. } = &stack.entities[0].handlers[0].source;
1786 assert_eq!(type_name, "pump::BuyIxState");
1787 assert_eq!(
1788 stack.entities[0].instruction_hooks[0].instruction_type,
1789 "pump::BuyIxState"
1790 );
1791 }
1792
1793 #[test]
1794 fn field_type_info_recognizes_128_bit_integers() {
1795 let unsigned = super::FieldTypeInfo::new("amount".to_string(), "u128".to_string());
1796 assert_eq!(unsigned.base_type, super::BaseType::Integer);
1797 assert!(!unsigned.is_optional);
1798 assert_eq!(unsigned.integer_kind, Some(super::IntegerKind::U128));
1799 assert_eq!(unsigned.raw_field_name(), "amount");
1800 assert_eq!(unsigned.canonical_field_name(), "amount");
1801
1802 let signed = super::FieldTypeInfo::new("delta".to_string(), "Option<i128>".to_string());
1803 assert_eq!(signed.base_type, super::BaseType::Integer);
1804 assert!(signed.is_optional);
1805 assert_eq!(signed.integer_kind, Some(super::IntegerKind::I128));
1806 assert_eq!(signed.inner_type.as_deref(), Some("i128"));
1807 }
1808
1809 #[test]
1810 fn field_type_info_preserves_timestamp_integer_kind_and_names() {
1811 let timestamp =
1812 super::FieldTypeInfo::new("last_updated_at".to_string(), "Option<i64>".to_string());
1813
1814 assert_eq!(timestamp.base_type, super::BaseType::Timestamp);
1815 assert_eq!(timestamp.integer_kind, Some(super::IntegerKind::I64));
1816 assert!(timestamp.is_optional);
1817 assert_eq!(timestamp.raw_field_name(), "last_updated_at");
1818 assert_eq!(timestamp.canonical_field_name(), "lastUpdatedAt");
1819 }
1820}
1821
1822#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1828#[serde(rename_all = "lowercase")]
1829pub enum SortOrder {
1830 #[default]
1831 Asc,
1832 Desc,
1833}
1834
1835#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1837pub enum CompareOp {
1838 Eq,
1839 Ne,
1840 Gt,
1841 Gte,
1842 Lt,
1843 Lte,
1844}
1845
1846#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1848pub enum PredicateValue {
1849 Literal(serde_json::Value),
1851 Dynamic(String),
1853 Field(FieldPath),
1855}
1856
1857#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1859pub enum Predicate {
1860 Compare {
1862 field: FieldPath,
1863 op: CompareOp,
1864 value: PredicateValue,
1865 },
1866 And(Vec<Predicate>),
1868 Or(Vec<Predicate>),
1870 Not(Box<Predicate>),
1872 Exists { field: FieldPath },
1874}
1875
1876#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1878pub enum ViewTransform {
1879 Filter { predicate: Predicate },
1881
1882 Sort {
1884 key: FieldPath,
1885 #[serde(default)]
1886 order: SortOrder,
1887 },
1888
1889 Take { count: usize },
1891
1892 Skip { count: usize },
1894
1895 First,
1897
1898 Last,
1900
1901 MaxBy { key: FieldPath },
1903
1904 MinBy { key: FieldPath },
1906}
1907
1908#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1910pub enum ViewSource {
1911 Entity { name: String },
1913 View { id: String },
1915}
1916
1917#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1919pub enum ViewOutput {
1920 #[default]
1922 Collection,
1923 Single,
1925 Keyed { key_field: FieldPath },
1927}
1928
1929#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1931pub struct ViewDef {
1932 pub id: String,
1934
1935 pub source: ViewSource,
1937
1938 #[serde(default)]
1940 pub pipeline: Vec<ViewTransform>,
1941
1942 #[serde(default)]
1944 pub output: ViewOutput,
1945}
1946
1947impl ViewDef {
1948 pub fn list(entity_name: &str) -> Self {
1950 ViewDef {
1951 id: format!("{}/list", entity_name),
1952 source: ViewSource::Entity {
1953 name: entity_name.to_string(),
1954 },
1955 pipeline: vec![],
1956 output: ViewOutput::Collection,
1957 }
1958 }
1959
1960 pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1962 ViewDef {
1963 id: format!("{}/state", entity_name),
1964 source: ViewSource::Entity {
1965 name: entity_name.to_string(),
1966 },
1967 pipeline: vec![],
1968 output: ViewOutput::Keyed {
1969 key_field: FieldPath::new(key_field),
1970 },
1971 }
1972 }
1973
1974 pub fn is_single(&self) -> bool {
1976 matches!(self.output, ViewOutput::Single)
1977 }
1978
1979 pub fn has_single_transform(&self) -> bool {
1981 self.pipeline.iter().any(|t| {
1982 matches!(
1983 t,
1984 ViewTransform::First
1985 | ViewTransform::Last
1986 | ViewTransform::MaxBy { .. }
1987 | ViewTransform::MinBy { .. }
1988 )
1989 })
1990 }
1991}
1992
1993#[macro_export]
1994macro_rules! define_accessor {
1995 ($name:ident, $state:ty, $path:expr) => {
1996 pub struct $name;
1997
1998 impl $crate::ast::FieldAccessor<$state> for $name {
1999 fn path(&self) -> String {
2000 $path.to_string()
2001 }
2002 }
2003 };
2004}