this repo has no description
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