How do I fetch HTML of URL using Vapor Swift? - swift

In one of my routes, I want to fetch HTML from a different site. https://docs.vapor.codes/4.0/content/ documents support for JSON and such but I couldn't find anything on raw HTML.
request.client.get(URI(string: "https://example.com/")).map { (response: ClientResponse) -> String? in
if response.status == .ok && response.content.contentType == .html {
return response.content... // How do I get raw html?
}
return nil
}
How do I get the raw HTML from the client response?

There are a couple of ways you can do this, depending on which String initializer you happen to fancy. Note that to use any of the following methods, you will need to unwrap the response body first:
guard let body = response.body else {
throw Abort(.internalServerError)
}
The first way is using the ByteBuffer.readString method.
guard let html = body.readString(length: body.readableBytes) else {
throw Abort(.internalServerError)
}
Another way is to use the String(decoding:as:) initializer, which can be used to convert any collection of UInt8 integers to a String:
let html = String(decoding: body.readableBytesView, as: UTF8.self)
Anf finally, you can use the String(buffer:) initializer that #iMike suggested.
let html = String(buffer: body)
Keep in mind that the .readBytes method will increment the .readIndex of the ByteBuffer, while the String initializers won't. Though I imagine that this doesn't really matter in your case.

Related

Code improvement - how to serve use pictures in Vapor?

I have a working directory that contains every user's picture and I am trying to implement a call that returns data containing the user's picture, defined in this structure:
struct ImageData: Content {
var picture: Data // UIImage data
}
I tried to implement a solution also partially using what I found in the book 'Server Side Swift with Vapor' (version 3) in chapter 26 but that's different for me because I am not using Leaf and I need to return the data directly.
I came up with this function to return the user picture, which does its job but I am trying to improve it.
func getProfilePictureHandler(_ req: Request) throws -> EventLoopFuture<ImageData> {
return User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { user in
// To do: throw error (flatMapThrowing?)
let filename = user.profilePicture!
let path = req.application.directory.workingDirectory
+ imageFolder
+ filename
// Improvement: Do I need this?
var data = Data()
return req.fileio.readFile(at: path) { buffer -> EventLoopFuture<Void> in
let additionalData = Data(buffer: buffer)
data.append(contentsOf: additionalData)
return req.eventLoop.makeSucceededVoidFuture()
}.map {
return ImageData(picture: data)
}
}
}
First:
How to implement this using flatMapThrowing? If I replace flatMap with flatMapThrowing I get this error: "Cannot convert return expression of type 'EventLoopFuture' to return type 'ImageData'". Which doesn't make sense to me considering that flatMap allows returning a future and not a value.
I didn't find any solution other than using a Data variable and appending chunks of data as more data is read. I am not sure that this is thread-safe, FIFO and I don't consider it an elegant solution. Does anybody know any better way of doing it?
The short answer is that as soon as you have the file path, Vapor can handle it all for you:
func getProfilePictureHandler(_ req: Request) throws -> EventLoopFuture<Response> {
return User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound))
.tryflatMap { user in
// To do: throw error (flatMapThrowing?)
guard let filename = user.profilePicture else {
throw Abort(.notFound)
}
let path = req.application.directory.workingDirectory
+ imageFolder
+ filename
return req.fileio.streamFile(at: path)
}
}
You can use tryFlatMap to have a flatMap that can throw and you want to return a Response. Manually messing around with Data is not usually a good idea.
However, the better answers are use async/await and the FileMiddleware as two tools to clean up your code and remove the handler altogether

Remove string literals from elements inside an array

Please guys i need to parse a string to look like these in swift
"[{"question":9, "answer":25}", "question\":10, "answer":27}]"
where the index and value are dynamically gotten from a loop. I was able to get to these
["{\"question\":9, \"answer\":25}", "{\"question\":10, \"answer\":27}", "{\"question\":11, \"answer\":29}", "{\"question\":12, \"answer\":33}", "{\"question\":13, \"answer\":37}"]
so i have tried this
for i in 0..<answersForQuestionInPage.count{
let questions = answersForQuestionInPage[i] as Answer
do {
let data = try JSONEncoder().encode(questions)
// 2
let string = String(data: data, encoding: .utf8)!
answers.append(string)
print("This is the main value \(string)")
} catch{
}
}
this still gives me an array with this format
["{\"question\":9, \"answer\":25}", "{\"question\":10,
\"answer\":27}", "{\"question\":11, \"answer\":29}",
"{\"question\":12, \"answer\":33}", "{\"question\":13,
\"answer\":37}"]
with the object
"{\"question\":9, \"answer\":25}"
still wrapped in a string liteal " " what i want is for this return array to be in this format
[{"question":9, "answer":25}, {"question":10,
"answer":27}, {"question":11, "answer":29},
"{"question":12, "answer":33}, {"question":13,
"answer":37}]
I didn't understand the whole thing, but you said you need to parse the String, but I think you meant JSON. So, you can do it like this and get the values. Do let me know if it is what you needed, otherwise please add clarity in your question and I will edit and update my answer accordingly.
struct Quiz: Decodable {
let question, answer: Int
}
private func fetchQuizzes() {
//After getting the data from API, you can do this
guard let quiz = try? JSONDecoder().decode([Quiz].self,from: data) else { print("Unable to parse"); return }
print(quiz)
print(quiz.first?.answer) //First Answer
}
Just like Rob said before, you have a JSON here.
Using Robs code you decode the given JSON and create an array of Quiz objects (Robs struct).
You can now work with that array and transform it to your needs.

