Serverside Swift and Vapor on Ubuntu - swift

I am trying to write an API based on Project2 from Server Side Swift by Paul Hudson
https://www.hackingwithswift.com/store/server-side-swift
I am running:
Swift version 5.0 (swift-5.0-RELEASE)
Target: x86_64-unknown-linux-gnu
and
Vapor Toolbox: 3.1.10
on Ubuntu 18.04.3 LTS (GNU/Linux 4.15.0-66-generic x86_64)
The idea is to use Fluent and SQLite to maintain (CRUD) property records.
The relevant part of configure.swift is:
let directoryConfig = DirectoryConfig.detect()
services.register(directoryConfig)
try services.register(FluentSQLiteProvider())
var databaseConfig = DatabasesConfig()
let db = try SQLiteDatabase(storage: .file(path: "\(directoryConfig.workDir)properties.db"))
databaseConfig.add(database: db, as: .sqlite)
services.register(databaseConfig)
var migrationConfig = MigrationConfig()
migrationConfig.add(model: Property.self, database: .sqlite)
services.register(migrationConfig)
In Routes.swift I have this:
router.post(Property.self, at: "properties", "create") { req, property -> Future<Property> in
return property.save(on: req)
}
router.get("properties", "list") { req -> Future<[Property]> in
return Property.query(on: req).all()
}
And Property.swift is this:
// Property.swift
//
// Created by Peter Matthews on 07/12/2019.
//
import Fluent
import FluentSQLite
import Foundation
import Vapor
enum TenureType: String, Codable {
case freehold
case cross_lease
case unit_title
// etc...
}
enum PropertyType: String, Codable {
case commercial
case office
case industrial
case retail
// etc...
}
struct Property: Content, SQLiteUUIDModel, Migration {
var id: UUID?
var tenureType: TenureType? // var tenureType: String?
var propertyType: PropertyType? // var propertyType: String?
// etc...
}
When tenureType and propertyType are defined as String? everything works fine, but I think there is a strong case for using enums to specify the possible values for tenureType and propertyType.
Terminal output:
pm#PHQ:~/PropertyHQ$ vapor run
Running PropertyHQ ...
[ INFO ] Migrating 'sqlite' database (/home/pm/PropertyHQ/.build/checkouts/fluent/Sources/Fluent/Migration/MigrationConfig.swift:69)
[ INFO ] Migrations complete (/home/pm/PropertyHQ/.build/checkouts/fluent/Sources/Fluent/Migration/MigrationConfig.swift:73)
Running default command: .build/debug/Run serve
The problem is that when I try to use enums for tenureType and propertyType, the app builds just fine but when I run the vapor server I get this error.
Terminal output:
pm#PHQ:~/PropertyHQ$ vapor run
Running PropertyHQ ...
[ INFO ] Migrating 'sqlite' database (/home/pm/PropertyHQ/.build/checkouts/fluent/Sources/Fluent/Migration/MigrationConfig.swift:69)
[ INFO ] Preparing migration 'Property' (/home/pm/PropertyHQ/.build/checkouts/fluent/Sources/Fluent/Migration/Migrations.swift:111)
⚠️ [DecodingError.dataCorrupted: Cannot initialize TenureType from invalid String value 1]
Error: Run failed.
pm#PHQ:~/PropertyHQ$
When I specify tenureType and propertyType as optional (which I think they should be) the error value is 1 - when they are not optional the error value is 0.
I should add that this error only occurs when I run the server for the first time ie: when it creates the (empty) database. If I run it for the first time with tenureType and propertyType defined as String? it runs OK and then I can change them to enums and it continues to work OK.

You need to conform your enum to SQLiteEnumType and ReflectionDecodable. You also need to supply string values for each case. Using your TenureType as an example, try:
enum TenureType: String, Codable, SQLiteEnumType, ReflectionDecodable {
static func reflectDecoded() throws -> (TenureType, TenureType) {
return ( .freehold, .cross_lease )
}
case freehold = "freehold"
case cross_lease = "cross_lease"
case unit_title = "unit_title"
// etc...
}

Related

Can I convert JSON file to kotlin multiplatform object in Swift?

