Is it possible to conditionally decode certain fields using elm-decode-pipeline - elm

I would like to decode an API response in which one of the fields value (category) would determine how to decode another field (configuration) using different sub-decoders.
I was able to accomplish such thing using Json.Decode.mapn functions and the andThen function, but I was wondering if there is any way to do such thing using elm-decode-pipeline as it has a nicer API and I will run out of mapn functions eventually.
A minimmum and somewhat trivial example would be like this:
type alias Machine =
{ name : String
, specs : MachineSpecs
}
type MachineSpecs
= ElectricMachine ElectricSpecs
| MechanicalMachine MechanicalSpecs
| UnknownMachine
type alias ElectricSpecs =
{ voltage : Int
}
type alias MechanicalSpecs =
{ gears : Int
}
And some valid JSON responses would have these shapes:
{
"name": "Foo electric machine",
"category": "electric",
"configuration": {
"voltage": 12
}
}
{
"name": "Bar mechanical machine",
"category": "mechanical",
"configuration": {
"gears": 5
}
}
{
"name": "Some machine of unknown category",
"category": "foo"
}
I tried a similar approach to the one I was using with the mapn functions, but it doesn't work.
decoder : Decoder Machine
decoder =
decode Machine
|> required "name" string
|> required "category" (string |> andThen catDec)
catDec : String -> Decoder MachineSpecs
catDec cat =
case cat of
"electric" ->
map ElectricMachine electricDecoder
"mechanical" ->
map MechanicalMachine mechanicalDecoder
_ ->
succeed UnknownMachine
electricDecoder : Decoder ElectricSpecs
electricDecoder =
decode ElectricSpecs
|> requiredAt [ "configuration", "voltage" ] int
mechanicalDecoder : Decoder MechanicalSpecs
mechanicalDecoder =
decode MechanicalSpecs
|> requiredAt [ "configuration", "gears" ] int
In fact, I haven't seen any example on the web or docs using both Json.Decode.Pipeline and andThen at the same time, so I'm not sure if it's even possible.
I have set up an online example of this issue showing how it fails to decode the conditional part: https://runelm.io/c/3ut

As an alternative, you could place your andThen bindings before the pipeline (ellie example):
decoder : Decoder Machine
decoder =
field "category" string
|> andThen catDec
|> andThen
(\cat ->
decode Machine
|> required "name" string
|> hardcoded cat
)
If you are running out of mapN numbers, consider switching to andMap (or the infix version |:) in the elm-community/json-extra package.

Chad Gilbert's answer just works (thanks!) and it lead me to read the Json.Decode.Pipeline source code to understand a bit more about how the piping was implemented and I've found an alternative solution that is a bit more concise, so I thought about sharing it here:
decoder : Decoder Machine
decoder =
decode Machine
|> required "name" string
|> custom (field "category" string |> andThen catDec)

Related

How do you iterate a List (Maybe a)

I have the following graphQL result:
[Just { details = Just "Engine failure at 33 seconds and loss of
vehicle", launch_year = Just "2006", links = Just { article_link =
Just
"https://www.space.com/2196-spacex-inaugural-falcon-1-rocket-lost-launch.html"
}, mission_name = Just "FalconSat" }]
Based on the following types:
type alias Launch =
{ mission_name : Maybe String
, details : Maybe String
, launch_year : Maybe String
, links : Maybe LaunchLinks
}
type alias Launches =
Maybe (List (Maybe Launch))
type alias LaunchLinks =
{ article_link : Maybe String
}
I want to List.map through and display the results in unordered list. I started with this:
renderLaunch : Launches -> Html Msg
renderLaunch launches =
div [] <|
case launches of
Nothing ->
[ text "Nothing here" ]
Just launch ->
launch
|> List.map (\x -> x)
|> ul []
But I keep getting this error:
This function cannot handle the argument sent through the (|>) pipe:
141| launch 142| |> List.map (\x
-> x) 143| |> ul []
^^^^^ The argument is:
List (Maybe Launch)
But (|>) is piping it a function that expects:
List (Html msg)
The problem is that the Just launch case needs to result in a List (Html msg) but the code results in a different type being returned.
When you are using List.map (\x -> x), it is essentially a no-op. You are iterating over a List (Maybe Launch) and returning the same thing. I'd recommend creating another function that takes a Maybe Launch value and use that as your mapping function. For example:
displayLaunch : Maybe Launch -> Html Msg
displayLaunch launch =
case launch of
Nothing -> text "No launch"
Just l -> text (Debug.toString l)
Now you can plug that into your mapping function:
Just launch ->
launch
|> List.map displayLaunch
|> ul []
But, whoops! Now you get a new error indicating:
The 2nd branch is:
Html Msg
But all the previous branches result in:
List (Html msg)
The problem here is that we are now returning a ul from the Just launch branch and we need to return a list of html. You can use List.singleton to create a list with just one item:
Just launch ->
launch
|> List.map displayLaunch
|> ul []
|> List.singleton