How to access the raw content from a response in Vapor 3 unit test?

I'm coming from using tooling such as SuperTest with NodeJS and looking for relevant equivalents to support testing with Vapor 3 and server side swift.
I see a pattern of using making a testable application with Vapor 3 to do testing of endpoints, examples being https://github.com/raywenderlich/vapor-til/blob/master/Tests/AppTests/Application%2BTestable.swift and the write-up at https://medium.com/swift2go/vapor-3-series-iii-testing-b192be079c9e.
When using these in tests, the format generally looks something like:
func testGettingASingleUserFromTheAPI() throws {
let user = try User.create(name: usersName, username: usersUsername, on: conn)
let receivedUser = try app.getResponse(to: "\(usersURI)\(user.id!)", decodeTo: User.Public.self)
XCTAssertEqual(receivedUser.name, usersName)
XCTAssertEqual(receivedUser.username, usersUsername)
XCTAssertEqual(receivedUser.id, user.id)
}
(from Vapor-TIL example code)
In all of these examples, the return values are really set to be handed back to something decodable (the decodeTo: kind of setup). In some cases in my Vapor 3 code, I want to just validate some non-JSON encoded results - just simple strings, and validate the results - but I've not found the methods to get into the content or convenient ways to validate it with XCTAssert.
response.content is available, a container around the overall response (of type ContentContainer). Are there some examples or good ways at getting to the underlying content representation to validate them directly?
You could write your own additional methods in Application+Testable like
func getRawResponse(to path: String) throws -> Response {
return try self.sendRequest(to: path, method: .GET)
}
func getStringResponse(to path: String) throws -> String {
let response = try self.getRawResponse(to: path)
guard let data = response.http.body.data,
let string = String(data: data, encoding: .utf8) else {
throw SomeError("Unable to decode response data into String")
}
return string
}
and then call them to get either raw Response or decoded String like
func testGettingHelloWorldStringFromTheAPI() throws {
let string = try app. getStringResponse(to: "some/endpoint")
XCTAssertEqual(string, "Hello world")
}

Alamofire XML request to PropertyList

