google/proto-lens

Show instance not working for first/default enum values

Opened this issue · 3 comments

I've got the following defined in a protobuf (proto3)

...

enum SODirection {
  SO_DIRECTION_FORWARD = 0;
  SO_DIRECTION_BACKWARD = 1;
}

message SOAccountEvent {
  bytes event_id = 1;
  uint64 order_number = 2;
  uint64 account_number = 3;
  SOStatus from_status = 4;
  SOStatus to_status = 5;
  SOStatus target_status = 6;
  SODirection direction = 7;
  string bump_user = 8;
}

...

which I've generated into the following haskell code with the compiler

...

{- | Fields :
     
         * 'Proto.SoEvent_Fields.eventId' @:: Lens' SOAccountEvent Data.ByteString.ByteString@
         * 'Proto.SoEvent_Fields.orderNumber' @:: Lens' SOAccountEvent Data.Word.Word64@
         * 'Proto.SoEvent_Fields.accountNumber' @:: Lens' SOAccountEvent Data.Word.Word64@
         * 'Proto.SoEvent_Fields.fromStatus' @:: Lens' SOAccountEvent SOStatus@
         * 'Proto.SoEvent_Fields.toStatus' @:: Lens' SOAccountEvent SOStatus@
         * 'Proto.SoEvent_Fields.targetStatus' @:: Lens' SOAccountEvent SOStatus@
         * 'Proto.SoEvent_Fields.direction' @:: Lens' SOAccountEvent SODirection@
         * 'Proto.SoEvent_Fields.bumpUser' @:: Lens' SOAccountEvent Data.Text.Text@ -}
data SOAccountEvent
  = SOAccountEvent'_constructor {_SOAccountEvent'eventId :: !Data.ByteString.ByteString,
                                 _SOAccountEvent'orderNumber :: !Data.Word.Word64,
                                 _SOAccountEvent'accountNumber :: !Data.Word.Word64,
                                 _SOAccountEvent'fromStatus :: !SOStatus,
                                 _SOAccountEvent'toStatus :: !SOStatus,
                                 _SOAccountEvent'targetStatus :: !SOStatus,
                                 _SOAccountEvent'direction :: !SODirection,
                                 _SOAccountEvent'bumpUser :: !Data.Text.Text,
                                 _SOAccountEvent'_unknownFields :: !Data.ProtoLens.FieldSet}
  deriving stock (Prelude.Eq, Prelude.Ord)

...

newtype SODirection'UnrecognizedValue
  = SODirection'UnrecognizedValue Data.Int.Int32
  deriving stock (Prelude.Eq, Prelude.Ord, Prelude.Show)
data SODirection
  = SO_DIRECTION_FORWARD |
    SO_DIRECTION_BACKWARD |
    SODirection'Unrecognized !SODirection'UnrecognizedValue
  deriving stock (Prelude.Show, Prelude.Eq, Prelude.Ord)
instance Data.ProtoLens.MessageEnum SODirection where
  maybeToEnum 0 = Prelude.Just SO_DIRECTION_FORWARD
  maybeToEnum 1 = Prelude.Just SO_DIRECTION_BACKWARD
  maybeToEnum k
    = Prelude.Just
        (SODirection'Unrecognized
           (SODirection'UnrecognizedValue (Prelude.fromIntegral k)))
  showEnum SO_DIRECTION_FORWARD = "SO_DIRECTION_FORWARD"
  showEnum SO_DIRECTION_BACKWARD = "SO_DIRECTION_BACKWARD"
  showEnum
    (SODirection'Unrecognized (SODirection'UnrecognizedValue k))
    = Prelude.show k
  readEnum k
    | (Prelude.==) k "SO_DIRECTION_FORWARD"
    = Prelude.Just SO_DIRECTION_FORWARD
    | (Prelude.==) k "SO_DIRECTION_BACKWARD"
    = Prelude.Just SO_DIRECTION_BACKWARD
    | Prelude.otherwise
    = (Prelude.>>=) (Text.Read.readMaybe k) Data.ProtoLens.maybeToEnum
instance Prelude.Bounded SODirection where
  minBound = SO_DIRECTION_FORWARD
  maxBound = SO_DIRECTION_BACKWARD
instance Prelude.Enum SODirection where
  toEnum k__
    = Prelude.maybe
        (Prelude.error
           ((Prelude.++)
              "toEnum: unknown value for enum SODirection: " (Prelude.show k__)))
        Prelude.id (Data.ProtoLens.maybeToEnum k__)
  fromEnum SO_DIRECTION_FORWARD = 0
  fromEnum SO_DIRECTION_BACKWARD = 1
  fromEnum
    (SODirection'Unrecognized (SODirection'UnrecognizedValue k))
    = Prelude.fromIntegral k
  succ SO_DIRECTION_BACKWARD
    = Prelude.error
        "SODirection.succ: bad argument SO_DIRECTION_BACKWARD. This value would be out of bounds."
  succ SO_DIRECTION_FORWARD = SO_DIRECTION_BACKWARD
  succ (SODirection'Unrecognized _)
    = Prelude.error
        "SODirection.succ: bad argument: unrecognized value"
  pred SO_DIRECTION_FORWARD
    = Prelude.error
        "SODirection.pred: bad argument SO_DIRECTION_FORWARD. This value would be out of bounds."
  pred SO_DIRECTION_BACKWARD = SO_DIRECTION_FORWARD
  pred (SODirection'Unrecognized _)
    = Prelude.error
        "SODirection.pred: bad argument: unrecognized value"
  enumFrom = Data.ProtoLens.Message.Enum.messageEnumFrom
  enumFromTo = Data.ProtoLens.Message.Enum.messageEnumFromTo
  enumFromThen = Data.ProtoLens.Message.Enum.messageEnumFromThen
  enumFromThenTo = Data.ProtoLens.Message.Enum.messageEnumFromThenTo
instance Data.ProtoLens.FieldDefault SODirection where
  fieldDefault = SO_DIRECTION_FORWARD
instance Control.DeepSeq.NFData SODirection where
  rnf x__ = Prelude.seq x__ ()

...

and when I use this in my test project and I try to show the whole record

...

mkSOAccountEvent ::
     ByteString
  -> Word64
  -> Word64
  -> SOStatus
  -> SOStatus
  -> SODirection
  -> Text
  -> SOAccountEvent
mkSOAccountEvent e o a f t d b =
  defMessage
  & eventId .~ e
  & orderNumber .~ o
  & accountNumber .~ a
  & fromStatus .~ f
  & toStatus .~ t
  & targetStatus .~ t
  & direction .~ d
  & bumpUser .~ b

...

let ev1 = mkSOAccountEvent "e1" 1 11 SO_STATUS_DISPATCH SO_STATUS_QA SO_DIRECTION_FORWARD "Tester1"
putStrLn $ show ev1
let ev2 = mkSOAccountEvent "e2" 2 22 SO_STATUS_DISPATCH SO_STATUS_QA SO_DIRECTION_BACKWARD "Tester2"
putStrLn $ show ev2

...

but it doesn't print the direction field when I have it set to the first/default value

{event_id: "e1" order_number: 1 account_number: 11 from_status: SO_STATUS_DISPATCH to_status: SO_STATUS_QA target_status: SO_STATUS_QA bump_user: "Tester1"}
{event_id: "e2" order_number: 2 account_number: 22 from_status: SO_STATUS_DISPATCH to_status: SO_STATUS_QA target_status: SO_STATUS_QA direction: SO_DIRECTION_BACKWARD bump_user: "Tester2"}

I think that I've removed all doubt that my custom code/protobuf has caused this behavior, and for some reason the generated haskell types are the culprit

For anyone still interested, this is a consequence of using proto3 without explicitly optional fields. Default scalar values in proto3 are indistinguishable from the field never having been explicitly set, so it would be semantically valid to skip the output of a default value field with text format. In fact, serialization officially does not include default scalar values even if they have been explicitly set (unless the field is explicitly marked as optional).

Thank you for spelling that out. It's been a while since I've been close to this so I almost don't have an investment anymore, but....

It does seem like a bit of a workaround to define a field as optional even though I want to required it is there. I understand that (at least in this case) since the field is an enum, setting it optional doesn't mean it can be null so there should be no trouble with parsing - though it introduces the need to check if the field has been set or not, which wasn't needed before.

Am I thinking about this in the wrong way, or is this just the cost of doing business? (i.e. the price to pay for defining complex types in a protobuf)

In any version of proto, you never need to check whether a field was set or not, if it's OK to use the default value if a field was unset. Even in proto2 and the default for Protobuf Editions 2023, the default value will be returned for a field if it was not unset. I.e., if one ignores field presence, all versions of protobuf behave the same from the code's perspective; proto3 just lacks the ability to determine whether a field has a default value because it was set explicitly or if because it was cleared or was never set in the first place.