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