Last active
November 13, 2024 17:59
-
-
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)
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 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: "\\") | |
} |
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 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) | |
} |
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 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 | |
} |
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
<!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> |
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
<!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