Vapor 3: transform array of Future object to an array of Future other objects - vapor

I tried to make the most basic example that I could think of for my problem. I have a Course model and a many-to-many table to User that also stores some extra properties (the progress in the example below).
import FluentPostgreSQL
import Vapor
final class Course: Codable, PostgreSQLModel {
var id: Int?
var name: String
var teacherId: User.ID
var teacher: Parent<Course, User> {
return parent(\.teacherId)
}
init(name: String, teacherId: User.ID) {
self.name = name
self.teacherId = teacherId
}
}
struct CourseUser: Pivot, PostgreSQLModel {
typealias Left = Course
typealias Right = User
static var leftIDKey: LeftIDKey = \.courseID
static var rightIDKey: RightIDKey = \.userID
var id: Int?
var courseID: Int
var userID: UUID
var progress: Int
var user: Parent<CourseUser, User> {
return parent(\.userID)
}
}
Now, when I return a Course object, I want the JSON output to be something like this:
{
"id": 1,
"name": "Course 1",
"teacher": {"name": "Mr. Teacher"},
"students": [
{"user": {"name": "Student 1"}, progress: 10},
{"user": {"name": "Student 2"}, progress: 60},
]
}
Instead of what I would normally get, which is this:
{
"id": 1,
"name": "Course 1",
"teacherID": 1,
}
So I created some extra models and a function to translate between them:
struct PublicCourseData: Content {
var id: Int?
let name: String
let teacher: User
let students: [Student]?
}
struct Student: Content {
let user: User
let progress: Int
}
extension Course {
func convertToPublicCourseData(req: Request) throws -> Future<PublicCourseData> {
let teacherQuery = self.teacher.get(on: req)
let studentsQuery = try CourseUser.query(on: req).filter(\.courseID == self.requireID()).all()
return map(to: PublicCourseData.self, teacherQuery, studentsQuery) { (teacher, students) in
return try PublicCourseData(id: self.requireID(),
name: self.name,
teacher: teacher,
students: nil) // <- students is the wrong type!
}
}
}
Now, I am almost there, but I am not able to convert studentsQuery from EventLoopFuture<[CourseUser]> to EventLoopFuture<[Student]>. I tried multiple combinations of map and flatMap, but I can't figure out how to translate an array of Futures to an array of different Futures.

The logic you're looking for will look like this
extension Course {
func convertToPublicCourseData(req: Request) throws -> Future<PublicCourseData> {
return teacher.get(on: req).flatMap { teacher in
return try CourseUser.query(on: req)
.filter(\.courseID == self.requireID())
.all().flatMap { courseUsers in
// here we should query a user for each courseUser
// and only then convert all of them into PublicCourseData
// but it will execute a lot of queries and it's not a good idea
}
}
}
}
I suggest you to use the SwifQL lib instead to build a custom query to get needed data in one request 🙂
You could mix Fluent's queries with SwifQL's in case if you want to get only one course, so you'll get it in 2 requests:
struct Student: Content {
let name: String
let progress: Int
}
extension Course {
func convertToPublicCourseData(req: Request) throws -> Future<PublicCourseData> {
return teacher.get(on: req).flatMap { teacher in
// we could use SwifQL here to query students in one request
return SwifQL.select(\CourseUser.progress, \User.name)
.from(CourseUser.table)
.join(.inner, User.table, on: \CourseUser.userID == \User.id)
.execute(on: req, as: .psql)
.all(decoding: Student.self).map { students in
return try PublicCourseData(id: self.requireID(),
name: self.name,
teacher: teacher,
students: students)
}
}
}
}
If you want to get a list of courses in one request you could use pure SwifQL query.
I simplified desired JSON a little bit
{
"id": 1,
"name": "Course 1",
"teacher": {"name": "Mr. Teacher"},
"students": [
{"name": "Student 1", progress: 10},
{"name": "Student 2", progress: 60},
]
}
first of all let's create a model to be able to decode query result into it
struct CoursePublic: Content {
let id: Int
let name: String
struct Teacher:: Codable {
let name: String
}
let teacher: Teacher
struct Student:: Codable {
let name: String
let progress: Int
}
let students: [Student]
}
Ok now we are ready to build a custom query. Let's build it in some request handler function
func getCourses(_ req: Request) throws -> Future<[CoursePublic]> {
/// create an alias for student
let s = User.as("student")
/// build a PostgreSQL's json object for student
let studentObject = PgJsonObject()
.field(key: "name", value: s~\.name)
.field(key: "progress", value: \CourseUser.progress)
/// Build students subquery
let studentsSubQuery = SwifQL
.select(Fn.coalesce(Fn.jsonb_agg(studentObject),
PgArray(emptyMode: .dollar) => .jsonb))
.from(s.table)
.where(s~\.id == \CourseUser.userID)
/// Finally build the whole query
let query = SwifQLSelectBuilder()
.select(\Course.id, \Course.name)
.select(Fn.to_jsonb(User.table) => "teacher")
.select(|studentsSubQuery| => "students")
.from(User.table)
.join(.inner, User.table, on: \Course.teacherId == \User.id)
.join(.leftOuter, CourseUser.table, on: \CourseUser.teacherId == \User.id)
.build()
/// this way you could print raw query
/// to execute it in postgres manually
/// for debugging purposes (e.g. in Postico app)
print("raw query: " + query.prepare(.psql).plain)
/// executes query with postgres dialect
return query.execute(on: req, as: .psql)
/// requests an array of results (or use .first if you need only one first row)
/// You also could decode query results into the custom struct
.all(decoding: CoursePublic.self)
}
Hope it will help you. There may be some mistakes in the query cause I wrote it without checking 🙂 You can try to print a raw query to copy it and execute in e.g. Postico app in postgres directly to understand what's wrong.

