Skip to content

Commit 6218165

Browse files
feat!(pollux): vcdm 2.0 implementation (#220)
Signed-off-by: goncalo-frade-iohk <[email protected]>
1 parent bb0e514 commit 6218165

File tree

85 files changed

+4107
-874
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+4107
-874
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ env:
1919
jobs:
2020
lint:
2121
name: build
22-
runs-on: macos-15
22+
runs-on: macos-26-xlarge
2323

2424
steps:
2525
- name: Checkout Code
2626
uses: actions/checkout@v3
2727

2828
- uses: maxim-lobanov/setup-xcode@v1
2929
with:
30-
xcode-version: '16.2'
30+
xcode-version: '26.0.0'
3131

3232
# - name: Install lcov
3333
# run: brew install [email protected] && brew link --overwrite --force [email protected]

Core/Sources/Helpers/AnyCodable.swift

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public struct AnyCodable {
77
self.value = value
88
}
99

10-
init(_ value: Any) {
10+
public init(value: Any) {
1111
self.value = value
1212
}
1313

@@ -25,7 +25,7 @@ extension AnyCodable: Codable {
2525
let container = try decoder.singleValueContainer()
2626

2727
if container.decodeNil() {
28-
self.init(())
28+
self.init(value:())
2929
} else if let bool = try? container.decode(Bool.self) {
3030
self.init(bool)
3131
} else if let int = try? container.decode(Int.self) {
@@ -37,9 +37,9 @@ extension AnyCodable: Codable {
3737
} else if let string = try? container.decode(String.self) {
3838
self.init(string)
3939
} else if let array = try? container.decode([AnyCodable].self) {
40-
self.init(array.map { $0.value })
40+
self.init(value: array.map { $0.value })
4141
} else if let dictionary = try? container.decode([String: AnyCodable].self) {
42-
self.init(dictionary.mapValues { $0.value })
42+
self.init(value: dictionary.mapValues { $0.value })
4343
} else {
4444
throw DecodingError.dataCorruptedError(
4545
in: container,
@@ -52,6 +52,8 @@ extension AnyCodable: Codable {
5252
var container = encoder.singleValueContainer()
5353

5454
switch self.value {
55+
case is NSNull:
56+
try container.encodeNil()
5557
case is Void:
5658
try container.encodeNil()
5759
case let bool as Bool:
@@ -87,9 +89,9 @@ extension AnyCodable: Codable {
8789
case let url as URL:
8890
try container.encode(url)
8991
case let array as [Any]:
90-
try container.encode(array.map { AnyCodable($0) })
92+
try container.encode(array.map { AnyCodable(value: $0) })
9193
case let dictionary as [String: Any]:
92-
try container.encode(dictionary.mapValues { AnyCodable($0) })
94+
try container.encode(dictionary.mapValues { AnyCodable(value: $0) })
9395
default:
9496
let context = EncodingError.Context(
9597
codingPath: container.codingPath,
@@ -170,34 +172,34 @@ extension AnyCodable: CustomDebugStringConvertible {
170172
extension AnyCodable: ExpressibleByNilLiteral, ExpressibleByBooleanLiteral, ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, ExpressibleByStringLiteral, ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral {
171173

172174
public init(nilLiteral: ()) {
173-
self.init(nil ?? ())
175+
self.init(value: nil ?? ())
174176
}
175177

176178
public init(booleanLiteral value: Bool) {
177-
self.init(value)
179+
self.init(value: value)
178180
}
179181

180182
public init(integerLiteral value: Int) {
181-
self.init(value)
183+
self.init(value: value)
182184
}
183185

184186
public init(floatLiteral value: Double) {
185-
self.init(value)
187+
self.init(value: value)
186188
}
187189

188190
public init(extendedGraphemeClusterLiteral value: String) {
189-
self.init(value)
191+
self.init(value: value)
190192
}
191193

192194
public init(stringLiteral value: String) {
193-
self.init(value)
195+
self.init(value: value)
194196
}
195197

196198
public init(arrayLiteral elements: Any...) {
197-
self.init(elements)
199+
self.init(value: elements)
198200
}
199201

200202
public init(dictionaryLiteral elements: (AnyHashable, Any)...) {
201-
self.init(Dictionary<AnyHashable, Any>(elements, uniquingKeysWith: { (first, _) in first }))
203+
self.init(value: Dictionary<AnyHashable, Any>(elements, uniquingKeysWith: { (first, _) in first }))
202204
}
203205
}

Core/Sources/Helpers/Data+tryString.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
import Domain
21
import Foundation
32

3+
public struct InvalidCoding: Error {
4+
let localizedDescription: String
5+
6+
public init(message: String) {
7+
self.localizedDescription = message
8+
}
9+
}
10+
411
public extension Data {
512
func toString(using: String.Encoding = .utf8) throws -> String {
613
guard let str = String(data: self, encoding: using) else {
7-
throw CommonError.invalidCoding(message: "Could not get String from Data value")
14+
throw InvalidCoding(message: "Could not get String from Data value")
815
}
916
return str
1017
}

Core/Sources/Helpers/JSONDecoder+Helper.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,10 @@ public extension JSONDecoder {
2020
})
2121
return decoder
2222
}
23+
24+
static var normalized: JSONDecoder {
25+
let decoder = JSONDecoder()
26+
decoder.dateDecodingStrategy = .iso8601
27+
return decoder
28+
}
2329
}

Core/Sources/Helpers/JSONEncoder+Helper.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,11 @@ public extension JSONEncoder {
2020
encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys]
2121
return encoder
2222
}
23+
24+
static var normalized: JSONEncoder {
25+
let encoder = JSONEncoder()
26+
encoder.dateEncodingStrategy = .iso8601
27+
encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys]
28+
return encoder
29+
}
2330
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/// A utility enum that models values which may appear as either a single item or an array of items,
2+
/// commonly seen in JSON APIs that are not schema-consistent.
3+
///
4+
/// `OneOrMany` normalizes access to such values while preserving the original shape when re-encoding.
5+
/// Use the `array` property to always work with `[T]`, regardless of whether the source was a single
6+
/// value or an array.
7+
///
8+
/// - Generic Parameter:
9+
/// - T: The element type. Must conform to `RawCodable`.
10+
///
11+
/// - Conforms To:
12+
/// - `RawCodable` (and, by extension, `Codable` if `RawCodable` refines it)
13+
///
14+
/// - Decoding Behavior:
15+
/// - Attempts to decode an array `[T]` first. If that fails, falls back to decoding a single `T`.
16+
/// - This means that if both `[T]` and `T` could plausibly decode from the same payload, the array
17+
/// form wins.
18+
/// - `null` is not accepted; use `OneOrMany<T>?` if the field can be `null` or omitted.
19+
///
20+
/// - Encoding Behavior:
21+
/// - Preserves shape: `.one` encodes as a single `T`; `.many` encodes as `[T]`.
22+
///
23+
/// - Normalization:
24+
/// - Use `array` to always obtain `[T]` for iteration and higher-level logic.
25+
///
26+
/// - Bridging:
27+
/// - The `raw` property exposes an `AnyCodable` representation of the wrapped value(s), satisfying
28+
/// `RawCodable` requirements.
29+
///
30+
/// - Use Cases:
31+
/// - Fields like `"tag": "swift"` vs `"tag": ["swift", "ios"]`
32+
///
33+
/// - Examples:
34+
/// ```swift
35+
/// struct Response: Codable {
36+
/// let tags: OneOrMany<Tag>
37+
/// }
38+
///
39+
/// // JSON: { "tags": "swift" }
40+
/// // -> Response.tags == .one(Tag("swift"))
41+
/// // -> Response.tags.array == [Tag("swift")]
42+
///
43+
/// // JSON: { "tags": ["swift", "ios"] }
44+
/// // -> Response.tags == .many([Tag("swift"), Tag("ios")])
45+
/// // -> Response.tags.array == [Tag("swift"), Tag("ios")]
46+
///
47+
/// // Encoding preserves shape:
48+
/// // OneOrMany.one(Tag("swift")) -> "swift"
49+
/// // OneOrMany.many([Tag("swift"), Tag("ios")]) -> ["swift", "ios"]
50+
/// ```
51+
///
52+
/// - See Also:
53+
/// - `RawCodable`
54+
/// - `AnyCodable`
55+
///
56+
///
57+
/// Case: `.one`
58+
/// Wraps a single value of type `T`.
59+
///
60+
///
61+
/// Case: `.many`
62+
/// Wraps multiple values as an array `[T]`.
63+
///
64+
///
65+
/// Property: `array`
66+
/// A normalized view of the contents as `[T]`.
67+
/// - If the receiver is `.one(v)`, returns `[v]`.
68+
/// - If the receiver is `.many(vs)`, returns `vs` unchanged.
69+
///
70+
///
71+
/// Property: `raw`
72+
/// The `RawCodable` raw representation as `AnyCodable`, mirroring the underlying shape:
73+
/// - For `.one(v)`, returns `AnyCodable(v)`.
74+
/// - For `.many(vs)`, returns `AnyCodable(vs)`.
75+
///
76+
///
77+
/// Initializer: `init(from:)`
78+
/// Decodes the value from a single-value container, preferring arrays.
79+
/// - Parameters:
80+
/// - decoder: The decoder to read data from.
81+
/// - Throws: An error if neither `[T]` nor `T` can be decoded from the payload.
82+
///
83+
///
84+
/// Method: `encode(to:)`
85+
/// Encodes the value into a single-value container, preserving shape.
86+
/// - Parameters:
87+
/// - encoder: The encoder to write data to.
88+
/// - Throws: An error if encoding fails.
89+
public enum OneOrMany<T: RawCodable>: RawCodable {
90+
case one(T)
91+
case many([T])
92+
93+
/// A normalized view of the wrapped value(s) as an array.
94+
///
95+
/// This property allows you to work with the contents uniformly as `[T]`,
96+
/// regardless of whether the underlying representation is a single value
97+
/// (`.one`) or an array (`.many`).
98+
///
99+
/// Behavior:
100+
/// - If the receiver is `.one(value)`, returns `[value]`.
101+
/// - If the receiver is `.many(values)`, returns `values` unchanged.
102+
///
103+
/// Notes:
104+
/// - Order and duplicates are preserved.
105+
/// - This is a computed convenience; accessing it does not mutate the enum.
106+
/// - If you need to preserve the original shape for re-encoding, use the enum
107+
/// cases directly; `array` is only for normalized access.
108+
///
109+
/// Example:
110+
/// ```swift
111+
/// switch tags.array {
112+
/// case []:
113+
/// // no tags
114+
/// default:
115+
/// // iterate uniformly over tags
116+
/// for tag in tags.array { /* ... */ }
117+
/// }
118+
/// ```
119+
public var array: [T] {
120+
switch self {
121+
case .one(let v): return [v]
122+
case .many(let v): return v
123+
}
124+
}
125+
126+
public var raw: AnyCodable? {
127+
switch self {
128+
case .one(let v): return .init(v)
129+
case .many(let v): return .init(v)
130+
}
131+
}
132+
133+
public init(from decoder: Decoder) throws {
134+
let c = try decoder.singleValueContainer()
135+
136+
if let many = try? c.decode([T].self) {
137+
self = .many(many)
138+
return
139+
}
140+
141+
self = .one(try c.decode(T.self))
142+
}
143+
144+
public func encode(to encoder: Encoder) throws {
145+
var c = encoder.singleValueContainer()
146+
switch self {
147+
case .one(let v): try c.encode(v)
148+
case .many(let v): try c.encode(v)
149+
}
150+
}
151+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
public extension Optional {
2+
func orThrow(_ error: Error) throws -> Wrapped {
3+
if let value = self { return value }
4+
throw error
5+
}
6+
}

0 commit comments

Comments
 (0)