Skip to content

Commit

Permalink
Add option to include fields that are equal to the default value in t…
Browse files Browse the repository at this point in the history
…he JSON encoding
  • Loading branch information
mrabiciu committed Dec 14, 2023
1 parent 4ac8a2e commit 59a6212
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 45 deletions.
5 changes: 5 additions & 0 deletions Sources/SwiftProtobuf/JSONEncodingOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public struct JSONEncodingOptions: Sendable {
/// by keys in lexographical order. This is an implementation detail
/// and subject to change.
public var useDeterministicOrdering: Bool = false

/// Include fields that are equal to the default value in the JSON encoding.
/// This only applies to non-optional fields. Optional fields that are unset will still be
/// omitted from encoded JSON.
public var includeDefaultValues: Bool = false

public init() {}
}
5 changes: 2 additions & 3 deletions Sources/SwiftProtobuf/JSONEncodingVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal struct JSONEncodingVisitor: Visitor {
private var nameMap: _NameMap
private var extensions: ExtensionFieldValueSet?
private let options: JSONEncodingOptions
let traversalOptions: TraversalOptions

/// The JSON text produced by the visitor, as raw UTF8 bytes.
var dataResult: [UInt8] {
Expand All @@ -41,6 +42,7 @@ internal struct JSONEncodingVisitor: Visitor {
throw JSONEncodingError.missingFieldNames
}
self.options = options
traversalOptions = TraversalOptions(visitDefaultValues: options.includeDefaultValues)
}

mutating func startArray() {
Expand Down Expand Up @@ -143,7 +145,6 @@ internal struct JSONEncodingVisitor: Visitor {
fieldNumber: Int,
encode: (inout JSONEncoder, T) throws -> ()
) throws {
assert(!value.isEmpty)
try startField(for: fieldNumber)
var comma = false
encoder.startArray()
Expand Down Expand Up @@ -318,7 +319,6 @@ internal struct JSONEncodingVisitor: Visitor {
}

mutating func visitRepeatedMessageField<M: Message>(value: [M], fieldNumber: Int) throws {
assert(!value.isEmpty)
try startField(for: fieldNumber)
var comma = false
encoder.startArray()
Expand Down Expand Up @@ -351,7 +351,6 @@ internal struct JSONEncodingVisitor: Visitor {
}

mutating func visitRepeatedGroupField<G: Message>(value: [G], fieldNumber: Int) throws {
assert(!value.isEmpty)
// Google does not serialize groups into JSON
}

Expand Down
53 changes: 21 additions & 32 deletions Sources/SwiftProtobuf/Visitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import Foundation
/// used for serialization. It is implemented by each serialization protocol:
/// Protobuf Binary, Protobuf Text, JSON, and the Hash encoder.
public protocol Visitor {

var traversalOptions: TraversalOptions { get }

/// Called for each non-repeated float field
///
Expand Down Expand Up @@ -445,8 +447,27 @@ public protocol Visitor {
mutating func visitUnknown(bytes: Data) throws
}

/// Provides options for how visitor traversal should be carried out
public struct TraversalOptions {
static let `default` = TraversalOptions()

/// Determines if non-optional fields that are equal to their default values should be visited.
/// Defaults to `false`.
public var visitDefaultValues: Bool

public init(visitDefaultValues: Bool = false) {
self.visitDefaultValues = visitDefaultValues
}
}


/// Forwarding default implementations of some visitor methods, for convenience.
extension Visitor {

// Use the default traversal options if not set
public var traversalOptions: TraversalOptions {
.default
}

// Default definitions of numeric serializations.
//
Expand Down Expand Up @@ -492,126 +513,108 @@ extension Visitor {
// repeated values differently from singular, so overrides these.

public mutating func visitRepeatedFloatField(value: [Float], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularFloatField(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedDoubleField(value: [Double], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularDoubleField(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedInt32Field(value: [Int32], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularInt32Field(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedInt64Field(value: [Int64], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularInt64Field(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedUInt32Field(value: [UInt32], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularUInt32Field(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedUInt64Field(value: [UInt64], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularUInt64Field(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedSInt32Field(value: [Int32], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularSInt32Field(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedSInt64Field(value: [Int64], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularSInt64Field(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedFixed32Field(value: [UInt32], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularFixed32Field(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedFixed64Field(value: [UInt64], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularFixed64Field(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedSFixed32Field(value: [Int32], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularSFixed32Field(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedSFixed64Field(value: [Int64], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularSFixed64Field(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedBoolField(value: [Bool], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularBoolField(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedStringField(value: [String], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularStringField(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedBytesField(value: [Data], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularBytesField(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedEnumField<E: Enum>(value: [E], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularEnumField(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedMessageField<M: Message>(value: [M], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularMessageField(value: v, fieldNumber: fieldNumber)
}
}

public mutating func visitRepeatedGroupField<G: Message>(value: [G], fieldNumber: Int) throws {
assert(!value.isEmpty)
for v in value {
try visitSingularGroupField(value: v, fieldNumber: fieldNumber)
}
Expand All @@ -623,73 +626,59 @@ extension Visitor {
// overridden by Protobuf Binary and Text.

public mutating func visitPackedFloatField(value: [Float], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitRepeatedFloatField(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedDoubleField(value: [Double], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitRepeatedDoubleField(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedInt32Field(value: [Int32], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitRepeatedInt32Field(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedInt64Field(value: [Int64], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitRepeatedInt64Field(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedUInt32Field(value: [UInt32], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitRepeatedUInt32Field(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedUInt64Field(value: [UInt64], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitRepeatedUInt64Field(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedSInt32Field(value: [Int32], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitPackedInt32Field(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedSInt64Field(value: [Int64], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitPackedInt64Field(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedFixed32Field(value: [UInt32], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitPackedUInt32Field(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedFixed64Field(value: [UInt64], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitPackedUInt64Field(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedSFixed32Field(value: [Int32], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitPackedInt32Field(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedSFixed64Field(value: [Int64], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitPackedInt64Field(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedBoolField(value: [Bool], fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitRepeatedBoolField(value: value, fieldNumber: fieldNumber)
}

public mutating func visitPackedEnumField<E: Enum>(value: [E],
fieldNumber: Int) throws {
assert(!value.isEmpty)
try visitRepeatedEnumField(value: value, fieldNumber: fieldNumber)
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/protoc-gen-swift/FieldGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import SwiftProtobuf
/// Interface for field generators.
protocol FieldGenerator {
var number: Int { get }

/// If the field uses the `visitDefaultValues` flag in its `generateTraverse` implementation.
var usesDefaultValueFlagForTraversal: Bool { get }

/// Name mapping entry for the field.
var fieldMapNames: String { get }
Expand Down
52 changes: 42 additions & 10 deletions Sources/protoc-gen-swift/MessageFieldGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class MessageFieldGenerator: FieldGeneratorBase, FieldGenerator {
return false
}
}

var usesDefaultValueFlagForTraversal: Bool {
!hasFieldPresence || (!isGroupOrMessage && fieldDescriptor.file.syntax == .proto2)
}

init(descriptor: FieldDescriptor,
generatorOptions: GeneratorOptions,
Expand Down Expand Up @@ -194,31 +198,59 @@ class MessageFieldGenerator: FieldGeneratorBase, FieldGenerator {
traitsArg = ""
}

let varName = hasFieldPresence ? "v" : storedProperty

var usesLocals = false
let conditional: String
let conditionals: [(condition: String, varName: String)]
if isRepeated { // Also covers maps
conditional = "!\(varName).isEmpty"
conditionals = [
(condition: "visitDefaultValues || !\(storedProperty).isEmpty", varName: storedProperty)
]
} else if hasFieldPresence {
conditional = "let v = \(storedProperty)"
usesLocals = true
if !isGroupOrMessage, fieldDescriptor.file.syntax == .proto2 {
conditionals = [
(condition: "let v = \(storedProperty)", varName: "v"),
(condition: "visitDefaultValues", varName: swiftName),
]
} else {
conditionals = [
(condition: "let v = \(storedProperty)", varName: "v"),
]
}
} else {
// At this point, the fields would be a primative type, and should only
// be visted if it is the non default value.
switch fieldDescriptor.type {
case .string, .bytes:
conditional = ("!\(varName).isEmpty")
conditionals = [
(condition: "visitDefaultValues || !\(storedProperty).isEmpty", varName: storedProperty)
]
default:
conditional = ("\(varName) != \(swiftDefaultValue)")
conditionals = [
(condition: "visitDefaultValues || \(storedProperty) != \(swiftDefaultValue)", varName: storedProperty)
]
}
}
assert(usesLocals == generateTraverseUsesLocals)
let prefix = usesLocals ? "try { " : ""
let suffix = usesLocals ? " }()" : ""
for (i, conditional) in conditionals.enumerated() {
if i == 0 {
p.print("\(prefix)if \(conditional.condition) {")
} else {
p.print("} else if \(conditional.condition) {")
}
p.printIndented("try visitor.\(visitMethod)(\(traitsArg)value: \(conditional.varName), fieldNumber: \(number))")
if i == conditionals.count - 1 {
p.print("}\(suffix)")
}

p.print("\(prefix)if \(conditional) {")
p.printIndented("try visitor.\(visitMethod)(\(traitsArg)value: \(varName), fieldNumber: \(number))")
p.print("}\(suffix)")
}
// p.print("\(prefix)if \(conditional) {")
// p.printIndented("try visitor.\(visitMethod)(\(traitsArg)value: \(varName), fieldNumber: \(number))")
// if shouldIncludeElseWithDefaultValueVisit {
// p.print("} else {")
// p.printIndented("try visitor.\(visitMethod)(\(traitsArg)value: \(swiftDefaultValue), fieldNumber: \(number))")
// }
// p.print("}\(suffix)")
}
}
5 changes: 5 additions & 0 deletions Sources/protoc-gen-swift/MessageGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,12 @@ class MessageGenerator {
/// - Parameter p: The code printer.
private func generateTraverse(printer p: inout CodePrinter) {
p.print("\(visibility)func traverse<V: \(namer.swiftProtobufModulePrefix)Visitor>(visitor: inout V) throws {")

p.withIndentation { p in
if fields.contains(where: { $0.usesDefaultValueFlagForTraversal }) {
p.print("let visitDefaultValues = visitor.traversalOptions.visitDefaultValues")
}

generateWithLifetimeExtension(printer: &p, throws: true) { p in
if let storage = storage {
storage.generatePreTraverse(printer: &p)
Expand Down

0 comments on commit 59a6212

Please sign in to comment.