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