use std::fs;
use std::path::Path;
type CollectionPair = (Vec<(String, String)>, Vec<(String, String)>);
#[derive(Debug, Clone)]
pub(crate) struct StandardVarInfo {
pub field: String,
pub value_range: String,
pub allows_unassigned: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct ListVarInfo {
pub field: String,
pub element_collection: String,
}
#[derive(Debug)]
pub(crate) struct EntityInfo {
pub field_name: String,
pub item_type: String,
pub planning_vars: Vec<StandardVarInfo>,
pub list_vars: Vec<ListVarInfo>,
}
#[derive(Debug)]
pub(crate) struct FactInfo {
pub field_name: String,
pub item_type: String,
}
#[derive(Debug)]
pub(crate) struct DomainModel {
pub solution_type: String,
pub score_type: String,
pub entities: Vec<EntityInfo>,
pub facts: Vec<FactInfo>,
}
pub(crate) fn parse_domain() -> Option<DomainModel> {
let domain_dir = Path::new("src/domain");
if !domain_dir.exists() {
return None;
}
let entries = fs::read_dir(domain_dir).ok()?;
let mut all_src = String::new();
let mut file_contents: Vec<(String, String)> = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
if let Ok(src) = fs::read_to_string(&path) {
if let Some(struct_name) = find_struct_name(&src) {
file_contents.push((struct_name, src.clone()));
}
all_src.push_str(&src);
all_src.push('\n');
}
}
if all_src.is_empty() {
return None;
}
let solution_type = find_annotated_struct(&all_src, "planning_solution")?;
let score_type =
find_score_type(&all_src, &solution_type).unwrap_or_else(|| "HardSoftScore".to_string());
let (entities_raw, facts_raw) = find_collections(&all_src, &solution_type);
let entities: Vec<EntityInfo> = entities_raw
.into_iter()
.map(|(field_name, item_type)| {
let planning_vars = find_planning_vars_for_type(&file_contents, &item_type);
let list_vars = find_list_vars_for_type(&file_contents, &item_type);
EntityInfo {
field_name,
item_type,
planning_vars,
list_vars,
}
})
.collect();
let facts: Vec<FactInfo> = facts_raw
.into_iter()
.map(|(field_name, item_type)| FactInfo {
field_name,
item_type,
})
.collect();
Some(DomainModel {
solution_type,
score_type,
entities,
facts,
})
}
fn find_struct_name(src: &str) -> Option<String> {
for line in src.lines() {
let t = line.trim();
if t.starts_with("pub struct ") || t.starts_with("struct ") {
let after = t.trim_start_matches("pub ").trim_start_matches("struct ");
let name: String = after
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if !name.is_empty() {
return Some(name);
}
}
}
None
}
pub(crate) fn find_annotated_struct(src: &str, attr: &str) -> Option<String> {
let lines: Vec<&str> = src.lines().collect();
for (i, line) in lines.iter().enumerate() {
let t = line.trim();
if t.contains(&format!("#[{}]", attr)) || t.contains(&format!("#[{}(", attr)) {
for next_line in lines.iter().skip(i + 1) {
let next = next_line.trim();
if next.is_empty() {
continue;
}
if next.starts_with("pub struct ") || next.starts_with("struct ") {
let after = next
.trim_start_matches("pub ")
.trim_start_matches("struct ");
let name: String = after
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if !name.is_empty() {
return Some(name);
}
}
}
}
}
None
}
pub(crate) fn find_score_type(src: &str, solution_type: &str) -> Option<String> {
let lines: Vec<&str> = src.lines().collect();
let mut in_solution_struct = false;
let mut brace_depth = 0i32;
for line in &lines {
let t = line.trim();
if !in_solution_struct {
if t.contains(&format!("struct {}", solution_type)) {
in_solution_struct = true;
brace_depth += t.chars().filter(|&c| c == '{').count() as i32;
brace_depth -= t.chars().filter(|&c| c == '}').count() as i32;
}
continue;
}
brace_depth += t.chars().filter(|&c| c == '{').count() as i32;
brace_depth -= t.chars().filter(|&c| c == '}').count() as i32;
if brace_depth <= 0 {
break;
}
if t.contains("score") && t.contains(':') {
for score in &[
"HardSoftDecimalScore",
"HardMediumSoftScore",
"HardSoftScore",
"BendableScore",
"SimpleScore",
] {
if t.contains(score) {
return Some(score.to_string());
}
}
}
}
None
}
fn find_collections(src: &str, solution_type: &str) -> CollectionPair {
let lines: Vec<&str> = src.lines().collect();
let mut in_solution_struct = false;
let mut brace_depth = 0i32;
let mut entities = Vec::new();
let mut facts = Vec::new();
let mut next_annotation: Option<&str> = None;
for line in &lines {
let t = line.trim();
if !in_solution_struct {
if t.contains(&format!("struct {}", solution_type)) {
in_solution_struct = true;
}
continue;
}
brace_depth += t.chars().filter(|&c| c == '{').count() as i32;
brace_depth -= t.chars().filter(|&c| c == '}').count() as i32;
if brace_depth <= 0 && in_solution_struct && t.contains('}') {
break;
}
if t.contains("#[planning_entity_collection]")
|| t.contains("#[planning_entity_collection(")
{
next_annotation = Some("entity");
} else if t.contains("#[problem_fact_collection]")
|| t.contains("#[problem_fact_collection(")
{
next_annotation = Some("fact");
} else if let Some(ann) = next_annotation.take() {
if let Some((field, item)) = parse_vec_field(t) {
match ann {
"entity" => entities.push((field, item)),
"fact" => facts.push((field, item)),
_ => {}
}
}
} else {
next_annotation = None;
}
}
(entities, facts)
}
pub(crate) fn parse_vec_field(line: &str) -> Option<(String, String)> {
let t = line.trim().trim_end_matches(',');
let t = t.trim_start_matches("pub ");
if let Some(colon) = t.find(':') {
let field = t[..colon].trim().to_string();
let type_part = t[colon + 1..].trim();
if let Some(inner) = extract_vec_inner(type_part) {
return Some((field, inner));
}
if type_part.starts_with("&[") || type_part.starts_with('[') {
let inner = type_part
.trim_start_matches('&')
.trim_start_matches('[')
.trim_end_matches(']');
if !inner.is_empty() {
return Some((field, inner.to_string()));
}
}
}
None
}
fn extract_vec_inner(s: &str) -> Option<String> {
let s = s.trim();
if s.starts_with("Vec<") && s.ends_with('>') {
Some(s[4..s.len() - 1].to_string())
} else if s.starts_with("Option<Vec<") && s.ends_with(">>") {
Some(s[11..s.len() - 2].to_string())
} else {
None
}
}
fn find_planning_vars_for_type(
file_contents: &[(String, String)],
type_name: &str,
) -> Vec<StandardVarInfo> {
for (struct_name, src) in file_contents {
if struct_name == type_name {
return find_planning_vars_in_src(src);
}
}
Vec::new()
}
fn find_list_vars_for_type(
file_contents: &[(String, String)],
type_name: &str,
) -> Vec<ListVarInfo> {
for (struct_name, src) in file_contents {
if struct_name == type_name {
return find_list_vars_in_src(src);
}
}
Vec::new()
}
fn find_planning_vars_in_src(src: &str) -> Vec<StandardVarInfo> {
let lines: Vec<&str> = src.lines().collect();
let mut vars = Vec::new();
let mut current_attr: Option<&str> = None;
for line in &lines {
let t = line.trim();
if t.contains("#[planning_variable]") || t.contains("#[planning_variable(") {
current_attr = Some(t);
} else if let Some(attr) = current_attr.take() {
let t = t.trim_start_matches("pub ").trim_end_matches(',');
if let Some(colon) = t.find(':') {
let field = t[..colon].trim().to_string();
if !field.is_empty() {
vars.push(StandardVarInfo {
field,
value_range: extract_attr_value(attr, "value_range").unwrap_or_default(),
allows_unassigned: attr.contains("allows_unassigned = true"),
});
}
}
}
}
vars
}
fn find_list_vars_in_src(src: &str) -> Vec<ListVarInfo> {
let lines: Vec<&str> = src.lines().collect();
let mut vars = Vec::new();
let mut current_attr: Option<&str> = None;
for line in &lines {
let t = line.trim();
if t.contains("#[planning_list_variable]") || t.contains("#[planning_list_variable(") {
current_attr = Some(t);
} else if let Some(attr) = current_attr.take() {
let t = t.trim_start_matches("pub ").trim_end_matches(',');
if let Some(colon) = t.find(':') {
let field = t[..colon].trim().to_string();
if !field.is_empty() {
vars.push(ListVarInfo {
field,
element_collection: extract_attr_value(attr, "element_collection")
.unwrap_or_default(),
});
}
}
}
}
vars
}
fn extract_attr_value(attr: &str, key: &str) -> Option<String> {
let pattern = format!("{key} = \"");
let start = attr.find(&pattern)? + pattern.len();
let rest = &attr[start..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
pub(crate) fn list_constraints(dir: &Path) -> Vec<String> {
let mut constraints = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("rs") {
let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
if name != "mod" {
constraints.push(name.to_string());
}
}
}
}
constraints.sort();
constraints
}