From 0a3495d90d9301dacc67eff47340ab3c037eac39 Mon Sep 17 00:00:00 2001 From: Anton Davydov Date: Fri, 19 Jan 2018 17:34:35 +0300 Subject: [PATCH 1/2] Implemented concurrent array rule --- Bender.xcodeproj/project.pbxproj | 6 + Bender/ConcurrentArrayRule.swift | 134 +++++++++++++++++++++++ BenderTests/BenderPerformanceTests.swift | 35 ++++++ 3 files changed, 175 insertions(+) create mode 100644 Bender/ConcurrentArrayRule.swift diff --git a/Bender.xcodeproj/project.pbxproj b/Bender.xcodeproj/project.pbxproj index 327af2c..42439a8 100644 --- a/Bender.xcodeproj/project.pbxproj +++ b/Bender.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 106249BD2012363C00D1E71B /* ConcurrentArrayRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106249BC2012363C00D1E71B /* ConcurrentArrayRule.swift */; }; + 106249BE2012394500D1E71B /* ConcurrentArrayRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106249BC2012363C00D1E71B /* ConcurrentArrayRule.swift */; }; 5B05ABA91C54F71100339915 /* BenderOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B05ABA81C54F71100339915 /* BenderOutputTests.swift */; }; 5B3C4B3E1C7A6A070032BA13 /* defaults_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 5B3C4B3D1C7A6A070032BA13 /* defaults_test.json */; }; 5B54EF101C3A90AE0072B2A8 /* BenderInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B54EF0F1C3A90AE0072B2A8 /* BenderInputTests.swift */; }; @@ -79,6 +81,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 106249BC2012363C00D1E71B /* ConcurrentArrayRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConcurrentArrayRule.swift; sourceTree = ""; }; 275234511DD0A3620096F1D0 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = ""; }; 275234521DD0A3620096F1D0 /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quick.framework; path = Carthage/Build/iOS/Quick.framework; sourceTree = ""; }; 5B05ABA81C54F71100339915 /* BenderOutputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BenderOutputTests.swift; sourceTree = ""; }; @@ -198,6 +201,7 @@ C147F5421DF83A6F00899BF7 /* RuleError.swift */, C147F5451DF83B8A00899BF7 /* StringifiedJSONRule.swift */, C147F5481DF83C1A00899BF7 /* TypeRule.swift */, + 106249BC2012363C00D1E71B /* ConcurrentArrayRule.swift */, ); path = Bender; sourceTree = ""; @@ -469,6 +473,7 @@ C147F5401DF8393800899BF7 /* ObjectRule.swift in Sources */, 5B54EF1B1C3A90FD0072B2A8 /* Bender.swift in Sources */, C147F5371DF8329300899BF7 /* ArrayRule.swift in Sources */, + 106249BD2012363C00D1E71B /* ConcurrentArrayRule.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -504,6 +509,7 @@ C1F6A62A1FE5168000FE716B /* ObjectRule.swift in Sources */, C1F6A62B1FE5168000FE716B /* Bender.swift in Sources */, C1F6A62C1FE5168000FE716B /* ArrayRule.swift in Sources */, + 106249BE2012394500D1E71B /* ConcurrentArrayRule.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Bender/ConcurrentArrayRule.swift b/Bender/ConcurrentArrayRule.swift new file mode 100644 index 0000000..6a448cf --- /dev/null +++ b/Bender/ConcurrentArrayRule.swift @@ -0,0 +1,134 @@ +// ConcurrentArrayRule.swift +// Bender +// +// Created by Anton Davydov on 25/10/2017. +// Original work Copyright © 2017 Anton Davydov +// +// The MIT License (MIT) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/** + Validator for arrays of items of type T, that should be validated by rule of type R, i.e. where R.V == T. + This version of ArrayRule validates/dumps array items separately on background thread. + */ +public class ConcurrentArrayRule: Rule where R.V == T { + public typealias V = [T] + fileprivate var itemRule: R + + /** + Validator initializer + + - parameter itemRule: rule for validating array items of type R.V + */ + public init(itemRule: R) { + self.itemRule = itemRule + } + + // MARK:- Rule + /** + Validates JSON array and returns [T] if succeeded. Validation throws if jsonValue is not a JSON array or if item rule throws for any item. + + - parameter jsonValue: JSON array to be validated and converted into [T] + + - throws: throws ValidateError + + - returns: array of objects of first generic parameter argument if validation was successful + */ + open func validate(_ jsonValue: AnyObject) throws -> V { + guard let jsonArray = jsonValue as? NSArray else { + throw RuleError.invalidJSONType("Value of unexpected type found: \"\(jsonValue)\". Expected array of \(T.self).", nil) + } + + return self.validate(input: jsonArray) + } + + /** + Dumps array of AnyObject type in case of success. Throws if cannot dump any item in source array. + + - parameter value: array with items of type T + + - throws: throws RuleError if cannot dump any item in source array + + - returns: returns array of AnyObject, dumped by item rule + */ + open func dump(_ value: V) throws -> AnyObject { + return self.dump(input: value) + } +} + +extension ConcurrentArrayRule { + + fileprivate func validate(input: NSArray) -> V { + let operationQueue = DispatchQueue(label: "", qos: .userInteractive, attributes: .concurrent) + let semaphore = DispatchSemaphore(value: 1) + let dispatchGroups = [DispatchGroup].init(repeating: DispatchGroup(), count: input.count) + var result = V() + + for (index, object) in input.enumerated() { + dispatchGroups[index].enter() + + operationQueue.async { + do { + let value = try self.itemRule.validate(object as AnyObject) + + semaphore.wait() + result.append(value) + semaphore.signal() + } catch let error { + // TODO: Handle error + } + dispatchGroups[index].leave() + } + } + + dispatchGroups.forEach { $0.wait() } + return result + } + + fileprivate func dump(input: V) -> NSArray { + let operationQueue = DispatchQueue(label: "", qos: .userInteractive, attributes: .concurrent) + let semaphore = DispatchSemaphore(value: 1) + let dispatchGroups = [DispatchGroup].init(repeating: DispatchGroup(), count: input.count) + let result = NSMutableArray.init(capacity: input.count) + + for (index, object) in input.enumerated() { + dispatchGroups[index].enter() + + operationQueue.async { + do { + let value = try self.itemRule.dump(object) + + semaphore.wait() + result.adding(value) + semaphore.signal() + } catch let error { + // TODO: Handle error + } + dispatchGroups[index].leave() + } + } + + dispatchGroups.forEach { $0.wait() } + return result + } +} diff --git a/BenderTests/BenderPerformanceTests.swift b/BenderTests/BenderPerformanceTests.swift index 3c8448c..2485e79 100644 --- a/BenderTests/BenderPerformanceTests.swift +++ b/BenderTests/BenderPerformanceTests.swift @@ -69,6 +69,41 @@ class BenderPerfTests: XCTestCase { } } + + func testPerformance_ConcurrentBinding_5Mb_JSON() { + let friendRule = ClassRule(Friend()) + .expect("id", IntRule, { $0.ID = $1 }) + .expect("name", StringRule, { $0.name = $1 }) + + let itemRule = ClassRule(Item()) + .expect("_id", StringRule, { $0.ID = $1 }) + .expect("index", IntRule, { $0.index = $1 }) + .expect("guid", StringRule, { $0.guid = $1 }) + .expect("isActive", BoolRule, { $0.isActive = $1 }) + .expect("balance", StringRule, { $0.balance = $1 }) + .expect("picture", StringRule, { $0.picture = $1 }) + .expect("age", IntRule, { $0.age = $1 }) + .expect("eyeColor", StringRule, { $0.eyeColor = $1 }) + .expect("name", StringRule, { $0.name = $1 }) + .expect("gender", StringRule, { $0.gender = $1 }) + .expect("company", StringRule, { $0.company = $1 }) + .expect("email", StringRule, { $0.email = $1 }) + .expect("phone", StringRule, { $0.phone = $1 }) + .expect("tags", ArrayRule(itemRule: StringRule), { $0.tags = $1 }) + .expect("latitude", DoubleRule, { $0.latitude = $1 }) + .expect("longitude", DoubleRule, { $0.longitude = $1 }) + .expect("friends", ArrayRule(itemRule: friendRule), { $0.friends = $1 }) + + let arrayRule = ConcurrentArrayRule(itemRule: itemRule) + + let path = Bundle(for: BenderPerfTests.self).path(forResource: "five_megs", ofType: "json")! + let data = try! Data(contentsOf: URL(fileURLWithPath: path)) + let json = try! JSONSerialization.jsonObject(with: data, options: []) as AnyObject + + measure { + let _ = try? arrayRule.validate(json) + } + } } class Friend { From c419f5c02f7884d6ffa1492fb0c62cc715931fe2 Mon Sep 17 00:00:00 2001 From: Anton Davydov Date: Mon, 22 Jan 2018 15:48:24 +0300 Subject: [PATCH 2/2] Implemented error handling for concurrent array rule. --- Bender.xcodeproj/project.pbxproj | 2 +- Bender/ConcurrentArrayRule.swift | 37 +++++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Bender.xcodeproj/project.pbxproj b/Bender.xcodeproj/project.pbxproj index 42439a8..583dfdb 100644 --- a/Bender.xcodeproj/project.pbxproj +++ b/Bender.xcodeproj/project.pbxproj @@ -195,13 +195,13 @@ C1F6A6341FE5168000FE716B /* Bender-macOS.plist */, 5B54EF1A1C3A90FD0072B2A8 /* Bender.swift */, C147F5361DF8329300899BF7 /* ArrayRule.swift */, + 106249BC2012363C00D1E71B /* ConcurrentArrayRule.swift */, C147F5391DF834C900899BF7 /* EnumRule.swift */, C147F53C1DF837F200899BF7 /* JSONPath.swift */, C147F53F1DF8393800899BF7 /* ObjectRule.swift */, C147F5421DF83A6F00899BF7 /* RuleError.swift */, C147F5451DF83B8A00899BF7 /* StringifiedJSONRule.swift */, C147F5481DF83C1A00899BF7 /* TypeRule.swift */, - 106249BC2012363C00D1E71B /* ConcurrentArrayRule.swift */, ); path = Bender; sourceTree = ""; diff --git a/Bender/ConcurrentArrayRule.swift b/Bender/ConcurrentArrayRule.swift index 6a448cf..e4a0297 100644 --- a/Bender/ConcurrentArrayRule.swift +++ b/Bender/ConcurrentArrayRule.swift @@ -33,15 +33,20 @@ import Foundation */ public class ConcurrentArrayRule: Rule where R.V == T { public typealias V = [T] - fileprivate var itemRule: R + public typealias InvalidItemHandler = (Int, Error)throws-> Void + fileprivate let itemRule: R + fileprivate let invalidItemHandler: InvalidItemHandler? /** Validator initializer - parameter itemRule: rule for validating array items of type R.V + - parameter invalidItemHandler: handler closure which is called when the item cannnot be validated. + Can throw is there is no need to keep checking. The handler is called on background thread. */ - public init(itemRule: R) { + public init(itemRule: R, invalidItemHandler: InvalidItemHandler? = nil) { self.itemRule = itemRule + self.invalidItemHandler = invalidItemHandler } // MARK:- Rule @@ -59,7 +64,7 @@ public class ConcurrentArrayRule: Rule where R.V == T { throw RuleError.invalidJSONType("Value of unexpected type found: \"\(jsonValue)\". Expected array of \(T.self).", nil) } - return self.validate(input: jsonArray) + return try self.validate(input: jsonArray) } /** @@ -72,17 +77,18 @@ public class ConcurrentArrayRule: Rule where R.V == T { - returns: returns array of AnyObject, dumped by item rule */ open func dump(_ value: V) throws -> AnyObject { - return self.dump(input: value) + return try self.dump(input: value) } } extension ConcurrentArrayRule { - fileprivate func validate(input: NSArray) -> V { + fileprivate func validate(input: NSArray) throws -> V { let operationQueue = DispatchQueue(label: "", qos: .userInteractive, attributes: .concurrent) let semaphore = DispatchSemaphore(value: 1) let dispatchGroups = [DispatchGroup].init(repeating: DispatchGroup(), count: input.count) var result = V() + var resultError: Error? for (index, object) in input.enumerated() { dispatchGroups[index].enter() @@ -95,21 +101,29 @@ extension ConcurrentArrayRule { result.append(value) semaphore.signal() } catch let error { - // TODO: Handle error + do { + try self.invalidItemHandler?(index, error) + } catch let itemHandlerError { + resultError = itemHandlerError + } } dispatchGroups[index].leave() } } dispatchGroups.forEach { $0.wait() } + if let resultError = resultError { + throw resultError + } return result } - fileprivate func dump(input: V) -> NSArray { + fileprivate func dump(input: V) throws -> NSArray { let operationQueue = DispatchQueue(label: "", qos: .userInteractive, attributes: .concurrent) let semaphore = DispatchSemaphore(value: 1) let dispatchGroups = [DispatchGroup].init(repeating: DispatchGroup(), count: input.count) let result = NSMutableArray.init(capacity: input.count) + var resultError: Error? for (index, object) in input.enumerated() { dispatchGroups[index].enter() @@ -122,13 +136,20 @@ extension ConcurrentArrayRule { result.adding(value) semaphore.signal() } catch let error { - // TODO: Handle error + do { + try self.invalidItemHandler?(index, error) + } catch let itemHandlerError { + resultError = itemHandlerError + } } dispatchGroups[index].leave() } } dispatchGroups.forEach { $0.wait() } + if let resultError = resultError { + throw resultError + } return result } }