From 48f1ae409c9c8ef1b460ec5ece9218613e46d945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veljko=20Tekelerovi=C4=87?= Date: Sat, 28 May 2022 03:06:28 +0200 Subject: [PATCH] Fridge 0.9 Added push method (#27) - Introduced push method to the main interface - Added simple Playground example (#34) Other: - Another (quick) README update (#30) - Removed do/catch blocks from the Grabber Closes #27 Closes #34 Fixes #30 Squashed commits: commit b2b6dbe0a6103b8f5805dc77702e20a6e15ec6fc commit 7196555823defa8c2dd9465cae194de93fbf6d7b --- .../Fridge basics.playground/Contents.swift | 54 ++++++++++ .../contents.xcplayground | 4 + Guides/Usage.md | 23 +++- Package.swift | 2 +- README.md | 101 ++++++++---------- Sources/Fridge/Errors.swift | 3 + Sources/Fridge/Fridge.swift | 7 ++ Sources/Fridge/Grabber.swift | 64 +++++++---- 8 files changed, 176 insertions(+), 82 deletions(-) create mode 100644 Guides/Examples/Fridge basics.playground/Contents.swift create mode 100644 Guides/Examples/Fridge basics.playground/contents.xcplayground diff --git a/Guides/Examples/Fridge basics.playground/Contents.swift b/Guides/Examples/Fridge basics.playground/Contents.swift new file mode 100644 index 0000000..5ff730c --- /dev/null +++ b/Guides/Examples/Fridge basics.playground/Contents.swift @@ -0,0 +1,54 @@ +import UIKit +import Fridge + +//MARK: Grab example + +let todoEndpoint = URL(string: "https://jsonplaceholder.typicode.com/todos/")! +struct ToDo: Decodable { + var id: Int + var title: String + var completed: Bool +} + +async { + print("--> Grabbing all TODO objects...") + do { + let results: [ToDo] = try await Fridge.grab🔮(from: todoEndpoint) + // print all the results + for item in results { + print("ID:\(item.id) - \(item.title)") + } + print("|\nSuccessfully grabbed \(results.count) ToDO objects\n-----") + } catch { + print("Grab failed.") + } +} + +//MARK: - Push example + +struct Comment: Codable, CustomDebugStringConvertible { + let postId: Int + let id: Int + let name: String + let email: String + let body: String + + var debugDescription: String { + return "[Comment] (ID:\(id)) \(name) - \(email). Body: \(body)" + } +} + +async { + print("--> Posting new comment...") + do { + + let newComment = Comment(postId: 12, id: 100, name: "New comment", email: "some@email.com", body: "Body of the new comment") + + let response: Comment = try await Fridge.push📡(newComment, to: "https://jsonplaceholder.typicode.com/comments/") + + print("New comment successfuly pushed.\nReturned (response) object:") + print(response) + } catch { + print("Push failed") + } +} diff --git a/Guides/Examples/Fridge basics.playground/contents.xcplayground b/Guides/Examples/Fridge basics.playground/contents.xcplayground new file mode 100644 index 0000000..ebe9554 --- /dev/null +++ b/Guides/Examples/Fridge basics.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Guides/Usage.md b/Guides/Usage.md index b2339e7..9aedf75 100644 --- a/Guides/Usage.md +++ b/Guides/Usage.md @@ -1,18 +1,19 @@ # 👨‍🚀 Fridge Usage Guide -In short, following guide describes **how to**: +Following guide describes **how to**: - [fetch data from network](#network-fetching) - [load/store an object](#data-handling) - [handle errors](#error-handling) -Checkout architecture overview in separate [diagram](Guides/Fridge.diagram.md) file. -For more information, you can always check provided, `in-code` documentation. +Checkout _architecture overview_ in separate [diagram](Guides/Fridge.diagram.md) file. +Find out different wasy to easily use`Fridge` in provided [playground file](Guides/Examples/Fridge-basics). +For deeper information, you can check `in-code` documentation. --- ## Network fetching With Fridge, network fetching is performed in just 3 steps: -1. Make sure your desired `struct` conforms to `Decodable` +1. Conform your desired `struct` to `Decodable` 2. Define `URL` endpoint where your model resides 2. Await for Fridge to `grab🔮(...)` the object @@ -96,4 +97,16 @@ If your store identifier cannot be found `Fridge` will throw an Error. Check the ## ⛔️ Error Handling Each of the Fridge method, is internally marked with `throws` keyword. Here's what you can deal with: -// list all errors here +```Swift +enum FridgeErrors: Error { + case grabFailed + case pushFailed + case decodingFailed +} +``` + +You can expect `FridgeErrors` on following calls: + - `grab🔮` + +_NOTE:_ +Fridge error model is not yet final. You may expect different error message or structures before `v1.0` release. diff --git a/Package.swift b/Package.swift index 6a4b236..9585806 100644 --- a/Package.swift +++ b/Package.swift @@ -35,7 +35,7 @@ let package = Package( .product(name: "bsoncoder", package: "BSONCoder") ], exclude: [ - "../../Guides/", + "../../Guides", "../../README.md" ] ), diff --git a/README.md b/README.md index 67a5c75..e9ca4dd 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,8 @@

- Fridge - Lightweight, asnyc and extreeemely simple to use fetch or store mechanism. -
- Let your fancy struct(s) raise and shine again, while focusing on 💬 🥊🤖⭐️🗝 other important stuff.

+ Fridge - extremely simple async/await fetch-and-store implementation you'll ever see !
+ Let your fancy struct(s) raise and shine again, allowing you to focus on 💬 🥊🤖⭐️🗝 stuff.



@@ -43,61 +42,65 @@

-

# 💠 Library interface -`Fridge` is a freezing device ❄️. It has to keep things cool enough, exposing following icy interface: -|Method name|TLDR| -|:-|:-| -|`Fridge.grab🔮(from: URL)`|Grabs your model from the network endpoint (_iOS 15+ only_)| -|`Fridge.freeze🧊(object, identifier)`|Safely "freezes" your `struct` to persistant store| -|`Fridge.unfreeze🪅🎉(identifier)`|"Unfreezes", your previously frozen, `struct` giving you control of it| -|`Fridge.isFrozen🔬(identifier)`|Quickly check if your `Fridge` have been loaded| - -### Async/Await networking -`Fridge` is supported by `async/await` and is here to reduce the pain of: - - _fetching your stuff from the network_, +`Fridge` is a freezing device ❄️. It has to keep things cool enough, exposing following icy interface. + +### Async/Await networking + - `Fridge.grab🔮(from: URL)` -> Grabs your model from the network endpoint (_iOS 15+ only_) + - `Fridge.push📡(object, to)` -> Pushes (sends) your model to designated network endpoint (_iOS 15+ only_) + +Fridge is supported by `async/await` and is here to reduce the pain of: + - fetching _your stuff_ from the network, - parsing or decoding (JSON) data, - - _storing and retreiving stuff_ to the persistant storage - - doing endless _error checking_ + - doing boring _error checking_ + - and yeah... good old **closures**. -And yeah... good old **closures**. You can even say goodbye to closures and CoreData if you want! +With Fridge, you can even _say goodbye to closures and CoreData_ if you want! ### Persistant storage -`Fridge` is built on Foundation principles and uses `BSON` as internal storage mechanism. -All you have to do is to conform your struct to `Encodable` and you're ready to go, Fridge will take care of the rest. + - `Fridge.freeze🧊(object, identifier)` -> Safely "freezes" your `struct` to persistant store + - `Fridge.unfreeze🪅🎉(identifier)` -> "Unfreezes" (previously frozen), `struct` giving you control of it + - `Fridge.isFrozen🔬(identifier)` -> Quickly check if your `Fridge` have been loaded with certain `struct` + +Fridge storage mechanics are built on Foundation principles and use `BSON` as internal storage mechanism. All you have to do is to conform your struct to `Encodable` and you're ready to go, Fridge will take care of the rest. Checkout internal documentation for more information. -### _"Talking is cheap. Show me the code."_ - Linus Torvalds +# Code examples +> _Talking is cheap. Show me the code._ - Linus Torvalds ```Swift -// there's a catch here (!) -struct GHRepo { +// Make your fancy struct conform to Decodable +struct GHRepo: Decodable { var name: String var repositoryURL: URL } +// Call grab🔮 method... let myRepo: GHRepo = try await Fridge.grab🔮(from: URL("https://github.com/vexy/")!) -// then, just + +// Then, at your will print(myRepo) print(myRepo.name) +``` -// ..and that's it ! (khm 🧟‍♂️) -``` - -### _"RTFM isn't a joke..."_ -Aside from discovering how to remove that _shocking build error_ from the 👆 code example, in the **[Docs](Guides/Usage.md)** you'll quickly figure out how to: +# Documentation +> _RTFM isn't a joke..._ 🥴 + +In the **[Docs](Guides/Usage.md)** you'll quickly figure out how to: + - *easily fetch object* from the network, - *persistently store* your objects, - *load them back* into your app, - *catch nasty errors* along the way - - (**COMMING SOON**) _fine grain_ fetch or store process - - dirtly little secrets about the Fridge + - all other dirtly little secrets about the Fridge -Oh yeah, `Fridge` can ofer way way more, _but it's alllll in the **Docs**_. Even the [architecture](Guides/Fridge.diagram.md) diagrams... ∰ 🥴 +Check [usage examples](Guides/Examples) or entire [Guides](Guides/) collection for more goodies. +`Xcode Playground` file can be found [here](Guides/Examples/Fridge-basics). +For a bigger picture overview, feel free to check [architecture](Guides/Fridge.diagram.md) diagrams... ∰ # Installation -Using `Swift Package Manager` is by far the sexiest way to install silly `Fridge`. -Update both `dependencies` and `targets` section in your `Package.swift`, to look something like this. +Using `Swift Package Manager` is by far the sexiest way to install `Fridge`. +Update both `dependencies` and `targets` section in your `Package.swift`, to look something like this: ```Swift // swift-tools-version:5.3 @@ -121,7 +124,7 @@ url: "https://github.com/vexy/Fridge.git" branch: "main" ``` -# Minimum versions required +## Minimum versions required For `Fridge` to work in full capacity, following Swift & iOS configuration is _recommended_: - Xcode `13.1+` - Swift `5.5` @@ -144,37 +147,27 @@ That may be helpfull for `Xcode 12.x`, assuming `Swift 5.x` is installed. Checkout official [Swift.org](https://www.swift.org/) website, for supporting earlier than minimums and other info. -# Documentation & Examples -Documentation can be found in [Guides](Guides/) folder. -You can find [usage examples](Guides/Usage.md) or standalone sample projects. (TODO: ADD LINK HERE) - -# External dependencies +## External dependencies Fridge uses [BSONCoder v0.9](https://github.com/vexy/bsoncoder) - Copyright by [Vexy](https://github.com/vexy). Check original library licencing information under licencing section in README file. -# Contribution -If you like `Fridge`, feel free to fire a [pull request](https://github.com/vexy/Fridge/pulls). -The prefered way is to branch off the `main` branch, complete feature or a fix and then merge to `development`. -After the pull request has been approved, your change will be merged to `main`. +# Contribution guidelines +If you like Fridge, feel free to fire a [pull request](https://github.com/vexy/Fridge/pulls). +The prefered way is to branch off the `main` branch, complete feature or a fix and then merge to `development`. After the pull request has been approved, your change will be merged to `main`. Don't be affraid to start any [discussions](https://github.com/vexy/Fridge/discussions) if you think so. [Issues](https://github.com/vexy/Fridge/issues) section is a good way to start, if you stumble upon the way. --- - -

- Fridge - perhaps the silliest Swift5 async/await fetch-and-store implementation you'll ever see !
-

- - -**FRIDGE IS UNDER ACTIVE DEVELOPMENT ALMOST REACHING v1.0** -Fridge **BETA** release : *v0.8.9 ( UTC2022-05-20 )* +(**FRIDGE IS UNDER ACTIVE DEVELOPMENT ALMOST REACHING v1.0**) +Fridge **BETA** release : *v0.9 ( UTC2022-05-28 )* Copyright © 2016 Veljko Tekelerović | MIT license -**PGP:** `6302D860 B74CBD34 6482DBA2 518766D0 8213DBC0` +**PGP:** `6302 D860 B74C BD34 6482 DBA2 5187 66D0 8213 DBC0` +

+ Fridge - Lightweight, fast and extreeemely simple to use fetch or store mechanism.
profile for Vexy on Stack Exchange, a network of free, community-driven Q&A sites

-
diff --git a/Sources/Fridge/Errors.swift b/Sources/Fridge/Errors.swift index 9f08c2c..d42abf6 100644 --- a/Sources/Fridge/Errors.swift +++ b/Sources/Fridge/Errors.swift @@ -8,7 +8,10 @@ import Foundation enum FridgeErrors: Error { + //add case grabFailed(reason: Error) or similar case grabFailed + case pushFailed + case decodingFailed } enum FreezingErrors: Error { diff --git a/Sources/Fridge/Fridge.swift b/Sources/Fridge/Fridge.swift index 4c7aef9..e38b782 100644 --- a/Sources/Fridge/Fridge.swift +++ b/Sources/Fridge/Fridge.swift @@ -49,6 +49,13 @@ extension Fridge { let theObject: D = try await grb.grab(using: urlRequest) return theObject } + + /// Tries to push an object to a given URL, returning the structured response + public static func push📡(_ object: E, to: String) async throws -> D { + let pusher = Grabber() + let pushResponseObject: D = try await pusher.push(object: object, urlString: to) + return pushResponseObject + } } //MARK: - Object persistent storage diff --git a/Sources/Fridge/Grabber.swift b/Sources/Fridge/Grabber.swift index 592e269..cb87f4e 100644 --- a/Sources/Fridge/Grabber.swift +++ b/Sources/Fridge/Grabber.swift @@ -31,34 +31,54 @@ import Foundation @available(iOS 15.0, *) final internal class Grabber { func grab(from url: URL) async throws -> D { - do { - let (data, _) = try await URLSession.shared.data(from: url) - - // try to serialize the data - guard let decodedObject = try? JSONDecoder().decode(D.self, from: data) else { - throw FridgeErrors.grabFailed - } - - // return grabbed object - return decodedObject - } catch { + guard let rawData = try? await URLSession.shared.data(from: url).0 else { throw FridgeErrors.grabFailed } + guard let decodedObject = try? JSONDecoder().decode(D.self, from: rawData) else { + throw FridgeErrors.decodingFailed + } + + // return decoded object + return decodedObject } func grab(using urlRequest: URLRequest) async throws -> D { - do { - let (data, _) = try await URLSession.shared.data(for: urlRequest) - - // serialize returned data - guard let decodedObject = try? JSONDecoder().decode(D.self, from: data) else { - throw FridgeErrors.grabFailed - } - - // finally returned real object - return decodedObject - } catch { + guard let rawData = try? await URLSession.shared.data(for: urlRequest).0 else { + throw FridgeErrors.grabFailed + } + guard let decodedObject = try? JSONDecoder().decode(D.self, from: rawData) else { throw FridgeErrors.grabFailed } + + // return decoded object + return decodedObject + } + + + func push(object: E, urlString: String) async throws -> D { + //construct a JSON based URLRequest + guard let urlObject = URL(string: urlString) else { throw FridgeErrors.decodingFailed } + var request = URLRequest(url: urlObject) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Fridge.grab", forHTTPHeaderField: "User-Agent") + + // serialize given object and attach it to request body + guard let serializedObject = try? JSONEncoder().encode(object.self) else { + throw FridgeErrors.decodingFailed + } + request.httpBody = serializedObject + + //execute request and wait for response + guard let responseRawData = try? await URLSession.shared.data(for: request).0 else { + throw FridgeErrors.pushFailed + } + + // try to decode the data into given respose object + guard let decodedResponse = try? JSONDecoder().decode(D.self, from: responseRawData) else { + throw FridgeErrors.decodingFailed + } + + return decodedResponse } }