1 | import json
|
---|
2 | from packson.JsonTypeInfo import Id,As, getTypeWrappingInfo
|
---|
3 | # from packson.JsonSubTypes import getSubTypes
|
---|
4 | from typing import _GenericAlias, cast , Dict, Any
|
---|
5 | from packson.JsonTools import isPrimitive, getActualClass, getInitArgs,addTypeInfo,getListClass
|
---|
6 | from pip._vendor.pyparsing import dictOf
|
---|
7 |
|
---|
8 | class ObjectMapper:
|
---|
9 | '''
|
---|
10 | A very simple packson-style objectmapper.
|
---|
11 | '''
|
---|
12 | def parse(self, data:object, clas:object )->object:
|
---|
13 | '''
|
---|
14 | @param data either a dict or a built-in object like an int
|
---|
15 | @param clas the expected class contained in the data.
|
---|
16 | If a dict, this class must have a __init__ function specifying the params
|
---|
17 | needed and the data in this case should be a dict.
|
---|
18 | @return a clas instance matching the data
|
---|
19 | '''
|
---|
20 | if getTypeWrappingInfo(clas):
|
---|
21 | if not isinstance(data, dict):
|
---|
22 | raise ValueError("Expected class, therefore data must be a dict but got "+str(data))
|
---|
23 | (data, clas) = getActualClass(cast(dict, data),clas)
|
---|
24 | # now distinguish
|
---|
25 | if isPrimitive(clas):
|
---|
26 | return self.parseBase(data, clas)
|
---|
27 | if (clas==list):
|
---|
28 | raise ValueError("Illegal type, use List[X] instead of list")
|
---|
29 | if (clas==dict):
|
---|
30 | raise ValueError("Illegal type, use Dict[X] instead of dict")
|
---|
31 | if type(clas)==_GenericAlias:
|
---|
32 | return self.parseGeneric(data,clas)
|
---|
33 | raise ValueError("GenericAlias not yet supported")
|
---|
34 | if type(data)==dict: # if it contains class, data must be dict
|
---|
35 | return self.parseClass(cast(dict,data), clas)
|
---|
36 | raise ValueError("Unexpected type of data:"+str(clas))
|
---|
37 |
|
---|
38 | def parseBase(self,obj, clas)->object:
|
---|
39 | '''
|
---|
40 | @param obj a built-in object like an int
|
---|
41 | @param clas the class of the expected object
|
---|
42 | class does not contain __init__, it must be a built-in type
|
---|
43 | @return the obj, after checking it's indeed a clas
|
---|
44 | '''
|
---|
45 | if not type(obj)==clas:
|
---|
46 | raise ValueError("expected "+str(clas)+", got "+str(obj))
|
---|
47 | return obj
|
---|
48 |
|
---|
49 |
|
---|
50 | def parseClass(self, data:dict, clas)->object:
|
---|
51 | '''
|
---|
52 | @param data a dict with the values for class.__init__ function
|
---|
53 | @return a clas instance matching the data
|
---|
54 | '''
|
---|
55 | if not isinstance(data,dict ):
|
---|
56 | raise ValueError("data "+str(data)+" must be dict")
|
---|
57 | # then this class needs initialization
|
---|
58 | initargs={}
|
---|
59 | argclasses=getInitArgs(clas)
|
---|
60 | for arg in argclasses:
|
---|
61 | if not arg in data:
|
---|
62 | raise ValueError(str(clas)+" constructor takes "+str(arg)+", but value missing in dict "+str(data))
|
---|
63 | try:
|
---|
64 | initargs[arg] = self.parse(data[arg], argclasses[arg])
|
---|
65 | except ValueError as e:
|
---|
66 | raise ValueError("Error parsing "+json.dumps(data),e)
|
---|
67 | return clas(**initargs)
|
---|
68 |
|
---|
69 | def parseGeneric(self, data, clas:_GenericAlias)->object:
|
---|
70 | '''
|
---|
71 | @param data may be list or dict, depending on the exact clas
|
---|
72 | @return instance of the clas. Don't know how to write this for typing
|
---|
73 | '''
|
---|
74 | gname =clas._name
|
---|
75 |
|
---|
76 | if gname=='List' or gname=='Set':
|
---|
77 | elementclas = clas.__args__[0]
|
---|
78 | if type(data)!=list:
|
---|
79 | raise ValueError("expected list[{elementclas}] but got "+str(data))
|
---|
80 | res=[self.parse(listitem, elementclas) for listitem in data]
|
---|
81 | if gname=='List':
|
---|
82 | return res
|
---|
83 | else:
|
---|
84 | return set(res)
|
---|
85 |
|
---|
86 | if gname=='Dict':
|
---|
87 | keyclas = clas.__args__[0]
|
---|
88 | if not keyclas.__hash__:
|
---|
89 | raise ValueError("Dict cannot be serialized, key class "+str(keyclas)+" does not have __hash__")
|
---|
90 | elementclas = clas.__args__[1]
|
---|
91 | if type(data)!=dict:
|
---|
92 | raise ValueError("expected dict[{keyclass, elementclas}] but got "+str(data))
|
---|
93 | return { self.parse(key, keyclas) : self.parse(val, elementclas)\
|
---|
94 | for key,val in data.items() }
|
---|
95 |
|
---|
96 | raise ValueError("Unsupported generic type "+gname)
|
---|
97 |
|
---|
98 | def toJson(self, data)->dict:
|
---|
99 | '''
|
---|
100 | @param data either a dict or a built-in object like an int
|
---|
101 | @return a dict containing this object
|
---|
102 | '''
|
---|
103 | res:dict
|
---|
104 | clas = type(data)
|
---|
105 | if isPrimitive(clas):
|
---|
106 | return self.toJsonBase(data)
|
---|
107 | if clas==list or clas==tuple or clas==set:
|
---|
108 | res=self.toJsonList(data)
|
---|
109 | elif clas==dict:
|
---|
110 | res=self.toJsonDict(data)
|
---|
111 | elif type(clas)==type:
|
---|
112 | # is it general class? FIXME can this be done better?
|
---|
113 | res = self.toJsonClass(data)
|
---|
114 | else:
|
---|
115 | raise ValueError("Unsupported object of type "+str(clas))
|
---|
116 | # check if wrapper setting is requested for this class
|
---|
117 | if getTypeWrappingInfo(clas):
|
---|
118 | res=addTypeInfo(clas,res);
|
---|
119 | return res
|
---|
120 |
|
---|
121 | def toJsonClass(self,data:object)->dict:
|
---|
122 | '''
|
---|
123 | @param data a class instance
|
---|
124 | @return data a dict with the values
|
---|
125 | The values are based on the class.__init__ function
|
---|
126 | '''
|
---|
127 | clas=type(data)
|
---|
128 | res={}
|
---|
129 | for arg in getInitArgs(clas):
|
---|
130 | gettername = 'get'+arg
|
---|
131 | if not hasattr(data, gettername) :# in clas.__dict__:
|
---|
132 | raise ValueError("The object "+str(data)+ "of type "+ str(clas)+" has no function "+gettername)
|
---|
133 | argvalue = getattr(data, gettername)() #.__dict__[gettername]()
|
---|
134 | res[arg]=self.toJson(argvalue)
|
---|
135 | return res
|
---|
136 |
|
---|
137 |
|
---|
138 | def toJsonBase(self,obj):
|
---|
139 | '''
|
---|
140 | @param obj a built-in object like an int
|
---|
141 | obj does not contain __init__, it must be a built-in type
|
---|
142 | '''
|
---|
143 | return obj
|
---|
144 |
|
---|
145 | def toJsonList(self, listofobj):
|
---|
146 | '''
|
---|
147 | @param listofobj list or tuple of objects each to be serialized separately.
|
---|
148 | @return list object to be put in the json representation,
|
---|
149 | '''
|
---|
150 | if len(listofobj)==0:
|
---|
151 | return [] # empty list has no type.
|
---|
152 | clas = getListClass(listofobj)
|
---|
153 | # if isPrimitive(clas):
|
---|
154 | # return listofobj
|
---|
155 | if not (isPrimitive(clas) or getTypeWrappingInfo(clas)):
|
---|
156 | raise ValueError("@JsonTypeInfo is required for list objects, but found "+str(clas))
|
---|
157 | return [self.toJson(elt) for elt in listofobj]
|
---|
158 |
|
---|
159 | def toJsonDict(self, dictofobj:Dict[Any,Any]):
|
---|
160 | '''
|
---|
161 | @param dictofobj dict with objects each to be serialized separately.
|
---|
162 | The keys must be primitive, values must be all the same class.
|
---|
163 | @return list object to be put in the json representation,
|
---|
164 | '''
|
---|
165 | if len(dictofobj)==0:
|
---|
166 | return {} # empty list has no type.
|
---|
167 | keyclas = getListClass(list(dictofobj.keys()))
|
---|
168 | valclas = getListClass(list(dictofobj.values()))
|
---|
169 | # if isPrimitive(clas):
|
---|
170 | # return listofobj
|
---|
171 | if not isPrimitive(keyclas):
|
---|
172 | raise ValueError("key of dict must be primitive, but keys are of type "+\
|
---|
173 | str(keyclas)+" in "+str(dictofobj))
|
---|
174 | if not getTypeWrappingInfo(valclas):
|
---|
175 | raise ValueError("@JsonTypeInfo is required for list objects, but found "+str(valclas))
|
---|
176 | return { self.toJson(key):self.toJson(val) for key,val in dictofobj.items()}
|
---|
177 |
|
---|
178 | |
---|