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
24 changes: 24 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,30 @@ authors = ["Amazon Web Services"]
keywords = ["openjd", "openjobdescription", "render-farm", "job-template", "rendering"]
categories = ["command-line-utilities", "parser-implementations"]

# Workspace lints. Inherited by each crate via `[lints] workspace = true`.
#
# `exhaustive_enums` / `exhaustive_structs` are `restriction`-group lints that
# fire only on *exported* (publicly reachable) types — private and `pub(crate)`
# items never trigger them. Enabling them here makes the SemVer growth question
# an explicit, reviewed decision for every new public type instead of something
# a contributor has to remember. Every existing site is either marked
# `#[non_exhaustive]` or carries an `#[expect(..., reason = "...")]` recording
# why it is intentionally closed.
#
# The rule for which way to go is documented in
# `specs/non-exhaustive-policy.md`. In short: types that mirror a numbered
# OpenJD spec section grow by extension RFC and are marked `#[non_exhaustive]`;
# types representing a decidable logical concept, and runtime state machines a
# consumer must react to, stay closed.
#
# Level is `deny`, not `warn`: a new public type that has not made this
# decision is a hard error at every invocation, not just under CI's
# `-D warnings`. Local `cargo clippy` / `cargo build` fails too, so the
# decision surfaces while the type is being written rather than in CI.
[workspace.lints.clippy]
exhaustive_enums = "deny"
exhaustive_structs = "deny"

[workspace.dependencies]
shlex = "2"
serde = { version = "1", features = ["derive"] }
Expand Down
7 changes: 7 additions & 0 deletions crates/openjd-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,10 @@ windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_System_Console",
] }

# Exempt: a binary crate: its `pub` items are clap argument structs and internal
# helpers, not a library API any downstream crate can depend on.
# See specs/non-exhaustive-policy.md.
[lints.clippy]
exhaustive_enums = "allow"
exhaustive_structs = "allow"
3 changes: 3 additions & 0 deletions crates/openjd-expr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ serde_json = { workspace = true }
ruff_python_parser = { package = "rustpython-ruff_python_parser", version = "0.15.8" }
ruff_python_ast = { package = "rustpython-ruff_python_ast", version = "0.15.8" }
ruff_text_size = { package = "rustpython-ruff_text_size", version = "0.15.8" }

[lints]
workspace = true
4 changes: 4 additions & 0 deletions crates/openjd-expr/src/eval/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ pub const DEFAULT_OPERATION_LIMIT: usize = 10_000_000;

