Swap part1 and part2
This commit is contained in:
@@ -10,4 +10,4 @@ Then open [http://localhost:3000](http://localhost:3000) in your browser.
|
|||||||
|
|
||||||
## Exercise
|
## Exercise
|
||||||
|
|
||||||
Open `src/Article.elm` in your editor and resolve the TODOs there.
|
Open `src/Viewer/Cred.elm` in your editor and resolve the TODOs there.
|
||||||
|
|||||||
@@ -56,22 +56,64 @@ import Viewer.Cred as Cred exposing (Cred)
|
|||||||
-- TYPES
|
-- TYPES
|
||||||
|
|
||||||
|
|
||||||
|
{-| An article, optionally with an article body.
|
||||||
|
|
||||||
|
To see the difference between { extraInfo : a } and { extraInfo : Maybe Body },
|
||||||
|
consider the difference between the "view individual article" page (which
|
||||||
|
renders one article, including its body) and the "article feed" -
|
||||||
|
which displays multiple articles, but without bodies.
|
||||||
|
|
||||||
|
This definition for `Article` means we can write:
|
||||||
|
|
||||||
|
viewArticle : Article Full -> Html msg
|
||||||
|
viewFeed : List (Article Preview) -> Html msg
|
||||||
|
|
||||||
|
This indicates that `viewArticle` requires an article _with a `body` present_,
|
||||||
|
wereas `viewFeed` accepts articles with no bodies. (We could also have written
|
||||||
|
it as `List (Article a)` to specify that feeds can accept either articles that
|
||||||
|
have `body` present or not. Either work, given that feeds do not attempt to
|
||||||
|
read the `body` field from articles.)
|
||||||
|
|
||||||
|
This is an important distinction, because in Request.Article, the `feed`
|
||||||
|
function produces `List (Article Preview)` because the API does not return bodies.
|
||||||
|
Those articles are useful to the feed, but not to the individual article view.
|
||||||
|
|
||||||
|
-}
|
||||||
type Article a
|
type Article a
|
||||||
= Article Internals a
|
= Article Internals a
|
||||||
|
|
||||||
|
|
||||||
|
{-| Metadata about the article - its title, description, and so on.
|
||||||
|
|
||||||
-- 💡 HINT: We can use these `Preview` and/or `Full` types to store information...
|
Importantly, this module's public API exposes a way to read this metadata, but
|
||||||
|
not to alter it. This is read-only information!
|
||||||
|
|
||||||
|
If we find ourselves using any particular piece of metadata often,
|
||||||
|
for example `title`, we could expose a convenience function like this:
|
||||||
|
|
||||||
type Preview
|
Article.title : Article a -> String
|
||||||
= Preview
|
|
||||||
|
|
||||||
|
If you like, it's totally reasonable to expose a function like that for every one
|
||||||
|
of these fields!
|
||||||
|
|
||||||
type Full
|
(Okay, to be completely honest, exposing one function per field is how I prefer
|
||||||
= Full
|
to do it, and that's how I originally wrote this module. However, I'm aware that
|
||||||
|
this code base has become a common reference point for beginners, and I think it
|
||||||
|
is _extremely important_ that slapping some "getters and setters" on a record
|
||||||
|
does not become a habit for anyone who is getting started with Elm. The whole
|
||||||
|
point of making the Article type opaque is to create guarantees through
|
||||||
|
_selectively choosing boundaries_ around it. If you aren't selective about
|
||||||
|
where those boundaries are, and instead expose a "getter and setter" for every
|
||||||
|
field in the record, the result is an API with no more guarantees than if you'd
|
||||||
|
exposed the entire record directly! It is so important to me that beginners not
|
||||||
|
fall into the terrible "getters and setters" trap that I've exposed this
|
||||||
|
Metadata record instead of exposing a single function for each of its fields,
|
||||||
|
as I did originally. This record is not a bad way to do it, by any means,
|
||||||
|
but if this seems at odds with <https://youtu.be/x1FU3e0sT1I> - now you know why!
|
||||||
|
See commit c2640ae3abd60262cdaafe6adee3f41d84cd85c3 for how it looked before.
|
||||||
|
)
|
||||||
|
|
||||||
|
-}
|
||||||
type alias Metadata =
|
type alias Metadata =
|
||||||
{ description : String
|
{ description : String
|
||||||
, title : String
|
, title : String
|
||||||
@@ -89,6 +131,14 @@ type alias Internals =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type Preview
|
||||||
|
= Preview
|
||||||
|
|
||||||
|
|
||||||
|
type Full
|
||||||
|
= Full Body
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- INFO
|
-- INFO
|
||||||
|
|
||||||
@@ -109,8 +159,8 @@ slug (Article internals _) =
|
|||||||
|
|
||||||
|
|
||||||
body : Article Full -> Body
|
body : Article Full -> Body
|
||||||
body _ =
|
body (Article _ (Full extraInfo)) =
|
||||||
"👉 TODO make this return the article's body"
|
extraInfo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -131,8 +181,8 @@ mapAuthor transform (Article info extras) =
|
|||||||
|
|
||||||
|
|
||||||
fromPreview : Body -> Article Preview -> Article Full
|
fromPreview : Body -> Article Preview -> Article Full
|
||||||
fromPreview _ _ =
|
fromPreview newBody (Article info Preview) =
|
||||||
"👉 TODO convert from an Article Preview to an Article Full"
|
Article info (Full newBody)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -150,16 +200,7 @@ fullDecoder : Maybe Cred -> Decoder (Article Full)
|
|||||||
fullDecoder maybeCred =
|
fullDecoder maybeCred =
|
||||||
Decode.succeed Article
|
Decode.succeed Article
|
||||||
|> custom (internalsDecoder maybeCred)
|
|> custom (internalsDecoder maybeCred)
|
||||||
|> required "body" "👉 TODO use `Body.decoder` (which is a `Decoder Body`) to decode the body into this Article Full"
|
|> required "body" (Decode.map Full Body.decoder)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{- If you're unfamiliar with Decode Pipeline, here's how ☝️ would look without it:
|
|
||||||
|
|
||||||
Decode.map2 Article
|
|
||||||
(internalsDecoder maybeCred)
|
|
||||||
(Decode.field "body" "use `Body.decoder` (which is a `Decoder Body`) to decode the body into this Article Full")
|
|
||||||
-}
|
|
||||||
|
|
||||||
|
|
||||||
internalsDecoder : Maybe Cred -> Decoder Internals
|
internalsDecoder : Maybe Cred -> Decoder Internals
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ username : Author -> Username
|
|||||||
username author =
|
username author =
|
||||||
case author of
|
case author of
|
||||||
IsViewer cred _ ->
|
IsViewer cred _ ->
|
||||||
Cred.username cred
|
cred.username
|
||||||
|
|
||||||
IsFollowing (FollowedAuthor val _) ->
|
IsFollowing (FollowedAuthor val _) ->
|
||||||
val
|
val
|
||||||
@@ -220,7 +220,7 @@ decodeFromPair maybeCred ( prof, uname ) =
|
|||||||
Decode.succeed (IsNotFollowing (UnfollowedAuthor uname prof))
|
Decode.succeed (IsNotFollowing (UnfollowedAuthor uname prof))
|
||||||
|
|
||||||
Just cred ->
|
Just cred ->
|
||||||
if uname == Cred.username cred then
|
if uname == cred.username then
|
||||||
Decode.succeed (IsViewer cred prof)
|
Decode.succeed (IsViewer cred prof)
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ viewMenu page maybeViewer =
|
|||||||
cred =
|
cred =
|
||||||
Viewer.cred viewer
|
Viewer.cred viewer
|
||||||
|
|
||||||
username =
|
{ username } =
|
||||||
Cred.username cred
|
cred
|
||||||
|
|
||||||
avatar =
|
avatar =
|
||||||
Profile.avatar (Viewer.profile viewer)
|
Profile.avatar (Viewer.profile viewer)
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ view model =
|
|||||||
titleForOther (Author.username author)
|
titleForOther (Author.username author)
|
||||||
|
|
||||||
Loading username ->
|
Loading username ->
|
||||||
if Just username == Maybe.map Cred.username (Session.cred model.session) then
|
if Just username == Maybe.map .username (Session.cred model.session) then
|
||||||
myProfileTitle
|
myProfileTitle
|
||||||
|
|
||||||
else
|
else
|
||||||
@@ -96,7 +96,7 @@ view model =
|
|||||||
|
|
||||||
Failed username ->
|
Failed username ->
|
||||||
-- We can't follow if it hasn't finished loading yet
|
-- We can't follow if it hasn't finished loading yet
|
||||||
if Just username == Maybe.map Cred.username (Session.cred model.session) then
|
if Just username == Maybe.map .username (Session.cred model.session) then
|
||||||
myProfileTitle
|
myProfileTitle
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ init session =
|
|||||||
{ avatar = Avatar.toMaybeString (Profile.avatar profile)
|
{ avatar = Avatar.toMaybeString (Profile.avatar profile)
|
||||||
, email = Email.toString (Viewer.email viewer)
|
, email = Email.toString (Viewer.email viewer)
|
||||||
, bio = Maybe.withDefault "" (Profile.bio profile)
|
, bio = Maybe.withDefault "" (Profile.bio profile)
|
||||||
, username = Username.toString (Cred.username cred)
|
, username = Username.toString cred.username
|
||||||
, password = Nothing
|
, password = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ encode : Viewer -> Value
|
|||||||
encode (Viewer info) =
|
encode (Viewer info) =
|
||||||
Encode.object
|
Encode.object
|
||||||
[ ( "email", Email.encode info.email )
|
[ ( "email", Email.encode info.email )
|
||||||
, ( "username", Username.encode (Cred.username info.cred) )
|
, ( "username", Username.encode info.cred.username )
|
||||||
, ( "bio", Maybe.withDefault Encode.null (Maybe.map Encode.string (Profile.bio info.profile)) )
|
, ( "bio", Maybe.withDefault Encode.null (Maybe.map Encode.string (Profile.bio info.profile)) )
|
||||||
, ( "image", Avatar.encode (Profile.avatar info.profile) )
|
, ( "image", Avatar.encode (Profile.avatar info.profile) )
|
||||||
, ( "token", Cred.encodeToken info.cred )
|
, ( "token", Cred.encodeToken info.cred )
|
||||||
|
|||||||
@@ -1,20 +1,4 @@
|
|||||||
module Viewer.Cred exposing (Cred, addHeader, addHeaderIfAvailable, decoder, encodeToken, username)
|
module Viewer.Cred exposing (Cred, addHeader, addHeaderIfAvailable, decoder, encodeToken)
|
||||||
|
|
||||||
{-| The authentication credentials for the Viewer (that is, the currently logged-in user.)
|
|
||||||
|
|
||||||
This includes:
|
|
||||||
|
|
||||||
- The cred's Username
|
|
||||||
- The cred's authentication token
|
|
||||||
|
|
||||||
By design, there is no way to access the token directly as a String.
|
|
||||||
It can be encoded for persistence, and it can be added to a header
|
|
||||||
to a HttpBuilder for a request, but that's it.
|
|
||||||
|
|
||||||
This token should never be rendered to the end user, and with this API, it
|
|
||||||
can't be!
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
import HttpBuilder exposing (RequestBuilder, withHeader)
|
import HttpBuilder exposing (RequestBuilder, withHeader)
|
||||||
import Json.Decode as Decode exposing (Decoder)
|
import Json.Decode as Decode exposing (Decoder)
|
||||||
@@ -23,30 +7,21 @@ import Json.Encode as Encode exposing (Value)
|
|||||||
import Username exposing (Username)
|
import Username exposing (Username)
|
||||||
|
|
||||||
|
|
||||||
{-| The authentication token for the currently logged-in user.
|
|
||||||
|
|
||||||
The token records the username associated with this token, which you can ask it for.
|
|
||||||
|
|
||||||
By design, there is no way to access the token directly as a String. You can encode it for persistence, and you can add it to a header to a HttpBuilder for a request, but that's it.
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- TYPES
|
-- TYPES
|
||||||
|
|
||||||
|
|
||||||
type Cred
|
type alias Cred =
|
||||||
= Cred Username String
|
-- 👉 TODO make Cred an opaque type, then fix the resulting compiler errors.
|
||||||
|
-- Afterwards, it should no longer be possible for any other module to access
|
||||||
|
-- this `token` vale directly!
|
||||||
|
--
|
||||||
-- INFO
|
-- 💡 HINT: Other modules still depend on being able to access the
|
||||||
|
-- `username` value. Expand this module's API to expose a new way for them
|
||||||
|
-- to access the `username` without also giving them access to `token`.
|
||||||
username : Cred -> Username
|
{ username : Username
|
||||||
username (Cred val _) =
|
, token : String
|
||||||
val
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -65,14 +40,14 @@ decoder =
|
|||||||
|
|
||||||
|
|
||||||
encodeToken : Cred -> Value
|
encodeToken : Cred -> Value
|
||||||
encodeToken (Cred _ str) =
|
encodeToken cred =
|
||||||
Encode.string str
|
Encode.string cred.token
|
||||||
|
|
||||||
|
|
||||||
addHeader : Cred -> RequestBuilder a -> RequestBuilder a
|
addHeader : Cred -> RequestBuilder a -> RequestBuilder a
|
||||||
addHeader (Cred _ str) builder =
|
addHeader cred builder =
|
||||||
builder
|
builder
|
||||||
|> withHeader "authorization" ("Token " ++ str)
|
|> withHeader "authorization" ("Token " ++ cred.token)
|
||||||
|
|
||||||
|
|
||||||
addHeaderIfAvailable : Maybe Cred -> RequestBuilder a -> RequestBuilder a
|
addHeaderIfAvailable : Maybe Cred -> RequestBuilder a -> RequestBuilder a
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ Then open [http://localhost:3000](http://localhost:3000) in your browser.
|
|||||||
|
|
||||||
## Exercise
|
## Exercise
|
||||||
|
|
||||||
Open `src/Viewer/Cred.elm` in your editor and resolve the TODOs there.
|
Open `src/Article.elm` in your editor and resolve the TODOs there.
|
||||||
|
|||||||
@@ -56,64 +56,22 @@ import Viewer.Cred as Cred exposing (Cred)
|
|||||||
-- TYPES
|
-- TYPES
|
||||||
|
|
||||||
|
|
||||||
{-| An article, optionally with an article body.
|
|
||||||
|
|
||||||
To see the difference between { extraInfo : a } and { extraInfo : Maybe Body },
|
|
||||||
consider the difference between the "view individual article" page (which
|
|
||||||
renders one article, including its body) and the "article feed" -
|
|
||||||
which displays multiple articles, but without bodies.
|
|
||||||
|
|
||||||
This definition for `Article` means we can write:
|
|
||||||
|
|
||||||
viewArticle : Article Full -> Html msg
|
|
||||||
viewFeed : List (Article Preview) -> Html msg
|
|
||||||
|
|
||||||
This indicates that `viewArticle` requires an article _with a `body` present_,
|
|
||||||
wereas `viewFeed` accepts articles with no bodies. (We could also have written
|
|
||||||
it as `List (Article a)` to specify that feeds can accept either articles that
|
|
||||||
have `body` present or not. Either work, given that feeds do not attempt to
|
|
||||||
read the `body` field from articles.)
|
|
||||||
|
|
||||||
This is an important distinction, because in Request.Article, the `feed`
|
|
||||||
function produces `List (Article Preview)` because the API does not return bodies.
|
|
||||||
Those articles are useful to the feed, but not to the individual article view.
|
|
||||||
|
|
||||||
-}
|
|
||||||
type Article a
|
type Article a
|
||||||
= Article Internals a
|
= Article Internals a
|
||||||
|
|
||||||
|
|
||||||
{-| Metadata about the article - its title, description, and so on.
|
|
||||||
|
|
||||||
Importantly, this module's public API exposes a way to read this metadata, but
|
-- 💡 HINT: We can use these `Preview` and/or `Full` types to store information...
|
||||||
not to alter it. This is read-only information!
|
|
||||||
|
|
||||||
If we find ourselves using any particular piece of metadata often,
|
|
||||||
for example `title`, we could expose a convenience function like this:
|
|
||||||
|
|
||||||
Article.title : Article a -> String
|
type Preview
|
||||||
|
= Preview
|
||||||
|
|
||||||
If you like, it's totally reasonable to expose a function like that for every one
|
|
||||||
of these fields!
|
|
||||||
|
|
||||||
(Okay, to be completely honest, exposing one function per field is how I prefer
|
type Full
|
||||||
to do it, and that's how I originally wrote this module. However, I'm aware that
|
= Full
|
||||||
this code base has become a common reference point for beginners, and I think it
|
|
||||||
is _extremely important_ that slapping some "getters and setters" on a record
|
|
||||||
does not become a habit for anyone who is getting started with Elm. The whole
|
|
||||||
point of making the Article type opaque is to create guarantees through
|
|
||||||
_selectively choosing boundaries_ around it. If you aren't selective about
|
|
||||||
where those boundaries are, and instead expose a "getter and setter" for every
|
|
||||||
field in the record, the result is an API with no more guarantees than if you'd
|
|
||||||
exposed the entire record directly! It is so important to me that beginners not
|
|
||||||
fall into the terrible "getters and setters" trap that I've exposed this
|
|
||||||
Metadata record instead of exposing a single function for each of its fields,
|
|
||||||
as I did originally. This record is not a bad way to do it, by any means,
|
|
||||||
but if this seems at odds with <https://youtu.be/x1FU3e0sT1I> - now you know why!
|
|
||||||
See commit c2640ae3abd60262cdaafe6adee3f41d84cd85c3 for how it looked before.
|
|
||||||
)
|
|
||||||
|
|
||||||
-}
|
|
||||||
type alias Metadata =
|
type alias Metadata =
|
||||||
{ description : String
|
{ description : String
|
||||||
, title : String
|
, title : String
|
||||||
@@ -131,14 +89,6 @@ type alias Internals =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Preview
|
|
||||||
= Preview
|
|
||||||
|
|
||||||
|
|
||||||
type Full
|
|
||||||
= Full Body
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- INFO
|
-- INFO
|
||||||
|
|
||||||
@@ -159,8 +109,8 @@ slug (Article internals _) =
|
|||||||
|
|
||||||
|
|
||||||
body : Article Full -> Body
|
body : Article Full -> Body
|
||||||
body (Article _ (Full extraInfo)) =
|
body _ =
|
||||||
extraInfo
|
"👉 TODO make this return the article's body"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -181,8 +131,8 @@ mapAuthor transform (Article info extras) =
|
|||||||
|
|
||||||
|
|
||||||
fromPreview : Body -> Article Preview -> Article Full
|
fromPreview : Body -> Article Preview -> Article Full
|
||||||
fromPreview newBody (Article info Preview) =
|
fromPreview _ _ =
|
||||||
Article info (Full newBody)
|
"👉 TODO convert from an Article Preview to an Article Full"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -200,7 +150,16 @@ fullDecoder : Maybe Cred -> Decoder (Article Full)
|
|||||||
fullDecoder maybeCred =
|
fullDecoder maybeCred =
|
||||||
Decode.succeed Article
|
Decode.succeed Article
|
||||||
|> custom (internalsDecoder maybeCred)
|
|> custom (internalsDecoder maybeCred)
|
||||||
|> required "body" (Decode.map Full Body.decoder)
|
|> required "body" "👉 TODO use `Body.decoder` (which is a `Decoder Body`) to decode the body into this Article Full"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{- If you're unfamiliar with Decode Pipeline, here's how ☝️ would look without it:
|
||||||
|
|
||||||
|
Decode.map2 Article
|
||||||
|
(internalsDecoder maybeCred)
|
||||||
|
(Decode.field "body" "use `Body.decoder` (which is a `Decoder Body`) to decode the body into this Article Full")
|
||||||
|
-}
|
||||||
|
|
||||||
|
|
||||||
internalsDecoder : Maybe Cred -> Decoder Internals
|
internalsDecoder : Maybe Cred -> Decoder Internals
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ username : Author -> Username
|
|||||||
username author =
|
username author =
|
||||||
case author of
|
case author of
|
||||||
IsViewer cred _ ->
|
IsViewer cred _ ->
|
||||||
cred.username
|
Cred.username cred
|
||||||
|
|
||||||
IsFollowing (FollowedAuthor val _) ->
|
IsFollowing (FollowedAuthor val _) ->
|
||||||
val
|
val
|
||||||
@@ -220,7 +220,7 @@ decodeFromPair maybeCred ( prof, uname ) =
|
|||||||
Decode.succeed (IsNotFollowing (UnfollowedAuthor uname prof))
|
Decode.succeed (IsNotFollowing (UnfollowedAuthor uname prof))
|
||||||
|
|
||||||
Just cred ->
|
Just cred ->
|
||||||
if uname == cred.username then
|
if uname == Cred.username cred then
|
||||||
Decode.succeed (IsViewer cred prof)
|
Decode.succeed (IsViewer cred prof)
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ viewMenu page maybeViewer =
|
|||||||
cred =
|
cred =
|
||||||
Viewer.cred viewer
|
Viewer.cred viewer
|
||||||
|
|
||||||
{ username } =
|
username =
|
||||||
cred
|
Cred.username cred
|
||||||
|
|
||||||
avatar =
|
avatar =
|
||||||
Profile.avatar (Viewer.profile viewer)
|
Profile.avatar (Viewer.profile viewer)
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ view model =
|
|||||||
titleForOther (Author.username author)
|
titleForOther (Author.username author)
|
||||||
|
|
||||||
Loading username ->
|
Loading username ->
|
||||||
if Just username == Maybe.map .username (Session.cred model.session) then
|
if Just username == Maybe.map Cred.username (Session.cred model.session) then
|
||||||
myProfileTitle
|
myProfileTitle
|
||||||
|
|
||||||
else
|
else
|
||||||
@@ -96,7 +96,7 @@ view model =
|
|||||||
|
|
||||||
Failed username ->
|
Failed username ->
|
||||||
-- We can't follow if it hasn't finished loading yet
|
-- We can't follow if it hasn't finished loading yet
|
||||||
if Just username == Maybe.map .username (Session.cred model.session) then
|
if Just username == Maybe.map Cred.username (Session.cred model.session) then
|
||||||
myProfileTitle
|
myProfileTitle
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ init session =
|
|||||||
{ avatar = Avatar.toMaybeString (Profile.avatar profile)
|
{ avatar = Avatar.toMaybeString (Profile.avatar profile)
|
||||||
, email = Email.toString (Viewer.email viewer)
|
, email = Email.toString (Viewer.email viewer)
|
||||||
, bio = Maybe.withDefault "" (Profile.bio profile)
|
, bio = Maybe.withDefault "" (Profile.bio profile)
|
||||||
, username = Username.toString cred.username
|
, username = Username.toString (Cred.username cred)
|
||||||
, password = Nothing
|
, password = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ encode : Viewer -> Value
|
|||||||
encode (Viewer info) =
|
encode (Viewer info) =
|
||||||
Encode.object
|
Encode.object
|
||||||
[ ( "email", Email.encode info.email )
|
[ ( "email", Email.encode info.email )
|
||||||
, ( "username", Username.encode info.cred.username )
|
, ( "username", Username.encode (Cred.username info.cred) )
|
||||||
, ( "bio", Maybe.withDefault Encode.null (Maybe.map Encode.string (Profile.bio info.profile)) )
|
, ( "bio", Maybe.withDefault Encode.null (Maybe.map Encode.string (Profile.bio info.profile)) )
|
||||||
, ( "image", Avatar.encode (Profile.avatar info.profile) )
|
, ( "image", Avatar.encode (Profile.avatar info.profile) )
|
||||||
, ( "token", Cred.encodeToken info.cred )
|
, ( "token", Cred.encodeToken info.cred )
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
module Viewer.Cred exposing (Cred, addHeader, addHeaderIfAvailable, decoder, encodeToken)
|
module Viewer.Cred exposing (Cred, addHeader, addHeaderIfAvailable, decoder, encodeToken, username)
|
||||||
|
|
||||||
|
{-| The authentication credentials for the Viewer (that is, the currently logged-in user.)
|
||||||
|
|
||||||
|
This includes:
|
||||||
|
|
||||||
|
- The cred's Username
|
||||||
|
- The cred's authentication token
|
||||||
|
|
||||||
|
By design, there is no way to access the token directly as a String.
|
||||||
|
It can be encoded for persistence, and it can be added to a header
|
||||||
|
to a HttpBuilder for a request, but that's it.
|
||||||
|
|
||||||
|
This token should never be rendered to the end user, and with this API, it
|
||||||
|
can't be!
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
import HttpBuilder exposing (RequestBuilder, withHeader)
|
import HttpBuilder exposing (RequestBuilder, withHeader)
|
||||||
import Json.Decode as Decode exposing (Decoder)
|
import Json.Decode as Decode exposing (Decoder)
|
||||||
@@ -7,21 +23,30 @@ import Json.Encode as Encode exposing (Value)
|
|||||||
import Username exposing (Username)
|
import Username exposing (Username)
|
||||||
|
|
||||||
|
|
||||||
|
{-| The authentication token for the currently logged-in user.
|
||||||
|
|
||||||
|
The token records the username associated with this token, which you can ask it for.
|
||||||
|
|
||||||
|
By design, there is no way to access the token directly as a String. You can encode it for persistence, and you can add it to a header to a HttpBuilder for a request, but that's it.
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- TYPES
|
-- TYPES
|
||||||
|
|
||||||
|
|
||||||
type alias Cred =
|
type Cred
|
||||||
-- 👉 TODO make Cred an opaque type, then fix the resulting compiler errors.
|
= Cred Username String
|
||||||
-- Afterwards, it should no longer be possible for any other module to access
|
|
||||||
-- this `token` vale directly!
|
|
||||||
--
|
|
||||||
-- 💡 HINT: Other modules still depend on being able to access the
|
-- INFO
|
||||||
-- `username` value. Expand this module's API to expose a new way for them
|
|
||||||
-- to access the `username` without also giving them access to `token`.
|
|
||||||
{ username : Username
|
username : Cred -> Username
|
||||||
, token : String
|
username (Cred val _) =
|
||||||
}
|
val
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -40,14 +65,14 @@ decoder =
|
|||||||
|
|
||||||
|
|
||||||
encodeToken : Cred -> Value
|
encodeToken : Cred -> Value
|
||||||
encodeToken cred =
|
encodeToken (Cred _ str) =
|
||||||
Encode.string cred.token
|
Encode.string str
|
||||||
|
|
||||||
|
|
||||||
addHeader : Cred -> RequestBuilder a -> RequestBuilder a
|
addHeader : Cred -> RequestBuilder a -> RequestBuilder a
|
||||||
addHeader cred builder =
|
addHeader (Cred _ str) builder =
|
||||||
builder
|
builder
|
||||||
|> withHeader "authorization" ("Token " ++ cred.token)
|
|> withHeader "authorization" ("Token " ++ str)
|
||||||
|
|
||||||
|
|
||||||
addHeaderIfAvailable : Maybe Cred -> RequestBuilder a -> RequestBuilder a
|
addHeaderIfAvailable : Maybe Cred -> RequestBuilder a -> RequestBuilder a
|
||||||
|
|||||||
Reference in New Issue
Block a user