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}
701
702fn is_false(value: &bool) -> bool {
703 !*value
704}
705
706#[derive(Debug, Clone, Serialize, Deserialize)]
708pub struct ConditionExpr {
709 pub expression: String,
711
712 pub parsed: Option<ParsedCondition>,
714}
715
716#[derive(Debug, Clone, Serialize, Deserialize)]
717pub enum ParsedCondition {
718 Comparison {
720 field: FieldPath,
721 op: ComparisonOp,
722 value: serde_json::Value,
723 },
724
725 Logical {
727 op: LogicalOp,
728 conditions: Vec<ParsedCondition>,
729 },
730
731 Guard { expr: ComputedExpr },
738}
739
740#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
741pub enum ComparisonOp {
742 Equal,
743 NotEqual,
744 GreaterThan,
745 GreaterThanOrEqual,
746 LessThan,
747 LessThanOrEqual,
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
751pub enum LogicalOp {
752 And,
753 Or,
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize)]
758pub struct SerializableHandlerSpec {
759 pub source: SourceSpec,
760 pub key_resolution: KeyResolutionStrategy,
761 pub mappings: Vec<SerializableFieldMapping>,
762 pub conditions: Vec<Condition>,
763 pub emit: bool,
764}
765
766impl SerializableHandlerSpec {
767 pub fn normalize_event_names(&mut self) {
768 let SourceSpec::Source { type_name, .. } = &mut self.source;
769 *type_name = crate::event_type_helpers::canonicalize_event_type_name(type_name);
770
771 for mapping in &mut self.mappings {
772 if let Some(when) = &mut mapping.when {
773 *when = crate::event_type_helpers::canonicalize_event_type_name(when);
774 }
775 if let Some(stop) = &mut mapping.stop {
776 *stop = crate::event_type_helpers::canonicalize_event_type_name(stop);
777 }
778 }
779 }
780}
781
782#[derive(Debug, Clone)]
783pub struct TypedHandlerSpec<S> {
784 pub source: SourceSpec,
785 pub key_resolution: KeyResolutionStrategy,
786 pub mappings: Vec<TypedFieldMapping<S>>,
787 pub conditions: Vec<Condition>,
788 pub emit: bool,
789 _phantom: PhantomData<S>,
790}
791
792impl<S> TypedHandlerSpec<S> {
793 pub fn new(
794 source: SourceSpec,
795 key_resolution: KeyResolutionStrategy,
796 mappings: Vec<TypedFieldMapping<S>>,
797 emit: bool,
798 ) -> Self {
799 TypedHandlerSpec {
800 source,
801 key_resolution,
802 mappings,
803 conditions: vec![],
804 emit,
805 _phantom: PhantomData,
806 }
807 }
808
809 pub fn to_serializable(&self) -> SerializableHandlerSpec {
811 SerializableHandlerSpec {
812 source: self.source.clone(),
813 key_resolution: self.key_resolution.clone(),
814 mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
815 conditions: self.conditions.clone(),
816 emit: self.emit,
817 }
818 }
819
820 pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
822 TypedHandlerSpec {
823 source: spec.source,
824 key_resolution: spec.key_resolution,
825 mappings: spec
826 .mappings
827 .into_iter()
828 .map(|m| TypedFieldMapping::from_serializable(m))
829 .collect(),
830 conditions: spec.conditions,
831 emit: spec.emit,
832 _phantom: PhantomData,
833 }
834 }
835}
836
837#[derive(Debug, Clone, Serialize, Deserialize)]
838pub enum KeyResolutionStrategy {
839 Embedded {
840 primary_field: FieldPath,
841 },
842 Lookup {
844 primary_field: FieldPath,
845 #[serde(default, skip_serializing_if = "Option::is_none")]
859 index_name: Option<String>,
860 },
861 Computed {
862 primary_field: FieldPath,
863 compute_partition: ComputeFunction,
864 },
865 TemporalLookup {
866 lookup_field: FieldPath,
867 timestamp_field: FieldPath,
868 index_name: String,
869 },
870}
871
872#[derive(Debug, Clone, Serialize, Deserialize)]
873pub enum SourceSpec {
874 Source {
875 program_id: Option<String>,
876 discriminator: Option<Vec<u8>>,
877 type_name: String,
878 #[serde(default, skip_serializing_if = "Option::is_none")]
879 serialization: Option<IdlSerializationSnapshot>,
880 #[serde(default)]
885 is_account: bool,
886 },
887}
888
889#[derive(Debug, Clone, Serialize, Deserialize)]
891pub struct SerializableFieldMapping {
892 pub target_path: String,
893 pub source: MappingSource,
894 pub transform: Option<Transformation>,
895 pub population: PopulationStrategy,
896 #[serde(default, skip_serializing_if = "Option::is_none")]
897 pub condition: Option<ConditionExpr>,
898 #[serde(default, skip_serializing_if = "Option::is_none")]
899 pub when: Option<String>,
900 #[serde(default, skip_serializing_if = "Option::is_none")]
901 pub stop: Option<String>,
902 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
903 pub emit: bool,
904}
905
906fn default_emit() -> bool {
907 true
908}
909
910fn default_instruction_discriminant_size() -> usize {
911 8
912}
913
914fn is_true(value: &bool) -> bool {
915 *value
916}
917
918#[derive(Debug, Clone)]
919pub struct TypedFieldMapping<S> {
920 pub target_path: String,
921 pub source: MappingSource,
922 pub transform: Option<Transformation>,
923 pub population: PopulationStrategy,
924 pub condition: Option<ConditionExpr>,
925 pub when: Option<String>,
926 pub stop: Option<String>,
927 pub emit: bool,
928 _phantom: PhantomData<S>,
929}
930
931impl<S> TypedFieldMapping<S> {
932 pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
933 TypedFieldMapping {
934 target_path,
935 source,
936 transform: None,
937 population,
938 condition: None,
939 when: None,
940 stop: None,
941 emit: true,
942 _phantom: PhantomData,
943 }
944 }
945
946 pub fn with_transform(mut self, transform: Transformation) -> Self {
947 self.transform = Some(transform);
948 self
949 }
950
951 pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
952 self.condition = Some(condition);
953 self
954 }
955
956 pub fn with_when(mut self, when: String) -> Self {
957 self.when = Some(when);
958 self
959 }
960
961 pub fn with_stop(mut self, stop: String) -> Self {
962 self.stop = Some(stop);
963 self
964 }
965
966 pub fn with_emit(mut self, emit: bool) -> Self {
967 self.emit = emit;
968 self
969 }
970
971 pub fn to_serializable(&self) -> SerializableFieldMapping {
973 SerializableFieldMapping {
974 target_path: self.target_path.clone(),
975 source: self.source.clone(),
976 transform: self.transform.clone(),
977 population: self.population.clone(),
978 condition: self.condition.clone(),
979 when: self.when.clone(),
980 stop: self.stop.clone(),
981 emit: self.emit,
982 }
983 }
984
985 pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
987 TypedFieldMapping {
988 target_path: mapping.target_path,
989 source: mapping.source,
990 transform: mapping.transform,
991 population: mapping.population,
992 condition: mapping.condition,
993 when: mapping.when,
994 stop: mapping.stop,
995 emit: mapping.emit,
996 _phantom: PhantomData,
997 }
998 }
999}
1000
1001#[derive(Debug, Clone, Serialize, Deserialize)]
1002pub enum MappingSource {
1003 FromSource {
1004 path: FieldPath,
1005 default: Option<Value>,
1006 transform: Option<Transformation>,
1007 },
1008 Constant(Value),
1009 Computed {
1010 inputs: Vec<FieldPath>,
1011 function: ComputeFunction,
1012 },
1013 FromState {
1014 path: String,
1015 },
1016 AsEvent {
1017 fields: Vec<Box<MappingSource>>,
1018 },
1019 WholeSource,
1020 AsCapture {
1023 field_transforms: BTreeMap<String, Transformation>,
1024 },
1025 FromContext {
1028 field: String,
1029 },
1030 FromExpr {
1035 expr: ComputedExpr,
1036 },
1037}
1038
1039impl MappingSource {
1040 pub fn with_transform(self, transform: Transformation) -> Self {
1041 match self {
1042 MappingSource::FromSource {
1043 path,
1044 default,
1045 transform: _,
1046 } => MappingSource::FromSource {
1047 path,
1048 default,
1049 transform: Some(transform),
1050 },
1051 other => other,
1052 }
1053 }
1054}
1055
1056#[derive(Debug, Clone, Serialize, Deserialize)]
1057pub enum ComputeFunction {
1058 Sum,
1059 Concat,
1060 Format(String),
1061 Custom(String),
1062}
1063
1064#[derive(Debug, Clone, Serialize, Deserialize)]
1065pub struct Condition {
1066 pub field: FieldPath,
1067 pub operator: ConditionOp,
1068 pub value: Value,
1069}
1070
1071#[derive(Debug, Clone, Serialize, Deserialize)]
1072pub enum ConditionOp {
1073 Equals,
1074 NotEquals,
1075 GreaterThan,
1076 LessThan,
1077 Contains,
1078 Exists,
1079}
1080
1081#[derive(Debug, Clone, Serialize, Deserialize)]
1083pub struct FieldTypeInfo {
1084 pub field_name: String,
1085 #[serde(default, skip_serializing_if = "Option::is_none")]
1086 pub raw_name: Option<String>,
1087 #[serde(default, skip_serializing_if = "Option::is_none")]
1088 pub canonical_name: Option<String>,
1089 pub rust_type_name: String, pub base_type: BaseType, #[serde(default, skip_serializing_if = "Option::is_none")]
1092 pub integer_kind: Option<IntegerKind>,
1093 pub is_optional: bool, pub is_array: bool, pub inner_type: Option<String>, pub source_path: Option<String>, #[serde(default)]
1099 pub resolved_type: Option<ResolvedStructType>,
1100 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
1101 pub emit: bool,
1102}
1103
1104#[derive(Debug, Clone, Serialize, Deserialize)]
1106pub struct ResolvedStructType {
1107 pub type_name: String,
1108 pub fields: Vec<ResolvedField>,
1109 pub is_instruction: bool,
1110 pub is_account: bool,
1111 pub is_event: bool,
1112 #[serde(default)]
1114 pub is_enum: bool,
1115 #[serde(default)]
1117 pub enum_variants: Vec<String>,
1118}
1119
1120#[derive(Debug, Clone, Serialize, Deserialize)]
1122pub struct ResolvedField {
1123 pub field_name: String,
1124 #[serde(default, skip_serializing_if = "Option::is_none")]
1125 pub raw_name: Option<String>,
1126 #[serde(default, skip_serializing_if = "Option::is_none")]
1127 pub canonical_name: Option<String>,
1128 pub field_type: String,
1129 pub base_type: BaseType,
1130 #[serde(default, skip_serializing_if = "Option::is_none")]
1131 pub integer_kind: Option<IntegerKind>,
1132 pub is_optional: bool,
1133 pub is_array: bool,
1134}
1135
1136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1137pub enum IntegerKind {
1138 U8,
1139 U16,
1140 U32,
1141 U64,
1142 U128,
1143 Usize,
1144 I8,
1145 I16,
1146 I32,
1147 I64,
1148 I128,
1149 Isize,
1150}
1151
1152impl IntegerKind {
1153 pub fn from_rust_type(type_str: &str) -> Option<Self> {
1154 match type_str.trim() {
1155 "u8" => Some(Self::U8),
1156 "u16" => Some(Self::U16),
1157 "u32" => Some(Self::U32),
1158 "u64" => Some(Self::U64),
1159 "u128" => Some(Self::U128),
1160 "usize" => Some(Self::Usize),
1161 "i8" => Some(Self::I8),
1162 "i16" => Some(Self::I16),
1163 "i32" => Some(Self::I32),
1164 "i64" => Some(Self::I64),
1165 "i128" => Some(Self::I128),
1166 "isize" => Some(Self::Isize),
1167 _ => None,
1168 }
1169 }
1170
1171 pub fn is_bigint(self) -> bool {
1172 matches!(self, Self::U64 | Self::U128 | Self::I64 | Self::I128)
1173 }
1174}
1175
1176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1178pub enum BaseType {
1179 Integer, Float, String, Boolean, Object, Array, Binary, Timestamp, Pubkey, Any, }
1195
1196#[derive(Debug, Clone, Serialize, Deserialize)]
1198pub struct EntitySection {
1199 pub name: String,
1200 pub fields: Vec<FieldTypeInfo>,
1201 pub is_nested_struct: bool,
1202 pub parent_field: Option<String>, }
1204
1205impl FieldTypeInfo {
1206 pub fn new(field_name: String, rust_type_name: String) -> Self {
1207 let (base_type, integer_kind, is_optional, is_array, inner_type) =
1208 Self::analyze_rust_type(&rust_type_name);
1209 let canonical_name = to_camel_case_owned(&field_name);
1210
1211 FieldTypeInfo {
1212 field_name: field_name.clone(),
1213 raw_name: Some(field_name.clone()),
1214 canonical_name: Some(canonical_name),
1215 rust_type_name,
1216 base_type: Self::infer_semantic_type(&field_name, base_type),
1217 integer_kind,
1218 is_optional,
1219 is_array,
1220 inner_type,
1221 source_path: None,
1222 resolved_type: None,
1223 emit: true,
1224 }
1225 }
1226
1227 pub fn with_source_path(mut self, source_path: String) -> Self {
1228 self.source_path = Some(source_path);
1229 self
1230 }
1231
1232 pub fn raw_field_name(&self) -> &str {
1233 self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1234 }
1235
1236 pub fn canonical_field_name(&self) -> String {
1237 self.canonical_name
1238 .clone()
1239 .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1240 }
1241
1242 pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1243 self.integer_kind.or_else(|| {
1244 IntegerKind::from_rust_type(
1245 self.inner_type
1246 .as_deref()
1247 .unwrap_or(self.rust_type_name.as_str()),
1248 )
1249 })
1250 }
1251
1252 fn analyze_rust_type(
1254 rust_type: &str,
1255 ) -> (BaseType, Option<IntegerKind>, bool, bool, Option<String>) {
1256 let type_str = rust_type.trim();
1257
1258 if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1260 let (inner_base_type, inner_integer_kind, _, inner_is_array, inner_inner_type) =
1261 Self::analyze_rust_type(&inner);
1262 return (
1263 inner_base_type,
1264 inner_integer_kind,
1265 true,
1266 inner_is_array,
1267 inner_inner_type.or(Some(inner)),
1268 );
1269 }
1270
1271 if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1273 let (_inner_base_type, inner_integer_kind, inner_is_optional, _, inner_inner_type) =
1274 Self::analyze_rust_type(&inner);
1275 return (
1276 BaseType::Array,
1277 inner_integer_kind,
1278 inner_is_optional,
1279 true,
1280 inner_inner_type.or(Some(inner)),
1281 );
1282 }
1283
1284 let integer_kind = IntegerKind::from_rust_type(type_str);
1286 let base_type = match integer_kind {
1287 Some(_) => BaseType::Integer,
1288 None => match type_str {
1289 "f32" | "f64" => BaseType::Float,
1290 "bool" => BaseType::Boolean,
1291 "String" | "&str" | "str" => BaseType::String,
1292 "Value" | "serde_json::Value" => BaseType::Any,
1293 "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1294 _ => {
1295 if type_str.contains("Bytes") || type_str.contains("bytes") {
1297 BaseType::Binary
1298 } else if type_str.contains("Pubkey") {
1299 BaseType::Pubkey
1300 } else {
1301 BaseType::Object
1302 }
1303 }
1304 },
1305 };
1306
1307 (base_type, integer_kind, false, false, None)
1308 }
1309
1310 fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1312 let pattern = format!("{}<", generic_name);
1313 if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1314 let start = pattern.len();
1315 let end = type_str.len() - 1;
1316 if end > start {
1317 return Some(type_str[start..end].trim().to_string());
1318 }
1319 }
1320 None
1321 }
1322
1323 fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1325 let lower_name = field_name.to_lowercase();
1326
1327 if base_type == BaseType::Integer
1329 && (lower_name.ends_with("_at")
1330 || lower_name.ends_with("_time")
1331 || lower_name.contains("timestamp")
1332 || lower_name.contains("created")
1333 || lower_name.contains("settled")
1334 || lower_name.contains("activated"))
1335 {
1336 return BaseType::Timestamp;
1337 }
1338
1339 base_type
1340 }
1341}
1342
1343impl ResolvedField {
1344 pub fn raw_field_name(&self) -> &str {
1345 self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1346 }
1347
1348 pub fn canonical_field_name(&self) -> String {
1349 self.canonical_name
1350 .clone()
1351 .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1352 }
1353
1354 pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1355 self.integer_kind
1356 .or_else(|| IntegerKind::from_rust_type(self.field_type.as_str()))
1357 }
1358}
1359
1360pub(crate) fn to_camel_case_owned(s: &str) -> String {
1361 let mut result = String::new();
1362 let mut uppercase_next = false;
1363
1364 for ch in s.chars() {
1365 if matches!(ch, '_' | '-' | '.') {
1366 uppercase_next = true;
1367 continue;
1368 }
1369
1370 if result.is_empty() {
1371 result.extend(ch.to_lowercase());
1372 continue;
1373 }
1374
1375 if uppercase_next {
1376 result.extend(ch.to_uppercase());
1377 uppercase_next = false;
1378 } else {
1379 result.push(ch);
1380 }
1381 }
1382
1383 result
1384}
1385
1386pub trait FieldAccessor<S> {
1387 fn path(&self) -> String;
1388}
1389
1390impl SerializableStreamSpec {
1395 pub fn compute_content_hash(&self) -> String {
1401 use sha2::{Digest, Sha256};
1402
1403 let mut spec_for_hash = self.clone();
1405 spec_for_hash.content_hash = None;
1406
1407 let json =
1409 serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1410
1411 let mut hasher = Sha256::new();
1413 hasher.update(json.as_bytes());
1414 let result = hasher.finalize();
1415
1416 hex::encode(result)
1418 }
1419
1420 pub fn verify_content_hash(&self) -> bool {
1423 match &self.content_hash {
1424 Some(hash) => {
1425 let computed = self.compute_content_hash();
1426 hash == &computed
1427 }
1428 None => true, }
1430 }
1431
1432 pub fn with_content_hash(mut self) -> Self {
1434 self.content_hash = Some(self.compute_content_hash());
1435 self
1436 }
1437}
1438
1439#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1446pub struct PdaDefinition {
1447 pub name: String,
1449
1450 pub seeds: Vec<PdaSeedDef>,
1452
1453 #[serde(default, skip_serializing_if = "Option::is_none")]
1456 pub program_id: Option<String>,
1457
1458 #[serde(default, skip_serializing_if = "Option::is_none")]
1461 pub program: Option<PdaProgramDef>,
1462}
1463
1464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1465#[serde(tag = "type", rename_all = "camelCase")]
1466pub enum PdaProgramDef {
1467 AccountRef { account_name: String },
1468 ArgRef { arg_name: String },
1469}
1470
1471#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1473#[serde(tag = "type", rename_all = "camelCase")]
1474pub enum PdaSeedDef {
1475 Literal { value: String },
1477
1478 Bytes { value: Vec<u8> },
1480
1481 ArgRef {
1483 arg_name: String,
1484 #[serde(default, skip_serializing_if = "Option::is_none")]
1486 arg_type: Option<String>,
1487 },
1488
1489 AccountRef { account_name: String },
1491}
1492
1493#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1495#[serde(tag = "category", rename_all = "camelCase")]
1496pub enum AccountResolution {
1497 Signer,
1501
1502 Known { address: String },
1504
1505 PdaRef { pda_name: String },
1507
1508 PdaInline {
1510 seeds: Vec<PdaSeedDef>,
1511 #[serde(default, skip_serializing_if = "Option::is_none")]
1512 program_id: Option<String>,
1513 #[serde(default, skip_serializing_if = "Option::is_none")]
1514 program: Option<PdaProgramDef>,
1515 },
1516
1517 UserProvided,
1519}
1520
1521#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1523pub struct InstructionAccountDef {
1524 pub name: String,
1526
1527 #[serde(default)]
1529 pub is_signer: bool,
1530
1531 #[serde(default)]
1533 pub is_writable: bool,
1534
1535 pub resolution: AccountResolution,
1537
1538 #[serde(default)]
1540 pub is_optional: bool,
1541
1542 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1544 pub docs: Vec<String>,
1545}
1546
1547#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1549#[serde(rename_all = "camelCase")]
1550pub struct InstructionAmountHint {
1551 pub decimals_source: AmountDecimalsSource,
1552}
1553
1554#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1555#[serde(
1556 tag = "kind",
1557 rename_all = "camelCase",
1558 rename_all_fields = "camelCase"
1559)]
1560pub enum AmountDecimalsSource {
1561 ArgMint { arg_name: String },
1562 ArgDecimals { arg_name: String },
1563 KnownAccount { account_name: String },
1564 Constant { decimals: u8 },
1565}
1566
1567#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1569pub struct InstructionArgDef {
1570 pub name: String,
1572
1573 #[serde(rename = "type")]
1575 pub arg_type: String,
1576
1577 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1579 pub docs: Vec<String>,
1580
1581 #[serde(default, skip_serializing_if = "Option::is_none")]
1583 pub amount_hint: Option<InstructionAmountHint>,
1584}
1585
1586#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1588pub struct InstructionDef {
1589 pub name: String,
1591
1592 pub discriminator: Vec<u8>,
1594
1595 #[serde(default = "default_instruction_discriminant_size")]
1597 pub discriminator_size: usize,
1598
1599 pub accounts: Vec<InstructionAccountDef>,
1601
1602 pub args: Vec<InstructionArgDef>,
1604
1605 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1607 pub errors: Vec<IdlErrorSnapshot>,
1608
1609 #[serde(default, skip_serializing_if = "Option::is_none")]
1611 pub program_id: Option<String>,
1612
1613 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1615 pub docs: Vec<String>,
1616}
1617
1618#[derive(Debug, Clone, Serialize, Deserialize)]
1625pub struct SerializableStackSpec {
1626 #[serde(default = "default_ast_version")]
1629 pub ast_version: String,
1630 pub stack_name: String,
1632 #[serde(default)]
1634 pub program_ids: Vec<String>,
1635 #[serde(default)]
1637 pub idls: Vec<IdlSnapshot>,
1638 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1640 pub program_specs: Vec<arete_hash::ProgramSpecV1>,
1641 pub entities: Vec<SerializableStreamSpec>,
1643 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1646 pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1647 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1649 pub instructions: Vec<InstructionDef>,
1650 #[serde(default, skip_serializing_if = "Option::is_none")]
1652 pub content_hash: Option<String>,
1653}
1654
1655impl SerializableStackSpec {
1656 pub fn normalize_event_names(&mut self) {
1658 for entity in &mut self.entities {
1659 entity.normalize_event_names();
1660 }
1661 }
1662
1663 pub fn compute_content_hash(&self) -> String {
1665 use sha2::{Digest, Sha256};
1666 let mut spec_for_hash = self.clone();
1667 spec_for_hash.content_hash = None;
1668 let json = serde_json::to_string(&spec_for_hash)
1669 .expect("Failed to serialize stack spec for hashing");
1670 let mut hasher = Sha256::new();
1671 hasher.update(json.as_bytes());
1672 hex::encode(hasher.finalize())
1673 }
1674
1675 pub fn with_content_hash(mut self) -> Self {
1676 self.content_hash = Some(self.compute_content_hash());
1677 self
1678 }
1679}
1680
1681#[cfg(test)]
1682mod tests {
1683 use super::{
1684 HookAction, IdentitySpec, InstructionHook, MappingSource, PopulationStrategy,
1685 SerializableFieldMapping, SerializableHandlerSpec, SerializableStackSpec,
1686 SerializableStreamSpec, SourceSpec, TypedStreamSpec, CURRENT_AST_VERSION,
1687 };
1688 use serde_json::Value;
1689
1690 fn legacy_stream_spec() -> SerializableStreamSpec {
1691 SerializableStreamSpec {
1692 ast_version: CURRENT_AST_VERSION.to_string(),
1693 state_name: "PumpfunToken".to_string(),
1694 program_id: None,
1695 idl: None,
1696 identity: IdentitySpec {
1697 primary_keys: vec!["id.mint".to_string()],
1698 lookup_indexes: vec![],
1699 },
1700 handlers: vec![SerializableHandlerSpec {
1701 source: SourceSpec::Source {
1702 program_id: None,
1703 discriminator: None,
1704 type_name: "pump::buyIxState".to_string(),
1705 serialization: None,
1706 is_account: false,
1707 },
1708 key_resolution: super::KeyResolutionStrategy::Embedded {
1709 primary_field: super::FieldPath::new(&["accounts", "mint"]),
1710 },
1711 mappings: vec![SerializableFieldMapping {
1712 target_path: "info.last_buy_at".to_string(),
1713 source: MappingSource::Constant(Value::Null),
1714 transform: None,
1715 population: PopulationStrategy::SetOnce,
1716 condition: None,
1717 when: Some("pump::sellIxState".to_string()),
1718 stop: Some("pump::buy_exact_sol_inIxState".to_string()),
1719 emit: true,
1720 }],
1721 conditions: vec![],
1722 emit: true,
1723 }],
1724 sections: vec![],
1725 field_mappings: Default::default(),
1726 resolver_hooks: vec![],
1727 instruction_hooks: vec![InstructionHook {
1728 instruction_type: "pump::buyIxState".to_string(),
1729 actions: vec![HookAction::SetField {
1730 target_field: "info.last_buy_at".to_string(),
1731 source: MappingSource::Constant(Value::Null),
1732 condition: None,
1733 }],
1734 lookup_by: None,
1735 }],
1736 resolver_specs: vec![],
1737 computed_fields: vec![],
1738 computed_field_specs: vec![],
1739 content_hash: None,
1740 views: vec![],
1741 scope_field: None,
1742 }
1743 }
1744
1745 #[test]
1746 fn typed_stream_spec_from_serializable_normalizes_legacy_instruction_event_names() {
1747 let typed = TypedStreamSpec::<Value>::from_serializable(legacy_stream_spec());
1748
1749 let handler = &typed.handlers[0];
1750 let SourceSpec::Source { type_name, .. } = &handler.source;
1751 assert_eq!(type_name, "pump::BuyIxState");
1752 assert_eq!(
1753 handler.mappings[0].when.as_deref(),
1754 Some("pump::SellIxState")
1755 );
1756 assert_eq!(
1757 handler.mappings[0].stop.as_deref(),
1758 Some("pump::BuyExactSolInIxState")
1759 );
1760 assert_eq!(
1761 typed.instruction_hooks[0].instruction_type,
1762 "pump::BuyIxState"
1763 );
1764 }
1765
1766 #[test]
1767 fn stack_spec_normalize_event_names_updates_all_entities() {
1768 let mut stack = SerializableStackSpec {
1769 ast_version: CURRENT_AST_VERSION.to_string(),
1770 stack_name: "PumpStack".to_string(),
1771 program_ids: vec![],
1772 idls: vec![],
1773 program_specs: vec![],
1774 entities: vec![legacy_stream_spec()],
1775 pdas: Default::default(),
1776 instructions: vec![],
1777 content_hash: None,
1778 };
1779
1780 stack.normalize_event_names();
1781
1782 let SourceSpec::Source { type_name, .. } = &stack.entities[0].handlers[0].source;
1783 assert_eq!(type_name, "pump::BuyIxState");
1784 assert_eq!(
1785 stack.entities[0].instruction_hooks[0].instruction_type,
1786 "pump::BuyIxState"
1787 );
1788 }
1789
1790 #[test]
1791 fn field_type_info_recognizes_128_bit_integers() {
1792 let unsigned = super::FieldTypeInfo::new("amount".to_string(), "u128".to_string());
1793 assert_eq!(unsigned.base_type, super::BaseType::Integer);
1794 assert!(!unsigned.is_optional);
1795 assert_eq!(unsigned.integer_kind, Some(super::IntegerKind::U128));
1796 assert_eq!(unsigned.raw_field_name(), "amount");
1797 assert_eq!(unsigned.canonical_field_name(), "amount");
1798
1799 let signed = super::FieldTypeInfo::new("delta".to_string(), "Option<i128>".to_string());
1800 assert_eq!(signed.base_type, super::BaseType::Integer);
1801 assert!(signed.is_optional);
1802 assert_eq!(signed.integer_kind, Some(super::IntegerKind::I128));
1803 assert_eq!(signed.inner_type.as_deref(), Some("i128"));
1804 }
1805
1806 #[test]
1807 fn field_type_info_preserves_timestamp_integer_kind_and_names() {
1808 let timestamp =
1809 super::FieldTypeInfo::new("last_updated_at".to_string(), "Option<i64>".to_string());
1810
1811 assert_eq!(timestamp.base_type, super::BaseType::Timestamp);
1812 assert_eq!(timestamp.integer_kind, Some(super::IntegerKind::I64));
1813 assert!(timestamp.is_optional);
1814 assert_eq!(timestamp.raw_field_name(), "last_updated_at");
1815 assert_eq!(timestamp.canonical_field_name(), "lastUpdatedAt");
1816 }
1817}
1818
1819#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1825#[serde(rename_all = "lowercase")]
1826pub enum SortOrder {
1827 #[default]
1828 Asc,
1829 Desc,
1830}
1831
1832#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1834pub enum CompareOp {
1835 Eq,
1836 Ne,
1837 Gt,
1838 Gte,
1839 Lt,
1840 Lte,
1841}
1842
1843#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1845pub enum PredicateValue {
1846 Literal(serde_json::Value),
1848 Dynamic(String),
1850 Field(FieldPath),
1852}
1853
1854#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1856pub enum Predicate {
1857 Compare {
1859 field: FieldPath,
1860 op: CompareOp,
1861 value: PredicateValue,
1862 },
1863 And(Vec<Predicate>),
1865 Or(Vec<Predicate>),
1867 Not(Box<Predicate>),
1869 Exists { field: FieldPath },
1871}
1872
1873#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1875pub enum ViewTransform {
1876 Filter { predicate: Predicate },
1878
1879 Sort {
1881 key: FieldPath,
1882 #[serde(default)]
1883 order: SortOrder,
1884 },
1885
1886 Take { count: usize },
1888
1889 Skip { count: usize },
1891
1892 First,
1894
1895 Last,
1897
1898 MaxBy { key: FieldPath },
1900
1901 MinBy { key: FieldPath },
1903}
1904
1905#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1907pub enum ViewSource {
1908 Entity { name: String },
1910 View { id: String },
1912}
1913
1914#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1916pub enum ViewOutput {
1917 #[default]
1919 Collection,
1920 Single,
1922 Keyed { key_field: FieldPath },
1924}
1925
1926#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1928pub struct ViewDef {
1929 pub id: String,
1931
1932 pub source: ViewSource,
1934
1935 #[serde(default)]
1937 pub pipeline: Vec<ViewTransform>,
1938
1939 #[serde(default)]
1941 pub output: ViewOutput,
1942}
1943
1944impl ViewDef {
1945 pub fn list(entity_name: &str) -> Self {
1947 ViewDef {
1948 id: format!("{}/list", entity_name),
1949 source: ViewSource::Entity {
1950 name: entity_name.to_string(),
1951 },
1952 pipeline: vec![],
1953 output: ViewOutput::Collection,
1954 }
1955 }
1956
1957 pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1959 ViewDef {
1960 id: format!("{}/state", entity_name),
1961 source: ViewSource::Entity {
1962 name: entity_name.to_string(),
1963 },
1964 pipeline: vec![],
1965 output: ViewOutput::Keyed {
1966 key_field: FieldPath::new(key_field),
1967 },
1968 }
1969 }
1970
1971 pub fn is_single(&self) -> bool {
1973 matches!(self.output, ViewOutput::Single)
1974 }
1975
1976 pub fn has_single_transform(&self) -> bool {
1978 self.pipeline.iter().any(|t| {
1979 matches!(
1980 t,
1981 ViewTransform::First
1982 | ViewTransform::Last
1983 | ViewTransform::MaxBy { .. }
1984 | ViewTransform::MinBy { .. }
1985 )
1986 })
1987 }
1988}
1989
1990#[macro_export]
1991macro_rules! define_accessor {
1992 ($name:ident, $state:ty, $path:expr) => {
1993 pub struct $name;
1994
1995 impl $crate::ast::FieldAccessor<$state> for $name {
1996 fn path(&self) -> String {
1997 $path.to_string()
1998 }
1999 }
2000 };
2001}