-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
On lines 97 & 98 of your
SQLiteStatement.swift
example,value
conforms toSQLiteStatementConvertible
, not aSQLiteStatement.StringInterpolation
; so the property you're accessing should besqliteStatement
, notstatement
there, right?