-
-
Notifications
You must be signed in to change notification settings - Fork 1
fix(release): port logging, score parsing, and sf-config wiring #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
dac2498
ecdbdef
e3d2294
ef152e8
c0f6571
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,4 +9,5 @@ pub mod info; | |
| pub mod new; | ||
| pub mod routes; | ||
| pub mod server; | ||
| pub mod sf_config; | ||
| pub mod test; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| // JSON-based sf-config.json reader/writer for UI wiring. | ||
| // Reads/modifies/writes static/sf-config.json. | ||
| // If the file doesn't exist, operations return Ok(()) silently. | ||
|
|
||
| use std::fs; | ||
| use std::path::Path; | ||
|
|
||
| use serde::{Deserialize, Serialize}; | ||
| use serde_json::Value; | ||
|
|
||
| use crate::error::CliResult; | ||
|
|
||
| #[derive(Debug, Deserialize, Serialize)] | ||
| struct EntityEntry { | ||
| name: String, | ||
| label: String, | ||
| plural: String, | ||
| } | ||
|
|
||
| #[derive(Debug, Deserialize, Serialize)] | ||
| struct SfConfig { | ||
| title: String, | ||
| subtitle: String, | ||
| #[serde(default)] | ||
| constraints: Vec<String>, | ||
|
Comment on lines
+21
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| #[serde(default)] | ||
| entities: Vec<EntityEntry>, | ||
| #[serde(default)] | ||
| facts: Vec<EntityEntry>, | ||
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| view: Option<Value>, | ||
| } | ||
|
|
||
| const CONFIG_PATH: &str = "static/sf-config.json"; | ||
|
|
||
| fn load() -> Option<SfConfig> { | ||
| let path = Path::new(CONFIG_PATH); | ||
| if !path.exists() { | ||
| return None; | ||
| } | ||
| let content = fs::read_to_string(path).ok()?; | ||
| serde_json::from_str(&content).ok() | ||
| } | ||
|
|
||
| fn save(config: &SfConfig) -> CliResult { | ||
| let json = serde_json::to_string_pretty(config).map_err(|e| { | ||
| crate::error::CliError::general(format!("sf-config serialize error: {}", e)) | ||
| })?; | ||
| fs::write(CONFIG_PATH, json).map_err(|e| crate::error::CliError::IoError { | ||
| context: "failed to write sf-config.json".to_string(), | ||
| source: e, | ||
| })?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn add_constraint(name: &str) -> CliResult { | ||
| let Some(mut config) = load() else { | ||
| return Ok(()); | ||
| }; | ||
| if !config.constraints.contains(&name.to_string()) { | ||
| config.constraints.push(name.to_string()); | ||
| save(&config)?; | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn remove_constraint(name: &str) -> CliResult { | ||
| let Some(mut config) = load() else { | ||
| return Ok(()); | ||
| }; | ||
| config.constraints.retain(|c| c != name); | ||
| save(&config) | ||
| } | ||
|
|
||
| pub fn add_entity(name: &str, label: &str, plural: &str) -> CliResult { | ||
| let Some(mut config) = load() else { | ||
| return Ok(()); | ||
| }; | ||
| if !config.entities.iter().any(|e| e.name == name) { | ||
| config.entities.push(EntityEntry { | ||
| name: name.to_string(), | ||
| label: label.to_string(), | ||
| plural: plural.to_string(), | ||
| }); | ||
| save(&config)?; | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn remove_entity(name: &str) -> CliResult { | ||
| let Some(mut config) = load() else { | ||
| return Ok(()); | ||
| }; | ||
| config.entities.retain(|e| e.name != name); | ||
| save(&config) | ||
| } | ||
|
|
||
| pub fn add_fact(name: &str, label: &str, plural: &str) -> CliResult { | ||
| let Some(mut config) = load() else { | ||
| return Ok(()); | ||
| }; | ||
| if !config.facts.iter().any(|f| f.name == name) { | ||
| config.facts.push(EntityEntry { | ||
| name: name.to_string(), | ||
| label: label.to_string(), | ||
| plural: plural.to_string(), | ||
| }); | ||
| save(&config)?; | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn remove_fact(name: &str) -> CliResult { | ||
| let Some(mut config) = load() else { | ||
| return Ok(()); | ||
| }; | ||
| config.facts.retain(|f| f.name != name); | ||
| save(&config) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use std::sync::{Mutex, OnceLock}; | ||
|
|
||
| static CWD_LOCK: OnceLock<Mutex<()>> = OnceLock::new(); | ||
|
|
||
| #[test] | ||
| fn preserves_arbitrary_view_shape_when_updating_constraints() { | ||
| let _guard = CWD_LOCK | ||
| .get_or_init(|| Mutex::new(())) | ||
| .lock() | ||
| .expect("cwd lock poisoned"); | ||
|
|
||
| let tmp = tempfile::tempdir().expect("failed to create temp dir"); | ||
| let original_dir = std::env::current_dir().expect("failed to get current dir"); | ||
|
|
||
| std::fs::create_dir_all(tmp.path().join("static")).expect("failed to create static dir"); | ||
| std::fs::write( | ||
| tmp.path().join("static").join("sf-config.json"), | ||
| r#"{ | ||
| "title": "demo", | ||
| "subtitle": "demo", | ||
| "constraints": ["all_assigned"], | ||
| "view": { | ||
| "type": "assignment_board", | ||
| "columns": [{"id": "todo"}], | ||
| "meta": {"accent": "red"} | ||
| } | ||
| }"#, | ||
| ) | ||
| .expect("failed to write sf-config"); | ||
|
|
||
| std::env::set_current_dir(tmp.path()).expect("failed to enter temp dir"); | ||
| add_constraint("capacity_limit").expect("add_constraint should succeed"); | ||
| let saved = std::fs::read_to_string(tmp.path().join("static").join("sf-config.json")) | ||
| .expect("failed to read saved sf-config"); | ||
| std::env::set_current_dir(original_dir).expect("failed to restore current dir"); | ||
|
|
||
| assert!(saved.contains("\"capacity_limit\"")); | ||
| assert!(saved.contains("\"assignment_board\"")); | ||
| assert!(saved.contains("\"columns\"")); | ||
| assert!(saved.contains("\"accent\": \"red\"")); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ use std::fmt::Debug; | |
|
|
||
| use solverforge_config::AcceptorConfig; | ||
| use solverforge_core::domain::PlanningSolution; | ||
| use solverforge_core::score::Score; | ||
| use solverforge_core::score::{ParseableScore, Score}; | ||
|
|
||
| use crate::phase::localsearch::{ | ||
| Acceptor, GreatDelugeAcceptor, HillClimbingAcceptor, LateAcceptanceAcceptor, | ||
|
|
@@ -121,7 +121,7 @@ impl AcceptorBuilder { | |
| /// Builds a concrete [`AnyAcceptor`] from configuration. | ||
| pub fn build<S: PlanningSolution>(config: &AcceptorConfig) -> AnyAcceptor<S> | ||
| where | ||
| S::Score: Score, | ||
| S::Score: Score + ParseableScore, | ||
| { | ||
| match config { | ||
| AcceptorConfig::HillClimbing => AnyAcceptor::HillClimbing(HillClimbingAcceptor::new()), | ||
|
|
@@ -138,7 +138,12 @@ impl AcceptorBuilder { | |
| let starting_temp = sa_config | ||
| .starting_temperature | ||
| .as_ref() | ||
| .and_then(|s| s.parse::<f64>().ok()) | ||
| .map(|s| { | ||
| let score = S::Score::parse(s).unwrap_or_else(|e| { | ||
| panic!("Invalid starting_temperature '{}': {}", s, e) | ||
| }); | ||
| score.to_scalar().abs() | ||
|
||
| }) | ||
| .unwrap_or(0.0); | ||
| AnyAcceptor::SimulatedAnnealing(SimulatedAnnealingAcceptor::new( | ||
| starting_temp, | ||
|
|
@@ -216,7 +221,7 @@ mod tests { | |
| #[test] | ||
| fn test_acceptor_builder_simulated_annealing() { | ||
| let config = AcceptorConfig::SimulatedAnnealing(SimulatedAnnealingConfig { | ||
| starting_temperature: Some("1.5".to_string()), | ||
| starting_temperature: Some("2".to_string()), | ||
| }); | ||
| let _acceptor: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In flat-shape constraint modules like
templates/list/vehicle-routing/src/constraints/mod.rs,generate constraintappendsfoo::constraint()into the same tuple line as the existing constraints. This branch drops any line containingfoo::, sodestroy constraint foodeletes the entire tuple expression instead of just that element, leavingcreate_constraints()with no return value and breaking compilation for projects using that layout.Useful? React with 👍 / 👎.