diff --git a/CHANGELOG.md b/CHANGELOG.md index 03771500..cd119337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ ##### Enhancements -* None. +* Allow specifying a `newLineScalarStyle` for encoding string scalars with newlines when using `YAMLEncoder` + [Tejas Sharma](https://github.com/tejassharma96) + [#405](https://github.com/jpsim/Yams/issues/405) ##### Bug Fixes diff --git a/Sources/Yams/Emitter.swift b/Sources/Yams/Emitter.swift index f2d0ffe6..6eeee9aa 100644 --- a/Sources/Yams/Emitter.swift +++ b/Sources/Yams/Emitter.swift @@ -41,7 +41,8 @@ public func dump( version: (major: Int, minor: Int)? = nil, sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any, - mappingStyle: Node.Mapping.Style = .any) throws -> String + mappingStyle: Node.Mapping.Style = .any, + newLineScalarStyle: Node.Scalar.Style = .any) throws -> String where Objects: Sequence { func representable(from object: Any) throws -> NodeRepresentable { if let representable = object as? NodeRepresentable { @@ -62,7 +63,8 @@ public func dump( version: version, sortKeys: sortKeys, sequenceStyle: sequenceStyle, - mappingStyle: mappingStyle + mappingStyle: mappingStyle, + newLineScalarStyle: newLineScalarStyle ) } @@ -96,7 +98,8 @@ public func dump( version: (major: Int, minor: Int)? = nil, sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any, - mappingStyle: Node.Mapping.Style = .any) throws -> String { + mappingStyle: Node.Mapping.Style = .any, + newLineScalarStyle: Node.Scalar.Style = .any) throws -> String { return try serialize( node: object.represented(), canonical: canonical, @@ -109,7 +112,8 @@ public func dump( version: version, sortKeys: sortKeys, sequenceStyle: sequenceStyle, - mappingStyle: mappingStyle + mappingStyle: mappingStyle, + newLineScalarStyle: newLineScalarStyle ) } @@ -143,7 +147,8 @@ public func serialize( version: (major: Int, minor: Int)? = nil, sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any, - mappingStyle: Node.Mapping.Style = .any) throws -> String + mappingStyle: Node.Mapping.Style = .any, + newLineScalarStyle: Node.Scalar.Style = .any) throws -> String where Nodes: Sequence, Nodes.Iterator.Element == Node { let emitter = Emitter( canonical: canonical, @@ -156,7 +161,8 @@ public func serialize( version: version, sortKeys: sortKeys, sequenceStyle: sequenceStyle, - mappingStyle: mappingStyle + mappingStyle: mappingStyle, + newLineScalarStyle: newLineScalarStyle ) try emitter.open() try nodes.forEach(emitter.serialize) @@ -194,7 +200,8 @@ public func serialize( version: (major: Int, minor: Int)? = nil, sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any, - mappingStyle: Node.Mapping.Style = .any) throws -> String { + mappingStyle: Node.Mapping.Style = .any, + newLineScalarStyle: Node.Scalar.Style = .any) throws -> String { return try serialize( nodes: [node], canonical: canonical, @@ -207,7 +214,8 @@ public func serialize( version: version, sortKeys: sortKeys, sequenceStyle: sequenceStyle, - mappingStyle: mappingStyle + mappingStyle: mappingStyle, + newLineScalarStyle: newLineScalarStyle ) } @@ -254,6 +262,9 @@ public final class Emitter { /// Set the style for mappings (dictionaries) public var mappingStyle: Node.Mapping.Style = .any + + /// Set the style for scalars that include newlines + public var newLineScalarStyle: Node.Scalar.Style = .any } /// Configuration options to use when emitting YAML. @@ -287,7 +298,8 @@ public final class Emitter { version: (major: Int, minor: Int)? = nil, sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any, - mappingStyle: Node.Mapping.Style = .any) { + mappingStyle: Node.Mapping.Style = .any, + newLineScalarStyle: Node.Scalar.Style = .any) { options = Options(canonical: canonical, indent: indent, width: width, @@ -298,7 +310,8 @@ public final class Emitter { version: version, sortKeys: sortKeys, sequenceStyle: sequenceStyle, - mappingStyle: mappingStyle) + mappingStyle: mappingStyle, + newLineScalarStyle: newLineScalarStyle) // configure emitter yaml_emitter_initialize(&emitter) yaml_emitter_set_output(&self.emitter, { pointer, buffer, size in @@ -420,7 +433,7 @@ extension Emitter.Options { public init(canonical: Bool = false, indent: Int = 0, width: Int = 0, allowUnicode: Bool = false, lineBreak: Emitter.LineBreak = .ln, version: (major: Int, minor: Int)? = nil, sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any, - mappingStyle: Node.Mapping.Style = .any) { + mappingStyle: Node.Mapping.Style = .any, newLineScalarStyle: Node.Scalar.Style = .any) { self.canonical = canonical self.indent = indent self.width = width @@ -430,6 +443,7 @@ extension Emitter.Options { self.sortKeys = sortKeys self.sequenceStyle = sequenceStyle self.mappingStyle = mappingStyle + self.newLineScalarStyle = newLineScalarStyle } } diff --git a/Sources/Yams/Encoder.swift b/Sources/Yams/Encoder.swift index 9c6d8549..a17711ee 100644 --- a/Sources/Yams/Encoder.swift +++ b/Sources/Yams/Encoder.swift @@ -29,7 +29,7 @@ public class YAMLEncoder { public func encode(_ value: T, userInfo: [CodingUserInfoKey: Any] = [:]) throws -> String { do { let encoder = _Encoder(userInfo: userInfo, sequenceStyle: options.sequenceStyle, - mappingStyle: options.mappingStyle) + mappingStyle: options.mappingStyle, newlineScalarStyle: options.newLineScalarStyle) var container = encoder.singleValueContainer() try container.encode(value) return try serialize(node: encoder.node, options: options) @@ -49,11 +49,12 @@ private class _Encoder: Swift.Encoder { var node: Node = .unused init(userInfo: [CodingUserInfoKey: Any] = [:], codingPath: [CodingKey] = [], sequenceStyle: Node.Sequence.Style, - mappingStyle: Node.Mapping.Style) { + mappingStyle: Node.Mapping.Style, newlineScalarStyle: Node.Scalar.Style) { self.userInfo = userInfo self.codingPath = codingPath self.sequenceStyle = sequenceStyle self.mappingStyle = mappingStyle + self.newlineScalarStyle = newlineScalarStyle } // MARK: - Swift.Encoder Methods @@ -62,6 +63,7 @@ private class _Encoder: Swift.Encoder { let userInfo: [CodingUserInfoKey: Any] let sequenceStyle: Node.Sequence.Style let mappingStyle: Node.Mapping.Style + let newlineScalarStyle: Node.Scalar.Style func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { if canEncodeNewValue { @@ -123,15 +125,21 @@ private class _ReferencingEncoder: _Encoder { init(referencing encoder: _Encoder, key: CodingKey) { self.encoder = encoder reference = .mapping(key.stringValue) - super.init(userInfo: encoder.userInfo, codingPath: encoder.codingPath + [key], - sequenceStyle: encoder.sequenceStyle, mappingStyle: encoder.mappingStyle) + super.init(userInfo: encoder.userInfo, + codingPath: encoder.codingPath + [key], + sequenceStyle: encoder.sequenceStyle, + mappingStyle: encoder.mappingStyle, + newlineScalarStyle: encoder.newlineScalarStyle) } init(referencing encoder: _Encoder, at index: Int) { self.encoder = encoder reference = .sequence(index) - super.init(userInfo: encoder.userInfo, codingPath: encoder.codingPath + [_YAMLCodingKey(index: index)], - sequenceStyle: encoder.sequenceStyle, mappingStyle: encoder.mappingStyle) + super.init(userInfo: encoder.userInfo, + codingPath: encoder.codingPath + [_YAMLCodingKey(index: index)], + sequenceStyle: encoder.sequenceStyle, + mappingStyle: encoder.mappingStyle, + newlineScalarStyle: encoder.newlineScalarStyle) } deinit { @@ -218,12 +226,30 @@ extension _Encoder: SingleValueEncodingContainer { func encode(_ value: T) throws where T: YAMLEncodable { assertCanEncodeNewValue() node = value.box() +#if swift(>=5.7) + if let stringValue = value as? (any StringProtocol), stringValue.contains("\n") { + node.scalar?.style = newlineScalarStyle + } +#else + if let stringValue = value as? String, stringValue.contains("\n") { + node.scalar?.style = newlineScalarStyle + } +#endif } func encode(_ value: T) throws where T: Encodable { assertCanEncodeNewValue() if let encodable = value as? YAMLEncodable { node = encodable.box() +#if swift(>=5.7) + if let stringValue = value as? (any StringProtocol), stringValue.contains("\n") { + node.scalar?.style = newlineScalarStyle + } +#else + if let stringValue = value as? String, stringValue.contains("\n") { + node.scalar?.style = newlineScalarStyle + } +#endif } else { try value.encode(to: self) } diff --git a/Tests/YamsTests/EncoderTests.swift b/Tests/YamsTests/EncoderTests.swift index 2eaa2269..bbda9273 100644 --- a/Tests/YamsTests/EncoderTests.swift +++ b/Tests/YamsTests/EncoderTests.swift @@ -151,6 +151,27 @@ class EncoderTests: XCTestCase { // swiftlint:disable:this type_body_length _testRoundTrip(of: OptionalTopLevelWrapper(url), expectedYAML: expectedYAML) } + func testNewlinesInString() throws { + let expectedYamlLiteral = """ + name: |- + This name + Has new lines + email: test@test.test + + """ + let expectedYamlFolded = """ + name: >- + This name + + Has new lines + email: test@test.test + + """ + let person = Person(name: "This name\nHas new lines", email: "test@test.test") + _testRoundTrip(of: person, with: .init(newLineScalarStyle: .literal), expectedYAML: expectedYamlLiteral) + _testRoundTrip(of: person, with: .init(newLineScalarStyle: .folded), expectedYAML: expectedYamlFolded) + } + func testNumberInString() throws { _testDecode(of: String.self, from: "'10'", expectedValue: "10") _testDecode(of: String.self, from: "'10.5'", expectedValue: "10.5")