Skip to content

Commit bcd3b10

Browse files
authored
feat(sickle): add granular feature flags and serde serialization (#44)
## Summary - Add granular feature flags for fine-grained control over sickle functionality - Implement serde `Serialize` support (`to_string`) to complement existing `Deserialize` ### Feature Dependency Graph ``` serde ─┬─> serde-deserialize ──> hierarchy ──> parse └─> serde-serialize ───> printer ────> hierarchy ──> parse intern (independent) ``` ### Features | Feature | Description | Dependencies | |---------|-------------|--------------| | `parse` | Core parsing | - | | `hierarchy` | Build hierarchical model | `parse` | | `printer` | CCL printer | `hierarchy` | | `serde-deserialize` | Serde Deserialize | `hierarchy` | | `serde-serialize` | Serde Serialize (NEW) | `printer` | | `serde` | Both ser/de | `serde-deserialize`, `serde-serialize` | | `full` | Everything | all | ## Test plan - [x] All existing tests pass - [x] New serialization tests pass - [x] Feature combinations compile correctly - [x] Downstream crates (santa-data, santa) still work
1 parent 453ff96 commit bcd3b10

File tree

5 files changed

+1752
-19
lines changed

5 files changed

+1752
-19
lines changed

Cargo.lock

Lines changed: 1 addition & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/sickle/Cargo.toml

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,39 @@ categories = ["config", "parser-implementations"]
1313
readme = "README.md"
1414

1515
[features]
16-
# Default features
17-
default = ["serde"]
16+
# Default features - includes only essential functionality
17+
# Users opt-in to additional features as needed
18+
default = []
1819

19-
# Core parsing features
20-
serde = ["dep:serde", "dep:serde_derive"]
20+
# Core parsing - parse CCL to flat key-value entries
21+
parse = []
22+
23+
# Hierarchy building - build hierarchical CclObject from entries (includes parse)
24+
hierarchy = ["parse"]
25+
26+
# Serde deserialization: CCL string -> Rust types (includes hierarchy)
27+
serde-deserialize = ["hierarchy", "dep:serde", "dep:serde_derive"]
28+
29+
# CCL printer - serialize CclObject back to canonical CCL text (includes hierarchy)
30+
printer = ["hierarchy"]
31+
32+
# Serde serialization: Rust types -> CCL string (includes printer)
33+
serde-serialize = ["printer", "dep:serde", "dep:serde_derive"]
34+
35+
# Both serde serialization and deserialization
36+
serde = ["serde-deserialize", "serde-serialize"]
37+
38+
# String interning for memory efficiency with large configs
2139
intern = ["dep:string-interner"]
2240

2341
# Reference implementation compatibility (opt-in for compatibility testing)
2442
# Note: Reverses duplicate-key list order to match reference implementation
2543
# Most users should NOT enable this - use insertion-order behavior (default)
2644
reference_compliant = []
2745

46+
# All features enabled
47+
full = ["serde", "printer", "intern"]
48+
2849
# Advanced parsing features (future)
2950
# section-headers = [] # Support == Section == style headers
3051
# typed-access = [] # Convenience methods: get_string, get_int, get_bool
@@ -43,8 +64,13 @@ string-interner = { version = "0.17", optional = true }
4364

4465
[dev-dependencies]
4566
# Testing framework
67+
serde.workspace = true
4668
serde_json.workspace = true
47-
serde_test = "1.0.177"
69+
70+
# Enable all features for tests
71+
[dev-dependencies.sickle]
72+
path = "."
73+
features = ["full"]
4874

4975
[[example]]
5076
name = "basic_parsing"

crates/sickle/src/lib.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,26 +50,43 @@
5050
//!
5151
//! ## Cargo Features
5252
//!
53-
//! - `serde` (default): Serde serialization/deserialization support
53+
//! By default, sickle includes only the core types (`CclObject`, `Entry`, `Error`).
54+
//! Enable features to add functionality:
55+
//!
56+
//! - `parse`: Core parsing (`parse`, `parse_indented`) - returns flat key-value entries
57+
//! - `hierarchy`: Build hierarchical model (`build_hierarchy`, `load`) - includes `parse`
58+
//! - `printer`: CCL printer for serializing back to canonical CCL text - includes `hierarchy`
59+
//! - `serde-deserialize`: Serde deserialization (`from_str`) - includes `hierarchy`
60+
//! - `serde-serialize`: Serde serialization (`to_string`) - includes `printer`
61+
//! - `serde`: Both serialization and deserialization
5462
//! - `intern`: String interning for memory efficiency with large configs
63+
//! - `full`: Enable all features
5564
//!
5665
//! ### Future Features (Planned)
5766
//!
5867
//! - `section-headers`: Support `== Section ==` style headers
59-
//! - `duplicate-key-lists`: Auto-create lists from duplicate keys
6068
//! - `typed-access`: Convenience methods like `get_string()`, `get_int()`
6169
//! - `list-indexing`: Advanced list operations and indexing
6270
6371
pub mod error;
6472
pub mod model;
73+
74+
#[cfg(feature = "parse")]
6575
mod parser;
76+
77+
#[cfg(feature = "printer")]
6678
pub mod printer;
6779

68-
#[cfg(feature = "serde")]
80+
#[cfg(feature = "serde-deserialize")]
6981
pub mod de;
7082

83+
#[cfg(feature = "serde-serialize")]
84+
pub mod ser;
85+
7186
pub use error::{Error, Result};
7287
pub use model::{CclObject, Entry};
88+
89+
#[cfg(feature = "printer")]
7390
pub use printer::{CclPrinter, PrinterConfig};
7491

7592
/// Parse a CCL string into a flat list of entries
@@ -78,6 +95,8 @@ pub use printer::{CclPrinter, PrinterConfig};
7895
/// without building the hierarchical structure. Use `build_hierarchy()` to
7996
/// construct the hierarchical model from these entries.
8097
///
98+
/// Requires the `parse` feature.
99+
///
81100
/// # Examples
82101
///
83102
/// ```rust
@@ -93,6 +112,7 @@ pub use printer::{CclPrinter, PrinterConfig};
93112
/// assert_eq!(entries[0].key, "name");
94113
/// assert_eq!(entries[0].value, "MyApp");
95114
/// ```
115+
#[cfg(feature = "parse")]
96116
pub fn parse(input: &str) -> Result<Vec<Entry>> {
97117
let map = parser::parse_to_map(input)?;
98118

@@ -112,6 +132,8 @@ pub fn parse(input: &str) -> Result<Vec<Entry>> {
112132
/// This is the second step of CCL processing, taking the entries from `parse()`
113133
/// and constructing a hierarchical structure with proper nesting and type inference.
114134
///
135+
/// Requires the `hierarchy` feature.
136+
///
115137
/// # Examples
116138
///
117139
/// ```rust
@@ -126,6 +148,7 @@ pub fn parse(input: &str) -> Result<Vec<Entry>> {
126148
/// let model = build_hierarchy(&entries).unwrap();
127149
/// assert_eq!(model.get_string("name").unwrap(), "MyApp");
128150
/// ```
151+
#[cfg(feature = "hierarchy")]
129152
pub fn build_hierarchy(entries: &[Entry]) -> Result<CclObject> {
130153
// Group entries by key (preserving order with IndexMap)
131154
let mut map: indexmap::IndexMap<String, Vec<String>> = indexmap::IndexMap::new();
@@ -141,6 +164,7 @@ pub fn build_hierarchy(entries: &[Entry]) -> Result<CclObject> {
141164

142165
/// Check if a string looks like a valid CCL key
143166
/// Valid keys: alphanumeric, underscores, dots, hyphens (not leading), slashes for comments
167+
#[cfg(feature = "hierarchy")]
144168
fn is_valid_ccl_key(key: &str) -> bool {
145169
if key.is_empty() {
146170
return true; // Empty keys are valid (for lists)
@@ -168,6 +192,7 @@ fn is_valid_ccl_key(key: &str) -> bool {
168192
/// - `key =` (empty value) becomes `{"key": {"": {}}}`
169193
/// - Multiple values become multiple nested keys
170194
/// - Nested CCL is recursively parsed
195+
#[cfg(feature = "hierarchy")]
171196
fn build_model(map: indexmap::IndexMap<String, Vec<String>>) -> Result<CclObject> {
172197
let mut result = indexmap::IndexMap::new();
173198

@@ -238,6 +263,8 @@ fn build_model(map: indexmap::IndexMap<String, Vec<String>>) -> Result<CclObject
238263
/// This is used for parsing nested CCL values where the entire block may be
239264
/// indented in the parent context.
240265
///
266+
/// Requires the `parse` feature.
267+
///
241268
/// # Examples
242269
///
243270
/// ```rust
@@ -247,6 +274,7 @@ fn build_model(map: indexmap::IndexMap<String, Vec<String>>) -> Result<CclObject
247274
/// let entries = parse_indented(nested).unwrap();
248275
/// assert_eq!(entries.len(), 3);
249276
/// ```
277+
#[cfg(feature = "parse")]
250278
pub fn parse_indented(input: &str) -> Result<Vec<Entry>> {
251279
// Find the minimum indentation level (common prefix)
252280
let min_indent = input
@@ -293,6 +321,7 @@ pub fn parse_indented(input: &str) -> Result<Vec<Entry>> {
293321
}
294322

295323
/// Parse all key=value pairs from input as flat entries, ignoring indentation hierarchy
324+
#[cfg(feature = "parse")]
296325
fn parse_flat_entries(input: &str) -> Result<Vec<Entry>> {
297326
let mut entries = Vec::new();
298327

@@ -319,6 +348,7 @@ fn parse_flat_entries(input: &str) -> Result<Vec<Entry>> {
319348
}
320349

321350
/// Parse input as a single entry, preserving the raw value including indentation
351+
#[cfg(feature = "parse")]
322352
fn parse_single_entry_with_raw_value(input: &str) -> Result<Vec<Entry>> {
323353
// Find the first line with '='
324354
let mut lines = input.lines();
@@ -358,6 +388,8 @@ fn parse_single_entry_with_raw_value(input: &str) -> Result<Vec<Entry>> {
358388
/// This is a convenience function that combines `parse()` and `build_hierarchy()`.
359389
/// Equivalent to: `build_hierarchy(&parse(input)?)`
360390
///
391+
/// Requires the `hierarchy` feature.
392+
///
361393
/// # Examples
362394
///
363395
/// ```rust
@@ -371,14 +403,18 @@ fn parse_single_entry_with_raw_value(input: &str) -> Result<Vec<Entry>> {
371403
/// let model = load(ccl).unwrap();
372404
/// assert_eq!(model.get_string("name").unwrap(), "MyApp");
373405
/// ```
406+
#[cfg(feature = "hierarchy")]
374407
pub fn load(input: &str) -> Result<CclObject> {
375408
let entries = parse(input)?;
376409
build_hierarchy(&entries)
377410
}
378411

379-
#[cfg(feature = "serde")]
412+
#[cfg(feature = "serde-deserialize")]
380413
pub use de::from_str;
381414

415+
#[cfg(feature = "serde-serialize")]
416+
pub use ser::to_string;
417+
382418
// Unit tests removed - all functionality is covered by data-driven tests in:
383419
// - api_core_ccl_parsing.json (basic parsing, multiline values, equals in values)
384420
// - api_core_ccl_hierarchy.json (build_hierarchy, nested structures, duplicate keys to lists)

crates/sickle/src/model.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ impl CclObject {
9797

9898
/// Create a Model from an IndexMap
9999
/// This is internal-only for crate operations
100+
#[cfg(feature = "hierarchy")]
100101
pub(crate) fn from_map(map: IndexMap<String, CclObject>) -> Self {
101102
CclObject(map)
102103
}
@@ -124,6 +125,7 @@ impl CclObject {
124125
}
125126

126127
/// Get the concrete IndexMap iterator for internal use (Serde)
128+
#[cfg(feature = "serde-deserialize")]
127129
pub(crate) fn iter_map(&self) -> indexmap::map::Iter<'_, String, CclObject> {
128130
self.0.iter()
129131
}
@@ -358,6 +360,7 @@ impl CclObject {
358360
///
359361
/// Creates the representation `{string: {}}`
360362
/// This is internal-only for Serde support
363+
#[cfg(feature = "serde-deserialize")]
361364
pub(crate) fn from_string(s: impl Into<String>) -> Self {
362365
let mut map = IndexMap::new();
363366
map.insert(s.into(), CclObject::new());
@@ -366,9 +369,36 @@ impl CclObject {
366369

367370
/// Extract the inner IndexMap, consuming the Model
368371
/// This is internal-only for crate operations
372+
#[cfg(feature = "hierarchy")]
369373
pub(crate) fn into_inner(self) -> IndexMap<String, CclObject> {
370374
self.0
371375
}
376+
377+
/// Insert a string value at the given key
378+
/// Creates the CCL representation: `{key: {value: {}}}`
379+
#[cfg(feature = "serde-serialize")]
380+
pub(crate) fn insert_string(&mut self, key: &str, value: String) {
381+
let mut inner = IndexMap::new();
382+
inner.insert(value, CclObject::new());
383+
self.0.insert(key.to_string(), CclObject(inner));
384+
}
385+
386+
/// Insert a list of string values at the given key
387+
/// Creates the CCL representation: `{key: {item1: {}, item2: {}, ...}}`
388+
#[cfg(feature = "serde-serialize")]
389+
pub(crate) fn insert_list(&mut self, key: &str, values: Vec<String>) {
390+
let mut inner = IndexMap::new();
391+
for value in values {
392+
inner.insert(value, CclObject::new());
393+
}
394+
self.0.insert(key.to_string(), CclObject(inner));
395+
}
396+
397+
/// Insert a nested object at the given key
398+
#[cfg(feature = "serde-serialize")]
399+
pub(crate) fn insert_object(&mut self, key: &str, obj: CclObject) {
400+
self.0.insert(key.to_string(), obj);
401+
}
372402
}
373403

374404
impl Default for CclObject {

0 commit comments

Comments
 (0)