Related

Is it possible to create Swift Codable for plain k-v json?

I've JSON data like:
{
"peopleA": "nnll",
"peopleB": "ihyt",
"peopleC": "udr",
"peopleD": "vhgd",
"peopleE": "llll"
}
There're thousands of data like that, basically what I wanna to do is read the JSON file, and fetch the relate info, like: input peopleC, return udr.
Trying to use some online solution, I got
struct Welcome: Codable {
let peopleA, peopleB, peopleC, peopleD: String
let peopleE: String
}
I know I can refactor the JSON file to:
{
"candidates": [
{
"name": "peopleA",
"info": "nnll"
},
{
"name": "peopleB",
"info": "ihyt"
},
{
"name": "peopleC",
"info": "udr"
}
]
}
And get the related Swift struct:
struct Welcome: Codable {
let candidates: [Candidate]
}
// MARK: - Candidate
struct Candidate: Codable {
let name, info: String
}
I'm just wondering if maybe we could make it work in Swift without postprocessing the json file?
You can simply decode it as a dictionary. Then you can map your dictionary into your array of Candidate structures if you would like to:
struct Welcome: Codable {
let candidates: [Candidate]
}
struct Candidate: Codable {
let name, info: String
}
let js = """
{
"peopleA": "nnll",
"peopleB": "ihyt",
"peopleC": "udr",
"peopleD": "vhgd",
"peopleE": "llll"
}
"""
do {
let dictionary = try JSONDecoder().decode([String: String].self, from: Data(js.utf8))
let welcome = Welcome(candidates: dictionary.map(Candidate.init))
print(welcome)
} catch {
print(error)
}
This will print:
Welcome(candidates: [Candidate(name: "peopleA", info: "nnll"), Candidate(name: "peopleC", info: "udr"), Candidate(name: "peopleB", info: "ihyt"), Candidate(name: "peopleE", info: "llll"), Candidate(name: "peopleD", info: "vhgd")])

How to retrieve values from a Nested JSON Swift

