Skip to content

Instantly share code, notes, and snippets.

@beccadax
Last active June 4, 2024 19:01
Show Gist options
  • Save beccadax/0b46ce25b7da1049e61b4669352094b6 to your computer and use it in GitHub Desktop.
Save beccadax/0b46ce25b7da1049e61b4669352094b6 to your computer and use it in GitHub Desktop.
Some non-`String`, non-`AttributedString` examples of types which might use `ExpressibleByStringInterpolation`. These designs assume we use an interpolation design which allows `appendInterpolatedSegment(_:)` to be overloaded, but an unconstrained generic design would work only a little bit less gracefully.
import Foundation
/// Contains an HTML-escaped string.
///
/// An `HTML` instance works like a string, except that it is marked as containing HTML code.
/// When the contents of an ordinary `String` are inserted or interpolated into an `HTML`
/// instance, they are escaped before insertion. Use of `HTML` can therefore help prevent
/// cross-site scripting attacks.
///
/// Types can customize the way they're added to `HTML` instances by adopting the `HTMLConvertible`
/// protocol.
///
/// Example:
///
/// func makeRow(for account: Account, origin: String, accountInfo: HTML) throws -> HTML {
/// return try """
/// <tr id="account_\(account.id)" class="account">
/// <td class="account_name">
/// <img src="\(account.avatarURL)" class="avatar">
/// <a href="/account/\(account.id)?origin=\(url: origin, withAllowedCharacters: .urlQueryAllowed)">
/// \(account.name)
/// </a>
/// </td>
/// <td class="account_info">
/// \(accountInfo)
/// </td>
/// <td class="account_actions">
/// <button class="flag" data-ajax-target-account="\(js: account)">
/// Flag Account
/// </button>
/// </td>
/// </tr>
/// """
/// }
struct HTML {
var raw: String
init(raw: String = "") { self.raw = raw }
mutating func append(raw: String) { self.raw += raw }
mutating func append(_ html: HTML) { append(raw: html.raw) }
mutating func append(_ text: String) {
append(raw: text.applyingTransform(.toXMLHex, reverse: false))
}
}
extension HTML: ExpressibleByStringInterpolation {
init(stringInterpolation: StringInterpolation) {
self = stringInterpolation.instance
}
struct StringInterpolation: StringInterpolationProtocol {
var instance = HTML()
init(literalCapacity: Int, interpolationCount: Int) {
let estimatedSize = literalCapacity + interpolationCount * 8
instance.raw.reserveCapacity(estimatedSize)
}
mutating func appendLiteral(_ literal: String) {
instance.append(raw: literal)
}
// Allow interpolation of other HTML instances.
mutating func appendInterpolation(_ html: HTML) {
instance.append(html)
}
// Allow interpolation of auto-escaped strings (and other types converted
// to strings).
mutating func appendInterpolation<T>(_ text: T) {
instance.append(String(describing: text))
}
// Allow interpolation of unescaped strings (and other types converted
// to strings) with the raw: label.
mutating func appendInterpolation<T>(raw: T) {
instance.append(raw: String(describing: raw))
}
// Allow interpolation of URLs.
mutating func appendInterpolation(_ url: URL) {
instance.append(url.absoluteString)
}
// Allow interpolation of URL-escaped strings with the url: label.
mutating func appendInterpolation(url string: String,
withAllowedCharacters allowedCharacters: CharacterSet = .urlPathAllowed) {
instance.append(raw: string.addingPercentEncoding(withAllowedCharacters: allowedCharacters))
}
// Allow interpolation of structured data as JSON, optionally specifying
// the encoder to use (for its strategy).
mutating func appendInterpolation<T: Encodable>
(js value: T, using encoder: JSONEncoder = JSONEncoder()) throws {
let jsonData = try encoder.encode(value)
let json = String(decoding: jsonData, as: UTF8.self)
instance.append(json)
}
}
}
/// Represents a string whose exact phrasing is locale-dependent.
///
/// `LocalizableString` contains a `key` and a series of `arguments`. When the `LocalizableString` is
/// passed to the `String(localized:)` initializer, its `key` is used to find a format string in a
/// `Bundle`'s localized string tables, and the format string is passed with the `arguments` to
/// `String(format:arguments:)` to create the final value.
///
/// `LocalizableString` can be instantiated directly to use it with any key and set of arguments, or
/// it can be instantiated with a string literal, with or without interpolations, to automatically
/// calculate a format string and use it as the key.
///
/// Example:
///
/// let alert = NSAlert()
/// alert.messageText = String(localized: "\(appName) could not add “\(name)” because a person with that name already exists.")
/// alert.addButton(withTitle: String(localized: "Cancel"))
/// alert.addButton(withTitle: String(localized: "Replace"))
public struct LocalizableString {
public var key: String
public var arguments: [CVarArg]
public init(key: String, arguments: [CVarArg]) {
self.key = key
self.arguments = arguments
}
public func format(inTable table: String? = nil, from bundle: Bundle = .main) -> String {
return bundle.localizedString(forKey: key, value: key, table: table)
}
}
extension String {
/// Converts a localizable string into a concrete, fully localized `String`.
public init(localized str: LocalizableString, inTable table: String? = nil, from bundle: Bundle = .main) -> String {
self.init(format: str.format(inTable: table, from: bundle), arguments: str.arguments)
}
}
public protocol LocalizableStringInterpolatable {
var localizableKeyFormat: String { get }
var localizableValue: CVarArg { get }
}
extension Int: LocalizableStringInterpolatable {
public var localizableKeyFormat: String { return "%lld" }
public var localizableValue: CVarArg { return Int64(self) }
}
// Other conformances omitted
extension LocalizableString: ExpressibleByStringInterpolation {
public init(stringInterpolation: StringInterpolation) {
self.init(key: stringInterpolation.key, arguments: stringInterpolation.arguments)
}
public struct StringInterpolation: StringInterpolationProtocol {
var key: String = ""
var arguments: [CVarArg] = []
init(literalCapacity: Int, interpolationCount: Int) {
let assumedSizeOfFormatSpecifier = 2
key.reserveCapacity(literalCapacity + interpolationCount * assumedSizeOfFormatSpecifier)
arguments.reserveCapacity(interpolationCount)
}
public mutating func appendLiteral(_ literal: String) {
// Escape any % characters in the literal.
key.append(contentsOf: literal.lazy.flatMap { $0 == "%" ? "%%" : String($0) })
}
public mutating func appendInterpolation(_ value: CVarArg, format: String) {
key += format
arguments.append(value)
}
public mutating func appendInterpolation<T: LocalizableStringInterpolatable>(_ value: T) {
appendInterpolation(value.localizableValue, format: value.localizableKeyFormat)
}
}
}
import sqlite3
/// Represents a statement and its parameters, ready to be executed with a database.
///
/// A `SQLStatement` can be created by either passing a SQL query string and an array of parameters,
/// or by using a string literal and interpolating any values you want to include as parameters.
/// Once you've created a statement, you can execute it with `SQLiteDatabase.execute(_:)`.
///
/// Example:
///
/// let resultSet = try db.execute("""
/// SELECT id, title, slug FROM posts WHERE user_id = \(user.id)
/// ORDER BY title \(raw: reversed ? "DESC" : "ASC")
/// """)
/// for row in resultSet {
/// print(row[0, as: Int.self], row[2, as: String.self], row[1, as: String.self])
/// }
public struct SQLiteStatement {
public var sql: String
public var parameters: [Parameter]
public init(sql: String, parameters: [Parameter]) {
self.sql = sql
self.parameters = parameters
}
public enum Parameter {
case blob(Data)
case int64(Int64)
case double(Double)
case text(String)
case null
private func bind(to resultSet: SQLiteResultSet, at i: Int) throws {
switch self {
case .blob(let data):
try data.withUnsafeBytes { buffer in
try resultSet.bindBlob(buffer, count: data.count, to: i, destructor: .transient)
}
// ...other cases omitted...
}
}
}
}
extension SQLiteDatabase {
public func execute(_ statement: SQLiteStatement) throws -> SQLiteResultSet {
var preparedStatement: UnsafeRawPointer?
try statement.sql.withCString { ptr in
try SQLiteError.check(sqlite3_prepare_v3(db, ptr, strlen(ptr), 0, &preparedStatement, nil))
}
let resultSet = SQLiteResultSet(preparedStatement: preparedStatement!)
for (i, parameter) in zip(1..., statement.parameters) {
try parameter.bind(to: resultSet, at: i)
}
return resultSet
}
}
public protocol SQLiteStatementConvertible {
var sqliteStatement: SQLiteStatement { get }
}
extension Data: SQLiteStatementConvertible {
public var sqliteStatement: SQLiteStatement {
return SQLStatement(sql: "?", parameters: [.blob(self)])
}
}
// Other conformances omitted
extension SQLiteStatement: SQLiteStatementConvertible {
public var sqliteStatement: SQLiteStatement {
return self
}
}
extension SQLiteStatement: ExpressibleByStringInterpolation {
public init(stringInterpolation: StringInterpolation) throws -> Void) rethrows {
self = stringInterpolation.statement
}
public struct StringInterpolation: StringInterpolationProtocol {
var statement: SQLiteStatement
init(literalCapacity: Int, interpolationCount: Int) {
statement.sql.reserveCapacity(literalCapacity + interpolationCount)
statement.parameters.reserveCapacity(interpolationCount)
}
public mutating func appendLiteral(_ literal: String) {
statement.sql += literal
}
public mutating func appendInterpolation<T: SQLiteStatementConvertible>(_ value: T?) {
statement.sql += value?.statement.sql ?? "?"
statement.parameters += value?.statement.parameters ?? [.null]
}
public mutating func appendInterpolation(raw sql: SQLiteStatement) {
appendInterpolation(sql)
}
}
}
@AliSoftware
Copy link

AliSoftware commented Sep 28, 2018

On lines 97 & 98 of your SQLiteStatement.swift example, value conforms to SQLiteStatementConvertible, not a SQLiteStatement.StringInterpolation; so the property you're accessing should be sqliteStatement, not statement there, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment