1pub mod account_policy;
34pub mod bus;
35pub mod cache;
36pub mod compression;
37pub mod config;
38pub mod health;
39mod http;
40pub mod http_health;
41pub mod http_server;
42pub mod journal;
43pub mod materialized_view;
44#[cfg(feature = "otel")]
45pub mod metrics;
46pub mod mutation_batch;
47pub mod program_runtime;
48pub mod projector;
49pub mod runtime;
50pub mod snapshot;
51pub mod sorted_cache;
52pub mod telemetry;
53pub mod view;
54pub mod websocket;
55
56pub use arete_auth::{
57 AsyncVerifier, KeyLoader, Limits, SolanaGatewayAuthorization, SolanaGatewayAuthorizationError,
58 SolanaGatewayScope, TargetKind, TokenVerifier, VerifyingKey, SCOPE_READ,
59 SCOPE_TRANSACTION_INSPECT, SCOPE_TRANSACTION_SEND, SOLANA_GATEWAY_AUDIENCE,
60};
61pub use bus::{BusManager, BusMessage};
62pub use cache::{EntityCache, EntityCacheConfig};
63pub use config::{
64 HealthConfig, HttpHealthConfig, HttpServerConfig, ReconnectionConfig, RuntimePlan,
65 ServerConfig, TransactionConfig, WebSocketConfig, WebSocketDeliveryConfig, YellowstoneConfig,
66};
67pub use health::{HealthMonitor, SlotTracker, StreamStatus};
68pub use http_health::HttpHealthServer;
69pub use http_server::HttpServer;
70pub use journal::{EventJournal, JournalConfig, ReplayWindow};
71pub use materialized_view::{MaterializedView, MaterializedViewRegistry, ViewEffect};
72#[cfg(feature = "otel")]
73pub use metrics::Metrics;
74pub use mutation_batch::{EventContext, MutationBatch, SlotContext};
75pub use program_runtime::{
76 IdlContentHash, NormalizedIdlHash, ProgramAccountReaderFn, ProgramReleaseHash,
77 ProgramRuntimeCatalog, ProgramRuntimeDefinition, ProgramSpecHash,
78};
79pub use projector::Projector;
80pub use runtime::{ConnectionServer, Runtime, RuntimeHandle};
81pub use snapshot::{SnapshotConfig, SnapshotService};
82pub use telemetry::{init as init_telemetry, TelemetryConfig};
83#[cfg(feature = "otel")]
84pub use telemetry::{init_with_otel, TelemetryGuard};
85pub use view::{Delivery, Filters, Projection, ViewIndex, ViewSpec};
86pub use websocket::{
87 AllowAllAuthPlugin, AuthContext, AuthDecision, AuthDeny, AuthErrorDetails, ChannelUsageEmitter,
88 ClientInfo, ClientManager, ConnectionAuthRequest, ErrorResponse, Frame, HttpUsageEmitter, Mode,
89 RateLimitConfig, RateLimitResult, RateLimiterConfig, RefreshAuthRequest, RefreshAuthResponse,
90 RetryPolicy, SignedSessionAuthPlugin, SnapshotOptions, SocketIssueMessage,
91 StaticTokenAuthPlugin, Subscription, SubscriptionQuery, WebSocketAuthPlugin,
92 WebSocketRateLimiter, WebSocketServer, WebSocketUsageBatch, WebSocketUsageEmitter,
93 WebSocketUsageEnvelope, WebSocketUsageEvent,
94};
95
96use anyhow::Result;
97use arete_interpreter::ast::ViewDef;
98use std::net::SocketAddr;
99use std::sync::Arc;
100
101pub type ParserSetupFn = Arc<
103 dyn Fn(
104 tokio::sync::mpsc::Sender<MutationBatch>,
105 Option<HealthMonitor>,
106 ReconnectionConfig,
107 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>
108 + Send
109 + Sync,
110>;
111
112pub struct Spec {
115 pub bytecode: arete_interpreter::compiler::MultiEntityBytecode,
116 pub program_ids: Vec<String>,
117 pub parser_setup: Option<ParserSetupFn>,
118 pub program_runtime_definitions: Vec<ProgramRuntimeDefinition>,
119 pub entity_specs: Vec<arete_interpreter::ast::SerializableStreamSpec>,
120 pub views: Vec<ViewDef>,
121}
122
123impl Spec {
124 pub fn new(
125 bytecode: arete_interpreter::compiler::MultiEntityBytecode,
126 program_id: impl Into<String>,
127 ) -> Self {
128 Self {
129 bytecode,
130 program_ids: vec![program_id.into()],
131 parser_setup: None,
132 program_runtime_definitions: Vec::new(),
133 entity_specs: Vec::new(),
134 views: Vec::new(),
135 }
136 }
137
138 pub fn with_parser_setup(mut self, setup_fn: ParserSetupFn) -> Self {
139 self.parser_setup = Some(setup_fn);
140 self
141 }
142
143 pub fn with_program_runtime_definitions(
144 mut self,
145 definitions: Vec<ProgramRuntimeDefinition>,
146 ) -> Self {
147 for definition in &definitions {
148 if !self.program_ids.contains(&definition.program_id) {
149 self.program_ids.push(definition.program_id.clone());
150 }
151 }
152 self.program_runtime_definitions = definitions;
153 self
154 }
155
156 pub fn with_entity_specs(
157 mut self,
158 entity_specs: Vec<arete_interpreter::ast::SerializableStreamSpec>,
159 ) -> Self {
160 self.entity_specs = entity_specs;
161 self
162 }
163
164 pub fn with_views(mut self, views: Vec<ViewDef>) -> Self {
165 self.views = views;
166 self
167 }
168}
169
170pub struct Server;
172
173impl Server {
174 pub fn builder() -> ServerBuilder {
176 ServerBuilder::new()
177 }
178
179 pub fn solana_gateway(target_id: impl Into<String>) -> SolanaGatewayBuilder {
181 SolanaGatewayBuilder::new(target_id.into())
182 }
183}
184
185pub struct SolanaGatewayBuilder {
191 inner: ServerBuilder,
192}
193
194impl SolanaGatewayBuilder {
195 fn new(target_id: String) -> Self {
196 let mut inner = ServerBuilder::new();
197 inner.config.http_health = Some(HttpHealthConfig::default());
198 inner.config.runtime_plan = RuntimePlan::solana_gateway();
199 inner.config.solana_gateway_target_id = Some(target_id);
200 Self { inner }
201 }
202
203 pub fn bind(mut self, addr: impl Into<SocketAddr>) -> Self {
205 self.inner.config.http_health = Some(HttpHealthConfig::new(addr));
206 self
207 }
208
209 pub fn auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
211 self.inner.http_auth_plugin = Some(plugin);
212 self
213 }
214
215 pub fn transactions_config(mut self, config: TransactionConfig) -> Self {
217 self.inner.config.transactions = Some(config);
218 self
219 }
220
221 fn finalize(mut self) -> Result<ServerBuilder> {
222 if self
223 .inner
224 .config
225 .solana_gateway_target_id
226 .as_deref()
227 .is_none_or(|target_id| target_id.trim().is_empty())
228 {
229 anyhow::bail!("the Solana gateway target ID must not be empty");
230 }
231 if let Some(config) = self.inner.config.transactions.as_ref() {
232 config.validate()?;
233 if !config.enabled {
234 anyhow::bail!("Solana gateway transaction configuration must be enabled");
235 }
236 }
237 self.inner.config.runtime_plan = RuntimePlan::solana_gateway();
238 Ok(self.inner)
239 }
240
241 pub fn build(self) -> Result<Runtime> {
243 self.finalize()?.build()
244 }
245
246 pub async fn start(self) -> Result<()> {
248 self.finalize()?.start().await
249 }
250}
251
252pub struct ServerBuilder {
254 spec: Option<Spec>,
255 views: Option<ViewIndex>,
256 materialized_views: Option<MaterializedViewRegistry>,
257 config: ServerConfig,
258 websocket_auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
259 http_auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
260 websocket_usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
261 websocket_max_clients: Option<usize>,
262 websocket_rate_limit_config: Option<crate::websocket::client_manager::RateLimitConfig>,
263 #[cfg(feature = "otel")]
264 metrics: Option<Arc<Metrics>>,
265}
266
267impl ServerBuilder {
268 fn new() -> Self {
269 Self {
270 spec: None,
271 views: None,
272 materialized_views: None,
273 config: ServerConfig::new(),
274 websocket_auth_plugin: None,
275 http_auth_plugin: None,
276 websocket_usage_emitter: None,
277 websocket_max_clients: None,
278 websocket_rate_limit_config: None,
279 #[cfg(feature = "otel")]
280 metrics: None,
281 }
282 }
283
284 pub fn spec(mut self, spec: Spec) -> Self {
286 self.spec = Some(spec);
287 self
288 }
289
290 pub fn views(mut self, views: ViewIndex) -> Self {
292 self.views = Some(views);
293 self
294 }
295
296 #[cfg(feature = "otel")]
298 pub fn metrics(mut self, metrics: Metrics) -> Self {
299 self.metrics = Some(Arc::new(metrics));
300 self
301 }
302
303 pub fn websocket(mut self) -> Self {
305 self.config.websocket = Some(WebSocketConfig::default());
306 self.config.runtime_plan.websocket = true;
307 self.config.runtime_plan.live_runtime = true;
308 self
309 }
310
311 pub fn websocket_config(mut self, config: WebSocketConfig) -> Self {
313 self.config.websocket = Some(config);
314 self.config.runtime_plan.websocket = true;
315 self.config.runtime_plan.live_runtime = true;
316 self
317 }
318
319 pub fn websocket_auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
321 self.websocket_auth_plugin = Some(plugin);
322 self
323 }
324
325 pub fn http_auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
327 self.http_auth_plugin = Some(plugin);
328 self
329 }
330
331 pub fn websocket_usage_emitter(mut self, emitter: Arc<dyn WebSocketUsageEmitter>) -> Self {
333 self.websocket_usage_emitter = Some(emitter);
334 self
335 }
336
337 pub fn websocket_max_clients(mut self, max_clients: usize) -> Self {
339 self.websocket_max_clients = Some(max_clients);
340 self
341 }
342
343 pub fn websocket_rate_limit_config(
349 mut self,
350 config: crate::websocket::client_manager::RateLimitConfig,
351 ) -> Self {
352 self.websocket_rate_limit_config = Some(config);
353 self
354 }
355
356 pub fn websocket_delivery_config(mut self, config: WebSocketDeliveryConfig) -> Self {
358 self.config.websocket_delivery = Some(config);
359 self.config.runtime_plan.live_runtime = true;
360 self
361 }
362
363 pub fn bind(mut self, addr: impl Into<SocketAddr>) -> Self {
365 if let Some(ws_config) = &mut self.config.websocket {
366 ws_config.bind_address = addr.into();
367 } else {
368 self.config.websocket = Some(WebSocketConfig::new(addr.into()));
369 }
370 self.config.runtime_plan.websocket = true;
371 self.config.runtime_plan.live_runtime = true;
372 self
373 }
374
375 pub fn yellowstone(mut self, config: YellowstoneConfig) -> Self {
377 self.config.yellowstone = Some(config);
378 self.config.runtime_plan.live_runtime = true;
379 self
380 }
381
382 pub fn health_monitoring(mut self) -> Self {
384 self.config.health = Some(HealthConfig::default());
385 self.config.runtime_plan.health = true;
386 self
387 }
388
389 pub fn health_config(mut self, config: HealthConfig) -> Self {
391 self.config.health = Some(config);
392 self.config.runtime_plan.health = true;
393 self
394 }
395
396 pub fn snapshots(mut self, config: crate::snapshot::SnapshotConfig) -> Self {
399 self.config.snapshots = Some(config);
400 self
401 }
402
403 pub fn journal(mut self, config: crate::journal::JournalConfig) -> Self {
406 self.config.journal = Some(config);
407 self
408 }
409
410 pub fn reconnection(mut self) -> Self {
412 self.config.reconnection = Some(ReconnectionConfig::default());
413 self
414 }
415
416 pub fn reconnection_config(mut self, config: ReconnectionConfig) -> Self {
418 self.config.reconnection = Some(config);
419 self
420 }
421
422 pub fn http(mut self) -> Self {
426 self.config.http_health = Some(HttpHealthConfig::default());
427 self.config.runtime_plan.health = true;
428 self.config.runtime_plan.chain_reads = true;
429 self.config.runtime_plan.program_reads = true;
430 self.config.runtime_plan.stack_queries = true;
431 self
432 }
433
434 pub fn http_config(mut self, config: crate::http_server::HttpServerConfig) -> Self {
436 self.config.http_health = Some(config);
437 self.config.runtime_plan.health = true;
438 self.config.runtime_plan.chain_reads = true;
439 self.config.runtime_plan.program_reads = true;
440 self.config.runtime_plan.stack_queries = true;
441 self
442 }
443
444 pub fn transactions_config(mut self, config: TransactionConfig) -> Self {
446 self.config.runtime_plan.transactions = config.enabled;
447 self.config.transactions = Some(config);
448 self
449 }
450
451 pub fn runtime_plan(mut self, plan: RuntimePlan) -> Self {
453 self.config.runtime_plan = plan;
454 self
455 }
456
457 pub fn program_reads(mut self) -> Self {
459 if self.config.http_health.is_none() {
460 self.config.http_health = Some(HttpHealthConfig::default());
461 }
462 self.config.runtime_plan.health = true;
463 self.config.runtime_plan.program_reads = true;
464 self
465 }
466
467 pub fn program_read_binding(mut self, target_id: impl Into<String>) -> Self {
469 if self.config.http_health.is_none() {
470 self.config.http_health = Some(HttpHealthConfig::default());
471 }
472 self.config.runtime_plan.health = true;
473 self.config.runtime_plan.program_reads = true;
474 self.config.program_read_binding_target_id = Some(target_id.into());
475 self
476 }
477
478 pub fn chain_reads(mut self) -> Self {
479 if self.config.http_health.is_none() {
480 self.config.http_health = Some(HttpHealthConfig::default());
481 }
482 self.config.runtime_plan.chain_reads = true;
483 self
484 }
485
486 pub fn stack_queries(mut self) -> Self {
487 if self.config.http_health.is_none() {
488 self.config.http_health = Some(HttpHealthConfig::default());
489 }
490 self.config.runtime_plan.stack_queries = true;
491 self
492 }
493
494 pub fn live_runtime(mut self) -> Self {
495 self.config.runtime_plan.live_runtime = true;
496 self
497 }
498
499 pub fn http_bind(mut self, addr: impl Into<SocketAddr>) -> Self {
501 if let Some(http_config) = &mut self.config.http_health {
502 http_config.bind_address = addr.into();
503 } else {
504 self.config.http_health = Some(HttpHealthConfig::new(addr.into()));
505 }
506 self.config.runtime_plan.health = true;
507 self.config.runtime_plan.chain_reads = true;
508 self.config.runtime_plan.program_reads = true;
509 self.config.runtime_plan.stack_queries = true;
510 self
511 }
512
513 pub fn http_health(self) -> Self {
515 self.http()
516 }
517
518 pub fn http_health_config(self, config: HttpHealthConfig) -> Self {
520 self.http_config(config)
521 }
522
523 pub fn health_bind(self, addr: impl Into<SocketAddr>) -> Self {
525 self.http_bind(addr)
526 }
527
528 pub async fn start(self) -> Result<()> {
530 self.build()?.run().await
531 }
532
533 pub fn build(self) -> Result<Runtime> {
539 let (view_index, materialized_registry) =
540 Self::build_view_index_and_registry(self.views, self.materialized_views, &self.spec);
541
542 #[cfg(feature = "otel")]
543 let mut runtime = Runtime::new(self.config, view_index, self.metrics);
544 #[cfg(not(feature = "otel"))]
545 let mut runtime = Runtime::new(self.config, view_index);
546
547 if let Some(plugin) = self.websocket_auth_plugin {
548 runtime = runtime.with_websocket_auth_plugin(plugin);
549 }
550
551 if let Some(plugin) = self.http_auth_plugin {
552 runtime = runtime.with_http_auth_plugin(plugin);
553 }
554
555 if let Some(emitter) = self.websocket_usage_emitter {
556 runtime = runtime.with_websocket_usage_emitter(emitter);
557 }
558
559 if let Some(max_clients) = self.websocket_max_clients {
560 runtime = runtime.with_websocket_max_clients(max_clients);
561 }
562
563 if let Some(rate_limit_config) = self.websocket_rate_limit_config {
564 runtime = runtime.with_websocket_rate_limit_config(rate_limit_config);
565 }
566
567 if let Some(registry) = materialized_registry {
568 runtime = runtime.with_materialized_views(registry);
569 }
570
571 if let Some(spec) = self.spec {
572 runtime = runtime.with_spec(spec)?;
573 }
574
575 Ok(runtime)
576 }
577
578 fn build_view_index_and_registry(
579 views: Option<ViewIndex>,
580 materialized_views: Option<MaterializedViewRegistry>,
581 spec: &Option<Spec>,
582 ) -> (ViewIndex, Option<MaterializedViewRegistry>) {
583 let mut index = views.unwrap_or_default();
584 let mut registry = materialized_views;
585
586 if let Some(ref spec) = spec {
587 let entity_wire_formats = spec
588 .entity_specs
589 .iter()
590 .map(|entity_spec| {
591 (
592 entity_spec.state_name.clone(),
593 ViewSpec::wire_format_from_entity_spec(entity_spec),
594 )
595 })
596 .collect::<std::collections::HashMap<_, _>>();
597
598 for entity_name in spec.bytecode.entities.keys() {
599 let wire_format = entity_wire_formats
600 .get(entity_name)
601 .cloned()
602 .unwrap_or_default();
603 index.add_spec(ViewSpec {
604 id: format!("{}/list", entity_name),
605 export: entity_name.clone(),
606 mode: Mode::List,
607 wire_format: wire_format.clone(),
608 projection: Projection::all(),
609 filters: Filters::all(),
610 delivery: Delivery::default(),
611 pipeline: None,
612 source_view: None,
613 });
614
615 index.add_spec(ViewSpec {
616 id: format!("{}/state", entity_name),
617 export: entity_name.clone(),
618 mode: Mode::State,
619 wire_format: wire_format.clone(),
620 projection: Projection::all(),
621 filters: Filters::all(),
622 delivery: Delivery::default(),
623 pipeline: None,
624 source_view: None,
625 });
626
627 index.add_spec(ViewSpec {
628 id: format!("{}/append", entity_name),
629 export: entity_name.clone(),
630 mode: Mode::Append,
631 wire_format,
632 projection: Projection::all(),
633 filters: Filters::all(),
634 delivery: Delivery::default(),
635 pipeline: None,
636 source_view: None,
637 });
638 }
639
640 if !spec.views.is_empty() {
641 let reg = registry.get_or_insert_with(MaterializedViewRegistry::new);
642
643 for view_def in &spec.views {
644 let export = match &view_def.source {
645 arete_interpreter::ast::ViewSource::Entity { name } => name.clone(),
646 arete_interpreter::ast::ViewSource::View { id } => {
647 id.split('/').next().unwrap_or(id).to_string()
648 }
649 };
650
651 if Self::is_canonical_entity_view(view_def, &export) {
658 tracing::debug!(
659 view_id = %view_def.id,
660 "Keeping canonical entity view backed by the native cache"
661 );
662 continue;
663 }
664
665 let wire_format = entity_wire_formats
666 .get(&export)
667 .cloned()
668 .unwrap_or_default();
669 let view_spec = ViewSpec::from_view_def(view_def, &export, wire_format);
670 let pipeline = view_spec.pipeline.clone().unwrap_or_default();
671 let source_id = view_spec.source_view.clone().unwrap_or_default();
672 tracing::debug!(
673 view_id = %view_def.id,
674 source = %source_id,
675 "Registering derived view"
676 );
677
678 index.add_spec(view_spec);
679
680 let materialized =
681 MaterializedView::new(view_def.id.clone(), source_id, pipeline);
682 reg.register(materialized);
683 }
684 }
685 }
686
687 (index, registry)
688 }
689
690 fn is_canonical_entity_view(view_def: &ViewDef, export: &str) -> bool {
691 use arete_interpreter::ast::{ViewOutput, ViewSource};
692
693 let ViewSource::Entity { name } = &view_def.source else {
694 return false;
695 };
696 if name != export || !view_def.pipeline.is_empty() {
697 return false;
698 }
699
700 match &view_def.output {
701 ViewOutput::Collection => view_def.id == format!("{export}/list"),
702 ViewOutput::Single | ViewOutput::Keyed { .. } => {
703 view_def.id == format!("{export}/state")
704 }
705 }
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712
713 #[test]
714 fn test_builder_pattern() {
715 let _builder = Server::builder()
716 .websocket()
717 .bind("[::]:8877".parse::<SocketAddr>().unwrap());
718 }
719
720 #[test]
721 fn test_spec_creation() {
722 let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new().build();
723 let spec = Spec::new(bytecode, "test_program");
724 assert_eq!(
725 spec.program_ids.first().map(String::as_str),
726 Some("test_program")
727 );
728 }
729
730 #[test]
731 fn http_without_websocket_has_a_read_only_runtime_plan() {
732 let builder = Server::builder().http();
733 assert!(builder.config.runtime_plan.program_reads);
734 assert!(!builder.config.runtime_plan.live_runtime_enabled());
735 }
736
737 #[test]
738 fn program_read_binding_configures_the_exact_auth_target() {
739 let builder = Server::builder().program_read_binding("binding-1");
740
741 assert!(builder.config.runtime_plan.program_reads);
742 assert_eq!(
743 builder.config.program_read_binding_target_id.as_deref(),
744 Some("binding-1")
745 );
746 }
747
748 #[test]
749 fn websocket_and_http_preserve_all_in_one_runtime_behavior() {
750 let builder = Server::builder().websocket().http();
751 assert!(builder.config.runtime_plan.websocket);
752 assert!(builder.config.runtime_plan.live_runtime_enabled());
753 }
754
755 #[test]
756 fn explicit_hosted_plan_disables_program_reads_after_http_helpers() {
757 let plan = RuntimePlan {
758 health: true,
759 chain_reads: true,
760 program_reads: false,
761 stack_queries: true,
762 transactions: true,
763 websocket: true,
764 live_runtime: true,
765 };
766 let builder = Server::builder()
767 .websocket()
768 .http_health()
769 .health_bind("[::]:8081".parse::<SocketAddr>().unwrap())
770 .runtime_plan(plan);
771
772 assert_eq!(builder.config.runtime_plan, plan);
773 }
774
775 #[test]
776 fn solana_gateway_builder_excludes_stack_and_live_capabilities() {
777 let builder = Server::solana_gateway("gateway-us-east-1");
778
779 assert_eq!(
780 builder.inner.config.runtime_plan,
781 RuntimePlan::solana_gateway()
782 );
783 assert!(builder.inner.config.http_health.is_some());
784 assert_eq!(
785 builder.inner.config.solana_gateway_target_id.as_deref(),
786 Some("gateway-us-east-1")
787 );
788 assert!(builder.inner.config.websocket.is_none());
789 assert!(builder.inner.config.yellowstone.is_none());
790 assert!(builder.inner.spec.is_none());
791 assert!(builder.inner.views.is_none());
792 assert!(builder.inner.materialized_views.is_none());
793 assert!(!builder.inner.config.runtime_plan.websocket);
794 assert!(!builder.inner.config.runtime_plan.live_runtime_enabled());
795 assert!(!builder.inner.config.runtime_plan.stack_queries);
796 assert!(!builder.inner.config.runtime_plan.program_reads);
797 }
798
799 #[test]
800 fn solana_gateway_builder_rejects_invalid_gateway_configuration() {
801 assert!(Server::solana_gateway("").build().is_err());
802 assert!(Server::solana_gateway("gateway-us-east-1")
803 .transactions_config(TransactionConfig::default())
804 .build()
805 .is_err());
806 }
807
808 #[test]
809 fn builder_rejects_mismatched_program_release_definitions() {
810 let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new().build();
811 let definition = ProgramRuntimeDefinition {
812 program_id: "Program111".to_string(),
813 program_spec_hash: ProgramSpecHash::from_digest([1; 32]),
814 idl_content_hash: IdlContentHash::from_digest([2; 32]),
815 normalized_idl_hash: NormalizedIdlHash::from_digest([3; 32]),
816 program_release_hash: ProgramReleaseHash::from_digest([4; 32]),
817 account_reader: Arc::new(|_, _| Ok(serde_json::Value::Null)),
818 };
819 let spec =
820 Spec::new(bytecode, "Program111").with_program_runtime_definitions(vec![definition]);
821
822 assert!(Server::builder().spec(spec).build().is_err());
823 }
824
825 #[test]
826 fn canonical_entity_views_are_not_reclassified_as_derived_views() {
827 use arete_interpreter::ast::{IdentitySpec, TypedStreamSpec, ViewDef, ViewSource};
828
829 let list = ViewDef::list("OreBoard");
830 let state = ViewDef::state("OreBoard", &["id", "address"]);
831 assert!(ServerBuilder::is_canonical_entity_view(&list, "OreBoard"));
832 assert!(ServerBuilder::is_canonical_entity_view(&state, "OreBoard"));
833
834 let mut named_view = ViewDef::list("OreBoard");
835 named_view.id = "OreBoard/all".to_string();
836 assert!(!ServerBuilder::is_canonical_entity_view(
837 &named_view,
838 "OreBoard"
839 ));
840
841 let mut chained_view = ViewDef::list("OreBoard");
842 chained_view.source = ViewSource::View {
843 id: "OreBoard/list".to_string(),
844 };
845 assert!(!ServerBuilder::is_canonical_entity_view(
846 &chained_view,
847 "OreBoard"
848 ));
849
850 let entity_spec = TypedStreamSpec::<serde_json::Value>::new(
851 "OreBoard".to_string(),
852 IdentitySpec {
853 primary_keys: vec!["id.address".to_string()],
854 lookup_indexes: Vec::new(),
855 },
856 Vec::new(),
857 );
858 let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new()
859 .add_entity("OreBoard".to_string(), entity_spec, 1)
860 .build();
861 let spec = Some(Spec::new(bytecode, "Program111").with_views(vec![list, state]));
862
863 let (index, _) = ServerBuilder::build_view_index_and_registry(None, None, &spec);
864 assert!(!index
865 .get_view("OreBoard/list")
866 .expect("list view should exist")
867 .is_derived());
868 assert!(!index
869 .get_view("OreBoard/state")
870 .expect("state view should exist")
871 .is_derived());
872 assert_eq!(index.by_export("OreBoard").len(), 3);
873 }
874}