Merge pull request #264 from ABridoux/release/4.0.4

Release 4.0.4
This commit is contained in:
Alexis Bridoux 2021-08-01 12:03:24 +02:00 committed by GitHub
commit 49587c0822
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1717 additions and 284 deletions

View File

@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file. `Scout` adheres to [Semantic Versioning](http://semver.org).
---
## [4.0.4]( https://github.com/ABridoux/scout/tree/4.0.4) (01/08/2021)
### Changed
- Bumped Swift version to 5.4
- Added list version requirements in the Readme [#251]
### Fixed
- tvOS version requirement [#263]
## [4.0.3]( https://github.com/ABridoux/scout/tree/4.0.3) (06/06/2021)
### Fixed
- Jaro-Winkler crash when comparing two strings of single characters. [#253]

View File

@ -1,11 +1,11 @@
// swift-tools-version:5.3
// swift-tools-version:5.4
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Scout",
platforms: [.macOS("10.13"), .iOS("10.0"), .tvOS("9.0"), .watchOS("3.0")],
platforms: [.macOS("10.13"), .iOS("10.0"), .tvOS("10.0"), .watchOS("4.0")],
products: [
.library(
name: "Scout",
@ -55,7 +55,7 @@ let package = Package(
name: "ScoutCLTCore",
dependencies: [
"Scout", "Parsing"]),
.target(
.executableTarget(
name: "ScoutCLT",
dependencies: [
"Scout",

View File

@ -41,6 +41,14 @@ Supported formats:
- YAML
- XML
#### Minimum requirements
- Swift: 5.4+
- macOS: 10.13+
- iOS: 10.0+
- tvOS: 10.0+
- watchOS: 4.0+
## Summary
- [Why](#why)
- [Features](#features)

View File

@ -5,9 +5,9 @@
enum Folding {
/// Use to name the single key when folding a dictionary
/// Used to name the single key when folding a dictionary
static let foldedKey = "Folded"
/// Use to replace the content of a dictionary or array when folding it
/// Used to replace the content of a dictionary or array when folding it
static let foldedMark = "~~SCOUT_FOLDED~~"
}

View File

@ -4,5 +4,5 @@
// MIT license, see LICENSE file for details
public enum ScoutVersion {
public static let current = "4.0.3"
public static let current = "4.0.4"
}

View File

@ -103,6 +103,7 @@ extension ExplorerValue {
}
/// The headers for the array of dictionaries
///
/// #### Complexity
/// `O(n)` where `n` is the number of elements in the array
private func headers(in array: ArrayValue) -> Set<Path>? {

View File

@ -62,6 +62,8 @@ extension ExplorerValue {
/// Holds the logic to validate a path built during paths listing
private struct PathValidation {
let filter: PathsFilter
/// The path leading to the value
private(set) var leading: Path
private var isInitial = true
private var hasOneKeyValidated = false

View File

@ -11,6 +11,7 @@ import Foundation
/// - note: Default implementation provided for types conforming to `Encodable`
public protocol ExplorerValueRepresentable {
/// Convert `self` to an ``ExplorerValue``
func explorerValue() throws -> ExplorerValue
}
@ -18,10 +19,11 @@ public protocol ExplorerValueRepresentable {
/// - note: Default implementation provided for types conforming to `Decodable`
public protocol ExplorerValueCreatable {
/// Instantiate a new value from an ``ExplorerValue``
init(from explorerValue: ExplorerValue) throws
}
/// Can be represented as and instantiated from as `ExplorerValue`
/// Can be represented *as* and instantiated *from* an `ExplorerValue`
/// - note: Default implementation provided for types conforming to `Codable`
public typealias ExplorerValueConvertible = ExplorerValueRepresentable & ExplorerValueCreatable

View File

@ -30,6 +30,11 @@ public struct ExplorerXML: PathExplorer {
public var bool: Bool? { element.bool }
public var int: Int? { element.int }
public var double: Double? { element.double }
/// XML `date` element is always `nil`
///
/// Date types are not natively supported by XML
public var date: Date? { nil }
@available(*, deprecated, renamed: "double")
public var real: Double? { element.double }

View File

@ -5,6 +5,7 @@
import Foundation
/// Errors that can be thrown when exploring data using a ``PathExplorer``
public struct ExplorerError: LocalizedError, Equatable {
public private(set) var path: Path
let description: String
@ -58,30 +59,37 @@ extension ExplorerError {
public extension ExplorerError {
/// The provided value is not convertible to an ``ExplorerValue``
static func invalid(value: Any) -> Self {
ExplorerError(description: "The value \(value) is not convertible to ExplorerValue")
}
/// The key used to subscript is missing. A best match in the existing key is provided if one is found.
static func missing(key: String, bestMatch: String?) -> Self {
ExplorerError(description: "Missing key '\(key)' in dictionary. Best match found: '\(bestMatch ?? "none")'")
}
/// Trying to subscript something with a key although it's not a dictionary
static var subscriptKeyNoDict: Self {
ExplorerError(description: "The value cannot be subscripted with a string as it is not a dictionary")
}
/// The provided index is out of bounds to subscript the array
static func wrong(index: Int, arrayCount: Int) -> Self {
ExplorerError(description: "Index \(index) out of bounds to subscript the array with \(arrayCount) elements")
}
/// Trying to subscript something with an index although it's not an array
static var subscriptIndexNoArray: Self {
ExplorerError(description: "The value cannot be subscripted with an index as it is not an array")
}
/// The ``PathElement`` is misplaced or forbidden for the feature
static func wrongUsage(of element: PathElement) -> Self {
return ExplorerError(description: "The element \(element.kindDescription) \(element) cannot be used here. \(element.usage)")
}
/// The bounds provided to the ``PathElement/slice(_:)`` element are not valid to slice the array.
static func wrong(bounds: Bounds, arrayCount: Int) -> Self {
let description =
"""
@ -93,14 +101,19 @@ public extension ExplorerError {
return ExplorerError(description: description)
}
/// The regular expression pattern is invalid
static func wrong(regexPattern: String) -> Self {
ExplorerError(description: "The string '\(regexPattern)' is not a valid regular expression pattern")
}
/// The conversion from an ``ExplorerValue`` to the provided type has failed
static func mismatchingType<T>(_ type: T.Type, value: ExplorerValue) -> Self {
ExplorerError(description: "ExplorerValue '\(value)' cannot be represented as \(T.self)")
}
/// The predicate in invalid.
///
/// For instance, a `String` value is evaluated against a predicate taking an `Int` as input
static func predicateNotEvaluatable(_ predicate: String, description: String) -> Self {
ExplorerError(description: #"Unable to evaluate the predicate "\#(predicate)". \#(description)"#)
}

View File

@ -5,60 +5,6 @@
import Foundation
// MARK: - Storage
private struct IndexedSlice {
var index: Int
var lowerBound: Int
var upperBound: Int
}
private struct Indexed<Value> {
var index: Int
var value: Value
}
private struct IndexedCollection<Value>: Collection {
typealias Element = Indexed<Value>
var elements: [Element]
var startIndex: Int { elements.startIndex }
var endIndex: Int { elements.endIndex }
init() {
elements = []
}
subscript(position: Int) -> Element { elements[position] }
func index(after i: Int) -> Int { elements.index(after: i) }
func makeIterator() -> IndexingIterator<[Element]> {
elements.makeIterator()
}
mutating func append(index: Int, value: Value) {
elements.append(.init(index: index, value: value))
}
mutating func popLast() -> Element? { elements.popLast() }
mutating func removeAll() { elements.removeAll() }
}
private struct IndexedElements {
var indexes = IndexedCollection<Int>()
var slices = [IndexedSlice]()
var keys = IndexedCollection<String>()
var filters = IndexedCollection<String>()
}
private extension Array where Element == IndexedSlice {
mutating func append(index: Int, lower: Int, upper: Int) {
append(.init(index: index, lowerBound: lower, upperBound: upper))
}
}
// MARK: - Functions
extension Path {
@ -66,7 +12,7 @@ extension Path {
/// Compute the path by changing the special path elements like slices or filters
///
/// Filters are changed to the key they correspond to. Slices are changed to indexes.
/// #### Complexity
/// ### Complexity
/// O(n) with `n` the count of elements in the path
public func flattened() -> Path {
var indexedElements = getIndexedElements()
@ -89,6 +35,11 @@ extension Path {
return Path(newPath)
}
}
// MARK: Helpers
extension Path {
/// Parse the path and store the relevant elements with their indexes
private func getIndexedElements() -> IndexedElements {
@ -184,3 +135,53 @@ extension Path {
}
}
}
// MARK: - Storage models
private struct IndexedSlice {
var index: Int
var lowerBound: Int
var upperBound: Int
}
private struct Indexed<Value> {
var index: Int
var value: Value
}
private struct IndexedCollection<Value>: Collection {
typealias Element = Indexed<Value>
var elements: [Element] = []
var startIndex: Int { elements.startIndex }
var endIndex: Int { elements.endIndex }
subscript(position: Int) -> Element { elements[position] }
func index(after i: Int) -> Int { elements.index(after: i) }
func makeIterator() -> IndexingIterator<[Element]> {
elements.makeIterator()
}
mutating func append(index: Int, value: Value) {
elements.append(.init(index: index, value: value))
}
mutating func popLast() -> Element? { elements.popLast() }
mutating func removeAll() { elements.removeAll() }
}
private struct IndexedElements {
var indexes = IndexedCollection<Int>()
var slices = [IndexedSlice]()
var keys = IndexedCollection<String>()
var filters = IndexedCollection<String>()
}
private extension Array where Element == IndexedSlice {
mutating func append(index: Int, lower: Int, upper: Int) {
append(.init(index: index, lowerBound: lower, upperBound: upper))
}
}

View File

@ -36,12 +36,15 @@ public extension Collection where Element == PathElement {
public extension Collection where SubSequence == Slice<Path> {
/// The greatest prefix that both paths have in common
func commonPrefix(with otherPath: Self) -> Slice<Path> {
var iterator = makeIterator()
var otherIterator = otherPath.makeIterator()
var lastIndex = 0
while let element = iterator.next(), let otherElement = otherIterator.next(), element == otherElement {
while
let element = iterator.next(), let otherElement = otherIterator.next(),
element == otherElement {
lastIndex += 1
}

View File

@ -8,7 +8,8 @@ import Foundation
public extension Collection where Element == PathElement {
/// Prints all the elements in the path, with the default separator
/// #### Complexity
///
/// ### Complexity
/// O(n) where `n`: element's count
var description: String {
var description = reduce(into: "", newDescription)

View File

@ -5,7 +5,7 @@
import Foundation
/// Collection of `PathElement`s to subscript a `PathExplorer`
/// Collection of ``PathElement``s to subscript a `PathExplorer`
public struct Path: Hashable {
// MARK: - Constants
@ -17,28 +17,27 @@ public struct Path: Hashable {
private var elements: [PathElement] = []
/// An empty `Path`
public static var empty: Path { .init() }
// MARK: - Initialization
/// Instantiate a `Path` for a string representing path components separated with the separator.
///
/// ### Example with default separator '.'
/// ## Example with default separator '.'
///
/// `computers[2].name` will make the path `["computers", 2, "name"]`
///
/// `computer.general.serial_number` will make the path `["computer", "general", "serial_number"]`
///
/// `company.computers[#]` will make the path `["company", "computers", PathElement.count]`
/// - `computers[2].name` will make the path `["computers", 2, "name"]`
/// - `computer.general.serial_number` will make the path `["computer", "general", "serial_number"]`
/// - `company.computers[#]` will make the path `["company", "computers", PathElement.count]`
///
/// - parameter string: The string representing the path
/// - parameter separator: The separator used to split the string. Default is ".".
///
/// ### Brackets
/// When enclosed with brackets, a path element will not be parsed. For example ```computer.(general.information).serial_number```
/// ## Brackets
/// When enclosed with brackets, a path element will not be parsed. For example `computer.(general.information).serial_number`
/// will make the path ["computer", "general.information", "serial_number"]
///
/// ### Excluded separators
/// ## Excluded separators
/// The following separators will not work: '[', ']', '(', ')'.
public init(string: String, separator: String = Self.defaultSeparator) throws {
if Self.forbiddenSeparators.contains(separator) { throw PathError.invalidSeparator(separator) }

View File

@ -7,6 +7,7 @@ import Foundation
extension PathElement {
/// Placed after an array to slice it with a `Bounds` value
public static func slice(_ lower: Bounds.Bound, _ upper: Bounds.Bound) -> PathElement {
.slice(Bounds(lower: lower, upper: upper))
}

View File

@ -5,7 +5,7 @@
import Foundation
/// Store the possible elements that can be used to subscript a `PathExplorer`
/// The possible elements that can be used to subscript a ``PathExplorer``
public enum PathElement: Hashable {
// MARK: - Constants
@ -50,7 +50,7 @@ public enum PathElement: Hashable {
}
}
public var usage: String {
var usage: String {
switch self {
case .key: return "A key subscript a dictionary and is specified with a dot '.' then the key name like 'dictionary.keyName'"
case .index: return "An index subscript an array and is specified as an integer enclosed with square brackets like '[1]'"

View File

@ -3,7 +3,7 @@
// Copyright (c) 2020-present Alexis Bridoux
// MIT license, see LICENSE file for details
/// Protocol to allow to subscript a `PathExplorer` without using directly the `PathElement` enum.
/// Protocol to allow to subscript a `PathExplorer` without using directly the ``PathElement`` enum.
///
/// As `PathElement` already conforms to `ExpressibleByStringLiteral` and `ExpressibleByIntegerLiteral`,
/// it is possible to instantiate a Path without the need of using the `PathElementRepresentable` protocol:
@ -12,7 +12,7 @@
/// ```
/// But the "Expressible" protocols do not allow to do the same with variables.
/// Thus, using `PathElementRepresentable` allows to instantiate a Path from a mix of Strings and Integers variables:
/// ```
/// ```swift
/// let tom = "Tom"
/// let hobbies = "hobbies"
/// let index = 1

View File

@ -7,7 +7,7 @@ import Foundation
/// A collection of paths arranged following their common prefixes.
///
/// Useful when building a PathExplorer from a list of paths to reuse the last created explorer
/// Useful when building a `PathExplorer` from a list of paths to reuse the last created explorer
/// to add children to it (rather than starting again from the root each time).
final class PathTree<Value: Equatable> {
@ -30,7 +30,7 @@ final class PathTree<Value: Equatable> {
return nil
}
// MARK: - Initialisation
// MARK: - Initialization
init(value: ValueType, element: PathElement) {
self.value = value

View File

@ -5,7 +5,9 @@
import Foundation
/// A `PathExplorer` using an `ExplorerValue` and can be encoded/decoded with the provided `CodableFormat`
/// A concrete implementation of `PathExplorer` with a specific ``CodableFormat``.
///
/// - note: Mainly a wrapper around ``ExplorerValue`` to offer a unified interface for all `Codable` `PathExplorer`s
public struct CodablePathExplorer<Format: CodableFormat>: PathExplorer {
// MARK: - Properties
@ -19,6 +21,7 @@ public struct CodablePathExplorer<Format: CodableFormat>: PathExplorer {
public var real: Double? { value.real }
public var double: Double? { value.double }
public var data: Data? { value.data }
public var date: Date? { value.date }
public func array<T>(of type: T.Type) throws -> [T] where T: ExplorerValueCreatable { try value.array(of: type) }
public func dictionary<T>(of type: T.Type) throws -> [String: T] where T: ExplorerValueCreatable { try value.dictionary(of: type) }

View File

@ -9,7 +9,8 @@ import Foundation
protocol EquatablePathExplorer: PathExplorer {
/// `true` when self is equal to the provided other element.
/// #### Complexity
///
/// ### Complexity
/// Most often `O(n)` where `n` is the children count.
func isEqual(to other: Self) -> Bool
}

View File

@ -7,6 +7,7 @@ import Foundation
public extension PathExplorer {
/// Same as ``init(value:name:)`` with a default `nil` value for `name`
init(value: ExplorerValue) {
self.init(value: value, name: nil)
}
@ -17,18 +18,10 @@ public extension PathExplorer {
public extension PathExplorer {
/// Get the key at the given path
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
func get(_ path: [PathElement]) throws -> Self { try get(Path(path)) }
/// Get the key at the given path
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
func get(_ path: PathElement...) throws -> Self { try get(path) }
}
@ -40,33 +33,18 @@ public extension PathExplorer {
// MARK: Mutating
/// Set the value of the key at the given path
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
mutating func set(_ path: PathElement..., to newValue: ExplorerValue) throws { try set(Path(path), to: newValue) }
// MARK: Mutating ExplorerValueRepresentable
/// Set the value of the key at the given path
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// Set the provided `ExplorerValueRepresentable`value of the key at the given path
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key), or if the `newValue.explorerValue()` function fails
mutating func set(_ path: Path, to newValue: ExplorerValueRepresentable) throws {
try set(path, to: newValue.explorerValue())
}
/// Set the value of the key at the given path
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// Set the provided `ExplorerValueRepresentable`value of the key at the given path
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key), or if the `newValue.explorerValue()` function fails
mutating func set(_ path: PathElement..., to newValue: ExplorerValueRepresentable) throws {
try set(Path(path), to: newValue.explorerValue())
@ -75,29 +53,14 @@ public extension PathExplorer {
// MARK: Non mutating
/// Set the value of the key at the given path and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
func setting(_ path: PathElement..., to newValue: ExplorerValue) throws -> Self { try setting(Path(path), to: newValue) }
/// Set the value of the key at the given path and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key), or if the `newValue.explorerValue()` function fails
func setting(_ path: Path, to newValue: ExplorerValueRepresentable) throws -> Self { try setting(path, to: newValue.explorerValue()) }
/// Set the value of the key at the given path and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key), or if the `newValue.explorerValue()` function fails
func setting(_ path: PathElement..., to newValue: ExplorerValueRepresentable) throws -> Self { try setting(Path(path), to: newValue.explorerValue()) }
}
@ -107,38 +70,18 @@ public extension PathExplorer {
public extension PathExplorer {
/// Set the name of the key at the given path
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary)
mutating func set(_ path: [PathElement], keyNameTo newKeyName: String) throws { try set(Path(path), keyNameTo: newKeyName) }
/// Set the name of the key at the given path
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary)
mutating func set(_ path: PathElement..., keyNameTo newKeyName: String) throws { try set(path, keyNameTo: newKeyName) }
/// Set the name of the key at the given path, and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary)
func setting(_ path: [PathElement], keyNameTo newKeyName: String) throws -> Self { try setting(Path(path), keyNameTo: newKeyName) }
/// Set the name of the key at the given path, and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary)
func setting(_ path: PathElement..., keyNameTo newKeyName: String) throws -> Self { try setting(path, keyNameTo: newKeyName) }
}
@ -148,51 +91,26 @@ public extension PathExplorer {
public extension PathExplorer {
/// Delete the key at the given path.
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - parameter deleteIfEmpty: When `true`, the dictionary or array holding the value will be deleted too if empty after the key deletion. Default: `false`
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
mutating func delete(_ path: Path) throws { try delete(Path(path), deleteIfEmpty: false) }
/// Delete the key at the given path.
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - parameter deleteIfEmpty: When `true`, the dictionary or array holding the value will be deleted too if empty after the key deletion. Default: `false`
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
mutating func delete(_ path: [PathElement], deleteIfEmpty: Bool = false) throws { try delete(Path(path), deleteIfEmpty: deleteIfEmpty) }
/// Delete the key at the given path.
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - parameter deleteIfEmpty: When `true`, the dictionary or array holding the value will be deleted too if empty after the key deletion. Default: `false`
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
mutating func delete(_ path: PathElement..., deleteIfEmpty: Bool = false) throws { try delete(path, deleteIfEmpty: deleteIfEmpty) }
/// Delete the key at the given path and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - parameter deleteIfEmpty: When `true`, the dictionary or array holding the value will be deleted too if empty after the key deletion. Default: `false`
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
func deleting(_ path: [PathElement], deleteIfEmpty: Bool = false) throws -> Self { try deleting(Path(path), deleteIfEmpty: deleteIfEmpty) }
/// Delete the key at the given path and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - parameter deleteIfEmpty: When `true`, the dictionary or array holding the value will be deleted too if empty after the key deletion. Default: `false`
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
func deleting(_ path: PathElement..., deleteIfEmpty: Bool = false) throws -> Self { try deleting(path, deleteIfEmpty: deleteIfEmpty) }
@ -206,36 +124,21 @@ public extension PathExplorer {
/// Add a value at the given path.
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// #### Appending
/// To add a key at the end of an array, specify the `PathElement.count`
///
/// ### Non-existing key
/// Any non existing key encountered in the path will be created.
/// ### Appending
/// To add a key at the end of an array, specify ``PathElement/count``
mutating func add(_ value: ExplorerValue, at path: PathElement...) throws { try add(value, at: Path(path)) }
/// Add a value at the given path.
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// #### Appending
/// To add a key at the end of an array, specify the `PathElement.count`
/// ### Appending
/// To add a key at the end of an array, specify ``PathElement/count``
/// - Throws: If the `newValue.explorerValue` function fails
mutating func add(_ value: ExplorerValueRepresentable, at path: Path) throws { try add(value.explorerValue(), at: path) }
/// Add a value at the given path.
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// #### Appending
/// To add a key at the end of an array, specify the `PathElement.count`
/// ### Appending
/// To add a key at the end of an array, specify ``PathElement/count``
/// - Throws: If the `newValue.explorerValue()` function fails
mutating func add(_ value: ExplorerValueRepresentable, at path: PathElement...) throws { try add(value.explorerValue(), at: Path(path)) }
@ -243,36 +146,21 @@ public extension PathExplorer {
/// Add a value at the given path, and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// #### Appending
/// To add a key at the end of an array, specify the `PathElement.count`
///
/// ### Non-existing key
/// Any non existing key encountered in the path will be created.
/// ### Appending
/// To add a key at the end of an array, specify ``PathElement/count``
func adding(_ value: ExplorerValue, at path: PathElement...) throws -> Self { try adding(value, at: Path(path)) }
/// Add a value at the given path, and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// #### Appending
/// To add a key at the end of an array, specify the `PathElement.count`
/// ### Appending
/// To add a key at the end of an array, specify ``PathElement/count``
/// - Throws: If the `newValue.explorerValue()` function fails
func adding(_ value: ExplorerValueRepresentable, at path: Path) throws -> Self { try adding(value.explorerValue(), at: path) }
/// Add a value at the given path, and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// #### Appending
/// To add a key at the end of an array, specify the `PathElement.count`
/// ### Appending
/// To add a key at the end of an array, specify ``PathElement/count``
/// - Throws: If the `newValue.explorerValue()` function fails
func adding(_ value: ExplorerValueRepresentable, at path: PathElement...) throws -> Self { try adding(value.explorerValue(), at: Path(path)) }
}

View File

@ -5,7 +5,7 @@
import Foundation
/// Wrap different structs to explore several format: Json, Plist and Xml
/// Wrap several structs to explore several format: Json, Plist, YAML and Xml
public protocol PathExplorer: CustomStringConvertible, CustomDebugStringConvertible,
ExpressibleByStringLiteral,
ExpressibleByBooleanLiteral,
@ -38,6 +38,9 @@ where
/// Non `nil` if the key is of the `Data` type
var data: Data? { get }
/// Non `nil` if the key is of the `Date` type
var date: Date? { get }
/// An array of the provided type
func array<T: ExplorerValueCreatable>(of type: T.Type) throws -> [T]
@ -56,77 +59,43 @@ where
// MARK: - Initialization
/// - Parameters:
/// - value: The value the explorer will take
/// - name: Optionally provide a name for a root element with Xml explorers
init(value: ExplorerValue, name: String?)
// MARK: - Get
/// Get the key at the given path
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
func get(_ path: Path) throws -> Self
// MARK: - Set
/// Set the value of the key at the given path
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
mutating func set(_ path: Path, to newValue: ExplorerValue) throws
/// Set the value of the key at the given path and returns a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
/// - note: The type of the `value` parameter will be automatically inferred. To force the `value`type, use the parameter `as type`
func setting(_ path: Path, to newValue: ExplorerValue) throws -> Self
// MARK: - Set key name
/// Set the name of the key at the given path
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary)
mutating func set(_ path: Path, keyNameTo newKeyName: String) throws
/// Set the name of the key at the given path, and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary)
func setting(_ path: Path, keyNameTo keyName: String) throws -> Self
// MARK: - Delete
/// Delete the key at the given path.
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - parameter deleteIfEmpty: When `true`, the dictionary or array holding the value will be deleted too if empty after the key deletion. Default: `false`
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
mutating func delete(_ path: Path, deleteIfEmpty: Bool) throws
/// Delete the key at the given path and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element of an array.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// - parameter deleteIfEmpty: When `true`, the dictionary or array holding the value will be deleted too if empty after the key deletion. Default: `false`
/// - Throws: If the path is invalid (e.g. a key does not exist in a dictionary, or indicating an index on a non-array key)
func deleting(_ path: Path, deleteIfEmpty: Bool) throws -> Self
@ -135,25 +104,14 @@ where
/// Add a value at the given path.
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// #### Appending
/// To add a key at the end of an array, specify the `PathElement.count`
/// To add a key at the end of an array, specify ``PathElement/count``
mutating func add(_ value: ExplorerValue, at path: Path) throws
/// Add a value at the given path, and return a new modified `PathExplorer`
///
/// #### Negative index
/// It's possible to specify a negative index to target the last nth element.
/// For example, -1 targets the last element and -3 the last 3rd element.
///
/// #### Appending
/// ### Appending
/// To add a key at the end of an array, specify the `PathElement.count`
///
/// ### Non-existing key
/// Any non existing key encountered in the path will be created.
func adding(_ value: ExplorerValue, at path: Path) throws -> Self
// MARK: - Paths listing
@ -161,6 +119,6 @@ where
/// Returns all the paths leading to single or group values
/// - Parameters:
/// - initialPath: Scope the returned paths with this path as a starting point
/// - filter: Optionally provide a filter on the key and/or value. Default is `noFilter`
/// - filter: Optionally provide a filter on the key and/or value. Default is ``PathsFilter/noFilter``
func listPaths(startingAt initialPath: Path?, filter: PathsFilter) throws -> [Path]
}

View File

@ -5,7 +5,9 @@
import Foundation
/// Namespace to find all PathExplorers in a single place
/// Namespace to find all default PathExplorers in a single place
///
/// Use default explorers for a format: `PathExplorers.Json`, `PathExplorers.Xml`...
public enum PathExplorers {
public typealias Json = CodablePathExplorer<CodableFormats.JsonDefault>

View File

@ -8,8 +8,12 @@ import Foundation
/// A `PathExplorer` which can be instantiated from data and export itself to another format
public protocol SerializablePathExplorer: PathExplorer {
/// The `DataFormat` of the serializable `PathExplorer`: JSON, Plist, XML, or YAML
static var format: DataFormat { get }
/// Initialize a new ``PathExplorer`` from the `Data`
///
/// - Throws: If the data cannot be serialized into the format
init(data: Data) throws
/// Export the path explorer value to data
@ -18,12 +22,14 @@ public protocol SerializablePathExplorer: PathExplorer {
/// Export the path explorer value to a String
///
/// - note: The single values will be exported correspondingly to the data format.
/// For instance: `<string>Hello</string>` and not ust `Hello`.
/// To get only the value of the `PathExplorer` without the data , use `description`
/// For instance: `<string>Hello</string>` and not `Hello`.
/// To get only the value of the `PathExplorer` without the format , use `description`
/// or the corresponding type (e.g. `pathExplorer.int` or `pathExplorer.bool`)
func exportString() throws -> String
/// Export the path explorer value to a CSV if possible. Use the default separator ';' if none specified
/// Export the path explorer value to a CSV if possible, using the provided separator.
///
/// - note: Not all values are exportable to CSV. For instance, a three dimensions array is not exportable, neither an array of heterogeneous dictionaries.
func exportCSV(separator: String?) throws -> String
/// Export the path explorer value to the specified format data with a default root name "root"
@ -32,24 +38,38 @@ public protocol SerializablePathExplorer: PathExplorer {
/// Export the path explorer value to the specified format string data with a default root name "root"
func exportString(to format: DataFormat, rootName: String?) throws -> String
/// Returns a new explorer from the provided CSV string when it's possible. Throws otherwise.
/// Returns a new explorer from the provided CSV string when it's possible.
/// - Parameters:
/// - string: The CSV as `String`
/// - separator: The `Character` used as separator in the CSV string
/// - hasHeaders: Specify whether the CSV string has named headers. Named headers can be full ``Path``s to structure the explorer
///
/// - Returns: A `SerializablePathExplorer` from the provided CSV
/// - Throws: If the CSV cannot be converted to Self
static func fromCSV(string: String, separator: Character, hasHeaders: Bool) throws -> Self
/// New explorer replacing the group values (array or dictionaries) sub values by a unique one
/// holding a fold mark to be replaced when exporting the string value.
/// - note: Use `exportFoldedString(upTo:)` to directly get the string value
/// - note: Use ``exportFoldedString(upTo:)`` to directly get the string value
func folded(upTo level: Int) -> Self
/// Folded explored description, replacing the group values (array or dictionaries) sub values by a single string "..."
///
/// - Important: To be used only for display purpose as the returned string will not have a proper format
func exportFoldedString(upTo level: Int) throws -> String
}
extension SerializablePathExplorer {
var defaultCSVSeparator: String { ";" }
var nullCSVValue: String { "NULL" }
}
public extension SerializablePathExplorer {
var defaultCSVSeparator: String { ";" }
var nullCSVValue: String { "NULL" }
/// Export the path explorer value to a CSV if possible. Use the default separator ';' if none specified
/// Export the path explorer value to a CSV if possible. Using the default separator ';'
///
/// - note: Not all values are exportable to CSV. For instance, a three dimensions array is not exportable, neither an array of heterogeneous dictionaries.
func exportCSV() throws -> String {
try exportCSV(separator: nil)
}

View File

@ -63,7 +63,7 @@ extension Path.ElementParsers {
extension Path {
/// Parse a `Path`
/// Parse a `Path` from a provided `String`
/// - Parameters:
/// - separator: Separator between keys elements
/// - keyForbiddenCharacters: Optionally prevent characters to be parsed in a key name

View File

@ -5,6 +5,7 @@
import Foundation
/// Provided to the ``PathExplorer/listPaths(startingAt:filter:)-4tkeq`` function to target specific paths when listing them.
public enum PathsFilter {
/// No filter on key or value.
case targetOnly(ValueTarget)

View File

@ -17,13 +17,13 @@ public protocol ValuePredicate {
extension PathsFilter {
/// Specify a boolean expression to filter the value
/// Specify a `String` boolean 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 BooleanExpressionEvaluation.Expression
/// - note: Public wrapper around `BooleanExpressionEvaluation/Expression`
public final class ExpressionPredicate: ValuePredicate {
private(set) var expression: Expression

View File

@ -0,0 +1,63 @@
# Custom types with ExplorerValue
Learn more about the back bone of the serializable ``PathExplorer``s and understand how you can use it to inject your own types when setting or adding values.
## Meet ExplorerValue
`ExplorerValue` is a type implementing the ``PathExplorer`` protocol. As it implements `Codable` too, it can be used as a `PathExplorer` as long as a coder exists for the data format. Thus, JSON, Plist and YAML `PathExplorer`s can use the `ExplorerValue` type to get simple conformance to the protocol.
That's why ``CodablePathExplorer`` is mainly a wrapper around `ExplorerValue` to provide a generic structure implementing `PathExplorer`. ``PathExplorers/Json``, ``PathExplorers/Plist`` and ``PathExplorers/Yaml`` are simply type aliases for `CodablePathExplorer`, and differs only by the generic type ``CodableFormat``.
As `ExplorerValue` conforms to `Codable`, it's possible to provide a custom `Encoder` or `Decoder` rather than using the default ones coming with the ``PathExplorers`` namespace. This allows to specify date coding strategies for example, or to support new data formats in a blink of an eye with a dedicated Encoder/Decoder.
## ExplorerValueCreatable
To take things further, it's also possible to convert any type to an `ExplorerValue` with ``ExplorerValueRepresentable``. This protocol's only requirement is a function that returns an `ExplorerValue`. This way, it's possible to set or add a value of a custom type with a `PathExplorer`.
It's worth to note that making a type conform to `Encodable` is enough to make it `ExplorerValueRepresentable` too. A value of this type will be *encoded* to an `ExplorerValue`. Thus, using the following structure:
```swift
struct Record: Codable, ExplorerValueConvertible {
var name: String
var score: Int
}
```
It's possible to set a `Record` value with any `PathExplorer`
```swift
let record = Record(name: "Riri", score: 20)
// plist: CodablePathExplorer<PlistFormat>
try plist.set("ducks", "records", 0, to: record)
```
> Note: Even if primitive types conform to `Encodable`, it would be less efficient to encode them. A simpler implementation for `ExplorerValueRepresentable` is provided. The same goes for an array of a primitive type and for a dictionary where `Value` is a primitive type.
## ExplorerValueCreatable
The counterpart of `ExplorerValueRepresentable` is ``ExplorerValueCreatable``. Types conforming to this protocol declare an initialization from an `ExplorerValue`. This allows to export the value of an `ExplorerValue` to the type.
> Tip: Similarly with `ExplorerValueRepresentable`, a default implementation is provided for primitive types and types conforming to `Decodable`.
With the `Record` structure from above,
```swift
struct Record: Codable, ExplorerValueConvertible {
var name: String
var score: Int
}
```
it's possible to try to export a value of a `PathExplorer` as an array of `Record`s with ``PathExplorer/array(of:)``
```swift
// plist: CodablePathExplorer<PlistFormat>
let records = try plist.get("Riri", "records").array(of: Record.self)
```
## ExplorerValueConvertible
``ExplorerValueConvertible`` is simply a type alias for both `ExplorerValueRepresentable` and `ExplorerValueCreatable` protocols.

View File

@ -0,0 +1,127 @@
# Getting started with Scout
Quickly learn how to use Scout's features.
## Overview
Scout uses types conforming to the protocols ``PathExplorer`` and ``SerializablePathExplorer`` to read and manipulate data. If it's possible to define your own types conforming to those protocols, it's also possible to use default implementations if they suit your needs. Those explorers can be found in ``PathExplorers`` and will be used for the examples in this article.
The provided examples will reference this "People" file (here as YAML).
> Note: The full "People" files are used to try Scout and can be found in the Playground folder.
```yaml
Robert:
age: 23
height: 181
hobbies:
- video games
- party
- tennis
Suzanne:
job: actress
movies:
- awards: Best speech for a silent movie
title: Tomorrow is so far
- awards: Best title
title: Yesterday will never go
- title: What about today?
Tom:
age: 68
height: 175
hobbies:
- cooking
- guitar
```
## Create a PathExplorer
The simplest way to read data in any of the supported format is to use one of the ``PathExplorers`` implementation and to call ``SerializablePathExplorer/init(data:)``.
For instance, let's imagine that the file is read and converted to a `Data` value. Here's how to make an explorer for the YAML format.
```swift
let yaml = try PathExplorers.Yaml(data: data)
```
Similarly, if the format was Plist:
```swift
let plist = try PathExplorers.Plist(data: data)
```
## Navigate through data
It's then possible to use the ``PathExplorer/get(_:)-2ghf1`` method to read the "height" value in the "Tom" dictionary.
```swift
let tomHeightYaml = try yaml.get("Tom", "height")
```
This will return a new `PathExplorers.Yaml`. That's the logic of Scout: when reading, setting, deleting or adding values in a `PathExplorer`, the returned value will be another `PathExplorer`. This allows to keep performing operation easily. When the explorer has the right value, use one of the several options to access its value. For instance here to get Tom's height as a `Double`
```swift
let tomHeight = tomHeightYaml.double
// tomHeight: Double?
```
More concisely, if you are only interested into getting Tom's height, you could write
```swift
let tomHeight = try yaml.get("Tom", "height").double
```
> Note: As you might have noticed, calling `get()` can throw an error. This is the case for most `PathExplorer` functions. Whenever an element in the provided path does not exist, for instance an index out of bounds, or a missing key, a relevant error will be thrown.
As a last example, here's how to read Robert first hobby inside an array:
```swift
let robertFirstHobby = try yaml.get("Robert", "hobbies", 0)
```
> Tip: Use negative indexes to specify an index from the *end* of the array.
To lean more about ``Path``s and how they can help you targeting specific pieces of data, you can read <doc:mastering-paths>.
## Manipulate data
Using the same logic, it's possible to set, delete or add values.
For instance, to set Robert's age to 45 with the ``PathExplorer/set(_:to:)-9d877`` function:
```swift
var yaml = try PathExplorers.Yaml(data: data)
try yaml.set("Robert", "age", to: 45)
```
Or to add a new key named "score" with a value of 25 to Tom's dictionary with the ``PathExplorer/add(_:at:)-2kii6`` function:
```swift
var yaml = try PathExplorers.Yaml(data: data)
try yaml.add(25, at: "Tom", "score")
```
Those modifications all have specificities, like the "delete" one that can also delete an array or dictionary when left empty. To get more information about those features, please refer to ``PathExplorer``.
Also, it's worth mentioning that there are counterparts of those functions that will rather return a modified copy of the explorer. This is useful to chain operations.
For instance, to set Tom's height to 160, add a new surname key to Robert's dictionary and remove Suzanne second movie:
```swift
let yaml = try PathExplorers.Yaml(data: data)
let result = yaml
.setting("Tom", "height", to: 160)
.adding("Bob", to: "Robert", "surname")
.deleting("Suzanne", "movies", 1)
```
> Note: Using plain strings, numbers and booleans is made possible because ``PathElement`` implements `ExpressibleByStringLiteral` and `ExpressibleByIntLiteral`.
## Export the results
If ``PathExplorer`` is used to navigate through data, the protocol ``SerializablePathExplorer`` refines it to offer import and export options.
Once you are satisfied with the resulting `SerializablePathExplorer` - regardless of the operations you performed - it's possible to export the explorer as a `Data` value or to another format.
To export it to a `Data` value, use ``SerializablePathExplorer/exportData()`` function.
When needed, it's possible to specify another format when exporting: for instance, if a plist was decoded from a file and has to be converted to a JSON format. See ``SerializablePathExplorer/exportData(to:)`` for more informations.
Similarly, other export features are available like export to a `String` or to a CSV string.

View File

@ -0,0 +1,250 @@
# Mastering Paths
``Path``s are provided to a ``PathExplorer`` to navigate through or manipulate data precisely.
## Overview
Basically, a `Path` is a collection of ``PathElement``s in a specific order. The sequence of `PathElement`s lets the explorer know what value to target next. When navigating to a value is not possible, the explorer will throw an error.
The examples in this article will refer to this json file, stored in a ``PathExplorers/Json`` value referred to as `json`.
> Note: The full "People" files are used to try Scout and can be found in the Playground folder.
```json
{
"Tom" : {
"age" : 68,
"hobbies" : [
"cooking",
"guitar"
],
"height" : 175
},
"Robert" : {
"age" : 23,
"hobbies" : [
"video games",
"party",
"tennis"
],
"running_records" : [
[
10,
12,
9,
10
],
[
9,
12,
11
]
],
"height" : 181
},
"Suzanne" : {
"job" : "actress",
"movies" : [
{
"title" : "Tomorrow is so far",
"awards" : "Best speech for a silent movie"
},
{
"title" : "Yesterday will never go",
"awards" : "Best title"
},
{
"title" : "What about today?"
}
]
}
}
```
## Basics
The simplest `PathElement`s are ``PathElement/key(_:)`` and ``PathElement/index(_:)``. As their name suggest, they are used to target a key in a dictionary or an index in an array.
A `Path` can be instantiated from `PathElement`s in an array or as variadic parameters. Then the path can be provided to the `PathExplorer` to read or modify a value. Here are some examples with variadic parameters.
- Make a `Path` targeting Robert's second hobby
```swift
let path = Path(elements: "Robert", "hobbies", 1)
let secondHobby = try json.get(path: path).string
print(secondHobby)
// "party"
```
> The `PathExplorer` functions always offer convenience versions to use `PathElement` directly. This is useful to avoid creating a `Path` when it does not already exist or when having a more "scripting" approach.
- Make a `Path` targeting Suzanne's first movie title
```swift
Path(elements: "Suzanne", "movies", 0, "title")
```
With indexes, it's possible to use negative numbers to target indexes *from the end* of the array.
For instance to target Suzanne's last movie:
```swift
Path(elements: "Suzanne", "movies", -1)
```
The following `ducks` array shows how negative indexes are handled with `PathElement.index`
```
["Riri", "Fifi", "Loulou", "Donald", "Daisy"]
[ 0 , 1 , 2 , 3 , 4 ] (Positive)
[ -5 , -4 , -3 , -2 , -1 ] (Negative)
```
- `ducks[1]` targets "Fifi"
- `ducks[-2`] targets "Donald"
## Group informations
### Count
Scout offers to get a dictionary or array count with ``PathElement/count``. This element has to be placed when the value is an array or dictionary. The returned `PathExplorer` will be a int single value.
For instance, to get Robert's hobbies count.
```swift
let path = Path(elements: "Robert", "hobbies", .count)
let count = try json.get(path: path).int
print(count) // 3
```
Similarly, to read the keys count in the overall dictionary, the following Path can be used.
```swift
Path(elements: .count)
```
### List keys
Another useful feature is to list all the keys in a dictionary. To do so, the ``PathElement/keysList`` can be used.
For instance, to list Tom's keys:
```swift
let path = Path(elements: "Tom", .keysList)
let tomKeys = try json.get(path: path).array(of: String.self)
print(tomKeys)
// ["age", "hobbies", "height"]
```
## Scope groups
When working with arrays and dictionaries, it might be useful to be able to target a specific part in the values. For instance to exclude the first and last value in an array, or to target only keys starting with a certain prefix in a dictionary.
Those features are available with ``PathElement/slice(_:)`` to slice an array and ``PathElement/filter(_:)`` to filter keys in a dictionary.
### Slice arrays
With ``PathElement/slice(_:)``, it's possible to target a contiguous part of an array. For instance to get Robert's first two hobbies.
> note: When represented as a `String`, the slice element is specified as two integers separated by a double point and enclosed by squared brackets like `[0:2]` or `[2:-4]`. When the left value is the first index, it is omitted. The same goes for the right value when it's the last valid index.
```swift
let path = Path(elements: "Robert", "hobbies", .slice(0, 1))
let robertFirstTwoHobbies = try json.get(path: path).array(of: String.self)
print(robertFirstTwoHobbies) // ["video games", "party"]
```
Similarly with the ``PathElement/index(_:)``, it's possible to use negative indexes. Here to get Suzanne last two movies' titles.
```swift
let path = Path(elements: "Suzanne", "movies", .slice(-2, -1), "title")
let titles = try json.get(path: path).array(of: String.self)
print(titles)
// ["Yesterday will never go", "What about today?"]
```
The following `ducks` array explains how positive and negative indexes are interpreted with `PathElement.slice`
```
["Riri", "Fifi", "Loulou", "Donald", "Daisy"]
[ 0 , 1 , 2 , 3 , 4 ] (Positive)
[ -5 , -4 , -3 , -2 , -1 ] (Negative)
```
- `ducks[0:2]` targets `["Riri", "Fifi", "Loulou"]`
- `ducks[2:-2]` targets `["Loulou", "Donald"]`
- `ducks[-3:-1]` targets `["Loulou", "Donald", "Daisy"]`
### Filter dictionaries
``PathElement/filter(_:)`` lets you provide a regular expression to match certain keys in a dictionary. All the keys that do not fully match the expression will be filtered.
For instance, to get all keys in Tom's dictionary that start with "h".
```swift
let path = Path(elements: "Tom", .filter("h.*"))
let filteredTom = try json.get(path: path)
print(filteredTom)
```
```json
{
"hobbies" : [
"cooking",
"guitar"
],
"height" : 175
}
```
Or to get Tom and Robert first hobby.
```swift
let path = Path(elements: .filter("Tom|Robert"), "hobbies", 0)
let firstHobbies = try json.get(path: path).dictionary(of: String.self)
print(firstHobbies)
// ["Tom": "cooking", "Robert": "video games"]
```
### Mixing up
It's possible to mix both array slicing and dictionary filtering in a same path. For instance to get Tom and Robert first two hobbies.
```swift
let path = Path(elements: .filter("Tom|Robert"), "hobbies", .slide(.first, 1))
let hobbies = try json.get(path: path)
print(hobbies)
```
```json
{
"Tom" : [
"cooking",
"guitar"
],
"Robert" : [
"video games",
"party"
]
}
```
## Literals and PathElementRepresentable
Using plain strings and numbers is made possible because ``PathElement`` implements `ExpressibleByStringLiteral` and `ExpressibleByIntLiteral`. But when it comes to use variables as `PathElement`, it is required to specify the element.
For instance with the first example path to target Robert's second hobby.
```swift
let robertKey = "Robert"
let hobbiesKey = "hobbies"
let hobbyIndex = 1
let path = Path(elements: .key(robertKey), .key(hobbiesKey), .index(hobbyIndex))
```
As this syntax might be a bit heavy, it's possible to use ``PathElementRepresentable`` to create the `Path` with ``Path/init(_:)-1b2iy``. With it, the code above can be rewritten like so.
```swift
let robertKey = "Robert"
let hobbiesKey = "hobbies"
let hobbyIndex = 1
let path = Path(robertKey, hobbiesKey, hobbyIndex)
```
The drawback is that this is possible only for `PathElement.index` and `PathElement.key`. When dealing with other elements like ``PathElement/count``, it is required to specify the `PathElement` type.
```swift
Path(robertKey, hobbiesKey, PathElement.count)
```
The convenience overloads for the `PathExplorer` functions similarly works with `PathElement` and `PathElementRepresentable`.

View File

@ -0,0 +1,180 @@
# What's new in Scout 4.0.0
Learn the new features of Scout 4.0.0 as well as what was broken or deprecated
## General
Scout 4.0.0 is a global refactor of the code base. This was a necessary step to offer new features. Also, the code is now more robust, faster and more flexible to welcome new features.
### New data structure
To reach this goal, a new data structure has been chosen to represent a ``PathExplorer`` values. The ``ExplorerValue`` in an indirect enum and thus is a purely functional structure. For Scout features, it allows to write less and cleaner code, but also to remove the need to manage the states a PathExplorer had in the previous versions.
This new data structure also allows to use Codable to encode and decode data, which offers several new possibilities, like customizing a `Coder` to better fit ones use case, or to set and add Codable types with no effort (more on that).
> Note: The XML parsing has not changed and still uses AEXML. I tried in several ways to use ExplorerValue with a XML coder but this always led to informations loss or strange behaviors. Thus I rather rewrote the XML features with this new ”functional mindset” and I believe it is clearer. Also, small new features like attributes reading are now offered.
### New path parsing
The Path parsing is now done with a Parser rather than with regular expressions. This is more robust and faster. The same goes for parsing a path and its value when adding or setting a value with the command-line tool.
### Breaking changes
- Adding a value to a path with non-existing keys and indexes will not work anymore. Only when an element of the path is the last will it be valid to add a new key. The solution is now to create empty dictionaries or arrays and fill them after.
- Setting or adding values will no more work with `Any` values but with ``ExplorerValueRepresentable``
### New features
- Usage of `Codable` for Plist, JSON and YAML rather than serialization types to encode and decode data. This means that any Encoder or Decoder can be used with it.
- `Data` and `Date` values are now supported.
- Its now possible to set or add a dictionary or an array value to an explorer. Also, in Swift, a type conforming to ``ExplorerValueRepresentable`` can be set or added. A default implementation is provided for Codable types.
- XML attributes can now be read. In Swift, new options offer to keep the attributes, and to specify a strategy to handle single child elements when exporting a XML explorer.
- Import a CSV file to one of the available formats with a precisely shaped structure.
## ExplorerValue
Serializable PathExplorers like Plist, JSON and YAML or others like XML can use this type to set, add, or read a value.
The new ``ExplorerValue`` is the following enum.
```swift
public indirect enum ExplorerValue {
case int(Int)
case double(Double)
case string(String)
case bool(Bool)
case data(Data)
case date(Date)
case array([ExplorerValue])
case dictionary([String: ExplorerValue])
}
```
### Expressible
``ExplorerValue`` implements the "Expressible" protocols when it's possible for the types it deals with. This means that it's possible to define an ExplorerValue like the following examples.
```swift
let string: ExplorerValue = "string"
let dict: ExplorerValue = ["foo": "bar"]
let array: ExplorerValue = ["Riri", "Fifi", "Loulou"]
```
### Codable
``ExplorerValue`` conforms to `Codable`. The new SerializablePathExplorer (used for JSON, Plist and XML) uses this conformance to offer initialization from Data. But this also means that any Coder can be used to read an `ExplorerValue` from Data. This was already possible to use a different serializer than the default one in the previous implementations. But customizing a Coder is much simpler and now more common in Swift. For instance, setting a custom `Date` decoding strategy is always offered in most coders.
be
### Conversion with ExplorerValueRepresentable
Setting and adding a value to an explorer now works with ExplorerValue. For instance, to set Toms age to 60:
```swift
json.set("Tom", "age", to: .int(60))
```
Of course, convenience types and functions are offered, so the line above can be written like this:
```swift
json.set("Tom", "age", to: 60)
```
This is made possible with the `ExplorerValueRepresentable` protocol. It only requires a function to convert the type to an ExplorerValue.
```swift
protocol ExplorerValueRepresentable {
func explorerValue() throws -> ExplorerValue
}
```
Default implementations are provided for the values mapped by ExplorerValue like `String`, `Double`, an `Array` if its `Element` conforms to ``ExplorerValueRepresentable`` and a `Dictionary` if its `Value` conforms to ExplorerValueRepresentable.
Some examples:
```swift
let stringValue = "toto"
try json.set("name", to: stringValue)
let dict = ["firstName": "Riri", "lastName": "Duck"]
try json.set("profile", to: dict)
```
Also, a default implementation for any `Encodable` type is provided. An Encoder is implemented to encode a type to an `ExplorerValue`. Similarly, a `Decoder` is implemented to decode an ExplorerValue to a Decodable type with the protocol ExplorerValueCreatable. A type alias is provided to group both protocols:
```swift
ExplorerValueConvertible = ExplorerValueRepresentable & ExplorerValueCreatable
```
For instance with a simple struct.
```swift
struct Record: Codable, ExplorerValueConvertible {
var name: String
var score: Int
}
let record = Record(name: "Riri", score: 20)
```
Its then possible to set the record value at a specific path.
```swift
plist.set("ducks", "records", 0, to: record)
```
### About XML
The new ``ExplorerXML`` can also set and add `ExplorerValues`, as well as be converted to one. Because XML is not serializable, this process might loose informations. Options to keep attributes and single child strategies are offered. This is useful in the conversion features like XML → JSON. Whenever its possible, `ExplorerXML` will keep as much information as possible. When its not possible, the type will act consistently. For instance, when setting a new `Array` value, the children of the XML element will all be named "Element".
In Swift, its possible to rather set an AEXMLElement to have more freedom on the children. This requires more work, but I believe its a good thing to have this possibility. To know how to create and edit AEXMLElements, you can checkout the [repo](https://github.com/tadija/AEXML).
## CSV import
A new CSV import feature is available to convert a CSV input as JSON, Plist, YAML or XML. A cool feature when working with named headers is that they will be treated as paths. This can shape very precisely the structure of the converted data. For instance, the following CSV
```csv
name.first;name.last;hobbies[0];hobbies[1]
Robert;Roni;acting;driving
Suzanne;Calvin;singing;play
Tom;Cattle;surfing;watching movies
```
will be converted to the following Json structure.
```json
[
{
"hobbies" : [
"acting",
"driving"
],
"name" : {
"first" : "Robert",
"last" : "Roni"
}
},
{
"hobbies" : [
"singing",
"play"
],
"name" : {
"first" : "Suzanne",
"last" : "Calvin"
}
},
{
"name" : {
"first" : "Tom",
"last" : "Cattle"
},
"hobbies" : [
"surfing",
"watching movies"
]
}
]
```
When there are no headers, the input will be treated as a one or two dimension(s) array.
To create a `PathExplorer` from a CSV string, use ``SerializablePathExplorer/fromCSV(string:separator:hasHeaders:)`` function.
```swift
let json = PathExplorers.Json.fromCSV(csvString, separator: ";", hasHeaders: true)
```
The `hasHeaders` boolean is needed to specify whether the CSV string begins with named headers.

View File

@ -0,0 +1,429 @@
# Paths listing
`PathExplorer` list path features is useful to get all paths leading to a value or a key.
## Overview
In this article, learn how to use the paths listing feature to precisely get the paths you want.
The examples will refer to the following JSON file.
```json
{
"Tom" : {
"age" : 68,
"hobbies" : [
"cooking",
"guitar"
],
"height" : 175
},
"Robert" : {
"age" : 23,
"hobbies" : [
"video games",
"party",
"tennis"
],
"running_records" : [
[
10,
12,
9,
10
],
[ 9,
12,
11
]
],
"height" : 181
},
"Suzanne" : {
"job" : "actress",
"movies" : [
{
"title" : "Tomorrow is so far",
"awards" : "Best speech for a silent movie"
},
{
"title" : "Yesterday will never go",
"awards" : "Best title"
},
{
"title" : "What about today?"
}
]
}
}
```
The reference to the JSON will be held by a ``PathExplorers/Json`` value.
```swift
let json = try PathExplorers.Json(data: data)
```
## Basics
Let's first see how we can list *all* the path in the file. The command
```swift
print(json.listPaths())
```
should output
```
Robert
Robert.age
Robert.height
Robert.hobbies
Robert.hobbies[0]
Robert.hobbies[1]
Robert.hobbies[2]
Robert.running_records
Robert.running_records[0]
Robert.running_records[0][0]
Robert.running_records[0][1]
Robert.running_records[0][2]
Robert.running_records[0][3]
Robert.running_records[1]
Robert.running_records[1][0]
Robert.running_records[1][1]
Robert.running_records[1][2]
Suzanne
Suzanne.job
Suzanne.movies
Suzanne.movies[0]
Suzanne.movies[0].awards
Suzanne.movies[0].title
Suzanne.movies[1]
Suzanne.movies[1].awards
Suzanne.movies[1].title
Suzanne.movies[2]
Suzanne.movies[2].title
Tom
Tom.age
Tom.height
Tom.hobbies
Tom.hobbies[0]
Tom.hobbies[1]
```
> Note: A `Path` is more clearly represented as a `String` with a separator (default is `'.'`). That's what you get by calling `path.description`. It's also the way paths are outputted in the terminal when using the `scout` command-line tool.
## Single and group values
It's possible to target only single values (e.g. string, number...), group values (e.g. array, dictionary) or both using a ``PathsFilter``.
The default target is both single and group.
For instance, to target only single values.
```swift
let path = json.listPaths(
filter: .targetOnly(.single)
)
print(paths)
```
output:
```
Robert.age
Robert.height
Robert.hobbies[0]
Robert.hobbies[1]
Robert.hobbies[2]
Robert.running_records[0][0]
Robert.running_records[0][1]
Robert.running_records[0][2]
Robert.running_records[0][3]
Robert.running_records[1][0]
Robert.running_records[1][1]
Robert.running_records[1][2]
Suzanne.job
Suzanne.movies[0].awards
Suzanne.movies[0].title
Suzanne.movies[1].awards
Suzanne.movies[1].title
Suzanne.movies[2].title
Tom.age
Tom.height
Tom.hobbies[0]
Tom.hobbies[1]
```
Similarly, to target only group values.
```swift
let path = json.listPaths(
filter: .targetOnly(.group)
)
print(paths)
```
outputs:
```
Robert
Robert.hobbies
Robert.running_records
Robert.running_records[0]
Robert.running_records[1]
Suzanne
Suzanne.movies
Suzanne.movies[0]
Suzanne.movies[1]
Suzanne.movies[2]
Tom
Tom.hobbies
```
## Initial path
To avoid listing all paths meeting the requirements, it's possible to target paths having a prefix ``PathExplorer/listPaths(startingAt:)``.
For instance to target only paths in the "Robert" dictionary".
```swift
let path = json.listPaths(
startingAt: "Robert"
)
print(paths)
```
outputs:
```
Robert.age
Robert.height
Robert.hobbies
Robert.hobbies[0]
Robert.hobbies[1]
Robert.hobbies[2]
Robert.running_records
Robert.running_records[0]
Robert.running_records[0][0]
Robert.running_records[0][1]
Robert.running_records[0][2]
Robert.running_records[0][3]
Robert.running_records[1]
Robert.running_records[1][0]
Robert.running_records[1][1]
Robert.running_records[1][2]
```
Do note that it's possible to use any ``PathElement`` in the provided initial path.
For instance to target paths in the Robert *or* Tom dictionaries.
```swift
let path = json.listPaths(
startingAt: .filter("Robert|Tom")
)
print(paths)
```
outputs:
```
Robert
Robert.age
Robert.height
Robert.hobbies
Robert.hobbies[0]
Robert.hobbies[1]
Robert.hobbies[2]
Robert.running_records
Robert.running_records[0]
Robert.running_records[0][0]
Robert.running_records[0][1]
Robert.running_records[0][2]
Robert.running_records[0][3]
Robert.running_records[1]
Robert.running_records[1][0]
Robert.running_records[1][1]
Robert.running_records[1][2]
Tom
Tom.age
Tom.height
Tom.hobbies
Tom.hobbies[0]
Tom.hobbies[1]
```
A last example with paths leading to Suzanne's movies titles.
```swift
let path = json.listPaths(
startingAt: "Suzanne", "movies", .slice(.first, .last), "title"
)
print(paths)
```
outputs:
```
Suzanne.movies[0].title
Suzanne.movies[1].title
Suzanne.movies[2].title
```
## Filter keys
It's possible to provide a regular expression to filter the paths final key. Only the paths that contain a key validated by the regular expression will be retrieved. It's required to provide a `NSRegularExpression`. Meanwhile, the convenience initialiser ``PathsFilter/key(pattern:target:)`` takes a `String` pattern and tries to convert to a `NSRegularExpression.`
List all the paths leading to a key "hobbies".
```swift
let path = try json.listPaths(
filter: .key(pattern: "hobbies")
)
print(paths)
```
outputs:
```
Robert.hobbies
Robert.hobbies[0]
Robert.hobbies[1]
Robert.hobbies[2]
Tom.hobbies
Tom.hobbies[0]
Tom.hobbies[1]
```
List all the paths leading to a key starting with "h".
```swift
let path = try json.listPaths(
filter: .key(pattern: "h.*")
)
print(paths)
```
outputs:
```
Robert.height
Robert.hobbies
Robert.hobbies[0]
Robert.hobbies[1]
Robert.hobbies[2]
Tom.height
Tom.hobbies
Tom.hobbies[0]
Tom.hobbies[1]
```
## Filter values
The values can be filtered with one ore more predicates with ``PathsFilter/value(_:)``. When such a filter is specified, only the single values are targeted.
A path whose value is validated by one of the provided predicates is retrieved.
> note: Two kinds of predicates are offered: ``PathsFilter/ExpressionPredicate`` that takes a `String` boolean expression and ``PathsFilter/FunctionPredicate`` that takes a function to filter the values. Both implement the ``ValuePredicate`` protocol.
List the paths leading to a value below 70 with a ``PathsFilter/FunctionPredicate``
```swift
let predicate = PathsFilter.FunctionPredicate { value in
switch value {
case let .int(int):
return int < 70
case let .double(double):
return double < 70
default:
return false // ignore other values
}
}
let path = try json.listPaths(
filter: .value(predicate)
)
print(paths)
```
outputs:
```
Robert.age
Robert.running_records[0][0]
Robert.running_records[0][1]
Robert.running_records[0][2]
Robert.running_records[0][3]
Robert.running_records[1][0]
Robert.running_records[1][1]
Robert.running_records[1][2]
Tom.age
```
> note: Returning false or throwing an error when the ``ExplorerValue`` parameter has not a proper type for the predicate depends on your needs.
To mention it once, the ``PathsFilter/ExpressionPredicate`` is used with a `String` boolean expression. If it's mainly used for the command-line tool, it's possible to use in Swift. The code above could be written like so
```swift
let path = try json.listPaths(
filter: .value("value < 70")
)
```
The name 'value' is used to specify the value that will be filtered, and is replaced by the value to evaluate at runtime by Scout.
It's possible to specify several predicates. Doing so, a value will be validated as long as one predicate validates it.
For instance to get string values starting with 'guit' *and* values that are greater than 20.
```swift
let prefixPredicate = PathsFilter.FunctionPredicate { value in
guard case let .string(string) = value else { return false }
return string.hasPrefix("guit")
}
let comparisonPredicate = PathsFilter.FunctionPredicate { value in
guard case let .int(int) = value else { return false }
return int > 20
}
let paths = try json.listPaths(
filter: .value(prefixPredicate, comparisonPredicate)
)
```
outputs:
```
Robert.age
Robert.height
Tom.age
Tom.height
Tom.hobbies[1]
```
## Mixing up
Finally, it's worthing noting that all features to filter paths can be mixed up.
For instance to list paths leading to Robert hobbies that contain the word "game".
```swift
let gamePredicate = PathsFilter.FunctionPredicate { value in
guard case let .string(string) = value else { return false }
return string.contains("games")
}
let paths = try json.listPaths(
startingAt: "Robert", "hobbies",
filter: .value(gamePredicate)
)
```
outputs:
```
Robert.hobbies[0]
```
List paths leading to Robert or Tom hobbies arrays (group values).
```swift
let paths = try json.listPaths(
startingAt: .filter("Tom|Robert"),
filter: .key(pattern: "hobbies", target: .group)
)
```
outputs:
```
Robert.hobbies
Tom.hobbies
```

View File

@ -0,0 +1,11 @@
# ``Scout/ExplorerValue``
@Metadata {
@DocumentationExtension(mergeBehavior: append)
}
## Overview
`ExplorerValue` is the back bone of serializable ``PathExplorer`` (JSON, Plist, YAML). It's the type that implements all the logic to conform to `PathExplorer`. Then ``CodablePathExplorer`` simply interfaces it with the proper data format to conform to ``SerializablePathExplorer``. Also, it's the type that is used to encode and decode to those formats.
But it also allows to use your own types to inject them in a `PathExplorer`. Read more with <doc:custom-types-explorerValue>

View File

@ -0,0 +1,23 @@
# ``Scout/PathElement``
@Metadata {
@DocumentationExtension(mergeBehavior: append)
}
## Topics
### Basics
- ``key(_:)``
- ``index(_:)``
### Getting a group information
- ``count``
- ``keysList``
### Scope groups
- ``filter(_:)``
- ``slice(_:)``
- ``slice(_:_:)``

View File

@ -0,0 +1,124 @@
# ``Scout/PathExplorer``
@Metadata {
@DocumentationExtension(mergeBehavior: append)
}
## Overview
Unifies the operations that can be performed with an explorer.
## Topics
### Initializers
An explorer takes an ``ExplorerValue`` to be instantiated.
- ``init(value:name:)``
- ``init(value:)``
### Accessing read values
- ``string``
- ``int``
- ``double``
- ``bool``
- ``data``
- ``date``
- ``array(of:)``
- ``dictionary(of:)``
### Singularity
- ``isSingle``
- ``isGroup``
### Reading values
All functions perform the same operation but offer to work with an array, variadic parameters or a ``Path``.
- ``get(_:)-8vyte``
- ``get(_:)-6pa9h``
- ``get(_:)-2ghf1``
### Setting values
All functions perform the same operation but offer to work with an array, variadic parameters or a ``Path``.
- ``set(_:to:)-5fgny``
- ``set(_:to:)-6yk0i``
- ``set(_:to:)-9d877``
- ``set(_:to:)-376n0``
### Setting values in a new explorer
The "setting" functions are the counterpart of the "set" ones, but will return a new explorer rather than modify it.
- ``setting(_:to:)-7q3g``
- ``setting(_:to:)-9vtr8``
- ``setting(_:to:)-5tdzy``
- ``setting(_:to:)-n2ij``
### Setting key names
All functions perform the same operation but offer to work with an array, variadic parameters or a ``Path``.
- ``set(_:keyNameTo:)-9i6hd``
- ``set(_:keyNameTo:)-5j60r``
- ``set(_:keyNameTo:)-1zwfv``
### Settings key names in a new explorer
The "setting key name" functions are the counterpart of the "set" ones, but will return a new explorer rather than modify it.
- ``setting(_:keyNameTo:)-7ar89``
- ``setting(_:keyNameTo:)-1vrh``
- ``setting(_:keyNameTo:)-1fmyp``
### Deleting values
All functions perform the same operation but offer to work with an array, variadic parameters or a ``Path``.
- ``delete(_:deleteIfEmpty:)-g45f``
- ``delete(_:)``
- ``delete(_:deleteIfEmpty:)-40w9g``
- ``delete(_:deleteIfEmpty:)-2uxwq``
### Deleting values in a new explorer
The "deleting" functions are the counterpart of the "set" ones, but will return a new explorer rather than modify it.
- ``deleting(_:deleteIfEmpty:)-32ufs``
- ``deleting(_:deleteIfEmpty:)-1byw9``
- ``deleting(_:deleteIfEmpty:)-2u4ud``
### Adding values
All functions perform the same operation but offer to work with an array, variadic parameters or a ``Path``.
- ``add(_:at:)-861h4``
- ``add(_:at:)-6wo3i``
- ``add(_:at:)-2kii6``
- ``add(_:at:)-2zxor``
### Adding values in a new explorer
The "adding" functions are the counterpart of the "set" ones, but will return a new explorer rather than modify it.
- ``adding(_:at:)-7fd9c``
- ``adding(_:at:)-4ju9b``
- ``adding(_:at:)-68mxp``
- ``adding(_:at:)-5uv86``
### Listing paths
List paths listing to keys based on regular expression or values based on filters.
- ``listPaths(startingAt:filter:)-4tkeq``
- ``listPaths(startingAt:)``
- ``listPaths(startingAt:filter:)-8y0x2``
### Deprecated
- ``real``

View File

@ -0,0 +1,64 @@
# ``Scout/Path``
@Metadata {
@DocumentationExtension(mergeBehavior: append)
}
## Overview
Paths are the way to feed a ``PathExplorer`` to navigate through data. `PathExplorer`'s operations will often take a `Path` (or a collection of ``PathElement``s) to target precisely where to run.
Basically, a `Path` is a collection of ``PathElement``s in a specific order. The sequence of `PathElement`s lets the explorer know what value to target next. When navigating to a value is not possible, the explorer will throw an error.
## Topics
### Instantiate an empty Path
- ``init()``
- ``empty``
### Instantiate from PathElement values
- ``init(elements:)-8dch4``
- ``init(elements:)-9i64v``
### Instantiate from PathElementRepresentable values
``PathElementRepresentable`` is a protocol to erase the `PathElement` type when instantiating a `Path` with non-literal values.
- ``init(_:)-1b2iy``
- ``init(_:)-cgb7``
- ``init(arrayLiteral:)``
### Instantiate from a String
A `Path` is easily represented as a `String`, which is especially useful when working in the command-line.
- ``init(string:separator:)``
- ``defaultSeparator``
- ``parser(separator:keyForbiddenCharacters:)``
### Appending elements
`Path` conforms to several collection protocols. Additionally, those convenience functions are offered.
- ``append(_:)-9l194``
- ``appending(_:)-2ptn6``
- ``appending(_:)-3mvwq``
### Flatten a Path
When a `Path` contains special group scoping elements like ``PathElement/slice(_:)`` or ``PathElement/filter(_:)``, specifying a `PathElement.index` or `PathElement.key` will not refer to an immediate dictionary or array. The "flatten" operation will replace the slices and the filters in the `Path` with the proper values when the path is complete. Mainly used in paths listing ``PathExplorer/listPaths(startingAt:)``.
- ``flattened()``
### Map elements (Collection)
- ``Path/compactMapIndexes``
- ``Path/compactMapKeys``
- ``Path/compactMapSlices``
- ``Path/compactMapFilter``
### Compare path (Collection)
- ``Path/commonPrefix(with:)``

View File

@ -0,0 +1,46 @@
# ``Scout/PathsFilter``
@Metadata {
@DocumentationExtension(mergeBehavior: append)
}
## Overview
Allows to target single or group values, specific keys with regular expressions and values with predicates.
When filtering keys or values, it's always possible to specify single, group values or both.
## Topics
### No filter
- ``noFilter``
- ``targetOnly(_:)``
- ``ValueTarget``
### Filter keys
- ``key(regex:)``
- ``key(regex:target:)``
- ``key(pattern:target:)``
### Filter values
- ``value(_:)``
- ``value(_:_:)-8tfx1``
- ``value(_:_:)-2wxh0``
### Filter keys and values
- ``keyAndValue(pattern:valuePredicate:)``
- ``keyAndValue(keyRegex:valuePredicate:)``
- ``keyAndValue(keyRegex:valuePredicates:)``
- ``keyAndValue(keyRegex:valuePredicates:_:)``
- ``keyAndValue(pattern:valuePredicatesFormat:_:)``
### Predicates
- ``ValuePredicate``
- ``ExpressionPredicate``
- ``FunctionPredicate``

View File

@ -0,0 +1,87 @@
# ``Scout/SerializablePathExplorer/exportFoldedString(upTo:)``
@Metadata {
@DocumentationExtension(mergeBehavior: append)
}
### Examples
With the following JSON stored in a `SerializablePathExplorer` named `json`.
```json
{
"Tom" : {
"age" : 68,
"hobbies" : [
"cooking",
"guitar"
],
"height" : 175
},
"Robert" : {
"age" : 23,
"hobbies" : [
"video games",
"party",
"tennis"
],
"running_records" : [
[
10,
12,
9,
10
],
[
9,
12,
11
]
],
"height" : 181
},
"Suzanne" : {
"job" : "actress",
"movies" : [
{
"title" : "Tomorrow is so far",
"awards" : "Best speech for a silent movie"
},
{
"title" : "Yesterday will never go",
"awards" : "Best title"
},
{
"title" : "What about today?"
}
]
}
}
```
The following
```swift
json.exportFoldedString(upTo: 2)
```
will return the string:
```json
{
"Suzanne" : {
"job" : "actress",
"movies" : [...]
},
"Tom" : {
"hobbies" : [...],
"age" : 68,
"height" : 175
},
"Robert" : {
"running_records" : [...],
"age" : 23,
"hobbies" : [...],
"height" : 181
}
}
```

View File

@ -0,0 +1,42 @@
# ``Scout/SerializablePathExplorer``
@Metadata {
@DocumentationExtension(mergeBehavior: append)
}
## Overview
Protocol refining ``PathExplorer`` to offer features like conversion to another format or serialization. Explorers in ``PathExplorers`` implement this protocol.
## Topics
### Initializers
- ``init(data:)``
- ``fromCSV(string:separator:hasHeaders:)``
### Get format info
- ``format``
### Export as Data
- ``exportData()``
- ``exportData(to:)``
- ``exportData(to:rootName:)``
### Export as String
- ``exportString()``
- ``exportString(to:)``
- ``exportString(to:rootName:)``
### Export as CSV
- ``exportCSV()``
- ``exportCSV(separator:)``
### Export folded String
- ``exportFoldedString(upTo:)``
- ``folded(upTo:)``

View File

@ -0,0 +1,56 @@
# ``Scout``
This library aims to make specific formats data values reading and writing simple when the data format is not known at build time.
## Overview
Supported formats:
- JSON
- Plist
- YAML
- XML
## Topics
### Essential
- <doc:getting-started>
- ``PathExplorer``
- ``Path``
### Explore data
- ``PathExplorer``
- ``PathExplorers``
- ``ExplorerError``
### Manipulate paths
- <doc:mastering-paths>
- <doc:paths-listing>
- ``Path``
- ``PathElement``
- ``PathElementRepresentable``
- ``PathsFilter``
- ``ValuePredicate``
- ``Bounds``
### Convert and export explorers
- ``SerializablePathExplorer``
- ``DataFormat``
- ``CodablePathExplorer``
- ``CodableFormat``
- ``CodableFormats``
- ``ExplorerXML``
- ``SerializationError``
### Set and add custom types
- <doc:custom-types-explorerValue>
- ``ExplorerValue``
- ``ExplorerValueCreatable``
- ``ExplorerValueRepresentable``
- ``ExplorerValueConvertible``
### Follow updates
- <doc:new-4.0.0>

View File

@ -5,7 +5,7 @@
import Scout
public indirect enum ValueType: Equatable {
public enum ValueType: Equatable {
case string(String)
case real(String)
case keyName(String)

View File

@ -91,8 +91,10 @@ final class PathExplorerGetTests: XCTestCase {
func testGetKey_MissingKeyThrows_BestMatch<P: EquatablePathExplorer>(_ type: P.Type) throws {
let explorer = P(value: ["Endo": 2, "toto": true, "Riri": "duck", "score": 12.5])
XCTAssertErrorsEqual(try explorer.get("tata"),
ExplorerError.missing(key: "tata", bestMatch: "toto"))
XCTAssertErrorsEqual(
try explorer.get("tata"),
.missing(key: "tata", bestMatch: "toto")
)
}
func testGetKey_NestedKey<P: EquatablePathExplorer>(_ type: P.Type) throws {
@ -104,8 +106,10 @@ final class PathExplorerGetTests: XCTestCase {
func testGet_MissingNestedKeyThrows<P: PathExplorer>(_ type: P.Type) throws {
let explorer = P(value: ["firstKey": ["secondKey": 23]])
XCTAssertErrorsEqual(try explorer.get("firstKey", "kirk"),
ExplorerError.missing(key: "kirk", bestMatch: nil).with(path: "firstKey"))
XCTAssertErrorsEqual(
try explorer.get("firstKey", "kirk"),
.missing(key: "kirk", bestMatch: nil).with(path: "firstKey")
)
}
func testGetKey_ThrowsOnNoDictionary<P: EquatablePathExplorer>(_ type: P.Type) throws {
@ -175,8 +179,10 @@ final class PathExplorerGetTests: XCTestCase {
func testGetCount_ThrowsOnNonGroup() throws {
let array: ExplorerValue = ["Endo", 1, false, 2.5]
XCTAssertErrorsEqual(try array.get(0, .count),
ExplorerError.wrongUsage(of: .count).with(path: 0))
XCTAssertErrorsEqual(
try array.get(0, .count),
.wrongUsage(of: .count).with(path: 0)
)
}
// MARK: - Keys list
@ -193,7 +199,10 @@ final class PathExplorerGetTests: XCTestCase {
func testGetKeysList_ThrowsOnNonDictionary<P: EquatablePathExplorer>(_ type: P.Type) throws {
let explorer = P(value: ["Endo", 1, false, 2.5])
XCTAssertErrorsEqual(try explorer.get(.keysList), .wrongUsage(of: .keysList))
XCTAssertErrorsEqual(
try explorer.get(.keysList),
.wrongUsage(of: .keysList)
)
}
// MARK: - Filter
@ -236,6 +245,7 @@ final class PathExplorerGetTests: XCTestCase {
func testGetFilter_ThrowsOnNonDictionary<P: EquatablePathExplorer>(_ type: P.Type) throws {
let explorer = P(value: ["Endo", 1, false, 2.5])
XCTAssertErrorsEqual(try explorer.get(.filter("toto")),
.wrongUsage(of: .filter("toto"))
)
@ -283,7 +293,10 @@ final class PathExplorerGetTests: XCTestCase {
func testGetSlice_ThrowsOnNonArray<P: EquatablePathExplorer>(_ type: P.Type) throws {
let explorer = P(value: ["Tom": 10, "Robert": true, "Suzanne": "Here"])
XCTAssertErrorsEqual(try explorer.get(.slice(0, 1)), .wrongUsage(of: .slice(0, 1)))
XCTAssertErrorsEqual(
try explorer.get(.slice(0, 1)),
.wrongUsage(of: .slice(0, 1))
)
}
}