For context: I'm using kotlin multiplatform to communicate with my backend for my both apps, I'm also trying to follow Clean Architecture + MVVM.
My multiplatform project holds my data layer (repositories and dtos) and my app project holds the presentation and domain layer (VC, View Model, Use Case).
I'm trying to do is mock my repository in the multiplatform project.
I have a class (DTO) on my Kotlin Multiplatform project that looks like this
#Serializable
data class LoginResponse(val data: LoginResponseBody){
#Serializable
data class LoginResponseBody(
var accessToken: String,
var lastChannel: String? = null,
var lastDateTime: String? = null,
var userId: String? = null,
var transactionalKeyStatus: String? = null,
var accessStatus: String? = null
)
}
When I add that multiplatform project into my xcode project, the object translates to:
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("LoginResponse")))
#interface SharedEmaLoginResponse : SharedEmaBase
- (instancetype)initWithData:(SharedEmaLoginResponseLoginResponseBody *)data __attribute__((swift_name("init(data:)"))) __attribute__((objc_designated_initializer));
- (SharedEmaLoginResponseLoginResponseBody *)component1 __attribute__((swift_name("component1()")));
- (SharedEmaLoginResponse *)doCopyData:(SharedEmaLoginResponseLoginResponseBody *)data __attribute__((swift_name("doCopy(data:)")));
- (BOOL)isEqual:(id _Nullable)other __attribute__((swift_name("isEqual(_:)")));
- (NSUInteger)hash __attribute__((swift_name("hash()")));
- (NSString *)description __attribute__((swift_name("description()")));
#property (readonly) SharedEmaLoginResponseLoginResponseBody *data __attribute__((swift_name("data")));
#end;
I thought that class LoginResponse would conform to 'Decodable' but it doesn't, this is what my fake repository looks like in my xcode project
final class LoginRepositoryTest : XCTestCase, IAuthRepository {
var hasError = false
func login(loginRequest: LoginRequest, success: #escaping (LoginResponse) -> Void, failure: #escaping (ErrorResponse) -> Void, completionHandler: #escaping (KotlinUnit?, Error?) -> Void) {
if hasError {
failure(ErrorResponse(messages: []))
} else {
let data = loadStub(name: "loginResponse", extension: "json")
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let loginData = try! decoder.decode(LoginResponse.self, from: data)
success(loginData)
}
}
func getKoin() -> Koin_coreKoin {}
}
Error
Instance method 'decode(_:from:)' requires that 'LoginResponse' conform to 'Decodable'
I think I have these two options:
I make that kotlin class conform to 'Decodable'
I find a way to convert that JSON to that object without using Decodable
Any help is appreciated
Kotlin serialization isn't meant to confirm Decodable protocol to the class. It's meant to be used from kotlin with kotlin serialization, like this:
val response: LoginResponse = Json.decodeFromString(
LoginResponse.serializer(),
string
)
p.s. originally you can use Json.decodeFromString<LoginResponse>(string), but this's not yet supported on multiplatform as far as I remember
Network and JSON parsing are things which are most easily done on KMM, and you can write tests there too, but in case you still need to use it directly from iOS, you can try the following wrapper:
object JsonHelper {
#Throws(Throwable::class)
fun decodeRtcMsg(string: String) =
Json.decodeFromString(
RtcMsg.serializer(),
string
)
}
Then you can use it from swift:
do {
try JsonHelper().decodeLoginResponse(string: "")
} catch {
print(error)
}
If you really need it to Decodable I'd create an expect/actual of Decodable for ex:
// in common
expect interface Decodable
// on Android
actual interface Decodable
// on iOS
actual typealias Decodable = Decodable
Then just implement Decodable for your DTOs. Perhaps the best example for this is how you could use the Parcelize in your shared code: https://github.com/icerockdev/moko-parcelize/blob/master/parcelize/src/androidMain/kotlin/dev/icerock/moko/parcelize/Parcelize.kt

Unable to infer closure type in the current context (SwiftSignalRClient)

I have created a new project and am trying to implement the SwiftSignalR framework. In my last project, there were no problems with the following code. However, in the new project I am getting a closure type error and could really do with some help!
The actual error reads: Unable to infer closure type in the current context
I really can't understand why this error is appearing now and not in my previous project beyond the fact I switched from Swift 4.2 to Swift 5.0 and iOS deployment target from 12.0 to 13.0
My code is as follows:
private var connection: HubConnection
connection.on(method: "LocationBroadcast") { locationUpdate, _ in
self.locationUpdateReceived(locationUpdate as! [Dictionary<String, Any>])
}
private func locationUpdateReceived(_ locationUpdate: [Dictionary<String, Any>]) {
print("Location update recieved")
}
The issue seem to be caused by changes in the SignalR-Client-Swift API, the earlier one expected two parameters of types [Any?] and TypeConverter, now it has ArgumentExtractor and a bunch of generic overloads. In general your new code will look something like this
private var connection: HubConnection
connection.on(method: "LocationBroadcast") { extractor in
let locationUpdate = try extractor.getArgument(type: [Dictionary<String, Any>].self)
self.locationUpdateReceived(locationUpdate)
}
private func locationUpdateReceived(_ locationUpdate: [Dictionary<String, Any>]) {
print("Location update recieved")
}
However, this wouldn't compile yet, because new api requires Decodable type so you can't use Any here and to change it some other type, so the code should look like this:
struct LocationInfo: Codable {
var value: String
}
private var connection: HubConnection
connection.on(method: "LocationBroadcast") { extractor in
let locationUpdate = try extractor.getArgument(type: [Dictionary<String, LocationInfo>].self)
self.locationUpdateReceived(locationUpdate)
}
private func locationUpdateReceived(_ locationUpdate: [Dictionary<String, LocationInfo>]) {
print("Location update recieved")
}
For Swift Codable I am using this.
private var connection: HubConnection
connection.on(method: "LocationBroadcast", callback: { (jsonResponse: JSON) in
print(jsonResponse)
do {
let locationBroadcast:LocationBroadcast = try JSONDecoder().decode(LocationBroadcast.self, from: jsonResponse.rawData())
}catch(let error) {
print(error.localizedDescription)
}
})
struct LocationBroadcast: Codable {
var value: String
}
Make pod version to
pod 'SwiftSignalRClient', '~> 0.5.0'
This has resolved the issue without changing the code for me.

