mirror of
https://github.com/qfpl/applied-fp-course.git
synced 2024-10-05 16:37:53 +03:00
Issue Fixes & More feedback driven development
Fixes: #15, #13, #14, #11, #8 Add the IDEAS to a 'Suggestions' section in the FUTURE_PLANS file. * Updated the cabal instructions for level 03 * Removed the duplicated config loading in level 05 * Add the implementations for File.hs for levels 06 & 07 * Renamed the slightly misleading 'readObject' function * Fixed the capitalisation of init and close DB functions between levels 04 & 05.
This commit is contained in:
parent
48132f0c2a
commit
959eb576aa
@ -79,3 +79,18 @@ Integration of 'classy mtl' style application design.
|
||||
* Build intuition for `AsFoo` & `HasFoo`
|
||||
* `mtl` classes are a start, but how do we make this easier?
|
||||
* Describe `CanFoo` and introduce `ConstraintKinds`
|
||||
|
||||
## Suggestions
|
||||
|
||||
* Lens
|
||||
* Mocking / Free
|
||||
* Dealing with callbacks (more info needed)
|
||||
* Streaming / Iteratees (safe resource handling)
|
||||
* ST / Safe Mutation techniques
|
||||
* Classy MTL
|
||||
* Generic / Meta programming
|
||||
* Daemons
|
||||
* Ermagerd reel librees!
|
||||
* Concurrency packages/techniques (book recommendation) - STM
|
||||
* Property Based Testing (Hedgehog, advanced testing)
|
||||
* Stats
|
||||
|
14
README.md
14
README.md
@ -29,13 +29,13 @@ IRC on [Freenode](https://freenode.net/) in #qfpl or #fp-course.
|
||||
* Have constructed a sequence of goals of increasing difficulty.
|
||||
* Have provided a framework within which to apply these goals.
|
||||
* Have included relevant components of larger applications:
|
||||
- Package dependencies
|
||||
- Project configuration
|
||||
- Application testing & building
|
||||
- Encoding / Decoding messages (JSON & Binary)
|
||||
- Persistent storage integration
|
||||
- App state & configuration management
|
||||
- Error handling & reporting
|
||||
* Package dependencies
|
||||
* Project configuration
|
||||
* Application testing & building
|
||||
* Encoding / Decoding messages (JSON & Binary)
|
||||
* Persistent storage integration
|
||||
* App state & configuration management
|
||||
* Error handling & reporting
|
||||
* Will utilise both type & test driven development techniques.
|
||||
* Will explain architectural and design trade-offs when appropriate.
|
||||
|
||||
|
@ -12,19 +12,19 @@ response and the message "Hello, World!".
|
||||
[Hackage]: (https://hackage.haskell.org/)
|
||||
[Wai]: (https://hackage.haskell.org/package/wai)
|
||||
|
||||
## Running the program:
|
||||
## Running the program
|
||||
|
||||
To run the application when you are finished:
|
||||
|
||||
```bash
|
||||
# With Cabal
|
||||
$ cabal exec level01-exe
|
||||
$ cabal run level01-exe
|
||||
|
||||
# With Stack
|
||||
$ stack exec level01-exe
|
||||
```
|
||||
|
||||
## Accessing the program:
|
||||
## Accessing the program
|
||||
|
||||
```bash
|
||||
# Using curl
|
||||
|
@ -44,7 +44,7 @@ The starting point for this exercise is the ``src/FirstApp/Types.hs``.
|
||||
|
||||
```bash
|
||||
# Using cabal
|
||||
$ cabal exec level02-exe
|
||||
$ cabal run level02-exe
|
||||
|
||||
# Using stack
|
||||
$ stack exec level02-exe
|
||||
|
@ -12,6 +12,22 @@ As is to be expected, there are multiple testing frameworks and packages
|
||||
available but we will only cover one here. We will use the [HSpec] framework,
|
||||
with the [hspec-wai] package to make our lives a bit easier.
|
||||
|
||||
### NB: Including Test Library Dependencies
|
||||
|
||||
For a cabal sandbox:
|
||||
|
||||
```shell
|
||||
$ cabal sandbox init
|
||||
$ cabal install --only-dependencies --enable-tests
|
||||
$ cabal configure --enable-tests
|
||||
```
|
||||
|
||||
For a stack environment:
|
||||
|
||||
```shell
|
||||
$ stack build --test
|
||||
```
|
||||
|
||||
Start in ``tests/Test.hs``.
|
||||
|
||||
[HSpec]: (http://hspec.github.io/)
|
||||
@ -45,12 +61,21 @@ section in the Cabal file are not loaded. You can manually tell ``ghcid`` to
|
||||
load and examine these files with the following command:
|
||||
|
||||
```bash
|
||||
$ ghcid -c "cabal repl level03"
|
||||
|
||||
# Or for using ghcid to check your tests
|
||||
$ ghcid -c "cabal repl level03-tests"
|
||||
|
||||
# A note for sandboxed ghcid, you may need to provide an
|
||||
# explicit path to the binary:
|
||||
$ .cabal-sandbox/bin/ghcid -c "cabal repl level03-tests"
|
||||
```
|
||||
|
||||
It should work with ``stack`` as well:
|
||||
```base
|
||||
```bash
|
||||
$ ghcid -c "stack repl level03-tests"
|
||||
```
|
||||
|
||||
Please read the ``ghcid``documentation for installation and usage instructions.
|
||||
Please read the ``ghcid``documentation for installation and usage
|
||||
instructions. The [FAQ](https://github.com/ndmitchell/ghcid#faq) provides
|
||||
some useful tips.
|
@ -2,8 +2,8 @@
|
||||
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
|
||||
module FirstApp.DB
|
||||
( FirstAppDB (FirstAppDB)
|
||||
, initDb
|
||||
, closeDb
|
||||
, initDB
|
||||
, closeDB
|
||||
, addCommentToTopic
|
||||
, getComments
|
||||
, getTopics
|
||||
@ -35,19 +35,19 @@ import FirstApp.Types (Comment, CommentText,
|
||||
data FirstAppDB = FirstAppDB
|
||||
|
||||
-- Quick helper to pull the connection and close it down.
|
||||
closeDb
|
||||
closeDB
|
||||
:: FirstAppDB
|
||||
-> IO ()
|
||||
closeDb =
|
||||
closeDB =
|
||||
error "closeDb not implemented"
|
||||
|
||||
-- Given a `FilePath` to our SQLite DB file, initialise the database and ensure
|
||||
-- our Table is there by running a query to create it, if it doesn't exist
|
||||
-- already.
|
||||
initDb
|
||||
initDB
|
||||
:: FilePath
|
||||
-> IO ( Either SQLiteResponse FirstAppDB )
|
||||
initDb fp =
|
||||
initDB fp =
|
||||
error "initDb not implemented"
|
||||
where
|
||||
-- Query has an `IsString` instance so string literals like this can be
|
||||
|
@ -70,18 +70,17 @@ decodeObj =
|
||||
|
||||
-- | Update these tests when you've completed this function.
|
||||
--
|
||||
-- | readObject
|
||||
-- >>> readObject "badFileName.no"
|
||||
-- | readConfFile
|
||||
-- >>> readConfFile "badFileName.no"
|
||||
-- Left (undefined "badFileName.no: openBinaryFile: does not exist (No such file or directory)")
|
||||
--
|
||||
-- >>> readObject "test.json"
|
||||
-- >>> readConfFile "test.json"
|
||||
-- Right "{\n \"foo\": 33\n}\n"
|
||||
--
|
||||
readObject
|
||||
readConfFile
|
||||
:: FilePath
|
||||
-> IO ( Either ConfigError ByteString )
|
||||
readObject =
|
||||
error "readObject not implemented"
|
||||
readConfFile =
|
||||
error "readConfFile not implemented"
|
||||
|
||||
-- Construct the function that will take a ``FilePath``, read it in and attempt
|
||||
-- to decode it as a valid JSON object, using the ``aeson`` package. Then pull
|
||||
|
@ -46,8 +46,8 @@ data StartUpError
|
||||
|
||||
runApp :: IO ()
|
||||
runApp = do
|
||||
-- Load up the configuration by providing a ``FilePath`` for the JSON config file.
|
||||
cfgE <- error "configuration not implemented"
|
||||
-- Load our configuration
|
||||
cfgE <- prepareAppReqs
|
||||
-- Loading the configuration can fail, so we have to take that into account now.
|
||||
case cfgE of
|
||||
Left err -> undefined
|
||||
|
@ -110,6 +110,7 @@ fromDbComment dbc =
|
||||
<$> (mkTopic $ dbCommentTopic dbc)
|
||||
<*> (mkCommentText $ dbCommentComment dbc)
|
||||
<*> (pure $ dbCommentTime dbc)
|
||||
|
||||
mkTopic
|
||||
:: Text
|
||||
-> Either Error Topic
|
||||
@ -200,7 +201,7 @@ confPortToWai
|
||||
:: Conf
|
||||
-> Int
|
||||
confPortToWai =
|
||||
error "portToInt not implemented"
|
||||
error "confPortToWai not implemented"
|
||||
|
||||
-- Similar to when we were considering our application types, leave this empty
|
||||
-- for now and add to it as you go.
|
||||
|
@ -11,11 +11,11 @@ import Data.Monoid (Last (Last))
|
||||
|
||||
import Control.Exception (try)
|
||||
|
||||
import Data.Aeson (FromJSON, Object)
|
||||
import Data.Aeson (FromJSON, Object, (.:))
|
||||
|
||||
import qualified Data.Aeson as Aeson
|
||||
|
||||
import FirstApp.Types (ConfigError,
|
||||
import FirstApp.Types (ConfigError (JSONDecodeError,ConfigFileReadError),
|
||||
PartialConf (PartialConf))
|
||||
-- Doctest setup section
|
||||
-- $setup
|
||||
@ -47,8 +47,8 @@ fromJsonObjWithKey
|
||||
-> (a -> b)
|
||||
-> Object
|
||||
-> Last b
|
||||
fromJsonObjWithKey =
|
||||
error "fromJsonObjWithKey not implemented"
|
||||
fromJsonObjWithKey k c o =
|
||||
Last $ c <$> Aeson.parseMaybe (.: k) o
|
||||
|
||||
-- |----
|
||||
-- | You will need to update these tests when you've completed the following functions!
|
||||
@ -66,22 +66,21 @@ decodeObj
|
||||
:: ByteString
|
||||
-> Either ConfigError Object
|
||||
decodeObj =
|
||||
error "decodeObj not implemented"
|
||||
first JSONDecodeError . Aeson.eitherDecode
|
||||
|
||||
-- | Update these tests when you've completed this function.
|
||||
--
|
||||
-- | readObject
|
||||
-- >>> readObject "badFileName.no"
|
||||
-- Left (undefined badFileName.no: openBinaryFile: does not exist (No such file or directory))
|
||||
-- | readConfFile
|
||||
-- >>> readConfFile "badFileName.no"
|
||||
-- Left (undefined "badFileName.no: openBinaryFile: does not exist (No such file or directory)")
|
||||
-- >>> readConfFile "test.json"
|
||||
-- Right "{\n \"foo\": 33\n}\n"
|
||||
--
|
||||
-- >>> readObject "test.json"
|
||||
-- Right "{\"foo\":33}\n"
|
||||
--
|
||||
readObject
|
||||
readConfFile
|
||||
:: FilePath
|
||||
-> IO ( Either ConfigError ByteString )
|
||||
readObject =
|
||||
error "readObject not implemented"
|
||||
readConfFile fp =
|
||||
first ConfigFileReadError <$> try (LBS.readFile fp)
|
||||
|
||||
-- Construct the function that will take a ``FilePath``, read it in and attempt
|
||||
-- to decode it as a valid JSON object, using the ``aeson`` package. Then pull
|
||||
@ -90,5 +89,13 @@ readObject =
|
||||
parseJSONConfigFile
|
||||
:: FilePath
|
||||
-> IO ( Either ConfigError PartialConf )
|
||||
parseJSONConfigFile =
|
||||
error "parseJSONConfigFile not implemented"
|
||||
parseJSONConfigFile fp =
|
||||
fmap toPartialConf . ( decodeObj =<< ) <$> readObject fp
|
||||
where
|
||||
toPartialConf
|
||||
:: Aeson.Object
|
||||
-> PartialConf
|
||||
toPartialConf cObj = PartialConf
|
||||
( fromJsonObjWithKey "port" Port cObj )
|
||||
( fromJsonObjWithKey "helloMsg" helloFromStr cObj )
|
||||
|
||||
|
@ -23,6 +23,8 @@ module FirstApp.Types
|
||||
, confPortToWai
|
||||
) where
|
||||
|
||||
import System.IO.Error (IOError)
|
||||
|
||||
import GHC.Generics (Generic)
|
||||
import GHC.Word (Word16)
|
||||
|
||||
@ -206,6 +208,8 @@ confPortToWai =
|
||||
data ConfigError
|
||||
= MissingPort
|
||||
| MissingDBFilePath
|
||||
| JSONDecodeError String
|
||||
| ConfigFileReadError IOError
|
||||
deriving Show
|
||||
|
||||
-- Our application will be able to load configuration from both a file and
|
||||
|
@ -11,11 +11,11 @@ import Data.Monoid (Last (Last))
|
||||
|
||||
import Control.Exception (try)
|
||||
|
||||
import Data.Aeson (FromJSON, Object)
|
||||
import Data.Aeson (FromJSON, Object, (.:))
|
||||
|
||||
import qualified Data.Aeson as Aeson
|
||||
|
||||
import FirstApp.Types (ConfigError,
|
||||
import FirstApp.Types (ConfigError (JSONDecodeError,ConfigFileReadError),
|
||||
PartialConf (PartialConf))
|
||||
-- Doctest setup section
|
||||
-- $setup
|
||||
@ -47,8 +47,8 @@ fromJsonObjWithKey
|
||||
-> (a -> b)
|
||||
-> Object
|
||||
-> Last b
|
||||
fromJsonObjWithKey =
|
||||
error "fromJsonObjWithKey not implemented"
|
||||
fromJsonObjWithKey k c o =
|
||||
Last $ c <$> Aeson.parseMaybe (.: k) o
|
||||
|
||||
-- |----
|
||||
-- | You will need to update these tests when you've completed the following functions!
|
||||
@ -66,22 +66,21 @@ decodeObj
|
||||
:: ByteString
|
||||
-> Either ConfigError Object
|
||||
decodeObj =
|
||||
error "decodeObj not implemented"
|
||||
first JSONDecodeError . Aeson.eitherDecode
|
||||
|
||||
-- | Update these tests when you've completed this function.
|
||||
--
|
||||
-- | readObject
|
||||
-- >>> readObject "badFileName.no"
|
||||
-- Left (undefined badFileName.no: openBinaryFile: does not exist (No such file or directory))
|
||||
-- | readConfFile
|
||||
-- >>> readConfFile "badFileName.no"
|
||||
-- Left (undefined "badFileName.no: openBinaryFile: does not exist (No such file or directory)")
|
||||
-- >>> readConfFile "test.json"
|
||||
-- Right "{\n \"foo\": 33\n}\n"
|
||||
--
|
||||
-- >>> readObject "test.json"
|
||||
-- Right "{\"foo\":33}\n"
|
||||
--
|
||||
readObject
|
||||
readConfFile
|
||||
:: FilePath
|
||||
-> IO ( Either ConfigError ByteString )
|
||||
readObject =
|
||||
error "readObject not implemented"
|
||||
readConfFile fp =
|
||||
first ConfigFileReadError <$> try (LBS.readFile fp)
|
||||
|
||||
-- Construct the function that will take a ``FilePath``, read it in and attempt
|
||||
-- to decode it as a valid JSON object, using the ``aeson`` package. Then pull
|
||||
@ -90,5 +89,12 @@ readObject =
|
||||
parseJSONConfigFile
|
||||
:: FilePath
|
||||
-> IO ( Either ConfigError PartialConf )
|
||||
parseJSONConfigFile =
|
||||
error "parseJSONConfigFile not implemented"
|
||||
parseJSONConfigFile fp =
|
||||
fmap toPartialConf . ( decodeObj =<< ) <$> readObject fp
|
||||
where
|
||||
toPartialConf
|
||||
:: Aeson.Object
|
||||
-> PartialConf
|
||||
toPartialConf cObj = PartialConf
|
||||
( fromJsonObjWithKey "port" Port cObj )
|
||||
( fromJsonObjWithKey "helloMsg" helloFromStr cObj )
|
@ -23,6 +23,8 @@ module FirstApp.Types
|
||||
, confPortToWai
|
||||
) where
|
||||
|
||||
import System.IO.Error (IOError)
|
||||
|
||||
import GHC.Generics (Generic)
|
||||
import GHC.Word (Word16)
|
||||
|
||||
@ -199,6 +201,8 @@ confPortToWai =
|
||||
data ConfigError
|
||||
= MissingPort
|
||||
| MissingDBFilePath
|
||||
| JSONDecodeError String
|
||||
| ConfigFileReadError IOError
|
||||
deriving Show
|
||||
|
||||
-- Our application will be able to load configuration from both a file and
|
||||
|
Loading…
Reference in New Issue
Block a user