Skip to content

Commit

Permalink
Support timeout in Swift (#229)
Browse files Browse the repository at this point in the history
## 🧰 Changes

- support `timeout` paramater
- remove `final newline`
- remove `FoundationNetworking`
- remove extra code in `multipart/form-data`

## 🧬 QA & Testing

I fixed test code in
`/httpsnippet/src/targets/swift/urlsession/fixtures/`

`npm run test`
  • Loading branch information
zunda-pixel authored Apr 12, 2024
1 parent fd0dd80 commit e3a78da
Show file tree
Hide file tree
Showing 24 changed files with 120 additions and 202 deletions.
77 changes: 30 additions & 47 deletions src/targets/swift/urlsession/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import type { Client } from '../../index.js';

import { CodeBuilder } from '../../../helpers/code-builder.js';
import { literalDeclaration } from '../helpers.js';
import { literalRepresentation, literalDeclaration } from '../helpers.js';

export interface UrlsessionOptions {
pretty?: boolean;
Expand All @@ -29,56 +29,44 @@ export const urlsession: Client<UrlsessionOptions> = {
const opts = {
indent: ' ',
pretty: true,
timeout: '10',
timeout: 10,
...options,
};

const { push, blank, join } = new CodeBuilder({ indent: opts.indent });

// Markers for headers to be created as litteral objects and later be set on the URLRequest if exist
const req = {
hasHeaders: false,
hasBody: false,
};

// We just want to make sure people understand that is the only dependency
push('import Foundation');
push('#if canImport(FoundationNetworking)');
push(' import FoundationNetworking');
push('#endif');

if (Object.keys(allHeaders).length) {
req.hasHeaders = true;
blank();
push(literalDeclaration('headers', allHeaders, opts));
}

if (postData.text || postData.jsonObj || postData.params) {
req.hasBody = true;
blank();

const hasBody = postData.text || postData.jsonObj || postData.params;
if (hasBody) {
switch (postData.mimeType) {
case 'application/x-www-form-urlencoded':
// By appending parameters one by one in the resulting snippet,
// we make it easier for the user to edit it according to his or her needs after pasting.
// The user can just add/remove lines adding/removing body parameters.
blank();
if (postData.params?.length) {
const [head, ...tail] = postData.params;
push(`${tail.length > 0 ? 'var' : 'let'} postData = Data("${head.name}=${head.value}".utf8)`);
tail.forEach(({ name, value }) => {
push(`postData.append(Data("&${name}=${value}".utf8))`);
});
} else {
req.hasBody = false;
const parameters = postData.params.map(p => `"${p.name}": "${p.value}"`);
if (opts.pretty) {
push('let parameters = [');
parameters.forEach(param => push(`${param},`, 1));
push(']');
} else {
push(`let parameters = [${parameters.join(', ')}]`);
}

push('let joinedParameters = parameters.map { "\\($0.key)=\\($0.value)" }.joined(separator: "&")');
push('let postData = Data(joinedParameters.utf8)');
blank();
}
break;

case 'application/json':
if (postData.jsonObj) {
push(`${literalDeclaration('parameters', postData.jsonObj, opts)} as [String : Any]`);
blank();

push('let postData = try JSONSerialization.data(withJSONObject: parameters, options: [])');
blank();
}
break;

Expand All @@ -94,34 +82,31 @@ export const urlsession: Client<UrlsessionOptions> = {
push(`let boundary = "${postData.boundary}"`);
blank();
push('var body = ""');
push('var error: NSError? = nil');
push('for param in parameters {');
push('let paramName = param["name"]!', 1);
push('body += "--\\(boundary)\\r\\n"', 1);
push('body += "Content-Disposition:form-data; name=\\"\\(paramName)\\""', 1);
push('if let filename = param["fileName"] {', 1);
push('let contentType = param["content-type"]!', 2);
push('let fileContent = String(contentsOfFile: filename, encoding: String.Encoding.utf8)', 2);
push('if (error != nil) {', 2);
push('print(error as Any)', 3);
push('}', 2);
push('let fileContent = try String(contentsOfFile: filename, encoding: .utf8)', 2);
push('body += "; filename=\\"\\(filename)\\"\\r\\n"', 2);
push('body += "Content-Type: \\(contentType)\\r\\n\\r\\n"', 2);
push('body += fileContent', 2);
push('} else if let paramValue = param["value"] {', 1);
push('body += "\\r\\n\\r\\n\\(paramValue)"', 2);
push('}', 1);
push('}');
blank();
push('let postData = Data(body.utf8)');
blank();
break;

default:
blank();
push(`let postData = Data("${postData.text}".utf8)`);
blank();
}
}

blank();

push(`let url = URL(string: "${uriObj.href}")!`);

const queries = queryObj ? Object.entries(queryObj) : [];
Expand All @@ -136,11 +121,11 @@ export const urlsession: Client<UrlsessionOptions> = {
const value = query[1];
switch (Object.prototype.toString.call(value)) {
case '[object String]':
push(`${opts.indent}URLQueryItem(name: "${key}", value: "${value}"),`);
push(`URLQueryItem(name: "${key}", value: "${value}"),`, 1);
break;
case '[object Array]':
value.forEach(val => {
push(`${opts.indent}URLQueryItem(name: "${key}", value: "${val}"),`);
(value as string[]).forEach((val: string) => {
push(`URLQueryItem(name: "${key}", value: "${val}"),`, 1);
});
break;
}
Expand All @@ -153,23 +138,21 @@ export const urlsession: Client<UrlsessionOptions> = {
}

push(`request.httpMethod = "${method}"`);
push(`request.timeoutInterval = ${opts.timeout}`);

if (req.hasHeaders) {
push('request.allHTTPHeaderFields = headers');
if (Object.keys(allHeaders).length) {
push(`request.allHTTPHeaderFields = ${literalRepresentation(allHeaders, opts)}`);
}

if (req.hasBody) {
if (hasBody) {
push('request.httpBody = postData');
}

blank();
// Retrieving the shared session will be less verbose than creating a new one.

push('let (data, response) = try await URLSession.shared.data(for: request)');
push('print(String(decoding: data, as: UTF8.self))');

blank();

return join();
},
};
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let headers = ["content-type": "application/x-www-form-urlencoded"]

var postData = Data("foo=bar".utf8)
postData.append(Data("&hello=world".utf8))
let parameters = [
"foo": "bar",
"hello": "world",
]
let joinedParameters = parameters.map { "\($0.key)=\($0.value)" }.joined(separator: "&")
let postData = Data(joinedParameters.utf8)

let url = URL(string: "https://httpbin.org/anything")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.timeoutInterval = 10
request.allHTTPHeaderFields = ["content-type": "application/x-www-form-urlencoded"]
request.httpBody = postData

let (data, response) = try await URLSession.shared.data(for: request)
print(String(decoding: data, as: UTF8.self))
print(String(decoding: data, as: UTF8.self))
9 changes: 3 additions & 6 deletions src/targets/swift/urlsession/fixtures/application-json.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let headers = ["content-type": "application/json"]
let parameters = [
"number": 1,
"string": "f\"oo",
Expand All @@ -18,8 +14,9 @@ let postData = try JSONSerialization.data(withJSONObject: parameters, options: [
let url = URL(string: "https://httpbin.org/anything")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.timeoutInterval = 10
request.allHTTPHeaderFields = ["content-type": "application/json"]
request.httpBody = postData

let (data, response) = try await URLSession.shared.data(for: request)
print(String(decoding: data, as: UTF8.self))
print(String(decoding: data, as: UTF8.self))
10 changes: 3 additions & 7 deletions src/targets/swift/urlsession/fixtures/cookies.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let headers = ["cookie": "foo=bar; bar=baz"]

let url = URL(string: "https://httpbin.org/cookies")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
request.timeoutInterval = 10
request.allHTTPHeaderFields = ["cookie": "foo=bar; bar=baz"]

let (data, response) = try await URLSession.shared.data(for: request)
print(String(decoding: data, as: UTF8.self))
print(String(decoding: data, as: UTF8.self))
6 changes: 2 additions & 4 deletions src/targets/swift/urlsession/fixtures/custom-method.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let url = URL(string: "https://httpbin.org/anything")!
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
request.timeoutInterval = 10

let (data, response) = try await URLSession.shared.data(for: request)
print(String(decoding: data, as: UTF8.self))
print(String(decoding: data, as: UTF8.self))
22 changes: 11 additions & 11 deletions src/targets/swift/urlsession/fixtures/full.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let headers = [
"cookie": "foo=bar; bar=baz",
"accept": "application/json",
"content-type": "application/x-www-form-urlencoded"
let parameters = [
"foo": "bar",
]

let postData = Data("foo=bar".utf8)
let joinedParameters = parameters.map { "\($0.key)=\($0.value)" }.joined(separator: "&")
let postData = Data(joinedParameters.utf8)

let url = URL(string: "https://httpbin.org/anything?key=value")!
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
Expand All @@ -23,8 +18,13 @@ components.queryItems = components.queryItems.map { $0 + queryItems } ?? queryIt

var request = URLRequest(url: components.url!)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.timeoutInterval = 10
request.allHTTPHeaderFields = [
"cookie": "foo=bar; bar=baz",
"accept": "application/json",
"content-type": "application/x-www-form-urlencoded"
]
request.httpBody = postData

let (data, response) = try await URLSession.shared.data(for: request)
print(String(decoding: data, as: UTF8.self))
print(String(decoding: data, as: UTF8.self))
16 changes: 6 additions & 10 deletions src/targets/swift/urlsession/fixtures/headers.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let headers = [
let url = URL(string: "https://httpbin.org/headers")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.timeoutInterval = 10
request.allHTTPHeaderFields = [
"accept": "application/json",
"x-foo": "Bar",
"x-bar": "Foo",
"quoted-value": "\"quoted\" 'string'"
]

let url = URL(string: "https://httpbin.org/headers")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers

let (data, response) = try await URLSession.shared.data(for: request)
print(String(decoding: data, as: UTF8.self))
print(String(decoding: data, as: UTF8.self))
6 changes: 2 additions & 4 deletions src/targets/swift/urlsession/fixtures/http-insecure.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let url = URL(string: "http://httpbin.org/anything")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.timeoutInterval = 10

let (data, response) = try await URLSession.shared.data(for: request)
print(String(decoding: data, as: UTF8.self))
print(String(decoding: data, as: UTF8.self))
6 changes: 2 additions & 4 deletions src/targets/swift/urlsession/fixtures/indent-option.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let url = URL(string: "https://httpbin.org/anything")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.timeoutInterval = 10

let (data, response) = try await URLSession.shared.data(for: request)
print(String(decoding: data, as: UTF8.self))
print(String(decoding: data, as: UTF8.self))
9 changes: 3 additions & 6 deletions src/targets/swift/urlsession/fixtures/json-null-value.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let headers = ["content-type": "application/json"]
let parameters = ["foo": nil] as [String : Any]

let postData = try JSONSerialization.data(withJSONObject: parameters, options: [])

let url = URL(string: "https://httpbin.org/anything")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.timeoutInterval = 10
request.allHTTPHeaderFields = ["content-type": "application/json"]
request.httpBody = postData

let (data, response) = try await URLSession.shared.data(for: request)
print(String(decoding: data, as: UTF8.self))
print(String(decoding: data, as: UTF8.self))
9 changes: 3 additions & 6 deletions src/targets/swift/urlsession/fixtures/jsonObj-multiline.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let headers = ["content-type": "application/json"]
let parameters = ["foo": "bar"] as [String : Any]

let postData = try JSONSerialization.data(withJSONObject: parameters, options: [])

let url = URL(string: "https://httpbin.org/anything")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.timeoutInterval = 10
request.allHTTPHeaderFields = ["content-type": "application/json"]
request.httpBody = postData

let (data, response) = try await URLSession.shared.data(for: request)
print(String(decoding: data, as: UTF8.self))
print(String(decoding: data, as: UTF8.self))
Loading

0 comments on commit e3a78da

Please sign in to comment.