diff options
Diffstat (limited to 'spec')
-rw-r--r-- | spec/Spec.hs | 1 | ||||
-rw-r--r-- | spec/src/Thermoprint/Printout.hs | 172 | ||||
-rw-r--r-- | spec/thermoprint-spec.cabal | 25 | ||||
-rw-r--r-- | spec/thermoprint-spec.nix | 4 |
4 files changed, 197 insertions, 5 deletions
diff --git a/spec/Spec.hs b/spec/Spec.hs new file mode 100644 index 0000000..a824f8c --- /dev/null +++ b/spec/Spec.hs | |||
@@ -0,0 +1 @@ | |||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-} | |||
diff --git a/spec/src/Thermoprint/Printout.hs b/spec/src/Thermoprint/Printout.hs new file mode 100644 index 0000000..d13d02e --- /dev/null +++ b/spec/src/Thermoprint/Printout.hs | |||
@@ -0,0 +1,172 @@ | |||
1 | {-# LANGUAGE DeriveGeneric, DeriveAnyClass #-} | ||
2 | {-# LANGUAGE OverloadedStrings #-} | ||
3 | {-# OPTIONS_HADDOCK show-extensions #-} | ||
4 | |||
5 | -- | This module contains the definition of the structure -- 'Printout' -- used to represent the content of a job | ||
6 | module Thermoprint.Printout | ||
7 | ( Printout(..) | ||
8 | , Paragraph(..) | ||
9 | , Chunk(..) | ||
10 | , Block(..) | ||
11 | , Line( HSpace | ||
12 | , SpaceSep | ||
13 | ) | ||
14 | , text, cotext | ||
15 | , prop_text | ||
16 | ) where | ||
17 | |||
18 | import Data.Sequence (Seq, (|>), (<|)) | ||
19 | |||
20 | import Data.Text.Lazy (Text) | ||
21 | |||
22 | import Data.ByteString.Lazy (ByteString) | ||
23 | |||
24 | import GHC.Generics (Generic) | ||
25 | import Control.DeepSeq (NFData) | ||
26 | |||
27 | import Test.QuickCheck.Arbitrary (Arbitrary(..), CoArbitrary, genericShrink) | ||
28 | import Test.QuickCheck.Modifiers (NonNegative(..)) | ||
29 | import Test.QuickCheck.Gen (oneof, suchThat) | ||
30 | import Test.QuickCheck.Instances | ||
31 | import Test.QuickCheck (forAll, Property) | ||
32 | |||
33 | |||
34 | import qualified Data.Text.Lazy as TL (lines, split, null, pack, filter, intercalate, map) | ||
35 | import Data.Char (isSpace) | ||
36 | |||
37 | import Data.Monoid (Monoid(..), (<>)) | ||
38 | |||
39 | import Data.List (dropWhile, dropWhileEnd, groupBy, genericLength, genericReplicate) | ||
40 | |||
41 | import Data.Sequence as Seq (fromList, null, singleton) | ||
42 | |||
43 | import Data.Function (on) | ||
44 | |||
45 | import Data.Foldable (toList) | ||
46 | |||
47 | |||
48 | -- | A 'Printout' is a sequence of visually seperated 'Paragraph's | ||
49 | type Printout = Seq Paragraph | ||
50 | |||
51 | -- | A 'Paragraph' is a non-seperated sequence of 'Chunk's | ||
52 | type Paragraph = Seq Chunk | ||
53 | |||
54 | -- | We introduce both 'Chunk' and 'Paragraph' mainly to allow 'Raw'. | ||
55 | -- | ||
56 | -- Were we to disallow 'Raw', 'Block' would be identical to 'Paragraph' | ||
57 | data Chunk = Cooked Block -- ^ text semantically structured to be rendered in accordance with the display format of printer | ||
58 | | Raw ByteString -- ^ direct instructions to the printer | ||
59 | deriving (Generic, NFData, Show, CoArbitrary) | ||
60 | |||
61 | -- | 'Block' is the entry point for our structured document format | ||
62 | data Block = Line Line -- ^ a single 'Line' of text | ||
63 | | VSpace Integer -- ^ vertical space of height equivalent to 'Integer' lines | ||
64 | | NewlSep (Seq Block) -- ^ A sequence of 'Block's seperated by newlines | ||
65 | deriving (Generic, NFData, Show, CoArbitrary) | ||
66 | |||
67 | {- | A 'Line' is one of: | ||
68 | |||
69 | * a single word | ||
70 | * horizontal space equivalent to the width of 'Integer' `em`. | ||
71 | * a sequence of words seperated by spaces | ||
72 | |||
73 | We don't export all constructors and instead encourage the use of 'text'. | ||
74 | -} | ||
75 | data Line = Word Text | ||
76 | | HSpace Integer | ||
77 | | SpaceSep (Seq Line) | ||
78 | deriving (Generic, NFData, Show, CoArbitrary) | ||
79 | |||
80 | instance Monoid Block where | ||
81 | mempty = NewlSep mempty | ||
82 | x@(NewlSep xs) `mappend` y@(NewlSep ys) | ||
83 | | Seq.null xs = y | ||
84 | | Seq.null ys = x | ||
85 | | otherwise = NewlSep (xs <> ys) | ||
86 | (NewlSep xs) `mappend` y = NewlSep (xs |> y) | ||
87 | x `mappend` (NewlSep ys) = NewlSep (x <| ys) | ||
88 | x `mappend` y = NewlSep $ Seq.fromList [x, y] | ||
89 | |||
90 | instance Monoid Line where | ||
91 | mempty = SpaceSep mempty | ||
92 | x@(SpaceSep xs) `mappend` y@(SpaceSep ys) | ||
93 | | Seq.null xs = y | ||
94 | | Seq.null ys = x | ||
95 | | otherwise = SpaceSep (xs <> ys) | ||
96 | (SpaceSep xs) `mappend` y = SpaceSep (xs |> y) | ||
97 | x `mappend` (SpaceSep ys) = SpaceSep (x <| ys) | ||
98 | x `mappend` y = SpaceSep $ Seq.fromList [x, y] | ||
99 | |||
100 | |||
101 | text :: Text -> Either Block Line | ||
102 | -- ^ Smart constructor for 'Line'/'Block' which maps word and line boundaries (as determined by 'isSpace' and 'TL.lines' respectively) to the structure of 'Block' and 'Line'. | ||
103 | -- | ||
104 | -- Since we are unwilling to duplicate the list of chars from 'isSpace' we cannot reasonably determine a width for the various whitespace 'Char's. | ||
105 | -- Thus they are all weighted equally as having width 1 `em`. | ||
106 | text t = case splitLines t of | ||
107 | [] -> Right mempty | ||
108 | [Line x] -> Right x | ||
109 | xs -> Left $ mconcat xs | ||
110 | where | ||
111 | splitLines :: Text -> [Block] | ||
112 | splitLines t = map toBlock | ||
113 | . groupBy ((==) `on` TL.null) | ||
114 | $ TL.lines t | ||
115 | splitWords :: Text -> [Line] | ||
116 | splitWords t = map toLine | ||
117 | . groupBy ((==) `on` TL.null) | ||
118 | $ TL.split isSpace t | ||
119 | toBlock [] = mempty | ||
120 | toBlock xs@(x:_) | ||
121 | | TL.null x = VSpace $ genericLength xs | ||
122 | | otherwise = mconcat . map (Line . mconcat . splitWords) $ xs | ||
123 | toLine [] = mempty | ||
124 | toLine xs@(x:_) | ||
125 | | TL.null x = HSpace $ genericLength xs | ||
126 | | otherwise = mconcat . map Word $ xs | ||
127 | list :: b -> (a -> [a] -> b) -> [a] -> b | ||
128 | list c _ [] = c | ||
129 | list _ f (x:xs) = f x xs | ||
130 | |||
131 | cotext :: Block -> Text | ||
132 | -- ^ inverse of | ||
133 | -- @ | ||
134 | -- either id Line . `text` | ||
135 | -- @ | ||
136 | cotext (VSpace n) = TL.pack . genericReplicate (n - 1) $ '\n' | ||
137 | cotext (NewlSep xs) = TL.intercalate "\n" . map cotext . toList $ xs | ||
138 | cotext (Line x) = cotext' x | ||
139 | where | ||
140 | cotext' (Word x) = x | ||
141 | cotext' (HSpace n) = TL.pack . genericReplicate (n - 1) $ ' ' | ||
142 | cotext' (SpaceSep xs) = TL.intercalate " " . map cotext' . toList $ xs | ||
143 | |||
144 | prop_text :: Property | ||
145 | -- ^ prop> cotext . either id Line . `text` $ x = x | ||
146 | -- | ||
147 | -- Where 'x' is restricted to those `TL.Text` which do not contain whitespace besides ' ' and '\n'. | ||
148 | prop_text = forAll (TL.map normSpace <$> arbitrary) $ \x -> (cotext . either id Line . text $ x) == x | ||
149 | where | ||
150 | normSpace c | ||
151 | | c == '\n' = '\n' | ||
152 | | isSpace c = ' ' -- We have to do this because all whitespace gets interpreted as width 1 | ||
153 | | otherwise = c | ||
154 | |||
155 | -- | We don't test 'Raw' 'Chunk's | ||
156 | instance Arbitrary Chunk where | ||
157 | shrink = genericShrink | ||
158 | arbitrary = Cooked <$> arbitrary | ||
159 | |||
160 | instance Arbitrary Block where | ||
161 | shrink = genericShrink | ||
162 | arbitrary = oneof [ Line <$> arbitrary | ||
163 | , VSpace . getNonNegative <$> arbitrary | ||
164 | , NewlSep <$> arbitrary | ||
165 | ] | ||
166 | |||
167 | instance Arbitrary Line where | ||
168 | shrink = genericShrink | ||
169 | arbitrary = oneof [ Word . TL.filter (not . isSpace) <$> arbitrary -- 'isSpace '\n' == True' | ||
170 | , HSpace . getNonNegative <$> arbitrary | ||
171 | , SpaceSep <$> arbitrary | ||
172 | ] | ||
diff --git a/spec/thermoprint-spec.cabal b/spec/thermoprint-spec.cabal index bde1b73..ba97420 100644 --- a/spec/thermoprint-spec.cabal +++ b/spec/thermoprint-spec.cabal | |||
@@ -17,9 +17,28 @@ build-type: Simple | |||
17 | cabal-version: >=1.10 | 17 | cabal-version: >=1.10 |
18 | 18 | ||
19 | library | 19 | library |
20 | -- exposed-modules: | 20 | hs-source-dirs: src |
21 | exposed-modules: Thermoprint.Printout | ||
21 | -- other-modules: | 22 | -- other-modules: |
22 | -- other-extensions: | 23 | -- other-extensions: |
23 | build-depends: base >=4.8 && <4.9 | 24 | extensions: DeriveGeneric |
25 | , DeriveAnyClass | ||
26 | , OverloadedStrings | ||
27 | build-depends: base >=4.8.1 && <5 | ||
28 | , containers >=0.5.6 && <1 | ||
29 | , text >=1.2.1 && <2 | ||
30 | , bytestring >=0.10.6 && <1 | ||
31 | , deepseq >=1.4.1 && <2 | ||
32 | , QuickCheck >=2.8.1 && <3 | ||
33 | , quickcheck-instances >=0.3.11 && <4 | ||
34 | , Cabal >=1.22.4 && <2 | ||
35 | , cabal-test-quickcheck >=0.1.6 && <1 | ||
24 | -- hs-source-dirs: | 36 | -- hs-source-dirs: |
25 | default-language: Haskell2010 \ No newline at end of file | 37 | default-language: Haskell2010 |
38 | |||
39 | Test-Suite tests | ||
40 | type: exitcode-stdio-1.0 | ||
41 | main-is: Spec.hs | ||
42 | build-depends: base >=4.8.1 && <5 | ||
43 | , thermoprint-spec -any | ||
44 | , hspec >=2.2.1 && <3 \ No newline at end of file | ||
diff --git a/spec/thermoprint-spec.nix b/spec/thermoprint-spec.nix index 2d1814e..82f018a 100644 --- a/spec/thermoprint-spec.nix +++ b/spec/thermoprint-spec.nix | |||
@@ -1,9 +1,9 @@ | |||
1 | { mkDerivation, base, stdenv }: | 1 | { mkDerivation, base, bytestring, containers, stdenv, text, QuickCheck, quickcheck-instances, cabal-test-quickcheck, hspec }: |
2 | mkDerivation { | 2 | mkDerivation { |
3 | pname = "thermoprint-spec"; | 3 | pname = "thermoprint-spec"; |
4 | version = "1.0.0"; | 4 | version = "1.0.0"; |
5 | src = ./.; | 5 | src = ./.; |
6 | libraryHaskellDepends = [ base ]; | 6 | libraryHaskellDepends = [ base bytestring containers text QuickCheck quickcheck-instances cabal-test-quickcheck hspec ]; |
7 | homepage = "http://dirty-haskell.org/tags/thermoprint.html"; | 7 | homepage = "http://dirty-haskell.org/tags/thermoprint.html"; |
8 | description = "A specification of the API and the payload datatypes and associated utilities"; | 8 | description = "A specification of the API and the payload datatypes and associated utilities"; |
9 | license = stdenv.lib.licenses.publicDomain; | 9 | license = stdenv.lib.licenses.publicDomain; |