diff --git a/Clue.xcodeproj/project.pbxproj b/Clue.xcodeproj/project.pbxproj index 20f028b..8f9b4ba 100644 --- a/Clue.xcodeproj/project.pbxproj +++ b/Clue.xcodeproj/project.pbxproj @@ -7,10 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + AF15549B1EB568F1005D1046 /* DataWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF15549A1EB568F1005D1046 /* DataWriter.swift */; }; AF5314621EB018E8005A5146 /* JSONWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF53145F1EB018E8005A5146 /* JSONWriter.swift */; }; - AF5314631EB018E8005A5146 /* JSONWriterError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5314601EB018E8005A5146 /* JSONWriterError.swift */; }; - AF5314671EB01931005A5146 /* JSONWriterErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5314651EB01931005A5146 /* JSONWriterErrorTests.swift */; }; + AF5314631EB018E8005A5146 /* DataWriterError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5314601EB018E8005A5146 /* DataWriterError.swift */; }; + AF5314671EB01931005A5146 /* DataWriterErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5314651EB01931005A5146 /* DataWriterErrorTests.swift */; }; AF5314681EB01931005A5146 /* JSONWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5314661EB01931005A5146 /* JSONWriterTests.swift */; }; + AF67568A1EB674380057CA08 /* DataWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF6756891EB674380057CA08 /* DataWriterTests.swift */; }; AF79EA561EAFCD42002231B9 /* Clue.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E272DD821CD5223300F1FECA /* Clue.framework */; }; AF79EA571EAFCD42002231B9 /* Clue.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E272DD821CD5223300F1FECA /* Clue.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E20F23401D30F94700654690 /* CLUInfoModule.h in Headers */ = {isa = PBXBuildFile; fileRef = E20F233F1D30F94700654690 /* CLUInfoModule.h */; }; @@ -201,10 +203,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + AF15549A1EB568F1005D1046 /* DataWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataWriter.swift; sourceTree = ""; }; AF53145F1EB018E8005A5146 /* JSONWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONWriter.swift; sourceTree = ""; }; - AF5314601EB018E8005A5146 /* JSONWriterError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONWriterError.swift; sourceTree = ""; }; - AF5314651EB01931005A5146 /* JSONWriterErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONWriterErrorTests.swift; sourceTree = ""; }; + AF5314601EB018E8005A5146 /* DataWriterError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataWriterError.swift; sourceTree = ""; }; + AF5314651EB01931005A5146 /* DataWriterErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataWriterErrorTests.swift; sourceTree = ""; }; AF5314661EB01931005A5146 /* JSONWriterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONWriterTests.swift; sourceTree = ""; }; + AF6756891EB674380057CA08 /* DataWriterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataWriterTests.swift; sourceTree = ""; }; E20F233F1D30F94700654690 /* CLUInfoModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CLUInfoModule.h; sourceTree = ""; }; E20F23411D30FAB100654690 /* CLUDeviceInfoModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CLUDeviceInfoModule.h; sourceTree = ""; }; E20F23421D30FAB100654690 /* CLUDeviceInfoModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CLUDeviceInfoModule.m; sourceTree = ""; }; @@ -676,7 +680,8 @@ E2A30C8F1CED0BF800A48C61 /* CLUVideoWriter.h */, E2A30C901CED0BF800A48C61 /* CLUVideoWriter.m */, AF53145F1EB018E8005A5146 /* JSONWriter.swift */, - AF5314601EB018E8005A5146 /* JSONWriterError.swift */, + AF15549A1EB568F1005D1046 /* DataWriter.swift */, + AF5314601EB018E8005A5146 /* DataWriterError.swift */, ); path = Writers; sourceTree = ""; @@ -697,7 +702,8 @@ isa = PBXGroup; children = ( E2AD27951E729B200062FE3D /* CLUVideoWriterTests.m */, - AF5314651EB01931005A5146 /* JSONWriterErrorTests.swift */, + AF6756891EB674380057CA08 /* DataWriterTests.swift */, + AF5314651EB01931005A5146 /* DataWriterErrorTests.swift */, AF5314661EB01931005A5146 /* JSONWriterTests.swift */, ); path = Writers; @@ -1044,7 +1050,7 @@ E2CDA6A81D3905D60079F784 /* sha1.c in Sources */, AF5314621EB018E8005A5146 /* JSONWriter.swift in Sources */, E232BA931CF830DC00F5DB52 /* UILabel+CLUViewRecordableAdditions.m in Sources */, - AF5314631EB018E8005A5146 /* JSONWriterError.swift in Sources */, + AF5314631EB018E8005A5146 /* DataWriterError.swift in Sources */, E29CE2AA1CF7994C00C27384 /* CLUViewStructureModule.m in Sources */, E2CDA6981D3905D60079F784 /* aeskey.c in Sources */, E2CDA6B31D3905D60079F784 /* zip.c in Sources */, @@ -1067,6 +1073,7 @@ E2ABF6481D424531006646B3 /* CLURecordIndicatorView.m in Sources */, E232BA8F1CF821D400F5DB52 /* UIView+CLUViewRecordableAdditions.m in Sources */, E23891401D1BDC3C00D688CD /* NSURLResponse+CLUNetworkAdditions.m in Sources */, + AF15549B1EB568F1005D1046 /* DataWriter.swift in Sources */, E28CC0961CEE619000C9DFD9 /* ClueController.m in Sources */, E2ABF64C1D425108006646B3 /* CLURecordIndicatorViewManager.m in Sources */, E21840D51CFCD7F80053422C /* UIImageView+CLUViewRecordableAdditions.m in Sources */, @@ -1087,6 +1094,7 @@ files = ( E2CEADBA1E8FFE8700CEF6AE /* CLUReportFileManagerTests.m in Sources */, E241F46B1E7BDE000075EF2C /* CLUExceptionAdditions.m in Sources */, + AF67568A1EB674380057CA08 /* DataWriterTests.swift in Sources */, E241F4671E7B39DE0075EF2C /* CLUErrorNetworkAdditions.m in Sources */, E24E2D711E74C1AF00804E13 /* CLUUIViewAdditionsTests.m in Sources */, E241F4631E7B333A0075EF2C /* CLUTextFieldViewRecordableAdditions.m in Sources */, @@ -1094,7 +1102,7 @@ E24E6CD01EAA13C100A2F0C1 /* CLUNSMutableDictionaryUtilsAdditionsTests.m in Sources */, E241F4691E7B3D9A0075EF2C /* CLUCommunicationNetworkAdditions.m in Sources */, E241F4651E7B37770075EF2C /* CLUTouchUserInteractionAdditions.m in Sources */, - AF5314671EB01931005A5146 /* JSONWriterErrorTests.swift in Sources */, + AF5314671EB01931005A5146 /* DataWriterErrorTests.swift in Sources */, E2C604991E80782900A0918D /* CLUTouchTests.m in Sources */, E24E2D6E1E74899600804E13 /* CLUGeneralGestureRecognizerTests.m in Sources */, E241F46E1E7BE12D0075EF2C /* CLUObserveModuleTests.m in Sources */, diff --git a/Clue/Classes/Protocols/CLUInfoModule.h b/Clue/Classes/Protocols/CLUInfoModule.h index 033fdff..dfa11b7 100644 --- a/Clue/Classes/Protocols/CLUInfoModule.h +++ b/Clue/Classes/Protocols/CLUInfoModule.h @@ -7,8 +7,7 @@ // #import - -@class JSONWriter; +#import "CLUWritable.h" /** `CLUInfoModule` protocol describe info modules (like Device Info module or Exception module), static one-time modules which needs to write their data only once during recording. @@ -20,12 +19,12 @@ @required /** - Initialize info module with specific `JSONWriter` instance. So module will be able to record/write required information. + Initialize info module with specific writer which implements `CLUWritable` protocol. So module will be able to record/write required information. - @param writer `JSONWriter` instance responsible for actual writing information to some specific file + @param writer Writer object which implements `CLUWritable` protocol. Responsible for actual writing information to some specific file @return New instance of info module */ -- (instancetype)initWithWriter:(JSONWriter *)writer; +- (instancetype)initWithWriter:(id )writer; /** Record actual information once and cleanup everything diff --git a/Clue/Classes/Writers/DataWriter.swift b/Clue/Classes/Writers/DataWriter.swift new file mode 100644 index 0000000..8a6b7f3 --- /dev/null +++ b/Clue/Classes/Writers/DataWriter.swift @@ -0,0 +1,101 @@ +// +// DataWriter.swift +// Clue +// +// Created by Andrea Prearo on 4/29/17. +// Copyright © 2017 Ahmed Sulaiman. All rights reserved. +// + +import Foundation + +/// The `DataWriter` class encapsulates the details of writing data to a stream. +public class DataWriter: NSObject { + let outputStream: OutputStream + var currentError: DataWriterError? + + /// The current error. + public var error: DataWriterError? { + if let currentError = currentError { + return currentError + } + guard let streamError = outputStream.streamError else { + return nil + } + return DataWriterError.error(streamError) + } + + /// Initializes an output stream. + /// + /// - Parameter outputURL: The URL for the output stream. + /// - Returns: An initialized output stream for writing to a specified URL. + public init?(outputURL: URL) { + guard let outputStream = OutputStream(url: outputURL, append: true) else { + return nil + } + self.outputStream = outputStream + super.init() + self.outputStream.delegate = self + } + + deinit { + finishWriting() + } + + /// Appends data to the stream. + /// + /// - Parameter data: The data to be written. + /// - Returns: The number of bytes that were written. In case the return value + /// is zero, the `error` property will contain the current error. + @discardableResult + public func append(data: Data) -> Int { + if !isReadyForWriting() { + startWriting() + } + let bytes = data.withUnsafeBytes { outputStream.write($0, maxLength: data.count) } + guard bytes > 0 else { + handleStreamError() + return bytes + } + + return bytes + } +} + +// MARK: - DataWriter + StreamDelegate +extension DataWriter: StreamDelegate { + public func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + switch eventCode { + case Stream.Event.errorOccurred: + handleStreamError() + default: + return + } + } +} + +// MARK: - DataWriter + CLUWritable +extension DataWriter: CLUWritable { + public func isReadyForWriting() -> Bool { + return outputStream.streamStatus == .open + } + + public func startWriting() { + if outputStream.streamStatus != .open { + outputStream.open() + } + } + + public func finishWriting() { + if outputStream.streamStatus != .closed { + outputStream.close() + } + } +} + +// MARK: - Internal Methods +extension DataWriter { + func handleStreamError() { + let error = outputStream.streamError ?? currentError + print("Stream error: \(String(describing: error?.localizedDescription))") + } +} diff --git a/Clue/Classes/Writers/DataWriterError.swift b/Clue/Classes/Writers/DataWriterError.swift new file mode 100644 index 0000000..8f1ce1f --- /dev/null +++ b/Clue/Classes/Writers/DataWriterError.swift @@ -0,0 +1,66 @@ +// +// DataWriterError.swift +// Clue +// +// Created by Andrea Prearo on 4/25/17. +// Copyright © 2017 Ahmed Sulaiman. All rights reserved. +// + +import Foundation + +/// An error that can be returned from a `DataWriter` instance. +/// +/// - error: Internal error. +/// - failure: Internal failure. +/// - invalidData: Invalid data. +/// - invalidJSON: Invalid JSON content. +/// - unknown: Unknown error. +public enum DataWriterError: Error { + case error(Error) + case failure(NSError) + case invalidData(Data) + case invalidJSON(Any) + case unknown +} + +extension DataWriterError: Equatable {} + +public func == (lhs: DataWriterError, rhs: DataWriterError) -> Bool { + switch lhs { + case .error(let error): + switch rhs { + case .error(let error2): + return String(describing: error) == String(describing: error2) + default: + return false + } + case .failure(let error): + switch rhs { + case .failure(let error2): + return error == error2 + default: + return false + } + case .invalidData(let data): + switch rhs { + case .invalidData(let data2): + return data == data2 + default: + return false + } + case .invalidJSON(let json): + switch rhs { + case .invalidJSON(let json2): + return String(describing: json) == String(describing: json2) + default: + return false + } + case .unknown: + switch rhs { + case .unknown: + return true + default: + return false + } + } +} diff --git a/Clue/Classes/Writers/JSONWriter.swift b/Clue/Classes/Writers/JSONWriter.swift index 4944cfe..c7cdd81 100644 --- a/Clue/Classes/Writers/JSONWriter.swift +++ b/Clue/Classes/Writers/JSONWriter.swift @@ -8,31 +8,17 @@ import Foundation -public class JSONWriter: NSObject { - fileprivate let outputStream: OutputStream - fileprivate var currentError: JSONWriterError? - - public var error: JSONWriterError? { - return currentError - } - - public init?(outputURL: URL) { - guard let outputStream = OutputStream(url: outputURL, append: true) else { - return nil - } - self.outputStream = outputStream - super.init() - self.outputStream.delegate = self - } - - deinit { - finishWriting() - } - +/// The `JSONWriter` class encapsulates the details of writing JSON data to a stream. +public class JSONWriter: DataWriter { + /// Appends JSON content. + /// + /// - Parameter json: The JSON content to append. + /// - Returns: The number of bytes that were appended. In case the return value + /// is zero, the `error` property will contain the current error. @discardableResult public func append(json: Any) -> Int { guard JSONSerialization.isValidJSONObject(json) else { - currentError = JSONWriterError.invalidObject(json) + currentError = DataWriterError.invalidJSON(json) handleStreamError() return 0 } @@ -43,9 +29,9 @@ public class JSONWriter: NSObject { let bytes = JSONSerialization.writeJSONObject(json, to: outputStream, options: [], error: &error) guard bytes > 0 else { if let error = error { - currentError = JSONWriterError.failure(error) + currentError = DataWriterError.failure(error) } else { - currentError = JSONWriterError.unknown + currentError = DataWriterError.unknown } handleStreamError() return bytes @@ -53,13 +39,13 @@ public class JSONWriter: NSObject { let lineSeparator = "\n" guard let stringData = lineSeparator.data(using: .utf8) else { - currentError = JSONWriterError.invalidObject(lineSeparator) + currentError = DataWriterError.invalidJSON(lineSeparator) handleStreamError() return bytes } - let lineSeparatorBytes = stringData.withUnsafeBytes { outputStream.write($0, maxLength: stringData.count) } + let lineSeparatorBytes = append(data: stringData) if lineSeparatorBytes != 1 { - currentError = JSONWriterError.unknown + currentError = DataWriterError.unknown handleStreamError() return bytes } @@ -67,42 +53,3 @@ public class JSONWriter: NSObject { return bytes + lineSeparatorBytes } } - -// MARK: - JSONWriter + StreamDelegate -extension JSONWriter: StreamDelegate { - public func stream(_ aStream: Stream, handle eventCode: Stream.Event) { - switch eventCode { - case Stream.Event.errorOccurred: - handleStreamError() - default: - return - } - } -} - -// MARK: - JSONWriter + CLUWritable -extension JSONWriter: CLUWritable { - public func isReadyForWriting() -> Bool { - return outputStream.streamStatus == .open - } - - public func startWriting() { - if outputStream.streamStatus != .open { - outputStream.open() - } - } - - public func finishWriting() { - if outputStream.streamStatus != .closed { - outputStream.close() - } - } -} - -// MARK: - Private Methods -fileprivate extension JSONWriter { - func handleStreamError() { - let error = outputStream.streamError ?? currentError - print("Stream error: \(String(describing: error?.localizedDescription))") - } -} diff --git a/Clue/Classes/Writers/JSONWriterError.swift b/Clue/Classes/Writers/JSONWriterError.swift deleted file mode 100644 index 50991ac..0000000 --- a/Clue/Classes/Writers/JSONWriterError.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// JSONWriterError.swift -// Clue -// -// Created by Andrea Prearo on 4/25/17. -// Copyright © 2017 Ahmed Sulaiman. All rights reserved. -// - -import Foundation - -public enum JSONWriterError: Error { - case unknown - case invalidObject(Any) - case failure(NSError) -} - -extension JSONWriterError: Equatable {} - -public func == (lhs: JSONWriterError, rhs: JSONWriterError) -> Bool { - switch lhs { - case .unknown: - switch rhs { - case .unknown: - return true - default: - return false - } - case .invalidObject(let object): - switch rhs { - case .invalidObject(let object2): - return String(describing: object) == String(describing: object2) - default: - return false - } - case .failure(let error): - switch rhs { - case .failure(let error2): - return error == error2 - default: - return false - } - } -} diff --git a/ClueTests/Classes/Writers/JSONWriterErrorTests.swift b/ClueTests/Classes/Writers/DataWriterErrorTests.swift similarity index 72% rename from ClueTests/Classes/Writers/JSONWriterErrorTests.swift rename to ClueTests/Classes/Writers/DataWriterErrorTests.swift index b58cd61..a00e93b 100644 --- a/ClueTests/Classes/Writers/JSONWriterErrorTests.swift +++ b/ClueTests/Classes/Writers/DataWriterErrorTests.swift @@ -1,5 +1,5 @@ // -// JSONWriterErrorTests.swift +// DataWriterErrorTests.swift // Clue // // Created by Andrea Prearo on 4/25/17. @@ -11,15 +11,15 @@ import Foundation import XCTest @testable import Clue -class JSONWriterErrorTests: XCTestCase { +class DataWriterErrorTests: XCTestCase { func testSameInvalidObject() { let json = "This is not a valid JSON" - XCTAssertTrue(JSONWriterError.invalidObject(json) == JSONWriterError.invalidObject("This is not a valid JSON")) + XCTAssertTrue(DataWriterError.invalidJSON(json) == DataWriterError.invalidJSON("This is not a valid JSON")) } func testDifferentInvalidObject() { let json = "This is not a valid JSON" - XCTAssertFalse(JSONWriterError.invalidObject(json) == JSONWriterError.invalidObject("This is a different invalid JSON")) + XCTAssertFalse(DataWriterError.invalidJSON(json) == DataWriterError.invalidJSON("This is a different invalid JSON")) } func testSameFailure() { @@ -27,7 +27,7 @@ class JSONWriterErrorTests: XCTestCase { let code = 123 let error1 = NSError(domain: domain, code: code, userInfo: nil) let error2 = NSError(domain: "com.testdomain", code: 123, userInfo: nil) - XCTAssertTrue(JSONWriterError.failure(error1) == JSONWriterError.failure(error2)) + XCTAssertTrue(DataWriterError.failure(error1) == DataWriterError.failure(error2)) } func testDifferentFailureDomain() { @@ -35,7 +35,7 @@ class JSONWriterErrorTests: XCTestCase { let code = 123 let error1 = NSError(domain: domain, code: code, userInfo: nil) let error2 = NSError(domain: "com.newtestdomain", code: 123, userInfo: nil) - XCTAssertFalse(JSONWriterError.failure(error1) == JSONWriterError.failure(error2)) + XCTAssertFalse(DataWriterError.failure(error1) == DataWriterError.failure(error2)) } func testDifferentFailureCode() { @@ -43,7 +43,7 @@ class JSONWriterErrorTests: XCTestCase { let code = 123 let error1 = NSError(domain: domain, code: code, userInfo: nil) let error2 = NSError(domain: "com.testdomain", code: 1234, userInfo: nil) - XCTAssertFalse(JSONWriterError.failure(error1) == JSONWriterError.failure(error2)) + XCTAssertFalse(DataWriterError.failure(error1) == DataWriterError.failure(error2)) } func testDifferentFailureUserInfo() { @@ -51,6 +51,6 @@ class JSONWriterErrorTests: XCTestCase { let code = 123 let error1 = NSError(domain: domain, code: code, userInfo: nil) let error2 = NSError(domain: "com.testdomain", code: 123, userInfo: ["key": "some uesr info"]) - XCTAssertFalse(JSONWriterError.failure(error1) == JSONWriterError.failure(error2)) + XCTAssertFalse(DataWriterError.failure(error1) == DataWriterError.failure(error2)) } } diff --git a/ClueTests/Classes/Writers/DataWriterTests.swift b/ClueTests/Classes/Writers/DataWriterTests.swift new file mode 100644 index 0000000..7a257b2 --- /dev/null +++ b/ClueTests/Classes/Writers/DataWriterTests.swift @@ -0,0 +1,67 @@ +// +// DataWriterTests.swift +// Clue +// +// Created by Andrea Prearo on 4/29/17. +// Copyright © 2017 Ahmed Sulaiman. All rights reserved. +// + +import XCTest +@testable import Clue + +class DataWriterTests: XCTestCase { + fileprivate lazy var testOutputURL: URL = { + return URL(fileURLWithPath: "test-file") + }() + fileprivate lazy var fileManager: FileManager = { + return FileManager.default + }() + fileprivate var writer: DataWriter? + + override func setUp() { + super.setUp() + writer = DataWriter(outputURL: testOutputURL) + writer?.startWriting() + } + + override func tearDown() { + writer?.finishWriting() + do { + try fileManager.removeItem(at: testOutputURL) + } catch { + XCTFail("Error removing test file: \(testOutputURL)") + } + super.tearDown() + } + + func testAppendValidData() { + guard let writer = writer else { + XCTFail("Invalid DataWriter instance") + return + } + let content = "Test Data String Content" + guard let contentData = content.data(using: .utf8) else { + XCTFail("Invalid data") + return + } + let bytesCount = writer.append(data: contentData) + let expectedBytesCount = content.characters.count + XCTAssertEqual(bytesCount, expectedBytesCount) + XCTAssertNil(writer.error) + } + + func testAppendEmptyData() { + guard let writer = writer else { + XCTFail("Invalid DataWriter instance") + return + } + let content = "" + guard let contentData = content.data(using: .utf8) else { + XCTFail("Invalid data") + return + } + let bytesCount = writer.append(data: contentData) + XCTAssertEqual(bytesCount, 0) + XCTAssertNil(writer.error) + } +} diff --git a/ClueTests/Classes/Writers/JSONWriterTests.swift b/ClueTests/Classes/Writers/JSONWriterTests.swift index f715f13..575c71a 100644 --- a/ClueTests/Classes/Writers/JSONWriterTests.swift +++ b/ClueTests/Classes/Writers/JSONWriterTests.swift @@ -66,6 +66,6 @@ class JSONWriterTests: XCTestCase { let json = "Invalid JSON Content" let bytesCount = writer.append(json: json) XCTAssertEqual(bytesCount, 0) - XCTAssertEqual(writer.error, JSONWriterError.invalidObject(json)) + XCTAssertEqual(writer.error, DataWriterError.invalidJSON(json)) } }