51 lines
5.6 KiB
PHP
51 lines
5.6 KiB
PHP
<?php
|
|
require_once __DIR__ . '/labdb.class.php';
|
|
class ParameterCatalog {
|
|
public static function schema(): ?array {
|
|
$path=lab_config()['catalog'];
|
|
if (!is_file($path)||!is_readable($path)) return null;
|
|
$schema=json_decode((string)file_get_contents($path),true);
|
|
return is_array($schema)&&isset($schema['fields'],$schema['model'],$schema['run'],$schema['model_defaults'],$schema['run_defaults'])?$schema:null;
|
|
}
|
|
private static function value($value,array $rule): array {
|
|
if($value===null) return !empty($rule['nullable'])?[$value,null]:[$value,'A value is required.'];
|
|
$types=$rule['types']??[$rule['type']??''];
|
|
$type=is_bool($value)?'boolean':(is_int($value)?'integer':(is_float($value)?'number':(is_string($value)?'string':(is_array($value)?(array_is_list($value)?'array':'object'):'invalid'))));
|
|
if($type==='integer'&&in_array('number',$types,true))$type='number';
|
|
if(!in_array($type,$types,true))return[$value,'Expected '.implode(' or ',$types).'.'];
|
|
if(is_int($value)||is_float($value)){if(!is_finite((float)$value))return[$value,'Use a finite number.'];if((isset($rule['min'])&&$value<$rule['min'])||(isset($rule['max'])&&$value>$rule['max']))return[$value,'The value is outside the allowed range.'];}
|
|
if(isset($rule['choices'])&&!in_array($value,$rule['choices'],true))return[$value,'Select an allowed value.'];
|
|
if(is_string($value)&&(mb_strlen($value)>200||(isset($rule['pattern'])&&!preg_match('~^(?:'.$rule['pattern'].')$~D',$value))))return[$value,'The value has an invalid format.'];
|
|
if(is_array($value)&&!array_is_list($value)){
|
|
$properties=$rule['properties']??[];$unknown=array_diff(array_keys($value),array_keys($properties));if($unknown){sort($unknown);return[$value,'Unknown keys: '.implode(', ',$unknown).'.'];}
|
|
$result=is_array($rule['default']??null)?$rule['default']:[];
|
|
foreach($value as $key=>$item){if(!isset($properties[$key]))continue;[$normalized,$error]=self::value($item,$properties[$key]);if($error)return[$value,$key.': '.$error];$result[$key]=$normalized;}
|
|
$missing=array_diff($rule['required']??[],array_keys($result));if($missing){sort($missing);return[$value,'Required keys: '.implode(', ',$missing).'.'];}$value=$result;
|
|
}
|
|
return[$value,null];
|
|
}
|
|
public static function validate($spec): array {
|
|
$schema=self::schema();if(!$schema)return['ok'=>false,'spec'=>$spec,'errors'=>['schema'=>'Parameter catalog is unavailable.']];
|
|
if(!is_array($spec)|| (array_is_list($spec)&&$spec!==[]))return['ok'=>false,'spec'=>$spec,'errors'=>['spec'=>'Expected an object.']];
|
|
$errors=[];$normalized=[];
|
|
foreach(array_diff(array_keys($spec),['model','run','parameters','surface','maps']) as $key)$errors[$key]='Unknown spec section.';
|
|
foreach(['model','run'] as $section){$values=array_key_exists($section,$spec)?$spec[$section]:[];if(!is_array($values)||(array_is_list($values)&&$values!==[])){$errors[$section]='Expected an object.';continue;}
|
|
$normalized[$section]=$section==='model'?($schema['model_defaults']):$schema['run_defaults'];
|
|
foreach($values as $key=>$value){if(!isset($schema[$section][$key])){$errors[$section.'.'.$key]='Unknown field.';continue;}[$result,$error]=self::value($value,$schema[$section][$key]);if($error)$errors[$section.'.'.$key]=$error;else $normalized[$section][$key]=$result;}
|
|
}
|
|
$parameters=array_key_exists('parameters',$spec)?$spec['parameters']:[];if($parameters===[])$parameters=[];$normalized['parameters']=[];
|
|
if(!is_array($parameters)||(array_is_list($parameters)&&$parameters!==[]))$errors['parameters']='Expected an object.';
|
|
else {$fields=[];foreach($schema['fields'] as $field)$fields[$field['name']]=$field;
|
|
foreach($parameters as $name=>$value){$field=$fields[$name]??null;if(!$field){$errors['parameters.'.$name]='Unknown parameter.';continue;}
|
|
if(empty($field['editable'])){if($value!==($field['default']??null))$errors['parameters.'.$name]='This input is managed by the application.';continue;}
|
|
$rule=$field;if($name==='ozone')$rule['types']=['boolean','object'];
|
|
if($name==='vegetation'&&($value===true||$value===false)){$normalized['parameters'][$name]=$value;continue;}
|
|
[$result,$error]=self::value($value,$rule);if($error)$errors['parameters.'.$name]=$error;else $normalized['parameters'][$name]=$result;
|
|
}
|
|
}
|
|
if(array_key_exists('maps',$spec)){$maps=$spec['maps'];$rules=$schema['maps']['fields']??[];if(!is_array($maps)||(array_is_list($maps)&&$maps!==[]))$errors['maps']='Expected a map version object.';else{$normalized['maps']=[];foreach($maps as $field=>$id){if(!isset($rules[$field])){$errors['maps.'.$field]='Unsupported map field.';continue;}if(!is_string($id)||!preg_match('/^[a-f0-9]{32}$/',$id))$errors['maps.'.$field]='Expected a saved map version ID.';else $normalized['maps'][$field]=$id;}}}
|
|
if(array_key_exists('surface',$spec)){$surface=$spec['surface'];$rules=$schema['surface']??[];if(!is_array($surface)||(array_is_list($surface)&&$surface!==[]))$errors['surface']='Expected an object.';else{$normalized['surface']=[];foreach(array_diff(array_keys($surface),array_keys($rules)) as $key)$errors['surface.'.$key]='Unknown field.';foreach($rules as $key=>$rule){$item=array_key_exists($key,$surface)?$surface[$key]:($rule['default']??null);[$result,$error]=self::value($item,$rule);if($error)$errors['surface.'.$key]=$error;else $normalized['surface'][$key]=$result;}}}
|
|
return['ok'=>!$errors,'spec'=>$normalized,'errors'=>$errors];
|
|
}
|
|
}
|