Skip to content

Commit

Permalink
feature: Serialized support for optionals and non-optionals, updated …
Browse files Browse the repository at this point in the history
…SerializedTransformable protocol, added comments, updated tests
  • Loading branch information
Dejan Skledar committed Sep 14, 2020
1 parent 30994c7 commit 000e6df
Show file tree
Hide file tree
Showing 17 changed files with 323 additions and 582 deletions.
84 changes: 33 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# SerializedSwift

[![Swift Package Manager](https://img.shields.io/badge/swift%20package%20manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager)
[![CocoaPods](https://img.shields.io/cocoapods/v/SerializedSwift.svg)](https://github.com/dejanskledar/SerializedSwift)
![Platforms](https://img.shields.io/static/v1?label=Platforms&message=iOS%20|%20macOS%20|%20tvOS%20|%20watchOS%20|%20Linux&color=brightgreen)

## A GSON inspired JSON decoding strategy in Swift using @propertyWrappers.
Expand All @@ -13,6 +14,22 @@
- Alternative coding keys
- Default values if JSON key is missing

```swift
struct Foo: Serializable {
@Serialized
var bar: String

@Serialized("globalId")
var id: String?

@Serialized(alternateKey: "mobileNumber")
var phoneNumber: String?

@Serialized(default: 0)
var score: Int
}
```

## Installation

### Codoapods
Expand Down Expand Up @@ -40,15 +57,15 @@ dependencies: [
class User: Serializable {
@Serialized
var name: String?
var name: String
@Serialized("globalId")
var id: String?
@Serialized(alternateKey: "mobileNumber")
var phoneNumber: String?
@SerializedRequired(default: 0)
@Serialized(default: 0)
var score: Int
required init() {}
Expand All @@ -62,7 +79,7 @@ class PowerUser: User {
@Serialized
var powerName: String?

@SerializedRequired(default: 0)
@Serialized(default: 0)
var credit: Int
}
```
Expand All @@ -74,7 +91,7 @@ class ChatRoom: Serializable {
@Serialized
var admin: PowerUser?

@SerializedRequired(default: [])
@Serialized(default: [])
var users: [User]
}
```
Expand All @@ -83,14 +100,14 @@ class ChatRoom: Serializable {
You can create own custom Transformable classes, for custom transformation logic.
```swift
class DateTransformer: Transformable {
static func transformFromJSON(from value: String) -> Date? {
static func transformFromJSON(from value: String?) -> Date? {
let formatter = DateFormatter()
return formatter.date(from: value)
return formatter.date(from: value ?? "")
}

static func transformToJson(from value: Date) -> String? {
static func transformToJson(from value: Date?) -> String? {
let formatter = DateFormatter()
return formatter.string(from: value)
return formatter.string(from: value ?? Date())
}
}

Expand All @@ -100,51 +117,39 @@ struct User: Serializable {
}
```

**`RequiredTransformable`** for non-optional properties:


## Features
### `Serializable`
- typealias-ed from `SerializableEncodable` & `SerializableDecodable`
- Custom decoding and encoding using propertyWrappers (listed below)
- Use this protocol for your classes and structures in the combination with the property wrappers belos

### `Serialized`
- Standard serialization propertyWrappers
- Used for wrapping an optional property.
- Serialization propertyWrapper for all properties, optional and non-optionals!
- Custom decoding Key
- By default using the propertyName as a Decoding Key
- Alternative Decoding Key support
- Optional Default value (if the key is missing). By default, the Default value is `nil`
- Optional Default value (if the key is missing). By default, the Default value is `nil`. For non-optionals, default value is recommended, to avoid crashes

```swift
@Serialized("primaryKey", alternativeKey: "backupKey", default: "")
@Serialized("mainKey", alternativeKey: "backupKey", default: "")
var key: String?
```

### `SerializedRequired`
- Similar to `Serialized`, but for wrapping a non-optional property.
- Will crash while accessing the property, if the key was missing in the JSON, and no `default` value was set

```swift
@SerializedRequired("primaryKey", alternativeKey: "backupKey", default: "")
var key: String
```

### `SerializedTransformable`
- Custom transforming property wrapper
- Create own Transformable classes
- Transforming Decodable objects to own types, like custom String Date format to native Date

```swift
class DateTransformer: Transformable {
static func transformFromJSON(from value: String) -> Date? {
static func transformFromJSON(from value: String?) -> Date? {
let formatter = DateFormatter()
return formatter.date(from: value)
return formatter.date(from: value ?? "")
}

static func transformToJson(from value: Date) -> String? {
static func transformToJson(from value: Date?) -> String? {
let formatter = DateFormatter()
return formatter.string(from: value)
return formatter.string(from: value ?? "")
}
}

Expand All @@ -155,29 +160,6 @@ var key: String
}
```

### `SerializedRequiredTransformable`
- Similar to `SerializedTransformable` but working with non-optionals

```swift
class DateTransformer: RequiredTransformable {
static func transformFromJSON(from value: String?) -> Date {
let formatter = DateFormatter()
return formatter.date(from: value)
}

static func transformToJson(from value: Date?) -> String {
let formatter = DateFormatter()
return formatter.string(from: value)
}
}

// Usage of `SerializedRequiredTransformable` with non-optional Date
struct User: Serializable {
@SerializedRequiredTransformable<DateTransformer>
var birthDate: Date
}
```

### Contribute

This is only a tip of the iceberg of what can one achieve using Property Wrappers and how se can improve Decoding and Encoding JSON in Swift. Feel free to colaborate.
6 changes: 6 additions & 0 deletions Sources/SerializedSwift/DecodableProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

import Foundation

///
///
/// Decodable property protocol implemented in Serialized where Wrapped Value is Decodable
///
///

public protocol DecodableProperty {
typealias DecodeContainer = KeyedDecodingContainer<SerializedCodingKeys>

Expand Down
6 changes: 6 additions & 0 deletions Sources/SerializedSwift/EncodableProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

import Foundation

///
///
/// Encodable property protocol implemented in Serialized where Wrapped Value is Encodable
///
///

public protocol EncodableProperty {
typealias EncodeContainer = KeyedEncodingContainer<SerializedCodingKeys>

Expand Down
24 changes: 0 additions & 24 deletions Sources/SerializedSwift/RequiredTransformable.swift

This file was deleted.

5 changes: 4 additions & 1 deletion Sources/SerializedSwift/SerializableDecodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ public protocol SerializableDecodable: Decodable {
//

public extension SerializableDecodable {


/// Main decoding logic. Decodes all properties marked with @Serialized
/// - Parameter decoder: The JSON Decoder
/// - Throws: Throws JSON Decoding error if present
func decode(from decoder: Decoder) throws {
// Get the container keyed by the SerializedCodingKeys defined by the propertyWrapper @Serialized
let container = try decoder.container(keyedBy: SerializedCodingKeys.self)
Expand Down
4 changes: 4 additions & 0 deletions Sources/SerializedSwift/SerializableEncodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public protocol SerializableEncodable: Encodable {}
//

public extension SerializableEncodable {

/// Encodes all properties wrapped with `SerializableEncodable` (or `Serialized`)
/// - Parameter encoder: The default encoder
/// - Throws: Throws JSON Encoding error
func encode(to encoder: Encoder) throws {
// Get the container keyed by the SerializedCodingKeys defined by the propertyWrapper @Serialized
var container = encoder.container(keyedBy: SerializedCodingKeys.self)
Expand Down
60 changes: 43 additions & 17 deletions Sources/SerializedSwift/Serialized.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,68 @@ import Foundation

public typealias Serializable = SerializableEncodable & SerializableDecodable

// Default value is by default nil. Can be used directly without arguments
@propertyWrapper
/// Property wrapper for Serializable (Encodable + Decodable) properties.
/// The Object itself must conform to Serializable (or SerializableEncodable / SerializableDecodable)
/// Default value is by default nil. Can be used directly without arguments
public final class Serialized<T> {
let key: String?
let alternateKey: String?
var value: T?
var key: String?
var alternateKey: String?

public var wrappedValue: T? {
private var _value: T?

/// Wrapped value getter for optionals
private func _wrappedValue<U>(_ type: U.Type) -> U? where U: ExpressibleByNilLiteral {
return _value as? U
}

/// Wrapped value getter for non-optionals
private func _wrappedValue<U>(_ type: U.Type) -> U {
return _value as! U
}

public var wrappedValue: T {
get {
return value
return _wrappedValue(T.self)
} set {
value = newValue
_value = newValue
}
}

/// Defualt init for Serialized wrapper
/// - Parameters:
/// - key: The JSON decoding key to be used. If `nil` (or not passed), the property name gets used for decoding
/// - alternateKey: The alternative JSON decoding key to be used, if the primary decoding key fails
/// - value: The default value to be used, if the decoding fails. If not passed, `nil` is used.
public init(_ key: String? = nil, alternateKey: String? = nil, default value: T? = nil) {
self.key = key
self.alternateKey = alternateKey
self.value = value
}

public init(default value: T? = nil) {
self.key = nil
self.alternateKey = nil
self.value = value
self._value = value
}
}

// Encodable support
/// Encodable support
extension Serialized: EncodableProperty where T: Encodable {

/// Basic property encoding with the key (if present), or propertyName if key not present
/// - Parameters:
/// - container: The default container
/// - propertyName: The Property Name to be used, if key is not present
/// - Throws: Throws JSON encoding errorj
public func encodeValue(from container: inout EncodeContainer, propertyName: String) throws {
let codingKey = SerializedCodingKeys(key: key ?? propertyName)
try container.encodeIfPresent(wrappedValue, forKey: codingKey)
}
}

// Decodable support
/// Decodable support
extension Serialized: DecodableProperty where T: Decodable {

/// Adding the DecodableProperty support for Serialized annotated objects, where the Object conforms to Decodable
/// - Parameters:
/// - container: The decoding container
/// - propertyName: The property name of the Wrapped property. Used if no key (or nil) is present
/// - Throws: Doesnt throws anything; Sets the wrappedValue to nil instead (possible crash for non-optionals if no default value was set)
public func decodeValue(from container: DecodeContainer, propertyName: String) throws {
let codingKey = SerializedCodingKeys(key: key ?? propertyName)

Expand All @@ -55,7 +79,9 @@ extension Serialized: DecodableProperty where T: Decodable {
} else {
guard let altKey = alternateKey else { return }
let altCodingKey = SerializedCodingKeys(key: altKey)
wrappedValue = try? container.decodeIfPresent(T.self, forKey: altCodingKey)
if let value = try? container.decodeIfPresent(T.self, forKey: altCodingKey) {
wrappedValue = value
}
}
}
}
8 changes: 7 additions & 1 deletion Sources/SerializedSwift/SerializedCodingKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@

import Foundation

///
///
/// Dynamic Coding Key Object
///
///

public struct SerializedCodingKeys: CodingKey {
public var stringValue: String
public var intValue: Int?

public init(key: String) {
stringValue = key
}

public init?(stringValue: String) {
self.stringValue = stringValue
}
Expand Down
Loading

0 comments on commit 000e6df

Please sign in to comment.