eriktim / elm-protocol-buffers / Protobuf.Encode

Library for turning Elm values into Protobuf messages.

The examples show Bytes values like this: <3A* 05* 68 65 6C 6C 6F>. The * means the byte is Protobuf metadata. It does not contain any real value. Here 3A means the next encoded field is a length delimited value for field number 7. 05 is the number of bytes that was used to encode the value that follows. Those five bytes contain the string hello. Read this if you want to learn more about how Protobuf encoding works.

Encoding

encode : Encoder -> Bytes

Turn an Encoder into Bytes.

 encode (int32 127)    -- <7F>
 encode (sint32 127)   -- <FE 01>
 encode (sfixed32 127) -- <7F 00 00 00>

Values are encoded together with a field number and the wire type conform the specification in a .proto file. This allows decoders to know what field it is decoding and to read the right number of Bytes.

import Protobuf.Encode as Encode

type alias Person =
    { age : Int
    , name : String
    }

toPersonEncoder : Person -> Encode.Encoder
toPersonEncoder person =
    Encode.message
        [ ( 1, Encode.uint32 person.age )
        , ( 2, Encode.string person.name )
        ]

Encode.encode (encodePerson (Person 33 "Tom")) -- <08* 21 12* 03* 54 6F 6D>

You probably want to send these Bytes in the body of an HTTP request:

import Http
import Protobuf.Encode as Encode

postPerson : (Result Http.Error () -> msg) -> Person -> Cmd msg
postPerson toMsg person =
    Http.post
        { url = "https://example.com/person"
        , body =
            Http.bytesBody "application/octet-stream" <|
                Encode.encode (encodePerson person)
        , expect = Http.expectWhatever
        }


type Encoder

Describes how to generate a sequence of bytes according to the specification of Protobuf.

message : List ( Basics.Int, Encoder ) -> Encoder

Encode a record into a message. For this you need to provide a list of unique field numbers (between 1 and 536870911) and their corresponding Encoders.

 type alias Foo =
     { a : Float
     , b : String
     , c : List Int
     }

 foo : Foo
 foo =
    Foo 1.25 "hello" [ 1, 2, 3, 4, 5 ]

 toEncoder : Encoder
 toEncoder =
     message
         [ ( 1, double foo.a )         -- <09* 00 00 00 00 00 00 F4 3F
         , ( 2, string foo.b )         --  12* 05* 68 65 6C 6C 6F
         , ( 3, repeated int32 foo.c ) --  1A* 05* 01 02 03 04 05>
         ]

Integers

int32 : Basics.Int -> Encoder

Encode integers from -2147483648 to 2147483647 into a message. Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.

 encode (int32 0)    -- <00>
 encode (int32 100)  -- <64>
 encode (int32 -100) -- <FF FF FF FF FF FF FF 9C>

This function can also be used to encode custom types as enumeration:

type Fruit
    = Apple
    | Banana
    | Mango
    | Unrecognized Int

toFruitEncoder : Fruit -> Encoder
toFruitEncoder value =
    Encode.int32 <|
        case value of
            Apple ->
                0

            Banana ->
                1

            Mango ->
                2

            Unrecognized v ->
                v

Note that for proto2 the Unrecognized Int field can be left out.

uint32 : Basics.Int -> Encoder

Encode integers from 0 to 4294967295 into a message. Uses variable-length encoding.

 encode (uint32 0)   -- <00>
 encode (uint32 100) -- <64>

sint32 : Basics.Int -> Encoder

Encode integers from -2147483648 to 2147483647 into a message. Uses variable-length encoding. This encoder encodes negative numbers more efficiently than int32.

 encode (sint32 0)    -- <00>
 encode (sint32 100)  -- <C8 01>
 encode (sint32 -100) -- <C7 01>

fixed32 : Basics.Int -> Encoder

Encode integers from 0 to 4294967295 into a message. Always four bytes. More efficient than uint32 if values are often greater than 268435456.

 encode (fixed32 0)   -- <00 00 00 00>
 encode (fixed32 100) -- <64 00 00 00>

sfixed32 : Basics.Int -> Encoder

Encode integers from -2147483648 to 2147483647 into a message. Always four bytes.

 encode (sfixed32 0)    -- <00 00 00 00>
 encode (sfixed32 100)  -- <64 00 00 00>
 encode (sfixed32 -100) -- <9C FF FF FF>

int64 : Protobuf.Types.Int64.Int64 -> Encoder

