| OLD | NEW | 
|---|
| (Empty) |  | 
|  | 1 // Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file | 
|  | 2 // for details. All rights reserved. Use of this source code is governed by a | 
|  | 3 // BSD-style license that can be found in the LICENSE file. | 
|  | 4 | 
|  | 5 import 'dart:collection'; | 
|  | 6 import 'dart:convert' hide JsonDecoder; | 
|  | 7 | 
|  | 8 import 'package:analyzer_plugin/protocol/generated_protocol.dart'; | 
|  | 9 import 'package:analyzer_plugin/protocol/protocol.dart'; | 
|  | 10 | 
|  | 11 final Map<String, RefactoringKind> REQUEST_ID_REFACTORING_KINDS = | 
|  | 12     new HashMap<String, RefactoringKind>(); | 
|  | 13 | 
|  | 14 /** | 
|  | 15  * Adds the given [sourceEdits] to the list in [sourceFileEdit]. | 
|  | 16  */ | 
|  | 17 void addAllEditsForSource( | 
|  | 18     SourceFileEdit sourceFileEdit, Iterable<SourceEdit> edits) { | 
|  | 19   edits.forEach(sourceFileEdit.add); | 
|  | 20 } | 
|  | 21 | 
|  | 22 /** | 
|  | 23  * Adds the given [sourceEdit] to the list in [sourceFileEdit]. | 
|  | 24  */ | 
|  | 25 void addEditForSource(SourceFileEdit sourceFileEdit, SourceEdit sourceEdit) { | 
|  | 26   List<SourceEdit> edits = sourceFileEdit.edits; | 
|  | 27   int index = 0; | 
|  | 28   while (index < edits.length && edits[index].offset > sourceEdit.offset) { | 
|  | 29     index++; | 
|  | 30   } | 
|  | 31   edits.insert(index, sourceEdit); | 
|  | 32 } | 
|  | 33 | 
|  | 34 /** | 
|  | 35  * Adds [edit] to the [FileEdit] for the given [file]. | 
|  | 36  */ | 
|  | 37 void addEditToSourceChange( | 
|  | 38     SourceChange change, String file, int fileStamp, SourceEdit edit) { | 
|  | 39   SourceFileEdit fileEdit = change.getFileEdit(file); | 
|  | 40   if (fileEdit == null) { | 
|  | 41     fileEdit = new SourceFileEdit(file, fileStamp); | 
|  | 42     change.addFileEdit(fileEdit); | 
|  | 43   } | 
|  | 44   fileEdit.add(edit); | 
|  | 45 } | 
|  | 46 | 
|  | 47 /** | 
|  | 48  * Get the result of applying the edit to the given [code].  Access via | 
|  | 49  * SourceEdit.apply(). | 
|  | 50  */ | 
|  | 51 String applyEdit(String code, SourceEdit edit) { | 
|  | 52   if (edit.length < 0) { | 
|  | 53     throw new RangeError('length is negative'); | 
|  | 54   } | 
|  | 55   return code.replaceRange(edit.offset, edit.end, edit.replacement); | 
|  | 56 } | 
|  | 57 | 
|  | 58 /** | 
|  | 59  * Get the result of applying a set of [edits] to the given [code].  Edits | 
|  | 60  * are applied in the order they appear in [edits].  Access via | 
|  | 61  * SourceEdit.applySequence(). | 
|  | 62  */ | 
|  | 63 String applySequenceOfEdits(String code, Iterable<SourceEdit> edits) { | 
|  | 64   edits.forEach((SourceEdit edit) { | 
|  | 65     code = edit.apply(code); | 
|  | 66   }); | 
|  | 67   return code; | 
|  | 68 } | 
|  | 69 | 
|  | 70 /** | 
|  | 71  * Returns the [FileEdit] for the given [file], maybe `null`. | 
|  | 72  */ | 
|  | 73 SourceFileEdit getChangeFileEdit(SourceChange change, String file) { | 
|  | 74   for (SourceFileEdit fileEdit in change.edits) { | 
|  | 75     if (fileEdit.file == file) { | 
|  | 76       return fileEdit; | 
|  | 77     } | 
|  | 78   } | 
|  | 79   return null; | 
|  | 80 } | 
|  | 81 | 
|  | 82 /** | 
|  | 83  * Compare the lists [listA] and [listB], using [itemEqual] to compare | 
|  | 84  * list elements. | 
|  | 85  */ | 
|  | 86 bool listEqual(List listA, List listB, bool itemEqual(a, b)) { | 
|  | 87   if (listA == null) { | 
|  | 88     return listB == null; | 
|  | 89   } | 
|  | 90   if (listB == null) { | 
|  | 91     return false; | 
|  | 92   } | 
|  | 93   if (listA.length != listB.length) { | 
|  | 94     return false; | 
|  | 95   } | 
|  | 96   for (int i = 0; i < listA.length; i++) { | 
|  | 97     if (!itemEqual(listA[i], listB[i])) { | 
|  | 98       return false; | 
|  | 99     } | 
|  | 100   } | 
|  | 101   return true; | 
|  | 102 } | 
|  | 103 | 
|  | 104 /** | 
|  | 105  * Compare the maps [mapA] and [mapB], using [valueEqual] to compare map | 
|  | 106  * values. | 
|  | 107  */ | 
|  | 108 bool mapEqual(Map mapA, Map mapB, bool valueEqual(a, b)) { | 
|  | 109   if (mapA == null) { | 
|  | 110     return mapB == null; | 
|  | 111   } | 
|  | 112   if (mapB == null) { | 
|  | 113     return false; | 
|  | 114   } | 
|  | 115   if (mapA.length != mapB.length) { | 
|  | 116     return false; | 
|  | 117   } | 
|  | 118   for (var key in mapA.keys) { | 
|  | 119     if (!mapB.containsKey(key)) { | 
|  | 120       return false; | 
|  | 121     } | 
|  | 122     if (!valueEqual(mapA[key], mapB[key])) { | 
|  | 123       return false; | 
|  | 124     } | 
|  | 125   } | 
|  | 126   return true; | 
|  | 127 } | 
|  | 128 | 
|  | 129 /** | 
|  | 130  * Translate the input [map], applying [keyCallback] to all its keys, and | 
|  | 131  * [valueCallback] to all its values. | 
|  | 132  */ | 
|  | 133 Map/*<KR, VR>*/ mapMap/*<KP, VP, KR, VR>*/(Map/*<KP, VP>*/ map, | 
|  | 134     {dynamic/*=KR*/ keyCallback(/*<KP>*/ key), | 
|  | 135     dynamic/*=VR*/ valueCallback(/*<VP>*/ value)}) { | 
|  | 136   Map/*<KR, VR>*/ result = new HashMap/*<KR, VR>*/(); | 
|  | 137   map.forEach((key, value) { | 
|  | 138     Object/*=KR*/ resultKey; | 
|  | 139     Object/*=VR*/ resultValue; | 
|  | 140     if (keyCallback != null) { | 
|  | 141       resultKey = keyCallback(key); | 
|  | 142     } else { | 
|  | 143       resultKey = key as Object/*=KR*/; | 
|  | 144     } | 
|  | 145     if (valueCallback != null) { | 
|  | 146       resultValue = valueCallback(value); | 
|  | 147     } else { | 
|  | 148       resultValue = value as Object/*=VR*/; | 
|  | 149     } | 
|  | 150     result[resultKey] = resultValue; | 
|  | 151   }); | 
|  | 152   return result; | 
|  | 153 } | 
|  | 154 | 
|  | 155 RefactoringProblemSeverity maxRefactoringProblemSeverity( | 
|  | 156     RefactoringProblemSeverity a, RefactoringProblemSeverity b) { | 
|  | 157   if (b == null) { | 
|  | 158     return a; | 
|  | 159   } | 
|  | 160   if (a == null) { | 
|  | 161     return b; | 
|  | 162   } else if (a == RefactoringProblemSeverity.INFO) { | 
|  | 163     return b; | 
|  | 164   } else if (a == RefactoringProblemSeverity.WARNING) { | 
|  | 165     if (b == RefactoringProblemSeverity.ERROR || | 
|  | 166         b == RefactoringProblemSeverity.FATAL) { | 
|  | 167       return b; | 
|  | 168     } | 
|  | 169   } else if (a == RefactoringProblemSeverity.ERROR) { | 
|  | 170     if (b == RefactoringProblemSeverity.FATAL) { | 
|  | 171       return b; | 
|  | 172     } | 
|  | 173   } | 
|  | 174   return a; | 
|  | 175 } | 
|  | 176 | 
|  | 177 /** | 
|  | 178  * Create a [RefactoringFeedback] corresponding the given [kind]. | 
|  | 179  */ | 
|  | 180 RefactoringFeedback refactoringFeedbackFromJson( | 
|  | 181     JsonDecoder jsonDecoder, String jsonPath, Object json, Map feedbackJson) { | 
|  | 182   RefactoringKind kind = jsonDecoder.refactoringKind; | 
|  | 183   if (kind == RefactoringKind.EXTRACT_LOCAL_VARIABLE) { | 
|  | 184     return new ExtractLocalVariableFeedback.fromJson( | 
|  | 185         jsonDecoder, jsonPath, json); | 
|  | 186   } | 
|  | 187   if (kind == RefactoringKind.EXTRACT_METHOD) { | 
|  | 188     return new ExtractMethodFeedback.fromJson(jsonDecoder, jsonPath, json); | 
|  | 189   } | 
|  | 190   if (kind == RefactoringKind.INLINE_LOCAL_VARIABLE) { | 
|  | 191     return new InlineLocalVariableFeedback.fromJson( | 
|  | 192         jsonDecoder, jsonPath, json); | 
|  | 193   } | 
|  | 194   if (kind == RefactoringKind.INLINE_METHOD) { | 
|  | 195     return new InlineMethodFeedback.fromJson(jsonDecoder, jsonPath, json); | 
|  | 196   } | 
|  | 197   if (kind == RefactoringKind.RENAME) { | 
|  | 198     return new RenameFeedback.fromJson(jsonDecoder, jsonPath, json); | 
|  | 199   } | 
|  | 200   return null; | 
|  | 201 } | 
|  | 202 | 
|  | 203 /** | 
|  | 204  * Create a [RefactoringOptions] corresponding the given [kind]. | 
|  | 205  */ | 
|  | 206 RefactoringOptions refactoringOptionsFromJson(JsonDecoder jsonDecoder, | 
|  | 207     String jsonPath, Object json, RefactoringKind kind) { | 
|  | 208   if (kind == RefactoringKind.EXTRACT_LOCAL_VARIABLE) { | 
|  | 209     return new ExtractLocalVariableOptions.fromJson( | 
|  | 210         jsonDecoder, jsonPath, json); | 
|  | 211   } | 
|  | 212   if (kind == RefactoringKind.EXTRACT_METHOD) { | 
|  | 213     return new ExtractMethodOptions.fromJson(jsonDecoder, jsonPath, json); | 
|  | 214   } | 
|  | 215   if (kind == RefactoringKind.INLINE_METHOD) { | 
|  | 216     return new InlineMethodOptions.fromJson(jsonDecoder, jsonPath, json); | 
|  | 217   } | 
|  | 218   if (kind == RefactoringKind.MOVE_FILE) { | 
|  | 219     return new MoveFileOptions.fromJson(jsonDecoder, jsonPath, json); | 
|  | 220   } | 
|  | 221   if (kind == RefactoringKind.RENAME) { | 
|  | 222     return new RenameOptions.fromJson(jsonDecoder, jsonPath, json); | 
|  | 223   } | 
|  | 224   return null; | 
|  | 225 } | 
|  | 226 | 
|  | 227 ///** | 
|  | 228 // * Create a [RefactoringFeedback] corresponding the given [kind]. | 
|  | 229 // */ | 
|  | 230 //RefactoringFeedback refactoringFeedbackFromJson( | 
|  | 231 //    JsonDecoder jsonDecoder, String jsonPath, Object json, Map feedbackJson) { | 
|  | 232 //  RefactoringKind kind = jsonDecoder.refactoringKind; | 
|  | 233 //  if (kind == RefactoringKind.EXTRACT_LOCAL_VARIABLE) { | 
|  | 234 //    return new ExtractLocalVariableFeedback.fromJson( | 
|  | 235 //        jsonDecoder, jsonPath, json); | 
|  | 236 //  } | 
|  | 237 //  if (kind == RefactoringKind.EXTRACT_METHOD) { | 
|  | 238 //    return new ExtractMethodFeedback.fromJson(jsonDecoder, jsonPath, json); | 
|  | 239 //  } | 
|  | 240 //  if (kind == RefactoringKind.INLINE_LOCAL_VARIABLE) { | 
|  | 241 //    return new InlineLocalVariableFeedback.fromJson( | 
|  | 242 //        jsonDecoder, jsonPath, json); | 
|  | 243 //  } | 
|  | 244 //  if (kind == RefactoringKind.INLINE_METHOD) { | 
|  | 245 //    return new InlineMethodFeedback.fromJson(jsonDecoder, jsonPath, json); | 
|  | 246 //  } | 
|  | 247 //  if (kind == RefactoringKind.RENAME) { | 
|  | 248 //    return new RenameFeedback.fromJson(jsonDecoder, jsonPath, json); | 
|  | 249 //  } | 
|  | 250 //  return null; | 
|  | 251 //} | 
|  | 252 // | 
|  | 253 ///** | 
|  | 254 // * Create a [RefactoringOptions] corresponding the given [kind]. | 
|  | 255 // */ | 
|  | 256 //RefactoringOptions refactoringOptionsFromJson(JsonDecoder jsonDecoder, | 
|  | 257 //    String jsonPath, Object json, RefactoringKind kind) { | 
|  | 258 //  if (kind == RefactoringKind.EXTRACT_LOCAL_VARIABLE) { | 
|  | 259 //    return new ExtractLocalVariableOptions.fromJson( | 
|  | 260 //        jsonDecoder, jsonPath, json); | 
|  | 261 //  } | 
|  | 262 //  if (kind == RefactoringKind.EXTRACT_METHOD) { | 
|  | 263 //    return new ExtractMethodOptions.fromJson(jsonDecoder, jsonPath, json); | 
|  | 264 //  } | 
|  | 265 //  if (kind == RefactoringKind.INLINE_METHOD) { | 
|  | 266 //    return new InlineMethodOptions.fromJson(jsonDecoder, jsonPath, json); | 
|  | 267 //  } | 
|  | 268 //  if (kind == RefactoringKind.MOVE_FILE) { | 
|  | 269 //    return new MoveFileOptions.fromJson(jsonDecoder, jsonPath, json); | 
|  | 270 //  } | 
|  | 271 //  if (kind == RefactoringKind.RENAME) { | 
|  | 272 //    return new RenameOptions.fromJson(jsonDecoder, jsonPath, json); | 
|  | 273 //  } | 
|  | 274 //  return null; | 
|  | 275 //} | 
|  | 276 | 
|  | 277 /** | 
|  | 278  * Type of callbacks used to decode parts of JSON objects.  [jsonPath] is a | 
|  | 279  * string describing the part of the JSON object being decoded, and [value] is | 
|  | 280  * the part to decode. | 
|  | 281  */ | 
|  | 282 typedef E JsonDecoderCallback<E>(String jsonPath, Object value); | 
|  | 283 | 
|  | 284 /** | 
|  | 285  * Instances of the class [HasToJson] implement [toJson] method that returns | 
|  | 286  * a JSON presentation. | 
|  | 287  */ | 
|  | 288 abstract class HasToJson { | 
|  | 289   /** | 
|  | 290    * Returns a JSON presentation of the object. | 
|  | 291    */ | 
|  | 292   Map<String, Object> toJson(); | 
|  | 293 } | 
|  | 294 | 
|  | 295 /** | 
|  | 296  * Base class for decoding JSON objects.  The derived class must implement | 
|  | 297  * error reporting logic. | 
|  | 298  */ | 
|  | 299 abstract class JsonDecoder { | 
|  | 300   /** | 
|  | 301    * Retrieve the RefactoringKind that should be assumed when decoding | 
|  | 302    * refactoring feedback objects, or null if no refactoring feedback object is | 
|  | 303    * expected to be encountered. | 
|  | 304    */ | 
|  | 305   RefactoringKind get refactoringKind; | 
|  | 306 | 
|  | 307   /** | 
|  | 308    * Decode a JSON object that is expected to be a boolean.  The strings "true" | 
|  | 309    * and "false" are also accepted. | 
|  | 310    */ | 
|  | 311   bool decodeBool(String jsonPath, Object json) { | 
|  | 312     if (json is bool) { | 
|  | 313       return json; | 
|  | 314     } else if (json == 'true') { | 
|  | 315       return true; | 
|  | 316     } else if (json == 'false') { | 
|  | 317       return false; | 
|  | 318     } | 
|  | 319     throw mismatch(jsonPath, 'bool', json); | 
|  | 320   } | 
|  | 321 | 
|  | 322   /** | 
|  | 323    * Decode a JSON object that is expected to be an integer.  A string | 
|  | 324    * representation of an integer is also accepted. | 
|  | 325    */ | 
|  | 326   int decodeInt(String jsonPath, Object json) { | 
|  | 327     if (json is int) { | 
|  | 328       return json; | 
|  | 329     } else if (json is String) { | 
|  | 330       return int.parse(json, onError: (String value) { | 
|  | 331         throw mismatch(jsonPath, 'int', json); | 
|  | 332       }); | 
|  | 333     } | 
|  | 334     throw mismatch(jsonPath, 'int', json); | 
|  | 335   } | 
|  | 336 | 
|  | 337   /** | 
|  | 338    * Decode a JSON object that is expected to be a List. The [decoder] is used | 
|  | 339    * to decode the items in the list. | 
|  | 340    * | 
|  | 341    * The type parameter [E] is the expected type of the elements in the list. | 
|  | 342    */ | 
|  | 343   List/*<E>*/ decodeList/*<E>*/(String jsonPath, Object json, | 
|  | 344       [JsonDecoderCallback/*<E>*/ decoder]) { | 
|  | 345     if (json == null) { | 
|  | 346       return/*<E>*/ []; | 
|  | 347     } else if (json is List) { | 
|  | 348       List/*<E>*/ result = /*<E>*/ []; | 
|  | 349       for (int i = 0; i < json.length; i++) { | 
|  | 350         result.add(decoder('$jsonPath[$i]', json[i])); | 
|  | 351       } | 
|  | 352       return result; | 
|  | 353     } else { | 
|  | 354       throw mismatch(jsonPath, 'List', json); | 
|  | 355     } | 
|  | 356   } | 
|  | 357 | 
|  | 358   /** | 
|  | 359    * Decode a JSON object that is expected to be a Map.  [keyDecoder] is used | 
|  | 360    * to decode the keys, and [valueDecoder] is used to decode the values. | 
|  | 361    */ | 
|  | 362   Map/*<K, V>*/ decodeMap/*<K, V>*/(String jsonPath, Object json, | 
|  | 363       {JsonDecoderCallback/*<K>*/ keyDecoder, | 
|  | 364       JsonDecoderCallback/*<V>*/ valueDecoder}) { | 
|  | 365     if (json == null) { | 
|  | 366       return {}; | 
|  | 367     } else if (json is Map) { | 
|  | 368       Map/*<K, V>*/ result = /*<K, V>*/ {}; | 
|  | 369       json.forEach((String key, value) { | 
|  | 370         Object/*=K*/ decodedKey; | 
|  | 371         if (keyDecoder != null) { | 
|  | 372           decodedKey = keyDecoder('$jsonPath.key', key); | 
|  | 373         } else { | 
|  | 374           decodedKey = key as Object/*=K*/; | 
|  | 375         } | 
|  | 376         if (valueDecoder != null) { | 
|  | 377           value = valueDecoder('$jsonPath[${JSON.encode(key)}]', value); | 
|  | 378         } | 
|  | 379         result[decodedKey] = value as Object/*=V*/; | 
|  | 380       }); | 
|  | 381       return result; | 
|  | 382     } else { | 
|  | 383       throw mismatch(jsonPath, 'Map', json); | 
|  | 384     } | 
|  | 385   } | 
|  | 386 | 
|  | 387   /** | 
|  | 388    * Decode a JSON object that is expected to be a string. | 
|  | 389    */ | 
|  | 390   String decodeString(String jsonPath, Object json) { | 
|  | 391     if (json is String) { | 
|  | 392       return json; | 
|  | 393     } else { | 
|  | 394       throw mismatch(jsonPath, 'String', json); | 
|  | 395     } | 
|  | 396   } | 
|  | 397 | 
|  | 398   /** | 
|  | 399    * Decode a JSON object that is expected to be one of several choices, | 
|  | 400    * where the choices are disambiguated by the contents of the field [field]. | 
|  | 401    * [decoders] is a map from each possible string in the field to the decoder | 
|  | 402    * that should be used to decode the JSON object. | 
|  | 403    */ | 
|  | 404   Object decodeUnion(String jsonPath, Map json, String field, | 
|  | 405       Map<String, JsonDecoderCallback> decoders) { | 
|  | 406     if (json is Map) { | 
|  | 407       if (!json.containsKey(field)) { | 
|  | 408         throw missingKey(jsonPath, field); | 
|  | 409       } | 
|  | 410       var disambiguatorPath = '$jsonPath[${JSON.encode(field)}]'; | 
|  | 411       String disambiguator = decodeString(disambiguatorPath, json[field]); | 
|  | 412       if (!decoders.containsKey(disambiguator)) { | 
|  | 413         throw mismatch( | 
|  | 414             disambiguatorPath, 'One of: ${decoders.keys.toList()}', json); | 
|  | 415       } | 
|  | 416       return decoders[disambiguator](jsonPath, json); | 
|  | 417     } else { | 
|  | 418       throw mismatch(jsonPath, 'Map', json); | 
|  | 419     } | 
|  | 420   } | 
|  | 421 | 
|  | 422   /** | 
|  | 423    * Create an exception to throw if the JSON object at [jsonPath] fails to | 
|  | 424    * match the API definition of [expected]. | 
|  | 425    */ | 
|  | 426   dynamic mismatch(String jsonPath, String expected, [Object actual]); | 
|  | 427 | 
|  | 428   /** | 
|  | 429    * Create an exception to throw if the JSON object at [jsonPath] is missing | 
|  | 430    * the key [key]. | 
|  | 431    */ | 
|  | 432   dynamic missingKey(String jsonPath, String key); | 
|  | 433 } | 
|  | 434 | 
|  | 435 /** | 
|  | 436  * JsonDecoder for decoding requests.  Errors are reporting by throwing a | 
|  | 437  * [RequestFailure]. | 
|  | 438  */ | 
|  | 439 class RequestDecoder extends JsonDecoder { | 
|  | 440   /** | 
|  | 441    * The request being deserialized. | 
|  | 442    */ | 
|  | 443   final Request _request; | 
|  | 444 | 
|  | 445   RequestDecoder(this._request); | 
|  | 446 | 
|  | 447   @override | 
|  | 448   RefactoringKind get refactoringKind { | 
|  | 449     // Refactoring feedback objects should never appear in requests. | 
|  | 450     return null; | 
|  | 451   } | 
|  | 452 | 
|  | 453   @override | 
|  | 454   dynamic mismatch(String jsonPath, String expected, [Object actual]) { | 
|  | 455     StringBuffer buffer = new StringBuffer(); | 
|  | 456     buffer.write('Expected to be '); | 
|  | 457     buffer.write(expected); | 
|  | 458     if (actual != null) { | 
|  | 459       buffer.write('; found "'); | 
|  | 460       buffer.write(JSON.encode(actual)); | 
|  | 461       buffer.write('"'); | 
|  | 462     } | 
|  | 463     return new RequestFailure(RequestErrorFactory.invalidParameter( | 
|  | 464         _request, jsonPath, buffer.toString())); | 
|  | 465   } | 
|  | 466 | 
|  | 467   @override | 
|  | 468   dynamic missingKey(String jsonPath, String key) { | 
|  | 469     return new RequestFailure(RequestErrorFactory.invalidParameter( | 
|  | 470         _request, jsonPath, 'Expected to contain key ${JSON.encode(key)}')); | 
|  | 471   } | 
|  | 472 } | 
|  | 473 | 
|  | 474 /** | 
|  | 475  * JsonDecoder for decoding responses from the server.  This is intended to be | 
|  | 476  * used only for testing.  Errors are reported using bare [Exception] objects. | 
|  | 477  */ | 
|  | 478 class ResponseDecoder extends JsonDecoder { | 
|  | 479   @override | 
|  | 480   final RefactoringKind refactoringKind; | 
|  | 481 | 
|  | 482   ResponseDecoder(this.refactoringKind); | 
|  | 483 | 
|  | 484   @override | 
|  | 485   dynamic mismatch(String jsonPath, String expected, [Object actual]) { | 
|  | 486     StringBuffer buffer = new StringBuffer(); | 
|  | 487     buffer.write('Expected '); | 
|  | 488     buffer.write(expected); | 
|  | 489     if (actual != null) { | 
|  | 490       buffer.write(' found "'); | 
|  | 491       buffer.write(JSON.encode(actual)); | 
|  | 492       buffer.write('"'); | 
|  | 493     } | 
|  | 494     buffer.write(' at '); | 
|  | 495     buffer.write(jsonPath); | 
|  | 496     return new Exception(buffer.toString()); | 
|  | 497   } | 
|  | 498 | 
|  | 499   @override | 
|  | 500   dynamic missingKey(String jsonPath, String key) { | 
|  | 501     return new Exception('Missing key $key at $jsonPath'); | 
|  | 502   } | 
|  | 503 } | 
|  | 504 | 
|  | 505 /** | 
|  | 506  * The result data associated with a response. | 
|  | 507  */ | 
|  | 508 abstract class ResponseResult implements HasToJson { | 
|  | 509   /** | 
|  | 510    * Return a response whose result data is this object for the request with the | 
|  | 511    * given [id]. | 
|  | 512    */ | 
|  | 513   Response toResponse(String id); | 
|  | 514 } | 
| OLD | NEW | 
|---|