Understanding Migrations in Vapor-Fluent (Server side Swift)

I'm writing a web service in Swift using Vapor framework.
I have model named Item. Intially it has only name and id properties.
typealias VaporModel = Content & PostgreSQLModel & Parameter
final class Item: VaporModel {
var id: Int?
var name: String
}
After I configure a controller for the model and add the routes, when I hit the post Item request, I get the error as Model.defaultDatabase is required to use as DatabaseConnectable. I think the error is because I have not added Item to Migrations in configure.swift and I do the same after conforming Item to PostgreSQLMigration.
var migrations = MigrationConfig()
migrations.add(model: Item.self, database: .psql)
services.register(migrations)
Now, I am able to hit the post request and create items in the database.
So I understand that Migration protocol creates the default schema for a model and adds a new table to the database with the model's properties as columns.
Now I want to add a property such as price to my Item class. Now when I hit the post request, I get the error as column "price" of relation "Item" does not exist.
I assume the Migration protocol will be able to identify the schema changes and the column to my table (that's what I was used to in while using Realm for my iOS apps). But I am wrong and I read through the Migration docs and implement the prepare and revert methods in migration like below.
extension Item: PostgreSQLMigration {
static func prepare(on conn: PostgreSQLConnection) -> Future<Void> {
return Database.create(self, on: conn) { creator in
creator.field(for: \.price)
}
}
static func revert(on connection: PostgreSQLConnection) -> EventLoopFuture<Void> {
return Future.map(on: connection) { }
}
}
I'm still struck with the same error column "price" of relation "Item" does not exist. What am I missing here? Is my migration code correct?
Also, I understand that if am not making any changes to the Model, I can comment out the migration config, because they need not run every time I run the service. Is that correct?
With your code you haven't added a new migration. You have implemented a manual initial migration, but the initial migration has run already as requested (migrations.add(model: Item.self, database: .psql). To create a new migration you would need sth like:
struct ItemAddPriceMigration: Migration {
typealias Database = PostgreSQLDatabase
static func prepare(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
return Database.update(Item.self, on: conn) { builder in
builder.field(for: \.price)
}
}
static func revert(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
return conn.future()
}
}
And then you need to add it in configure:
migrations.add(migration: ItemAddPriceMigration.self, database: .psql)

What is context in makeNode(in: Context) vapor 3?

Using Vapor for return model to node :
func indexView(request: Request) throws -> ResponseRepresentable
{
let acro = try Acronym.makeQuery().sort(Acronym.idKey, .ascending)
return try acro.all().makeNode(in: <#T##Context?#>)
}
It always return error and don't know how to fixed it.
Contexts are used generally to pass current-use information around inside Vapor. The Vapor 3 documentation does not appear to contain detailed information (yet), but see https://docs.vapor.codes/2.0/node/getting-started/ for Vapor 2. I have never found a need for them in makeNode, so putting:
return try acro.all().makeNode(in:nil)
should make it work.
Node has been phased out in Vapor3. There's no need for it since it relies in Swift's native data types+protocols. In this case, Vapor 3 uses Content to work with JSON. (Content conforms to Codable by default)
e.g.
final class Person: Content, MySQLModel, Migration {
var id: Int?
var name: String
init(id: Int? = nil, name: String) {
self.id = id
self.name = name
}
}
func person(_ req: Request) throws -> Future<Person> {
return Person(name: "Mark")
}
router.get("test") { req -> Future<[Person]> in
return try Person.query(on: req).sort(\.id, .ascending).all()
}
Excerpt from the docs:
In Vapor 3, all content types (JSON, protobuf, URLEncodedForm, Multipart, etc) are treated the same. All you need to parse and serialize content is a Codable class or struct.

Setting variables with custom class using Websockets and vapor

I am trying to setup a simple vapor web socket. My issue is that I need to maintain the web sockets that are open. In order to do that they need to be stored in a variable. Here is my current code:
import Vapor
var test = [Instance]()
final class Instance {
var socket:WebSocket
var message:String = ""
var id:String = ""
}
extension Droplet {
func setupRoutes() throws {
get("hello") { req in
var json = JSON()
try json.set("hello", "world")
return json
}
}
//Socket code goes here as well
}
I keep getting an error while trying to run my project saying, Use of unresolved identifier 'Instance', Why am I getting this error and how do I fix it? What is the best practicing while using web sockets with vapor?

Categories