From 85bd2d7ed9f36c9b22583aab9007ccfe4f80f8b5 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 30 Aug 2022 20:28:01 +0100
Subject: [PATCH] Implement custom JSONEncoder and JSONDecoder

---
 Sources/XXClient/Helpers/JSONDecoder.swift    | 64 ++++++++++++++
 Sources/XXClient/Helpers/JSONEncoder.swift    | 52 ++++++++++++
 .../Helpers/JSONDecoderTests.swift            | 84 +++++++++++++++++++
 .../Helpers/JSONEncoderTests.swift            | 84 +++++++++++++++++++
 4 files changed, 284 insertions(+)
 create mode 100644 Sources/XXClient/Helpers/JSONDecoder.swift
 create mode 100644 Sources/XXClient/Helpers/JSONEncoder.swift
 create mode 100644 Tests/XXClientTests/Helpers/JSONDecoderTests.swift
 create mode 100644 Tests/XXClientTests/Helpers/JSONEncoderTests.swift

diff --git a/Sources/XXClient/Helpers/JSONDecoder.swift b/Sources/XXClient/Helpers/JSONDecoder.swift
new file mode 100644
index 00000000..fc92d05f
--- /dev/null
+++ b/Sources/XXClient/Helpers/JSONDecoder.swift
@@ -0,0 +1,64 @@
+import CustomDump
+import Foundation
+
+public class JSONDecoder: Foundation.JSONDecoder {
+  public override init() {
+    super.init()
+  }
+
+  public override func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
+    do {
+      let data = convertNumberToString(in: data, at: "Value")
+      return try super.decode(type, from: data)
+    } catch {
+      throw JSONDecodingError(error, data: data)
+    }
+  }
+
+  func convertNumberToString(
+    in input: Data,
+    at key: String
+  ) -> Data {
+    guard var string = String(data: input, encoding: .utf8) else {
+      return input
+    }
+    string = string.replacingOccurrences(
+      of: #""\#(key)"( *):( *)([0-9]+)( *)(,*)"#,
+      with: #""\#(key)"$1:$2"$3"$4$5"#,
+      options: [.regularExpression]
+    )
+    guard let output = string.data(using: .utf8) else {
+      return input
+    }
+    return output
+  }
+}
+
+public struct JSONDecodingError: Error, CustomStringConvertible, CustomDumpReflectable {
+  public init(_ underlayingError: Error, data: Data) {
+    self.underlayingError = underlayingError
+    self.data = data
+    self.string = String(data: data, encoding: .utf8)
+  }
+
+  public var underlayingError: Error
+  public var data: Data
+  public var string: String?
+
+  public var description: String {
+    var description = ""
+    customDump(self, to: &description)
+    return description
+  }
+
+  public var customDumpMirror: Mirror {
+    Mirror(
+      self,
+      children: [
+        "underlayingError": underlayingError,
+        "data": String(data: data, encoding: .utf8) ?? data
+      ],
+      displayStyle: .struct
+    )
+  }
+}
diff --git a/Sources/XXClient/Helpers/JSONEncoder.swift b/Sources/XXClient/Helpers/JSONEncoder.swift
new file mode 100644
index 00000000..ba9c2a31
--- /dev/null
+++ b/Sources/XXClient/Helpers/JSONEncoder.swift
@@ -0,0 +1,52 @@
+import CustomDump
+import Foundation
+
+public class JSONEncoder: Foundation.JSONEncoder {
+  public override init() {
+    super.init()
+  }
+
+  public override func encode<T>(_ value: T) throws -> Data where T: Encodable {
+    do {
+      var data = try super.encode(value)
+      data = convertStringToNumber(in: data, at: "Value")
+      return data
+    } catch {
+      throw JSONEncodingError(error, value: value)
+    }
+  }
+
+  func convertStringToNumber(
+    in input: Data,
+    at key: String
+  ) -> Data {
+    guard var string = String(data: input, encoding: .utf8) else {
+      return input
+    }
+    string = string.replacingOccurrences(
+      of: #""\#(key)"( *):( *)"([0-9]+)"( *)(,*)"#,
+      with: #""\#(key)"$1:$2$3$4$5"#,
+      options: [.regularExpression]
+    )
+    guard let output = string.data(using: .utf8) else {
+      return input
+    }
+    return output
+  }
+}
+
+public struct JSONEncodingError: Error, CustomStringConvertible {
+  public init(_ underlayingError: Error, value: Any) {
+    self.underlayingError = underlayingError
+    self.value = value
+  }
+
+  public var underlayingError: Error
+  public var value: Any
+
+  public var description: String {
+    var description = ""
+    customDump(self, to: &description)
+    return description
+  }
+}
diff --git a/Tests/XXClientTests/Helpers/JSONDecoderTests.swift b/Tests/XXClientTests/Helpers/JSONDecoderTests.swift
new file mode 100644
index 00000000..16f2afcb
--- /dev/null
+++ b/Tests/XXClientTests/Helpers/JSONDecoderTests.swift
@@ -0,0 +1,84 @@
+import CustomDump
+import XCTest
+@testable import XXClient
+
+final class JSONDecoderTests: XCTestCase {
+  func testConvertingNumberToString() {
+    assertConvertingNumberToString(
+      input: #"{"number":1234567890,"text":"hello"}"#,
+      key: "number",
+      expectedOutput: #"{"number":"1234567890","text":"hello"}"#
+    )
+
+    assertConvertingNumberToString(
+      input: #"{"text":"hello","number":1234567890}"#,
+      key: "number",
+      expectedOutput: #"{"text":"hello","number":"1234567890"}"#
+    )
+
+    assertConvertingNumberToString(
+      input: #"{  "number"  :  1234567890  ,  "text"  :  "hello"  }"#,
+      key: "number",
+      expectedOutput: #"{  "number"  :  "1234567890"  ,  "text"  :  "hello"  }"#
+    )
+
+    assertConvertingNumberToString(
+      input: #"{  "text"  :  "hello"  ,  "number"  :  1234567890  }"#,
+      key: "number",
+      expectedOutput: #"{  "text"  :  "hello"  ,  "number"  :  "1234567890"  }"#
+    )
+
+    assertConvertingNumberToString(
+      input: """
+      {
+        "number": 1234567890,
+        "text": "hello"
+      }
+      """,
+      key: "number",
+      expectedOutput: """
+      {
+        "number": "1234567890",
+        "text": "hello"
+      }
+      """
+    )
+
+    assertConvertingNumberToString(
+      input: """
+      {
+        "text": "hello",
+        "number": 1234567890
+      }
+      """,
+      key: "number",
+      expectedOutput: """
+      {
+        "text": "hello",
+        "number": "1234567890"
+      }
+      """
+    )
+  }
+}
+
+private func assertConvertingNumberToString(
+  input: String,
+  key: String,
+  expectedOutput: String,
+  file: StaticString = #file,
+  line: UInt = #line
+) {
+  XCTAssertNoDifference(
+    String(
+      data: JSONDecoder().convertNumberToString(
+        in: input.data(using: .utf8)!,
+        at: key
+      ),
+      encoding: .utf8
+    )!,
+    expectedOutput,
+    file: file,
+    line: line
+  )
+}
diff --git a/Tests/XXClientTests/Helpers/JSONEncoderTests.swift b/Tests/XXClientTests/Helpers/JSONEncoderTests.swift
new file mode 100644
index 00000000..58fbfae7
--- /dev/null
+++ b/Tests/XXClientTests/Helpers/JSONEncoderTests.swift
@@ -0,0 +1,84 @@
+import CustomDump
+import XCTest
+@testable import XXClient
+
+final class JSONEncoderTests: XCTestCase {
+  func testConvertingStringToNumber() {
+    assertConvertingStringToNumber(
+      input: #"{"number":"1234567890","text":"hello"}"#,
+      key: "number",
+      expectedOutput: #"{"number":1234567890,"text":"hello"}"#
+    )
+
+    assertConvertingStringToNumber(
+      input: #"{"text":"hello","number":"1234567890"}"#,
+      key: "number",
+      expectedOutput: #"{"text":"hello","number":1234567890}"#
+    )
+
+    assertConvertingStringToNumber(
+      input: #"{  "number"  :  "1234567890"  ,  "text"  :  "hello"  }"#,
+      key: "number",
+      expectedOutput: #"{  "number"  :  1234567890  ,  "text"  :  "hello"  }"#
+    )
+
+    assertConvertingStringToNumber(
+      input: #"{  "text"  :  "hello"  ,  "number"  :  "1234567890"  }"#,
+      key: "number",
+      expectedOutput: #"{  "text"  :  "hello"  ,  "number"  :  1234567890  }"#
+    )
+
+    assertConvertingStringToNumber(
+      input: """
+      {
+        "number": "1234567890",
+        "text": "hello"
+      }
+      """,
+      key: "number",
+      expectedOutput: """
+      {
+        "number": 1234567890,
+        "text": "hello"
+      }
+      """
+    )
+
+    assertConvertingStringToNumber(
+      input: """
+      {
+        "text": "hello",
+        "number": "1234567890"
+      }
+      """,
+      key: "number",
+      expectedOutput: """
+      {
+        "text": "hello",
+        "number": 1234567890
+      }
+      """
+    )
+  }
+}
+
+private func assertConvertingStringToNumber(
+  input: String,
+  key: String,
+  expectedOutput: String,
+  file: StaticString = #file,
+  line: UInt = #line
+) {
+  XCTAssertNoDifference(
+    String(
+      data: JSONEncoder().convertStringToNumber(
+        in: input.data(using: .utf8)!,
+        at: key
+      ),
+      encoding: .utf8
+    )!,
+    expectedOutput,
+    file: file,
+    line: line
+  )
+}
-- 
GitLab