Some checks failed
rules-freshness / rules (push) Failing after 12m24s
The JSON writer escapes the characters a YAML reader mangles. PureYAML.JSON.parse and parseLocated read JSON with those characters held back and given back, so no position moves, and Convert.convert takes the format of its source.
125 lines
7.3 KiB
Swift
125 lines
7.3 KiB
Swift
import PureYAML
|
|
@testable import PureYAMLJSON
|
|
import Testing
|
|
|
|
/// A string that JSON holds is the same string after JSON is read, written, or converted to YAML and back. The characters RFC 8259 lets a string hold as they are,
|
|
/// and the YAML reader folds, refuses or drops, are the ones that are checked: U+0085, U+2028 and U+2029, U+007F and the C1 controls, the controls below U+0020 written
|
|
/// as escapes, and a byte order mark.
|
|
@Suite("JSON string characters")
|
|
struct JSONStringCharactersTests {
|
|
static let characters: [String] = [
|
|
"\u{0}", "\u{1}", "\u{1F}", "\u{7F}", "\u{80}", "\u{85}", "\u{9F}", "\u{A0}", "\u{2028}", "\u{2029}", "a\u{2028}b", "a\u{85}b", "\u{FEFF}", "\u{FEFF}x", "x\u{FEFF}",
|
|
"\u{FFFE}", "é", "日本語", "😀", " ", "", "a b", "true", "1.5", "- x", "tab\there", "line\nbreak", "cr\rlf",
|
|
]
|
|
|
|
/// The JSON text of a string, written from RFC 8259 and not with the code under test: every character that has to be escaped is, and nothing else is, so a raw
|
|
/// U+2028 or U+0085 stays raw, as JSON lets it.
|
|
static func json(_ string: String) -> String {
|
|
var out = "\""
|
|
for scalar in string.unicodeScalars {
|
|
switch scalar {
|
|
case "\"": out += "\\\""
|
|
case "\\": out += "\\\\"
|
|
case "\n": out += "\\n"
|
|
case "\r": out += "\\r"
|
|
case "\t": out += "\\t"
|
|
case let control where control.value < 0x20: out += "\\u00" + String(control.value >> 4, radix: 16) + String(control.value & 0xF, radix: 16)
|
|
default: out.unicodeScalars.append(scalar)
|
|
}
|
|
}
|
|
return out + "\""
|
|
}
|
|
|
|
@Test("JSON that holds a character as it is, is read as the string it means", arguments: characters)
|
|
func test_parseKeepsEveryCharacter(_ string: String) throws {
|
|
#expect(try PureYAML.JSON.parse(Self.json(string)) == .string(string))
|
|
#expect(try PureYAML.JSON.parse("[\(Self.json(string))]") == .sequence([.string(string)]))
|
|
#expect(try PureYAML.JSON.parse("{\(Self.json(string)): \(Self.json(string))}") == .mapping(.init([.init(key: string, value: .string(string))])))
|
|
}
|
|
|
|
@Test("A string is written as JSON that a JSON reader and the YAML reader give back as the same string", arguments: characters)
|
|
func test_writerIsReadBack(_ string: String) throws {
|
|
let value = PureYAML.Model.Value.string(string)
|
|
let written = try PureYAML.JSON.Writer.string(from: value, options: .init(pretty: false, indent: 2))
|
|
#expect(try PureYAML.JSON.parse(written) == value, "\(written.debugDescription)")
|
|
#expect(try PureYAML.parse(written) == value, "\(written.debugDescription)")
|
|
}
|
|
|
|
@Test("Characters JSON may hold as they are are written escaped, so that no YAML reader can fold or drop them")
|
|
func test_writerEscapes() throws {
|
|
let value = PureYAML.Model.Value.string("\u{7F}\u{80}\u{85}\u{9F}\u{2028}\u{2029}\u{FEFF}")
|
|
let written = try PureYAML.JSON.Writer.string(from: value, options: .init(pretty: false, indent: 2))
|
|
#expect(written == "\"\\u007f\\u0080\\u0085\\u009f\\u2028\\u2029\\ufeff\"")
|
|
}
|
|
|
|
@Test("A string converts from JSON to YAML to JSON and back to YAML as the same string", arguments: characters)
|
|
func test_convertKeepsTheString(_ string: String) throws {
|
|
let source = Self.json(string)
|
|
let yaml = try PureYAML.JSON.Convert.convert(source, from: .json, to: .yaml)
|
|
#expect(try PureYAML.parse(yaml) == .string(string), "\(yaml.debugDescription)")
|
|
let json = try PureYAML.JSON.Convert.convert(yaml, from: .yaml, to: .json)
|
|
#expect(try PureYAML.JSON.parse(json) == .string(string), "\(json.debugDescription)")
|
|
#expect(try PureYAML.JSON.Convert.convert(json, from: .json, to: .yaml) == yaml)
|
|
}
|
|
|
|
@Test("A key converts as the same key", arguments: characters)
|
|
func test_convertKeepsTheKey(_ key: String) throws {
|
|
let source = "{\(Self.json(key)): 1}"
|
|
let yaml = try PureYAML.JSON.Convert.convert(source, from: .json, to: .yaml)
|
|
#expect(try PureYAML.parse(yaml) == .mapping(.init([.init(key: key, value: .int(1))])), "\(yaml.debugDescription)")
|
|
}
|
|
|
|
@Test("Only characters inside a string are held back: an escaped quote does not end the string, and text outside strings is left as it is")
|
|
func test_onlyInsideStrings() throws {
|
|
let inside = try PureYAML.JSON.Source("\"a\\\"\u{2028}\"")
|
|
#expect(inside.masked.unicodeScalars.count == 6 && !inside.masked.unicodeScalars.contains("\u{2028}"))
|
|
let afterEscapedBackslash = try PureYAML.JSON.Source("\"a\\\\\"\u{2028}")
|
|
#expect(afterEscapedBackslash.masked == "\"a\\\\\"\u{2028}", "the string ended at the second quote, so the character after it is outside")
|
|
let two = try PureYAML.JSON.Source("[\"\u{85}\", \"\u{85}\"]")
|
|
let scalars = Array(two.masked.unicodeScalars)
|
|
#expect(scalars[2] == scalars[7] && scalars[2] != "\u{85}", "one replacement for one character")
|
|
#expect(try PureYAML.JSON.Source("{\"k\": 1}").masked == "{\"k\": 1}")
|
|
}
|
|
|
|
@Test("A private-use character that the text holds is never used as a replacement for another character")
|
|
func test_replacementIsNotInTheText() throws {
|
|
let text = "[\"\u{E000}\", \"\u{85}\"]"
|
|
#expect(try PureYAML.JSON.parse(text) == .sequence([.string("\u{E000}"), .string("\u{85}")]))
|
|
}
|
|
|
|
/// The spans of a JSON text that holds characters the YAML reader cannot be given are the spans of the same text with each of them written as one ordinary character,
|
|
/// which is a statement about positions that does not use the code under test to say what they are.
|
|
@Test("A located parse of JSON that holds such characters has the spans of the text with ordinary characters in their place")
|
|
func test_locatedSpansDoNotMove() throws {
|
|
let held = "{\n \"a\": \"x\u{2028}y\u{85}z\",\n \"b\u{7F}\": [\"\u{9F}\", 1],\n \"c\": {\"d\": \"\u{FFFE}\"}\n}\n"
|
|
let plain = String(held.unicodeScalars.map { PureYAML.JSON.Source.isHeldBack($0) ? "q" : Character($0) })
|
|
let located = try PureYAML.JSON.parseLocated(held)
|
|
let reference = try PureYAML.parseLocated(plain)
|
|
#expect(Self.ranges(of: located) == Self.ranges(of: reference))
|
|
#expect(located.value == .mapping(.init([
|
|
.init(key: "a", value: .string("x\u{2028}y\u{85}z")),
|
|
.init(key: "b\u{7F}", value: .sequence([.string("\u{9F}"), .int(1)])),
|
|
.init(key: "c", value: .mapping(.init([.init(key: "d", value: .string("\u{FFFE}"))]))),
|
|
])))
|
|
#expect(try located.value == PureYAML.JSON.parse(held))
|
|
}
|
|
|
|
/// Every span of a located tree, in order.
|
|
private static func ranges(of located: PureYAML.Model.LocatedValue) -> [PureYAML.Model.SourceRange] {
|
|
switch located {
|
|
case let .scalar(_, range):
|
|
[range]
|
|
case let .sequence(items, range):
|
|
[range] + items.flatMap(ranges)
|
|
case let .mapping(pairs, range):
|
|
[range] + pairs.flatMap { [$0.keyRange] + ranges(of: $0.value) }
|
|
}
|
|
}
|
|
|
|
@Test("Without the format of the source, the source is read as YAML as it was before")
|
|
func test_unspecifiedSourceIsYAML() throws {
|
|
#expect(try PureYAML.JSON.Convert.convert("a: 1\n", to: .json, pretty: false) == "{\"a\":1}")
|
|
#expect(try PureYAML.JSON.Convert.convert("{\"a\":1}", to: .yaml) == "a: 1\n")
|
|
}
|
|
}
|