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