summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadREST/Relay/LocationIdentifier.swift
blob: cb087c255fb67e63b05292b59bc28c90002b7f48 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//
//  LocationIdentifier.swift
//  MullvadVPN
//
//  Created by Andrew Bulhak on 2025-01-24.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

extension REST {
    // locations are currently always "aa-bbb" for some country code aa and city code bbb. Should this change, this type can be extended.
    public struct LocationIdentifier: Sendable {
        public let country: String
        public let city: String

        fileprivate static func parse(_ input: String) -> (String, String)? {
            let components = input.split(separator: "-")
            guard components.count == 2 else { return nil }
            return (String(components[0]), String(components[1]))
        }
    }
}

extension REST.LocationIdentifier: RawRepresentable {
    public var rawValue: String { "\(country)-\(city)" }

    public init?(rawValue: String) {
        guard let parsed = Self.parse(rawValue) else { return nil }
        (country, city) = parsed
    }
}

extension REST.LocationIdentifier: ExpressibleByStringLiteral {
    public init(stringLiteral value: StringLiteralType) {
        guard let parsed = Self.parse(value) else {
            // this is ugly, but it will only ever be called for
            // code initialised from a literal in code, and
            // never from real-world input, so it'll have to do.
            fatalError("Invalid LocationIdentifier: \(value)")
        }
        (country, city) = parsed
    }
}

// Allow LocationIdentifier to code to/from JSON Strings
extension REST.LocationIdentifier: Codable {
    enum ParsingError: Error {
        case malformed
    }

    public init(from decoder: any Decoder) throws {
        let container = try decoder.singleValueContainer()
        guard let parsed = Self.parse(try container.decode(String.self)) else {
            throw ParsingError.malformed
        }
        (country, city) = parsed
    }

    public func encode(to encoder: any Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(rawValue)
    }
}

// As the location's values are Substrings of the same String, to which they maintain references, we use the base String for holistic operations such as equality and hashing
extension REST.LocationIdentifier: Hashable {
    public static func == (lhs: REST.LocationIdentifier, rhs: REST.LocationIdentifier) -> Bool {
        lhs.rawValue == rhs.rawValue
    }

    public func hash(into hasher: inout Hasher) {
        hasher.combine(rawValue)
    }
}