1use crate::errors::CoreError;
4use serde_json::{Map, Value};
5use std::collections::HashSet;
6
7#[derive(Debug, Clone, Default)]
8pub struct GraphGenValidationResult {
9 pub errors: Vec<Value>,
10 pub warnings: Vec<String>,
11}
12
13#[derive(Debug, Clone)]
14struct GraphOptModels {
15 policy_models: HashSet<String>,
16}
17
18fn load_graph_opt_models() -> Option<GraphOptModels> {
19 let raw = include_str!("../../assets/supported_models.json");
20 let value: Value = serde_json::from_str(raw).ok()?;
21 let graph_opt = value.get("graph_opt")?.as_object()?;
22 let policy_models = graph_opt.get("policy_models")?.as_array()?;
23 let mut set = HashSet::new();
24 for item in policy_models {
25 if let Some(name) = item.as_str() {
26 set.insert(name.to_string());
27 }
28 }
29 Some(GraphOptModels { policy_models: set })
30}
31
32static GRAPH_OPT_MODELS: once_cell::sync::Lazy<Option<GraphOptModels>> =
33 once_cell::sync::Lazy::new(load_graph_opt_models);
34
35fn value_to_string(value: &Value) -> Option<String> {
36 match value {
37 Value::String(s) => Some(s.to_string()),
38 Value::Number(n) => Some(n.to_string()),
39 Value::Bool(b) => Some(b.to_string()),
40 _ => None,
41 }
42}
43
44fn parse_int(value: &Value) -> Option<i64> {
45 match value {
46 Value::Number(n) => n.as_i64().or_else(|| n.as_f64().map(|f| f as i64)),
47 Value::String(s) => s.trim().parse::<i64>().ok(),
48 _ => None,
49 }
50}
51
52fn similarity(a: &str, b: &str) -> f64 {
53 let a_chars: Vec<char> = a.chars().collect();
54 let b_chars: Vec<char> = b.chars().collect();
55 let max_len = a_chars.len().max(b_chars.len()).max(1);
56 let dist = levenshtein(&a_chars, &b_chars) as f64;
57 1.0 - (dist / max_len as f64)
58}
59
60fn levenshtein(a: &[char], b: &[char]) -> usize {
61 let mut costs: Vec<usize> = (0..=b.len()).collect();
62 for (i, ca) in a.iter().enumerate() {
63 let mut prev = costs[0];
64 costs[0] = i + 1;
65 for (j, cb) in b.iter().enumerate() {
66 let temp = costs[j + 1];
67 let mut new_cost = prev + if ca == cb { 0 } else { 1 };
68 new_cost = new_cost.min(costs[j] + 1).min(temp + 1);
69 costs[j + 1] = new_cost;
70 prev = temp;
71 }
72 }
73 costs[b.len()]
74}
75
76fn find_similar_models(model: &str, supported: &HashSet<String>) -> Vec<String> {
77 let mut scored: Vec<(f64, String)> = supported
78 .iter()
79 .map(|candidate| (similarity(model, candidate), candidate.clone()))
80 .filter(|(score, _)| *score >= 0.4)
81 .collect();
82 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
83 scored.into_iter().take(3).map(|(_, m)| m).collect()
84}
85
86fn push_error(
87 errors: &mut Vec<Value>,
88 field: &str,
89 error: String,
90 suggestion: Option<String>,
91 similar: Option<Vec<String>>,
92) {
93 let mut map = Map::new();
94 map.insert("field".to_string(), Value::String(field.to_string()));
95 map.insert("error".to_string(), Value::String(error));
96 if let Some(suggestion) = suggestion {
97 map.insert("suggestion".to_string(), Value::String(suggestion));
98 }
99 if let Some(similar) = similar {
100 map.insert(
101 "similar".to_string(),
102 Value::Array(similar.into_iter().map(Value::String).collect()),
103 );
104 }
105 errors.push(Value::Object(map));
106}
107
108pub fn validate_graphgen_job_config(config: &Value, dataset: &Value) -> GraphGenValidationResult {
109 let mut result = GraphGenValidationResult::default();
110
111 let config_map = match config.as_object() {
112 Some(map) => map,
113 None => {
114 push_error(
115 &mut result.errors,
116 "policy_models",
117 "policy_models is required".to_string(),
118 None,
119 None,
120 );
121 return result;
122 }
123 };
124
125 let policy_models_raw = config_map
126 .get("policy_models")
127 .or_else(|| config_map.get("policy_model"))
128 .or_else(|| config_map.get("model"));
129 let mut policy_models_list: Vec<String> = Vec::new();
130 match policy_models_raw {
131 None => {
132 push_error(
133 &mut result.errors,
134 "policy_models",
135 "policy_models is required".to_string(),
136 Some("Supported models: see graph_opt.policy_models".to_string()),
137 None,
138 );
139 }
140 Some(Value::Array(arr)) => {
141 for item in arr {
142 if let Some(name) = value_to_string(item) {
143 policy_models_list.push(name);
144 } else {
145 push_error(
146 &mut result.errors,
147 "policy_models",
148 "policy_models contains empty value".to_string(),
149 Some("Supported models: see graph_opt.policy_models".to_string()),
150 None,
151 );
152 }
153 }
154 }
155 Some(value) => {
156 if let Some(name) = value_to_string(value) {
157 policy_models_list.push(name);
158 } else {
159 push_error(
160 &mut result.errors,
161 "policy_models",
162 "policy_models contains empty value".to_string(),
163 Some("Supported models: see graph_opt.policy_models".to_string()),
164 None,
165 );
166 }
167 }
168 }
169
170 if let Some(models) = GRAPH_OPT_MODELS.as_ref() {
171 for policy_model in &policy_models_list {
172 let clean = policy_model.trim();
173 if clean.is_empty() {
174 push_error(
175 &mut result.errors,
176 "policy_models",
177 "policy_models contains empty value".to_string(),
178 Some("Supported models: see graph_opt.policy_models".to_string()),
179 None,
180 );
181 continue;
182 }
183 if !models.policy_models.contains(clean) {
184 let similar = find_similar_models(clean, &models.policy_models);
185 push_error(
186 &mut result.errors,
187 "policy_models",
188 format!("Unsupported policy model: {}", clean),
189 Some(format!("Supported models: {:?}", models.policy_models)),
190 if similar.is_empty() {
191 None
192 } else {
193 Some(similar)
194 },
195 );
196 }
197 }
198 }
199
200 if let Some(effort) = config_map.get("proposer_effort").and_then(|v| v.as_str()) {
201 if effort != "low" && effort != "medium" && effort != "high" {
202 push_error(
203 &mut result.errors,
204 "proposer_effort",
205 format!("Invalid proposer_effort: {}", effort),
206 Some("Must be one of: 'low', 'medium', 'high'".to_string()),
207 None,
208 );
209 }
210 }
211
212 let rollout_budget = config_map
213 .get("rollout_budget")
214 .or_else(|| config_map.get("budget"))
215 .and_then(parse_int)
216 .unwrap_or(100);
217 if rollout_budget < 10 {
218 push_error(
219 &mut result.errors,
220 "rollout_budget",
221 format!("rollout_budget must be >= 10, got {}", rollout_budget),
222 None,
223 None,
224 );
225 }
226 if rollout_budget > 10000 {
227 push_error(
228 &mut result.errors,
229 "rollout_budget",
230 format!("rollout_budget must be <= 10000, got {}", rollout_budget),
231 None,
232 None,
233 );
234 }
235
236 let dataset_map = match dataset.as_object() {
237 Some(map) => map,
238 None => {
239 push_error(
240 &mut result.errors,
241 "dataset",
242 "dataset must be a dict".to_string(),
243 None,
244 None,
245 );
246 return result;
247 }
248 };
249
250 match dataset_map.get("tasks") {
251 Some(Value::Array(tasks)) => {
252 if tasks.is_empty() {
253 push_error(
254 &mut result.errors,
255 "dataset.tasks",
256 "Dataset must contain at least one task".to_string(),
257 None,
258 None,
259 );
260 } else if tasks.len() < 2 {
261 result.warnings.push(
262 "GraphGen datasets with <2 tasks are unlikely to optimize meaningfully."
263 .to_string(),
264 );
265 }
266 }
267 _ => {
268 push_error(
269 &mut result.errors,
270 "dataset.tasks",
271 "Dataset must contain at least one task".to_string(),
272 None,
273 None,
274 );
275 }
276 }
277
278 result
279}
280
281pub fn graph_opt_supported_models() -> Value {
283 let raw = include_str!("../../assets/supported_models.json");
284 let value: Value = match serde_json::from_str(raw) {
285 Ok(v) => v,
286 Err(_) => return Value::Object(Map::new()),
287 };
288 value
289 .get("graph_opt")
290 .cloned()
291 .unwrap_or_else(|| Value::Object(Map::new()))
292}
293
294pub fn validate_graphgen_taskset(dataset: &Value) -> Vec<Value> {
296 let mut errors: Vec<Value> = Vec::new();
297
298 let dataset_map = match dataset.as_object() {
299 Some(map) => map,
300 None => {
301 push_error(
302 &mut errors,
303 "dataset",
304 "dataset must be an object".to_string(),
305 None,
306 None,
307 );
308 return errors;
309 }
310 };
311
312 match dataset_map.get("metadata") {
314 Some(Value::Object(meta)) => match meta.get("name") {
315 Some(Value::String(name)) if !name.trim().is_empty() => {}
316 _ => {
317 push_error(
318 &mut errors,
319 "metadata.name",
320 "metadata.name is required".to_string(),
321 None,
322 None,
323 );
324 }
325 },
326 _ => {
327 push_error(
328 &mut errors,
329 "metadata",
330 "metadata is required".to_string(),
331 None,
332 None,
333 );
334 }
335 }
336
337 let mut task_ids: HashSet<String> = HashSet::new();
339 match dataset_map.get("tasks") {
340 Some(Value::Array(tasks)) => {
341 if tasks.is_empty() {
342 push_error(
343 &mut errors,
344 "tasks",
345 "dataset must contain at least one task".to_string(),
346 None,
347 None,
348 );
349 }
350 for (idx, task) in tasks.iter().enumerate() {
351 match task.as_object() {
352 Some(task_map) => match task_map.get("id") {
353 Some(Value::String(id)) if !id.trim().is_empty() => {
354 if !task_ids.insert(id.to_string()) {
355 push_error(
356 &mut errors,
357 "tasks.id",
358 format!("duplicate task id '{}'", id),
359 None,
360 None,
361 );
362 }
363 }
364 _ => {
365 push_error(
366 &mut errors,
367 &format!("tasks[{}].id", idx),
368 "task id is required".to_string(),
369 None,
370 None,
371 );
372 }
373 },
374 None => {
375 push_error(
376 &mut errors,
377 &format!("tasks[{}]", idx),
378 "task must be an object".to_string(),
379 None,
380 None,
381 );
382 }
383 }
384 }
385 }
386 _ => {
387 push_error(
388 &mut errors,
389 "tasks",
390 "dataset.tasks must be a non-empty list".to_string(),
391 None,
392 None,
393 );
394 }
395 }
396
397 if let Some(Value::Array(gold_outputs)) = dataset_map.get("gold_outputs") {
399 for (idx, gold) in gold_outputs.iter().enumerate() {
400 if let Some(gold_map) = gold.as_object() {
401 if let Some(Value::String(task_id)) = gold_map.get("task_id") {
402 if !task_id.is_empty() && !task_ids.contains(task_id) {
403 push_error(
404 &mut errors,
405 &format!("gold_outputs[{}].task_id", idx),
406 format!("invalid task_id '{}'", task_id),
407 None,
408 None,
409 );
410 }
411 }
412 }
413 }
414 }
415
416 let select_output = dataset_map.get("select_output").or_else(|| {
418 dataset_map
419 .get("metadata")
420 .and_then(|m| m.as_object())
421 .and_then(|m| m.get("select_output"))
422 });
423 if let Some(value) = select_output {
424 match value {
425 Value::Null | Value::String(_) => {}
426 Value::Array(items) => {
427 if !items.iter().all(|item| item.as_str().is_some()) {
428 push_error(
429 &mut errors,
430 "select_output",
431 "select_output must be a string or list of strings".to_string(),
432 None,
433 None,
434 );
435 }
436 }
437 _ => {
438 push_error(
439 &mut errors,
440 "select_output",
441 "select_output must be a string or list of strings".to_string(),
442 None,
443 None,
444 );
445 }
446 }
447 }
448
449 let output_config = dataset_map.get("output_config").or_else(|| {
451 dataset_map
452 .get("metadata")
453 .and_then(|m| m.as_object())
454 .and_then(|m| m.get("output_config"))
455 });
456 if let Some(value) = output_config {
457 if !value.is_null() && !value.is_object() {
458 push_error(
459 &mut errors,
460 "output_config",
461 "output_config must be an object".to_string(),
462 None,
463 None,
464 );
465 }
466 }
467
468 for field in ["input_schema", "output_schema"] {
470 let value = dataset_map.get(field).or_else(|| {
471 dataset_map
472 .get("metadata")
473 .and_then(|m| m.as_object())
474 .and_then(|m| m.get(field))
475 });
476 if let Some(v) = value {
477 if !v.is_null() && !v.is_object() {
478 push_error(
479 &mut errors,
480 field,
481 format!("{} must be an object", field),
482 None,
483 None,
484 );
485 }
486 }
487 }
488
489 errors
490}
491
492pub fn parse_graphgen_taskset(dataset: &Value) -> Result<Value, CoreError> {
494 let errors = validate_graphgen_taskset(dataset);
495 if !errors.is_empty() {
496 return Err(CoreError::Validation(format!(
497 "invalid GraphGenTaskSet: {} errors",
498 errors.len()
499 )));
500 }
501 Ok(dataset.clone())
502}
503
504pub fn load_graphgen_taskset(path: &std::path::Path) -> Result<Value, CoreError> {
506 let contents = std::fs::read_to_string(path).map_err(|e| {
507 CoreError::InvalidInput(format!(
508 "failed to read dataset file '{}': {}",
509 path.display(),
510 e
511 ))
512 })?;
513 let value: Value = serde_json::from_str(&contents).map_err(|e| {
514 CoreError::Validation(format!(
515 "failed to parse dataset JSON '{}': {}",
516 path.display(),
517 e
518 ))
519 })?;
520 parse_graphgen_taskset(&value)
521}
522
523fn push_graph_error(
528 errors: &mut Vec<Value>,
529 field: &str,
530 error: String,
531 suggestion: Option<String>,
532) {
533 let mut map = Map::new();
534 map.insert("field".to_string(), Value::String(field.to_string()));
535 map.insert("error".to_string(), Value::String(error));
536 if let Some(suggestion) = suggestion {
537 map.insert("suggestion".to_string(), Value::String(suggestion));
538 }
539 errors.push(Value::Object(map));
540}
541
542fn value_to_bool(value: &Value, default_value: bool) -> bool {
543 match value {
544 Value::Bool(v) => *v,
545 Value::Number(n) => n.as_i64().map(|v| v != 0).unwrap_or(default_value),
546 Value::String(s) => {
547 let trimmed = s.trim().to_lowercase();
548 match trimmed.as_str() {
549 "true" | "1" | "yes" => true,
550 "false" | "0" | "no" => false,
551 _ => default_value,
552 }
553 }
554 _ => default_value,
555 }
556}
557
558fn normalize_policy_models(raw: Option<&Value>, errors: &mut Vec<Value>) -> Vec<String> {
559 match raw {
560 None => {
561 push_graph_error(
562 errors,
563 "policy_models",
564 "policy_models is required".to_string(),
565 None,
566 );
567 Vec::new()
568 }
569 Some(Value::Array(arr)) => arr
570 .iter()
571 .filter_map(value_to_string)
572 .collect::<Vec<String>>(),
573 Some(value) => value_to_string(value).map(|v| vec![v]).unwrap_or_default(),
574 }
575}
576
577fn build_graph_config(section: &Map<String, Value>, errors: &mut Vec<Value>) -> Value {
578 let policy_models_raw = section
579 .get("policy_models")
580 .or_else(|| section.get("policy_model"))
581 .or_else(|| section.get("model"));
582 let policy_models = normalize_policy_models(policy_models_raw, errors);
583
584 let rollout_budget = section
585 .get("rollout_budget")
586 .or_else(|| section.get("budget"))
587 .and_then(parse_int)
588 .unwrap_or(100);
589
590 let proposer_effort = section
591 .get("proposer_effort")
592 .or_else(|| section.get("effort"))
593 .and_then(|v| v.as_str())
594 .unwrap_or("medium")
595 .to_string();
596
597 let mut map = Map::new();
598 map.insert(
599 "policy_models".to_string(),
600 Value::Array(policy_models.into_iter().map(Value::String).collect()),
601 );
602 if let Some(provider) = section.get("policy_provider").and_then(|v| v.as_str()) {
603 map.insert(
604 "policy_provider".to_string(),
605 Value::String(provider.to_string()),
606 );
607 }
608 map.insert(
609 "rollout_budget".to_string(),
610 Value::Number(rollout_budget.into()),
611 );
612 map.insert(
613 "proposer_effort".to_string(),
614 Value::String(proposer_effort),
615 );
616 if let Some(verifier_model) = section.get("verifier_model").and_then(|v| v.as_str()) {
617 map.insert(
618 "verifier_model".to_string(),
619 Value::String(verifier_model.to_string()),
620 );
621 }
622 if let Some(verifier_provider) = section.get("verifier_provider").and_then(|v| v.as_str()) {
623 map.insert(
624 "verifier_provider".to_string(),
625 Value::String(verifier_provider.to_string()),
626 );
627 }
628 if let Some(population_size) = section.get("population_size").and_then(parse_int) {
629 map.insert(
630 "population_size".to_string(),
631 Value::Number(population_size.into()),
632 );
633 } else {
634 map.insert("population_size".to_string(), Value::Number(4.into()));
635 }
636 if let Some(num_generations) = section.get("num_generations").and_then(parse_int) {
637 map.insert(
638 "num_generations".to_string(),
639 Value::Number(num_generations.into()),
640 );
641 }
642
643 Value::Object(map)
644}
645
646pub fn validate_graph_job_section(
647 section: &Value,
648 base_dir: Option<&std::path::Path>,
649) -> (Option<Value>, Vec<Value>) {
650 let mut errors: Vec<Value> = Vec::new();
651 let section_map = match section.as_object() {
652 Some(map) => map,
653 None => {
654 push_graph_error(
655 &mut errors,
656 "graph",
657 "graph section must be a table".to_string(),
658 None,
659 );
660 return (None, errors);
661 }
662 };
663
664 let dataset_ref = section_map
665 .get("dataset_path")
666 .or_else(|| section_map.get("dataset"))
667 .and_then(|v| v.as_str())
668 .map(|s| s.to_string());
669
670 let (dataset_path, dataset_value) = if let Some(path_str) = dataset_ref {
671 let mut path = std::path::PathBuf::from(path_str.clone());
672 if let Some(base) = base_dir {
673 if path.is_relative() {
674 path = base.join(path);
675 }
676 }
677 let resolved = path.clone();
678 let data = std::fs::read_to_string(&resolved);
679 match data {
680 Ok(contents) => match serde_json::from_str::<Value>(&contents) {
681 Ok(value) => (Some(resolved), Some(value)),
682 Err(err) => {
683 push_graph_error(
684 &mut errors,
685 "graph.dataset",
686 format!("Invalid GraphGenTaskSet JSON: {}", err),
687 None,
688 );
689 (Some(resolved), None)
690 }
691 },
692 Err(_) => {
693 push_graph_error(
694 &mut errors,
695 "graph.dataset",
696 format!("Dataset file not found: {}", resolved.display()),
697 None,
698 );
699 (Some(resolved), None)
700 }
701 }
702 } else {
703 push_graph_error(
704 &mut errors,
705 "graph.dataset",
706 "dataset (path) is required".to_string(),
707 Some("Set graph.dataset = \"my_tasks.json\"".to_string()),
708 );
709 (None, None)
710 };
711
712 let config_value = build_graph_config(section_map, &mut errors);
713
714 let auto_start = section_map
715 .get("auto_start")
716 .map(|v| value_to_bool(v, true))
717 .unwrap_or(true);
718 let metadata = match section_map.get("metadata") {
719 Some(Value::Object(map)) => Value::Object(map.clone()),
720 _ => Value::Object(Map::new()),
721 };
722 let initial_prompt = section_map
723 .get("initial_prompt")
724 .and_then(|v| v.as_str())
725 .map(|s| Value::String(s.to_string()));
726
727 if let Some(dataset) = dataset_value.as_ref() {
728 let validation = validate_graphgen_job_config(&config_value, dataset);
729 errors.extend(validation.errors);
730 }
731
732 if !errors.is_empty() {
733 return (None, errors);
734 }
735
736 let mut result = Map::new();
737 if let Some(path) = dataset_path {
738 result.insert(
739 "dataset_path".to_string(),
740 Value::String(path.to_string_lossy().to_string()),
741 );
742 }
743 if let Some(dataset) = dataset_value {
744 result.insert("dataset".to_string(), dataset);
745 }
746 result.insert("config".to_string(), config_value);
747 result.insert("auto_start".to_string(), Value::Bool(auto_start));
748 result.insert("metadata".to_string(), metadata);
749 if let Some(prompt) = initial_prompt {
750 result.insert("initial_prompt".to_string(), prompt);
751 }
752
753 (Some(Value::Object(result)), errors)
754}
755
756pub fn load_graph_job_toml(path: &std::path::Path) -> (Option<Value>, Vec<Value>) {
757 let content = match std::fs::read_to_string(path) {
758 Ok(content) => content,
759 Err(err) => {
760 let mut errors = Vec::new();
761 push_graph_error(
762 &mut errors,
763 "graph",
764 format!("Failed to read TOML: {}", err),
765 None,
766 );
767 return (None, errors);
768 }
769 };
770
771 let toml_value: Value = match crate::config::parse_toml(&content) {
772 Ok(value) => value,
773 Err(err) => {
774 let mut errors = Vec::new();
775 push_graph_error(
776 &mut errors,
777 "graph",
778 format!("Failed to parse TOML: {}", err),
779 None,
780 );
781 return (None, errors);
782 }
783 };
784
785 let graph_section = toml_value
786 .get("graph")
787 .cloned()
788 .unwrap_or_else(|| Value::Object(Map::new()));
789
790 validate_graph_job_section(&graph_section, path.parent())
791}
792
793pub fn validate_graph_job_payload(payload: &Value) -> Vec<Value> {
794 let mut errors = Vec::new();
795 let payload_map = match payload.as_object() {
796 Some(map) => map,
797 None => {
798 push_graph_error(
799 &mut errors,
800 "payload",
801 "Payload must be an object".to_string(),
802 None,
803 );
804 return errors;
805 }
806 };
807
808 let dataset = match payload_map.get("dataset") {
809 Some(Value::Object(map)) => Value::Object(map.clone()),
810 Some(_) => {
811 push_graph_error(
812 &mut errors,
813 "dataset",
814 "dataset must be a dict".to_string(),
815 None,
816 );
817 return errors;
818 }
819 None => {
820 push_graph_error(
821 &mut errors,
822 "dataset",
823 "dataset must be a dict".to_string(),
824 None,
825 );
826 return errors;
827 }
828 };
829
830 let metadata: Map<String, Value> = match payload_map.get("metadata") {
831 Some(Value::Object(map)) => map.clone(),
832 _ => Map::new(),
833 };
834
835 let policy_models_raw = payload_map
836 .get("policy_models")
837 .or_else(|| payload_map.get("policy_model"));
838 let policy_models = normalize_policy_models(policy_models_raw, &mut errors);
839
840 let rollout_budget = payload_map
841 .get("rollout_budget")
842 .and_then(parse_int)
843 .unwrap_or(100);
844 let proposer_effort = payload_map
845 .get("proposer_effort")
846 .and_then(|v| v.as_str())
847 .unwrap_or("medium")
848 .to_string();
849
850 let mut config_map = Map::new();
851 config_map.insert(
852 "policy_models".to_string(),
853 Value::Array(policy_models.into_iter().map(Value::String).collect()),
854 );
855 if let Some(policy_provider) = payload_map.get("policy_provider").and_then(|v| v.as_str()) {
856 config_map.insert(
857 "policy_provider".to_string(),
858 Value::String(policy_provider.to_string()),
859 );
860 }
861 config_map.insert(
862 "rollout_budget".to_string(),
863 Value::Number(rollout_budget.into()),
864 );
865 config_map.insert(
866 "proposer_effort".to_string(),
867 Value::String(proposer_effort),
868 );
869 if let Some(verifier_model) = payload_map.get("verifier_model").and_then(|v| v.as_str()) {
870 config_map.insert(
871 "verifier_model".to_string(),
872 Value::String(verifier_model.to_string()),
873 );
874 }
875 if let Some(verifier_provider) = payload_map
876 .get("verifier_provider")
877 .and_then(|v| v.as_str())
878 {
879 config_map.insert(
880 "verifier_provider".to_string(),
881 Value::String(verifier_provider.to_string()),
882 );
883 }
884 if let Some(population_size) = metadata.get("population_size").and_then(parse_int) {
885 config_map.insert(
886 "population_size".to_string(),
887 Value::Number(population_size.into()),
888 );
889 }
890 if let Some(num_generations) = metadata.get("num_generations").and_then(parse_int) {
891 config_map.insert(
892 "num_generations".to_string(),
893 Value::Number(num_generations.into()),
894 );
895 }
896
897 let config_value = Value::Object(config_map);
898 let validation = validate_graphgen_job_config(&config_value, &dataset);
899 errors.extend(validation.errors);
900 errors
901}