Mastering paths (end)

This commit is contained in:
Alexis Bridoux 2021-06-29 00:42:25 +02:00
parent 4ae5cdb922
commit 723bb2d229
12 changed files with 132 additions and 207 deletions

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

@ -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
/// ## 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

@ -5,7 +5,7 @@
import Foundation
/// Store the possible elements that can be used to subscript a `PathExplorer`
/// Tthe possible elements that can be used to subscript a ``PathExplorer``
public enum PathElement: Hashable {
// MARK: - Constants

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

@ -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

@ -18,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) }
}
@ -41,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 provided `ExplorerValueRepresentable`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), 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())
@ -76,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()) }
}
@ -108,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) }
}
@ -149,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) }
@ -207,35 +124,20 @@ 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
/// ### 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.
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
/// ### Appending
/// To add a key at the end of an array, specify the `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
/// ### Appending
/// To add a key at the end of an array, specify the `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)) }
@ -244,11 +146,7 @@ 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
/// ### Appending
/// To add a key at the end of an array, specify the `PathElement.count`
///
/// ### Non-existing key
@ -257,22 +155,14 @@ 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
/// ### Appending
/// To add a key at the end of an array, specify the `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
/// ### Appending
/// To add a key at the end of an array, specify the `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

@ -8,9 +8,10 @@ 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 }
/// Initialise a new ``PathExplorer`` from the `Data`
/// Initialize a new ``PathExplorer`` from the `Data`
///
/// - Throws: If the data cannot be serialized into the format
init(data: Data) throws
@ -49,7 +50,7 @@ public protocol SerializablePathExplorer: PathExplorer {
/// 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 "..."

View File

@ -177,13 +177,15 @@ For instance, to get all keys in Tom's dictionary that start with "h".
let path = Path(elements: "Tom", .filter("h.*"))
let filteredTom = try json.get(path: path)
print(filteredTom)
// {
// "hobbies" : [
// "cooking",
// "guitar"
// ],
// "height" : 175
// }
```
```json
{
"hobbies" : [
"cooking",
"guitar"
],
"height" : 175
}
```
Or to get Tom and Robert first hobby.
@ -200,33 +202,49 @@ print(firstHobbies)
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
try json.get(.filter("Tom|Robert"), "hobbies", .slide(.first, 1))
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`. When it comes to use variables as `PathElement`, it is required to specify the element.
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 firstKey = "Robert"
let secondKey = "hobbies"
let firstIndex = 1
let path = Path(elements: .key(firstKey), .key(secondKey), .index(firstIndex))
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 firstKey = "Robert"
let secondKey = "hobbies"
let firstIndex = 1
let path = Path(firstKey, secondKey, firstIndex)
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:
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(firstKey, secondKey, PathElement.count)
Path(robertKey, hobbiesKey, PathElement.count)
```
The convenience overloads for the `PathExplorer` functions similarly works with `PathElement` and `PathElementRepresentable`.

View File

@ -51,3 +51,14 @@ A `Path` is easily represented as a `String`, which is especially useful when wo
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:)``