packages feed

advent-of-code-api 0.1.2.3 → 0.2.0.0

raw patch · 6 files changed

+578/−252 lines, 6 filesdep +aesondep +bytestringdep +http-api-datadep −curldep −uri-encodePVP ok

version bump matches the API change (PVP)

Dependencies added: aeson, bytestring, http-api-data, http-client, http-client-tls, http-media, servant, servant-client, servant-client-core, stm

Dependencies removed: curl, uri-encode

API changes (from Hackage documentation)

- Advent: AoCCurlError :: CurlCode -> String -> AoCError
- Advent: [_aCurlOpts] :: AoCOpts -> [CurlOption]
- Advent: instance GHC.Classes.Eq Advent.Part
- Advent: instance GHC.Classes.Eq Advent.SubmitRes
- Advent: instance GHC.Classes.Ord Advent.Part
- Advent: instance GHC.Classes.Ord Advent.SubmitRes
- Advent: instance GHC.Enum.Bounded Advent.Part
- Advent: instance GHC.Enum.Enum Advent.Part
- Advent: instance GHC.Generics.Generic Advent.Part
- Advent: instance GHC.Generics.Generic Advent.SubmitRes
- Advent: instance GHC.Read.Read Advent.Part
- Advent: instance GHC.Read.Read Advent.SubmitRes
- Advent: instance GHC.Show.Show Advent.Part
- Advent: instance GHC.Show.Show Advent.SubmitRes
- Advent: parseSubmitRes :: Text -> SubmitRes
- Advent: processHTML :: String -> [Text]
+ Advent: AoCClientError :: ClientError -> AoCError
+ Advent: LB :: Integer -> Integer -> Map Integer LeaderboardMember -> Leaderboard
+ Advent: LBM :: Integer -> Maybe Text -> Integer -> Integer -> Maybe UTCTime -> Int -> Map Day (Map Part UTCTime) -> LeaderboardMember
+ Advent: [AoCLeaderboard] :: Integer -> AoC Leaderboard
+ Advent: [lbEvent] :: Leaderboard -> Integer
+ Advent: [lbMembers] :: Leaderboard -> Map Integer LeaderboardMember
+ Advent: [lbOwnerId] :: Leaderboard -> Integer
+ Advent: [lbmCompletion] :: LeaderboardMember -> Map Day (Map Part UTCTime)
+ Advent: [lbmGlobalScore] :: LeaderboardMember -> Integer
+ Advent: [lbmId] :: LeaderboardMember -> Integer
+ Advent: [lbmLastStarTS] :: LeaderboardMember -> Maybe UTCTime
+ Advent: [lbmLocalScore] :: LeaderboardMember -> Integer
+ Advent: [lbmName] :: LeaderboardMember -> Maybe Text
+ Advent: [lbmStars] :: LeaderboardMember -> Int
+ Advent: aocBase :: BaseUrl
+ Advent: aocReq :: Integer -> AoC a -> ClientM a
+ Advent: data Leaderboard
+ Advent: data LeaderboardMember
+ Advent.API: Day :: Finite 25 -> Day
+ Advent.API: LB :: Integer -> Integer -> Map Integer LeaderboardMember -> Leaderboard
+ Advent.API: LBM :: Integer -> Maybe Text -> Integer -> Integer -> Maybe UTCTime -> Int -> Map Day (Map Part UTCTime) -> LeaderboardMember
+ Advent.API: Part1 :: Part
+ Advent.API: Part2 :: Part
+ Advent.API: PublicCode :: Integer -> PublicCode
+ Advent.API: SubCorrect :: Maybe Integer -> SubmitRes
+ Advent.API: SubIncorrect :: Int -> Maybe String -> SubmitRes
+ Advent.API: SubInvalid :: SubmitRes
+ Advent.API: SubUnknown :: String -> SubmitRes
+ Advent.API: SubWait :: Int -> SubmitRes
+ Advent.API: SubmitInfo :: Part -> String -> SubmitInfo
+ Advent.API: [dayFinite] :: Day -> Finite 25
+ Advent.API: [getPublicCode] :: PublicCode -> Integer
+ Advent.API: [lbEvent] :: Leaderboard -> Integer
+ Advent.API: [lbMembers] :: Leaderboard -> Map Integer LeaderboardMember
+ Advent.API: [lbOwnerId] :: Leaderboard -> Integer
+ Advent.API: [lbmCompletion] :: LeaderboardMember -> Map Day (Map Part UTCTime)
+ Advent.API: [lbmGlobalScore] :: LeaderboardMember -> Integer
+ Advent.API: [lbmId] :: LeaderboardMember -> Integer
+ Advent.API: [lbmLastStarTS] :: LeaderboardMember -> Maybe UTCTime
+ Advent.API: [lbmLocalScore] :: LeaderboardMember -> Integer
+ Advent.API: [lbmName] :: LeaderboardMember -> Maybe Text
+ Advent.API: [lbmStars] :: LeaderboardMember -> Int
+ Advent.API: [siAnswer] :: SubmitInfo -> String
+ Advent.API: [siLevel] :: SubmitInfo -> Part
+ Advent.API: adventAPI :: Proxy AdventAPI
+ Advent.API: adventAPIClient :: Integer -> (Day -> ClientM (Map Part Text) :<|> (ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)))) :<|> (PublicCode -> ClientM Leaderboard)
+ Advent.API: adventAPIPuzzleClient :: Integer -> Day -> ClientM (Map Part Text) :<|> (ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)))
+ Advent.API: class FromArticles a
+ Advent.API: data Articles
+ Advent.API: data Leaderboard
+ Advent.API: data LeaderboardMember
+ Advent.API: data Part
+ Advent.API: data RawText
+ Advent.API: data SubmitInfo
+ Advent.API: data SubmitRes
+ Advent.API: dayInt :: Day -> Integer
+ Advent.API: fromArticles :: FromArticles a => [Text] -> a
+ Advent.API: instance (Advent.API.FromArticles a, Advent.API.FromArticles b) => Advent.API.FromArticles (a Servant.API.Alternative.:<|> b)
+ Advent.API: instance (GHC.Classes.Ord a, GHC.Enum.Enum a, GHC.Enum.Bounded a) => Advent.API.FromArticles (Data.Map.Internal.Map a Data.Text.Internal.Text)
+ Advent.API: instance Advent.API.FromArticles Advent.API.SubmitRes
+ Advent.API: instance Advent.API.FromArticles Data.Text.Internal.Text
+ Advent.API: instance Advent.API.FromArticles [Data.Text.Internal.Text]
+ Advent.API: instance Advent.API.FromArticles a => Servant.API.ContentTypes.MimeUnrender Advent.API.Articles a
+ Advent.API: instance Data.Aeson.Types.FromJSON.FromJSON Advent.API.Day
+ Advent.API: instance Data.Aeson.Types.FromJSON.FromJSON Advent.API.Leaderboard
+ Advent.API: instance Data.Aeson.Types.FromJSON.FromJSON Advent.API.LeaderboardMember
+ Advent.API: instance Data.Aeson.Types.FromJSON.FromJSON Advent.API.Part
+ Advent.API: instance Data.Aeson.Types.FromJSON.FromJSONKey Advent.API.Day
+ Advent.API: instance Data.Aeson.Types.FromJSON.FromJSONKey Advent.API.Part
+ Advent.API: instance GHC.Classes.Eq Advent.API.Day
+ Advent.API: instance GHC.Classes.Eq Advent.API.Leaderboard
+ Advent.API: instance GHC.Classes.Eq Advent.API.LeaderboardMember
+ Advent.API: instance GHC.Classes.Eq Advent.API.Part
+ Advent.API: instance GHC.Classes.Eq Advent.API.PublicCode
+ Advent.API: instance GHC.Classes.Eq Advent.API.SubmitInfo
+ Advent.API: instance GHC.Classes.Eq Advent.API.SubmitRes
+ Advent.API: instance GHC.Classes.Ord Advent.API.Day
+ Advent.API: instance GHC.Classes.Ord Advent.API.Leaderboard
+ Advent.API: instance GHC.Classes.Ord Advent.API.LeaderboardMember
+ Advent.API: instance GHC.Classes.Ord Advent.API.Part
+ Advent.API: instance GHC.Classes.Ord Advent.API.PublicCode
+ Advent.API: instance GHC.Classes.Ord Advent.API.SubmitInfo
+ Advent.API: instance GHC.Classes.Ord Advent.API.SubmitRes
+ Advent.API: instance GHC.Enum.Bounded Advent.API.Day
+ Advent.API: instance GHC.Enum.Bounded Advent.API.Part
+ Advent.API: instance GHC.Enum.Enum Advent.API.Day
+ Advent.API: instance GHC.Enum.Enum Advent.API.Part
+ Advent.API: instance GHC.Generics.Generic Advent.API.Day
+ Advent.API: instance GHC.Generics.Generic Advent.API.Leaderboard
+ Advent.API: instance GHC.Generics.Generic Advent.API.LeaderboardMember
+ Advent.API: instance GHC.Generics.Generic Advent.API.Part
+ Advent.API: instance GHC.Generics.Generic Advent.API.PublicCode
+ Advent.API: instance GHC.Generics.Generic Advent.API.SubmitInfo
+ Advent.API: instance GHC.Generics.Generic Advent.API.SubmitRes
+ Advent.API: instance GHC.Read.Read Advent.API.Part
+ Advent.API: instance GHC.Read.Read Advent.API.PublicCode
+ Advent.API: instance GHC.Read.Read Advent.API.SubmitInfo
+ Advent.API: instance GHC.Read.Read Advent.API.SubmitRes
+ Advent.API: instance GHC.Show.Show Advent.API.Day
+ Advent.API: instance GHC.Show.Show Advent.API.Leaderboard
+ Advent.API: instance GHC.Show.Show Advent.API.LeaderboardMember
+ Advent.API: instance GHC.Show.Show Advent.API.Part
+ Advent.API: instance GHC.Show.Show Advent.API.PublicCode
+ Advent.API: instance GHC.Show.Show Advent.API.SubmitInfo
+ Advent.API: instance GHC.Show.Show Advent.API.SubmitRes
+ Advent.API: instance Servant.API.ContentTypes.Accept Advent.API.Articles
+ Advent.API: instance Servant.API.ContentTypes.Accept Advent.API.RawText
+ Advent.API: instance Servant.API.ContentTypes.MimeUnrender Advent.API.RawText Data.Text.Internal.Text
+ Advent.API: instance Web.Internal.FormUrlEncoded.ToForm Advent.API.SubmitInfo
+ Advent.API: instance Web.Internal.HttpApiData.ToHttpApiData Advent.API.Day
+ Advent.API: instance Web.Internal.HttpApiData.ToHttpApiData Advent.API.Part
+ Advent.API: instance Web.Internal.HttpApiData.ToHttpApiData Advent.API.PublicCode
+ Advent.API: mkDay :: Integer -> Maybe Day
+ Advent.API: mkDay_ :: Integer -> Day
+ Advent.API: newtype Day
+ Advent.API: newtype PublicCode
+ Advent.API: parseSubmitRes :: Text -> SubmitRes
+ Advent.API: partChar :: Part -> Char
+ Advent.API: partInt :: Part -> Int
+ Advent.API: processHTML :: Text -> [Text]
+ Advent.API: showSubmitRes :: SubmitRes -> String
+ Advent.API: type AdventAPI = Capture "year" Integer :> ("day" :> Capture "day" Day :> (Get '[Articles] (Map Part Text) :<|> "input" :> Get '[RawText] Text :<|> "answer" :> ReqBody '[FormUrlEncoded] SubmitInfo :> Post '[Articles] (Text :<|> SubmitRes)) :<|> "leaderboard" :> "private" :> "view" :> Capture "code" PublicCode :> Get '[JSON] Leaderboard)
- Advent: AoCOpts :: String -> Integer -> Maybe FilePath -> Bool -> Int -> [CurlOption] -> AoCOpts
+ Advent: AoCOpts :: String -> Integer -> Maybe FilePath -> Bool -> Int -> AoCOpts
- Advent: [AoCInput] :: Finite 25 -> AoC Text
+ Advent: [AoCInput] :: Day -> AoC Text
- Advent: [AoCPrompt] :: Finite 25 -> AoC (Map Part Text)
+ Advent: [AoCPrompt] :: Day -> AoC (Map Part Text)
- Advent: [AoCSubmit] :: Finite 25 -> Part -> String -> AoC (Text, SubmitRes)
+ Advent: [AoCSubmit] :: Day -> Part -> String -> AoC (Text, SubmitRes)
- Advent: aocDay :: AoC a -> Finite 25
+ Advent: aocDay :: AoC a -> Maybe Day
- Advent: challengeReleaseTime :: Integer -> Finite 25 -> UTCTime
+ Advent: challengeReleaseTime :: Integer -> Day -> UTCTime
- Advent: challengeReleased :: Integer -> Finite 25 -> IO Bool
+ Advent: challengeReleased :: Integer -> Day -> IO Bool
- Advent: dayInt :: Finite 25 -> Integer
+ Advent: dayInt :: Day -> Integer
- Advent: mkDay :: Integer -> Maybe (Finite 25)
+ Advent: mkDay :: Integer -> Maybe Day
- Advent: mkDay_ :: Integer -> Finite 25
+ Advent: mkDay_ :: Integer -> Day
- Advent: timeToRelease :: Integer -> Finite 25 -> IO NominalDiffTime
+ Advent: timeToRelease :: Integer -> Day -> IO NominalDiffTime

Files

CHANGELOG.md view
@@ -1,6 +1,19 @@ Changelog ========= +Version 0.2.0.0+---------------++*November 3, 2019*++<https://github.com/mstksg/advent-of-code-api/releases/tag/v0.2.0.0>++*   Switch from libcurl to servant, which allows for shedding of external+    dependencies.+*   Support leaderboard API with data type.+*   Expose raw servant API and client functions, for those who want to build+    documentation or a mock server or low-level client.+ Version 0.1.2.X --------------- 
README.md view
@@ -5,7 +5,7 @@ [![Build Status](https://travis-ci.org/mstksg/advent-of-code-api.svg?branch=master)](https://travis-ci.org/mstksg/advent-of-code-api)  Haskell bindings for Advent of Code REST API.  Caches and throttles requests-automatically.+automatically, and parses responses into meaningful data types.  [advent-of-code-api]: https://hackage.haskell.org/package/advent-of-code-api @@ -29,10 +29,8 @@ request every three seconds, with ability to adjust up to as fast as a hard-coded limit of one request per second. -Note that leaderboard API is not yet supported.--Requires *libcurl*, with future plans to move to a "pure Haskell"-networking backend.+The neatly exported bindings (handling cookies/authentication, cacheing,+throttling) are in *Advent*.  Session Keys ------------@@ -43,4 +41,12 @@ The session key can be found by logging in on a web client and checking the cookies.  You can usually check these with in-browser developer tools.++Servant API+-----------++A Servant API (for integrating with *servant* for features like mock servers,+documentation and low-level client methods) is also exported in *Advent.API*.+The Servant API also parses into meaningful types, but lacks management of+cookies/auth, cacheing, and throttling.  Please use especially responsibly. 
advent-of-code-api.cabal view
@@ -1,18 +1,17 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.0.+-- This file has been generated from package.yaml by hpack version 0.31.2. -- -- see: https://github.com/sol/hpack ----- hash: 981dd4bf9967973046dfd1d35527c986c3f5609336fd03d8d7faf5944537342a+-- hash: 9e35c93b909c0ae1a536d52a0eb26455a393d111063a82c5cc5577f4fab16c13  name:           advent-of-code-api-version:        0.1.2.3-synopsis:       Advent of Code REST API bindings-description:    Haskell bindings for Advent of Code REST API.  Please use responsibly!-                See README.md or "Advent" module for an introduction and tutorial.-                .-                Requires libcurl.+version:        0.2.0.0+synopsis:       Advent of Code REST API bindings and servant API+description:    Haskell bindings for Advent of Code REST API and a servant API.  Please use+                responsibly! See README.md or "Advent" module for an introduction and+                tutorial. category:       Web homepage:       https://github.com/mstksg/advent-of-code-api#readme bug-reports:    https://github.com/mstksg/advent-of-code-api/issues@@ -21,7 +20,7 @@ copyright:      (c) Justin Le 2018 license:        BSD3 license-file:   LICENSE-tested-with:    GHC >= 8.0 && < 8.8+tested-with:    GHC >= 8.0 build-type:     Simple extra-source-files:     README.md@@ -43,27 +42,35 @@ library   exposed-modules:       Advent+      Advent.API   other-modules:-      Advent.Cache       Advent.Throttle-      Paths_advent_of_code_api+      Advent.Cache   hs-source-dirs:       src   ghc-options: -Wall -Wcompat -Werror=incomplete-patterns   build-depends:-      attoparsec+      aeson+    , attoparsec     , base >=4.9 && <5+    , bytestring     , containers-    , curl     , deepseq     , directory     , filepath     , finite-typelits+    , http-api-data+    , http-client+    , http-client-tls+    , http-media     , mtl+    , servant+    , servant-client+    , servant-client-core+    , stm     , tagsoup     , text     , time-    , uri-encode   default-language: Haskell2010  test-suite advent-of-code-api-test
src/Advent.hs view
@@ -13,7 +13,7 @@  -- | -- Module      : Advent--- Copyright   : (c) Justin Le 2018+-- Copyright   : (c) Justin Le 2019 -- License     : BSD3 -- -- Maintainer  : justin@jle.im@@ -42,8 +42,6 @@ -- Please use responsibly.  All actions are by default rate limited to one -- per three seconds, but this can be adjusted to a hard-limited cap of one -- per second.------ Note that leaderboard API is not yet supported.  module Advent (   -- * API@@ -51,6 +49,7 @@   , Part(..)   , AoCOpts(..)   , SubmitRes(..), showSubmitRes+  , Leaderboard(..), LeaderboardMember(..)   , runAoC   , defaultAoCOpts   , AoCError(..)@@ -67,42 +66,44 @@   -- ** Throttler   , setAoCThrottleLimit, getAoCThrottleLimit   -- * Internal-  , parseSubmitRes-  , processHTML+  , aocReq+  , aocBase   ) where +import           Advent.API import           Advent.Cache import           Advent.Throttle-import           Control.Applicative+import           Control.Concurrent.STM import           Control.Exception import           Control.Monad.Except-import           Data.Char-import           Data.Finite-import           Data.Foldable import           Data.Kind-import           Data.Map               (Map)+import           Data.Map                (Map) import           Data.Maybe-import           Data.Set               (Set)-import           Data.Text              (Text)-import           Data.Time+import           Data.Set                (Set)+import           Data.Text               (Text)+import           Data.Time hiding        (Day) import           Data.Typeable-import           GHC.Generics           (Generic)-import           Network.Curl+import           GHC.Generics            (Generic)+import           Network.HTTP.Client+import           Network.HTTP.Client.TLS+import           Servant.API+import           Servant.Client import           System.Directory import           System.FilePath-import           Text.HTML.TagSoup.Tree (TagTree(..)) import           Text.Printf-import qualified Data.Attoparsec.Text   as P-import qualified Data.Map               as M-import qualified Data.Set               as S-import qualified Data.Text              as T-import qualified Network.URI.Encode     as URI-import qualified System.IO.Unsafe       as Unsafe-import qualified Text.HTML.TagSoup      as H-import qualified Text.HTML.TagSoup.Tree as H+import qualified Data.Map                as M+import qualified Data.Set                as S+import qualified Data.Text               as T+import qualified Data.Text.Encoding      as T+import qualified System.IO.Unsafe        as Unsafe -#if !MIN_VERSION_base(4,11,0)+#if MIN_VERSION_base(4,11,0)+import           Data.Functor+#else import           Data.Semigroup ((<>))++(<&>) :: Functor f => f a -> (a -> b) -> f b+(<&>) = flip fmap #endif  initialThrottleLimit :: Int@@ -120,54 +121,22 @@ getAoCThrottleLimit :: IO Int getAoCThrottleLimit = getLimit aocThrottler --- | The result of a submission.-data SubmitRes-    -- | Correct submission, including global rank (if reported, which-    -- usually happens if rank is under 1000)-    = SubCorrect (Maybe Integer)-    -- | Incorrect submission.  Contains the number of /seconds/ you must-    -- wait before trying again.  The 'Maybe' contains possible hints given-    -- by the server (usually "too low" or "too high").-    | SubIncorrect Int (Maybe String)-    -- | Submission was rejected because an incorrect submission was-    -- recently submitted.  Contains the number of /seconds/ you must wait-    -- before trying again.-    | SubWait Int-    -- | Submission was rejected because it was sent to an invalid question-    -- or part.  Usually happens if you submit to a part you have already-    -- answered or have not yet unlocked.-    | SubInvalid-    -- | Could not parse server response.  Contains parse error.-    | SubUnknown String-  deriving (Show, Read, Eq, Ord, Typeable, Generic)---- | A given part of a problem.  All Advent of Code challenges are--- two-parts.------ You can usually get 'Part1' (if it is already released) with a nonsense--- session key, but 'Part2' always requires a valid session key.------ Note also that Challenge #25 typically only has a single part.-data Part = Part1 | Part2-  deriving (Show, Read, Eq, Ord, Enum, Bounded, Typeable, Generic)- -- | An API command.  An @'AoC' a@ an AoC API request that returns -- results of type @a@. ----- A lot of these commands take @'Finite' 25@, which represents a day of--- December up to and including Christmas Day (December 25th).  You can--- convert an integer day (1 - 25) into a @'Finite' 25@ representing that--- day using 'mkDay' or 'mkDay_'.+-- A lot of these commands take 'Day', which represents a day of December+-- up to and including Christmas Day (December 25th).  You can convert an+-- integer day (1 - 25) into a 'Day' using 'mkDay' or 'mkDay_'. data AoC :: Type -> Type where     -- | Fetch prompts for a given day.  Returns a 'Map' of 'Part's and     -- their associated promps, as HTML.     AoCPrompt-        :: Finite 25+        :: Day         -> AoC (Map Part Text)      -- | Fetch input, as plaintext.  Returned verbatim.  Be aware that     -- input might contain trailing newlines.-    AoCInput :: Finite 25 -> AoC Text+    AoCInput :: Day -> AoC Text      -- | Submit a plaintext answer (the 'String') to a given day and part.     -- Receive a server reponse (as HTML) and a response code 'SubmitRes'.@@ -176,25 +145,55 @@     -- of leading and trailing whitespace and run through 'URI.encode'     -- before submitting.     AoCSubmit-        :: Finite 25+        :: Day         -> Part         -> String         -> AoC (Text, SubmitRes) +    -- | Fetch the leaderboard for a given leaderboard public code (owner+    -- member ID).  Requires session key.+    --+    -- The public code can be found in the URL of the leaderboard:+    --+    -- > https://adventofcode.com/2019/leaderboard/private/view/12345+    --+    -- (the @12345@ above)+    --+    -- __NOTE__: This is the most expensive and taxing possible API call,+    -- and makes up the majority of bandwidth to the Advent of Code+    -- servers.  As a courtesy to all who are participating in Advent of+    -- Code, please use this super respectfully, especially in December: if+    -- you set up automation for this, please do not use it more than once+    -- per day.+    --+    -- @since 0.2.0.0+    AoCLeaderboard+        :: Integer+        -> AoC Leaderboard+ deriving instance Show (AoC a) deriving instance Typeable (AoC a) --- | Get the day associated with a given API command.-aocDay :: AoC a -> Finite 25-aocDay (AoCPrompt d    ) = d-aocDay (AoCInput  d    ) = d-aocDay (AoCSubmit d _ _) = d+-- | Get the day associated with a given API command, if there is one.+aocDay :: AoC a -> Maybe Day+aocDay (AoCPrompt d     ) = Just d+aocDay (AoCInput  d     ) = Just d+aocDay (AoCSubmit d _ _ ) = Just d+aocDay (AoCLeaderboard _) = Nothing  -- | A possible (syncronous, logical, pure) error returnable from 'runAoC'. -- Does not cover any asynchronous or IO errors. data AoCError-    -- | A libcurl error, with response code and response body-    = AoCCurlError CurlCode String+    -- | An error in the http request itself+    --+    -- Note that if you are building this with servant-client-core <= 0.16,+    -- this will contain @ServantError@ instead of @ClientError@, which was+    -- the previous name of ths type.+#if MIN_VERSION_servant_client_core(0,16,0)+    = AoCClientError ClientError+#else+    = AoCClientError ServantError+#endif     -- | Tried to interact with a challenge that has not yet been     -- released.  Contains the amount of time until release.     | AoCReleaseError NominalDiffTime@@ -229,14 +228,6 @@       -- | Throttle delay, in milliseconds.  Minimum is 1000000.  Default       -- is 3000000 (3 seconds).     , _aThrottle   :: Int-      -- | (Low-level usage) Extra 'CurlOption' options to feed to the-      -- libcurl bindings.  Meant for things like proxy options and custom-      -- SSL certificates.  You should normally not have to add anything-      -- here, since the library manages cookies, request methods, etc. for-      -- you.  Anything other than tweaking low-level network options (like-      -- the ones mentioned previously) will likely break everything.-      -- Default is @[]@.-    , _aCurlOpts   :: [CurlOption]     }   deriving (Show, Typeable, Generic) @@ -254,34 +245,22 @@     , _aCache      = Nothing     , _aForce      = False     , _aThrottle   = 3000000-    , _aCurlOpts   = []     } --- | API endpoint for a given command.-apiUrl :: Integer -> AoC a -> FilePath-apiUrl y = \case-    AoCPrompt i     -> printf "https://adventofcode.com/%04d/day/%d"        y (dayInt i)-    AoCInput  i     -> printf "https://adventofcode.com/%04d/day/%d/input"  y (dayInt i)-    AoCSubmit i _ _ -> printf "https://adventofcode.com/%04d/day/%d/answer" y (dayInt i)+-- | HTTPS base of Advent of Code API.+aocBase :: BaseUrl+aocBase = BaseUrl Https "adventofcode.com" 443 "" --- | Create a cookie option from a session key.-sessionKeyCookie :: String -> CurlOption-sessionKeyCookie = CurlCookie . printf "session=%s"+-- | 'ClientM' request for a given 'AoC' API call.+aocReq :: Integer -> AoC a -> ClientM a+aocReq yr = \case+    AoCPrompt i       -> let r :<|> _        = adventAPIPuzzleClient yr i in r+    AoCInput  i       -> let _ :<|> r :<|> _ = adventAPIPuzzleClient yr i in r+    AoCSubmit i p ans -> let _ :<|> _ :<|> r = adventAPIPuzzleClient yr i+                         in  r (SubmitInfo p ans) <&> \(x :<|> y) -> (x, y)+    AoCLeaderboard c  -> let _ :<|> r = adventAPIClient yr+                         in  r (PublicCode c) -apiCurl :: String -> AoC a -> [CurlOption]-apiCurl sess = \case-    AoCPrompt _       -> sessionKeyCookie sess-                       : method_GET-    AoCInput  _       -> sessionKeyCookie sess-                       : method_GET-    AoCSubmit _ p ans -> sessionKeyCookie sess-                       : CurlPostFields [ printf "level=%d"  (partInt p)-                                        , printf "answer=%s" (enc ans  )-                                        ]-                       : CurlHttpHeaders ["Content-Type: application/x-www-form-urlencoded"]-                       : method_POST-  where-    enc = URI.encode . strip  -- | Cache file for a given 'AoC' command apiCache@@ -290,9 +269,10 @@     -> AoC a     -> Maybe FilePath apiCache sess yr = \case-    AoCPrompt d -> Just $ printf "prompt/%04d/%02d.html"        yr (dayInt d)-    AoCInput  d -> Just $ printf "input/%s%04d/%02d.txt" keyDir yr (dayInt d)-    AoCSubmit{} -> Nothing+    AoCPrompt d      -> Just $ printf "prompt/%04d/%02d.html"        yr (dayInt d)+    AoCInput  d      -> Just $ printf "input/%s%04d/%02d.txt" keyDir yr (dayInt d)+    AoCSubmit{}      -> Nothing+    AoCLeaderboard{} -> Nothing   where     keyDir = case sess of       Nothing -> ""@@ -317,97 +297,41 @@                          then noCache                          else saverLoader a -    cacher . withCurlDo . runExceptT $ do-      rel <- liftIO $ timeToRelease _aYear (aocDay a)-      when (rel > 0) $-        throwError $ AoCReleaseError rel--      (cc, r) <- (maybe (throwError AoCThrottleError) pure =<<)-               . liftIO-               . throttling aocThrottler (max 1000000 _aThrottle)-               $ curlGetString u (apiCurl _aSessionKey a ++ _aCurlOpts)-      case cc of-        CurlOK -> return ()-        _      -> throwError $ AoCCurlError cc r-      pure $ processAoC a r-  where-    u = apiUrl _aYear a+    cacher . runExceptT $ do+      forM_ (aocDay a) $ \d -> do+        rel <- liftIO $ timeToRelease _aYear d+        when (rel > 0) $+          throwError $ AoCReleaseError rel --- | Process a string response into the type desired.-processAoC :: AoC a -> String -> a-processAoC = \case-    AoCPrompt _ -> M.fromList-                 . zip [Part1 ..]-                 . processHTML-    AoCInput{}  -> T.pack-    AoCSubmit{} -> (\o -> (o, parseSubmitRes o))-                 . fromMaybe ""-                 . listToMaybe-                 . processHTML+      mtr <- liftIO+           . throttling aocThrottler (max 1000000 _aThrottle)+           $ runClientM (aocReq _aYear a) =<< aocClientEnv _aSessionKey+      mcr <- maybe (throwError AoCThrottleError) pure mtr+      either (throwError . AoCClientError) pure mcr --- | Process an HTML webpage into a list of all contents in <article>s-processHTML :: String -> [Text]-processHTML = map H.renderTree-            . mapMaybe isArticle-            . H.universeTree-            . H.parseTree-            . T.pack+aocClientEnv :: String -> IO ClientEnv+aocClientEnv s = do+    t <- getCurrentTime+    v <- atomically . newTVar $ createCookieJar [c t]+    mgr <- newTlsManager+    pure $ ClientEnv mgr aocBase (Just v)   where-    isArticle :: TagTree Text -> Maybe [TagTree Text]-    isArticle (TagBranch n _ ts) = ts <$ guard (n == "article")-    isArticle _                  = Nothing+    c t = Cookie+      { cookie_name             = "session"+      , cookie_value            = T.encodeUtf8 . T.pack $ s+      , cookie_expiry_time      = addUTCTime oneYear t+      , cookie_domain           = "adventofcode.com"+      , cookie_path             = "/"+      , cookie_creation_time    = t+      , cookie_last_access_time = t+      , cookie_persistent       = True+      , cookie_host_only        = True+      , cookie_secure_only      = True+      , cookie_http_only        = True+      }+    oneYear = 60 * 60 * 24 * 356.25 --- | Parse 'Text' into a 'SubmitRes'.-parseSubmitRes :: Text -> SubmitRes-parseSubmitRes = either SubUnknown id-               . P.parseOnly choices-               . mconcat-               . mapMaybe deTag-               . H.parseTags-  where-    deTag (H.TagText t) = Just t-    deTag _             = Nothing-    choices             = asum [ parseCorrect   P.<?> "Correct"-                               , parseIncorrect P.<?> "Incorrect"-                               , parseWait      P.<?> "Wait"-                               , parseInvalid   P.<?> "Invalid"-                               ]-    parseCorrect = do-      _ <- P.manyTill P.anyChar (P.asciiCI "that's the right answer") P.<?> "Right answer"-      r <- optional . (P.<?> "Rank") $ do-        P.manyTill P.anyChar (P.asciiCI "rank")-          *> P.skipMany (P.satisfy (not . isDigit))-        P.decimal-      pure $ SubCorrect r-    parseIncorrect = do-      _ <- P.manyTill P.anyChar (P.asciiCI "that's not the right answer") P.<?> "Not the right answer"-      hint <- optional . (P.<?> "Hint") $ do-        P.manyTill P.anyChar "your answer is" *> P.skipSpace-        P.takeWhile1 (/= '.')-      P.manyTill P.anyChar (P.asciiCI "wait") *> P.skipSpace-      waitAmt <- (1 <$ P.asciiCI "one") <|> P.decimal-      pure $ SubIncorrect (waitAmt * 60) (T.unpack <$> hint)-    parseWait = do-      _ <- P.manyTill P.anyChar (P.asciiCI "an answer too recently") P.<?> "An answer too recently"-      P.skipMany (P.satisfy (not . isDigit))-      m <- optional . (P.<?> "Delay minutes") $-              P.decimal <* P.char 'm' <* P.skipSpace-      s <- P.decimal <* P.char 's' P.<?> "Delay seconds"-      pure . SubWait $ maybe 0 (* 60) m + s-    parseInvalid = SubInvalid <$ P.manyTill P.anyChar (P.asciiCI "solving the right level") --- | Pretty-print a 'SubmitRes'-showSubmitRes :: SubmitRes -> String-showSubmitRes = \case-    SubCorrect Nothing    -> "Correct"-    SubCorrect (Just r)   -> printf "Correct (Rank %d)" r-    SubIncorrect i Nothing  -> printf "Incorrect (%d minute wait)" (i `div` 60)-    SubIncorrect i (Just h) -> printf "Incorrect (%s) (%d minute wait)" h (i `div` 60)-    SubWait i             -> let (m,s) = i `divMod` 60-                             in   printf "Wait (%d min %d sec wait)"  m s-    SubInvalid            -> "Invalid"-    SubUnknown r          -> printf "Unknown (%s)" r- saverLoader :: AoC a -> SaverLoader (Either AoCError a) saverLoader = \case     AoCPrompt d -> SL { _slSave = either (const Nothing) (Just . encodeMap)@@ -420,11 +344,12 @@                       , _slLoad = Just . Right                       }     AoCSubmit{} -> noCache+    AoCLeaderboard{} -> noCache   where-    expectedParts :: Finite 25 -> Set Part-    expectedParts n-      | n == 24   = S.singleton Part1-      | otherwise = S.fromDistinctAscList [Part1 ..]+    expectedParts :: Day -> Set Part+    expectedParts d+      | d == maxBound = S.singleton Part1+      | otherwise     = S.fromDistinctAscList [Part1 ..]     sep = ">>>>>>>>>"     encodeMap mp = T.intercalate "\n" . concat $                             [ maybeToList $ M.lookup Part1 mp@@ -438,61 +363,27 @@           | T.null (T.strip ln) = M.empty           | otherwise           = M.singleton p ln --- | Construct a @'Finite' 25@ (the type of a Day) from a day--- integer (1 - 25).  If input is out of range, 'Nothing' is returned.  See--- 'mkDay_' for an unsafe version useful for literals.------ Inverse of 'dayInt'.-mkDay :: Integer -> Maybe (Finite 25)-mkDay = packFinite . subtract 1---- | Construct a @'Finite' 25@ (the type of a Day) from a day--- integer (1 - 25).  Is undefined if input is out of range.  Can be useful--- for compile-time literals, like @'mkDay_' 4@------ Inverse of 'dayInt'.-mkDay_ :: Integer -> Finite 25-mkDay_ = fromMaybe e . mkDay-  where-    e = errorWithoutStackTrace "Advent.mkDay_: Date out of range (1 - 25)"- -- | Get time until release of a given challenge. timeToRelease     :: Integer              -- ^ year-    -> Finite 25            -- ^ day+    -> Day                  -- ^ day     -> IO NominalDiffTime timeToRelease y d = (challengeReleaseTime y d `diffUTCTime`) <$> getCurrentTime  -- | Check if a challenge has been released yet. challengeReleased     :: Integer              -- ^ year-    -> Finite 25            -- ^ day+    -> Day                  -- ^ day     -> IO Bool challengeReleased y = fmap (<= 0) . timeToRelease y  -- | Prompt release time challengeReleaseTime     :: Integer              -- ^ year-    -> Finite 25            -- ^ day+    -> Day                  -- ^ day     -> UTCTime challengeReleaseTime y d = UTCTime (fromGregorian y 12 (fromIntegral (dayInt d)))                                    (5 * 60 * 60)---- | Convert a @'Finite' 25@ day into a day integer (1 - 25).  Inverse of--- 'mkDay'.-dayInt :: Finite 25 -> Integer-dayInt = (+ 1) . getFinite---- | Convert a 'Part' to an 'Int'.-partInt :: Part -> Int-partInt Part1 = 1-partInt Part2 = 2---- | A character associated with a given part.  'Part1' is associated with--- @\'a\'@, and 'Part2' is associated with @\'b\'@-partChar :: Part -> Char-partChar Part1 = 'a'-partChar Part2 = 'b'  strip :: String -> String strip = T.unpack . T.strip . T.pack
+ src/Advent/API.hs view
@@ -0,0 +1,409 @@+{-# LANGUAGE CPP                        #-}+{-# LANGUAGE DeriveGeneric              #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase                 #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE TypeInType                 #-}+{-# LANGUAGE TypeOperators              #-}++-- |+-- Module      : Advent.API+-- Copyright   : (c) Justin Le 2019+-- License     : BSD3+--+-- Maintainer  : justin@jle.im+-- Stability   : experimental+-- Portability : non-portable+--+-- Raw Servant API for Advent of Code.  Can be useful for building mock+-- servers, generating documentation and other servanty things, or+-- low-level raw requests.+--+-- If you use this to make requests directly, please use responsibly: do+-- not make automated requests more than once per day and throttle all+-- manual requestes.  See notes in "Advent".+--+-- @since 0.2.0.0+--++module Advent.API (+  -- * Types+    Day(..)+  , Part(..)+  , SubmitInfo(..)+  , SubmitRes(..), showSubmitRes+  , PublicCode(..)+  , Leaderboard(..)+  , LeaderboardMember(..)+  -- * Servant API+  , AdventAPI+  , adventAPI+  , adventAPIClient+  , adventAPIPuzzleClient+  -- * Util+  , mkDay, mkDay_, dayInt+  , partInt+  , partChar+  -- * Internal+  , processHTML+  , parseSubmitRes+  , Articles+  , FromArticles(..)+  , RawText+  ) where++import           Control.Applicative+import           Control.Monad+import           Data.Aeson+import           Data.Bifunctor+import           Data.Char+import           Data.Finite+import           Data.Foldable+import           Data.Functor.Classes+import           Data.Map               (Map)+import           Data.Maybe+import           Data.Proxy+import           Data.Text              (Text)+import           Data.Time.Clock+import           Data.Time.Clock.POSIX+import           Data.Typeable+import           GHC.Generics+import           Servant.API+import           Servant.Client+import           Text.HTML.TagSoup.Tree (TagTree(..))+import           Text.Printf+import           Text.Read              (readMaybe)+import qualified Data.Attoparsec.Text   as P+import qualified Data.ByteString.Lazy   as BSL+import qualified Data.Map               as M+import qualified Data.Text              as T+import qualified Data.Text.Encoding     as T+import qualified Network.HTTP.Media     as M+import qualified Text.HTML.TagSoup      as H+import qualified Text.HTML.TagSoup.Tree as H+import qualified Web.FormUrlEncoded     as WF++#if !MIN_VERSION_base(4,11,0)+import           Data.Semigroup ((<>))+#endif++-- | Describes the day: a number between 1 and 25 inclusive.+--+-- Represented by a 'Finite' ranging from 0 to 24 inclusive; you should+-- probably make one using the smart constructor 'mkDay'.+newtype Day = Day { dayFinite :: Finite 25 }+  deriving (Eq, Ord, Enum, Bounded, Typeable, Generic)++instance Show Day where+    showsPrec = showsUnaryWith (\d -> showsPrec d . dayInt) "mkDay"++-- | A given part of a problem.  All Advent of Code challenges are+-- two-parts.+--+-- You can usually get 'Part1' (if it is already released) with a nonsense+-- session key, but 'Part2' always requires a valid session key.+--+-- Note also that Challenge #25 typically only has a single part.+data Part = Part1 | Part2+  deriving (Show, Read, Eq, Ord, Enum, Bounded, Typeable, Generic)++-- | Info required to submit an answer for a part.+data SubmitInfo = SubmitInfo+    { siLevel  :: Part+    , siAnswer :: String+    }+  deriving (Show, Read, Eq, Ord, Typeable, Generic)++-- | The result of a submission.+data SubmitRes+    -- | Correct submission, including global rank (if reported, which+    -- usually happens if rank is under 1000)+    = SubCorrect (Maybe Integer)+    -- | Incorrect submission.  Contains the number of /seconds/ you must+    -- wait before trying again.  The 'Maybe' contains possible hints given+    -- by the server (usually "too low" or "too high").+    | SubIncorrect Int (Maybe String)+    -- | Submission was rejected because an incorrect submission was+    -- recently submitted.  Contains the number of /seconds/ you must wait+    -- before trying again.+    | SubWait Int+    -- | Submission was rejected because it was sent to an invalid question+    -- or part.  Usually happens if you submit to a part you have already+    -- answered or have not yet unlocked.+    | SubInvalid+    -- | Could not parse server response.  Contains parse error.+    | SubUnknown String+  deriving (Show, Read, Eq, Ord, Typeable, Generic)++-- | Member ID of public leaderboard (the first part of the registration+-- code, before the hyphen).  It can be found as the number in the URL:+--+-- > https://adventofcode.com/2019/leaderboard/private/view/12345+--+-- (the @12345@ above)+newtype PublicCode = PublicCode { getPublicCode :: Integer }+  deriving (Show, Read, Eq, Ord, Typeable, Generic)++-- | Leaderboard type, representing private leaderboard information.+data Leaderboard = LB+    { lbEvent   :: Integer                        -- ^ The year of the event+    , lbOwnerId :: Integer                        -- ^ The Member ID of the owner, or the public code+    , lbMembers :: Map Integer LeaderboardMember  -- ^ A map from member IDs to their leaderboard info+    }+  deriving (Show, Eq, Ord, Typeable, Generic)++-- | Leaderboard position for a given member.+data LeaderboardMember = LBM+    { lbmGlobalScore :: Integer                     -- ^ Global leaderboard score+    , lbmName        :: Maybe Text                  -- ^ Username, if user specifies one+    , lbmLocalScore  :: Integer                     -- ^ Score for this leaderboard+    , lbmId          :: Integer                     -- ^ Member ID+    , lbmLastStarTS  :: Maybe UTCTime               -- ^ Time of last puzzle solved, if any+    , lbmStars       :: Int                         -- ^ Number of stars (puzzle parts) solved+    , lbmCompletion  :: Map Day (Map Part UTCTime)  -- ^ Completion times of each day and puzzle part+    }+  deriving (Show, Eq, Ord, Typeable, Generic)++instance ToHttpApiData Part where+    toUrlPiece = T.pack . show . partInt+    toQueryParam = toUrlPiece++instance ToHttpApiData Day where+    toUrlPiece = T.pack . show . dayInt+    toQueryParam = toUrlPiece++instance ToHttpApiData PublicCode where+    toUrlPiece   = (<> ".json") . T.pack . show . getPublicCode+    toQueryParam = toUrlPiece++instance WF.ToForm SubmitInfo where+    toForm = WF.genericToForm WF.FormOptions+      { WF.fieldLabelModifier = camelTo2 '-' . drop 2 }++-- | Raw "text/plain" MIME type+data RawText++instance Accept RawText where+    contentType _ = "text" M.// "plain"++instance MimeUnrender RawText Text where+    mimeUnrender _ = first show . T.decodeUtf8' . BSL.toStrict++-- | Interpret repsonse as a list of HTML 'Text' in @<article>@ tags.+data Articles++-- | Class for interpreting a list of 'Text' in article tags to some+-- desired output.+class FromArticles a where+    fromArticles :: [Text] -> a++instance Accept Articles where+    contentType _ = "text" M.// "html"++instance FromArticles a => MimeUnrender Articles a where+    mimeUnrender _ = fmap fromArticles+                   . bimap show processHTML+                   . T.decodeUtf8'+                   . BSL.toStrict++instance FromArticles [Text] where+    fromArticles = id++instance FromArticles Text where+    fromArticles = T.unlines++instance (Ord a, Enum a, Bounded a) => FromArticles (Map a Text) where+    fromArticles = M.fromList . zip [minBound ..]++instance (FromArticles a, FromArticles b) => FromArticles (a :<|> b) where+    fromArticles xs = fromArticles xs :<|> fromArticles xs++instance FromArticles SubmitRes where+    fromArticles = parseSubmitRes . fold  . listToMaybe++instance FromJSON Leaderboard where+    parseJSON = withObject "Leaderboard" $ \o ->+        LB <$> (strInt =<< (o .: "event"))+           <*> (strInt =<< (o .: "owner_id"))+           <*> o .: "members"+      where+        strInt t = case readMaybe t of+          Nothing -> fail "bad int"+          Just i  -> pure i++instance FromJSON LeaderboardMember where+    parseJSON = withObject "LeaderboardMember" $ \o ->+        LBM <$> o .: "global_score"+            <*> optional (o .: "name")+            <*> o .: "local_score"+            <*> (strInt =<< (o .: "id"))+            <*> optional (fromEpoch =<< (o .: "last_star_ts"))+            <*> o .: "stars"+            <*> (do cdl <- o .: "completion_day_level"+                    (traverse . traverse) ((fromEpoch =<<) . (.: "get_star_ts")) cdl+                )+      where+        strInt t = case readMaybe t of+          Nothing -> fail "bad int"+          Just i  -> pure i+        fromEpoch t = case readMaybe t of+          Nothing -> fail "bad stamp"+          Just i  -> pure . posixSecondsToUTCTime $ fromInteger i++instance FromJSONKey Day where+    fromJSONKey = FromJSONKeyTextParser (parseJSON . String)+instance FromJSONKey Part where+    fromJSONKey = FromJSONKeyTextParser (parseJSON . String)++instance FromJSON Part where+    parseJSON = withText "Part" $ \case+      "1" -> pure Part1+      "2" -> pure Part2+      _   -> fail "Bad part"+instance FromJSON Day where+    parseJSON = withText "Day" $ \t ->+      case readMaybe (T.unpack t) of+        Nothing -> fail "No read day"+        Just i  -> case mkDay i of+          Nothing -> fail "Day out of range"+          Just d  -> pure d++++-- | REST API of Advent of Code.+--+-- Note that most of these requests assume a "session=" cookie.+type AdventAPI =+      Capture "year" Integer+   :> ("day" :> Capture "day" Day+             :> (Get '[Articles] (Map Part Text)+            :<|> "input" :> Get '[RawText] Text+            :<|> "answer"+                     :> ReqBody '[FormUrlEncoded] SubmitInfo+                     :> Post    '[Articles] (Text :<|> SubmitRes)+                )+  :<|> "leaderboard" :> "private" :> "view"+                     :> Capture "code" PublicCode+                     :> Get '[JSON] Leaderboard+      )++-- | 'Proxy' used for /servant/ functions.+adventAPI :: Proxy AdventAPI+adventAPI = Proxy++-- | 'ClientM' requests based on 'AdventAPI', generated by servant.+adventAPIClient+    :: Integer+    -> (Day -> ClientM (Map Part Text) :<|> ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)) )+  :<|> (PublicCode -> ClientM Leaderboard)+adventAPIClient = client adventAPI++-- | A subset of 'adventAPIClient' for only puzzle-related API routes, not+-- leaderboard ones.+adventAPIPuzzleClient+    :: Integer+    -> Day+    -> ClientM (Map Part Text) :<|> ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes))+adventAPIPuzzleClient y = pis+  where+    pis :<|> _ = adventAPIClient y++-- | Process an HTML webpage into a list of all contents in <article>s+processHTML :: Text -> [Text]+processHTML = map H.renderTree+            . mapMaybe isArticle+            . H.universeTree+            . H.parseTree+  where+    isArticle :: TagTree Text -> Maybe [TagTree Text]+    isArticle (TagBranch n _ ts) = ts <$ guard (n == "article")+    isArticle _                  = Nothing++-- | Parse 'Text' into a 'SubmitRes'.+parseSubmitRes :: Text -> SubmitRes+parseSubmitRes = either SubUnknown id+               . P.parseOnly choices+               . mconcat+               . mapMaybe deTag+               . H.parseTags+  where+    deTag (H.TagText t) = Just t+    deTag _             = Nothing+    choices             = asum [ parseCorrect   P.<?> "Correct"+                               , parseIncorrect P.<?> "Incorrect"+                               , parseWait      P.<?> "Wait"+                               , parseInvalid   P.<?> "Invalid"+                               , fail "No option recognized"+                               ]+    parseCorrect = do+      _ <- P.manyTill P.anyChar (P.asciiCI "that's the right answer") P.<?> "Right answer"+      r <- optional . (P.<?> "Rank") $ do+        P.manyTill P.anyChar (P.asciiCI "rank")+          *> P.skipMany (P.satisfy (not . isDigit))+        P.decimal+      pure $ SubCorrect r+    parseIncorrect = do+      _ <- P.manyTill P.anyChar (P.asciiCI "that's not the right answer") P.<?> "Not the right answer"+      hint <- optional . (P.<?> "Hint") $ do+        P.manyTill P.anyChar "your answer is" *> P.skipSpace+        P.takeWhile1 (/= '.')+      P.manyTill P.anyChar (P.asciiCI "wait") *> P.skipSpace+      waitAmt <- (1 <$ P.asciiCI "one") <|> P.decimal+      pure $ SubIncorrect (waitAmt * 60) (T.unpack <$> hint)+    parseWait = do+      _ <- P.manyTill P.anyChar (P.asciiCI "an answer too recently") P.<?> "An answer too recently"+      P.skipMany (P.satisfy (not . isDigit))+      m <- optional . (P.<?> "Delay minutes") $+              P.decimal <* P.char 'm' <* P.skipSpace+      s <- P.decimal <* P.char 's' P.<?> "Delay seconds"+      pure . SubWait $ maybe 0 (* 60) m + s+    parseInvalid = SubInvalid <$ P.manyTill P.anyChar (P.asciiCI "solving the right level")++-- | Pretty-print a 'SubmitRes'+showSubmitRes :: SubmitRes -> String+showSubmitRes = \case+    SubCorrect Nothing    -> "Correct"+    SubCorrect (Just r)   -> printf "Correct (Rank %d)" r+    SubIncorrect i Nothing  -> printf "Incorrect (%d minute wait)" (i `div` 60)+    SubIncorrect i (Just h) -> printf "Incorrect (%s) (%d minute wait)" h (i `div` 60)+    SubWait i             -> let (m,s) = i `divMod` 60+                             in   printf "Wait (%d min %d sec wait)"  m s+    SubInvalid            -> "Invalid"+    SubUnknown r          -> printf "Unknown (%s)" r++-- | Convert a @'Finite' 25@ day into a day integer (1 - 25).  Inverse of+-- 'mkDay'.+dayInt :: Day -> Integer+dayInt = (+ 1) . getFinite . dayFinite++-- | Convert a 'Part' to an 'Int'.+partInt :: Part -> Int+partInt Part1 = 1+partInt Part2 = 2++-- | Construct a 'Day' from a day integer (1 - 25).  If input is out of+-- range, 'Nothing' is returned.  See 'mkDay_' for an unsafe version useful+-- for literals.+--+-- Inverse of 'dayInt'.+mkDay :: Integer -> Maybe Day+mkDay = fmap Day . packFinite . subtract 1++-- | Construct a @'Finite' 25@ (the type of a Day) from a day+-- integer (1 - 25).  Is undefined if input is out of range.  Can be useful+-- for compile-time literals, like @'mkDay_' 4@+--+-- Inverse of 'dayInt'.+mkDay_ :: Integer -> Day+mkDay_ = fromMaybe e . mkDay+  where+    e = errorWithoutStackTrace "Advent.mkDay_: Date out of range (1 - 25)"++-- | A character associated with a given part.  'Part1' is associated with+-- @\'a\'@, and 'Part2' is associated with @\'b\'@+partChar :: Part -> Char+partChar Part1 = 'a'+partChar Part2 = 'b'+
test/Spec.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE ViewPatterns #-} -import           Advent+import           Advent.API import           Control.Monad import           Data.List import           System.Directory