Transformations
Introduction#
Field transformations were introduced in ScalaPB 0.10.10 and allow you to automatically apply ScalaPB field-level options when a given field match certain conditions. In the future, we expect to have transformations for additional protobuf entities.
This document assumes that you are already familiar with:
- Protocol Buffer custom options.
- ScalaPB customizations and type mappers.
- Protobuf Descriptors, and specifically FieldDescriptorProto.
Example use-case#
Assume that your project uses a custom option called sensitive, and whenever this option is set to true on a string field, you would ScalaPB to use a custom type, SensitiveString instead of a standard String.
The custom option definition could look like this:
Example usage of the custom options:
We want the type of secret in the case class to be SensitiveString. We could manually set
(scalapb.field).type to SensitiveString:
but it would be nice if there was a way to automatically apply (scalapb.field).type automatically whenever sensitive was set to true on a string field. This is the problem field transformations are set to solve:
The transformation above matches when the custom option senstive is true, and the field type
is string. When it matches, it sets the ScalaPB option type.
Syntax#
FieldTransformations are defined in scalapb.proto:
ScalaPB has a file-level option field_transformations which is a repeated FieldTransformation. The scope
of the field transformations is the same proto-file, and can be passed down to the entire package as a package-scoped
option (when scope: PACKAGE option is set).
A field transformation matches on the when condition which a FieldDescriptorProto. This allows it to match on the field's type, or label (LABEL_REPEATED, LABEL_OPTIONAL, LABEL_REQUIRED), as well as on custom options like in the previous example. There are few matching modes that are described below and can be selected using match_type. The set field tells ScalaPB what options to apply to the field if the rule conditions match. Currently, only [scalapb.field] options may appear in the set field.
There are three matching modes available:
CONTAINSis the default matching mode. In this mode, ScalaPB checks that all the options in thewhenpattern are defined on the field descriptor and having the same value (even if the field is repeated). Additional fields may be defined on the field besides the ones on thewhenpattern.EXACTis a strict equality comparison between thewhenpattern and the field descriptor.PRESENCEchecks whether every field that is present on thewhenpattern is also present on the field's rules. The specific value the option has is not compared. This allows matching on any value. For example,{int32: {gt: 1}}would match for any number assigned toint32.gt. For repeated fields,PRESENCEverifies that the value is not empty.
Referencing rules values#
It is possible to reference values in the rules and use them on the set part. Whenever there is a singular string field under the field descriptor, ScalaPB would replace tokens in the format $(p) with the value of the field's option at the path p, relative to the FieldDescriptorProto of the field. To reference extension fields, wrap the extension full name in brackets ([]). For example, $(options.[pkg.opts].num) would be substituted with the value of that option on the field. If the option is not set on the field, a default value will be replaced (0 for numeric types, empty string, and so on).
The paths that are referenced don't have to appear on the when pattern. While referencing rule values is useful when the matching mode is PRESENCE, it is supported to reference rule values in all matching modes.
One application for this is in conjunction with refined types. See example in ScalaPB validate documentation.
Example: customizing third-party types#
When you want to customize your own messages, ScalaPB lets you add custom
options within the message is defined. You may also want to apply customizations to types defined in third-party protos which you can not change. To accomplish that, we can use field transformations. In the following example, we match on google.protobuf.Timestamp and map it to a custom type. In src/main/protobuf/myexample/options.proto:
note
Note the . (dot) prefix in the type_name field above. It is needed as explained here. In this example we assume the user's package is not named google or google.protobuf since then type_name could be relative and would not match.
Now, we need to make sure there is an implicit typemapper converting between google.protobuf.timestamp.Timestamp and com.myexample.MyType. The typemapper can be defined in the companion object of MyType as exampled in custom types.