Protobuf Message DSL

The Protobuf Message DSL generator creates lambda based builders and utility extensions for message types.

Getting Started

To enable this generator, at least one entry for protoBuilders must be present within the configuration file supplied to the protoc-gen-kroto-plus plugin. All available options are documented in ProtoBuildersGenOptions

protoBuilders:
- unwrapBuilders: false
  useDslMarkers: true
{
    "protoBuilders": [
        {
            "unwrapBuilders": false,
            "useDslMarkers": true
        }
    ]
}
proto_builders {
    unwrap_builders: false
    use_dsl_markers: true
}

Message DSL

The message DSL is achieved via a combination of top level and extension functions. Any message containing a field of type message will also receive an extension to allow construction of its nested field. The generated extensions allow composition of proto messages in a dsl style.

By default the generated utility methods for building messages are wrapped in an object similar to a proto outer class. The naming convention used for this outer object is {{java_outer_classname}}Builders.

// Using default generator output
val starPlatinum = StandProtoBuilders.Stand {
  ...
}

// Using static import of the methods present
// in the 'StandProtoBuilders' object 
val starPlatinum = Stand {
    name = "Star Platinum"
    powerLevel = 500
    speed = 550
      
    // Nested message field   
    attack {
        name = "ORA ORA ORA"
        damage = 100
        range = Attack.Range.CLOSE
    }
}

// starPlatinum.attack.damage == 100

For a more concise usage of the generated API is it recommended to statically importing the nested builder methods. Alternatively, the builder methods can be generated at the top level of the destination file without the wrapper object. Doing so may produce name collisions in certain circumstances. In these cases the generator will still implement these methods with a wrapper object to prevent breaking compilation. This output can be enabled using the following configuration option:

protoBuilders:
- unwrapBuilders: true
{
    "protoBuilders": [
        {
            "unwrapBuilders": true
        }
    ]
}
proto_builders {
    unwrap_builders: true
}

DslMarker Support

🚧

Enabling DslMarker Support

Using dsl markers relies on protoc insertions. Care must be taken to ensure that the kroto-plus output directory is the same as the directory for generated java code.

Access to the parent lamba scope from within a nested builder scope can be restricted using Kotlin's DslMarker. This is done by generating an empty tagging interface annotated with @DslMarker. The tagging interface is then inserted as a super type of the target messages builder. This is achieved using protoc insertions API, and more information regarding this API can be found in the official protobuf docs. This feature can be enabled using the following configuration option:

protoBuilders:
- useDslMarkers: true
{
    "protoBuilders": [
        {
            "useDslMarkers": true
        }
    ]
}
proto_builders {
    use_dsl_markers: true
}

❗️

Protobuf Maven Plugin

Dsl markers cannot be configured when using the Protobuf Maven Plugin for code generation. The issue lies with the lack of support for protoc insertions in the Protobuf Maven Plugin. This is documented in depth in Kroto+ issue #28 on Github.

Message Utility Extensions

Convenience extensions are also generated for common message operations.

Copy Operator

// Generator Output
inline fun MyMessage.copy(block: MyMessage.Builder.() -> Unit): MyMessage =
    this.toBuilder().apply(block).build()
  
// Example Usage
val myMessage = MyMessage {
    count = 1
    someField = "foo"
}  
  
val myCopy = myMessage.copy {
    someField = "bar"
}

myCopy.count == 1
myCopy.someField == "bar"

Plus Operator

// Generator Output
operator fun MyMessage.plus(other: MyMessage): MyMessage =
    this.toBuilder().mergeFrom(other).build()
  
// Example Usage
val myMessage = MyMessage {
    count = 1
    someField = "foo"
}  
  
val anotherMessage = MyMessage {
    someField = "bar"
}  

val mergedMessage = myMessage + anotherMessage

mergedMessage.count == 1
mergedMessage.someField == "bar"

OrDefault Operator

// Generator Output
fun MyMessage?.orDefault(): MyMessage = 
		this ?: MyMessage.getDefaultInstance()
  
// Example Usage
val myMessage: MyMessage? = null
  
myMessage.orDefault() == MyMessage.getDefaultInstance()