TSFoster / elm-bytes-extra / Bytes.Decode.Extra

Helpers for working with Bytes.Decoders.

list : Basics.Int -> Bytes.Decode.Decoder a -> Bytes.Decode.Decoder (List a)

Run a decoder a set amount of times, and collect the result in a list.

import Bytes.Decode
import Bytes.Extra exposing (fromByteValues)


fromByteValues (List.range 0 20)
    |> Bytes.Decode.decode (Bytes.Decode.Extra.list 21 Bytes.Decode.unsignedInt8)
--> Just (List.range 0 20)

byteValues : Basics.Int -> Bytes.Decode.Decoder (List Basics.Int)

Before the release of [elm/bytes], many packages would use List Int to represent bytes. byteValues aids interaciton between those packages and Bytes.

import Bytes exposing (Bytes)
import Bytes.Decode
import Bytes.Extra exposing (fromByteValues)

input : Bytes
input =
    fromByteValues [ 0x32, 0xFF, 0x53, 0x54, 0x55 ]

Bytes.Decode.decode (byteValues 3) input
--> Just [ 0x32, 0xFF, 0x53 ]

24-bit Integers

One notable use of 24-bit integers is in 24-bit color, a.k.a. "True color".

unsignedInt24 : Bytes.Endianness -> Bytes.Decode.Decoder Basics.Int

Decode three bytes into an integer from 0 to 16777215.

signedInt24 : Bytes.Endianness -> Bytes.Decode.Decoder Basics.Int

Decode three bytes into an integer from -8388608 to 8388607.

Pipeline-style

Json.Decode.Pipeline provides a neat, scalable way for decoding JSON. This module provides a few helpers for adopting a similar style when decoding Bytes.

It is important to note that decoding bytes poses more restrictions than decoding JSON, as bytes are consumed in order, and a maximum of one time. This makes an equivalent to Json.Decode.Pipeline.optional difficult to impossible.

import Bytes exposing (Endianness(..))
import Bytes.Decode exposing (Decoder, succeed, andThen, unsignedInt32, string, float64, decode)
import Bytes.Extra exposing (fromByteValues)

type Status = Downloaded | LocallyGenerated

type alias MyData =
  { id: Int
  , status : Status
  , name: String
  , value: Float
  }

myDataDecoder : Decoder MyData
myDataDecoder =
    succeed MyData
        |> andMap (unsignedInt32 BE)
        |> hardcoded Downloaded
        |> andMap (unsignedInt32 BE |> andThen string)
        |> andMap (float64 BE)


decode myDataDecoder (fromByteValues [0,0,0,252,0,0,0,8,116,104,101,95,110,97,109,101,63,224,0,0,0,0,0,0])
--> Just
-->     { id = 252
-->     , status = Downloaded
-->     , name = "the_name"
-->     , value = 0.5
-->     }

andMap : Bytes.Decode.Decoder a -> Bytes.Decode.Decoder (a -> b) -> Bytes.Decode.Decoder b

andMap behaves in a similar way to Json.Decode.Pipeline.required. It is named andMap to match the naming of similar functions in other packages, and because this package does not provide an equivalent to Json.Decode.Pipeline.optional.

modelDecoder : Decoder Model
modelDecoder =
    succeed Model
        |> andMap (unsignedInt32 BE)
        |> andMap (unsignedInt32 BE |> andThen string)
        |> andMap myCustomTypeDecoder
        |> andMap myCustomBooleanDecoder

hardcoded : a -> Bytes.Decode.Decoder (a -> b) -> Bytes.Decode.Decoder b

A neat way to fill in predetermined information that's not part of the data being decoded, similar to Json.Decode.Pipeline.hardcoded.

modelDecoder : Decoder Model
modelDecoder =
    succeed Model
        |> andMap (unsignedInt32 BE)
        |> andMap (unsignedInt32 BE |> andThen string)
        |> hardcoded []
        |> andMap myCustomBooleanDecoder
        |> hardcoded SubModel.default

Skipping bytes

withOffset : Basics.Int -> Bytes.Decode.Decoder a -> Bytes.Decode.Decoder a

Sometimes the value you need to decode is preceded by a bunch of bytes you just don't need.

import Bytes exposing (Endianness(..))
import Bytes.Decode exposing (decode, unsignedInt16)
import Bytes.Extra exposing (fromByteValues)

fromByteValues [ 0xff, 0xff, 0xff, 0x21, 0x30 ]
    |> decode (withOffset 3 (unsignedInt16 BE))
--> Just 8496

Extra maps

map6 : (a -> b -> c -> d -> e -> f -> result) -> Bytes.Decode.Decoder a -> Bytes.Decode.Decoder b -> Bytes.Decode.Decoder c -> Bytes.Decode.Decoder d -> Bytes.Decode.Decoder e -> Bytes.Decode.Decoder f -> Bytes.Decode.Decoder result

map7 : (a -> b -> c -> d -> e -> f -> g -> result) -> Bytes.Decode.Decoder a -> Bytes.Decode.Decoder b -> Bytes.Decode.Decoder c -> Bytes.Decode.Decoder d -> Bytes.Decode.Decoder e -> Bytes.Decode.Decoder f -> Bytes.Decode.Decoder g -> Bytes.Decode.Decoder result

map8 : (a -> b -> c -> d -> e -> f -> g -> h -> result) -> Bytes.Decode.Decoder a -> Bytes.Decode.Decoder b -> Bytes.Decode.Decoder c -> Bytes.Decode.Decoder d -> Bytes.Decode.Decoder e -> Bytes.Decode.Decoder f -> Bytes.Decode.Decoder g -> Bytes.Decode.Decoder h -> Bytes.Decode.Decoder result

map9 : (a -> b -> c -> d -> e -> f -> g -> h -> i -> result) -> Bytes.Decode.Decoder a -> Bytes.Decode.Decoder b -> Bytes.Decode.Decoder c -> Bytes.Decode.Decoder d -> Bytes.Decode.Decoder e -> Bytes.Decode.Decoder f -> Bytes.Decode.Decoder g -> Bytes.Decode.Decoder h -> Bytes.Decode.Decoder i -> Bytes.Decode.Decoder result

map16 : (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l -> m -> n -> o -> p -> result) -> Bytes.Decode.Decoder a -> Bytes.Decode.Decoder b -> Bytes.Decode.Decoder c -> Bytes.Decode.Decoder d -> Bytes.Decode.Decoder e -> Bytes.Decode.Decoder f -> Bytes.Decode.Decoder g -> Bytes.Decode.Decoder h -> Bytes.Decode.Decoder i -> Bytes.Decode.Decoder j -> Bytes.Decode.Decoder k -> Bytes.Decode.Decoder l -> Bytes.Decode.Decoder m -> Bytes.Decode.Decoder n -> Bytes.Decode.Decoder o -> Bytes.Decode.Decoder p -> Bytes.Decode.Decoder result

Working with Results and Maybes

onlyOks : Bytes.Decode.Decoder (Result err a) -> Bytes.Decode.Decoder a

Take a Decoder (Result err a) and make it fail if it decodes an Err.

onlyJusts : Bytes.Decode.Decoder (Maybe a) -> Bytes.Decode.Decoder a

Take a Decoder (Maybe a) and make it fail if it decodes to Nothing.

import Bytes.Extra exposing (fromByteValues)
import Bytes.Decode exposing (decode, map, string)

fromByteValues [ 0x30, 0x30, 0x30, 0x31, 0x30 ] -- "00010"
    |> decode (onlyJusts (map String.toInt (string 5)))
--> Just 10