Encoding & Decoding Abilities

An example for how to implement the builtin Encoding and Decoding abilities for an opaque type (ItemKind).

Implementing these abilites for an opaque type like ItemKind, enables it to be used seamlessly within other data structures. This is useful when you would like to provide a custom mapping, such as in this example, between an integer and a tag union.

Implementation



ItemKind := [
    Text,
    Method,
    Function,
    Constructor,
    Field,
    Variable,
    Class,
    Interface,
    Module,
    Property,
]
    implements [
        Decoding { decoder: decode_items },
        Encoding { to_encoder: encode_items },
        Inspect,
        Eq,
    ]

encode_items : ItemKind -> Encoder fmt where fmt implements EncoderFormatting
encode_items = |@ItemKind(kind)|
    Encode.u32(
        when kind is
            Text -> 1
            Method -> 2
            Function -> 3
            Constructor -> 4
            Field -> 5
            Variable -> 6
            Class -> 7
            Interface -> 8
            Module -> 9
            Property -> 10,
    )

decode_items : Decoder ItemKind _
decode_items =
    Decode.custom(
        |bytes, fmt|
            # Helper function to wrap our [tag](https://www.roc-lang.org/tutorial#tags)
            ok = |tag| Ok(@ItemKind(tag))

            bytes
            |> Decode.from_bytes_partial(fmt)
            |> try_map_result(
                |num|
                    when num is
                        1 -> ok(Text)
                        2 -> ok(Method)
                        3 -> ok(Function)
                        4 -> ok(Constructor)
                        5 -> ok(Field)
                        6 -> ok(Variable)
                        7 -> ok(Class)
                        8 -> ok(Interface)
                        9 -> ok(Module)
                        10 -> ok(Property)
                        _ -> Err(TooShort),
            ),
    )

# Converts `DecodeResult U32` to `DecodeResult ItemKind` using a given function
try_map_result : DecodeResult U32, (U32 -> Result ItemKind DecodeError) -> DecodeResult ItemKind
try_map_result = |decoded, num_to_item_kind_fun|
    when decoded.result is
        Err(e) -> { result: Err(e), rest: decoded.rest }
        Ok(res) -> { result: num_to_item_kind_fun(res), rest: decoded.rest }

Demo



# make a list of ItemKind's
original_list : List ItemKind
original_list = [
    @ItemKind(Text),
    @ItemKind(Method),
    @ItemKind(Function),
    @ItemKind(Constructor),
    @ItemKind(Field),
    @ItemKind(Variable),
    @ItemKind(Class),
    @ItemKind(Interface),
    @ItemKind(Module),
    @ItemKind(Property),
]

# encode them into JSON bytes
encoded_bytes : List U8
encoded_bytes = Encode.to_bytes(original_list, Json.utf8)

# check that encoding is correct
expect
    expected_bytes : List U8
    expected_bytes = "[1,2,3,4,5,6,7,8,9,10]" |> Str.to_utf8

    encoded_bytes == expected_bytes

# decode back to a list of ItemKind's
decoded_list : List ItemKind
decoded_list = Decode.from_bytes(encoded_bytes, Json.utf8) |> Result.with_default([])
# don't use `Result.with_default([])` for professional applications; check https://www.roc-lang.org/examples/ErrorHandling/README.html

# check that decoding is correct
expect decoded_list == original_list

main! = |_args|
    # prints decoded items to stdout
    decoded_list
    |> List.map(Inspect.to_str)
    |> Str.join_with("\n")
    |> Stdout.line!

Output

Run this from the directory that has main.roc in it:

$ roc dev
(@ItemKind Text)
(@ItemKind Method)
(@ItemKind Function)
(@ItemKind Constructor)
(@ItemKind Field)
(@ItemKind Variable)
(@ItemKind Class)
(@ItemKind Interface)
(@ItemKind Module)
(@ItemKind Property)

You can also use roc test to run the tests.