Skip to main content

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,
121    build_http_outbound_adapters_from_spec, build_service_from_spec,
122};
123use std::path::Path;
124
125pub mod component_builders;
126pub mod pattern_registry;
127pub mod runtime;
128pub use pattern_registry::{
129    PatternRegistry, STRATEGY_CONCAT_TEXT, STRATEGY_EMIT_SIGNAL, STRATEGY_JSON_ARRAY,
130};
131use runtime::AlloraRuntime;
132
133/// Supported DSL input formats.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum DslFormat {
136    Yaml,
137    Json,
138    Xml,
139}
140
141impl DslFormat {
142    /// Infer format from file extension.
143    pub fn from_path(path: &Path) -> Option<Self> {
144        match path
145            .extension()
146            .and_then(|e| e.to_str())
147            .map(|s| s.to_ascii_lowercase())
148        {
149            Some(ref ext) if ext == "yml" || ext == "yaml" => Some(DslFormat::Yaml),
150            Some(ref ext) if ext == "json" => Some(DslFormat::Json),
151            Some(ref ext) if ext == "xml" => Some(DslFormat::Xml),
152            _ => None,
153        }
154    }
155}
156
157/// Build channel from raw string + specified format.
158pub fn build_channel_from_str(raw: &str, format: DslFormat) -> Result<Box<dyn Channel>> {
159    match format {
160        DslFormat::Yaml => {
161            let spec = ChannelSpecYamlParser::parse_str(raw)?;
162            build_channel_from_spec(spec)
163        }
164        DslFormat::Json => Err(Error::serialization("json format not yet supported")),
165        DslFormat::Xml => Err(Error::serialization("xml format not yet supported")),
166    }
167}
168
169/// Convenience: build channel from a file path (auto-detect format via extension).
170pub fn build_channel(path: impl AsRef<Path>) -> Result<Box<dyn Channel>> {
171    let path_ref = path.as_ref();
172    let raw =
173        std::fs::read_to_string(path_ref).map_err(|e| Error::other(format!("read error: {e}")))?;
174    let format = DslFormat::from_path(path_ref)
175        .ok_or_else(|| Error::serialization("cannot infer DSL format from extension"))?;
176    build_channel_from_str(&raw, format)
177}
178
179/// Build filter from raw string + specified format.
180fn build_filter_from_str(raw: &str, format: DslFormat) -> Result<Filter> {
181    match format {
182        DslFormat::Yaml => {
183            let spec = FilterSpecYamlParser::parse_str(raw)?;
184            build_filter_from_spec(spec)
185        }
186        DslFormat::Json => Err(Error::serialization("json format not yet supported")),
187        DslFormat::Xml => Err(Error::serialization("xml format not yet supported")),
188    }
189}
190
191/// Convenience: build filter from a file path (auto-detect format via extension).
192pub fn build_filter(path: impl AsRef<Path>) -> Result<Filter> {
193    let path_ref = path.as_ref();
194    let raw =
195        std::fs::read_to_string(path_ref).map_err(|e| Error::other(format!("read error: {e}")))?;
196    let format = DslFormat::from_path(path_ref)
197        .ok_or_else(|| Error::serialization("cannot infer DSL format from extension"))?;
198    build_filter_from_str(&raw, format)
199}
200
201/// Build service from raw string + specified format.
202fn build_service_from_str(
203    raw: &str,
204    format: DslFormat,
205) -> Result<component_builders::ServiceProcessor> {
206    match format {
207        DslFormat::Yaml => {
208            let spec = ServiceSpecYamlParser::parse_str(raw)?;
209            build_service_from_spec(spec)
210        }
211        DslFormat::Json => Err(Error::serialization("json format not yet supported")),
212        DslFormat::Xml => Err(Error::serialization("xml format not yet supported")),
213    }
214}
215
216/// Convenience: build service from a file path (auto-detect format via extension).
217pub fn build_service(path: impl AsRef<Path>) -> Result<component_builders::ServiceProcessor> {
218    let path_ref = path.as_ref();
219    let raw =
220        std::fs::read_to_string(path_ref).map_err(|e| Error::other(format!("read error: {e}")))?;
221    let format = DslFormat::from_path(path_ref)
222        .ok_or_else(|| Error::serialization("cannot infer DSL format from extension"))?;
223    build_service_from_str(&raw, format)
224}
225
226/// Internal helper: build full runtime from raw + format.
227///
228/// `pub(crate)` so unit tests in `crate::runtime` can build an
229/// `AlloraRuntime` from an inline YAML string without writing a temp
230/// file.
231pub(crate) fn build_runtime_from_str(raw: &str, format: DslFormat) -> Result<AlloraRuntime> {
232    match format {
233        DslFormat::Yaml => {
234            let top = AlloraSpecYamlParser::parse_str(raw)?;
235            let filters_spec = top.filters_spec().cloned();
236            let services_spec = top.services_spec().cloned();
237            let http_inbound_spec = top.http_inbound_adapters_spec().cloned();
238            let http_outbound_spec = top.http_outbound_adapters_spec().cloned();
239            let channels_spec = top.into_channels_spec();
240            let channels = build_channels_from_spec(channels_spec)?;
241            let mut rt = AlloraRuntime::new(channels);
242            if let Some(fspec) = filters_spec {
243                let filters = build_filters_from_spec(fspec)?;
244                rt = rt.with_filters(filters);
245            }
246            if let Some(sspec) = services_spec {
247                let services =
248                    component_builders::build_service_activators_from_spec(sspec.clone())?;
249                rt = rt.with_services(services);
250                // Build activator processors from original specs (clone services spec entries)
251                let mut procs = Vec::new();
252                for s in sspec.services_activators() {
253                    procs.push(ServiceActivatorProcessor::new(s.clone()));
254                }
255                rt = rt.with_service_processors(procs);
256            }
257            if let Some(hspec) = http_inbound_spec {
258                let lookup = |id: &str| rt.channel_ref_by_id(id);
259                let adapters = build_http_inbound_adapters_from_spec(hspec, &lookup)?;
260                rt = rt.with_http_inbound_adapters(adapters);
261            }
262            if let Some(ospec) = http_outbound_spec {
263                let outbound = build_http_outbound_adapters_from_spec(ospec)?;
264                rt = rt.with_http_outbound_adapters(outbound);
265            }
266            Ok(rt)
267        }
268        DslFormat::Json => Err(Error::serialization("json format not yet supported")),
269        DslFormat::Xml => Err(Error::serialization("xml format not yet supported")),
270    }
271}
272
273/// Public: build full runtime from a file path (future: endpoints, filters, etc.).
274pub fn build(path: impl AsRef<Path>) -> Result<AlloraRuntime> {
275    let path_ref = path.as_ref();
276    let raw =
277        std::fs::read_to_string(path_ref).map_err(|e| Error::other(format!("read error: {e}")))?;
278    let format = DslFormat::from_path(path_ref)
279        .ok_or_else(|| Error::serialization("cannot infer DSL format from extension"))?;
280    build_runtime_from_str(&raw, format)
281}