Created
May 14, 2021 04:33
-
-
Save lnfnunes/f9af5d188b806f9f2538ef43ebe8b70c to your computer and use it in GitHub Desktop.
Generic property-wrapper to provide default values for non-optional properties when they are not present or have a nil value.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Decodable.swift | |
// | |
// Copyright © 2021 Leandro Nunes Fantinatto. All rights reserved. | |
// Ref: https://github.com/gonzalezreal/DefaultCodable | |
// Ref: https://github.com/JohnSundell/Codextended | |
// | |
import Foundation | |
/// Generic property-wrapper to provide default values for non-optional properties when they are not present or have a nil value. | |
/// Use-case examples: | |
/// - @DecodableDefault.EmptyString var title: String | |
/// - @DecodableDefault.False var isFeaturedEnabled: Bool | |
/// - @DecodableDefault.True var isFeatureEnabled: Bool | |
/// - @DecodableDefault.EmptyList var data: [SomeModel] | |
/// - @DecodableDefault.EmptyMap var dict: [String : Any] | |
/// | |
enum DecodableDefault {} | |
protocol DecodableDefaultSource { | |
associatedtype Value: Decodable | |
static var defaultValue: Value { get } | |
} | |
extension DecodableDefault { | |
@propertyWrapper | |
struct Wrapper<Source: DecodableDefaultSource> { | |
typealias Value = Source.Value | |
var wrappedValue = Source.defaultValue | |
} | |
typealias Source = DecodableDefaultSource | |
typealias List = Decodable & ExpressibleByArrayLiteral | |
typealias Map = Decodable & ExpressibleByDictionaryLiteral | |
// Convenience aliases to reference the provided `enum Sources` as specialized versions of our property wrapper type. | |
typealias True = Wrapper<Sources.True> | |
typealias False = Wrapper<Sources.False> | |
typealias EmptyString = Wrapper<Sources.EmptyString> | |
typealias EmptyList<T: List> = Wrapper<Sources.EmptyList<T>> | |
typealias EmptyMap<T: Map> = Wrapper<Sources.EmptyMap<T>> | |
typealias Zero = Wrapper<Sources.Zero> | |
typealias One = Wrapper<Sources.One> | |
enum Sources { | |
enum True: Source { | |
static var defaultValue: Bool { true } | |
} | |
enum False: Source { | |
static var defaultValue: Bool { false } | |
} | |
enum EmptyString: Source { | |
static var defaultValue: String { "" } | |
} | |
enum EmptyList<T: List>: Source { | |
static var defaultValue: T { [] } | |
} | |
enum EmptyMap<T: Map>: Source { | |
static var defaultValue: T { [:] } | |
} | |
enum Zero: Source { | |
static var defaultValue: Int { 0 } | |
} | |
enum One: Source { | |
static var defaultValue: Int { 1 } | |
} | |
} | |
} | |
extension DecodableDefault.Wrapper: Decodable { | |
init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
wrappedValue = try container.decode(Value.self) | |
} | |
} | |
extension DecodableDefault.Wrapper: Equatable where Value: Equatable {} | |
extension DecodableDefault.Wrapper: Hashable where Value: Hashable {} | |
extension DecodableDefault.Wrapper: Encodable where Value: Encodable { | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
try container.encode(wrappedValue) | |
} | |
} | |
extension KeyedDecodingContainer { | |
func decode<T>(_ type: DecodableDefault.Wrapper<T>.Type, forKey key: Key) throws -> DecodableDefault.Wrapper<T> { | |
try decodeIfPresent(type, forKey: key) ?? .init() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Decodable.swift | |
// | |
// Created by Leandro Nunes Fantinatto on 13/05/21. | |
// Copyright © 2021 Leandro Nunes Fantinatto. All rights reserved. | |
// Ref: https://github.com/gonzalezreal/DefaultCodable | |
// Ref: https://github.com/JohnSundell/Codextended | |
// | |
import Foundation | |
/// Generic property-wrapper to provide default values for non-optional properties when they are not present or have a nil value. | |
/// Use-case examples: | |
/// - @DecodableDefault<EmptyString> var description: String | |
/// - @DecodableDefault<True|False> var isFoo: Bool | |
/// - @DecodableDefault<Zero|One> var floatingPoint: Int | |
/// - @DecodableDefault<ZeroDouble> var floatingPoint: Double | |
/// - @DecodableDefault<EmptyDictionary> var entities: [String: String] | |
/// | |
@propertyWrapper | |
public struct DecodableDefault<Provider: DefaultValueProvider>: Codable { | |
public var wrappedValue: Provider.Value | |
public init() { | |
wrappedValue = Provider.default | |
} | |
public init(wrappedValue: Provider.Value) { | |
self.wrappedValue = wrappedValue | |
} | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if container.decodeNil() { | |
wrappedValue = Provider.default | |
} else { | |
wrappedValue = try container.decode(Provider.Value.self) | |
} | |
} | |
} | |
extension DecodableDefault: Equatable where Provider.Value: Equatable {} | |
extension KeyedDecodingContainer { | |
func decode<P>(_: DecodableDefault<P>.Type, forKey key: Key) throws -> DecodableDefault<P> { | |
if let value = try decodeIfPresent(DecodableDefault<P>.self, forKey: key) { | |
return value | |
} else { | |
return DecodableDefault() | |
} | |
} | |
} | |
extension KeyedEncodingContainer { | |
mutating func encode<P>(_ value: DecodableDefault<P>, forKey key: Key) throws { | |
guard value.wrappedValue != P.default else { return } | |
try encode(value.wrappedValue, forKey: key) | |
} | |
} | |
public protocol DefaultValueProvider { | |
associatedtype Value: Equatable & Codable | |
static var `default`: Value { get } | |
} | |
// MARK: - Default Value Providers | |
public enum False: DefaultValueProvider { | |
public static let `default` = false | |
} | |
public enum True: DefaultValueProvider { | |
public static let `default` = true | |
} | |
public enum EmptyString: DefaultValueProvider { | |
public static let `default` = "" | |
} | |
public enum Empty<A>: DefaultValueProvider where A: Codable, A: Equatable, A: RangeReplaceableCollection { | |
public static var `default`: A { A() } | |
} | |
public enum EmptyDictionary<K, V>: DefaultValueProvider where K: Hashable & Codable, V: Equatable & Codable { | |
public static var `default`: [K: V] { Dictionary() } | |
} | |
public enum FirstCase<A>: DefaultValueProvider where A: Codable, A: Equatable, A: CaseIterable { | |
public static var `default`: A { A.allCases.first! } | |
} | |
public enum Zero: DefaultValueProvider { | |
public static let `default` = 0 | |
} | |
public enum One: DefaultValueProvider { | |
public static let `default` = 1 | |
} | |
public enum ZeroDouble: DefaultValueProvider { | |
public static let `default`: Double = 0 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment