Line data Source code
1 : /*
2 : * Package : JsonObjectLite
3 : * Author : S. Hamblett <steve.hamblett@linux.com>
4 : * Date : 22/09/2017
5 : * Copyright : S.Hamblett
6 : * Based on json_object (C) 2013 Chris Buckett (chrisbuckett@gmail.com)
7 : */
8 :
9 : library json_object_lite;
10 :
11 : import "dart:convert";
12 :
13 : /// Set to true to as required
14 : bool enableJsonObjectLiteDebugMessages = false;
15 :
16 : /// Debug logger
17 : void _log(String obj) {
18 : if (enableJsonObjectLiteDebugMessages) {
19 1 : print(obj);
20 : }
21 : }
22 :
23 : /// JsonObjectLite allows .property name access to JSON by using
24 : /// noSuchMethod. The object is set to not immutable so properties can be
25 : /// added.
26 : @proxy
27 : class JsonObjectLite<E> extends Object implements Map, Iterable {
28 : /// Default constructor.
29 : /// Creates a new empty map.
30 1 : JsonObjectLite() {
31 2 : _objectData = new Map();
32 1 : isImmutable = false;
33 : }
34 :
35 : /// Eager constructor parses [jsonString] using [JsonDecoder].
36 : ///
37 : /// If [t] is given, will replace [t]'s contents from the string and return [t].
38 : ///
39 : /// If [recursive] is true, replaces all maps recursively with JsonObjects.
40 : /// The default value is [true].
41 : /// The object is set to immutable, the user must reset this to add more properties.
42 : factory JsonObjectLite.fromJsonString(String jsonString,
43 : [JsonObjectLite t, bool recursive = true]) {
44 : if (t == null) {
45 1 : t = new JsonObjectLite();
46 : }
47 3 : t._objectData = decoder.convert(jsonString);
48 : if (recursive) {
49 2 : t._extractElements(t._objectData);
50 : }
51 1 : t.isImmutable = true;
52 : return t;
53 : }
54 :
55 : /// An alternate constructor, allows creating directly from a map
56 : /// rather than a json string.
57 : ///
58 : /// If [recursive] is true, all values of the map will be converted
59 : /// to [JsonObjectLite]s as well. The default value is [true].
60 : /// The object is set to immutable, the user must reset this to add more properties.
61 1 : JsonObjectLite.fromMap(Map map, [bool recursive = true]) {
62 1 : _objectData = map;
63 : if (recursive) {
64 2 : _extractElements(_objectData);
65 : }
66 1 : isImmutable = true;
67 : }
68 :
69 : /// Typed JsonObjectLite
70 : static JsonObjectLite toTypedJsonObjectLite(
71 : JsonObjectLite src, JsonObjectLite dest) {
72 2 : dest._objectData = src._objectData;
73 1 : if (src.isImmutable) {
74 1 : dest.isImmutable = true;
75 : }
76 : return dest;
77 : }
78 :
79 : /// Contains either a [List] or [Map]
80 : dynamic _objectData;
81 :
82 : static JsonEncoder encoder = new JsonEncoder();
83 : static JsonDecoder decoder = new JsonDecoder(null);
84 :
85 : /// isImmutable indicates if a new item can be added to the internal
86 : /// map via the noSuchMethod property, or the functions inherited from the
87 : /// map interface.
88 : ///
89 : /// If set to true, then only the properties that were
90 : /// in the original map or json string passed in can be used.
91 : ///
92 : /// If set to false, then calling o.blah="123" will create a new blah property
93 : /// if it didn't already exist.
94 : ///
95 : /// Set to true by default when a JsonObjectLite is created with [JsonObjectLite.fromJsonString()]
96 : /// or [JsonObjectLite.fromMap()].
97 : /// The default constructor [JsonObjectLite()], sets this value to
98 : /// false so properties can be added.
99 1 : set isImmutable(bool state) => isExtendable = !state;
100 :
101 1 : bool get isImmutable => !isExtendable;
102 :
103 : @deprecated
104 :
105 : /// For compatibility the isExtendable boolean is preserved, however new usage
106 : /// should use isImmutable above. Usage is as per JsonObject.
107 : bool isExtendable;
108 :
109 : /// Returns a string representation of the underlying object data
110 : String toString() {
111 3 : return encoder.convert(_objectData);
112 : }
113 :
114 : /// Returns either the underlying parsed data as an iterable list (if the
115 : /// underlying data contains a list), or returns the map.values (if the
116 : /// underlying data contains a map).
117 : Iterable toIterable() {
118 2 : if (_objectData is Iterable) {
119 1 : return _objectData;
120 : }
121 2 : return _objectData.values;
122 : }
123 :
124 : /// noSuchMethod()
125 : /// If we try to access a property using dot notation (eg: o.wibble ), then
126 : /// noSuchMethod will be invoked, and identify the getter or setter name.
127 : /// It then looks up in the map contained in _objectData (represented using
128 : /// this (as this class implements [Map], and forwards it's calls to that
129 : /// class.
130 : /// If it finds the getter or setter then it either updates the value, or
131 : /// replaces the value.
132 : ///
133 : /// If isImmutable = true, then it will disallow the property access
134 : /// even if the property doesn't yet exist.
135 : dynamic noSuchMethod(Invocation mirror) {
136 : int positionalArgs = 0;
137 1 : if (mirror.positionalArguments != null)
138 2 : positionalArgs = mirror.positionalArguments.length;
139 : String property = "Not Found";
140 :
141 2 : if (mirror.isGetter && (positionalArgs == 0)) {
142 : // Synthetic getter
143 2 : property = _symbolToString(mirror.memberName);
144 1 : if (this.containsKey(property)) {
145 1 : return this[property];
146 : }
147 2 : } else if (mirror.isSetter && positionalArgs == 1) {
148 : // Synthetic setter
149 : // If the property doesn't exist, it will only be added
150 : // if isImmutable = false
151 2 : property = _symbolToString(mirror.memberName, true);
152 1 : if (!isImmutable) {
153 3 : this[property] = mirror.positionalArguments[0];
154 : }
155 1 : return this[property];
156 : }
157 :
158 : // If we get here, then we've not found it - throw.
159 2 : _log("noSuchMethod:: Not found: ${property}");
160 3 : _log("noSuchMethod:: IsGetter: ${mirror.isGetter}");
161 3 : _log("noSuchMethod:: IsSetter: ${mirror.isGetter}");
162 3 : _log("noSuchMethod:: isAccessor: ${mirror.isAccessor}");
163 1 : return super.noSuchMethod(mirror);
164 : }
165 :
166 : /// If the object passed in is a MAP, then we iterate through each of
167 : /// the values of the map, and if any value is a map, then we create a new
168 : /// [JsonObjectLite] replacing that map in the original data with that [JsonObjectLite]
169 : /// to a new [JsonObjectLite]. If the value is a Collection, then we call this
170 : /// function recursively.
171 : ///
172 : /// If the object passed in is a Collection, then we iterate through
173 : /// each item. If that item is a map, then we replace the item with a
174 : /// [JsonObjectLite] created from the map. If the item is a Collection, then we
175 : /// call this function recursively.
176 : ///
177 : void _extractElements(data) {
178 1 : if (data is Map) {
179 : // Iterate through each of the k,v pairs, replacing maps with jsonObjects
180 1 : data.forEach((key, value) {
181 1 : if (value is Map) {
182 : // Replace the existing Map with a JsonObject
183 2 : data[key] = new JsonObjectLite.fromMap(value);
184 1 : } else if (value is List) {
185 : // Recurse
186 1 : _extractElements(value);
187 : }
188 : });
189 1 : } else if (data is List) {
190 : // Iterate through each of the items
191 : // If any of them is a list, check to see if it contains a map
192 :
193 3 : for (int i = 0; i < data.length; i++) {
194 : // Use the for loop so that we can index the item to replace it if req'd
195 1 : final listItem = data[i];
196 1 : if (listItem is List) {
197 : // Recurse
198 1 : _extractElements(listItem);
199 1 : } else if (listItem is Map) {
200 : // Replace the existing Map with a JsonObject
201 2 : data[i] = new JsonObjectLite.fromMap(listItem);
202 : }
203 : }
204 : }
205 : }
206 :
207 : /// Convert the incoming method name(symbol) into a string, without using mirrors.
208 : String _symbolToString(dynamic value, [bool isSetter = false]) {
209 : String ret;
210 1 : if (value is Symbol) {
211 : // Brittle but we avoid mirrors
212 1 : final String name = value.toString();
213 4 : ret = name.substring((name.indexOf('"') + 1), name.lastIndexOf('"'));
214 : // Setters have an '=' on the end, remove it
215 : if (isSetter) {
216 3 : ret = ret.replaceFirst("=", "", ret.length - 1);
217 : }
218 : } else {
219 1 : ret = value.toString();
220 : }
221 2 : _log("_symbolToString:: Method name is: ${ret}");
222 : return ret;
223 : }
224 :
225 : ///
226 : /// Iterable implementation methods and properties
227 : ///
228 :
229 2 : bool any(bool f(dynamic element)) => this.toIterable().any(f);
230 :
231 2 : bool contains(dynamic element) => this.toIterable().contains(element);
232 :
233 2 : E elementAt(int index) => this.toIterable().elementAt(index);
234 :
235 2 : bool every(bool f(dynamic element)) => this.toIterable().every(f);
236 :
237 : Iterable<T> expand<T>(dynamic f(dynamic element)) =>
238 2 : this.toIterable().expand(f);
239 :
240 : dynamic firstWhere(bool test(dynamic value), {dynamic orElse}) =>
241 2 : this.toIterable().firstWhere(test, orElse: orElse);
242 :
243 : T fold<T>(T initialValue, T combine(T a, dynamic b)) =>
244 2 : this.toIterable().fold(initialValue, combine);
245 :
246 2 : String join([String separator = ""]) => this.toIterable().join(separator);
247 :
248 : dynamic lastWhere(bool test(dynamic value), {dynamic orElse}) =>
249 2 : this.toIterable().firstWhere(test, orElse: orElse);
250 :
251 2 : Iterable<T> map<T>(dynamic f(dynamic element)) => this.toIterable().map(f);
252 :
253 : dynamic reduce(dynamic combine(dynamic value, dynamic element)) =>
254 2 : this.toIterable().reduce(combine);
255 :
256 : dynamic singleWhere(bool test(dynamic value), {dynamic orElse}) =>
257 2 : this.toIterable().firstWhere(test, orElse: orElse);
258 :
259 2 : Iterable<E> skip(int n) => this.toIterable().skip(n);
260 :
261 : Iterable<E> skipWhile(bool test(dynamic value)) =>
262 2 : this.toIterable().skipWhile(test);
263 :
264 2 : Iterable<E> take(int n) => this.toIterable().take(n);
265 :
266 : Iterable<E> takeWhile(bool test(dynamic value)) =>
267 2 : this.toIterable().takeWhile(test);
268 :
269 : List<dynamic> toList({bool growable: true}) =>
270 2 : this.toIterable().toList(growable: growable);
271 :
272 2 : Set<dynamic> toSet() => this.toIterable().toSet();
273 :
274 2 : Iterable<E> where(bool f(dynamic element)) => this.toIterable().where(f);
275 :
276 2 : E get first => this.toIterable().first;
277 :
278 2 : Iterator<E> get iterator => this.toIterable().iterator;
279 :
280 2 : E get last => this.toIterable().last;
281 :
282 2 : E get single => this.toIterable().single;
283 :
284 : ///
285 : /// Map implementation methods and properties *
286 : ///
287 :
288 : // Pass through to the inner _objectData map.
289 2 : bool containsValue(dynamic value) => _objectData.containsValue(value);
290 :
291 : // Pass through to the inner _objectData map.
292 : bool containsKey(dynamic value) {
293 3 : return _objectData.containsKey(_symbolToString(value));
294 : }
295 :
296 : // Pass through to the innter _objectData map.
297 2 : bool get isNotEmpty => _objectData.isNotEmpty;
298 :
299 : // Pass through to the inner _objectData map.
300 2 : dynamic operator [](dynamic key) => _objectData[key];
301 :
302 : // Pass through to the inner _objectData map.
303 : void forEach(void func(dynamic key, dynamic value)) =>
304 2 : _objectData.forEach(func);
305 :
306 : // Pass through to the inner _objectData map.
307 2 : Iterable get keys => _objectData.keys;
308 :
309 : // Pass through to the inner _objectData map.
310 2 : Iterable get values => _objectData.values;
311 :
312 : // Pass through to the inner _objectData map.
313 2 : int get length => _objectData.length;
314 :
315 : // Pass through to the inner _objectData map.
316 2 : bool get isEmpty => _objectData.isEmpty;
317 :
318 : // Pass through to the inner _objectData map.
319 2 : void addAll(dynamic items) => _objectData.addAll(items);
320 :
321 : /// Specific implementations which check isImmtable to determine if an
322 : /// unknown key should be allowed.
323 : ///
324 : /// If [isImmutable] is false, or the key already exists,
325 : /// then allow the edit.
326 : /// Throw [JsonObjectLiteException] if we're not allowed to add a new
327 : /// key
328 : void operator []=(dynamic key, dynamic value) {
329 : // If the map is not immutable, or it already contains the key, then
330 3 : if (this.isImmutable == false || this.containsKey(key)) {
331 : //allow the edit, as we don't care if it's a new key or not
332 2 : return _objectData[key] = value;
333 : } else {
334 1 : throw new JsonObjectLiteException("JsonObject is not extendable");
335 : }
336 : }
337 :
338 : /// If [isImmutable] is false, or the key already exists,
339 : /// then allow the edit.
340 : /// Throw [JsonObjectLiteException] if we're not allowed to add a new
341 : /// key
342 : void putIfAbsent(dynamic key, ifAbsent()) {
343 3 : if (this.isImmutable == false || this.containsKey(key)) {
344 2 : return _objectData.putIfAbsent(key, ifAbsent);
345 : } else {
346 1 : throw new JsonObjectLiteException("JsonObject is not extendable");
347 : }
348 : }
349 :
350 : /// If [isImmutable] is false, or the key already exists,
351 : /// then allow the removal.
352 : /// Throw [JsonObjectLiteException] if we're not allowed to remove a
353 : /// key
354 : dynamic remove(dynamic key) {
355 3 : if (this.isImmutable == false || this.containsKey(key)) {
356 2 : return _objectData.remove(key);
357 : } else {
358 1 : throw new JsonObjectLiteException("JsonObject is not extendable");
359 : }
360 : }
361 :
362 : /// If [isImmutable] is false, then allow the map to be cleared
363 : /// Throw [JsonObjectLiteException] if we're not allowed to clear.
364 : void clear() {
365 2 : if (this.isImmutable == false) {
366 2 : _objectData.clear();
367 : } else {
368 1 : throw new JsonObjectLiteException("JsonObject is not extendable");
369 : }
370 : }
371 : }
372 :
373 : /// Exception class thrown by JsonObjectLite
374 : class JsonObjectLiteException implements Exception {
375 1 : const JsonObjectLiteException([String message]) : this._message = message;
376 1 : String toString() => (this._message != null
377 1 : ? "JsonObjectException: $_message"
378 1 : : "JsonObjectException");
379 : final String _message;
380 : }
|