Reputation: 1021
I have an elm decoder and a test for it. The test is passing, but when I use the decoder on the Application, I get a strange error that does not seem consistent with the data passed. I cannot figure out what is wrong. For disclosure, I am new to Elm and this is my first application with it, that is targeted to production.
type alias ProviderData =
{ uid : String
, displayName : Maybe String
, photoURL : Maybe String
, email : String
, phoneNumber : Maybe String
, providerId : String
}
type alias TokenManager =
{ apiKey : String
, refreshToken : String
, accessToken : String
, expirationTime : Time.Posix
}
type alias User =
{ uid : String
, displayName : Maybe String
, photoURL : Maybe String
, email : String
, emailVerified : Bool
, phoneNumber : Maybe String
, isAnonymous : Bool
, tenantId : Maybe String
, providerData : List ProviderData
, apiKey : String
, appName : String
, authDomain : String
, stsTokenManager : TokenManager
, redirectEventId : Maybe String
, lastLoginAt : Time.Posix
, createdAt : Time.Posix
}
type alias Model =
{ isUserSignedIn : Bool
, isWaitingForSignInLink : Bool
, user : Maybe User
}
unauthenticatedUser : Model
unauthenticatedUser =
{ isUserSignedIn = False
, isWaitingForSignInLink = False
, user = Nothing
}
decoder : Json.Value -> Model
decoder json =
case Json.decodeValue authDecoder (Debug.log ("Decoding " ++ Json.Encode.encode 4 json) json) of
Ok model ->
model
Err err ->
Debug.log (Debug.toString err) unauthenticatedUser
authDecoder : Json.Decoder Model
authDecoder =
Json.succeed Model
|> required "isUserSignedIn" Json.bool
|> required "isWaitingForSignInLink" Json.bool
|> optional "user" (Json.map Just decodeUser) Nothing
decodeUser : Json.Decoder User
decodeUser =
Json.succeed User
|> required "uid" Json.string
|> optional "displayName" (Json.map Just Json.string) Nothing
|> optional "photoURL" (Json.map Just Json.string) Nothing
|> required "email" Json.string
|> required "emailVerified" Json.bool
|> optional "phoneNumber" (Json.map Just Json.string) Nothing
|> required "isAnonymous" Json.bool
|> optional "tenantId" (Json.map Just Json.string) Nothing
|> required "providerData" (Json.list decodeProviderData)
|> required "apiKey" Json.string
|> required "appName" Json.string
|> required "authDomain" Json.string
|> required "stsTokenManager" decodeTokenManager
|> optional "redirectEventId" (Json.map Just Json.string) Nothing
|> required "lastLoginAt" timestampFromString
|> required "createdAt" timestampFromString
decodeProviderData : Json.Decoder ProviderData
decodeProviderData =
Json.succeed ProviderData
|> required "uid" Json.string
|> optional "displayName" (Json.map Just Json.string) Nothing
|> optional "photoURL" (Json.map Just Json.string) Nothing
|> required "email" Json.string
|> optional "phoneNumber" (Json.map Just Json.string) Nothing
|> required "providerId" Json.string
decodeTokenManager : Json.Decoder TokenManager
decodeTokenManager =
Json.succeed TokenManager
|> required "apiKey" Json.string
|> required "refreshToken" Json.string
|> required "accessToken" Json.string
|> required "expirationTime" timestampFromInt
timestampFromString : Json.Decoder Time.Posix
timestampFromString =
Json.andThen
(\str ->
case String.toInt str of
Just ts ->
Json.succeed (Time.millisToPosix ts)
Nothing ->
Json.fail (str ++ " is not a timetamp")
)
Json.string
timestampFromInt : Json.Decoder Time.Posix
timestampFromInt =
Json.andThen
(\ts ->
Json.succeed (Time.millisToPosix ts)
)
Json.int
unauthenticatedUser : Model
unauthenticatedUser =
{ isUserSignedIn = False
, isWaitingForSignInLink = False
, user = Nothing
}
Failure "Json.Decode.oneOf failed in the following 2 ways:
(1) Problem with the given value:
{
"uid": "",
"displayName": null,
"photoURL": null,
"email": "[email protected]",
"emailVerified": true,
"phoneNumber": null,
"isAnonymous": false,
"tenantId": null,
"providerData": [
{
"uid": "[email protected]",
"displayName": null,
"photoURL": null,
"email": "[email protected]",
"phoneNumber": null,
"providerId": "password"
}
],
"apiKey": "",
"appName": "[DEFAULT]",
"authDomain": "example.firebaseapp.com",
"stsTokenManager": {
"apiKey": "",
"refreshToken": "",
"accessToken": "",
"expirationTime": 1603998440000
},
"redirectEventId": null,
"lastLoginAt": "1603576515267",
"createdAt": "1603573117442",
"multiFactor": {
"enrolledFactors": []
}
}
Expecting an OBJECT with a field named `createdAt`
(2) Problem with the given value:
{
"uid": "",
"displayName": null,
"photoURL": null,
"email": "[email protected]",
"emailVerified": true,
"phoneNumber": null,
"isAnonymous": false,
"tenantId": null,
"providerData": [
{
"uid": "[email protected]",
"displayName": null,
"photoURL": null,
"email": "[email protected]",
"phoneNumber": null,
"providerId": "password"
}
],
"apiKey": "",
"appName": "[DEFAULT]",
"authDomain": "example.firebaseapp.com",
"stsTokenManager": {
"apiKey": "",
"refreshToken": "",
"accessToken": "",
"expirationTime": 1603998440000
},
"redirectEventId": null,
"lastLoginAt": "1603576515267",
"createdAt": "1603573117442",
"multiFactor": {
"enrolledFactors": []
}
}
Expecting null" <internals>: { isUserSignedIn = False, isWaitingForSignInLink = False, user = Nothing }
module Tests exposing (..)
import Expect
import Json.Decode as Json
import Modules.Firebase as Firebase
import Test exposing (..)
import Time
all : Test
all =
describe "Test Firebase"
[ test "Test auth JSON" <|
\_ ->
let
input =
"""
{
"isUserSignedIn": true,
"isWaitingForSignInLink": false,
"user": {
"uid": "xxxxxxxxxxxx",
"displayName": null,
"photoURL": null,
"email": "[email protected]",
"emailVerified": true,
"phoneNumber": null,
"isAnonymous": false,
"tenantId": null,
"providerData": [
{
"uid": "[email protected]",
"displayName": null,
"photoURL": null,
"email": "[email protected]",
"phoneNumber": null,
"providerId": "password"
}
],
"apiKey": "apikey.xxxxxxxxx",
"appName": "[DEFAULT]",
"authDomain": "example.com",
"stsTokenManager": {
"apiKey": "apikey.xxxxxxxxx",
"refreshToken": "refresh.xxxxxxxxxxxx",
"accessToken": "access.xxxxxxxxxxxx",
"expirationTime": 1603825391000
},
"redirectEventId": null,
"lastLoginAt": "1603576515267",
"createdAt": "1603573117442",
"multiFactor": {
"enrolledFactors": []
}
}
}
"""
decodedOutput =
case Json.decodeString Json.value input of
Ok value ->
Ok (Firebase.decoder value)
Err err ->
Err err
in
Expect.equal decodedOutput
(Ok firebaseModel)
]
firebaseModel : Firebase.Model
firebaseModel =
{ isUserSignedIn = True
, isWaitingForSignInLink = False
, user =
Just
{ uid = "xxxxxxxxxxxx"
, displayName = Nothing
, photoURL = Nothing
, email = "[email protected]"
, emailVerified = True
, phoneNumber = Nothing
, isAnonymous = False
, tenantId = Nothing
, providerData =
[ { uid = "[email protected]"
, displayName = Nothing
, photoURL = Nothing
, email = "[email protected]"
, phoneNumber = Nothing
, providerId = "password"
}
]
, apiKey = "apikey.xxxxxxxxx"
, appName = "[DEFAULT]"
, authDomain = "example.com"
, stsTokenManager =
{ apiKey = "apikey.xxxxxxxxx"
, refreshToken = "refresh.xxxxxxxxxxxx"
, accessToken = "access.xxxxxxxxxxxx"
, expirationTime = Time.millisToPosix 1603825391000
}
, redirectEventId = Nothing
, lastLoginAt = Time.millisToPosix 1603576515267
, createdAt = Time.millisToPosix 1603573117442
}
}
Edited at 2020-10-30
Trying to debug this issue I started to incrementally build the model, field by field, to understand what was wrong. The following model and decoder work fine:
...
type alias User =
{ uid : String
, displayName : Maybe String
, photoURL : Maybe String
, email : String
, emailVerified : Bool
, phoneNumber : Maybe String
, isAnonymous : Bool
, tenantId : Maybe String
, providerData : List ProviderData
}
...
decodeUser : Json.Decoder User
decodeUser =
Json.succeed User
|> required "uid" Json.string
|> optional "displayName" (Json.nullable Json.string) Nothing
|> optional "photoURL" (Json.nullable Json.string) Nothing
|> required "email" Json.string
|> required "emailVerified" Json.bool
|> optional "phoneNumber" (Json.nullable Json.string) Nothing
|> required "isAnonymous" Json.bool
|> optional "tenantId" (Json.nullable Json.string) Nothing
|> required "providerData" (Json.list decodeProviderData)
...
Note 1- All the other code remains the same.
Note 2- The input data remains the same.
Note 3 - I changed Json.map Just
to Json.nullable
by @5ndG suggestion. I also tried optional "field" (Json.nullable Json.string) Nothing
to required "field" (Json.nullable Json.string)
but the issue persist.
With this change, the model gets to the expected state, minus the fields I removed. By adding the next field, |> required "apiKey" Json.string
, it starts failing with the same issue.
Upvotes: 1
Views: 611
Reputation: 1021
I fixed this issue by making the first parameter a string and force my service to pass a JSON string instead of a Json.Decode.Value.
I change the decorator to:
decoder : String -> Model
decoder json =
case Json.decodeString Json.value json of
Ok value ->
decodeValue value
Err _ ->
unauthenticatedUser
decodeValue : Json.Value -> Model
decodeValue json =
case Json.decodeValue authDecoder json of
Ok model ->
model
Err _ ->
Debug.log unauthenticatedUser
Not sure why the can't I use the Json.Decode.Value from the service, but this solves the problem.
Upvotes: 2
Reputation: 1382
Edit: this answer is wrong, so ignore it.
I can reproduce your error with this input in the test:
{
"isUserSignedIn": true,
"isWaitingForSignInLink": false
}
That is, miss out the user
field entirely.
From reading the docs, I think this is a bug in the optional
function.
As described here, you can fix this by replacing the user
decoder with:
|> optional "user" (Json.nullable decodeUser) Nothing
Upvotes: 0