allora_runtime/dsl/
mod.rs

1//! DSL Facade: multi-format (YAML today; JSON/XML forthcoming) configuration entry points.
2//!
3//! This module orchestrates conversion of external textual configuration into runtime
4//! components by coordinating three layers:
5//! 1. Parsers (in `spec/`): format-specific translation into strongly typed specs.
6//! 2. Builders (in `dsl/component_builders.rs`): spec -> concrete runtime objects.
7//! 3. Facade (this file): public ergonomic API + format inference.
8//!
9//! # Goals
10//! * Provide a minimal, stable surface (`build`, `build_channel`, `build_channel_from_str`, `build_filter`, `DslFormat`).
11//! * Keep parsing & instantiation decoupled so additional formats/components add minimal code.
12//! * Fail fast with clear, categorized errors (`Error::Serialization` vs `Error::Other`).
13//!
14//! # Supported Components (v1)
15//! * Channel (InMemory kind)
16//! * Filter (single filter spec via `build_filter` AND aggregated when present in top-level `allora.yml` into `AlloraRuntime`)
17//! * Service (single service spec via `build_service`; aggregation forthcoming when services spec collection is added)
18//!
19//! # Supported Formats
20//! * YAML (`DslFormat::Yaml`)
21//! * JSON / XML reserved (emit explicit unsupported errors)
22//!
23//! # Building a Single Channel
24//! ```no_run
25//! use allora_core::Channel;
26//! use allora_runtime::build_channel;
27//! let ch = build_channel("tests/fixtures/channel.yml").unwrap();
28//! println!("channel id={}", ch.id());
29//! ```
30//!
31//! # Building a Single Filter
32//! ```no_run
33//! use allora_runtime::build_filter;
34//! // Requires tests/fixtures/filter.yml present.
35//! let f = build_filter("tests/fixtures/filter.yml").unwrap();
36//! // apply using f.accepts(exchange)
37//! ```
38//!
39//! # Building a Single Service
40//! ```no_run
41//! use allora_runtime::build_service;
42//! // Requires tests/fixtures/service.yml present.
43//! let svc = build_service("tests/fixtures/service.yml").unwrap();
44//! // invoke via svc.process_sync(&mut exchange)
45//! ```
46//!
47//! # Building Multiple Filters (collection spec)
48//! ```no_run
49//! use allora_runtime::spec::FiltersSpecYamlParser;
50//! use allora_runtime::dsl::component_builders::build_filters_from_spec;
51//! let raw = std::fs::read_to_string("tests/fixtures/filters.yml").unwrap();
52//! let spec = FiltersSpecYamlParser::parse_str(&raw).unwrap();
53//! let filters = build_filters_from_spec(spec).unwrap();
54//! assert!(!filters.is_empty());
55//! ```
56//!
57//! # Building the Full Runtime (AlloraRuntime)
58//! ```no_run
59//! use allora_runtime::{build, Channel, Filter};
60//! let rt = build("tests/fixtures/allora.yml").unwrap();
61//! assert!(rt.channel_by_id("inbound.orders").is_some());
62//! assert!(rt.filters().len() >= 1);
63//! ```
64//! Runtime accessors now: `channels()`, `channel_by_id()`, `channel_count()`, `filters()`, `filter_count()`, plus ownership via `into_channels()` / `into_filters()`.
65//!
66//! # Access Patterns
67//! * Borrow: `rt.channels()`, `rt.filters()`
68//! * Lookup: `rt.channel_by_id("inbound.orders")`
69//! * Counts: `rt.channel_count()`, `rt.filter_count()`
70//! * Consume: `rt.into_channels()`, `rt.into_filters()`
71//!
72//! # Error Semantics
73//! * `Error::Other` – I/O failures (e.g. unreadable file path)
74//! * `Error::Serialization` – structural issues (missing fields / invalid values / unsupported version / unsupported format)
75//!
76//! # Extension Guide (Adding New Components)
77//! 1. Define data model spec (`*_spec.rs`)
78//! 2. Add parser (`*_spec_yaml.rs`) validating version + fields
79//! 3. Extend `AlloraSpec` to hold new spec collection (e.g. filters, endpoints)
80//! 4. Add builder in `component_builders.rs`
81//! 5. Augment `build_runtime_from_str` to assemble new runtime objects
82//! 6. Add accessors on `AlloraRuntime`
83//!
84//! # Format Addition (JSON Outline)
85//! * Introduce `*_json_parser` implementing `parse_str`
86//! * Branch in `build_*_from_str` and `build_runtime_from_str`
87//! * Reuse existing builders (format-agnostic)
88//!
89//! # Testing Strategy
90//! * Parser edge cases near parser modules (`*_spec_yaml.rs`)
91//! * Builder invariants in dedicated tests (channels, filters)
92//! * Facade behavior & runtime aggregation in runtime-focused tests
93//!
94//! # Versioning
95//! * Each spec parser validates an explicit integer `version`
96//! * Breaking changes add parallel parser modules (`*_v2_yaml.rs`) preserving old behavior
97//!
98//! # Internal Helpers (Non-Public)
99//! * `build_runtime_from_str` – dispatcher from raw text + format (now aggregates filters when present)
100//! * `build_filter_from_str` – kept private
101//!
102//! # Roadmap
103//! * Multiple channel kinds (kafka, amqp)
104//! * Additional pattern components (routers, splitters) aggregated in runtime
105//! * Endpoints & adapters (HTTP, file, custom transport)
106//! * JSON/XML DSL formats
107//! * Expanded expression language (parentheses, negation, path navigation)
108//!
109//! This documentation focuses on architecture & extensibility; see component modules for specifics.
110
111use crate::{
112    service_activator_processor::ServiceActivatorProcessor,
113    spec::{
114        AlloraSpecYamlParser, ChannelSpecYamlParser, FilterSpecYamlParser, ServiceSpecYamlParser,
115    },
116    Channel, Error, Filter, Result,
117};
118use component_builders::{
119    build_channel_from_spec, build_channels_from_spec, build_filter_from_spec,
120    build_filters_from_spec, build_http_inbound_adapters_from_spec, build_service_from_spec,
121    build_http_outbound_adapters_from_spec,
122};
123use std::path::Path;
124
125pub mod component_builders;
126pub mod runtime;
127use runtime::AlloraRuntime;
128
129/// Supported DSL input formats.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum DslFormat {
132    Yaml,
133    Json,
134    Xml,
135}
136
137impl DslFormat {
138    /// Infer format from file extension.
139    pub fn from_path(path: &Path) -> Option<Self> {
140        match path
141            .extension()
142            .and_then(|e| e.to_str())
143            .map(|s| s.to_ascii_lowercase())
144        {
145            Some(ref ext) if ext == "yml" || ext == "yaml" => Some(DslFormat::Yaml),
146            Some(ref ext) if ext == "json" => Some(DslFormat::Json),
147            Some(ref ext) if ext == "xml" => Some(DslFormat::Xml),
148            _ => None,
149        }
150    }
151}
152
153/// Build channel from raw string + specified format.
154pub fn build_channel_from_str(raw: &str, format: DslFormat) -> Result<Box<dyn Channel>> {
155    match format {
156        DslFormat::Yaml => {
157            let spec = ChannelSpecYamlParser::parse_str(raw)?;
158            build_channel_from_spec(spec)
159        }
160        DslFormat::Json => Err(Error::serialization("json format not yet supported")),
161        DslFormat::Xml => Err(Error::serialization("xml format not yet supported")),
162    }
163}
164
165/// Convenience: build channel from a file path (auto-detect format via extension).
166pub fn build_channel(path: impl AsRef<Path>) -> Result<Box<dyn Channel>> {
167    let path_ref = path.as_ref();
168    let raw =
169        std::fs::read_to_string(path_ref).map_err(|e| Error::other(format!("read error: {e}")))?;
170    let format = DslFormat::from_path(path_ref)
171        .ok_or_else(|| Error::serialization("cannot infer DSL format from extension"))?;
172    build_channel_from_str(&raw, format)
173}
174
175/// Build filter from raw string + specified format.
176fn build_filter_from_str(raw: &str, format: DslFormat) -> Result<Filter> {
177    match format {
178        DslFormat::Yaml => {
179            let spec = FilterSpecYamlParser::parse_str(raw)?;
180            build_filter_from_spec(spec)
181        }
182        DslFormat::Json => Err(Error::serialization("json format not yet supported")),
183        DslFormat::Xml => Err(Error::serialization("xml format not yet supported")),
184    }
185}
186
187/// Convenience: build filter from a file path (auto-detect format via extension).
188pub fn build_filter(path: impl AsRef<Path>) -> Result<Filter> {
189    let path_ref = path.as_ref();
190    let raw =
191        std::fs::read_to_string(path_ref).map_err(|e| Error::other(format!("read error: {e}")))?;
192    let format = DslFormat::from_path(path_ref)
193        .ok_or_else(|| Error::serialization("cannot infer DSL format from extension"))?;
194    build_filter_from_str(&raw, format)
195}
196
197/// Build service from raw string + specified format.
198fn build_service_from_str(
199    raw: &str,
200    format: DslFormat,
201) -> Result<component_builders::ServiceProcessor> {
202    match format {
203        DslFormat::Yaml => {
204            let spec = ServiceSpecYamlParser::parse_str(raw)?;
205            build_service_from_spec(spec)
206        }
207        DslFormat::Json => Err(Error::serialization("json format not yet supported")),
208        DslFormat::Xml => Err(Error::serialization("xml format not yet supported")),
209    }
210}
211
212/// Convenience: build service from a file path (auto-detect format via extension).
213pub fn build_service(path: impl AsRef<Path>) -> Result<component_builders::ServiceProcessor> {
214    let path_ref = path.as_ref();
215    let raw =
216        std::fs::read_to_string(path_ref).map_err(|e| Error::other(format!("read error: {e}")))?;
217    let format = DslFormat::from_path(path_ref)
218        .ok_or_else(|| Error::serialization("cannot infer DSL format from extension"))?;
219    build_service_from_str(&raw, format)
220}
221
222/// Internal helper: build full runtime from raw + format.
223fn build_runtime_from_str(raw: &str, format: DslFormat) -> Result<AlloraRuntime> {
224    match format {
225        DslFormat::Yaml => {
226            let top = AlloraSpecYamlParser::parse_str(raw)?;
227            let filters_spec = top.filters_spec().cloned();
228            let services_spec = top.services_spec().cloned();
229            let http_inbound_spec = top.http_inbound_adapters_spec().cloned();
230            let http_outbound_spec = top.http_outbound_adapters_spec().cloned();
231            let channels_spec = top.into_channels_spec();
232            let channels = build_channels_from_spec(channels_spec)?;
233            let mut rt = AlloraRuntime::new(channels);
234            if let Some(fspec) = filters_spec {
235                let filters = build_filters_from_spec(fspec)?;
236                rt = rt.with_filters(filters);
237            }
238            if let Some(sspec) = services_spec {
239                let services =
240                    component_builders::build_service_activators_from_spec(sspec.clone())?;
241                rt = rt.with_services(services);
242                // Build activator processors from original specs (clone services spec entries)
243                let mut procs = Vec::new();
244                for s in sspec.services_activators() {
245                    procs.push(ServiceActivatorProcessor::new(s.clone()));
246                }
247                rt = rt.with_service_processors(procs);
248            }
249            if let Some(hspec) = http_inbound_spec {
250                let lookup = |id: &str| rt.channel_ref_by_id(id);
251                let adapters = build_http_inbound_adapters_from_spec(hspec, &lookup)?;
252                rt = rt.with_http_inbound_adapters(adapters);
253            }
254            if let Some(ospec) = http_outbound_spec {
255                let outbound = build_http_outbound_adapters_from_spec(ospec)?;
256                rt = rt.with_http_outbound_adapters(outbound);
257            }
258            Ok(rt)
259        }
260        DslFormat::Json => Err(Error::serialization("json format not yet supported")),
261        DslFormat::Xml => Err(Error::serialization("xml format not yet supported")),
262    }
263}
264
265/// Public: build full runtime from a file path (future: endpoints, filters, etc.).
266pub fn build(path: impl AsRef<Path>) -> Result<AlloraRuntime> {
267    let path_ref = path.as_ref();
268    let raw =
269        std::fs::read_to_string(path_ref).map_err(|e| Error::other(format!("read error: {e}")))?;
270    let format = DslFormat::from_path(path_ref)
271        .ok_or_else(|| Error::serialization("cannot infer DSL format from extension"))?;
272    build_runtime_from_str(&raw, format)
273}