validator.py (19176B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\fsd\schemas\validator.py 2 from __future__ import absolute_import 3 import re 4 import collections 5 import os 6 import yaml 7 import decimal 8 CLIENT = 'Client' 9 SERVER = 'Server' 10 EDITOR = 'Editor' 11 ASCENDING = 'Ascending' 12 DESCENDING = 'Descending' 13 from . import persistence 14 15 class SchemaMismatch(Exception): 16 17 def __init__(self, path, message): 18 self.filePath = None 19 self.path = path 20 self.message = message 21 22 def SetFilePath(self, filePath): 23 self.filePath = filePath 24 25 def __str__(self): 26 if self.filePath is None: 27 return self.message 28 else: 29 return self.filePath + ':\n\t' + self.message 30 31 32 class SchemaTypeError(SchemaMismatch): 33 pass 34 35 36 class SchemaComparisonError(SchemaMismatch): 37 pass 38 39 40 class SchemaObjectAttributeMissingError(SchemaMismatch): 41 pass 42 43 44 class SchemaObjectAttributeNotInSchemaError(SchemaMismatch): 45 pass 46 47 48 class ExternalReferenceError(SchemaMismatch): 49 pass 50 51 52 class ValidationException(SchemaMismatch): 53 54 def __init__(self, path, errors): 55 self.filePath = None 56 self.path = path 57 self.message = '\n\t'.join([ str(error) for error in errors ]) 58 59 60 class SchemaError(Exception): 61 62 def __init__(self, path, message, node, data): 63 self.path = path 64 self.message = message 65 self.node = node 66 self.data = data 67 68 def __str__(self): 69 return self.message 70 71 72 def ListContainsType(errorList, errorType): 73 match = [ e for e in errorList if isinstance(e, errorType) ] 74 return len(match) > 0 75 76 77 def AllFloatValues(i): 78 return all(map(lambda x: type(x) in (float, 79 int, 80 long, 81 decimal.Decimal), i)) 82 83 84 def ValidateInt(schemaNode, o, path = 'root', state = {}): 85 if type(o) not in (int, long): 86 return [SchemaTypeError(path, '%s: Type Mismatch - should be an integer: %s' % (path, str(o)))] 87 errors = [] 88 if 'min' in schemaNode: 89 if o < schemaNode['min']: 90 errors.append(SchemaComparisonError(path, '%s: Range mismatch - value %i is less than the minimum value of %i' % (path, o, schemaNode['min']))) 91 elif 'exclusiveMin' in schemaNode: 92 if o <= schemaNode['exclusiveMin']: 93 errors.append(SchemaComparisonError(path, '%s: Range mismatch - value %i is less than or equal to the minimum value of %i' % (path, o, schemaNode['exclusiveMin']))) 94 if 'max' in schemaNode: 95 if o > schemaNode['max']: 96 errors.append(SchemaComparisonError(path, '%s: Range mismatch - value %i is less than the maximum value of %i' % (path, o, schemaNode['max']))) 97 elif 'exclusiveMax' in schemaNode: 98 if o >= schemaNode['exclusiveMax']: 99 errors.append(SchemaComparisonError(path, '%s: Range mismatch - value %i is less than or equal to the maximum value of %i' % (path, o, schemaNode['exclusiveMax']))) 100 return errors 101 102 103 def ValidateGenericId(schemaNode, o, path = 'root', state = {}): 104 errors = ValidateInt(schemaNode, o, path, state) 105 if not ListContainsType(errors, SchemaTypeError) and o < 0: 106 errors.append(SchemaComparisonError(path, '%s: Range Mismatch - Integer Identifier should be > 0, this is %i' % (path, o))) 107 return errors 108 109 110 def ValidateTypeID(schemaNode, o, path = 'root', state = {}): 111 errors = ValidateInt(schemaNode, o, path, state) 112 if not ListContainsType(errors, SchemaTypeError) and o < 0: 113 errors.append(SchemaComparisonError(path, '%s: Range Mismatch - typeIDs should be > 0, this is %i' % (path, o))) 114 return errors 115 116 117 def ValidateDogmaAttributeID(schemaNode, o, path = 'root', state = {}): 118 errors = ValidateInt(schemaNode, o, path, state) 119 if not ListContainsType(errors, SchemaTypeError) and o < 0: 120 errors.append(SchemaComparisonError(path, '%s: Range Mismatch - dogmaAttributeIDs should be > 0, this is %i' % (path, o))) 121 return errors 122 123 124 def ValidateLocalizationID(schemaNode, o, path = 'root', state = {}): 125 errors = ValidateInt(schemaNode, o, path, state) 126 if not ListContainsType(errors, SchemaTypeError) and o < 0: 127 errors.append(SchemaComparisonError(path, '%s: Range Mismatch - localizationIDs should be > 0, this is %i' % (path, o))) 128 return errors 129 130 131 def ValidateFloat(schemaNode, o, path = 'root', state = {}): 132 errors = [] 133 if type(o) not in (float, 134 int, 135 long, 136 decimal.Decimal): 137 return [SchemaTypeError(path, '%s: Type Mismatch - should be a float' % path)] 138 if 'min' in schemaNode: 139 if o < schemaNode['min']: 140 errors.append(SchemaComparisonError(path, '%s: Range mismatch - value %.1g is less than the minimum value of %.1g' % (path, o, schemaNode['min']))) 141 elif 'exclusiveMin' in schemaNode: 142 if o <= schemaNode['exclusiveMin']: 143 errors.append(SchemaComparisonError(path, '%s: Range mismatch - value %.1g is less than or equal to the minimum value of %.1g' % (path, o, schemaNode['exclusiveMin']))) 144 if 'max' in schemaNode: 145 if o > schemaNode['max']: 146 errors.append(SchemaComparisonError(path, '%s: Range mismatch - value %.1g is less than the maximum value of %.1g' % (path, o, schemaNode['max']))) 147 elif 'exclusiveMax' in schemaNode: 148 if o >= schemaNode['exclusiveMax']: 149 errors.append(SchemaComparisonError(path, '%s: Range mismatch - value %.1g is less than or equal to the maximum value of %.1g' % (path, o, schemaNode['exclusiveMax']))) 150 return errors 151 152 153 def ValidateBool(schemaNode, o, path = 'root', state = {}): 154 errors = [] 155 if type(o) is not bool: 156 errors.append(SchemaTypeError(path, '%s: Type Mismatch - should be a bool' % path)) 157 return errors 158 159 160 def ValidateCommonStringAttributes(schemaNode, o, path): 161 typeName = 'string' 162 if isinstance(o, unicode): 163 typeName = 'unicode' 164 errors = [] 165 if 'length' in schemaNode: 166 if len(o) != schemaNode['length']: 167 errors.append(SchemaComparisonError(path, "%s: Length mismatch - %s '%s' should be %i characters long" % (path, 168 typeName, 169 o.encode('utf-8'), 170 schemaNode['length']))) 171 if 'minLength' in schemaNode: 172 if len(o) < schemaNode['minLength']: 173 errors.append(SchemaComparisonError(path, "%s: Length mismatch - %s '%s' should be at least %i characters long" % (path, 174 typeName, 175 o.encode('utf-8'), 176 schemaNode['minLength']))) 177 if 'maxLength' in schemaNode: 178 if len(o) > schemaNode['maxLength']: 179 errors.append(SchemaComparisonError(path, "%s: Length mismatch - %s '%s' should be at most %i characters long" % (path, 180 typeName, 181 o.encode('utf-8'), 182 schemaNode['maxLength']))) 183 if 'regex' in schemaNode: 184 if isinstance(o, unicode): 185 regex = re.compile(schemaNode['regex'], re.UNICODE) 186 else: 187 regex = re.compile(schemaNode['regex']) 188 if not regex.match(o): 189 errors.append(SchemaComparisonError(path, "%s: Regex mismatch - %s '%s' does not match the regex '%s'" % (path, 190 typeName, 191 o.encode('utf-8'), 192 schemaNode['regex'].encode('utf-8')))) 193 return errors 194 195 196 def ValidateString(schemaNode, o, path = 'root', state = {}): 197 if type(o) is not str: 198 return [SchemaTypeError(path, '%s: Type Mismatch - should be a string' % path)] 199 return ValidateCommonStringAttributes(schemaNode, o, path) 200 201 202 def ValidateUnicode(schemaNode, o, path = 'root', state = {}): 203 if type(o) not in (unicode, str): 204 return [SchemaTypeError(path, '%s: Type Mismatch - should be unicode' % path)] 205 return ValidateCommonStringAttributes(schemaNode, o, path) 206 207 208 def ValidateResPath(schemaNode, o, path = 'root', state = {}): 209 errors = ValidateString(schemaNode, o, path, state) 210 if not ListContainsType(errors, SchemaTypeError): 211 if not o.startswith('res:/'): 212 errors.append(SchemaComparisonError(path, "%s: Type Mismatch - resPaths should start with 'res:/' - %s" % (path, o))) 213 if 'extensions' in schemaNode: 214 fileName, fileExtension = os.path.splitext(o) 215 fileExtension = fileExtension[1:] 216 if fileExtension not in schemaNode['extensions']: 217 supportedExtension = ', '.join([ "'%s'" % extension for extension in schemaNode['extensions'] ]) 218 errors.append(SchemaComparisonError(path, '%s: Type Mismatch - this resPath supports the following extensions: %s - %s' % (path, supportedExtension, o))) 219 if ' ' in o: 220 errors.append(SchemaComparisonError(path, '%s: Type Mismatch - resPaths should not contain spaces - %s' % (path, o))) 221 if '\\' in o: 222 errors.append(SchemaComparisonError(path, '%s: Type Mismatch - resPaths should not contain backslashes - %s' % (path, o))) 223 return errors 224 225 226 def ValidateVector2(schemaNode, o, path = 'root', state = {}): 227 if type(o) not in (tuple, list): 228 return [SchemaTypeError(path, '%s: Type Mismatch - should be a vector2' % path)] 229 errors = [] 230 if len(o) != 2: 231 errors.append(SchemaComparisonError(path, '%s: Length Mismatch - should be a vector2' % path)) 232 if not AllFloatValues(o): 233 errors.append(SchemaComparisonError(path, '%s: Type Mismatch - should be a vector2 (contents are not all numeric!)' % path)) 234 return errors 235 236 237 def ValidateVector3(schemaNode, o, path = 'root', state = {}): 238 if type(o) not in (tuple, list): 239 return [SchemaTypeError(path, '%(path)s: Type Mismatch - should be a vector3: (%(repr)s)' % {'path': path, 240 'repr': repr(o)})] 241 errors = [] 242 if len(o) != 3: 243 errors.append(SchemaComparisonError(path, '%s: Length Mismatch - should be a vector3' % path)) 244 if not AllFloatValues(o): 245 errors.append(SchemaComparisonError(path, '%s: Type Mismatch - should be a vector3 (contents are not all numeric!)' % path)) 246 return errors 247 248 249 def ValidateVector4(schemaNode, o, path = 'root', state = {}): 250 if type(o) not in (tuple, list): 251 return [SchemaTypeError(path, '%s: Type Mismatch - should be a vector4' % path)] 252 errors = [] 253 if len(o) != 4: 254 errors.append(SchemaComparisonError(path, '%s: Length Mismatch - should be a vector4' % path)) 255 if not AllFloatValues(o): 256 errors.append(SchemaComparisonError(path, '%s: Type Mismatch - should be a vector4 (contents are not all numeric!)' % path)) 257 return errors 258 259 260 def ValidateObject(schemaNode, o, path = 'root', state = {}): 261 errors = [] 262 if type(o) is not dict: 263 return [SchemaTypeError(path, '%s: Type Mismatch - should be an object' % path)] 264 for attrName, attrValue in schemaNode.get('attributes').iteritems(): 265 if _IsAttributeValueMissing(o, attrName, attrValue): 266 errors.append(SchemaObjectAttributeMissingError(path, "%s: Attribute Mismatch - required attribute '%s' is missing!" % (path, attrName))) 267 if attrName in o: 268 errors.extend(Validate(attrValue, o[attrName], path + '.%s' % attrName, state)) 269 270 for attrName in o: 271 if attrName not in schemaNode.get('attributes'): 272 errors.append(SchemaObjectAttributeNotInSchemaError(path, "%s: Attribute Mismatch - attribute '%s' does not exist in the schema!" % (path, attrName))) 273 274 return errors 275 276 277 def _IsAttributeValueMissing(obj, attrName, attrValue): 278 if attrName in obj: 279 return False 280 if attrValue.get('isOptional', False): 281 return False 282 if attrValue.get('generatedData', False): 283 return False 284 return True 285 286 287 def ValidateList(schemaNode, o, path = 'root', state = {}): 288 try: 289 it = enumerate(o) 290 except TypeError: 291 return [SchemaTypeError(path, '%s: Type Mismatch - should be itterable' % path)] 292 293 sortKey = schemaNode.get('sortKey', None) 294 sortOrder = schemaNode.get('sortOrder', ASCENDING) 295 originalLast = object() 296 last = originalLast 297 errors = [] 298 listOutOfOrder = False 299 for index, i in it: 300 if last is not originalLast: 301 compare1 = last 302 compare2 = i 303 if sortKey is not None: 304 if sortKey not in last: 305 return [SchemaObjectAttributeMissingError(path + '[%i]' % (index - 1), "%s: Sort Attribute Missing - required attribute '%s' is missing!" % (path + '[%i]' % (index - 1), sortKey))] 306 if sortKey not in i: 307 return [SchemaObjectAttributeMissingError(path + '[%i]' % index, "%s: Sort Attribute Missing - required attribute '%s' is missing!" % (path + '[%i]' % index, sortKey))] 308 compare1 = last[sortKey] 309 compare2 = i[sortKey] 310 if sortOrder not in (ASCENDING, DESCENDING): 311 errors.append(SchemaError(path, "sortOrder attribute in schema must be either '%s' or '%s', not %s" % (ASCENDING, DESCENDING, sortOrder), schemaNode, o)) 312 if sortOrder == ASCENDING and not listOutOfOrder: 313 if compare1 >= compare2: 314 errors.append(SchemaComparisonError(path + '[%i]' % index, '%s: Order Error! Sort key: %s. List should be sorted ASCENDING. %s >= %s!' % (path + '[%i]' % index, 315 sortKey, 316 str(compare1), 317 str(compare2)))) 318 listOutOfOrder = True 319 elif sortOrder == DESCENDING and not listOutOfOrder: 320 if compare1 <= compare2: 321 errors.append(SchemaComparisonError(path + '[%i]' % index, '%s: Order Error! Sort key: %s. List should be sorted DESCENDING. %s <= %s!' % (path + '[%i]' % index, 322 sortKey, 323 str(compare1), 324 str(compare2)))) 325 listOutOfOrder = True 326 errors.extend(Validate(schemaNode.get('itemTypes'), i, path + '[%i]' % index, state)) 327 last = i 328 329 return errors 330 331 332 def ValidateDict(schemaNode, o, path = 'root', state = {}): 333 if type(o) not in (dict, collections.OrderedDict, collections.defaultdict): 334 return [SchemaTypeError(path, '%s: Type Mismatch - should be itterable' % path)] 335 errors = [] 336 for dictKey, dictValue in o.iteritems(): 337 errors.extend(Validate(schemaNode['keyTypes'], dictKey, path + '<%s>' % str(dictKey), state)) 338 errors.extend(Validate(schemaNode['valueTypes'], dictValue, path + '[%s]' % str(dictKey), state)) 339 340 return errors 341 342 343 def ValidateEnum(schemaNode, o, path = 'root', state = {}): 344 if o not in schemaNode.get('values', {}): 345 return [SchemaTypeError(path, '%s: Enum value not found in schema: %s' % (path, repr(o)))] 346 return [] 347 348 349 def ValidateUnion(schemaNode, o, path = 'root', state = {}): 350 for s in schemaNode['optionTypes']: 351 validationErrors = Validate(s, o, path, state) 352 if len(validationErrors) == 0: 353 break 354 else: 355 return [SchemaTypeError(path, '%s: Did not match any of the possible schema types: %s' % (path, repr(o)))] 356 357 return [] 358 359 360 builtInValdationFunctions = {'int': ValidateInt, 361 'typeID': ValidateTypeID, 362 'groupID': ValidateInt, 363 'categoryID': ValidateInt, 364 'localizationID': ValidateLocalizationID, 365 'float': ValidateFloat, 366 'vector2': ValidateVector2, 367 'vector3': ValidateVector3, 368 'vector4': ValidateVector4, 369 'dict': ValidateDict, 370 'list': ValidateList, 371 'object': ValidateObject, 372 'enum': ValidateEnum, 373 'string': ValidateString, 374 'unicode': ValidateUnicode, 375 'resPath': ValidateResPath, 376 'bool': ValidateBool, 377 'union': ValidateUnion, 378 'color': ValidateVector4, 379 'dogmaAttributeID': ValidateDogmaAttributeID, 380 'npcTag': ValidateGenericId, 381 'deploymentType': ValidateGenericId, 382 'npcEnemyFleetTypeID': ValidateGenericId, 383 'groupBehaviorTreeID': ValidateGenericId, 384 'npcCorporationID': ValidateGenericId, 385 'spawnTableID': ValidateGenericId, 386 'npcFleetCounterTableID': ValidateGenericId, 387 'dungeonID': ValidateGenericId, 388 'typeListID': ValidateGenericId, 389 'npcFleetTypeID': ValidateGenericId, 390 'metaGroupID': ValidateGenericId, 391 'fsdReference': ValidateGenericId, 392 'raceID': ValidateGenericId, 393 'marketGroupID': ValidateGenericId, 394 'ShipGroupID': ValidateGenericId, 395 'certificateTemplateID': ValidateGenericId, 396 'factionID': ValidateGenericId} 397 398 def ValidateReference(schemaNode, o, path, state): 399 referenceInfo = schemaNode.get('reference') 400 if not state.get('validateReferences', False): 401 return [] 402 if 'schema' in referenceInfo and 'branchRoot' in state: 403 if 'openSchemas' not in state: 404 state['openSchemas'] = {} 405 state['openData'] = {} 406 otherSchemaPath = os.path.abspath(os.path.join(state['branchRoot'], referenceInfo['schema'])) 407 if 'schemaCache' in state and otherSchemaPath in state['schemaCache']: 408 otherSchema = state['schemaCache'].Get(otherSchemaPath) 409 elif otherSchemaPath not in state['openSchemas']: 410 with open(otherSchemaPath, 'r') as otherSchemaFile: 411 otherSchema = state['openSchemas'][otherSchemaPath] = persistence.LoadSchema(otherSchemaFile) 412 else: 413 otherSchema = state['openSchemas'][otherSchemaPath] 414 editorFile = otherSchema['editorFile'] % {'key': o} 415 fullEditorFilePath = os.path.abspath(os.path.join(state['branchRoot'], editorFile)) 416 if 'dataCache' in state and fullEditorFilePath in state['dataCache']: 417 otherData = state['dataCache'].Get(fullEditorFilePath) 418 elif fullEditorFilePath not in state['openData']: 419 with open(fullEditorFilePath, 'r') as otherDataFile: 420 otherData = state['openData'][fullEditorFilePath] = yaml.load(otherDataFile) 421 else: 422 otherData = state['openData'][fullEditorFilePath] 423 if o not in otherData: 424 errorString = '%(path)s: ' + referenceInfo['errorMessage'] 425 return [ExternalReferenceError(path, errorString % {'path': path, 426 'key': str(o), 427 'dataFile': fullEditorFilePath})] 428 return [] 429 430 431 def Validate(schemaNode, o, path = 'root', state = {}): 432 nodeType = schemaNode.get('type', None) 433 if nodeType is None: 434 return [SchemaError(path, "%s: Could not find a 'type' for the schema node" % path, schemaNode, o)] 435 errors = [] 436 if schemaNode.get('generatedData', False): 437 return errors 438 if 'reference' in schemaNode: 439 errors.extend(ValidateReference(schemaNode, o, path, state)) 440 if nodeType in state.get('overrides', {}): 441 errors.extend(state.get('overrides', {})[nodeType](schemaNode, o, path, state)) 442 elif nodeType in builtInValdationFunctions: 443 errors.extend(builtInValdationFunctions[nodeType](schemaNode, o, path, state)) 444 else: 445 return [SchemaError(path, "%s: Could not find a known 'type' for the schema node: %s" % (path, nodeType), schemaNode, o)] 446 return errors 447 448 449 def ValidateWithExceptions(schemaNode, o, path = 'root', state = {}, showOnlyFirstError = True): 450 errors = Validate(schemaNode, o, path, state) 451 if len(errors): 452 if showOnlyFirstError: 453 raise ValidationException(path, [errors[0]]) 454 else: 455 raise ValidationException(path, errors)