A Webmachine-style library for scotty inspired by Erlang's cowboy_rest module.
{-# LANGUAGE OverloadedStrings #-}
import Network.Wai.Middleware.RequestLogger
import Web.Scotty.Rest
import Web.Scotty.Trans
main :: IO ()
main = scottyT 3000 id $ do
middleware logStdoutDev
rest "/" defaultConfig {
contentTypesProvided = return [("text/html", html "Hello, World!")]
}
This library implements server-side HTTP semantics as illustrated by the Webmachine diagram. Instead of simply having handlers for endpoints that then have to take full responsibility for handling all of the behaviour expected from an HTTP server (using the correct return code, including the correct headers that go with some return codes, etc.), you implement callbacks for the various decision points and get correct semantics βfor freeβ. These callbacks are regular functions and they have default implementations that do what you expect, meaning that you only need to implement the ones whose behaviour you want to change.
Some examples of callback functions and their default implementations:
serviceAvailable
:return True
isAuthorized
:return Authorized
charSetsProvided
:Nothing
(i.e. ignore theAccept-Charsets
header)allowedMethods
:[GET, HEAD, OPTIONS]
resourceMoved
:NotMoved
The two functions that you almost always is going to override is contentTypesProvided
and/or contentTypesAccepted
. These are both a list of pairs of content types (e.g. application/json
) together with a handler for that type. A handler is a function that is responsible for producing a response for its associated content type (in the case of contentTypesProvided
) or consuming a value of its associated content type (in the case of contentTypesAccepted
).
In the example above, the only function overridden is contentTypesProvided
. Our implementations says that we provide only one content type (text/html
) and if someone asks for text/html
(or */*
) we will use Scotty's
html
function to return the text "Hello World".
Some example interactions with this server:
GET / HTTP/1.1
Accept: */*
Host: localhost:3000
HTTP/1.1 200 OK
Date: Sat, 19 Nov 2016 14:32:20 GMT
Server: Warp/3.2.9
Transfer-Encoding: chunked
Content-Type: text/html
Hello, World!
GET / HTTP/1.1
Accept: application/json
Host: localhost:3000
HTTP/1.1 406 Not Acceptable
Date: Sat, 19 Nov 2016 14:32:30 GMT
Server: Warp/3.2.9
Transfer-Encoding: chunked
POST / HTTP/1.1
Accept: application/json, */*
Content-Length: 13
Content-Type: application/json
Host: localhost:3000
{
"test": "1"
}
HTTP/1.1 405 Method Not Allowed
Date: Sat, 19 Nov 2016 14:32:57 GMT
Server: Warp/3.2.9
Transfer-Encoding: chunked
Allow: GET, HEAD, OPTIONS
You will often want to have some state per request. You could for example imagine having a serviceAvailable
function that attempts to get a database connection from a connections pool and if successful it would add the acquired connection handle to the current state. The isAuthorized
call could then use this connection to authenticate/authorize a user based on a token and then add the user record to the current state, and so on.
The diagrams below show the flow of how scotty-rest
will handle a request and the functions that are called at each decision point.
Start
β
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ False ββββββββββββββββββββββββββββββ
β serviceAvailable ββββββββββββββββββββΆβ503 Service Unavailable β
ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
β
β True
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ False ββββββββββββββββββββββββββββββ
β method is known ββββββββββββββββββββΆβ501 Not Implemented β
ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
β
β True
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ True ββββββββββββββββββββββββββββββ
β uriTooLong ββββββββββββββββββββΆβ414 Request URI Too Long β
ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
β
β False
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ False ββββββββββββββββββββββββββββββ
β method β allowedMethods ββββββββββββββββββββΆβ405 Method Not Allowed β
ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
β
β True
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ True ββββββββββββββββββββββββββββββ
β malformedRequest ββββββββββββββββββββΆβ400 Bad Request β
ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
β
β False
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ NotAuthorized ββββββββββββββββββββββββββββββ
β isAuthorized ββββββββββββββββββββΆβ401 Unauthorized β
ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
β
β Authorized
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ True ββββββββββββββββββββββββββββββ
β forbidden ββββββββββββββββββββΆβ403 Forbidden β
ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
β
β True
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ False ββββββββββββββββββββββββββββββ
β validContentHeaders ββββββββββββββββββββΆβ501 Not Implemented β
ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
β
β True
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ False ββββββββββββββββββββββββββββββ
β validEntityLength ββββββββββββββββββββΆβ413 Request Entity Too Largeβ
ββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
β
β
β βββββββββββββββββββββββββββββββββββ
βΌ β OPTIONSβ
ββββββββββββββββββββββββ β β
β β Yes β ββββββββββββββββββββββββββββββ β
β Method is OPTIONS? ββββββββββββββββββββββββββΌββΆβ options β β
β β β ββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββ β β β
β β β β
β β βΌ β
β β ββββββββββββββββββββββββββββββ β
β β β 200 OK β β
β β ββββββββββββββββββββββββββββββ β
β β β
β β β
β βββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β βΌ Content negotiationβ
β ββββββββββββββββββββββββ β
β No β β β
β ββββββββββββ Has βAcceptβ Header? β β
β β β β β
β β ββββββββββββββββββββββββ β
β β β β
β β β Yes β
β β βΌ Not β
β β ββββββββββββββββββββββββββββββββββββββββ Provided β
β β β contentTypesProvided ββββββββββββββββββββββββββββββββββ β
β β ββββββββββββββββββββββββββββββββββββββββ β β
β β β β β
β β β Provided β β
β β βΌ β β
β β ββββββββββββββββββββββββ β β
β βββββββββββΆβHas βAccept-Languageβ β β β
β β Header? β β β
β ββββββββββββ β β β
β β No ββββββββββββββββββββββββ β β
β β β β β
β β β Yes β β
β β βΌ Not βΌ β
β β ββββββββββββββββββββββββββββββββββββββββ provided βββββββββββββββββββββ β
β β β languagesProvided βββββββββββββββββββββββΆβ406 Not Acceptable β β
β β ββββββββββββββββββββββββββββββββββββββββ βββββββββββββββββββββ β
β β β β² β
β β β Provided β β
β β βΌ β β
β β ββββββββββββββββββββββββ β β
β βββββββββββΆβ Has βAccept-Charsetβ β β β
β β Header? β β β
β ββββββββββββ β β β
β β No ββββββββββββββββββββββββ β β
β β β β β
β β β Yes β β
β β βΌ Not β β
β β ββββββββββββββββββββββββββββββββββββββββ provided β β
β β β charSetsProvided ββββββββββββββββββββββββββββββββββ β
β β ββββββββββββββββββββββββββββββββββββββββ β
β β β β
β β β Provided β
β β βΌ β
β β ββββββββββββββββββββββββββββββββββββββββ β
β βββΆβ variances β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β β
βββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β
β
βΌ
See GET/HEAD, PUT/POST/PATCH or DELETE
diagram, depending on the method.
Continuing from
Content Negotiationβ¦
β
β
βββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β GET/HEADβ
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββ False β
β β resourceExists ββββββββββββββββββββββββββββββ β
β ββββββββββββββββββββββββββββββββββββββββ β β
β β β β
β β β β
β β True β β
β β βΌ β
β βΌ ββββββββββββββββββββββββ β
β β β False β
β See βConditional requestsβ diagram βHas βIf-Matchβ Header?βββββββββ β
β β β β β
β β ββββββββββββββββββββββββ β β
β β β β β
β βΌ β True β β
β ββββββββββββββββββββββββββββββββββ βΌ β β
β β generateEtag β ββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββ β 412 Precond. Failed β β β
β β ββββββββββββββββββββββββ β β
β β β β
β βΌ β β
β ββββββββββββββββββββββββββββββββββ β β
β β lastModified β ββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββ ββββ previouslyExisted ββββ β
β β β ββββββββββββββββββββββββββββββββββ β
β β β β β
β βΌ β True β False β
β ββββββββββββββββββββββββββββββββββ β βΌ β
β β expires β β ββββββββββββββββββββββββ β
β ββββββββββββββββββββββββββββββββββ β β 404 Not Found β β
β β β ββββββββββββββββββββββββ β
β β β β
β βΌ β β
β ββββββββββββββββββββββββββββββββββ β β
β β β β ββββββββββββββββββββββββββββββββββ β
β β Run callback for negotiated β βββΆβ moved β β
β β content type from list in β ββββββββββββββββββββββββββββββββββ β
β β contentTypesProvided β β β
β β β β β
β ββββββββββββββββββββββββββββββββββ ββββββββββββββΌββββββββββββββ β
β β β β β β
β β β β β β
β βΌ β β β β
β ββββββββββββββββββββββββββββββββββ MovedTemporarily β MovedPermantly β
β ββββ multipleChoices β β β β β
β β ββββββββββββββββββββββββββββββββββ β β β β
β β β β NotMoved β β
β β β β β β β
β β Multiple* β UniqueRepresentation β β β β
β β β βΌ β βΌ β
β β βΌ ββββββββββββββββββββββββ β βββββββββββββββββββββββββ
β β ββββββββββββββββββββββββ β307 Moved Temporarily β β β301 Moved Permanently ββ
β β β 200 OK β ββββββββββββββββββββββββ β βββββββββββββββββββββββββ
β β ββββββββββββββββββββββββ β β
β β βΌ β
β β ββββββββββββββββββββββββ ββββββββββββββββββββββββ β
β ββββββββΆβ 300 Multiple Choices β β 410 Gone β β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Continuing from
Content Negotiationβ¦
β
β
βββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β PUT/POST/PATCHβ
β βΌ β
β βββββββββββββββββββββββββββββββββ False β
β β resourceExists βββββββββββββββββββββββββββββββββ β
β βββββββββββββββββββββββββββββββββ β β
β β β β
β β β β
β β True β β
β β βΌ β
β βΌ ββββββββββββββββββββββββ β
β β β False β
β See βConditional requestsβ diagram βHas βIf-Matchβ Header?βββββββββββ β
β β β β β
β β ββββββββββββββββββββββββ β β
β β β β β
β β β True β β
β β βΌ β β
β βββββββββββββββββββββββ ββββββββββββββββββββββββ β β
β β β 412 Precond. Failed β β β
β β ββββββββββββββββββββββββ β β
β β β β
β β β β
β β β β
β β ββββββββββββββββββββββββ β β
β β False β β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββMethod is POST/PATCH? βββββββββββ β
β β β β β β
β β β ββββββββββββββββββββββββ β
β β β β β
β β β β True β
β β β ββββββββββββββββββββββββ βΌ β
β β β β β False ββββββββββββββββββββββββββββββββββ β
β β β β Method is POST? ββββββββββββββββ previouslyExisted β β
β β β β β ββββββββββββββββββββββββββββββββββ β
β β β βββββββββββββββββββββ³βββ β β
β β β β β β β
β β β True β β False β β
β β β βΌ ββββββββββββ β β
β β β βββββββββββββββββββββββββββββββββ β β True β
β β β βββββ allowMissingPost β β β β
β β β β βββββββββββββββββββββββββββββββββ β β β
β β β β β β β β
β β β β True β False β βΌ β
β β β β βΌ β ββββββββββββββββββββββββββββββββββ β
β β β β ββββββββββββββββββββββββ β β moved β β
β β β β β 404 Not Found βββββββββ ββββββββββββββββββββββββββββββββββ β
β β β β ββββββββββββββββββββββββ β β
β β β β β β
β β β β ββββββββββββββΌββββββββββββββ β
β β β β β β β β
β β β β β β β β
β β β ββββββββββββββββ β β β β
β β β β β β β β
β β β βΌ MovedTemporarily β MovedPermantly β
β β β ββββββββββββββββββββββββ β β β β
β β βββββββΆβ β β NotMoved β β
β β β Method is PUT? β β β β β
β ββββββββββΆβ β β β β β
β ββββββββββββββββββββββββ βΌ β βΌ β
β β β ββββββββββββββββββββββββ β βββββββββββββββββββββββ΄β
β False β β True β307 Moved Temporarily β β β301 Moved Permanently β
β ββββββββββββββββββ β ββββββββββββββββββββββββ β βββββββββββββββββββββββ¬β
β β β β β
β β β β β
β β β βΌ β
β β β ββββββββββββββββββββββββ β
β β β β β β
β β β β Method is POST? β β
β β β β β β
β β β ββββββββββββββββββββββββ β
β β β β β
β β β β True β
β β βΌ βΌ β
β β ββββββββββββββββββββββββ False βββββββββββββββββββββββββββββββββ β
β β β isConflict βββββββββββββββ β allowMissingPost β ββββββ β
β β ββββββββββββββββββββββββ β βββββββββββββββββββββββββββββββββ β β
β β β β β β β
β β β True β β False True β β
β β βΌ β βΌ β β
β β ββββββββββββββββββββββββ β ββββββββββββββββββββββββ β β
β β β 409 Conflict β β β 410 Gone β β β
β β ββββββββββββββββββββββββ β ββββββββββββββββββββββββ β β
β β β β β
β β β β β
β β β β β
β β βΌ β β
β β ββββββββββββββββββββββββββββββ β β
β β β Handler for type in β β β
β βββββββββββββββββββββββββββββΆββContent-Typeβ header existsββββββββββββββββββββββββββββββββ β
β β in contentTypesAccepted β β
β βββββββββββ³βββββββββ³ββββββββββ β
β Yes β β No β
β ββββββββββββββββββββββ ββββββββββββββββββββββββββ β
β β β β
β βΌ βΌ β
β ββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββ β
β β Run the callback function from β Failed β415 Unsupported Media Type β β
β β contentTypesAccepted for the ββββββββββββββ βββββββββββββββββββββββββββββ β
β β content type we were given β β β
β βββββββββ¦βββββββββββββββββ¦βββββ¦βββ β β
β β β β β β β
β β β β β β βββββββββββββββββββββββββββββ β
β β β β β βββββββββββββΆβ 400 Bad Request β β
β β β β β βββββββββββββββββββββββββββββ β
β β β β β Redirect β
β β β β βββββββββββββββββ β
β β β β β β
β β β β β βββββββββββββββββββββββββββββ β
β β β βββββ βββββββββββββΆβ 303 See Other β β
β β β β βββββββββββββββββββββββββββββ β
β β β β β
β β β β β
β β β β β
β ββββββ β Succeeded β
β β β β β
β β βββββββ β β
β β β β False βββββββββββββββββββββββββββββ β
β β β β βββββββββΆβ 201 Created β β
β β β βΌ β βββββββββββββββββββββββββββββ β
β β β ββββββββββββββββββββββββββββββ β β
β β β β resourceExists βββββββββ€ β
β β β ββββββββββββββββββββββββββββββ β β
β β β β βββββββββββββββββββββββββββββ β
β β β βββββββββΆβ 204 No Content β β
β β β True βββββββββββββββββββββββββββββ β
β β β β
β β SuccededWithContent β
β β β False βββββββββββββββββββββββββββββ β
β β β βββββββββΆβ 201 Created β β
β β β β βββββββββββββββββββββββββββββ β
β β β ββββββββββββββββββββββββββββββ β β
β β βββΆβ resourceExists βββββββββ€ β
β β ββββββββββββββββββββββββββββββ β β
β β β βββββββββββββββββββββ β
β β ββββββββββΆβ multipleChoices βββββ β
β SuccededWithLocation True βββββββββββββββββββββ β β
β β β β β
β β β β β
β β ββββββββββββββββββββββββββββββ UniqueRepresentation β β
β βββββββββββΆβ resourceExists β β β β
β βββββββββ¦ββββββββββββββ¦βββββββ β β β
β β β β Multiple* β
β True β βFalse βΌ β β
β βββββββββββββ βββββββ ββββββββββββββββββββββββ β β
β β β β 200 OK β β β
β β β ββββββββββββββββββββββββ β β
β β β β β
β βΌ βΌ ββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββ β 300 Multiple Choices ββββ β
β β 204 No Content β β 201 Created β ββββββββββββββββββββββββ β
β β + βLocationβ header β β + βLocationβ header β β
β βββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Continuing from
Content Negotiationβ¦
β
β
β
βββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β βΌ DELETEβ
β ββββββββββββββββββββββββββββββββββββββββ β
β β resourceExists ββββββββββββββββββββββββββββββ β
β ββββββββββββββββββββββββββββββββββββββββ β β
β β β β
β β β β
β β β β
β β βΌ β
β βΌ ββββββββββββββββββββββββ β
β β β β
β See βConditional requestsβ diagram βHas βIf-Matchβ Header?βββββββββ β
β β β β β
β β ββββββββββββββββββββββββ β β
β β β β β
β β β β β
β βΌ βΌ β β
β ββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββ β β
β β deleteResource β β 412 Precond. Failed β β β
β ββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββ β β
β β β β β β β
β β β β β β β
β β β Deleted β DeleteEnacted β β
β βββββ β β β ββββββββββββββββββββββββββββββββββ β β
β β NotDeleted β β ββββ previouslyExisted ββββ β
β β β βββββββββββ β β ββββββββββββββββββββββββββββββββββ β
β β β β β β β β
β β β β βΌ β β False β
β β β β ββββββββββββββββββββββββ β βΌ β
β β β β β 202 Accepted β β True ββββββββββββββββββββββββ β
β β β β ββββββββββββββββββββββββ β β 404 Not Found β β
β β β β β ββββββββββββββββββββββββ β
β β β β ββββββββββββββββββββββββ β β
β β β βββΆβ 204 No Content β β β
β β β ββββββββββββββββββββββββ β β
β β β β ββββββββββββββββββββββββββββββββββ β
β β β ββββββββββββββββββββββββ βββΆβ moved β β
β β ββββββββΆβ 500 Error β ββββββββββββββββββββββββββββββββββ β
β β ββββββββββββββββββββββββ β β
β βββββββββββββββββββ β β
β β ββββββββββββββΌββββββββββββββ β
β β β β β β
β β β β β β
β β β β β β
β β β β β β
β DeletedWithResponse MovedTemporarily β MovedPermantly β
β β β β β β
β β β NotMoved β β
β β β β β β
β β β β β β
β βΌ βΌ β βΌ β
β ββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββ β βββββββββββββββββββββββββ
β ββββ multipleChoices β β307 Moved Temporarily β β β301 Moved Permanently ββ
β β ββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββ β βββββββββββββββββββββββββ
β β β β β
β β β βΌ β
β β Multiple* β UniqueRepresentation ββββββββββββββββββββββββ β
β β β β 410 Gone β β
β β βΌ ββββββββββββββββββββββββ β
β β ββββββββββββββββββββββββ β
β β β 200 OK β β
β β ββββββββββββββββββββββββ β
β β β
β β ββββββββββββββββββββββββ β
β ββββββββΆβ 300 Multiple Choices β β
β ββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Conditional requestsβ
β ββββββββββββββββββββββββ β
β β β β
β βββββββββHas βIf-Matchβ Header?β β
β β β β β
β β ββββββββββββββββββββββββ β
β β β β
β β β True β
β β βΌ β
β β ββββββββββββββββββββββββββββββββββ tags didn't match β
β Falseβ β generateEtag βββββββββββββββββββββββββββββ β
β β ββββββββββββββββββββββββββββββββββ β β
β β β β β
β β β tags matched β β
β β βΌ β β
β β ββββββββββββββββββββββββ β β
β ββββββββΆβ Has β β β
β ββIf-Unmodified-Sinceβ β β β
β βββββββββ Header? β β β
β β ββββββββββββββββββββββββ β β
β β β β β
β β β True β β
β β βΌ βΌ β
β β ββββββββββββββββββββββββββββββββββ modfied ββββββββββββββββββββββββ β
β False β β lastModified ββββββββββββββββΆβ 412 Precond. Failed β β
β β ββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββ β
β β β β² β
β β β not modified β β
β β βΌ β β
β β ββββββββββββββββββββββββ β β
β ββββββββΆβ Has βIf-None-Matchβ β β β
β β Header? β β Yes β
β βββββββββ β β β
β β ββββββββββββββββββββββββ β β
β β β β β
β β β True β β
β β βΌ ββββββββββββββββββββββββ β
β β ββββββββββββββββββββββββββββββββββ tags matched β β β
β False β β generateEtag ββββββββββββββββΆβ Method is GET/HEAD? β β
β β ββββββββββββββββββββββββββββββββββ β β β
β β β ββββββββββββββββββββββββ β
β β β True β β
β β βΌ β β
β β ββββββββββββββββββββββββ β β
β ββββββββΆβ Has β β β
β β βIf-Modified-Sinceβ β β No β
β βββββββββ Header? β β β
β β ββββββββββββββββββββββββ β β
β β β β β
β β β True β β
β β βΌ not βΌ β
β β ββββββββββββββββββββββββββββββββββ modified βββββββββββββββββββββββββββββββββββ
β False β β lastModified βββββββββββΆβ generateEtag ββ
β β ββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ
β β β β β
β β β modified β β
β βββββββββββββββββββ β βΌ β
β β β βββββββββββββββββββββββββββββββββββ
β β β β expires ββ
β β β βββββββββββββββββββββββββββββββββββ
β β β β β
β β β β β
β β β βΌ β
β β β ββββββββββββββββββββββββ β
β β β β 304 Not Modified β β
β β β ββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββΌββΌβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β β
βΌ βΌ