Skip to content

Commit

Permalink
Refactored PathsFilter
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexis Bridoux committed Feb 3, 2021
1 parent cfbad43 commit bd4e640
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 132 deletions.
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 5 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down
9 changes: 9 additions & 0 deletions Sources/Scout/Definitions/Path.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 0 additions & 29 deletions Sources/Scout/Definitions/PathElementFilter.swift

This file was deleted.

4 changes: 2 additions & 2 deletions Sources/Scout/Definitions/PathExplorer+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
5 changes: 2 additions & 3 deletions Sources/Scout/Definitions/PathExplorer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
111 changes: 111 additions & 0 deletions Sources/Scout/Definitions/PathsFilter.swift
Original file line number Diff line number Diff line change
@@ -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) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}
}
49 changes: 9 additions & 40 deletions Sources/Scout/Implementations/XML/PathExplorerXML+Paths.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -23,63 +23,32 @@ 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)
}

element.children.enumerated().forEach { (index, child) in
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)
}
}
}
Expand Down
Loading

0 comments on commit bd4e640

Please sign in to comment.