Skip to content
Merged
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
14 changes: 11 additions & 3 deletions src/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,19 @@ pub fn run() -> CliResult {
}

for variable in &entity.scalar_vars {
if !variable.value_range_provider.is_empty()
&& !fact_fields.contains(variable.value_range_provider.as_str())
let provider = variable.value_range_provider.as_str();
// A provider names a solution fact collection or a
// `Vec<usize>` field on the same entity, which gives
// each entity its own legal domain.
if !provider.is_empty()
&& !fact_fields.contains(provider)
&& !entity
.entity_value_ranges
.iter()
.any(|field| field == provider)
{
errors.push(format!(
"Entity '{}.{}' references missing fact collection '{}' via value_range_provider",
"Entity '{}.{}' references value_range_provider '{}', which is neither a fact collection nor a Vec<usize> field on the entity",
entity.item_type, variable.field, variable.value_range_provider
));
}
Expand Down
60 changes: 60 additions & 0 deletions src/commands/check_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,66 @@ impl Task {
);
}

#[test]
fn check_accepts_entity_scoped_value_range_providers() {
let _cwd_guard = test_support::lock_cwd();
let tmp = tempfile::tempdir().expect("failed to create temp dir");
let _dir_guard = CwdGuard::enter(tmp.path());
write_project_layout();

fs::write(
"src/domain/task.rs",
r#"use serde::{Deserialize, Serialize};
use solverforge::prelude::*;

#[planning_entity]
#[derive(Serialize, Deserialize)]
pub struct Task {
#[planning_id]
pub id: String,
pub allowed_resources: Vec<usize>,
// @solverforge:begin entity-variables
#[planning_variable(value_range_provider = "allowed_resources", allows_unassigned = true)]
pub resource_idx: Option<usize>,
// @solverforge:end entity-variables
}

impl Task {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
allowed_resources: Vec::new(),
// @solverforge:begin entity-variable-init
resource_idx: None,
// @solverforge:end entity-variable-init
}
}
}
"#,
)
.expect("failed to rewrite task");

run().expect("a Vec<usize> field on the entity is a valid value range");

fs::write(
"src/domain/task.rs",
fs::read_to_string("src/domain/task.rs")
.expect("failed to read task")
.replace(
"pub allowed_resources: Vec<usize>,",
"pub allowed_resources: Vec<String>,",
),
)
.expect("failed to retype allowed_resources");

let err = run().expect_err("a non-index field is not a value range");
let err = err.to_string();
assert!(
err.contains("1 error(s), 0 warning(s)"),
"unexpected summary: {err}"
);
}

#[test]
fn check_rejects_stale_scalar_group_variable_targets() {
let _cwd_guard = test_support::lock_cwd();
Expand Down
11 changes: 11 additions & 0 deletions src/commands/generate_constraint/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pub(crate) struct ListVarInfo {
pub(crate) struct EntityInfo {
pub field_name: String,
pub item_type: String,
/// `Vec<usize>` fields that can serve as this entity's own value range.
pub entity_value_ranges: Vec<String>,
pub scalar_vars: Vec<ScalarVarInfo>,
pub list_vars: Vec<ListVarInfo>,
}
Expand Down Expand Up @@ -81,6 +83,7 @@ struct SolutionInfo {
}

struct EntityStructInfo {
entity_value_ranges: Vec<String>,
scalar_vars: Vec<ScalarVarInfo>,
list_vars: Vec<ListVarInfo>,
}
Expand Down Expand Up @@ -342,6 +345,7 @@ fn collect_domain_model(
domain_entities.push(EntityInfo {
field_name: field_name.clone(),
item_type: collection_type.to_string(),
entity_value_ranges: entity.entity_value_ranges.clone(),
scalar_vars: entity.scalar_vars.clone(),
list_vars: entity.list_vars.clone(),
});
Expand Down Expand Up @@ -439,10 +443,16 @@ fn parse_solution(item_struct: &ItemStruct) -> Result<SolutionInfo, String> {

fn parse_entity(item_struct: &ItemStruct) -> Result<EntityStructInfo, String> {
let fields = named_fields(item_struct, "#[planning_entity] requires named fields")?;
let mut entity_value_ranges = Vec::new();
let mut scalar_vars = Vec::new();
let mut list_vars = Vec::new();

for field in fields {
if collection_type_name(&field.ty).as_deref() == Some("usize") {
if let Some(field_ident) = field.ident.as_ref() {
entity_value_ranges.push(field_ident.to_string());
}
}
if has_attribute(&field.attrs, "planning_variable") {
if !field_is_option_usize(&field.ty) {
continue;
Expand Down Expand Up @@ -482,6 +492,7 @@ fn parse_entity(item_struct: &ItemStruct) -> Result<EntityStructInfo, String> {
}

Ok(EntityStructInfo {
entity_value_ranges,
scalar_vars,
list_vars,
})
Expand Down
6 changes: 6 additions & 0 deletions src/commands/generate_constraint/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ fn test_generate_skeleton_unary_hard() {
scalar_groups_path: None,
conflict_repairs_path: None,
entities: vec![EntityInfo {
entity_value_ranges: Vec::new(),
field_name: "shifts".to_string(),
item_type: "Shift".to_string(),
scalar_vars: vec![ScalarVarInfo {
Expand Down Expand Up @@ -541,6 +542,7 @@ fn test_generate_skeleton_pair_hard() {
scalar_groups_path: None,
conflict_repairs_path: None,
entities: vec![EntityInfo {
entity_value_ranges: Vec::new(),
field_name: "shifts".to_string(),
item_type: "Shift".to_string(),
scalar_vars: vec![ScalarVarInfo {
Expand Down Expand Up @@ -582,6 +584,7 @@ fn test_generate_skeleton_join_hard() {
scalar_groups_path: None,
conflict_repairs_path: None,
entities: vec![EntityInfo {
entity_value_ranges: Vec::new(),
field_name: "shifts".to_string(),
item_type: "Shift".to_string(),
scalar_vars: vec![ScalarVarInfo {
Expand Down Expand Up @@ -632,6 +635,7 @@ fn test_generate_skeleton_balance_soft() {
scalar_groups_path: None,
conflict_repairs_path: None,
entities: vec![EntityInfo {
entity_value_ranges: Vec::new(),
field_name: "shifts".to_string(),
item_type: "Shift".to_string(),
scalar_vars: vec![ScalarVarInfo {
Expand Down Expand Up @@ -669,6 +673,7 @@ fn test_generate_skeleton_reward_soft_is_compile_safe() {
scalar_groups_path: None,
conflict_repairs_path: None,
entities: vec![EntityInfo {
entity_value_ranges: Vec::new(),
field_name: "shifts".to_string(),
item_type: "Shift".to_string(),
scalar_vars: vec![ScalarVarInfo {
Expand Down Expand Up @@ -819,6 +824,7 @@ fn grouped_skeleton_domain() -> DomainModel {
scalar_groups_path: None,
conflict_repairs_path: None,
entities: vec![EntityInfo {
entity_value_ranges: Vec::new(),
field_name: "shifts".to_string(),
item_type: "Shift".to_string(),
scalar_vars: vec![ScalarVarInfo {
Expand Down
1 change: 1 addition & 0 deletions src/model_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@ mod tests {
scalar_groups_path: Some("scalar_groups".to_string()),
conflict_repairs_path: None,
entities: vec![EntityInfo {
entity_value_ranges: Vec::new(),
field_name: "tasks".to_string(),
item_type: "Task".to_string(),
scalar_vars: vec![ScalarVarInfo {
Expand Down
Loading