Inside a decoder, create a tuple with Decode.Value and its decoded version

I have a file decoder to which I now want to add drag and drop support. I didn’t need the decoded file up until now, because in the update function I just sent the file through a port as Decode.Value.

So in order to support drag and drop, I changed this:

fileDecoder : Decode.Decoder Msg
fileDecoder =
    Decode.at [ "target", "files" ] (Decode.oneOrMore (\firstFile rest -> GotFile firstFile) Decode.value)

To this:

fileDecoder : Decode.Decoder Msg
fileDecoder =
    let
        keepFirstFile =
            \firstFile rest -> firstFile
    in
    Decode.oneOf
        [ Decode.at [ "target", "files" ] (Decode.oneOrMore keepFirstFile Decode.value) -- input[type=file]
        , Decode.at [ "dataTransfer", "files" ] (Decode.oneOrMore keepFirstFile Decode.value) -- drag and drop
        ]
        |> Decode.map GotFile

The problem with this is that I need to be sure the file is a PDF file. For <input> I just have the accept attribute, but drag and drop has no such thing.

Now, there’s a myriad of other ways I could solve my actual problem, for example grabbing the Decode.Value that comes out of the decoder and decoding it to a file in the update function. However, I would like to keep all the file processing inside the decoder, so I wanted to add something like this:

fileDecoder : Decode.Decoder Msg
fileDecoder =
    let
        keepFirstFile =
            \firstFile rest -> firstFile
    in
    Decode.oneOf
        [ Decode.at [ "target", "files" ] (Decode.oneOrMore keepFirstFile Decode.value) -- input[type=file]
        , Decode.at [ "dataTransfer", "files" ] (Decode.oneOrMore keepFirstFile Decode.value) -- drag and drop
        ]
        |> Decode.map2 Tuple.pair File.decoder
        |> Decode.andThen
            (\( file, fileValue ) ->
                if File.mime file == "application/pdf" then
                    Decode.succeed fileValue

                else
                    Decode.fail "Not PDF"
            )
        |> Decode.map GotFile

But this doesn’t work at all. Matter of fact, it appears as if the decoder is not even executed. I can stick |> Decode.map (Debug.log "decoder") into all possible places in the pipeline, but no log message is emmited. If I remove the map2 and andThen calls, I can see the logs.

So, what’s going on here? Is there a way to debug this?

1 Like

|> Decode.map2 Tuple.pair File.decoder does not do what you think it does.
It will try to decode the whole fileDecoder input value as a file, not the value of dataTransfers.files which is never used by File.decoder (it is just put in a tuple with it).

You may want something like the following instead:

fileDecoder : Decoder Msg
fileDecoder =
    let
        keepFirstFile =
            \firstFile rest -> firstFile
    in
    Decode.oneOf
        [ Decode.at [ "target", "files" ] (Decode.oneOrMore keepFirstFile Decode.value) -- input[type=file]
        , Decode.at [ "dataTransfer", "files" ] (Decode.oneOrMore keepFirstFile Decode.value) -- drag and drop
        ]
        |> Decode.andThen
            (\value ->
                case Decode.decodeValue (Decode.map File.mime File.decoder) value of
                    Ok "application/pdf" ->
                        Decode.succeed value

                    _ ->
                        Decode.fail "Not PDF"
            )
        |> Decode.map GotFile

Note that your keepFirstFile function is the same as Basics.always, your name is more explicit though in this context.

1 Like

Thanks, that should work! I thought that with the right function calls I could avoid calling decodeValue inside the decoder, but there’s nothing wrong with that really.

Right, I looked at the docs of map2 and saw that it’s used mostly for composing finished records and I thought that instead of a record I could just use a tuple. What I forgot about is exactly what you said. After all, even the docs for map2 have this example: map2 Point (field "x" float) (field "y" float), which is basically "grab this field from the entire JSON and then the other field from the entire JSON and pass them as arguments to Point".

I think my problem was that I thought I can somehow within the decoding pipeline get a value using one decoder and then “map” the value through another decoder – but that’s not possible without using andThen and “manually” calling decodeValue, since map only works on the value that’s within the context of the decoder. It was just a mismatch of what I thought the API allows me to do vs what it’s actually for.

1 Like

I see. Then you can avoid the decodeValue by decoding together the File and the value:

fileDecoder : Decoder Msg
fileDecoder =
    let
        firstFileAndValueDecoder =
            Decode.oneOrMore (\first rest -> first)
                (Decode.map2 Tuple.pair File.decoder Decode.value)
    in
    Decode.oneOf
        [ Decode.at [ "target", "files" ] firstFileAndValueDecoder -- input[type=file]
        , Decode.at [ "dataTransfer", "files" ] firstFileAndValueDecoder -- drag and drop
        ]
        |> Decode.andThen
            (\( file, value ) ->
                if File.mime file == "application/pdf" then
                    Decode.succeed value

                else
                    Decode.fail "Not PDF"
            )
        |> Decode.map GotFile
1 Like

Oh wow, this is just wonderful. :smiley: Thanks!

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.