this repo has no description
at main 163 lines 6.8 kB view raw
1// 2// URLQueryItemDecoder.swift 3// URLQueryItemCoder 4// 5// Created by Kyle Hughes on 1/17/23. 6// 7 8import Foundation 9 10/// An object that decodes instances of a data type from `URLQueryItem` name-value pairs. 11public struct URLQueryItemDecoder { 12 /// The strategies used by this decoder for decoding `Decodable` values. 13 public var strategies: DecodingStrategies 14 15 // MARK: Public Initialization 16 17 /// Creates a new, reusable `URLQueryItem` decoder. 18 /// 19 /// The default decoding strategies are used if none are supplied. 20 /// 21 /// - Parameter dataStrategy: The strategy to use for decoding `Data` values. 22 /// - Parameter dateStrategy: The strategy to use for decoding `Date` values. 23 /// - Parameter keyStrategy: The strategy to use for decoding keys on keyed containers. 24 /// - Parameter nonConformingFloatStrategy: The strategy to use for non-`URLQueryItem`-conforming floating-point 25 /// values (IEEE 754 infinity and NaN). 26 /// - Returns: A new, reusable `URLQueryItem` decoder. 27 @inlinable 28 public init( 29 dataStrategy: DataDecodingStrategy = .default, 30 dateStrategy: DateDecodingStrategy = .default, 31 keyStrategy: KeyDecodingStrategy = .default, 32 nonConformingFloatStrategy: NonConformingFloatDecodingStrategy = .default 33 ) { 34 self.init( 35 strategies: DecodingStrategies( 36 dataStrategy: dataStrategy, 37 dateStrategy: dateStrategy, 38 keyStrategy: keyStrategy, 39 nonConformingFloatStrategy: nonConformingFloatStrategy 40 ) 41 ) 42 } 43 44 /// Creates a new, reusable `URLQueryItem` decoder.. 45 /// 46 /// - Parameter strategies: The strategies for this decoder to use. 47 /// - Returns: A new, reusable `URLQueryItem` decoder. 48 public init(strategies: DecodingStrategies) { 49 self.strategies = strategies 50 } 51 52 // MARK: Private Instance Interface 53 54 private func decodeToIntermediateRepresentation( 55 from queryItems: [URLQueryItem] 56 ) throws -> DecodingContainer<String> { 57 let codingPath: [StringCodingKey] = [] 58 59 guard let firstQueryItem = queryItems.first else { 60 return .empty(at: codingPath, using: strategies) 61 } 62 63 guard 64 let firstQueryItemKeyComponents = firstQueryItem.name.removingPercentEncoding?.components(separatedBy: "."), 65 let firstQueryItemFirstKeyComponents = firstQueryItemKeyComponents.first, 66 !firstQueryItemFirstKeyComponents.isEmpty 67 else { 68 let singleValueContainer = DecodingContainer<String>.SingleValue( 69 codingPath: codingPath, 70 configuration: strategies 71 ) 72 singleValueContainer.store(firstQueryItem.value?.removingPercentEncoding) 73 74 return .singleValue(singleValueContainer) 75 } 76 77 let multiValueContainer = DecodingContainer<String>.MultiValue( 78 codingPath: codingPath, 79 configuration: strategies 80 ) 81 82 for queryItem in queryItems { 83 guard let keyComponents = queryItem.name.removingPercentEncoding?.components(separatedBy: ".") else { 84 throw DecodingError.dataCorrupted( 85 DecodingError.Context( 86 codingPath: codingPath, 87 debugDescription: 88 "Cannot have an empty key in a keyed container with multiple keys. " + 89 "Impossible to infer appropriate structure for decoded type." 90 ) 91 ) 92 } 93 94 let lastKeyComponentIndex = keyComponents.index(before: keyComponents.endIndex) 95 96 var currentCodingPath = codingPath 97 var currentMultiValueContainer = multiValueContainer 98 99 for index in keyComponents.indices { 100 let keyComponent = keyComponents[index] 101 102 let codingKey = StringCodingKey(stringValue: keyComponent) 103 currentCodingPath.append(codingKey) 104 105 if index == lastKeyComponentIndex { 106 let singleValueContainer = DecodingContainer<String>.SingleValue( 107 codingPath: currentCodingPath, 108 configuration: strategies 109 ) 110 singleValueContainer.store(queryItem.value?.removingPercentEncoding) 111 currentMultiValueContainer.set(keyComponent, to: .singleValue(singleValueContainer)) 112 } else if let childContainer = currentMultiValueContainer.children[keyComponent] { 113 switch childContainer { 114 case let .multiValue(existingMultiValueContainer): 115 currentMultiValueContainer = existingMultiValueContainer 116 case .singleValue: 117 throw DecodingError.dataCorrupted( 118 DecodingError.Context( 119 codingPath: currentCodingPath, 120 debugDescription: 121 "Single value container has to be the last key in a path. " + 122 "It does not support child keys." 123 ) 124 ) 125 } 126 } else { 127 let newMultiValueContainer = DecodingContainer<String>.MultiValue( 128 codingPath: currentCodingPath, 129 configuration: strategies 130 ) 131 currentMultiValueContainer.set(keyComponent, to: .multiValue(newMultiValueContainer)) 132 currentMultiValueContainer = newMultiValueContainer 133 } 134 } 135 } 136 137 return .multiValue(multiValueContainer) 138 } 139} 140 141// MARK: - TopLevelDecoder Extension 142 143#if canImport(Combine) 144import Combine 145extension URLQueryItemDecoder: TopLevelDecoder { 146 // MARK: Public Instance Interface 147 148 /// Decodes an instance of the indicated type. 149 /// 150 /// - Parameter type: The type to decode the query items into. 151 /// - Parameter queryItems: The query items to decode the type from. 152 /// - Returns: The instance of the given `Type` represented by the given query items. 153 public func decode<Value>( 154 _ type: Value.Type, 155 from queryItems: [URLQueryItem] 156 ) throws -> Value where Value: Decodable { 157 let container = try decodeToIntermediateRepresentation(from: queryItems) 158 let lowLevelDecoder = LowLevelDecoder(container: container) 159 160 return try lowLevelDecoder.decodeWithSpecialTreatment(as: type) 161 } 162} 163#endif