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}