-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
954 additions
and
92 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,90 +1,7 @@ | ||
# Xcode | ||
# | ||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore | ||
|
||
## User settings | ||
.DS_Store | ||
/.build | ||
/Packages | ||
/*.xcodeproj | ||
xcuserdata/ | ||
|
||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) | ||
*.xcscmblueprint | ||
*.xccheckout | ||
|
||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) | ||
build/ | ||
DerivedData/ | ||
*.moved-aside | ||
*.pbxuser | ||
!default.pbxuser | ||
*.mode1v3 | ||
!default.mode1v3 | ||
*.mode2v3 | ||
!default.mode2v3 | ||
*.perspectivev3 | ||
!default.perspectivev3 | ||
|
||
## Obj-C/Swift specific | ||
*.hmap | ||
|
||
## App packaging | ||
*.ipa | ||
*.dSYM.zip | ||
*.dSYM | ||
|
||
## Playgrounds | ||
timeline.xctimeline | ||
playground.xcworkspace | ||
|
||
# Swift Package Manager | ||
# | ||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. | ||
# Packages/ | ||
# Package.pins | ||
# Package.resolved | ||
# *.xcodeproj | ||
# | ||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata | ||
# hence it is not needed unless you have added a package configuration file to your project | ||
# .swiftpm | ||
|
||
.build/ | ||
|
||
# CocoaPods | ||
# | ||
# We recommend against adding the Pods directory to your .gitignore. However | ||
# you should judge for yourself, the pros and cons are mentioned at: | ||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control | ||
# | ||
# Pods/ | ||
# | ||
# Add this line if you want to avoid checking in source code from the Xcode workspace | ||
# *.xcworkspace | ||
|
||
# Carthage | ||
# | ||
# Add this line if you want to avoid checking in source code from Carthage dependencies. | ||
# Carthage/Checkouts | ||
|
||
Carthage/Build/ | ||
|
||
# Accio dependency management | ||
Dependencies/ | ||
.accio/ | ||
|
||
# fastlane | ||
# | ||
# It is recommended to not store the screenshots in the git repo. | ||
# Instead, use fastlane to re-generate the screenshots whenever they are needed. | ||
# For more information about the recommended setup visit: | ||
# https://docs.fastlane.tools/best-practices/source-control/#source-control | ||
|
||
fastlane/report.xml | ||
fastlane/Preview.html | ||
fastlane/screenshots/**/*.png | ||
fastlane/test_output | ||
|
||
# Code Injection | ||
# | ||
# After new code Injection tools there's a generated folder /iOSInjectionProject | ||
# https://github.com/johnno1962/injectionforxcode | ||
|
||
iOSInjectionProject/ | ||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// swift-tools-version:5.4 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "JSEN", | ||
products: [ | ||
.library(name: "JSEN", targets: ["JSEN"]), | ||
], | ||
targets: [ | ||
.target( | ||
name: "JSEN", | ||
dependencies: [], | ||
path: "Sources" | ||
), | ||
.testTarget( | ||
name: "JSENTests", | ||
dependencies: ["JSEN"], | ||
path: "Tests" | ||
), | ||
] | ||
) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,162 @@ | ||
# Open Source Template | ||
# JSEN | ||
|
||
> _/ˈdʒeɪsən/ JAY-sən_ | ||
## License | ||
JSEN (JSON Swift Enum Notation) is a lightweight enum representation of a JSON, written in Swift. | ||
|
||
This project is open source and covered by a standard 2-clause BSD license. That means you have to mention *Roger Oba* as the original author of this code and reproduce the LICENSE text inside your app, repository, project or research paper. | ||
A JSON, as defined in the [ECMA-404 standard](https://www.json.org) , can be: | ||
|
||
- A number | ||
- A boolean | ||
- A string | ||
- Null | ||
- An array of those things | ||
- A dictionary of those things | ||
|
||
Thus, JSONs can be represented as a recursive enum (or `indirect enum`, in Swift), effectively creating a statically-typed JSON payload in Swift. | ||
|
||
# Installation | ||
|
||
Using Swift Package Manager: | ||
|
||
```swift | ||
dependencies: [ | ||
.package(name: "JSEN", url: "https://github.com/rogerluan/JSEN", .upToNextMajor(from: "1.0.0")), | ||
] | ||
``` | ||
|
||
# Usage | ||
|
||
I think it's essential for the understanding of how simple this is, for you to visualize the JSEN declaration: | ||
|
||
```swift | ||
/// A simple JSON value representation using enum cases. | ||
public enum JSEN : Equatable { | ||
/// An integer value. | ||
case int(Int) | ||
/// A floating point value. | ||
case double(Double) | ||
/// A string value. | ||
case string(String) | ||
/// A boolean value. | ||
case bool(Bool) | ||
/// An array value in which all elements are also JSEN values. | ||
indirect case array([JSEN]) | ||
/// An object value, also known as dictionary, hash, and map. | ||
/// All values of this object are also JSEN values. | ||
indirect case dictionary([String:JSEN]) | ||
/// A null value. | ||
case null | ||
} | ||
``` | ||
|
||
That's it. | ||
|
||
###### `ExpressibleBy…Literal` | ||
|
||
Now that you're familiar with JSEN, it provides a few syntactic sugary utilities, such as conformance to most `ExpressibleBy…Literal` protocols: | ||
|
||
- `ExpressibleByIntegerLiteral` initializer returns an `.int(…)`. | ||
- `ExpressibleByFloatLiteral` initializer returns a `.double(…)`. | ||
- `ExpressibleByStringLiteral` initializer returns a `.string(…)`. | ||
- `ExpressibleByBooleanLiteral` initializer returns a `.bool(…)`. | ||
- `ExpressibleByArrayLiteral` initializer returns an `.array(…)` as long as its Elements are JSENs. | ||
- `ExpressibleByDictionaryLiteral` initializer returns an `.dictionary(…)` as long as its keys are Strings and Values JSENs. | ||
- `ExpressibleByNilLiteral` initializer returns a `.null`. | ||
|
||
Conformance to `ExpressibleBy…Literal` protocols are great when you want to build a JSON structure like this: | ||
|
||
```swift | ||
let request: [String:JSEN] = [ | ||
"key": "value", | ||
"another_key": 42, | ||
] | ||
``` | ||
|
||
But what if you're not working with literals? | ||
|
||
```swift | ||
let request: [String:JSEN] = [ | ||
"amount": normalizedAmount // This won't compile | ||
] | ||
``` | ||
|
||
Enters the… | ||
|
||
### `%` Suffix Operator | ||
|
||
```swift | ||
let request: [String:JSEN] = [ | ||
"amount": %normalizedAmount // This works! | ||
] | ||
``` | ||
|
||
The custom `%` suffix operator transforms any `Int`, `Double`, `String`, `Bool`, `[JSEN]` and `[String:JSEN]` values into its respective JSEN value. | ||
|
||
|
||
By design, no support was added to transform `Optional` into a `.null` to prevent misuse. | ||
|
||
<details><summary>Click here to expand the reason why it could lead to mistakes</summary> | ||
<p> | ||
|
||
To illustrate the possible problems around an `%optionalValue` operation, picture the following scenario: | ||
|
||
```swift | ||
let request: [String:JSEN] = [ | ||
"middle_name": %optionalString | ||
] | ||
|
||
network.put(request) | ||
``` | ||
|
||
Now, if the `%` operator detected a nonnull String, great. But if it detected its underlying value to be `.none` (aka `nil`), it would convert the value to `.null`, which, when encoded, would be converted to `NSNull()` (more on this below in the Codable section). As you imagine, `NSNull()` and `nil` have very different behaviors when it comes to RESTful APIs - the former might delete the key information on the database, while the latter will simply be ignored by Swift Dictionary (as if the field wasn't even there). | ||
|
||
Hence, if you want to use an optional value, make the call explicit by using either `.null` if you know the value must be encoded into a `NSNull()` instance, or unwrap its value and wrap it around one of the non-null JSEN cases. | ||
|
||
</p> | ||
</details> | ||
|
||
### Conformance to Codable | ||
|
||
Of course! We couldn't miss this. JSEN has native support to `Encodable & Decodable` (aka `Codable`), so you can easily parse JSEN to/from JSON-like structures. | ||
|
||
One additional utility was added as well, which's the `decode(as:)` function. It receives a Decodable-conformant Type as parameter and will attempt to decode the JSEN value into the given type using a two-pass strategy: | ||
- First, it encodes the JSEN to `Data`, and attempts to decode that `Data` into the given type. | ||
- If that fails and the JSEN is a `.string(…)` case, it attempts to encode the JSEN's string using `.utf8`. If it is able to encode it, it attempts to decode the resulting `Data` into the given type. | ||
|
||
### Subscript Using KeyPath | ||
|
||
Last, but not least, comes the `KeyPath` subscript. | ||
|
||
Based on [@olebegemann](https://twitter.com/olebegemann)'s [article](https://oleb.net/blog/2017/01/dictionary-key-paths), `KeyPath` is a simple struct used to represent multiple segments of a string. It is initializable by a string literal such as `"this.is.a.keypath"` and, when initialized, the string gets separated by periods, which compounds the struct's segments. | ||
|
||
The subscript to JSEN allows the following syntax: | ||
|
||
```swift | ||
let request: [String:JSEN] = [ | ||
"1st": [ | ||
"2nd": [ | ||
"3rd": "Hello!" | ||
] | ||
] | ||
] | ||
print(request[keyPath: "1st.2nd.3rd"]) // "Hello!" | ||
``` | ||
|
||
Without this syntax, to access a nested value in a dictionary you'd have to create multiple chains of awkward optionals and unwrap them in weird and verbosy ways. I'm not a fan of doing that :) | ||
|
||
# Contributions | ||
|
||
If you spot something wrong, missing, or if you'd like to propose improvements to this project, please open an Issue or a Pull Request with your ideas and I promise to get back to you within 24 hours! 😇 | ||
|
||
# References | ||
|
||
JSEN was heavily based on [Statically-typed JSON payload in Swift](https://jobandtalent.engineering/statically-typed-json-payload-in-swift-bd193a9e8cf2) and other various implementations of this same utility spread throughout Stack Overflow and Swift Forums. I brought everything I needed together in this project because I couldn't something similar as a Swift Package, that had everything I needed. | ||
|
||
# License | ||
|
||
This project is open source and covered by a standard 2-clause BSD license. That means you can use (publicly, commercially and privately), modify and distribute this project's content, as long as you mention *Roger Oba* as the original author of this code and reproduce the LICENSE text inside your app, repository, project or research paper. | ||
|
||
# Contact | ||
|
||
Twitter: [@rogerluan_](https://twitter.com/rogerluan_) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// Copyright © 2021 Roger Oba. All rights reserved. | ||
|
||
import Foundation | ||
|
||
extension JSEN : Encodable { | ||
public func encode(to encoder: Encoder) throws { | ||
var container = encoder.singleValueContainer() | ||
switch self { | ||
case .int(let int): try container.encode(int) | ||
case .double(let double): try container.encode(double) | ||
case .string(let string): try container.encode(string) | ||
case .bool(let bool): try container.encode(bool) | ||
case .array(let array): try container.encode(array) | ||
case .dictionary(let dictionary): try container.encode(dictionary) | ||
case .null: try container.encodeNil() | ||
} | ||
} | ||
} | ||
|
||
extension JSEN : Decodable { | ||
public init(from decoder: Decoder) throws { | ||
let container = try decoder.singleValueContainer() | ||
if let value = try? container.decode(Int.self) { | ||
self = .int(value) | ||
} else if let value = try? container.decode(Double.self) { | ||
self = .double(value) | ||
} else if let value = try? container.decode(String.self) { | ||
self = .string(value) | ||
} else if let value = try? container.decode(Bool.self) { | ||
self = .bool(value) | ||
} else if let value = try? container.decode([JSEN].self) { | ||
self = .array(value) | ||
} else if let value = try? container.decode([String:JSEN].self) { | ||
self = .dictionary(value) | ||
} else if container.decodeNil() { | ||
self = .null | ||
} else { | ||
throw NSError(domain: "domain.codable.jsen", code: 1, userInfo: [ "message" : "Failed to decode JSEN into any known type." ]) | ||
} | ||
} | ||
|
||
/// Decodes **self** into the given type, if possible. | ||
/// | ||
/// This method will attempt to decode to the given type by first encoding **self** to Data, and then attempting to decode that Data. | ||
/// If this step fails, it will attempt to encode **self** using utf8 if **self** is a `.string` case. If it succeeds, it will attempt to | ||
/// decode into the given type using the resulting Data. | ||
/// | ||
/// - Parameters: | ||
/// - type: the Decodable type to decode **self** into. | ||
/// - dumpingErrorOnFailure: whether the function should dump the error on the console, upon failure. Set true for debugging purposes. Defaults to false. | ||
/// - Returns: An instance of the given type, or nil if the decoding wasn't possible. | ||
public func decode<T : Decodable>(as type: T.Type, dumpingErrorOnFailure: Bool = false) -> T? { | ||
do { | ||
let data = try JSONEncoder().encode(self) | ||
return try JSONDecoder().decode(type, from: data) | ||
} catch { | ||
do { | ||
switch self { | ||
case .string(let string): | ||
guard let data = string.data(using: .utf8) else { | ||
// Should never happen | ||
assertionFailure("Received a string that is utf8-encoded. This is a provider precondition, please investigate why this provider is sending strings encoded in something different than utf8.") | ||
return nil | ||
} | ||
return try JSONDecoder().decode(type, from: data) | ||
default: throw error | ||
} | ||
} catch { | ||
if dumpingErrorOnFailure { | ||
dump(error) | ||
} | ||
return nil | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.