Encode integers from -9223372036854775808 to 9223372036854775807 into a message. Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.

uint64 : Protobuf.Types.Int64.Int64 -> Encoder

Encode integers from 0 to 18446744073709551615 into a message. Uses variable-length encoding.

sint64 : Protobuf.Types.Int64.Int64 -> Encoder

Encode integers from -9223372036854775808 to 9223372036854775807 into a message. Uses variable-length encoding. This encoder encodes negative numbers more efficiently than int64.

fixed64 : Protobuf.Types.Int64.Int64 -> Encoder

Encode integers from 0 to 18446744073709551615 into a message. Always eight bytes.

sfixed64 : Protobuf.Types.Int64.Int64 -> Encoder

Encode integers from -9223372036854775808 to 9223372036854775807 into a message. Always eight bytes.

Floats

double : Basics.Float -> Encoder

Encode 64-bit floating point numbers into a message.

 encode (double 0)    -- <00 00 00 00 00 00 00 00>
 encode (double 100)  -- <00 00 00 00 00 00 59 40>
 encode (double -100) -- <00 00 00 00 00 00 59 C0>

float : Basics.Float -> Encoder

Encode 32-bit floating point numbers into a message. The value may lose some precision by encoding it as a float.

 encode (float 0)    -- <00 00 00 00>
 encode (float 100)  -- <00 00 C8 42>
 encode (float -100) -- <00 00 C8 C2>

Strings

string : String -> Encoder

Encode strings into a message.

 encode (string "$20")   -- <24 32 30>
 encode (string "£20")   -- <C2 A3 32 30>
 encode (string "€20")   -- <E2 82 AC 32 30>
 encode (string "bread") -- <62 72 65 61 64>
 encode (string "brød")  -- <62 72 C3 B8 64>

Booleans

bool : Basics.Bool -> Encoder

Encode booleans into a message.

 encode (bool False) -- <00>
 encode (bool True)  -- <01>

Bytes

bytes : Bytes -> Encoder

Copy raw Bytes into a message.

-- bs == <0A 0B 0C>
encode (bytes bs) -- <0A 0B 0C>

None

none : Encoder

Encode nothing. Note that you can easily combine this encoder with any field number to pass to message as literally nothing will be encoded.

This can be useful when encoding embedded messages:

type alias Report =
    { title : String
    , contents : String
    , attachment : Maybe Attachment
    }

toReportEncoder : Report -> Encoder
toReportEncoder report =
    message
        [ ( 1, string report.title )
        , ( 2, string report.contents )
        , ( 3, Maybe.withDefault none <| Maybe.map toAttachmentEncoder report.attachment )
        ]

Or when encoding custom types:

type alias FormValue =
    { key : String
    , value : Maybe Value
    }

type Value
    = StringValue String
    | IntValue Int

toKeyValueEncoder : FormValue -> Encoder
toKeyValueEncoder formValue =
    message
        [ ( 1, string formValue.key )
        , Maybe.withDefault ( 0, none ) <| Maybe.map toValueEncoder formValue.value
        ]

toValueEncoder : Value -> ( Int, Encoder )
toValueEncoder model =
    case model of
        StringValue value ->
            ( 2, string value )

        IntValue value ->
            ( 3, int32 value )

Data Structures

list : (a -> Encoder) -> List a -> Encoder

Encode a list of values into a message. Protobuf support two kind of encodings:

 -- packed encoding
 message
     [ ( 1, list int32 [ 1, 2, 3 ] ) -- <0A* 03* 01 02 03>
     ]

 -- non-packed encoding
 message
     [ ( 1
       , list string
             [ "one"   -- <0A* 03* 6F 6E 65
             , "two"   --  0A* 03* 74 77 6F
             , "three" --  0A* 05* 74 68 72 65 65>
             ]
       )
     ]

Packed encoding is preferred as it uses less bytes on the wire. list will automatically fall-back to non-packed encoding for non-scalar numeric types.

dict : (k -> Encoder) -> (v -> Encoder) -> Dict k v -> Encoder

Encode a dictionary of key-value pairs. This requires providing one encoder for the keys and one for the values.

let
    value =
        Dict.fromList
            [ ( 1, "foo" ) -- <0A* 07* 08* 01 12* 03* 66 6F 6F
            , ( 2, "bar" ) --  0A* 07* 08* 02 12* 03* 62 61 72>
            ]
in
message [ ( 1, dict int32 string value ) ]