1
1
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:
Sean Chalmers 2018-01-22 11:40:11 +10:00
parent 48132f0c2a
commit 959eb576aa
13 changed files with 122 additions and 61 deletions

View File

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

View File

@ -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.

View File

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

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

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