Skip to content

Commit

Permalink
Command-line paths command updated with predicates
Browse files Browse the repository at this point in the history
The paths command now offers the possibility to specify several predicate to filter the value.
  • Loading branch information
Alexis Bridoux committed Feb 4, 2021
1 parent 6e2da96 commit dbcd809
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 38 deletions.
18 changes: 9 additions & 9 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
"repositoryURL": "https://github.com/tadija/AEXML.git",
"state": {
"branch": null,
"revision": "8623e73b193386909566a9ca20203e33a09af142",
"version": "4.5.0"
"revision": "502c4d43a6cc9c395d19111e09dc62ad834977b5",
"version": "4.6.0"
}
},
{
"package": "BooleanExpressionEvaluation",
"repositoryURL": "https://github.com/ABridoux/BooleanExpressionEvaluation",
"state": {
"branch": "develop",
"revision": "04713e1d1f4d237c00a16334158553281f590a7d",
"revision": "0cc620e646f7622dd8c7b578dc7ada85f45ff495",
"version": null
}
},
Expand All @@ -33,26 +33,26 @@
"repositoryURL": "https://github.com/JohnSundell/Splash",
"state": {
"branch": null,
"revision": "f25dd8c9f16be1f81a152f6861d7216c3c9302da",
"version": "0.14.0"
"revision": "81de0389558ad40579027735841593b21a511fa8",
"version": "0.15.0"
}
},
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "92646c0cdbaca076c8d3d0207891785b3379cbff",
"version": "0.3.1"
"revision": "9564d61b08a5335ae0a36f789a7d71493eacadfc",
"version": "0.3.2"
}
},
{
"package": "Yams",
"repositoryURL": "https://github.com/jpsim/Yams.git",
"state": {
"branch": null,
"revision": "88caa2e6fffdbef2e91c2022d038576062042907",
"version": "4.0.0"
"revision": "9003d51672e516cc59297b7e96bff1dfdedcb4ea",
"version": "4.0.4"
}
}
]
Expand Down
5 changes: 0 additions & 5 deletions Sources/Scout/Definitions/KeyAllowedType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,28 +44,23 @@ extension KeyAllowedType {

extension String: KeyAllowedType {
public static let typeDescription = "String"
static var valueType: PathsFilter.Predicate.ValueType { .string }
}

extension Int: KeyAllowedType {
public static let typeDescription = "Integer"
static var valueType: PathsFilter.Predicate.ValueType { .int }
}

extension Double: KeyAllowedType {
public static let typeDescription = "Real"
static var valueType: PathsFilter.Predicate.ValueType { .double }
}

extension Bool: KeyAllowedType {
public static let typeDescription = "Boolean"
static var valueType: PathsFilter.Predicate.ValueType { .bool }

static let trueSet: Set<String> = ["y", "yes", "Y", "Yes", "YES", "t", "true", "T", "True", "TRUE"]
static let falseSet: Set<String> = ["n", "no", "N", "No", "NO", "f", "false", "F", "False", "FALSE"]
}

extension AnyHashable: KeyAllowedType {
public static let typeDescription = "Automatic"
static var valueType: PathsFilter.Predicate.ValueType { .string }
}
4 changes: 2 additions & 2 deletions Sources/Scout/Definitions/PathExplorerError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public enum PathExplorerError: LocalizedError, Equatable {
case csvExportWrongGroupValue
case csvExportAmbiguous(expected: String, path: Path)

case predicateError(description: String)
case predicateError(predicate: String, description: String)

public var errorDescription: String? {
switch self {
Expand Down Expand Up @@ -80,7 +80,7 @@ public enum PathExplorerError: LocalizedError, Equatable {
case .csvExportWrongGroupValue: return "CSV export requires either first object to be an array or a dictionary of arrays"
case .csvExportAmbiguous(let expectedType, let path): return "Ambiguous type for value at '\(path.description). Expected \(expectedType) as the first value is of type \(expectedType)"

case .predicateError(let description): return description
case .predicateError(let predicate, let description): return #"Unable to evaluate the predicate "\#(predicate)". \#(description)"#
}
}
}
50 changes: 39 additions & 11 deletions Sources/Scout/Definitions/PathsFilter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,35 +116,39 @@ extension PathsFilter {
/// - note: Public wrapper around BoleeanExpressionEvaluation.Expression
public final class Predicate {
private(set) var expression: Expression
private(set) var mismatchedTypes: Set<ValueType> = []

/// The value types that the operators in the expression support
private(set) var operatorsValueTypes: Set<ValueType>

/// 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)
operatorsValueTypes = expression.operators.reduce(Self.allValueTypesSet) { $0.intersection(Self.valueTypes(of: $1)) }
}

/// Evaluate the predicate with the value.
///
/// Ignore the error of mismatching type between the value and an operand and retruen `false
/// - note: Ignore the error of mismatching types between the value and an operand and return `false`
public func evaluate(with value: Any) throws -> Bool {
// if the type has already be invalidated, return false immediately
if mismatchedTypes.contains(type(of: value)) { return false }
let valueType = type(of: value)

// exit immediately if the operators do not support the value type
guard operatorsValueTypes.contains(valueType) else { return false }

do {
return try expression.evaluate(with: ["value": String(describing: value)])
} catch ExpressionError.mismatchingType {
mismatchedTypes.insert(type(of: value))
// error of mistmatching type for `valueType`. Remove it from the supported types
operatorsValueTypes.remove(valueType)
return false //ignore the error of wrong value type
} catch {
throw PathExplorerError.predicateError(description: error.localizedDescription)
throw PathExplorerError.predicateError(predicate: expression.description, description: error.localizedDescription)
}
}

func type(of value: Any) -> ValueType {
// use the initialisation from any allowing a string value
if let _ = try? Int(value: value) {
return .int
} else if let _ = try? Double(value: value) {
if let _ = try? Double(value: value) {
return .double
} else if let _ = try? Bool(value: value) {
return .bool
Expand All @@ -157,7 +161,31 @@ extension PathsFilter {

extension PathsFilter.Predicate {

enum ValueType: Hashable {
case string, int, double, bool
enum ValueType: Hashable, CaseIterable {
case string, double, bool
}

static var allValueTypesSet: Set<ValueType> {
Set(ValueType.allCases)
}
}

extension PathsFilter.Predicate {

static func valueTypes(of comparisonOperator: Operator) -> Set<ValueType> {
switch comparisonOperator {
case .equal, .nonEqual:
return allValueTypesSet

case .greaterThan, .greaterThanOrEqual, .lesserThan, .lesserThanOrEqual:
return [.double, .string]

case .contains, .isIn, .hasPrefix, .hasSuffix:
return [.string]

default:
assertionFailure("Operator not handled: \(comparisonOperator)")
return allValueTypesSet
}
}
}
8 changes: 8 additions & 0 deletions Sources/ScoutCLT/Main/ScoutCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,12 @@ extension ScoutCommand {
return xmlInjector
}
}

/// Try to get the regex from the pattern, throwing a `RuntimeError` when failing
func regexFrom(pattern: String) throws -> NSRegularExpression {
guard let regex = try? NSRegularExpression(pattern: pattern) else {
throw RuntimeError.invalidRegex(pattern)
}
return regex
}
}
2 changes: 2 additions & 0 deletions Sources/ScoutCLT/Models/RuntimeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum RuntimeError: LocalizedError {
case unknownFormat(String)
case completionScriptInstallation(description: String)
case invalidRegex(String)
case invalidArgumentsCombination(description: String)

var errorDescription: String? {
switch self {
Expand All @@ -19,6 +20,7 @@ enum RuntimeError: LocalizedError {
case .unknownFormat(let description): return description
case .completionScriptInstallation(let description): return "Error while installating the completion script. \(description)"
case .invalidRegex(let pattern): return "The regular expression '\(pattern)' is invalid"
case .invalidArgumentsCombination(let description): return description
}
}
}
48 changes: 38 additions & 10 deletions Sources/ScoutCLT/Paths/PathsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,60 @@ struct PathsCommand: ScoutCommand {

// MARK: - Properties

@Argument(help: .readingPath)
var readingPath: Path?
@Argument(help: "Initial path from which the paths should be listed")
var initialPath: Path?

@Option(name: [.short, .customLong("input")], help: "A file path from which to read the data", completion: .file())
var inputFilePath: String?

@Option(name: [.short, .customLong("key")], help: "Specify a regular expression to filter the keys")
var keyRegexPattern: String?
var keyRegexPattren: String?

@Flag(help: "")
@Option(name: [.short, .customLong("value")], help: "Specify a predicate to filter the values of the paths. Several predicates can be specified. A value validated by any of the predicates will be valid.")
var valuePredicates = [String]()

@Flag(help: "Target single values (stirng, number, bool), group values (array, dictionary), or both.")
var valueTarget = PathsFilter.ValueTarget.singleAndGroup

// MARK: - Functions

func inferred<P>(pathExplorer: P) throws where P: PathExplorer {
var pathsFilter = PathsFilter.targetOnly(valueTarget)
let valuePredicates = self.valuePredicates.isEmpty ? nil : try self.valuePredicates.map { try PathsFilter.Predicate(format: $0) }
var keyRegex: NSRegularExpression?

if let pattern = keyRegexPattren {
keyRegex = try regexFrom(pattern: pattern)
}

switch (keyRegex, valuePredicates, valueTarget) {

case (nil, nil, let target):
pathsFilter = .targetOnly(target)

case (.some(let regex), nil, let target):
pathsFilter = .key(regex: regex, target: target)

case (.some(let regex), .some(let predicates), nil):
pathsFilter = .keyAndValue(keyRegex: regex, valuePredicates: predicates)

case (nil, .some(let predicates), nil):
pathsFilter = .value(predicates)

case (nil, .some(let predicates), let target):
if target != .singleAndGroup {
throw RuntimeError.invalidArgumentsCombination(description: "Using the target flag is not allowed with the (--value|-v) option. Consider removing '--\(target.rawValue)'")
}
pathsFilter = .value(predicates)

if let keyRegexPattern = keyRegexPattern {
guard let regex = try? NSRegularExpression(pattern: keyRegexPattern) else {
throw RuntimeError.invalidRegex(keyRegexPattern)
case (.some(let regex), .some(let predicates), let target):
if target != .singleAndGroup {
throw RuntimeError.invalidArgumentsCombination(description: "Using the target flag is not allowed with the (--value|-v) option. Consider removing '--\(target.rawValue)'")
}
pathsFilter = .key(regex: regex, target: valueTarget)
pathsFilter = .keyAndValue(keyRegex: regex, valuePredicates: predicates)
}

let readingPath = self.readingPath ?? Path()
let paths = try pathExplorer.listPaths(startingAt: readingPath, filter: pathsFilter)
let paths = try pathExplorer.listPaths(startingAt: initialPath, filter: pathsFilter)

paths.forEach { print($0.flattened()) }
}
Expand Down
8 changes: 7 additions & 1 deletion Tests/ScoutTests/Models/PathsFilterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ final class PathsFilterTests: XCTestCase {

_ = try predicate.evaluate(with: "yo")

XCTAssertEqual(predicate.mismatchedTypes, Set(arrayLiteral: .string))
XCTAssertEqual(predicate.operatorsValueTypes, Set(arrayLiteral: .double))
}

func testPredicateMismatchedTypesReturnsFalse() throws {
let predicate = try PathsFilter.Predicate(format: "value > 10")

XCTAssertFalse(try predicate.evaluate(with: "yo"))
}

func testPredicateValueTypes() throws {
let predicate = try PathsFilter.Predicate(format: "!(value hasPrefix 'yo')")

XCTAssertFalse(try predicate.evaluate(with: 10))
}
}

0 comments on commit dbcd809

Please sign in to comment.