|
1 |
| -# Open Source Template |
| 1 | +# JSEN |
2 | 2 |
|
| 3 | +> _/ˈdʒeɪsən/ JAY-sən_ |
3 | 4 |
|
4 |
| -## License |
| 5 | +JSEN (JSON Swift Enum Notation) is a lightweight enum representation of a JSON, written in Swift. |
5 | 6 |
|
6 |
| -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. |
| 7 | +A JSON, as defined in the [ECMA-404 standard](https://www.json.org) , can be: |
| 8 | + |
| 9 | +- A number |
| 10 | +- A boolean |
| 11 | +- A string |
| 12 | +- Null |
| 13 | +- An array of those things |
| 14 | +- A dictionary of those things |
| 15 | + |
| 16 | +Thus, JSONs can be represented as a recursive enum (or `indirect enum`, in Swift), effectively creating a statically-typed JSON payload in Swift. |
| 17 | + |
| 18 | +# Installation |
| 19 | + |
| 20 | +Using Swift Package Manager: |
| 21 | + |
| 22 | +```swift |
| 23 | +dependencies: [ |
| 24 | + .package(name: "JSEN", url: "https://github.com/rogerluan/JSEN", .upToNextMajor(from: "1.0.0")), |
| 25 | +] |
| 26 | +``` |
| 27 | + |
| 28 | +# Usage |
| 29 | + |
| 30 | +I think it's essential for the understanding of how simple this is, for you to visualize the JSEN declaration: |
| 31 | + |
| 32 | +```swift |
| 33 | +/// A simple JSON value representation using enum cases. |
| 34 | +public enum JSEN : Equatable { |
| 35 | + /// An integer value. |
| 36 | + case int(Int) |
| 37 | + /// A floating point value. |
| 38 | + case double(Double) |
| 39 | + /// A string value. |
| 40 | + case string(String) |
| 41 | + /// A boolean value. |
| 42 | + case bool(Bool) |
| 43 | + /// An array value in which all elements are also JSEN values. |
| 44 | + indirect case array([JSEN]) |
| 45 | + /// An object value, also known as dictionary, hash, and map. |
| 46 | + /// All values of this object are also JSEN values. |
| 47 | + indirect case dictionary([String:JSEN]) |
| 48 | + /// A null value. |
| 49 | + case null |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +That's it. |
| 54 | + |
| 55 | +###### `ExpressibleBy…Literal` |
| 56 | + |
| 57 | +Now that you're familiar with JSEN, it provides a few syntactic sugary utilities, such as conformance to most `ExpressibleBy…Literal` protocols: |
| 58 | + |
| 59 | +- `ExpressibleByIntegerLiteral` initializer returns an `.int(…)`. |
| 60 | +- `ExpressibleByFloatLiteral` initializer returns a `.double(…)`. |
| 61 | +- `ExpressibleByStringLiteral` initializer returns a `.string(…)`. |
| 62 | +- `ExpressibleByBooleanLiteral` initializer returns a `.bool(…)`. |
| 63 | +- `ExpressibleByArrayLiteral` initializer returns an `.array(…)` as long as its Elements are JSENs. |
| 64 | +- `ExpressibleByDictionaryLiteral` initializer returns an `.dictionary(…)` as long as its keys are Strings and Values JSENs. |
| 65 | +- `ExpressibleByNilLiteral` initializer returns a `.null`. |
| 66 | + |
| 67 | +Conformance to `ExpressibleBy…Literal` protocols are great when you want to build a JSON structure like this: |
| 68 | + |
| 69 | +```swift |
| 70 | +let request: [String:JSEN] = [ |
| 71 | + "key": "value", |
| 72 | + "another_key": 42, |
| 73 | +] |
| 74 | +``` |
| 75 | + |
| 76 | +But what if you're not working with literals? |
| 77 | + |
| 78 | +```swift |
| 79 | +let request: [String:JSEN] = [ |
| 80 | + "amount": normalizedAmount // This won't compile |
| 81 | +] |
| 82 | +``` |
| 83 | + |
| 84 | +Enters the… |
| 85 | + |
| 86 | +### `%` Suffix Operator |
| 87 | + |
| 88 | +```swift |
| 89 | +let request: [String:JSEN] = [ |
| 90 | +"amount": %normalizedAmount // This works! |
| 91 | +] |
| 92 | +``` |
| 93 | + |
| 94 | +The custom `%` suffix operator transforms any `Int`, `Double`, `String`, `Bool`, `[JSEN]` and `[String:JSEN]` values into its respective JSEN value. |
| 95 | + |
| 96 | + |
| 97 | +By design, no support was added to transform `Optional` into a `.null` to prevent misuse. |
| 98 | + |
| 99 | +<details><summary>Click here to expand the reason why it could lead to mistakes</summary> |
| 100 | +<p> |
| 101 | + |
| 102 | +To illustrate the possible problems around an `%optionalValue` operation, picture the following scenario: |
| 103 | + |
| 104 | +```swift |
| 105 | +let request: [String:JSEN] = [ |
| 106 | +"middle_name": %optionalString |
| 107 | +] |
| 108 | + |
| 109 | +network.put(request) |
| 110 | +``` |
| 111 | + |
| 112 | +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). |
| 113 | + |
| 114 | +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. |
| 115 | + |
| 116 | +</p> |
| 117 | +</details> |
| 118 | + |
| 119 | +### Conformance to Codable |
| 120 | + |
| 121 | +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. |
| 122 | + |
| 123 | +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: |
| 124 | +- First, it encodes the JSEN to `Data`, and attempts to decode that `Data` into the given type. |
| 125 | +- 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. |
| 126 | + |
| 127 | +### Subscript Using KeyPath |
| 128 | + |
| 129 | +Last, but not least, comes the `KeyPath` subscript. |
| 130 | + |
| 131 | +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. |
| 132 | + |
| 133 | +The subscript to JSEN allows the following syntax: |
| 134 | + |
| 135 | +```swift |
| 136 | +let request: [String:JSEN] = [ |
| 137 | + "1st": [ |
| 138 | + "2nd": [ |
| 139 | + "3rd": "Hello!" |
| 140 | + ] |
| 141 | + ] |
| 142 | +] |
| 143 | +print(request[keyPath: "1st.2nd.3rd"]) // "Hello!" |
| 144 | +``` |
| 145 | + |
| 146 | +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 :) |
| 147 | + |
| 148 | +# Contributions |
| 149 | + |
| 150 | +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! 😇 |
| 151 | + |
| 152 | +# References |
| 153 | + |
| 154 | +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. |
| 155 | + |
| 156 | +# License |
| 157 | + |
| 158 | +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. |
| 159 | + |
| 160 | +# Contact |
| 161 | + |
| 162 | +Twitter: [@rogerluan_](https://twitter.com/rogerluan_) |
0 commit comments