So I've been working with a nested JSON file (that I added locally to my project) in Swift. I've included a part of the JSON file I'm working on below. The data is structured as follows:
{
"categories": [
{
"categoryName": "Albatrosses",
"exercisesInCategory": [
"Wandering albatross",
"Grey-headed albatross",
"Black-browed albatross",
"Sooty albatross",
"Light-mantled albatross"
]
},
{
"categoryName": "Cormorants",
"exercisesInCategory": [
"Antarctic shag",
"Imperial shag",
"Crozet shag"
]
},
{
"categoryName": "Diving petrels",
"exercisesInCategory": [
"South Georgia diving petrel",
"Common diving petrel"
]
},
{
"categoryName": "Ducks, geese and swans",
"exercisesInCategory": [
"Yellow-billed pintail"
]
}
]
}
In order to retrieve the data I made 2 structures that represent the data in the JSON so I can then retrieve values from it. These are as follows:
struct Response:Codable{
let categories: [Categories]
}
struct Categories:Codable{
let categoryName : String?
let exercisesInCategory : [String]
}
The file name is fitnessData.json and I'm trying to retrieve the data from it by using this code:
private func parse(){
print("Retrieving JSON Data...")
if let url = Bundle.main.url(forResource: "fitnessData", withExtension: "json") {
do {
let data = try Data(contentsOf: url)
self.response = try JSONDecoder().decode(Response.self, from: data)
if let responseJSON = self.response {
print("The categories are: ", responseJSON.categories[1].categoryName!)
}
} catch {
print(error)
}
}
}
The problem is that I would like to retrieve ALL the 'categoryName' values from the JSON file, and ALL the 'exercisesInCategory' values. But so far I've only managed to navigate towards a specific item in the JSON file and retrieving that item i.e.
responseJSON.categories[1].categoryName!
I would like to iterate over the JSON file to get all of the 'categoryName' values for example. However in order to do that I'd have to write something like this:
for value in responseJSON.categories[1].categoryName! {
print(value)
}
Where '1' represents all the values for the categories struct. The code above will obviously print only the categoryName of the second index in the categories array in the JSON file. Could someone point me in the right direction?
You can do that like:
for category in responseJSON.categories {
print(category.categoryName!)
}
Or you can use map function for getting all the categoryName like:
let categoryNames = responseJSON.categories.map {$0.categoryName}
Simply like this.
response.categories.forEach { print($0.categoryName) }
If you would like to put both values in different arrays:
var categoryNameList = [String]
var excercisesInCategory = [[String]]
for category in responseJSON.categories {
categoryNameList.append(category.categoryName)
excercisesInCategory.append(category. exercisesInCategory)
}
this.
let categories = responseJSON.categories.map { $0.categoryName }
categories.forEach { print($0) }
If you iterate through the String, each item is single character of string.
You can iterate through categories array
for category in responseJSON.categories {
print(category.categoryName ?? "No name")
}
To get all names, you can use compactMap which removes nil values from an array
let names = responseJSON.categories.compactMap { $0.categoryName }
Next:
If each category has its name, make type of this property non-optional String (then you can use map instead of compactMap)
I would improve your naming. Rename categoryName key of json to name using CodingKeys enum. Then you could do
category.name
If you wanted to get all the categoryName and exercisesInCategory form JSON file, then you don't need to pass hard coded index. Use the following modified function, it will do the work..
private func parse(){
print("Retrieving JSON Data...")
if let url = Bundle.main.url(forResource: "fitnessData", withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let response = try JSONDecoder().decode(Response.self, from: data)
for category in response.categories {
print("Category Name: \(category.categoryName!)")
for exercises in category.exercisesInCategory {
print("Exercise in category: \(exercises)")
}
}
} catch {
print(error)
}
}
}

Everything is parsed correctly but one element always pulls null even though it's inside the Json? Swift 4.1

