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, 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 bind(mut self, addr: impl Into<SocketAddr>) -> Self {
358 if let Some(ws_config) = &mut self.config.websocket {
359 ws_config.bind_address = addr.into();
360 } else {
361 self.config.websocket = Some(WebSocketConfig::new(addr.into()));
362 }
363 self.config.runtime_plan.websocket = true;
364 self.config.runtime_plan.live_runtime = true;
365 self
366 }
367
368 pub fn yellowstone(mut self, config: YellowstoneConfig) -> Self {
370 self.config.yellowstone = Some(config);
371 self.config.runtime_plan.live_runtime = true;
372 self
373 }
374
375 pub fn health_monitoring(mut self) -> Self {
377 self.config.health = Some(HealthConfig::default());
378 self.config.runtime_plan.health = true;
379 self
380 }
381
382 pub fn health_config(mut self, config: HealthConfig) -> Self {
384 self.config.health = Some(config);
385 self.config.runtime_plan.health = true;
386 self
387 }
388
389 pub fn snapshots(mut self, config: crate::snapshot::SnapshotConfig) -> Self {
392 self.config.snapshots = Some(config);
393 self
394 }
395
396 pub fn journal(mut self, config: crate::journal::JournalConfig) -> Self {
399 self.config.journal = Some(config);
400 self
401 }
402
403 pub fn reconnection(mut self) -> Self {
405 self.config.reconnection = Some(ReconnectionConfig::default());
406 self
407 }
408
409 pub fn reconnection_config(mut self, config: ReconnectionConfig) -> Self {
411 self.config.reconnection = Some(config);
412 self
413 }
414
415 pub fn http(mut self) -> Self {
419 self.config.http_health = Some(HttpHealthConfig::default());
420 self.config.runtime_plan.health = true;
421 self.config.runtime_plan.chain_reads = true;
422 self.config.runtime_plan.program_reads = true;
423 self.config.runtime_plan.stack_queries = true;
424 self
425 }
426
427 pub fn http_config(mut self, config: crate::http_server::HttpServerConfig) -> Self {
429 self.config.http_health = Some(config);
430 self.config.runtime_plan.health = true;
431 self.config.runtime_plan.chain_reads = true;
432 self.config.runtime_plan.program_reads = true;
433 self.config.runtime_plan.stack_queries = true;
434 self
435 }
436
437 pub fn transactions_config(mut self, config: TransactionConfig) -> Self {
439 self.config.runtime_plan.transactions = config.enabled;
440 self.config.transactions = Some(config);
441 self
442 }
443
444 pub fn runtime_plan(mut self, plan: RuntimePlan) -> Self {
446 self.config.runtime_plan = plan;
447 self
448 }
449
450 pub fn program_reads(mut self) -> Self {
452 if self.config.http_health.is_none() {
453 self.config.http_health = Some(HttpHealthConfig::default());
454 }
455 self.config.runtime_plan.health = true;
456 self.config.runtime_plan.program_reads = true;
457 self
458 }
459
460 pub fn program_read_binding(mut self, target_id: impl Into<String>) -> Self {
462 if self.config.http_health.is_none() {
463 self.config.http_health = Some(HttpHealthConfig::default());
464 }
465 self.config.runtime_plan.health = true;
466 self.config.runtime_plan.program_reads = true;
467 self.config.program_read_binding_target_id = Some(target_id.into());
468 self
469 }
470
471 pub fn chain_reads(mut self) -> Self {
472 if self.config.http_health.is_none() {
473 self.config.http_health = Some(HttpHealthConfig::default());
474 }
475 self.config.runtime_plan.chain_reads = true;
476 self
477 }
478
479 pub fn stack_queries(mut self) -> Self {
480 if self.config.http_health.is_none() {
481 self.config.http_health = Some(HttpHealthConfig::default());
482 }
483 self.config.runtime_plan.stack_queries = true;
484 self
485 }
486
487 pub fn live_runtime(mut self) -> Self {
488 self.config.runtime_plan.live_runtime = true;
489 self
490 }
491
492 pub fn http_bind(mut self, addr: impl Into<SocketAddr>) -> Self {
494 if let Some(http_config) = &mut self.config.http_health {
495 http_config.bind_address = addr.into();
496 } else {
497 self.config.http_health = Some(HttpHealthConfig::new(addr.into()));
498 }
499 self.config.runtime_plan.health = true;
500 self.config.runtime_plan.chain_reads = true;
501 self.config.runtime_plan.program_reads = true;
502 self.config.runtime_plan.stack_queries = true;
503 self
504 }
505
506 pub fn http_health(self) -> Self {
508 self.http()
509 }
510
511 pub fn http_health_config(self, config: HttpHealthConfig) -> Self {
513 self.http_config(config)
514 }
515
516 pub fn health_bind(self, addr: impl Into<SocketAddr>) -> Self {
518 self.http_bind(addr)
519 }
520
521 pub async fn start(self) -> Result<()> {
523 self.build()?.run().await
524 }
525
526 pub fn build(self) -> Result<Runtime> {
532 let (view_index, materialized_registry) =
533 Self::build_view_index_and_registry(self.views, self.materialized_views, &self.spec);
534
535 #[cfg(feature = "otel")]
536 let mut runtime = Runtime::new(self.config, view_index, self.metrics);
537 #[cfg(not(feature = "otel"))]
538 let mut runtime = Runtime::new(self.config, view_index);
539
540 if let Some(plugin) = self.websocket_auth_plugin {
541 runtime = runtime.with_websocket_auth_plugin(plugin);
542 }
543
544 if let Some(plugin) = self.http_auth_plugin {
545 runtime = runtime.with_http_auth_plugin(plugin);
546 }
547
548 if let Some(emitter) = self.websocket_usage_emitter {
549 runtime = runtime.with_websocket_usage_emitter(emitter);
550 }
551
552 if let Some(max_clients) = self.websocket_max_clients {
553 runtime = runtime.with_websocket_max_clients(max_clients);
554 }
555
556 if let Some(rate_limit_config) = self.websocket_rate_limit_config {
557 runtime = runtime.with_websocket_rate_limit_config(rate_limit_config);
558 }
559
560 if let Some(registry) = materialized_registry {
561 runtime = runtime.with_materialized_views(registry);
562 }
563
564 if let Some(spec) = self.spec {
565 runtime = runtime.with_spec(spec)?;
566 }
567
568 Ok(runtime)
569 }
570
571 fn build_view_index_and_registry(
572 views: Option<ViewIndex>,
573 materialized_views: Option<MaterializedViewRegistry>,
574 spec: &Option<Spec>,
575 ) -> (ViewIndex, Option<MaterializedViewRegistry>) {
576 let mut index = views.unwrap_or_default();
577 let mut registry = materialized_views;
578
579 if let Some(ref spec) = spec {
580 let entity_wire_formats = spec
581 .entity_specs
582 .iter()
583 .map(|entity_spec| {
584 (
585 entity_spec.state_name.clone(),
586 ViewSpec::wire_format_from_entity_spec(entity_spec),
587 )
588 })
589 .collect::<std::collections::HashMap<_, _>>();
590
591 for entity_name in spec.bytecode.entities.keys() {
592 let wire_format = entity_wire_formats
593 .get(entity_name)
594 .cloned()
595 .unwrap_or_default();
596 index.add_spec(ViewSpec {
597 id: format!("{}/list", entity_name),
598 export: entity_name.clone(),
599 mode: Mode::List,
600 wire_format: wire_format.clone(),
601 projection: Projection::all(),
602 filters: Filters::all(),
603 delivery: Delivery::default(),
604 pipeline: None,
605 source_view: None,
606 });
607
608 index.add_spec(ViewSpec {
609 id: format!("{}/state", entity_name),
610 export: entity_name.clone(),
611 mode: Mode::State,
612 wire_format: wire_format.clone(),
613 projection: Projection::all(),
614 filters: Filters::all(),
615 delivery: Delivery::default(),
616 pipeline: None,
617 source_view: None,
618 });
619
620 index.add_spec(ViewSpec {
621 id: format!("{}/append", entity_name),
622 export: entity_name.clone(),
623 mode: Mode::Append,
624 wire_format,
625 projection: Projection::all(),
626 filters: Filters::all(),
627 delivery: Delivery::default(),
628 pipeline: None,
629 source_view: None,
630 });
631 }
632
633 if !spec.views.is_empty() {
634 let reg = registry.get_or_insert_with(MaterializedViewRegistry::new);
635
636 for view_def in &spec.views {
637 let export = match &view_def.source {
638 arete_interpreter::ast::ViewSource::Entity { name } => name.clone(),
639 arete_interpreter::ast::ViewSource::View { id } => {
640 id.split('/').next().unwrap_or(id).to_string()
641 }
642 };
643
644 if Self::is_canonical_entity_view(view_def, &export) {
651 tracing::debug!(
652 view_id = %view_def.id,
653 "Keeping canonical entity view backed by the native cache"
654 );
655 continue;
656 }
657
658 let wire_format = entity_wire_formats
659 .get(&export)
660 .cloned()
661 .unwrap_or_default();
662 let view_spec = ViewSpec::from_view_def(view_def, &export, wire_format);
663 let pipeline = view_spec.pipeline.clone().unwrap_or_default();
664 let source_id = view_spec.source_view.clone().unwrap_or_default();
665 tracing::debug!(
666 view_id = %view_def.id,
667 source = %source_id,
668 "Registering derived view"
669 );
670
671 index.add_spec(view_spec);
672
673 let materialized =
674 MaterializedView::new(view_def.id.clone(), source_id, pipeline);
675 reg.register(materialized);
676 }
677 }
678 }
679
680 (index, registry)
681 }
682
683 fn is_canonical_entity_view(view_def: &ViewDef, export: &str) -> bool {
684 use arete_interpreter::ast::{ViewOutput, ViewSource};
685
686 let ViewSource::Entity { name } = &view_def.source else {
687 return false;
688 };
689 if name != export || !view_def.pipeline.is_empty() {
690 return false;
691 }
692
693 match &view_def.output {
694 ViewOutput::Collection => view_def.id == format!("{export}/list"),
695 ViewOutput::Single | ViewOutput::Keyed { .. } => {
696 view_def.id == format!("{export}/state")
697 }
698 }
699 }
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705
706 #[test]
707 fn test_builder_pattern() {
708 let _builder = Server::builder()
709 .websocket()
710 .bind("[::]:8877".parse::<SocketAddr>().unwrap());
711 }
712
713 #[test]
714 fn test_spec_creation() {
715 let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new().build();
716 let spec = Spec::new(bytecode, "test_program");
717 assert_eq!(
718 spec.program_ids.first().map(String::as_str),
719 Some("test_program")
720 );
721 }
722
723 #[test]
724 fn http_without_websocket_has_a_read_only_runtime_plan() {
725 let builder = Server::builder().http();
726 assert!(builder.config.runtime_plan.program_reads);
727 assert!(!builder.config.runtime_plan.live_runtime_enabled());
728 }
729
730 #[test]
731 fn program_read_binding_configures_the_exact_auth_target() {
732 let builder = Server::builder().program_read_binding("binding-1");
733
734 assert!(builder.config.runtime_plan.program_reads);
735 assert_eq!(
736 builder.config.program_read_binding_target_id.as_deref(),
737 Some("binding-1")
738 );
739 }
740
741 #[test]
742 fn websocket_and_http_preserve_all_in_one_runtime_behavior() {
743 let builder = Server::builder().websocket().http();
744 assert!(builder.config.runtime_plan.websocket);
745 assert!(builder.config.runtime_plan.live_runtime_enabled());
746 }
747
748 #[test]
749 fn explicit_hosted_plan_disables_program_reads_after_http_helpers() {
750 let plan = RuntimePlan {
751 health: true,
752 chain_reads: true,
753 program_reads: false,
754 stack_queries: true,
755 transactions: true,
756 websocket: true,
757 live_runtime: true,
758 };
759 let builder = Server::builder()
760 .websocket()
761 .http_health()
762 .health_bind("[::]:8081".parse::<SocketAddr>().unwrap())
763 .runtime_plan(plan);
764
765 assert_eq!(builder.config.runtime_plan, plan);
766 }
767
768 #[test]
769 fn solana_gateway_builder_excludes_stack_and_live_capabilities() {
770 let builder = Server::solana_gateway("gateway-us-east-1");
771
772 assert_eq!(
773 builder.inner.config.runtime_plan,
774 RuntimePlan::solana_gateway()
775 );
776 assert!(builder.inner.config.http_health.is_some());
777 assert_eq!(
778 builder.inner.config.solana_gateway_target_id.as_deref(),
779 Some("gateway-us-east-1")
780 );
781 assert!(builder.inner.config.websocket.is_none());
782 assert!(builder.inner.config.yellowstone.is_none());
783 assert!(builder.inner.spec.is_none());
784 assert!(builder.inner.views.is_none());
785 assert!(builder.inner.materialized_views.is_none());
786 assert!(!builder.inner.config.runtime_plan.websocket);
787 assert!(!builder.inner.config.runtime_plan.live_runtime_enabled());
788 assert!(!builder.inner.config.runtime_plan.stack_queries);
789 assert!(!builder.inner.config.runtime_plan.program_reads);
790 }
791
792 #[test]
793 fn solana_gateway_builder_rejects_invalid_gateway_configuration() {
794 assert!(Server::solana_gateway("").build().is_err());
795 assert!(Server::solana_gateway("gateway-us-east-1")
796 .transactions_config(TransactionConfig::default())
797 .build()
798 .is_err());
799 }
800
801 #[test]
802 fn builder_rejects_mismatched_program_release_definitions() {
803 let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new().build();
804 let definition = ProgramRuntimeDefinition {
805 program_id: "Program111".to_string(),
806 program_spec_hash: ProgramSpecHash::from_digest([1; 32]),
807 idl_content_hash: IdlContentHash::from_digest([2; 32]),
808 normalized_idl_hash: NormalizedIdlHash::from_digest([3; 32]),
809 program_release_hash: ProgramReleaseHash::from_digest([4; 32]),
810 account_reader: Arc::new(|_, _| Ok(serde_json::Value::Null)),
811 };
812 let spec =
813 Spec::new(bytecode, "Program111").with_program_runtime_definitions(vec![definition]);
814
815 assert!(Server::builder().spec(spec).build().is_err());
816 }
817
818 #[test]
819 fn canonical_entity_views_are_not_reclassified_as_derived_views() {
820 use arete_interpreter::ast::{IdentitySpec, TypedStreamSpec, ViewDef, ViewSource};
821
822 let list = ViewDef::list("OreBoard");
823 let state = ViewDef::state("OreBoard", &["id", "address"]);
824 assert!(ServerBuilder::is_canonical_entity_view(&list, "OreBoard"));
825 assert!(ServerBuilder::is_canonical_entity_view(&state, "OreBoard"));
826
827 let mut named_view = ViewDef::list("OreBoard");
828 named_view.id = "OreBoard/all".to_string();
829 assert!(!ServerBuilder::is_canonical_entity_view(
830 &named_view,
831 "OreBoard"
832 ));
833
834 let mut chained_view = ViewDef::list("OreBoard");
835 chained_view.source = ViewSource::View {
836 id: "OreBoard/list".to_string(),
837 };
838 assert!(!ServerBuilder::is_canonical_entity_view(
839 &chained_view,
840 "OreBoard"
841 ));
842
843 let entity_spec = TypedStreamSpec::<serde_json::Value>::new(
844 "OreBoard".to_string(),
845 IdentitySpec {
846 primary_keys: vec!["id.address".to_string()],
847 lookup_indexes: Vec::new(),
848 },
849 Vec::new(),
850 );
851 let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new()
852 .add_entity("OreBoard".to_string(), entity_spec, 1)
853 .build();
854 let spec = Some(Spec::new(bytecode, "Program111").with_views(vec![list, state]));
855
856 let (index, _) = ServerBuilder::build_view_index_and_registry(None, None, &spec);
857 assert!(!index
858 .get_view("OreBoard/list")
859 .expect("list view should exist")
860 .is_derived());
861 assert!(!index
862 .get_view("OreBoard/state")
863 .expect("state view should exist")
864 .is_derived());
865 assert_eq!(index.by_export("OreBoard").len(), 3);
866 }
867}