1# This file is part of Buildbot. Buildbot is free software: you can 2# redistribute it and/or modify it under the terms of the GNU General Public 3# License as published by the Free Software Foundation, version 2. 4# 5# This program is distributed in the hope that it will be useful, but WITHOUT 6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 7# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 8# details. 9# 10# You should have received a copy of the GNU General Public License along with 11# this program; if not, write to the Free Software Foundation, Inc., 51 12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 13# 14# Copyright Buildbot Team Members 15 16# See "Type Validation" in master/docs/developer/tests.rst 17import datetime 18import json 19import re 20 21from buildbot import util 22from buildbot.util import bytes2unicode 23 24 25def capitalize(word): 26 return ''.join(x.capitalize() or '_' for x in word.split('_')) 27 28 29class Type: 30 31 name = None 32 doc = None 33 graphQLType = "unknown" 34 35 @property 36 def ramlname(self): 37 return self.name 38 39 def valueFromString(self, arg): 40 # convert a urldecoded bytestring as given in a URL to a value, or 41 # raise an exception trying. This parent method raises an exception, 42 # so if the method is missing in a subclass, it cannot be created from 43 # a string. 44 raise TypeError 45 46 def cmp(self, val, arg): 47 argVal = self.valueFromString(arg) 48 if val < argVal: 49 return -1 50 elif val == argVal: 51 return 0 52 return 1 53 54 def validate(self, name, object): 55 raise NotImplementedError 56 57 def getSpec(self): 58 r = dict(name=self.name) 59 if self.doc is not None: 60 r["doc"] = self.doc 61 return r 62 63 def toGraphQL(self): 64 return self.graphQLType 65 66 def toGraphQLTypeName(self): 67 return self.graphQLType 68 69 def graphQLDependentTypes(self): 70 return [] 71 72 def getGraphQLInputType(self): 73 return self.toGraphQLTypeName() 74 75 76class NoneOk(Type): 77 78 def __init__(self, nestedType): 79 assert isinstance(nestedType, Type) 80 self.nestedType = nestedType 81 self.name = self.nestedType.name + " or None" 82 83 @property 84 def ramlname(self): 85 return self.nestedType.ramlname 86 87 def valueFromString(self, arg): 88 return self.nestedType.valueFromString(arg) 89 90 def cmp(self, val, arg): 91 return self.nestedType.cmp(val, arg) 92 93 def validate(self, name, object): 94 if object is None: 95 return 96 for msg in self.nestedType.validate(name, object): 97 yield msg 98 99 def getSpec(self): 100 r = self.nestedType.getSpec() 101 r["can_be_null"] = True 102 return r 103 104 def toRaml(self): 105 return self.nestedType.toRaml() 106 107 def toGraphQL(self): 108 # remove trailing ! 109 if isinstance(self.nestedType, Entity): 110 return self.nestedType.graphql_name 111 return self.nestedType.toGraphQL()[:-1] 112 113 def graphQLDependentTypes(self): 114 return [self.nestedType] 115 116 def getGraphQLInputType(self): 117 return self.nestedType.getGraphQLInputType() 118 119 120class Instance(Type): 121 122 types = () 123 ramlType = "unknown" 124 graphQLType = "unknown" 125 126 @property 127 def ramlname(self): 128 return self.ramlType 129 130 def validate(self, name, object): 131 if not isinstance(object, self.types): 132 yield "{} ({}) is not a {}".format(name, repr(object), self.name or repr(self.types)) 133 134 def toRaml(self): 135 return self.ramlType 136 137 def toGraphQL(self): 138 return self.graphQLType + "!" 139 140 141class Integer(Instance): 142 143 name = "integer" 144 types = (int,) 145 ramlType = "integer" 146 graphQLType = "Int" 147 148 def valueFromString(self, arg): 149 return int(arg) 150 151 152class DateTime(Instance): 153 154 name = "datetime" 155 types = (datetime.datetime,) 156 ramlType = "date" 157 graphQLType = "Date" # custom 158 159 def valueFromString(self, arg): 160 return int(arg) 161 162 def validate(self, name, object): 163 if isinstance(object, datetime.datetime): 164 return 165 if isinstance(object, int): 166 try: 167 datetime.datetime.fromtimestamp(object) 168 except (OverflowError, OSError): 169 pass 170 else: 171 return 172 yield "{} ({}) is not a valid timestamp".format(name, object) 173 174 175class String(Instance): 176 177 name = "string" 178 types = (str,) 179 ramlType = "string" 180 graphQLType = "String" 181 182 def valueFromString(self, arg): 183 val = util.bytes2unicode(arg) 184 return val 185 186 187class Binary(Instance): 188 189 name = "binary" 190 types = (bytes,) 191 ramlType = "string" 192 graphQLType = "Binary" # custom 193 194 def valueFromString(self, arg): 195 return arg 196 197 198class Boolean(Instance): 199 200 name = "boolean" 201 types = (bool,) 202 ramlType = "boolean" 203 graphQLType = "Boolean" # custom 204 205 def valueFromString(self, arg): 206 return util.string2boolean(arg) 207 208 209class Identifier(Type): 210 211 name = "identifier" 212 identRe = re.compile('^[a-zA-Z_-][a-zA-Z0-9._-]*$') 213 ramlType = "string" 214 graphQLType = "String" 215 216 def __init__(self, len=None, **kwargs): 217 super().__init__(**kwargs) 218 self.len = len 219 220 def valueFromString(self, arg): 221 val = util.bytes2unicode(arg) 222 if not self.identRe.match(val) or len(val) > self.len or not val: 223 raise TypeError 224 return val 225 226 def validate(self, name, object): 227 if not isinstance(object, str): 228 yield "{} - {} - is not a unicode string".format(name, repr(object)) 229 elif not self.identRe.match(object): 230 yield "{} - {} - is not an identifier".format(name, repr(object)) 231 elif not object: 232 yield "{} - identifiers cannot be an empty string".format(name) 233 elif len(object) > self.len: 234 yield "{} - {} - is longer than {} characters".format(name, repr(object), self.len) 235 236 def toRaml(self): 237 return {'type': self.ramlType, 238 'pattern': self.identRe.pattern} 239 240 241class List(Type): 242 243 name = "list" 244 ramlType = "list" 245 246 @property 247 def ramlname(self): 248 return self.of.ramlname 249 250 def __init__(self, of=None, **kwargs): 251 super().__init__(**kwargs) 252 self.of = of 253 254 def validate(self, name, object): 255 if not isinstance(object, list): # we want a list, and NOT a subclass 256 yield "{} ({}) is not a {}".format(name, repr(object), self.name) 257 return 258 259 for idx, elt in enumerate(object): 260 for msg in self.of.validate("{}[{}]".format(name, idx), elt): 261 yield msg 262 263 def valueFromString(self, arg): 264 # valueFromString is used to process URL args, which come one at 265 # a time, so we defer to the `of` 266 return self.of.valueFromString(arg) 267 268 def getSpec(self): 269 return dict(type=self.name, 270 of=self.of.getSpec()) 271 272 def toRaml(self): 273 return {'type': 'array', 'items': self.of.name} 274 275 def toGraphQL(self): 276 return f"[{self.of.toGraphQLTypeName()}]!" 277 278 def toGraphQLTypeName(self): 279 return f"[{self.of.toGraphQLTypeName()}]" 280 281 def graphQLDependentTypes(self): 282 return [self.of] 283 284 def getGraphQLInputType(self): 285 return self.of.getGraphQLInputType() 286 287 288def ramlMaybeNoneOrList(k, v): 289 if isinstance(v, NoneOk): 290 return k + "?" 291 if isinstance(v, List): 292 return k + "[]" 293 return k 294 295 296class SourcedProperties(Type): 297 298 name = "sourcedproperties" 299 300 def validate(self, name, object): 301 if not isinstance(object, dict): # we want a dict, and NOT a subclass 302 yield "{} is not sourced properties (not a dict)".format(name) 303 return 304 for k, v in object.items(): 305 if not isinstance(k, str): 306 yield "{} property name {} is not unicode".format(name, repr(k)) 307 if not isinstance(v, tuple) or len(v) != 2: 308 yield "{} property value for '{}' is not a 2-tuple".format(name, k) 309 return 310 propval, propsrc = v 311 if not isinstance(propsrc, str): 312 yield "{}[{}] source {} is not unicode".format(name, k, repr(propsrc)) 313 try: 314 json.loads(bytes2unicode(propval)) 315 except ValueError: 316 yield "{}[{}] value is not JSON-able".format(name, repr(k)) 317 318 def toRaml(self): 319 return {'type': "object", 320 'properties': 321 {'[]': {'type': 'object', 322 'properties': { 323 1: 'string', 324 2: 'integer | string | object | array | boolean' 325 } 326 }}} 327 328 def toGraphQL(self): 329 return "[Property]!" 330 331 def graphQLDependentTypes(self): 332 return [PropertyEntityType("property", 'Property')] 333 334 def getGraphQLInputType(self): 335 return None 336 337 338class JsonObject(Type): 339 name = "jsonobject" 340 ramlname = 'object' 341 graphQLType = "JSON" 342 343 def validate(self, name, object): 344 if not isinstance(object, dict): 345 yield "{} ({}) is not a dictionary (got type {})".format(name, repr(object), 346 type(object)) 347 return 348 349 # make sure JSON can represent it 350 try: 351 json.dumps(object) 352 except Exception as e: 353 yield "{} is not JSON-able: {}".format(name, e) 354 return 355 356 def toRaml(self): 357 return "object" 358 359 360class Entity(Type): 361 362 # NOTE: this type is defined by subclassing it in each resource type class. 363 # Instances are generally accessed at e.g., 364 # * buildsets.Buildset.entityType or 365 # * self.master.data.rtypes.buildsets.entityType 366 367 name = None # set in constructor 368 graphql_name = None # set in constructor 369 fields = {} 370 fieldNames = set([]) 371 372 def __init__(self, name, graphql_name): 373 fields = {} 374 for k, v in self.__class__.__dict__.items(): 375 if isinstance(v, Type): 376 fields[k] = v 377 self.fields = fields 378 self.fieldNames = set(fields) 379 self.name = name 380 self.graphql_name = graphql_name 381 382 def validate(self, name, object): 383 # this uses isinstance, allowing dict subclasses as used by the DB API 384 if not isinstance(object, dict): 385 yield "{} ({}) is not a dictionary (got type {})".format(name, repr(object), 386 type(object)) 387 return 388 389 gotNames = set(object.keys()) 390 391 unexpected = gotNames - self.fieldNames 392 if unexpected: 393 yield "{} has unexpected keys {}".format(name, ", ".join([repr(n) for n in unexpected])) 394 395 missing = self.fieldNames - gotNames 396 if missing: 397 yield "{} is missing keys {}".format(name, ", ".join([repr(n) for n in missing])) 398 399 for k in gotNames & self.fieldNames: 400 f = self.fields[k] 401 for msg in f.validate("{}[{}]".format(name, repr(k)), object[k]): 402 yield msg 403 404 def getSpec(self): 405 return dict(type=self.name, 406 fields=[dict(name=k, 407 type=v.name, 408 type_spec=v.getSpec()) 409 for k, v in self.fields.items() 410 ]) 411 412 def toRaml(self): 413 return {'type': "object", 414 'properties': { 415 ramlMaybeNoneOrList(k, v): {'type': v.ramlname, 'description': ''} 416 for k, v in self.fields.items()}} 417 418 def toGraphQL(self): 419 return dict(type=self.graphql_name, 420 fields=[dict(name=k, 421 type=v.toGraphQL()) 422 for k, v in self.fields.items() 423 # in graphql, we handle properties as queriable sub resources 424 # instead of hardcoded attributes like in rest api 425 if k != "properties" 426 ]) 427 428 def toGraphQLTypeName(self): 429 return self.graphql_name 430 431 def graphQLDependentTypes(self): 432 return self.fields.values() 433 434 def getGraphQLInputType(self): 435 # for now, complex types are not query able 436 # in the future, we may want to declare (and implement) graphql input types 437 return None 438 439 440class PropertyEntityType(Entity): 441 name = String() 442 source = String() 443 value = JsonObject() 444