I am trying to parse the messages and it just pulls nil EVERY time. It would be fine if it was like once or twice, but it does it every time so something is. definitely going wrong here.
Here is what the console output is looking like
commitJson(sha: "3665294d1e813d35594d6bcdc0a61983caa6e0cd", message: nil, url: "https://api.github.com/repos/apple/swift/commits/3665294d1e813d35594d6bcdc0a61983caa6e0cd", commit: GitHubCommits.commit(author: GitHubCommits.author(date: Optional("2018-10-03T19:12:15Z"), name: "Karoy Lorentey")))
It is pulling everything but the message. I might be missing something, but I think it's better if I let my code talk. Sorry for the struct layout......
Here is the struct with the json
struct author : Codable{
var date: String
var name: String
}
struct commit : Codable {
var author: author
}
struct commitJson : Codable {
var sha: String
var message: String?
var url: String
var commit: commit
}
seems solid right? I need the optional or the thing will crash on me....
Here is the parsing
guard let url = URL(string: "https://api.github.com/repos/apple/swift/commits?per_page=100") else {return}
URLSession.shared.dataTask(with: url) { (data, statusCode, error) in
//print(statusCode)
if let error = error{
print("error : \(error)")
return
}
guard let data = data else {return}
do{
let decoder = JSONDecoder()
self.commitsArray = try decoder.decode([commitJson].self, from: data)
for commit in self.commitsArray{
print(commit)
}
} catch {
print("I have failed you with \(error)")
}
}.resume()
I feel like I am not doing anything wrong, but I wouldn't be here if I wasn't. I tried converting the thing into a string and switching the some stuff like the quotes then back into a data object, but I either got it wrong or it doesn't help at all.
Here is a cleaner sample to show what I want out of there.
*note this is all wrapped around an array brackets at the start and end
{
"sha": "80d765034c61d8bcad1d858cfa38ec599017a2f0",
"commit": {
"author": {
"name": "swift-ci",
"date": "2018-10-08T18:59:06Z"
}
"message": "Merge pull request #19764 from tokorom/vim-syntax-case-label-region",
}
Here is what a sample of what the GitHub full data block example looks like.
{
"sha": "80d765034c61d8bcad1d858cfa38ec599017a2f0",
"node_id": "MDY6Q29tbWl0NDQ4Mzg5NDk6ODBkNzY1MDM0YzYxZDhiY2FkMWQ4NThjZmEzOGVjNTk5MDE3YTJmMA==",
"commit": {
"author": {
"name": "swift-ci",
"email": "swift-ci#users.noreply.github.com",
"date": "2018-10-08T18:59:06Z"
},
"committer": {
"name": "GitHub",
"email": "noreply#github.com",
"date": "2018-10-08T18:59:06Z"
},
"message": "Merge pull request #19764 from tokorom/vim-syntax-case-label-region",
"tree": {
"sha": "d6bd4fe23f4efabcfee7fbfb6e91e5aac9b4bf6d",
"url": "https://api.github.com/repos/apple/swift/git/trees/d6bd4fe23f4efabcfee7fbfb6e91e5aac9b4bf6d"
},
"url": "https://api.github.com/repos/apple/swift/git/commits/80d765034c61d8bcad1d858cfa38ec599017a2f0",
"comment_count": 0,
"verification": {
"verified": true,
"reason": "valid",
"signature": "-----BEGIN PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJbu6j6CRBK7hj4Ov3rIwAAdHIIAKv4lE8AwQ/hrqfjNaOdW/EW\nsFqNisjTOhj1YiW64VSU7l2uztogJJG0Shl/+zQQQGFNVcvxlNXjq3JF9rrThrPl\nFKwvNZoSZBgNoEbTNoMPCkS+GMVDlMw96VVHrSo4Nae4yiU+Y+WSnCqf6I+TUSRp\n5JyL6oMlSqaihgq9gkIqlDnp6i0lRJWtMyGJ7xUrJ0C985RyGyb6fG20/34UJ4TT\nzT/Beb0RyYOdwnXy+mOm/NnmhcVozOrBbZlR3X2e4myQJ6Q7INOOyYPpmAZxEXps\nmajg6J73cwaH2x6PxRmMJ3+qxCau+bX3v4pEEeT5nYEIH+hDK2uC2wC/PkM7VsU=\n=2jhi\n-----END PGP SIGNATURE-----\n",
"payload": "tree d6bd4fe23f4efabcfee7fbfb6e91e5aac9b4bf6d\nparent 52deae30eb5833e53ba68ebc8a9a87614630751d\nparent ea2c860ddb4817dc83c7152035aa05569f3a2770\nauthor swift-ci <swift-ci#users.noreply.github.com> 1539025146 -0700\ncommitter GitHub <noreply#github.com> 1539025146 -0700\n\nMerge pull request #19764 from tokorom/vim-syntax-case-label-region\n\n"
}
}
Here is the link to the API. It does have like a 60 requests per hour without an API Key limit, so be wary of that.
GitHub Json Swift
message is part of the commit, not part of the outer object.
You need:
struct author : Codable{
var date: String
var name: String
}
struct commit : Codable {
var author: author
var message: String?
}
struct commitJson : Codable {
var sha: String
var url: String
var commit: commit
}

Xcode suddenly quit in runtime when parsing JSON

I'm trying to parse JSON with struct :
this is the JSON looks alike (the object value on SEAT has more than 100):
{
"Message": "Success",
"Status": 200,
"data": {
"SEATS": [
{
"SEAT_LOC_NO": "01404301",
"ROW_NM": "A",
"SEAT_NO": 1
},
{
"SEAT_LOC_NO": "01404401",
"ROW_NM": "A",
"SEAT_NO": 2
}
],
"SEATCOUNT": {
"COL_CNT": 42,
"ROW_CNT": 12,
}
}
}
and my struct be like:
struct Response : Codable {
var data : datas?
var Message : String?
var Status : Int64?
}
struct datas : Codable {
var SEATS : [SEATS]?
var SEATCOUNT : SEATINFO?
}
struct SEATS : Codable {
var SEAT_LOC_NO : String?
var ROW_NM : String?
var SEAT_NO : String?
}
struct SEATINFO : Codable {
var COL_CNT : Int64?
var ROW_CNT : Int64?
}
and this is the process of parsing:
var getSeat = Response()
getSeat = try? JSONDecoder().decode(Response.self, from: json) as Response
The xcode exit when trying to run this progress, json is a Data type which has been called in API Service.
I've been trying to delete derived data, remove some data in xcodeproject, restarting xcode and OS. However this problem still comes. And I wonder why but only on this process xcode suddenly quit while the others process is fine (different data to parse).
I'm using the latest XCODE Version.
Is it a bug, failed parsing or something else?
Your "SEAT_NO" key is integer value. You declare it as String? in struct.
Change var SEAT_NO : String? to var SEAT_NO : Int? or value in "SEAT_NO" key to String
If there is more problems try to catch errors
do {
getSeat = try JSONDecoder().decode(Response.self, from: data)
} catch {
print(error)
}
#Vanillatte please check your JSON first it's not valid please remove extra semicolon seat count dictionary. and try and to use error handling during parsing response.

Handle populating a tableview with asynchronous API calls

I have 2 objects. Note and User.
public class Note {
var userId: Int
var createdAtDate: NSDate
var content: String?
init(userId: Int, createdAtDate: NSDate) {
self.userId = userId
self.createdAtDate = createdAtDate
}
}
public class User {
var id: Int
var firstName: String
var lastName: String
var email: String
var avatar: UIImage?
init(id: Int, firstName: String, lastName: String, email: String) {
self.id = id
self.firstName = firstName
self.lastName = lastName
self.email = email
}
}
And there are two API methods. One to list out all the notes. This is the JSON response you get.
{
"status": "success",
"data": [
{
"user_id": 2,
"note": "This is a test\r\n\r\nThis is only a test.\r\n\r\nbeep boop",
"created_at": "2015-07-29 04:39:25"
}
]
}
Note that you only get the user's ID in this response. So to get the user details (first name ,last name etc), there's another API call. When you pass the user ID, it gives that user's details. This is its JSON response.
{
"status": "success",
"data": {
"id": 2,
"email": "dev#example.com",
"first_name": "John",
"last_name": "Appleseed",
"avatar": "avt.png"
}
}
Note that both these API methods return results asynchronously. Which brings me to my issue.
I need to display all these notes in a section-ed tableview with each note's user's full name in the footer.
The problem is since the user detail API call is asynchronous, so it happens in a different thread and it takes time to receive the results. But loading notes to the tableview happens separately so I can't figure out how to map each note's user when the user's detail arrives.
func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
let note = notes[section]
APIClient.getUserDetails(id: id, success: { (data) -> Void in
}) { (error) -> Void in
}
return ""
}
For example, let's take the first note in the notes array. It gets the userId of that note and pass it to the getUserDetails method to retrieve the user details. But since this is async, the tableview won't wait until it receives that user's details. By the time you get that, it might have moved on to another note and spinning off async calls all over the place!
Anyone got an idea how to properly tackle this situation?
The approach to solve this might help you.
You could fire the second request (getUserDetails) in the success block of your first API call. (Or whichever method/delegate you would be using to handle receiving data from the server)
On success of the second request, you could simply reload your table to show the appropriate data
Since it takes time to show details you could add an activity indicator and hide the table till you receive all required data from the server.
I am assuming that you are using a separate block or delegate that would do the job of handling data response from the server.
If you strategically place your server requests on the success handling methods, one after the other, even though your data is interlinked and the calls are asynchronous - you get yourself a synchronous-like flow.
Hope that helps!

Resources