ccs2/lib.rs
1//! # Welcome to CCS2!
2//!
3//! Let's get started with an example to show some of the common use-cases. Given a file called
4//! `doc.ccs` with the following contents:
5//!
6//! ```text
7//! a, f b e, c {
8//! c d {
9//! x = y
10//! }
11//! e f {
12//! foobar = abc
13//! }
14//! }
15//!
16//! x = 42
17//! ```
18//! ... you may want to do something like this:
19//!
20//! ```
21//! use ccs2::{Context, ToType};
22//!
23//! let context = Context::logging("examples/configs/doc.ccs", log::Level::Info)?;
24//!
25//! let constrained = context.constrain("a").constrain("c").constrain("d");
26//! assert_eq!(&constrained.get_type::<String>("x")?, "y");
27//! assert!(constrained.get("foobar").is_err());
28//!
29//! // Original context is untouched:
30//! assert_eq!(context.get("x")?.to_type::<i32>()?, 42);
31//! # Ok::<(), ccs2::CcsError>(())
32//! ```
33//!
34//! Example output (if logger is configured). Note that the actual origin will typically be absolute:
35//!
36//! ```text
37//! [2025-10-27T15:22:32Z INFO ccs2::tracer::log] Found property: x = y
38//! in context: [ a > c > d ]
39//! origin: examples/configs/doc.ccs:3
40//! [2025-10-27T15:22:32Z INFO ccs2::tracer::log] Property not found: foobar
41//! in context: [ a > c > d ]
42//! [2025-10-27T15:22:32Z INFO ccs2::tracer::log] Found property: x = 42
43//! in context: [ ]
44//! origin: examples/configs/doc.ccs:10
45//! ```
46//!
47//! Most of the public API is in [`Context`], so check there for a resonable starting point.
48//!
49//! # Incomplete Requirements
50//!
51//! The following requirements are not yet complete:
52//! - [x] `@import` does not work; I still need to add support for import resolvers and filename
53//! tracking.
54//! - [x] The parser doesn't track files right now, which isn't great.
55//! - [x] Log when a property could not be found, and when it's ambiguous.
56//! - [x] Support search with default value, which doesn't return a `Result`.
57//! - [ ] String interpolation from environment variables (or whatever injected mapping)
58//! - [ ] `ContextState::debug_context` probably doesn't correctly communicate `@constrain` or
59//! `@context` statements... Instead of a separate queue, we may want to compute it directly
60//! from the dag.
61//! - [ ] `stable` channel support: I'm currently on `nightly` for some odd `thiserror` reasons, but I
62//! don't think I should require that. I'll need to figure that out.
63//! - [ ] Opt-in Arc vs Rc context?
64//! - [ ] Much better error information for CCS syntax issues.
65//!
66//! - ... Probably a bunch of other stuff, I'll add to this as I think of things.
67//!
68//! # Features
69//!
70//! The crate provides the following features:
71//! - `log` (default): Adds the [`LogTracer`] for logging when properties are found. Adds a
72//! dependency on [`log`].
73//! - `extra_conversions` (default): Adds a parsers for getting [`std::time::Duration`] and
74//! [`std::time::SystemTime`] from CCS property strings. Adds a dependency on [`chrono`] and
75//! [`humantime`].
76//! - `dot`: Adds tools for exporting the underlying DAG to `dot` syntax, which allows for
77//! visualizing the context's state. Adds a dependency on [`petgraph`].
78
79// Used for thiserror backtrace:
80#![feature(error_generic_member_access)]
81
82pub mod ast;
83pub mod dag;
84
85mod load_helper;
86mod property_helper;
87mod search;
88mod tracer;
89
90use std::{path::Path, sync::Arc};
91
92#[cfg(feature = "log")]
93pub use crate::tracer::log::LogTracer;
94pub use crate::{
95 ast::{AstError, AstResult, ImportResolver, PersistentStr, PropertyValue},
96 dag::Stats as DagStats,
97 load_helper::{IoError, IoResult, RelativePathResolver},
98 property_helper::{
99 CommaSeparatedList, ConversionFailed, ConversionResult, ToType, TypedProperty,
100 },
101 search::{AsKey, DisplayContext, SearchError, SearchResult},
102 tracer::{ClonablePropertyTracer, NullTracer, PropertyTracer},
103};
104
105/// A common error type for code that tries to find and then convert a property
106///
107/// See [`ContextResult`]
108#[derive(thiserror::Error, Debug)]
109pub enum ContextError {
110 #[error(transparent)]
111 SearchError(#[from] SearchError),
112
113 #[error(transparent)]
114 ConversionError(#[from] ConversionFailed),
115}
116pub type ContextResult<T> = Result<T, ContextError>;
117
118/// Slightly more flexible than `ContextError`, as it also handles issues in parsing/interpreting
119///
120/// This is the catch-all type for anything that can go wrong
121///
122/// See [`CcsResult`]
123#[derive(thiserror::Error, Debug)]
124pub enum CcsError {
125 #[error(transparent)]
126 IoError(#[from] IoError),
127
128 #[error(transparent)]
129 AstError(#[from] AstError),
130
131 #[error(transparent)]
132 SearchError(#[from] SearchError),
133
134 #[error(transparent)]
135 ConversionError(#[from] ConversionFailed),
136
137 #[error(transparent)]
138 ContextError(#[from] ContextError),
139}
140pub type CcsResult<T> = Result<T, CcsError>;
141
142/// The primary representation of a CCS search state
143///
144/// Typically you'll want the [`Context::logging`] constructor, or (if not using the `log` feature),
145/// the [`Context::load_with_tracer`]. See [`PropertyTracer`] for more information.
146///
147/// In tests, you may want [`Context::from_str`] with test implementations of resolvers and tracers.
148#[derive(Clone)]
149pub struct Context {
150 context: search::Context<search::MaxAccumulator, Arc<dyn PropertyTracer>>,
151}
152
153#[cfg(feature = "log")]
154impl Context {
155 /// Loads a CCS file, and creates a context that logs when and where properties are found
156 ///
157 /// Uses the [`RelativePathResolver`]
158 ///
159 /// See [`PropertyTracer`] and [`LogTracer`] for more.
160 pub fn logging(path: impl AsRef<Path>, level: log::Level) -> CcsResult<Self> {
161 let path = path.as_ref();
162 Self::load(
163 path,
164 Self::default_resolver(&path)?,
165 LogTracer::single_level(level),
166 )
167 }
168}
169
170impl Context {
171 /// Creates a context that does not trace when or where properties are found
172 ///
173 /// Will use an empty [`ImportResolver`], that just skips over import statements. To provide a
174 /// different `ImportResolver`, use [`Context::from_str`]
175 ///
176 /// Generally this is most useful in tests.
177 pub fn from_str_without_tracing(ccs: impl AsRef<str>) -> AstResult<Self> {
178 Self::from_str(ccs, ast::NullResolver(), NullTracer {})
179 }
180}
181
182impl Context {
183 fn default_resolver(path: impl AsRef<Path>) -> CcsResult<impl ImportResolver> {
184 Ok(RelativePathResolver::siblings_with(path)?)
185 }
186
187 /// Loads a CCS file, and creates a context with the provided tracer
188 ///
189 /// Uses the [`RelativePathResolver`]
190 ///
191 /// See [`PropertyTracer`] for more.
192 pub fn load_with_tracer(
193 path: impl AsRef<Path>,
194 tracer: impl PropertyTracer + 'static,
195 ) -> CcsResult<Self> {
196 let path = path.as_ref();
197 Self::load(path, Self::default_resolver(&path)?, tracer)
198 }
199
200 /// Loads a CCS file, and creates a context with the provided import resolver and tracer
201 ///
202 /// See [`ImportResolver`] [`PropertyTracer`] for more.
203 pub fn load(
204 path: impl AsRef<Path>,
205 resolver: impl ImportResolver,
206 tracer: impl PropertyTracer + 'static,
207 ) -> CcsResult<Self> {
208 let tracer: Arc<dyn PropertyTracer> = Arc::new(tracer);
209 Ok(Self {
210 context: search::Context::load(path, resolver, tracer)?,
211 })
212 }
213
214 /// Creates a context from a provided CCS string, using the provided import resolver and tracer
215 ///
216 /// See [`ImportResolver`] [`PropertyTracer`] for more.
217 ///
218 /// Mostly useful for tests.
219 pub fn from_str(
220 ccs: impl AsRef<str>,
221 resolver: impl ImportResolver,
222 tracer: impl PropertyTracer + 'static,
223 ) -> AstResult<Self> {
224 let tracer: Arc<dyn PropertyTracer> = Arc::new(tracer);
225 Ok(Self {
226 context: search::Context::from_ccs_with(ccs, resolver, tracer)?,
227 })
228 }
229
230 /// Create a new context augmented with the given constraint
231 ///
232 /// # Example: Key-only constraint
233 /// Given the following CCS:
234 /// ```text
235 /// module : a = z
236 /// ```
237 /// the property `a` can be retrieved through constraining with `"module"`:
238 ///
239 /// ```
240 /// # let context = ccs2::Context::logging("examples/configs/doc.ccs", log::Level::Info).unwrap();
241 /// assert!(context.get_value("a").is_err());
242 ///
243 /// let x: &str = &context.constrain("module").get_value("a")?;
244 /// assert_eq!(x, "z");
245 /// # Ok::<(), ccs2::SearchError>(())
246 /// ```
247 ///
248 /// # Example: Key-value constraint
249 /// Given the following CCS;
250 /// ```text
251 /// env.prod : a = z
252 /// ```
253 /// the property `a` can be retrieved through constraining with `("env", "prod")`:
254 ///
255 /// ```
256 /// # let context = ccs2::Context::logging("examples/configs/doc.ccs", log::Level::Info).unwrap();
257 /// assert!(context.constrain("env").get_value("a").is_err());
258 ///
259 /// let x: &str = &context.constrain(("env", "prod")).get_value("a")?;
260 /// assert_eq!(x, "z");
261 /// # Ok::<(), ccs2::SearchError>(())
262 /// ```
263 pub fn constrain(&self, constraint: impl AsKey) -> Self {
264 Self {
265 context: self.context.augment(constraint),
266 }
267 }
268
269 /// Retrieves the value of a property from the current context, if possible
270 pub fn get(&self, prop: impl AsRef<str>) -> SearchResult<PropertyValue> {
271 self.context.get_single_property(prop)
272 }
273
274 /// Helper function for [`Context::get`] to get [`PropertyValue::value`]
275 pub fn get_value(&self, prop: impl AsRef<str>) -> SearchResult<PersistentStr> {
276 self.context.get_single_value(prop)
277 }
278
279 /// Helper for the ever-common "get and convert" pattern
280 ///
281 /// `context.get_type::<T>(prop)` is basically equivalent to `context.get(prop)?.to_type::<T>()`
282 ///
283 /// ```
284 /// # let context = ccs2::Context::logging("examples/configs/doc.ccs", log::Level::Info).unwrap();
285 /// assert_eq!(context.get_type::<i32>("x")?, 42);
286 /// # Ok::<(), ccs2::ContextError>(())
287 /// ```
288 pub fn get_type<T: TypedProperty>(&self, prop: impl AsRef<str>) -> ContextResult<T> {
289 Ok(self.get(prop)?.to_type::<T>()?)
290 }
291
292 /// Get a typed value, or provide the default if it cannot be found
293 ///
294 /// ```
295 /// # let context = ccs2::Context::logging("examples/configs/doc.ccs", log::Level::Info).unwrap();
296 /// assert_eq!(&context.get_or("undefined", "default_val".to_string()), "default_val");
297 /// ```
298 pub fn get_or<T: TypedProperty>(&self, prop: impl AsRef<str>, default: T) -> T {
299 self.get_type::<T>(prop).unwrap_or(default)
300 }
301
302 /// Get a typed value, or return the type's [`Default`] value
303 ///
304 /// ```
305 /// # let context = ccs2::Context::logging("examples/configs/doc.ccs", log::Level::Info).unwrap();
306 /// assert_eq!(&context.get_or_default::<String>("undefined"), "");
307 /// ```
308 pub fn get_or_default<T: TypedProperty + Default>(&self, prop: impl AsRef<str>) -> T {
309 self.get_type::<T>(prop).unwrap_or_default()
310 }
311
312 /// Retrieves the current context's queue of applied constraints, in the order they were applied
313 pub fn get_current_context(&self) -> DisplayContext {
314 self.context.get_debug_context()
315 }
316
317 /// Retrieves information about the DAG that underpins the activation algorithm
318 pub fn get_dag_stats(&self) -> DagStats {
319 self.context.get_dag_stats()
320 }
321
322 /// Turns the underlying DAG into a DOT string, which can be used for visualization.
323 ///
324 /// Requires the `dot` feature
325 #[cfg(feature = "dot")]
326 pub fn dag_as_dot_str(&self) -> String {
327 self.context.dag_to_dot_str()
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::ast::NullResolver;
335
336 #[test]
337 fn context_is_send() {
338 fn needs_send(_: impl Send) {}
339 needs_send(Context::from_str("", NullResolver(), NullTracer {}));
340 }
341
342 #[test]
343 fn context_is_sync() {
344 fn needs_sync(_: impl Sync) {}
345 needs_sync(Context::from_str("", NullResolver(), NullTracer {}));
346 }
347}