Skip to content

Instantly share code, notes, and snippets.

@vzsg
Last active November 13, 2024 17:59
Show Gist options
  • Save vzsg/05b6a70df8f1b961ecfe92c0a509db84 to your computer and use it in GitHub Desktop.
Save vzsg/05b6a70df8f1b961ecfe92c0a509db84 to your computer and use it in GitHub Desktop.
Custom Leaf tag for printing anything in the context as JSON (Vapor 4)
import LeafKit
final class JSONifyTag: UnsafeUnescapedLeafTag {
func render(_ ctx: LeafContext) throws -> LeafData {
guard let param = ctx.parameters.first else {
throw "no parameter provided to JSONify"
}
return LeafData.string(param.jsonString)
}
}
private extension LeafData {
var jsonString: String {
guard !isNil else {
return "null"
}
switch celf {
case .array:
let items = array!.map { $0.jsonString }
.joined(separator: ", ")
return "[\(items)]"
case .bool:
return bool! ? "true" : "false"
case .data:
return "\"\(data!.base64EncodedString())\""
case .dictionary:
let items = dictionary!.map { key, value in "\"\(key)\": \(value.jsonString)" }
.joined(separator: ", ")
return "{\(items)}"
case .double:
return String(double!)
case .int:
return String(int!)
case .string:
var encoded: [UInt8] = []
encodeString(string!, to: &encoded)
return String(decoding: encoded, as: UTF8.self)
case .void:
return "null"
}
}
}
// MARK: - String escaping
// The following section is copied from apple/swift-corelibs/foundation.
// https://github.com/apple/swift-corelibs-foundation/blob/cf3320cce8e19da3d2b31bb58522e8b1ef7bd5ef/Sources/Foundation/JSONEncoder.swift#L1109
// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
private func encodeString(_ string: String, to bytes: inout [UInt8]) {
bytes.append(UInt8(ascii: "\""))
let stringBytes = string.utf8
var startCopyIndex = stringBytes.startIndex
var nextIndex = startCopyIndex
while nextIndex != stringBytes.endIndex {
switch stringBytes[nextIndex] {
case 0 ..< 32, UInt8(ascii: "\""), UInt8(ascii: "\\"):
// All Unicode characters may be placed within the
// quotation marks, except for the characters that MUST be escaped:
// quotation mark, reverse solidus, and the control characters (U+0000
// through U+001F).
// https://tools.ietf.org/html/rfc8259#section-7
// copy the current range over
bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex])
switch stringBytes[nextIndex] {
case UInt8(ascii: "\""): // quotation mark
bytes.append(contentsOf: [._backslash, ._quote])
case UInt8(ascii: "\\"): // reverse solidus
bytes.append(contentsOf: [._backslash, ._backslash])
case 0x08: // backspace
bytes.append(contentsOf: [._backslash, UInt8(ascii: "b")])
case 0x0C: // form feed
bytes.append(contentsOf: [._backslash, UInt8(ascii: "f")])
case 0x0A: // line feed
bytes.append(contentsOf: [._backslash, UInt8(ascii: "n")])
case 0x0D: // carriage return
bytes.append(contentsOf: [._backslash, UInt8(ascii: "r")])
case 0x09: // tab
bytes.append(contentsOf: [._backslash, UInt8(ascii: "t")])
default:
func valueToAscii(_ value: UInt8) -> UInt8 {
switch value {
case 0 ... 9:
return value + UInt8(ascii: "0")
case 10 ... 15:
return value - 10 + UInt8(ascii: "a")
default:
preconditionFailure()
}
}
bytes.append(UInt8(ascii: "\\"))
bytes.append(UInt8(ascii: "u"))
bytes.append(UInt8(ascii: "0"))
bytes.append(UInt8(ascii: "0"))
let first = stringBytes[nextIndex] / 16
let remaining = stringBytes[nextIndex] % 16
bytes.append(valueToAscii(first))
bytes.append(valueToAscii(remaining))
}
nextIndex = stringBytes.index(after: nextIndex)
startCopyIndex = nextIndex
case UInt8(ascii: "/"):
bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex])
bytes.append(contentsOf: [._backslash, UInt8(ascii: "/")])
nextIndex = stringBytes.index(after: nextIndex)
startCopyIndex = nextIndex
default:
nextIndex = stringBytes.index(after: nextIndex)
}
}
// copy everything, that hasn't been copied yet
bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex])
bytes.append(UInt8(ascii: "\""))
}
private extension UInt8 {
static let _space = UInt8(ascii: " ")
static let _return = UInt8(ascii: "\r")
static let _newline = UInt8(ascii: "\n")
static let _tab = UInt8(ascii: "\t")
static let _colon = UInt8(ascii: ":")
static let _comma = UInt8(ascii: ",")
static let _openbrace = UInt8(ascii: "{")
static let _closebrace = UInt8(ascii: "}")
static let _openbracket = UInt8(ascii: "[")
static let _closebracket = UInt8(ascii: "]")
static let _quote = UInt8(ascii: "\"")
static let _backslash = UInt8(ascii: "\\")
}
import Leaf
// ... other imports omitted for brevity
public func configure(_ app: Application) throws {
// ... other configuration lines omitted for brevity
app.views.use(.leaf)
app.leaf.tags["jsonify"] = JSONifyTag()
// ... other configuration lines omitted for brevity
try routes(app)
}
import Vapor
// ... other imports omitted for brevity
func routes(_ app: Application) throws {
app.get("example") { req async throws -> View in
struct ChartData: Encodable {
let x: [Double]
let labels: [String]
let y: [Double]
}
struct Context: Encodable {
let chartData: ChartData
}
let context = Context(
chartData: ChartData(
x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
y: [10, 3, 2, 6, 3, 7, 4, 2, 1, 5, 1, 5]
)
)
return try await req.view.render("example", context)
}
// ... other route handlers omitted for brevity
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>JSONify Example</title>
</head>
<body>
<h2>Check the DevTools console :)</h2>
<script>
const data = #jsonify(chartData);
console.log(data);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>JSONify Example</title>
</head>
<body>
<h2>Check the DevTools console :)</h2>
<script>
const data = {"labels": [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ], "y": [ 10.0, 3.0, 2.0, 6.0, 3.0, 7.0, 4.0, 2.0, 1.0, 5.0, 1.0, 5.0 ], "x": [ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0 ]};
console.log(data);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment