|
| 1 | +import util |
| 2 | +import validators |
| 3 | + |
| 4 | + |
| 5 | +class ProtocolBase(object): |
| 6 | + __propinfo__ = {} |
| 7 | + __required__ = set() |
| 8 | + |
| 9 | + __SCHEMA_TYPES__ = { |
| 10 | + 'array': list, |
| 11 | + 'boolean': bool, |
| 12 | + 'integer': int, |
| 13 | + 'number': float, |
| 14 | + 'null': None, |
| 15 | + 'string': unicode, |
| 16 | + 'object': dict |
| 17 | + } |
| 18 | + |
| 19 | + def __str__(self): |
| 20 | + return repr(self) |
| 21 | + |
| 22 | + def __repr__(self): |
| 23 | + props = ["%s=%s" % (k, str(v)) for k, v in |
| 24 | + self._properties.iteritems()] |
| 25 | + return "<%s %s>" % ( |
| 26 | + self.__class__.__name__, |
| 27 | + " ".join(props) |
| 28 | + ) |
| 29 | + |
| 30 | + def __init__(this, **props): |
| 31 | + for prop in props: |
| 32 | + try: |
| 33 | + propname = this.__prop_names__[prop] |
| 34 | + except KeyError: |
| 35 | + raise AttributeError( |
| 36 | + "{0} is not valid property " |
| 37 | + "of '{1}'".format(prop, |
| 38 | + this.__class__)) |
| 39 | + |
| 40 | + setattr(this, propname, props[prop]) |
| 41 | + |
| 42 | + @classmethod |
| 43 | + def propinfo(cls, propname): |
| 44 | + if propname not in cls.__propinfo__: |
| 45 | + return {} |
| 46 | + return cls.__propinfo__[propname] |
| 47 | + |
| 48 | + def serialize(self): |
| 49 | + enc = util.ProtocolJSONEncoder() |
| 50 | + return enc.encode(self) |
| 51 | + |
| 52 | + def validate(this): |
| 53 | + missing = [x for x in this.__required__ |
| 54 | + if this._properties[x] is None] |
| 55 | + |
| 56 | + if len(missing) > 0: |
| 57 | + raise validators.ValidationError( |
| 58 | + "'{0}' are required attributes for {1}" |
| 59 | + .format(missing, this.__class__)) |
| 60 | + |
| 61 | + for prop, val in this._properties.iteritems(): |
| 62 | + info = this.propinfo(prop) |
| 63 | + |
| 64 | + for param, paramval in info.iteritems(): |
| 65 | + validator = getattr(validators, param, None) |
| 66 | + if validator is not None: |
| 67 | + validator(paramval, val) |
| 68 | + |
| 69 | + return True |
| 70 | + |
| 71 | + def validate_property(self, propinfo, propval): |
| 72 | + """Validate a property value, and return true or false |
| 73 | +
|
| 74 | + :propinfo: A dictionary containing property info |
| 75 | + :propval: The property value |
| 76 | + :returns: True or False |
| 77 | + """ |
| 78 | + |
| 79 | + |
| 80 | +class ClassBuilder(object): |
| 81 | + |
| 82 | + def __init__(self, resolver): |
| 83 | + self.resolver = resolver |
| 84 | + self.resolved = {} |
| 85 | + |
| 86 | + def resolve_classes(self, iterable): |
| 87 | + pp = [] |
| 88 | + for elem in iterable: |
| 89 | + if '$ref' in elem: |
| 90 | + ref = elem['$ref'] |
| 91 | + uri = util.resolve_ref_uri(self.resolver.resolution_scope, ref) |
| 92 | + if uri in self.resolved: |
| 93 | + pp.append(self.resolved[uri]) |
| 94 | + else: |
| 95 | + with self.resolver.resolving(ref) as resolved: |
| 96 | + self.resolved[uri] = self.construct( |
| 97 | + uri, |
| 98 | + resolved, |
| 99 | + (ProtocolBase,)) |
| 100 | + pp.append(self.resolved[uri]) |
| 101 | + else: |
| 102 | + pp.append(elem) |
| 103 | + |
| 104 | + return pp |
| 105 | + |
| 106 | + def construct(self, uri, clsdata, parent=(ProtocolBase,)): |
| 107 | + if 'oneOf' in clsdata: |
| 108 | + potential_parents = self.resolve_classes(clsdata['oneOf']) |
| 109 | + |
| 110 | + for p in potential_parents: |
| 111 | + if issubclass(p, ProtocolBase): |
| 112 | + self.resolved[uri] = self._build_object( |
| 113 | + uri, |
| 114 | + clsdata, |
| 115 | + (p,)) |
| 116 | + else: |
| 117 | + raise Exception("Don't know how to deal with this") |
| 118 | + |
| 119 | + elif 'anyOf' in clsdata: |
| 120 | + raise NotImplementedError( |
| 121 | + "anyOf is not supported as bare property") |
| 122 | + |
| 123 | + elif 'allOf' in clsdata: |
| 124 | + potential_parents = self.resolve_classes(clsdata['allOf']) |
| 125 | + parents = [] |
| 126 | + for p in potential_parents: |
| 127 | + if isinstance(p, dict): |
| 128 | + # This is additional constraints |
| 129 | + clsdata.update(p) |
| 130 | + elif issubclass(p, ProtocolBase): |
| 131 | + parents.append(p) |
| 132 | + |
| 133 | + self.resolved[uri] = self._build_object( |
| 134 | + uri, |
| 135 | + clsdata, |
| 136 | + parents) |
| 137 | + return self.resolved[uri] |
| 138 | + |
| 139 | + elif '$ref' in clsdata: |
| 140 | + |
| 141 | + if uri in self.resolved: |
| 142 | + return self.resolved[uri] |
| 143 | + else: |
| 144 | + reffed_doc = self.resolver.resolve_remote(uri) |
| 145 | + self.resolved[uri] = self._build_object( |
| 146 | + uri, |
| 147 | + reffed_doc, |
| 148 | + parent) |
| 149 | + return self.resolved[uri] |
| 150 | + |
| 151 | + elif (clsdata.get('type', None) == 'object' or |
| 152 | + clsdata.get('properties', None) is not None): |
| 153 | + self.resolved[uri] = self._build_object( |
| 154 | + uri, |
| 155 | + clsdata, |
| 156 | + parent) |
| 157 | + return self.resolved[uri] |
| 158 | + else: |
| 159 | + raise NotImplementedError( |
| 160 | + "Unable to parse schema object with " |
| 161 | + "no type and no reference") |
| 162 | + |
| 163 | + def _build_object(self, nm, clsdata, parents): |
| 164 | + |
| 165 | + props = {} |
| 166 | + |
| 167 | + properties = {} |
| 168 | + for p in parents: |
| 169 | + properties = util.propmerge(properties, p.__propinfo__) |
| 170 | + |
| 171 | + if 'properties' in clsdata: |
| 172 | + properties = util.propmerge(properties, clsdata['properties']) |
| 173 | + |
| 174 | + name_translation = {} |
| 175 | + |
| 176 | + for prop, detail in properties.items(): |
| 177 | + properties[prop]['raw_name'] = prop |
| 178 | + name_translation[prop] = prop.replace('@', '') |
| 179 | + prop = name_translation[prop] |
| 180 | + |
| 181 | + if 'type' not in detail and '$ref' in detail: |
| 182 | + ref = detail['$ref'] |
| 183 | + uri = util.resolve_ref_uri(self.resolver.resolution_scope, ref) |
| 184 | + if uri in self.resolved: |
| 185 | + props[prop] = make_property(prop, |
| 186 | + {'type': self.resolved[uri]}, |
| 187 | + self.resolved[uri].__doc__) |
| 188 | + properties[prop]['$ref'] = uri |
| 189 | + properties[prop]['type'] = self.resolved[uri] |
| 190 | + else: |
| 191 | + with self.resolver.resolving(ref) as resolved: |
| 192 | + self.resolved[uri] = self.construct( |
| 193 | + uri, |
| 194 | + resolved, |
| 195 | + (ProtocolBase,)) |
| 196 | + elif 'oneOf' in detail: |
| 197 | + potential = self.resolve_classes(detail['oneOf']) |
| 198 | + desc = detail[ |
| 199 | + 'description'] if 'description' in detail else "" |
| 200 | + props[prop] = make_property(prop, |
| 201 | + {'type': potential}, desc |
| 202 | + ) |
| 203 | + |
| 204 | + else: |
| 205 | + desc = detail[ |
| 206 | + 'description'] if 'description' in detail else "" |
| 207 | + |
| 208 | + props[prop] = make_property(prop, detail, desc) |
| 209 | + |
| 210 | + props['_properties'] = dict(zip(props.keys(), |
| 211 | + [None for x in |
| 212 | + xrange(len(props.keys()))])) |
| 213 | + |
| 214 | + """ If this object itself has a 'oneOf' designation, then |
| 215 | + make the validation 'type' the list of potential objects. |
| 216 | + """ |
| 217 | + if 'oneOf' in clsdata: |
| 218 | + klasses = self.resolve_classes(clsdata['oneOf']) |
| 219 | + # Need a validation to check that it meets one of them |
| 220 | + props['__validation__'] = {'type': klasses} |
| 221 | + |
| 222 | + props['__prop_names__'] = name_translation |
| 223 | + |
| 224 | + props['__propinfo__'] = properties |
| 225 | + required = set.union(*[p.__required__ for p in parents]) |
| 226 | + |
| 227 | + if 'required' in clsdata: |
| 228 | + for prop in clsdata['required']: |
| 229 | + required.add(prop.replace('@', '')) |
| 230 | + |
| 231 | + props['__required__'] = required |
| 232 | + |
| 233 | + cls = type(str(nm.split('/')[-1]), tuple(parents), props) |
| 234 | + |
| 235 | + return cls |
| 236 | + |
| 237 | + |
| 238 | +def make_property(prop, info, desc=""): |
| 239 | + |
| 240 | + def getprop(this): |
| 241 | + try: |
| 242 | + return this._properties[prop] |
| 243 | + except KeyError: |
| 244 | + raise AttributeError("No such attribute") |
| 245 | + |
| 246 | + def setprop(this, val): |
| 247 | + if isinstance(info['type'], (list, tuple)): |
| 248 | + ok = False |
| 249 | + for typ in info['type']: |
| 250 | + if isinstance(val, typ): |
| 251 | + ok = True |
| 252 | + break |
| 253 | + elif not isinstance(val, ProtocolBase): |
| 254 | + try: |
| 255 | + val = typ(**val) |
| 256 | + except: |
| 257 | + pass |
| 258 | + else: |
| 259 | + val.validate() |
| 260 | + ok = True |
| 261 | + break |
| 262 | + |
| 263 | + if not ok: |
| 264 | + raise TypeError( |
| 265 | + "Value must be one of {0}".format(info['type'])) |
| 266 | + |
| 267 | + if info['type'] in this.__SCHEMA_TYPES__.keys() and val |
| 268 | + is not None: |
| 269 | + val = this.__SCHEMA_TYPES__[info['type']](val) |
| 270 | + |
| 271 | + elif issubclass(info['type'], ProtocolBase): |
| 272 | + if not isinstance(val, info['type']): |
| 273 | + val = info['type'](**val) |
| 274 | + |
| 275 | + val.validate() |
| 276 | + |
| 277 | + this._properties[prop] = val |
| 278 | + |
| 279 | + def delprop(this): |
| 280 | + if prop in this.__required__: |
| 281 | + raise AttributeError("'%s' is required" % prop) |
| 282 | + else: |
| 283 | + del this._properties[prop] |
| 284 | + |
| 285 | + return property(getprop, setprop, delprop, desc) |
0 commit comments