|
| 1 | +module PayloadHttpApiNode.Main where |
| 2 | + |
| 3 | +import Prelude |
| 4 | + |
| 5 | +import Data.Either (Either(..)) |
| 6 | +import Data.Map (Map) |
| 7 | +import Data.Map as Map |
| 8 | +import Data.Maybe (Maybe(..)) |
| 9 | +import Data.Tuple (Tuple(..)) |
| 10 | +import Effect (Effect) |
| 11 | +import Effect.AVar (AVar) |
| 12 | +import Effect.AVar as EffVar |
| 13 | +import Effect.Aff (Aff) |
| 14 | +import Effect.Aff.AVar as AffVar |
| 15 | +import Payload.ResponseTypes (Failure(..)) |
| 16 | +import Payload.Server as Payload |
| 17 | +import Payload.Spec (GET, Spec(Spec), POST) |
| 18 | + |
| 19 | +-- | This is used as a return type for whenever we don't have something |
| 20 | +-- | meaningful to return. |
| 21 | +type StatusCodeResponse = |
| 22 | + { statusCode :: Int |
| 23 | + , statusMessage :: String |
| 24 | + } |
| 25 | + |
| 26 | +-- | Our API is all about quotes, and this is how we represent a single quote. |
| 27 | +type Quote = |
| 28 | + { text :: String |
| 29 | + , id :: Int |
| 30 | + } |
| 31 | + |
| 32 | +-- | This fully describes our API as a big record under the 'Spec' constructor. |
| 33 | +-- | |
| 34 | +-- | Each label represents an endpoint in our quote API: |
| 35 | +-- | 'quote' is a GET-by-id endpoint. We match the URL by the <id> parameter. |
| 36 | +-- | 'addQUote' is a POST-to-insert a new quote. The body is just a string. |
| 37 | +-- | 'getAll' is a GET everything in our quote 'database'. |
| 38 | +-- | |
| 39 | +-- | The response type is also encoded in each endpoint. |
| 40 | +-- | Note that the implementation is trivial: this only represents type-level |
| 41 | +-- | information used downstream. |
| 42 | +spec |
| 43 | + :: Spec |
| 44 | + { quote :: GET "/quote/<id>" |
| 45 | + { params :: { id :: Int } |
| 46 | + , response :: String |
| 47 | + } |
| 48 | + , addQuote :: POST "/quote" |
| 49 | + { body :: String |
| 50 | + , response :: StatusCodeResponse |
| 51 | + } |
| 52 | + , getAll :: GET "/quote" |
| 53 | + { response :: Array Quote |
| 54 | + } |
| 55 | + } |
| 56 | +spec = Spec |
| 57 | + |
| 58 | +-- | This is where everything comes together: we create a record of HANDLERS |
| 59 | +-- | for each of the routes we defined in 'spec'. |
| 60 | +-- | The types must follow the definitions, so for example 'quote' must take |
| 61 | +-- | a record containing "params :: { id :: Int }". |
| 62 | +-- | |
| 63 | +-- | Note that we are taking an 'AVar (Map Int String)'. We do this in order to |
| 64 | +-- | keep state across calls. |
| 65 | +-- | |
| 66 | +-- | AVars are variables that you can read and update within an 'Aff' context. |
| 67 | +-- | The 'AVar' created in 'main' is threaded through 'handlers' to each |
| 68 | +-- | individual handler. |
| 69 | +handlers :: _ |
| 70 | +handlers quotes = |
| 71 | + { quote: quote quotes |
| 72 | + , addQuote: addQuote quotes |
| 73 | + , getAll: getAll quotes |
| 74 | + } |
| 75 | + |
| 76 | +-- | Main entry point: we just create a new 'AVar' (fake "database") and |
| 77 | +-- | launch the payload server. |
| 78 | +main :: Effect Unit |
| 79 | +main = do |
| 80 | + quotes <- EffVar.new $ Map.singleton 1 "This is a quote" |
| 81 | + Payload.launch spec (handlers quotes) |
| 82 | + |
| 83 | +-- | This represents our application's state. It's essentially our database |
| 84 | +-- | (except it gets reset when application gets restarted). |
| 85 | +type State = AVar (Map Int String) |
| 86 | + |
| 87 | +-- | This is the handler for getting a quote by id. |
| 88 | +-- | We start by reading the "database". |
| 89 | +-- | If we can't find the requested 'id', then we just return an error. |
| 90 | +-- | Otherwise, we return the requested quote. |
| 91 | +quote :: State -> { params :: { id :: Int } } -> Aff (Either Failure String) |
| 92 | +quote st { params : {id} } = do |
| 93 | + quotes <- AffVar.read st |
| 94 | + case Map.lookup id quotes of |
| 95 | + Nothing -> pure $ Left $ Forward "" |
| 96 | + Just v -> pure (pure v) |
| 97 | + |
| 98 | +-- | Adding a quote requires us to 'block' reads/writes to our 'database', which |
| 99 | +-- | is precisely what 'take' does. |
| 100 | +-- | We then look for the highest id in the 'database' and increment it by one. |
| 101 | +-- | We finish off by 'unblocking' and pushing our updated 'database'. |
| 102 | +addQuote :: State -> { body :: String } -> Aff StatusCodeResponse |
| 103 | +addQuote st { body } = do |
| 104 | + quotes <- AffVar.take st |
| 105 | + id <- case Map.findMax quotes of |
| 106 | + Nothing -> pure 1 |
| 107 | + Just { key } -> pure (key + 1) |
| 108 | + AffVar.put (Map.insert id body quotes) st |
| 109 | + pure { statusCode: 200, statusMessage: body } |
| 110 | + |
| 111 | +-- | We start by reading the current 'database'. Since we can't easily encode |
| 112 | +-- | a Map structure directly, we transform it to an Array by calling |
| 113 | +-- | 'toUnfoldable' (the map works over 'Aff'). |
| 114 | +-- | Our type now is 'Aff (Array (Tuple Int String))'. Unfortunately, 'Tuples' |
| 115 | +-- | are also not trivial to encode, so we also need to 'map' over the |
| 116 | +-- | Array and change all Tuples to records. |
| 117 | +-- | |
| 118 | +-- | We end up returning all data in the 'database' as an array of records. |
| 119 | +getAll :: forall r. State -> { |r } -> Aff (Array Quote) |
| 120 | +getAll st _ = map tupleToRecord <<< Map.toUnfoldable <$> AffVar.read st |
| 121 | + where |
| 122 | + tupleToRecord :: Tuple Int String -> Quote |
| 123 | + tupleToRecord (Tuple id text) = {id, text} |
| 124 | + |
0 commit comments