Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/generators/idl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ quote = "1"

[dev-dependencies]
blueberry-parser = { path = "../../parser", version = "0.1.0" }
insta = "1.40"
44 changes: 17 additions & 27 deletions crates/generators/idl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,7 @@ impl IdlGenerator {
}

fn emit_definitions(&mut self, definitions: &[Definition], indent: usize) {
for (idx, definition) in definitions.iter().enumerate() {
if idx > 0 {
self.buffer.push('\n');
}
for definition in definitions {
self.emit_definition(definition, indent);
}
}
Expand Down Expand Up @@ -90,15 +87,13 @@ impl IdlGenerator {
self.emit_comments(&enum_def.comments, indent);
self.emit_annotations(&enum_def.annotations, indent);
let name = ident(&enum_def.node.name);
let assignment_width = enum_def
// Calculate the width needed to align all `=` signs vertically
let alignment_width = enum_def
.node
.enumerators
.iter()
.filter(|member| member.value.is_some())
.map(|member| {
let member_ident = ident(&member.name);
tokens_to_line(quote!(#member_ident)).len()
})
.map(|member| member.name.len())
.max()
.unwrap_or(0);
let header = if let Some(base_type) = &enum_def.node.base_type {
Expand All @@ -110,15 +105,21 @@ impl IdlGenerator {
self.write_tokens_with_suffix(indent, header, " {");
for member in &enum_def.node.enumerators {
self.emit_comments(&member.comments, indent + 1);
let member_name = ident(&member.name);
let member_name = &member.name;
let suffix = ",";
if let Some(value) = &member.value {
let value_fragment = render_const_value_fragment(value);
let line =
format_aligned_enum_assignment(&member_name, assignment_width, &value_fragment);
let padding = alignment_width.saturating_sub(member_name.len());
let line = format!(
"{}{} = {}",
member_name,
" ".repeat(padding),
value_fragment
);
self.write_raw_line_with_suffix(indent + 1, &line, suffix);
} else {
let tokens = quote!(#member_name);
let member_ident = ident(member_name);
let tokens = quote!(#member_ident);
self.write_tokens_with_suffix(indent + 1, tokens, suffix);
}
}
Expand Down Expand Up @@ -432,17 +433,6 @@ fn format_assignment(lhs: TokenStream, value: &LiteralFragment) -> String {
lhs_text
}

fn format_aligned_enum_assignment(name: &Ident, width: usize, value: &LiteralFragment) -> String {
let name_text = tokens_to_line(quote!(#name));
let padding = width.saturating_sub(name_text.len()) + 1;
let mut line = String::new();
line.push_str(&name_text);
line.push_str(&" ".repeat(padding));
line.push_str("= ");
line.push_str(&value.to_string());
line
}

fn format_fixed_point_literal(literal: &FixedPointLiteral) -> String {
let mut output = String::new();
if literal.negative {
Expand Down Expand Up @@ -767,12 +757,12 @@ mod tests {
}

#[test]
fn aligns_enum_assignments() {
fn enums_align_assignments() {
let defs = load_fixture("enum_only.idl");
let emitted = generate_idl(&defs);
assert!(
emitted.contains(" ACTIVE = 0,\n INACTIVE = 1,\n PENDING = 2"),
"expected enum assignments to align:\n{}",
emitted.contains(" ACTIVE = 0,\n INACTIVE = 1,\n PENDING = 2,"),
"expected enum assignments with aligned = signs:\n{}",
emitted
);
}
Expand Down
94 changes: 94 additions & 0 deletions crates/generators/idl/tests/idl_snapshot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//! Snapshot tests for IDL round-trip: parse -> generate -> compare.
//!
//! These tests ensure that parsing an IDL file and generating it back
//! produces a consistent, normalized output. All `.idl` files in the
//! fixtures folder are automatically discovered and tested.

use blueberry_generator_idl::generate_idl;
use blueberry_parser::parse_idl;
use std::{fs, path::PathBuf};

fn fixture_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../parser/tests/fixtures")
}

/// Snapshot test for all IDL fixtures.
///
/// Parses each `.idl` file in the fixtures folder, generates it back to IDL,
/// and compares against stored snapshots.
#[test]
fn snapshot_all_fixtures() {
let fixture_dir = fixture_dir();
let mut entries: Vec<_> = fs::read_dir(&fixture_dir)
.unwrap_or_else(|err| panic!("failed to read fixture dir: {}", err))
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.path()
.extension()
.map(|ext| ext == "idl")
.unwrap_or(false)
})
.collect();

// Sort for deterministic ordering
entries.sort_by_key(|e| e.path());

for entry in entries {
let path = entry.path();
let name = path.file_stem().unwrap().to_string_lossy();

let input = fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("failed to read {}: {}", path.display(), err));

let defs =
parse_idl(&input).unwrap_or_else(|err| panic!("failed to parse {}: {}", name, err));

let generated = generate_idl(&defs);

insta::assert_snapshot!(name.to_string(), generated);
}
}

/// Test that all fixtures can be round-tripped: parse -> generate -> reparse.
///
/// Note: This test verifies that the generated IDL is valid and can be reparsed,
/// but does not require byte-for-byte AST equality. Comment formatting may differ
/// (e.g., multiline block comments become multiple single-line comments).
#[test]
fn round_trip_all_fixtures() {
let fixture_dir = fixture_dir();
let entries = fs::read_dir(&fixture_dir)
.unwrap_or_else(|err| panic!("failed to read fixture dir: {}", err));

for entry in entries {
let entry = entry.expect("failed to read entry");
let path = entry.path();
if path.extension().map(|ext| ext == "idl").unwrap_or(false) {
let name = path.file_name().unwrap().to_string_lossy();
let input = fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("failed to read {}: {}", name, err));

let original_defs =
parse_idl(&input).unwrap_or_else(|err| panic!("failed to parse {}: {}", name, err));

let generated = generate_idl(&original_defs);

// Verify the generated IDL can be parsed
let reparsed_defs = parse_idl(&generated).unwrap_or_else(|err| {
panic!(
"failed to reparse generated {}: {}\n\nGenerated:\n{}",
name, err, generated
)
});

// Verify the reparsed IDL generates identical output (stable round-trip)
let regenerated = generate_idl(&reparsed_defs);
assert_eq!(
generated, regenerated,
"round-trip not stable for {}: second generation differs from first",
name
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
source: crates/generators/idl/tests/idl_snapshot.rs
expression: generated
---
// Tests annotation parsing and comment capture.
@Blueprint::Marker
@Blueprint::Experimental(role = 1)
struct Annotated {
// Primary identifier
@Wire::Encoded(format = Status::ACTIVE)
long id;
// Display label
@Wire::Label("primary")
string label;
// Toggle flag
@Wire::Flag
@Wire::FlagOptions(mode = "test", retries = 10)
boolean enabled;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
source: crates/generators/idl/tests/idl_snapshot.rs
expression: generated
---
// *
// * Blueberry full dictionary example that uses every documented feature.
// Demonstrates combined block and line comments.
const uint16 BLUEBERRY_NAMESPACE = 0x4242;
const uint16 DIAGNOSTICS_NAMESPACE = 0x4343;
const uint32 FIRMWARE_SIGNATURE = 0x42FF00AA;
const octet FEATURE_MASK = 0b10100101;
const string PROTOCOL_NAME = "Blueberry";
const boolean ENABLE_JSON_STREAM = TRUE;
@file_path("/dictionary/blueberry/full")
@module_key(0x4242)
module Blueberry {
typedef uint32 NodeId;
typedef string<32> DeviceAlias;
typedef float Vec3[3];
typedef float Matrix3x3[3][3];
typedef octet SerialNumber[12];
typedef sequence<octet, 128> ByteLog;
typedef sequence<Vec3, 16> Path;
typedef sequence<float, 9> CovarianceMatrix;
enum HwType : uint16 {
HW_UNKNOWN = 0,
HW_ESC = 1,
HW_LUMEN = 2,
HW_BRIDGE = 3,
HW_SENSOR_HUB = 4,
};
enum MessagePriority : octet {
PRIORITY_LOW = 0,
PRIORITY_MEDIUM = 1,
PRIORITY_HIGH = 10,
PRIORITY_CRITICAL = 11,
};
struct FirmwareVersion {
uint16 major;
uint16 minor;
uint32 build;
string<16> codename;
};
struct Calibration {
Vec3 bias;
Matrix3x3 transform;
CovarianceMatrix covariance;
};
struct ImuSample {
int32 timestamp_us;
Vec3 accel;
Vec3 gyro;
Vec3 mag;
CovarianceMatrix covariance;
};
// Message metadata included via annotations and inheritance.
@topic(value = "blueberry/devices/{device_type}/{nid}/version")
@message_key(value = 0x100)
message VersionMessage {
NodeId node;
HwType hardware;
FirmwareVersion firmware;
DeviceAlias label;
SerialNumber serial;
boolean betaOptIn;
};
@serialization(CDR)
@topic(value = "blueberry/devices/{device_type}/{nid}/imu")
@message_key(value = 0x101)
message ImuStream {
MessagePriority priority;
Calibration calibration;
sequence<ImuSample, 32> samples;
Path plannedPath;
string operatorName;
};
@topic(value = "blueberry/devices/{device_type}/{nid}/health")
@message_key(value = 0x102)
message DeviceHealth : VersionMessage {
octet warningCount;
ByteLog latestLog;
boolean requiresMaintenance;
};
};
import Blueberry;
@file_path("/dictionary/blueberry/diagnostics")
@module_key(0x4343)
module Diagnostics {
struct ModuleHealth {
uint32 uptimeSeconds;
HwType hardware;
boolean requiresMaintenance;
Vec3 orientation;
ByteLog logSlice;
};
@topic(value = "blueberry/diagnostics/{nid}/health")
@message_key(value = 0x200)
message HealthReport {
ModuleHealth heath;
sequence<MessagePriority, 4> outstanding;
string<64> note;
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
source: crates/generators/idl/tests/idl_snapshot.rs
expression: generated
---
// Fixture covering numeric constant folding scenarios.
const long GLOBAL_BASE = 12;
const long GLOBAL_ALIAS = 12;
const double PI_VALUE = 3.141592653589793;
const double TAU_ALIAS = 3.141592653589793;
const long FIXED_BASE = 12.5d;
const long FIXED_ALIAS = 12.5d;
const string PRODUCT = "Blueberry";
const string PRODUCT_ALIAS = PRODUCT;
const long FORWARD_ALIAS = 64;
const long FORWARD_BASE = 64;
@Meta(limit = 12)
const long ANNOTATED_LIMIT = 12;
module Outer {
const long MODULE_BASE = 21;
const long MODULE_COMPLEX = 18;
const double MODULE_RATIO = 2.75;
const long MODULE_MAGIC = 39;
const long MODULE_ALIAS = 21;
module Inner {
const long INNER_BASE = 7;
const long FROM_PARENT = 21;
const long FROM_OUTER_SCOPED = 21;
const long FROM_ABSOLUTE = 21;
};
};
enum WithConst {
START = 12,
NEXT = 13,
};
@Range(max = 12)
struct AnnotatedStruct {
@Field(defaultValue = 12)
long threshold;
};
15 changes: 15 additions & 0 deletions crates/generators/idl/tests/snapshots/idl_snapshot__consts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: crates/generators/idl/tests/idl_snapshot.rs
expression: generated
---
// Constants fixture covering literal forms and scoped values.
enum Status {
ACTIVE = 0,
INACTIVE = 1,
};
const long MAX_CLIENTS = 42;
const double PI = 3.141592653589793;
const boolean FEATURE_FLAG = TRUE;
const char NEWLINE = '\n';
const string PRODUCT = "Blueberry";
const Status DEFAULT_STATUS = Status::ACTIVE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
source: crates/generators/idl/tests/idl_snapshot.rs
expression: generated
---
// Fixture covering implicit enum value propagation.
enum AutoStatus {
FIRST = 0,
SECOND = 1,
THIRD = 10,
FOURTH = 11,
FIFTH = 12,
};
10 changes: 10 additions & 0 deletions crates/generators/idl/tests/snapshots/idl_snapshot__enum_only.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: crates/generators/idl/tests/idl_snapshot.rs
expression: generated
---
// Enum fixture showcasing enumerator lists.
enum Status : uint16 {
ACTIVE = 0,
INACTIVE = 1,
PENDING = 2,
};
Loading