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}