254 lines
7.5 KiB
Elm
254 lines
7.5 KiB
Elm
module Todo exposing (Full, Preview, Todo, author, body, favorite, favoriteButton, fetch, fromPreview, fullDecoder, mapAuthor, metadata, previewDecoder, unfavorite, unfavoriteButton, uuid)
|
|
|
|
{-| The interface to the Todo data structure.
|
|
This includes:
|
|
|
|
- The Todo type itself
|
|
- Ways to make HTTP requests to retrieve and modify Todos
|
|
- Ways to access information about an Todo
|
|
- Converting between various types
|
|
|
|
-}
|
|
|
|
import Api exposing (Cred)
|
|
import Api.Endpoint as Endpoint
|
|
import Author exposing (Author)
|
|
import Element exposing (..)
|
|
import Http
|
|
import Iso8601
|
|
import Json.Decode as Decode exposing (Decoder)
|
|
import Json.Encode as Encode
|
|
import Markdown
|
|
import Time
|
|
import Todo.Body as Body exposing (Body)
|
|
import Todo.Tag as Tag exposing (Tag)
|
|
import Todo.UUID exposing (UUID)
|
|
import Username as Username exposing (Username)
|
|
import Viewer exposing (Viewer)
|
|
|
|
|
|
|
|
-- TYPES
|
|
|
|
|
|
{-| An Todo, optionally with an Todo body.
|
|
To see the difference between { extraInfo : a } and { extraInfo : Maybe Body },
|
|
consider the difference between the "view individual Todo" page (which
|
|
renders one Todo, including its body) and the "Todo feed" -
|
|
which displays multiple Todos, but without bodies.
|
|
This definition for `Todo` means we can write:
|
|
viewTodo : Todo Full -> Html msg
|
|
viewFeed : List (Todo Preview) -> Html msg
|
|
This indicates that `viewTodo` requires an Todo _with a `body` present_,
|
|
wereas `viewFeed` accepts Todos with no bodies. (We could also have written
|
|
it as `List (Todo a)` to specify that feeds can accept either Todos that
|
|
have `body` present or not. Either work, given that feeds do not attempt to
|
|
read the `body` field from Todos.)
|
|
This is an important distinction, because in Request.Todo, the `feed`
|
|
function produces `List (Todo Preview)` because the API does not return bodies.
|
|
Those Todos are useful to the feed, but not to the individual Todo view.
|
|
-}
|
|
type Todo a
|
|
= Todo Internals a
|
|
|
|
|
|
{-| Metadata about the Todo - its title, description, and so on.
|
|
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:
|
|
Todo.title : Todo a -> String
|
|
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
|
|
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 Todo 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!
|
|
)
|
|
-}
|
|
type alias Metadata =
|
|
{ description : String
|
|
, title : String
|
|
, tags : List String
|
|
, createdAt : Time.Posix
|
|
, favorited : Bool
|
|
, favoritesCount : Int
|
|
}
|
|
|
|
|
|
type alias Internals =
|
|
{ uuid : UUID
|
|
, author : Author
|
|
, metadata : Metadata
|
|
}
|
|
|
|
|
|
type Preview
|
|
= Preview
|
|
|
|
|
|
type Full
|
|
= Full Body
|
|
|
|
|
|
|
|
-- INFO
|
|
|
|
|
|
author : Todo a -> Author
|
|
author (Todo internals _) =
|
|
internals.author
|
|
|
|
|
|
metadata : Todo a -> Metadata
|
|
metadata (Todo internals _) =
|
|
internals.metadata
|
|
|
|
|
|
uuid : Todo a -> UUID
|
|
uuid (Todo internals _) =
|
|
internals.uuid
|
|
|
|
|
|
body : Todo Full -> Body
|
|
body (Todo _ (Full extraInfo)) =
|
|
extraInfo
|
|
|
|
|
|
|
|
-- TRANSFORM
|
|
|
|
|
|
{-| This is the only way you can transform an existing Todo:
|
|
you can change its author (e.g. to follow or unfollow them).
|
|
All other Todo data necessarily comes from the server!
|
|
We can tell this for sure by looking at the types of the exposed functions
|
|
in this module.
|
|
-}
|
|
mapAuthor : (Author -> Author) -> Todo a -> Todo a
|
|
mapAuthor transform (Todo info extras) =
|
|
Todo { info | author = transform info.author } extras
|
|
|
|
|
|
fromPreview : Body -> Todo Preview -> Todo Full
|
|
fromPreview newBody (Todo info Preview) =
|
|
Todo info (Full newBody)
|
|
|
|
|
|
|
|
-- SERIALIZATION
|
|
|
|
|
|
previewDecoder : Maybe Cred -> Decoder (Todo Preview)
|
|
previewDecoder maybeCred =
|
|
Decode.succeed Todo
|
|
|> custom (internalsDecoder maybeCred)
|
|
|> hardcoded Preview
|
|
|
|
|
|
fullDecoder : Maybe Cred -> Decoder (Todo Full)
|
|
fullDecoder maybeCred =
|
|
Decode.succeed Todo
|
|
|> custom (internalsDecoder maybeCred)
|
|
|> required "body" (Decode.map Full Body.decoder)
|
|
|
|
|
|
internalsDecoder : Maybe Cred -> Decoder Internals
|
|
internalsDecoder maybeCred =
|
|
Decode.succeed Internals
|
|
|> required "uuid" UUID.decoder
|
|
|> required "author" (Author.decoder maybeCred)
|
|
|> custom metadataDecoder
|
|
|
|
|
|
metadataDecoder : Decoder Metadata
|
|
metadataDecoder =
|
|
Decode.succeed Metadata
|
|
|> required "description" (Decode.map (Maybe.withDefault "") (Decode.nullable Decode.string))
|
|
|> required "title" Decode.string
|
|
|> required "tagList" (Decode.list Decode.string)
|
|
|> required "createdAt" Iso8601.decoder
|
|
|> required "favorited" Decode.bool
|
|
|> required "favoritesCount" Decode.int
|
|
|
|
|
|
|
|
-- SINGLE
|
|
|
|
|
|
fetch : Maybe Cred -> UUID -> Http.Request (Todo Full)
|
|
fetch maybeCred uuid =
|
|
Decode.field "Todo" (fullDecoder maybeCred)
|
|
|> Api.get (Endpoint.Todo uuid) maybeCred
|
|
|
|
|
|
|
|
-- FAVORITE
|
|
|
|
|
|
favorite : UUID -> Cred -> Http.Request (Todo Preview)
|
|
favorite uuid cred =
|
|
Api.post (Endpoint.favorite uuid) (Just cred) Http.emptyBody (faveDecoder cred)
|
|
|
|
|
|
unfavorite : UUID -> Cred -> Http.Request (Todo Preview)
|
|
unfavorite uuid cred =
|
|
Api.delete (Endpoint.favorite uuid) cred Http.emptyBody (faveDecoder cred)
|
|
|
|
|
|
faveDecoder : Cred -> Decoder (Todo Preview)
|
|
faveDecoder cred =
|
|
Decode.field "Todo" (previewDecoder (Just cred))
|
|
|
|
|
|
{-| This is a "build your own element" API.
|
|
You pass it some configuration, followed by a `List (Attribute msg)` and a
|
|
`List (Html msg)`, just like any standard Html element.
|
|
-}
|
|
favoriteButton :
|
|
Cred
|
|
-> msg
|
|
-> List (Attribute msg)
|
|
-> List (Element msg)
|
|
-> Element msg
|
|
favoriteButton _ msg attrs kids =
|
|
toggleFavoriteButton "btn btn-sm btn-outline-primary" msg attrs kids
|
|
|
|
|
|
unfavoriteButton :
|
|
Cred
|
|
-> msg
|
|
-> List (Attribute msg)
|
|
-> List (Element msg)
|
|
-> Element msg
|
|
unfavoriteButton _ msg attrs kids =
|
|
toggleFavoriteButton "btn btn-sm btn-primary" msg attrs kids
|
|
|
|
|
|
toggleFavoriteButton :
|
|
String
|
|
-> msg
|
|
-> List (Attribute msg)
|
|
-> List (Element msg)
|
|
-> Element msg
|
|
toggleFavoriteButton classStr msg attrs kids =
|
|
Html.button
|
|
(class classStr :: onClickStopPropagation msg :: attrs)
|
|
(i [ class "ion-heart" ] [] :: kids)
|
|
|
|
|
|
onClickStopPropagation : msg -> Attribute msg
|
|
onClickStopPropagation msg =
|
|
stopPropagationOn "click"
|
|
(Decode.succeed ( msg, True ))
|