I am trying to parse an XML data using Codable from the sample https://www.w3schools.com/xml/note.xml.
My struct is
struct Note: Codable {
var to: String?
var from: String?
var heading: String?
var body: String?
}
However if I make the following request I get the error responseSerializationFailed : ResponseSerializationFailureReason "PropertyList could not be serialized because of error:\nThe data couldn’t be read because it isn’t in the correct format."
let url = URL(string: "https://www.w3schools.com/xml/note.xml")
Alamofire.request(url!, method: .get, encoding: PropertyListEncoding.default).responsePropertyList { (response) in
guard response.error == nil else {
print(response.error!)
exp.fulfill()
return
}
print(response)
if let data = response.data {
print(data)
let decoder = PropertyListDecoder()
let note = try! decoder.decode(Note.self, from: data)
print(note)
}
}
How do you exactly work with the responsePropertyList in Alamofire?
Currently, Apple's Codable protocol does not have a way to decode XML. While a Plist is XML, XML is not necessarily a Plist unless it follows a certain format.
While there are plenty of third party libraries, I would suggest you take a look at the XMLParsing library. This library contains a XMLDecoder and a XMLEncoder that uses Apple's own Codable protocol, and is based on Apple's JSONEncoder/JSONDecoder with changes to fit the XML standard.
Link: https://github.com/ShawnMoore/XMLParsing
W3School's XML To Parse:
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
Swift Struct conforming to Codable:
struct Note: Codable {
var to: String
var from: String
var heading: String
var body: String
}
XMLDecoder:
let data = Data(forResource: "note", withExtension: "xml") else { return nil }
let decoder = XMLDecoder()
do {
let note = try decoder.decode(Note.self, from: data)
} catch {
print(error)
}
XMLEncoder:
let encoder = XMLEncoder()
do {
let data = try encoder.encode(self, withRootKey: "note")
print(String(data: data, encoding: .utf8))
} catch {
print(error)
}
There are a number of benefits for using Apple's Codable protocol over that of a third-party's protocol. Take for example if Apple decides to begin supporting XML, you would not have to refactor.
For a full list of examples of this library, see the Sample XML folder in the repository.
There are a few differences between Apple's Decoders and Encoders to fit the XML standard. These are as follows:
Differences between XMLDecoder and JSONDecoder
XMLDecoder.DateDecodingStrategy has an extra case titled keyFormatted. This case takes a closure that gives you a CodingKey, and it is up to you to provide the correct DateFormatter for the provided key. This is simply a convenience case on the DateDecodingStrategy of JSONDecoder.
XMLDecoder.DataDecodingStrategy has an extra case titled keyFormatted. This case takes a closure that gives you a CodingKey, and it is up to you to provide the correct data or nil for the provided key. This is simply a convenience case on the DataDecodingStrategy of JSONDecoder.
If the object conforming to the Codable protocol has an array, and the XML being parsed does not contain the array element, XMLDecoder will assign an empty array to the attribute. This is because the XML standard says if the XML does not contain the attribute, that could mean that there are zero of those elements.
Differences between XMLEncoder and JSONEncoder
Contains an option called StringEncodingStrategy, this enum has two options, deferredToString and cdata. The deferredToString option is default and will encode strings as simple strings. If cdata is selected, all strings will be encoded as CData.
The encode function takes in two additional parameters than JSONEncoder does. The first additional parameter in the function is a RootKey string that will have the entire XML wrapped in an element named that key. This parameter is required. The second parameter is an XMLHeader, which is an optional parameter that can take the version, encoding strategy and standalone status, if you want to include this information in the encoded xml.
PropertyList files although are in XML format, they need to follow Apple's PropertyList DTD: http://www.apple.com/DTDs/PropertyList-1.0.dtd
If you want to map a regular XML file (that do not follow PropertyList DTD) into a model object and you don't mind using an external library you can try XMLMapper.
You model for this XML should look like this:
class Note: XMLMappable {
var nodeName: String!
var to: String?
var from: String?
var heading: String?
var body: String?
required init(map: XMLMap) { }
func mapping(map: XMLMap) {
to <- map["to"]
from <- map["from"]
heading <- map["heading"]
body <- map["body"]
}
}
And you can map it from string using XMLMapper:
let note = XMLMapper<Note>().map(XMLString: xmlString)
Or if you install Requests subspec you can use responseXMLObject(queue:keyPath:mapToObject:completionHandler:) function like:
let url = URL(string: "https://www.w3schools.com/xml/note.xml")
Alamofire.request(url!, method: .get, encoding: XMLEncoding.default).responseXMLObject { (response: DataResponse<Note>) in
let note = response.result.value
print(note?.from ?? "nil")
}
Hope this helps.

Delete parameter from URL with swift

I have an URL that looks like myapp://jhb/test/deeplink/url?id=4567 .
I want to delete every thing after the ? char. At the end the URL should look like myapp://jhb/test/deeplink/url. how. can I achieve that? convert the url to a string? Regex?
Use URLComponents to separate the different URL parts, manipulate them and then extract the new url:
var components = URLComponents(string: "myapp://jhb/test/deeplink/url?id=4567")!
components.query = nil
print(components.url!)
myapp://jhb/test/deeplink/url
A convenient extension on URL
private extension URL {
var removingQueries: URL {
if var components = URLComponents(string: absoluteString) {
components.query = nil
return components.url ?? self
} else {
return self
}
}
}
can I achieve that? convert the url to a string? Regex?
When working with URLs, it would be better to treat it as URLComponent:
A structure that parses URLs into and constructs URLs from their
constituent parts.
therefore, referring to URLComponent what are you asking is to remove the the query subcomponent from the url:
if var componenets = URLComponents(string: "myapp://jhb/test/deeplink/url?id=4567") {
componenets.query = nil
print(componenets) // myapp://jhb/test/deeplink/url
}
Note that query is an optional string, which means it could be nil (as mentioned in the code snippet, which should leads to your desired output).
You can do like this
let values = utl?.components(separatedBy: "?")[0]
It will break the string with ? and return the array.
The first object of values give you your resultant string.
You can get each URL component separated from URl using
print("\(url.host!)") //Domain name
print("\(url.path)") // Path
print("\(url.query)") // query string