Swap part1 and part2

This commit is contained in:
Richard Feldman
2018-08-13 05:04:35 -04:00
parent 353623f108
commit 493b5681d4
16 changed files with 156 additions and 156 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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 )

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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 )

View File

@@ -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