diff --git a/Package.resolved b/Package.resolved index 52900f8e..f7b789ff 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,6 +10,15 @@ "version": "4.5.0" } }, + { + "package": "BooleanExpressionEvaluation", + "repositoryURL": "https://github.com/ABridoux/BooleanExpressionEvaluation", + "state": { + "branch": "develop", + "revision": "04713e1d1f4d237c00a16334158553281f590a7d", + "version": null + } + }, { "package": "Lux", "repositoryURL": "https://github.com/ABridoux/lux", diff --git a/Package.swift b/Package.swift index c12cf911..519e0883 100644 --- a/Package.swift +++ b/Package.swift @@ -29,12 +29,15 @@ let package = Package( from: "0.1.0"), .package( url: "https://github.com/jpsim/Yams.git", - from: "4.0.0") + from: "4.0.0"), + .package( + url: "https://github.com/ABridoux/BooleanExpressionEvaluation", + .branch("develop")) ], targets: [ .target( name: "Scout", - dependencies: ["AEXML", "Yams"]), + dependencies: ["AEXML", "Yams", "BooleanExpressionEvaluation"]), .target( name: "ScoutCLTCore", dependencies: ["Scout"]), diff --git a/Sources/Scout/Definitions/Path.swift b/Sources/Scout/Definitions/Path.swift index 61b965cb..f29bd83e 100644 --- a/Sources/Scout/Definitions/Path.swift +++ b/Sources/Scout/Definitions/Path.swift @@ -208,6 +208,15 @@ extension Path: ExpressibleByArrayLiteral { extension Path { + public var lastKeyElementName: String? { + let lastKey = elements.last { (element) -> Bool in + if case .key = element { return true } + return false + } + guard case let .key(name) = lastKey else { return nil } + return name + } + /// Last key component matching the regular expression public func lastKeyComponent(matches regularExpression: NSRegularExpression) -> Bool { let lastKey = elements.last { (element) -> Bool in diff --git a/Sources/Scout/Definitions/PathElementFilter.swift b/Sources/Scout/Definitions/PathElementFilter.swift deleted file mode 100644 index ac3287c3..00000000 --- a/Sources/Scout/Definitions/PathElementFilter.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Scout -// Copyright (c) Alexis Bridoux 2020 -// MIT license, see LICENSE file for details - -import Foundation - -public enum PathElementFilter { - case key(regex: NSRegularExpression) -} - -extension PathElementFilter { - - /// Specifies if group (array, dictionary) values, single (string, bool...) values or both should be targeted - public enum ValueType: String, CaseIterable { - /// Allows the key with a single or a group value - case singleAndGroup - /// Allows the key with a single value - case group - /// Allows the key with a group (array, dictionary) value - case single - - /// Allows group values (array, dictionaries) - var groupAllowed: Bool { [.singleAndGroup, .group].contains(self) } - - /// Allow single values (string, bool...) - var singleAllowed: Bool { [.singleAndGroup, .single].contains(self) } - } -} diff --git a/Sources/Scout/Definitions/PathExplorer+Extensions.swift b/Sources/Scout/Definitions/PathExplorer+Extensions.swift index 28a66bd7..4fa8483d 100644 --- a/Sources/Scout/Definitions/PathExplorer+Extensions.swift +++ b/Sources/Scout/Definitions/PathExplorer+Extensions.swift @@ -170,7 +170,7 @@ extension PathExplorer { extension PathExplorer { - public func getPaths(startingAt initialPath: Path?, for filter: PathElementFilter?) throws -> [Path] { - try listPaths(startingAt: initialPath, for: filter, valueType: .singleAndGroup) + public func getPaths(startingAt initialPath: Path) throws -> [Path] { + try listPaths(startingAt: initialPath, filter: .noFilter) } } diff --git a/Sources/Scout/Definitions/PathExplorer.swift b/Sources/Scout/Definitions/PathExplorer.swift index 7aaf1148..74d80831 100644 --- a/Sources/Scout/Definitions/PathExplorer.swift +++ b/Sources/Scout/Definitions/PathExplorer.swift @@ -242,9 +242,8 @@ where /// Returns all the paths leading to single or group values /// - Parameters: /// - initialPath: Scope the return paths with this path as a starting point - /// - filter: Optionnally provide a filter on the key - /// - valueType: Allow group, single values or both. Default to both. - func listPaths(startingAt initialPath: Path?, for filter: PathElementFilter?, valueType: PathElementFilter.ValueType) throws -> [Path] + /// - filter: Optionnally provide a filter on the key and/or value. Default is `noFilter` + func listPaths(startingAt initialPath: Path?, filter: PathsFilter) throws -> [Path] // MARK: Conversion diff --git a/Sources/Scout/Definitions/PathsFilter.swift b/Sources/Scout/Definitions/PathsFilter.swift new file mode 100644 index 00000000..11417ad9 --- /dev/null +++ b/Sources/Scout/Definitions/PathsFilter.swift @@ -0,0 +1,111 @@ +// +// Scout +// Copyright (c) Alexis Bridoux 2020 +// MIT license, see LICENSE file for details + +import Foundation +import BooleanExpressionEvaluation + +public enum PathsFilter { + case targetOnly(ValueTarget) + case key(regex: NSRegularExpression, target: ValueTarget) + case value(Predicate) + case keyAndValue(keyRegex: NSRegularExpression, valuePredicate: Predicate) + + /// Allows group values (array, dictionaries) + var groupAllowed: Bool { + switch self { + case .targetOnly(let target), .key(_, let target): return target.groupAllowed + case .value, .keyAndValue: return false + } + } + + /// Allow single values (string, bool...) + var singleAllowed: Bool { + switch self { + case .targetOnly(let target), .key(_, let target): return target.singleAllowed + case .value, .keyAndValue: return true + } + } + + /// Validate a key when the filter has a key regex + func validate(key: String) -> Bool { + switch self { + case .key(let regex, _), .keyAndValue(let regex, _): return regex.validate(key) + case .value, .targetOnly: return true + } + } + + /// Validate an index when the filter is has no key regex + func validate(index: Int) -> Bool { + switch self { + case .key: return false + case .value, .keyAndValue, .targetOnly: return true + } + } + + /// Validate a value when the filter has a value predicate + func validate(value: Any) -> Bool { + switch self { + case .value(let predicate), .keyAndValue(_, let predicate): return predicate.evaluate(with: value) + case .key, .targetOnly: + return true + } + } +} + +extension PathsFilter { + + /// Specifies if group (array, dictionary) values, single (string, bool...) values or both should be targeted + public enum ValueTarget: String, CaseIterable { + /// Allows the key with a single or a group value + case singleAndGroup + /// Allows the key with a single value + case group + /// Allows the key with a group (array, dictionary) value + case single + + /// Allows group values (array, dictionaries) + var groupAllowed: Bool { [.singleAndGroup, .group].contains(self) } + + /// Allow single values (string, bool...) + var singleAllowed: Bool { [.singleAndGroup, .single].contains(self) } + } +} + +extension PathsFilter { + + /// Allow to specify a bollean expression to filter the value + /// + /// The value is specified as the variable 'value' in the expression. + /// - `value > 10` + /// - `value hasPrefix 'Lou' && value hasSuffix 'lou'` + /// + /// - note: Public wrapper around BoleeanExpressionEvaluation.Expression + public struct Predicate { + var expression: Expression + + init(expression: Expression) { + self.expression = expression + } + + /// Specify a predicate with a 'value' variable that will be replaced with a concrete value during evaluation + public init(format: String) throws { + expression = try Expression(format) + } + + public func evaluate(with value: Any) -> Bool { + let result = try? expression.evaluate(with: ["variable": String(describing: value)]) + return result ?? true //ignore the error of wrong value type + } + } +} + +extension PathsFilter { + + /// No filter with target `singleAndGroup` + public static var noFilter: PathsFilter { .targetOnly(.singleAndGroup) } + + /// Key filter with `singleAndGroup` target + public static func key(regex: NSRegularExpression) -> PathsFilter { .key(regex: regex, target: .singleAndGroup) } +} diff --git a/Sources/Scout/Implementations/Serialization/PathExplorerSerialization+Add.swift b/Sources/Scout/Implementations/Serialization/PathExplorerSerialization+Add.swift index 45a17a2d..63472f6f 100644 --- a/Sources/Scout/Implementations/Serialization/PathExplorerSerialization+Add.swift +++ b/Sources/Scout/Implementations/Serialization/PathExplorerSerialization+Add.swift @@ -28,7 +28,7 @@ extension PathExplorerSerialization { case .index(let index): let computedIndex = index < 0 ? array.count + index : index - + if (array.isEmpty && computedIndex == 0) || computedIndex == array.count { // empty array so the value should be added anyway array.append(newValue) diff --git a/Sources/Scout/Implementations/Serialization/PathExplorerSerialization+Paths.swift b/Sources/Scout/Implementations/Serialization/PathExplorerSerialization+Paths.swift index cf09640a..a33f78c9 100644 --- a/Sources/Scout/Implementations/Serialization/PathExplorerSerialization+Paths.swift +++ b/Sources/Scout/Implementations/Serialization/PathExplorerSerialization+Paths.swift @@ -7,7 +7,7 @@ import Foundation extension PathExplorerSerialization { - public func listPaths(startingAt initialPath: Path?, for filter: PathElementFilter?, valueType: PathElementFilter.ValueType) throws -> [Path] { + public func listPaths(startingAt initialPath: Path?, filter: PathsFilter) throws -> [Path] { var explorer = Self(value: value, path: .empty) if let path = initialPath { @@ -23,67 +23,41 @@ extension PathExplorerSerialization { } var paths = [Path]() - switch filter { - case .key(let regex): explorer.collectKeysPaths(in: &paths, whereKeyMatches: regex, valueType: valueType) - case nil: explorer.collectKeysPaths(in: &paths, valueType: valueType) - } - + explorer.collectKeysPaths(in: &paths, filter: filter) return paths.map { $0.flattened() }.sortedByKeysAndIndexes() } - func collectKeysPaths(in paths: inout [Path], valueType: PathElementFilter.ValueType) { + func collectKeysPaths(in paths: inout [Path], filter: PathsFilter) { switch value { case let dict as DictionaryValue: dict.forEach { (key, value) in - if valueType.groupAllowed, isGroup(value: value) { + if filter.groupAllowed, filter.validate(key: key), isGroup(value: value) { paths.append(readingPath.appending(key)) } let explorer = PathExplorerSerialization(value: value, path: readingPath.appending(key)) - explorer.collectKeysPaths(in: &paths, valueType: valueType) + explorer.collectKeysPaths(in: &paths, filter: filter) } case let array as ArrayValue: array.enumerated().forEach { (index, value) in - if valueType.groupAllowed, isGroup(value: value) { + if filter.groupAllowed, isGroup(value: value), filter.validate(index: index) { paths.append(readingPath.appending(index)) } let explorer = PathExplorerSerialization(value: value, path: readingPath.appending(index)) - explorer.collectKeysPaths(in: &paths, valueType: valueType) - } - default: - if valueType.singleAllowed { - paths.append(readingPath) - } - } - } - - func collectKeysPaths(in paths: inout [Path], whereKeyMatches regularExpression: NSRegularExpression, valueType: PathElementFilter.ValueType) { - switch value { - - case let dict as DictionaryValue: - dict.forEach { (key, value) in - if valueType.groupAllowed, regularExpression.validate(key), isGroup(value: value) { - paths.append(readingPath.appending(key)) - } - - let explorer = PathExplorerSerialization(value: value, path: readingPath.appending(key)) - explorer.collectKeysPaths(in: &paths, whereKeyMatches: regularExpression, valueType: valueType) - } - - case let array as ArrayValue: - array.enumerated().forEach { (index, value) in - - let explorer = PathExplorerSerialization(value: value, path: readingPath.appending(index)) - explorer.collectKeysPaths(in: &paths, whereKeyMatches: regularExpression, valueType: valueType) + explorer.collectKeysPaths(in: &paths, filter: filter) } default: - if valueType.singleAllowed, readingPath.lastKeyComponent(matches: regularExpression) { + guard filter.singleAllowed else { break } + guard let name = readingPath.lastKeyElementName else { paths.append(readingPath) + return } + guard filter.validate(key: name) else { break } + paths.append(readingPath) } } } diff --git a/Sources/Scout/Implementations/XML/PathExplorerXML+Paths.swift b/Sources/Scout/Implementations/XML/PathExplorerXML+Paths.swift index 85ebf7c9..7b2cb59e 100644 --- a/Sources/Scout/Implementations/XML/PathExplorerXML+Paths.swift +++ b/Sources/Scout/Implementations/XML/PathExplorerXML+Paths.swift @@ -8,7 +8,7 @@ import AEXML extension PathExplorerXML { - public func listPaths(startingAt initialPath: Path?, for filter: PathElementFilter?, valueType: PathElementFilter.ValueType) throws -> [Path] { + public func listPaths(startingAt initialPath: Path?, filter: PathsFilter) throws -> [Path] { var explorer = PathExplorerXML(element: element, path: .empty) if let path = initialPath { @@ -23,20 +23,16 @@ extension PathExplorerXML { } } var paths = [Path]() - - switch filter { - case .key(let regex): explorer.collectKeysPaths(in: &paths, whereKeyMatches: regex, valueType: valueType) - case nil: explorer.collectKeysPaths(in: &paths, valueType: valueType) - } + explorer.collectKeysPaths(in: &paths, filter: filter) return paths.map { $0.flattened() }.sortedByKeysAndIndexes() } - func collectKeysPaths(in paths: inout [Path], valueType: PathElementFilter.ValueType) { + func collectKeysPaths(in paths: inout [Path], filter: PathsFilter) { - if valueType.singleAllowed, - let value = element.value?.trimmingCharacters(in: .whitespacesAndNewlines), - !value.isEmpty { + if filter.singleAllowed, + let value = element.value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty, + filter.validate(key: element.name) { paths.append(readingPath) } @@ -44,42 +40,15 @@ extension PathExplorerXML { let newElement: PathElement = element.differentiableChildren ? .key(child.name) : .index(index) if child.children.isEmpty { - if valueType.singleAllowed { - paths.append(readingPath.appending(newElement)) - } - } else { - if valueType.groupAllowed { - paths.append(readingPath.appending(newElement)) - } - let explorer = PathExplorerXML(element: child, path: readingPath.appending(newElement)) - explorer.collectKeysPaths(in: &paths, valueType: valueType) - } - } - } - - func collectKeysPaths(in paths: inout [Path], whereKeyMatches regularExpression: NSRegularExpression, valueType: PathElementFilter.ValueType) { - - if valueType.singleAllowed, - let value = element.value?.trimmingCharacters(in: .whitespacesAndNewlines), - !value.isEmpty, regularExpression.validate(element.name) { - paths.append(readingPath) - } - - let differentiableChildren = element.differentiableChildren - - element.children.enumerated().forEach { (index, child) in - let newElement: PathElement = differentiableChildren ? .key(child.name) : .index(index) - - if child.children.isEmpty { - if valueType.singleAllowed, regularExpression.validate(child.name) { + if filter.singleAllowed, filter.validate(key: child.name) { paths.append(readingPath.appending(newElement)) } } else { - if valueType.groupAllowed, differentiableChildren, regularExpression.validate(child.name) { + if filter.groupAllowed, filter.validate(key: child.name) { paths.append(readingPath.appending(newElement)) } let explorer = PathExplorerXML(element: child, path: readingPath.appending(newElement)) - explorer.collectKeysPaths(in: &paths, whereKeyMatches: regularExpression, valueType: valueType) + explorer.collectKeysPaths(in: &paths, filter: filter) } } } diff --git a/Sources/ScoutCLT/Paths/PathsCommand.swift b/Sources/ScoutCLT/Paths/PathsCommand.swift index 75c6921e..25a89e19 100644 --- a/Sources/ScoutCLT/Paths/PathsCommand.swift +++ b/Sources/ScoutCLT/Paths/PathsCommand.swift @@ -7,7 +7,7 @@ import Foundation import Scout import ArgumentParser -extension PathElementFilter.ValueType: EnumerableFlag {} +extension PathsFilter.ValueTarget: EnumerableFlag {} struct PathsCommand: ScoutCommand { @@ -30,22 +30,22 @@ struct PathsCommand: ScoutCommand { var keyRegexPattern: String? @Flag(help: "") - var valueType = PathElementFilter.ValueType.singleAndGroup + var valueTarget = PathsFilter.ValueTarget.singleAndGroup // MARK: - Functions func inferred

(pathExplorer: P) throws where P: PathExplorer { - var pathFilter: PathElementFilter? + var pathsFilter = PathsFilter.targetOnly(valueTarget) if let keyRegexPattern = keyRegexPattern { guard let regex = try? NSRegularExpression(pattern: keyRegexPattern) else { throw RuntimeError.invalidRegex(keyRegexPattern) } - pathFilter = .key(regex: regex) + pathsFilter = .key(regex: regex, target: valueTarget) } let readingPath = self.readingPath ?? Path() - let paths = try pathExplorer.listPaths(startingAt: readingPath, for: pathFilter, valueType: valueType) + let paths = try pathExplorer.listPaths(startingAt: readingPath, filter: pathsFilter) paths.forEach { print($0.flattened()) } } diff --git a/Tests/ScoutTests/Implementations/Serialization/PathExplorerSerailizationTest+Paths.swift b/Tests/ScoutTests/Implementations/Serialization/PathExplorerSerailizationTest+Paths.swift index 6deaf05e..9c817ac9 100644 --- a/Tests/ScoutTests/Implementations/Serialization/PathExplorerSerailizationTest+Paths.swift +++ b/Tests/ScoutTests/Implementations/Serialization/PathExplorerSerailizationTest+Paths.swift @@ -33,7 +33,7 @@ final class PathExplorerSerializationPathsTest: XCTestCase { let explorer = Json(value: players) var paths = [Path]() - explorer.collectKeysPaths(in: &paths, valueType: .single) + explorer.collectKeysPaths(in: &paths, filter: .targetOnly(.single)) let expectedPaths: Set = [Path("duration"), Path("players", 0, "name"), Path("players", 0, "score"), Path("players", 1, "name"), Path("players", 1, "score")] XCTAssertEqual(Set(paths), expectedPaths) @@ -43,7 +43,7 @@ final class PathExplorerSerializationPathsTest: XCTestCase { let explorer = Json(value: players) var paths = [Path]() - explorer.collectKeysPaths(in: &paths, valueType: .group) + explorer.collectKeysPaths(in: &paths, filter: .targetOnly(.group)) let expectedPaths: Set = [Path("players"), Path("players", 0), Path("players", 1)] XCTAssertEqual(Set(paths), expectedPaths) @@ -53,7 +53,7 @@ final class PathExplorerSerializationPathsTest: XCTestCase { let explorer = Json(value: players) var paths = [Path]() - explorer.collectKeysPaths(in: &paths, valueType: .singleAndGroup) + explorer.collectKeysPaths(in: &paths, filter: .targetOnly(.singleAndGroup)) let expectedPaths: Set = [Path("players"), Path("duration"), @@ -75,7 +75,7 @@ final class PathExplorerSerializationPathsTest: XCTestCase { let explorer = Json(value: dict) var paths = [Path]() - explorer.collectKeysPaths(in: &paths, valueType: .single) + explorer.collectKeysPaths(in: &paths, filter: .targetOnly(.single)) let expectedPaths = [Path("players", 0, "name"), Path("players", 1, "name"), Path("players", 2, "name")] XCTAssertEqual(paths, expectedPaths) @@ -86,7 +86,7 @@ final class PathExplorerSerializationPathsTest: XCTestCase { var paths = [Path]() let regex = try NSRegularExpression(pattern: "name") - explorer.collectKeysPaths(in: &paths, whereKeyMatches: regex, valueType: .singleAndGroup) + explorer.collectKeysPaths(in: &paths, filter: .key(regex: regex)) let expectedPaths: Set = [Path("name"), Path("name", 0), Path("name", 1), Path("players", 0, "name"), Path("players", 1, "name")] XCTAssertEqual(Set(paths), expectedPaths) @@ -97,7 +97,7 @@ final class PathExplorerSerializationPathsTest: XCTestCase { var paths = [Path]() let regex = try NSRegularExpression(pattern: "name") - explorer.collectKeysPaths(in: &paths, whereKeyMatches: regex, valueType: .group) + explorer.collectKeysPaths(in: &paths, filter: .key(regex: regex, target: .group)) let expectedPaths: Set = [Path("name")] XCTAssertEqual(Set(paths), expectedPaths) diff --git a/Tests/ScoutTests/Implementations/XML/PathExplorerXMLTests+Paths.swift b/Tests/ScoutTests/Implementations/XML/PathExplorerXMLTests+Paths.swift index 4aedb9b0..30b039af 100644 --- a/Tests/ScoutTests/Implementations/XML/PathExplorerXMLTests+Paths.swift +++ b/Tests/ScoutTests/Implementations/XML/PathExplorerXMLTests+Paths.swift @@ -59,7 +59,7 @@ final class PathExplorerXMLPathsTests: XCTestCase { let explorer = Xml(element: players, path: .empty) var paths = [Path]() - explorer.collectKeysPaths(in: &paths, valueType: .single) + explorer.collectKeysPaths(in: &paths, filter: .targetOnly(.single)) let expectedPaths: Set = [Path("duration"), Path("players", 0, "name"), Path("players", 0, "score"), Path("players", 1, "name"), Path("players", 1, "score")] XCTAssertEqual(Set(paths), expectedPaths) @@ -69,7 +69,7 @@ final class PathExplorerXMLPathsTests: XCTestCase { let explorer = Xml(element: players, path: .empty) var paths = [Path]() - explorer.collectKeysPaths(in: &paths, valueType: .group) + explorer.collectKeysPaths(in: &paths, filter: .targetOnly(.group)) let expectedPaths: Set = [Path("players"), Path("players", 0), Path("players", 1)] XCTAssertEqual(Set(paths), expectedPaths) @@ -79,7 +79,7 @@ final class PathExplorerXMLPathsTests: XCTestCase { let explorer = Xml(element: players, path: .empty) var paths = [Path]() - explorer.collectKeysPaths(in: &paths, valueType: .singleAndGroup) + explorer.collectKeysPaths(in: &paths, filter: .noFilter) let expectedPaths: Set = [Path("players"), Path("duration"), @@ -107,7 +107,7 @@ final class PathExplorerXMLPathsTests: XCTestCase { let explorer = Xml(element: root, path: .empty) var paths = [Path]() - explorer.collectKeysPaths(in: &paths, valueType: .single) + explorer.collectKeysPaths(in: &paths, filter: .targetOnly(.single)) let expectedPaths = [Path("duration"), Path("players", 0, "name"), Path("players", 0, "score"), Path("players", 1, "name"), Path("players", 1, "score")] XCTAssertEqual(paths, expectedPaths) @@ -118,7 +118,7 @@ final class PathExplorerXMLPathsTests: XCTestCase { var paths = [Path]() let regex = try NSRegularExpression(pattern: "name") - explorer.collectKeysPaths(in: &paths, whereKeyMatches: regex, valueType: .singleAndGroup) + explorer.collectKeysPaths(in: &paths, filter: .key(regex: regex)) let expectedPaths: Set = [Path("name"), Path("name", 0), Path("name", 1), Path("players", 0, "name"), Path("players", 1, "name")] XCTAssertEqual(Set(paths), expectedPaths) @@ -129,7 +129,7 @@ final class PathExplorerXMLPathsTests: XCTestCase { var paths = [Path]() let regex = try NSRegularExpression(pattern: "name") - explorer.collectKeysPaths(in: &paths, whereKeyMatches: regex, valueType: .group) + explorer.collectKeysPaths(in: &paths, filter: .key(regex: regex, target: .group)) let expectedPaths: Set = [Path("name")] XCTAssertEqual(Set(paths), expectedPaths)