|
1 | 1 | import copy
|
| 2 | +import json |
2 | 3 | import logging
|
3 | 4 | import os
|
4 | 5 | import re
|
5 | 6 | from datetime import datetime
|
| 7 | +from json import JSONDecodeError |
6 | 8 | from re import Pattern
|
7 | 9 | from typing import Any, Callable, Optional, Protocol
|
8 | 10 |
|
@@ -375,3 +377,78 @@ def replace_val(s):
|
375 | 377 | f"Registering text pattern '{self.text}' in snapshot with '{self.replacement}'"
|
376 | 378 | )
|
377 | 379 | return input_data
|
| 380 | + |
| 381 | + |
| 382 | +class JsonStringTransformer: |
| 383 | + """ |
| 384 | + Parses JSON string at the specified key. |
| 385 | + Additionally, attempts to parse any JSON strings inside the parsed JSON |
| 386 | +
|
| 387 | + This transformer complements the default parsing of JSON strings in |
| 388 | + localstack_snapshot.snapshots.prototype.SnapshotSession._transform_dict_to_parseable_values |
| 389 | +
|
| 390 | + Shortcomings of the default parser that this transformer addresses: |
| 391 | + - parsing of nested JSON strings '{"a": "{\\"b\\":42}"}' |
| 392 | + - parsing of JSON arrays at the specified key, e.g. '["a", "b"]' |
| 393 | +
|
| 394 | + Such parsing allows applying transformations further to the elements of the parsed JSON - timestamps, ARNs, etc. |
| 395 | +
|
| 396 | + Such parsing is not done by default because it's not a common use case. |
| 397 | + Whether to parse a JSON string or not should be decided by the user on a case by case basis. |
| 398 | + Limited general parsing that we already have is preserved for backwards compatibility. |
| 399 | + """ |
| 400 | + |
| 401 | + key: str |
| 402 | + |
| 403 | + def __init__(self, key: str): |
| 404 | + self.key = key |
| 405 | + |
| 406 | + def transform(self, input_data: dict, *, ctx: TransformContext = None) -> dict: |
| 407 | + return self._transform_dict(input_data, ctx=ctx) |
| 408 | + |
| 409 | + def _transform(self, input_data: Any, ctx: TransformContext = None) -> Any: |
| 410 | + if isinstance(input_data, dict): |
| 411 | + return self._transform_dict(input_data, ctx=ctx) |
| 412 | + elif isinstance(input_data, list): |
| 413 | + return self._transform_list(input_data, ctx=ctx) |
| 414 | + return input_data |
| 415 | + |
| 416 | + def _transform_dict(self, input_data: dict, ctx: TransformContext = None) -> dict: |
| 417 | + for k, v in input_data.items(): |
| 418 | + if k == self.key and isinstance(v, str) and v.strip().startswith(("{", "[")): |
| 419 | + try: |
| 420 | + SNAPSHOT_LOGGER.debug(f"Replacing string value of {k} with parsed JSON") |
| 421 | + json_value = json.loads(v) |
| 422 | + input_data[k] = self._transform_nested(json_value) |
| 423 | + except JSONDecodeError: |
| 424 | + SNAPSHOT_LOGGER.exception( |
| 425 | + f'Value mapped to "{k}" key is not a valid JSON string and won\'t be transformed. Value: {v}' |
| 426 | + ) |
| 427 | + else: |
| 428 | + input_data[k] = self._transform(v, ctx=ctx) |
| 429 | + return input_data |
| 430 | + |
| 431 | + def _transform_list(self, input_data: list, ctx: TransformContext = None) -> list: |
| 432 | + return [self._transform(item, ctx=ctx) for item in input_data] |
| 433 | + |
| 434 | + def _transform_nested(self, input_data: Any) -> Any: |
| 435 | + """ |
| 436 | + Separate method from the main `_transform_dict` one because |
| 437 | + it checks every string while the main one attempts to load at specified key only. |
| 438 | + This one is implicit, best-effort attempt, |
| 439 | + while the main one is explicit about at which key transform should happen |
| 440 | + """ |
| 441 | + if isinstance(input_data, list): |
| 442 | + input_data = [self._transform_nested(item) for item in input_data] |
| 443 | + if isinstance(input_data, dict): |
| 444 | + for k, v in input_data.items(): |
| 445 | + input_data[k] = self._transform_nested(v) |
| 446 | + if isinstance(input_data, str) and input_data.strip().startswith(("{", "[")): |
| 447 | + try: |
| 448 | + json_value = json.loads(input_data) |
| 449 | + input_data = self._transform_nested(json_value) |
| 450 | + except JSONDecodeError: |
| 451 | + SNAPSHOT_LOGGER.debug( |
| 452 | + f"The value is not a valid JSON string and won't be transformed. The value: {input_data}" |
| 453 | + ) |
| 454 | + return input_data |
0 commit comments