Elixir - Manipulating a 2 dimensional list

Hope everybody is having a beautiful 2019 even though we're just a day in.
I am currently working on a small Phoenix app where I'm manipulating PDF files (in the context of this question I'm splitting them) and then uploading them to S3. Later on I have to delete the temporary files created by pdftk ( a pdf tool ) I use to split them up and also show the s3 links in the response body since this is an API request.
The way I have structured this is as following:
Inside my Split module where the core business logic is:
filenames = []
s3_links = []
Enum.map(pages, fn(item) ->
split_filename = item
|> split(filename)
link = split_filename
|> FileHelper.result_file_bytes()
|> ManageS3.upload()
|> FileHelper.save_file(work_group_id, pass)
[filenames ++ split_filename, s3_links ++ link]
end)
|> transform()
{filenames, s3_links}
The important things are split_filename and link
This is what I'm getting when I call an IO.inspect in the transform() method:
[
["87cdcd73-5b27-4757-a472-78aaf6cc6864.pdf",
"Some_S3_LINK00"],
["0ab460ca-5019-4864-b0ff-343966c7d72a.pdf",
"Some_S3_LINK01"]
]
The structuring is [[filename, s3_link], [filename, s3_link]] whereas the desired outcome would be that of [ [list of all filenames], [list of s3 links].
If anybody can lend a hand I would be super grateful. Thanks in advance!
Sidenotes:
Assigning filenames = []; s3_links = [] in the very beginning makes zero sense. Enum.map already maps the input. What you need is probably Enum.reduce/3.
Don’t use the pipe |> operator when the pipe consists of the only call, it is considered an anti-pattern by Elixir core team.
Always start pipes with a term.
Solution:
Reduce the input into the result using Enum.reduce/3 directly to what you need.
pages
|> Enum.reduce([[], []], fn item, [files, links] ->
split_filename = split(item, filename)
link =
split_filename
|> FileHelper.result_file_bytes()
|> ManageS3.upload()
|> FileHelper.save_file(work_group_id, pass)
[[split_filename | files], [link | links]]
end)
|> Enum.map(&Enum.reverse/1)
|> IO.inspect(label: "Before transform")
|> transform()
You did not provide the input to test it, but I believe it should work.
Instead of working on lists of lists, you may want to consider using tuples with lists. Something like the following should work for you.
List.foldl(pages, {[], []}, fn(item, {filenames, links}) ->
filename = split(item, filename)
link =
file_name
|> FileHelper.result_file_bytes()
|> ManagerS3.upload()
|> FileHelper.save_file(work_group_id, pass)
{[filename | filenames], [link | links]}
end)
This will return a value that looks like
{
["87cdcd73-5b27-4757-a472-78aaf6cc6864.pdf",
"0ab460ca-5019-4864-b0ff-343966c7d72a.pdf"],
["Some_S3_LINK00",
"Some_S3_LINK01"]
}
Though, depending on how you are using these values, maybe a list of tuples would be more appropriate. Something like
Enum.map(pages, fn(item) ->
filename = split(item, filename)
link =
filename
|> FileHelper.result_file_bytes()
|> ManageS3.upload()
|> FileHelper.save_file(work_group_id, pass)
{filename, link}
end)
would return
[
{"87cdcd73-5b27-4757-a472-78aaf6cc6864.pdf", "Some_S3_LINK00"},
{"0ab460ca-5019-4864-b0ff-343966c7d72a.pdf", "Some_S3_LINK01"}
]

Using Elm Json.Decode to move vales from parent to child record

I'm writing an elm json decoder, and want to move a value from a 'parent' record into a 'child'.
In this example I want to move the beta key/value to live in the Bar type.
My incoming JSON
{ "alpha": 1,
"beta: 2,
"bar": {
"gamma": 3
}
}
My types
type alias Foo =
{ alpha : Int
, bar : Bar
}
type alias Bar =
{ beta : Int
, gamma : Int
}
How can I do that in a decoder? I feel like I want to pass the decoder for beta down to the fooDecode. But this clearly isn't right...
fooDecode =
decode Foo
|> required "alpha" Json.Decode.int
|> required "bar" barDecode (Json.Decode.at "beta" Json.Decode.int)
barDecode betaDecoder =
decode Bar
|> betaDecoder
|> required "gamma" Json.Decode.int
Note: My actual use case has a list of children, but hopefully I'll be able to work that out with a pointer on this. I'm using Decode.Pipeline as it's a large JSON object
You can use Json.Decode.andThen here to parse "beta" and then pass it on to barDecode and Json.Decode.Pipeline.custom to make it work with the pipeline:
fooDecode : Decoder Foo
fooDecode =
decode Foo
|> required "alpha" Json.Decode.int
|> custom
(Json.Decode.field "beta" Json.Decode.int
|> Json.Decode.andThen (\beta -> Json.Decode.field "bar" (barDecode beta))
)
barDecode : Int -> Decoder Bar
barDecode beta =
decode Bar
|> hardcoded beta
|> required "gamma" Json.Decode.int
With this change,
main : Html msg
main =
Html.text <| toString <| decodeString fooDecode <| """
{ "alpha": 1,
"beta": 2,
"bar": {
"gamma": 3
}
}
"""
prints:
Ok { alpha = 1, bar = { beta = 2, gamma = 3 } }

Recursion related exception: Unable to get property 'tag' of undefined or null reference

I receive the following error after performing an HTTP post:
Unable to get property 'tag' of undefined or null reference
I believe the error occurs when executing the following decoder function:
sourceDecoder : Decoder JsonSource
sourceDecoder =
Decode.map5 JsonSource
...
(field "Links" providerLinksDecoder)
Decoder Dependencies:
providerLinksDecoder : Decoder JsonProviderLinks
providerLinksDecoder =
Decode.map JsonLinkFields
(field "Links" <| Decode.list (Decode.lazy (\_ -> linkDecoder)))
|> Decode.map JsonProviderLinks
linkDecoder : Decoder JsonLink
linkDecoder =
Decode.map6 JsonLink
(field "Profile" profileDecoder)
...
profileDecoder : Decoder JsonProfile
profileDecoder =
Decode.map7 JsonProfile
...
(field "Sources" <| Decode.list (Decode.lazy (\_ -> sourceDecoder)))
Appendix:
type JsonProviderLinks
= JsonProviderLinks JsonLinkFields
type alias JsonLinkFields =
{ links : List JsonLink
}
The source code can be found on here.
Note: I attempted to research this error and came across this page.
As a result, I attempted to use the Decode.lazy function. However, my attempt failed.
There's a lot of decoders that rely on other decoders in your examples. You've changed some of them to use Decode.lazy, but not all, and that error you've received will happen when there's some out of control recursion.
You don't need a list to be able to use lazy. Try - as a first step towards debugging, at least - to change all decoders that reference other decoders to use Decode.lazy. For example:
sourceDecoder : Decoder JsonSource
sourceDecoder =
Decode.map5 JsonSource
...
(field "Links" (Decode.lazy (\_ -> providerLinksDecoder)))

F# webpage Authentication

I've been trying to authenticate the bitstamp api however, I keep on getting the following error:
"{\"error\": \"Missing key, signature and nonce parameters\"}"
The code I have written to do this is below:
let nounce = System.DateTime.Today.Ticks
let hexdigest (bytes : byte[]) =
let sb = System.Text.StringBuilder()
bytes |> Array.iter (fun b -> b.ToString("X2") |> sb.Append |> ignore)
string sb
let signature =
use hmac = new HMACSHA256(System.Text.Encoding.ASCII.GetBytes(stampsecret))
hmac.ComputeHash(mes)
|> hexdigest
I am calling the website with the following:
let ordersBTCbuy()=
Http.Request("https://www.bitstamp.net/api/buy", meth="Post", query=["key", stampkey; "signature", signature.ToLower(); "nonce", string(nounce); "amount", "1"; "price", string(convertB)])
A reference to the API can be found here: https://www.bitstamp.net/api/
Update I've changed the web address to:
let ordersBTCbuy()=
Http.Request("https://www.bitstamp.net/api/buy/", meth="Post", query=["key", stampkey; "signature", signature.ToLower(); "nonce", string(nounce); "amount", "1"; "price", string(convertB)])
My new issue is now the signature my representation is 64 characters long and upper case however I still seem to have an error.
When creating a POST request. Your key-value pairs need to go in the body.
To do that using FSharp data do the following:
let postBody = FormValues([ "key", stampkey; "signature", signature.ToLower(); "nonce", string(nounce); "amount", "1"; "price", string(convertB)])
let ordersBTCbuy()=
Http.Request("https://www.bitstamp.net/api/buy", httpMethod="Post", body=postBody)