/// Result of expression evaluation.
#[derive(Debug)]
#[expect(
clippy::exhaustive_structs,
reason = "fixed tuple of a decidable concept"
)]
pub struct EvalResult {
pub value: ExprValue,
pub peak_memory: usize,
Expand Down
4 changes: 4 additions & 0 deletions crates/openjd-expr/src/format_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,10 @@ fn parse_segments(input: &str, profile: &ExprProfile) -> Result<Vec<Segment>, Ex
/// Carries the position of the failing interpolation within the format string
/// so callers can produce caret-style diagnostics or structured error responses.
#[derive(Debug, Clone)]
#[expect(
clippy::exhaustive_structs,
reason = "fixed tuple of a decidable concept"
)]
pub struct FormatStringValidationError {
/// Description of what went wrong (e.g. "Undefined variable 'Param.X'").
pub message: String,
Expand Down
4 changes: 4 additions & 0 deletions crates/openjd-expr/src/function_library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ pub type FunctionImpl = Arc<

/// A registered function overload.
#[derive(Clone)]
#[expect(
clippy::exhaustive_structs,
reason = "fixed tuple of a decidable concept"
)]
pub struct FunctionEntry {
pub signature: ExprType, // TypeCode::Signature
pub implementation: FunctionImpl,
Expand Down
28 changes: 28 additions & 0 deletions crates/openjd-expr/src/path_mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
/// Path format (POSIX, Windows, or URI).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this exhaustive because supporting a new path format seems worth doing a breaking change for.

pub enum PathFormat {
#[serde(alias = "posix", alias = "Posix")]
Posix,
Expand All @@ -36,13 +37,40 @@ impl PathFormat {
/// <https://github.com/OpenJobDescription/openjd-specifications/wiki/How-Jobs-Are-Run#path-mapping>
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have OpenJobDescription/openjd-specifications#47 suggesting to add the destination path format here, which seems like a good idea to me. But, that would be a breaking change so I don't think non_exhaustive is right for this.

pub struct PathMappingRule {
pub source_path_format: PathFormat,
pub source_path: String,
pub destination_path: String,
}

impl PathMappingRule {
/// Construct a path mapping rule.
///
/// Prefer this over a struct literal: `PathMappingRule` is
/// `#[non_exhaustive]`, so literal construction is not available to
/// other crates.
///
/// # Examples
///
/// ```
/// use openjd_expr::{PathFormat, PathMappingRule};
///
/// let rule = PathMappingRule::new(PathFormat::Posix, "/mnt/shared", "Z:\\shared");
/// assert_eq!(rule.source_path, "/mnt/shared");
/// ```
pub fn new(
source_path_format: PathFormat,
source_path: impl Into<String>,
destination_path: impl Into<String>,
) -> Self {
Self {
source_path_format,
source_path: source_path.into(),
destination_path: destination_path.into(),
}
}

/// Apply this rule using host-native output separators.
/// Equivalent to Python's behavior (uses `os.name` to pick separator).
pub fn apply(&self, path: &str) -> Option<String> {
Expand Down
1 change: 1 addition & 0 deletions crates/openjd-expr/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ impl ExprExtension {
/// the previous split between `FunctionLibrary::with_host_context` and
/// `FunctionLibrary::with_unresolved_host_context`.
#[derive(Debug, Clone, Default)]
#[non_exhaustive]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why shouldn't this one be exhaustive?

pub enum HostContext {
/// No host-context functions are registered. Default.
#[default]
Expand Down
4 changes: 4 additions & 0 deletions crates/openjd-expr/src/range_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ pub const MAX_RANGE_EXPR_CHUNKS: usize = 10_000;

/// Error raised when parsing a range expression fails.
#[derive(Debug, Clone)]
#[expect(
clippy::exhaustive_structs,
reason = "fixed tuple of a decidable concept"
)]
pub struct RangeExprError {
pub expr: String,
pub message: String,
Expand Down
8 changes: 8 additions & 0 deletions crates/openjd-expr/src/symbol_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ pub const MAX_SYMBOL_TABLE_ENTRIES: usize = 100_000;
///
/// For example, setting `"A.B.C"` when `"A.B"` is already a scalar value.
#[derive(Debug, Clone)]
#[expect(
clippy::exhaustive_structs,
reason = "fixed tuple of a decidable concept"
)]
pub struct SymbolTableError {
pub key: String,
pub conflict: String,
Expand Down Expand Up @@ -90,6 +94,10 @@ impl From<SymbolTableError> for crate::error::ExpressionError {

/// Entry in a symbol table: either a nested table or a value.
#[derive(Debug, Clone)]
#[expect(
clippy::exhaustive_enums,
reason = "closed dichotomy: matching every arm is the API"
)]
pub enum SymbolTableEntry {
Table(SymbolTable),
Value(ExprValue),
Expand Down
4 changes: 4 additions & 0 deletions crates/openjd-expr/src/uri_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

/// Parsed URI: authority (`scheme://host`) and path segments.
#[derive(Debug, Clone)]
#[expect(
clippy::exhaustive_structs,
reason = "internal mechanics, publicly reachable but not a spec surface"
)]
pub struct UriParts {
pub authority: String,
pub path_parts: Vec<String>,
Expand Down
4 changes: 4 additions & 0 deletions crates/openjd-expr/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,10 @@ impl From<crate::types::ExprType> for ExprValue {
}

/// Zero-allocation iterator over list elements.
#[expect(
clippy::exhaustive_enums,
reason = "structural mirror of another type; not independently extensible"
)]
pub enum ListIter<'a> {
Bool(std::slice::Iter<'a, bool>),
Int(std::slice::Iter<'a, i64>),
Expand Down
24 changes: 4 additions & 20 deletions crates/openjd-expr/tests/integration/test_function_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,7 @@ fn method_syntax_with_host_context() {
// === Path mapping rules ===
#[test]
fn with_path_mapping_rules() {
let rule = PathMappingRule {
source_path_format: PathFormat::Posix,
source_path: "/old".into(),
destination_path: "/new".into(),
};
let rule = PathMappingRule::new(PathFormat::Posix, "/old", "/new");
let mut st = SymbolTable::new();
st.set("P", ExprValue::String("/old/file.txt".into()))
.unwrap();
Expand All @@ -199,11 +195,7 @@ fn with_path_mapping_rules() {
}
#[test]
fn unmatched_path_unchanged() {
let rule = PathMappingRule {
source_path_format: PathFormat::Posix,
source_path: "/old".into(),
destination_path: "/new".into(),
};
let rule = PathMappingRule::new(PathFormat::Posix, "/old", "/new");
let mut st = SymbolTable::new();
st.set("P", ExprValue::String("/other/file.txt".into()))
.unwrap();
Expand Down Expand Up @@ -334,11 +326,7 @@ fn path_with_suffix_without_host_context() {
// === Function-syntax apply_path_mapping with rules ===
#[test]
fn function_syntax_with_path_mapping_rules() {
let rule = PathMappingRule {
source_path_format: PathFormat::Posix,
source_path: "/old/path".into(),
destination_path: "/new/path".into(),
};
let rule = PathMappingRule::new(PathFormat::Posix, "/old/path", "/new/path");
let lib = FunctionLibrary::for_profile(
&ExprProfile::current().with_host_context(HostContext::with_rules(vec![rule])),
);
Expand All @@ -354,11 +342,7 @@ fn function_syntax_with_path_mapping_rules() {
}
#[test]
fn function_syntax_unmatched_path_unchanged() {
let rule = PathMappingRule {
source_path_format: PathFormat::Posix,
source_path: "/specific/path".into(),
destination_path: "/mapped/path".into(),
};
let rule = PathMappingRule::new(PathFormat::Posix, "/specific/path", "/mapped/path");
let lib = FunctionLibrary::for_profile(
&ExprProfile::current().with_host_context(HostContext::with_rules(vec![rule])),
);
Expand Down
Loading
Loading